管10台服务器和管200家门店的IT设备,复杂度完全不是一个量级。
服务器在机房里,网络稳、环境好、出了问题SSH上去排查。门店不一样------设备分散在全国各地,网络走的是商业宽带(质量随缘),出了问题现场只有店员,你远程都不一定连得上去。
我们接手一个连锁零售客户的时候,情况是这样的:
- 236家门店,分布在5个大区
- 每家门店标准配置:1台路由器(多数是华为AR系列)、1台接入交换机、2-3台POS终端、1台本地收银服务器
- 之前的"监控"方式:店长发现网断了打电话给IT部,IT部远程ping一下,ping不通就派人去现场
这种模式最大的问题:所有故障都是被动发现的,而且发现的时候已经影响营业了。
下面是我们实际跑通的方案。
一、连锁门店监控的3个核心难点
在上方案之前先说清楚难在哪,否则直接看配置会觉得"为什么搞这么复杂"。
| 难点 | 具体表现 | 导致的后果 |
|---|---|---|
| 网络不可靠 | 商业宽带、动态IP、偶尔断线 | 传统Agent直连Server的架构,断网=监控盲区 |
| 设备异构 | 不同门店可能用不同品牌路由器/交换机 | 一套模板覆盖不了,配置管理复杂 |
| 告警洪水 | 200+门店×5+设备=1000+监控点 | 不按区域分组就是告警轰炸 |
所以架构设计必须同时解决这三个问题:断网容灾(Proxy本地缓存)、设备异构(自动发现+多模板)、告警分治(按区域/门店路由)。
二、架构设计:Zabbix Proxy分区域部署

整体架构是三层:
[Zabbix Server - 总部机房]
├── [Proxy-华东] → 覆盖上海/杭州/南京 60家门店
├── [Proxy-华南] → 覆盖深圳/广州/厦门 55家门店
├── [Proxy-华北] → 覆盖北京/天津/青岛 48家门店
├── [Proxy-西南] → 覆盖成都/重庆/昆明 38家门店
└── [Proxy-华中] → 覆盖武汉/长沙/郑州 35家门店
为什么用Proxy而不是Agent直连Server?
三个原因:
- 断网缓存:门店宽带断了,Proxy在本地缓存监控数据,网络恢复后自动上传,不丢数据
- 减轻Server压力:1000+设备全直连Server,Server的poller进程会被打满。Proxy分担采集压力
- 网络穿透简单:只需要Proxy→Server的一条出站连接(10051端口),不需要Server能访问门店内网
2.1 Proxy部署配置
每个区域部署一台Proxy(可以是云服务器,也可以是某个核心门店的本地服务器):
ini
# /etc/zabbix/zabbix_proxy.conf(华东Proxy示例)
# 基础配置
Server=10.0.1.100 # 总部Zabbix Server地址
Hostname=proxy-east-china # Proxy名称,需在Server侧注册
DBName=/var/lib/zabbix/zabbix_proxy.db # 本地SQLite数据库
# 性能参数(按60家门店×5设备=300台设备调优)
StartPollers=10
StartPollersUnreachable=3
StartPingers=5
StartDiscoverers=3
# 离线缓存配置(关键!)
ProxyLocalBuffer=48 # 本地保留48小时历史数据
ProxyOfflineBuffer=72 # 与Server断连时最多缓存72小时
DataSenderFrequency=5 # 每5秒向Server发送一次数据
# 超时设置(门店网络慢,适当放宽)
Timeout=15
UnreachablePeriod=60
UnavailableDelay=120
# 日志
LogFile=/var/log/zabbix/zabbix_proxy.log
LogFileSize=50
Server侧注册Proxy:
bash
# 在Zabbix Server的Web界面中:
# Administration → Proxies → Create proxy
# Proxy name: proxy-east-china
# Proxy mode: Active(Proxy主动连Server)
# Proxy address: 不填(Active模式不需要)
# 或者用API批量注册5个Proxy:
curl -X POST http://zabbix-server/api_jsonrpc.php \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "proxy.create",
"params": {
"host": "proxy-east-china",
"status": 5,
"description": "华东区域Proxy - 覆盖上海/杭州/南京60家门店"
},
"auth": "YOUR_API_TOKEN",
"id": 1
}'
三、门店设备监控项设计

