全栈之路3---zmagent 开发实现

前情提要

上一篇我们解决了架构设计的实时性、安全性、可扩展性问题,定义了 ProtocolHeader 协议头和两条通信链路。这一篇我们把镜头对准 zmagent——整个系统中最贴近资源、最复杂的终端。

不同于 HTTP 的短连接,client 采用 TCP 长连接与 server 通信,优势在于方便命令下发。client 首先需要完成注册、认证,然后持续心跳保活链路,再就是任务执行与结果返回。

任务执行分两种:一种为实时命令,一种为定时任务,都需要放到单独的线程中执行,完成后统一通过通信链路返回 server。

从功能上看,client 分三种,底层协议一样,但分工不同:

  • agent:保持与 server 的长连接,定时采集 cpu/mem/disk/process 信息

  • bussiness:短连接,主要执行上传文件、提交入库命令,为扩展性服务

  • admin:主要用于 web 层对 agent 的管理命令下发


一、系统组成

 

命令字方向作用
0xA101Agent → Server主机注册
0xAf0fAgent → Server心跳保活
0xA301Server → Agent执行脚本命令
0xA303Agent → Server执行结果上报
0xA304Agent → Server定时上报(JSON)
0xA402Admin → Server管理命令下发

协议层不做业务解析,只负责拆包、校验同步头(非 0x1234 直接断连)、按 datalen 读取载荷。


二、线程模型:各司其职

agent 是多线程协作的常驻进程,避免单线程阻塞导致命令响应延迟:

线程职责触发条件
main初始化、注册、守护启动时执行,断线后触发重连
heartbeat保活每 30 秒发送 0xAf0f
recvThread网络收包、协议解析阻塞 recv,区分即时命令与配置更新
execThread即时命令执行cmdQueue + condition_variable 驱动
scheduler定时任务轮询每 60 秒扫描一次任务表

两条任务通道互不干扰:

  • 即时命令:recvThread 收到 0xA301 → 入 cmdQueue → notify → execThread 消费

  • 定时任务:scheduler 扫描本地 g_tasks → 到期即执行 → 0xA304 JSON 上报


三、启动与注册

启动流程是严格顺序的:

  1. 读配置basecomm.conf 中拿 serveriphostid

  2. 建连:阻塞 connect 到 server:6783,成功后切非阻塞

  3. 注册:若 conf 中无 hostid,发送 0xA101 注册包。此时 socket 临时切回阻塞模式,5 秒超时等待 Server 分配 ID,写入配置后后续复用

  4. 起线程:recv、heartbeat、exec 开始工作

  5. 守护:main 线程每秒 isConnected(),断线即进入重连

cpp

// 重连逻辑(简化)
void reconnect() {
    disconnect();
    sleep_for(5s);
    if (init("basecomm.conf")) {
        registerHost();  // 重新注册
    }
}

四、即时命令执行:从收包到上报

Server 下发 0xA301 后,agent 侧完整处理链:

cpp

// 1. recvThread 解析入队
ServerCmd cmd;
cmd.cmd_id = packet.cmd_id;
cmd.script = packet.script_name;  // 如 "cpu_check.sh"
cmd.params = packet.params;       // 如 "-i 5"
{
    lock_guard<mutex> lock(cmdMutex);
    cmdQueue.push(cmd);
}
cmdCond.notify_one();

// 2. execThread 消费执行
void execThread() {
    while (running) {
        unique_lock<mutex> lock(cmdMutex);
        cmdCond.wait(lock, []{ return !cmdQueue.empty(); });
        auto cmd = cmdQueue.front(); cmdQueue.pop();
        lock.unlock();
        
        // 3. 安全校验
        if (cmd.script.find("..") != string::npos || 
            cmd.script.find('/') != string::npos) {
            reportResult(cmd.cmd_id, -1, "非法脚本路径");
            continue;
        }
        
        // 4. 执行(白名单目录)
        string fullCmd = "/opt/zmagent/scripts/" + cmd.script 
                       + " " + sanitizeParams(cmd.params);
        FILE* fp = popen(fullCmd.c_str(), "r");
        char buf[1024];
        string output;
        while (fgets(buf, sizeof(buf), fp)) {
            output += buf;
            if (output.size() > 900) break;  // 截断防撑爆
        }
        int code = pclose(fp);
        
        // 5. 上报结果 0xA303
        reportResult(cmd.cmd_id, code, output);
    }
}

安全校验三条铁律:

  • 脚本必须在 /opt/zmagent/scripts/ 下,禁止 ../

  • 参数字符白名单:[a-zA-Z0-9_.-],其余直接丢弃

  • 输出截断 900 字节,防止单条结果包过大


五、定时任务与上报

定时任务不走队列,由 scheduler 线程独立驱动:

cpp

// scheduler 线程(60 秒周期)
while (running) {
    sleep(60);
    for (auto& task : g_tasks) {
        if (task.isDue()) {
            string json = executeAndFormatJson(task);  // 采集 cpu/mem/...
            sendTimedReport(json);  // datatype=0xA304
        }
    }
}

