乐于分享
好东西不私藏

十个顶层模块,拼出 Zabbix 源码全貌

十个顶层模块,拼出 Zabbix 源码全貌

Zabbix 源码 90 讲 · Day 03

源码基线:Zabbix 7.0.28

Revision:86f12eab509

假设你接到一个很小的需求:把 Zabbix Server 未显式配置时的 StartPollers 默认值从 5 调高,并同步配置样例。
第一次改源码的人很容易搜索到 conf/zabbix_server.conf:
于是他把这里StartPollers=5改成 10,重新编译,再启动一个没有显式设置 StartPollers 的 Server——Poller 数量仍然是 5。
这不是编译缓存,也不是配置没有刷新。conf/ 保存的是示例配置;真正的程序默认值在 src/zabbix_server/server.c 的 config_forks[] 中,参数解析表也在同一个入口文件里。搜到了同名参数,不等于找到了决定运行行为的位置。
Day 02 已经固定了版本坐标。今天要解决下一件事:面对一棵确定为 7.0.28 的源码树,第一步应该走进哪个区域?

“十个模块”不是十个同构文件夹

标题里的“十个顶层模块”,更严谨地说,是源码定位时的十个职责区域。它们不一定都会编译成独立程序,也不一定只包含一个物理目录:
configure.ac 与 m4/ 合并为构建检测区;
misc/ 与 man/ 合并为部署运维配套区;
build/ 与 bin/ 合并为 Windows/MinGW 构建区;
根目录的 Makefile.am 单独作为构建编排区;
src/libs/、src/go/ 等仍属于 src/,不另行计数。
当前工作区中的 docs/wechat/ 是本系列文章和图片的保存位置,不属于 Zabbix 上游源码模块;.agents/ 等工作区元数据同样不计入产品源码。
图 1:十个区域按运行与数据、接口与交付、构建与平台分层;这是定位地图,不是运行时数据流图。

第一层:运行行为与数据结构

01 src/:程序真正运行的地方

src/ 不只是 Zabbix Server,也不全是 C 代码。它包含:
src/zabbix_server/、src/zabbix_proxy/、src/zabbix_agent/ 等组件入口;
src/zabbix_get/、src/zabbix_sender/、src/zabbix_js/ 等工具;
src/libs/ 中按 zbx<领域> 命名的大量公共实现;
src/go/ 中的 Agent 2、Web Service 和插件;
src/zabbix_java/ 中的 Java Gateway。
入口文件通常负责参数解析、初始化和进程分派,真实业务能力经常继续下沉到 src/libs/。只在 src/zabbix_server/server.c 中搜索到底,很容易错过公共库里的实现。
根目录的 src/Makefile.am 也表明,实际进入构建的子目录会随 AGENT、SERVER、PROXY、AGENT2、WEBSERVICE 和 JAVA 等条件变化,并不是把 src/ 下所有内容无条件打进一个程序。

02 ui/:页面、API 与控制面

ui/ 是部署型 PHP 应用,包含 app/、include/、js/、widgets/、静态资源以及 Composer 依赖。页面、权限、JSON-RPC API、SCIM 和 Dashboard Widget 问题优先从这里开始。
但它不是每个监控值的实时传输路径。页面保存配置后,Server 还要从数据库同步到运行缓存;采集失败也不能只因为页面正常就排除后端问题。

03 database/:首次安装的数据库入口

database/ 按 MySQL、PostgreSQL、Oracle 和 SQLite 保存初始 schema.sql、数据文件和方言差异,还包含 TimescaleDB 与 Elasticsearch 相关内容。
它回答“全新数据库应是什么结构”,却不是所有数据库逻辑的总仓库。运行期 SQL 在 C/PHP 代码里,完整升级链还要查 src/libs/zbxdbupgrade/。因此,改字段不能只改一份 MySQL schema,更不能把当前 schema.sql 当作完整升级历史。

第二层:接口、样例与交付材料

04 include/:公共契约,不是主要实现

include/ 保存公共 C 类型、常量、宏、跨库接口、模块 API 和版本头,例如 version.h、module.h 与大量 zbx*.h。
看到函数声明时,下一步通常要回到 src/libs/ 或组件目录找实现。include/common/config.h 还是配置阶段的生成物,源码分析应同时注意它的模板和条件编译来源。

05 conf/:示例配置,不是唯一事实源

conf/ 包含 Server、Proxy、经典 Agent 的示例配置和 UserParameter 示例。它很适合确认参数名称、注释和写法,但不能单独证明:
程序内部默认值是什么;
参数由哪个组件解析;
当前进程实际加载了哪一个配置文件;
Agent 2、Web Service 或 UI 是否使用同一套配置入口。
这正是开头修改 StartPollers 失败的原因。

06 sass/:前端样式的源头

