Steve Sun

监控系统项目复盘

English

这篇文章对我过去 3 年的一大块工作内容进行复盘。我作为项目组的架构师,在下文中也对项目早期遗留的一些问题进行反思,并分享我个人的解决思路。

项目核心依赖开源组件,定制化过度

我们的项目是一个运行在边缘设备上的日志、软硬件性能指标的采集/监控系统。考虑到边缘计算设备(IPC)的性能,选择开源组件时就侧重于轻量化、支持丰富的输出标准。早期部门架构师采用了 Fluent-Bit 作为项目的核心组件。Fluent-Bit 是 C 编写的开源、轻量、可轻度扩展的数据收集器。它最开始用作日志收集,后来逐渐发展成全功能的 Agent。对比流行的OpenTelemetry ,Fluent-Bit 更开箱即用、更轻量,但是不易于修改和扩展。

最开始整个团队都没接触过监控系统,所以在设计系统时挖了不少坑。首先,用户在 UI 上操作过度繁琐,需要依次配置输出的目标(地址、端口、协议、格式、加密方式等等)、采集的指标类型,最后还要手动点击 Apply (应用)一下。

经过几轮迭代,适当简化了操作逻辑。但是像大部分工业 PC 上运行的程序一样,用户在初始化配置过后,通常不会主动去 UI 上修改配置。终端用户更关心占用系统资源多少、稳定性如何。所以最开始团队把这个项目做成了一个重交互的 C 端产品,这是个教训。

第二,后端开发为了满足 UI 设计的流程(比如,用户可以创建多份不同的配置项到不同的目标地址),做了复杂的 Work-around。因为 Fluent-Bit 是单进程事件驱动模型,只有单一配置文件,每次修改配置文件都要重启 Fluent-bit 进程。这就造成了 UI 上用户添加一个配置项,后台就要重新生成整个配置文件并重启 Fluent-Bit。这对于一个稳定运行的监控系统来说,无疑增加了重启过程中数据丢失的风险。另外,如果新增的配置项出错,就会让整个生成的配置文件报错,导致 Fluent-Bit 进程假死等问题。

为了解决这些问题,后端工程师又对 Fluent-Bit 的各项参数玩出各种花活儿。比如利用不同 tag 来分流不同用户配置项,为每个配置项单独配置参数和过滤规则。再比如设定缓存数据包大小和缓存 timeout 时间为 0,这样 Fluent-Bit 重启之后会首先尝试重发缓存在文件系统里的数据,这样间接防止用户数据丢失。

这些花活儿不但提高了维护难度,从用户角度看,也并没有带来任何真正的价值提升。

回顾来看,如果早期的 UI 设计改成单独的配置页面,不但简化的操作流程,还给业务代码降低的复杂度。

第三,核心项目依赖 Fluent-Bit 造成项目迁移到其他开源组件非常困难。加上 Fluent-Bit 更新频率高,公司对安全性合规要求使得我们团队每隔一段时间要对 Fluent-Bit 进行升级,同时对所有配置选项做回归测试。加上 Fluent-Bit 订制性很差,它虽然支持使用 Go 语言实现 Output 插件,但是只能用 C 语言编写 Input 插件。导致我们采集内部应用的数据,不得不用到它的 TCP 和 HTTP 插件来中转。部署多个 Agent 采集不同的内部服务。这让后期集成测试更添难度。

总体来说,Fluent-bit 的性能基本达到了预期,但是各种小 bug(比如 pgsql 插件在目标不可达时直接 Block 整个进程),开源社区维护者并没有引起重视,我们提交给开源社区的代码也被以各种理由驳回。如果让我重新选择,我更倾向于使用其他扩展性更强的开源组件。

对 Go 语言不熟悉,项目结构混乱

团队遇到的第二个挑战是对 Go 语言不熟悉。大部分开发成员只有 Java 开发经验,所以顺理成章把 Go 写成了 Java。因为框架(Go-Gin)的限制,导致开发中问题频出。

