调用第三方一定要封装,哪怕仅仅是一个命令行

调用第三方工具一定要封装,哪怕仅仅是一个命令行。封装的好处:应对版本变化、方便做切面、简化使用、提供统一方法。 现象 qemu-img 是一个典型的例子。 为啥要封装? 底层工具版本变化导致的参数校验变化; 方便做切面,可以做一些统计、日志、重试、timeout 等; 简化使用,底层工具参数众多,大部分都用不上; 提供实现目的的统一方法,少即是多,实现一个目的最好只有一种方法。 封装得好是什么样 qemu-img 自认为封装的挺好。 对于一些版本不支持的参数,明确打印出来了,通过 error 日志一眼看出问题,这效率,QA 也能排查了。 比如: logger.error("QemuImg commit: qemu-img commit [-d] not supported") 一个封装示例 class QemuImg(object): """ qemu-img Package """ @classmethod def commit_str(cls, filename, image_opts=None, fmt=None, skip_empty=False, host=None, ): """ 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. """ if fmt and fmt not in ["raw", "qcow2"]: logger.error("QemuImg commit: unsupported format: %s" % fmt) return None if image_opts and not isinstance(image_opts, dict): logger.error("QemuImg commit: image_opts must be a dict") return None if skip_empty: has_d_cmd = r'qemu-img -h | grep commit | grep "\-d"' ret = exec_cmd(has_d_cmd, remote_host=host) if not is_cmd_ok(ret): logger.error("QemuImg commit: qemu-img commit [-d] not supported") return None cmd_str = "qemu-img commit" if fmt: cmd_str += " -f %s" % fmt if skip_empty: cmd_str += " -d" if image_opts: image_opts_str_li = ["%s=%s" % (key, value) for key, value in image_opts.items()] if image_opts_str_li: cmd_str += " --image-opts %s" % ",".join(image_opts_str_li) if filename: cmd_str += " %s" % filename logger.info("QemuImg commit cmd: %s", cmd_str) return cmd_str 小结 调用第三方工具一定要封装,哪怕仅仅是一个命令行; 封装应对版本变化、方便做切面、简化使用、提供统一方法; 对于版本不支持的参数,明确打印出来,QA 也能排查; 少即是多,实现一个目的最好只有一种方法。

February 21, 2026 · 1 min · 212 words · 文叨叨

抽象流程还是抽象对象?一写二抄三重构

要不要抽象,关键在于要不要复用。业务流程都是独立的,不需要复用;技术类流程需要复用。一写二抄三重构,第二次大量重复时就可以开始重构了。 现象 写代码的时候,总纠结要不要抽象流程、抽象对象、面向组合。 这样很难,因为没有明确的复用需求,不知道稳定点在哪。 核心:一写二抄三重构 一写,二抄,三重构。 要不要抽象,关键在于要不要复用。 业务流程都是独立的,不需要复用,所以谈不上抽象; 技术类的流程,是需要复用的。 但是没必要第一次就绞尽脑汁抽象流程、抽象对象、面向组合。 第二次是抄还是重构 不是说第二次就一定是抄。 如果是一个方法,抄没问题; 如果是一个流程,一堆方法,第二次抄就不合理了。 这样的后果是第三次重构非常困难,甚至会因为功能的兼容影响没法进行。 如果是大量的话,第二次就可以开始重构了。 抽象流程 vs 抽象对象 k8s 为什么牛逼,原因在于它定义了一套声明式的流程。 我们创建主机、创建备份等,看起来流程不一样,但是上升一个维度,都能用声明式的流程来处理。 但 2026 年更新后的认知是:k8s 是极致的面向对象。 虽然它的控制器是面向流程的,但是控制器仅仅是对象下的血肉。 顶层还是围绕对象设计的; 控制器是属于对象的; k8s 极致面向对象,消除流程,将流程隐藏在对象中。 流程是易变的,对象是相对稳定的,所以不会去抽象流程,而是抽象对象。 一个例子:采集 pv-lv 和 lv info 数据采集、数据同步,涉及到新增和删除。 所以这里的流程最后抽象为: 采集; 从 db 获取; diff 得到 insert 和 delete; 执行 insert 和 delete。 不同的数据对象,提供支持 diff、insert 和 delete 的方法即可。 这个流程也不是一开始就想出来的,也是边写、边思考做出来的。 小结 要不要抽象,关键在于要不要复用; 业务流程独立,不需要复用;技术类流程需要复用; 一写二抄三重构,第二次大量重复时就可以开始重构; 流程是易变的,对象是相对稳定的,所以抽象对象而非流程; 好的抽象是边写边思考做出来的,不是一开始就设计好的。