每家门店的设备监控需求其实高度标准化------不需要每家门店单独设计,用模板+自动发现覆盖。
3.1 路由器监控(SNMP)
门店路由器最关键的两个指标:WAN口是否通 和带宽是否打满。
yaml
# Zabbix模板:Template_Store_Router
# 监控项配置(导入模板后自动应用)
items:
- name: "WAN口ICMP连通性"
type: SIMPLE_CHECK # icmpping
key: "icmpping[{HOST.CONN}]"
delay: 30s
triggers:
- expression: "max(/Template_Store_Router/icmpping[{HOST.CONN}],3m)=0"
severity: HIGH
name: "门店路由器WAN口不可达({HOST.NAME})"
- expression: "avg(/Template_Store_Router/icmpping[{HOST.CONN}],5m)<0.8"
severity: WARNING
name: "门店路由器丢包率>20%({HOST.NAME})"
- name: "WAN口入流量"
type: SNMP_AGENT
key: "net.if.in[ifInOctets.2]" # ifIndex=2通常是WAN口
delay: 60s
preprocessing:
- type: CHANGE_PER_SECOND # 转换为速率
- type: MULTIPLIER
params: "8" # bytes→bits
- name: "WAN口出流量"
type: SNMP_AGENT
key: "net.if.out[ifOutOctets.2]"
delay: 60s
preprocessing:
- type: CHANGE_PER_SECOND
- type: MULTIPLIER
params: "8"
- name: "WAN口带宽利用率"
type: CALCULATED
key: "net.if.util"
formula: "last(//net.if.in[ifInOctets.2]) / {$IF_BANDWIDTH} * 100"
# 宏 {$IF_BANDWIDTH} 设为门店签约带宽(如100Mbps=100000000)
triggers:
- expression: "avg(/Template_Store_Router/net.if.util,10m)>70"
severity: WARNING
name: "门店带宽利用率>70%({HOST.NAME})"
- expression: "avg(/Template_Store_Router/net.if.util,10m)>90"
severity: HIGH
name: "门店带宽利用率>90%({HOST.NAME})"
3.2 POS终端监控(Zabbix Agent)
POS终端装Zabbix Agent,监控核心进程存活和网络延迟:
ini
# POS终端 Zabbix Agent配置
# /etc/zabbix/zabbix_agentd.conf
Server=10.0.2.1 # 指向本区域Proxy内网地址
ServerActive=10.0.2.1
Hostname=store-sh-001-pos-01 # 命名规则:store-{区域}-{门店编号}-{设备类型}-{序号}
# 自定义监控项:POS收银软件进程
UserParameter=pos.process.alive,pgrep -c "pos_cashier" 2>/dev/null || echo 0
# 自定义监控项:到网关的延迟
UserParameter=pos.gateway.latency,ping -c 3 -W 2 $(ip route | grep default | awk '{print $3}') 2>/dev/null | tail -1 | awk -F'/' '{print $5}' || echo 9999
四、门店自动发现与批量注册
236家门店不可能一台一台手动添加。用Zabbix的网络发现(Network Discovery)自动纳管:
4.1 按子网段自动发现门店路由器
bash
# Zabbix Server Web界面配置:
# Configuration → Discovery → Create discovery rule
#
# Name: 华东区域门店路由器发现
# Discovery by proxy: proxy-east-china
# IP range: 10.10.1.1-10.10.60.1 (每家门店路由器网关固定为x.x.x.1)
# Update interval: 1h
# Checks:
# - SNMP v2, OID: sysName (1.3.6.1.2.1.1.5.0), Community: {$SNMP_COMMUNITY}
# - ICMP ping
# 发现后自动执行的Actions:
# Configuration → Actions → Discovery actions → Create
# Conditions: Discovery status = Up, Service type = SNMP
# Operations:
# - Add host
# - Add to host group: "华东区域门店路由器"
# - Link template: Template_Store_Router
# - Set host monitored by proxy: proxy-east-china
4.2 批量注册脚本(适合初始部署阶段)
如果门店信息已经在Excel/CSV里,用API批量注册更快:
python
#!/usr/bin/env python3
"""
batch_register_stores.py
批量注册门店设备到Zabbix(通过API)
"""
import csv
import requests
import json
ZABBIX_URL = "http://zabbix-server/api_jsonrpc.php"
API_TOKEN = "YOUR_API_TOKEN" # 通过user.login获取
# 区域→Proxy映射
REGION_PROXY_MAP = {
"华东": "proxy-east-china",
"华南": "proxy-south-china",
"华北": "proxy-north-china",
"西南": "proxy-southwest-china",
"华中": "proxy-central-china",
}
# 设备类型→模板映射
DEVICE_TEMPLATE_MAP = {
"router": "Template_Store_Router",
"switch": "Template_Store_Switch",
"pos": "Template_Store_POS",
"server": "Template_Store_CashServer",
}
def zabbix_api(method, params):
payload = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"auth": API_TOKEN,
"id": 1,
}
resp = requests.post(ZABBIX_URL, json=payload)
result = resp.json()
if "error" in result:
print(f"API Error: {result['error']}")
return None
return result.get("result")
def get_proxy_id(proxy_name):
result = zabbix_api("proxy.get", {"filter": {"host": proxy_name}})
return result[0]["proxyid"] if result else None
def get_template_id(template_name):
result = zabbix_api("template.get", {"filter": {"host": template_name}})
return result[0]["templateid"] if result else None
def get_or_create_hostgroup(group_name):
result = zabbix_api("hostgroup.get", {"filter": {"name": group_name}})
if result:
return result[0]["groupid"]
result = zabbix_api("hostgroup.create", {"name": group_name})
return result["groupids"][0]
def register_host(store_info, device_type, device_ip):
"""注册单台设备"""
region = store_info["region"]
store_id = store_info["store_id"]
store_name = store_info["store_name"]
hostname = f"store-{store_id}-{device_type}"
visible_name = f"{store_name}-{device_type}"
group_name = f"{region}区域门店{device_type}"
proxy_name = REGION_PROXY_MAP.get(region)
template_name = DEVICE_TEMPLATE_MAP.get(device_type)
proxy_id = get_proxy_id(proxy_name)
template_id = get_template_id(template_name)
group_id = get_or_create_hostgroup(group_name)
params = {
"host": hostname,
"name": visible_name,
"interfaces": [{
"type": 2 if device_type in ["router", "switch"] else 1, # 2=SNMP, 1=Agent
"main": 1,
"useip": 1,
"ip": device_ip,
"dns": "",
"port": "161" if device_type in ["router", "switch"] else "10050",
}],
"groups": [{"groupid": group_id}],
"templates": [{"templateid": template_id}],
"proxy_hostid": proxy_id,
"macros": [
{"macro": "{$STORE_ID}", "value": store_id},
{"macro": "{$STORE_NAME}", "value": store_name},
{"macro": "{$REGION}", "value": region},
],
}
result = zabbix_api("host.create", params)
if result:
print(f"✓ 注册成功: {visible_name} ({device_ip})")
else:
print(f"✗ 注册失败: {visible_name}")
def main():
"""从CSV读取门店信息,批量注册"""
# CSV格式:store_id,store_name,region,router_ip,switch_ip,pos_ip_1,server_ip
with open("stores.csv", "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
store_info = {
"store_id": row["store_id"],
"store_name": row["store_name"],
"region": row["region"],
}
# 注册路由器
if row.get("router_ip"):
register_host(store_info, "router", row["router_ip"])
# 注册交换机
if row.get("switch_ip"):
register_host(store_info, "switch", row["switch_ip"])
# 注册POS
if row.get("pos_ip_1"):
register_host(store_info, "pos", row["pos_ip_1"])
# 注册收银服务器
if row.get("server_ip"):
register_host(store_info, "server", row["server_ip"])
if __name__ == "__main__":
main()
CSV模板:
csv
store_id,store_name,region,router_ip,switch_ip,pos_ip_1,server_ip
sh-001,上海南京路店,华东,10.10.1.1,10.10.1.2,10.10.1.101,10.10.1.200
sh-002,上海徐汇店,华东,10.10.2.1,10.10.2.2,10.10.2.101,10.10.2.200
gz-001,广州天河店,华南,10.10.61.1,10.10.61.2,10.10.61.101,10.10.61.200
五、按区域分组告警路由
1000+设备的告警全发到一个群里等于没发。必须按区域分组路由:
5.1 Zabbix告警Action配置思路
告警触发
→ 判断主机所属Host Group
→ "华东区域门店*" → 发送给华东运维企微群 + 华东值班人
→ "华南区域门店*" → 发送给华南运维企微群 + 华南值班人
→ ...
→ 同时判断告警级别
→ HIGH/DISASTER → 额外通知区域主管 + 总部NOC
5.2 企微Webhook按区域分发配置
python
#!/usr/bin/env python3
"""
alert_router.py
接收Zabbix告警Webhook,按门店区域路由到不同企微群
"""
from flask import Flask, request, jsonify
import requests
import re
app = Flask(__name__)
# 区域→企微群Webhook映射
REGION_WEBHOOK_MAP = {
"华东": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=KEY_EAST",
"华南": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=KEY_SOUTH",
"华北": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=KEY_NORTH",
"西南": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=KEY_SW",
"华中": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=KEY_CENTRAL",
}
# 总部NOC群(接收所有HIGH及以上告警)
NOC_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=KEY_NOC"
# 从主机名解析区域(命名规则:store-{区域缩写}-{编号}-{设备})
REGION_PREFIX_MAP = {
"sh": "华东", "hz": "华东", "nj": "华东",
"sz": "华南", "gz": "华南", "xm": "华南",
"bj": "华北", "tj": "华北", "qd": "华北",
"cd": "西南", "cq": "西南", "km": "西南",
"wh": "华中", "cs": "华中", "zz": "华中",
}
def detect_region(hostname):
"""从主机名中提取区域"""
# hostname格式:store-sh-001-router
match = re.match(r"store-([a-z]+)-", hostname)
if match:
prefix = match.group(1)
return REGION_PREFIX_MAP.get(prefix, "未知")
return "未知"
def send_wechat(webhook_url, message):
"""发送企微消息"""
payload = {
"msgtype": "markdown",
"markdown": {"content": message}
}
requests.post(webhook_url, json=payload, timeout=5)
@app.route('/zabbix/alert', methods=['POST'])
def handle_alert():
data = request.json
hostname = data.get("hostname", "")
alert_name = data.get("alert_name", "")
severity = data.get("severity", "")
message = data.get("message", "")
store_macro = data.get("store_name", "") # 从Zabbix宏传入
region = detect_region(hostname)
webhook_url = REGION_WEBHOOK_MAP.get(region)
# 构建消息
alert_msg = (
f"**🔔 门店告警**\n"
f"> 门店:{store_macro}\n"
f"> 设备:{hostname}\n"
f"> 告警:{alert_name}\n"
f"> 级别:{severity}\n"
f"> 详情:{message}\n"
)
# 发送到区域群
if webhook_url:
send_wechat(webhook_url, alert_msg)
# HIGH及以上同时发NOC
if severity in ["High", "Disaster"]:
send_wechat(NOC_WEBHOOK, f"**⚠️ 高优告警升级**\n{alert_msg}")
return jsonify({"status": "ok"}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8091)
5.3 Zabbix Webhook Media Type配置
在Zabbix的Media Type里配置HTTP Webhook,指向上面的路由服务:
javascript
// Zabbix Webhook脚本(Administration → Media types → Create)
// Type: Webhook
// Parameters: hostname, alert_name, severity, message, store_name
var params = JSON.parse(value);
var req = new HttpRequest();
req.addHeader('Content-Type: application/json');
var payload = JSON.stringify({
hostname: params.hostname,
alert_name: params.alert_name,
severity: params.severity,
message: params.message,
store_name: params.store_name
});
var resp = req.post('http://localhost:8091/zabbix/alert', payload);
if (req.getStatus() !== 200) {
throw 'Failed to send alert: ' + resp;
}
return 'OK';
六、落地过程中的4个坑
6.1 门店宽带动态IP,Proxy地址变了Server连不上
问题:部分门店用的是拨号宽带,公网IP每天变,Proxy用Active模式没问题(Proxy主动连Server),但如果有人配成了Passive模式就会断。
解法 :所有Proxy统一用Active模式(ProxyMode=0),Proxy主动向Server发起连接。Server侧不需要知道Proxy的IP,只需要Proxy的Hostname匹配即可。
6.2 门店路由器SNMP不通
问题 :部分门店路由器出厂默认SNMP是关闭的,或者Community String不是默认的public。
解法:门店开业部署时有一个标准化Checklist,其中一项是开启SNMP并设置统一的Community String。对于已经营业的门店,写一个远程配置脚本:
bash
#!/bin/bash
# enable_snmp_huawei.sh - 远程开启华为路由器SNMP(需SSH连通)
# 用法: ./enable_snmp_huawei.sh <router_ip> <ssh_user> <ssh_pass>
ROUTER_IP=$1
SSH_USER=${2:-"admin"}
COMMUNITY="store_monitor_2026" # 统一Community
sshpass -p "$3" ssh -o StrictHostKeyChecking=no ${SSH_USER}@${ROUTER_IP} << 'EOF'
system-view
snmp-agent
snmp-agent sys-info version v2c
snmp-agent community read ${COMMUNITY}
snmp-agent trap enable
quit
save
y
EOF
echo "SNMP已开启: ${ROUTER_IP}, Community: ${COMMUNITY}"
6.3 告警风暴:区域网络波动导致几十家门店同时告警
问题:运营商区域性网络抖动,华东60家门店的路由器同时触发"WAN口不可达"告警,企微群瞬间刷屏60条。
解法:在告警路由层做收敛------5分钟内同一区域超过10台设备触发相同告警,合并为一条"区域性故障"通知。我们实际项目里用的是冠服云EMS平台做告警收敛和路由,EMS内置了时间窗口收敛规则(同Host Group + 同告警类型 + 5分钟窗口),把60条告警归并成1条"华东区域疑似运营商故障,影响门店60家",NOC一眼就能判断是运营商问题还是自有设备问题。
6.4 新开门店上线慢,每次都要手动配一堆东西
解法:标准化门店IT部署Checklist + 自动注册脚本。新门店网络通了之后,只需要在CSV里加一行然后跑一次批量注册脚本,5分钟内设备就进监控了。模板和告警规则全自动继承,不需要任何手动配置。
七、完整落地Checklist
bash
# ===== 第一步:Proxy部署(每个区域执行一次) =====
# 安装Zabbix Proxy
apt install zabbix-proxy-sqlite3 -y # Ubuntu
# 编辑配置文件
vim /etc/zabbix/zabbix_proxy.conf # 参考第二节配置
systemctl enable --now zabbix-proxy
# 在Server Web界面注册Proxy
# ===== 第二步:模板准备 =====
# 导入门店设备模板(路由器/交换机/POS/收银服务器)
# Configuration → Templates → Import
# 模板文件提前准备好,包含监控项+触发器+图形
# ===== 第三步:门店信息录入 =====
# 整理门店CSV(store_id, store_name, region, 各设备IP)
cat stores.csv | wc -l # 确认门店数量
# ===== 第四步:批量注册 =====
python3 batch_register_stores.py
# 验证注册结果
# Monitoring → Hosts → 筛选对应Host Group
# ===== 第五步:告警路由服务部署 =====
pip install flask requests
python3 alert_router.py &
# 配置Zabbix Webhook Media Type指向路由服务
# ===== 第六步:验证 =====
# 手动触发一台门店设备的告警(如临时调低阈值)
# 确认对应区域企微群收到消息
# 确认HIGH告警同时通知NOC群
# 验证完成后恢复阈值
# ===== 第七步:新门店上线SOP =====
# 1. 门店网络通 → 2. 设备SNMP/Agent配置 → 3. CSV加一行 → 4. 跑注册脚本 → 5. 确认监控数据正常
觉得有用的话点个赞/收藏,连锁门店IT的监控方案一旦跑通,后面新开门店就是流水线作业了,边际成本几乎为零。
站内参考链接
CSDN-41(多源告警统一接入):https://blog.csdn.net/weixin_70758133/article/details/161214549?spm=1011.2415.3001.5331
CSDN-46(告警升级策略):https://blog.csdn.net/weixin_70758133/article/details/161418720?spm=1011.2415.3001.5331