长运行与临时调整要分离

存量 VM 不切换,我们可以提供一个接口来调整虚拟机的 cpu_mode 来实现。这样长运行的逻辑简单,而调整和长运行逻辑分离,降低复杂性。 核心:长运行与临时调整分离 存量 VM 不切换。我们可以提供一个接口来调整虚拟机的 cpu_mode 来实现。 这样长运行的逻辑简单,而调整和长运行逻辑分离,降低复杂性。 一定要区分长运行,有些需求确实是真需求,但不属于长运行逻辑。 比如存储虚拟机的切换。 我们太多控制开关,就是为了做这些东西,本来是一个脚本能批量处理的事,结果把逻辑搞的异常复杂。 一个例子 对,有个接口层的。 我们一个查询接口很慢,最后排查发现这个查询接口里一行一行的在更新数据。 追溯发现,是增加了某个字段,这个字段在后面的流程里比较重要,且不能简单的设置某个值。 所以在查询的时候,通过运算实时赋值。简直了。 大哥,你弄个脚本全部跑一遍就行了啊,升级的时候执行这个脚本啊。 小结 长运行与临时调整要分离; 存量 VM 不切换,提供一个接口来调整,长运行的逻辑简单; 一定要区分长运行,有些需求是真需求,但不属于长运行逻辑; 批量、整体操作,用脚本跑一遍就行,不要把逻辑搞的异常复杂; 不要在查询接口里一行一行的更新数据。

May 24, 2026 · 1 min · 29 words · 文叨叨

魔数、默认值使用常量封装:唯一来源