February 15, 2026 · 1 min · 64 words · 文叨叨

代码写三遍:跑通、防御、优化

写代码高价值套路:代码写三遍。第一遍跑通,第二遍防御,第三遍优化。过早的优化是万恶之源,尤其是新服务新项目的时候。 现象 写代码的时候,总想一步到位。 前期查看各种基础知识,耗费时间;后面组织结构,想日志封装、调度封装,又耗费时间比较多。 结果两周没写完。 核心:代码写三遍 来自华哥(李运华)的写代码高价值套路原则: 防御性编程; 代码写三遍; 快速失败; 面向对象原则; 短函数。 其中"代码写三遍"是核心: 跑通:先把功能跑通; 防御:加上防御性编程; 优化:最后再优化。 过早的优化是万恶之源。尤其是在新服务新项目的时候。 为什么写三遍反而更快 比如这次的"自愈组件"。 如果按照写三遍: 跑通,最多 4 天; 异常场景最多 1 天; 后面针对性优化、针对性学习,学习效率高。 比如先针对学习日志,针对优化调度,这个过程中会思考是否过度优化,其实发现很多没必要。 关键这是一个快速交付的路径,很快有结果,没有汇报压力。 没有压力,有条不紊,是非常重要的一个因素。 防御性编程 防御性编程包括: 安全:边界、非法值校验、特殊情况、异常情况等; 快速失败、回滚等; 追踪:日志等。 小结 代码写三遍:跑通、防御、优化; 过早的优化是万恶之源,尤其是新服务新项目; 写三遍反而更快,因为快速交付、没有汇报压力; 没有压力、有条不紊,是非常重要的因素; 防御性编程:安全、快速失败、追踪。

February 14, 2026 · 1 min · 41 words · 文叨叨

no space left on device:不一定是磁盘满了,可能是 inode 满了

报 no space left on device,看上去是磁盘空间不足,但有可能不是存储满了,而是 inode 满了。用 df -i 查看 inode 使用情况,如果有 100% 的就是 inode 满了。 现象 报 no space left on device,看上去是磁盘空间不足了,那么找没用的文件删了即可。 但有可能不是存储满了,而是 inode 满了。 排查 使用 df -i 查看 inode 的使用情况,如果有 100% 的,那就说明是 inode 满了: root@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 然后查找哪里小文件特别多。 ...

February 8, 2026 · 1 min · 142 words · 文叨叨

删除文件后磁盘空间为什么没释放?

