决策树还是决策层?决策流程适合树

决策流程这个场景,适合树而不是层。树是前后依赖的,层是前后不依赖的、每一层是通用的。规则引擎天生就是规则流,前后有依赖,所以写规则配置就是树的方式。 现象 树的方式对人是友好的,非常直观、快速理解。 如果我想知道我们是否支持 xxx + xxx 的组合,从左到右很快能得到答案。 如果是层,那么你需要知道每一层的逻辑,同时你还需要知道层与层之间的关联。 所以,决策流程这个场景,适合树,而不是层。 我们的场景 我们的场景是通过环境信息(cpu 架构、型号、qemu 版本等)设置 xml 配置。 这个场景推荐用树的方式,核心原因是架构(arm/x86)是"分支决定性条件",后续逻辑完全依赖该分支,树结构能天然贴合独立流程、降低耦合。 逻辑贴合度高:你的需求本质是"架构分流 → 各自校验 qemu 版本 → 各自输出结果",树结构的"分支独立"特性和这个流程完全匹配; 维护成本更低:后续若扩展(比如 arm 新增"内存≥4G"条件、x86 修改 qemu 版本要求为 > 5.0),树结构可直接在对应分支修改,不影响另一分支; 可读性更强:树结构能直观呈现"arm 线"和"x86 线"的完整流程,排查问题时可直接定位到对应分支。 还有关键一点,我们的场景每个流程线能复用的层比较少,比如 arm 直接就是 passthrough,后面流程都是针对 x86 设计的。 树 vs 层 如果是无依赖的过滤,反而适合写代码,比如 k8s apiserver 里的 webhook。 如果是决策流程,其实是规则流程,就适合规则引擎。 因为规则引擎天生的逻辑就是上一个规则结果传入下一个规则结果,它是前后有依赖的。 而无依赖的是独立的,代码更容易实现扩展性、精确性和复杂逻辑,且就算变化也是局部变化。 层结构更适合"条件无依赖、逐步缩小范围“的场景(比如先过滤系统类型,再过滤内存,最后过滤硬盘,每个条件不依赖前一个的具体结果)。 而你的场景中,qemu 层的判断逻辑(“满足版本后输出什么”)完全依赖前一层的架构结果,用层结构会导致: 同一层(qemu 层)需要嵌入"架构 arm"“架构 x86"的分支判断,违背"层是通用过滤"的设计初衷; 新增架构(比如 risc-v)时,需要修改所有后续层的逻辑,扩展性极差。 核心总结 树是前后依赖的; 层是前后不依赖的,每一层是通用的; 规则引擎的设计,天生就是为了规则流,前后就是有依赖的; 所以写规则配置的时候,就是树的方式,很难用层的方式; 层因为其通用,很可能有较大的复杂性,且还需要扩展性,一般适合用代码实现; 但是因为其通用和局部性,修改难度低,用代码反而比用规则引擎简单。 一切都是为了 kiss。 ...

April 19, 2026 · 1 min · 77 words · 文叨叨

mq 相比直接传递消息的优势

mq 不仅是削峰填谷,也不仅是解耦服务,还有降低代码复杂性、增加稳定性的作用。中间件是一个稳定的、没有逻辑的、仅仅存储数据的。 现象 比如我们代码中安置逻辑里的 resource 的同步。 节点定时上报给 zoneleader,为了避免重复上报,节点自身又缓存了资源信息,每次上报前,先计算当前资源信息,然后和上一次缓存的资源信息对比,如果有差异,再汇报。 zoneleader 还会变。 上面两点,带来的复杂性: zoneleader 变了之后,节点得知道; 节点得区分 zoneleader 是否变化了,变了就直接上报资源信息; 节点不知道 zoneleader 用的资源信息是什么,说不定自己的缓存因为各种原因就不对呢; 处理异常情况能力弱。比如 zoneleader 因为个别原因更新失败,后续节点因为缓存的原因就不重试了。 这种方式虽然没有中心化的依赖问题,但是每个功能都需要处理上述类似的问题。 这是一种繁琐的、让代码复杂性大幅膨胀的方式,也是一种功能之间重复的、却不能统一处理的多余。 也是客户端和服务端相互紧密耦合的一种方式。 用中间件的方式 如果用中间件的方式: 客户端可以拉下来和当前资源对比; 服务端定期从中间件拉取资源即可; 中间件是一个稳定的、没有逻辑的、仅仅存储数据的。 所以: 客户端的逻辑只需要客户端处理,不变接口就不会影响服务端; 服务端也一样,自己处理逻辑,不用管客户端; 节点也不用关心 zoneleader 是不是变化了,它只想一个固定的中间件地址推送就行了; zoneleader 变化了,也不用想办法通知到其他节点,存储到中间件就行。 好处: 解耦; 不会相互影响了; 节点也知道当前使用的资源信息是什么样的,可以对比。 中间件依赖本身可以通过各种高可用方式保证其可用性,在现在已经不是什么主要矛盾或问题了。 小结 mq 不仅是削峰填谷,也不仅是解耦服务; 还有降低代码复杂性、增加稳定性的作用; 中间件是一个稳定的、没有逻辑的、仅仅存储数据的; 客户端和服务端通过中间件解耦,互不影响; 中间件依赖可以通过高可用方式保证可用性,不是主要矛盾。

