多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Zabbix API 批量获取主机与监控项最新值实战

Zabbix API 批量获取主机与监控项最新值实战 手里攒了几年 Zabbix 的运维活最常被问到的一类需求就是能不能把现在所有主机、所有监控项的当前值给我导一份出来提问的可能是做资产盘点的新同事也可能是要做大屏的数据组还有一类是审计要求留档。打开 Web 界面一个个点、一个个复制几百台主机直接劝退所以每次我都会掏出同一套东西Zabbix API。它能让你用一段脚本把主机清单、监控项 key、最新采集值一次性拉下来落成 JSON 或者 CSV想怎么用就怎么用。这篇文章就把我这些年用 API 取主机和监控项值的完整路子讲清楚包括认证怎么选、host.get 和 item.get 的参数怎么配、history.get 为什么经常返回空、数据量大时怎么切分请求。不管你是刚部署完 Zabbix 想练手的新人还是已经在用但对 API 只有零星了解的老手看完都能直接拿去改。1. 先把路线定下来为什么用 API 而不是导出 CSV1.1 三种取数方式的真实对比在动手写代码之前得先把从哪拿数据这件事想明白。我见过太多人一上来就写脚本结果取到的数据缺字段、口径不对返工的时间比写脚本还长。日常能用的取数方式其实就三种Web 界面手工导出、直连数据库查表、走 API。这三条路我都走过也都在生产环境里踩过坑下面把它们的真实差别摊开说。Web 界面最省事选中主机导出 CSV 是现成按钮但问题也很明显导出的是历史数据而不是当前值一次能选的主机数量有限字段固定想要自定义列基本没戏。更麻烦的是它没法自动化今天导一次明天想再导一次还得人来点。适合的场景只有临时看一眼做定时任务完全不现实。直连数据库是很多老运维的直觉选择。Zabbix 的数据都躺在 MySQL 或 PostgreSQL 里hosts、items、history_uint、history这些表看起来一目了然写条 SQL 就能连表查。实测下来这条路在只读场景下确实快但代价是你要对表结构非常熟而且一旦 Zabbix 版本升级、表结构变动脚本就要跟着改。举个最典型的坑历史值不是一张表而是按数据类型拆成了history浮点、history_uint整数、history_str字符串、history_text长文本、history_log日志五张表。你得先去items表看value_type再决定去哪张表捞数据。不知道这个拆分的人往往在history表里怎么也查不到 CPU 使用率因为那是浮点值存history、整数存history_uint混着来的。API 这条路的优势在于它是官方维护的接口层字段语义清晰版本升级基本向后兼容权限控制也沿用 Zabbix 自己的用户体系。你要的主机、监控项、最新值它都有对应的 method返回结构规整直接丢给下游消费就行。缺点是它不适合拉海量历史明细几百个 item 取一年数据会跑得很慢这点后面会详细讲。对比项Web 手工导出直连数据库Zabbix API能否自动化基本不能能能字段灵活度低固定列高但要懂表结构高参数可控版本兼容性高低表结构会变高接口稳定权限隔离跟随登录账号无拿到库权限就是全量跟随 API 账号权限适合取历史明细少量可以最快较慢需分片上手成本最低中需熟悉表结构中需理解参数我自己的选型习惯是这样的做资产清单、监控项盘点、当前值快照一律走 API做超大批量的历史数据分析直接连库跑离线任务但只读从库绝不在主库上压查询。两条路各管一段比硬用一条路硬撑要舒服得多。1.2 API 能拿到什么、拿不到什么明确了选型还得知道边界在哪。Zabbix API 能直接拿到的东西包括主机列表及全部属性、主机接口agent、SNMP、JMX、IPMI、所属主机组、关联模板、主机标签监控项列表及其 key、单位、采集间隔、数据类型、是否启用监控项的最近一次值和最近采集时间。这些加在一起基本覆盖了所有主机和监控项的值这个需求。它拿不到或者不好拿的东西也要说清楚免得你写脚本写到一半才发现方向错了。第一已删除对象的历史不留痕主机删了API 里就查不到了。第二长时间的原始历史数据要靠history.get一段段拉效率不如直接查库且受返回条数限制。第三聚合计算比如某主机一周的平均值要靠trend.get取趋势表趋势表是按小时聚合的精度和原始值不是一回事。还有一个容易被忽略的点API 返回的数据范围受调用账号的权限约束。你用超级管理员账号能拿到全站主机用一个只对某个主机组有读权限的账号返回的就只有那个组里的东西。这个特性其实是好事做多租户采集的时候可以直接复用 Zabbix 的权限体系不用在脚本里自己写过滤逻辑。但如果你明明觉得数据少了第一反应应该是去检查账号权限而不是怀疑接口。提示不要把超级管理员的账号密码硬编码在脚本里长期跑定时任务。正确做法是新建一个只读角色User role主机组权限按需分配再给这个账号生成 API Token。后面第 2 章会具体讲怎么建。2. 打通认证从 user.login 到 API Token2.1 准备一个只读账号与 Token认证这一步看着简单实际是最容易出问题的地方。Zabbix 的 API 入口就一个地址http://你的服务器/api_jsonrpc.php6.0 之后前端路径可能带子目录比如http://你的服务器/zabbix/api_jsonrpc.php这个地址一定要用浏览器确认过能打开再写进脚本否则你会对着一堆 404 发呆。账号方面我强烈建议单独建一个采集专用账号。做法是在用户里新建用户用户角色选一个只读角色内置的User role里可以基于模板复刻一个把写权限全关掉然后在用户组里把这个账号放进一个有读权限的组。如果你要采集全站主机权限就给到所有主机组如果只采集业务系统就只给对应主机组。这个动作花两分钟能省掉未来所有脚本误删配置的风险。拿到账号之后认证方式有两条路。老版本6.0 之前统一用user.login传用户名密码接口返回一个auth字符串后续每个请求都带上它。从 6.0 开始官方推荐用 API Token做法是在用户设置 - API 令牌里生成生成时会让你填名称和过期时间生成后那串长字符串只显示一次一定要当场复制存好。Token 的好处是不用担心密码轮换导致脚本失效也不用在请求体里反复传凭据直接放请求头就行。2.2 两种认证方式的请求写法先看老的user.login写法用 curl 演示方便你直接在终端验证curl -s -X POST http://192.168.1.10/api_jsonrpc.php \ -H Content-Type: application/json-rpc \ -d { jsonrpc: 2.0, method: user.login, params: { username: api-readonly, password: 你的密码 }, id: 1, auth: null }返回大概长这样result就是后续要用的认证串{jsonrpc:2.0,result:8b1f3c2e0d9a4f5b6c7d8e9f0a1b2c3d,id:1}再说 6.0 之后的 Token 方式这时候请求头里加Authorization请求体里不再需要auth字段curl -s -X POST http://192.168.1.10/api_jsonrpc.php \ -H Content-Type: application/json-rpc \ -H Authorization: Bearer 你生成的API令牌 \ -d { jsonrpc: 2.0, method: host.get, params: {output: [hostid,host]}, id: 2 }两种方式我都跑过Token 明显更省心。唯一要注意的是 Token 也有有效期创建时如果填了过期时间到期后请求会直接报权限错误定时任务就会静默失败。我的做法是给采集类 Token 设一个较长的有效期同时在监控告警里加一条采集任务连续失败的规则双保险。2.3 请求骨架与统一封装Zabbix API 的请求体结构其实很固定就四个字段jsonrpc固定2.0、method是接口名、params是参数对象、id是自增的请求标识。params怎么填决定了你拿到什么数据这也是后面几章的重点。返回统一是{jsonrpc:2.0,result:...,id:...}出错时result位置换成error里面有code、message、data三个字段排查问题主要看data。把请求封装成一个函数是所有脚本的第一步我用 Python 的requests写一个通用版本后面所有调用都复用它import requests API_URL http://192.168.1.10/api_jsonrpc.php TOKEN 你生成的API令牌 def zabbix_call(method, paramsNone, req_id1): payload { jsonrpc: 2.0, method: method, params: params or {}, id: req_id, } headers { Content-Type: application/json-rpc, Authorization: Bearer TOKEN, } resp requests.post(API_URL, jsonpayload, headersheaders, timeout30) resp.raise_for_status() data resp.json() if error in data: raise RuntimeError(f{method} 调用失败: {data[error]}) return data[result]这段代码有三个细节值得说。第一timeout30必须加我遇到过接口卡住导致脚本挂一整晚的情况加了超时至少能失败重试。第二resp.raise_for_status()只处理 HTTP 层错误Zabbix 自己的错误是在 JSON 里返回的所以还要单独判断error字段这两层不能混。第三id字段虽然叫自增但如果你要并发调用建议用一个线程安全的计数器生成避免请求和响应错位。虽然 Zabbix 本身不强制 id 唯一但排查问题时看着 id 对不上会很烦。注意Content-Type用application/json-rpc是最稳妥的虽然很多版本用application/json也能通但遇到强制校验的网关或反向代理时前者更保险。3. 一次性拿到所有主机host.get 的用法拆解3.1 最小可用参数与输出裁剪主机是这份清单的主键先把它拿全。最简调用是host.get不传任何参数此时返回所有你对它有权限的主机但每个主机的字段会非常多包括各种内部状态字段JSON 体积能吓你一跳。我一般会显式指定output只要真正需要的字段hosts zabbix_call(host.get, { output: [hostid, host, name, status, available, description], sortfield: host, sortorder: ASC, })这里host是技术名称通常就是主机名name是显示名称status里 0 表示启用、1 表示停用available是可用性0 未知、1 可用、2 不可用。这四个字段基本能拼出一份像样的资产表。加上sortfield和sortorder是为了让输出稳定不然每次跑出来的顺序不一样做 diff 对比时会很痛苦。我特别想强调output这个参数。有人图省事写output: extend意思是返回全部字段。这在主机数量少的时候无所谓几百台主机、每台几十个字段的时候返回的 JSON 可能几兆网络传输和解析都变慢。更关键的是extend返回的字段会随 Zabbix 版本变化你的下游解析逻辑可能因为多了一个字段就崩掉。显式列字段是让脚本长期稳定的基本功。3.2 把接口、分组、模板、标签一起带出来光有主机名不够用实际清单里通常还要知道这台主机用什么 IP 监控的属于哪个业务组关联了哪些模板打了什么标签。这些信息不在主机主表里要靠select系列参数带出来这是 host.get 最好用的地方hosts zabbix_call(host.get, { output: [hostid, host, name, status], selectInterfaces: [interfaceid, ip, dns, port, type, main], selectGroups: [groupid, name], selectParentTemplates: [templateid, host], selectTags: [tag, value], sortfield: host, sortorder: ASC, })返回的每个主机对象里就多了interfaces、groups、parentTemplates、tags四个数组。这里有个性能细节要留意selectInterfaces之类的子查询是逐个主机展开的主机数量上千时带三四个 select 的请求会明显变慢。我的经验是主机数在 500 以内可以放心全带超过这个量级先只取selectInterfaces分组和标签用另一个接口hostgroup.get、host.get分段拿或者干脆拆成两个脚本跑。接口的type字段是个数字枚举1 是 Zabbix agent2 是 SNMP3 是 IPMI4 是 JMX。这个对照关系建议写在注释里不然过两个月你自己都忘了。main字段表示是否为主接口一台主机同一类型可能有多个接口只有main为 1 的那个是用来采集的做清单时别把备用接口当成主 IP 统计我就犯过这个错导致 IP 数量比主机数量还多。3.3 分页、limit 与搜索的几个细节主机列表拉取最容易踩的坑是以为可以像普通 REST 接口那样用offset翻页。Zabbix API 的 get 类方法没有offset参数只有limit也就是说它只能控制最多返回多少条没法从第 N 条开始。想分批取全量只能靠sortfield加一个过滤条件来切比如按 hostid 范围筛选或者按主机组拆开取。这一点和很多人的直觉相反第一次写分页逻辑时基本都会卡住。limit的默认值也要注意。不同版本的默认上限不一样而且它会静默截断不会告诉你还有更多数据。如果你要的是全量最稳妥的做法是把limit设成一个明显大于预期主机数的值比如 10000或者干脆不传limit让它返回全部。但传太大的limit又可能因为响应体过大触发 PHP 侧的内存或超时限制返回一个 500 错误。折中方案是先用不传 limit 的调用统计总数如果数量在几百以内就一把梭超过就按主机组循环取。搜索参数也值得说两句。search做的是模糊匹配filter做的是精确匹配两者可以混用。比如想找所有名字里带 db 的主机用{search: {name: db}, searchWildcardsEnabled: True}想精确找某几台用{filter: {host: [web01, web02]}}。我通常会用 filter 做增量采集把要采集的主机名写成列表配置在外部文件里脚本读配置去拉这样既灵活又不用改代码。searchByAny参数控制多个条件是或还是与默认为 false 即与需要或的时候记得显式打开。参数作用常见误用output指定返回字段用 extend 导致响应过大selectInterfaces带出主机接口忽略 main 字段多算 IPselectGroups带出所属主机组主机多时拖慢响应filter精确匹配与 search 混用导致结果不符预期search模糊匹配未开 searchWildcardsEnabledlimit限制返回条数以为能配合 offset 翻页sortfield指定排序字段不排序导致输出顺序随机4. 拉取监控项与最新值item.get 与 history.get4.1 item.get 按主机批量取监控项有了主机清单下一步就是取每台主机的监控项。这里直接用item.get配合hostids参数传入主机 ID 数组就能批量取不用一台台循环。这是很多人不知道的优化点我见过有人写个 for 循环挨个主机调一次几百台主机跑十几分钟改成传数组之后一次请求几秒钟就回来了items zabbix_call(item.get, { output: [itemid, hostid, name, key_, value_type, units, lastvalue, lastclock, status, delay], hostids: host_id_list, monitored: True, sortfield: name, })这段参数里有三个点需要重点解释。key_这个字段名带下划线因为key在部分语言里是保留字Zabbix 为了兼容就统一加了后缀写脚本时千万别写成key否则返回里没有这一列你会以为接口坏了。value_type是后面取历史值的关键它的数字含义必须背下来。monitored: True表示只取启用状态且被监控的项如果不加这个参数那些被禁用、被不支持的项也会混进来清单会脏一大截。关于数量控制这里有个很实际的问题。如果hostids里塞了 1000 台主机每个主机 200 个监控项这一次请求就要返回 20 万条记录即便只取几个字段响应体也可能超过几十兆。我的做法是给主机分批每批 50 台左右用循环拼结果。批大小没有标准答案取决于你 Zabbix 服务器的配置和网络我一般从 20 台开始试逐步加到响应时间稳定在 5 秒以内为止。4.2 value_type 对照表与 history.get 的取数规则value_type是整个取数逻辑的分水岭这里必须停下来讲透。Zabbix 把监控项的数据类型分成五类取值范围是 0 到 4这个数字不是随便编的它直接决定了数据存在哪张表、能用哪个接口取。value_type含义典型监控项对应历史表0浮点数CPU 使用率、负载history1字符型版本号、状态字符串history_str2日志日志文件内容history_log3无符号整数网卡流量字节数、连接数history_uint4文本大段命令输出、配置内容history_text看懂这张表你就能理解为什么history.get经常返回空数组。history.get有一个必填参数就叫history值就是上面的 value_type。如果你不传接口默认按 0 处理也就是只查浮点表。于是问题来了你明明知道某个网卡流量监控项有数据调history.get却返回空因为你取的是整数类型value_type 3却用了默认的 0 去查自然查不到。我刚用 API 的时候就栽在这上面查了半天以为权限有问题最后发现是漏了history参数。正确的调用长这样history zabbix_call(history.get, { output: extend, history: 3, itemids: item_id_list, time_from: 1735689600, time_till: 1735776000, sortfield: clock, sortorder: DESC, limit: 5000, })time_from和time_till是 Unix 时间戳不传的话默认只取最近一段具体范围不同版本有差异。limit一定要设不然大表查询可能直接把接口拖垮。sortorder: DESC配合limit是取最近 N 条的常用组合比取一个时间窗口再自己排序要高效。还有个细节容易被忽略history.get的itemids一次不要塞太多。因为接口内部会对每个 itemid 拼一段查询条件item 太多时 SQL 会非常长执行计划变差甚至超出数据库的查询长度限制。我的经验值是单次不超过 100 个 itemid超了就切批。4.3 lastvalue 与 history 的取舍很多人的需求其实是要当前值而不是要历史曲线。这种情况下有一个比history.get简单得多的办法在item.get的output里带上lastvalue和lastclock一次请求就把所有监控项的最近值和最近采集时间全拿回来了省掉了取历史这一步。items zabbix_call(item.get, { output: [itemid, hostid, name, key_, lastvalue, lastclock], hostids: host_id_list, monitored: True, }) for it in items: it[lastclock_readable] ( __import__(datetime).datetime.fromtimestamp(int(it[lastclock])).strftime(%Y-%m-%d %H:%M:%S) if it[lastclock] and it[lastclock] ! 0 else 从未采集 )lastvalue的取值逻辑是取该监控项最近一次成功采集到的值它不区分数据类型字符串、数字都以字符串形式返回所以下游要自己按value_type做类型转换。lastclock是采集时间戳如果某个监控项从来没采到过数据比如 agent 连不上这两个字段会返回0脚本必须判断这个边界否则会导出一堆 1970 年的日期。那什么时候必须用history.get呢三种情况。第一是要看趋势比如对比昨天和今天的值第二是要取一段时间内的原始明细做分析第三是要取超过趋势表精度的数据。除此之外能用lastvalue就用lastvalue它快、简单、不占数据库资源。我做过一个粗略对比同样取 5000 个监控项的值item.get带lastvalue大概 2 到 3 秒用history.get取最近一条要 10 秒以上差距很明显。提示lastvalue是接口层的缓存字段反映的是最后一次采集入库的结果。如果某个监控项采集间隔是 1 小时它的lastvalue可能已经是一小时前的了。做实时性要求高的场景要结合delay字段判断这个值的新鲜度别把一小时前的数据当当前值报上去。5. 完整实操写一个可复现的全量采集脚本5.1 环境准备与目录结构前面把零件都拆开了这一章把它们拼成一个能直接跑的脚本。环境上没什么特殊要求Python 3.8 以上加一个requests库就够不需要装 Zabbix 的任何 SDK因为 API 就是纯 HTTP 调用用官方 SDK 反而多一层依赖要维护。python3 -m venv venv source venv/bin/activate pip install requests目录结构我习惯这样摆配置文件和数据分开方便打包部署zabbix-collect/ ├── config.py # 接口地址、Token、批量参数 ├── client.py # 通用请求封装 ├── collect.py # 主流程 └── output/ # 落盘结果config.py里放三类配置连接信息、批量大小、输出路径。把批量大小做成可配置非常重要因为不同规模的 Zabbix 服务器能承受的请求量差别很大在测试环境调好的参数换到生产可能要重新调。我一般会设三个值主机批量 50、监控项批量 100、历史项批量 80这三个数是踩坑之后调出来的平衡点。5.2 代码实现从主机到监控项再到值的完整链路主流程分四步认证、取主机、按批取监控项、按需取最新值并落盘。下面是最核心的collect.pyimport csv import json import time from concurrent.futures import ThreadPoolExecutor, as_completed import requests from config import API_URL, TOKEN, HOST_BATCH, ITEM_BATCH, OUT_DIR def zabbix_call(method, paramsNone): payload {jsonrpc: 2.0, method: method, params: params or {}, id: 1} headers { Content-Type: application/json-rpc, Authorization: Bearer TOKEN, } for attempt in range(3): try: r requests.post(API_URL, jsonpayload, headersheaders, timeout60) r.raise_for_status() data r.json() if error in data: raise RuntimeError(data[error]) return data[result] except Exception as e: if attempt 2: raise time.sleep(2 * (attempt 1)) def fetch_hosts(): return zabbix_call(host.get, { output: [hostid, host, name, status], selectInterfaces: [ip, port, type, main], selectGroups: [name], sortfield: host, }) def fetch_items_batch(host_ids): return zabbix_call(item.get, { output: [itemid, hostid, name, key_, value_type, units, lastvalue, lastclock, delay], hostids: host_ids, monitored: True, }) def chunk(seq, size): for i in range(0, len(seq), size): yield seq[i:i size] def main(): hosts fetch_hosts() print(f主机总数: {len(hosts)}) host_map {h[hostid]: h for h in hosts} all_items [] host_ids list(host_map.keys()) with ThreadPoolExecutor(max_workers4) as pool: futures {pool.submit(fetch_items_batch, b): b for b in chunk(host_ids, HOST_BATCH)} for fut in as_completed(futures): all_items.extend(fut.result()) print(f监控项总数: {len(all_items)}) rows [] now int(time.time()) for it in all_items: h host_map.get(it[hostid], {}) lc int(it.get(lastclock) or 0) rows.append({ host: h.get(host, ), host_name: h.get(name, ), groups: |.join(g[name] for g in h.get(groups, [])), item: it[name], key: it[key_], value_type: it[value_type], units: it.get(units, ), lastvalue: it.get(lastvalue, ), lastclock: time.strftime(%Y-%m-%d %H:%M:%S, time.localtime(lc)) if lc else , delay_s: it.get(delay, ), stale: 是 if lc and now - lc 3600 else 否, }) with open(f{OUT_DIR}/zabbix_snapshot.json, w, encodingutf-8) as f: json.dump(rows, f, ensure_asciiFalse, indent2) if rows: with open(f{OUT_DIR}/zabbix_snapshot.csv, w, newline, encodingutf-8-sig) as f: w csv.DictWriter(f, fieldnameslist(rows[0].keys())) w.writeheader() w.writerows(rows) print(f已导出 {len(rows)} 条记录) if __name__ __main__: main()这段代码里有几处是我特意加的工程化处理值得逐条说明。第一zabbix_call里做了三次重试加退避因为 Zabbix 在采集高峰期偶尔会返回超时或 500单纯失败退出会让整批数据白跑。退避时间用2 * (attempt 1)第一次等 2 秒第二次等 4 秒避免连续冲击。第二监控项是按主机分片后用线程池并发拉的4 个线程是我实测比较稳的并发度再往上加对 Zabbix 服务端的压力明显上升收益却不大。第三输出里加了stale字段判断最近采集时间是否超过一小时这一列在做数据质量检查时非常有用一眼就能看出哪些监控项其实早就没数据了。CSV 的编码用utf-8-sig是为了让 Excel 打开中文不乱码这个细节看起来小但交付给非技术同事时能省掉一堆为什么打开是乱码的沟通。JSON 和 CSV 同时输出JSON 给程序消费CSV 给人看。5.3 跑起来看结果与性能调优在一台约 300 台主机、两万多个监控项的环境里实测完整跑一遍大概 18 秒左右落盘文件 CSV 约 6 兆。这个速度对定时任务来说绰绰有余跑成每小时一次完全没压力。如果主机数涨到 2000 台耗时会涨到两分多钟这时候有两个优化方向把并发线程提到 6 到 8 个或者把主机按业务组拆成多个任务分布式跑。调优的时候要盯两个指标单次请求的响应时间和 Zabbix 服务端的负载。前者在脚本里加个耗时打印就能看到后者看服务端的 CPU 和数据库连接数。我遇到过一次把并发开到 10 个之后Zabbix 前端明显变卡后来降到 4 个就恢复正常了。这就是为什么并发度要写在配置里而不是硬编码环境不同甜点值不同。还有一个降低数据量的技巧如果只是做资产盘点根本不需要带lastvalue去掉之后返回体积能小一半以上。很多人习惯性地把所有字段都加上结果大部分都没人看。做采集脚本要有按需取数的意识需求没提到的字段一律不取这是让脚本跑得快的根本办法。6. 踩坑记录与常见问题速查6.1 报错对照表调 API 的过程里报错基本集中在几个固定位置。我把这几年遇到的和帮人排查过的整理成一张表遇到报错先来这里对照能省不少时间。报错信息关键词大概率原因处理办法Not authorizedToken 失效或 auth 串过期重新生成 Token检查有效期No permissions to referred object账号对目标主机组无读权限检查用户组权限范围Invalid params参数名写错或类型不对核对字段名注意 key_ 的下划线返回空数组但确认有数据history 参数与 value_type 不匹配按 value_type 传对应数字500 Internal Server Error响应体过大或 SQL 过长减小批量、加 limit返回结果被截断且无提示触发了默认 limit 上限显式传足够大的 limit时间格式错误time_from 传了字符串而非时间戳用 int 时间戳这里我想特别说说返回空数组这一类。它不报错最容易被当成没数据放过。除了history参数不匹配还有一个常见原因是monitored和filter条件叠加后把数据过滤没了。排查方法很简单把参数一项项去掉退到最小调用看数据能不能出来能出来再逐项加回去很快就能定位到是哪个条件在起作用。还有一个属于环境层面的坑反向代理或网关对请求体大小和响应体大小有限制。有时候脚本在本地跑得好好的部署到另一台机器走网关访问就返回 413 或者 502。这种情况要去看代理的client_max_body_size之类的配置或者干脆让采集脚本和 Zabbix 服务器在同一内网绕开网关。6.2 权限、性能、数据的三个坑第一个坑是权限的隐形缩水。前面提过 API 返回受账号权限限制但更隐蔽的情况是账号本身是管理员但所在的用户组被限制了主机组范围而创建账号的人忘了这回事。表现就是数据少了但没有任何报错。排查方法是用同一个 Token 调一次host.get不传任何过滤看返回数量和 Web 界面上主机页显示的总数是否一致。不一致就是权限问题别往别处想。第二个坑是性能的雪崩效应。采集脚本本身不重但它查的是 Zabbix 的数据库。如果历史表数据量大、又没有合适的索引一个history.get可能让数据库的慢查询堆积起来进而影响告警和前端。我的纪律是采集任务避开告警高峰期history.get必须带时间范围绝不做全表扫描式的调用。有条件的话让 Zabbix 的数据库从库来承担这类只读查询。第三个坑是数据的看起来对但实际错。典型例子是lastvalue拿来当实时值用结果那个监控项的采集间隔是 10 分钟你拿到的是 10 分钟前的数。另一个例子是接口的main字段没过滤导致一台主机的 IP 被统计了多次。这类问题不会报错只会让报表悄悄失真。我的应对办法是在脚本里加数据校验统计主机数、监控项数、值为空的比例、超过一小时未更新的比例这几个数字一异常就能及时发现问题。6.3 我在实际项目里沉淀的几条经验关于认证我的建议是彻底告别user.login加密码的方式全面切到 API Token。原因不只是安全更是运维成本密码有轮换策略一旦到期所有用密码认证的脚本会同时失效而且报错是未授权这种不痛不痒的提示排查起来要翻好几个地方。Token 可以单独控制有效期采集专用的 Token 设长一点出问题也只影响一条链路。关于批量参数别迷信一次拉完最省事。我最初写这个脚本的时候图省事一次性传了全部主机 ID结果在测试环境跑通了到生产环境直接 500。后来改成按批处理后不仅稳定了总耗时反而更短因为小批量请求能更好地利用并发也不会长时间占用数据库连接。批量大小这件事宁小勿大。关于字段选择养成先看文档再取字段的习惯。Zabbix 官方文档里每个 method 的参数说明都很详细花十分钟读一遍比在脚本里反复试错要快。尤其select系列参数文档会标明它是否影响性能这个提示很多人直接跳过结果就是脚本慢还找不到原因。关于结果落盘我强烈建议同时输出 JSON 和 CSV 两份。JSON 给后续的自动化流程消费字段类型保真CSV 给业务同事看用utf-8-sig编码避免 Excel 乱码。另外文件名带上日期比如zabbix_snapshot_20250115.csv做历史对比和问题回溯的时候会方便很多也不容易覆盖上一次的结果。最后一条是关于代码维护的。这类采集脚本的生命周期往往比想象的长一开始只是导一份数据后来会不断加需求加过滤条件、加字段、加定时。所以第一版就要把配置和逻辑分开把请求封装抽出来把批量参数做成可配置。多花的那点时间会在后面每次改需求的时候加倍还给你。我自己那个脚本从最早的一百多行长到现在四百多行支撑了三四个不同部门的取数需求靠的就是一开始把结构搭对了。
返回列表