
手上有几百台主机老板临时要一份「所有主机、所有监控项、当前值」的清单你点开 Zabbix 前端一台一台翻翻到第三十台就想砸键盘。这个场景几乎每个做运维的人都遇到过而真正能一次性解决问题的就是Zabbix API。它不需要你装额外插件不需要改数据库只要一个只读权限的令牌就能把主机清单、监控项定义、最新值、历史曲线全部拉到你自己的脚本里。这篇文章讲的就是怎么用 API 把「所有主机」和「所有监控项的值」完整、稳定、可复现地取出来包括请求怎么写、参数怎么填、几万条监控项的时候怎么不被超时打断、以及我实际跑了一轮之后踩到的那些坑。不管你是刚开始接触 Zabbix 的新手还是已经能写几个item.get的老手都能从里面拿到能直接抄的代码和判断依据。1. 先把需求说清楚为什么要走 API 拉主机和监控项1.1 一个很典型的巡检场景最常见的触发点有三个。第一种是定期资产盘点需要一个「谁在监控、监控了什么、现在是什么状态」的静态快照用来和 CMDB 或者工单系统对账。第二种是迁移或升级前的基线采集比如从旧版本往新版本搬或者从自建机房搬到云上先要把现有监控项 key 全部导出来不然新环境里少采了哪个指标可能过两个月才发现。第三种是把 Zabbix 的数据喂给自建看板或者告警联动比如按主机组聚合一下「不可用主机的数量」超过阈值就推到内部群里。这三种需求有一个共同点它们是批量的、周期性的、需要落成结构化数据的。而 Zabbix 前端恰恰在这三点上都很弱。前端能看能筛选但导出能力有限「最新数据」页面一次显示多少条还受配置限制几百个筛选条件点下来人也废了。所以只要涉及批量API 就是唯一正路。1.2 API 拿到的和前端看到的其实是同一份数据这一点很多人有误解觉得 API 是「另一套数据」。不是的。Zabbix 的前端本身就是 API 的第一个调用方你在浏览器里点「主机」列表页面背后就是一串发往api_jsonrpc.php的 JSON 请求。所以 API 返回的字段名、状态值、时间戳跟前端显示的东西是一一对应的包括那个让人迷惑的available字段。区别只在于前端帮你做了二次加工把status1渲染成灰色的「已停用」API 给你的都是原始值。这意味着你脚本里的每一个字段都得自己解释好处是你也就彻底掌握了口径——报表里「不可用主机」到底算不算停用主机这个决定权在你手上不在前端。1.3 版本先行6.0 / 6.4 / 7.0 的认证差异写脚本之前必须先确认目标环境版本因为认证方式这几年改过两次网上抄来的老脚本大概率直接报错。查版本最省事的方式是调apiinfo.version这个方法不需要认证就能调。curl -s -X POST https://zabbix.example.com/api_jsonrpc.php \ -H Content-Type: application/json-rpc \ -d {jsonrpc:2.0,method:apiinfo.version,params:{},id:1}返回类似{jsonrpc:2.0,result:7.0.3,id:1}。对着结果看下面这张表版本区间认证方式body 里的 auth 字段其他变化6.0 以前user.login拿 token随请求放在auth必填selectGroups是旧名字6.0 – 6.2支持 API Token走Authorization头可用主机对象的groups改名hostgroupsselectGroups改名selectHostGroups6.4API Token 成为推荐做法已标记废弃但仍能用前端出现专门的「API 令牌」管理入口7.0 及以后只能用Authorization: Bearer彻底移除继续传auth会直接报参数错误如果你维护的脚本要同时跑在 6.0 和 7.0 上最稳的做法是统一用 API Token Authorization 头。这个组合在 6.0 之后的版本里都能用只有非常老的 5.x 环境才需要回退到user.login。1.4 开工前要准备的四样东西第一样是一个专用的只读账号。别图省事用 Admin理由后面第 7 节会讲那是个真实的坑。给这个账号建一个只读角色只勾选「读取」权限主机组范围按需要放开。第二样是API Token。在「用户设置 - API 令牌」里创建绑定到上面那个只读账号有效期按需设置。令牌明文只在创建时显示一次先复制出来存好。第三样是确认API 端点 URL。默认是http(s)://host/api_jsonrpc.php。如果你前面挂了反向代理并且带子路径比如/zabbix/api_jsonrpc.php那脚本里就得写全否则会返回 404 或者一段 HTML而不是 JSON。第四样是脚本运行环境。一台能访问到 Zabbix Server 的机器就行Python 3 加requests库足够不需要装 Zabbix 官方 SDK——当然你要用 SDK 也可以但纯 HTTP 调用更能看清每一步发生了什么。2. 打通第一个请求认证、端点与 JSON-RPC 报文结构2.1 用 user.login 换一个会话令牌老版本环境下没有 API Token 概念只能先登录再调用curl -s -X POST https://zabbix.example.com/api_jsonrpc.php \ -H Content-Type: application/json-rpc \ -d { jsonrpc: 2.0, method: user.login, params: {username: api_reader, password: 你的密码}, id: 1 }返回的result是一串 32 位十六进制字符串就是会话令牌。后续请求把它原样带回去。这里有个容易被忽略的副作用每调用一次user.login数据库里就会新开一条会话记录。如果你把登录逻辑写在循环里面一天下来能攒出几千条会话轻则「管理 - 用户 - 会话」页面卡顿重则触发会话数上限。所以标准姿势是「登录一次、用完登出」也就是在收尾时调一次user.logout把这个令牌主动作废。2.2 API Token 才是现在的推荐姿势用令牌就不存在这个问题因为令牌不是会话不用登出。整个请求只需要多一个头curl -s -X POST https://zabbix.example.com/api_jsonrpc.php \ -H Content-Type: application/json-rpc \ -H Authorization: Bearer 你的APIToken \ -d {jsonrpc:2.0,method:host.get,params:{output:[hostid,host],limit:2},id:1}Content-Type用application/json-rpc或者application/json都行Zabbix 两种都收。但如果你是照着官方文档抄的用application/json-rpc更符合习惯。2.3 报文里四个字段到底在说什么不管调哪个方法请求体的骨架都一样就四个键jsonrpc固定写2.0声明协议版本。method方法名比如host.get、item.get、history.get。全部是小写字母加点的形式。params参数字典。不传参数的方法比如apiinfo.version写成空对象{}。id请求标识。服务端会把这个值原样回传用来把响应和请求对上。单次请求随便写个递增整数就行。id这个字段在批量请求时才体现价值。Zabbix 支持把多个请求打包成一个数组一次发出去[ {jsonrpc:2.0,method:host.get,params:{output:extend,limit:1},id:1}, {jsonrpc:2.0,method:item.get,params:{output:extend,limit:1},id:2} ]响应也是一个数组靠id区分哪条对应哪条。批量请求能减少网络往返但它没有事务语义——其中一个失败其他的照样执行。所以只建议在读取类方法上用写操作老老实实一条一条来。2.4 用 curl 做一次最小验证正式写脚本前我习惯用 curl 跑一遍最小用例把可能的问题先暴露出来。顺序是先apiinfo.version确认网络和端点没问题再host.get带limit: 1确认认证没问题curl -s -X POST https://zabbix.example.com/api_jsonrpc.php \ -H Content-Type: application/json-rpc \ -H Authorization: Bearer 你的APIToken \ -d { jsonrpc: 2.0, method: host.get, params: { output: [hostid, host, status, available], limit: 1 }, id: 1 }能拿到一条主机记录说明认证、端点、权限三件事都通了可以开始正式干活。如果这一步就报错先把错误信息看清楚——Invalid params.和Not authorized.是两类完全不同的问题前者是参数写错后者是令牌或权限的问题别混着查。3. host.get把主机清单完整捞干净3.1 output 怎么填才不浪费带宽output参数决定返回哪些字段有三种写法extend返回全部字段shorten只返回 hostid 和 host 两个或者直接给一个字段名数组。很多人图省事用extend在几十台主机的环境里完全没问题但主机上千之后就很难看了。主机对象的完整字段有四十多个包含大量ipmi_*、snmptrap_*这类你多半用不上的东西。一次host.get返回的 JSON 可能好几兆解析慢、内存占用高还会拖慢 Zabbix Server 那一侧。我常用的字段清单是这样的字段说明是否常用hostid主机唯一 ID是后续串联 item 的关键必取host主机名host 字段通常是技术名必取name可见名称很多环境里填的是中文描述必取status0 启用监控、1 停用监控必取available0 未知、1 可用、2 不可用必取active_available主动模式 agent 的可用性6.0 之后才有建议取maintenance_status0 正常、1 维护中建议取description主机描述常被当成资产备注用按需active_available和maintenance_status在部分版本上可能不返回遇到这种情况别慌maintenance_status可以退化成单独调一次maintenance.get自己判断active_available则要看你的 agent 是不是主动模式。3.2 selectInterfaces / selectHostGroups / selectParentTemplates 的取舍output只控制主机自身的字段接口、主机组、模板这些关联对象要用select*参数显式带出来。每一个select*都是一次额外的关联查询带得越多服务端压力越大。params { output: [hostid, host, name, status, available, active_available], selectInterfaces: [interfaceid, ip, dns, port, type, main, available], selectHostGroups: [groupid, name], selectParentTemplates: [templateid, name], sortfield: hostid, sortorder: ASC, }注意这里写的是selectHostGroups而不是老教程里的selectGroups。6.0 之后主机对象的这个属性已经改名返回的键也变成了hostgroups。如果你在老版本上跑就换回selectGroups返回键是groups。两者不兼容脚本里最好根据版本做个分支。还有个小提醒selectInterfaces千万别写extend。接口对象里有一堆bulk、details字段尤其是 SNMP 接口每个接口都可能带一段很长的 JSON 配置。三五百台主机乘上每台两三个接口返回体瞬间膨胀好几倍。3.3 status、available、maintenance_status 这三个字段最容易读错这三个字段名字都挺直白但含义经常被搞混做出的报表自然就偏了。status是一个「人的决定」表示这台主机在 Zabbix 里是否启用监控0 启用、1 停用。停用的主机采集器根本不会去采它它的所有监控项数据都会停在那里不动。available是一个「系统的判断」表示 Zabbix Server 对这台主机最近一次探活的结果0 未知、1 可用、2 不可用。它的更新依赖于 server 的可用性检查频率默认是一分钟一次。刚加进来的主机会是 0要等一轮检查之后才会变。它反映的是「采得到」不是「机器活着」——一台主机的 agent 端口不通但机器本身在跑available就是 2。maintenance_status表示主机是否处于维护窗口0 正常、1 维护中。维护期间数据照常采集只是不触发告警。做「需要处理的问题」统计时维护中的主机通常要单独剔掉否则每天凌晨的备份窗口会给你制造一堆假问题。把这三个组合起来判断逻辑就清楚了。比如「真正需要关注的主机」应该是status 0且available 2且maintenance_status 0。3.4 关于分页Zabbix API 没有 offset这是个硬伤这一点必须单独讲因为它和绝大多数 REST API 的习惯完全不同。host.get只有limit参数没有offset也没有page。你没法像调普通接口那样「先取前 100 条再取 101 到 200 条」。那主机特别多的时候怎么办三个可用的思路切成小批显式传 ID。先想别的办法拿到一批主机 ID比如按主机组、按环境命名规则再用hostids参数分批请求。这是最可控的方式。用 limit 卡住总量接受一次拿不全。适合只做抽样统计的场景。按业务维度拆。groupids、filter、search都能帮你把数据集缩小到一次能拿完的规模。我实测过三千台主机、不选任何关联对象的情况下单次host.get大概一两秒返回体量在可接受范围内。真正会出问题的是再叠加selectItems这种重型关联那就千万别一次拉。所以我的习惯是主机和监控项分两次拉靠 hostid 在脚本里做关联而不是让 Zabbix 在服务端帮我 join。4. item.get监控项是配置不是值4.1 item.get 到底返回什么、不返回什么这是新手最容易迷路的一个点。item.get返回的是监控项的定义它叫什么、key 是什么、多久采一次、数值类型是什么、属于哪台主机。这些属于「配置」层面的信息存在items表里。真正的「值」存在另外的地方取决于时间跨度近期数据在history表长期聚合在trends表由history.get和trend.get两个方法读取。不过item.get会附送几个非常实用的字段——lastvalue、lastclock、prevvalue。这三个是从历史数据里拎出来的「最近一条值」「那条值的时间」「上一条值」。对于「当前快照」这种需求它们就够了你不需要为每个监控项再调一次history.get。这也是我强烈建议的路线先做快照再按需补历史。4.2 value_type 与 units决定后面怎么解析value_type是个 0 到 4 的整数它同时决定了三件事数据存在哪张表、history.get里该传什么参数、拿到值之后该怎么转换。value_type含义history 参数趋势数据解析方式0浮点数0有float()1字符1无直接当字符串2日志2无直接当字符串通常不做数值计算3无符号整数3有int()4文本4无字符串注意可能不落历史库这张表里最需要记住的是history.get的history参数值就等于监控项的value_type。所以你在代码里不需要维护一张映射表直接history: int(item[value_type])就行。另外units字段也别扔。它告诉你这个值是秒、是字节、还是百分比。做报表的时候把units拼到数值后面可读性完全不一样。而且在推导容量趋势的时候知道单位是字节还是千字节能省掉一次单位换算的争论。4.3 status / state / error过滤掉不该进报表的行这三个字段名字很像功能完全不同status0 启用、1 禁用。这是「人的决定」禁用后不再采集。state0 正常、1 不支持。这是「系统的判断」表示最近一次采集失败了。error非空字符串说明采集失败的具体原因比如 key 写错了、OID 不存在、脚本超时。做「当前值报表」的时候怎么过滤这三类数据是个取舍点没有标准答案。我的做法是分两档主报表只保留status 0 且 state 0保证里面的数字是可信的另外单独出一张「异常监控项清单」把state 1的挑出来带上error字段直接发给负责的同事。这样既不污染主数据又不会漏掉采集故障。顺便说一句item.get有个monitored参数设为true时会「只返回属于已监控主机的已启用监控项」。听起来正好是你想要的但要小心它同时会把停用主机上的所有监控项一起过滤掉。如果你是想统计「哪些监控项被禁用了」用这个参数就什么都查不到得手动按status过滤。4.4 lastvalue 是个好东西但别全信lastvalue能省掉大量请求但它有四个特性必须知道否则报表会莫名其妙地不对。第一数值也是字符串。浮点类型的监控项lastvalue返回的是1.2345这样的字符串不是数字。直接拿去排序会变成字典序9排在10后面。必须显式转成float或int。第二停用或不支持的监控项lastvalue是旧值。它不会变空只会一直停在那里。判断「这个值是不是还新鲜」要看lastclock——那是个 Unix 时间戳和当前时间一比就知道有多旧了。我的习惯是设一个阈值比如lastclock距今超过采集间隔的三倍就在报表里标一个「疑似过期」。第三文本类监控项的lastvalue可能被截断。Zabbix 为了避免单条数据过大对文本类型是有长度限制的。想拿完整内容得走history.get。第四主动模式的监控项在 Server 刚重启后可能没有最新值。因为主动模式的服务器端和 agent 端有各自的缓冲区重启后需要等一轮采集周期数据才会补上。这时候lastvalue可能是空字符串。5. 真要历史数值就得用 history.get5.1 调用形态与必需参数history.get的骨架不长但每个参数都很关键rows call(history.get, { output: extend, history: 0, itemids: [42269, 42270], time_from: int(start_ts), time_till: int(end_ts), sortfield: clock, sortorder: DESC, limit: 500, })返回的每条记录包含itemid、clockUnix 时间戳、value字符串形式的数值。注意value依然是字符串转类型这件事得你自己做。5.2 time_from / time_till 的坑时间窗开大了会拖垮数据库history表是一张按月甚至按天分区的大表数据量随监控项数量线性增长。你查一个「最近 30 天所有主机所有监控项」的范围等于让数据库把所有分区都扫一遍哪怕只返回几条扫描成本也已经付出了。所以有三条我踩过之后定下来的规矩时间窗能小不要大。拿最新一条值时用sortfield: clock加sortorder: DESC加limit: 1是最省的方式固定扫一个分区就够比不排序直接拉全量快一个数量级。不传时间范围不是好习惯。time_from和time_till不传的话Zabbix 不会自动帮你限制窗口很容易一次拉出几十万条。我见过把内存打爆的脚本就是这么写的。时间戳必须是整数。time_from传字符串会直接报参数错误。用 Python 的话就是int(time.time())别用str()包一层。5.3 按 value_type 分组批量拉取history.get有个限制一次调用只能指定一个history值。所以你没法把浮点数和整数混在一次请求里拉。解决办法是按value_type把监控项分四组每组内部再把itemids批量塞进去一批 100 到 200 个from collections import defaultdict by_type defaultdict(list) for it in items: by_type[int(it[value_type])].append(it[itemid]) for vt, ids in by_type.items(): rows fetch_history(ids, vt, start_ts, end_ts) print(value_type{} 监控项 {} 个历史 {} 条.format(vt, len(ids), len(rows)))批量带来的效率提升非常明显。一万个监控项逐个调用是一万次 HTTP 往返按类型分成四组、每组每批 100 个总共几十次请求就够了。中间那个数量级的差别在实际跑起来的时候就是几分钟和几个小时的差别。limit参数也别忘。不设limit的话一批 100 个监控项乘上各自的采集点返回体可能上百兆。我一般给 500 到 2000配合时间窗控制总量。5.4 超过一周的数据走 trend.gethistory表的保留期通常不长很多环境只留 7 到 30 天。想看更长时间的走势得走trend.gettrends call(trend.get, { output: [itemid, clock, num, value_min, value_avg, value_max], itemids: ids, time_from: int(start_ts), time_till: int(end_ts), })trends表是按小时聚合的每条记录里有这小时里的样本数量num、最小值、平均值、最大值。所以查询量比history小得多一个监控项看 7 天也就 168 条记录。画趋势图、算日均值、做容量预测都该用trend.get。用history.get去算一个月均值不仅慢而且因为原始数据可能已经过期你还未必拿得到完整的一个月。还有个细节trend.get只对value_type是 0 和 3 的监控项有效字符、日志、文本类型没有趋势数据这是从第 4.2 节那张表里就能推出来的。6. 完整脚本主机 监控项 最新值一次拉出来6.1 脚本整体设计思路整个流程分三步顺序不能反调host.get拿到主机清单同时建立hostid → 主机信息的映射字典。用主机的 ID 列表分片调item.get把监控项拉全。在本地把监控项按hostid拼到主机上输出 CSV。关键设计决策是不在服务端做 join。虽然item.get有selectHosts参数能直接带出所属主机但那会让每条监控项都重复携带一份主机信息。一万个监控项、每个都带一遍主机名和接口列表JSON 体量能翻好几倍。本地内存里建个字典 join 一下成本几乎为零。6.2 认证与请求封装封装这一层主要解决三件事统一加认证头、统一判错、统一重试。# -*- coding: utf-8 -*- import csv import json import time import requests ZABBIX_URL https://zabbix.example.com/api_jsonrpc.php API_TOKEN 把这里换成你的 API Token SESSION requests.Session() SESSION.headers.update({ Content-Type: application/json-rpc, Authorization: Bearer API_TOKEN, }) _req_id 0 def call(method, paramsNone, retries3, timeout(5, 120)): 统一封装发请求、判错、指数退避重试。 global _req_id _req_id 1 payload { jsonrpc: 2.0, method: method, params: params or {}, id: _req_id, } body json.dumps(payload) # 用 data 而不是 json避免 requests 覆盖 Content-Type last_err None for attempt in range(retries): try: resp SESSION.post(ZABBIX_URL, databody, timeouttimeout, verifyTrue) resp.raise_for_status() data resp.json() except (requests.RequestException, ValueError) as exc: last_err exc time.sleep(2 ** attempt) continue if error in data: err data[error] raise RuntimeError( API error {code}: {msg} | {data}.format( codeerr.get(code), msgerr.get(message), dataerr.get(data), ) ) return data[result] raise RuntimeError(重试 {} 次仍失败: {}.format(retries, last_err))这里面有两处细节值得说。一是用databody而不是jsonpayload。requests在检测到json参数时会强制把 Content-Type 改写为application/json覆盖你手动设的application/json-rpc。虽然 Zabbix 两种都收但如果你后面接的是别的网关或者做了严格的头校验这一步就会出问题。二是timeout用了元组(连接超时, 读取超时)。单值的timeout120只约束连接阶段读取阶段会无限等下去一个卡住的请求能把整个脚本挂死。6.3 主机与监控项分批抓取HOST_OUTPUT [hostid, host, name, status, available, active_available, maintenance_status, description] ITEM_OUTPUT [itemid, hostid, name, key_, value_type, units, status, state, error, delay, lastclock, lastvalue, prevvalue] ITEM_CHUNK 50 def fetch_hosts(group_idsNone): params { output: HOST_OUTPUT, selectInterfaces: [interfaceid, ip, dns, port, type, main, available], selectHostGroups: [groupid, name], sortfield: hostid, sortorder: ASC, } if group_ids: params[groupids] group_ids return call(host.get, params) def fetch_items(host_ids, include_disabledFalse): result [] for i in range(0, len(host_ids), ITEM_CHUNK): batch host_ids[i:i ITEM_CHUNK] params { output: ITEM_OUTPUT, hostids: batch, sortfield: itemid, sortorder: ASC, } if not include_disabled: params[monitored] True result.extend(call(item.get, params)) time.sleep(0.1) # 轻微节流别把 API 打满 return resultITEM_CHUNK定在 50 是个经验值。太小了请求次数多太大了一次返回体过重、超时概率高。如果你环境里监控项特别密集比如每台主机上千个可以降到 20 到 30。time.sleep(0.1)那一行不是多余的。Zabbix 的 API 走的是前端的 PHP 执行池你这边密集请求挤占的是前端页面自己的资源。加个 100 毫秒的间隔脚本总时长多个几十秒但同事打开前端页面不会卡这是团队协作的基本礼貌。6.4 输出 CSV 的字段设计与合并逻辑NUMERIC_TYPES {0, 3} VALUE_TYPE_NAME {0: float, 1: char, 2: log, 3: uint, 4: text} def to_number(value_type, raw): 把字符串形式的 lastvalue 转成数字转不了返回 None。 if raw is None or raw or value_type not in NUMERIC_TYPES: return None try: return float(raw) if value_type 0 else int(float(raw)) except (TypeError, ValueError): return None def dump_csv(hosts, items, pathzabbix_snapshot.csv): host_map {h[hostid]: h for h in hosts} fields [hostid, host, host_name, host_status, host_available, groups, interfaces, itemid, item_name, item_key, value_type, units, item_state, lastclock, lastvalue, numeric_value] with open(path, w, newline, encodingutf-8-sig) as fp: writer csv.DictWriter(fp, fieldnamesfields) writer.writeheader() for it in items: h host_map.get(it[hostid], {}) groups ,.join(g[name] for g in h.get(hostgroups, [])) ifaces ,.join( {}:{}.format(ifc.get(ip) or ifc.get(dns), ifc.get(port)) for ifc in h.get(interfaces, []) ) vt int(it[value_type]) writer.writerow({ hostid: h.get(hostid), host: h.get(host), host_name: h.get(name), host_status: h.get(status), host_available: h.get(available), groups: groups, interfaces: ifaces, itemid: it[itemid], item_name: it[name], item_key: it[key_], value_type: VALUE_TYPE_NAME.get(vt, vt), units: it.get(units), item_state: it.get(state), lastclock: it.get(lastclock), lastvalue: it.get(lastvalue), numeric_value: to_number(vt, it.get(lastvalue)), }) return path两个细节。一是encodingutf-8-sig多带一个 BOM 头直接双击用 Excel 打开中文不会乱码这是个纯粹的实用主义选择。二是numeric_value这一列单独存数值型结果和原始字符串的lastvalue并存。这样既保留了原始数据便于追溯又能直接拿去做透视表和排序——把两个用途塞进一列后面一定会有人被排序结果搞糊涂。主流程就简单了def main(): t0 time.time() hosts fetch_hosts() print(主机数{}.format(len(hosts))) items fetch_items([h[hostid] for h in hosts]) print(监控项数{}.format(len(items))) path dump_csv(hosts, items) print(已输出 {}耗时 {:.1f}s.format(path, time.time() - t0)) if __name__ __main__: main()6.5 并发与限流别把 Server 打满有人会想用线程池加速比如开 20 个线程同时发请求。我实测下来的结论是不太值而且有风险。Zabbix 的 API 请求最终落到后端的 PHP 进程上进程池是有限的。你开 20 个并发只是把本来平铺的请求堆成一坨总耗时未必下降多少但前端页面会明显卡顿甚至出现 504。我的建议是并发控制在 3 到 5 之间配合requests.Session的连接复用效果已经够好。另外两个时间点的经验一是避开整点很多环境里整点是采集和告警的高峰你在这时候猛拉数据容易抢不到资源二是给单次调用设读取超时60 到 120 秒比较合理超了就放弃这一批重试不要无限等。7. 实测踩过的坑以及我怎么绕过去的7.1 数值变成了字符串这是第一个坑也是最隐蔽的一个。第一版脚本跑完我拿lastvalue直接做了个sum()结果出来一个字符串拼接的怪物——1.2 3.4得到的是1.23.4。排查了十分钟才反应过来Zabbix API 返回的value和lastvalue一律是字符串不管value_type是 0 还是 3。修复方式就是第 6.4 节里的to_number函数。里面int(float(raw))这个双转换是为了兼容一种特殊情况无符号整数类型的监控项有时候返回的值会带小数点比如1024.0。直接int(1024.0)会抛异常先float再int就稳了。7.2 监控项数到五万之后的两次超时环境里有五万多条监控项一开始我用ITEM_CHUNK 200跑起来接连两次在读取阶段超时。把output从extend改成显式字段列表、ITEM_CHUNK降到 50 之后同样的数据量稳稳跑完总耗时反而因为少了无关字段的传输而下降了三成。这里得到的经验是排查 API 超时先看返回体大小再看服务端负载。很多人第一反应是去调服务端的max_execution_time那是治标。返回体瘦下来问题自己就没了。7.3 权限不足导致的数据静默缺失这是我踩过最深的一个坑也是最值得记住的一个。用 Admin 账号跑脚本导出来是 812 台主机。后来换成专用的只读账号跑同样的脚本导出来只有 610 台。原因在于非超级管理员账号调host.get时Zabbix 只会返回该账号所属用户组有权限访问的主机而且不会报错、不会有任何提示。它就安安静静地少给你两百台。同理item.get也只返回有权限的监控项。所以如果你拿到的数据和前端页面对不上在前端用同一个账号登录看一下总数基本立刻就能确认是不是权限问题。这也解释了为什么我不建议用 Admin 跑脚本——用高权限账号跑出来的数字和生产环境下真正该看到的数字可能压根不是一回事那份报表拿去对账是要出事的。7.4 会话泄漏与 user.logout用 API Token 不会有这个问题但如果你维护的是老版本上基于user.login的脚本一定要在finally块里加一次user.logout。我见过一个每小时跑一次、每次都不登出的定时任务跑了一周之后「管理 - 用户 - 会话」列表里有三百多条记录页面前端打开要转圈好几秒。写法就是把登录和登出包起来token call(user.login, {username: u, password: p}) SESSION.headers[Authorization] Bearer token try: # 所有业务调用都放这里 pass finally: call(user.logout, {}) # 新版请求体不再需要传 token靠 Authorization 头识别7.5 400 Invalid params 的几种典型触发方式最后把报参数错误的常见原因整理一下这类报错信息很短但非常容易卡住人报错场景错误写法正确写法output 写法output: hostid,hostoutput: [hostid, host]时间戳类型time_from: 1735660800time_from: 1735660800ID 列表hostids: 10084hostids: [10084]select 参数selectInterfaces: trueselectInterfaces: [ip, port]7.0 传 auth请求体里带auth: xxx改用Authorization: Bearer头混合版本字段名6.0 上写selectHostGroups确认版本老版本用selectGroups核心规律其实只有一条Zabbix API 对参数类型很严格尤其是数组和字符串的区别。它不会帮你做隐式转换。所以我在call封装里直接抛异常而不是静默返回就是为了让这类问题在开发阶段立刻暴露而不是等到报表出来才发现数字不对。我个人跑这套脚本的习惯是先在小范围一个主机组验证一遍字段和口径确认没问题再放到全量上跑输出的 CSV 除了给业务方自己留一份带时间戳的存档下次有人问「上个月这时候这台主机的 agent 是什么状态」翻出来就能答比重新查 API 快得多。