监控系统项目复盘
这篇文章对我过去 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 个月才能得到有效的反馈。迭代周期太长,研发闭门造车。