Skip to main content

我的Tmux快捷键

· 3 min read

万一忘记了,过来看看

会话、窗口与面板快捷键

会话管理 (Session Management)

快捷键功能
Prefix + s列表显示所有会话,并可以交互式切换
Prefix + : then new创建新会话
Prefix + $重命名当前会话
Prefix + d分离客户端。

窗口管理 (Window Management)

快捷键功能
Prefix + c创建一个新窗口
Prefix + w列表显示所有窗口,并可以交互式切换
Prefix + ,重命名当前窗口
Prefix + &关闭当前窗口 (会提示确认)
Prefix + p切换到上一个窗口 (我使用 Prefix + h)
Prefix + n切换到下一个窗口 (我使用 Prefix + l)

面板管理 (Pane Management)

快捷键功能
Prefix + x关闭当前面板 (会提示确认)
Prefix + z切换当前面板为全屏/还原
Prefix + Space在预设的几种布局之间循环切换
Prefix + q短暂显示所有面板的编号
Prefix + {将当前面板与上方的面板交换
Prefix + }将当前面板与下方的面板交换
Prefix + !将当前面板移动到一个新窗口 (break-pane)
Prefix + : then join-pane -t [window_index]将当前面板移动到指定窗口

交互式选择模式快捷键 (choose-tree mode)

当使用 Prefix + sPrefix + w 时,会进入一个交互式树状选择界面。在这个界面中,可以使用以下快捷键:

快捷键功能
j, k, , 上下移动光标
Enter选择并切换到目标会话/窗口/面板
x关闭选中的会话/窗口/面板 (会提示确认)
r重命名选中的会话/窗口
t标记/取消标记当前项目
v展开/折叠会话下的所有窗口
:对所有标记的项目执行命令

Vim模式复制粘贴

Tmux的复制模式默认是Emacs风格的,我将其改为了Vim风格,并与系统剪贴板集成。使用流程如下:

快捷键功能
Prefix + [进入复制模式
(在复制模式中)使用Vim的移动命令(如 h/j/k/l, w, b)移动光标
v (在复制模式中)开始选择文本
y (在复制模式中)复制选中内容到系统剪贴板

其他实用快捷键

快捷键功能
Prefix + r重新加载配置文件
Prefix + d清屏并清除历史记录
Prefix + C-j使用 tms 切换会话 (弹出窗口)
Prefix + C-k使用 tms 切换窗口 (弹出窗口)
Prefix + C-l切换一个全屏的弹出式窗口

插件管理

我使用 TPM (Tmux Plugin Manager) 来管理插件。

快捷键功能
Prefix + I安装所有插件 (TPM)
Prefix + Ctrl-r恢复会话 (tmux-resurrect)

当Docker命令无响应时如何强制杀死容器

· 4 min read

在资源有限的服务器上部署了消耗大量资源的容器,有时会导致 Docker 守护进程或相关命令(如 docker ps, docker rm)无响应,甚至整个服务器的命令执行都变得非常迟缓。 在这种紧急情况下,如果 docker rm -f <container_id> 命令也无法使用,我们如何在不重启服务器的前提下,强制停止这个失控的容器呢?

本文将介绍一种绕过 Docker 命令,直接从操作系统层面杀死容器进程的方法。

问题分析:为什么 Docker 命令会无响应?

Docker 容器本质上是运行在宿主机上的一个或多个进程。当容器内应用过度消耗 CPU 或内存时,会严重影响宿主机的性能。 这不仅使得容器本身的应用无响应,还可能导致 Docker 守护进程(dockerd)无法正常处理新的命令请求,因为它也需要系统资源来管理容器。因此,docker CLI 命令就会卡住或超时。

解决方案:直接杀死容器的 Shim 进程

既然无法通过 Docker 的上层管理工具进行操作,我们可以转向底层,直接操作容器对应的宿主机进程。 每个正在运行的 Docker 容器都有一个名为 containerd-shim-runc-v2 的父进程(shim process),它负责管理容器的生命周期。通过杀死这个 shim 进程,我们就可以有效地终止整个容器。

步骤一:查找容器的 Shim 进程

我们可以使用 ps 命令来列出系统上所有的 containerd-shim 进程。

ps aux | grep containerd-shim

执行该命令后,你可能会看到类似下面的输出,每一行都代表一个正在运行的容器的 shim 进程:

root      654787  0.0  0.2 1237996 4788 ?        Sl   Jun13   2:36 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 748f1d...
root 1482834 0.0 0.4 1238252 8236 ? Sl Jun23 1:39 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 8d35cd...
root 2132820 0.0 0.2 1238252 4536 ? Sl 17:46 0:03 /usr/bin/containerd-shim-runc-v2 -namespace moby -id df4664...

提示: 输出结果中的 -id 参数后面跟着的是容器的完整 ID。你可以通过这个 ID 来匹配 docker ps -a (如果可用) 中的容器。

步骤二:识别并杀死目标进程

在进程列表中,你需要找到那个导致问题的容器所对应的进程。你可以根据以下几点来判断:

  • 容器ID (-id): 如果你知道问题容器的部分或完整ID,可以直接在输出中找到它。
  • 启动时间: 如果问题是最近才出现的,可以查看进程的启动时间(例如,上面例子中的 17:46),找到最新启动的那个。
  • 资源占用: 在服务器非常卡顿的情况下,你也可以使用 tophtop 命令,按 CPU 或内存排序,找到消耗资源最多的 containerd-shim-runc-v2 进程。

一旦确定了目标进程的 PID (Process ID,输出的第一列数字),就可以使用 kill 命令来终止它。

注意: kill -9 是一个非常强硬的命令,它会立即终止进程,不会给进程任何清理或保存工作的机会。请确保你选择了正确的 PID。

# 使用 sudo 是因为这些进程通常由 root 用户运行
# 将 2132820 替换为你要杀死的进程的实际 PID
sudo kill -9 2132820

杀死 shim 进程后,与之关联的容器及其所有子进程都会被终止,从而释放其占用的系统资源。之后,服务器的响应应该会恢复正常。

总结

当遇到因容器资源耗尽而导致 Docker 命令无响应的棘手问题时,直接从宿主机层面杀死容器的 containerd-shim 进程是一种有效的应急手段。 虽然这种方法很强大,但也应该谨慎使用,确保你终止的是正确的进程,以避免影响其他正常运行的服务。

端口开放但浏览器无法访问时的网络问题调试指南

· 3 min read

想象一下:你对一台服务器运行 nmap 扫描,发现某个特定端口(比如 10080)是开放的。你尝试在浏览器中访问它,但却收到像 “此网站无法访问” 或类似内容的错误。这是怎么回事?端口是开放的——为什么你无法访问该服务?

这篇博客文章将逐步引导你如何调试这类问题,从网络工具到浏览器怪癖。

步骤 1:使用 Netcat (nc) 验证端口可访问性

首先,检查该端口是否真的开放并在 TCP 级别接受连接。你可以使用 netcat:

nc -v <server-ip> 10080

如果你看到类似以下的消息:

Connection to <server-ip> port 10080 [tcp/*] succeeded!

那么该端口在网络上是可达的。

步骤 2:使用 curl 检查应用层

接下来,验证在该端口上运行的服务是否确实在提供 HTTP 流量:

curl http://<server-ip>:10080

如果你收到一个 HTML 页面(比如登录界面或 Web 应用程序),则说明服务已启动并正常响应。

步骤 3:确认服务器上的服务绑定

如果你有服务器的访问权限,SSH 到服务器并确认哪个进程正在监听该端口:

sudo lsof -i :10080
# 或
sudo netstat -tulnp | grep 10080

确保服务监听在所有接口 (0.0.0.0) 或至少你的客户端尝试连接的接口上,而不仅仅是 127.0.0.1

步骤 4:检查防火墙和安全组

  • 在服务器上,检查防火墙规则(例如,iptablesfirewalld),确保允许端口 10080。
  • 如果你的服务器托管在云端(如 AWS),确认安全组或网络 ACLs 允许端口 10080 的入站流量。
  • 确保没有网络防火墙或路由器正在阻止或过滤该端口。

步骤 5:理解浏览器端口限制

这里有一个许多人会遇到的陷阱:现代浏览器出于安全原因默认会阻止某些“不安全”端口。端口 10080 就是 Chrome 和基于 Chromium 的浏览器中被阻止的端口之一。

如果你的浏览器抛出类似以下的错误:

ERR_UNSAFE_PORT

这意味着浏览器拒绝连接到该端口,即使服务已启动且端口可达。

步骤 6:如何绕过浏览器端口阻塞

选项 1:将服务更改为使用安全端口

最简单、最面向未来的解决方案是将你的服务更改为监听浏览器允许的端口,例如:

  • 8080
  • 8888
  • 3000
  • 5000

然后通过以下方式访问你的服务:

http://<server-ip>:8080

选项 2:覆盖浏览器设置(不推荐)

在 Chrome 中,你可以通过 --explicitly-allowed-ports=10080 标志启动浏览器来绕过此限制,但这不安全,应仅在临时测试时使用。

总结

问题解决方案
端口开放但浏览器无法访问使用 nccurl 验证服务和端口
服务仅绑定到本地主机重新绑定到 0.0.0.0
防火墙阻止端口在防火墙/安全组中开放端口
浏览器阻止端口(例如端口 10080)更改端口或使用 Firefox

最终思考

调试网络问题涉及检查多个层面:

  • 网络连通性(使用 nmap, nc 等工具)
  • 应用程序响应性(curl
  • 服务器配置(监听地址)
  • 防火墙和安全组规则
  • 客户端/浏览器限制

OpenTelemetry Collector 的 filelog receiver:Filebeat 和 Promtail 的替代方案?

· 4 min read

在构建可观测性(Observability)平台时,日志采集是至关重要的一环。谈到日志采集工具,我们通常会想到 FilebeatPromtail。然而,随着 OpenTelemetry (OTel) 生态的成熟,其 Collector 组件中的 filelog receiver 正在成为一个强大且值得考虑的替代方案。

正如标题所言,OpenTelemetry Collector 的 filelog receiver,在功能上确实非常类似于 Filebeat 或 Promtail。它专为从文件中读取、解析和转发日志而设计。那么,它们之间究竟有何异同?我们又该如何选择呢?

功能对比:OTel Collector vs. Filebeat vs. Promtail

为了更直观地理解它们的区别,我们可以从以下几个维度进行对比:

功能OpenTelemetry Collector (filelog)FilebeatPromtail
采集文件日志✅ 支持多格式(JSON、正则等)✅ 功能强大,支持多格式✅ 主要为 Loki 设计,采集日志
日志处理链✅ 支持解析、转换、过滤 (Processors)✅ 支持 Processors✅ 支持 Pipeline Stages
输出目标✅ 极丰富 (OTLP, Loki, Kafka, etc.)✅ Logstash, Elasticsearch 等✅ 主要发送到 Loki
集成方式✅ 原生支持 OTel 生态 (Trace/Metrics/Log)✅ 与 ELK (Elastic Stack) 紧密集成✅ 与 Grafana Loki 紧密集成
配置复杂度⚙️ 中等偏低 (单一 YAML)⚙️ 中等偏高 (可能多文件)⚙️ 中等偏低 (YAML)
推荐场景🚀 统一 OTel 可观测性平台🐘 ELK (Elastic) 技术栈Loki + Grafana 技术栈
性能与资源消耗⚡️ 高性能,资源消耗取决于配置⚡️ 高性能,轻量级⚡️ 极轻量级,专为 Loki 优化

性能与资源消耗对比

在性能方面,这三者都表现出色,因为它们都是用 Go 语言编写的,并且为高性能日志处理进行了优化。然而,它们的资源消耗和适用场景略有不同:

  • Promtail:通常被认为是最轻量级的。它的设计目标非常明确:高效地将日志发送到 Loki。由于其功能集相对精简,它的 CPU 和内存占用通常是最低的。
  • Filebeat:同样是高性能和轻量级的。作为 Beats 家族的一员,它在资源效率方面有着良好的声誉。它比 Promtail 功能更丰富,因此在进行复杂处理时资源消耗可能会略高一些。
  • OpenTelemetry Collector:性能高度依赖于配置。在一个简单的 filelog receiver -> otlp exporter 管道中,它的性能可以与 Filebeat 相媲美。但 OTel Collector 的强大之处在于其处理链(Processors),如果添加了复杂的解析、过滤或路由逻辑,资源消耗会相应增加。不过,它的设计初衷就是为了可扩展性和高性能。

总的来说,对于大多数用例,这三者的性能差异可能不是决定性因素。选择更多应基于技术栈的统一性、生态系统和特定功能需求。

如何选择:场景驱动决策

根据上面的对比,我可以得出一个清晰的选择指南:

如果你的场景是...那么推荐的工具是...
希望统一收集 Traces, Metrics 和 Logs 到一个统一的后端(如 Tempo, Mimir, Loki)OpenTelemetry Collector
正在使用 Elastic Stack (ELK) 作为你的日志平台Filebeat
正在使用 Grafana + Loki 作为你的日志系统Promtail

OTel Collector 的核心优势

为什么 OTel Collector 在现代云原生环境中越来越受欢迎?其优势主要体现在:

  • 高度统一:一个 Collector 服务实例就可以同时处理日志(logs)、追踪(traces)和指标(metrics),大大简化了架构和运维。
  • 生态开放:支持极其丰富的导出器(Exporters),可以轻松地将数据发送到各种后端,如 OTLP、Loki、Kafka、Prometheus、gRPC 等,避免了厂商锁定。
  • 云原生与中立性:相比于 Filebeat(与 ELK 强绑定)和 Promtail(与 Loki 强绑定),OTel Collector 更加“云原生”和“中立”,提供了更强的灵活性和通用性。

总结

总而言之,OpenTelemetry Collector 的 filelog receiver 确实是 Promtail 和 Filebeat 的“同类”。它作为 OTel 生态中专门负责日志采集和转发的核心模块,最适用于那些希望构建统一可观测性平台的场景,尤其是在需要将日志与 Tracing 和 Metrics 数据进行关联分析时,其优势会更加突出。

如果你正在评估新的日志采集方案,或者希望简化你现有的可观测性架构,那么 OpenTelemetry Collector 绝对值得一试。

Mac上SSH与Tmux访问外置硬盘权限差异之谜

· 5 min read

最近,有朋友遇到了一个有趣的现象:当他从安卓平板通过 SSH 连接到自己的 Mac 电脑时,无法直接访问挂载在 Mac 上的外置硬盘。然而,如果他先 SSH 登录,然后 attach 到一个先前在 Mac 本地开启的 tmux 会话中,就能够顺利访问该外置硬盘了。这究竟是为什么呢?

这个现象确实引人深思,它巧妙地揭示了不同进程环境和权限管理在 macOS 系统中的运作方式。本文将深入探讨导致这种差异的背后原因。

问题现象回顾

简单来说,遇到的情况是:

  1. 从外部设备(如安卓平板)通过 SSH 直接登录 Mac。
  2. 尝试访问外置硬盘(例如 ls /Volumes/MyExternalDrive),操作失败或提示权限不足。
  3. 断开 SSH,或在同一个 SSH 连接中,attach 到一个已在 Mac 上运行的 tmux 会话。
  4. 在 tmux 会话内的 shell 中,再次尝试访问同一个外置硬盘,操作成功。

核心原因:环境与权限的“继承”

这种行为差异的核心在于,tmux 会话继承了其启动环境所拥有的权限和上下文设置,而新建立的直接 SSH 会话则在一个相对隔离和受限的环境中运行。

详细解析

让我们来分析几个关键的技术点:

1. macOS 隐私与安全设置 (最主要原因)

macOS 系统拥有非常严格的隐私和安全控制机制。对于访问敏感数据和位置,例如“完全磁盘访问权限” (Full Disk Access) 或访问“可移除宗卷” (Removable Volumes),都需要应用程序明确获得授权。

  • tmux 会话的优势:通常,在 Mac 上启动 tmux 会话的应用程序(如系统自带的 Terminal.appiTerm2 或其他终端模拟器)可能已经在 系统设置 > 隐私与安全性 中被用户授予了这些必要的权限。因此,tmux 服务进程以及在其中运行的所有 shell 进程,都会继承这些权限。
  • 直接 SSH 会话的限制:相比之下,通过 SSH 远程登录时,负责处理连接的是 sshd (SSH daemon) 服务进程。默认情况下,sshd 以及由它为新登录会话创建的 shell,可能没有被授予访问特定用户数据或可移除宗卷的权限。这就导致了直接 SSH 登录后无法访问外置硬盘。

2. 用户上下文与挂载作用域

  • tmux 会话的上下文:在 Mac 图形界面下登录的用户启动的 tmux 会话,其运行的用户上下文与当前登录用户紧密相关。外置硬盘的挂载信息和访问权限,通常对当前活跃的图形界面用户是完全开放的。
  • SSH 会话的上下文:直接 SSH 登录时,虽然也使用特定的用户账户,但其会话环境可能并未完全加载用户的图形界面环境。某些用户级别的挂载点或权限设置,在这种“无头” (headless) 的会话中可能表现不同。

3. Shell 环境与初始化差异

尽管这一点通常不是导致完全无法访问的根本原因,但也可能产生影响:

  • tmux 内的 Shell:它继承了启动 tmux 的那个 Shell 的完整环境,包括 PATH 变量、各种已加载的模块和自定义设置。
  • 新的 SSH Shell:它会经历一个标准的 Shell 初始化过程(例如读取 .bash_profile, .zshrc 等),其初始环境可能与图形界面下或 tmux 父 Shell 的环境有所不同。

总结:Tmux 会话的“特权”从何而来

综上所述,当您 attach到一个现有的 tmux 会话时,您实际上是进入了一个已经“预热”并且获得了相应权限的执行环境。这个环境(由最初启动 tmux 的应用程序所创建)已经被 macOS 的安全系统所信任,可以访问外置硬盘等资源。

而当您通过 SSH 直接建立一个新的连接时,您得到的是一个“干净”的、遵循系统默认安全策略的会话环境。在这个环境中,对某些资源的访问会受到更严格的限制,除非 sshd 服务本身或其派生的进程被明确授予了更高级别的权限。

理解这一点,不仅能帮助我们解决类似的技术问题,更能加深我们对现代操作系统中进程管理、权限控制以及安全模型的认识。

给遇到类似问题的朋友一些建议

如果您也遇到了类似的问题,可以考虑以下几点:

  1. 检查权限设置:可以尝试检查 系统设置 > 隐私与安全性 > 完全磁盘访问权限 以及 文件和文件夹,看看是否有相关的条目。但请注意,直接修改 sshd 的系统级权限可能比较复杂,并可能带来安全风险。
  2. 启动源的权限:确保您用于启动长期运行会话(如 tmux)的应用程序(如 Terminal.app)拥有访问外部驱动器的权限。
  3. 理解工作机制:认识到这种差异是 macOS 安全机制的一部分。在很多情况下,通过一个已经由图形界面用户认证并授权的会话(如 tmux)来执行需要访问敏感资源的操作,是一种符合安全模型且实际有效的做法。

PVE 网页版无法访问排查清单

· 5 min read

当 Proxmox Virtual Environment (PVE) 的网页管理界面(通常在 https://<PVE_IP_地址>:8006)突然无法访问, 但通过 SSH 仍能正常登录到 PVE 主机时,此问题通常表明 PVE 系统本身仍在运行。 问题可能源于网络配置变更、相关服务故障,或客户端(如您的浏览器)的某些设置。 本清单提供了一系列结构化的排查步骤,旨在帮助您系统地定位并解决此类访问问题。

一、网络连通性检查

首要步骤是确认客户端设备与 PVE 主机之间的基础网络通信是否畅通。

  1. 核实网络环境与IP地址:

    • 确保您的操作设备(例如笔记本电脑)与 PVE 主机位于同一IP网段。
    • 如果跨网段访问,请检查路由器、防火墙是否正确配置了路由规则和访问策略以允许通信。
    • 确认 PVE 主机的 IP 地址是否如预期,特别是在网络环境发生过变更(如更换路由器、修改DHCP设置)后。
  2. 执行 ping 命令测试:

    • 从您的客户端设备打开命令行终端,使用 ping 命令测试到 PVE 主机的网络可达性。
      ping <PVE_IP_地址>
    • 结果分析:
      • 如果 ping 成功(收到回复),则基础网络连接通常没有问题,可以继续排查更高层面的问题。
      • 如果 ping 失败(请求超时或目标主机不可达),则需要先解决网络物理连接、IP配置(IP地址、子网掩码、网关)、 或防火墙阻塞等问题。例如,检查网线连接、Wi-Fi网络选择、IP地址冲突等。

二、PVE 系统层面检查

在确认网络连通性后,下一步是在 PVE 主机本身上检查相关服务和配置。这些操作通常通过 SSH 连接完成。

  1. 确认 SSH 访问:

    • 再次确认您可以通过 SSH 客户端(如 macOS/Linux 终端、PuTTY、Xshell 等)成功登录 PVE 主机。
      ssh root@<PVE_IP_地址>
    • 能够通过 SSH 登录是进行后续系统层面排查的前提。
  2. 检查 PVE Web 服务端口监听状态:

    • PVE 的 Web UI 服务(pveproxy)默认监听 TCP 端口 8006。登录 PVE 主机后,使用以下命令检查端口监听情况:
      ss -tulnp | grep 8006
      # 或者,如果 ss 命令不可用,可以尝试 netstat (可能需要安装 net-tools 包)
      # netstat -tulnp | grep 8006
    • 预期输出: 您应该能看到类似 LISTEN 0 4096 *:8006 *:* 的行,表明服务正在所有网络接口上监听 8006 端口。
    • 如果未监听到此端口,或监听在错误的 IP 地址上(例如 127.0.0.1:8006 而非 *:8006 或特定LAN IP), 则 pveproxy 服务可能未启动或配置不当。
  3. 检查 PVE 关键服务状态:

    • PVE 的正常运行依赖多个核心服务。检查以下主要服务的状态:
      systemctl status pveproxy.service  # PVE API 代理和 Web UI 服务
      systemctl status pvedaemon.service # PVE 集群守护进程
      systemctl status pvestatd.service # PVE 状态守护进程
    • 服务异常处理: 如果发现有服务处于 inactive (dead)failed 状态,尝试启动或重启它:
      # 示例:重启 pveproxy 服务
      systemctl restart pveproxy.service
      # 根据需要对其他服务执行类似操作
      systemctl start pveproxy.service # 如果是 inactive
    • 重启服务后,再次使用 systemctl status 命令检查其是否已成功运行。留意日志输出(如 journalctl -u pveproxy.service -f) 以获取更详细的错误信息。

三、客户端浏览器及环境检查

如果 PVE 主机端服务均正常,问题可能出在访问 PVE Web UI 的客户端设备上。

  1. 使用浏览器无痕/隐私模式访问:

    • 这是排除浏览器缓存、Cookies 或扩展插件干扰的有效方法。
    • 打开您常用浏览器(如 Chrome, Firefox, Edge)的无痕浏览窗口 (Incognito/Private Window), 然后尝试访问 PVE 的 URL (例如 https://<PVE_IP_地址>:8006)。
    • 如果无痕模式下可以正常访问,则问题很可能与浏览器缓存或某个扩展有关。
  2. 清理浏览器数据或管理扩展:

    • 针对性清理: 如果无痕模式有效,尝试清除浏览器中针对 PVE 站点的缓存文件和 Cookies。
    • 禁用扩展: 逐个禁用浏览器扩展插件,每禁用一个后尝试重新访问 PVE,以找出可能冲突的扩展。
  3. 尝试使用其他浏览器或设备:

    • 在同一台设备上的不同类型的浏览器中测试访问。
    • 如果可能,尝试从另一台设备(在同一网络中)访问 PVE Web UI,以判断问题是否特定于某个客户端设备。
  4. 检查客户端防火墙或安全软件:

    • 确保客户端设备上的防火墙(如 Windows Defender Firewall, macOS Firewall)或第三方安全软件没有错误地阻止 到 PVE 主机 8006 端口的出站 HTTPS 连接。
  5. 检查本地代理或 VPN 设置:

    • 如果您正在使用网络代理或 VPN,请尝试临时禁用它们,然后直接访问 PVE,以排除代理或 VPN 配置错误的可能性。

四、总结与后续步骤

通过以上系统性的排查步骤——从基础网络连通性,到 PVE 主机服务状态,再到客户端浏览器和环境设置—— 您通常能够定位导致 PVE Web UI 无法访问的具体原因。

如果问题依然存在,建议仔细查阅 PVE 主机上的相关服务日志(如 /var/log/daemon.log, /var/log/syslog, 以及通过 journalctl 获取的服务日志),这些日志中可能包含更具体的错误信息,有助于进一步诊断。

深入理解 Docker 容器日志机制

· 6 min read

在使用 Docker 运行应用时,日志管理是一个至关重要的环节。无论是用于调试、监控还是审计,理解 Docker 如何处理容器日志以及我们如何有效地收集和利用这些日志都非常关键。本文将结合一次实际的文件系统探索,深入解析 Docker 默认的日志机制。

Docker 日志:为何重要?

容器化应用的标准做法是将日志输出到标准输出 (stdout) 和标准错误 (stderr)。Docker 引擎会捕获这些输出流,并为我们提供了多种处理方式。这些日志是洞察容器内部运行状态的窗口,帮助我们:

  • 调试问题:当应用出现故障或异常行为时,日志是定位问题的首要线索。
  • 监控应用健康:通过分析日志,可以了解应用的实时性能和健康状况。
  • 安全审计:记录关键操作和事件,用于安全分析和合规性检查。

Docker 的默认日志驱动:json-file

默认情况下,Docker 使用 json-file 日志驱动程序。这意味着 Docker 会将容器的 stdout 和 stderr 输出捕获下来,并以 JSON 格式存储在宿主机的文件系统中。

文件系统探索之旅

让我们通过一次实际的文件系统探索,看看这些日志文件究竟存放在哪里,以及它们的内容是什么样的。

假设我们有一个在 Docker 中运行的应用,例如在 docker-compose.yml 中定义的 my-stdout-app 服务,它会周期性地向标准输出打印日志。

# docker-compose.yml (片段)
services:
my-stdout-app:
image: alpine
restart: always
command: >
sh -c "
echo 'My STDOUT App started.';
apk add --no-cache util-linux; # 尝试安装 uuidgen
while true; do
echo \"$$(date) - My STDOUT App Log Entry: $$(uuidgen)\";
sleep 3;
done
"
# ...

(注意:为了演示 stderr,我们在命令中加入了 apk add --no-cache util-linux,如果基础镜像中没有 uuidgen,后续调用会产生错误。在 alpine 镜像中,uuidgen 通常需要通过 util-linux 包安装。)

当这个容器运行时,Docker 会在宿主机的特定目录下为它创建日志文件。这个目录通常是 /var/lib/docker/containers

root@colima:/# cd /var/lib/docker/containers
root@colima:/var/lib/docker/containers# ls
05ec3afa1147550bcead37ba674e5397786b5fd515420da4becd927409fa5a6b
0715fc9a164ea5ae542f41d58967c7b3b04b182d13ffb573fa4987bf27955c5d # <--- 假设这是 my-stdout-app 的容器ID目录
7a2bf54d2fbeeb4aa8e04dcaa2a478af9b5dd7578f02632280e332af78ded800
# ... 其他容器ID ...

/var/lib/docker/containers 目录下,每个子目录的名称都是一个完整的容器 ID。我们可以进入其中一个目录(例如,对应于 my-stdout-app 的容器)查看其内容:

root@colima:/var/lib/docker/containers# cd 0715fc9a164ea5ae542f41d58967c7b3b04b182d13ffb573fa4987bf27955c5d/
root@colima:/var/lib/docker/containers/0715fc9a164ea5ae542f41d58967c7b3b04b182d13ffb573fa4987bf27955c5d# ls
0715fc9a164ea5ae542f41d58967c7b3b04b182d13ffb573fa4987bf27955c5d-json.log # <--- 这就是日志文件!
checkpoints/
config.v2.json
hostconfig.json
hostname
hosts
mounts/
resolv.conf
resolv.conf.hash

关键文件就是那个以容器完整 ID 命名并以 -json.log 结尾的文件。这就是 json-file 驱动程序存储日志的地方。

现在,让我们查看一下这个日志文件的内容:

root@colima:/var/lib/docker/containers/0715fc9a164ea5ae542f41d58967c7b3b04b182d13ffb573fa4987bf27955c5d# cat 0715fc9a164ea5ae542f41d58967c7b3b04b182d13ffb573fa4987bf27955c5d-json.log
{"log":"My STDOUT App started.\n","stream":"stdout","time":"2025-06-16T08:33:59.825309484Z"}
{"log":"Mon Jun 16 08:33:59 UTC 2025 - My STDOUT App Log Entry: \n","stream":"stdout","time":"2025-06-16T08:33:59.876601547Z"}
{"log":"sh: uuidgen: not found\n","stream":"stderr","time":"2025-06-16T08:33:59.883753556Z"}
{"log":"sh: uuidgen: not found\n","stream":"stderr","time":"2025-06-16T08:34:02.977451644Z"}

正如所见,文件中的每一行都是一个 JSON 对象,包含了三个关键字段:

  • log: 实际的日志内容。注意内容末尾通常会包含换行符 \n
  • stream: 指示日志来源,是 stdout (标准输出) 还是 stderr (标准错误)。在上面的例子中,我们可以看到应用启动信息和正常的日志条目来自 stdout,而 uuidgen: not found 的错误信息则来自 stderr
  • time: 日志条目的时间戳,采用 UTC 时间和 RFC3339Nano 格式。

这个简单的 JSON 结构使得日志易于被程序解析。

Promtail 如何利用这些日志?

像 Promtail 这样的日志收集代理可以配置为抓取这些 Docker 生成的 JSON 日志文件。在我们的 docker-compose.ymlpromtail/config.yml 配置中:

  1. docker-compose.yml (promtail 服务):

    services:
    promtail:
    # ...
    volumes:
    # ...
    # 将宿主机的 Docker 容器日志目录挂载到 Promtail 容器内部
    - /var/lib/docker/containers:/var/lib/docker/containers:ro
    # ...

    通过这个卷挂载,Promtail 容器可以访问到宿主机上所有容器的日志文件。

  2. promtail/config.yml:

    scrape_configs:
    - job_name: container_stdout_logs
    static_configs:
    - targets:
    - localhost
    labels:
    job: container_stdout
    # Promtail 在其容器内部的这个路径下查找日志文件
    __path__: /var/lib/docker/containers/*/*-json.log
    pipeline_stages:
    - docker: {} # 使用 docker 处理阶段解析 JSON 日志

    Promtail 的配置中,__path__ 指向了这些 JSON 日志文件。关键在于 pipeline_stages 中的 docker: {}。这个阶段专门用于解析 Docker json-file 驱动生成的日志格式,它会自动提取 log 字段作为日志内容,并可以利用 time 字段作为时间戳,同时保留 stream 等信息作为标签。

这样,即使应用本身只是简单地将日志打印到标准输出/错误,我们也能通过 Promtail 高效地收集、解析这些日志,并将其发送到 Loki 进行存储和查询。

其他日志驱动程序

https://docs.docker.com/engine/logging/configure/#supported-logging-drivers

虽然 json-file 是默认且常用的驱动,Docker 还支持许多其他日志驱动程序,例如:

选择哪种驱动取决于你的具体需求,比如日志量、存储位置、是否需要实时分析、以及现有的日志基础设施等。如果只是想将容器的标准输出日志收集到 Loki,并且不想或不能修改应用,那么通过 Promtail 抓取 json-file 日志是一个非常实用的方案。

总结

Docker 通过日志驱动程序提供了一个灵活的日志管理框架。默认的 json-file 驱动将容器的 stdout 和 stderr 输出以结构化的 JSON 格式存储在宿主机上,这为后续的日志收集和分析提供了便利。通过像 Promtail 这样的工具,我们可以轻松地集成这些默认的日志文件到集中的日志系统中,如 Loki,从而实现对容器化应用全面有效的监控和问题排查。理解这一机制,有助于我们更好地设计和维护健壮的容器化应用。

深入理解 Telescope entry_maker 的返回字段

· 5 min read

Telescope.nvim 是 Neovim 中一个高度可扩展的模糊查找器。它的强大功能之一在于其灵活性,允许用户自定义查找源(pickers)的行为。entry_maker 函数在自定义 picker 时扮演着至关重要的角色,它负责将原始数据转换为 Telescope 可以理解和显示的条目。本文将详细解释 entry_maker 返回的表中各个字段的含义和用途。

entry_maker 概述

在 Telescope 中,当你创建一个新的 picker 时,通常会提供一个 finder。这个 finder 负责生成候选结果。entry_makerfinder 配置的一部分,它的主要工作是将每个原始结果(例如,一个文件名、一个 LSP 定义或一个自定义数据结构)转换成一个包含特定字段的 Lua 表。这些字段告诉 Telescope 如何显示条目、如何排序以及在选择条目时执行什么操作。

一个典型的 entry_maker 函数可能看起来像这样:

finders.new_table({
results = my_raw_data,
entry_maker = function(raw_item)
return {
value = raw_item, -- 原始数据
display = raw_item.name, -- 显示的文本或函数
ordinal = raw_item.name, -- 用于排序的文本
-- 以下是可选但常用的字段
filename = raw_item.path, -- 文件路径,用于预览等
lnum = raw_item.line_number, -- 行号
col = raw_item.column_number, -- 列号
}
end,
})

下面我们将详细探讨 entry_maker 返回的表中常见的核心字段。

核心返回字段详解

1. value

  • 类型: any
  • 描述: value 字段存储了条目的原始数据或一个标识符。这个值本身不会直接显示给用户,但它会在选中条目并执行操作(如默认的 select_default)时传递给回调函数。这允许你将复杂的数据结构与列表中的每个条目关联起来。
  • 用途: 在选中操作的回调函数中,你可以通过 selected_entry.value 来访问这个原始数据,从而执行具体逻辑,例如打开文件、跳转到特定位置或处理更复杂的数据对象。

2. display

  • 类型: stringfunction
  • 描述: display 字段定义了条目在 Telescope 列表中如何向用户显示。
    • 如果是一个字符串,该字符串将直接用作显示文本。
    • 如果是一个函数,该函数将被调用,并且其返回值(通常是一个包含文本和高亮信息的列表)将用于显示。这允许进行更复杂的自定义显示,例如为文本的不同部分应用不同的颜色或样式。
  • 用途: 这是用户直接看到的文本内容。精心设计的 display 文本可以极大地提高 picker 的可用性。

3. ordinal

  • 类型: string
  • 描述: ordinal 字段提供了一个用于排序和初步筛选的文本表示。Telescope 的模糊查找算法会主要针对 ordinal 文本进行匹配。
  • 用途: 通常,你会希望 ordinal 包含最关键的、用户最可能用来搜索的信息。例如,对于文件查找器,ordinal 可能是文件名或完整路径。它不一定与 display 字段完全相同。如果 display 包含复杂的格式或多个信息片段,ordinal 可以是其中最重要的部分,以便于搜索。

可选但常用的增强字段

除了上述核心字段外,entry_maker 还可以返回一些可选字段,这些字段可以增强 Telescope 的功能,例如文件预览、跳转到文件特定位置等。

4. filename

  • 类型: string
  • 描述: 如果条目与某个文件相关联,此字段应包含该文件的完整路径。
  • 用途: Telescope 使用 filename 字段来实现多种功能:
    • 文件预览: 当配置了预览器(如 grep_previewer 或自定义预览器)时,Telescope 会使用 filename 来加载和显示文件内容。
    • 跳转操作: 像 select_horizontal (在新分割中打开)、select_vertical (在新垂直分割中打开) 和 select_tab (在新标签页中打开) 这样的操作会使用 filename (配合 lnumcol,如果提供的话) 来打开文件。
    • 在某些 Telescope 扩展或自定义操作中,也可能依赖此字段来定位文件。

5. lnum

  • 类型: number
  • 描述: 如果条目不仅与文件相关,还与文件中的特定行相关(例如,LSP 定义、搜索结果),此字段应包含该行的行号。
  • 用途: 与 filename 结合使用,允许 Telescope 或其操作在打开文件后直接跳转到指定的行。

6. col

  • 类型: number
  • 描述: 类似于 lnum,此字段表示文件特定行中的列号。
  • 用途: 与 filenamelnum 结合使用,可以更精确地定位到文件中的具体位置。

示例回顾

让我们回顾一下 bookmarks.picker.bookmark-picker.luaentry_maker 的例子:

---@param bookmark Bookmarks.Node
entry_maker = function(bookmark)
local entry_display = vim.g.bookmarks_config.picker.entry_display or format_entry
local display = entry_display(bookmark, _bookmarks)
return {
value = bookmark, -- 存储整个 bookmark 对象
display = display, -- 自定义格式化后的显示字符串
ordinal = display, -- 使用显示字符串进行排序和搜索
filename = bookmark.location.path, -- 书签所在的文件路径
col = bookmark.location.col, -- 书签的列号
lnum = bookmark.location.line, -- 书签的行号
}
end

在这个例子中:

  • value 存储了完整的 bookmark 对象,以便在选中时可以访问其所有属性。
  • display 是通过 format_entry 函数生成的自定义字符串,用于美观地展示书签信息。
  • ordinal 也使用了这个 display 字符串,意味着用户将根据看到的文本进行搜索。
  • filename, lnum, col 则直接从 bookmark.location 中获取,使得 Telescope 能够预览书签所在的文件内容,并在选中时跳转到精确位置。

结论

理解 entry_maker 返回的各个字段对于充分利用 Telescope 的强大自定义能力至关重要。通过精心构造这些字段,你可以创建出既美观又高效的模糊查找器,极大地提升 Neovim 的使用体验。希望本文能帮助你更好地掌握 Telescope entry_maker 的使用。

使用 Vim 的 :global 命令高效删除匹配行

· 3 min read

在 Vim 中编辑文本时,我们经常会遇到需要删除所有包含特定模式的行的场景。虽然可以通过多种方式实现,例如使用替换命令 (:s) 将匹配行替换为空,但一个更符合 Vim 习惯且通常更简洁的方法是使用 :global 命令(或其简写 :g)。

本文将介绍如何使用 :global 命令来高效地删除匹配特定模式的行。

:global 命令简介

:global 命令是 Vim 中一个非常强大的 Ex 命令,它允许你在匹配特定模式的所有行上执行另一个 Ex 命令。其基本语法如下:

:g/{pattern}/{command}
  • {pattern}: 你想要搜索的正则表达式模式。
  • {command}: 你想要在每个匹配行上执行的 Ex 命令(例如 d 表示删除,s 表示替换,p 表示打印等)。

使用 :g/.../d 删除匹配行

要删除所有包含特定模式的行,最直接的命令组合是 :g/{pattern}/d

具体示例:删除所有包含 "---@type" 的行

假设你想要删除文件中所有包含字符串 ---@type 的行,你可以使用以下命令:

:g/---@type/d

让我们分解这个命令:

  • :g: 这是 global 命令的简写,告诉 Vim 在符合条件的行上执行操作。
  • /---@type/: 这是要搜索的模式。Vim 会查找所有包含这个精确字符串 ---@type 的行。你需要将 / 替换为其他分隔符,如果你的模式本身包含 /
  • d: 这是 delete 命令。它会删除当前行。当与 :g 结合使用时,它会删除所有匹配模式的行。

为什么 :global 命令更佳?

与使用替换命令(例如 :s/.*---@type.*//n 然后手动删除空行,或者更复杂的 :s/^.*---@type.*\n// 但这可能不总是按预期工作或留下空行)相比,:g/.../d 命令有以下优点:

  1. 直接性: 它的意图非常明确——在匹配的行上执行删除操作。
  2. 简洁性: 命令本身非常简短易记。
  3. 高效性: Vim 专门为此类操作优化了 :global 命令。
  4. 准确性: 它准确地删除整个匹配行,不会留下不必要的空行或部分内容(除非模式设计不当)。

结论

Vim 的 :global 命令提供了一种强大而简洁的方式来对文件中的行进行批量操作。通过组合 :gd 命令,你可以轻松高效地删除所有匹配特定模式的行。这是每个 Vim 用户都应该掌握的一个实用技巧,能够显著提高文本编辑效率。

使用阿里云容器镜像服务管理Docker镜像

· 4 min read

阿里云容器镜像服务(Container Registry, CR)提供了一种安全高效的方式来存储和管理您的Docker镜像。 本文将指导您完成登录、标记镜像、推送镜像的过程,并介绍一个有趣的Docker配置特性,它可以简化跨多台机器的身份验证。

登录阿里云容器镜像服务

在您可以从私有阿里云容器镜像服务推送或拉取镜像之前,您需要登录。阿里云会为您的镜像实例提供特定的凭证。

通常,登录命令如下所示:

docker login --username=<您的阿里云用户名> registry.cn-hangzhou.aliyuncs.com

系统将提示您输入密码。对于阿里云容器镜像服务,这通常是特定的镜像实例登录密码或访问密钥。

注意: 确切的镜像仓库URL(例如 registry.cn-hangzhou.aliyuncs.com 或个性化域名如 crpi-wtz8s2mdlrli98p4.cn-hangzhou.personal.cr.aliyuncs.com)将在您的阿里云控制台中提供。

标记和推送Docker镜像

登录后,您可以标记本地Docker镜像并将其推送到您的阿里云CR。该过程包括:

  1. 标记现有的本地镜像: 您需要使用完整的镜像仓库路径、您的命名空间、镜像名称和标签来标记您的镜像。
  2. 推送已标记的镜像: 使用带有新标签的 docker push 命令。

以下是您用作示例的命令:

标记并推送 nginx:alpine 镜像:

# 标记本地 nginx:alpine 镜像
docker tag nginx:alpine crpi-wtz8s2mdlrli98p4.cn-hangzhou.personal.cr.aliyuncs.com/oatnil/nginx:alpine

# 将标记的镜像推送到阿里云CR
docker push crpi-wtz8s2mdlrli98p4.cn-hangzhou.personal.cr.aliyuncs.com/oatnil/nginx:alpine

拉取镜像

要从您的阿里云CR拉取镜像,请使用 docker pull 命令,后跟完整的镜像路径:

docker pull crpi-wtz8s2mdlrli98p4.cn-hangzhou.personal.cr.aliyuncs.com/oatnil/nginx:alpine

理解 ~/.docker/config.json

当您使用 docker login 成功登录到Docker镜像仓库时,Docker会将身份验证凭据存储在位于 ~/.docker/config.json 的文件中。这个JSON文件包含一个 auths 部分,其中存储了各个镜像仓库的凭据,通常是经过base64编码的。

例如,登录到您的阿里云CR后,config.json 文件可能如下所示:

{
"auths": {
"crpi-wtz8s2mdlrli98p4.cn-hangzhou.personal.cr.aliyuncs.com": {
"auth": "base64_encoded_username_password_string"
}
// ... 其他镜像仓库
}
// ... 其他配置
}

auth 字符串是 用户名:密码 的base64编码。

在其他机器上跳过 docker login

对于CI/CD流水线或设置新机器时,一个方便的技巧是重用这个 ~/.docker/config.json 文件。 如果您将已成功运行 docker login 的机器上的 ~/.docker/config.json 文件复制到另一台机器(将其放置在将运行Docker命令的用户的相同 ~/.docker/ 目录中),新机器上的Docker将使用这些缓存的凭据。这使您可以跳过 docker login 步骤。

如何操作:

  1. 在已登录的机器上,找到 ~/.docker/config.json
  2. 安全地将此文件复制到目标机器,放置在 ~/.docker/config.json
  3. 确保文件权限适当(例如,用户可读)。

注意:config.json 文件包含敏感凭据。请安全处理,并确保只有受信任的用户和进程才能访问它。 除非加密,否则避免将其提交到版本控制系统。

结论

阿里云容器镜像服务为管理您的Docker镜像提供了一个强大的解决方案。 通过了解登录过程、如何标记和推送镜像以及 ~/.docker/config.json 的作用, 您可以简化Docker工作流程,尤其是在跨多个环境工作或自动化部署时。请记住, 由于其包含敏感凭据,务必小心处理您的Docker配置文件。