乐于分享
好东西不私藏

先钉牢版本坐标,再谈源码结论

先钉牢版本坐标,再谈源码结论

Zabbix 源码 90 讲 · Day 02

源码基线:Zabbix 7.0.28

Revision:86f12eab509

设想一个很常见的排障现场:一套自行编译的 Zabbix Server 上,SNMP 监控突然全部失败。值班同事执行 zabbix_server -V,看到版本确实是 7.0.28;再打开源码,也能找到完整的 SNMP Poller 和协议实现,于是把时间都花在网络、MIB 和设备权限上。

真正改变排查方向的,却是启动日志中的一行:

SNMP monitoring: NO

源码存在,不等于目标二进制编入了这项能力。“同为 Zabbix 7.0”也不等于运行产物、源码快照、构建选项和数据库状态完全相同。

Day 01 建立了系统地图,告诉我们问题属于哪个组件;Day 02 要做的,是先校准坐标,证明自己分析的究竟是哪一份代码、哪一个程序。

只写“Zabbix 7.0”,为什么不够

7.0 只确定了主版本和次版本。实际环境里还可能同时存在:

  • 不同的 7.0.x 维护版本;
  • Server、Proxy、Agent 和 PHP 前端版本不一致;
  • 相同源码用不同依赖和功能开关编译出的程序;
  • 官方源码上叠加的本地补丁或厂商 backport;
  • 尚未执行完全部升级补丁的数据库。

因此,一条可复现的源码结论至少需要四层证据。

图 1:运行产物、源码快照、构建能力和数据库状态共同限定一条源码结论的适用范围。

第一层:先认运行中的目标程序

排查哪个组件,就直接询问哪个程序,不要用 UI 页脚、安装包文件名或运维文档代替运行证据。

zabbix_server -Vzabbix_proxy -Vzabbix_agentd -Vzabbix_agent2 -V

C 组件最终调用 src/libs/zbxcommon/misc.c 中的 zbx_print_version(),输出格式包含发布版本、revision、revision date 和实际编译时间:

<组件名> (Zabbix) 7.0.28Revision 86f12eab509 6 July 2026, compilation time: <日期> <时间>

这里有两个容易混淆的时间:6 July 2026 来自源码中的 ZABBIX_REVDATE;后面的 compilation time 来自实际构建。两者不是一回事。

Go 组件通过 src/go/pkg/version/version.go 的 Display() 输出相同核心信息,并额外显示 Go runtime。正式构建还会由 src/go/Makefile.am 注入构建日期、操作系统和架构;一次没有这些参数的裸 go build,不能自动等同于正式发布构建。

前端也有自己的观测点。apiinfo.version 返回的是 PHP 代码中的 ZABBIX_API_VERSION,它可以证明前端/API 是 7.0.28,却不能证明正在运行的 Server 二进制具有相同 revision。

第二层:再认手里的源码快照

当前源码的 C 版本主锚点是 include/version.h:

#define ZABBIX_REVDATE ”6 July 2026”#define ZABBIX_VERSION_MAJOR 7#define ZABBIX_VERSION_MINOR 0#define ZABBIX_VERSION_PATCH 28#define ZABBIX_VERSION_REVISION 86f12eab509#define ZABBIX_VERSION_RC ””

configure.ac 中的 AC_INIT([Zabbix],[7.0.28]) 则定义了 Autoconf 的包版本。Go、Java 和 PHP 还分别在 src/go/pkg/version/version.go、src/zabbix_java/src/com/zabbix/gateway/GeneralInformation.java、ui/include/defines.inc.php 中保留自己的版本常量。

为什么手册始终把 86f12eab509 称为 revision,而不直接写成“当前工作区完整 Git commit”?答案在仓库根目录的 Makefile.am。制作发布包时,dist-hook 会取得短 Git 标识,并把它写入 C、Go 和 Java 的版本文件:

zabbix_revision=`git rev-parse --short HEAD`; \cat $(top_distdir)/include/version.h | \sed ”s/{ZABBIX_RC_NUM}/$$branch$$type_num$$type_count/g” | \sed ”s/{ZABBIX_REVISION}/$$zabbix_revision/g” \$(top_distdir)/include/version.h.new;

这说明它是发布打包时嵌入的短 revision。解压后的源码目录可能没有 .git,也可能后来被人工修改;仅凭这个字符串,既不能恢复完整 SHA,也不能证明当前文件没有本地改动。

所以,准确的记录方式应当是:

源码包内嵌 revision 为 86f12eab509;本地补丁与来源另行记录。