魔数、默认值使用常量封装。提高可读性,统一定义唯一来源,唯一来源相当于是统一了标准。常量在离使用代码最近的地方定义比较好。 现象 const ( // NamespaceDefault means the object is in the default namespace which is applied when not specified by clients NamespaceDefault string = "default" // NamespaceAll is the default argument to specify on a context when you want to list or filter resources across all namespaces NamespaceAll string = "" // NamespaceNodeLease is the namespace where we place node lease objects (used for node heartbeats) NamespaceNodeLease string = "kube-node-lease" ) 这是 k8s 的 core 的 type 资源定义。 ...

March 28, 2026 · 1 min · 99 words · 文叨叨

符合设计原则,可以规避一些想不到的问题

把只读状态写到了事务字段里,明显不符合单一原则。一个字段怎么能表示多种意思呢?如果一开始就不违反单一原则,即便没想到并发冲突问题,也不会出现问题。 现象 比如做只读,将只读状态写到了事务字段里。 这明显就不符合设计原则,单一原则。 一个字段怎么能表示多种意思的呢? 开始觉得事务字段已经在页面展示了,加个只读前端自动展示就完事了。 结果事务字段还有其他的作用,防止并发操作。 和上面一样,打补丁按照流程,没有按照对象,那在处理事务字段的判断里跳过 readonly 的操作。 结果根本改不完啊,只读检查和所有流程耦合在一起,简直了。 根因:违反单一原则 如果一开始就不要违反单一原则,即便没有想到并发冲突问题,也不会出现问题。 小结 符合设计原则,可以规避一些想不到的问题; 一个字段不能表示多种意思,违反单一原则; 打补丁要按照对象,而不是按照流程; 只读检查和所有流程耦合在一起,根本改不完; 如果一开始就不违反单一原则,即便没想到并发冲突问题,也不会出现问题。

March 22, 2026 · 1 min · 19 words · 文叨叨

即便是补丁,也要面向对象,而不是针对流程打补丁

哪里遇到问题了,就找个地方加个开关、加个检查、加个条件判断来处理掉。这导致不够系统化,引入了局部的分支点,会造成多流程的耦合交叉,扩展容易遗漏。 现象 我们的代码这种情况非常的多。 就是哪里遇到问题了,好,我们按照这个问题找个地方加一个开关,或加一个检查,或加一个条件判断来处理掉。 这导致的问题,就是不够系统化,引入了局部的分支点,但是对于整体的影响没有分析。 这个分叉会造成后续扩展的问题,扩展的时候压根不会注意到有这个分叉。而且类似的问题可能还会继续分叉。 不够系统化,这样会造成多流程的耦合交叉,扩展容易遗漏。 例子 比如我们关机对于资源的可见性的管理(主要是 image),没有统一的约束和要求。 克隆的时候发现路径不在,报错了,然后加一个 open 修复一下,其他流程也是一样。 哇,这完全是黑盒,完全没法分析和预判,就等错误暴露了修复。 比如 drbd 发现脑裂了,直接操作了 --discard-my-data,这样很有问题,这样上层还在正常的创建资源。 实际需要给 node 抽象状态,当然我们有个 failed 状态,但是这是完全不可用的状态,相当于是宕机状态。 那么你没有准备好的状态,需要独立抽象的。发现了脑裂,其实是需要修复的,标记为未准备好的状态,那么其他流程都根据这个状态决定自己的操作。这是一个解耦的操作。 反常识的组合 包括下面的检查,开启了灾难检查,又开了一个 flag 的情况下跳过检查,很奇怪。 为什么不直接统一为关闭灾难检查呢,或者就统一的 flag 文件。 两个组合在一起,还是相反意思的,非常反常识,非常不结构化、不系统化。 小结 打补丁要面向对象,而不是针对流程打补丁; 哪里遇到问题就加开关、加检查、加条件判断,会导致不够系统化; 不够系统化会造成多流程的耦合交叉,扩展容易遗漏; 给 node 抽象状态,其他流程根据状态决定自己的操作,是解耦的操作; 两个相反意思的 flag 组合在一起,非常反常识、不系统化。

March 21, 2026 · 1 min · 42 words · 文叨叨

代码也有一致性:面向接口而非面向实现

不仅数据有一致性问题,代码的一致性问题更加遍地都是。判断主机是否运行应该面向 libvirt 接口,而不是面向实现中的文件、过程。 现象 我们有个查看虚拟机如果异常关闭、将其拉起的机制。 它判断主机是否在运行的依据是 xml!这非常不对。 而且在服务启动的时候,会去清理 xml,到处都是零散的变更逻辑。你改一个功能都不知道影响哪些。 根因:面向实现,而不是面向接口 判断 xml 显然不符合一致性,因为 virsh 才是根本。 大家都面向 libvirt 接口来判断,就不存在那么多问题了,也不存在要清理 xml,或者说清理 xml 影响不大了。 典型的面向实现,而不是面向接口。 事实是我们依赖 libvirt 搭建的 iaas 平台,所以得围绕 libvirt api,而不是实现中的文件、过程。 面向接口而非面向实现。 一致性来源 另外,主机属于哪个节点、状态是什么,应该有一个一致性的来源。 比如 k8s 就是 etcd。 我们这玩意,自己在那判断然后和数据库做对比,但不是以数据库为准,而是自己判断,美其名是为了根据实际情况避免双写,实际更加复杂和错乱。 go 的 defer 就是典型的一致性表现 这种一致性,在哪里创建,就在那里跟着关闭。 而不是在其他零散的地方关闭,容易忘,也容易乱套。 小结 代码也有一致性问题,而且更加遍地都是; 判断主机是否运行应该面向 libvirt 接口,而不是面向实现中的文件、过程; 面向接口而非面向实现; 主机属于哪个节点、状态是什么,应该有一个一致性的来源(如 k8s 的 etcd); go 的 defer 是典型的一致性表现:在哪里创建,就在那里跟着关闭。

March 14, 2026 · 1 min · 57 words · 文叨叨

底层最好有默认值,上层可以少硬编码

底层(被依赖者)最好有默认值,上层(依赖者)可以少硬编码。判断不是问题,问题是重复判断;硬编码本身是必须的,问题是重复的硬编码。 现象 比如: def get_storage_health(): """ return storage health info: 'healthy'/'unhealthy' """ if not sub_container.is_tps_container(): # Other storage types do not have this check, default healthy return "healthy" 这样上层就不需要硬编码判断是什么存储类型了,只要判断 unhealthy 做处理即可。 核心:底层默认值,上层少硬编码 底层(被依赖者)最好有默认值,上层(依赖者)可以少硬编码。 而 get_storage_health 的硬编码是必不可少的,因为这里本身就是触发的地方,且其他类型本身就不支持此操作。 判断不是问题,重复判断才是问题 这就是说的**“判断不是问题,问题是重复判断”**。 这里硬编码其实也一样: 硬编码本身是必须的; 问题是重复的硬编码; 况且这里的硬编码就是判断。 小结 底层(被依赖者)最好有默认值,上层(依赖者)可以少硬编码; 判断不是问题,问题是重复判断; 硬编码本身是必须的,问题是重复的硬编码; 底层默认值让上层只需要处理异常情况,逻辑更简单。

March 1, 2026 · 1 min · 47 words · 文叨叨

一个目的一种实现:少即是多

不要封装太多的实现方法,会造成使用复杂度。一个目的提供一种方法,提供的太多,反而让使用者陷入学习复杂性。 现象 写一个方法要到处去找合适的调用,挺难受。 尤其是 sql 查询的方法。 为什么会有这种感觉: 确实是项目大小导致的复杂性区别; 确实是归类的约束不强,导致有些零散; 写一个新的成本太大了,少即是多,只提供一种实现方法。 核心:一个目的一种实现 不要封装太多的实现方法,会造成使用复杂度。 一个目的提供一种方法,提供的太多,反而让使用者陷入学习复杂性。 良好的 util 封装 高内聚:一类方法放在一起,不然难找,也不知道想要的有没有、需不需要新写一个。比如 sql 一个文件,qemu-img 一个文件; 使用文档:必须的,要不然找代码效率很低; 一个目的提供一种方法:提供的太多,反而让使用者陷入学习复杂性。 小结 一个目的一种实现,少即是多; 不要封装太多的实现方法,会造成使用复杂度; 良好的 util 封装:高内聚、使用文档、一个目的一种方法; 提供的太多,反而让使用者陷入学习复杂性。

February 28, 2026 · 1 min · 29 words · 文叨叨

封装不在于使用复杂度,而在于使用频率

如果一个工具仅仅在一两个地方调用,那无所谓封装。如果一个工具通篇各处都在调用,那就一定要封装了,即便这个工具仅仅是一行命令。 核心观点 这也是上面 qemu-img 要封装的一个原因。 如果一个工具仅仅在一两个地方调用了,那无所谓封装。 如果一个工具通篇各处都在调用,那就一定要封装了。即便这个工具仅仅是一行命令。 比如我们的 qemu-img、virsh、sql。 为什么 因为使用频率高,意味着: 出问题的概率高,需要统一处理; 版本升级的影响面大,需要统一应对; 日志、统计、重试等切面能力需要统一提供。 小结 封装不在于使用复杂度,而在于使用频率; 一个工具仅在一两个地方调用,无所谓封装; 一个工具通篇各处都在调用,一定要封装,即便只是一行命令; 使用频率高,出问题概率高、影响面大,需要统一处理。

February 22, 2026 · 1 min · 19 words · 文叨叨

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

调用第三方工具一定要封装,哪怕仅仅是一个命令行。封装的好处:应对版本变化、方便做切面、简化使用、提供统一方法。 现象 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 · 文叨叨

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

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

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