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

魔数、默认值使用常量封装。提高可读性,统一定义唯一来源,唯一来源相当于是统一了标准。常量在离使用代码最近的地方定义比较好。 现象 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 · 文叨叨

开发的时候就要想部署交付:为什么要云原生

开发决定了部署。开发的时候就要想哪种方式部署简单、能标准化部署、能尽量减少环境依赖。这就是为什么要云原生。 核心:开发决定了部署 开发的时候,就要想: 哪种方式部署简单; 哪种方式能标准化部署; 哪种方式能尽量地减少环境依赖,让部署不容易出错。 比如部署 BM,开发的时候不能只想着能把 BM 跑起来,还要想后续部署能否简单、标准化、少定制化配置。 比如动态启动 pxe server(tftp、dhcpd)和静态配置好这两个服务,是否可行,对于部署哪个友好。 当然,当前最好的方式当然是云原生化。 为什么要云原生 部署/交付是一个很重要的事,且很消耗成本,且能算得清的成本的事情。 所以为什么云原生很重要,它把应用交付的效率大大提高了。 而开发决定的是部署交付。 这就是为什么要云原生,就是从第一天开发,就要想着以后是 k8s 上部署交付的。 如果你不想着 k8s 上部署,像我们屎山代码,各种通过 hostname 点对点通信,各种 ssh 执行命令,各种 exec 执行宿主机的命令,后续很难部署到容器中。 云计算 vs 云原生 云计算解决的是 IT 资源的交付效率、基础设施的运维; 云原生解决的是软件交付效率、软件生命周期的运维。 小结 开发决定了部署; 开发的时候就要想哪种方式部署简单、能标准化部署、能尽量减少环境依赖; 部署/交付是很消耗成本、且能算得清成本的事情; 云原生把应用交付的效率大大提高了; 从第一天开发,就要想着以后是 k8s 上部署交付的。

March 15, 2026 · 1 min · 45 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 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,太难维护了,扩展性很差。 这个时候,如果我们用的是接口: 那么所有类似 disk_build 的地方,参数都是接口; disk_build 内没有条件判断,只有接口调用; 而调用 disk_build 的时候传入具体的存储类型; 大量的 disk_build,就避免了大量的条件判断。 条件判断不是问题,重复判断才是问题。 而接口,也可以方便的扩展存储,因为不用去调整大量的 disk_build 函数,只需要在调用 disk_build 的地方传入新的类型。 接口不是完全避免了新引入类型的工作量,入口处还需要处理,但是依然保证了内部核心流程的稳定性,因为入口处肯定不是核心流程嘛。 小结 面向接口,一写二抄三重构; 前面没有必要的时候不用接口,后面重复的时候可以引入接口; 二次重复的时候,就必须要重构了; 条件判断不是问题,重复判断才是问题; 接口保证内部核心流程的稳定性,入口处处理新类型即可。

March 8, 2026 · 1 min · 46 words · 文叨叨

封装和独立的区别:复用 vs 管理能力

封装仅仅是为了复用,但是独立有管理能力。封装调用了就得执行,独立有自己的一套管理规则,比如排队、限流、调整优先级、丢弃、转换等。 核心:封装 vs 独立 封装仅仅是为了复用,但是独立有管理能力。 封装:调用了就得执行,里面通过各种条件逻辑控制行为; 独立:有自己的一套管理能力,有自己的一套规则。 请求来了,不是立马执行逻辑,而是先进管理规则。 比如排队、限流、调整优先级、丢弃、转换等等。 独立不一定要独立服务 独立并不是说要一个独立服务,服务内的一个模块、一个库,都可以是独立的。 一个例子:report 服务 应该有个独立的 report。 如果是 report 函数,大家分散调用,调用的频率不一样: A 30 秒调用一次; B 20 秒调用一次; … 这玩意没法统一处理,调整和排查的时候也麻烦。加一个监控项的时候也很麻烦,触发的地方、触发的间隔、获取数据都需要考量。 如果像 k8s 一样统一上报,比如 kubelet 收集 node 信息上报给 k8s,至于收集哪些监控项,kubelet 再提供相应的接口,插件对接 kubelet 的接口即可。 这样的好处是,扩展仅仅是本地收集的处理,上报流程不需要考虑,甚至不需要知道。 核心还是提取不变的,封装变化的。 小结 封装仅仅是为了复用,独立有管理能力; 封装调用了就得执行,独立有自己的一套管理规则; 独立不一定要独立服务,服务内的模块、库都可以是独立的; 统一管理:其他地方过来调用仅仅是一个请求,什么时候调用、如何调用、什么优先级,有自己的管理; 核心是提取不变的,封装变化的。

March 7, 2026 · 1 min · 45 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 · 文叨叨