第一个问题来自面向对象和依赖反转。依赖反转对于使用 Java Spring 的人来说不会陌生,但是用 Go 实现依赖反转,需要利用 Interface 封装,并结合 Go-Mock 库做单元测试。团队成员早期不熟悉语言特性,经常错误封装抽象,或者干脆直接函数套函数,写成面条型代码 。这充分暴露了大部分国内 Java 程序员其实没有受过良好的 OOP 训练。对于单元测试、集成测试这些工程实践也是流于形式。软件质量在大部分企业里仍然靠测试人员手动验证。

第二个问题是 Go 语言不鼓励过度抽象。如泛型、异常处理,都要一步步重复琐碎的代码片段,这让 Sonar 静态检查经常 failed。没经验的同事就会用各种奇技淫巧逃避静态检查。这也说明开发团队定期 code review 的必要性。

第四,Go 语言其实是一个社区不那么完善的编程语言,它的很多框架(如最热门的 gorm 居然是个人开发项目),像 Flyway 这种 Java 工具链中很成熟的迁移工具,在 Go 里竟然需要组合多个开源项目来替代。所以 Go 只适合来开发中等以下规模的项目,或者对性能要求较高的平台核心组件。(在国内)不适合做复杂的业务场景。

API 接口粒度过细,没有对资源对象做好抽象

团队早期由于管理混乱:架构上,没有对业务模型做好抽象,资源对象拆分太碎;管理上,任务拆解太简单粗暴,给每个同事单独负责一个模块,导致每个业务流程都设计了专门的 API,维护压力大。好在业务场景少,用自动化测试能一定程度上保证了接口可靠性。

最开始做自动化集成测试时,我们仍然使用 BDD 的形式,以业务操作为基础编写,后来逐渐发现这种监控系统,其实真正的用户操作逻辑非常简单,复杂的部分是不同类型的数据、不同的 Input、Output 配置可能引起的异常。所以我们改成了数据驱动测试,用配置文件对不同类型的 Fluent-Bit 配置做全面的测试。

总结起来,Fluent-Bit 配置的修改,其实完全可以用 3~4 个宽泛的 API 来实现,除了前文提到流程过度设计原因,项目初期的不确定性,导致开发人员过度关注松耦合,而忽略了维护性。

错误的流水线设计

最开始项目沿用的部门其他团队的集成测试、部署模式,把 Python 写的测试用例和项目部署脚本放在单独的 Gitlab Repo 里。结果是每次项目部署时,要人工去网页上修改版本号触发流水线。从持续集成的角度看,业务代码和测试用例分开,造成了每次 commit 都要到不同 repo 里去提交,且一旦冲突又要分别执行多次集成测试(时间长,反馈慢)。

后期我们做了一些调整,把多个小模块合并成一个Monorepo ,同时把部分 API 相关的集成测试放在后端代码里,减少提交次数,也让原子提交更容易。

不过部署问题依然没有被解决,原因是边缘平台上的模块太多,系统集成需要多个团队合作,部署、发布版本时间长,出错的环节太多。对于这种情况,部门技术负责人设定了严格的代码提交、测试、review、文档更新流程,但是根本问题还在于团队责任模糊、部门团队跨多个国家和时区,缺少统一的调度和沟通机制。这些问题只能留给管理层逐渐缓解,或者随着业务收敛,减少、分流项目组。

小结

整体来看,我们团队遇到的很多问题出自项目早期,缺少项目和技术团队管理经验。对业务的愿景不了解,把做 C 端 Saas 产品的经验带到工业领域,用熟悉的开发范式套用到制造业。当然,不回避地说,在业务上,部门多流程长,业务负责人只能盲人摸象,用户反馈要先到达 Support 团队,再反馈给上层,最后才到开发团队。这让我们开发出来的产品要经过至少 3-6 个月才能得到有效的反馈。迭代周期太长,研发闭门造车。

Steve Sun
软件架构师,项目负责人,前 Thoughtworker,效率工具、桌游和赛车运动爱好者。

#边缘计算