配套概览见 ../modules/itl.md · 协议 ../protocol.md · 总入口 ../../README.md
本文件是 itl(淘米内部运维监控平台,源码代号 "OA 监控 / ITL 后台")的"深水区"补充。概览文档已经把"每个目录是什么"讲清楚了;这里只讲一件事:进程跑起来之后,一条监控数据是怎么从被监控机器一路流到落库 / 画图 / 发短信的,以及每个进程在这条链路上实际做的动作(带真实源码行号)。
┌──────────────── MySQL: db_itl ────────────────┐
│ 资产/机房台账 + t_metric_info / t_alarm_* │
│ + 触发器把"配置变更"写进 t_update_notice │
└───────┬──────────────────────────────▲────────┘
│ (db 进程是唯一访 db_itl 的人) │
被监控机 (每台一份) ▼ │ INSERT notice
┌─────────────────────┐ ┌──────────┐ 配置/告警结果 │
│ itl-node (agent) │ │ itl-db │◀──────────────────────── (各进程都连 db)
│ ├ collect 定时器 │ │ 进程 │
│ ├ dlopen metric-so │ └────▲─────┘
│ │ ├ libcommon.so │ │ send_metric_to_db (类型&4)
│ │ ├ libmysql.so │ │
│ │ └ libmount.so │ ┌────┴───────────────┐
│ └ db_mgr 管本机MySQL│ │ itl-head │ ── send_metric_to_rrd ──▶ ┌────────┐
└──────────┬──────────┘ │ (中枢/网关) │ (类型&2) │ itl-rrd│─▶ *.rrd
│ head_p_metric_data_cmd │ └────────┘
│ (打包整组指标 TCP 上报) │ dispatch_metric() │ ── send_metric_to_alarm ─▶┌─────────┐
└─────────────────────▶│ 按 metric_type 分流 │ (类型&1 或采集失败) │itl-alarm│
└──────────┬──────────┘ └────┬────┘
┌────────────┐ 控制命令(重启/换so/ │ node 注册时 │ 阈值状态机
│ itl-control│ 下发配置/执行shell) │ db_store_host_status │ 命中→HTTP POST
│ 轮询notice │◀────────────────────────────┘ alarm_node_up ▼
│ push 到node│ http://…/alarm-server/index.php
└─────┬──────┘ (cmd=10001 短信 / 10002 邮件 / 10003 RTX)
│ notify_config_change_cmd
▼ (node 收到→refresh_collect)
itl-node
── 独立子系统(不走上面这条 node→head 链)──
switch-monitor : 自己多线程 SNMP 轮询交换机 → 自己写 RRD + 自己 HTTP POST 告警 + 自己写 db_itl
itl-speed : CDN/IDC 测速,FCGI 收数据 → analyze_server 按 IP/省运营商聚合 → dbser(SO)落库
一句话:node 只负责"采集 + 上报",所有判断都在 head 这一侧的内存里做完,再扇出给 rrd/db/alarm 三个后端进程。switch-monitor 和 itl-speed 是两个自带完整采集-处理-落库链路的独立子系统,只是复用了同一个 db_itl 库和告警网关。
所有 itl 进程共用一个 #pragma pack(1) 的二进制包头 itl_proto_t,见 itl/itl-common/itl_proto.h#L31:len / seq / cmd / ret / id / body[]。head/node/db/rrd/alarm/control 全部 typedef 成同一个结构(#L48-L54)。seq 注释里写明是 (p->fd << 16) | p->waitcmd,即把"哪个连接 + 在等哪个命令的回包"编码进序列号,用来做异步请求/回包配对。
head 处理类型(metric_type)是整套数据流的"开关",定义在 itl/itl-common/define.h#L99:METRIC_TYPE_ALARM=1 / METRIC_TYPE_RRD=2 / METRIC_TYPE_MYSQL=4,是按位组合的——一个指标可以同时既报警又画图。被监控对象类型 ITL_TYPE_SERVER=1 / SWITCH=2 / MYSQL=3 见 #L90。
实际职责:dlopen 采集 so → 定时调用 → 打包整组指标 TCP 推给 head。它自己完全不做阈值/画图判断。
- 采集 so 的加载:
register_so()用dlopen(RTLD_NOW)打开.so,再dlsym取出固定的四个符号handle_init / handle_fini / get_metric_info / metric_handler(见 itl/itl-node/src/collect/collect.cpp#L295-L335)。哪些 so 要加载,是 db 进程根据本机 IP 下发的so_name_list(分号分隔),so 不在本地就download_metric_so现下载(#L103-L122)。 - 定时采集循环:每个 metric_group 一个定时器,
route_metric_group()→do_group_collect()(#L343)。它先检查两道闸:g_collect_flag(采集总开关)和g_head->is_connected()(没连上 head 就根本不采,#L346-L353),然后逐个指标do_metric_collect调 so 的metric_handler,把整组打成一个head_p_metric_data_cmd包发给 head(#L414)。注意它先发一次再注册定时器,即"启动即采一帧"。 - 配置热更新:head/control 下发
node_p_notify_config_change,node 收到只做一件事——refresh_collect()重新向 db 拉本机配置(itl/itl-node/src/itl_node.cpp#L36-L54)。 - node 是个被远程操控的 agent,命令面很危险:
node_p_execute_command直接g_spawn_command_line_sync("sh -c '…'")跑任意 shell(#L273-L300);node_p_update_so/node_p_update_metric_so从 URL 下载新 so + 校验 md5 后restart_node()热替换(#L186、#L234);还有 download/upload(ftp)、切采集开关等。这是典型的"中心可以让每台机器干任何事"的运维 agent。 - db_mgr:node 还兼管本机的 MySQL 实例(本模块最大单文件之一 itl/itl-node/src/db_mgr/db_mgr.cpp),
node_p_notify_mysql_instance_change触发refresh_mysql_instance。
三个 so 都遵守同一个 ABI(node 靠 dlsym 这四个符号驱动):
- libcommon.so 主机通用指标。采集项是一张 X-macro 表 itl/metric-so/src/libcommon/metric_define.h,45 个指标:网卡收发 bit/s、内存/swap、磁盘、负载 load_one/five/fifteen、CPU user/system/idle/wio…、内外网 IP、机型/OS/版本、系统时钟偏移等。X-macro 同时生成"指标元信息数组 + 枚举 + handler 分发"三份代码,见 itl/metric-so/src/libcommon/common_proto.cpp#L31-L48 与
metric_handler的case name: return name##_func(val)(#L64-L172)。底层都是读/proc/loadavg、/proc/meminfo、/proc/stat、/sys/.../scaling_max_freq等(itl/metric-so/src/libcommon/metrics.cpp#L118)。 - libmysql.so MySQL 性能指标(show status / innodb 等,itl/metric-so/src/libmysql/mysql_metric.cpp);
handle_init收的参数是 node 传进来的mysql_so_param_t向量(端口 + sock + 要采的指标名),见 node 侧 collect.cpp#L168-L215。 - libmount.so 磁盘挂载点容量(itl/metric-so/src/libmount/mount_info_metrics.cpp);libdmidecode 解析硬件资产(itl/metric-so/src/libcommon/dmidecode.cpp)。
get_metric_info(index) 返回每个指标的"名字/值类型/单位/格式/slope",node 据此知道该怎么解释和上报值——所以 head 那边收到的指标自带元信息(见下)。
head 是无状态判断的"路由器 + 内存缓存":它把每个 node 的最新指标缓存在内存(c_node→c_metric→c_metric_arg),并按 metric_type 把数据扇给三个后端进程。
- 连接分发:
dispatch()先按 fd 判断是不是已注册 node(itl/itl-head/src/dispatch.cpp#L26),是就走dispatch_node,否则按find_head_cmd(cmd)查命令表回调(#L70-L87)。命令表cmd_proto_t由 gen_proto 生成。 - node 注册:
head_p_node_register里alloc_node建内存对象,并立刻db_store_host_status(STATUS_HOST_UP)+alarm_node_up(通知 db 主机上线、通知 alarm 重置状态),见 itl/itl-head/src/itl_head.cpp#L37-L88。 - 收指标:
head_p_metric_data遍历包里的指标向量,刷新内存里的c_metric元信息和c_metric_arg值,然后对每个 arg 调dispatch_metric(itl/itl-head/src/itl_head.cpp#L115-L169)。 - 扇出逻辑(最关键的 35 行) itl/itl-head/src/metric.cpp#L27-L64:
- 采集失败(
m_fail)→ 只发 alarm(让 alarm 发"采集失败"告警); type & ALARM或 是"相对指标" →send_metric_to_alarm;type & RRD→send_metric_to_rrd;type & MYSQL→send_metric_to_db。
- 采集失败(
- head 到 rrd/db/alarm 三个后端是三条带自动重连的 TCP 长连(
set_auto_reconnect),IP/端口从配置读,见 itl/itl-head/src/rrd.cpp#L32、db.cpp#L30、alarm.cpp#L33。三种扇出各有不同的打包函数pack_metric_to_rrd/alarm/db(metric.cpp#L67/#L91/#L110)——比如发给 rrd 的只带 name/arg/value/slope/interval,发给 alarm 的额外带 value_type 和原始 c_value(供阈值比较)。
alarm 进程收 head 转来的指标,在自己内存里跑状态机,命中阈值就 HTTP POST 给告警网关。
- 阈值配置很灵活:warning/critical 值可以写成
0.5*$26(系数 × 另一个指标 26 的当前值),get_metric_and_coeff负责解析这种"相对阈值"(itl/alarm/src/metric_alarm.cpp#L519-L552),init_alarm_value把它解析成系数 + 关联 metric_id(#L556)。比较运算符支持= > < >= <=(compare_value,#L201)。 - 两道"静默"门:
is_ban(屏蔽时间窗,#L121)和is_alarm_off(每天的免打扰时间段,默认 0–6 点,#L169,解析见init_alarm_off_time#L370)。 - 状态机本体
handle_alarmitl/alarm/src/alarm.cpp#L342-L454:- 正常态:连续异常达到
m_abnormal_threshold次才真正翻成告警态并发第一条告警(滞回去抖,默认异常 2 次,metric_alarm.cpp#L35); - 告警态:连续正常达到
m_normal_threshold次(默认 5 次,#L34)才发"恢复"并清零; - 持续告警时按
get_span(m_alarm_count)做退避重发——告警次数越多间隔越大(配置次数区间:间隔分钟,init_alarm_span#L453;没配就默认 1 小时,#L106)。
- 正常态:连续异常达到
- 三种告警通道,按 warn/crit 各自的
way位掩码选择 MOBILE/EMAIL/RTX(alarm.cpp#L573-L617);恢复时"先通知收到 critical 的人,再用异或排掉重复只通知额外收 warn 的人"(#L592-L598)。 - 告警出口其实是一个 PHP 网关:三种
send_*_alarm全部http_post到同一个 URL,只是cmd不同(10001 短信 / 10002 邮件 / 10003 RTX),默认地址http://10.1.1.63/itl-server/alarm-server/index.php,见 itl/alarm/src/alarm_interface.cpp#L24-L74。真正接短信网关的是这个 PHP,不在泄漏树里。 - 另外 alarm 还会 ping 探活:
check_host_alive直接/bin/ping -n -U -w%u -c5(itl/alarm/src/check_host_alive.cpp#L31)。
收 head 的指标,rrd_update 写入对应 .rrd,文件不存在就 rrd_create。RRA 归档策略写死在宏里(server 与 switch 各一套),例如分钟粒度保 10080/25920 点、再按 6/24/288 倍降采样保更长周期,见 itl/rrd/src/c_rrd_handler.cpp#L34-L57。这套粒度就是前端"近 1 天 / 近 1 周 / 近 1 月"趋势图的数据源。
统一 SQL 层(最大单文件 itl/db/src/db_interface.cpp,~5.8 万字节)。head/node/alarm/control 都不直连 MySQL,而是发命令给 db 进程代办——既做配置下发(节点该采哪些 so、阈值多少),也做结果回写(主机状态、告警事件、db_mgr 执行结果)。
control 是把 DB 里的配置变更"推"给在线 node 的环节,它和触发器配合实现热生效:
- 定时轮询:
init_update_timely每GET_CONFIG_UPDATE_INTERVAL秒向 db 发db_p_get_update_notice_cmd拉一次待办(itl/control/src/update_notice.cpp#L21-L27)。 - 回包处理
db_p_get_update_notice_cb:把待办按collect_update_flag/alarm_update_flag分成 switch/node/alarm 三组,对在线 node 调notify_config_change+notify_mysql_instance_change,对 alarm 调alarm_change_node_alarm_config(itl/control/src/db.cpp#L96-L150)。 - 它也接 node 注册、并能下发重启/换 so/执行命令等控制命令(
control_p_node_register等,itl/control/src/control.cpp#L29)。
不走 node→head,自成一套多线程采集器(itl/switch-monitor/src/main.cpp):从 db_itl 读交换机列表(IP + SNMP community,SNMP_VERSION_2c 明文社区串,snmplib.cpp#L93)→ 采集线程 simple_snmp_walk/get 轮询标准接口 MIB(ifInOctets/ifOutOctets/ifSpeed/ifOperStatus 等,OID 表见 itl/switch-monitor/src/lib/snmplib.h#L9-L29)+ 厂商私有 CPU/内存 OID(cisco / h3c 各一套,#L23-L29)→ data_processer 处理 → 自己写 RRD + 自己 HTTP POST 告警网关 + 自己回写 db_itl。默认告警/采集间隔(60/15s)、队列长、线程数、告警 URL、rrd 目录都在 main 里写死兜底(main.cpp#L319)。安全降权:得到 rrd 目录后 setuid nobody,所以只能 access 检查不能创建(main.cpp#L176)。
体量虚高(408 文件 / 54MB,大量重复 qdes.c/proto.h 副本),真正核心是 FCGI 收数据 → analyze_server 聚合 → dbser(SO 插件)落库。命令字是泄漏树里唯一完整保留的 itl 协议枚举 itl/itl-speed/dbser/src/proto/cdn_rate_enum.h:cdn_report_ip_cmd=0x1001、cdn_report_prov_cmd=0x1002(按省市运营商上报 CDN 速度)、net_stat_ip_report_cmd=0x1211(替换旧 1003/1005/1007 的延迟/丢包/跳数)等。analyze_server 用一个 IP 字典(mmap 的 IP→省/市/运营商映射)把原始 IP 数据归约成"按省市运营商"的聚合结果再上报(itl/itl-speed/tst_spd/analyze_server/process_packet.cpp#L193-L203),并用 cache 去重(#L117-L120)。
整套系统的优雅之处:node 端不知道一个指标是用来画图还是报警的,它只管采集并带上 metric_type(这个类型来自 metric-so 的元信息 / DB 配置)。判断完全集中在 head 的 35 行 dispatch_metric(metric.cpp#L27):因为 ALARM=1/RRD=2/MYSQL=4 是按位的,DB 里把一个指标配成 3 就同时画图 + 报警,配成 7 就三路全开。改监控行为只需改 DB 一个整数,无需动任何 node。这就是它能管几千台机器还保持简单的原因。
这是"在 OA 网页上改个阈值,几十秒内全网生效"的实现链,横跨 SQL 和 C++:
- 运维在网页改了
t_metric_info的warning_val/critical_val/operation等; - 触发器
tr_update_metric_info(itl/script/db_itl_trigger_v3.sql#L7-L52)AFTER UPDATE触发,用游标找出所有受影响的 node,判断这次改动是影响"采集"还是"告警"(@node_update/@alarm_update),INSERT … ON DUPLICATE KEY UPDATE写进t_update_notice(用位或合并多次变更); tr_update_metric_group_info(#L56)同理处理采集间隔变更;还有 alarm_strategy / host_info / node_info 等一整套增删改触发器(共 40+ 个,见概览);- control 进程定时轮询取走
t_update_notice,推notify_config_change给在线 node(control/src/db.cpp#L96); - node 收到只是
refresh_collect()重新拉配置(itl_node.cpp#L36)。
也就是说"业务逻辑的一半写在 MySQL 触发器里"——这是 2012 年很典型、但今天会被诟病的设计。
handle_alarm(alarm.cpp#L342)把"瞬时抖动不告警、恢复要稳、长时间不恢复别刷屏"做成了三层:连续异常 N 次才告(默认 2)、连续正常 M 次才恢复(默认 5)、持续告警按次数退避重发(get_span)。这是 Nagios 式 flapping 抑制的手写实现。
- 直用:基本为零。 itl 监控的是淘米真实生产机群,不含任何摩尔庄园玩法/协议/配置。它和 mole 唯一的交集只是共用同一套
itl_proto_t包头风格与 gen_proto 范式(要看协议骨架请优先读 c.md / protocol.md)。 - 可借鉴的设计思路(若 moleworld 复活后要做私服运维):
- "中心 head + 每机 agent + dlopen so 采集"的可插拔架构、metric_type 位掩码一次采集多路扇出、告警三件套去抖、触发器驱动热配置——这些都是好范式,但实现层完全可以用现代栈替代(Prometheus node_exporter + Alertmanager + Grafana),不必移植这套 2010 年的 C++/RRDtool/net-snmp 代码。
- itl-speed 的"IP→省/市/运营商映射 + 按运营商聚合测速"思路,若将来要做 CDN 选路可参考,但 IP 库本身已严重过期。
- 要自己补的:告警出口的 PHP 网关(
alarm-server/index.php)、gen_proto 生成的各模块*_enum.h命令字、整套db_itl配置数据,泄漏树里都没有或不全。 - 安全提示(仅作史料,勿照搬):node 的
node_p_execute_command是远程任意 shell、update_so是从 URL 热加载 so、switch-monitor 用明文 SNMPv2c community——这套 agent 一旦中心被攻破等于整个机群沦陷,现代复活务必不要复刻这种"全权 agent"模型。
评级仍为 ★:工程质量不低,但对"摩尔庄园复活"几乎零直用价值,了解架构即可。