April 18, 2026 · 1 min · 50 words · 文叨叨

check 和修复,要从大流程上就分开

check 有很多项,每个项都有修复。check 是一条线,修复是一条线。不要在 check 中发现某个项有问题就进行修复,而是要等 check 完再调用修复。 核心:check 和修复职责分离 check 有很多项,每个项都有修复。 那么: check 是一条线; 修复是一条线。 不要在 check 中,发现某个项有问题就进行修复,而是要等 check 完,再调用修复。 修复的里面比如也包含 check,但是这不能代替 check 流程。 因为有些情况,只需要 check,最典型的是告警和健康状态。 check 就得响应迅速,迅速告警。 里面掺杂了修复,有几个问题 健康状态不好管理:难道项目修复前是不健康,修复后改成健康,那下一个检查项呢?这样变来变去,很不稳定,上层没法用; 代码零散:就算在流程里更新健康状态,那得多零散,代码处理也不太好; 响应不及时:修复操作有的很耗时,影响 check 的效率。 检测和修复完全的职责分离 健康状态是检测修复。 那么修复的流程,即便修复完成,也不要管理健康状态。 下一次检测流程自然会更新健康状态。 小结 check 和修复要从大流程上就分开; check 是一条线,修复是一条线; 不要在 check 中发现某个项有问题就进行修复,要等 check 完再调用修复; check 就得响应迅速,迅速告警; 检测和修复完全的职责分离,修复完成也不要管理健康状态,下一次检测自然会更新。

April 12, 2026 · 1 min · 51 words · 文叨叨

底层单一,上层组合

底层逻辑尤其要保证单一,一个能力只提供一种方法。想要更多,通过上层组合、上层封装来实现。千万不要把所有需求都放到底层原子能力上。 核心:底层单一,上层组合 底层逻辑尤其要保证单一,一个能力只提供一种方法。 想要更多,那么通过上层组合、上层封装来实现。 千万不要把所有需求都放到底层原子能力上,虽然有些看起来好改,实际就是埋大坑。 符合设计原则、编程哲学是很重要和长久的。 来一个需求找一个好改的方式,这个评估维度是片面和短视的。 一个例子:网卡带宽 这个带宽控制是从精细化到模糊化管理。 从 iaas 的角度,这个管理方式是合理的,能适配多样化产品形态; 从产品的角度,需设计好统一的、标准化的控制方法。 nic 是网卡维度的控制; instance_extra 是云主机维度的控制; server.yaml 是全局控制。 看起来合理,实际没有人捋清楚,看代码看很久。每次梳理了就忘。 合理的方式应该是: 底层保持单一,容易理解; 云主机调整和全局的调整可以通过 api 来实现。 这样比较明确,以后也可以实现大会上说的"功能使用频率统计"。 批量、整体,这类操作,千万不要隐藏在配置或者底层的逻辑中,一定要以功能、api 的方式提供。 这样明确、可量化统计、可观测、可追溯。 小结 底层逻辑尤其要保证单一,一个能力只提供一种方法; 想要更多,通过上层组合、上层封装来实现; 千万不要把所有需求都放到底层原子能力上,那是埋大坑; 批量、整体操作,一定要以功能、api 的方式提供; 这样明确、可量化统计、可观测、可追溯。

April 11, 2026 · 1 min · 36 words · 文叨叨

Hook、插件、Middleware:三种扩展机制的区别