主题颜色、组件样式、图标与字体生成素材位于 sass/。根 Makefile.am 的 css 和 icons 目标会把生成结果写入或复制到 ui/assets/。
所以改页面逻辑去 ui/,改纯样式或图标先看 sass/;浏览器最终加载的则是生成后的前端资源。

07 misc/ + man/:部署和运维配套

misc/ 中有多发行版 init 脚本、SNMP trap 接入脚本和地图图片等素材;man/ 提供 Server、Proxy、Agent、Get、Sender 和 Web Service 的命令手册页。
这些内容对部署与使用很重要,却不是核心 daemon 逻辑的实现位置。历史 init 样例和 man page 也不能替代当前入口源码与配置解析代码的核验。

第三层:源码怎样变成产物

08 configure.ac + m4/:决定“能不能编”

configure.ac 定义组件开关和配置总流程,m4/ 保存 MySQL、PostgreSQL、TLS、SNMP、LDAP、libcurl 等依赖检测宏。遇到依赖找不到、功能宏没有启用或平台能力判断错误时,先查这一层。
它们负责检测与开关,不负责列出每个目标的全部源文件;修改手写规则后,也不应只去改生成的 configure。

09 Makefile.am:决定“哪些源码进入目标”

根目录与各子目录的 Makefile.am 负责编排递归目录、目标、源文件和链接库。根文件中的几行已经说明,仓库目录图与编译依赖图不是同一张图:
ACLOCAL_AMFLAGS = -I m4
SUBDIRS = include src database man misc
EXTRA_DIST = README.md bin build ui include conf sass
ui/ 出现在 EXTRA_DIST 而不在递归 SUBDIRS 中,不代表它不属于产品,只表示它不走同一条 C/Automake 递归构建路径。新增 C 文件后真正需要更新的通常是相应 Makefile.am,而不是只改生成的 Makefile.in。

10 build/ + bin/:Windows 与 MinGW 路径

build/ 保存 Windows/MSVC、MinGW 的 Makefile、资源描述和平台配置;bin/ 当前主要包含 Windows 开发头文件,并不是一个放满 Linux 编译产物的通用目录。
所以,“Unix 构建缺依赖”和“Windows 工程没纳入源文件”虽然都表现为编译失败,进入的源码区域并不相同。

遇到问题,先做目录第一跳

图 2:根据责任域选择第一个目录,再沿声明、实现、调用者和构建关系继续深入。
这张路由图不是说一个问题只会涉及一个目录,而是先找最有可能产生事实的第一落点:
运行行为先进入 src/,再判断是组件入口、公共库还是 Go 组件;
页面和 API 进入 ui/,纯样式再转向 sass/;
初装表结构进入 database/,升级与运行期操作继续追到实现代码;
公共声明在 include/,实现通常回到 src/;
配置样例在 conf/,默认值、解析与校验必须核对组件源码;
编译问题先分清依赖检测、目标编排还是 Windows 构建路径。
先选责任域,可以减少搜索噪声;随后再全仓搜索符号,才不会把“搜索命中”误当成“答案命中”。

回放 StartPollers 需求

现在重新处理开头的需求,修改边界会清楚很多:
conf/zabbix_server.conf 只负责同步样例说明;
Server 的内部默认值在 src/zabbix_server/server.c 的 config_forks[] 中;
同一文件的配置参数表把 StartPollers 映射到 ZBX_PROCESS_TYPE_POLLER;
Proxy 有自己的示例配置和 src/zabbix_proxy/proxy.c,是否一起修改必须由需求明确,不能因为同名就自动扩散;
没有新增源文件时,不需要无故修改 Makefile.am;这个需求也不应牵连 ui/ 或 database/。
真正高效的源码阅读,不是搜得更多,而是知道每一次命中承担什么职责。

今天只需要记住五句话

第一,根目录不是平铺的文件仓库,而是一组职责不同的阅读区域。
第二,src/ 是运行实现的大本营,但入口文件和公共实现仍要分开看。
第三,database/、include/ 和 conf/ 分别偏向初装结构、公共契约和配置样例,不能替代运行实现。
第四,目录是否进入顶层 SUBDIRS,不等于它在产品中是否重要。
第五,先选责任域,再搜符号,最后沿调用链和构建关系深入。

留给读者的小练习

在当前源码中搜索一次 StartPollers,然后写下三行定位卡:
现象或需求:
第一跳区域:
暂时排除的区域及理由:
请分别解释 conf/zabbix_server.conf、src/zabbix_server/server.c 和 Proxy 对应文件承担什么职责,以及为什么 ui/、database/、Makefile.am 不是这个需求的第一跳。

下一篇预告

同一个 src/ 中已经出现 C、Go 和 Java,ui/ 又以 PHP、JavaScript 为主,这不是偶然拼接。
下一篇《C、Go、PHP、Java 为什么共存于同一仓库》,我们将解释各语言对应的组件职责、构建与部署边界,以及面对不同问题时应该选择哪套阅读入口。