删除文件后磁盘空间没释放,是因为文件被进程打开,进程还持有文件描述符。用 lsof | grep deleted 找到占用进程,最好的方法是在线清空文件。 现象 在 Linux 或者 Unix 系统中,通过 rm 或者文件管理器删除文件,磁盘空间却没有释放。 根因 通过 rm 删除文件,只是将它从文件系统的目录结构上解除链接(unlink),也就是说只是删除了文件和系统目录结构的链接。 如果文件在删除时是被打开的(有一个进程正在使用该文件,文件被进程锁定或者有进程一直在向这个文件写数据等)状态,那么进程将仍然可以读取该文件,进程还持有文件的文件描述符 fd,所以磁盘空间也就会一直被占用。 一个文件在文件系统中的存放分为两个部分:数据部分和指针部分。 指针位于文件系统的 meta-data 中,数据被删除后,这个指针就从 meta-data 中清除了; 而数据部分存储在磁盘中,数据对应的指针从 meta-data 中清除后,文件数据部分占用的空间就可以被覆盖并写入新的内容; 之所以出现删除文件后空间还没释放,就是因为有进程还在一直向这个文件写入内容,导致虽然删除了文件,但文件对应的指针部分由于进程锁定,并未从 meta-data 中清除; 而由于指针并未被删除,那么系统内核就认为文件并未被删除,因此通过 df 命令查询空间并未释放也就不足为奇了。 解决办法 1. 找到占用进程并 kill 通过 lsof | grep deleted 命令获取到已经被删除但是仍然被应用程序占用的文件列表,然后 kill 掉还在占用所删除文件的进程。 需要注意的是:如果有很多进程都在使用所删除文件,那么采用这种方式 kill 进程就有点麻烦了,而且风险也比较大。 因为 kill 进程是通过截断 proc 文件系统中的文件可以强制要求系统回收分配给正在使用的文件。 必须要确定不会对运行中的进程造成影响时才能使用,应用程序对这种方式支持的并不好,当一个正在使用的文件被截断可能会引发不可预知的问题。 2. 停掉或重启应用 停掉或重启使用这个所删除文件的应用,让 OS 自动回收磁盘空间。 3. 重启操作系统 也可以重启操作系统,不过这并不是最好的方法。 4. 在线清空文件(推荐) 对待这种进程不停对文件写日志的操作,要释放文件占用的磁盘空间,最好的方法是在线清空这个文件。 通过这种方法,磁盘空间不但可以马上释放,也可保障进程继续向文件写入日志。 在线清空文件(比如 /var/log/app.log)的方式: ...

February 7, 2026 · 1 min · 94 words · 文叨叨

virsh attach/detach 的延时问题:重试不是灵药,可能是毒药

virsh detach 后 domblklist 有短暂延时,detach 成功后立即 detach 相同卷也能成功,两次重复 detach 后再 attach 会失败。根因是 libvirt 不是实时状态,有一定延时。 现象 已测试:连续 attach、detach、attach、detach… 反复是没有问题的。 有问题的: detach 后,virsh domblklist 是没有卸载的,要过一两秒,有短暂延时; detach 成功后,立即 detach 相同的卷,居然也可以成功。预期应该是会报错提示没有对应的卷; 两次重复的 detach 后,再 attach,attach 命令成功,但实际没有把卷挂载上。 根因 我们挺多奇怪的问题,可能都是类似的原因造成的。 libvirt 和 qemu: libvirt 不是实时状态,有一定延时; 然后一些重复操作,看上去是幂等的,但是它对后续操作有影响(其实也是 libvirt 的 bug)。 比如之前有过 domblk 和 qmp 查询卷不一样的情况,造成后续丢数据等。 设计思考 做重试之前,要充分测试。 是否达到了重试条件(状态、时间); 这个动作是否幂等; 对后续动作是否有影响。 重试不是灵药,可能是毒药。 小结 virsh detach 后 domblklist 有短暂延时,要过一两秒; detach 成功后立即 detach 相同卷也能成功,不符合预期; 两次重复 detach 后再 attach,attach 成功但实际没挂载上; 根因是 libvirt 不是实时状态,有一定延时; 做重试之前要充分测试,重试不是灵药,可能是毒药。

February 1, 2026 · 1 min · 73 words · 文叨叨

libvirt CPU feature 的 disable 和 forbid 有什么区别?

libvirt CPU feature 的 policy 有 force、require、optional、disable、forbid 五种。disable 是虚拟 CPU 不支持该特性,forbid 是宿主机支持该特性时创建会失败。 现象 在 libvirt XML 中配置 CPU feature: <cpu> <model>EPYC</model> <feature name="pclmuldq" policy="disable"/> <feature name="aes" policy="disable"/> <feature name="sha-ni" policy="disable"/> <feature name="monitor" policy="disable"/> <feature name="svm" policy="disable"/> <feature name="npt" policy="disable"/> 【物理机支撑 npt 的】 <topology cores="1" sockets="1" threads="1"/> </cpu> 物理 CPU 是有 npt 的,virsh create 的时候就报错,而且这是 disable 也报错。 足以说明,不是 CPU 问题,而是 libvirt 不支持问题: 2025-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 是可以创建虚拟机的 <qemu:commandline> <qemu:arg value="-cpu"/> <qemu:arg value="EPYC,vendor=AuthenticAMD,model_id=Example CPU 7380 32-core Processor,-x2apic,-hypervisor,-topoext,-npt,-nrip-save,-monitor,-svm"/> </qemu:commandline> 说明什么? ...