0xA304 帧复用 STRUCT_REMOTE_TASK_PACKET 结构,但实际只发有效 JSON 长度,不填充固定 512 字节,节省带宽:

cpp

pkt.header.datatype = 0xA304;
pkt.header.datalen = json.size();
memcpy(pkt.taskParams, json.data(), json.size());
send(socket, &pkt.header, sizeof(header) + json.size() + 1, 0);

Server 收到后,直接通过 Unix Socket 转发给 zm_db_service.py 入库。


六、Server 侧如何配合

agent 不是孤立的,它的很多行为依赖 Server 的配合:

1. HostID 持久化 Server 维护 host_ids.txt,按 IP 分配 ID。agent 首次注册拿到 ID 后写回本地 conf,重连时直接携带,避免重复分配。

2. 命令暂存与转发 Admin 通过 0xA402 下发的命令,若目标 agent 当时未就绪,Server 将其压入该 hostid 对应的 pendingCmds。agent 连接的心跳间隙,Server 检查并下发 0xA301

3. 结果入库 agent 上报的 0xA303/0xA304,Server 提取 JSON 后,通过 /tmp/zm_python_service.sock 转给 Python DB Service,异步写入 PostgreSQL,避免 C++ 层直接碰数据

七、附检查脚本样例

check_disk.sh*
check_docker_container.sh*
check_process.sh*
check_resource.sh*

check_disk.sh 
#!/bin/bash

mode="${1:-/}"

# 排除的虚拟文件系统类型(不区分大小写)
EXCLUDE_TYPES="tmpfs|devtmpfs|proc|sysfs|cgroup|cgroup2|pstore|configfs|debugfs|tracefs|fusectl|fuse|overlay|squashfs|ramfs|hugetlbfs|mqueue|securityfs|bpf"

# ==================== 单路径模式(兼容旧版) ====================
if [ "$mode" != "all_disk" ]; then
    target_dir="$mode"
    
    if [ -z "$target_dir" ]; then
        echo '{"error":"缺少目录参数"}'
        exit 1
    fi
    
    if [ ! -d "$target_dir" ]; then
        echo '{"error":"目录不存在"}'
        exit 1
    fi
    
    df_output=$(df -P "$target_dir" 2>/dev/null)
    if [ $? -ne 0 ] || [ -z "$df_output" ]; then
        echo '{"error":"获取磁盘信息失败"}'
        exit 1
    fi
    
    # 提取并转换为 JSON
    echo "$df_output" | awk 'NR==2 {
        total_mb = int($2 / 1024);
        used_mb = int($3 / 1024);
        avail_mb = int($4 / 1024);
        gsub(/%/, "", $5);
        printf "{\"filesystem\":\"%s\",\"type\":\"unknown\",\"size_mb\":%d,\"used_mb\":%d,\"avail_mb\":%d,\"use_percent\":%s,\"mounted_on\":\"%s\"}", 
               $1, total_mb, used_mb, avail_mb, $5, $6
    }'
    exit 0
fi

# ==================== all_disk 模式 ====================

# 使用 timeout 5秒防止 NFS 挂起阻塞
df_output=$(timeout 5 df -PT 2>/dev/null)

if [ $? -ne 0 ] || [ -z "$df_output" ]; then
    echo '[]'
    exit 0
fi

# 用 awk 直接生成 JSON 数组,过滤虚拟文件系统
echo "$df_output" | awk -v exclude="$EXCLUDE_TYPES" '
BEGIN {
    n = split(exclude, arr, "|");
    for (i = 1; i <= n; i++) skip[tolower(arr[i])] = 1;
    printf "[";
    first = 1;
}
NR > 1 {
    type = tolower($2);
    if (type in skip) next;
    
    total_mb = int($3 / 1024);
    used_mb = int($4 / 1024);
    avail_mb = int($5 / 1024);
    gsub(/%/, "", $6);
    
    if (!first) printf ",";
    first = 0;
    
    printf "{\"filesystem\":\"%s\",\"type\":\"%s\",\"size_mb\":%d,\"used_mb\":%d,\"avail_mb\":%d,\"use_percent\":%s,\"mounted_on\":\"%s\"}", 
           $1, $2, total_mb, used_mb, avail_mb, $6, $7;
}
END {
    printf "]";
}


八、小结

zmagent 的核心就这几件事:

  1. 长连接保活:注册 → 心跳 → 断线重连

  2. 双任务通道:即时命令走队列 + 条件变量;定时任务走轮询

  3. 安全执行:路径白名单、参数过滤、输出截断

  4. 零业务耦合:采集逻辑交给 shell 脚本,agent 只负责"收-转-发"

代码量不大,但多线程的边界、阻塞调用的锁范围、重连时的状态清理,全是细节。下一篇我们把镜头转向 server 端——它要同时面对上千个 agent 的连接、Admin 的指令、Python DB Service 的转发,是怎么扛住的。


 

下篇预告:《全栈之路4---zmserver 开发实现》,看 C++ Server 如何处理并发连接与命令调度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值