[{"content":"","date":"16 September 2025","externalUrl":null,"permalink":"/zh-cn/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"16 September 2025","externalUrl":null,"permalink":"/zh-cn/tags/devops/","section":"Tags","summary":"","title":"Devops","type":"tags"},{"content":"","date":"16 September 2025","externalUrl":null,"permalink":"/zh-cn/categories/linux/","section":"Categories","summary":"","title":"Linux","type":"categories"},{"content":"","date":"16 September 2025","externalUrl":null,"permalink":"/zh-cn/tags/linux/","section":"Tags","summary":"","title":"Linux","type":"tags"},{"content":"","date":"16 September 2025","externalUrl":null,"permalink":"/zh-cn/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"16 September 2025","externalUrl":null,"permalink":"/zh-cn/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"16 September 2025","externalUrl":null,"permalink":"/zh-cn/","section":"涧雨暖云","summary":"","title":"涧雨暖云","type":"page"},{"content":"这是一个为 Nushell 设计的自定义欢迎信息脚本，旨在替换默认的启动横幅，并在每次启动 Shell 时，提供一个关于系统状态的、信息丰富的仪表盘。\n功能概览 # 系统信息: 显示主机名、内核版本和当前登录用户。 上次登录: 展示最后一次登录系统的用户信息、来源IP和时间。 资源状态: CPU: 显示 CPU 型号和实时的1分钟、5分钟、15分钟系统平均负载。 内存: 以 GB 为单位，显示已用和总内存大小，以及使用百分比。 磁盘: 以 GB 为单位，显示磁盘的已用和总空间大小，以及使用百分比。 容器状态: Podman 容器: 汇总显示已发现的容器总数，以及其中正在运行和已退出的容器数量。 Podman Pods: 汇总显示已发现的 Pod 总数，以及其中正在运行的 Pod 数量。 安全状态: Fail2ban: 以表格形式，展示 Jails 的“当前失败”、“累计失败”、“当前封禁”和“累计封禁”四个关键指标。 依赖 # 核心工具: uname, last, df (这些是所有现代 Linux 发行版的基础组件)。 可选工具: podman: 如果未安装或服务未运行，相关部分将显示一条友好的警告信息。 fail2ban-client: 如果未安装，相关部分将显示警告。 sudo: 为了让非 root 用户能够查询 Fail2ban 状态，需要预先配置 sudo 规则。 安装 # 备份旧配置: 在进行任何修改前，请先备份您现有的 config.nu 文件。 1cp ~/.config/nushell/config.nu ~/.config/nushell/config.nu.bak 替换配置: 将下面的完整代码，复制并完全替换掉您 ~/.config/nushell/config.nu 文件中的内容。 (可选， 不推荐) 配置 Sudo for Fail2ban: 为了让非 root 用户能够看到 Fail2ban 状态，请使用 sudo visudo 命令，在文件末尾添加以下规则 (请将 your_username 替换真实用户名): 1your_username ALL=(ALL) NOPASSWD: /usr/bin/fail2ban-client status 重启 Nushell: 关闭并重新打开终端，即可看到全新的自定义欢迎界面。 代码地址 # https://github.com/yuzjing/devScripts/blob/main/nu_banner\n","date":"16 September 2025","externalUrl":null,"permalink":"/zh-cn/posts/linux/nushell_bootinfo/","section":"Posts","summary":"","title":"自定义 Nushell 欢迎信息","type":"posts"},{"content":"这份文档记录了我个人在不同平台上使用的核心工具与软件。V5.0 版本新增了“浏览器扩展”分类，并进一步丰富了自托管与命令行工具。\n平台图标说明:\n🐧 - Linux 🪟 - Windows 🤖 - Android 🌐 - Web/跨平台 🐧 系统部署与维护 (System Deployment \u0026amp; Maintenance) # Arch Linux Install Script - 🐧 官方的 archinstall 脚本，极大地简化了初始安装。 reinstall.sh - 🐧 常用的 VPS 一键重装（DD）脚本，能将服务商提供的系统模板替换为纯净的 Arch Linux。 Reflector - 🐧 自动获取最快的 pacman 下载镜像源，装机后优化的第一步。 virtualization \u0026amp; Subsystems (虚拟化与子系统) # Rolling LTS Kernel for WSL2 - 🪟 为 WSL2 提供的滚动更新 LTS 内核，用于获得最新的内核功能与性能优化。 Podman - 🐧 Docker 的无守护进程替代品，更安全、更轻量。 ⌨️ 终端与 Shell 环境 (Terminal \u0026amp; Shell Environment) # Nushell - 🐧🪟 我的主力 Shell，将一切视为结构化数据。 Oh My Zsh - 🐧 Zsh 的终极配置框架。 Konsole - 🐧 KDE Plasma 下的主力终端。 Windows Terminal - 🪟 Windows 平台终端的现代答案。 Termux - 🤖 在 Android 上提供一个功能强大的 Linux 终端环境。 🎨 个性化与美化 (Customization \u0026amp; Theming) # Oh My Posh - 🐧🪟🌐 跨平台的终端提示符美化引擎。 Windows Terminal Themes - 🌐 为 Windows Terminal 提供精美配色方案的网站。 Catppuccin for Fcitx5 - 🐧 Catppuccin 色系主题，应用于 Fcitx5 输入法。 🛠️ 命令行核心工具 (Core Command-Line Tools) # Paru - 🐧 Arch Linux 上的 AUR 助手。 Bat - 🐧🪟 cat 的现代化替代品，带语法高亮。 Bottom - 🐧🪟 top/htop 的现代化替代品，图形化系统监控。 Aria2 - 🐧🪟 命令行下载神器，支持多线程和 BT，通常作为后台服务运行。 dust - 🐧 Rust 重写的 du。 🌐 浏览器扩展与脚本 (Browser Extensions \u0026amp; Userscripts) # 增强浏览器功能的核心插件。\nViolentmonkey - 🌐 开源的用户脚本管理器，用于在网页上运行自定义 JavaScript 脚本。 Kiss Translator - 🌐 沉浸式的网页翻译扩展，简洁、高效且支持多种翻译引擎。 ZeroOmega - 🌐 可能是 SwitchyOmega 的一个分支或类似工具，用于轻松管理和切换浏览器代理设置。 📝 文本、笔记与开发 (Text, Notes \u0026amp; Development) # Helix - 🐧🪟 类 Vim 的现代化终端文本编辑器。 Acode editor - 🤖 Android 上的轻量级代码编辑器。 NoteGen - 🤖 简洁的 Material Design 3 风格笔记应用。 Hugo - 🐧🪟🌐 极速静态网站生成器。 🖼️ 媒体处理与捕获 (Media Processing \u0026amp; Capture) # Haruna Video Player - 🐧 KDE 生态下的优秀视频播放器。 MPV-KT - 🤖 基于 MPV 的本地视频播放器。 ShareX - 🪟 Windows 截图、录屏、工作流自动化的瑞士军刀。 Seal - 🤖 基于 yt-dlp 的 Android 视频/音频下载器。 📦 自托管服务与 NAS (Self-Hosting \u0026amp; NAS) # 在我自己的服务器或 NAS 上运行的服务。\nImmich - 🐧🌐 自托管的 Google Photos 替代品，用于备份和管理个人照片与视频。 Aria2NG - 🐧🌐 一个美观易用的 Web UI，用于远程管理 Aria2 下载任务。 🌐 网络工具 (Networking Tools) # GUI for Sing-box - 🪟 用于 Sing-box 核心的图形化代理客户端。 📖 阅读 (Reading) # 阅读 - 🤖 开源小说阅读软件，通过自定义书源实现全网搜书。 ⌨️ 输入法 (Input Methods) # Fcitx5 - 🐧 Linux 下的主力输入法框架。 Rime-ice - 🐧 Rime 输入法引擎的-配置方案，提供强大的智能拼音。 Gboard - 🤖 谷歌输入法。 ✨ 系统增强与自动化 (System Enhancement \u0026amp; Automation) # GKD (搞快点) - 🤖 App 广告自动跳过工具。 Shizuku - 🤖 为普通应用提供使用系统级 API 的桥梁。 雹 (Hail) - 🤖 利用 Shizuku 或 root 权限冻结应用，防止后台运行。 🎓 教育与学习 (Education \u0026amp; Learning) # z2h字帖 - 🌐 在线生成和打印书法字帖。 ","date":"15 September 2025","externalUrl":null,"permalink":"/zh-cn/posts/notes/toolkit/","section":"Posts","summary":"","title":"🚀 My Tools","type":"posts"},{"content":"","date":"15 September 2025","externalUrl":null,"permalink":"/zh-cn/categories/notes/","section":"Categories","summary":"","title":"Notes","type":"categories"},{"content":"","date":"15 September 2025","externalUrl":null,"permalink":"/zh-cn/tags/software/","section":"Tags","summary":"","title":"Software","type":"tags"},{"content":"","date":"15 September 2025","externalUrl":null,"permalink":"/zh-cn/tags/theme/","section":"Tags","summary":"","title":"Theme","type":"tags"},{"content":"","date":"15 September 2025","externalUrl":null,"permalink":"/zh-cn/tags/tool/","section":"Tags","summary":"","title":"Tool","type":"tags"},{"content":"","date":"8 September 2025","externalUrl":null,"permalink":"/zh-cn/categories/devops/","section":"Categories","summary":"","title":"DevOps","type":"categories"},{"content":"","date":"8 September 2025","externalUrl":null,"permalink":"/zh-cn/tags/network/","section":"Tags","summary":"","title":"Network","type":"tags"},{"content":"","date":"8 September 2025","externalUrl":null,"permalink":"/zh-cn/tags/wsl/","section":"Tags","summary":"","title":"WSL","type":"tags"},{"content":"在 WSL2 的使用中，一个常见的网络现象是：从 Linux 环境访问 Windows 主机服务时，127.0.0.1 可用，而 ::1 失败。本文档旨在通过对比分析 NAT 和镜像 (Mirrored) 两种网络模式下的真实接口数据，深入总结这一现象的底层原因，并对镜像模式下出现的 loopback0 接口进行探讨。\n核心原则：两个内核，两个隔离的回环接口 # 在深入分析两种模式之前，必须明确一个核心原则：WSL2 (运行一个完整的 Linux 内核) 和 Windows (运行 NT 内核) 是两个独立的操作系统。回环接口 (localhost, lo) 本质上是内核级的“内部事务”，其流量被设计为永远不会离开本地操作系统的网络堆栈。\n因此，WSL2 的 ::1 指向 Linux 内核自己，而 Windows 的 ::1 指向 Windows 内核自己。它们是两个隔离的、无法互通的地址。当您在 WSL2 中尝试连接 ::1 时，请求永远不会离开 WSL2 去访问 Windows，连接失败是符合网络基本原理的。\n127.0.0.1 之所以能神奇地工作，是因为微软为 IPv4 实现了一个特殊的跨系统边界的转发/中继机制。这个机制在不同网络模式下实现方式不同。\n1. 默认 NAT 模式：隔离的虚拟网络 # 在此模式下，WSL2 通过一个虚拟交换机与主机通信，其网络环境相对独立。\n接口观察 # WSL2 ip a 输出 (NAT 模式):\n1# ... 22: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 3 link/ether 00:15:5d:xx:xx:xx # 典型的 Hyper-V 虚拟 MAC 地址 4 inet 172.20.x.x/20 # 位于私有 IP 段的虚拟地址 5# ... Windows ipconfig /all 输出:\n1# ... 2Description . . . . . . . . . . . : Realtek PCIe GBE Family Controller 3Physical Address. . . . . . . . . : AA-BB-CC-DD-EE-FF 4IPv4 Address. . . . . . . . . . . : 192.168.1.100 5# ... 分析结论:\n接口不匹配: WSL2 内 eth0 的 MAC 地址 (00:15:5d:...) 和 IP 地址 (172.20.x.x) 与 Windows 的物理网卡完全不同。这在物理层面上证明了 WSL2 运行在一个由 Hyper-V 管理的、与主机隔离的虚拟网络中。 localhost 行为解释: 127.0.0.1 (IPv4): 流量能成功到达 Windows，是因为 WSL2 的网络服务在出口处对发往此地址的流量进行了特殊的网络地址转换 (NAT) 处理，将其路由到了主机在虚拟网络中的网关地址。 ::1 (IPv6): 此 NAT 机制并未对 IPv6 实现。因此，发往 ::1 的流量遵循标准路由，终点是 WSL2 自身的、与 Windows 隔离的回环接口 lo，导致连接失败。 2. 镜像模式 (Mirrored Mode)：共享的物理网络与特殊的内部通道 # 此模式旨在消除网络隔离，使 WSL2 在网络层面成为主机的“对等体”。\n接口观察 # WSL2 ip a 输出 (镜像模式):\n1# ... 22: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 3 link/ether aa:bb:cc:dd:ee:ff # \u0026lt;-- 与 Windows 物理 MAC 完全相同 4 inet 192.168.1.100/24 # \u0026lt;-- 与 Windows IP 完全相同 5 inet6 fe80::xxxx:xxxx:xxxx:xxxx/64 # \u0026lt;-- 与 Windows Link-local IPv6 完全相同 63: loopback0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 7 link/ether 00:15:5d:yy:yy:yy # \u0026lt;-- 一个独立的虚拟 MAC 地址 8# ... 分析结论:\neth0 的镜像: WSL2 的 eth0 接口的 MAC 和 IP 地址与 Windows 物理网卡完全一致。这证明了 WSL2 的外部网络连接是主机网卡的直接镜像，它们共享同一个网络身份。 localhost 行为解释: 127.0.0.1 (IPv4): 尽管外部网络共享，但 WSL2 (Linux 内核) 与 Windows (NT 内核) 的回环接口 lo 依然是相互隔离的。其成功通信依赖于一个名为 \u0026ldquo;localhost relay\u0026rdquo; (本地回环中继) 的机制。 ::1 (IPv6): 此 \u0026ldquo;localhost relay\u0026rdquo; 机制是已知的、仅支持 IPv4 的限制。因此，发往 ::1 的流量无法被中继，终点仍是 WSL2 自身隔离的 lo 接口。 对 loopback0 接口的探讨 # 在镜像模式下出现的 loopback0 接口，其命名和虚拟 MAC 地址 (00:15:5d:...) 表明，它是一个专用的、由 Hyper-V 提供的虚拟接口，即使在镜像模式下，部分虚拟化组件依然在工作。\n其作用可推测为： 它是实现 Windows 与 WSL2 之间内核级内部通信的专用通道。\nlocalhost 中继的载体: 上文提到的 \u0026ldquo;localhost relay\u0026rdquo; 机制，很可能就是通过这个 loopback0 接口实现的。当 WSL2 向 127.0.0.1 发送流量时，该流量被特殊路由到 loopback0，再由 Windows 端的对应虚拟接口接收并注入到主机的网络堆栈中。 其他管理通信: 除了 localhost 中继，此接口也可能承载其他 WSL2 与 Windows 间的管理和控制流量，它提供了一条不经过外部物理网络 (eth0) 的、稳定可靠的内部通信路径。 经验总结 # localhost 的隔离性是根本: 无论网络模式如何，WSL2 和 Windows 作为两个独立的内核，其回环接口 lo 始终是隔离的。::1 的连接失败忠实地反映了这一底层事实，是符合网络原理的正常行为。 127.0.0.1 的可用性是特例: 其成功通信是微软通过不同技术（NAT 模式下的地址转换，镜像模式下的中继通道）实现的、一个仅限 IPv4 的“便利功能”。 接口差异是关键证据: 通过 ip a 对比可以清晰地看出两种模式的架构差异：NAT 模式下是隔离的虚拟 eth0，而镜像模式下是共享的物理 eth0。 loopback0 是镜像模式的关键组件: 它的出现是为了在共享外部网络的同时，提供一个独立的内部通信桥梁，以实现 localhost 转发等特殊的宿主机-虚拟机交互功能。 ","date":"8 September 2025","externalUrl":null,"permalink":"/zh-cn/posts/devops/wsl2_ipv6/","section":"Posts","summary":"","title":"WSL2跨系统访问中::1的坑","type":"posts"},{"content":"记录了一次在使用 Caddy 反向代理中的正向代理时, 牵扯出 Caddy 指令的演进、版本间的行为差异，甚至一度让我们怀疑官方文档的准确性。\nforward_proxy_url 的迷雾与版本之谜 # 我们的核心需求是在 Caddy 反向代理中，再通过一个内部的正向代理去连接最终的后端服务。这形成了一个“代理套代理”的经典场景。\n目标架构：用户 -\u0026gt; Caddy (反代) -\u0026gt; 内部代理 -\u0026gt; 后端应用\n初次尝试：forward_proxy_url # 根据经验，我们在 Caddyfile 中写下了如下配置：\n1app.example.com { 2 # ... 3 reverse_proxy backend-service:8188 { 4 transport http { 5 # 使用 forward_proxy_url 指定下一跳代理 6 forward_proxy_url http://internal-proxy:1025 7 } 8 } 9} 然而，在重载 Caddy 配置时，我收到了第一条令人困惑的信息：\n{\u0026quot;level\u0026quot;:\u0026quot;warn\u0026quot;, ... \u0026quot;msg\u0026quot;:\u0026quot;The 'forward_proxy_url' field is deprecated. Use 'network_proxy \u0026lt;url\u0026gt;' instead.\u0026quot;}\n这是一个明确的弃用警告。它告诉我们 forward_proxy_url 已经过时，应该使用新的 network_proxy 指令。\n第二次尝试：network_proxy 的挫败 # 我们遵从警告的指示，将配置修改为：\n1# 方案 A: 将 network_proxy 放在 transport http 内部 2reverse_proxy backend-service:8188 { 3 transport http { 4 network_proxy http://internal-proxy:1055 5 } 6} 7 8# 方案 B: 将 network_proxy 作为 reverse_proxy 的直接子指令 9reverse_proxy backend-service:8188 { 10 network_proxy http://internal-proxy:1055 11} 然而，无论采用哪种方案，Caddy 都返回了错误：unrecognized subdirective network_proxy。\n这让我有点疑惑：Caddy 一方面警告我们旧指令已弃用，另一方面又不认识它推荐的新指令。 另外Caddy官方文档中的介绍是forward_proxy_url, 没有任何提到network_proxy的内容。\n居然是版本“Bug” # 在反复确认我使用的 Caddy（v2.10.0）版本较新且, 并且不需要任何build自定义插件后，问题的焦点最终落在了 Caddy 自身。\n最终艰难的找到了问题原因https://github.com/caddyserver/caddy/pull/6978\n居然是2.10.0的版本BUG, 2.10.1中已修复\u0026hellip;坑啊\n解决方案：\n最终我重新用2.10.2版本的caddy重新build了我的caddy镜像, 至于弃用警告我直接忽略了，network_proxy参数并不能正常工作, 继续使用 forward_proxy_url。\n1# 最终有效配置：忽略警告，继续使用 2reverse_proxy backend-service:8188 { 3 transport http { 4 forward_proxy_url http://internal-proxy:1055 5 } 6} 经过这次，发现即使是稳定版的软件，也可能存在文档、警告和实际行为之间的微小脱节。当遇到类似情况时，应以实际运行结果为准。\nHost 头引发的浏览器重定向循环 # 在解决了代理指令的问题后，我遇到了第二个难题。通过 curl 在 Caddy 容器内测试代理链路是完全成功的：\ncurl -x http://internal-proxy:1055 http://backend-service:8188\ncurl 能完美获取后端应用的页面。然而，通过浏览器访问 https://app.example.com 却陷入了无限重定向的循环。\n症状与根源 # 分析caddy日志以及研究curl和caddy动作区别发现:\ncurl 之所以成功，是因为它在请求后端的 Host 头中，使用的是后端的 IP 地址。\n而浏览器通过 Caddy 访问时，Host 头被传递为公网域名 (app.example.com)。许多后端应用在收到一个与自身监听地址不符的 Host 头时，会出于安全或规范化考虑，发起一次重定向。这个重定向往往会与 Caddy 的自动 HTTPS 功能形成冲突，导致循环。\n解决方案：伪装 Host 头 # 为了让 Caddy 的行为与成功的 curl 命令保持一致，我们使用 header_up 强制修改了发往后端的 Host 头。\n1reverse_proxy backend-service:8188 { 2 # 强制将 Host 头修改为后端自身的地址 3 header_up Host {http.reverse_proxy.upstream.hostport} 4 5 transport http { 6 forward_proxy_url http://internal-proxy:1055 7 } 8} 通过这一行简单的配置，我们让后端应用收到了它“期望”的 Host 头，从而停止了不必要的重定向，浏览器访问恢复正常。\n总结 # 理性看待警告：软件的弃用警告虽值得关注，但当新方案无法工作时，应相信当前版本下依然有效的旧方案。 Host 头是魔鬼：在反向代理环境中，Host 头是导致“curl可以，浏览器不行”这类问题的首要疑犯。通过 header_up 控制。 ","date":"29 August 2025","externalUrl":null,"permalink":"/zh-cn/posts/devops/caddy_forward_proxy/","section":"Posts","summary":"","title":"Caddy 代理疑云：从指令弃用到版本“Bug","type":"posts"},{"content":"","date":"27 August 2025","externalUrl":null,"permalink":"/zh-cn/tags/container/","section":"Tags","summary":"","title":"Container","type":"tags"},{"content":"","date":"27 August 2025","externalUrl":null,"permalink":"/zh-cn/categories/container--virtualization/","section":"Categories","summary":"","title":"Container \u0026 Virtualization","type":"categories"},{"content":"","date":"27 August 2025","externalUrl":null,"permalink":"/zh-cn/tags/podman/","section":"Tags","summary":"","title":"Podman","type":"tags"},{"content":"","date":"27 August 2025","externalUrl":null,"permalink":"/tags/virtualization/","section":"Tags","summary":"","title":"Virtualization","type":"tags"},{"content":"","date":"27 August 2025","externalUrl":null,"permalink":"/zh-cn/tags/%E5%AE%B9%E5%99%A8/","section":"Tags","summary":"","title":"容器","type":"tags"},{"content":"记录了在使用 Podman以无根容器（Rootless）方式部署 Warpgate 时遇到的坑.\n目标环境:\n容器引擎: Podman (无根模式) 服务管理: Systemd / Quadlet 反向代理: Caddy (同样为无根容器) 网络: Caddy 和 Warpgate 位于同一个自定义 Podman 网络中。 坑一：服务无法启动，提示 warpgate.yaml 配置文件不存在 # 这是部署过程中的第一个障碍。通过 systemctl 启动服务后，日志反复显示 Warpgate 因找不到 /data/warpgate.yaml 配置文件而失败退出。\n解决方案：使用 podman run 执行一次性 setup 命令 # Warpgate 的设计是在首次运行时，通过一个交互式的 setup 命令来生成配置文件，而不是手动创建。Quadlet 用于管理长期服务，因此需要先用 podman run 手动完成这个一次性的设置任务。\n执行 setup 命令: 该命令会启动一个临时容器，运行交互式设置向导，并将生成的配置存入一个 Podman 卷中。\n1podman run -it --rm --name warpgate-setup -v warpgate-data:/data --network=caddy ghcr.io/warp-tech/warpgate:latest setup -it: 启用交互式终端，用于回答设置向导的问题。 --rm: 命令结束后自动删除这个临时容器。 -v warpgate-data:/data: 使用名为 warpgate-data 的卷来持久化配置，这个卷之后会被 Quadlet 服务使用。 使用 Quadlet 运行服务: setup 完成后，warpgate-data 卷里就已经有配置文件了。此时 systemctl 启动的服务就可以找到配置，并正常运行了。\n坑二：服务正常运行，但 Caddy 反向代理报 502 Bad Gateway # Warpgate 容器成功启动并监听端口，但在通过 Caddy 反代访问时，浏览器显示 502 错误。\n解决方案：将 Caddy 反代协议从 HTTP 修改为 HTTPS # 经排查，问题的根源在于 Warpgate 内部的 8888 端口正在监听 HTTPS 流量，而不是常规的 HTTP。这很可能是在交互式 setup 过程中，当被问及 Public URL 时，输入了 https:// 开头的地址，Warpgate 据此自动启用了内部 TLS。\nCaddy 默认使用 HTTP 连接后端，因此连接被 Warpgate 拒绝，导致 502 错误。\n解决方案是修改 Caddyfile，让它使用 HTTPS 连接到 Warpgate，并忽略其内部自签名证书的验证错误。\n1\u0026lt;your-domain\u0026gt; { 2 # ... 其他配置 ... 3 4 # 使用 https 协议反代到 warpgate，并跳过内部通信的 TLS 证书验证 5 reverse_proxy https://warpgate:8888 { 6 transport http { 7 tls_insecure_skip_verify 8 } 9 } 10} https://warpgate:8888: 告诉 Caddy 上游是 HTTPS 服务。 tls_insecure_skip_verify: 告诉 Caddy 忽略上游证书的验证错误，这在反向代理到使用自签名证书的内部服务时是标准做法。 最终配置总结 # ~/.config/containers/systemd/warpgate.container # 1[Unit] 2Description=Warpgate Secure Access Gateway 3 4 5[Container] 6ContainerName=warpgate 7Image=ghcr.io/warp-tech/warpgate:latest 8AutoUpdate=image 9PodmanArgs=--network=caddy 10Volume=warpgate-data:/data 11 12[Service] 13Restart=on-failure 14 15[Install] 16WantedBy=default.target Caddyfile 中的相关配置 # 1\u0026lt;your-domain\u0026gt; { 2 3 4 encode zstd gzip 5 6 reverse_proxy https://warpgate:8888 { 7 transport http { 8 tls_insecure_skip_verify 9 } 10 } 11} ","date":"27 August 2025","externalUrl":null,"permalink":"/zh-cn/posts/container/warpgate/","section":"Posts","summary":"","title":"使用  Quadlet 部署 Warpgate 无根容器的踩坑总结","type":"posts"},{"content":"","date":"27 August 2025","externalUrl":null,"permalink":"/zh-cn/tags/%E8%99%9A%E6%8B%9F%E5%8C%96/","section":"Tags","summary":"","title":"虚拟化","type":"tags"},{"content":" 容器运行时解析：Podman vs. Docker vs. Containerd # 在云原生和容器化领域，Podman、Docker 和 Containerd 是构建和管理容器的三大核心技术。尽管它们都能够运行符合 OCI (Open Container Initiative) 规范的容器，但其架构设计、核心哲学和适用场景却截然不同。\n核心架构：Daemonless vs. Client-Server # 一切差异的根源在于它们截然不同的架构模型。\n架构模型 Podman Docker / Containerd 范式 无守护进程 (Daemonless) 客户端-服务器 (Client-Server) 进程模型 podman CLI 工具通过传统的 fork/exec 模型直接创建和管理容器。它会启动一个轻量级的容器监视器 conmon，该监视器作为容器进程的直接父进程，负责日志流、TTY 交互和退出码报告。podman 命令本身在容器启动后即可退出。 docker 或 nerdctl CLI 作为一个客户端，通过 UNIX 套接字或 TCP 与一个长期在后台运行的、有状态的守护进程 (dockerd 或 Containerd) 进行 API 通信。这个守护进程是所有容器的中心管理者和父进程。 故障域 分布式。每个容器由其独立的 conmon 进程监视。一个容器或其监视器的失败不会影响其他任何容器。 中心化。守护进程是所有容器的单点故障源（SPOF）。如果守护进程崩溃或需要重启，默认情况下它会终止其管理的所有正在运行的容器。 系统集成 原生集成。由于其无守护进程的特性，Podman 可以被 systemd 像管理任何普通系统进程一样进行管理，实现了无缝集成。这催生了像 Quadlet 这样的声明式容器管理工具。 适配集成。dockerd 作为一个长期运行的服务，可以被 systemd 管理。但其管理的容器生命周期与 systemd 的服务模型是脱钩的，需要额外适配。 安全性 更优。消除了守护进程这一中心化的、通常以高权限运行的攻击面。其架构天然支持并鼓励无根（rootless）模式，极大地降低了容器逃逸的风险。 存在固有风险。守护进程通常以 root 权限运行，控制着系统上的所有容器，使其成为一个高价值的攻击目标。对 Docker Socket 的访问权几乎等同于系统的 root 权限。 技术堆栈与 OCI 运行时 # 虽然上层架构不同，但它们在最底层都汇聚到了 OCI 运行时规范上。\n高层运行时 (High-level Runtime): 负责镜像管理（拉取、存储、分发）、存储卷管理、网络配置等复杂的生命周期任务。\nDocker: dockerd 守护进程内部集成了这些功能，并委托 Containerd。 Containerd: Containerd 守护进程本身就是一个纯粹的高层运行时。 Podman: podman CLI 工具自身实现了这些高层管理功能。 低层运行时 (Low-level / OCI Runtime): 负责根据 OCI 规范，使用内核功能（Namespaces, Cgroups）来创建和运行一个隔离的容器进程。\nrunc: 由 Docker 开发并捐赠给 OCI 的参考实现，是 Containerd 和 Docker 的默认选择。 crun: 由 Red Hat 开发，用 C 语言编写，性能更高、内存占用更低，是 Podman 的默认选择。 关键洞察: 它们共享同一个行业标准（OCI），但实现了不同的上层管理逻辑。Podman 的架构更为直接 (Podman -\u0026gt; conmon -\u0026gt; crun)，而 Docker/Containerd 则是层层委托 (CLI -\u0026gt; Daemon -\u0026gt; OCI Runtime)。\n优劣势对比与专业场景选择 # 工具 核心优势 核心劣势 专业选择建议 Docker 无与伦比的生态系统：拥有最广泛的第三方工具、文档和社区支持。跨平台一致性：Docker Desktop 在 Windows/macOS 上提供了最无缝的开发体验。 守护进程架构的固有缺陷：安全风险、单点故障、与现代 Linux 系统管理（systemd）的集成不够原生。 场景：在需要与庞大、成熟的 Docker 生态工具链深度集成的团队；需要最高优先级的跨平台开发体验时；在遗留系统或教程丰富的入门学习阶段。 Podman 卓越的安全性与系统集成：无守护进程架构和原生无根模式是其最大亮点。与 systemd 的完美融合：通过 Quadlet 实现了声明式的“基础设施即代码”。轻量级：无后台服务，资源占用更低。 生态系统相对较新：虽然 CLI 兼容 Docker，但某些依赖 Docker Socket 的第三方工具可能需要适配。非 Linux 平台体验：在 Windows/macOS 上依赖于虚拟机，体验不如 Docker Desktop 丝滑。 场景：所有现代 Linux 服务器环境下的首选。构建安全、可预测、易于自动化和声明式管理的生产系统。在 CI/CD 流水线中寻求更轻量、更安全的构建环境。 Containerd 稳定、高效、遵循标准：被设计为云原生平台的基石，经过 Kubernetes 等大规模系统的严格验证。组件化：只做容器运行时这一件事，并做到极致。 非面向最终用户：它是一个底层组件，而非“一体化”工具。缺少用户友好的 CLI (nerdctl 正在弥补) 和开箱即用的网络、存储管理方案。 场景：作为容器编排平台（如 Kubernetes）的底层运行时。当你需要构建自己的容器化平台或 PaaS 时，Containerd 是理想的、可插拔的核心引擎。普通开发者和运维人员几乎不需要直接与其交互。 Tips # Podman 代表了容器技术的演进方向，尤其是在与现代 Linux 操作系统哲学的融合上。对于追求安全性、可预测性和声明式管理的专业人士来说，它是 Linux 环境下无可争议的未来。 Docker 凭借其先发优势和庞大生态，在可预见的未来仍将是重要的行业参与者，尤其是在跨平台开发和遗留系统中。 Containerd 则是这一切背后的无名英雄，是驱动整个云原生世界稳定运行的工业级标准组件。 作为一名专业技术人员，选择哪种工具取决于对特定场景下架构、安全性和运维模型的权衡，而非简单的功能列表对比。\n","date":"14 August 2025","externalUrl":null,"permalink":"/zh-cn/posts/container/oci_compare/","section":"Posts","summary":"","title":"容器运行时解析：Podman vs. Docker vs. Containerd","type":"posts"},{"content":" Podman 容器自动更新：原生集成 vs. Watchtower 对比 # 在生产和开发环境中，确保容器镜像是最新的，是维持系统安全与功能性的关键。本文将探讨 Podman 容器的两种主流自动更新方案——Watchtower 和 Podman 原生的 podman-auto-update 机制。\n方案对比：架构哲学与设计思想 # 选择哪种方案，本质上是在选择两种不同的管理哲学。\n对比维度 Podman 原生方案 (podman-auto-update) Watchtower (外部管理方案) 核心架构 声明式 \u0026amp; 系统集成。你只需声明“这个容器需要更新”，剩下的工作完全交由操作系统核心的 systemd 定时器来处理。 命令式 \u0026amp; 外部轮询。你运行一个独立的管理容器，它主动、持续地轮询 Podman API，检查并执行更新。 设计哲学 “让系统来管理我”。利用宿主机已有的、极其可靠的 init 系统，实现零额外开销的管理。 “我来管理你们”。引入一个外部的、有状态的管理者来监控和操作其他容器。 可靠性 极高。其可靠性等同于 systemd 本身，是 Linux 系统中最健壮的组件之一。 中等。依赖于 Watchtower 容器自身的稳定。它也是一个需要被管理、可能出现配置错误或崩溃的软件。 开销 几乎为零。平时无任何进程或内存占用，仅在触发检查的瞬间启动一个短暂进程。 持续性开销。需要一个长期运行的容器，持续占用少量 CPU 和内存。 安全 更优。无额外运行的守护进程，减少了攻击面。与 Podman 的无根（rootless）理念完美契合。 引入额外风险。需要将 Podman Socket 挂载到容器内，这本身就是一个需要谨慎处理的授权操作。 功能 专注于核心的更新功能。功能纯粹，无内置通知等附加功能。 功能丰富。支持发送更新通知（Webhook, Email）、更精细的过滤和控制等。 Tips：Podman 原生方案体现了与操作系统深度集成的现代 DevOps 思想，它更安全、更可靠、资源效率更高。Watchtower 作为一个通用解决方案，虽然功能丰富，但在 Podman 生态中引入了不必要的复杂性和管理开销。对于追求健壮性和简洁性的专业用户，原生方案是明确的首选。\n原生方案：podman-auto-update # 该方法同时适用于无根（Rootless）和 Root 模式。\n第 1 步：为容器声明“自动更新”意图 # 通过 Podman 的 Quadlet 工具，我们只需在容器的 .container 配置文件中添加一行即可。\n定位或创建您的 Quadlet 文件。\n无根模式 (推荐): 文件位于 ~/.config/containers/systemd/ Root 模式: 文件位于 /etc/containers/systemd/ 在 [Container] 部分添加 AutoUpdate=image。\n这是一个典型的 Caddy 容器配置文件 caddy.container 示例：\n1[Unit] 2Description=Caddy web server 3After=network-online.target 4Wants=network-online.target 5 6[Container] 7ContainerName=caddy 8Image=docker.io/library/caddy:latest 9# 核心：声明此容器需要被自动更新 10AutoUpdate=image 11Port=80:80 12Port=443:443 13 14[Install] 15WantedBy=default.target 应用更改。 修改或创建文件后，重载 systemd 并重启您的容器服务来应用新的标签。\n无根模式: systemctl --user daemon-reload \u0026amp;\u0026amp; systemctl --user restart caddy.service Root 模式: sudo systemctl daemon-reload \u0026amp;\u0026amp; sudo systemctl restart caddy.service 第 2 步：激活全局更新定时器 # Podman 自带一个 systemd 定时器，我们只需激活它。\n无根模式: systemctl --user enable --now podman-auto-update.timer\nRoot 模式: sudo systemctl enable --now podman-auto-update.timer\nenable 确保开机自启，--now 则立即启动定时器。至此，您的自动更新已配置完毕，将按默认计划执行。\n第 3 步：(可选) 自定义更新频率 # 默认的更新频率是每天凌晨。要安全地修改它，推荐使用 systemd 的覆盖文件（override）机制。\n打开编辑界面。此命令会自动创建覆盖文件供您编辑。\n无根模式: systemctl --user edit podman-auto-update.timer Root 模式: sudo systemctl edit podman-auto-update.timer 在编辑器中输入您的新计划。 OnCalendar= 字段遵循 systemd.time 的日历事件格式。\n1# 2# /etc/systemd/system/podman-auto-update.timer.d/override.conf 3# 4[Timer] 5# 清空从原文件继承的计划，以确保我们的设置是唯一的 6OnCalendar= 7# 设置新的计划，例如：每天凌晨 3:30 8OnCalendar=*-*-* 03:30:00 一些 OnCalendar 常用示例:\n每小时执行一次: hourly 每周一凌晨4点: Mon *-*-* 04:00:00 每15分钟一次: *:0/15 应用并验证您的更改。\n无根模式: systemctl --user daemon-reload \u0026amp;\u0026amp; systemctl --user restart podman-auto-update.timer Root 模式: sudo systemctl daemon-reload \u0026amp;\u0026amp; sudo systemctl restart podman-auto-update.timer 您可以通过 systemctl --user list-timers (或 sudo systemctl list-timers) 查看新的触发时间，确认配置已生效。\n","date":"14 August 2025","externalUrl":null,"permalink":"/zh-cn/posts/container/podman_autoupdate/","section":"Posts","summary":"","title":"Podman 容器自动更新：原生集成 vs. Watchtower 对比","type":"posts"},{"content":"摘要: 本文记录了在将服务从Docker迁移至Podman（无根模式）时，遇到的一个典型网络问题：容器无法通过宿主机IP访问宿主机服务。文章通过分析Podman的网络模型，并结合一个复杂的SSH代理场景，最终定位并解决了问题。\n1. 问题陈述 # 在迁移过程中，一个依赖于\u0026quot;容器访问宿主机\u0026quot;模式的服务出现故障。该服务在Docker环境中，可以直接使用宿主机的局域网IP或公网IP进行通信，但在Podman无根容器中则连接超时。\n2. 原理分析：Docker vs. Podman无根网络模型 # 初步分析表明，问题源于两者网络模型的根本差异。\nDocker (Rootful): Docker守护进程以root权限运行，会创建内核级虚拟网桥（如 docker0），并主动修改宿主机的iptables/nftables规则，以支持\u0026quot;发夹弯NAT\u0026quot;（Hairpin NAT）。这使得容器发往宿主机真实IP的流量能被正确路由回环。 Podman (Rootless): 出于安全考虑，Podman在无根模式下使用用户空间网络堆栈（如Netavark），不具备修改系统级iptables/nftables的权限。因此，当容器尝试访问宿主机的真实IP时，产生的Hairpin NAT流量会被内核的默认策略丢弃，导致连接失败。 3. 解决方案：正确的内部通信机制 # 既然外部回环路径不通，就需要采用Podman提供的内部通信机制。\n3.1. 抽象层：host.containers.internal # Podman提供了一个特殊的DNS名称 host.containers.internal，它在容器内部会被自动解析为宿主机在当前容器网络中的地址。这是访问宿主机的首选方式，因为它解耦了对具体IP的依赖。\n在容器内测试连通性：\n1$ podman exec -it my_container /bin/sh 2ping host.containers.internal 3PING host.containers.internal (169.254.1.2): 56 data bytes 464 bytes from 169.254.1.2: seq=0 ttl=42 time=0.285 ms 5... 测试结果表明，容器到宿主机的L3层网络是通畅的。\n3.2. 防火墙策略 # 流量需要通过宿主机的防火墙。规则的核心是允许来自Podman子网的入站流量。\n获取Podman网络子网: 1 $ podman network inspect podman | grep subnet 2 \u0026#34;subnet\u0026#34;: \u0026#34;10.89.0.0/24\u0026#34;, 3 向nftables添加规则 (以目标端口TARGET_PORT/tcp为例): 1# 向默认的inet filter表的input链添加规则 2sudo nft add rule inet filter input ip saddr 10.89.0.0/24 tcp dport TARGET_PORT accept 3 持久化nftables规则: nft命令添加的规则在重启后会失效。为使其永久生效，需将当前规则集保存到配置文件并启用服务。 1# 将当前规则集写入配置文件 2sudo nft list ruleset \u0026gt; /etc/nftables.conf 3 4# 确保nftables服务开机自启以加载规则 5sudo systemctl enable nftables.service 6 4. 实战复盘：通过sing-box代理SSH访问宿主机 # 理论验证后，我将其应用到一个复杂的实际场景中进行测试。\n场景: 远程SSH客户端(WindTerm) -\u0026gt; 本地sing-box客户端 -\u0026gt; 服务器sing-box容器 -\u0026gt; 宿主机SSH服务(监听于TARGET_PORT)。 客户端配置: WindTerm的代理设置为本地sing-box（监听于LOCAL_PROXY_PORT），并启用了“远程DNS解析”。SSH主机名设置为host.containers.internal。 尽管如此，连接依然失败。这表明问题不在于基础网络，而在于代理链的逻辑。\n4.1. 服务器端验证 # 为排除服务器端问题，我直接在sing-box容器内发起了SSH连接。\n1# 在sing-box容器内安装并执行ssh 2$ podman exec -it sing-box /bin/sh 3/ # apk add openssh-client 4/ # ssh my_username@host.containers.internal -p TARGET_PORT 5The authenticity of host ... can\u0026#39;t be established. 6... 7Are you sure you want to continue connecting (yes/no)? yes 8# 成功收到密码提示 9my_username@host.containers.internal\u0026#39;s password: 10 结论: 服务器端配置完全正确。 ssh连接成功证明从容器到宿主机的网络路径、防火墙、SSH服务本身均无问题。\n4.2. 客户端日志分析与最终定位 # 既然服务器端无误，问题必然出在客户端的代理链上。我检查本地sing-box客户端的日志，发现了以下关键条目：\n1error [timestamp] connection: open outbound connection: NXDOMAIN 2info [timestamp] outbound/direct[]: outbound connection to host.containers.internal:TARGET_PORT 3 日志清晰地指出了问题：\n客户端收到了发往host.containers.internal的请求。 但其路由规则错误地将此请求匹配到了direct（直连）出站。 客户端尝试在本地解析host.containers.internal，导致NXDOMAIN（域名不存在）错误。 4.3. 解决方案 # 修正**客户端sing-box**的路由配置，添加一条高优先级的规则，将host.containers.internal的流量强制导向服务器代理出站（此处tag为proxy-out）。\n1 { 2 \u0026#34;routing\u0026#34;: { 3 \u0026#34;rules\u0026#34;: [ 4 { 5 \u0026#34;domain\u0026#34;: [\u0026#34;host.containers.internal\u0026#34;], 6 \u0026#34;outbound\u0026#34;: \u0026#34;proxy-out\u0026#34; 7 }, 8 // ... 其他规则 9 ] 10 } 11} 应用此规则后，重启客户端，SSH连接成功。\n5. 总结 # Podman无根网络的隔离性是其安全性的基础，但也带来了与Docker不同的网络行为，特别是对于Hairpin NAT场景。 host.containers.internal**是Podman容器访问宿主机的标准抽象层，应优先使用。 必须为Podman子网配置相应的防火墙入站规则。 在复杂的代理或网络链中，端到端的日志分析和在关键节点进行原生协议测试，是最高效、最可靠的故障排查方法。 ","date":"12 August 2025","externalUrl":null,"permalink":"/zh-cn/posts/container/podman_network/","section":"Posts","summary":"","title":"Podman容器访问宿主机网络排错","type":"posts"},{"content":"","date":"6 August 2025","externalUrl":null,"permalink":"/zh-cn/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"","date":"6 August 2025","externalUrl":null,"permalink":"/zh-cn/categories/python/","section":"Categories","summary":"","title":"Python","type":"categories"},{"content":" 🧠 项目目标 # 使用 transformers 库加载 Hugging Face remark模型，通过 FastAPI 暴露 REST API 接口。\n🧪 代码示例 # 1from fastapi import FastAPI, HTTPException 2from pydantic import BaseModel 3from transformers import AutoTokenizer, AutoModel 4import torch 5from torch.nn import functional as F 6 7# 加载模型和分词器 8tokenizer = AutoTokenizer.from_pretrained(\u0026#34;BAAI/bge-reranker-large\u0026#34;) 9model = AutoModel.from_pretrained(\u0026#34;BAAI/bge-reranker-large\u0026#34;) 10 11# 切换到 GPU（可选） 12if torch.cuda.is_available(): 13 model = model.to(\u0026#34;cuda\u0026#34;) 14 15app = FastAPI(title=\u0026#34;BGE Reranker API\u0026#34;, version=\u0026#34;1.0\u0026#34;) 16 17# 定义请求体结构 18class RerankRequest(BaseModel): 19 query: str 20 documents: list[str] 21 22@app.post(\u0026#34;/rerank\u0026#34;) 23async def rerank(request: RerankRequest): 24 try: 25 # 为查询和每个文档分别编码 26 query_inputs = tokenizer([request.query], padding=True, truncation=True, return_tensors=\u0026#34;pt\u0026#34;) 27 doc_inputs = tokenizer(request.documents, padding=True, truncation=True, return_tensors=\u0026#34;pt\u0026#34;) 28 29 if torch.cuda.is_available(): 30 query_inputs = {k: v.to(\u0026#34;cuda\u0026#34;) for k, v in query_inputs.items()} 31 doc_inputs = {k: v.to(\u0026#34;cuda\u0026#34;) for k, v in doc_inputs.items()} 32 33 with torch.no_grad(): 34 query_outputs = model(**query_inputs, return_dict=True) 35 doc_outputs = model(**doc_inputs, return_dict=True) 36 37 query_embedding = query_outputs.pooler_output 38 document_embeddings = doc_outputs.pooler_output 39 40 # 计算查询嵌入和每个文档嵌入的余弦相似度 41 scores = F.cosine_similarity(query_embedding, document_embeddings, dim=1).tolist() 42 43 ranked_docs = sorted( 44 zip(request.documents, scores), 45 key=lambda x: x[1], 46 reverse=True 47 ) 48 49 return {\u0026#34;results\u0026#34;: [{\u0026#34;document\u0026#34;: doc, \u0026#34;score\u0026#34;: score} for doc, score in ranked_docs]} 50 except Exception as e: 51 raise HTTPException(status_code=500, detail=str(e)) 52 53if __name__ == \u0026#34;__main__\u0026#34;: 54 import uvicorn 55 uvicorn.run(app, host=\u0026#34;0.0.0.0\u0026#34;, port=58222) ","date":"6 August 2025","externalUrl":null,"permalink":"/zh-cn/posts/python/hgface/","section":"Posts","summary":"","title":"Python：Hugging Face 模型 API 实现","type":"posts"},{"content":"","date":"5 August 2025","externalUrl":null,"permalink":"/zh-cn/categories/go/","section":"Categories","summary":"","title":"Go","type":"categories"},{"content":" 🌐 项目目标 # 使用 Gin 框架搭建一个简单web服务.\n🛠️ 项目结构 # 1todo-api/ 2├── main.go # 入口文件 3├── routes/ 4│ └── todo_routes.go # 路由定义 5├── models/ 6│ └── todo.go # 数据结构 7├── middleware/ 8│ └── logging.go # 自定义中间件 9├── config/ 10│ └── config.go # 配置管理 11└── go.mod # Go 模块 📦 安装依赖 # 1go mod init todo-api 2go get -u github.com/gin-gonic/gin 3go get -u github.com/jackc/pgx/v4 🧪 代码示例 # 1. 配置管理（config/config.go） # 1package configimport \u0026#34;github.com/joho/godotenv\u0026#34;func LoadEnv() { 2err := godotenv.Load() 3if err != nil { 4panic(\u0026#34;Error loading .env file\u0026#34;) 5 } 6} 2. 数据库连接（main.go） # 1package mainimport ( 2\u0026#34;context\u0026#34; 3\u0026#34;fmt\u0026#34; 4\u0026#34;log\u0026#34; 5\u0026#34;github.com/gin-gonic/gin\u0026#34; 6\u0026#34;github.com/jackc/pgx/v4\u0026#34; 7\u0026#34;todo-api/config\u0026#34; 8\u0026#34;todo-api/routes\u0026#34; 9)type Todo struct { 10ID int `json:\u0026#34;id\u0026#34;` 11Title string `json:\u0026#34;title\u0026#34;` 12}func main() { 13// 1. 加载环境变量 14config.LoadEnv()// 2. 连接 PostgreSQL 15connStr := fmt.Sprintf( 16\u0026#34;postgres://%s:%s@%s:%s/%s?sslmode=disable\u0026#34;, 17os.Getenv(\u0026#34;DB_USER\u0026#34;), 18os.Getenv(\u0026#34;DB_PASSWORD\u0026#34;), 19os.Getenv(\u0026#34;DB_HOST\u0026#34;), 20os.Getenv(\u0026#34;DB_PORT\u0026#34;), 21os.Getenv(\u0026#34;DB_NAME\u0026#34;), 22)conn, err := pgx.Connect(context.Background(), connStr) 23if err != nil { 24log.Fatal(\u0026#34;无法连接数据库:\u0026#34;, err) 25} 26defer conn.Close(context.Background())// 3. 创建 Gin 应用 27r := gin.Default()// 4. 注册路由 28todoRoutes := routes.TodoRoutes{DB: conn} 29r.POST(\u0026#34;/todos\u0026#34;, todoRoutes.CreateTodo) 30r.GET(\u0026#34;/todos\u0026#34;, todoRoutes.GetAllTodos)// 5. 启动服务 31r.Run(\u0026#34;:8080\u0026#34;) 32} 3. 路由实现（routes/todo_routes.go） # 1package routesimport ( 2\u0026#34;github.com/gin-gonic/gin\u0026#34; 3\u0026#34;todo-api/models\u0026#34; 4)type TodoRoutes struct { 5DB *pgx.Conn 6}func (tr *TodoRoutes) CreateTodo(c *gin.Context) { 7var todo models.Todo 8if err := c.ShouldBindJSON(\u0026amp;todo); err != nil { 9c.JSON(400, gin.H{\u0026#34;error\u0026#34;: err.Error()}) 10return 11}_, err := tr.DB.Exec(context.Background(), \u0026#34;INSERT INTO todos (title) VALUES ($1)\u0026#34;, todo.Title) 12if err != nil { 13 c.JSON(500, gin.H{\u0026#34;error\u0026#34;: \u0026#34;数据库操作失败\u0026#34;}) 14 return 15} 16 17c.JSON(201, gin.H{\u0026#34;message\u0026#34;: \u0026#34;成功创建任务\u0026#34;}) 18_, err := tr.DB.Exec(context.Background(), \u0026#34;INSERT INTO todos (title) VALUES ($1)\u0026#34;, todo.Title) 19if err != nil { 20 c.JSON(500, gin.H{\u0026#34;error\u0026#34;: \u0026#34;数据库操作失败\u0026#34;}) 21 return 22} 23 24c.JSON(201, gin.H{\u0026#34;message\u0026#34;: \u0026#34;成功创建任务\u0026#34;}) 25}func (tr *TodoRoutes) GetAllTodos(c *gin.Context) { 26rows, err := tr.DB.Query(context.Background(), \u0026#34;SELECT id, title FROM todos\u0026#34;) 27if err != nil { 28c.JSON(500, gin.H{\u0026#34;error\u0026#34;: \u0026#34;查询失败\u0026#34;}) 29return 30}var todos []models.Todo 31for rows.Next() { 32 var t models.Todo 33 if err := rows.Scan(\u0026amp;t.ID, \u0026amp;t.Title); err != nil { 34 c.JSON(500, gin.H{\u0026#34;error\u0026#34;: \u0026#34;解析结果失败\u0026#34;}) 35 return 36 } 37 todos = append(todos, t) 38} 39 40c.JSON(200, todos) 41var todos []models.Todo 42for rows.Next() { 43 var t models.Todo 44 if err := rows.Scan(\u0026amp;t.ID, \u0026amp;t.Title); err != nil { 45 c.JSON(500, gin.H{\u0026#34;error\u0026#34;: \u0026#34;解析结果失败\u0026#34;}) 46 return 47 } 48 todos = append(todos, t) 49} 50 51c.JSON(200, todos) 52} ","date":"5 August 2025","externalUrl":null,"permalink":"/zh-cn/posts/go/simpleweb/","section":"Posts","summary":"","title":"Go：Web 简单后端搭建步骤","type":"posts"},{"content":"","date":"2 August 2025","externalUrl":null,"permalink":"/zh-cn/tags/k8s/","section":"Tags","summary":"","title":"K8s","type":"tags"},{"content":" 检查残留: # 1kubectl get all -n efk -l app=kibana 2kubectl get configmaps,secrets -n efk | grep kibana 3kubectl get pvc -n efk | grep kibana 清理 # 1kubectl delete configmap kibana-kibana-helm-scripts -n efk --force --grace-period=0 \u0026amp;\u0026amp; \\ 2kubectl delete serviceaccount pre-install-kibana-kibana -n efk --force --grace-period=0 \u0026amp;\u0026amp; \\ 3kubectl delete role pre-install-kibana-kibana -n efk --force --grace-period=0 \u0026amp;\u0026amp; \\ 4kubectl delete rolebinding pre-install-kibana-kibana -n efk --force --grace-period=0 \u0026amp;\u0026amp; \\ 5kubectl delete job pre-install-kibana-kibana -n efk --force --grace-period=0 \u0026amp;\u0026amp; \\ 6kubectl delete secret sh.helm.release.v1.kibana.v1 -n efk --force --grace-period=0 \u0026amp;\u0026amp; \\ 7kubectl delete secret kibana-kibana-es-token -n efk ","date":"2 August 2025","externalUrl":null,"permalink":"/zh-cn/posts/devops/kubectl/","section":"Posts","summary":"","title":"kubectl清理资源命令","type":"posts"},{"content":"你好，我是 Yuzjing 👋\n很高兴你能逛到我的个人角落。我是一名DevOps工程师，总是对新技术充满好奇，因为我相信它们是驱动变革的引擎，能用更优雅的方式解决老问题，甚至创造出全新的可能性, 减少生命的浪费。\n这个网站主要是为了记录那些学习过程中的灵感、踩过的坑，以及一些碎碎念。\n在技术之外，我也在探索\u0026hellip;\n🌲 走进自然：周末喜欢去附近的山里徒步，呼吸新鲜空气能让大脑清空重启。\n🎵 多元的音乐世界：我的播放列表是个大熔炉，从流行的旋律（Pop），到 House 的节奏，再到 Hardcore 的冲击力，都能给我带来能量。音乐是构建情绪和专注力的绝佳背景。\n🚀 探索未知的故事：无论是科幻小说里的遥远星系，还是策略游戏中的复杂战局，我着迷于探索那些由想象力构建的宏大世界。\n我的小项目 (My Project) # 这里是我最的一个小作品：\nGithub Action IP 防火墙自动更新器 # 问题：每次通过 GitHub Actions 更新使用白名单模式(Nftables)的服务器时，都会因为 IP 地址是动态变化有失败风险。 我的解决方案：于是我用 Go 写了这个小工具，它能自动获取 GitHub Actions 使用的最新 IP 地址，并更新到防火墙规则里，结合Crontab 定时任务实现真正的“一次配置，永远运行”。 项目地址：你可以在 GitHub 上找到它的源码。 联系我 # 如果你对我的项目感兴趣，或者想和我聊聊新的技术、推荐一首好歌，都欢迎在 GitHub 上与我交流。\n","date":"1 August 2025","externalUrl":null,"permalink":"/zh-cn/about/","section":"涧雨暖云","summary":"","title":"关于我","type":"page"},{"content":"摘要: 本文记录了在Podman无根（Rootless）模式下实现容器服务持久化的探索过程。文章从一个常见的手动创建systemd服务失败的案例出发，分析其根本原因，并最终过渡到使用Podman官方推荐的Quadlet工具。通过对Quadlet工作机制的深入分析，本文阐述了其声明式的服务管理方案，并给出了最佳实践。\n1. 目标与初始尝试的失败 # 从Docker迁移至Podman后，核心目标之一是实现容器化服务的持久化与开机自启。\n最初，我尝试为podman-compose项目编写一个传统的systemd用户服务，但此方案因systemd对后台进程（podman-compose up -d）的生命周期管理与预期不符而失败，导致服务陷入“启动-停止”的无限循环。这促使我转向Podman官方推荐的Quadlet方案。\n2. Quadlet方案的初步探索与新的困惑 # Quadlet的核心思想是用户仅需编写一份简单的.container文件，由systemd的生成器（Generator）在后台自动“翻译”为复杂的.service文件。\n我根据compose.yml为sing-box容器创建了对应的Quadlet文件~/.config/containers/systemd/sing-box.container，并确保包含了[Install]区段以定义自启动行为。\n然而，在执行systemctl --user daemon-reload之后，尝试使用systemctl --user enable sing-box.service命令时，却反复遇到Failed to enable unit: Unit ... is transient or generated的错误。有趣的是，systemctl --user start sing-box.service却能成功启动容器。\n这一现象表明，Quadlet文件本身是有效的，但其与systemd的enable机制之间存在一种超出常规理解的交互。\n3. 根源剖析：Quadlet生成器与[Install]的内在机制 # 通过查阅资料和反复试验，问题的根源得以明朗：Quadlet的自启动能力，并非由systemctl enable命令赋予，而是由其生成器在读取.container文件中的[Install]部分时，已经【自动完成】了。\nsystemd的工作流程如下：\n编写: 用户在指定目录¹下创建包含[Install]部分的.container文件。这即是向systemd下达的“自启动指令”。\ndaemon-reload触发: 执行systemctl daemon-reload²时，quadlet-generator被激活。它扫描目录，找到.container文件，并执行两个关键动作：\n生成临时服务文件: 在一个临时的运行时目录（如/run/systemd/generator/）下，创建一个不包含[Install]部分的.service文件，用于实际的start和stop操作。 自动创建符号链接: 它会读取“蓝图”中的[Install]部分，并直接代为执行enable的核心工作——根据WantedBy的配置，在相应的.wants/目录下，创建指向那个临时.service文件的符号链接。 enable命令的问题: 在此之后，当用户再手动执行systemctl enable时，systemd发现该服务已被一个生成器“安装”了，并且指向一个临时文件。根据其设计原则，用户不应直接enable一个由生成器管理的临时单元，因此它会返回Unit is transient or generated的错误。这个错误，实际上是一个提示：“这件事我已经替你做好了。”\n¹ Quadlet文件路径: 对于用户服务（Rootless），路径是 ~/.config/containers/systemd/。对于系统服务（Rootful），路径是 /etc/containers/systemd/。 ² 命令注意: 对于用户服务，命令是 systemctl --user daemon-reload。对于系统服务，则是 sudo systemctl daemon-reload。\n4. [Install]配置与最终工作流 # 4.1. [Install]区段的正确配置 # WantedBy=的值取决于您运行Podman的模式：\n无根模式 (Rootless): 服务属于用户会话，应随用户会话启动（或在linger启用后随系统启动）。\n1[Install] 2WantedBy=default.target Root模式 (Rootful): 服务属于系统，应随系统进入多用户状态时启动。\n1[Install] 2WantedBy=multi-user.target 4.2. 最终的、最简化的工作流 # 编写或修改Quadlet源文件: 确保文件语法正确，并且包含适合您运行模式的[Install]区段。\n应用变更: 每当修改完.container文件后，执行以下命令来通知systemd并触发生成器的工作。这是唯一必要的管理命令。\n1# 对于用户服务 2systemctl --user daemon-reload 3# 对于系统服务 4sudo systemctl daemon-reload 启动与验证: daemon-reload执行完毕后，服务就已经处于“已安装”和“已启用”的状态。\n1# 启动服务 (根据模式选择是否加sudo和--user) 2systemctl --user start your-service.service 3 4# 验证其是否已启用 5systemctl --user is-enabled your-service.service 6# 预期输出: generated 5. 总结 # 这次排错过程，从一个看似简单的服务持久化需求，深入到了systemd与Quadlet生成器之间复杂的交互机制。它揭示了声明式工具背后强大的自动化能力，也澄清了传统systemctl命令在与生成器交互时的角色变化。\n最终结论是，Quadlet通过读取.container文件中的[Install]配置，并在daemon-reload时自动完成服务的“安装”（即enable的核心工作），从而实现了高度自动化和声明式的持久化管理。理解rootful与rootless模式下[Install]配置的差异，并遵循“修改源文件 -\u0026gt; daemon-reload -\u0026gt; start”的工作流，是精通Podman容器部署的关键。\n","date":"31 July 2025","externalUrl":null,"permalink":"/zh-cn/posts/container/quadlets/","section":"Posts","summary":"","title":"Podman无根容器持久化探索: Quadlets","type":"posts"},{"content":"","date":"30 July 2025","externalUrl":null,"permalink":"/zh-cn/categories/k8s/","section":"Categories","summary":"","title":"K8s","type":"categories"},{"content":" 1. 通用部署注意事项 # 版本兼容性：K8s 各项组件都存在版本兼容性问题, 因此在部署时需要注意版本的选择, 尤其是 containerd, kubernetes-dashboard, etcd 等组件。 Calico 网络插件：部署时可能需翻墙。若使用国内源，需修改 containerd 的 config.toml 文件。如还无法部署成功, 可尝试手动拉取镜像再部署。 Helm 国内源：Helm 本质是 Charts, 实际的拉取镜像还是在集群上进行, 所以还是要保证集群的 containerd 源可用。 本地管理工具：在个人电脑上安装 helm 和 kubectl, 并配置好环境变量后, 可以直接使用这两个命令来管理远程的 K8s 集群。 2. Dashboard 没数据问题 # 安装好 Dashboard 后发现没有数据，可能是以下原因：\n命名空间选择错误：请检查是否选择了正确的命名空间，默认的 default 命名空间是没有监控数据的。 Metrics Server 未安装：需要安装 metrics-server 组件来采集指标。 1kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml Kubelet 证书问题：如果 Kubelet 的证书不是由 Metrics Server 信任的 CA 签发的，TLS 握手会失败，导致无法获取指标。此时需要在 metrics-server 的部署配置中，为其 args 添加以下参数，以信任不安全的 Kubelet 证书： 1- --kubelet-insecure-tls=true 3. K8s 集群高可用流程 # 检测故障：Kubernetes (通过 Node Controller 或 Liveness Probe) 检测到 Pod 或其所在 Node 出现问题。 触发重建：副本控制器 (Deployment/StatefulSet) 发现运行的 Pod 数量不足。 调度新 Pod：Kubernetes 调度器在另一个健康的 Node 上选择一个位置来创建新的 Pod。 使用持久化配置 (如果启用)： 新的 Pod 会根据其定义请求之前使用的同一个 PersistentVolumeClaim (PVC)。 如果是网络存储 (如 NFS)，新 Pod 直接挂载访问。 如果是块存储 (如 EBS)，Kubernetes 和 CSI 驱动会确保存储卷从故障 Node 安全分离，然后附加到新 Node，再由新 Pod 挂载。 服务发现：Service 会将流量自动导向这个新的、健康的 Pod 实例。 4. 如何查看期望副本数 # 对于 Deployment: 1kubectl get deployment \u0026lt;deployment-name\u0026gt; -n \u0026lt;namespace\u0026gt; -o wide 观察 DESIRED 列即可。\n对于 StatefulSet:\rcode Shell\rIGNORE_WHEN_COPYING_START IGNORE_WHEN_COPYING_END\nkubectl get statefulset -n -o wide\n对于 ReplicaSet: code Shell\nIGNORE_WHEN_COPYING_START\rIGNORE_WHEN_COPYING_END\rkubectl get replicaset \u0026lt;replicaset-name\u0026gt; -n \u0026lt;namespace\u0026gt; -o wide\r5. 理解 Pod 的 READY 列和副本数 # kubectl get pods 中 READY 列的分母（例如 1/1 中的第二个 1）是单个 Pod 内部定义的容器数量。\n这与控制器的期望副本数是两个概念。期望副本数指的是控制器应该运行多少个这样的 Pod 实例。\n\u0026quot;Ready的容器数可以理解为编排\u0026quot;：是的，Pod 内可以有多个容器，它们被 Kubernetes \u0026quot;编排\u0026quot;在一起，作为一个整体来调度和管理。\r控制器的期望副本数需要通过 kubectl get deployment/statefulset 来查看。\r6. 关于持久化存储 # 如果你的 Pod 没有配置持久化存储（即它使用的是 Pod 的临时存储，如 emptyDir，或者是容器文件系统本身的临时层），那么：当 Pod 因任何原因被删除并重新创建时，Pod 内所有容器写入到其临时存储中的数据都会丢失。\n新的 Pod 实例会以一个全新的、干净的状态启动。\n7. 理解 Service 中的端口 # 一个请求的流向通常是这样的： 外部请求 -\u0026gt; Node(NodeIP:nodePort) -\u0026gt; Service(ClusterIP:port) -\u0026gt; Pod(PodIP:targetPort)\ntargetPort (目标端口)\r定义：Service 将流量最终转发到后端 Pod 容器监听的端口。\r作用域：Pod 内部。\r值：可以是数字（8080）或在 Pod 中定义的端口名称（http-api）。\rnodePort (节点端口)\r定义：当 Service 类型为 NodePort 或 LoadBalancer 时，在每个 Node 的 IP 上暴露的静态端口。\r作用域：集群 Node 外部。\r值：默认范围是 30000-32767。允许从集群外部通过 http://\u0026lt;Node-IP\u0026gt;:\u0026lt;nodePort\u0026gt; 访问。\rport (服务端口)\r定义：Service 自身在其内部 ClusterIP 地址上暴露的端口。\r作用域：集群内部。\r作用：集群内部的其他 Pod 可以通过 http://\u0026lt;ServiceName\u0026gt;.\u0026lt;Namespace\u0026gt;.svc.cluster.local:\u0026lt;port\u0026gt; 访问此服务。\r8. 部署 Fluentd 遇到的坑 # 部署 bitnami/fluentd 时遇到了以下问题 (成功的 values 见 Fluentd.yaml 文件)：\n缺少 CRI 插件：官方 chart 缺少 forword 的 CRI 插件, 默认是 drop all, 导致容器日志无法被正确处理和解析。\r缺少 Elasticsearch 插件：其 Aggregator 同样没有 Elasticsearch 的插件, 导致无法将日志发送到 ES。\r协议错误：连接 ES 时, 默认的 scheme 是 http, 但 ES 的 9200 端口通常是 https, 需要修改。\r认证失败：连接 ES 时需要指定用户名和密码（通常是 Kibana 的登录凭据），否则会报 401 错误。\r命名空间标签问题：发现 efk 命名空间的日志被 ES 全部拒绝。AI 提示可能是标签中包含 . 和 / 影响。检查发现该命名空间的 labels 多了一行 name: efk。\rcode Yaml\rIGNORE_WHEN_COPYING_START\rIGNORE_WHEN_COPYING_END\r# 错误的命名空间标签\r❯ kubectl get ns efk -o yaml\rapiVersion: v1\rkind: Namespace\rmetadata:\rcreationTimestamp: \u0026quot;2025-06-06T06:56:03Z\u0026quot;\rlabels:\rkubernetes.io/metadata.name: efk\rname: efk # \u0026lt;-- 这行是多余的\r...\r# 正常的命名空间标签\r❯ kubectl get ns monitoring -o yaml\rapiVersion: v1\rkind: Namespace\rmetadata:\rcreationTimestamp: \u0026quot;2025-05-22T05:45:59Z\u0026quot;\rlabels:\rkubernetes.io/metadata.name: monitoring\rname: monitoring\r...\r神奇的是，尝试删除多余标签并用 Ruby 脚本替换特殊字符都无效。但第二天该问题自动解决，看来确实是 name 标签的问题。\r9. Ingress 和 MetalLB：解决外部访问问题 # Ingress 工作在7层（应用层），可以实现内部反向代理和负载均衡。外部通过访问 Ingress Controller 的 nodePort 来访问服务。\r缺点：nodePort 必须使用高位端口（如 30000+），外部访问不方便且不优雅。\r解决方案：安装 MetalLB 插件。\r作用：当 Service 类型为 LoadBalancer 时，MetalLB 会自动从预设的外部地址池中分配一个 IP 给这个 Service，作为其 EXTERNAL-IP。\r优点：\r解决了访问需要加高位端口的问题，可以直接用 IP 访问。\r提供了高可用方案，避免了使用 iptables 转发流量时可能出现的单点故障。 ","date":"30 July 2025","externalUrl":null,"permalink":"/zh-cn/posts/devops/k8s_deployproblems/","section":"Posts","summary":"","title":"K8s 集群部署踩过的坑","type":"posts"},{"content":"背景: 在部署一个服务时, 发现容器的网络行为总是有问题, 由此查阅资料才发现时本地交付机制搞的鬼, 于是做个笔记.\n当一个数据包的目标 IP 是本机的一个 IP 地址时（不管是不是接收接口的地址），操作系统会将该数据包直接交给本地的应用程序处理，而不是转发出去或丢弃。这种行为称为：\nLocal delivery（本地交付） 或者说是：IP 属于本机地址（local delivery decision） 这在 TCP/IP 协议栈中是一个标准的行为，不管是 Windows 还是 Linux 都支持。\n🔍 举个例子说明 # 假设你有一台 Linux 主机，它拥有两个网卡：\neth0: 192.168.1.4/24 eth1: 192.168.5.4/24 你在该主机上运行了一个 Web 服务，监听在 0.0.0.0:80。\n现在，从另一台机器 192.168.5.30 发送一个 HTTP 请求到 192.168.1.4:80。但由于路由错误或不正确的 ARP 响应等原因，这个数据包实际上被发送到了主机的 eth1 接口（192.168.5.4）上。\n结果是：Linux 仍然可以正常处理这个请求。因为它检查数据包的目标 IP 192.168.1.4，发现这个 IP 属于本机，于是就会将数据包直接交付给上层正在监听的 Web 服务。\n这就是典型的“本地交付”。\n⚙️ Linux 和 Windows 的区别在哪里？ # 功能 Windows Linux 默认启用本地交付 ✅ 是 ✅ 是 默认启用 IP 转发 ❌ 否 ❌ 否（默认） 控制方式 图形界面 / 注册表 / PowerShell sysctl 参数（如 net.ipv4.ip_forward） 允许跨接口访问本机 IP ✅ 是 ✅ 是 所以，它们在“本地交付”这一点上核心行为是一致的。\n🛠️ Linux 中影响本地交付的一些内核参数 # 虽然本地交付是默认行为，但在某些特定场景下，你可以通过调整内核参数来改变它。\n1. rp_filter（反向路径过滤） # 如果启用了严格的 rp_filter，Linux 可能会丢弃那些“从错误接口进入”的数据包。这是一种防止源 IP 欺骗的安全机制。\n1net.ipv4.conf.all.rp_filter = 1 rp_filter 通常有几种模式：\nstrict (严格模式)：要求数据包的入站接口必须是系统路由表中通往其源地址的最佳路径。在我们的例子中，这可能会导致数据包被丢弃。\rloose (宽松模式)：只要求源地址在路由表中可达即可，不关心从哪个接口进来。\rdisabled (禁用)：完全禁用 rp_filter。\r你可以通过以下命令查看当前设置： code Shell IGNORE_WHEN_COPYING_START IGNORE_WHEN_COPYING_END\nsysctl net.ipv4.conf.all.rp_filter\naccept_local code Shell IGNORE_WHEN_COPYING_START IGNORE_WHEN_COPYING_END net.ipv4.conf.all.accept_local = 1\n这个参数明确允许接口接受那些目标 IP 是本机、但不属于该接口的 IP 地址的数据包。这个参数默认就是开启的，是实现本地交付的关键之一。\n","date":"30 July 2025","externalUrl":null,"permalink":"/zh-cn/posts/devops/ip_delivert/","section":"Posts","summary":"","title":"网络中的本地交付机制","type":"posts"},{"content":" 1. 简介 # 在 Kubernetes 中，dnsPolicy 是 Pod spec 中的一个关键字段，它精确地定义了 Pod 内部应如何进行 DNS 解析。当你 Pod 内的应用程序尝试解析一个域名时（无论是集群内部服务如 my-service，还是外部域名如 google.com），dnsPolicy 决定了系统应采用哪种解析流程。\n2. 四种 DNS 策略 # Kubernetes 提供了四种不同的 DNS 策略，以适应不同的应用场景。\n策略 (dnsPolicy) 核心行为 主要应用场景 ClusterFirst (默认策略) 集群优先：DNS 请求首先发送到集群内部的 DNS 服务（如 CoreDNS）。如果域名是集群服务，则直接解析；否则，请求被转发到节点的上游 DNS 服务器。 绝大多数标准应用。这是最常用、最推荐的配置，因为它无缝地同时支持集群内部和外部的域名解析。 Default 继承节点：Pod 完全忽略集群的 DNS 服务，直接继承其所在工作节点的 /etc/resolv.conf 文件配置。 1. 当 Pod 不需要访问集群内其他服务时。\n2. 解决与集群 DNS 的特殊兼容性问题。 None 无策略 / 完全自定义：Kubernetes 不为 Pod 应用任何 DNS 配置。你 必须 通过 dnsConfig 字段手动提供完整的 DNS 设置。 需要使用一个完全独立的、自定义的 DNS 解析方案，适用于高级网络配置。 ClusterFirstWithHostNet hostNetwork 版集群优先：专为设置了 hostNetwork: true 的 Pod 设计。其行为与 ClusterFirst 类似，但配置方式会适应宿主机网络。 当 Pod 需要直接使用宿主机的网络，但同时又希望它能解析集群内的服务时。 3. 如何配置 # dnsPolicy 和 dnsConfig 都在 Pod 的 YAML 文件的 spec 部分进行配置。\n示例 1：使用 ClusterFirst (默认) # 如果你不显式设置 dnsPolicy，它将自动默认为 ClusterFirst。\n1apiVersion: v1 2kind: Pod 3metadata: 4 name: my-pod-default 5spec: 6 containers: 7 - name: my-app 8 image: my-image 9 # dnsPolicy: ClusterFirst \u0026lt;-- 此行可省略，因为是默认值 示例 2：使用 Default 策略 # 此 Pod 将直接使用其所在节点的 DNS 配置来解析所有域名。\n1apiVersion: v1 2kind: Pod 3metadata: 4 name: my-pod-node-dns 5spec: 6 containers: 7 - name: my-app 8 image: my-image 9 # 在这里设置 DNS 策略 10 dnsPolicy: Default 示例 3：使用 None 和 dnsConfig 进行完全自定义 # 这个例子中，我们完全绕过 Kubernetes 的 DNS，直接将 Google DNS 服务器配置给 Pod。\n1apiVersion: v1 2kind: Pod 3metadata: 4 name: my-pod-custom-dns 5spec: 6 containers: 7 - name: my-app 8 image: my-image 9 10 # 1. 设置策略为 \u0026#34;None\u0026#34; 以启用完全自定义 11 dnsPolicy: \u0026#34;None\u0026#34; 12 13 # 2. 手动提供完整的 DNS 配置 14 dnsConfig: 15 # 指定 DNS 服务器的 IP 地址 16 nameservers: 17 - 8.8.8.8 18 - 8.8.4.4 19 # 指定 DNS 搜索域 (用于解析短域名) 20 searches: 21 - my-namespace.svc.cluster.local 22 - svc.cluster.local 23 - cluster.local 24 # DNS 解析器选项 25 options: 26 - name: ndots 27 value: \u0026#34;5\u0026#34; 4. dnsConfig 字段详解 # dnsConfig 字段允许对 DNS 进行更精细的控制，可以与 dnsPolicy 结合使用。\nnameservers: 一个 IP 地址列表，用作 Pod 的 DNS 服务器。最多可以指定 3 个。 searches: 一个 DNS 搜索域列表。当解析一个不包含点 (.) 的短域名（如 my-service）时，系统会按顺序在这些搜索域下查找。例如，它会依次尝试解析 my-service.my-namespace.svc.cluster.local、my-service.svc.cluster.local 等。 options: 一个对象列表，用于设置 DNS 解析器的选项。 name: 选项的名称 (如 ndots, timeout)。 value: 选项的值。 注意：当设置了 dnsPolicy 时，dnsConfig 的行为会有所不同：\n如果 dnsPolicy 是 ClusterFirst，dnsConfig 中提供的 nameservers 会被 添加 到 CoreDNS 之后，作为备用。searches 和 options 则会与默认值 合并。 如果 dnsPolicy 是 None，dnsConfig 必须提供 所有必要 的信息，因为它将是 Pod 唯一的 DNS 配置。 5. 总结 # 日常使用：保持默认的 ClusterFirst 策略，它能满足 99% 的需求。 特殊需求：当需要绕过集群 DNS 时，使用 Default。 高级定制：当需要接入一个完全不同的 DNS 系统时，使用 None 并配合 dnsConfig。 ","date":"2 July 2025","externalUrl":null,"permalink":"/zh-cn/posts/devops/k8s_dnspolicy/","section":"Posts","summary":"","title":"Kubernetes DNS 策略 (`dnsPolicy`) 详解","type":"posts"},{"content":"排查 K8s 问题时，有三大核心命令：\nkubectl describe pod/node ：查看资源事件 (Events)，定位根本原因。 kubectl logs ：查看应用日志，解决程序问题。 kubectl get ：查看资源状态。 第一层：Pod 状态码 # 状态码 (Status) 核心原因 核心排查步骤 Pending 无法调度：调度器找不到合适的节点。 1. kubectl describe pod \u0026lt;name\u0026gt;，检查 Events，定位具体原因：\n- Insufficient cpu/memory (资源不足)。\n- Taints/Tolerations (污点与容忍不匹配)。\n- Affinity rules (亲和性/反亲和性规则不匹配)。\n- PVC not bound (存储卷未就绪)。 ImagePullBackOff / ErrImagePull 镜像拉取失败：Kubelet 无法从仓库拉取镜像。 1. kubectl describe pod \u0026lt;name\u0026gt;，检查 Events，定位具体原因：\n- 镜像名或 Tag 错误 (检查 YAML)。\n- 私有仓库认证失败 (检查 imagePullSecrets)。\n- 网络不通 (登录节点手动 docker/crictl pull 测试)。 CrashLoopBackOff 容器反复崩溃：容器启动后立即退出，Kubelet 不断重启。 1. kubectl logs \u0026lt;pod-name\u0026gt; --previous (查看 上一次 崩溃的日志，极其重要)。\n2. kubectl logs \u0026lt;pod-name\u0026gt; (查看当前日志)。\n3. 根据日志排查应用 Bug、配置错误或内存溢出。 RunContainerError 容器运行时错误：配置正确，但底层容器运行时（如 containerd）无法启动容器。 1. kubectl describe pod \u0026lt;name\u0026gt;，Events 会显示 RunContainerError。\n2. SSH 登录到节点，使用 journalctl -u containerd (或 docker) 查看运行时日志，寻找更底层的错误信息。 CreateContainerConfigError 容器配置错误：创建容器所需的配置（如 ConfigMap/Secret）有问题。 1. kubectl describe pod \u0026lt;name\u0026gt;，Events 会明确指出哪个资源找不到或格式错误。 Running (但 Ready 为 0/1) 就绪探针 (Readiness Probe) 失败：Pod 在运行，但未准备好接收流量。 1. kubectl describe pod \u0026lt;name\u0026gt;，Events 会记录 Readiness probe failed。\n2. 检查 ReadinessProbe 配置（初始延迟、超时）或应用依赖的下游服务是否故障。 Terminating (卡住) Pod 无法正常终止：通常是因为有阻止其删除的 finalizer，或存储卷无法卸载。 1. kubectl describe pod \u0026lt;name\u0026gt;，检查 Events 是否有 FailedDetachVolume 等存储相关错误。\n2. kubectl edit pod \u0026lt;name\u0026gt;，查看 metadata.finalizers 字段，可能是某个控制器添加的 finalizer 未被清理。 Unknown 状态未知：通常是节点控制器无法与该 Pod 所在节点的 Kubelet 通信。 1. 这几乎等同于节点 NotReady。立即检查该 Pod 所在节点的健康状况（见第四层）。 Job 失败: BackoffLimitExceeded 任务重试次数超限：Job 创建的 Pod 因执行失败，重试次数达到上限后，Job 被标记为失败。 1. kubectl get pods -l job-name=\u0026lt;job-name\u0026gt; 找到该 Job 创建的失败 Pods。\n2. kubectl logs \u0026lt;failed-pod-name\u0026gt; 查看日志，定位任务失败的根本原因。 第二层：容器退出码 # 退出码 (Exit Code) 含义 核心排查步骤 1 通用程序错误 1. 查看应用日志：kubectl logs \u0026lt;pod-name\u0026gt; --previous。 126 / 127 命令不可执行 / 未找到 1. 检查 Dockerfile (chmod +x) 和 YAML command 路径。 137 OOMKilled (内存溢出) 1. kubectl describe pod \u0026lt;name\u0026gt; 确认 Reason: OOMKilled。\n2. 增加 resources.limits.memory。 139 段错误 (SIGSEGV)：代码 Bug。 1. 通知开发人员 进行代码调试。 143 优雅终止 (SIGTERM)：正常行为。 1. 在删除或更新 Pod 时出现，无需处理。 第三层：网络状态码与错误 # 错误码/状态 核心原因 核心排查步骤 Endpoints 为空 Service Selector 未匹配到Pod 1. kubectl describe svc \u0026lt;name\u0026gt; 检查 Selector。\n2. kubectl get pods --show-labels 对比 Pod 的 Labels。 HTTP 502/503/504 Ingress 网关错误/服务不可用/超时。 1. 综合排查 Endpoints、Pod 健康状态 (CrashLoopBackOff, 0/1 Ready)。\n2. 504: 检查 Pod 日志和资源占用 (kubectl top pod)，判断是否应用处理慢。 HTTP 499 客户端关闭请求, 非标准的 Nginx 状态码。简单说就是后端服务响应时间过长。 1. 检查后端服务响应时间：\n通过 kubectl logs 查看日志，定位是哪个接口(URL)频繁出现 499，确认其 request_time 是否过长。\n2. 检查客户端超时设置：\n确认调用该服务的客户端（浏览器、App或其他微服务）设置的请求超时时间是否过短。\n3. 排查应用性能瓶颈：\n分析对应服务的代码，检查是否存在数据库慢查询、调用第三方服务慢等问题。 Connection refused 连接被拒绝：网络路径通，但目标 Pod 的端口上没有进程在监听。 1. kubectl exec -it \u0026lt;pod-name\u0026gt; -- netstat -tulnp，确认应用是否在正确的端口上监听。\n2. 检查应用的启动日志，看是否有端口绑定失败的错误。 Connection timed out 连接超时：数据包在网络中丢失，通常是网络策略或防火墙问题。 1. 检查网络策略：kubectl get networkpolicy -A，确认是否有策略阻止了此流量。\n2. 检查节点安全组或底层网络防火墙。 No route to host 没有到主机的路由：通常是节点间的网络（CNI）出了问题。 1. 检查 CNI 插件的 Pods (calico-node, flannel-ds 等) 是否在所有节点都正常运行。 第四层：节点状态码 # 状态码 (Status) 核心原因 核心排查步骤 NotReady 节点失联：Kubelet 与 API Server 通信中断。 1. SSH 登录节点，依次检查 kubelet, containerd, df -h, free -m。 SchedulingDisabled 调度已禁用：节点被 cordon ( cordon 隔离) 了，不再接受新 Pod。 1. 这是管理员操作，非故障。使用 kubectl uncordon \u0026lt;node-name\u0026gt; 可恢复调度。 MemoryPressure 内存压力：节点可用内存过低。 1. 节点可能会开始 驱逐 (Evict) Pods。登录节点用 top 找出内存大户。 DiskPressure 磁盘压力：节点磁盘空间不足。 1. 登录节点，df -h 定位分区，清理镜像、容器和日志。 PIDPressure PID 压力：节点上进程 ID 资源耗尽。 1. 登录节点，检查是否有进程 fork 炸弹或应用创建了过多线程/进程。 第五层：存储状态码 # 状态码 (Status) / 事件 核心原因 核心排查步骤 PVC: Pending PVC 无法绑定到 PV。 1. kubectl describe pvc \u0026lt;name\u0026gt;，检查 Events，看是 PV 不匹配还是 StorageClass 问题。 Pod Event: FailedMount 卷挂载失败。 1. kubectl describe pod \u0026lt;name\u0026gt;，Events 会提供详细原因，如 NFS 权限、云盘状态等。 Pod Event: FailedDetachVolume 卷卸载失败：通常是底层存储（如云盘）正忙或出现问题。 1. 此问题会导致 Pod 卡在 Terminating 状态。\n2. 检查 CSI 插件日志或云厂商控制台，看该存储卷的状态。 应用日志: Read-only file system 文件系统只读：Pod 写入 PV 时报错。 1. kubectl exec -it \u0026lt;pod-name\u0026gt; -- mount 查看挂载信息，确认挂载选项是否为 ro (只读)。\n2. 可能是存储后端本身出现问题，进入了只读保护模式。 ","date":"2 July 2025","externalUrl":null,"permalink":"/zh-cn/posts/devops/k8s_error_code/","section":"Posts","summary":"","title":"Kubernetes 运维常见错误代码","type":"posts"},{"content":"","externalUrl":null,"permalink":"/zh-cn/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/zh-cn/series/","section":"Series","summary":"","title":"Series","type":"series"}]