[{"content":"创建 NBD 镜像时，为什么要把 backing 的所有镜像全部设置为 NBD？\n因为镜像链的解析是 qemu driver 负责的，NBD 和本地 file 都只是独立的块设备，没有解析能力。\n现象 创建 NBD 镜像时报错：\ncmd [qemu-img create -f qcow2 -b nbd://10.0.0.10:54195 -F qcow2 /dev/qbd/vol/vol-example.img] on [local] failed: ret[1] out[] err[qemu-img: /dev/qbd/vol/vol-example.img: Could not open backing file: Could not open \u0026#39;/data/image/centos79x64a\u0026#39;: No such file or directory Could not open backing image.]. exec_time is [0.093]s 怪不得创建 NBD 镜像的时候，会把 backing 的所有镜像全部设置为 NBD。之前很费解。\n有一个误区 以为通过 NBD 找到 nbd://10.0.0.10:54195 head 镜像，那么 NBD 镜像的 backing 镜像，自然通过 NBD head 链接一起使用了。\n现在看起来压根不是。\n真正的原理 backing 仅仅是元数据信息，真正要读写信息的，还是在使用端，而且是分开去读的。\n元数据上，一串 backing 体现为一串 img。\n但是在读端，这些就是独立的、一个一个的 img，读端需要挨个去读 img。\nqcow2 镜像链的基本原理 qcow2 的链式存储（CoW - Copy on Write）依赖于文件头（Header）中的元数据指针：\nHeader 记录：当镜像 B 以镜像 A 为 Backing File 创建时，镜像 B 的 Header 中会以字符串形式硬编码写入 A 的路径（如 /data/image/centos79x64a）。 读写路由（Read/Write Routing）： 写操作：始终只写入最新的镜像 B（Top Image）。 读操作：先查镜像 B 的 Cluster 分配表（L1/L2 Table）。 如果该数据块已被修改，直接读取 B； 如果未修改（Unallocated），则由 QEMU 的 Block Driver 沿着 Backing 路径去打开并读取基础盘 A。 QEMU 存储驱动架构解析 qcow qemu block driver 解析逻辑：\n[本地 qemu-img] │ ├─► 1. 连接网络协议：nbd://10.0.0.10:54195 │ ├─► 2. 读取 NBD 镜像的 Header 元数据 │ │ │ └─► 解析出 Backing 路径字符串：\u0026#34;/data/image/centos79x64a\u0026#34; │ └─► 3. 本地 Block Driver 校验链条安全性 │ ├─► Driver 试图在 [本地文件系统] 打开：/data/image/centos79x64a │ └─► 结果：本地无此文件 ──► 报错终止！ 镜像链的解析是 qemu driver 负责的。\n不管是 NBD，还是本地 file，它们仅仅是一个独立的块设备，没有解析能力。\n解析能力是 qemu 的 driver 才有的。\n就像有一个 tree 的数据结构，这个 tree 本身没有解析能力。\n有解析能力的是算法程序。\n小结 backing 只是元数据信息，真正读写时读端要挨个去读每个 img； 镜像链的解析是 qemu driver 负责的，NBD 和本地 file 都只是独立块设备； 所以创建 NBD 镜像时，要把 backing 的所有镜像全部设置为 NBD，否则读端无法解析链条； 理解 qcow2 的 CoW 机制和 qemu block driver 的解析逻辑，是理解这类问题的关键。 ","permalink":"/posts/nbd-nested-nbd-qcow2-chain/","summary":"\u003cp\u003e创建 NBD 镜像时，为什么要把 backing 的所有镜像全部设置为 NBD？\u003c/p\u003e\n\u003cp\u003e因为镜像链的解析是 qemu driver 负责的，NBD 和本地 file 都只是独立的块设备，没有解析能力。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e创建 NBD 镜像时报错：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecmd \u003cspan class=\"o\"\u003e[\u003c/span\u003eqemu-img create -f qcow2 -b nbd://10.0.0.10:54195 -F qcow2 /dev/qbd/vol/vol-example.img\u003cspan class=\"o\"\u003e]\u003c/span\u003e on \u003cspan class=\"o\"\u003e[\u003c/span\u003elocal\u003cspan class=\"o\"\u003e]\u003c/span\u003e failed: ret\u003cspan class=\"o\"\u003e[\u003c/span\u003e1\u003cspan class=\"o\"\u003e]\u003c/span\u003e out\u003cspan class=\"o\"\u003e[]\u003c/span\u003e err\u003cspan class=\"o\"\u003e[\u003c/span\u003eqemu-img: /dev/qbd/vol/vol-example.img: Could not open backing file: Could not open \u003cspan class=\"s1\"\u003e\u0026#39;/data/image/centos79x64a\u0026#39;\u003c/span\u003e: No such file or directory\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eCould not open backing image.\u003cspan class=\"o\"\u003e]\u003c/span\u003e. exec_time is \u003cspan class=\"o\"\u003e[\u003c/span\u003e0.093\u003cspan class=\"o\"\u003e]\u003c/span\u003es\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e怪不得创建 NBD 镜像的时候，会把 backing 的所有镜像全部设置为 NBD。之前很费解。\u003c/p\u003e\n\u003ch2 id=\"有一个误区\"\u003e有一个误区\u003c/h2\u003e\n\u003cp\u003e以为通过 NBD 找到 \u003ccode\u003enbd://10.0.0.10:54195\u003c/code\u003e head 镜像，那么 NBD 镜像的 backing 镜像，自然通过 NBD head 链接一起使用了。\u003c/p\u003e","title":"为什么 NBD 要嵌套 NBD？qcow2 镜像链解析原理"},{"content":"qemu64 是 qemu 的默认兼容性 CPU 模型，指令集很基础。但查看它的定义，居然包含 svm 这类虚拟化指令，这背后是 TCG 与 KVM 两种支持场景的差异。\n现象 查看 qemu64 CPU 模型定义：\ncat /usr/share/libvirt/cpu_map/x86_qemu64.xml \u0026lt;cpus\u0026gt; \u0026lt;model name=\u0026#39;qemu64\u0026#39;\u0026gt; \u0026lt;decode host=\u0026#39;on\u0026#39; guest=\u0026#39;on\u0026#39;/\u0026gt; \u0026lt;!-- These are supported only by TCG. KVM supports them only if the host does. So we leave them out: \u0026lt;feature name=\u0026#39;abm\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;lahf_lm\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;popcnt\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;sse4a\u0026#39;/\u0026gt; --\u0026gt; \u0026lt;feature name=\u0026#39;apic\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;clflush\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;cmov\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;cx16\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;cx8\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;de\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;fpu\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;fxsr\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;lm\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;mca\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;mce\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;mmx\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;msr\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;mtrr\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;nx\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;pae\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;pat\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;pge\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;pni\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;pse\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;pse36\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;sep\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;sse\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;sse2\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;svm\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;syscall\u0026#39;/\u0026gt; \u0026lt;feature name=\u0026#39;tsc\u0026#39;/\u0026gt; \u0026lt;/model\u0026gt; \u0026lt;/cpus\u0026gt; 分析 qemu64 是 qemu 为了兼容性提供的默认 CPU 模型，指令集非常基础。 但有意思的是，它居然包含了 svm（AMD 的虚拟化扩展指令）。\n注释里也说明了：abm、lahf_lm、popcnt、sse4a 这些指令只被 TCG 支持，KVM 只有在宿主 CPU 支持时才支持，所以被排除在外。\n而 svm 之所以保留，是因为它作为虚拟化指令，在 KVM 场景下由宿主 CPU 提供，qemu64 模型里声明它并不会影响兼容性。\n小结 qemu64 是 qemu 的默认兼容性 CPU 模型，指令集很基础； 它包含 svm 等虚拟化指令，但排除了 abm、popcnt、sse4a 等只被 TCG 支持的指令； 理解 CPU 模型，要区分\u0026quot;TCG 支持\u0026quot;和\u0026quot;KVM 支持\u0026quot;两种场景； 现代操作系统需要较新的指令集（如 x86-64-v2），默认 qemu64 往往不够用。 ","permalink":"/posts/qemu64-cpu-model-svm/","summary":"\u003cp\u003eqemu64 是 qemu 的默认兼容性 CPU 模型，指令集很基础。但查看它的定义，居然包含 svm 这类虚拟化指令，这背后是 TCG 与 KVM 两种支持场景的差异。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e查看 qemu64 CPU 模型定义：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecat /usr/share/libvirt/cpu_map/x86_qemu64.xml\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;cpus\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;model\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;qemu64\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;decode\u003c/span\u003e \u003cspan class=\"na\"\u003ehost=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;on\u0026#39;\u003c/span\u003e \u003cspan class=\"na\"\u003eguest=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;on\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c\"\u003e\u0026lt;!-- These are supported only by TCG.  KVM supports them only if the\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e         host does.  So we leave them out:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e    \u0026lt;feature name=\u0026#39;abm\u0026#39;/\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e    \u0026lt;feature name=\u0026#39;lahf_lm\u0026#39;/\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e    \u0026lt;feature name=\u0026#39;popcnt\u0026#39;/\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e    \u0026lt;feature name=\u0026#39;sse4a\u0026#39;/\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e    --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;apic\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;clflush\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;cmov\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;cx16\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;cx8\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;de\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;fpu\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;fxsr\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;lm\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;mca\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;mce\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;mmx\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;msr\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;mtrr\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;nx\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;pae\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;pat\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;pge\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;pni\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;pse\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;pse36\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;sep\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;sse\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;sse2\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;svm\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;syscall\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;tsc\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;/model\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/cpus\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"分析\"\u003e分析\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eqemu64\u003c/code\u003e 是 qemu 为了兼容性提供的默认 CPU 模型，指令集非常基础。\n但有意思的是，它居然包含了 \u003ccode\u003esvm\u003c/code\u003e（AMD 的虚拟化扩展指令）。\u003c/p\u003e","title":"qemu64 CPU 模型为什么有 svm？兼容性模型的指令集设计"},{"content":"文件系统只读有两种：卷只读和 Linux 只读。卷只读是存储侧控制，能恢复；Linux 只读是存储 IO 错误触发的保护机制，remount 基本失败，只能重启。\n现象 虚拟机文件系统变成只读：\nroot@host:~# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT vda 253:0 0 20G 0 disk └─vda1 253:1 0 20G 0 part / vdb 253:16 0 4G 0 disk [SWAP] vdc 253:32 0 100G 0 disk root@host:~# touch test touch: cannot touch \u0026#39;test\u0026#39;: Read-only file system 同事说另一个类型的虚拟机只读了能恢复，这个怎么恢复。\n排查 先要分清是\u0026quot;卷只读\u0026quot;还是\u0026quot;Linux 只读\u0026quot;，两者恢复方式完全不同。\n再确认存储是否中断过，因为 Linux 只读通常由存储 IO 错误触发。\n根因 同事那个能恢复，是因为不是 Linux 只读，而是卷只读。那个卷是我们公司自己的存储，能控制其读写权限。\n但这里 Linux 只读，一般是存储读写有 IO error，触发了 Linux 保护机制，系统只读。\n我问他是不是存储中断过，他说是的。\n这种 Linux 只读没法恢复，remount 基本失败，只能重启虚拟机。\n小结 Linux 文件系统只读，通常是存储 IO 错误触发的保护机制； 卷只读（存储侧控制）可以恢复，Linux 只读（IO 错误触发）基本无法 remount 恢复； 排查时先确认是\u0026quot;卷只读\u0026quot;还是\u0026quot;Linux 只读\u0026quot;，两者恢复方式完全不同； Linux 只读只能重启虚拟机恢复。 ","permalink":"/posts/linux-readonly-filesystem/","summary":"\u003cp\u003e文件系统只读有两种：卷只读和 Linux 只读。卷只读是存储侧控制，能恢复；Linux 只读是存储 IO 错误触发的保护机制，remount 基本失败，只能重启。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e虚拟机文件系统变成只读：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eroot@host:~# lsblk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eNAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evda    253:0    \u003cspan class=\"m\"\u003e0\u003c/span\u003e   20G  \u003cspan class=\"m\"\u003e0\u003c/span\u003e disk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e└─vda1 253:1    \u003cspan class=\"m\"\u003e0\u003c/span\u003e   20G  \u003cspan class=\"m\"\u003e0\u003c/span\u003e part /\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evdb    253:16   \u003cspan class=\"m\"\u003e0\u003c/span\u003e    4G  \u003cspan class=\"m\"\u003e0\u003c/span\u003e disk \u003cspan class=\"o\"\u003e[\u003c/span\u003eSWAP\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evdc    253:32   \u003cspan class=\"m\"\u003e0\u003c/span\u003e  100G  \u003cspan class=\"m\"\u003e0\u003c/span\u003e disk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eroot@host:~# touch \u003cspan class=\"nb\"\u003etest\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etouch: cannot touch \u003cspan class=\"s1\"\u003e\u0026#39;test\u0026#39;\u003c/span\u003e: Read-only file system\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e同事说另一个类型的虚拟机只读了能恢复，这个怎么恢复。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e先要分清是\u0026quot;卷只读\u0026quot;还是\u0026quot;Linux 只读\u0026quot;，两者恢复方式完全不同。\u003c/p\u003e\n\u003cp\u003e再确认存储是否中断过，因为 Linux 只读通常由存储 IO 错误触发。\u003c/p\u003e\n\u003ch2 id=\"根因\"\u003e根因\u003c/h2\u003e\n\u003cp\u003e同事那个能恢复，是因为\u003cstrong\u003e不是 Linux 只读，而是卷只读\u003c/strong\u003e。那个卷是我们公司自己的存储，能控制其读写权限。\u003c/p\u003e\n\u003cp\u003e但这里 Linux 只读，一般是\u003cstrong\u003e存储读写有 IO error，触发了 Linux 保护机制，系统只读\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e我问他是不是存储中断过，他说是的。\u003c/p\u003e\n\u003cp\u003e这种 Linux 只读\u003cstrong\u003e没法恢复，remount 基本失败，只能重启虚拟机\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLinux 文件系统只读，通常是存储 IO 错误触发的保护机制；\u003c/li\u003e\n\u003cli\u003e卷只读（存储侧控制）可以恢复，Linux 只读（IO 错误触发）基本无法 remount 恢复；\u003c/li\u003e\n\u003cli\u003e排查时先确认是\u0026quot;卷只读\u0026quot;还是\u0026quot;Linux 只读\u0026quot;，两者恢复方式完全不同；\u003c/li\u003e\n\u003cli\u003eLinux 只读只能重启虚拟机恢复。\u003c/li\u003e\n\u003c/ul\u003e","title":"为什么 Linux 只读 remount 失败，只能重启？"},{"content":"虚拟机通过 nbd 秒级启动，但 nbd-server 连接总被误清理。根因是新版本 nbd-server 默认用 tcp6 双栈监听，而清理代码用 netstat -4 只过滤 tcp4，把 tcp6 上的连接误判为\u0026quot;无连接\u0026quot;而误清理。\n现象 虚拟机通过 nbd 可以实现秒级启动，之前跑的好好的。 最近一个测试环境，总是会出现 nbd 连接丢失的情况。\n排查是 base img 的 nbd-server 进程被 kill。 且通过日志看，就是我们的程序给 kill 的。\n但是我们的程序是正常的清理，会判断这个 nbd-server 没有连接，才会去 kill。 到底是哪里的问题？是真没有连接了，还是哪里判断有问题。\n排查 代码会先检查已经有连接的，这些是肯定不会去 kill：\nexec_cmd(\u0026#34;netstat -4 --tcp -p -n | grep ESTABLISHED | grep nbd-server | awk \u0026#39;{print $4}\u0026#39; | awk -F\u0026#39;:\u0026#39; \u0026#39;{print $2}\u0026#39; | sort -u \u0026gt; %s\u0026#34; % tmp_file) 代码已经十年了，看着也很完善，没出过类似问题。\n后发现是 tcp6 和 tcp4 的原因，和 nbd-server 的版本有关系：\n以前部署的是 This is nbd-server version 3.9.1 现在部署的是 This is nbd-server version 3.23 看起来新版本默认用的是 tcp6 双栈，老版本默认是 tcp4。\n新版本 nbd-server 的监听是这样：\nroot@:~# netstat -tpln |grep nbd-server tcp6 0 0 :::43403 :::* LISTEN 2468990/nbd-server 老版本 nbd-server 的监听是这样：\nroot@host:~# netstat -tpln |grep nbd-server tcp 0 0 0.0.0.0:43403 0.0.0.0:* LISTEN 2468990/nbd-server 关键区别在于，开头的 tcp6 和 tcp，还有 ::: 和 0.0.0.0。\n根因 查看内核参数 net.ipv6.bindv6only 是 0，也就是说默认是开启双栈的。内核参数就不改了。\n双栈兼容处理：当一个程序在 IPv6 套接字（AF_INET6）上监听时，如果未显式启用 IPV6_V6ONLY 选项，这个监听套接字就可以同时接收 IPv4 和 IPv6 的传入连接。 协议栈隐式转换：当纯 IPv4 客户端连接进来时，Linux 内核会自动将该 IPv4 地址转换为 IPv6 格式（例如将 10.0.0.10 在内部表示为 ::ffff:10.0.0.10），并通过 IPv6 的套接字来处理这个连接。 netstat 展示机制：因为这个连接在系统内核层面被归类在 tcp6 套接字下处理，所以 netstat 读取 /proc/net/tcp6 时就会把它显示为 tcp6，即便输出界面上渲染出的远端 IP 依然是 IPv4 的格式。 解决办法 现在有两种方式：\n修改代码，netstat 不加 -4，这样 tcp 和 tcp6 都能过滤到； 应该有配置，约束 nbd-server 的行为，让其和老版本一样，只启动 tcp4 的连接。 调整配置：各种 AI 都是这个结论：\n/etc/nbd-server/config [generic] bindaddr = 0.0.0.0 一顿折腾猛如虎，回头一看原地杵。后查看手册发现正解：\nman 5 nbd-server listenaddr = 0.0.0.0 一点思考 虽然 2026 年了，虽然 AI 很强了，但是这个问题依然需要人去解决。 前面各种 AI 给的解法都是：一直不行，AI 说各种格式问题，疯狂尝试。 结果可想而知，从源头参数就是错的，围绕错误的开头，AI 做再多都是徒劳，浪费算力和时间。 从这个角度看，AI 是个很强的智能工具，依然需要人去驾驭。\n有些时候，这取舍啊，真是理解不了。 这里为啥要用 netstat -4 呢？是怕匹配到非我们平台的 nbd，导致我们漏清理了吗？ 但是漏清理就是多占用个进程，而误清理，直接影响虚拟机运行，这是要直接中断业务的呀。 这两个结果，怎么能选择规避漏清理，而埋下误清理的选择呢，不能理解！\n小结 新版本 nbd-server 默认用 tcp6 双栈监听，老版本默认 tcp4； 清理代码用 netstat -4 只过滤 tcp4，导致 tcp6 上的连接被误判为\u0026quot;无连接\u0026quot;而误清理； 双栈下 IPv4 连接也会被归类到 tcp6 套接字，netstat -4 会漏掉； 正解是 listenaddr = 0.0.0.0（不是 bindaddr）； 排查问题要从源头参数入手，AI 再强也需要人去驾驭。 ","permalink":"/posts/nbd-server-tcp6-miscleanup/","summary":"\u003cp\u003e虚拟机通过 nbd 秒级启动，但 nbd-server 连接总被误清理。根因是新版本 nbd-server 默认用 tcp6 双栈监听，而清理代码用 \u003ccode\u003enetstat -4\u003c/code\u003e 只过滤 tcp4，把 tcp6 上的连接误判为\u0026quot;无连接\u0026quot;而误清理。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e虚拟机通过 nbd 可以实现秒级启动，之前跑的好好的。\n最近一个测试环境，总是会出现 nbd 连接丢失的情况。\u003c/p\u003e\n\u003cp\u003e排查是 base img 的 nbd-server 进程被 kill。\n且通过日志看，就是我们的程序给 kill 的。\u003c/p\u003e\n\u003cp\u003e但是我们的程序是正常的清理，会判断这个 nbd-server 没有连接，才会去 kill。\n到底是哪里的问题？是真没有连接了，还是哪里判断有问题。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e代码会先检查已经有连接的，这些是肯定不会去 kill：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eexec_cmd\u003cspan class=\"o\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;netstat -4 --tcp -p -n | grep ESTABLISHED | grep nbd-server | awk \u0026#39;{print \u003c/span\u003e\u003cspan class=\"nv\"\u003e$4\u003c/span\u003e\u003cspan class=\"s2\"\u003e}\u0026#39; | awk -F\u0026#39;:\u0026#39; \u0026#39;{print \u003c/span\u003e\u003cspan class=\"nv\"\u003e$2\u003c/span\u003e\u003cspan class=\"s2\"\u003e}\u0026#39; | sort -u \u0026gt; %s\u0026#34;\u003c/span\u003e % tmp_file\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e代码已经十年了，看着也很完善，没出过类似问题。\u003c/p\u003e\n\u003cp\u003e后发现是 \u003cstrong\u003etcp6 和 tcp4\u003c/strong\u003e 的原因，和 nbd-server 的版本有关系：\u003c/p\u003e","title":"nbd-server 连接被误清理：tcp6 双栈的坑"},{"content":"裸金属创建时报固件错误，网卡驱动 bnx2x 加载不了固件文件，导致 bond 从设备打开失败。根因是镜像里缺少驱动需要的固件，用 modinfo 查出来补上即可。\n现象 创建裸金属报固件错误，带外上看，保持类似如下：\nbnx2x: Can\u0026#39;t load firmware file bnx2x/bnx2x-e2-7.13.11.0.fw bnx2x: Error loading firmware bond0: (slave eth110): Opening slave failed 排查 把镜像通过 qemu-nbd 挂载上来， 然后用 modinfo 查看这个驱动， 里面会列出需要的固件：\nfilename: /mnt/bootcoree/lib/modules/5.4.0-59-generic/kernel/drivers/net/ethernet/broadcom/bnx2x/bnx2x.ko firmware: bnx2x/bnx2x-e2-7.13.11.0.fw firmware: bnx2x/bnx2x-e1h-7.13.11.0.fw firmware: bnx2x/bnx2x-e1-7.13.11.0.fw 根因与解决 根因是镜像里缺少 bnx2x 驱动需要的固件文件。\n把 modinfo 查到的固件下载下来， 放到镜像的 /lib/firmware 对应目录中，就可以了。\n小结 网卡驱动加载固件失败，会导致 bond 从设备打开失败； 用 modinfo 查看驱动模块声明的 firmware 字段，就能知道需要哪些固件文件； 把固件放到镜像的 /lib/firmware 对应目录即可解决； 排查固件类问题，modinfo 是最直接的入口。 ","permalink":"/posts/bm-firmware-error-bnx2x/","summary":"\u003cp\u003e裸金属创建时报固件错误，网卡驱动 bnx2x 加载不了固件文件，导致 bond 从设备打开失败。根因是镜像里缺少驱动需要的固件，用 \u003ccode\u003emodinfo\u003c/code\u003e 查出来补上即可。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e创建裸金属报固件错误，带外上看，保持类似如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ebnx2x: Can\u003cspan class=\"err\"\u003e\u0026#39;\u003c/span\u003et load firmware file bnx2x/bnx2x-e2-7.13.11.0.fw\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ebnx2x: Error loading firmware\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ebond0: \u003cspan class=\"o\"\u003e(\u003c/span\u003eslave eth110\u003cspan class=\"o\"\u003e)\u003c/span\u003e: Opening slave failed\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e把镜像通过 \u003ccode\u003eqemu-nbd\u003c/code\u003e 挂载上来，\n然后用 \u003ccode\u003emodinfo\u003c/code\u003e 查看这个驱动，\n里面会列出需要的固件：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efilename:       /mnt/bootcoree/lib/modules/5.4.0-59-generic/kernel/drivers/net/ethernet/broadcom/bnx2x/bnx2x.ko\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirmware:       bnx2x/bnx2x-e2-7.13.11.0.fw\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirmware:       bnx2x/bnx2x-e1h-7.13.11.0.fw\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efirmware:       bnx2x/bnx2x-e1-7.13.11.0.fw\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"根因与解决\"\u003e根因与解决\u003c/h2\u003e\n\u003cp\u003e根因是镜像里缺少 bnx2x 驱动需要的固件文件。\u003c/p\u003e\n\u003cp\u003e把 \u003ccode\u003emodinfo\u003c/code\u003e 查到的固件下载下来，\n放到镜像的 \u003ccode\u003e/lib/firmware\u003c/code\u003e 对应目录中，就可以了。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e网卡驱动加载固件失败，会导致 bond 从设备打开失败；\u003c/li\u003e\n\u003cli\u003e用 \u003ccode\u003emodinfo\u003c/code\u003e 查看驱动模块声明的 \u003ccode\u003efirmware\u003c/code\u003e 字段，就能知道需要哪些固件文件；\u003c/li\u003e\n\u003cli\u003e把固件放到镜像的 \u003ccode\u003e/lib/firmware\u003c/code\u003e 对应目录即可解决；\u003c/li\u003e\n\u003cli\u003e排查固件类问题，\u003ccode\u003emodinfo\u003c/code\u003e 是最直接的入口。\u003c/li\u003e\n\u003c/ul\u003e","title":"裸金属报固件错误：bnx2x 加载不了固件文件"},{"content":"裸金属装机一直卡在 iscsistart，根因是 ethX 网卡乱序，iSCSI 连接建在了错误的网卡上。在 PXELINUX 配置里加 IPAPPEND 2，让引导程序自动把引导网卡的 MAC 追加给内核，即可解决。\n现象 创建裸金属时，网卡乱序导致 iSCSI 镜像加载不了，装机一直卡在 iscsistart。\n排查 看 PXELINUX 配置：\n/pxelinux.cfg# cat 01-8c-96-a5-2d-fb-70 DEFAULT linux LABEL linux menu default menu label Linux KERNEL vmlinuz APPEND initrd=initrd.img ip=10.0.0.64:10.0.0.185::255.255.255.0:bm-example:eth0:off ISCSI_TARGET_NAME=iqn.2012-04.example.com:bm-example-boot.img ISCSI_TARGET_IP=10.0.0.185 ISCSI_TARGET_PORT=3260 root=UUID=4a99650e-84e7-4c6a-83f9-1b1c964f39de rw net.ifnames=0 biosdevname=0 这里 APPEND 里有个重要参数：eth0，就是通过 eth0 建立 iSCSI 连接，切换 rootfs。\n根因 问题就在这里：ethX 天生就有乱序问题。\n我们期望网卡 1 是 eth0，物理链路上和 iSCSI 在一个网络上。 但是因为乱序问题，网卡 2 名称是 eth1，这样 iSCSI 就不通了，因为物理链路就不通。\n这个时候 BM 装机就一直卡在 iscsistart。\n解决办法 在 PXELINUX 配置中加上 IPAPPEND 2。\n添加 IPAPPEND 2 后，PXELINUX 引导程序在加载内核时，会自动把刚才成功下载到 PXE 文件的那个网卡的 MAC 地址以 BOOTIF=01-8c-96-a5-2d-fb-70 的形式动态追加到 APPEND 末尾。内核根据 BOOTIF 准确地把 IP 赋给这张网卡，完全不需要在配置文件里手动写死 MAC 或 eth0。\n/pxelinux.cfg# cat 01-8c-96-a5-2d-fb-70 DEFAULT linux LABEL linux menu default menu label Linux KERNEL vmlinuz IPAPPEND 2 # 新加参数 APPEND initrd=initrd.img ip=10.0.0.64:10.0.0.185::255.255.255.0:bm-example::off ISCSI_TARGET_NAME=iqn.2012-04.example.com:bm-example-boot.img ISCSI_TARGET_IP=10.0.0.185 ISCSI_TARGET_PORT=3260 root=UUID=4a99650e-84e7-4c6a-83f9-1b1c964f39de rw net.ifnames=0 biosdevname=0 APPEND 中去掉 eth0（ethX）。\n小结 ethX 网卡命名天生有乱序问题，不能依赖 eth0 来建立 iSCSI 连接； IPAPPEND 2 让 PXELINUX 自动把引导网卡的 MAC 以 BOOTIF 形式追加给内核； 内核根据 BOOTIF 准确绑定网卡，彻底解决乱序问题； 配置里不要写死 MAC 或 eth0，交给 PXELINUX 动态处理。 ","permalink":"/posts/bm-nic-order-iscsi-ipappend/","summary":"\u003cp\u003e裸金属装机一直卡在 \u003ccode\u003eiscsistart\u003c/code\u003e，根因是 ethX 网卡乱序，iSCSI 连接建在了错误的网卡上。在 PXELINUX 配置里加 \u003ccode\u003eIPAPPEND 2\u003c/code\u003e，让引导程序自动把引导网卡的 MAC 追加给内核，即可解决。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e创建裸金属时，网卡乱序导致 iSCSI 镜像加载不了，装机一直卡在 \u003ccode\u003eiscsistart\u003c/code\u003e。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e看 PXELINUX 配置：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/pxelinux.cfg# cat 01-8c-96-a5-2d-fb-70\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eDEFAULT linux\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eLABEL linux\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emenu default\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emenu label Linux\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eKERNEL vmlinuz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eAPPEND \u003cspan class=\"nv\"\u003einitrd\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003einitrd.img \u003cspan class=\"nv\"\u003eip\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e10.0.0.64:10.0.0.185::255.255.255.0:bm-example:eth0:off \u003cspan class=\"nv\"\u003eISCSI_TARGET_NAME\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003eiqn.2012-04.example.com:bm-example-boot.img \u003cspan class=\"nv\"\u003eISCSI_TARGET_IP\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e10.0.0.185 \u003cspan class=\"nv\"\u003eISCSI_TARGET_PORT\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e3260\u003c/span\u003e \u003cspan class=\"nv\"\u003eroot\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"nv\"\u003eUUID\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e4a99650e-84e7-4c6a-83f9-1b1c964f39de rw net.ifnames\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e \u003cspan class=\"nv\"\u003ebiosdevname\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这里 \u003ccode\u003eAPPEND\u003c/code\u003e 里有个重要参数：\u003ccode\u003eeth0\u003c/code\u003e，就是通过 \u003ccode\u003eeth0\u003c/code\u003e 建立 iSCSI 连接，切换 rootfs。\u003c/p\u003e\n\u003ch2 id=\"根因\"\u003e根因\u003c/h2\u003e\n\u003cp\u003e问题就在这里：\u003cstrong\u003eethX 天生就有乱序问题\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e我们期望网卡 1 是 \u003ccode\u003eeth0\u003c/code\u003e，物理链路上和 iSCSI 在一个网络上。\n但是因为乱序问题，网卡 2 名称是 \u003ccode\u003eeth1\u003c/code\u003e，这样 iSCSI 就不通了，因为物理链路就不通。\u003c/p\u003e\n\u003cp\u003e这个时候 BM 装机就一直卡在 \u003ccode\u003eiscsistart\u003c/code\u003e。\u003c/p\u003e\n\u003ch2 id=\"解决办法\"\u003e解决办法\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e在 PXELINUX 配置中加上 \u003ccode\u003eIPAPPEND 2\u003c/code\u003e\u003c/strong\u003e。\u003c/p\u003e","title":"裸金属网卡乱序导致 iSCSI 加载不了：IPAPPEND 2 的妙用"},{"content":"创建裸金属时一直刷 NBP file downloaded successfully，但怎么都进不去系统。根因是启动方式被改成了 UEFI，而裸金属注册时一般是 Legacy，两者的 NBP 文件完全不同。\n现象 创建裸金属（BM）时，一直刷 NBP file downloaded successfully，但怎么都跑不进去。\nNBP 是 Network Bootstrap Program（网络引导程序）。 看起来 PXE 的程序已经获取成功了，但怎么都跑不进去。\n然后只刷这个页面，只不过每次会换一个网卡，看起来还挨个网卡去找 PXE 服务端。\n排查 后注意到启动方式是 UEFI，但是我们 BM 注册的时候一般是 Legacy。\n且 UEFI 的 NBP 和 Legacy 的 NBP 也不一样，所以肯定是启动不起来的。\n后修改裸金属的启动方式为 Legacy，部署成功。\n根因：UEFI 与 Legacy 的 NBP 差异 启动模式 常见 NBP 文件名 对应软件 / 来源 Legacy BIOS pxelinux.0 SYSLINUX 引导工具 Legacy BIOS undionly.kpxe / ipxe.pxe iPXE 开源网络引导固件 UEFI 64-bit bootx64.efi / syslinux.efi UEFI 版 SYSLINUX UEFI 64-bit grubx64.efi GNU GRUB2 引导器 小结 NBP 是网络引导程序，PXE 获取成功但进不去，往往是引导模式不匹配； UEFI 和 Legacy 的 NBP 文件完全不同，不能混用； 裸金属注册时一般是 Legacy，如果启动方式被改成了 UEFI，就会一直刷 NBP 但起不来； 排查时先确认启动模式（UEFI / Legacy），再确认对应的 NBP 文件是否正确。 ","permalink":"/posts/bm-nbp-uefi-legacy/","summary":"\u003cp\u003e创建裸金属时一直刷 \u003ccode\u003eNBP file downloaded successfully\u003c/code\u003e，但怎么都进不去系统。根因是启动方式被改成了 UEFI，而裸金属注册时一般是 Legacy，两者的 NBP 文件完全不同。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e创建裸金属（BM）时，一直刷 \u003ccode\u003eNBP file downloaded successfully\u003c/code\u003e，但怎么都跑不进去。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eNBP\u003c/strong\u003e 是 \u003cstrong\u003eNetwork Bootstrap Program\u003c/strong\u003e（网络引导程序）。\n看起来 PXE 的程序已经获取成功了，但怎么都跑不进去。\u003c/p\u003e\n\u003cp\u003e然后只刷这个页面，只不过每次会换一个网卡，看起来还挨个网卡去找 PXE 服务端。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e后注意到启动方式是 \u003cstrong\u003eUEFI\u003c/strong\u003e，但是我们 BM 注册的时候一般是 \u003cstrong\u003eLegacy\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e且 UEFI 的 NBP 和 Legacy 的 NBP 也不一样，所以肯定是启动不起来的。\u003c/p\u003e\n\u003cp\u003e后修改裸金属的启动方式为 \u003cstrong\u003eLegacy\u003c/strong\u003e，部署成功。\u003c/p\u003e\n\u003ch2 id=\"根因uefi-与-legacy-的-nbp-差异\"\u003e根因：UEFI 与 Legacy 的 NBP 差异\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003e启动模式\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003e常见 NBP 文件名\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e\u003cstrong\u003e对应软件 / 来源\u003c/strong\u003e\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eLegacy BIOS\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003epxelinux.0\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSYSLINUX 引导工具\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eLegacy BIOS\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eundionly.kpxe\u003c/code\u003e / \u003ccode\u003eipxe.pxe\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eiPXE 开源网络引导固件\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eUEFI 64-bit\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003ebootx64.efi\u003c/code\u003e / \u003ccode\u003esyslinux.efi\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUEFI 版 SYSLINUX\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eUEFI 64-bit\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003egrubx64.efi\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGNU GRUB2 引导器\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eNBP 是网络引导程序，PXE 获取成功但进不去，往往是引导模式不匹配；\u003c/li\u003e\n\u003cli\u003eUEFI 和 Legacy 的 NBP 文件完全不同，不能混用；\u003c/li\u003e\n\u003cli\u003e裸金属注册时一般是 Legacy，如果启动方式被改成了 UEFI，就会一直刷 NBP 但起不来；\u003c/li\u003e\n\u003cli\u003e排查时先确认启动模式（UEFI / Legacy），再确认对应的 NBP 文件是否正确。\u003c/li\u003e\n\u003c/ul\u003e","title":"创建裸金属一直刷 NBP：UEFI 与 Legacy 引导差异"},{"content":"ZFS 副本通过 zfs send 从主本复制过来，逻辑数据一样，但物理占用却大很多。根因是副本节点的 raidZ 布局（高可用 + 条带）与 16k 块大小不适配，造成空间膨胀。\n现象 ZFS 副本比主本大很多。主本数据：\nroot@node1:~# zfs get all vpool/vol-example NAME PROPERTY VALUE SOURCE vpool/vol-example type filesystem - vpool/vol-example creation Thu Apr 24 13:59 2025 - vpool/vol-example used 3.94T - vpool/vol-example available 6.02T - vpool/vol-example referenced 3.94T 上面是主本数据，下面是副本数据。 副本是通过 zfs send 过来的，按道理两者应该是一样的。 但是副本的 used 是 4.75T，主本是 3.94T：\nroot@node2:~# zfs get all vpool/vol-example NAME PROPERTY VALUE SOURCE vpool/vol-example type filesystem - vpool/vol-example creation Sun Aug 23 10:48 2026 - vpool/vol-example used 4.75T - vpool/vol-example available 627G - vpool/vol-example referenced 4.75T 排查 后排查了解发现，实际逻辑数据也是一样的，都是 3.92T：\nvpool/vol-example logicalreferenced 3.92T - 根因 副本的占用空间为什么大？是因为副本节点的 zpool 是 raidZ 组成的。\nraidZ 高可用，加条带原因和 16k 的块大小不适配，导致实际占用膨胀。\n另一方面也需要看压缩，zfs 自带压缩，这个要注意。\n小结 逻辑数据一样（logicalreferenced 都是 3.92T），但物理占用不同； 根因是副本节点的 raidZ 布局（高可用 + 条带）与 16k 块大小不适配，造成空间膨胀； 排查 ZFS 空间问题时，要区分 used（物理占用）和 logicalreferenced（逻辑数据量）； 还要注意 zfs 自带的压缩特性对空间的影响。 ","permalink":"/posts/zfs-replica-larger-than-primary/","summary":"\u003cp\u003eZFS 副本通过 \u003ccode\u003ezfs send\u003c/code\u003e 从主本复制过来，逻辑数据一样，但物理占用却大很多。根因是副本节点的 raidZ 布局（高可用 + 条带）与 16k 块大小不适配，造成空间膨胀。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003eZFS 副本比主本大很多。主本数据：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eroot@node1:~# zfs get all vpool/vol-example\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eNAME                PROPERTY                    VALUE                                SOURCE\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evpool/vol-example   \u003cspan class=\"nb\"\u003etype\u003c/span\u003e                        filesystem                           -\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evpool/vol-example   creation                    Thu Apr \u003cspan class=\"m\"\u003e24\u003c/span\u003e 13:59 \u003cspan class=\"m\"\u003e2025\u003c/span\u003e                -\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evpool/vol-example   used                        3.94T                                -\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evpool/vol-example   available                   6.02T                                -\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evpool/vol-example   referenced                  3.94T\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e上面是主本数据，下面是副本数据。\n副本是通过 \u003ccode\u003ezfs send\u003c/code\u003e 过来的，按道理两者应该是一样的。\n但是副本的 \u003ccode\u003eused\u003c/code\u003e 是 4.75T，主本是 3.94T：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eroot@node2:~# zfs get all vpool/vol-example\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eNAME                PROPERTY                    VALUE                                SOURCE\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evpool/vol-example   \u003cspan class=\"nb\"\u003etype\u003c/span\u003e                        filesystem                           -\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evpool/vol-example   creation                    Sun Aug \u003cspan class=\"m\"\u003e23\u003c/span\u003e 10:48 \u003cspan class=\"m\"\u003e2026\u003c/span\u003e                -\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evpool/vol-example   used                        4.75T                                -\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evpool/vol-example   available                   627G                                 -\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evpool/vol-example   referenced                  4.75T\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e后排查了解发现，实际逻辑数据也是一样的，都是 3.92T：\u003c/p\u003e","title":"ZFS 副本为什么比主本占用大：raidZ 条带与块大小不适配"},{"content":"多阶段构建里，COPY --from=\u0026lt;stage\u0026gt; 的源路径并不相对源阶段的 WORKDIR 解析，而是相对目标阶段根目录。所以跨阶段 COPY 时，相对路径会指向错误位置，报 \u0026ldquo;No such file or directory\u0026rdquo;。\n现象 Docker 多阶段构建时，builder 阶段设置了 WORKDIR /src，文件实际位于 /src/internal/xmlbuild/cpu_rule.json 和 /src/cmd/configs/compute.yaml。\n但 COPY --from=builder 后，internal/... 这种相对路径会被解析为相对于 /（即 /internal/...），而不是相对于 WORKDIR /src。\n所以 Docker 找不到文件。\n排查 先确认报错是不是 \u0026ldquo;No such file or directory\u0026rdquo;，且文件确实存在于源阶段。\n再检查 COPY 的源路径写法，看是否用了相对路径。\n根因 多阶段构建中，COPY --from=\u0026lt;stage\u0026gt; 的源路径是相对于构建上下文或目标阶段的根目录解析的，而不是相对于源阶段的 WORKDIR。\n也就是说，COPY --from=builder 之后，就不能用上一个阶段的相对路径了。\n比如你写：\nFROM builder AS build WORKDIR /src # ... 构建，生成 /src/internal/xmlbuild/cpu_rule.json FROM scratch COPY --from=build internal/xmlbuild/cpu_rule.json /app/ 这里的 internal/xmlbuild/cpu_rule.json 会被解析为 /internal/xmlbuild/cpu_rule.json，而不是 /src/internal/xmlbuild/cpu_rule.json，自然找不到文件。\n解决办法是写绝对路径，或带上 WORKDIR 前缀：\n# 方式一：绝对路径 COPY --from=build /src/internal/xmlbuild/cpu_rule.json /app/ # 方式二：相对 WORKDIR 的路径 COPY --from=build src/internal/xmlbuild/cpu_rule.json /app/ 小结 COPY --from=\u0026lt;stage\u0026gt; 的源路径是相对于目标阶段根目录解析的，不是相对于源阶段的 WORKDIR； 跨阶段 COPY 时，务必使用绝对路径或带上 WORKDIR 前缀； 这类问题报错往往很隐晦（\u0026ldquo;No such file or directory\u0026rdquo;），但根因就是路径解析规则。 ","permalink":"/posts/docker-copy-from-path-trap/","summary":"\u003cp\u003e多阶段构建里，\u003ccode\u003eCOPY --from=\u0026lt;stage\u0026gt;\u003c/code\u003e 的源路径并不相对源阶段的 \u003ccode\u003eWORKDIR\u003c/code\u003e 解析，而是相对目标阶段根目录。所以跨阶段 COPY 时，相对路径会指向错误位置，报 \u0026ldquo;No such file or directory\u0026rdquo;。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003eDocker 多阶段构建时，builder 阶段设置了 \u003ccode\u003eWORKDIR /src\u003c/code\u003e，文件实际位于 \u003ccode\u003e/src/internal/xmlbuild/cpu_rule.json\u003c/code\u003e 和 \u003ccode\u003e/src/cmd/configs/compute.yaml\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e但 \u003ccode\u003eCOPY --from=builder\u003c/code\u003e 后，\u003ccode\u003einternal/...\u003c/code\u003e 这种相对路径会被解析为相对于 \u003ccode\u003e/\u003c/code\u003e（即 \u003ccode\u003e/internal/...\u003c/code\u003e），而不是相对于 \u003ccode\u003eWORKDIR /src\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e所以 Docker 找不到文件。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e先确认报错是不是 \u0026ldquo;No such file or directory\u0026rdquo;，且文件确实存在于源阶段。\u003c/p\u003e\n\u003cp\u003e再检查 COPY 的源路径写法，看是否用了相对路径。\u003c/p\u003e\n\u003ch2 id=\"根因\"\u003e根因\u003c/h2\u003e\n\u003cp\u003e多阶段构建中，\u003ccode\u003eCOPY --from=\u0026lt;stage\u0026gt;\u003c/code\u003e 的源路径是相对于\u003cstrong\u003e构建上下文\u003c/strong\u003e或\u003cstrong\u003e目标阶段的根目录\u003c/strong\u003e解析的，而不是相对于源阶段的 \u003ccode\u003eWORKDIR\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e也就是说，\u003ccode\u003eCOPY --from=builder\u003c/code\u003e 之后，\u003cstrong\u003e就不能用上一个阶段的相对路径了\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e比如你写：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-dockerfile\" data-lang=\"dockerfile\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eFROM\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003ebuilder\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003eAS\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003ebuild\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eWORKDIR\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e/src\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c\"\u003e# ... 构建，生成 /src/internal/xmlbuild/cpu_rule.json\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eFROM\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003escratch\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eCOPY\u003c/span\u003e --from\u003cspan class=\"o\"\u003e=\u003c/span\u003ebuild internal/xmlbuild/cpu_rule.json /app/\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这里的 \u003ccode\u003einternal/xmlbuild/cpu_rule.json\u003c/code\u003e 会被解析为 \u003ccode\u003e/internal/xmlbuild/cpu_rule.json\u003c/code\u003e，而不是 \u003ccode\u003e/src/internal/xmlbuild/cpu_rule.json\u003c/code\u003e，自然找不到文件。\u003c/p\u003e","title":"为什么 COPY --from 找不到文件？相对路径不相对 WORKDIR"},{"content":"虚拟机无响应，virsh console 卡住进不去。一个思路是重启后快速进入 console，看内核启动日志，往往能直接暴露根因。\n现象 虚拟机无响应，virsh console 卡在下面这个地方：\nvirsh console vm-example Connected to domain vm-example Escape character is ^] 排查思路 virsh console 卡在这地方，分两步：\n多等一会看看，能否进去，可能是启动慢； 等几分钟还不行，那就是有问题了。 一个思路是重新启动，然后快速的进入 virsh console，可能能看到 kernel 启动日志，可能有线索：\nvirsh destroy vm-example; virsh create vm-example.xml virsh console vm-example 比如这里就打印了日志：\n[ 2.313329][ T1] Run /init as init process Fatal glibc error: CPU does not support x86-64-v2 # 这里就是关键点，cpu 不支持 x86-64-v2 [ 2.315336][ T1] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00007f00 [ 2.316635][ T1] CPU: 0 PID: 1 Comm: init Not tainted 6.6.0-28.0.0.34.oe2403.x86_64 #1 [ 2.317855][ T1] Hardware name: QingCloud E3, BIOS rel-1.12.1-0-ga5cab58e9a3f-prebuilt.qemu.org 04/01/2014 [ 2.319339][ T1] Call Trace: 根因 日志里 Fatal glibc error: CPU does not support x86-64-v2 就是关键线索——CPU 不支持 x86-64-v2 指令集，导致 init 进程直接 panic。\n小结 virsh console 卡住，先等一会，可能是启动慢； 等几分钟还不行，重启后快速进入 console，看内核启动日志； 启动日志往往能直接暴露根因（比如这里就是 CPU 指令集不支持）； 这个案例和\u0026quot;现代 OS 不支持默认 qemu 指令集\u0026quot;是同一个根因，只是排查入口不同。 ","permalink":"/posts/vm-no-response-restart-console/","summary":"\u003cp\u003e虚拟机无响应，\u003ccode\u003evirsh console\u003c/code\u003e 卡住进不去。一个思路是重启后快速进入 console，看内核启动日志，往往能直接暴露根因。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e虚拟机无响应，\u003ccode\u003evirsh console\u003c/code\u003e 卡在下面这个地方：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evirsh console vm-example\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eConnected to domain vm-example\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eEscape character is ^\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"排查思路\"\u003e排查思路\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003evirsh console\u003c/code\u003e 卡在这地方，分两步：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e多等一会看看\u003c/strong\u003e，能否进去，可能是启动慢；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e等几分钟还不行\u003c/strong\u003e，那就是有问题了。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e一个思路是\u003cstrong\u003e重新启动，然后快速的进入 \u003ccode\u003evirsh console\u003c/code\u003e，可能能看到 kernel 启动日志，可能有线索\u003c/strong\u003e：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evirsh destroy vm-example\u003cspan class=\"p\"\u003e;\u003c/span\u003e virsh create vm-example.xml\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evirsh console vm-example\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e比如这里就打印了日志：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    2.313329\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e Run /init as init process\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eFatal glibc error: CPU does not support x86-64-v2    \u003cspan class=\"c1\"\u003e# 这里就是关键点，cpu 不支持 x86-64-v2\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    2.315336\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e Kernel panic - not syncing: Attempted to \u003cspan class=\"nb\"\u003ekill\u003c/span\u003e init! \u003cspan class=\"nv\"\u003eexitcode\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e0x00007f00\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    2.316635\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e CPU: \u003cspan class=\"m\"\u003e0\u003c/span\u003e PID: \u003cspan class=\"m\"\u003e1\u003c/span\u003e Comm: init Not tainted 6.6.0-28.0.0.34.oe2403.x86_64 \u003cspan class=\"c1\"\u003e#1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    2.317855\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e Hardware name: QingCloud E3, BIOS rel-1.12.1-0-ga5cab58e9a3f-prebuilt.qemu.org 04/01/2014\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    2.319339\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e Call Trace:\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"根因\"\u003e根因\u003c/h2\u003e\n\u003cp\u003e日志里 \u003ccode\u003eFatal glibc error: CPU does not support x86-64-v2\u003c/code\u003e 就是关键线索——CPU 不支持 x86-64-v2 指令集，导致 init 进程直接 panic。\u003c/p\u003e","title":"虚拟机无响应？重启后快速进 console 看内核日志"},{"content":"LXC 销毁后，hyper 内核报 unregister_netdevice 网卡错误，困扰了很久。根因是网卡 down 后 IPv6 协议栈仍持有 netdevice 引用，在 hyper 中加一个 disable_ipv6=1 的操作就好了。\n现象 LXC 销毁后，hyper 内核报网卡错误 unregister_netdevice，这个问题困扰了很久没有解决。\n按道理，应该是清理网卡之前，清理相关资源。但我们的程序是直接 down 网卡。\n后来有人在网卡 down 后加了一个清理 ipv6 neigh 的操作，然后就好了。卧槽。\n排查 先看接口没 down 的时候，比如 eth0 上有 neigh：\nroot@host:~# ip n s 10.0.0.252 dev eth1 lladdr 36:95:2f:f5:af:85 STALE 10.0.0.1 dev eth0 lladdr 02:54:d7:af:4b:cb STALE 10.0.0.253 dev eth1 lladdr 52:54:d7:c6:e8:47 STALE fe80::5054:d7ff:fec6:e847 dev eth1 lladdr 52:54:d7:c6:e8:47 router STALE fd00:2024:822:0:167:ffff:ffff:fffd dev eth1 lladdr 52:54:d7:c6:e8:47 router STALE fe80::e01e:53ff:fe85:5ca0 dev eth0 lladdr 02:54:d7:af:4b:cb router STALE 把接口 eth0 down 掉：\nroot@host:~# ip link set dev eth0 down ; ip n s 10.0.0.252 dev eth1 lladdr 36:95:2f:f5:af:85 STALE 10.0.0.253 dev eth1 lladdr 52:54:d7:c6:e8:47 STALE fe80::5054:d7ff:fec6:e847 dev eth1 lladdr 52:54:d7:c6:e8:47 router STALE fd00:2024:822:0:167:ffff:ffff:fffd dev eth1 lladdr 52:54:d7:c6:e8:47 router STALE 已经看不到 eth0 的 neigh 了。\n尝试的修复 一开始加的是清理 neigh 的操作：\ndef clear_ip_neigh(instance_id): cmd = \u0026#34;ip neigh show | grep -E \u0026#39;%s\u0026#39;\u0026#34; % \u0026#39;|\u0026#39;.join(hwaddrs) ret = exec_cmd(cmd) ip_neighs = [] ... # 网卡 down 掉之后应该就执行不到下面，但是问题就是好了，这也太牛逼了。 for ip_neigh in ip_neighs: exec_cmd(\u0026#34;ip neigh del %s\u0026#34; % ip_neigh) 神奇呀。但最后发现，生效的可能不是上面这段，而是下面这段：\ndef let_iface_degraded(instance_id): \u0026#34;\u0026#34;\u0026#34; let interfaces degraded in the local host. \u0026#34;\u0026#34;\u0026#34; ifaces_in_host = get_interfaces_in_host(instance_id) # 1. set dev down for iface in ifaces_in_host: exec_cmd(\u0026#34;ip link set dev %s down;\u0026#34; % iface) # 2. disable ipv6 for iface in ifaces_in_host: exec_cmd(\u0026#34;sysctl -w net.ipv6.conf.%s.disable_ipv6=1;\u0026#34; % iface) return 重点是：加了一个 disable_ipv6 的操作，是在 hyper 中执行的。\ndisable_ipv6=1 才是针对性的关键修复。\n根因 问题出在 unregister_netdevice 的竞态窗口：\nLXC stop 时，容器网络命名空间（netns）在销毁前，veth pair 的宿主侧接口上 IPv6 协议栈仍在运行，可能还在：\n响应 Router Solicitation / Router Advertisement 消息 进行 Duplicate Address Detection (DAD) 检测 持有未清理的 IPv6 neighbor entries 此时如果内核正在执行 unregister_netdevice，而 IPv6 stack 仍然持有对该 netdevice 的引用，就会触发类似：\nunregister_netdevice: waiting for eth0 to become free. Usage count = 1 小结 清理网卡前，要先清理相关资源（neigh、IPv6 协议栈等）； unregister_netdevice 的竞态窗口是根因，IPv6 协议栈持有 netdevice 引用导致无法释放； 关键修复是 disable_ipv6=1，在 hyper 中执行； 底层操作往往\u0026quot;不讲道理\u0026quot;，多尝试认知之外的事情，往往有意外收获。 ","permalink":"/posts/cleanup-netdev-before-down/","summary":"\u003cp\u003eLXC 销毁后，hyper 内核报 \u003ccode\u003eunregister_netdevice\u003c/code\u003e 网卡错误，困扰了很久。根因是网卡 down 后 IPv6 协议栈仍持有 netdevice 引用，在 hyper 中加一个 \u003ccode\u003edisable_ipv6=1\u003c/code\u003e 的操作就好了。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003eLXC 销毁后，hyper 内核报网卡错误 \u003ccode\u003eunregister_netdevice\u003c/code\u003e，这个问题困扰了很久没有解决。\u003c/p\u003e\n\u003cp\u003e按道理，应该是\u003cstrong\u003e清理网卡之前，清理相关资源\u003c/strong\u003e。但我们的程序是直接 \u003ccode\u003edown\u003c/code\u003e 网卡。\u003c/p\u003e\n\u003cp\u003e后来有人在网卡 \u003ccode\u003edown\u003c/code\u003e 后加了一个清理 ipv6 neigh 的操作，然后就好了。卧槽。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e先看接口没 down 的时候，比如 \u003ccode\u003eeth0\u003c/code\u003e 上有 neigh：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eroot@host:~# ip n s\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e10.0.0.252 dev eth1 lladdr 36:95:2f:f5:af:85 STALE\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e10.0.0.1 dev eth0 lladdr 02:54:d7:af:4b:cb STALE\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e10.0.0.253 dev eth1 lladdr 52:54:d7:c6:e8:47 STALE\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efe80::5054:d7ff:fec6:e847 dev eth1 lladdr 52:54:d7:c6:e8:47 router STALE\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efd00:2024:822:0:167:ffff:ffff:fffd dev eth1 lladdr 52:54:d7:c6:e8:47 router STALE\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003efe80::e01e:53ff:fe85:5ca0 dev eth0 lladdr 02:54:d7:af:4b:cb router STALE\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e把接口 \u003ccode\u003eeth0\u003c/code\u003e down 掉：\u003c/p\u003e","title":"清理网卡前先清理相关资源：unregister_netdevice 的坑"},{"content":"新系统跑不起来，报 \u0026ldquo;CPU does not support x86-64-v2\u0026rdquo;，根因是现代 OS 默认按较新指令集编译，而默认的 qemu64 模型只提供最基础指令。给虚拟机配上合适的 CPU 指令集就能解决。\n现象 一台 primusos2403sp1x64 的虚拟机，没有配置 CPU 指令集的情况下，运行僵死。配置指令集后，就能正常运行了。\n排查 用 virsh reset vm-example \u0026amp;\u0026amp; virsh console vm-example 可以看到 glibc 打印的错误：\n[ 1.536627][ T1] Run /init as init process Fatal glibc error: CPU does not support x86-64-v2 [ 1.538855][ T1] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00007f00 [ 1.540681][ T1] CPU: 6 PID: 1 Comm: init Not tainted 6.6.0-103.0.0.106.oe2403sp1.x86_64 #1 [ 1.542633][ T1] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.12.1-0-ga5cab58e9a3f-prebuilt.qemu.org 04/01/2014 [ 1.545203][ T1] Call Trace: [ 1.545959][ T1] \u0026lt;TASK\u0026gt; [ 1.546568][ T1] dump_stack_lvl+0x32/0x50 [ 1.547593][ T1] panic+0x304/0x340 [ 1.548411][ T1] do_exit+0x3f4/0x410 [ 1.549284][ T1] do_group_exit+0x2d/0x80 [ 1.550272][ T1] __x64_sys_exit_group+0x14/0x20 [ 1.551151][ T1] do_syscall_64+0x55/0x100 [ 1.551963][ T1] entry_SYSCALL_64_after_hwframe+0x78/0xe2 [ 1.553370][ T1] RIP: 0033:0x7f40667d3db5 [ 1.557729][ T1] RSP: 002b:00007ffcb3db2f68 EFLAGS: 00000202 ORIG_RAX: 00000000000000e7 [ 1.559088][ T1] RAX: ffffffffffffffda RBX: 00007f40667cf490 RCX: 00007f40667d3db5 [ 1.560108][ T1] RDX: 0000000000000000 RSI: 00007ffcb3db2630 RDI: 000000000000007f [ 1.561078][ T1] RBP: 00007ffcb3db3140 R08: 0000000000000010 R09: 0000000000000000 [ 1.562048][ T1] R10: 0000000000000001 R11: 0000000000000202 R12: 00007f40667e2ab0 [ 1.563025][ T1] R13: 0000000000000000 R14: 0000000000000000 R15: 00007f40667b4000 [ 1.563990][ T1] \u0026lt;/TASK\u0026gt; [ 1.564790][ T1] Kernel Offset: 0x38c00000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff) [ 1.566210][ T1] ---[ end Kernel panic - not syncing: Attempted to kill init! exitcode=0x00007f00 ]--- 根因 这是已知问题：现代操作系统都开启了指令集优化，纯 qemu 的模拟指令无法满足运行的基本要求，必须配上指令集才能运行。\n也可能是我们的 qemu 版本太低了。操作系统是 24 年的，但 qemu 版本还是多年前的 6.2，而最新版本已经是 10.x 了。\n先解释报错里的 x86-64-v2：\nx86-64-v2 是 GCC 对 x86-64 架构的扩展，要求支持较新的 CPU 指令（如 SSE3、SSSE3、SSE4.1 等）。\n较新的 glibc 版本（如 2.34 或更高）可能默认编译为支持 x86-64-v2，而不再兼容旧的 x86-64 CPU。\n如果您的 CPU 较旧（如第一代 x86-64 CPU），可能无法运行这些程序。\n再看为什么 qemu64 不扩展指令集。查看最新的 qemu 代码，qemu64 其实也没有新的指令扩展。而且 qemu64 就是为了兼容性设计的，不会轻易扩展。\n所以问题就变成了：现代操作系统（尤其是 24 年之后的新系统）默认按较新的指令集编译，而默认的 qemu64 模型为了兼容老 CPU 只提供最基础的指令，两者不匹配，系统自然起不来。\n小结 现代 OS 默认开启指令集优化，纯 qemu 模拟指令无法满足运行要求； 遇到新系统跑不起来，优先检查是否配置了合适的 CPU 指令集； qemu64 为了兼容性不会轻易扩展指令，这是设计使然，不是 bug。 ","permalink":"/posts/modern-os-need-cpu-model/","summary":"\u003cp\u003e新系统跑不起来，报 \u0026ldquo;CPU does not support x86-64-v2\u0026rdquo;，根因是现代 OS 默认按较新指令集编译，而默认的 qemu64 模型只提供最基础指令。给虚拟机配上合适的 CPU 指令集就能解决。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e一台 \u003ccode\u003eprimusos2403sp1x64\u003c/code\u003e 的虚拟机，没有配置 CPU 指令集的情况下，运行僵死。配置指令集后，就能正常运行了。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e用 \u003ccode\u003evirsh reset vm-example \u0026amp;\u0026amp; virsh console vm-example\u003c/code\u003e 可以看到 glibc 打印的错误：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.536627\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e Run /init as init process\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eFatal glibc error: CPU does not support x86-64-v2\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.538855\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e Kernel panic - not syncing: Attempted to \u003cspan class=\"nb\"\u003ekill\u003c/span\u003e init! \u003cspan class=\"nv\"\u003eexitcode\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e0x00007f00\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.540681\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e CPU: \u003cspan class=\"m\"\u003e6\u003c/span\u003e PID: \u003cspan class=\"m\"\u003e1\u003c/span\u003e Comm: init Not tainted 6.6.0-103.0.0.106.oe2403sp1.x86_64 \u003cspan class=\"c1\"\u003e#1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.542633\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e Hardware name: QEMU Standard PC \u003cspan class=\"o\"\u003e(\u003c/span\u003ei440FX + PIIX, 1996\u003cspan class=\"o\"\u003e)\u003c/span\u003e, BIOS rel-1.12.1-0-ga5cab58e9a3f-prebuilt.qemu.org 04/01/2014\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.545203\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e Call Trace:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.545959\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e  \u0026lt;TASK\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.546568\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e  dump_stack_lvl+0x32/0x50\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.547593\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e  panic+0x304/0x340\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.548411\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e  do_exit+0x3f4/0x410\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.549284\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e  do_group_exit+0x2d/0x80\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.550272\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e  __x64_sys_exit_group+0x14/0x20\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.551151\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e  do_syscall_64+0x55/0x100\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.551963\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e  entry_SYSCALL_64_after_hwframe+0x78/0xe2\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.553370\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e RIP: 0033:0x7f40667d3db5\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.557729\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e RSP: 002b:00007ffcb3db2f68 EFLAGS: \u003cspan class=\"m\"\u003e00000202\u003c/span\u003e ORIG_RAX: 00000000000000e7\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.559088\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e RAX: ffffffffffffffda RBX: 00007f40667cf490 RCX: 00007f40667d3db5\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.560108\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e RDX: \u003cspan class=\"m\"\u003e0000000000000000\u003c/span\u003e RSI: 00007ffcb3db2630 RDI: 000000000000007f\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.561078\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e RBP: 00007ffcb3db3140 R08: \u003cspan class=\"m\"\u003e0000000000000010\u003c/span\u003e R09: \u003cspan class=\"m\"\u003e0000000000000000\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.562048\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e R10: \u003cspan class=\"m\"\u003e0000000000000001\u003c/span\u003e R11: \u003cspan class=\"m\"\u003e0000000000000202\u003c/span\u003e R12: 00007f40667e2ab0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.563025\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e R13: \u003cspan class=\"m\"\u003e0000000000000000\u003c/span\u003e R14: \u003cspan class=\"m\"\u003e0000000000000000\u003c/span\u003e R15: 00007f40667b4000\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.563990\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e  \u0026lt;/TASK\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.564790\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e Kernel Offset: 0x38c00000 from 0xffffffff81000000 \u003cspan class=\"o\"\u003e(\u003c/span\u003erelocation range: 0xffffffff80000000-0xffffffffbfffffff\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e    1.566210\u003cspan class=\"o\"\u003e][\u003c/span\u003e    T1\u003cspan class=\"o\"\u003e]\u003c/span\u003e ---\u003cspan class=\"o\"\u003e[\u003c/span\u003e end Kernel panic - not syncing: Attempted to \u003cspan class=\"nb\"\u003ekill\u003c/span\u003e init! \u003cspan class=\"nv\"\u003eexitcode\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e0x00007f00 \u003cspan class=\"o\"\u003e]\u003c/span\u003e---\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"根因\"\u003e根因\u003c/h2\u003e\n\u003cp\u003e这是已知问题：\u003cstrong\u003e现代操作系统都开启了指令集优化\u003c/strong\u003e，纯 qemu 的\u003cstrong\u003e模拟指令\u003c/strong\u003e无法满足运行的基本要求，必须配上指令集才能运行。\u003c/p\u003e","title":"为什么新系统跑不起来？默认 qemu 指令集不够用"},{"content":"热迁移后虚拟机卡死、CPU 100%，往往是时间处理出了问题。用 crash 看 dump 能确认死循环卡在内核路径，再用 perf 在虚拟机内和宿主机上对比，就能定位到时间中断处理异常。\n现象 把一台虚拟机从 Ubuntu 20 宿主机热迁移到 Ubuntu 22 宿主机后，虚拟机卡死了。\n从宿主机看，virsh migrate 是成功的，虚拟机处于 running 状态，qemu 进程也正常。但被迁移的虚拟机 CPU 占用 100%，完全无响应。\n排查 用 crash 分析 virsh dump 出来的内存转储，发现内核一直在刷下面的报错，看起来是死循环卡在了调度进程 swapper 上：\n[7686175795.936645] Call Trace: [7686175795.936647] \u0026lt;IRQ\u0026gt; [7686175795.936651] run_timer_softirq+0x2a/0x50 [7686175795.936655] __do_softirq+0xd1/0x2c1 [7686175795.936658] irq_exit+0xae/0xb0 [7686175795.936660] smp_apic_timer_interrupt+0x7b/0x140 [7686175795.936662] apic_timer_interrupt+0xf/0x20 [7686175795.936662] \u0026lt;/IRQ\u0026gt; [7686175795.936664] RIP: 0010:native_safe_halt+0xe/0x10 [7686175823.935201] watchdog: BUG: soft lockup - CPU#0 stuck for 23s! [swapper/0:0] 0 号进程就是 swapper 进程，也就是调度进程。\n这种 CPU 100% 表现出来的卡死，必然是因为某种原因让 CPU 进入了死循环——CPU 被占住放不出来，无法执行其他进程，所以表现出来就是\u0026quot;卡死\u0026quot;。\n这种问题必须用 perf 来分析，分别在虚拟机内和宿主机上进行分析对比：\n# 虚拟机内 perf record -e msr:write_msr -g -a perf script 根因 大概原因是：\n虚拟机没配置 CPU 的情况下，是从一个精度很低的时间设备上维持高精度计时器； 如果虚拟机配置了 CPU，则是通过特定的指令集 rdtsc 指令从物理 CPU 读取计数器。 在热迁移过程中，时间处理这方面存在问题，导致迁移完成后，虚拟机卡死在时间中断处理流程上。\n小结 迁移和扩容，时间处理都是常出问题的地方。\n这类问题排查的关键在于：\n用 crash 分析 dump，确认是不是死循环、卡在哪个内核路径上； 用 perf 在虚拟机内和宿主机上对比分析，定位时间处理相关的异常； 热迁移涉及 CPU 状态、时间源、中断等底层细节，任何一个环节不一致都可能引发迁移后的\u0026quot;假死\u0026quot;。 ","permalink":"/posts/migrate-vm-hang-cpu-100/","summary":"\u003cp\u003e热迁移后虚拟机卡死、CPU 100%，往往是时间处理出了问题。用 crash 看 dump 能确认死循环卡在内核路径，再用 perf 在虚拟机内和宿主机上对比，就能定位到时间中断处理异常。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e把一台虚拟机从 Ubuntu 20 宿主机热迁移到 Ubuntu 22 宿主机后，虚拟机卡死了。\u003c/p\u003e\n\u003cp\u003e从宿主机看，\u003ccode\u003evirsh migrate\u003c/code\u003e 是成功的，虚拟机处于 \u003ccode\u003erunning\u003c/code\u003e 状态，qemu 进程也正常。但被迁移的虚拟机 CPU 占用 \u003cstrong\u003e100%\u003c/strong\u003e，完全无响应。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e用 \u003ccode\u003ecrash\u003c/code\u003e 分析 \u003ccode\u003evirsh dump\u003c/code\u003e 出来的内存转储，发现内核一直在刷下面的报错，看起来是死循环卡在了调度进程 \u003ccode\u003eswapper\u003c/code\u003e 上：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e7686175795.936645\u003cspan class=\"o\"\u003e]\u003c/span\u003e Call Trace:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e7686175795.936647\u003cspan class=\"o\"\u003e]\u003c/span\u003e  \u0026lt;IRQ\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e7686175795.936651\u003cspan class=\"o\"\u003e]\u003c/span\u003e  run_timer_softirq+0x2a/0x50\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e7686175795.936655\u003cspan class=\"o\"\u003e]\u003c/span\u003e  __do_softirq+0xd1/0x2c1\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e7686175795.936658\u003cspan class=\"o\"\u003e]\u003c/span\u003e  irq_exit+0xae/0xb0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e7686175795.936660\u003cspan class=\"o\"\u003e]\u003c/span\u003e  smp_apic_timer_interrupt+0x7b/0x140\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e7686175795.936662\u003cspan class=\"o\"\u003e]\u003c/span\u003e  apic_timer_interrupt+0xf/0x20\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e7686175795.936662\u003cspan class=\"o\"\u003e]\u003c/span\u003e  \u0026lt;/IRQ\u0026gt;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e7686175795.936664\u003cspan class=\"o\"\u003e]\u003c/span\u003e RIP: 0010:native_safe_halt+0xe/0x10\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003e7686175823.935201\u003cspan class=\"o\"\u003e]\u003c/span\u003e watchdog: BUG: soft lockup - CPU#0 stuck \u003cspan class=\"k\"\u003efor\u003c/span\u003e 23s! \u003cspan class=\"o\"\u003e[\u003c/span\u003eswapper/0:0\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e0 号进程就是 \u003ccode\u003eswapper\u003c/code\u003e 进程，也就是调度进程。\u003c/p\u003e\n\u003cp\u003e这种 CPU 100% 表现出来的卡死，必然是因为某种原因让 CPU 进入了死循环——CPU 被占住放不出来，无法执行其他进程，所以表现出来就是\u0026quot;卡死\u0026quot;。\u003c/p\u003e","title":"热迁移后虚拟机卡死：CPU 100% 的时间中断死循环"},{"content":"存量 VM 不切换，我们可以提供一个接口来调整虚拟机的 cpu_mode 来实现。这样长运行的逻辑简单，而调整和长运行逻辑分离，降低复杂性。\n核心：长运行与临时调整分离 存量 VM 不切换。我们可以提供一个接口来调整虚拟机的 cpu_mode 来实现。\n这样长运行的逻辑简单，而调整和长运行逻辑分离，降低复杂性。\n一定要区分长运行，有些需求确实是真需求，但不属于长运行逻辑。\n比如存储虚拟机的切换。\n我们太多控制开关，就是为了做这些东西，本来是一个脚本能批量处理的事，结果把逻辑搞的异常复杂。\n一个例子 对，有个接口层的。\n我们一个查询接口很慢，最后排查发现这个查询接口里一行一行的在更新数据。\n追溯发现，是增加了某个字段，这个字段在后面的流程里比较重要，且不能简单的设置某个值。\n所以在查询的时候，通过运算实时赋值。简直了。\n大哥，你弄个脚本全部跑一遍就行了啊，升级的时候执行这个脚本啊。\n小结 长运行与临时调整要分离； 存量 VM 不切换，提供一个接口来调整，长运行的逻辑简单； 一定要区分长运行，有些需求是真需求，但不属于长运行逻辑； 批量、整体操作，用脚本跑一遍就行，不要把逻辑搞的异常复杂； 不要在查询接口里一行一行的更新数据。 ","permalink":"/posts/long-running-vs-temp-adjust/","summary":"\u003cp\u003e存量 VM 不切换，我们可以提供一个接口来调整虚拟机的 cpu_mode 来实现。这样长运行的逻辑简单，而调整和长运行逻辑分离，降低复杂性。\u003c/p\u003e\n\u003ch2 id=\"核心长运行与临时调整分离\"\u003e核心：长运行与临时调整分离\u003c/h2\u003e\n\u003cp\u003e存量 VM 不切换。我们可以提供一个接口来调整虚拟机的 cpu_mode 来实现。\u003c/p\u003e\n\u003cp\u003e这样长运行的逻辑简单，而调整和长运行逻辑分离，降低复杂性。\u003c/p\u003e\n\u003cp\u003e一定要区分长运行，有些需求确实是真需求，但不属于长运行逻辑。\u003c/p\u003e\n\u003cp\u003e比如存储虚拟机的切换。\u003c/p\u003e\n\u003cp\u003e我们太多控制开关，就是为了做这些东西，本来是一个脚本能批量处理的事，结果把逻辑搞的异常复杂。\u003c/p\u003e\n\u003ch2 id=\"一个例子\"\u003e一个例子\u003c/h2\u003e\n\u003cp\u003e对，有个接口层的。\u003c/p\u003e\n\u003cp\u003e我们一个查询接口很慢，最后排查发现这个查询接口里一行一行的在更新数据。\u003c/p\u003e\n\u003cp\u003e追溯发现，是增加了某个字段，这个字段在后面的流程里比较重要，且不能简单的设置某个值。\u003c/p\u003e\n\u003cp\u003e所以在查询的时候，通过运算实时赋值。简直了。\u003c/p\u003e\n\u003cp\u003e大哥，你弄个脚本全部跑一遍就行了啊，升级的时候执行这个脚本啊。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e长运行与临时调整要分离；\u003c/li\u003e\n\u003cli\u003e存量 VM 不切换，提供一个接口来调整，长运行的逻辑简单；\u003c/li\u003e\n\u003cli\u003e一定要区分长运行，有些需求是真需求，但不属于长运行逻辑；\u003c/li\u003e\n\u003cli\u003e批量、整体操作，用脚本跑一遍就行，不要把逻辑搞的异常复杂；\u003c/li\u003e\n\u003cli\u003e不要在查询接口里一行一行的更新数据。\u003c/li\u003e\n\u003c/ul\u003e","title":"长运行与临时调整要分离"},{"content":"架构决定下限，代码决定上限。虽然代码影响很大，但是整体的稳定性，依然是架构占大部分决定因素。linux 网络栈足够复杂，但又有足够的扩展性，且非常的高性能，为什么？因为它流水线 + sk_buff。\n核心：架构决定稳定性 架构决定下限，代码决定上限。\n虽然代码影响很大，但是整体的稳定性，依然是架构占大部分决定因素。\nlinux 网络栈（sk_buff），足够复杂，但又有足够的扩展性，且非常的高性能。\n它确很稳定，为什么？\n因为他流水线 + sk_buff：\n它的核心对象，是高内聚的、面向对象的； 它子模块是松耦合的，通过核心对象来协作。 我们的 iaas 流程呢？ 没有核心对象，全是临时去数据库查，临时到数据库更新，临时到 hyper 获取环境信息。\n修改也是修改很细微的变量，而不是一个从一而终的对象。\n这种构成了各种千丝万缕的关系，且环节之间直接调用，相互耦合。\n缺乏一个解耦式的流水线的设计。\n即便不是 k8s 那种声明式的架构，即便是流程式的、面向过程的架构，也不至于这么乱呀。\n最简单的解法 最简单的，主机是一个核心对象，从创建开始就确定了的，后续就不要再改了。\nvolume 是核心对象，和主机绑在一起。再加个网络核心对象，和主机绑定。\n全流程从头到尾，都操作这三个核心对象，且通过责任链的方式，A 环节操作完了，B 环节。\n这样每个环节是解耦的，他们不关心上下环节，只关心核心对象。\n这种简单、稳定多了呀。\nweb 框架 gin，也是这样呀，大家都操作一个 context 对象，这样各个处理模块互不耦合，且扩展性好。\n小结 架构决定下限，代码决定上限； 整体的稳定性，依然是架构占大部分决定因素； linux 网络栈稳定，因为它流水线 + sk_buff，核心对象高内聚，子模块松耦合； 我们的 iaas 流程没有核心对象，全是临时去数据库查、临时更新，构成千丝万缕的关系； 主机、volume、网络是核心对象，全流程操作这三个核心对象，通过责任链的方式，每个环节解耦。 ","permalink":"/posts/architecture-determines-stability/","summary":"\u003cp\u003e架构决定下限，代码决定上限。虽然代码影响很大，但是整体的稳定性，依然是架构占大部分决定因素。linux 网络栈足够复杂，但又有足够的扩展性，且非常的高性能，为什么？因为它流水线 + sk_buff。\u003c/p\u003e\n\u003ch2 id=\"核心架构决定稳定性\"\u003e核心：架构决定稳定性\u003c/h2\u003e\n\u003cp\u003e架构决定下限，代码决定上限。\u003c/p\u003e\n\u003cp\u003e虽然代码影响很大，但是整体的稳定性，依然是架构占大部分决定因素。\u003c/p\u003e\n\u003cp\u003elinux 网络栈（sk_buff），足够复杂，但又有足够的扩展性，且非常的高性能。\u003c/p\u003e\n\u003cp\u003e它确很稳定，为什么？\u003c/p\u003e\n\u003cp\u003e因为他\u003cstrong\u003e流水线 + sk_buff\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e它的核心对象，是高内聚的、面向对象的；\u003c/li\u003e\n\u003cli\u003e它子模块是松耦合的，通过核心对象来协作。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"我们的-iaas-流程呢\"\u003e我们的 iaas 流程呢？\u003c/h2\u003e\n\u003cp\u003e没有核心对象，全是临时去数据库查，临时到数据库更新，临时到 hyper 获取环境信息。\u003c/p\u003e\n\u003cp\u003e修改也是修改很细微的变量，而不是一个从一而终的对象。\u003c/p\u003e\n\u003cp\u003e这种构成了各种千丝万缕的关系，且环节之间直接调用，相互耦合。\u003c/p\u003e\n\u003cp\u003e缺乏一个\u003cstrong\u003e解耦式的流水线的设计\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e即便不是 k8s 那种声明式的架构，即便是流程式的、面向过程的架构，也不至于这么乱呀。\u003c/p\u003e\n\u003ch2 id=\"最简单的解法\"\u003e最简单的解法\u003c/h2\u003e\n\u003cp\u003e最简单的，主机是一个核心对象，从创建开始就确定了的，后续就不要再改了。\u003c/p\u003e\n\u003cp\u003evolume 是核心对象，和主机绑在一起。再加个网络核心对象，和主机绑定。\u003c/p\u003e\n\u003cp\u003e全流程从头到尾，都操作这三个核心对象，且通过责任链的方式，A 环节操作完了，B 环节。\u003c/p\u003e\n\u003cp\u003e这样每个环节是解耦的，他们不关心上下环节，只关心核心对象。\u003c/p\u003e\n\u003cp\u003e这种简单、稳定多了呀。\u003c/p\u003e\n\u003cp\u003eweb 框架 gin，也是这样呀，大家都操作一个 context 对象，这样各个处理模块互不耦合，且扩展性好。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e架构决定下限，代码决定上限；\u003c/li\u003e\n\u003cli\u003e整体的稳定性，依然是架构占大部分决定因素；\u003c/li\u003e\n\u003cli\u003elinux 网络栈稳定，因为它流水线 + sk_buff，核心对象高内聚，子模块松耦合；\u003c/li\u003e\n\u003cli\u003e我们的 iaas 流程没有核心对象，全是临时去数据库查、临时更新，构成千丝万缕的关系；\u003c/li\u003e\n\u003cli\u003e主机、volume、网络是核心对象，全流程操作这三个核心对象，通过责任链的方式，每个环节解耦。\u003c/li\u003e\n\u003c/ul\u003e","title":"架构决定稳定性：核心对象 + 流水线"},{"content":"很多时候讨论方案，\u0026ldquo;这个改起来简单\u0026rdquo;，然后就没有人说话了。简单没有错，问题是简单就是指这一次改动的简单呢？那运维的简单性、扩展的简单性、稳定的简单性、排查的简单性呢？\n现象 很多时候讨论方案，\u0026ldquo;这个改起来简单\u0026rdquo;，然后就没有人说话了。\n因为这东西真有点无从反驳。\n改起来简单，风险就低，你怎么反驳，又不是你改，又不是你背锅。\n包括网络上有一些图，基本表达是\u0026quot;能跑就别动\u0026quot;。\n但是这玩意真的对吗？\n有些时候不是跑的问题，是业务发展、竞品拼杀、商业博弈，必须有做一些特性和功能，这个时候是必须要动。\n简单不是唯一标准 动不是问题，问题是怎么动，衡量标准是什么。\n简单没有错，问题是简单就是指这一次改动的简单呢？\n那运维的简单性呢？扩展的简单性呢？稳定的简单性呢？排查的简单性呢？这些都不谈？\n比如要对接第三方备份厂商，居然有其他厂商入侵到我们计算节点，并且到我们计算节点删除 img 的操作方案。\n可能这次是简单了，那下次呢？下个备份厂商呢？\n包括备份对接 s3 接口，多好的需求，符合生态，非常标准化。\n结果硬是做成了非标能力，绑定存储，绑定各种特定流程，非得模仿老功能，依赖中转节点。\n难道就不能从计算节点直接上传 s3？\n数不胜数的例子，这类问题，基本都是当时评审的时候\u0026quot;这个方案改动小\u0026quot;导致的。\n这类问题真无解 这类问题其实真无解，就看 leader 的品味。\n因为其他队员发言是没有权威的，文无第一。\n尤其是\u0026quot;改动小\u0026quot;，比如你的方案实现了从计算节点直接上传备份到 s3，并且是标准 s3 接口，以后不做改动对接任何备份厂商。\n很好吧，所有人都知道这是对的，但是不会有人主动去赞同，因为改动大，得背锅。\n这类问题短期没有人在意，但是时间长了，赚钱的部门、商业线第一个跳出来：\n\u0026ldquo;你们写的代码怎么这么多限制，为什么别人支持，我们不支持，很奇怪。为什么这个支持 s3，那个不支持，很奇怪。\u0026rdquo;\n不过到这依然没有结论，依然看 leader 的品味，看老板的品味。\n小结 简单是唯一标准吗？不是； 简单没有错，问题是简单就是指这一次改动的简单呢； 还要考虑运维的简单性、扩展的简单性、稳定的简单性、排查的简单性； 这类问题真无解，就看 leader 的品味； 所有人都知道是对的，但不会有人主动去赞同，因为改动大，得背锅。 ","permalink":"/posts/is-simple-the-only-criterion/","summary":"\u003cp\u003e很多时候讨论方案，\u0026ldquo;这个改起来简单\u0026rdquo;，然后就没有人说话了。简单没有错，问题是简单就是指这一次改动的简单呢？那运维的简单性、扩展的简单性、稳定的简单性、排查的简单性呢？\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e很多时候讨论方案，\u0026ldquo;这个改起来简单\u0026rdquo;，然后就没有人说话了。\u003c/p\u003e\n\u003cp\u003e因为这东西真有点无从反驳。\u003c/p\u003e\n\u003cp\u003e改起来简单，风险就低，你怎么反驳，又不是你改，又不是你背锅。\u003c/p\u003e\n\u003cp\u003e包括网络上有一些图，基本表达是\u0026quot;能跑就别动\u0026quot;。\u003c/p\u003e\n\u003cp\u003e但是这玩意真的对吗？\u003c/p\u003e\n\u003cp\u003e有些时候不是跑的问题，是业务发展、竞品拼杀、商业博弈，必须有做一些特性和功能，这个时候是必须要动。\u003c/p\u003e\n\u003ch2 id=\"简单不是唯一标准\"\u003e简单不是唯一标准\u003c/h2\u003e\n\u003cp\u003e动不是问题，问题是怎么动，衡量标准是什么。\u003c/p\u003e\n\u003cp\u003e简单没有错，问题是简单就是指这一次改动的简单呢？\u003c/p\u003e\n\u003cp\u003e那运维的简单性呢？扩展的简单性呢？稳定的简单性呢？排查的简单性呢？这些都不谈？\u003c/p\u003e\n\u003cp\u003e比如要对接第三方备份厂商，居然有其他厂商入侵到我们计算节点，并且到我们计算节点删除 img 的操作方案。\u003c/p\u003e\n\u003cp\u003e可能这次是简单了，那下次呢？下个备份厂商呢？\u003c/p\u003e\n\u003cp\u003e包括备份对接 s3 接口，多好的需求，符合生态，非常标准化。\u003c/p\u003e\n\u003cp\u003e结果硬是做成了非标能力，绑定存储，绑定各种特定流程，非得模仿老功能，依赖中转节点。\u003c/p\u003e\n\u003cp\u003e难道就不能从计算节点直接上传 s3？\u003c/p\u003e\n\u003cp\u003e数不胜数的例子，这类问题，基本都是当时评审的时候\u0026quot;这个方案改动小\u0026quot;导致的。\u003c/p\u003e\n\u003ch2 id=\"这类问题真无解\"\u003e这类问题真无解\u003c/h2\u003e\n\u003cp\u003e这类问题其实真无解，就看 leader 的品味。\u003c/p\u003e\n\u003cp\u003e因为其他队员发言是没有权威的，文无第一。\u003c/p\u003e\n\u003cp\u003e尤其是\u0026quot;改动小\u0026quot;，比如你的方案实现了从计算节点直接上传备份到 s3，并且是标准 s3 接口，以后不做改动对接任何备份厂商。\u003c/p\u003e\n\u003cp\u003e很好吧，所有人都知道这是对的，但是不会有人主动去赞同，因为改动大，得背锅。\u003c/p\u003e\n\u003cp\u003e这类问题短期没有人在意，但是时间长了，赚钱的部门、商业线第一个跳出来：\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;你们写的代码怎么这么多限制，为什么别人支持，我们不支持，很奇怪。为什么这个支持 s3，那个不支持，很奇怪。\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e不过到这依然没有结论，依然看 leader 的品味，看老板的品味。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e简单是唯一标准吗？不是；\u003c/li\u003e\n\u003cli\u003e简单没有错，问题是简单就是指这一次改动的简单呢；\u003c/li\u003e\n\u003cli\u003e还要考虑运维的简单性、扩展的简单性、稳定的简单性、排查的简单性；\u003c/li\u003e\n\u003cli\u003e这类问题真无解，就看 leader 的品味；\u003c/li\u003e\n\u003cli\u003e所有人都知道是对的，但不会有人主动去赞同，因为改动大，得背锅。\u003c/li\u003e\n\u003c/ul\u003e","title":"简单是唯一标准吗？"},{"content":"拿快照来说，为啥我们支持快照的多链这么复杂？按道理虚拟机只需要一个卷，至于这个卷背后的链是完全不需要关心的。如果计算存储分离，快照局限于存储模块，计算只需要一个卷。\n现象：快照的多链为什么这么复杂 拿快照来说，为啥我们支持快照的多链这么复杂？\n按道理虚拟机只需要一个卷，至于这个卷背后的链是完全不需要关心的。\n但是放到我们代码，复杂无比，即便是单链也异常复杂，多链简直就做不下去了。\n如果分开，不过多复杂的快照链，只需要在快照模块处理。\n但是我们现在的代码，各种功能和存储都在一起：\n开关机要管理快照链（不是调接口，而是一行一行的命令呀）； 克隆要管理快照链； 迁移对快照链的操作更多了； 回滚快照、打快照，那更复杂了（快照的管理，一行一行的命令，元数据的修改，都在各种流程中交错）。简直了！ 为什么这么难改 你可能会说，改呀，重构呀。\n几十万行的屎山，牵一发动全身。不是一个人就能改的了的。\n而且你改的过程中，别人还在持续扩展新功能，别人要怎么配合你的修改，都是很难的事。\n这种推动，一定是自上而下的。不是说一人之力不能改，而是没人敢让你上。\n得需要资源做 QA 大规模测试，还需要新扩展能力的配合，还需要领导有魄力，胆大心细。\n执行人能做到心细，但是执行人胆大没用，别人怕出了问题你承担不了呀，得别人背锅。\n需要决策人有魄力，自顶向下的推动。\n但是如果老板或决策者离一线（售后、研发）远了，很可能不看重这些。\n决策者大概率觉得，能跑就行，技术是工具，能满足客户需求即可。\n但实际是，很多客户因为稳定性而流失，稳定性是因为屎山代码、架构问题、代码问题。\n但是这些东西不能给财务数据带来改善，销售才能改善财务数据，所以继续怼销售提的需求，继续挖坑，继续流失客户。\n根因：快照和计算绑定 就是因为我们的快照的处理、链的管理，都在计算里面。\n因为我们主要用的 qcow2，qcow2 的快照性能高呀。\n但是它有个问题，qcow2 的快照是和计算绑定的。\n它为啥性能高，因为它需要计算层联动切换读写头。\n如果要计算存储分离，就需要存储提供一种快照模式，不切换读写头，就能做快照。\nlvm 是一种方式，bitmap，但是它做快照后写性能很低。\n不考虑性能的因素下，计算、存储分离，极大降低快照开发的复杂性。\n快照局限于存储模块，甚至是存储模块的快照子模块。计算只需要一个卷，不管其背后的链。\n快照模块如何实现多链 A-\u0026gt;B-\u0026gt;C-\u0026gt; active\n回滚到 A 后，A-\u0026gt;active。\n这个时候，要实现 B、C 的保留，且不被影响，也不影响别人，保留且独立。\n如果是 qcow2 快照，那么天然是独立的，保留卷、保留链的数据关系即可； 如果是 lvm 这种，每个快照本身是独立的，保留即可，保留链关系元数据即可。 归根到底，链关系好处理，复杂性还是在存储端，性能和空间的取舍。\n数据表的设计也简单，就是一个以原卷为根的一个树结构。\n小结 边界：计算、存储、网络要分离； 按道理虚拟机只需要一个卷，至于这个卷背后的链是完全不需要关心的； 计算存储分离，极大降低快照开发的复杂性； 快照局限于存储模块，计算只需要一个卷，不管其背后的链； 几十万行的屎山，牵一发动全身，这种推动一定是自上而下的。 ","permalink":"/posts/boundary-compute-storage-network/","summary":"\u003cp\u003e拿快照来说，为啥我们支持快照的多链这么复杂？按道理虚拟机只需要一个卷，至于这个卷背后的链是完全不需要关心的。如果计算存储分离，快照局限于存储模块，计算只需要一个卷。\u003c/p\u003e\n\u003ch2 id=\"现象快照的多链为什么这么复杂\"\u003e现象：快照的多链为什么这么复杂\u003c/h2\u003e\n\u003cp\u003e拿快照来说，为啥我们支持快照的多链这么复杂？\u003c/p\u003e\n\u003cp\u003e按道理虚拟机只需要一个卷，至于这个卷背后的链是完全不需要关心的。\u003c/p\u003e\n\u003cp\u003e但是放到我们代码，复杂无比，即便是单链也异常复杂，多链简直就做不下去了。\u003c/p\u003e\n\u003cp\u003e如果分开，不过多复杂的快照链，只需要在快照模块处理。\u003c/p\u003e\n\u003cp\u003e但是我们现在的代码，各种功能和存储都在一起：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e开关机要管理快照链（不是调接口，而是一行一行的命令呀）；\u003c/li\u003e\n\u003cli\u003e克隆要管理快照链；\u003c/li\u003e\n\u003cli\u003e迁移对快照链的操作更多了；\u003c/li\u003e\n\u003cli\u003e回滚快照、打快照，那更复杂了（快照的管理，一行一行的命令，元数据的修改，都在各种流程中交错）。简直了！\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"为什么这么难改\"\u003e为什么这么难改\u003c/h2\u003e\n\u003cp\u003e你可能会说，改呀，重构呀。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e几十万行的屎山，牵一发动全身\u003c/strong\u003e。不是一个人就能改的了的。\u003c/p\u003e\n\u003cp\u003e而且你改的过程中，别人还在持续扩展新功能，别人要怎么配合你的修改，都是很难的事。\u003c/p\u003e\n\u003cp\u003e这种推动，一定是\u003cstrong\u003e自上而下\u003c/strong\u003e的。不是说一人之力不能改，而是没人敢让你上。\u003c/p\u003e\n\u003cp\u003e得需要资源做 QA 大规模测试，还需要新扩展能力的配合，还需要领导有魄力，胆大心细。\u003c/p\u003e\n\u003cp\u003e执行人能做到心细，但是执行人胆大没用，别人怕出了问题你承担不了呀，得别人背锅。\u003c/p\u003e\n\u003cp\u003e需要决策人有魄力，自顶向下的推动。\u003c/p\u003e\n\u003cp\u003e但是如果老板或决策者离一线（售后、研发）远了，很可能不看重这些。\u003c/p\u003e\n\u003cp\u003e决策者大概率觉得，能跑就行，技术是工具，能满足客户需求即可。\u003c/p\u003e\n\u003cp\u003e但实际是，很多客户因为稳定性而流失，稳定性是因为屎山代码、架构问题、代码问题。\u003c/p\u003e\n\u003cp\u003e但是这些东西不能给财务数据带来改善，销售才能改善财务数据，所以继续怼销售提的需求，继续挖坑，继续流失客户。\u003c/p\u003e\n\u003ch2 id=\"根因快照和计算绑定\"\u003e根因：快照和计算绑定\u003c/h2\u003e\n\u003cp\u003e就是因为我们的快照的处理、链的管理，都在计算里面。\u003c/p\u003e\n\u003cp\u003e因为我们主要用的 qcow2，qcow2 的快照性能高呀。\u003c/p\u003e\n\u003cp\u003e但是它有个问题，qcow2 的快照是和计算绑定的。\u003c/p\u003e\n\u003cp\u003e它为啥性能高，因为它需要计算层联动\u003cstrong\u003e切换读写头\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e如果要计算存储分离，就需要存储提供一种快照模式，不切换读写头，就能做快照。\u003c/p\u003e\n\u003cp\u003elvm 是一种方式，bitmap，但是它做快照后写性能很低。\u003c/p\u003e\n\u003cp\u003e不考虑性能的因素下，计算、存储分离，极大降低快照开发的复杂性。\u003c/p\u003e\n\u003cp\u003e快照局限于存储模块，甚至是存储模块的快照子模块。计算只需要一个卷，不管其背后的链。\u003c/p\u003e\n\u003ch2 id=\"快照模块如何实现多链\"\u003e快照模块如何实现多链\u003c/h2\u003e\n\u003cp\u003eA-\u0026gt;B-\u0026gt;C-\u0026gt; active\u003c/p\u003e\n\u003cp\u003e回滚到 A 后，A-\u0026gt;active。\u003c/p\u003e\n\u003cp\u003e这个时候，要实现 B、C 的保留，且不被影响，也不影响别人，保留且独立。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e如果是 qcow2 快照，那么天然是独立的，保留卷、保留链的数据关系即可；\u003c/li\u003e\n\u003cli\u003e如果是 lvm 这种，每个快照本身是独立的，保留即可，保留链关系元数据即可。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e归根到底，链关系好处理，复杂性还是在存储端，性能和空间的取舍。\u003c/p\u003e\n\u003cp\u003e数据表的设计也简单，就是一个以原卷为根的一个树结构。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e边界：计算、存储、网络要分离；\u003c/li\u003e\n\u003cli\u003e按道理虚拟机只需要一个卷，至于这个卷背后的链是完全不需要关心的；\u003c/li\u003e\n\u003cli\u003e计算存储分离，极大降低快照开发的复杂性；\u003c/li\u003e\n\u003cli\u003e快照局限于存储模块，计算只需要一个卷，不管其背后的链；\u003c/li\u003e\n\u003cli\u003e几十万行的屎山，牵一发动全身，这种推动一定是自上而下的。\u003c/li\u003e\n\u003c/ul\u003e","title":"边界：计算、存储、网络要分离"},{"content":"设计模式一个基本点两个原则：一个基本点是找到变化、封装变化；两个原则是基于接口而不是基于实现、组合优于继承。核心是可变与不可变的隔离、解耦。\n核心：找到变化，封装变化 找到变化，封装变化。\n易变与不易变的隔离，可变与不可变的隔离，稳定与不稳定的隔离。\n设计模式一个基本点两个原则：\n一个基本点：find what varies and then encapsulate it。找到变化、封装变化； 两个原则：基于接口，而不是基于实现；组合优于继承。 找到变化、封装变化：核心是可变与不可变的隔离、解耦。开闭原则也有这个思想。\n所有的扩展性都是因为变化，分解可变与不可变作为一个基本点，没毛病。\n基于接口，其实目的也是为了组合 基于接口，其实本质也是为了解耦可变与稳定，接口应该是稳定的，实现是易变的。\n组合优于继承，因为继承是紧耦合的。\n组合更利于可变，组合的前提是抽出来不可变的接口，即封装可变与不可变。\n接口和组合，其实都是可变与不可变的实践。\n通过不可变的组合实现可变。\n一个例子：iaas 的调度方案 存储组，会选一个默认存储池，它会影响调度结果，所以这个也应该做到调度里。\n这是一个很容易进入误区的陷阱。\n实际上上层很多东西都会影响到底层呀，因为底层接收的是参数，上层参数变了，底层结果当然也会变。这不能作为划分边界的依据呀。\n比如调解手机声音，不能说，腾讯音乐能调整，所以我要把腾讯音乐做到手机的扬声器模块。\n我们调度，肯定是一个原子调度模块，我们提供参数，上层传参，控制调度结果。\n我们提供各种单一的原子的能力，上层通过组合这些原子能力实现多样化可扩展的调度。\n比如选择存储池，底层原子接口只接受存储池参数，不应该帮去改变或者帮他去选择。这是单一的原子的。如果加了选择的逻辑，就不单一了呀。\n同时，底层应该是不易变的。\n自动选择就是一个易变的逻辑，不同客户需求不同。但是根据存储池调度，这个是稳定的。\n所以，我们只需要接受存储池参数即可。至于这个参数怎么玩出花，是上层的事。\n所以存储组交给上层服务是 ok 的。\n分层，哪些是产品层，哪些是应用层，哪些是基础层，边界。\n基础层必然是稳定的、原子的、提供基础能力，这就是边界。\n小结 找到变化，封装变化； 设计模式一个基本点两个原则：找到变化封装变化、基于接口而非实现、组合优于继承； 核心是可变与不可变的隔离、解耦； 接口应该是稳定的，实现是易变的； 通过不可变的组合实现可变； 基础层必然是稳定的、原子的、提供基础能力，这就是边界。 ","permalink":"/posts/variable-vs-invariable/","summary":"\u003cp\u003e设计模式一个基本点两个原则：一个基本点是找到变化、封装变化；两个原则是基于接口而不是基于实现、组合优于继承。核心是可变与不可变的隔离、解耦。\u003c/p\u003e\n\u003ch2 id=\"核心找到变化封装变化\"\u003e核心：找到变化，封装变化\u003c/h2\u003e\n\u003cp\u003e找到变化，封装变化。\u003c/p\u003e\n\u003cp\u003e易变与不易变的隔离，可变与不可变的隔离，稳定与不稳定的隔离。\u003c/p\u003e\n\u003cp\u003e设计模式一个基本点两个原则：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e一个基本点：find what varies and then encapsulate it。\u003cstrong\u003e找到变化、封装变化\u003c/strong\u003e；\u003c/li\u003e\n\u003cli\u003e两个原则：\u003cstrong\u003e基于接口，而不是基于实现；组合优于继承\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e找到变化、封装变化：核心是\u003cstrong\u003e可变与不可变的隔离、解耦\u003c/strong\u003e。开闭原则也有这个思想。\u003c/p\u003e\n\u003cp\u003e所有的扩展性都是因为变化，分解可变与不可变作为一个基本点，没毛病。\u003c/p\u003e\n\u003ch2 id=\"基于接口其实目的也是为了组合\"\u003e基于接口，其实目的也是为了组合\u003c/h2\u003e\n\u003cp\u003e基于接口，其实本质也是为了解耦\u003cstrong\u003e可变与稳定，接口应该是稳定的，实现是易变的\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e组合优于继承，因为继承是紧耦合的。\u003c/p\u003e\n\u003cp\u003e组合更利于可变，组合的前提是抽出来不可变的接口，即封装可变与不可变。\u003c/p\u003e\n\u003cp\u003e接口和组合，其实都是可变与不可变的实践。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e通过不可变的组合实现可变。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"一个例子iaas-的调度方案\"\u003e一个例子：iaas 的调度方案\u003c/h2\u003e\n\u003cp\u003e存储组，会选一个默认存储池，它会影响调度结果，所以这个也应该做到调度里。\u003c/p\u003e\n\u003cp\u003e这是一个很容易进入误区的陷阱。\u003c/p\u003e\n\u003cp\u003e实际上上层很多东西都会影响到底层呀，因为底层接收的是参数，上层参数变了，底层结果当然也会变。这不能作为划分边界的依据呀。\u003c/p\u003e\n\u003cp\u003e比如调解手机声音，不能说，腾讯音乐能调整，所以我要把腾讯音乐做到手机的扬声器模块。\u003c/p\u003e\n\u003cp\u003e我们调度，肯定是一个原子调度模块，我们提供参数，上层传参，控制调度结果。\u003c/p\u003e\n\u003cp\u003e我们提供各种单一的原子的能力，上层通过组合这些原子能力实现多样化可扩展的调度。\u003c/p\u003e\n\u003cp\u003e比如选择存储池，底层原子接口只接受存储池参数，不应该帮去改变或者帮他去选择。这是单一的原子的。如果加了选择的逻辑，就不单一了呀。\u003c/p\u003e\n\u003cp\u003e同时，底层应该是\u003cstrong\u003e不易变\u003c/strong\u003e的。\u003c/p\u003e\n\u003cp\u003e自动选择就是一个易变的逻辑，不同客户需求不同。但是根据存储池调度，这个是稳定的。\u003c/p\u003e\n\u003cp\u003e所以，我们只需要接受存储池参数即可。至于这个参数怎么玩出花，是上层的事。\u003c/p\u003e\n\u003cp\u003e所以存储组交给上层服务是 ok 的。\u003c/p\u003e\n\u003cp\u003e分层，哪些是产品层，哪些是应用层，哪些是基础层，边界。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e基础层必然是稳定的、原子的、提供基础能力\u003c/strong\u003e，这就是边界。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e找到变化，封装变化；\u003c/li\u003e\n\u003cli\u003e设计模式一个基本点两个原则：找到变化封装变化、基于接口而非实现、组合优于继承；\u003c/li\u003e\n\u003cli\u003e核心是可变与不可变的隔离、解耦；\u003c/li\u003e\n\u003cli\u003e接口应该是稳定的，实现是易变的；\u003c/li\u003e\n\u003cli\u003e通过不可变的组合实现可变；\u003c/li\u003e\n\u003cli\u003e基础层必然是稳定的、原子的、提供基础能力，这就是边界。\u003c/li\u003e\n\u003c/ul\u003e","title":"可变与不可变：找到变化，封装变化"},{"content":"《UNIX编程艺术》里讲的：cmd + 管道 + 文本的设计理念，让单个命令足够简单清晰，而命令的编排又可以创造无限扩展的能力。单元简单，才容易组合。\n核心：简单 + 组合才是最强的 《UNIX编程艺术》里面讲的：\ncmd + 管道 + 文本的设计理念，让单个命令足够简单清晰，而命令的编排又可以创造无限扩展的能力。\n单元简单，才容易组合； 单元复杂，组合困难； 世界复杂，业务多元，提供简单的单元，让业务按照自己的需求自己组合，这才是终极形态。 真实世界也是这样，原子、质子，组合成万物。\n小结 简单 + 组合才是最强的； cmd + 管道 + 文本，让单个命令足够简单清晰，而命令的编排又可以创造无限扩展的能力； 单元简单，才容易组合；单元复杂，组合困难； 提供简单的单元，让业务按照自己的需求自己组合，这才是终极形态。 ","permalink":"/posts/simple-plus-combination/","summary":"\u003cp\u003e《UNIX编程艺术》里讲的：cmd + 管道 + 文本的设计理念，让单个命令足够简单清晰，而命令的编排又可以创造无限扩展的能力。单元简单，才容易组合。\u003c/p\u003e\n\u003ch2 id=\"核心简单--组合才是最强的\"\u003e核心：简单 + 组合才是最强的\u003c/h2\u003e\n\u003cp\u003e《UNIX编程艺术》里面讲的：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ecmd + 管道 + 文本的设计理念，让单个命令足够简单清晰，而命令的编排又可以创造无限扩展的能力。\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e单元简单，才容易组合；\u003c/li\u003e\n\u003cli\u003e单元复杂，组合困难；\u003c/li\u003e\n\u003cli\u003e世界复杂，业务多元，提供简单的单元，让业务按照自己的需求自己组合，这才是终极形态。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e真实世界也是这样，原子、质子，组合成万物。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e简单 + 组合才是最强的；\u003c/li\u003e\n\u003cli\u003ecmd + 管道 + 文本，让单个命令足够简单清晰，而命令的编排又可以创造无限扩展的能力；\u003c/li\u003e\n\u003cli\u003e单元简单，才容易组合；单元复杂，组合困难；\u003c/li\u003e\n\u003cli\u003e提供简单的单元，让业务按照自己的需求自己组合，这才是终极形态。\u003c/li\u003e\n\u003c/ul\u003e","title":"简单 + 组合，才是最强的"},{"content":"稳定性相关的，要么不做，要么就要完善，要不然做了高可用，反而造成雪崩。高可用必备 fence，fence 就是 ipmi 强制关机，这是最稳妥的方式。\n核心：高可用要么不做，要么做完善 稳定性相关的，要么不做，要么就要完善。\n要不然做了高可用，反而造成雪崩。\n比如一个 hyper 负载很高，本来可能 hyper 上个别业务有影响，结果触发灾难迁移，然后又迁移失败，反而导致多业务受影响，且恢复 2 天。哪个客户能接受！\n比如我们的灾难，触发了，迁移到一半又发现空间不足，迁移失败。就是不完整嘛，能不能迁移，要提前规划好，不能迁移就不要动 hyper。告警通知给用户，用户自己处理。\n半死不活的状态，客户更希望给他们通知，他们自己处理，而不是我们下线 hyper。\n灾难当然复杂，尤其是半死不活的状态，就是薛定谔的猫，这种确实不好处理。\n所以更需要多方参照开源系统的设计，看他们背后的考量和策略，结合我们自己的业务来系统性的设计，而不是打补丁式的缝缝补补。\n比如增加超时时间，比如增加个 ping 检查，比如增加个本地自检。\n高可用必备 fence fence 就是 ipmi 强制关机，这就是最稳妥的方式。\n其他方式都会有各种问题，且会带来其他复杂性。\n比如自检，节点都有异常了，怎么保证自检能正常运行？\n外部观察是通过网络观察的，所有网络如果有问题就会判断为有问题，这种情况自检是正常的，两者如何协作？\n自检和外部检测是独立的，如果要协作就涉及到网络和协同，这两个本身就很复杂，再加上两个复杂的检测。\n如果不协同，保持独立，那么两者的时间差会导致哪些状况？又是复杂场景。\n如果采用集中式观察，采用 fence，这是业界主流的做法。\n状态，状态，状态 这里面的要素是什么？\nnode 和资源，node 和资源的状态，非常非常重要。\n要通过状态在分布式环境下协同控制。\n状态，状态，状态。一定要围绕最终状态，一定要维护好最终状态，一定要确保最终状态的一致性、正确性。\n然后其他机制都围绕最终状态设计。\n这样通过最终状态，来解耦各种前因后果，解耦异常事件和恢复处理。\n为什么这两个解耦非常重要？\n因为异常事件千千万，不同的异常的恢复处理也不同，形成千丝万缕，一个网状复杂性。\n但是通过状态来解耦后，各种异常回归到状态，状态再对应到恢复处理。这是一个港股，两头各种线，线性复杂性。\n这其实又对应到 k8s 的声明式哲学。\n在当前系统已有架构下，核心状态就是两个维度：\nnode，状态：是否正常； vm，状态：属于哪个 node，是否应该运行。 node 可能无法期望，因为硬件可能客观因素故障。vm 是有期望状态的，比如期望是运行，实际是关机的。\n小结 高可用要么不做，要么做完善，要不然做了高可用反而造成雪崩； 能不能迁移要提前规划好，不能迁移就不要动 hyper； 高可用必备 fence，fence 就是 ipmi 强制关机，这是最稳妥的方式； 状态，状态，状态，一定要围绕最终状态，维护好最终状态的一致性、正确性； 通过最终状态来解耦异常事件和恢复处理，把网状复杂性变成线性复杂性。 ","permalink":"/posts/ha-fence-essential/","summary":"\u003cp\u003e稳定性相关的，要么不做，要么就要完善，要不然做了高可用，反而造成雪崩。高可用必备 fence，fence 就是 ipmi 强制关机，这是最稳妥的方式。\u003c/p\u003e\n\u003ch2 id=\"核心高可用要么不做要么做完善\"\u003e核心：高可用要么不做，要么做完善\u003c/h2\u003e\n\u003cp\u003e稳定性相关的，要么不做，要么就要完善。\u003c/p\u003e\n\u003cp\u003e要不然\u003cstrong\u003e做了高可用，反而造成雪崩\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e比如一个 hyper 负载很高，本来可能 hyper 上个别业务有影响，结果触发灾难迁移，然后又迁移失败，反而导致多业务受影响，且恢复 2 天。哪个客户能接受！\u003c/p\u003e\n\u003cp\u003e比如我们的灾难，触发了，迁移到一半又发现空间不足，迁移失败。就是不完整嘛，能不能迁移，要提前规划好，不能迁移就不要动 hyper。告警通知给用户，用户自己处理。\u003c/p\u003e\n\u003cp\u003e半死不活的状态，客户更希望给他们通知，他们自己处理，而不是我们下线 hyper。\u003c/p\u003e\n\u003cp\u003e灾难当然复杂，尤其是半死不活的状态，就是薛定谔的猫，这种确实不好处理。\u003c/p\u003e\n\u003cp\u003e所以更需要\u003cstrong\u003e多方参照开源系统的设计，看他们背后的考量和策略\u003c/strong\u003e，结合我们自己的业务来系统性的设计，而不是打补丁式的缝缝补补。\u003c/p\u003e\n\u003cp\u003e比如增加超时时间，比如增加个 ping 检查，比如增加个本地自检。\u003c/p\u003e\n\u003ch2 id=\"高可用必备-fence\"\u003e高可用必备 fence\u003c/h2\u003e\n\u003cp\u003efence 就是 ipmi 强制关机，这就是最稳妥的方式。\u003c/p\u003e\n\u003cp\u003e其他方式都会有各种问题，且会带来其他复杂性。\u003c/p\u003e\n\u003cp\u003e比如自检，节点都有异常了，怎么保证自检能正常运行？\u003c/p\u003e\n\u003cp\u003e外部观察是通过网络观察的，所有网络如果有问题就会判断为有问题，这种情况自检是正常的，两者如何协作？\u003c/p\u003e\n\u003cp\u003e自检和外部检测是独立的，如果要协作就涉及到网络和协同，这两个本身就很复杂，再加上两个复杂的检测。\u003c/p\u003e\n\u003cp\u003e如果不协同，保持独立，那么两者的时间差会导致哪些状况？又是复杂场景。\u003c/p\u003e\n\u003cp\u003e如果采用集中式观察，采用 fence，这是业界主流的做法。\u003c/p\u003e\n\u003ch2 id=\"状态状态状态\"\u003e状态，状态，状态\u003c/h2\u003e\n\u003cp\u003e这里面的要素是什么？\u003c/p\u003e\n\u003cp\u003enode 和资源，node 和资源的状态，非常非常重要。\u003c/p\u003e\n\u003cp\u003e要通过状态在分布式环境下协同控制。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e状态，状态，状态。一定要围绕最终状态，一定要维护好最终状态，一定要确保最终状态的一致性、正确性。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e然后其他机制都围绕最终状态设计。\u003c/p\u003e\n\u003cp\u003e这样通过最终状态，来解耦各种前因后果，解耦异常事件和恢复处理。\u003c/p\u003e\n\u003cp\u003e为什么这两个解耦非常重要？\u003c/p\u003e\n\u003cp\u003e因为异常事件千千万，不同的异常的恢复处理也不同，形成千丝万缕，一个网状复杂性。\u003c/p\u003e\n\u003cp\u003e但是通过状态来解耦后，各种异常回归到状态，状态再对应到恢复处理。这是一个港股，两头各种线，线性复杂性。\u003c/p\u003e\n\u003cp\u003e这其实又对应到 k8s 的声明式哲学。\u003c/p\u003e\n\u003cp\u003e在当前系统已有架构下，核心状态就是两个维度：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003enode，状态：是否正常；\u003c/li\u003e\n\u003cli\u003evm，状态：属于哪个 node，是否应该运行。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003enode 可能无法期望，因为硬件可能客观因素故障。vm 是有期望状态的，比如期望是运行，实际是关机的。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e高可用要么不做，要么做完善，要不然做了高可用反而造成雪崩；\u003c/li\u003e\n\u003cli\u003e能不能迁移要提前规划好，不能迁移就不要动 hyper；\u003c/li\u003e\n\u003cli\u003e高可用必备 fence，fence 就是 ipmi 强制关机，这是最稳妥的方式；\u003c/li\u003e\n\u003cli\u003e状态，状态，状态，一定要围绕最终状态，维护好最终状态的一致性、正确性；\u003c/li\u003e\n\u003cli\u003e通过最终状态来解耦异常事件和恢复处理，把网状复杂性变成线性复杂性。\u003c/li\u003e\n\u003c/ul\u003e","title":"高可用要么不做，要么做完善：fence 是必备"},{"content":"状态机的核心是围绕状态，而不是围绕具体流程。当前状态、输入、转移状态，就是围绕着三要素设计的。它可以把一个超长流程、超多条件判断的流程，解耦为一个个独立的状态。\n核心：围绕状态，而不是围绕具体流程 状态机的核心是围绕状态，而不是围绕具体流程。\n当前状态、输入、转移状态，就是围绕着三要素设计的。\n这个模式，可以把一个超长流程、超多条件判断的流程，解耦为一个个独立的状态。\n将时序和条件判断，解耦为独立的状态，状态受输入/事件的影响，转移状态。\n是一种分治，是一种将大的易变的脆弱流程，转换为一个个独立的、不易变的状态处理，也就是状态机。\n状态机不是一种算法，而是一种通过\u0026quot;降维拆解\u0026quot;来控制复杂度的设计哲学。\n它把混沌庞大的系统，解耦成了一个个简单、确定、封闭的小状态。\n状态机不是循环，只是表现像循环。\n我现在是 State A。如果来信号 X，我就变 State B；如果来信号 Y，我就变 State C。（循环只是状态切换过程中产生的一种表现形式）\n状态机的核心四大要素 状态（State）：系统当前所处的情境（如\u0026quot;关门\u0026quot;、\u0026ldquo;开门\u0026rdquo;）； 输入（Input）：触发变化的外部信号（如\u0026quot;按下开门按钮\u0026quot;）； 转移（Transition）：从一个状态变到另一个状态的过程； 输出（Output）：系统在特定状态或转移时执行的动作（如\u0026quot;响铃\u0026quot;）。 状态机和循环流程的区别 状态机和循环流程看起来确实非常像，但它们在设计思想、状态控制权以及处理复杂度的能力上有本质的区别：\n维度 普通循环流程（Loop / Flowchart） 状态机（State Machine） 控制中心 代码行/指令指针（PC）：按预设顺序一行行往下走 \u0026ldquo;状态\u0026quot;本身：当前处于什么状态，直接决定能响应什么信号 执行逻辑 主控驱动：A -\u0026gt; B -\u0026gt; C -\u0026gt; 再循环，路线基本固定 事件驱动：没有固定的下一步，完全看此时来了什么输入 分支处理 靠大量的 if-else 或 switch 嵌套，条件极易膨胀 状态独立隔离，只处理当前状态下合法的事件，拒绝非法输入 记忆能力 依赖全局变量或多层局部变量来标记\u0026quot;走到哪一步了\u0026rdquo; 状态本身就是记忆，当前处于哪个状态，就代表了过去发生的一切 状态机也是解耦 状态机解耦的是：\u0026ldquo;行为（做什么）\u0026ldquo;与\u0026quot;时序/条件判断（什么时候能做）\u0026rdquo;。\n状态机通过把系统拆成\u0026quot;状态\u0026quot;和\u0026quot;事件\u0026rdquo;，实现了以下四个维度的彻底解耦：\n1. 解耦：逻辑与当前上下文（消灭全局 Flag 泥潭） 传统代码（紧耦合）：为了记住系统\u0026quot;走到哪一步了\u0026quot;，你不得不定义一堆全局变量或布尔标志位（Flag）：\nif is_logged_in and not is_paying and is_verified and has_coupon: # 你必须在每一个逻辑里，手动判断这一大串 Flag 的组合 问题：变量越多，组合情况成指数级爆炸（2^n），你根本记不清 is_paying 为 True 时，has_coupon 能不能为 True。\n状态机（解耦）：它把这堆乱七八糟的变量组合，直接抽象成一个确定的状态（如 WaitingPayment）。你不再需要关心过去的变量组合，\u0026ldquo;当前状态\u0026quot;本身就代表了过去发生的一切上下文。\n2. 解耦：各种\u0026quot;状态\u0026quot;之间的业务逻辑（消灭嵌套 if-else） 传统代码（紧耦合）：所有的业务逻辑都写在一个大函数或大循环里。修改\u0026quot;发货\u0026quot;逻辑，一不小心就会把\u0026quot;退款\u0026quot;逻辑改出 Bug，因为它们挤在同一个 switch-case 或 if-else 里； 状态机（解耦）：每个状态都是一个独立自洽的\u0026quot;小王国\u0026rdquo;。PaidState 只管【已支付】时能做什么，ShippedState 只管【已发货】时能做什么。修改\u0026quot;已发货\u0026quot;的代码，物理上不可能影响到\u0026quot;已支付\u0026quot;的代码。这直接满足了软件工程的\u0026quot;单一职责原则\u0026quot;和\u0026quot;开闭原则\u0026quot;。 3. 解耦：事件触发（外部）与动作执行（内部） 传统代码（紧耦合）：\u0026ldquo;用户点击了按钮 → 代码立刻去检查各种条件 → 代码执行扣款 → 代码改写数据库\u0026rdquo;，这一切都强行捆绑在同一个调用链路里； 状态机（解耦）：外部只管发送事件（Event），根本不需要知道内部怎么处理；内部只管根据当前状态（State）响应事件。发送信号的人，和处理信号的人，实现了完全的解耦。 4. 解耦：时间顺序（把\u0026quot;死流程\u0026quot;变成\u0026quot;活响应\u0026quot;） 传统代码（紧耦合）：写死在 while 循环或顺序代码里的逻辑，时间顺序是固定的。一旦遇到用户\u0026quot;跳跃式操作\u0026quot;或\u0026quot;突发中断\u0026quot;，代码就会卡死或崩溃； 状态机（解耦）：它把\u0026quot;时间的先后顺序\u0026quot;解耦成了\u0026quot;状态的跳转关系\u0026quot;。系统不再被死板的时间线绑架，而是静静躺在某个状态里，无论外部什么时候、按什么顺序抛进来事件，它都能按预设的状态图优雅应对。 状态机优势 优势一：真正优雅的状态机，能彻底消除 if-else 用状态模式（State Pattern），把每一个状态变成一个独立的对象。\n极强的扩展性：如果明天要新增一个\u0026quot;等待人脸识别\u0026quot;的状态，你完全不需要修改原有任何代码，只需要新建一个 WaitingFaceState 类接进去； 符合开闭原则：循环流程只要加新需求，就必须去改那个巨大的 while-if-else 函数，极其容易改出 Bug。 优势二：拒绝\u0026quot;非法状态\u0026quot;与\u0026quot;非法操作\u0026quot;（安全性） 在普通的循环或多变量流程中，最头疼的就是变量状态组合爆炸，导致系统进入非法状态。\n状态机直接从数学层面消除了这种可能性：\n状态的排他性：角色在某一时刻只能处于 Jumping、Dead 或 Attacking 中的一种状态； 输入的合法性：处于 Dead（死亡）状态时，任何\u0026quot;按下攻击键\u0026quot;的事件会被直接丢弃或忽略。不需要写庞大的 if (!is_dead) 检查，状态机机制本身就禁止了这个转移。 优势三：状态机可以导出\u0026quot;一张图\u0026quot;（设计与维护） 状态机在写代码之前，必须先画出状态转移图：\n[待支付] ---(用户付款)---\u0026gt; [待发货] ---(商家发货)---\u0026gt; [已发货] | | (超时未付) (用户退款) v v [已取消] [退款处理中] 代码即图纸：产品经理、测试人员、开发人员讨论的是同一张状态图； 死锁/漏洞检测：通过这张图，你能一眼看出\u0026quot;是不是漏掉了一种情况？\u0026quot;。循环流程代码很难做出这种数学级的完备性检查。 当系统变得非常庞大（如 TCP 网络协议、游戏 AI、操作系统内核、订单系统）：\n循环流程会让你陷在无数的标志位（Flag）、全局变量和深层嵌套的 if-else 泥潭里，改一处坏三处； 状态机则是把复杂的业务拆解为\u0026quot;一个个互不干扰的独立状态\u0026quot;，让系统永远在一个可控、已知、安全的状态图里运转。 这就是为什么芯片硬件、TCP 协议、游戏引擎等高度要求稳定性的大型工程，底层清一色全是状态机。\nk8s 声明式也是状态机实现的 Kubernetes（K8s）的声明式 API 本质上就是一个极度庞大的、分布式且最终一致的有限状态机（FSM）。\n声明式（Declarative）只是用户交互的界面方式（用 YAML 描述\u0026quot;我要什么\u0026quot;）；而状态机（State Machine）则是底层硬件与控制系统的实现灵魂（靠控制器不断消除期望与现实的偏差）。\nK8s 的强大，本质上就是用\u0026quot;声明式 API\u0026quot;作为抽象外衣，配合\u0026quot;控制器状态机\u0026quot;作为底层引擎，从而彻底解决了分布式系统极其复杂的自愈与运维问题。\n对比传统的部署脚本（命令式）与 K8s 声明式（状态机）的区别：\n维度 命令式脚本（Imperative / 过程循环） K8s 声明式（Declarative / 状态机） 关注点 关注\u0026quot;过程\u0026quot;：逐步告诉系统\u0026quot;先做 A，再做 B，如果出错就做 C\u0026quot; 关注\u0026quot;状态\u0026quot;：只告诉系统\u0026quot;我不管你怎么弄，最终状态必须是 X\u0026quot; 错误恢复 如果脚本在第 3 步执行到一半卡死，重新运行脚本可能导致重复创建资源或直接报错 无论中途断电还是崩溃，重启后控制器重新对比\u0026quot;当前\u0026quot;与\u0026quot;期望\u0026quot;状态，继续驱动状态转移 幂等性 极难保证。需要写大量的 if (x_exists) 判断 天生幂等。你把同一个 YAML apply 100 次，系统的最终状态永远只有一个 自愈能力 无自愈能力。服务器挂了需要人工重新跑脚本 自我修复。硬件故障导致物理状态偏离期望状态时，状态机自动触发转移回到期望状态 小结 状态机的核心是围绕状态，而不是围绕具体流程； 当前状态、输入、转移状态，就是围绕着三要素设计的； 状态机把超长流程、超多条件判断的流程，解耦为一个个独立的状态； 状态机不是循环，只是表现像循环； 状态机解耦的是\u0026quot;行为\u0026quot;与\u0026quot;时序/条件判断\u0026quot;； k8s 声明式也是状态机实现的，天生幂等、自我修复。 ","permalink":"/posts/state-machine-around-state/","summary":"\u003cp\u003e状态机的核心是围绕状态，而不是围绕具体流程。当前状态、输入、转移状态，就是围绕着三要素设计的。它可以把一个超长流程、超多条件判断的流程，解耦为一个个独立的状态。\u003c/p\u003e\n\u003ch2 id=\"核心围绕状态而不是围绕具体流程\"\u003e核心：围绕状态，而不是围绕具体流程\u003c/h2\u003e\n\u003cp\u003e状态机的核心是\u003cstrong\u003e围绕状态，而不是围绕具体流程\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e当前状态、输入、转移状态\u003c/strong\u003e，就是围绕着三要素设计的。\u003c/p\u003e\n\u003cp\u003e这个模式，可以把一个超长流程、超多条件判断的流程，\u003cstrong\u003e解耦为一个个独立的状态\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e将\u003cstrong\u003e时序和条件判断，解耦为独立的状态\u003c/strong\u003e，状态受输入/事件的影响，转移状态。\u003c/p\u003e\n\u003cp\u003e是一种分治，是一种将大的易变的脆弱流程，转换为一个个独立的、不易变的状态处理，也就是状态机。\u003c/p\u003e\n\u003cp\u003e状态机不是一种算法，而是一种\u003cstrong\u003e通过\u0026quot;降维拆解\u0026quot;来控制复杂度的设计哲学\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e它把混沌庞大的系统，解耦成了一个个简单、确定、封闭的小状态。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e状态机不是循环，只是表现像循环。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e我现在是 State A。如果来信号 X，我就变 State B；如果来信号 Y，我就变 State C。（循环只是状态切换过程中产生的一种表现形式）\u003c/p\u003e\n\u003ch2 id=\"状态机的核心四大要素\"\u003e状态机的核心四大要素\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e状态（State）\u003c/strong\u003e：系统当前所处的情境（如\u0026quot;关门\u0026quot;、\u0026ldquo;开门\u0026rdquo;）；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e输入（Input）\u003c/strong\u003e：触发变化的外部信号（如\u0026quot;按下开门按钮\u0026quot;）；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e转移（Transition）\u003c/strong\u003e：从一个状态变到另一个状态的过程；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e输出（Output）\u003c/strong\u003e：系统在特定状态或转移时执行的动作（如\u0026quot;响铃\u0026quot;）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"状态机和循环流程的区别\"\u003e状态机和循环流程的区别\u003c/h2\u003e\n\u003cp\u003e状态机和循环流程看起来确实非常像，但它们在设计思想、状态控制权以及处理复杂度的能力上有本质的区别：\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e维度\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e普通循环流程（Loop / Flowchart）\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e状态机（State Machine）\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e控制中心\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e代码行/指令指针（PC）：按预设顺序一行行往下走\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026ldquo;状态\u0026quot;本身：当前处于什么状态，直接决定能响应什么信号\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e执行逻辑\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e主控驱动：A -\u0026gt; B -\u0026gt; C -\u0026gt; 再循环，路线基本固定\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e事件驱动：没有固定的下一步，完全看此时来了什么输入\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e分支处理\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e靠大量的 if-else 或 switch 嵌套，条件极易膨胀\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e状态独立隔离，只处理当前状态下合法的事件，拒绝非法输入\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e记忆能力\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e依赖全局变量或多层局部变量来标记\u0026quot;走到哪一步了\u0026rdquo;\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e状态本身就是记忆，当前处于哪个状态，就代表了过去发生的一切\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"状态机也是解耦\"\u003e状态机也是解耦\u003c/h2\u003e\n\u003cp\u003e状态机解耦的是：\u003cstrong\u003e\u0026ldquo;行为（做什么）\u0026ldquo;与\u0026quot;时序/条件判断（什么时候能做）\u0026rdquo;\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e状态机通过把系统拆成\u0026quot;状态\u0026quot;和\u0026quot;事件\u0026rdquo;，实现了以下四个维度的彻底解耦：\u003c/p\u003e\n\u003ch3 id=\"1-解耦逻辑与当前上下文消灭全局-flag-泥潭\"\u003e1. 解耦：逻辑与当前上下文（消灭全局 Flag 泥潭）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e传统代码（紧耦合）\u003c/strong\u003e：为了记住系统\u0026quot;走到哪一步了\u0026quot;，你不得不定义一堆全局变量或布尔标志位（Flag）：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eif is_logged_in and not is_paying and is_verified and has_coupon:\n    # 你必须在每一个逻辑里，手动判断这一大串 Flag 的组合\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e问题：变量越多，组合情况成指数级爆炸（2^n），你根本记不清 is_paying 为 True 时，has_coupon 能不能为 True。\u003c/p\u003e","title":"状态机：围绕状态，而不是围绕具体流程"},{"content":"当流程跨越了多个服务，一旦中途某一步失败，如何把已经扭转的状态干净地恢复原状，是软件架构里最重的活。优秀的架构师会尽量避免物理回滚，把流程设计为只能向前。\n核心：正向流程容易，逆向回滚才是灾难 当流程跨越了多个服务（分布式事务）或者多个本地对象时，一旦中途某一步失败，如何把已经扭转的状态、已经扣减的库存、已经划走的资金\u0026quot;干净地\u0026quot;恢复原状，是软件架构里最重的活。\n一个设计大智慧：在真正的高并发高可用设计中，优秀的架构师会尽量避免\u0026quot;物理回滚\u0026quot;。\n他们会把流程设计为\u0026quot;只能向前\u0026quot;。\n比如扣款失败了，不是去把前面的流程撤销，而是生成一张\u0026quot;退款单\u0026quot;走新的正向流程。\n用正向的业务去解决逆向的错误，这往往是把\u0026quot;面向对象\u0026quot;和\u0026quot;面向流程\u0026quot;结合得最好的艺术。\n业务流失败的逆向控制（回滚） 正向流程容易，逆向清理（异常、回滚）才是分布式与复杂系统里的重活。\n根据对\u0026quot;时序\u0026quot;和\u0026quot;状态\u0026quot;的处理方式不同，业界演进出了两类完全对立的治理哲学：\n1. 命令式哲学：严格的因果追踪（有状态的设计） 传统的工作流（BPMN）或分布式事务（Saga / 2PC）认为\u0026quot;时间过程是可控的\u0026quot;。\n编排式 Saga：由中央指挥官严格记录每一步的进度（Context），成功则前行，失败则按记忆逆向调用补偿接口； 两阶段提交 (2PC)：基础设施层强锁定，追求强一致性。 哲学本质：有状态的设计（Stateful）。 它极度依赖对时间轨迹的记忆。一旦回滚流程自身在网络闪断中崩溃，系统必须设计更复杂的\u0026quot;状态来套娃状态\u0026quot;，容易陷入脆弱的死锁。\n2. 声明式哲学：消除流程的对账自愈（无状态的设计） 以 Kubernetes (K8s) 为代表的现代云原生架构，彻底推翻了\u0026quot;时间流水线\u0026quot;，转向了【声明式 API + 状态机 + 最终一致性】。\n无回滚流：抛弃\u0026quot;先做 A、再做 B、失败了执行 C\u0026quot;的因果控制，直接定义对象的期望状态 (Desired State)； 控制循环 (Control Loop)：无数个健忘但勤奋的 Controller 每一秒都在比对\u0026quot;期望状态\u0026quot;与\u0026quot;实际状态\u0026quot;，计算出 Diff 并发起正向调节（Reconcile）。 哲学本质：无状态的设计（Stateless）。 控制器没有记忆，每一次触发都是纯函数的计算。它用无限重复的\u0026quot;空间对账\u0026quot;，消灭了\u0026quot;时间回滚\u0026quot;的痛苦。\n声明式架构（K8s 风格）的边界与盲区 K8s 的\u0026quot;空间切分（容器化）+ 状态治理（etcd）+ 时间治理（控制循环）\u0026ldquo;在基础设施编排上近乎神作，但这套\u0026quot;无状态对账\u0026quot;并非万灵药。\n声明式对账的天然盲区 当业务打破了\u0026quot;结果可定义、过程不重要、失败可重试、状态可覆盖\u0026quot;的假设时，必须坚决退回到命令式流程：\n强时序/因果链业务（如银行转账）：必须具备完美的隔离性，无法容忍最终一致性走向一致期间的中间不稳定态（防止超卖或负资产）； 非幂等/不可逆操作（如发短信、物理扣费）：发生过就留下痕迹，无法通过\u0026quot;覆盖快照\u0026quot;来抹去，对账重试会导致严重的业务资损； 高频短暂任务（如 Serverless / 50ms 函数）：控制循环的对账代价（数百毫秒）远大于任务执行时间。 动作/流程的\u0026quot;对象化\u0026quot;误区 论点确认：转账、通知、审批、扣费，完全可以（且应该）被抽象为对象（如 DDD 中的交易单实体）； 关键分野：即使将它们抽象为了对象，它们也是\u0026quot;事务型对象\u0026rdquo;，而非 K8s 那样的\u0026quot;资源型对象\u0026quot;。 资源型对象 (Pod/Disk)：行为是\u0026quot;修剪\u0026quot;，多了可以删，少了可以建，允许短暂混乱； 事务型对象 (转账单/发票)：行为是\u0026quot;追加流水\u0026quot;，发生了就永远存在，必须用流水线一样严密的命令式流程（Saga）去呵护它的状态机跃迁。 四种回滚方案 1. 事务补偿模式（Saga Pattern） Saga 是分布式事务中最常用的\u0026quot;最终一致性\u0026quot;方案。它的核心思想是：不追求过程中的完美隔离，允许中间状态可见，但如果失败，就按原路反向执行\u0026quot;补偿操作\u0026quot;。\n方式 A：编排式 Saga (Orchestration) —— 纯粹的【面向流程】\n有一个核心的\u0026quot;流程指挥官\u0026quot;（Orchestrator），它知道整个业务流的每一步，并负责调用各个服务的正向和反向接口。\n如何回滚：流程走到第三步失败了，指挥官收到失败信号，便开始按倒序调用第二步、第一步的补偿接口； 优点：流程一目了然，所有的控制逻辑都在指挥官这里，服务之间没有直接耦合； 缺点：指挥官容易变成一个巨大的、臃肿的\u0026quot;万能类\u0026quot;（God Class），维护成本高。 方式 B：协同式 Saga (Choreography) —— 典型的【面向对象/事件】\n没有中央指挥官。每个服务通过监听事件来决定自己要做什么。\n如何回滚：第一步做完发个\u0026quot;事件A\u0026quot;，第二步监听到\u0026quot;事件A\u0026quot;后执行并发出\u0026quot;事件B\u0026quot;，第三步失败了，发出一个\u0026quot;第三步失败事件\u0026quot;。第一、二步的服务监听到这个失败事件后，自动在各自内部执行回滚； 优点：高度解耦，符合面向对象的\u0026quot;自治\u0026quot;原则，没有单点瓶颈； 缺点：极难调试。你看不到一个完整的\u0026quot;业务流\u0026quot;在哪里，流程淹没在事件的海洋里（常称作\u0026quot;银河系面条代码\u0026quot;）。 2. 可逆状态机模式（State Machine）——【面向对象】的极致 在复杂的业务流中（如订单生命周期），与其用代码去写 if-else 的流程，不如直接定义一个状态机。\n如何回滚：状态机不认为这是\u0026quot;回滚\u0026quot;，它认为这只是状态的又一次转移。例如，\u0026ldquo;已付款\u0026quot;由于某种原因资源扣减失败，状态机显式地定义一条流转路径：已付款 + 资源不足事件 → 退款中/已取消； 优点：极其严密。状态机的每一个状态、每一个变迁都是可控的，杜绝了非法状态； 缺点：如果业务流程经常变，状态机的矩阵图会变得异常复杂，开发和理解成本极高。 3. 两阶段提交模式（2PC / Atomics）——追求【强一致性】 像 Seata 的 AT 模式、或者是数据库底层的 XA 事务。\n如何回滚：第一阶段（Prepare）所有人锁定资源，执行 SQL，但不提交；第二阶段（Commit/Rollback）只要有一个人说\u0026quot;我失败了\u0026rdquo;，全局事务管理器会向所有人发送 Rollback 指令，底层框架利用数据库的反向日志（Undo Log）自动把数据恢复； 优点：业务代码最爽。程序员完全不需要写回滚代码，框架/数据库帮你做了（强一致性）； 缺点：性能极差。整个流程中资源一直被锁定，并发高了系统直接卡死。 4. 定时检查与幂等对账（Reconciliation）——【终极兜底方案】 任何依赖在线流程（无论正向逆向）的系统，都可能因为网络闪断、服务器宕机而导致回滚流程本身也失败（比如补偿接口调不通了）。这时候必须依赖离线对账。\n如何回滚：正向流程失败时，只在数据库记录一个 Status = FAILED_NEED_CLEAN（需要清理）。有一个后台定时任务，每隔 5 分钟捞出这些数据，顽固地、不断重试地去调用清理接口，直到成功为止； 优点：可靠性最高。是分布式系统解决死锁和资源泄漏的最后一道防线； 缺点：非实时，有延迟。数据会出现短暂的不一致。 总结 对象是空间的划分，流程是时间的推进； 管理\u0026quot;资源\u0026quot;时，学 K8s：用无状态的声明式对账去无情地践踏、重建与自愈，用空间消灭时间； 处理\u0026quot;钱财与因果事件\u0026quot;时，学传统金融：用有状态的命令式流程去敬畏每一个步骤，小心翼翼地向前推进或逆向冲正。 两者的完美结合，才是构建抗震、优雅、高可用复杂系统的至高艺术。\n对于强因果、强顺序的业务（如支付、审批）：承认流程的第一地位。在宏观上，用面向对象划分出干净的领域边界；在微观的应用层，用最清晰的面向过程（编排服务）去写正向流，并提供显式的补偿机制（Saga）来处理回滚； 对于重资源、重自愈、生命周期长的业务（如自动化运维、资产同步、多端状态同步）：果断抛弃\u0026quot;流程和回滚\u0026quot;的执念。效仿 K8s，采用声明式设计 + 状态机 + 最终一致性。把流程拆碎，塞进各个 Controller 的肚子里。让系统没有显式的回滚，只有永无止境的\u0026quot;对账自愈\u0026quot;。 小结 正向流程容易，逆向回滚才是灾难； 优秀的架构师会尽量避免物理回滚，把流程设计为只能向前； 用正向的业务去解决逆向的错误； 命令式哲学（Saga/2PC）是有状态的设计，声明式哲学（K8s）是无状态的设计； 资源型对象用声明式对账，事务型对象用命令式流程； 定时检查与幂等对账是终极兜底方案。 ","permalink":"/posts/forward-easy-rollback-hard/","summary":"\u003cp\u003e当流程跨越了多个服务，一旦中途某一步失败，如何把已经扭转的状态干净地恢复原状，是软件架构里最重的活。优秀的架构师会尽量避免物理回滚，把流程设计为只能向前。\u003c/p\u003e\n\u003ch2 id=\"核心正向流程容易逆向回滚才是灾难\"\u003e核心：正向流程容易，逆向回滚才是灾难\u003c/h2\u003e\n\u003cp\u003e当流程跨越了多个服务（分布式事务）或者多个本地对象时，一旦中途某一步失败，如何把已经扭转的状态、已经扣减的库存、已经划走的资金\u0026quot;干净地\u0026quot;恢复原状，是软件架构里最重的活。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e一个设计大智慧：在真正的高并发高可用设计中，优秀的架构师会尽量避免\u0026quot;物理回滚\u0026quot;。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e他们会把流程设计为\u0026quot;只能向前\u0026quot;。\u003c/p\u003e\n\u003cp\u003e比如扣款失败了，不是去把前面的流程撤销，而是生成一张\u0026quot;退款单\u0026quot;走新的正向流程。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e用正向的业务去解决逆向的错误\u003c/strong\u003e，这往往是把\u0026quot;面向对象\u0026quot;和\u0026quot;面向流程\u0026quot;结合得最好的艺术。\u003c/p\u003e\n\u003ch2 id=\"业务流失败的逆向控制回滚\"\u003e业务流失败的逆向控制（回滚）\u003c/h2\u003e\n\u003cp\u003e正向流程容易，逆向清理（异常、回滚）才是分布式与复杂系统里的重活。\u003c/p\u003e\n\u003cp\u003e根据对\u0026quot;时序\u0026quot;和\u0026quot;状态\u0026quot;的处理方式不同，业界演进出了两类完全对立的治理哲学：\u003c/p\u003e\n\u003ch3 id=\"1-命令式哲学严格的因果追踪有状态的设计\"\u003e1. 命令式哲学：严格的因果追踪（有状态的设计）\u003c/h3\u003e\n\u003cp\u003e传统的工作流（BPMN）或分布式事务（Saga / 2PC）认为\u0026quot;时间过程是可控的\u0026quot;。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e编排式 Saga\u003c/strong\u003e：由中央指挥官严格记录每一步的进度（Context），成功则前行，失败则按记忆逆向调用补偿接口；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e两阶段提交 (2PC)\u003c/strong\u003e：基础设施层强锁定，追求强一致性。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e哲学本质：有状态的设计（Stateful）。\u003c/strong\u003e 它极度依赖对时间轨迹的记忆。一旦回滚流程自身在网络闪断中崩溃，系统必须设计更复杂的\u0026quot;状态来套娃状态\u0026quot;，容易陷入脆弱的死锁。\u003c/p\u003e\n\u003ch3 id=\"2-声明式哲学消除流程的对账自愈无状态的设计\"\u003e2. 声明式哲学：消除流程的对账自愈（无状态的设计）\u003c/h3\u003e\n\u003cp\u003e以 Kubernetes (K8s) 为代表的现代云原生架构，彻底推翻了\u0026quot;时间流水线\u0026quot;，转向了【声明式 API + 状态机 + 最终一致性】。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e无回滚流\u003c/strong\u003e：抛弃\u0026quot;先做 A、再做 B、失败了执行 C\u0026quot;的因果控制，直接定义对象的期望状态 (Desired State)；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e控制循环 (Control Loop)\u003c/strong\u003e：无数个健忘但勤奋的 Controller 每一秒都在比对\u0026quot;期望状态\u0026quot;与\u0026quot;实际状态\u0026quot;，计算出 Diff 并发起正向调节（Reconcile）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e哲学本质：无状态的设计（Stateless）。\u003c/strong\u003e 控制器没有记忆，每一次触发都是纯函数的计算。\u003cstrong\u003e它用无限重复的\u0026quot;空间对账\u0026quot;，消灭了\u0026quot;时间回滚\u0026quot;的痛苦。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"声明式架构k8s-风格的边界与盲区\"\u003e声明式架构（K8s 风格）的边界与盲区\u003c/h2\u003e\n\u003cp\u003eK8s 的\u0026quot;空间切分（容器化）+ 状态治理（etcd）+ 时间治理（控制循环）\u0026ldquo;在基础设施编排上近乎神作，但这套\u0026quot;无状态对账\u0026quot;并非万灵药。\u003c/p\u003e\n\u003ch3 id=\"声明式对账的天然盲区\"\u003e声明式对账的天然盲区\u003c/h3\u003e\n\u003cp\u003e当业务打破了\u0026quot;结果可定义、过程不重要、失败可重试、状态可覆盖\u0026quot;的假设时，必须坚决退回到命令式流程：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e强时序/因果链业务（如银行转账）\u003c/strong\u003e：必须具备完美的隔离性，无法容忍最终一致性走向一致期间的中间不稳定态（防止超卖或负资产）；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e非幂等/不可逆操作（如发短信、物理扣费）\u003c/strong\u003e：发生过就留下痕迹，无法通过\u0026quot;覆盖快照\u0026quot;来抹去，对账重试会导致严重的业务资损；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e高频短暂任务（如 Serverless / 50ms 函数）\u003c/strong\u003e：控制循环的对账代价（数百毫秒）远大于任务执行时间。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"动作流程的对象化误区\"\u003e动作/流程的\u0026quot;对象化\u0026quot;误区\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e论点确认\u003c/strong\u003e：转账、通知、审批、扣费，完全可以（且应该）被抽象为对象（如 DDD 中的交易单实体）；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e关键分野\u003c/strong\u003e：即使将它们抽象为了对象，它们也是\u0026quot;事务型对象\u0026rdquo;，而非 K8s 那样的\u0026quot;资源型对象\u0026quot;。\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e资源型对象 (Pod/Disk)\u003c/strong\u003e：行为是\u0026quot;修剪\u0026quot;，多了可以删，少了可以建，允许短暂混乱；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e事务型对象 (转账单/发票)\u003c/strong\u003e：行为是\u0026quot;追加流水\u0026quot;，发生了就永远存在，必须用流水线一样严密的命令式流程（Saga）去呵护它的状态机跃迁。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"四种回滚方案\"\u003e四种回滚方案\u003c/h2\u003e\n\u003ch3 id=\"1-事务补偿模式saga-pattern\"\u003e1. 事务补偿模式（Saga Pattern）\u003c/h3\u003e\n\u003cp\u003eSaga 是分布式事务中最常用的\u0026quot;最终一致性\u0026quot;方案。它的核心思想是：不追求过程中的完美隔离，允许中间状态可见，但如果失败，就按原路反向执行\u0026quot;补偿操作\u0026quot;。\u003c/p\u003e","title":"正向流程容易，逆向回滚才是灾难"},{"content":"流程是易变的、脆弱的，对象是稳定的、高内聚的。面向对象的流程是分治，将易变的流程拆分成多个稳定的对象，对象之间面向接口协作，复杂性拆分成局部复杂性。\n现象 我们 iaas 很多代码都是面向过程，而架构上也有很多是面向过程，而没有面向对象。\n比如创建主机，存储资源没有抽象，全都是在过程中各种条件判断来处理的。其实需要的是一个存储系统、存储对象。\n创建备份也是一样，创建备份和存储备份耦合在一起，没有区分。\n存储备份完全是面向过程，写了两个流程：\nrsync 上传到 snapshot； snapshot 之间复制副本。 其实创建备份和存储备份完全是独立的两个事务，而且是差别很大的两个事务。\n很明显，存储应该是一个独立系统，是一个存储对象。\n我们需要的是面向对象，过程应该是在对象内的。\n核心：对象是骨架，流程是血肉 流程是易变的、脆弱的，编排流程； 对象是稳定的、高内聚的。 最终：\n面向过程的流程，是一个大的、长的、前后依赖的、复杂性指数增加的判断组合； 面向对象的流程，是分治，将易变的、脆弱的，拆分成多个稳定的对象，对象之间面向接口协作，复杂性拆分成局部复杂性，在大流程里是一个线性复杂性，在对象内是常数复杂性。 相比面向过程的流程的指数复杂性，这是数学上可证明的面向对象的优势。\n软件工程设计的目标就是 kiss，一切围绕控制复杂性设计。\n宏观面向对象，微观面向流程 上层的业务流程确实是面向过程的，但它又不负责具体的业务规则，而是编排。\n宏观架构上，要\u0026quot;面向对象\u0026quot;：划分好边界，把复杂的业务拆解成一个个职责明确、内聚的对象，以此来抗住需求的变更和系统的规模； 微观执行上，要\u0026quot;面向流程\u0026quot;：在 Service 层，清晰、直观地把这些对象串联成一条条业务流，满足用户的实际需求。 对象是骨架，流程是血肉。\n面向过程 vs 面向对象 面向过程 优点：逻辑简单直接，符合人类\u0026quot;按步骤做事\u0026quot;的直觉；代码体积小、执行效率高；调试和上手成本低，适合小型程序； 缺点：可维护性差，需求变更时需修改大量相关函数，牵一发动全身；复用性低；扩展性差，难以应对复杂系统的需求迭代。 面向对象 优点：可维护性强，封装使对象内部变化不影响外部，模块化降低耦合；复用性高，继承和多态大幅减少重复代码；扩展性好，新增功能只需定义新类/方法，无需修改原有代码（开闭原则）；更贴合复杂问题的本质； 缺点：复杂度高，需设计类的结构、关系，上手门槛高；性能稍低；过度设计风险，简单问题可能被复杂化。 业务的悖论：用\u0026quot;空间对象\u0026quot;承载\u0026quot;时间流程\u0026quot; \u0026ldquo;业务的本质是流程（时间推进），但为什么现代软件工程推崇对象（空间治理）？\u0026rdquo;\n这是软件设计中最迷人的核心矛盾。\n我们通过 DDD（领域驱动设计）思想完成了这两者的空间与时间平衡：\n宏观架构上（空间划分）——面向对象：划分出干净、内聚的领域模型（如用户、商品、订单）。这些对象是系统的\u0026quot;零件\u0026quot;，负责锁住状态与核心业务规则； 微观执行上（时间推进）——面向流程：在应用服务层，用直观的面向过程逻辑，将这些对象串联成一条条满足用户最终需求的\u0026quot;业务流\u0026quot;。 架构大智慧：对象是骨架（空间），流程是血肉（时间）。 好的代码，是用面向对象的画笔，画出面向过程的蓝图。\n为什么业务是流，我们却要用对象？ 为了应对\u0026quot;变化\u0026quot;和\u0026quot;规模\u0026quot;。\n如果一个业务流程永远不改，面向过程绝对是最高效的。但现实是，业务流程会无限膨胀和变化。\n面向过程的灾难：假设一个\u0026quot;下单流程\u0026quot;，起初只有 3 步。随着业务发展，变成了 30 步，里面充斥着各种 if-else。最后这个流程长达几千行，没人敢改，因为改了第 5 步，可能会调断第 28 步的数据； 面向对象的解法：面向对象不是消灭流程，而是把流程中的\u0026quot;参与者（实体）\u0026ldquo;抽离出来，让它们自己负责自己的事情。下单流程不再是一条黑盒管道，而是变成了用户对象、商品对象、营销引擎对象、库存对象之间的交互。 用一个比喻来理解：\n面向过程是\u0026quot;流水线工厂\u0026rdquo;：传送带上一条道走到黑，一旦产品要加个新功能，整条流水线可能都要停工重组； 面向对象是\u0026quot;现代企业组织\u0026quot;：划分为财务部、销售部、研发部。公司的业务依然是一个个流程，但流程是由各个部门通过协作完成的。如果财务流程变了，只需要财务部内部调整，销售部根本不需要知道。 小结 流程是易变的、脆弱的，对象是稳定的、高内聚的； 面向对象的流程是分治，复杂性拆分成局部复杂性； 面向过程的流程是指数复杂性，面向对象是线性复杂性； 宏观架构上要面向对象，微观执行上要面向流程； 对象是骨架，流程是血肉； 用面向对象的画笔，画出面向过程的蓝图。 ","permalink":"/posts/oop-vs-procedural/","summary":"\u003cp\u003e流程是易变的、脆弱的，对象是稳定的、高内聚的。面向对象的流程是分治，将易变的流程拆分成多个稳定的对象，对象之间面向接口协作，复杂性拆分成局部复杂性。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e我们 iaas 很多代码都是面向过程，而架构上也有很多是面向过程，而没有面向对象。\u003c/p\u003e\n\u003cp\u003e比如创建主机，存储资源没有抽象，全都是在过程中各种条件判断来处理的。其实需要的是一个存储系统、存储对象。\u003c/p\u003e\n\u003cp\u003e创建备份也是一样，创建备份和存储备份耦合在一起，没有区分。\u003c/p\u003e\n\u003cp\u003e存储备份完全是面向过程，写了两个流程：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003ersync 上传到 snapshot；\u003c/li\u003e\n\u003cli\u003esnapshot 之间复制副本。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e其实\u003cstrong\u003e创建备份和存储备份完全是独立的两个事务，而且是差别很大的两个事务\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e很明显，存储应该是一个独立系统，是一个存储对象。\u003c/p\u003e\n\u003cp\u003e我们需要的是\u003cstrong\u003e面向对象，过程应该是在对象内的\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"核心对象是骨架流程是血肉\"\u003e核心：对象是骨架，流程是血肉\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e流程是易变的、脆弱的，编排流程；\u003c/li\u003e\n\u003cli\u003e对象是稳定的、高内聚的。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e最终：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e面向过程\u003c/strong\u003e的流程，是一个大的、长的、前后依赖的、复杂性\u003cstrong\u003e指数增加\u003c/strong\u003e的判断组合；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e面向对象\u003c/strong\u003e的流程，是分治，将易变的、脆弱的，拆分成多个稳定的对象，对象之间面向接口协作，复杂性拆分成局部复杂性，在大流程里是一个\u003cstrong\u003e线性复杂性\u003c/strong\u003e，在对象内是\u003cstrong\u003e常数复杂性\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e相比面向过程的流程的指数复杂性，这是数学上可证明的面向对象的优势。\u003c/p\u003e\n\u003cp\u003e软件工程设计的目标就是 kiss，\u003cstrong\u003e一切围绕控制复杂性设计\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"宏观面向对象微观面向流程\"\u003e宏观面向对象，微观面向流程\u003c/h2\u003e\n\u003cp\u003e上层的业务流程确实是面向过程的，但它又不负责具体的业务规则，而是编排。\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e宏观架构上，要\u0026quot;面向对象\u0026quot;\u003c/strong\u003e：划分好边界，把复杂的业务拆解成一个个职责明确、内聚的对象，以此来抗住需求的变更和系统的规模；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e微观执行上，要\u0026quot;面向流程\u0026quot;\u003c/strong\u003e：在 Service 层，清晰、直观地把这些对象串联成一条条业务流，满足用户的实际需求。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003e对象是骨架，流程是血肉。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"面向过程-vs-面向对象\"\u003e面向过程 vs 面向对象\u003c/h2\u003e\n\u003ch3 id=\"面向过程\"\u003e面向过程\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e优点\u003c/strong\u003e：逻辑简单直接，符合人类\u0026quot;按步骤做事\u0026quot;的直觉；代码体积小、执行效率高；调试和上手成本低，适合小型程序；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e缺点\u003c/strong\u003e：可维护性差，需求变更时需修改大量相关函数，牵一发动全身；复用性低；扩展性差，难以应对复杂系统的需求迭代。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"面向对象\"\u003e面向对象\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e优点\u003c/strong\u003e：可维护性强，封装使对象内部变化不影响外部，模块化降低耦合；复用性高，继承和多态大幅减少重复代码；扩展性好，新增功能只需定义新类/方法，无需修改原有代码（开闭原则）；更贴合复杂问题的本质；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e缺点\u003c/strong\u003e：复杂度高，需设计类的结构、关系，上手门槛高；性能稍低；过度设计风险，简单问题可能被复杂化。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"业务的悖论用空间对象承载时间流程\"\u003e业务的悖论：用\u0026quot;空间对象\u0026quot;承载\u0026quot;时间流程\u0026quot;\u003c/h2\u003e\n\u003cp\u003e\u0026ldquo;业务的本质是流程（时间推进），但为什么现代软件工程推崇对象（空间治理）？\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e这是软件设计中最迷人的核心矛盾。\u003c/p\u003e\n\u003cp\u003e我们通过 DDD（领域驱动设计）思想完成了这两者的空间与时间平衡：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e宏观架构上（空间划分）——面向对象\u003c/strong\u003e：划分出干净、内聚的领域模型（如用户、商品、订单）。这些对象是系统的\u0026quot;零件\u0026quot;，负责锁住状态与核心业务规则；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e微观执行上（时间推进）——面向流程\u003c/strong\u003e：在应用服务层，用直观的面向过程逻辑，将这些对象串联成一条条满足用户最终需求的\u0026quot;业务流\u0026quot;。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e架构大智慧：对象是骨架（空间），流程是血肉（时间）。\u003c/strong\u003e\n好的代码，是用面向对象的画笔，画出面向过程的蓝图。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"为什么业务是流我们却要用对象\"\u003e为什么业务是流，我们却要用对象？\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e为了应对\u0026quot;变化\u0026quot;和\u0026quot;规模\u0026quot;。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e如果一个业务流程永远不改，面向过程绝对是最高效的。但现实是，业务流程会无限膨胀和变化。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e面向过程的灾难\u003c/strong\u003e：假设一个\u0026quot;下单流程\u0026quot;，起初只有 3 步。随着业务发展，变成了 30 步，里面充斥着各种 if-else。最后这个流程长达几千行，没人敢改，因为改了第 5 步，可能会调断第 28 步的数据；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e面向对象的解法\u003c/strong\u003e：面向对象不是消灭流程，而是把流程中的\u0026quot;参与者（实体）\u0026ldquo;抽离出来，让它们自己负责自己的事情。下单流程不再是一条黑盒管道，而是变成了用户对象、商品对象、营销引擎对象、库存对象之间的交互。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e用一个比喻来理解：\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e面向过程是\u0026quot;流水线工厂\u0026rdquo;：传送带上一条道走到黑，一旦产品要加个新功能，整条流水线可能都要停工重组；\u003c/li\u003e\n\u003cli\u003e面向对象是\u0026quot;现代企业组织\u0026quot;：划分为财务部、销售部、研发部。公司的业务依然是一个个流程，但流程是由各个部门通过协作完成的。如果财务流程变了，只需要财务部内部调整，销售部根本不需要知道。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e流程是易变的、脆弱的，对象是稳定的、高内聚的；\u003c/li\u003e\n\u003cli\u003e面向对象的流程是分治，复杂性拆分成局部复杂性；\u003c/li\u003e\n\u003cli\u003e面向过程的流程是指数复杂性，面向对象是线性复杂性；\u003c/li\u003e\n\u003cli\u003e宏观架构上要面向对象，微观执行上要面向流程；\u003c/li\u003e\n\u003cli\u003e对象是骨架，流程是血肉；\u003c/li\u003e\n\u003cli\u003e用面向对象的画笔，画出面向过程的蓝图。\u003c/li\u003e\n\u003c/ul\u003e","title":"面向过程还是面向对象？对象是骨架，流程是血肉"},{"content":"决策流程这个场景，适合树而不是层。树是前后依赖的，层是前后不依赖的、每一层是通用的。规则引擎天生就是规则流，前后有依赖，所以写规则配置就是树的方式。\n现象 树的方式对人是友好的，非常直观、快速理解。\n如果我想知道我们是否支持 xxx + xxx 的组合，从左到右很快能得到答案。\n如果是层，那么你需要知道每一层的逻辑，同时你还需要知道层与层之间的关联。\n所以，决策流程这个场景，适合树，而不是层。\n我们的场景 我们的场景是通过环境信息（cpu 架构、型号、qemu 版本等）设置 xml 配置。\n这个场景推荐用树的方式，核心原因是架构（arm/x86）是\u0026quot;分支决定性条件\u0026quot;，后续逻辑完全依赖该分支，树结构能天然贴合独立流程、降低耦合。\n逻辑贴合度高：你的需求本质是\u0026quot;架构分流 → 各自校验 qemu 版本 → 各自输出结果\u0026quot;，树结构的\u0026quot;分支独立\u0026quot;特性和这个流程完全匹配； 维护成本更低：后续若扩展（比如 arm 新增\u0026quot;内存≥4G\u0026quot;条件、x86 修改 qemu 版本要求为 \u0026gt; 5.0），树结构可直接在对应分支修改，不影响另一分支； 可读性更强：树结构能直观呈现\u0026quot;arm 线\u0026quot;和\u0026quot;x86 线\u0026quot;的完整流程，排查问题时可直接定位到对应分支。 还有关键一点，我们的场景每个流程线能复用的层比较少，比如 arm 直接就是 passthrough，后面流程都是针对 x86 设计的。\n树 vs 层 如果是无依赖的过滤，反而适合写代码，比如 k8s apiserver 里的 webhook。\n如果是决策流程，其实是规则流程，就适合规则引擎。\n因为规则引擎天生的逻辑就是上一个规则结果传入下一个规则结果，它是前后有依赖的。\n而无依赖的是独立的，代码更容易实现扩展性、精确性和复杂逻辑，且就算变化也是局部变化。\n层结构更适合\u0026quot;条件无依赖、逐步缩小范围\u0026ldquo;的场景（比如先过滤系统类型，再过滤内存，最后过滤硬盘，每个条件不依赖前一个的具体结果）。\n而你的场景中，qemu 层的判断逻辑（\u0026ldquo;满足版本后输出什么\u0026rdquo;）完全依赖前一层的架构结果，用层结构会导致：\n同一层（qemu 层）需要嵌入\u0026quot;架构 arm\u0026quot;\u0026ldquo;架构 x86\u0026quot;的分支判断，违背\u0026quot;层是通用过滤\u0026quot;的设计初衷； 新增架构（比如 risc-v）时，需要修改所有后续层的逻辑，扩展性极差。 核心总结 树是前后依赖的； 层是前后不依赖的，每一层是通用的； 规则引擎的设计，天生就是为了规则流，前后就是有依赖的； 所以写规则配置的时候，就是树的方式，很难用层的方式； 层因为其通用，很可能有较大的复杂性，且还需要扩展性，一般适合用代码实现； 但是因为其通用和局部性，修改难度低，用代码反而比用规则引擎简单。 一切都是为了 kiss。\n小结 决策流程这个场景，适合树而不是层； 树是前后依赖的，层是前后不依赖的、每一层是通用的； 规则引擎天生就是规则流，前后有依赖，所以写规则配置就是树的方式； 无依赖的过滤适合写代码，决策流程适合规则引擎； 一切都是为了 kiss。 ","permalink":"/posts/decision-tree-vs-layer/","summary":"\u003cp\u003e决策流程这个场景，适合树而不是层。树是前后依赖的，层是前后不依赖的、每一层是通用的。规则引擎天生就是规则流，前后有依赖，所以写规则配置就是树的方式。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e树的方式对人是友好的，非常直观、快速理解。\u003c/p\u003e\n\u003cp\u003e如果我想知道我们是否支持 xxx + xxx 的组合，从左到右很快能得到答案。\u003c/p\u003e\n\u003cp\u003e如果是层，那么你需要知道每一层的逻辑，同时你还需要知道层与层之间的关联。\u003c/p\u003e\n\u003cp\u003e所以，\u003cstrong\u003e决策流程这个场景，适合树，而不是层\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"我们的场景\"\u003e我们的场景\u003c/h2\u003e\n\u003cp\u003e我们的场景是通过环境信息（cpu 架构、型号、qemu 版本等）设置 xml 配置。\u003c/p\u003e\n\u003cp\u003e这个场景\u003cstrong\u003e推荐用树的方式\u003c/strong\u003e，核心原因是架构（arm/x86）是\u0026quot;分支决定性条件\u0026quot;，后续逻辑完全依赖该分支，树结构能天然贴合独立流程、降低耦合。\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e逻辑贴合度高\u003c/strong\u003e：你的需求本质是\u0026quot;架构分流 → 各自校验 qemu 版本 → 各自输出结果\u0026quot;，树结构的\u0026quot;分支独立\u0026quot;特性和这个流程完全匹配；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e维护成本更低\u003c/strong\u003e：后续若扩展（比如 arm 新增\u0026quot;内存≥4G\u0026quot;条件、x86 修改 qemu 版本要求为 \u0026gt; 5.0），树结构可直接在对应分支修改，不影响另一分支；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e可读性更强\u003c/strong\u003e：树结构能直观呈现\u0026quot;arm 线\u0026quot;和\u0026quot;x86 线\u0026quot;的完整流程，排查问题时可直接定位到对应分支。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e还有关键一点，我们的场景每个流程线能复用的层比较少，比如 arm 直接就是 passthrough，后面流程都是针对 x86 设计的。\u003c/p\u003e\n\u003ch2 id=\"树-vs-层\"\u003e树 vs 层\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e如果是无依赖的过滤，反而适合写代码，比如 k8s apiserver 里的 webhook。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e如果是决策流程，其实是规则流程，就适合规则引擎。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e因为规则引擎天生的逻辑就是上一个规则结果传入下一个规则结果，它是前后有依赖的。\u003c/p\u003e\n\u003cp\u003e而无依赖的是独立的，代码更容易实现扩展性、精确性和复杂逻辑，且就算变化也是局部变化。\u003c/p\u003e\n\u003cp\u003e层结构更适合\u0026quot;\u003cstrong\u003e条件无依赖、逐步缩小范围\u003c/strong\u003e\u0026ldquo;的场景（比如先过滤系统类型，再过滤内存，最后过滤硬盘，每个条件不依赖前一个的具体结果）。\u003c/p\u003e\n\u003cp\u003e而你的场景中，qemu 层的判断逻辑（\u0026ldquo;满足版本后输出什么\u0026rdquo;）完全依赖前一层的架构结果，用层结构会导致：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e同一层（qemu 层）需要嵌入\u0026quot;架构 arm\u0026quot;\u0026ldquo;架构 x86\u0026quot;的分支判断，违背\u0026quot;层是通用过滤\u0026quot;的设计初衷；\u003c/li\u003e\n\u003cli\u003e新增架构（比如 risc-v）时，需要修改所有后续层的逻辑，扩展性极差。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"核心总结\"\u003e核心总结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e树是前后依赖的；\u003c/li\u003e\n\u003cli\u003e层是前后不依赖的，每一层是通用的；\u003c/li\u003e\n\u003cli\u003e规则引擎的设计，天生就是为了规则流，前后就是有依赖的；\u003c/li\u003e\n\u003cli\u003e所以写规则配置的时候，就是树的方式，很难用层的方式；\u003c/li\u003e\n\u003cli\u003e层因为其通用，很可能有较大的复杂性，且还需要扩展性，一般适合用代码实现；\u003c/li\u003e\n\u003cli\u003e但是因为其通用和局部性，修改难度低，用代码反而比用规则引擎简单。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e一切都是为了 kiss。\u003c/p\u003e","title":"决策树还是决策层？决策流程适合树"},{"content":"mq 不仅是削峰填谷，也不仅是解耦服务，还有降低代码复杂性、增加稳定性的作用。中间件是一个稳定的、没有逻辑的、仅仅存储数据的。\n现象 比如我们代码中安置逻辑里的 resource 的同步。\n节点定时上报给 zoneleader，为了避免重复上报，节点自身又缓存了资源信息，每次上报前，先计算当前资源信息，然后和上一次缓存的资源信息对比，如果有差异，再汇报。\nzoneleader 还会变。\n上面两点，带来的复杂性：\nzoneleader 变了之后，节点得知道； 节点得区分 zoneleader 是否变化了，变了就直接上报资源信息； 节点不知道 zoneleader 用的资源信息是什么，说不定自己的缓存因为各种原因就不对呢； 处理异常情况能力弱。比如 zoneleader 因为个别原因更新失败，后续节点因为缓存的原因就不重试了。 这种方式虽然没有中心化的依赖问题，但是每个功能都需要处理上述类似的问题。\n这是一种繁琐的、让代码复杂性大幅膨胀的方式，也是一种功能之间重复的、却不能统一处理的多余。\n也是客户端和服务端相互紧密耦合的一种方式。\n用中间件的方式 如果用中间件的方式：\n客户端可以拉下来和当前资源对比； 服务端定期从中间件拉取资源即可； 中间件是一个稳定的、没有逻辑的、仅仅存储数据的。 所以：\n客户端的逻辑只需要客户端处理，不变接口就不会影响服务端； 服务端也一样，自己处理逻辑，不用管客户端； 节点也不用关心 zoneleader 是不是变化了，它只想一个固定的中间件地址推送就行了； zoneleader 变化了，也不用想办法通知到其他节点，存储到中间件就行。 好处：\n解耦； 不会相互影响了； 节点也知道当前使用的资源信息是什么样的，可以对比。 中间件依赖本身可以通过各种高可用方式保证其可用性，在现在已经不是什么主要矛盾或问题了。\n小结 mq 不仅是削峰填谷，也不仅是解耦服务； 还有降低代码复杂性、增加稳定性的作用； 中间件是一个稳定的、没有逻辑的、仅仅存储数据的； 客户端和服务端通过中间件解耦，互不影响； 中间件依赖可以通过高可用方式保证可用性，不是主要矛盾。 ","permalink":"/posts/mq-advantage-over-direct-message/","summary":"\u003cp\u003emq 不仅是削峰填谷，也不仅是解耦服务，还有降低代码复杂性、增加稳定性的作用。中间件是一个稳定的、没有逻辑的、仅仅存储数据的。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e比如我们代码中安置逻辑里的 resource 的同步。\u003c/p\u003e\n\u003cp\u003e节点定时上报给 zoneleader，为了避免重复上报，节点自身又缓存了资源信息，每次上报前，先计算当前资源信息，然后和上一次缓存的资源信息对比，如果有差异，再汇报。\u003c/p\u003e\n\u003cp\u003ezoneleader 还会变。\u003c/p\u003e\n\u003cp\u003e上面两点，带来的复杂性：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003ezoneleader 变了之后，节点得知道；\u003c/li\u003e\n\u003cli\u003e节点得区分 zoneleader 是否变化了，变了就直接上报资源信息；\u003c/li\u003e\n\u003cli\u003e节点不知道 zoneleader 用的资源信息是什么，说不定自己的缓存因为各种原因就不对呢；\u003c/li\u003e\n\u003cli\u003e处理异常情况能力弱。比如 zoneleader 因为个别原因更新失败，后续节点因为缓存的原因就不重试了。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e这种方式虽然没有中心化的依赖问题，但是每个功能都需要处理上述类似的问题。\u003c/p\u003e\n\u003cp\u003e这是一种繁琐的、让代码复杂性大幅膨胀的方式，也是一种功能之间重复的、却不能统一处理的多余。\u003c/p\u003e\n\u003cp\u003e也是\u003cstrong\u003e客户端和服务端相互紧密耦合的一种方式\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"用中间件的方式\"\u003e用中间件的方式\u003c/h2\u003e\n\u003cp\u003e如果用中间件的方式：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e客户端可以拉下来和当前资源对比；\u003c/li\u003e\n\u003cli\u003e服务端定期从中间件拉取资源即可；\u003c/li\u003e\n\u003cli\u003e中间件是一个稳定的、没有逻辑的、仅仅存储数据的。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e所以：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e客户端的逻辑只需要客户端处理，不变接口就不会影响服务端；\u003c/li\u003e\n\u003cli\u003e服务端也一样，自己处理逻辑，不用管客户端；\u003c/li\u003e\n\u003cli\u003e节点也不用关心 zoneleader 是不是变化了，它只想一个\u003cstrong\u003e固定的\u003c/strong\u003e中间件地址推送就行了；\u003c/li\u003e\n\u003cli\u003ezoneleader 变化了，也不用想办法通知到其他节点，存储到中间件就行。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e好处：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e解耦；\u003c/li\u003e\n\u003cli\u003e不会相互影响了；\u003c/li\u003e\n\u003cli\u003e节点也知道当前使用的资源信息是什么样的，可以对比。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003e中间件依赖本身可以通过各种高可用方式保证其可用性，在现在已经不是什么主要矛盾或问题了。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003emq 不仅是削峰填谷，也不仅是解耦服务；\u003c/li\u003e\n\u003cli\u003e还有降低代码复杂性、增加稳定性的作用；\u003c/li\u003e\n\u003cli\u003e中间件是一个稳定的、没有逻辑的、仅仅存储数据的；\u003c/li\u003e\n\u003cli\u003e客户端和服务端通过中间件解耦，互不影响；\u003c/li\u003e\n\u003cli\u003e中间件依赖可以通过高可用方式保证可用性，不是主要矛盾。\u003c/li\u003e\n\u003c/ul\u003e","title":"mq 相比直接传递消息的优势"},{"content":"check 有很多项，每个项都有修复。check 是一条线，修复是一条线。不要在 check 中发现某个项有问题就进行修复，而是要等 check 完再调用修复。\n核心：check 和修复职责分离 check 有很多项，每个项都有修复。\n那么：\ncheck 是一条线； 修复是一条线。 不要在 check 中，发现某个项有问题就进行修复，而是要等 check 完，再调用修复。\n修复的里面比如也包含 check，但是这不能代替 check 流程。\n因为有些情况，只需要 check，最典型的是告警和健康状态。\ncheck 就得响应迅速，迅速告警。\n里面掺杂了修复，有几个问题 健康状态不好管理：难道项目修复前是不健康，修复后改成健康，那下一个检查项呢？这样变来变去，很不稳定，上层没法用； 代码零散：就算在流程里更新健康状态，那得多零散，代码处理也不太好； 响应不及时：修复操作有的很耗时，影响 check 的效率。 检测和修复完全的职责分离 健康状态是检测修复。\n那么修复的流程，即便修复完成，也不要管理健康状态。\n下一次检测流程自然会更新健康状态。\n小结 check 和修复要从大流程上就分开； check 是一条线，修复是一条线； 不要在 check 中发现某个项有问题就进行修复，要等 check 完再调用修复； check 就得响应迅速，迅速告警； 检测和修复完全的职责分离，修复完成也不要管理健康状态，下一次检测自然会更新。 ","permalink":"/posts/check-and-fix-separate/","summary":"\u003cp\u003echeck 有很多项，每个项都有修复。check 是一条线，修复是一条线。不要在 check 中发现某个项有问题就进行修复，而是要等 check 完再调用修复。\u003c/p\u003e\n\u003ch2 id=\"核心check-和修复职责分离\"\u003e核心：check 和修复职责分离\u003c/h2\u003e\n\u003cp\u003echeck 有很多项，每个项都有修复。\u003c/p\u003e\n\u003cp\u003e那么：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003echeck 是一条线\u003c/strong\u003e；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e修复是一条线\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e不要在 check 中，发现某个项有问题就进行修复，而是要等 check 完，再调用修复。\u003c/p\u003e\n\u003cp\u003e修复的里面比如也包含 check，但是这不能代替 check 流程。\u003c/p\u003e\n\u003cp\u003e因为有些情况，只需要 check，最典型的是告警和健康状态。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003echeck 就得响应迅速，迅速告警。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"里面掺杂了修复有几个问题\"\u003e里面掺杂了修复，有几个问题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e健康状态不好管理\u003c/strong\u003e：难道项目修复前是不健康，修复后改成健康，那下一个检查项呢？这样变来变去，很不稳定，上层没法用；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e代码零散\u003c/strong\u003e：就算在流程里更新健康状态，那得多零散，代码处理也不太好；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e响应不及时\u003c/strong\u003e：修复操作有的很耗时，影响 check 的效率。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"检测和修复完全的职责分离\"\u003e检测和修复完全的职责分离\u003c/h2\u003e\n\u003cp\u003e健康状态是检测修复。\u003c/p\u003e\n\u003cp\u003e那么修复的流程，即便修复完成，也不要管理健康状态。\u003c/p\u003e\n\u003cp\u003e下一次检测流程自然会更新健康状态。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003echeck 和修复要从大流程上就分开；\u003c/li\u003e\n\u003cli\u003echeck 是一条线，修复是一条线；\u003c/li\u003e\n\u003cli\u003e不要在 check 中发现某个项有问题就进行修复，要等 check 完再调用修复；\u003c/li\u003e\n\u003cli\u003echeck 就得响应迅速，迅速告警；\u003c/li\u003e\n\u003cli\u003e检测和修复完全的职责分离，修复完成也不要管理健康状态，下一次检测自然会更新。\u003c/li\u003e\n\u003c/ul\u003e","title":"check 和修复，要从大流程上就分开"},{"content":"底层逻辑尤其要保证单一，一个能力只提供一种方法。想要更多，通过上层组合、上层封装来实现。千万不要把所有需求都放到底层原子能力上。\n核心：底层单一，上层组合 底层逻辑尤其要保证单一，一个能力只提供一种方法。\n想要更多，那么通过上层组合、上层封装来实现。\n千万不要把所有需求都放到底层原子能力上，虽然有些看起来好改，实际就是埋大坑。\n符合设计原则、编程哲学是很重要和长久的。\n来一个需求找一个好改的方式，这个评估维度是片面和短视的。\n一个例子：网卡带宽 这个带宽控制是从精细化到模糊化管理。\n从 iaas 的角度，这个管理方式是合理的，能适配多样化产品形态； 从产品的角度，需设计好统一的、标准化的控制方法。 nic 是网卡维度的控制； instance_extra 是云主机维度的控制； server.yaml 是全局控制。\n看起来合理，实际没有人捋清楚，看代码看很久。每次梳理了就忘。\n合理的方式应该是：\n底层保持单一，容易理解； 云主机调整和全局的调整可以通过 api 来实现。 这样比较明确，以后也可以实现大会上说的\u0026quot;功能使用频率统计\u0026quot;。\n批量、整体，这类操作，千万不要隐藏在配置或者底层的逻辑中，一定要以功能、api 的方式提供。\n这样明确、可量化统计、可观测、可追溯。\n小结 底层逻辑尤其要保证单一，一个能力只提供一种方法； 想要更多，通过上层组合、上层封装来实现； 千万不要把所有需求都放到底层原子能力上，那是埋大坑； 批量、整体操作，一定要以功能、api 的方式提供； 这样明确、可量化统计、可观测、可追溯。 ","permalink":"/posts/bottom-single-top-combine/","summary":"\u003cp\u003e底层逻辑尤其要保证单一，一个能力只提供一种方法。想要更多，通过上层组合、上层封装来实现。千万不要把所有需求都放到底层原子能力上。\u003c/p\u003e\n\u003ch2 id=\"核心底层单一上层组合\"\u003e核心：底层单一，上层组合\u003c/h2\u003e\n\u003cp\u003e底层逻辑尤其要保证单一，一个能力只提供一种方法。\u003c/p\u003e\n\u003cp\u003e想要更多，那么通过上层组合、上层封装来实现。\u003c/p\u003e\n\u003cp\u003e千万不要把所有需求都放到底层原子能力上，虽然有些看起来好改，实际就是埋大坑。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e符合设计原则、编程哲学是很重要和长久的。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e来一个需求找一个好改的方式，这个评估维度是片面和短视的。\u003c/p\u003e\n\u003ch2 id=\"一个例子网卡带宽\"\u003e一个例子：网卡带宽\u003c/h2\u003e\n\u003cp\u003e这个带宽控制是从精细化到模糊化管理。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e从 iaas 的角度，这个管理方式是合理的，能适配多样化产品形态；\u003c/li\u003e\n\u003cli\u003e从产品的角度，需设计好统一的、标准化的控制方法。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003enic 是网卡维度的控制；\ninstance_extra 是云主机维度的控制；\nserver.yaml 是全局控制。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e看起来合理，实际没有人捋清楚，看代码看很久。每次梳理了就忘。\u003c/p\u003e\n\u003cp\u003e合理的方式应该是：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e底层保持单一，容易理解\u003c/strong\u003e；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e云主机调整和全局的调整可以通过 api 来实现\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这样比较明确，以后也可以实现大会上说的\u0026quot;功能使用频率统计\u0026quot;。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e批量、整体，这类操作，千万不要隐藏在配置或者底层的逻辑中，一定要以功能、api 的方式提供。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e这样明确、可量化统计、可观测、可追溯。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e底层逻辑尤其要保证单一，一个能力只提供一种方法；\u003c/li\u003e\n\u003cli\u003e想要更多，通过上层组合、上层封装来实现；\u003c/li\u003e\n\u003cli\u003e千万不要把所有需求都放到底层原子能力上，那是埋大坑；\u003c/li\u003e\n\u003cli\u003e批量、整体操作，一定要以功能、api 的方式提供；\u003c/li\u003e\n\u003cli\u003e这样明确、可量化统计、可观测、可追溯。\u003c/li\u003e\n\u003c/ul\u003e","title":"底层单一，上层组合"},{"content":"Hook 侧重现有流程上运行时的局部修改，插件侧重模块化功能扩展，Middleware 是流程中可以扩展的横切面。三者核心目标都是不侵入主程序的前提下实现扩展。\nHook vs 插件 Hook 与插件是两种常见的扩展机制，它们在实现方式、应用场景和解决的问题上存在显著差异。\n**Hook（钩子）**是一种通过拦截或修改现有流程的执行逻辑，插入自定义代码的机制。\n插件是一种模块化的功能扩展机制，通过动态加载的独立模块向主程序注入新功能。\n维度 Hook 插件 实现方式 拦截或替换现有流程（如函数调用、事件触发） 动态加载独立模块，通过 API 与主程序交互 侵入性 低（无需修改源码，直接拦截流程） 低（主程序无需依赖插件） 控制粒度 细粒度（可精确到单个函数或事件） 粗粒度（通常针对模块级功能） 动态性 运行时生效（如热补丁） 运行时加载/卸载（如插件管理器） 典型用途 行为拦截、动态修改、事件统一处理 功能扩展、模块化开发、多租户定制 hook 侧重现有流程上运行时的局部修改，插件侧重模块化、功能扩展、架构层的设计。\nMiddleware vs Hook Middleware（中间件）：是一种串联在核心流程上下游的组件，用于对核心操作的输入/输出进行预处理、后处理或增强。它通常形成一条\u0026quot;管道（Pipeline）\u0026quot;，请求/数据会按顺序经过多个中间件，最终到达核心逻辑，再沿原路返回。核心目标是解耦通用功能（如日志、认证、缓存）与核心业务逻辑。\nHook（钩子）：是在程序特定生命周期节点（如初始化后、销毁前、事件触发时）预留的\u0026quot;接口\u0026quot;，允许开发者注册自定义逻辑，在节点触发时自动执行。钩子不直接干预核心流程的输入/输出，而是\u0026quot;挂靠\u0026quot;在关键节点上响应事件。核心目标是提供扩展点，允许开发者在不修改核心代码的前提下，介入程序的生命周期或事件处理。\nmiddleware 是流程中可以扩展横切面，可以扩展多个。hook 是某些固定的点上，留下 hook 口。\nmiddleware 是流程上的扩展； hook 是功能点上的扩展，比如 delete 后，调用一个 hook 做一些其他资源清理或检查，但是这 delete 不一定会调用，不是流程必须的； middleware 就是主流程中的扩展。 Middleware vs 微内核 Middleware 是一种嵌入核心业务流程前后的组件，用于处理通用化、横切性的功能（即\u0026quot;横切关注点\u0026quot;）。它的核心作用是增强核心流程的能力，同时保持核心业务逻辑的简洁性。\n微内核架构的核心是一个最小化的\u0026quot;内核\u0026quot;（仅包含系统最基础的能力，如模块管理、通信机制），所有业务功能通过插件（Plugin）动态扩展。内核不包含具体业务逻辑，仅负责插件的加载、卸载、通信和生命周期管理。\n维度 Middleware 中间件 微内核架构 设计目标 处理\u0026quot;横切关注点\u0026quot;，增强核心流程的通用能力 支持功能动态扩展，实现系统模块化和解耦 与核心的关系 嵌入核心流程的执行链中，依赖核心流程存在 插件独立于内核，内核仅负责管理插件（不依赖插件） 调用方式 链式调用（前一个中间件处理后传递给下一个） 插件通过内核注册/调用，插件间通过内核间接通信 动态性 通常静态配置（启动时确定中间件组合），动态性弱 支持运行时动态加载/卸载插件，动态性强 功能定位 聚焦\u0026quot;流程增强\u0026quot;（如日志、认证、缓存） 聚焦\u0026quot;功能扩展\u0026quot;（如新增业务模块、工具） 典型示例 Gin 日志中间件、JWT 认证中间件 VS Code 插件系统、Linux 内核模块 关键区别总结 与核心流程的关系：\n中间件是核心流程的\u0026quot;参与者\u0026quot;，直接处理输入/输出，影响流程走向（如拦截未认证请求）； 钩子是核心流程的\u0026quot;旁观者\u0026quot;，仅在节点触发时响应，不直接影响流程（如记录日志但不中断操作）。 复用与扩展的侧重点：\n中间件侧重通用功能的复用（如认证、日志可在多个接口中复用）； 钩子侧重个性化扩展（如不同业务在同一生命周期节点执行不同逻辑）。 执行顺序：\n中间件的执行顺序严格按注册顺序（管道式），且存在\u0026quot;去程\u0026quot;和\u0026quot;回程\u0026quot;（如 c.Next() 前后）； 钩子的执行顺序通常按注册顺序（或框架定义的优先级），但仅在节点触发时执行一次。 小结 Hook 侧重现有流程上运行时的局部修改； 插件侧重模块化、功能扩展、架构层的设计； Middleware 是流程中可以扩展的横切面，可以扩展多个； Hook 是某些固定的点上，留下 hook 口； 三者核心目标都是不侵入主程序的前提下实现扩展。 ","permalink":"/posts/hook-plugin-middleware/","summary":"\u003cp\u003eHook 侧重现有流程上运行时的局部修改，插件侧重模块化功能扩展，Middleware 是流程中可以扩展的横切面。三者核心目标都是不侵入主程序的前提下实现扩展。\u003c/p\u003e\n\u003ch2 id=\"hook-vs-插件\"\u003eHook vs 插件\u003c/h2\u003e\n\u003cp\u003eHook 与插件是两种常见的扩展机制，它们在实现方式、应用场景和解决的问题上存在显著差异。\u003c/p\u003e\n\u003cp\u003e**Hook（钩子）**是一种通过拦截或修改现有流程的执行逻辑，插入自定义代码的机制。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e插件\u003c/strong\u003e是一种模块化的功能扩展机制，通过动态加载的独立模块向主程序注入新功能。\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e维度\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eHook\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e插件\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e实现方式\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e拦截或替换现有流程（如函数调用、事件触发）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e动态加载独立模块，通过 API 与主程序交互\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e侵入性\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e低（无需修改源码，直接拦截流程）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e低（主程序无需依赖插件）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e控制粒度\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e细粒度（可精确到单个函数或事件）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e粗粒度（通常针对模块级功能）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e动态性\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e运行时生效（如热补丁）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e运行时加载/卸载（如插件管理器）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e典型用途\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e行为拦截、动态修改、事件统一处理\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e功能扩展、模块化开发、多租户定制\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003ehook 侧重现有流程上运行时的局部修改，插件侧重模块化、功能扩展、架构层的设计。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"middleware-vs-hook\"\u003eMiddleware vs Hook\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eMiddleware（中间件）\u003c/strong\u003e：是一种串联在核心流程上下游的组件，用于对核心操作的输入/输出进行预处理、后处理或增强。它通常形成一条\u0026quot;管道（Pipeline）\u0026quot;，请求/数据会按顺序经过多个中间件，最终到达核心逻辑，再沿原路返回。核心目标是解耦通用功能（如日志、认证、缓存）与核心业务逻辑。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eHook（钩子）\u003c/strong\u003e：是在程序特定生命周期节点（如初始化后、销毁前、事件触发时）预留的\u0026quot;接口\u0026quot;，允许开发者注册自定义逻辑，在节点触发时自动执行。钩子不直接干预核心流程的输入/输出，而是\u0026quot;挂靠\u0026quot;在关键节点上响应事件。核心目标是提供扩展点，允许开发者在不修改核心代码的前提下，介入程序的生命周期或事件处理。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003emiddleware 是流程中可以扩展横切面，可以扩展多个。hook 是某些固定的点上，留下 hook 口。\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003emiddleware 是流程上的扩展；\u003c/li\u003e\n\u003cli\u003ehook 是功能点上的扩展，比如 delete 后，调用一个 hook 做一些其他资源清理或检查，但是这 delete 不一定会调用，不是流程必须的；\u003c/li\u003e\n\u003cli\u003emiddleware 就是主流程中的扩展。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"middleware-vs-微内核\"\u003eMiddleware vs 微内核\u003c/h2\u003e\n\u003cp\u003eMiddleware 是一种嵌入核心业务流程前后的组件，用于处理通用化、横切性的功能（即\u0026quot;横切关注点\u0026quot;）。它的核心作用是增强核心流程的能力，同时保持核心业务逻辑的简洁性。\u003c/p\u003e\n\u003cp\u003e微内核架构的核心是一个最小化的\u0026quot;内核\u0026quot;（仅包含系统最基础的能力，如模块管理、通信机制），所有业务功能通过插件（Plugin）动态扩展。内核不包含具体业务逻辑，仅负责插件的加载、卸载、通信和生命周期管理。\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e维度\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eMiddleware 中间件\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e微内核架构\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e设计目标\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e处理\u0026quot;横切关注点\u0026quot;，增强核心流程的通用能力\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e支持功能动态扩展，实现系统模块化和解耦\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e与核心的关系\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e嵌入核心流程的执行链中，依赖核心流程存在\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e插件独立于内核，内核仅负责管理插件（不依赖插件）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e调用方式\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e链式调用（前一个中间件处理后传递给下一个）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e插件通过内核注册/调用，插件间通过内核间接通信\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e动态性\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e通常静态配置（启动时确定中间件组合），动态性弱\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e支持运行时动态加载/卸载插件，动态性强\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e功能定位\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e聚焦\u0026quot;流程增强\u0026quot;（如日志、认证、缓存）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e聚焦\u0026quot;功能扩展\u0026quot;（如新增业务模块、工具）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e典型示例\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGin 日志中间件、JWT 认证中间件\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eVS Code 插件系统、Linux 内核模块\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"关键区别总结\"\u003e关键区别总结\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e与核心流程的关系\u003c/strong\u003e：\u003c/p\u003e","title":"Hook、插件、Middleware：三种扩展机制的区别"},{"content":"跟随主流技术非常重要，不要老用一些偏门的、古怪的技术凸显自己的别具一格。技术是手段，不是目的，所以越主流越好。\n核心：跟随主流技术 跟随主流技术，非常重要。\n不要老用一些偏门的、古怪的技术，凸显自己的别具一格。\n比如，用 lxc 还是 docker。\n别说两者底层原理都一样，用 lxc 自身可控性更好。生态和工具的适配完全是天和地的区别，这已经不是跨级别的问题，而是天壤之别。\n再比如网络，主流都是用 ovs，所以别人智能网卡、dpu、新的一些设备、网络卸载等，适配的都是 ovs。\n你自己搞一套，或者用小众的，别人不适配你啊。\n还有国产化，都是靠着主流去适配的。\n技术是手段，不是目的 技术是手段，不是目的。\n所以，越主流越好。\n主流代表着稳定、可靠、被大规模多场景验证过，同时有大量的解决方案和排查知识，也有大量的人才支撑。\n手段不用在意创新，只需要稳定、可达、低成本。目的才需要考虑创新，创造价值差异。\n小结 跟随主流技术非常重要； 不要老用偏门的、古怪的技术凸显自己的别具一格； 生态和工具的适配是天壤之别； 技术是手段，不是目的，越主流越好； 主流代表稳定、可靠、被大规模验证过，有大量解决方案和人才支撑。 ","permalink":"/posts/use-mainstream-technology/","summary":"\u003cp\u003e跟随主流技术非常重要，不要老用一些偏门的、古怪的技术凸显自己的别具一格。技术是手段，不是目的，所以越主流越好。\u003c/p\u003e\n\u003ch2 id=\"核心跟随主流技术\"\u003e核心：跟随主流技术\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e跟随主流技术，非常重要。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e不要老用一些偏门的、古怪的技术，凸显自己的别具一格。\u003c/p\u003e\n\u003cp\u003e比如，用 lxc 还是 docker。\u003c/p\u003e\n\u003cp\u003e别说两者底层原理都一样，用 lxc 自身可控性更好。生态和工具的适配完全是天和地的区别，这已经不是跨级别的问题，而是天壤之别。\u003c/p\u003e\n\u003cp\u003e再比如网络，主流都是用 ovs，所以别人智能网卡、dpu、新的一些设备、网络卸载等，适配的都是 ovs。\u003c/p\u003e\n\u003cp\u003e你自己搞一套，或者用小众的，别人不适配你啊。\u003c/p\u003e\n\u003cp\u003e还有国产化，都是靠着主流去适配的。\u003c/p\u003e\n\u003ch2 id=\"技术是手段不是目的\"\u003e技术是手段，不是目的\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e技术是手段，不是目的。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e所以，越主流越好。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e主流代表着稳定、可靠、被大规模多场景验证过，同时有大量的解决方案和排查知识，也有大量的人才支撑。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e手段不用在意创新，只需要稳定、可达、低成本。目的才需要考虑创新，创造价值差异。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e跟随主流技术非常重要；\u003c/li\u003e\n\u003cli\u003e不要老用偏门的、古怪的技术凸显自己的别具一格；\u003c/li\u003e\n\u003cli\u003e生态和工具的适配是天壤之别；\u003c/li\u003e\n\u003cli\u003e技术是手段，不是目的，越主流越好；\u003c/li\u003e\n\u003cli\u003e主流代表稳定、可靠、被大规模验证过，有大量解决方案和人才支撑。\u003c/li\u003e\n\u003c/ul\u003e","title":"用主流技术：技术是手段，不是目的"},{"content":"什么是耦合，怎么判断有没有耦合？可以从结果反推：A 变化了，B 要不要跟着变化。如果跟着变，那就是耦合了；如果没有跟着变，或者变动很小，那就是松耦合。\n核心：耦合的松紧关键在于变化 什么是耦合，怎么判断有没有耦合？\n可以从结果反推：\nA 变化了，B 要不要跟着变化； 如果跟着变，那就是耦合了； 如果没有跟着变，或者变动很小，那就是松耦合。 耦合的松紧关键在于变化。\n小结 判断耦合可以从结果反推； A 变化了，B 跟着变，就是耦合； A 变化了，B 不变或变动很小，就是松耦合； 耦合的松紧关键在于变化。 ","permalink":"/posts/coupling-key-is-change/","summary":"\u003cp\u003e什么是耦合，怎么判断有没有耦合？可以从结果反推：A 变化了，B 要不要跟着变化。如果跟着变，那就是耦合了；如果没有跟着变，或者变动很小，那就是松耦合。\u003c/p\u003e\n\u003ch2 id=\"核心耦合的松紧关键在于变化\"\u003e核心：耦合的松紧关键在于变化\u003c/h2\u003e\n\u003cp\u003e什么是耦合，怎么判断有没有耦合？\u003c/p\u003e\n\u003cp\u003e可以从结果反推：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eA 变化了，B 要不要跟着变化；\u003c/li\u003e\n\u003cli\u003e如果跟着变，那就是耦合了；\u003c/li\u003e\n\u003cli\u003e如果没有跟着变，或者变动很小，那就是松耦合。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e耦合的松紧关键在于变化。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e判断耦合可以从结果反推；\u003c/li\u003e\n\u003cli\u003eA 变化了，B 跟着变，就是耦合；\u003c/li\u003e\n\u003cli\u003eA 变化了，B 不变或变动很小，就是松耦合；\u003c/li\u003e\n\u003cli\u003e耦合的松紧关键在于变化。\u003c/li\u003e\n\u003c/ul\u003e","title":"耦合的松紧，关键在于变化"},{"content":"魔数、默认值使用常量封装。提高可读性，统一定义唯一来源，唯一来源相当于是统一了标准。常量在离使用代码最近的地方定义比较好。\n现象 const ( // NamespaceDefault means the object is in the default namespace which is applied when not specified by clients NamespaceDefault string = \u0026#34;default\u0026#34; // NamespaceAll is the default argument to specify on a context when you want to list or filter resources across all namespaces NamespaceAll string = \u0026#34;\u0026#34; // NamespaceNodeLease is the namespace where we place node lease objects (used for node heartbeats) NamespaceNodeLease string = \u0026#34;kube-node-lease\u0026#34; ) 这是 k8s 的 core 的 type 资源定义。\n这个常量定义非常好 提高可读性； 统一定义，唯一来源，比如 ALL 到底是 \u0026quot;\u0026quot; 还是 \u0026ldquo;all\u0026rdquo;，如果没有这个常量，就得翻全量代码； 唯一来源，如果后面真好修改，一个地方修改。 唯一来源相当于是统一了标准。\n常量在哪里定义 另外，常量定义不用单独一个文件定义，在离使用代码最近的地方定义比较好，容易找。\nk8s 源码写得好。\n小结 魔数、默认值使用常量封装； 提高可读性； 统一定义，唯一来源； 唯一来源相当于是统一了标准； 常量在离使用代码最近的地方定义比较好，容易找。 ","permalink":"/posts/constant-single-source/","summary":"\u003cp\u003e魔数、默认值使用常量封装。提高可读性，统一定义唯一来源，唯一来源相当于是统一了标准。常量在离使用代码最近的地方定义比较好。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-go\" data-lang=\"go\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003econst\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"c1\"\u003e// NamespaceDefault means the object is in the default namespace which is applied when not specified by clients\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"nx\"\u003eNamespaceDefault\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;default\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"c1\"\u003e// NamespaceAll is the default argument to specify on a context when you want to list or filter resources across all namespaces\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"nx\"\u003eNamespaceAll\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"c1\"\u003e// NamespaceNodeLease is the namespace where we place node lease objects (used for node heartbeats)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\t\u003c/span\u003e\u003cspan class=\"nx\"\u003eNamespaceNodeLease\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;kube-node-lease\u0026#34;\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这是 k8s 的 core 的 type 资源定义。\u003c/p\u003e","title":"魔数、默认值使用常量封装：唯一来源"},{"content":"把只读状态写到了事务字段里，明显不符合单一原则。一个字段怎么能表示多种意思呢？如果一开始就不违反单一原则，即便没想到并发冲突问题，也不会出现问题。\n现象 比如做只读，将只读状态写到了事务字段里。\n这明显就不符合设计原则，单一原则。\n一个字段怎么能表示多种意思的呢？\n开始觉得事务字段已经在页面展示了，加个只读前端自动展示就完事了。\n结果事务字段还有其他的作用，防止并发操作。\n和上面一样，打补丁按照流程，没有按照对象，那在处理事务字段的判断里跳过 readonly 的操作。\n结果根本改不完啊，只读检查和所有流程耦合在一起，简直了。\n根因：违反单一原则 如果一开始就不要违反单一原则，即便没有想到并发冲突问题，也不会出现问题。\n小结 符合设计原则，可以规避一些想不到的问题； 一个字段不能表示多种意思，违反单一原则； 打补丁要按照对象，而不是按照流程； 只读检查和所有流程耦合在一起，根本改不完； 如果一开始就不违反单一原则，即便没想到并发冲突问题，也不会出现问题。 ","permalink":"/posts/design-principles-avoid-problems/","summary":"\u003cp\u003e把只读状态写到了事务字段里，明显不符合单一原则。一个字段怎么能表示多种意思呢？如果一开始就不违反单一原则，即便没想到并发冲突问题，也不会出现问题。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e比如做只读，将只读状态写到了事务字段里。\u003c/p\u003e\n\u003cp\u003e这明显就不符合设计原则，单一原则。\u003c/p\u003e\n\u003cp\u003e一个字段怎么能表示多种意思的呢？\u003c/p\u003e\n\u003cp\u003e开始觉得事务字段已经在页面展示了，加个只读前端自动展示就完事了。\u003c/p\u003e\n\u003cp\u003e结果事务字段还有其他的作用，防止并发操作。\u003c/p\u003e\n\u003cp\u003e和上面一样，打补丁按照流程，没有按照对象，那在处理事务字段的判断里跳过 readonly 的操作。\u003c/p\u003e\n\u003cp\u003e结果根本改不完啊，只读检查和所有流程耦合在一起，简直了。\u003c/p\u003e\n\u003ch2 id=\"根因违反单一原则\"\u003e根因：违反单一原则\u003c/h2\u003e\n\u003cp\u003e如果一开始就不要违反单一原则，即便没有想到并发冲突问题，也不会出现问题。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e符合设计原则，可以规避一些想不到的问题；\u003c/li\u003e\n\u003cli\u003e一个字段不能表示多种意思，违反单一原则；\u003c/li\u003e\n\u003cli\u003e打补丁要按照对象，而不是按照流程；\u003c/li\u003e\n\u003cli\u003e只读检查和所有流程耦合在一起，根本改不完；\u003c/li\u003e\n\u003cli\u003e如果一开始就不违反单一原则，即便没想到并发冲突问题，也不会出现问题。\u003c/li\u003e\n\u003c/ul\u003e","title":"符合设计原则，可以规避一些想不到的问题"},{"content":"哪里遇到问题了，就找个地方加个开关、加个检查、加个条件判断来处理掉。这导致不够系统化，引入了局部的分支点，会造成多流程的耦合交叉，扩展容易遗漏。\n现象 我们的代码这种情况非常的多。\n就是哪里遇到问题了，好，我们按照这个问题找个地方加一个开关，或加一个检查，或加一个条件判断来处理掉。\n这导致的问题，就是不够系统化，引入了局部的分支点，但是对于整体的影响没有分析。\n这个分叉会造成后续扩展的问题，扩展的时候压根不会注意到有这个分叉。而且类似的问题可能还会继续分叉。\n不够系统化，这样会造成多流程的耦合交叉，扩展容易遗漏。\n例子 比如我们关机对于资源的可见性的管理（主要是 image），没有统一的约束和要求。\n克隆的时候发现路径不在，报错了，然后加一个 open 修复一下，其他流程也是一样。\n哇，这完全是黑盒，完全没法分析和预判，就等错误暴露了修复。\n比如 drbd 发现脑裂了，直接操作了 --discard-my-data，这样很有问题，这样上层还在正常的创建资源。\n实际需要给 node 抽象状态，当然我们有个 failed 状态，但是这是完全不可用的状态，相当于是宕机状态。\n那么你没有准备好的状态，需要独立抽象的。发现了脑裂，其实是需要修复的，标记为未准备好的状态，那么其他流程都根据这个状态决定自己的操作。这是一个解耦的操作。\n反常识的组合 包括下面的检查，开启了灾难检查，又开了一个 flag 的情况下跳过检查，很奇怪。\n为什么不直接统一为关闭灾难检查呢，或者就统一的 flag 文件。\n两个组合在一起，还是相反意思的，非常反常识，非常不结构化、不系统化。\n小结 打补丁要面向对象，而不是针对流程打补丁； 哪里遇到问题就加开关、加检查、加条件判断，会导致不够系统化； 不够系统化会造成多流程的耦合交叉，扩展容易遗漏； 给 node 抽象状态，其他流程根据状态决定自己的操作，是解耦的操作； 两个相反意思的 flag 组合在一起，非常反常识、不系统化。 ","permalink":"/posts/patch-object-oriented/","summary":"\u003cp\u003e哪里遇到问题了，就找个地方加个开关、加个检查、加个条件判断来处理掉。这导致不够系统化，引入了局部的分支点，会造成多流程的耦合交叉，扩展容易遗漏。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e我们的代码这种情况非常的多。\u003c/p\u003e\n\u003cp\u003e就是哪里遇到问题了，好，我们按照这个问题找个地方加一个开关，或加一个检查，或加一个条件判断来处理掉。\u003c/p\u003e\n\u003cp\u003e这导致的问题，就是不够系统化，引入了局部的分支点，但是对于整体的影响没有分析。\u003c/p\u003e\n\u003cp\u003e这个分叉会造成后续扩展的问题，扩展的时候压根不会注意到有这个分叉。而且类似的问题可能还会继续分叉。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e不够系统化，这样会造成多流程的耦合交叉，扩展容易遗漏。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"例子\"\u003e例子\u003c/h2\u003e\n\u003cp\u003e比如我们关机对于资源的可见性的管理（主要是 image），没有统一的约束和要求。\u003c/p\u003e\n\u003cp\u003e克隆的时候发现路径不在，报错了，然后加一个 open 修复一下，其他流程也是一样。\u003c/p\u003e\n\u003cp\u003e哇，这完全是黑盒，完全没法分析和预判，就等错误暴露了修复。\u003c/p\u003e\n\u003cp\u003e比如 drbd 发现脑裂了，直接操作了 \u003ccode\u003e--discard-my-data\u003c/code\u003e，这样很有问题，这样上层还在正常的创建资源。\u003c/p\u003e\n\u003cp\u003e实际需要给 node 抽象状态，当然我们有个 failed 状态，但是这是完全不可用的状态，相当于是宕机状态。\u003c/p\u003e\n\u003cp\u003e那么你没有准备好的状态，需要独立抽象的。发现了脑裂，其实是需要修复的，标记为未准备好的状态，那么其他流程都根据这个\u003cstrong\u003e状态决定自己的操作\u003c/strong\u003e。这是一个解耦的操作。\u003c/p\u003e\n\u003ch2 id=\"反常识的组合\"\u003e反常识的组合\u003c/h2\u003e\n\u003cp\u003e包括下面的检查，开启了灾难检查，又开了一个 flag 的情况下跳过检查，很奇怪。\u003c/p\u003e\n\u003cp\u003e为什么不直接统一为关闭灾难检查呢，或者就统一的 flag 文件。\u003c/p\u003e\n\u003cp\u003e两个组合在一起，还是相反意思的，非常反常识，非常不结构化、不系统化。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e打补丁要面向对象，而不是针对流程打补丁；\u003c/li\u003e\n\u003cli\u003e哪里遇到问题就加开关、加检查、加条件判断，会导致不够系统化；\u003c/li\u003e\n\u003cli\u003e不够系统化会造成多流程的耦合交叉，扩展容易遗漏；\u003c/li\u003e\n\u003cli\u003e给 node 抽象状态，其他流程根据状态决定自己的操作，是解耦的操作；\u003c/li\u003e\n\u003cli\u003e两个相反意思的 flag 组合在一起，非常反常识、不系统化。\u003c/li\u003e\n\u003c/ul\u003e","title":"即便是补丁，也要面向对象，而不是针对流程打补丁"},{"content":"开发决定了部署。开发的时候就要想哪种方式部署简单、能标准化部署、能尽量减少环境依赖。这就是为什么要云原生。\n核心：开发决定了部署 开发的时候，就要想：\n哪种方式部署简单； 哪种方式能标准化部署； 哪种方式能尽量地减少环境依赖，让部署不容易出错。 比如部署 BM，开发的时候不能只想着能把 BM 跑起来，还要想后续部署能否简单、标准化、少定制化配置。\n比如动态启动 pxe server（tftp、dhcpd）和静态配置好这两个服务，是否可行，对于部署哪个友好。\n当然，当前最好的方式当然是云原生化。\n为什么要云原生 部署/交付是一个很重要的事，且很消耗成本，且能算得清的成本的事情。\n所以为什么云原生很重要，它把应用交付的效率大大提高了。\n而开发决定的是部署交付。\n这就是为什么要云原生，就是从第一天开发，就要想着以后是 k8s 上部署交付的。\n如果你不想着 k8s 上部署，像我们屎山代码，各种通过 hostname 点对点通信，各种 ssh 执行命令，各种 exec 执行宿主机的命令，后续很难部署到容器中。\n云计算 vs 云原生 云计算解决的是 IT 资源的交付效率、基础设施的运维； 云原生解决的是软件交付效率、软件生命周期的运维。 小结 开发决定了部署； 开发的时候就要想哪种方式部署简单、能标准化部署、能尽量减少环境依赖； 部署/交付是很消耗成本、且能算得清成本的事情； 云原生把应用交付的效率大大提高了； 从第一天开发，就要想着以后是 k8s 上部署交付的。 ","permalink":"/posts/think-deploy-when-developing/","summary":"\u003cp\u003e开发决定了部署。开发的时候就要想哪种方式部署简单、能标准化部署、能尽量减少环境依赖。这就是为什么要云原生。\u003c/p\u003e\n\u003ch2 id=\"核心开发决定了部署\"\u003e核心：开发决定了部署\u003c/h2\u003e\n\u003cp\u003e开发的时候，就要想：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e哪种方式部署简单；\u003c/li\u003e\n\u003cli\u003e哪种方式能标准化部署；\u003c/li\u003e\n\u003cli\u003e哪种方式能尽量地减少环境依赖，让部署不容易出错。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e比如部署 BM，开发的时候不能只想着能把 BM 跑起来，还要想后续部署能否简单、标准化、少定制化配置。\u003c/p\u003e\n\u003cp\u003e比如动态启动 pxe server（tftp、dhcpd）和静态配置好这两个服务，是否可行，对于部署哪个友好。\u003c/p\u003e\n\u003cp\u003e当然，当前最好的方式当然是云原生化。\u003c/p\u003e\n\u003ch2 id=\"为什么要云原生\"\u003e为什么要云原生\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e部署/交付是一个很重要的事，且很消耗成本，且能算得清的成本的事情。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e所以为什么云原生很重要，它把应用交付的效率大大提高了。\u003c/p\u003e\n\u003cp\u003e而\u003cstrong\u003e开发决定的是部署交付\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e这就是为什么要云原生，就是从第一天开发，就要想着以后是 k8s 上部署交付的。\u003c/p\u003e\n\u003cp\u003e如果你不想着 k8s 上部署，像我们屎山代码，各种通过 hostname 点对点通信，各种 ssh 执行命令，各种 exec 执行宿主机的命令，后续很难部署到容器中。\u003c/p\u003e\n\u003ch2 id=\"云计算-vs-云原生\"\u003e云计算 vs 云原生\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e云计算\u003c/strong\u003e解决的是 IT 资源的交付效率、基础设施的运维；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e云原生\u003c/strong\u003e解决的是软件交付效率、软件生命周期的运维。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e开发决定了部署；\u003c/li\u003e\n\u003cli\u003e开发的时候就要想哪种方式部署简单、能标准化部署、能尽量减少环境依赖；\u003c/li\u003e\n\u003cli\u003e部署/交付是很消耗成本、且能算得清成本的事情；\u003c/li\u003e\n\u003cli\u003e云原生把应用交付的效率大大提高了；\u003c/li\u003e\n\u003cli\u003e从第一天开发，就要想着以后是 k8s 上部署交付的。\u003c/li\u003e\n\u003c/ul\u003e","title":"开发的时候就要想部署交付：为什么要云原生"},{"content":"不仅数据有一致性问题，代码的一致性问题更加遍地都是。判断主机是否运行应该面向 libvirt 接口，而不是面向实现中的文件、过程。\n现象 我们有个查看虚拟机如果异常关闭、将其拉起的机制。\n它判断主机是否在运行的依据是 xml！这非常不对。\n而且在服务启动的时候，会去清理 xml，到处都是零散的变更逻辑。你改一个功能都不知道影响哪些。\n根因：面向实现，而不是面向接口 判断 xml 显然不符合一致性，因为 virsh 才是根本。\n大家都面向 libvirt 接口来判断，就不存在那么多问题了，也不存在要清理 xml，或者说清理 xml 影响不大了。\n典型的面向实现，而不是面向接口。\n事实是我们依赖 libvirt 搭建的 iaas 平台，所以得围绕 libvirt api，而不是实现中的文件、过程。\n面向接口而非面向实现。\n一致性来源 另外，主机属于哪个节点、状态是什么，应该有一个一致性的来源。\n比如 k8s 就是 etcd。\n我们这玩意，自己在那判断然后和数据库做对比，但不是以数据库为准，而是自己判断，美其名是为了根据实际情况避免双写，实际更加复杂和错乱。\ngo 的 defer 就是典型的一致性表现 这种一致性，在哪里创建，就在那里跟着关闭。\n而不是在其他零散的地方关闭，容易忘，也容易乱套。\n小结 代码也有一致性问题，而且更加遍地都是； 判断主机是否运行应该面向 libvirt 接口，而不是面向实现中的文件、过程； 面向接口而非面向实现； 主机属于哪个节点、状态是什么，应该有一个一致性的来源（如 k8s 的 etcd）； go 的 defer 是典型的一致性表现：在哪里创建，就在那里跟着关闭。 ","permalink":"/posts/code-consistency-interface/","summary":"\u003cp\u003e不仅数据有一致性问题，代码的一致性问题更加遍地都是。判断主机是否运行应该面向 libvirt 接口，而不是面向实现中的文件、过程。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e我们有个查看虚拟机如果异常关闭、将其拉起的机制。\u003c/p\u003e\n\u003cp\u003e它判断主机是否在运行的依据是 \u003cstrong\u003exml\u003c/strong\u003e！这非常不对。\u003c/p\u003e\n\u003cp\u003e而且在服务启动的时候，会去清理 xml，到处都是零散的变更逻辑。你改一个功能都不知道影响哪些。\u003c/p\u003e\n\u003ch2 id=\"根因面向实现而不是面向接口\"\u003e根因：面向实现，而不是面向接口\u003c/h2\u003e\n\u003cp\u003e判断 xml 显然不符合一致性，因为 virsh 才是根本。\u003c/p\u003e\n\u003cp\u003e大家都面向 libvirt 接口来判断，就不存在那么多问题了，也不存在要清理 xml，或者说清理 xml 影响不大了。\u003c/p\u003e\n\u003cp\u003e典型的面向实现，而不是面向接口。\u003c/p\u003e\n\u003cp\u003e事实是我们依赖 libvirt 搭建的 iaas 平台，所以得围绕 libvirt api，而不是实现中的文件、过程。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e面向接口而非面向实现。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"一致性来源\"\u003e一致性来源\u003c/h2\u003e\n\u003cp\u003e另外，主机属于哪个节点、状态是什么，应该有一个\u003cstrong\u003e一致性的来源\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e比如 k8s 就是 etcd。\u003c/p\u003e\n\u003cp\u003e我们这玩意，自己在那判断然后和数据库做对比，但不是以数据库为准，而是自己判断，美其名是为了根据实际情况避免双写，实际更加复杂和错乱。\u003c/p\u003e\n\u003ch2 id=\"go-的-defer-就是典型的一致性表现\"\u003ego 的 defer 就是典型的一致性表现\u003c/h2\u003e\n\u003cp\u003e这种一致性，在哪里创建，就在那里跟着关闭。\u003c/p\u003e\n\u003cp\u003e而不是在其他零散的地方关闭，容易忘，也容易乱套。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e代码也有一致性问题，而且更加遍地都是；\u003c/li\u003e\n\u003cli\u003e判断主机是否运行应该面向 libvirt 接口，而不是面向实现中的文件、过程；\u003c/li\u003e\n\u003cli\u003e面向接口而非面向实现；\u003c/li\u003e\n\u003cli\u003e主机属于哪个节点、状态是什么，应该有一个一致性的来源（如 k8s 的 etcd）；\u003c/li\u003e\n\u003cli\u003ego 的 defer 是典型的一致性表现：在哪里创建，就在那里跟着关闭。\u003c/li\u003e\n\u003c/ul\u003e","title":"代码也有一致性：面向接口而非面向实现"},{"content":"接口很多时候听起来很有道理，写的时候会质疑是不是为了接口而接口。一写二抄三重构，前面没有必要的时候不用接口，后面重复的时候可以引入接口。\n现象 接口很多时候听起来很有道理，写的时候会质疑是不是为了接口而接口。\n实际上，一写二抄三重构：\n前面没有必要的时候，不用接口； 后面重复的时候，可以引入接口； 二次重复的时候，就必须要重构了，引入接口是其中一种。 接口作用很大 def disk_build: if PitrixFolder.is_tps_container(): image_name = ctx.tpsc_mgr.get_head_img(instance_id) else: image_name = PitrixFolder.get_head_img(instance_id) 就像存储，存储有多种，我们遍地的 if else，太难维护了，扩展性很差。\n这个时候，如果我们用的是接口：\n那么所有类似 disk_build 的地方，参数都是接口； disk_build 内没有条件判断，只有接口调用； 而调用 disk_build 的时候传入具体的存储类型； 大量的 disk_build，就避免了大量的条件判断。 条件判断不是问题，重复判断才是问题。\n而接口，也可以方便的扩展存储，因为不用去调整大量的 disk_build 函数，只需要在调用 disk_build 的地方传入新的类型。\n接口不是完全避免了新引入类型的工作量，入口处还需要处理，但是依然保证了内部核心流程的稳定性，因为入口处肯定不是核心流程嘛。\n小结 面向接口，一写二抄三重构； 前面没有必要的时候不用接口，后面重复的时候可以引入接口； 二次重复的时候，就必须要重构了； 条件判断不是问题，重复判断才是问题； 接口保证内部核心流程的稳定性，入口处处理新类型即可。 ","permalink":"/posts/interface-oriented/","summary":"\u003cp\u003e接口很多时候听起来很有道理，写的时候会质疑是不是为了接口而接口。一写二抄三重构，前面没有必要的时候不用接口，后面重复的时候可以引入接口。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e接口很多时候听起来很有道理，写的时候会质疑是不是为了接口而接口。\u003c/p\u003e\n\u003cp\u003e实际上，\u003cstrong\u003e一写二抄三重构\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e前面没有必要的时候，不用接口；\u003c/li\u003e\n\u003cli\u003e后面重复的时候，可以引入接口；\u003c/li\u003e\n\u003cli\u003e二次重复的时候，就必须要重构了，引入接口是其中一种。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"接口作用很大\"\u003e接口作用很大\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003edisk_build\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003ePitrixFolder\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eis_tps_container\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eimage_name\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ectx\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etpsc_mgr\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eget_head_img\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einstance_id\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eimage_name\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ePitrixFolder\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eget_head_img\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003einstance_id\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e就像存储，存储有多种，我们遍地的 if else，太难维护了，扩展性很差。\u003c/p\u003e\n\u003cp\u003e这个时候，如果我们用的是接口：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e那么所有类似 disk_build 的地方，参数都是接口；\u003c/li\u003e\n\u003cli\u003edisk_build 内没有条件判断，只有接口调用；\u003c/li\u003e\n\u003cli\u003e而调用 disk_build 的时候传入具体的存储类型；\u003c/li\u003e\n\u003cli\u003e大量的 disk_build，就避免了大量的条件判断。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e条件判断不是问题，重复判断才是问题。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e而接口，也可以方便的扩展存储，因为不用去调整大量的 disk_build 函数，只需要在调用 disk_build 的地方传入新的类型。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e接口不是完全避免了新引入类型的工作量，入口处还需要处理，但是依然保证了内部核心流程的稳定性\u003c/strong\u003e，因为入口处肯定不是核心流程嘛。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e面向接口，一写二抄三重构；\u003c/li\u003e\n\u003cli\u003e前面没有必要的时候不用接口，后面重复的时候可以引入接口；\u003c/li\u003e\n\u003cli\u003e二次重复的时候，就必须要重构了；\u003c/li\u003e\n\u003cli\u003e条件判断不是问题，重复判断才是问题；\u003c/li\u003e\n\u003cli\u003e接口保证内部核心流程的稳定性，入口处处理新类型即可。\u003c/li\u003e\n\u003c/ul\u003e","title":"面向接口，面向接口，面向接口"},{"content":"封装仅仅是为了复用，但是独立有管理能力。封装调用了就得执行，独立有自己的一套管理规则，比如排队、限流、调整优先级、丢弃、转换等。\n核心：封装 vs 独立 封装仅仅是为了复用，但是独立有管理能力。\n封装：调用了就得执行，里面通过各种条件逻辑控制行为； 独立：有自己的一套管理能力，有自己的一套规则。 请求来了，不是立马执行逻辑，而是先进管理规则。\n比如排队、限流、调整优先级、丢弃、转换等等。\n独立不一定要独立服务 独立并不是说要一个独立服务，服务内的一个模块、一个库，都可以是独立的。\n一个例子：report 服务 应该有个独立的 report。\n如果是 report 函数，大家分散调用，调用的频率不一样：\nA 30 秒调用一次； B 20 秒调用一次； \u0026hellip; 这玩意没法统一处理，调整和排查的时候也麻烦。加一个监控项的时候也很麻烦，触发的地方、触发的间隔、获取数据都需要考量。\n如果像 k8s 一样统一上报，比如 kubelet 收集 node 信息上报给 k8s，至于收集哪些监控项，kubelet 再提供相应的接口，插件对接 kubelet 的接口即可。\n这样的好处是，扩展仅仅是本地收集的处理，上报流程不需要考虑，甚至不需要知道。\n核心还是提取不变的，封装变化的。\n小结 封装仅仅是为了复用，独立有管理能力； 封装调用了就得执行，独立有自己的一套管理规则； 独立不一定要独立服务，服务内的模块、库都可以是独立的； 统一管理：其他地方过来调用仅仅是一个请求，什么时候调用、如何调用、什么优先级，有自己的管理； 核心是提取不变的，封装变化的。 ","permalink":"/posts/encapsulation-vs-independent/","summary":"\u003cp\u003e封装仅仅是为了复用，但是独立有管理能力。封装调用了就得执行，独立有自己的一套管理规则，比如排队、限流、调整优先级、丢弃、转换等。\u003c/p\u003e\n\u003ch2 id=\"核心封装-vs-独立\"\u003e核心：封装 vs 独立\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e封装仅仅是为了复用，但是独立有管理能力。\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e封装\u003c/strong\u003e：调用了就得执行，里面通过各种条件逻辑控制行为；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e独立\u003c/strong\u003e：有自己的一套管理能力，有自己的一套规则。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e请求来了，不是立马执行逻辑，而是先进管理规则。\u003c/p\u003e\n\u003cp\u003e比如排队、限流、调整优先级、丢弃、转换等等。\u003c/p\u003e\n\u003ch2 id=\"独立不一定要独立服务\"\u003e独立不一定要独立服务\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e独立并不是说要一个独立服务，服务内的一个模块、一个库，都可以是独立的。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"一个例子report-服务\"\u003e一个例子：report 服务\u003c/h2\u003e\n\u003cp\u003e应该有个独立的 report。\u003c/p\u003e\n\u003cp\u003e如果是 report 函数，大家分散调用，调用的频率不一样：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eA 30 秒调用一次；\u003c/li\u003e\n\u003cli\u003eB 20 秒调用一次；\u003c/li\u003e\n\u003cli\u003e\u0026hellip;\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这玩意没法统一处理，调整和排查的时候也麻烦。加一个监控项的时候也很麻烦，触发的地方、触发的间隔、获取数据都需要考量。\u003c/p\u003e\n\u003cp\u003e如果像 k8s 一样统一上报，比如 kubelet 收集 node 信息上报给 k8s，至于收集哪些监控项，kubelet 再提供相应的接口，插件对接 kubelet 的接口即可。\u003c/p\u003e\n\u003cp\u003e这样的好处是，\u003cstrong\u003e扩展仅仅是本地收集的处理，上报流程不需要考虑，甚至不需要知道。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e核心还是\u003cstrong\u003e提取不变的，封装变化的\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e封装仅仅是为了复用，独立有管理能力；\u003c/li\u003e\n\u003cli\u003e封装调用了就得执行，独立有自己的一套管理规则；\u003c/li\u003e\n\u003cli\u003e独立不一定要独立服务，服务内的模块、库都可以是独立的；\u003c/li\u003e\n\u003cli\u003e统一管理：其他地方过来调用仅仅是一个请求，什么时候调用、如何调用、什么优先级，有自己的管理；\u003c/li\u003e\n\u003cli\u003e核心是提取不变的，封装变化的。\u003c/li\u003e\n\u003c/ul\u003e","title":"封装和独立的区别：复用 vs 管理能力"},{"content":"底层（被依赖者）最好有默认值，上层（依赖者）可以少硬编码。判断不是问题，问题是重复判断；硬编码本身是必须的，问题是重复的硬编码。\n现象 比如：\ndef get_storage_health(): \u0026#34;\u0026#34;\u0026#34; return storage health info: \u0026#39;healthy\u0026#39;/\u0026#39;unhealthy\u0026#39; \u0026#34;\u0026#34;\u0026#34; if not sub_container.is_tps_container(): # Other storage types do not have this check, default healthy return \u0026#34;healthy\u0026#34; 这样上层就不需要硬编码判断是什么存储类型了，只要判断 unhealthy 做处理即可。\n核心：底层默认值，上层少硬编码 底层（被依赖者）最好有默认值，上层（依赖者）可以少硬编码。\n而 get_storage_health 的硬编码是必不可少的，因为这里本身就是触发的地方，且其他类型本身就不支持此操作。\n判断不是问题，重复判断才是问题 这就是说的**\u0026ldquo;判断不是问题，问题是重复判断\u0026rdquo;**。\n这里硬编码其实也一样：\n硬编码本身是必须的； 问题是重复的硬编码； 况且这里的硬编码就是判断。 小结 底层（被依赖者）最好有默认值，上层（依赖者）可以少硬编码； 判断不是问题，问题是重复判断； 硬编码本身是必须的，问题是重复的硬编码； 底层默认值让上层只需要处理异常情况，逻辑更简单。 ","permalink":"/posts/bottom-default-value/","summary":"\u003cp\u003e底层（被依赖者）最好有默认值，上层（依赖者）可以少硬编码。判断不是问题，问题是重复判断；硬编码本身是必须的，问题是重复的硬编码。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e比如：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_storage_health\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e    return storage health info:\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e    \u0026#39;healthy\u0026#39;/\u0026#39;unhealthy\u0026#39;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003esub_container\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eis_tps_container\u003c/span\u003e\u003cspan class=\"p\"\u003e():\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e# Other storage types do not have this check, default healthy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;healthy\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这样上层就不需要硬编码判断是什么存储类型了，只要判断 unhealthy 做处理即可。\u003c/p\u003e\n\u003ch2 id=\"核心底层默认值上层少硬编码\"\u003e核心：底层默认值，上层少硬编码\u003c/h2\u003e\n\u003cp\u003e底层（被依赖者）最好有默认值，上层（依赖者）可以少硬编码。\u003c/p\u003e\n\u003cp\u003e而 \u003ccode\u003eget_storage_health\u003c/code\u003e 的硬编码是必不可少的，因为这里本身就是触发的地方，且其他类型本身就不支持此操作。\u003c/p\u003e\n\u003ch2 id=\"判断不是问题重复判断才是问题\"\u003e判断不是问题，重复判断才是问题\u003c/h2\u003e\n\u003cp\u003e这就是说的**\u0026ldquo;判断不是问题，问题是重复判断\u0026rdquo;**。\u003c/p\u003e\n\u003cp\u003e这里硬编码其实也一样：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e硬编码本身是必须的；\u003c/li\u003e\n\u003cli\u003e问题是重复的硬编码；\u003c/li\u003e\n\u003cli\u003e况且这里的硬编码就是判断。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e底层（被依赖者）最好有默认值，上层（依赖者）可以少硬编码；\u003c/li\u003e\n\u003cli\u003e判断不是问题，问题是重复判断；\u003c/li\u003e\n\u003cli\u003e硬编码本身是必须的，问题是重复的硬编码；\u003c/li\u003e\n\u003cli\u003e底层默认值让上层只需要处理异常情况，逻辑更简单。\u003c/li\u003e\n\u003c/ul\u003e","title":"底层最好有默认值，上层可以少硬编码"},{"content":"不要封装太多的实现方法，会造成使用复杂度。一个目的提供一种方法，提供的太多，反而让使用者陷入学习复杂性。\n现象 写一个方法要到处去找合适的调用，挺难受。\n尤其是 sql 查询的方法。\n为什么会有这种感觉：\n确实是项目大小导致的复杂性区别； 确实是归类的约束不强，导致有些零散； 写一个新的成本太大了，少即是多，只提供一种实现方法。 核心：一个目的一种实现 不要封装太多的实现方法，会造成使用复杂度。\n一个目的提供一种方法，提供的太多，反而让使用者陷入学习复杂性。\n良好的 util 封装 高内聚：一类方法放在一起，不然难找，也不知道想要的有没有、需不需要新写一个。比如 sql 一个文件，qemu-img 一个文件； 使用文档：必须的，要不然找代码效率很低； 一个目的提供一种方法：提供的太多，反而让使用者陷入学习复杂性。 小结 一个目的一种实现，少即是多； 不要封装太多的实现方法，会造成使用复杂度； 良好的 util 封装：高内聚、使用文档、一个目的一种方法； 提供的太多，反而让使用者陷入学习复杂性。 ","permalink":"/posts/one-goal-one-implementation/","summary":"\u003cp\u003e不要封装太多的实现方法，会造成使用复杂度。一个目的提供一种方法，提供的太多，反而让使用者陷入学习复杂性。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e写一个方法要到处去找合适的调用，挺难受。\u003c/p\u003e\n\u003cp\u003e尤其是 sql 查询的方法。\u003c/p\u003e\n\u003cp\u003e为什么会有这种感觉：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e确实是项目大小导致的复杂性区别；\u003c/li\u003e\n\u003cli\u003e确实是归类的约束不强，导致有些零散；\u003c/li\u003e\n\u003cli\u003e写一个新的成本太大了，少即是多，只提供一种实现方法。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"核心一个目的一种实现\"\u003e核心：一个目的一种实现\u003c/h2\u003e\n\u003cp\u003e不要封装太多的实现方法，会造成使用复杂度。\u003c/p\u003e\n\u003cp\u003e一个目的提供一种方法，提供的太多，反而让使用者陷入学习复杂性。\u003c/p\u003e\n\u003ch2 id=\"良好的-util-封装\"\u003e良好的 util 封装\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e高内聚\u003c/strong\u003e：一类方法放在一起，不然难找，也不知道想要的有没有、需不需要新写一个。比如 sql 一个文件，qemu-img 一个文件；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e使用文档\u003c/strong\u003e：必须的，要不然找代码效率很低；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e一个目的提供一种方法\u003c/strong\u003e：提供的太多，反而让使用者陷入学习复杂性。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e一个目的一种实现，少即是多；\u003c/li\u003e\n\u003cli\u003e不要封装太多的实现方法，会造成使用复杂度；\u003c/li\u003e\n\u003cli\u003e良好的 util 封装：高内聚、使用文档、一个目的一种方法；\u003c/li\u003e\n\u003cli\u003e提供的太多，反而让使用者陷入学习复杂性。\u003c/li\u003e\n\u003c/ul\u003e","title":"一个目的一种实现：少即是多"},{"content":"如果一个工具仅仅在一两个地方调用，那无所谓封装。如果一个工具通篇各处都在调用，那就一定要封装了，即便这个工具仅仅是一行命令。\n核心观点 这也是上面 qemu-img 要封装的一个原因。\n如果一个工具仅仅在一两个地方调用了，那无所谓封装。\n如果一个工具通篇各处都在调用，那就一定要封装了。即便这个工具仅仅是一行命令。\n比如我们的 qemu-img、virsh、sql。\n为什么 因为使用频率高，意味着：\n出问题的概率高，需要统一处理； 版本升级的影响面大，需要统一应对； 日志、统计、重试等切面能力需要统一提供。 小结 封装不在于使用复杂度，而在于使用频率； 一个工具仅在一两个地方调用，无所谓封装； 一个工具通篇各处都在调用，一定要封装，即便只是一行命令； 使用频率高，出问题概率高、影响面大，需要统一处理。 ","permalink":"/posts/encapsulation-frequency-not-complexity/","summary":"\u003cp\u003e如果一个工具仅仅在一两个地方调用，那无所谓封装。如果一个工具通篇各处都在调用，那就一定要封装了，即便这个工具仅仅是一行命令。\u003c/p\u003e\n\u003ch2 id=\"核心观点\"\u003e核心观点\u003c/h2\u003e\n\u003cp\u003e这也是上面 qemu-img 要封装的一个原因。\u003c/p\u003e\n\u003cp\u003e如果一个工具仅仅在一两个地方调用了，那无所谓封装。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e如果一个工具通篇各处都在调用，那就一定要封装了。即便这个工具仅仅是一行命令。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e比如我们的 qemu-img、virsh、sql。\u003c/p\u003e\n\u003ch2 id=\"为什么\"\u003e为什么\u003c/h2\u003e\n\u003cp\u003e因为使用频率高，意味着：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e出问题的概率高，需要统一处理；\u003c/li\u003e\n\u003cli\u003e版本升级的影响面大，需要统一应对；\u003c/li\u003e\n\u003cli\u003e日志、统计、重试等切面能力需要统一提供。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e封装不在于使用复杂度，而在于使用频率；\u003c/li\u003e\n\u003cli\u003e一个工具仅在一两个地方调用，无所谓封装；\u003c/li\u003e\n\u003cli\u003e一个工具通篇各处都在调用，一定要封装，即便只是一行命令；\u003c/li\u003e\n\u003cli\u003e使用频率高，出问题概率高、影响面大，需要统一处理。\u003c/li\u003e\n\u003c/ul\u003e","title":"封装不在于使用复杂度，而在于使用频率"},{"content":"调用第三方工具一定要封装，哪怕仅仅是一个命令行。封装的好处：应对版本变化、方便做切面、简化使用、提供统一方法。\n现象 qemu-img 是一个典型的例子。\n为啥要封装？\n底层工具版本变化导致的参数校验变化； 方便做切面，可以做一些统计、日志、重试、timeout 等； 简化使用，底层工具参数众多，大部分都用不上； 提供实现目的的统一方法，少即是多，实现一个目的最好只有一种方法。 封装得好是什么样 qemu-img 自认为封装的挺好。\n对于一些版本不支持的参数，明确打印出来了，通过 error 日志一眼看出问题，这效率，QA 也能排查了。\n比如：\nlogger.error(\u0026#34;QemuImg commit: qemu-img commit [-d] not supported\u0026#34;) 一个封装示例 class QemuImg(object): \u0026#34;\u0026#34;\u0026#34; qemu-img Package \u0026#34;\u0026#34;\u0026#34; @classmethod def commit_str(cls, filename, image_opts=None, fmt=None, skip_empty=False, host=None, ): \u0026#34;\u0026#34;\u0026#34; qemu-img commit [--object objectdef] [--image-opts] [-q] [-f fmt] [-t cache] [-b base] [-d] [-p] filename The image FILENAME is emptied after the operation has succeeded. If you do not need FILENAME afterwards and intend to drop it, you may skip emptying FILENAME by specifying the -d flag. \u0026#34;\u0026#34;\u0026#34; if fmt and fmt not in [\u0026#34;raw\u0026#34;, \u0026#34;qcow2\u0026#34;]: logger.error(\u0026#34;QemuImg commit: unsupported format: %s\u0026#34; % fmt) return None if image_opts and not isinstance(image_opts, dict): logger.error(\u0026#34;QemuImg commit: image_opts must be a dict\u0026#34;) return None if skip_empty: has_d_cmd = r\u0026#39;qemu-img -h | grep commit | grep \u0026#34;\\-d\u0026#34;\u0026#39; ret = exec_cmd(has_d_cmd, remote_host=host) if not is_cmd_ok(ret): logger.error(\u0026#34;QemuImg commit: qemu-img commit [-d] not supported\u0026#34;) return None cmd_str = \u0026#34;qemu-img commit\u0026#34; if fmt: cmd_str += \u0026#34; -f %s\u0026#34; % fmt if skip_empty: cmd_str += \u0026#34; -d\u0026#34; if image_opts: image_opts_str_li = [\u0026#34;%s=%s\u0026#34; % (key, value) for key, value in image_opts.items()] if image_opts_str_li: cmd_str += \u0026#34; --image-opts %s\u0026#34; % \u0026#34;,\u0026#34;.join(image_opts_str_li) if filename: cmd_str += \u0026#34; %s\u0026#34; % filename logger.info(\u0026#34;QemuImg commit cmd: %s\u0026#34;, cmd_str) return cmd_str 小结 调用第三方工具一定要封装，哪怕仅仅是一个命令行； 封装应对版本变化、方便做切面、简化使用、提供统一方法； 对于版本不支持的参数，明确打印出来，QA 也能排查； 少即是多，实现一个目的最好只有一种方法。 ","permalink":"/posts/wrap-third-party-call/","summary":"\u003cp\u003e调用第三方工具一定要封装，哪怕仅仅是一个命令行。封装的好处：应对版本变化、方便做切面、简化使用、提供统一方法。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003eqemu-img 是一个典型的例子。\u003c/p\u003e\n\u003cp\u003e为啥要封装？\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e底层工具版本变化\u003c/strong\u003e导致的参数校验变化；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e方便做切面\u003c/strong\u003e，可以做一些统计、日志、重试、timeout 等；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e简化使用\u003c/strong\u003e，底层工具参数众多，大部分都用不上；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e提供实现目的的统一方法\u003c/strong\u003e，少即是多，实现一个目的最好只有一种方法。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"封装得好是什么样\"\u003e封装得好是什么样\u003c/h2\u003e\n\u003cp\u003eqemu-img 自认为封装的挺好。\u003c/p\u003e\n\u003cp\u003e对于一些版本不支持的参数，明确打印出来了，通过 error 日志一眼看出问题，这效率，QA 也能排查了。\u003c/p\u003e\n\u003cp\u003e比如：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;QemuImg commit: qemu-img commit [-d] not supported\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"一个封装示例\"\u003e一个封装示例\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eQemuImg\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e    qemu-img Package\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e    \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nd\"\u003e@classmethod\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003ecommit_str\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"bp\"\u003ecls\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                   \u003cspan class=\"n\"\u003efilename\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                   \u003cspan class=\"n\"\u003eimage_opts\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                   \u003cspan class=\"n\"\u003efmt\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                   \u003cspan class=\"n\"\u003eskip_empty\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"kc\"\u003eFalse\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                   \u003cspan class=\"n\"\u003ehost\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                   \u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"s2\"\u003e\u0026#34;\u0026#34;\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e        qemu-img commit [--object objectdef] [--image-opts] [-q]\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e          [-f fmt] [-t cache] [-b base] [-d] [-p] filename\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e        The image FILENAME is emptied after the operation has succeeded.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e        If you do not need FILENAME afterwards and intend to drop it,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e        you may skip emptying FILENAME by specifying the -d flag.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"s2\"\u003e        \u0026#34;\u0026#34;\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003efmt\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"n\"\u003efmt\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;raw\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;qcow2\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e]:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;QemuImg commit: unsupported format: \u003c/span\u003e\u003cspan class=\"si\"\u003e%s\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e%\u003c/span\u003e \u003cspan class=\"n\"\u003efmt\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eimage_opts\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"nb\"\u003eisinstance\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eimage_opts\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"nb\"\u003edict\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;QemuImg commit: image_opts must be a dict\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eskip_empty\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ehas_d_cmd\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"sa\"\u003er\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;qemu-img -h | grep commit | grep \u0026#34;\\-d\u0026#34;\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eret\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eexec_cmd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ehas_d_cmd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eremote_host\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"n\"\u003ehost\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003eis_cmd_ok\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eret\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;QemuImg commit: qemu-img commit [-d] not supported\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"kc\"\u003eNone\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003ecmd_str\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;qemu-img commit\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003efmt\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ecmd_str\u003c/span\u003e \u003cspan class=\"o\"\u003e+=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34; -f \u003c/span\u003e\u003cspan class=\"si\"\u003e%s\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e%\u003c/span\u003e \u003cspan class=\"n\"\u003efmt\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eskip_empty\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ecmd_str\u003c/span\u003e \u003cspan class=\"o\"\u003e+=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34; -d\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eimage_opts\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003eimage_opts_str_li\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"si\"\u003e%s\u003c/span\u003e\u003cspan class=\"s2\"\u003e=\u003c/span\u003e\u003cspan class=\"si\"\u003e%s\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e%\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003ekey\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003evalue\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003eimage_opts\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eitems\u003c/span\u003e\u003cspan class=\"p\"\u003e()]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eimage_opts_str_li\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003ecmd_str\u003c/span\u003e \u003cspan class=\"o\"\u003e+=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34; --image-opts \u003c/span\u003e\u003cspan class=\"si\"\u003e%s\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e%\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;,\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ejoin\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eimage_opts_str_li\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003efilename\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003ecmd_str\u003c/span\u003e \u003cspan class=\"o\"\u003e+=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34; \u003c/span\u003e\u003cspan class=\"si\"\u003e%s\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e \u003cspan class=\"o\"\u003e%\u003c/span\u003e \u003cspan class=\"n\"\u003efilename\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einfo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;QemuImg commit cmd: \u003c/span\u003e\u003cspan class=\"si\"\u003e%s\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecmd_str\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003ecmd_str\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e调用第三方工具一定要封装，哪怕仅仅是一个命令行；\u003c/li\u003e\n\u003cli\u003e封装应对版本变化、方便做切面、简化使用、提供统一方法；\u003c/li\u003e\n\u003cli\u003e对于版本不支持的参数，明确打印出来，QA 也能排查；\u003c/li\u003e\n\u003cli\u003e少即是多，实现一个目的最好只有一种方法。\u003c/li\u003e\n\u003c/ul\u003e","title":"调用第三方一定要封装，哪怕仅仅是一个命令行"},{"content":"要不要抽象，关键在于要不要复用。业务流程都是独立的，不需要复用；技术类流程需要复用。一写二抄三重构，第二次大量重复时就可以开始重构了。\n现象 写代码的时候，总纠结要不要抽象流程、抽象对象、面向组合。\n这样很难，因为没有明确的复用需求，不知道稳定点在哪。\n核心：一写二抄三重构 一写，二抄，三重构。\n要不要抽象，关键在于要不要复用。\n业务流程都是独立的，不需要复用，所以谈不上抽象； 技术类的流程，是需要复用的。 但是没必要第一次就绞尽脑汁抽象流程、抽象对象、面向组合。\n第二次是抄还是重构 不是说第二次就一定是抄。\n如果是一个方法，抄没问题； 如果是一个流程，一堆方法，第二次抄就不合理了。 这样的后果是第三次重构非常困难，甚至会因为功能的兼容影响没法进行。\n如果是大量的话，第二次就可以开始重构了。\n抽象流程 vs 抽象对象 k8s 为什么牛逼，原因在于它定义了一套声明式的流程。\n我们创建主机、创建备份等，看起来流程不一样，但是上升一个维度，都能用声明式的流程来处理。\n但 2026 年更新后的认知是：k8s 是极致的面向对象。\n虽然它的控制器是面向流程的，但是控制器仅仅是对象下的血肉。\n顶层还是围绕对象设计的； 控制器是属于对象的； k8s 极致面向对象，消除流程，将流程隐藏在对象中。 流程是易变的，对象是相对稳定的，所以不会去抽象流程，而是抽象对象。\n一个例子：采集 pv-lv 和 lv info 数据采集、数据同步，涉及到新增和删除。\n所以这里的流程最后抽象为：\n采集； 从 db 获取； diff 得到 insert 和 delete； 执行 insert 和 delete。 不同的数据对象，提供支持 diff、insert 和 delete 的方法即可。\n这个流程也不是一开始就想出来的，也是边写、边思考做出来的。\n小结 要不要抽象，关键在于要不要复用； 业务流程独立，不需要复用；技术类流程需要复用； 一写二抄三重构，第二次大量重复时就可以开始重构； 流程是易变的，对象是相对稳定的，所以抽象对象而非流程； 好的抽象是边写边思考做出来的，不是一开始就设计好的。 ","permalink":"/posts/abstract-flow-vs-object/","summary":"\u003cp\u003e要不要抽象，关键在于要不要复用。业务流程都是独立的，不需要复用；技术类流程需要复用。一写二抄三重构，第二次大量重复时就可以开始重构了。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e写代码的时候，总纠结要不要抽象流程、抽象对象、面向组合。\u003c/p\u003e\n\u003cp\u003e这样很难，因为没有明确的复用需求，不知道稳定点在哪。\u003c/p\u003e\n\u003ch2 id=\"核心一写二抄三重构\"\u003e核心：一写二抄三重构\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e一写，二抄，三重构。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e要不要抽象，关键在于要不要复用。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e业务流程\u003c/strong\u003e都是独立的，不需要复用，所以谈不上抽象；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e技术类的流程\u003c/strong\u003e，是需要复用的。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e但是没必要第一次就绞尽脑汁抽象流程、抽象对象、面向组合。\u003c/p\u003e\n\u003ch3 id=\"第二次是抄还是重构\"\u003e第二次是抄还是重构\u003c/h3\u003e\n\u003cp\u003e不是说第二次就一定是抄。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e如果是一个\u003cstrong\u003e方法\u003c/strong\u003e，抄没问题；\u003c/li\u003e\n\u003cli\u003e如果是一个\u003cstrong\u003e流程\u003c/strong\u003e，一堆方法，第二次抄就不合理了。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这样的后果是第三次重构非常困难，甚至会因为功能的兼容影响没法进行。\u003c/p\u003e\n\u003cp\u003e如果是\u003cstrong\u003e大量的话，第二次就可以开始重构了\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"抽象流程-vs-抽象对象\"\u003e抽象流程 vs 抽象对象\u003c/h2\u003e\n\u003cp\u003ek8s 为什么牛逼，原因在于它定义了一套声明式的流程。\u003c/p\u003e\n\u003cp\u003e我们创建主机、创建备份等，看起来流程不一样，但是上升一个维度，都能用声明式的流程来处理。\u003c/p\u003e\n\u003cp\u003e但 2026 年更新后的认知是：\u003cstrong\u003ek8s 是极致的面向对象\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e虽然它的控制器是面向流程的，但是控制器仅仅是对象下的血肉。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e顶层还是围绕对象设计的；\u003c/li\u003e\n\u003cli\u003e控制器是属于对象的；\u003c/li\u003e\n\u003cli\u003ek8s 极致面向对象，\u003cstrong\u003e消除流程，将流程隐藏在对象中\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e流程是易变的，对象是相对稳定的，所以不会去抽象流程，而是抽象对象。\u003c/p\u003e\n\u003ch2 id=\"一个例子采集-pv-lv-和-lv-info\"\u003e一个例子：采集 pv-lv 和 lv info\u003c/h2\u003e\n\u003cp\u003e数据采集、数据同步，涉及到新增和删除。\u003c/p\u003e\n\u003cp\u003e所以这里的流程最后抽象为：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e采集；\u003c/li\u003e\n\u003cli\u003e从 db 获取；\u003c/li\u003e\n\u003cli\u003ediff 得到 insert 和 delete；\u003c/li\u003e\n\u003cli\u003e执行 insert 和 delete。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e不同的数据对象，提供支持 diff、insert 和 delete 的方法即可。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e这个流程也不是一开始就想出来的，也是边写、边思考做出来的。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e要不要抽象，关键在于要不要复用；\u003c/li\u003e\n\u003cli\u003e业务流程独立，不需要复用；技术类流程需要复用；\u003c/li\u003e\n\u003cli\u003e一写二抄三重构，第二次大量重复时就可以开始重构；\u003c/li\u003e\n\u003cli\u003e流程是易变的，对象是相对稳定的，所以抽象对象而非流程；\u003c/li\u003e\n\u003cli\u003e好的抽象是边写边思考做出来的，不是一开始就设计好的。\u003c/li\u003e\n\u003c/ul\u003e","title":"抽象流程还是抽象对象？一写二抄三重构"},{"content":"写代码高价值套路：代码写三遍。第一遍跑通，第二遍防御，第三遍优化。过早的优化是万恶之源，尤其是新服务新项目的时候。\n现象 写代码的时候，总想一步到位。\n前期查看各种基础知识，耗费时间；后面组织结构，想日志封装、调度封装，又耗费时间比较多。\n结果两周没写完。\n核心：代码写三遍 来自华哥（李运华）的写代码高价值套路原则：\n防御性编程； 代码写三遍； 快速失败； 面向对象原则； 短函数。 其中\u0026quot;代码写三遍\u0026quot;是核心：\n跑通：先把功能跑通； 防御：加上防御性编程； 优化：最后再优化。 过早的优化是万恶之源。尤其是在新服务新项目的时候。\n为什么写三遍反而更快 比如这次的\u0026quot;自愈组件\u0026quot;。\n如果按照写三遍：\n跑通，最多 4 天； 异常场景最多 1 天； 后面针对性优化、针对性学习，学习效率高。 比如先针对学习日志，针对优化调度，这个过程中会思考是否过度优化，其实发现很多没必要。\n关键这是一个快速交付的路径，很快有结果，没有汇报压力。\n没有压力，有条不紊，是非常重要的一个因素。\n防御性编程 防御性编程包括：\n安全：边界、非法值校验、特殊情况、异常情况等； 快速失败、回滚等； 追踪：日志等。 小结 代码写三遍：跑通、防御、优化； 过早的优化是万恶之源，尤其是新服务新项目； 写三遍反而更快，因为快速交付、没有汇报压力； 没有压力、有条不紊，是非常重要的因素； 防御性编程：安全、快速失败、追踪。 ","permalink":"/posts/code-write-three-times/","summary":"\u003cp\u003e写代码高价值套路：代码写三遍。第一遍跑通，第二遍防御，第三遍优化。过早的优化是万恶之源，尤其是新服务新项目的时候。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e写代码的时候，总想一步到位。\u003c/p\u003e\n\u003cp\u003e前期查看各种基础知识，耗费时间；后面组织结构，想日志封装、调度封装，又耗费时间比较多。\u003c/p\u003e\n\u003cp\u003e结果两周没写完。\u003c/p\u003e\n\u003ch2 id=\"核心代码写三遍\"\u003e核心：代码写三遍\u003c/h2\u003e\n\u003cp\u003e来自华哥（李运华）的写代码高价值套路原则：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e防御性编程；\u003c/li\u003e\n\u003cli\u003e代码写三遍；\u003c/li\u003e\n\u003cli\u003e快速失败；\u003c/li\u003e\n\u003cli\u003e面向对象原则；\u003c/li\u003e\n\u003cli\u003e短函数。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e其中\u0026quot;代码写三遍\u0026quot;是核心：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e跑通\u003c/strong\u003e：先把功能跑通；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e防御\u003c/strong\u003e：加上防御性编程；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e优化\u003c/strong\u003e：最后再优化。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e过早的优化是万恶之源。尤其是在新服务新项目的时候。\u003c/p\u003e\n\u003ch2 id=\"为什么写三遍反而更快\"\u003e为什么写三遍反而更快\u003c/h2\u003e\n\u003cp\u003e比如这次的\u0026quot;自愈组件\u0026quot;。\u003c/p\u003e\n\u003cp\u003e如果按照写三遍：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e跑通，最多 4 天；\u003c/li\u003e\n\u003cli\u003e异常场景最多 1 天；\u003c/li\u003e\n\u003cli\u003e后面针对性优化、针对性学习，学习效率高。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e比如先针对学习日志，针对优化调度，这个过程中会思考是否过度优化，其实发现很多没必要。\u003c/p\u003e\n\u003cp\u003e关键这是一个快速交付的路径，很快有结果，没有汇报压力。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e没有压力，有条不紊，是非常重要的一个因素。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"防御性编程\"\u003e防御性编程\u003c/h2\u003e\n\u003cp\u003e防御性编程包括：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e安全\u003c/strong\u003e：边界、非法值校验、特殊情况、异常情况等；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e快速失败、回滚\u003c/strong\u003e等；\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e追踪\u003c/strong\u003e：日志等。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e代码写三遍：跑通、防御、优化；\u003c/li\u003e\n\u003cli\u003e过早的优化是万恶之源，尤其是新服务新项目；\u003c/li\u003e\n\u003cli\u003e写三遍反而更快，因为快速交付、没有汇报压力；\u003c/li\u003e\n\u003cli\u003e没有压力、有条不紊，是非常重要的因素；\u003c/li\u003e\n\u003cli\u003e防御性编程：安全、快速失败、追踪。\u003c/li\u003e\n\u003c/ul\u003e","title":"代码写三遍：跑通、防御、优化"},{"content":"报 no space left on device，看上去是磁盘空间不足，但有可能不是存储满了，而是 inode 满了。用 df -i 查看 inode 使用情况，如果有 100% 的就是 inode 满了。\n现象 报 no space left on device，看上去是磁盘空间不足了，那么找没用的文件删了即可。\n但有可能不是存储满了，而是 inode 满了。\n排查 使用 df -i 查看 inode 的使用情况，如果有 100% 的，那就说明是 inode 满了：\nroot@host:~# df -i Filesystem Inodes IUsed IFree IUse% Mounted on udev 16059428 1017 16058411 1% /dev tmpfs 16078645 1812 16076833 1% /run /dev/sda2 3276800 154963 3121837 5% / tmpfs 16078645 4 16078641 1% /dev/shm tmpfs 16078645 13 16078632 1% /run/lock tmpfs 16078645 18 16078627 1% /sys/fs/cgroup /dev/sda4 31162368 4308 31158060 1% /data /dev/sdb1 182939648 564 182939084 1% /data/container 然后查找哪里小文件特别多。\n解决办法 删除没用的文件，释放出分区的一部分 inode；\n用软连接将空闲分区 /opt 中的目录连接到 inode 紧张的目录，使用 /opt 分区的 inode 来缓解 /data 分区 inode 不足的问题：\nln -s /opt/newcache /data/cache 更换服务器，用高配置的服务器替换低配置的服务器。\n很多时候用钱去解决问题比用技术更有效。一般不建议。\n小结 no space left on device 不一定是磁盘满了，可能是 inode 满了； 用 df -i 查看 inode 使用情况，有 100% 的就是 inode 满了； 小文件特别多会耗尽 inode； 解决办法：删除没用的文件、用软连接借用其他分区的 inode、更换高配置服务器。 ","permalink":"/posts/no-space-left-inode-full/","summary":"\u003cp\u003e报 \u003ccode\u003eno space left on device\u003c/code\u003e，看上去是磁盘空间不足，但有可能不是存储满了，而是 inode 满了。用 \u003ccode\u003edf -i\u003c/code\u003e 查看 inode 使用情况，如果有 100% 的就是 inode 满了。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e报 \u003ccode\u003eno space left on device\u003c/code\u003e，看上去是磁盘空间不足了，那么找没用的文件删了即可。\u003c/p\u003e\n\u003cp\u003e但有可能不是存储满了，而是 inode 满了。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e使用 \u003ccode\u003edf -i\u003c/code\u003e 查看 inode 的使用情况，如果有 100% 的，那就说明是 inode 满了：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eroot@host:~# df -i\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eFilesystem        Inodes  IUsed     IFree IUse% Mounted on\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eudev            \u003cspan class=\"m\"\u003e16059428\u003c/span\u003e   \u003cspan class=\"m\"\u003e1017\u003c/span\u003e  \u003cspan class=\"m\"\u003e16058411\u003c/span\u003e    1% /dev\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etmpfs           \u003cspan class=\"m\"\u003e16078645\u003c/span\u003e   \u003cspan class=\"m\"\u003e1812\u003c/span\u003e  \u003cspan class=\"m\"\u003e16076833\u003c/span\u003e    1% /run\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/dev/sda2        \u003cspan class=\"m\"\u003e3276800\u003c/span\u003e \u003cspan class=\"m\"\u003e154963\u003c/span\u003e   \u003cspan class=\"m\"\u003e3121837\u003c/span\u003e    5% /\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etmpfs           \u003cspan class=\"m\"\u003e16078645\u003c/span\u003e      \u003cspan class=\"m\"\u003e4\u003c/span\u003e  \u003cspan class=\"m\"\u003e16078641\u003c/span\u003e    1% /dev/shm\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etmpfs           \u003cspan class=\"m\"\u003e16078645\u003c/span\u003e     \u003cspan class=\"m\"\u003e13\u003c/span\u003e  \u003cspan class=\"m\"\u003e16078632\u003c/span\u003e    1% /run/lock\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etmpfs           \u003cspan class=\"m\"\u003e16078645\u003c/span\u003e     \u003cspan class=\"m\"\u003e18\u003c/span\u003e  \u003cspan class=\"m\"\u003e16078627\u003c/span\u003e    1% /sys/fs/cgroup\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/dev/sda4       \u003cspan class=\"m\"\u003e31162368\u003c/span\u003e   \u003cspan class=\"m\"\u003e4308\u003c/span\u003e  \u003cspan class=\"m\"\u003e31158060\u003c/span\u003e    1% /data\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/dev/sdb1      \u003cspan class=\"m\"\u003e182939648\u003c/span\u003e    \u003cspan class=\"m\"\u003e564\u003c/span\u003e \u003cspan class=\"m\"\u003e182939084\u003c/span\u003e    1% /data/container\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e然后查找哪里小文件特别多。\u003c/p\u003e","title":"no space left on device：不一定是磁盘满了，可能是 inode 满了"},{"content":"删除文件后磁盘空间没释放，是因为文件被进程打开，进程还持有文件描述符。用 lsof | grep deleted 找到占用进程，最好的方法是在线清空文件。\n现象 在 Linux 或者 Unix 系统中，通过 rm 或者文件管理器删除文件，磁盘空间却没有释放。\n根因 通过 rm 删除文件，只是将它从文件系统的目录结构上解除链接（unlink），也就是说只是删除了文件和系统目录结构的链接。\n如果文件在删除时是被打开的（有一个进程正在使用该文件，文件被进程锁定或者有进程一直在向这个文件写数据等）状态，那么进程将仍然可以读取该文件，进程还持有文件的文件描述符 fd，所以磁盘空间也就会一直被占用。\n一个文件在文件系统中的存放分为两个部分：数据部分和指针部分。\n指针位于文件系统的 meta-data 中，数据被删除后，这个指针就从 meta-data 中清除了； 而数据部分存储在磁盘中，数据对应的指针从 meta-data 中清除后，文件数据部分占用的空间就可以被覆盖并写入新的内容； 之所以出现删除文件后空间还没释放，就是因为有进程还在一直向这个文件写入内容，导致虽然删除了文件，但文件对应的指针部分由于进程锁定，并未从 meta-data 中清除； 而由于指针并未被删除，那么系统内核就认为文件并未被删除，因此通过 df 命令查询空间并未释放也就不足为奇了。 解决办法 1. 找到占用进程并 kill 通过 lsof | grep deleted 命令获取到已经被删除但是仍然被应用程序占用的文件列表，然后 kill 掉还在占用所删除文件的进程。\n需要注意的是：如果有很多进程都在使用所删除文件，那么采用这种方式 kill 进程就有点麻烦了，而且风险也比较大。\n因为 kill 进程是通过截断 proc 文件系统中的文件可以强制要求系统回收分配给正在使用的文件。\n必须要确定不会对运行中的进程造成影响时才能使用，应用程序对这种方式支持的并不好，当一个正在使用的文件被截断可能会引发不可预知的问题。\n2. 停掉或重启应用 停掉或重启使用这个所删除文件的应用，让 OS 自动回收磁盘空间。\n3. 重启操作系统 也可以重启操作系统，不过这并不是最好的方法。\n4. 在线清空文件（推荐） 对待这种进程不停对文件写日志的操作，要释放文件占用的磁盘空间，最好的方法是在线清空这个文件。\n通过这种方法，磁盘空间不但可以马上释放，也可保障进程继续向文件写入日志。\n在线清空文件（比如 /var/log/app.log）的方式：\necho \u0026#34; \u0026#34; \u0026gt; /var/log/app.log cat /dev/null \u0026gt; /var/log/app.log \u0026gt; /var/log/app.log 小结 删除文件只是解除链接（unlink），不是真正释放空间； 文件被进程打开时，进程还持有文件描述符，空间一直被占用； 用 lsof | grep deleted 找到占用进程； 最好的方法是在线清空文件（\u0026gt; file），既释放空间又保障进程继续写日志； kill 进程有风险，要确定不会影响运行中的进程。 ","permalink":"/posts/delete-file-disk-space-not-released/","summary":"\u003cp\u003e删除文件后磁盘空间没释放，是因为文件被进程打开，进程还持有文件描述符。用 \u003ccode\u003elsof | grep deleted\u003c/code\u003e 找到占用进程，最好的方法是在线清空文件。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e在 Linux 或者 Unix 系统中，通过 \u003ccode\u003erm\u003c/code\u003e 或者文件管理器删除文件，磁盘空间却没有释放。\u003c/p\u003e\n\u003ch2 id=\"根因\"\u003e根因\u003c/h2\u003e\n\u003cp\u003e通过 \u003ccode\u003erm\u003c/code\u003e 删除文件，只是将它\u003cstrong\u003e从文件系统的目录结构上解除链接（unlink）\u003c/strong\u003e，也就是说只是删除了文件和系统目录结构的链接。\u003c/p\u003e\n\u003cp\u003e如果文件在删除时是被打开的（有一个进程正在使用该文件，文件被进程锁定或者有进程一直在向这个文件写数据等）状态，那么进程将仍然可以读取该文件，\u003cstrong\u003e进程还持有文件的文件描述符 fd\u003c/strong\u003e，所以磁盘空间也就会一直被占用。\u003c/p\u003e\n\u003cp\u003e一个文件在文件系统中的存放分为两个部分：数据部分和指针部分。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e指针位于文件系统的 meta-data 中，数据被删除后，这个指针就从 meta-data 中清除了；\u003c/li\u003e\n\u003cli\u003e而数据部分存储在磁盘中，数据对应的指针从 meta-data 中清除后，文件数据部分占用的空间就可以被覆盖并写入新的内容；\u003c/li\u003e\n\u003cli\u003e之所以出现删除文件后空间还没释放，就是因为有进程还在一直向这个文件写入内容，导致虽然删除了文件，但文件对应的指针部分由于进程锁定，并未从 meta-data 中清除；\u003c/li\u003e\n\u003cli\u003e而由于指针并未被删除，那么系统内核就认为文件并未被删除，因此通过 \u003ccode\u003edf\u003c/code\u003e 命令查询空间并未释放也就不足为奇了。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"解决办法\"\u003e解决办法\u003c/h2\u003e\n\u003ch3 id=\"1-找到占用进程并-kill\"\u003e1. 找到占用进程并 kill\u003c/h3\u003e\n\u003cp\u003e通过 \u003ccode\u003elsof | grep deleted\u003c/code\u003e 命令获取到已经被删除但是仍然被应用程序占用的文件列表，然后 kill 掉还在占用所删除文件的进程。\u003c/p\u003e\n\u003cp\u003e需要注意的是：如果有很多进程都在使用所删除文件，那么采用这种方式 kill 进程就有点麻烦了，而且风险也比较大。\u003c/p\u003e\n\u003cp\u003e因为 kill 进程是通过截断 proc 文件系统中的文件可以强制要求系统回收分配给正在使用的文件。\u003c/p\u003e\n\u003cp\u003e必须要确定不会对运行中的进程造成影响时才能使用，应用程序对这种方式支持的并不好，当一个正在使用的文件被截断可能会引发不可预知的问题。\u003c/p\u003e\n\u003ch3 id=\"2-停掉或重启应用\"\u003e2. 停掉或重启应用\u003c/h3\u003e\n\u003cp\u003e停掉或重启使用这个所删除文件的应用，让 OS 自动回收磁盘空间。\u003c/p\u003e\n\u003ch3 id=\"3-重启操作系统\"\u003e3. 重启操作系统\u003c/h3\u003e\n\u003cp\u003e也可以重启操作系统，不过这并不是最好的方法。\u003c/p\u003e\n\u003ch3 id=\"4-在线清空文件推荐\"\u003e4. 在线清空文件（推荐）\u003c/h3\u003e\n\u003cp\u003e对待这种进程不停对文件写日志的操作，要释放文件占用的磁盘空间，\u003cstrong\u003e最好的方法\u003c/strong\u003e是在线清空这个文件。\u003c/p\u003e\n\u003cp\u003e通过这种方法，磁盘空间不但可以马上释放，也可保障进程继续向文件写入日志。\u003c/p\u003e\n\u003cp\u003e在线清空文件（比如 /var/log/app.log）的方式：\u003c/p\u003e","title":"删除文件后磁盘空间为什么没释放？"},{"content":"virsh detach 后 domblklist 有短暂延时，detach 成功后立即 detach 相同卷也能成功，两次重复 detach 后再 attach 会失败。根因是 libvirt 不是实时状态，有一定延时。\n现象 已测试：连续 attach、detach、attach、detach\u0026hellip; 反复是没有问题的。\n有问题的：\ndetach 后，virsh domblklist 是没有卸载的，要过一两秒，有短暂延时； detach 成功后，立即 detach 相同的卷，居然也可以成功。预期应该是会报错提示没有对应的卷； 两次重复的 detach 后，再 attach，attach 命令成功，但实际没有把卷挂载上。 根因 我们挺多奇怪的问题，可能都是类似的原因造成的。\nlibvirt 和 qemu：\nlibvirt 不是实时状态，有一定延时； 然后一些重复操作，看上去是幂等的，但是它对后续操作有影响（其实也是 libvirt 的 bug）。 比如之前有过 domblk 和 qmp 查询卷不一样的情况，造成后续丢数据等。\n设计思考 做重试之前，要充分测试。\n是否达到了重试条件（状态、时间）； 这个动作是否幂等； 对后续动作是否有影响。 重试不是灵药，可能是毒药。\n小结 virsh detach 后 domblklist 有短暂延时，要过一两秒； detach 成功后立即 detach 相同卷也能成功，不符合预期； 两次重复 detach 后再 attach，attach 成功但实际没挂载上； 根因是 libvirt 不是实时状态，有一定延时； 做重试之前要充分测试，重试不是灵药，可能是毒药。 ","permalink":"/posts/virsh-attach-detach-delay/","summary":"\u003cp\u003evirsh detach 后 domblklist 有短暂延时，detach 成功后立即 detach 相同卷也能成功，两次重复 detach 后再 attach 会失败。根因是 libvirt 不是实时状态，有一定延时。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e已测试：连续 attach、detach、attach、detach\u0026hellip; 反复是没有问题的。\u003c/p\u003e\n\u003cp\u003e有问题的：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003edetach 后，\u003ccode\u003evirsh domblklist\u003c/code\u003e 是没有卸载的，要过一两秒，有短暂延时；\u003c/li\u003e\n\u003cli\u003edetach 成功后，立即 detach 相同的卷，居然也可以成功。预期应该是会报错提示没有对应的卷；\u003c/li\u003e\n\u003cli\u003e两次重复的 detach 后，再 attach，attach 命令成功，但实际没有把卷挂载上。\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"根因\"\u003e根因\u003c/h2\u003e\n\u003cp\u003e我们挺多奇怪的问题，可能都是类似的原因造成的。\u003c/p\u003e\n\u003cp\u003elibvirt 和 qemu：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003elibvirt 不是实时状态，有一定延时；\u003c/li\u003e\n\u003cli\u003e然后一些重复操作，看上去是幂等的，但是它对后续操作有影响（其实也是 libvirt 的 bug）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e比如之前有过 \u003ccode\u003edomblk\u003c/code\u003e 和 qmp 查询卷不一样的情况，造成后续丢数据等。\u003c/p\u003e\n\u003ch2 id=\"设计思考\"\u003e设计思考\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e做重试之前，要充分测试。\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e是否达到了重试条件（状态、时间）；\u003c/li\u003e\n\u003cli\u003e这个动作是否幂等；\u003c/li\u003e\n\u003cli\u003e对后续动作是否有影响。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e重试不是灵药，可能是毒药。\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003evirsh detach 后 domblklist 有短暂延时，要过一两秒；\u003c/li\u003e\n\u003cli\u003edetach 成功后立即 detach 相同卷也能成功，不符合预期；\u003c/li\u003e\n\u003cli\u003e两次重复 detach 后再 attach，attach 成功但实际没挂载上；\u003c/li\u003e\n\u003cli\u003e根因是 libvirt 不是实时状态，有一定延时；\u003c/li\u003e\n\u003cli\u003e做重试之前要充分测试，重试不是灵药，可能是毒药。\u003c/li\u003e\n\u003c/ul\u003e","title":"virsh attach/detach 的延时问题：重试不是灵药，可能是毒药"},{"content":"libvirt CPU feature 的 policy 有 force、require、optional、disable、forbid 五种。disable 是虚拟 CPU 不支持该特性，forbid 是宿主机支持该特性时创建会失败。\n现象 在 libvirt XML 中配置 CPU feature：\n\u0026lt;cpu\u0026gt; \u0026lt;model\u0026gt;EPYC\u0026lt;/model\u0026gt; \u0026lt;feature name=\u0026#34;pclmuldq\u0026#34; policy=\u0026#34;disable\u0026#34;/\u0026gt; \u0026lt;feature name=\u0026#34;aes\u0026#34; policy=\u0026#34;disable\u0026#34;/\u0026gt; \u0026lt;feature name=\u0026#34;sha-ni\u0026#34; policy=\u0026#34;disable\u0026#34;/\u0026gt; \u0026lt;feature name=\u0026#34;monitor\u0026#34; policy=\u0026#34;disable\u0026#34;/\u0026gt; \u0026lt;feature name=\u0026#34;svm\u0026#34; policy=\u0026#34;disable\u0026#34;/\u0026gt; \u0026lt;feature name=\u0026#34;npt\u0026#34; policy=\u0026#34;disable\u0026#34;/\u0026gt; 【物理机支撑 npt 的】 \u0026lt;topology cores=\u0026#34;1\u0026#34; sockets=\u0026#34;1\u0026#34; threads=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;/cpu\u0026gt; 物理 CPU 是有 npt 的，virsh create 的时候就报错，而且这是 disable 也报错。\n足以说明，不是 CPU 问题，而是 libvirt 不支持问题：\n2025-06-24 12:50:13,539 WARNING -140207347541760- cmd [virsh create /data/vmxml/vm-example.xml] on [local] failed: ret[1] out[] err[error: Failed to create domain from /data/vmxml/vm-example.xml error: unsupported configuration: unknown CPU feature: npt]. exec_time is [0.089]s 排查 在 commandline 中禁用 npt 是可以创建虚拟机的 \u0026lt;qemu:commandline\u0026gt; \u0026lt;qemu:arg value=\u0026#34;-cpu\u0026#34;/\u0026gt; \u0026lt;qemu:arg value=\u0026#34;EPYC,vendor=AuthenticAMD,model_id=Example CPU 7380 32-core Processor,-x2apic,-hypervisor,-topoext,-npt,-nrip-save,-monitor,-svm\u0026#34;/\u0026gt; \u0026lt;/qemu:commandline\u0026gt; 说明什么？\n说明是 libvirt 的问题，libvirt 校验不通过，这个 libvirt 版本压根不认识 npt 这个 feature。\nforbid 的时候，如果宿主机支持该特性，真的会 virsh create 失败 \u0026lt;cpu\u0026gt; \u0026lt;model\u0026gt;EPYC\u0026lt;/model\u0026gt; \u0026lt;feature name=\u0026#34;pclmuldq\u0026#34; policy=\u0026#34;disable\u0026#34;/\u0026gt; \u0026lt;feature name=\u0026#34;aes\u0026#34; policy=\u0026#34;disable\u0026#34;/\u0026gt; \u0026lt;feature name=\u0026#34;sha-ni\u0026#34; policy=\u0026#34;disable\u0026#34;/\u0026gt; \u0026lt;feature name=\u0026#34;monitor\u0026#34; policy=\u0026#34;disable\u0026#34;/\u0026gt; \u0026lt;feature name=\u0026#34;svm\u0026#34; policy=\u0026#34;disable\u0026#34;/\u0026gt; \u0026lt;feature name=\u0026#34;npt\u0026#34; policy=\u0026#34;forbid\u0026#34;/\u0026gt; 【禁用真不行】 \u0026lt;topology cores=\u0026#34;1\u0026#34; sockets=\u0026#34;1\u0026#34; threads=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;/cpu\u0026gt; root@host:/data/vmxml# virsh destroy vm-example; virsh create vm-example.xml Domain vm-example destroyed error: Failed to create domain from vm-example.xml error: the CPU is incompatible with host CPU: Host CPU provides forbidden features: npt 根因 feature 的 policy 五种取值 根据 libvirt 官方文档，feature 元素的 policy 属性有以下五种取值：\nforce（强制）\n虚拟 CPU 将声明该功能受支持，无论主机 CPU 是否支持该功能。\nrequire（需要）\n除非主机 CPU 支持该功能，或者虚拟机管理程序能够模拟该功能，否则来宾创建将失败。\noptional（自选）\n当且仅当主机 CPU 支持该功能时，虚拟 CPU 才会支持该功能。\ndisable（禁用）\n虚拟 CPU 不支持此功能。\nforbid（禁止）\n如果宿主机支持该功能，来宾创建将失败。\n关键区别 disable：虚拟 CPU 不支持该特性，但宿主机支持与否无所谓，创建不会因此失败； forbid：如果宿主机支持该特性，创建会失败。 为什么 disable npt 也报错 unknown CPU feature: npt 说明这个 libvirt 版本压根不认识 npt 这个 feature。\n这是 libvirt 版本问题，不是 CPU 问题。\n小结 libvirt CPU feature 的 policy 有 force、require、optional、disable、forbid 五种； disable 是虚拟 CPU 不支持该特性，宿主机支持与否无所谓； forbid 是宿主机支持该特性时创建会失败； unknown CPU feature 是 libvirt 版本不认识该 feature，不是 CPU 问题； 在 commandline 中禁用 npt 可以创建虚拟机，说明是 libvirt 校验不通过。 ","permalink":"/posts/libvirt-cpu-feature-disable-forbid/","summary":"\u003cp\u003elibvirt CPU feature 的 policy 有 force、require、optional、disable、forbid 五种。disable 是虚拟 CPU 不支持该特性，forbid 是宿主机支持该特性时创建会失败。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e在 libvirt XML 中配置 CPU feature：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;cpu\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;model\u0026gt;\u003c/span\u003eEPYC\u003cspan class=\"nt\"\u003e\u0026lt;/model\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;pclmuldq\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003epolicy=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;disable\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;aes\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003epolicy=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;disable\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;sha-ni\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003epolicy=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;disable\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;monitor\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003epolicy=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;disable\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;svm\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003epolicy=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;disable\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;npt\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003epolicy=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;disable\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e   【物理机支撑 npt 的】\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;topology\u003c/span\u003e \u003cspan class=\"na\"\u003ecores=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003esockets=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ethreads=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/cpu\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e物理 CPU 是有 npt 的，\u003ccode\u003evirsh create\u003c/code\u003e 的时候就报错，\u003cstrong\u003e而且这是 disable 也报错\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e足以说明，不是 CPU 问题，而是 libvirt 不支持问题：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e2025-06-24 12:50:13,539 WARNING -140207347541760- cmd \u003cspan class=\"o\"\u003e[\u003c/span\u003evirsh create /data/vmxml/vm-example.xml\u003cspan class=\"o\"\u003e]\u003c/span\u003e on \u003cspan class=\"o\"\u003e[\u003c/span\u003elocal\u003cspan class=\"o\"\u003e]\u003c/span\u003e failed: ret\u003cspan class=\"o\"\u003e[\u003c/span\u003e1\u003cspan class=\"o\"\u003e]\u003c/span\u003e out\u003cspan class=\"o\"\u003e[]\u003c/span\u003e err\u003cspan class=\"o\"\u003e[\u003c/span\u003eerror: Failed to create domain from /data/vmxml/vm-example.xml\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eerror: unsupported configuration: unknown CPU feature: npt\u003cspan class=\"o\"\u003e]\u003c/span\u003e. exec_time is \u003cspan class=\"o\"\u003e[\u003c/span\u003e0.089\u003cspan class=\"o\"\u003e]\u003c/span\u003es\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003ch3 id=\"在-commandline-中禁用-npt-是可以创建虚拟机的\"\u003e在 commandline 中禁用 npt 是可以创建虚拟机的\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;qemu:commandline\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;qemu:arg\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;-cpu\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;qemu:arg\u003c/span\u003e \u003cspan class=\"na\"\u003evalue=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;EPYC,vendor=AuthenticAMD,model_id=Example CPU 7380 32-core Processor,-x2apic,-hypervisor,-topoext,-npt,-nrip-save,-monitor,-svm\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/qemu:commandline\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e说明什么？\u003c/p\u003e","title":"libvirt CPU feature 的 disable 和 forbid 有什么区别？"},{"content":"ARM 虚拟机配置中 \u0026lt;features\u0026gt; 区块缺少 \u0026lt;acpi/\u0026gt;，表面现象是 virsh console 登录没反应。根因是 ACPI 缺失导致系统底层硬件/服务初始化异常，间接引发登录流程失败。\n现象 ARM 虚拟机配置中 \u0026lt;features\u0026gt; 区块里缺少 \u0026lt;acpi/\u0026gt;，虚拟机异常。\n表面现象是 virsh console 的时候登录没有反应。\n这种时候其实都是虚拟机启动阶段就有问题了。\n排查 各 feature 的作用 \u0026lt;apic/\u0026gt; - 高级可编程中断控制器（Advanced Programmable Interrupt Controller）\n这是 x86 架构的中断控制器标准，替代老式的 8259 PIC； 负责管理 CPU 的中断请求（比如硬件设备的信号、定时器中断等）； 支持多核心 CPU 的中断分发、中断优先级控制、中断重映射等； 对 ARM 环境的意义：ARM 架构不使用 APIC，这个配置属于冗余配置（无实际作用），是兼容 x86 配置模板留下的，删除也不会影响 ARM 虚拟机运行。 \u0026lt;pae/\u0026gt; - 物理地址扩展（Physical Address Extension）\n这是 32 位 x86 架构的内存扩展技术，让 32 位 x86 系统突破 4GB 物理内存的访问限制（最大支持 64GB）； 对 ARM 环境的意义：你的环境是 64 位 ARM（aarch64），天然支持大于 4GB 的内存寻址，\u0026lt;pae/\u0026gt; 完全无作用，属于冗余配置。 \u0026lt;acpi/\u0026gt; - 高级配置与电源接口（Advanced Configuration and Power Interface）\n这是跨 x86/ARM 的通用标准，是操作系统与硬件之间的\u0026quot;硬件抽象层协议\u0026quot;； 核心功能包括： 硬件枚举：让系统识别并初始化硬件设备（如磁盘、网卡、串口、键盘、电源按钮等）； 电源管理：处理待机、休眠、关机、电源状态检测等； 系统事件通知：硬件状态变化（如插拔设备、电源异常）通知操作系统； 硬件配置：提供硬件的配置信息（如中断路由、IO 地址分配）； 对 ARM 环境的意义：ACPI 是 ARM64 虚拟机正常运行的核心必备项。 \u0026lt;gic version=\u0026quot;3\u0026quot;/\u0026gt; - 通用中断控制器（Generic Interrupt Controller）\n这是 ARM 架构的中断控制器标准（替代 x86 的 APIC）； 负责接收并分发硬件/软件中断到 ARM CPU 核心； 管理中断优先级、中断屏蔽、中断虚拟化； 是 ARM 虚拟机处理所有硬件中断（如网卡、磁盘、定时器）的核心组件； 对 ARM 环境的意义：GICv3 是 ARM64 虚拟机正常运行的核心必备项。 根因 登录失败不是 ACPI 直接控制登录认证，而是 ACPI 缺失导致系统底层硬件/服务初始化异常，间接引发登录流程失败。\n具体原因包括：\n硬件识别/初始化失败： ACPI 负责向操作系统提供硬件枚举信息，缺少 ACPI 时，系统无法正确识别串口、终端（tty/pty）、网卡、输入设备等关键硬件。 登录依赖终端（本地登录）或网卡（SSH 登录），硬件初始化失败会导致登录入口（如 getty、sshd、login 程序）无法绑定到正确的设备，表现为\u0026quot;登录界面无响应\u0026quot;\u0026ldquo;SSH 连接超时\u0026quot;\u0026ldquo;认证后无法创建会话\u0026rdquo;。\n系统服务启动异常： 登录相关的服务（如 systemd-logind、sshd、pam 认证模块）依赖 ACPI 提供的系统状态（如电源就绪、硬件就绪）。 缺少 ACPI 时，这些服务会因无法获取硬件状态而启动失败，或启动后无法处理登录请求（比如 pam 模块无法验证用户会话的硬件上下文）。\n电源/系统状态异常： ACPI 提供系统的电源状态（如\u0026quot;开机完成\u0026quot;\u0026ldquo;硬件就绪\u0026rdquo;），缺少 ACPI 时，系统会认为硬件未就绪，登录管理器会拒绝创建用户会话，防止在硬件未就绪的状态下登录导致系统崩溃。\n驱动加载失败： 很多基础驱动（如磁盘、串口、USB）的加载依赖 ACPI 提供的硬件配置信息，缺少 ACPI 会导致驱动加载失败，进而让登录程序无法读写用户认证文件（如 /etc/passwd、/etc/shadow），最终登录失败。\n小结 ARM 虚拟机缺 \u0026lt;acpi/\u0026gt;，表面现象是 virsh console 登录没反应； 根因是 ACPI 缺失导致系统底层硬件/服务初始化异常，间接引发登录流程失败； \u0026lt;apic/\u0026gt; 和 \u0026lt;pae/\u0026gt; 是 x86 的冗余配置，对 ARM 无作用； \u0026lt;acpi/\u0026gt; 和 \u0026lt;gic version=\u0026quot;3\u0026quot;/\u0026gt; 是 ARM64 虚拟机正常运行的核心必备项； 登录没反应时，优先怀疑虚拟机启动阶段就有问题。 ","permalink":"/posts/arm-vm-missing-acpi-login/","summary":"\u003cp\u003eARM 虚拟机配置中 \u003ccode\u003e\u0026lt;features\u0026gt;\u003c/code\u003e 区块缺少 \u003ccode\u003e\u0026lt;acpi/\u0026gt;\u003c/code\u003e，表面现象是 virsh console 登录没反应。根因是 ACPI 缺失导致系统底层硬件/服务初始化异常，间接引发登录流程失败。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003eARM 虚拟机配置中 \u003ccode\u003e\u0026lt;features\u0026gt;\u003c/code\u003e 区块里缺少 \u003ccode\u003e\u0026lt;acpi/\u0026gt;\u003c/code\u003e，虚拟机异常。\u003c/p\u003e\n\u003cp\u003e表面现象是 virsh console 的时候登录没有反应。\u003c/p\u003e\n\u003cp\u003e这种时候其实都是虚拟机启动阶段就有问题了。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003ch3 id=\"各-feature-的作用\"\u003e各 feature 的作用\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003e\u0026lt;apic/\u0026gt;\u003c/code\u003e\u003c/strong\u003e - 高级可编程中断控制器（Advanced Programmable Interrupt Controller）\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e这是 x86 架构的中断控制器标准，替代老式的 8259 PIC；\u003c/li\u003e\n\u003cli\u003e负责管理 CPU 的中断请求（比如硬件设备的信号、定时器中断等）；\u003c/li\u003e\n\u003cli\u003e支持多核心 CPU 的中断分发、中断优先级控制、中断重映射等；\u003c/li\u003e\n\u003cli\u003e对 ARM 环境的意义：ARM 架构不使用 APIC，这个配置属于冗余配置（无实际作用），是兼容 x86 配置模板留下的，删除也不会影响 ARM 虚拟机运行。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003e\u0026lt;pae/\u0026gt;\u003c/code\u003e\u003c/strong\u003e - 物理地址扩展（Physical Address Extension）\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e这是 32 位 x86 架构的内存扩展技术，让 32 位 x86 系统突破 4GB 物理内存的访问限制（最大支持 64GB）；\u003c/li\u003e\n\u003cli\u003e对 ARM 环境的意义：你的环境是 64 位 ARM（aarch64），天然支持大于 4GB 的内存寻址，\u003ccode\u003e\u0026lt;pae/\u0026gt;\u003c/code\u003e 完全无作用，属于冗余配置。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003e\u0026lt;acpi/\u0026gt;\u003c/code\u003e\u003c/strong\u003e - 高级配置与电源接口（Advanced Configuration and Power Interface）\u003c/p\u003e","title":"ARM 虚拟机缺 acpi 为什么登录没反应？"},{"content":"qemu 只支持一个 IDE controller，一个 IDE controller 只支持 4 个设备（hda 到 hdd）。定义两个 IDE controller 或使用 hde 都会报错。\n现象 在 libvirt XML 中定义两个 IDE controller：\n\u0026lt;controller type=\u0026#39;ide\u0026#39; index=\u0026#39;0\u0026#39;/\u0026gt; \u0026lt;controller type=\u0026#39;ide\u0026#39; index=\u0026#39;1\u0026#39;/\u0026gt; 直接会报如下错误：\nerror: Failed to create domain from vm-example.xml error: unsupported configuration: Only a single IDE controller is supported for this machine type 排查 默认有一个 IDE controller 默认有一个 IDE controller。\n所以不定义 \u0026lt;controller type='ide' index='0'/\u0026gt;，也是有一个 IDE controller 的。\n直接定义 IDE disk 的情况 \u0026lt;disk type=\u0026#34;network\u0026#34; device=\u0026#34;cdrom\u0026#34;\u0026gt; \u0026lt;driver name=\u0026#34;qemu\u0026#34; type=\u0026#34;raw\u0026#34;\u0026gt;\u0026lt;/driver\u0026gt; \u0026lt;source protocol=\u0026#34;http\u0026#34; name=\u0026#34;/yy/img-example.iso\u0026#34;\u0026gt; \u0026lt;host name=\u0026#34;qa1b01\u0026#34; port=\u0026#34;8021\u0026#34;\u0026gt;\u0026lt;/host\u0026gt; \u0026lt;/source\u0026gt; \u0026lt;target dev=\u0026#34;hdd\u0026#34; bus=\u0026#34;ide\u0026#34;\u0026gt;\u0026lt;/target\u0026gt; 【注意 hdd】 \u0026lt;readonly\u0026gt;\u0026lt;/readonly\u0026gt; \u0026lt;shareable\u0026gt;\u0026lt;/shareable\u0026gt; \u0026lt;/disk\u0026gt; 直接定义了 IDE 的 disk，没有定义 IDE 的 controller。\nhda 到 hdd 都可以用，但如果是 hde，就会直接报错：\nerror: failed to get domain \u0026#39;vm-example\u0026#39; error: Failed to create domain from vm-example.xml error: internal error: Only 1 IDE controller is supported 根因 qemu 会根据 dev 名称的序号自动固定地选择 controller。\n且一个 IDE controller 只支持 4 个设备（hda、hdb、hdc、hdd）。\n所以：\n定义两个 IDE controller 会报错； 使用 hde 及以后的设备名会报错； 默认有一个 IDE controller，不定义也有。 小结 qemu 只支持一个 IDE controller； 一个 IDE controller 只支持 4 个设备（hda 到 hdd）； 默认有一个 IDE controller，不定义也有； 定义两个 IDE controller 或使用 hde 都会报错； qemu 会根据 dev 名称的序号自动固定地选择 controller。 ","permalink":"/posts/qemu-single-ide-controller/","summary":"\u003cp\u003eqemu 只支持一个 IDE controller，一个 IDE controller 只支持 4 个设备（hda 到 hdd）。定义两个 IDE controller 或使用 hde 都会报错。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e在 libvirt XML 中定义两个 IDE controller：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;controller\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;ide\u0026#39;\u003c/span\u003e \u003cspan class=\"na\"\u003eindex=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;0\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;controller\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;ide\u0026#39;\u003c/span\u003e \u003cspan class=\"na\"\u003eindex=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#39;1\u0026#39;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e直接会报如下错误：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eerror: Failed to create domain from vm-example.xml\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eerror: unsupported configuration: Only a single IDE controller is supported \u003cspan class=\"k\"\u003efor\u003c/span\u003e this machine \u003cspan class=\"nb\"\u003etype\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003ch3 id=\"默认有一个-ide-controller\"\u003e默认有一个 IDE controller\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e默认有一个 IDE controller\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e所以不定义 \u003ccode\u003e\u0026lt;controller type='ide' index='0'/\u0026gt;\u003c/code\u003e，也是有一个 IDE controller 的。\u003c/p\u003e\n\u003ch3 id=\"直接定义-ide-disk-的情况\"\u003e直接定义 IDE disk 的情况\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;disk\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;network\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003edevice=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;cdrom\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;driver\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;qemu\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003etype=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;raw\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/driver\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;source\u003c/span\u003e \u003cspan class=\"na\"\u003eprotocol=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;http\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;/yy/img-example.iso\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;host\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;qa1b01\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eport=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;8021\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/host\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;/source\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;target\u003c/span\u003e \u003cspan class=\"na\"\u003edev=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;hdd\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ebus=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ide\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/target\u0026gt;\u003c/span\u003e   【注意 hdd】\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;readonly\u0026gt;\u0026lt;/readonly\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;shareable\u0026gt;\u0026lt;/shareable\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/disk\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e直接定义了 IDE 的 disk，没有定义 IDE 的 controller。\u003c/p\u003e","title":"qemu 为什么只支持一个 IDE controller？"},{"content":"qemu-img commit 的链必须是完整的，虽然 commit 的是 head，看起来和底层几级镜像没关系，但实际还是需要。commit 链的关系还可以是临时的，-d 参数决定 commit 后是否清空文件。\n现象 qemu-img commit 的链，必须是完整的。\n虽然你 commit 的是 head，看起来和底层几级的镜像没有关系，但实际还是需要的：\nroot@host:~/tmp# qemu-img commit -f qcow2 /root/tmp/vm-example_tmp.img qemu-img: Could not open \u0026#39;/root/tmp/vm-example_tmp.img\u0026#39;: Could not open backing file: Could not open backing file: Could not open \u0026#39;/root/tmp/focal1x64a\u0026#39;: No such file or directory 排查 commit 链的关系还可以是临时的 下面命令不会改变 /dev/vg2/vm-example_lss-sjjgi703 的 backing file，但是确可以提交数据到其他链上，非常方便：\nqemu-img commit --image-opts file.filename=/dev/vg2/vm-example_lss-sjjgi703,backing.file.filename=/root/tmp/vm-example.img 底层链有 nbd 镜像也会失败 如果底层的链中有 nbd 镜像，那么即便 commit 没到 nbd img，也会失败：\n2024-12-09 18:08:26,809 INFO -139636383823616- commit_snap command is [qemu-img commit -f qcow2 -d /data/container/vm-example/vm-example.resize_tmp.img] ret is [(1, \u0026#39;\u0026#39;, u\u0026#34;qemu-img: Block format \u0026#39;nbd\u0026#39; used by node \u0026#39;#block817\u0026#39; does not support reopening files\u0026#34;)] 如果底层的链中有 nbd 镜像，那么即便 commit 没到 nbd img，也会失败。\n所以，如果镜像是 nbd 的，一些包含 commit 的功能会失败。\n上面的链是这样：\n/data/container/vm-example/vm-example.resize_tmp.img -\u0026gt; /vm-example.img -\u0026gt; nbd:img 根因 -d 参数非常重要 qemu-img commit -d -d 参数非常重要：\nThe image FILENAME is emptied after the operation has succeeded. If you do not need FILENAME afterwards and intend to drop it, you may skip emptying FILENAME by specifying the -d flag.\n也就是说：\n不加 -d，那么 commit 后，filename 里的数据会被清空； 加了 -d，就可以保留。 libvirt 8 之后检查加强了 libvirt 8 之后，很多检查都加强了。ubuntu22 上是 libvirt8。\n典型的是：\nqemu-img create qemu-img convert qemu-img rebase 如果存在 backing file，就必须要加 -F 显式指定 type。\n所以时刻举一反三。\n小结 qemu-img commit 的链必须是完整的，虽然 commit 的是 head，但底层镜像还是需要； commit 链的关系还可以是临时的，用 --image-opts 指定 backing 即可； 底层链中有 nbd 镜像，即便 commit 没到 nbd img 也会失败； -d 参数决定 commit 后是否清空文件，不加 -d 会清空，加了保留； libvirt 8 之后，有 backing file 的命令必须加 -F 显式指定 type。 ","permalink":"/posts/qemu-img-commit-chain-complete/","summary":"\u003cp\u003eqemu-img commit 的链必须是完整的，虽然 commit 的是 head，看起来和底层几级镜像没关系，但实际还是需要。commit 链的关系还可以是临时的，\u003ccode\u003e-d\u003c/code\u003e 参数决定 commit 后是否清空文件。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eqemu-img commit\u003c/code\u003e 的链，必须是完整的。\u003c/p\u003e\n\u003cp\u003e虽然你 commit 的是 head，看起来和底层几级的镜像没有关系，但实际还是需要的：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eroot@host:~/tmp# qemu-img commit -f qcow2 /root/tmp/vm-example_tmp.img\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eqemu-img: Could not open \u003cspan class=\"s1\"\u003e\u0026#39;/root/tmp/vm-example_tmp.img\u0026#39;\u003c/span\u003e: Could not open backing file: Could not open backing file: Could not open \u003cspan class=\"s1\"\u003e\u0026#39;/root/tmp/focal1x64a\u0026#39;\u003c/span\u003e: No such file or directory\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003ch3 id=\"commit-链的关系还可以是临时的\"\u003ecommit 链的关系还可以是临时的\u003c/h3\u003e\n\u003cp\u003e下面命令不会改变 \u003ccode\u003e/dev/vg2/vm-example_lss-sjjgi703\u003c/code\u003e 的 backing file，但是确可以提交数据到其他链上，非常方便：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eqemu-img commit --image-opts file.filename\u003cspan class=\"o\"\u003e=\u003c/span\u003e/dev/vg2/vm-example_lss-sjjgi703,backing.file.filename\u003cspan class=\"o\"\u003e=\u003c/span\u003e/root/tmp/vm-example.img\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"底层链有-nbd-镜像也会失败\"\u003e底层链有 nbd 镜像也会失败\u003c/h3\u003e\n\u003cp\u003e如果底层的链中有 nbd 镜像，那么即便 commit 没到 nbd img，也会失败：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e2024-12-09 18:08:26,809 INFO -139636383823616- commit_snap \u003cspan class=\"nb\"\u003ecommand\u003c/span\u003e is \u003cspan class=\"o\"\u003e[\u003c/span\u003eqemu-img commit -f qcow2 -d /data/container/vm-example/vm-example.resize_tmp.img\u003cspan class=\"o\"\u003e]\u003c/span\u003e ret is \u003cspan class=\"o\"\u003e[(\u003c/span\u003e1, \u003cspan class=\"s1\"\u003e\u0026#39;\u0026#39;\u003c/span\u003e, u\u003cspan class=\"s2\"\u003e\u0026#34;qemu-img: Block format \u0026#39;nbd\u0026#39; used by node \u0026#39;#block817\u0026#39; does not support reopening files\u0026#34;\u003c/span\u003e\u003cspan class=\"o\"\u003e)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e如果底层的链中有 nbd 镜像，那么即便 commit 没到 nbd img，也会失败。\u003c/strong\u003e\u003c/p\u003e","title":"qemu-img commit 的链为什么必须完整？-d 参数又是什么"},{"content":"sdf 设备总是加不到 multipath 下，配置没问题，重启 multipathd 也不行。根因是 /etc/multipath/wwids 被人手动加了个 # 注释，导致对应的盘一直加载不上。\n现象 sdf 的设备总是加不到 multipath 下。\n/etc/multipath.conf 的配置没有问题，重启 multipathd 依然不行，报错也不明显。\n排查 后来发现是 /etc/multipath/wwids 下有人手动改了：\n# Multipath wwids, Version : 1.0 # NOTE: This file is automatically maintained by multipath and multipathd. # You should not need to edit this file in normal circumstances. # # Valid WWIDs: /wwid-example-0001/ /wwid-example-0002/ /wwid-example-0003/ #wwid-example-0004/ # 这里多一个 # 导致对应的 sdf 一直记载不上 /wwid-example-0005/ /wwid-example-0006/ 问题就出在那一行多了一个 #，导致对应的 sdf 一直加载不上。\n根因 正常来说，我们配置 /etc/multipath.conf 的 multipaths 部分，wwids 里的内容是自动生成的。\n这个文件是 multipath 和 multipathd 自动维护的，正常情况下不需要手动编辑。\n一旦有人手动改了（比如加了个 # 注释），multipath 就会认为这个盘不在 wwids 列表里，从而不把它加入 multipath。\n小结 /etc/multipath/wwids 是自动维护的，正常情况下不要手动编辑； 盘加不到 multipath 下，优先检查 wwids 是否被手动改过； 一个多余的 # 注释，就可能导致对应的盘一直加载不上； 排查问题要从源头入手，配置没问题不代表文件没问题。 ","permalink":"/posts/multipath-wwids-manual-edit/","summary":"\u003cp\u003esdf 设备总是加不到 multipath 下，配置没问题，重启 multipathd 也不行。根因是 \u003ccode\u003e/etc/multipath/wwids\u003c/code\u003e 被人手动加了个 \u003ccode\u003e#\u003c/code\u003e 注释，导致对应的盘一直加载不上。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003esdf 的设备总是加不到 multipath 下。\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003e/etc/multipath.conf\u003c/code\u003e 的配置没有问题，重启 multipathd 依然不行，报错也不明显。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e后来发现是 \u003ccode\u003e/etc/multipath/wwids\u003c/code\u003e 下有人手动改了：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Multipath wwids, Version : 1.0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# NOTE: This file is automatically maintained by multipath and multipathd.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# You should not need to edit this file in normal circumstances.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e#\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Valid WWIDs:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/wwid-example-0001/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/wwid-example-0002/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/wwid-example-0003/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e#wwid-example-0004/   # 这里多一个 # 导致对应的 sdf 一直记载不上\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/wwid-example-0005/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/wwid-example-0006/\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e问题就出在那一行多了一个 \u003ccode\u003e#\u003c/code\u003e，导致对应的 sdf 一直加载不上。\u003c/p\u003e","title":"multipath 的 wwids 被手动改，导致盘一直加不上"},{"content":"pvscan 报 Global lock failed 且没有返回信息，不一定是数据没了。停掉 sanlock 和 lvmlockd 再 pvscan，只要 pv 在，一切都好说。\n现象 在 ubuntu18 arm 上，pvscan 没有返回信息：\nroot@host:/etc/lvm/archive# pvscan Global lock failed: error -210 No matching physical volumes found 这个时候不要慌，不一定是数据没了。\n排查 把 sanlock 和 lvmlockd 停掉，再 pvscan 看看是否有，一般来说是在的：\nroot@host:/etc/lvm/archive# pvscan WARNING: lvmlockd process is not running. Reading without shared global lock. Reading VG vg_b without a lock. PV /dev/mapper/mpath-b VG vg_b lvm2 [\u0026lt;500.00 GiB / \u0026lt;499.00 GiB free] Reading VG vg_a without a lock. PV /dev/mapper/mpath-a VG vg_a lvm2 [\u0026lt;1000.00 GiB / \u0026lt;545.00 GiB free] Total: 2 [1.46 TiB] / in use: 2 [1.46 TiB] / in no VG: 0 [0 ] 只要 pv 在，那么一切都好说。\n把 sanlock 和 lvmlockd 启动起来后，vgs 还是一样，只有一行输出 Global lock failed: error -210。\n和平时 VG vg_b lock skipped: error -32 下面打印 vg 信息的场景很不一样。\n这种情况不要慌，直接执行 vgchange --lockstart：\nroot@host:/etc/lvm/archive# vgchange --lockstart Global lock failed: error -210 VG vg_a starting sanlock lockspace Starting locking. Waiting for sanlock may take 20 sec to 3 min... 根因 1. ubuntu18 arm 上 lvmlockd 服务名问题 ubuntu18 arm lvm2.02 上没有 lvmlockd 的服务，原来它叫 lvm2-lvmlockd。\n关键是 lvm2-lvmlockd 里启动命令还是 /sbin/lvmlockd -f。\n解决：\ncp /lib/systemd/system/lvm2-lvmlockd.service /lib/systemd/system/lvmlockd.service 2. lvmetad 缓存和 sanlock 锁不协调 ubuntu18 上默认开启了 lvm2-lvmetad，这个缓存和 sanlock 锁不协调，要禁用掉：\nsystemctl stop lvm2-lvmetad systemctl stop lvm2-lvmetad.socket lvm.conf 中的配置也要改 use_lvmetad=0。\n这个问题会导致 vgchange --lockstart 锁总是启动不起来。\n3. 多 vg 时单独启动 ubuntu18 上的 vgchange --lockstart 感觉是只启动了一个 vg，如果是多个记得记上 vg 名称单独启动试试。\n小结 pvscan 报 Global lock failed 且无返回，不一定是数据没了； 停掉 sanlock 和 lvmlockd 再 pvscan，只要 pv 在，一切都好说； ubuntu18 arm 上 lvmlockd 服务名是 lvm2-lvmlockd，需要复制一份； lvmetad 缓存和 sanlock 锁不协调，要禁用掉； 多 vg 时 vgchange --lockstart 可能只启动一个，要单独指定 vg 名称。 ","permalink":"/posts/lvmlockd-pvscan-global-lock-failed/","summary":"\u003cp\u003epvscan 报 \u003ccode\u003eGlobal lock failed\u003c/code\u003e 且没有返回信息，不一定是数据没了。停掉 sanlock 和 lvmlockd 再 pvscan，只要 pv 在，一切都好说。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e在 ubuntu18 arm 上，\u003ccode\u003epvscan\u003c/code\u003e 没有返回信息：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eroot@host:/etc/lvm/archive# pvscan\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  Global lock failed: error -210\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  No matching physical volumes found\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这个时候不要慌，不一定是数据没了。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e把 sanlock 和 lvmlockd 停掉，再 pvscan 看看是否有，一般来说是在的：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eroot@host:/etc/lvm/archive# pvscan\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  WARNING: lvmlockd process is not running.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  Reading without shared global lock.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  Reading VG vg_b without a lock.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  PV /dev/mapper/mpath-b   VG vg_b     lvm2 \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u0026lt;500.00 GiB / \u0026lt;499.00 GiB free\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  Reading VG vg_a without a lock.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  PV /dev/mapper/mpath-a   VG vg_a     lvm2 \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u0026lt;1000.00 GiB / \u0026lt;545.00 GiB free\u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  Total: \u003cspan class=\"m\"\u003e2\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e1.46 TiB\u003cspan class=\"o\"\u003e]\u003c/span\u003e / in use: \u003cspan class=\"m\"\u003e2\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e1.46 TiB\u003cspan class=\"o\"\u003e]\u003c/span\u003e / in no VG: \u003cspan class=\"m\"\u003e0\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e   \u003cspan class=\"o\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e只要 pv 在，那么一切都好说。\u003c/p\u003e","title":"pvscan 没返回信息别慌：Global lock failed 的排查"},{"content":"sanlock 的 wdmd 看门狗在多 vg 场景下不适合启用：只要一个锁空间续约失败，wdmd 就不喂狗，导致宿主机被强制重启。上层的事应该上层做，双写并发控制应该用业务全局锁，而不是依赖底层存储机制。\n现象 sanlock 这里，还是应该把 wdmd 给禁用了。尤其是在多 vg 场景下，不太适合。\n根因 wdmd 是什么 wdmd 是 Watchdog Multiplexing Daemon（看门狗多路复用守护进程）。\n为什么多 vg 场景下不适合 一个锁空间，sanlock 会和 wdmd 建立一个连接。一个 vg 一个锁空间。 只要有一个锁空间续约失败，wdmd 就不会喂狗。 所以，如果一个 vg 断联了，其他 vg 正常，客户其实不希望关闭宿主机。\n哪些情况会触发 reset 存储 IO 压力大，也会导致 sanlock 续约失败，但是上面的虚拟机是正常的； 如果 wdmd 自身异常了，也会触发 watchdog reset； 如果 sanlock 服务异常了，也会触发 watchdog reset； 有些慢存储，sanlock 容易锁过期、失效，也会触发 reset。 这些都不是用户想看到的。\n设计思考 sanlock 是个底层存储锁，但是上层应用有自己的场景和控制逻辑。\n底层做，只能一刀切：\n要么很严格，动不动就关宿主机了； 要么很宽松，那可能还是会造成双写数据不安全。 上层的事上层做。\n双写，终究是因为有两个虚拟机。\n所以，要在创建虚拟机那里使用业务的全局锁，来做并发控制，而不是依赖底层存储机制。\n小结 wdmd 看门狗在多 vg 场景下不适合启用； 只要一个锁空间续约失败，wdmd 就不喂狗，导致宿主机被强制重启； 存储 IO 压力大、wdmd 异常、sanlock 异常、慢存储锁过期，都可能触发 reset； 底层锁只能一刀切，双写并发控制应该用业务全局锁，上层的事上层做。 ","permalink":"/posts/sanlock-wdmd-multi-vg-disable/","summary":"\u003cp\u003esanlock 的 wdmd 看门狗在多 vg 场景下不适合启用：只要一个锁空间续约失败，wdmd 就不喂狗，导致宿主机被强制重启。上层的事应该上层做，双写并发控制应该用业务全局锁，而不是依赖底层存储机制。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003esanlock 这里，还是应该把 wdmd 给禁用了。尤其是在多 vg 场景下，不太适合。\u003c/p\u003e\n\u003ch2 id=\"根因\"\u003e根因\u003c/h2\u003e\n\u003ch3 id=\"wdmd-是什么\"\u003ewdmd 是什么\u003c/h3\u003e\n\u003cp\u003ewdmd 是 Watchdog Multiplexing Daemon（看门狗多路复用守护进程）。\u003c/p\u003e\n\u003ch3 id=\"为什么多-vg-场景下不适合\"\u003e为什么多 vg 场景下不适合\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e一个锁空间，sanlock 会和 wdmd 建立一个连接。一个 vg 一个锁空间。\u003c/li\u003e\n\u003cli\u003e只要有一个锁空间续约失败，wdmd 就不会喂狗。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e所以，如果一个 vg 断联了，其他 vg 正常，客户其实不希望关闭宿主机。\u003c/p\u003e\n\u003ch3 id=\"哪些情况会触发-reset\"\u003e哪些情况会触发 reset\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e存储 IO 压力大，也会导致 sanlock 续约失败，但是上面的虚拟机是正常的；\u003c/li\u003e\n\u003cli\u003e如果 wdmd 自身异常了，也会触发 watchdog reset；\u003c/li\u003e\n\u003cli\u003e如果 sanlock 服务异常了，也会触发 watchdog reset；\u003c/li\u003e\n\u003cli\u003e有些慢存储，sanlock 容易锁过期、失效，也会触发 reset。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这些都不是用户想看到的。\u003c/p\u003e\n\u003ch2 id=\"设计思考\"\u003e设计思考\u003c/h2\u003e\n\u003cp\u003esanlock 是个底层存储锁，但是上层应用有自己的场景和控制逻辑。\u003c/p\u003e\n\u003cp\u003e底层做，只能一刀切：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e要么很严格，动不动就关宿主机了；\u003c/li\u003e\n\u003cli\u003e要么很宽松，那可能还是会造成双写数据不安全。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e上层的事上层做\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e双写，终究是因为有两个虚拟机。\u003c/p\u003e\n\u003cp\u003e所以，要在创建虚拟机那里使用业务的全局锁，来做并发控制，而不是依赖底层存储机制。\u003c/p\u003e\n\u003ch2 id=\"小结\"\u003e小结\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003ewdmd 看门狗在多 vg 场景下不适合启用；\u003c/li\u003e\n\u003cli\u003e只要一个锁空间续约失败，wdmd 就不喂狗，导致宿主机被强制重启；\u003c/li\u003e\n\u003cli\u003e存储 IO 压力大、wdmd 异常、sanlock 异常、慢存储锁过期，都可能触发 reset；\u003c/li\u003e\n\u003cli\u003e底层锁只能一刀切，双写并发控制应该用业务全局锁，上层的事上层做。\u003c/li\u003e\n\u003c/ul\u003e","title":"sanlock 的 wdmd 为什么在多 vg 场景下要禁用？"},{"content":"在 ARM（aarch64）架构下，libvirt/QEMU 的 CPU 模型与特性禁用设置不生效，虚拟机看到的 CPU 依然是透传的物理 CPU。所以 ARM 架构下，别探索什么指令集、热迁移了，全部用 passthrough。\n现象 将虚拟机 CPU 设置为 custom 模式，指定 Kunpeng-920 指令集，并通过 \u0026lt;feature policy=\u0026quot;disable\u0026quot;\u0026gt; 禁用 asimddp、asimdfhm 两个 CPU 特性。\n对比宿主机与虚拟机内的 lscpu / /proc/cpuinfo 输出，发现设置未生效。\n尽管 QEMU 进程命令行已携带 -cpu Kunpeng-920,asimddp=off,asimdfhm=off，虚拟机内部看到的 CPU 依然是透传的物理 CPU（特性、implementer、part 号与宿主机完全一致）。\n排查 测试环境 项目 值 宿主机架构 aarch64 (ARM64) 宿主机 CPU HUAWEI Kunpeng 920 3210 (HiSilicon) 虚拟机架构 aarch64, machine virt-4.1 QEMU qemu-system-aarch64 (4.1.0.2) 虚拟化 KVM (accel=kvm) 虚拟机内存/CPU 1024 MiB / 1 vCPU 虚拟机 XML 的 CPU 部分 \u0026lt;cpu mode=\u0026#34;custom\u0026#34; match=\u0026#34;exact\u0026#34;\u0026gt; \u0026lt;model\u0026gt;Kunpeng-920\u0026lt;/model\u0026gt; \u0026lt;feature policy=\u0026#34;disable\u0026#34; name=\u0026#34;asimddp\u0026#34;/\u0026gt; \u0026lt;feature policy=\u0026#34;disable\u0026#34; name=\u0026#34;asimdfhm\u0026#34;/\u0026gt; \u0026lt;topology sockets=\u0026#34;1\u0026#34; cores=\u0026#34;1\u0026#34; threads=\u0026#34;1\u0026#34;\u0026gt;\u0026lt;/topology\u0026gt; \u0026lt;numa\u0026gt; \u0026lt;cell id=\u0026#34;0\u0026#34; cpus=\u0026#34;0\u0026#34; memory=\u0026#34;1048576\u0026#34; unit=\u0026#34;KiB\u0026#34; discard=\u0026#34;yes\u0026#34;\u0026gt;\u0026lt;/cell\u0026gt; \u0026lt;/numa\u0026gt; \u0026lt;/cpu\u0026gt; QEMU 进程的 -cpu 参数 虚拟机启动后，QEMU 进程实际命令行中的 CPU 相关参数：\n-cpu Kunpeng-920,asimddp=off,asimdfhm=off 完整命令行关键片段：\n-machine virt-4.1,accel=kvm,usb=off,dump-guest-core=off,gic-version=3 -cpu Kunpeng-920,asimddp=off,asimdfhm=off -smp 1,sockets=1,dies=1,cores=1,threads=1 -numa node,nodeid=0,cpus=0,mem=1024 说明 QEMU 命令行确实携带了 Kunpeng-920 模型及 asimddp=off,asimdfhm=off 特性禁用参数，说明 libvirt 已正确将 XML 翻译为 QEMU 参数。\n宿主机 lscpu（物理机） Architecture: aarch64 CPU op-mode(s): 64-bit Byte Order: Little Endian CPU(s): 48 On-line CPU(s) list: 0-47 Thread(s) per core: 1 Core(s) per socket: 24 Socket(s): 2 NUMA node(s): 2 Vendor ID: HiSilicon Model name: Kunpeng-920 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma dcpop asimddp asimdfhm 虚拟机内 lscpu Architecture: aarch64 CPU op-mode(s): 64-bit Byte Order: Little Endian CPU(s): 1 On-line CPU(s) list: 0 Vendor ID: HiSilicon Model name: Kunpeng-920 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma dcpop asimddp asimdfhm NUMA node(s): 1 NUMA node0 CPU(s): 0 关键差异点：asimddp / asimdfhm 特性 项目 宿主机 虚拟机 预期（若设置生效） Flags 中的 asimddp 存在 存在 应被禁用 Flags 中的 asimdfhm 存在 存在 应被禁用 QEMU 参数 - -cpu Kunpeng-920,asimddp=off,asimdfhm=off - 核心矛盾：QEMU 命令行明确携带 asimddp=off,asimdfhm=off，但虚拟机内 lscpu 与 /proc/cpuinfo 的 Flags 中仍然包含 asimddp asimdfhm。\n说明特性禁用参数在 ARM/KVM 下未对 guest 生效。\nCPU 身份信息对比 项目 宿主机 虚拟机 是否一致 CPU implementer 0x48 0x48 一致 CPU part 0xd01 0xd01 一致 CPU variant 0x1 0x1 一致 CPU revision 0 0 一致 Vendor ID HiSilicon HiSilicon 一致 Model name Kunpeng-920 Kunpeng-920 一致 虚拟机内 CPU 的 implementer（0x48 = HiSilicon）与 part（0xd01 = Kunpeng-920）与宿主机完全一致，这是 CPU 透传（passthrough）的典型特征——guest 直接读取了物理 CPU 的 MIDR 寄存器值。\n根因 ARM 架构下，libvirt/QEMU 的 CPU 模型与特性禁用设置未对 guest 生效。\n虚拟机内看到的 CPU 依然是透传的物理 CPU：\nguest 的 Flags 仍包含本应被禁用的 asimddp、asimdfhm guest 的 CPU implementer（0x48）、part（0xd01）与宿主机完全一致，直接暴露物理 CPU 身份 QEMU 命令行参数已正确生成，说明问题不在 libvirt 的 XML 解析/翻译层，而在于 ARM/KVM 虚拟化层对 CPU 模型与特性的处理方式。\nARM 架构下 KVM 默认将物理 CPU 特性透传给 guest，-cpu 指定的模型/特性开关在 aarch64 + KVM 场景下未产生实际约束效果。\n仅资源拓扑层面生效：vCPU 数量、socket/core 拓扑、NUMA 拓扑等资源分配参数正常生效。\n与 x86 的差异：x86 架构下 -cpu \u0026lt;model\u0026gt;,\u0026lt;feature\u0026gt;=off 会真实改变 guest 可见的 CPUID 特性；ARM 架构下该机制不适用，guest 直接读取物理 CPU 的 MIDR/特性寄存器。\n小结 ARM 架构下，CPU 模型设置基本失效，实际只有 passthrough； 要热迁移，就必须要保证物理机的 CPU 基本一样； 同系列、不同型号的 CPU，把 flags 设置为一样，大概率可以热迁移，但需要测试； 不同厂商的 ARM CPU，基本不可能热迁移； 别在 ARM 上探索指令集、热迁移这些 x86 的玩法了，全部用 passthrough。 ","permalink":"/posts/arm-cpu-model-passthrough/","summary":"\u003cp\u003e在 ARM（aarch64）架构下，libvirt/QEMU 的 CPU 模型与特性禁用设置不生效，虚拟机看到的 CPU 依然是透传的物理 CPU。所以 ARM 架构下，别探索什么指令集、热迁移了，全部用 passthrough。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e将虚拟机 CPU 设置为 \u003ccode\u003ecustom\u003c/code\u003e 模式，指定 \u003ccode\u003eKunpeng-920\u003c/code\u003e 指令集，并通过 \u003ccode\u003e\u0026lt;feature policy=\u0026quot;disable\u0026quot;\u0026gt;\u003c/code\u003e 禁用 \u003ccode\u003easimddp\u003c/code\u003e、\u003ccode\u003easimdfhm\u003c/code\u003e 两个 CPU 特性。\u003c/p\u003e\n\u003cp\u003e对比宿主机与虚拟机内的 \u003ccode\u003elscpu\u003c/code\u003e / \u003ccode\u003e/proc/cpuinfo\u003c/code\u003e 输出，发现设置\u003cstrong\u003e未生效\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e尽管 QEMU 进程命令行已携带 \u003ccode\u003e-cpu Kunpeng-920,asimddp=off,asimdfhm=off\u003c/code\u003e，虚拟机内部看到的 CPU 依然是\u003cstrong\u003e透传的物理 CPU\u003c/strong\u003e（特性、implementer、part 号与宿主机完全一致）。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003ch3 id=\"测试环境\"\u003e测试环境\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e项目\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e值\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e宿主机架构\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eaarch64 (ARM64)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e宿主机 CPU\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHUAWEI Kunpeng 920 3210 (HiSilicon)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e虚拟机架构\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eaarch64, machine \u003ccode\u003evirt-4.1\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eQEMU\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eqemu-system-aarch64 (4.1.0.2)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e虚拟化\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eKVM (accel=kvm)\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e虚拟机内存/CPU\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1024 MiB / 1 vCPU\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"虚拟机-xml-的-cpu-部分\"\u003e虚拟机 XML 的 CPU 部分\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;cpu\u003c/span\u003e \u003cspan class=\"na\"\u003emode=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;custom\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ematch=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;exact\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;model\u0026gt;\u003c/span\u003eKunpeng-920\u003cspan class=\"nt\"\u003e\u0026lt;/model\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003epolicy=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;disable\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;asimddp\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;feature\u003c/span\u003e \u003cspan class=\"na\"\u003epolicy=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;disable\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ename=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;asimdfhm\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;topology\u003c/span\u003e \u003cspan class=\"na\"\u003esockets=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ecores=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ethreads=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/topology\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;numa\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026lt;cell\u003c/span\u003e \u003cspan class=\"na\"\u003eid=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ecpus=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;0\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ememory=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;1048576\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003eunit=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;KiB\u0026#34;\u003c/span\u003e \u003cspan class=\"na\"\u003ediscard=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;yes\u0026#34;\u003c/span\u003e\u003cspan class=\"nt\"\u003e\u0026gt;\u0026lt;/cell\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026lt;/numa\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nt\"\u003e\u0026lt;/cpu\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"qemu-进程的--cpu-参数\"\u003eQEMU 进程的 -cpu 参数\u003c/h3\u003e\n\u003cp\u003e虚拟机启动后，QEMU 进程实际命令行中的 CPU 相关参数：\u003c/p\u003e","title":"ARM 架构下 CPU 模型设置为什么失效？只有 passthrough"},{"content":"k8s 环境里 pod 之间 ICMP 通但 TCP 不通，client 收到 synack 却不响应。根因是 server 端 tunl 设备的 tx-checksumming 没生效，发出的包校验和错误，被 client 丢弃。\n现象 有个 k8s 的环境，pod 之间 ICMP 是通的，TCP 不通。\nclient 发送的 SYN 报文，可以收到 server 的 SYNACK 回复，但是 client 并不响应这个 SYNACK 报文。\n排查 通过 tcpdump -vvv 抓包，看到 server 发出的包，在 client 端依然 checksum incorrect。\n怀疑是 server 端的 tx-checksumming: on 功能对 tunl 设备没有生效。\n关闭 server 的该功能后正常：\nethtool -K tunl0 tx off 根因 1. tunl0 是个软件设备 client 和 server 用 tunnel 隧道来通信，这里的 tunl0 是个软件设备。\n2. tx-checksumming 的作用 tx-checksumming: on 表示发送侧硬件校验和计算。\n如果设置为 on，表示网卡支持该特性，kernel 发包的时候不计算校验和，让网卡发送的时候计算。\nethtool -K eth0 tx-checksum-ip-generic on|off 3. 问题出在哪 server 的 tunl 设备，也支持 tx-checksumming: on 选项。\n这一批 server 发出的 SYNACK 报文，从 tunl0 网卡发出来的时候，期待着能在离开网卡的时候，报文的校验和被重新计算为正确的。\n也就是 tx-checksumming: on 这个功能生效，但是，没生效，报文校验和依然错误。\n4. client 端看到的现象 client 收到的 SYNACK 报文，用 tcpdump -vvv 看到有提示：\n如果收到的报文校验和错误，有提示 incorrect 如果正确，有提示 correct server 端的网卡信息：\nroot@host:~# ip -d link show dev tunl0 26: tunl0@NONE: \u0026lt;NOARP,UP,LOWER_UP\u0026gt; mtu 1480 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/ipip 0.0.0.0 brd 0.0.0.0 promiscuity 0 minmtu 0 maxmtu 0 ipip any remote any local any ttl inherit nopmtudisc addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 root@host:~# ethtool -k tunl0 | head Features for tunl0: rx-checksumming: off [fixed] tx-checksumming: on tx-checksum-ipv4: off [fixed] tx-checksum-ip-generic: on tx-checksum-ipv6: off [fixed] tx-checksum-fcoe-crc: off [fixed] tx-checksum-sctp: off [fixed] scatter-gather: on tx-scatter-gather: on client 端抓包，tcpdump -vvv 加上 -vvv 才能看到 checksum 的结果。\n发送的时候，cksum 0x972f (incorrect -\u0026gt; 0x4615) 没关系，因为离开网卡的时候，网卡会计算校验和，符合 tx-checksumming: on 这个功能的预期：\n15:27:52.923926 ip: (tos 0x0, ttl 64, id 23082, offset 0, flags [DF], proto TCP (6), length 60) 10.0.0.42.43332 \u0026gt; 10.0.0.4.9400: Flags [S], cksum 0x972f (incorrect -\u0026gt; 0x4615), seq 4089076775, win 64800, options [mss 1440,sackOK,TS val 20860605 ecr 0,nop,wscale 10], length 0 接收的回复，这里 cksum 0xae7a (correct) 是正确的，如果：\n15:27:52.924453 ip: (tos 0x0, ttl 63, id 0, offset 0, flags [DF], proto TCP (6), length 60) 10.0.0.4.9400 \u0026gt; 10.0.0.42.43332: Flags [S.], cksum 0xae7a (correct), seq 1828286424, ack 4089076776, win 64260, options [mss 1440,sackOK,TS val 3284203794 ecr 20860605,nop,wscale 10], length 0 解决办法 因为这个 tunl 设备是 calico 创建的，想改 tunl 设备还要找好的时机。\n可以修改物理机上的真实设备的 ethtool -K eth10 tx off 来解决。\n同时要修改 rc.local 将该操作记录到开机启动中。\n一点思考 同样的一批 64 台服务器，同样的 tunl 设备通信，有 16 台有问题。\n看到这一批网卡跟正常网卡的区别是，异常服务器是定制的网卡，可能跟这个网卡不支持 tunl 设备的校验和计算有关系。\n小结 pod 之间 ICMP 通但 TCP 不通，优先怀疑校验和问题； tx-checksumming: on 表示让网卡在发送时计算校验和，但软件设备（tunl）上可能不生效； 用 tcpdump -vvv 抓包，看 checksum incorrect / correct 提示； 关闭 ethtool -K tunl0 tx off 可解决； 同型号网卡也可能有差异，异常网卡可能不支持 tunl 设备的校验和计算。 ","permalink":"/posts/calico-icmp-ok-tcp-fail-checksum/","summary":"\u003cp\u003ek8s 环境里 pod 之间 ICMP 通但 TCP 不通，client 收到 synack 却不响应。根因是 server 端 tunl 设备的 \u003ccode\u003etx-checksumming\u003c/code\u003e 没生效，发出的包校验和错误，被 client 丢弃。\u003c/p\u003e\n\u003ch2 id=\"现象\"\u003e现象\u003c/h2\u003e\n\u003cp\u003e有个 k8s 的环境，pod 之间 \u003cstrong\u003eICMP 是通的，TCP 不通\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003eclient 发送的 SYN 报文，可以收到 server 的 SYNACK 回复，但是 client 并不响应这个 SYNACK 报文。\u003c/p\u003e\n\u003ch2 id=\"排查\"\u003e排查\u003c/h2\u003e\n\u003cp\u003e通过 \u003ccode\u003etcpdump -vvv\u003c/code\u003e 抓包，看到 server 发出的包，在 client 端依然 \u003ccode\u003echecksum incorrect\u003c/code\u003e。\u003c/p\u003e\n\u003cp\u003e怀疑是 server 端的 \u003ccode\u003etx-checksumming: on\u003c/code\u003e 功能对 tunl 设备没有生效。\u003c/p\u003e\n\u003cp\u003e关闭 server 的该功能后正常：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-sh\" data-lang=\"sh\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eethtool -K tunl0 tx off\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"根因\"\u003e根因\u003c/h2\u003e\n\u003ch3 id=\"1-tunl0-是个软件设备\"\u003e1. tunl0 是个软件设备\u003c/h3\u003e\n\u003cp\u003eclient 和 server 用 tunnel 隧道来通信，这里的 tunl0 是个软件设备。\u003c/p\u003e","title":"为什么 ICMP 通但 TCP 不通？tunl 设备校验和没生效"}]