连锁门店IT运维监控实战:200+门店网络设备+POS统一纳管+按区域分组告警路由完整配置(Zabbix Proxy架构)

管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?

三个原因:

  1. 断网缓存:门店宽带断了,Proxy在本地缓存监控数据,网络恢复后自动上传,不丢数据
  2. 减轻Server压力:1000+设备全直连Server,Server的poller进程会被打满。Proxy分担采集压力
  3. 网络穿透简单:只需要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

相关推荐
56AI1 小时前
360 智语 AI 企业智能体平台深度评测:从 L4 蜂群架构到政企落地实战
人工智能·架构
换个昵称都难1 小时前
webrtc 音频模块FEC模块
网络·音视频·webrtc
youngerwang2 小时前
【从搬运工到协处理器:网卡芯片架构、算法、验证与边缘演进深度剖析】
网络·算法·架构·芯片
老毛肚2 小时前
JeecgBoot 后端架构与技术栈全景导读 01
架构
@insist1233 小时前
系统架构设计师-操作系统进程管理核心知识点详解
架构·系统架构·软考·系统架构设计师·软件水平考试
云计算磊哥@3 小时前
运维开发宝典026-MySQL02数据库表操作
运维·数据库·运维开发
天天进步20154 小时前
Tunnelto 源码解析 #9:控制服务器设计:Warp、WebSocket、Ping/Pong 与连接保活
运维·服务器·websocket
●VON4 小时前
AtomGit Flutter鸿蒙客户端:用户资料
flutter·华为·架构·跨平台·harmonyos·鸿蒙
SL-staff4 小时前
Web 白板技术架构深度解析:从渲染到协作的选型哲学
前端·架构
前端冒菜师4 小时前
别急着做 Agent,AI 工程化的第一步是 Skill 化
架构·ai编程