Hook 侧重现有流程上运行时的局部修改,插件侧重模块化功能扩展,Middleware 是流程中可以扩展的横切面。三者核心目标都是不侵入主程序的前提下实现扩展。 Hook vs 插件 Hook 与插件是两种常见的扩展机制,它们在实现方式、应用场景和解决的问题上存在显著差异。 **Hook(钩子)**是一种通过拦截或修改现有流程的执行逻辑,插入自定义代码的机制。 插件是一种模块化的功能扩展机制,通过动态加载的独立模块向主程序注入新功能。 维度 Hook 插件 实现方式 拦截或替换现有流程(如函数调用、事件触发) 动态加载独立模块,通过 API 与主程序交互 侵入性 低(无需修改源码,直接拦截流程) 低(主程序无需依赖插件) 控制粒度 细粒度(可精确到单个函数或事件) 粗粒度(通常针对模块级功能) 动态性 运行时生效(如热补丁) 运行时加载/卸载(如插件管理器) 典型用途 行为拦截、动态修改、事件统一处理 功能扩展、模块化开发、多租户定制 hook 侧重现有流程上运行时的局部修改,插件侧重模块化、功能扩展、架构层的设计。 Middleware vs Hook Middleware(中间件):是一种串联在核心流程上下游的组件,用于对核心操作的输入/输出进行预处理、后处理或增强。它通常形成一条"管道(Pipeline)",请求/数据会按顺序经过多个中间件,最终到达核心逻辑,再沿原路返回。核心目标是解耦通用功能(如日志、认证、缓存)与核心业务逻辑。 Hook(钩子):是在程序特定生命周期节点(如初始化后、销毁前、事件触发时)预留的"接口",允许开发者注册自定义逻辑,在节点触发时自动执行。钩子不直接干预核心流程的输入/输出,而是"挂靠"在关键节点上响应事件。核心目标是提供扩展点,允许开发者在不修改核心代码的前提下,介入程序的生命周期或事件处理。 middleware 是流程中可以扩展横切面,可以扩展多个。hook 是某些固定的点上,留下 hook 口。 middleware 是流程上的扩展; hook 是功能点上的扩展,比如 delete 后,调用一个 hook 做一些其他资源清理或检查,但是这 delete 不一定会调用,不是流程必须的; middleware 就是主流程中的扩展。 Middleware vs 微内核 Middleware 是一种嵌入核心业务流程前后的组件,用于处理通用化、横切性的功能(即"横切关注点")。它的核心作用是增强核心流程的能力,同时保持核心业务逻辑的简洁性。 微内核架构的核心是一个最小化的"内核"(仅包含系统最基础的能力,如模块管理、通信机制),所有业务功能通过插件(Plugin)动态扩展。内核不包含具体业务逻辑,仅负责插件的加载、卸载、通信和生命周期管理。 维度 Middleware 中间件 微内核架构 设计目标 处理"横切关注点",增强核心流程的通用能力 支持功能动态扩展,实现系统模块化和解耦 与核心的关系 嵌入核心流程的执行链中,依赖核心流程存在 插件独立于内核,内核仅负责管理插件(不依赖插件) 调用方式 链式调用(前一个中间件处理后传递给下一个) 插件通过内核注册/调用,插件间通过内核间接通信 动态性 通常静态配置(启动时确定中间件组合),动态性弱 支持运行时动态加载/卸载插件,动态性强 功能定位 聚焦"流程增强"(如日志、认证、缓存) 聚焦"功能扩展"(如新增业务模块、工具) 典型示例 Gin 日志中间件、JWT 认证中间件 VS Code 插件系统、Linux 内核模块 关键区别总结 与核心流程的关系: ...

April 5, 2026 · 1 min · 110 words · 文叨叨

耦合的松紧,关键在于变化

什么是耦合,怎么判断有没有耦合?可以从结果反推:A 变化了,B 要不要跟着变化。如果跟着变,那就是耦合了;如果没有跟着变,或者变动很小,那就是松耦合。 核心:耦合的松紧关键在于变化 什么是耦合,怎么判断有没有耦合? 可以从结果反推: A 变化了,B 要不要跟着变化; 如果跟着变,那就是耦合了; 如果没有跟着变,或者变动很小,那就是松耦合。 耦合的松紧关键在于变化。 小结 判断耦合可以从结果反推; A 变化了,B 跟着变,就是耦合; A 变化了,B 不变或变动很小,就是松耦合; 耦合的松紧关键在于变化。

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