January 31, 2026 · 2 min · 267 words · 文叨叨

ARM 虚拟机缺 acpi 为什么登录没反应?

ARM 虚拟机配置中 <features> 区块缺少 <acpi/>,表面现象是 virsh console 登录没反应。根因是 ACPI 缺失导致系统底层硬件/服务初始化异常,间接引发登录流程失败。 现象 ARM 虚拟机配置中 <features> 区块里缺少 <acpi/>,虚拟机异常。 表面现象是 virsh console 的时候登录没有反应。 这种时候其实都是虚拟机启动阶段就有问题了。 排查 各 feature 的作用 <apic/> - 高级可编程中断控制器(Advanced Programmable Interrupt Controller) 这是 x86 架构的中断控制器标准,替代老式的 8259 PIC; 负责管理 CPU 的中断请求(比如硬件设备的信号、定时器中断等); 支持多核心 CPU 的中断分发、中断优先级控制、中断重映射等; 对 ARM 环境的意义:ARM 架构不使用 APIC,这个配置属于冗余配置(无实际作用),是兼容 x86 配置模板留下的,删除也不会影响 ARM 虚拟机运行。 <pae/> - 物理地址扩展(Physical Address Extension) 这是 32 位 x86 架构的内存扩展技术,让 32 位 x86 系统突破 4GB 物理内存的访问限制(最大支持 64GB); 对 ARM 环境的意义:你的环境是 64 位 ARM(aarch64),天然支持大于 4GB 的内存寻址,<pae/> 完全无作用,属于冗余配置。 <acpi/> - 高级配置与电源接口(Advanced Configuration and Power Interface) ...

January 25, 2026 · 1 min · 191 words · 文叨叨

qemu 为什么只支持一个 IDE controller?

qemu 只支持一个 IDE controller,一个 IDE controller 只支持 4 个设备(hda 到 hdd)。定义两个 IDE controller 或使用 hde 都会报错。 现象 在 libvirt XML 中定义两个 IDE controller: <controller type='ide' index='0'/> <controller type='ide' index='1'/> 直接会报如下错误: error: 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。 所以不定义 <controller type='ide' index='0'/>,也是有一个 IDE controller 的。 直接定义 IDE disk 的情况 <disk type="network" device="cdrom"> <driver name="qemu" type="raw"></driver> <source protocol="http" name="/yy/img-example.iso"> <host name="qa1b01" port="8021"></host> </source> <target dev="hdd" bus="ide"></target> 【注意 hdd】 <readonly></readonly> <shareable></shareable> </disk> 直接定义了 IDE 的 disk,没有定义 IDE 的 controller。 ...

January 24, 2026 · 1 min · 174 words · 文叨叨

qemu-img commit 的链为什么必须完整?-d 参数又是什么

qemu-img commit 的链必须是完整的,虽然 commit 的是 head,看起来和底层几级镜像没关系,但实际还是需要。commit 链的关系还可以是临时的,-d 参数决定 commit 后是否清空文件。 现象 qemu-img commit 的链,必须是完整的。 虽然你 commit 的是 head,看起来和底层几级的镜像没有关系,但实际还是需要的: root@host:~/tmp# qemu-img commit -f qcow2 /root/tmp/vm-example_tmp.img qemu-img: Could not open '/root/tmp/vm-example_tmp.img': Could not open backing file: Could not open backing file: Could not open '/root/tmp/focal1x64a': No such file or directory 排查 commit 链的关系还可以是临时的 下面命令不会改变 /dev/vg2/vm-example_lss-sjjgi703 的 backing file,但是确可以提交数据到其他链上,非常方便: qemu-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,也会失败: 2024-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, '', u"qemu-img: Block format 'nbd' used by node '#block817' does not support reopening files")] 如果底层的链中有 nbd 镜像,那么即便 commit 没到 nbd img,也会失败。 ...

January 18, 2026 · 2 min · 224 words · 文叨叨