第三层:代码存在,不等于功能已编入

回到开头的 SNMP 故障。src/zabbix_server/server.c 根据条件编译宏决定启动日志显示什么:

#ifdef HAVE_NETSNMPdefine SNMP_FEATURE_STATUS ”YES”#elsedefine SNMP_FEATURE_STATUS ” NO”#endifzabbix_log(LOG_LEVEL_INFORMATION, ”SNMP monitoring: ” SNMP_FEATURE_STATUS);

HAVE_NETSNMP 是否成立,取决于配置阶段的选项、依赖检测和生成的构建配置,而不是取决于源码目录里有没有 SNMP 文件。同样的判断还适用于 IPMI、Web monitoring、VMware、ODBC、SSH、IPv6 和 TLS。

因此,验证构建能力至少要保存三类证据:

    如果启动日志已经写着 SNMP monitoring: NO,继续追网络报文没有意义。排查应返回 configure.ac、m4/、Net-SNMP 依赖和条件编译结果。

    第四层:数据库有独立的 schema 坐标

    软件版本是 7.0.28,不代表数据库版本就写成 7.0.28。Zabbix 使用 dbversion 表记录 schema 补丁位置:

    SELECT mandatory, optional FROM dbversion;

    这份 7.0.28 源码中的 MySQL、PostgreSQL、Oracle 和 SQLite 全新建库脚本都写入:

    mandatory = 7000000optional = 7000030

    7000030 是 schema patch 编号,不是“Zabbix 7.0.30”。在 src/libs/zbxdbupgrade/dbupgrade_7000.c 中,7000000 被注册为 mandatory patch,7000001 到 7000030 是 optional patches。

    src/libs/zbxdbupgrade/dbupgrade.c 会读取 mandatory 和 optional,计算程序要求的 mandatory 版本,并执行尚未完成的补丁。PHP 前端的 ui/include/defines.inc.php 则定义 ZABBIX_DB_VERSION = 7000000,前端检查的是 mandatory 边界。

    这也是为什么研究升级问题时,不能只打开当前的 database//schema.sql。它描述全新安装的目标结构,不是完整升级历史;真实升级链还要继续查看 dbupgrade_.c 的补丁注册与执行逻辑。

    对不上源码时,按这个顺序缩小范围

    图 2:先排除版本、revision、构建能力和数据库状态差异,再进入当前源码的函数调用链。

    这套顺序的价值,在于每一步都能排除一大类无效搜索:

    • 运行版本不对,先找对应版本的源码;
    • revision 不同,先确认本地补丁或打包来源;
    • Enabled features 不同,先回到构建条件;
    • 涉及表、字段或升级,再核对 dbversion 和数据库补丁;
    • 以上边界都明确后,函数和调用链分析才有稳定起点。

    建议保留一张“版本证据卡”

    每次准备给出源码结论前,可以先填写下面这张记录:

    目标组件:运行版本:Revision:源码来源:本地或厂商补丁:构建平台与编译时间:Enabled features:数据库 mandatory / optional:关键日志:

    并非每个问题都会用到所有字段。例如纯 Agent key 问题通常与数据库无关。但凡某个坐标可能改变结论,就必须记录,或者明确写出“尚未核实”的边界。

    回放开头的 SNMP 故障

    现在重新看最初的现场,排查路径会变得很短:

      这不是多做了一轮检查,而是用几分钟避免在错误的源码和错误的层次里排查数小时。

      今天只需要记住四句话

      第一,7.0 只是系列标识,不能唯一确定源码和运行行为。

      第二,revision 是重要的发布快照锚点,但不能代替完整 Git commit、本地修改记录或实际二进制证据。

      第三,代码存在不等于功能已编入;可选能力必须核对构建条件和启动日志。

      第四,数据库 schema 有自己的 mandatory/optional 补丁坐标,不能从产品版本号直接推算。

      留给读者的小练习

      选择你环境中的一个 Zabbix Server、Proxy 或 Agent,填写一张版本证据卡。然后比较 -V、启动日志、对应源码版本文件,以及问题涉及数据库时的 dbversion。

      如果四层坐标中有一层无法确认,就先不要急着下“源码就是这样”的结论——那一层往往正是故障边界。

      下一篇预告

      版本坐标确定后,下一步不是从入口文件第一行开始顺序阅读,而是先认识仓库的地形。

      下一篇《十个顶层模块,拼出 Zabbix 源码全貌》,我们将按组件和语言拆开 src/、ui/、database/ 等顶层目录,建立从需求到源码目录的第一跳。