☰
CMDB资产数据失准?从零搭建自动化资产扫描最小闭环
2026/10/15 5:37:07 网站建设 项目流程

1. CMDB资产为什么总和现实对不上

接手CMDB维护工作的第一周,我就被现实狠狠教育了一顿。线上排查一个业务故障,打开配置管理数据库查目标机器的归属、用途、网络区域,记录里写着两年前的信息,IP、主机名、硬件配置跟实际情况差了十万八千里。后来花了大半天一台一台对账,才发现漏洞百出:有机器早被回收了记录还没删,有机器加过内存没人更新,还有一批虚拟机根本没人知道是谁开的。

这不是个例,而是几乎所有靠手工维护资产台账的团队都会遇到的共性问题。CMDB的核心价值在于"配置项数据准确可用",但如果数据的录入、更新、删除全都依赖人工,那这套系统大概率会在规模上来之后迅速失真。自动化资产扫描,就是用来解决这个失真的关键手段。

1.1 手工登记模式逃不掉的三个失控点

早期团队机器少,用Excel表或者简单的记录页面维护资产完全够用。机器三五十台的时候,每个人心里都有本账,偶尔改一下记录不会有太大问题。但规模一上来,手工模式会从三个方向同时失控:

  • 录入动作本身依赖人:新开机器要登记、下线老机器要标记、变更配置要更新,每一件事都依赖某个"记得做这件事的人"。运维一旦忙起来,漏登记是常态,补登记靠缘分。
  • 缺少变更回写机制:物理服务器加了内存、虚拟机伸缩了规格、业务程序换了监听端口,这些资源层面的变化通常不会自动回流到CMDB。底层变了,台账纹丝不动。
  • 信息口径不统一:有人填IP,有人填主机名,有人把用途写在备注里,字段格式全靠个人习惯。到了后期想按某个维度做统计,数据根本聚不到一起。

这三个失控点带来的直接后果,就是CMDB的数据只能"仅供参考"。而自动化资产扫描要解决的,恰恰就是这三件事:用程序替代人工录入,定时自动刷新,用统一的字段结构强制口径一致。

1.2 自动化扫描的核心价值不止是"自动录入"

很多刚接触这个方向的人会有一个误区,觉得资产扫描就是"把机器信息自动填到系统里"。实际做下来,自动化扫描的价值是分层的:

第一层,替代人工录入。采集程序在每台主机上运行,自动读取操作系统版本、硬件配置、IP地址、主机名、运行状态,上报到服务端。这一步解决的是"没人记得去登记"的问题。

第二层,持续校验与变更感知。每次扫描结果都会和CMDB中已有的历史记录做比对,发现内存变了、IP网段换了、主机名改了,自动生成变更记录并触发通知。这一步解决的是"数据悄悄过期"的问题,也是资产扫描最有价值的部分。

第三层,与工单和流程联动。资产数据可以作为资源申请、容量规划、安全基线扫描的基础上下文。比如开虚拟机时自动带上所属资源池的容量信息,续费时自动关联到期时间。这一层属于流程业务范畴,要在前两层跑稳之后再考虑。

本系列的内容,会重点围绕第一层和第二层展开,先把采集、上报、入库、比对这条主干链路打通。第三层等主干稳定后,可以单独再聊。

1.3 这个系列的整体规划

按照我自己的实践路径,这个系列的安排大致如下:

  • 第1篇(本篇):介绍整体思路,完成环境搭建,跑通最小闭环数据链路。
  • 第2篇:Agent采集器的完整实现,覆盖系统信息、硬件信息、网络信息、业务信息四类数据。
  • 第3篇:服务端API与数据校验,处理重复上报、脏数据清洗、字段归一化。
  • 第4篇:数据入库与CMDB分层建模,加入变更比对和告警通知。
  • 第5篇:部署落地、守护进程托管、批量下发与整体优化。

有一点想提前说明:资产规模不大的团队,完全可以从一个简单可用的扫描方案开始,先让数据准起来,再逐步增加建模和流程联动。不要一上来就想着做一个功能齐全的庞大平台,很容易在漫长开发周期里耗尽耐心,最后连最小闭环都没跑通。

2. 整体方案推演与技术选型:先想清楚再动手

动手写代码之前,我建议先花半天时间把方案推演一遍,把每个环节的选型定下来。资产扫描工具一旦跑起来就是常驻系统,后期改动架构的成本往往比重新写还大。这里把我当时的推演过程和最终选择记录下来,供参考。

2.1 三层结构:Agent采集、上报服务、数据存储

这套方案的架构非常经典,分成三层:

第一层是运行在被扫描主机上的采集Agent。它负责读取本机信息,包括操作系统版本、内核版本、CPU型号与物理核数、内存容量、磁盘分区与使用率、网卡MAC与IP、主机名、硬件序列号,以及关键业务的监听端口。Agent用Python编写,后续会打包分发到各主机上运行。

第二层是部署在服务端的上报API。它接收Agent提交的JSON数据,做基础校验(Agent版本、主机唯一ID、采集时间),然后交给数据层处理。这里选择了FastAPI框架,看重三点:一是异步性能足够,内部工具一天跑一次全量上报,并发压力不大;二是自带自动生成的接口文档,调试接口非常方便;三是基于Pydantic的模型校验可以直接复用,省去手写参数检查的麻烦。

第三层是数据存储。起步阶段使用SQLite,后续数据规模上来再平滑迁移到MySQL。这么选的原因很现实:搭建前期可能只有几台测试机在跑,直接上MySQL会引入服务安装、账号权限、远程访问配置等一系列前置成本,容易把注意力从核心逻辑上拉走。SQLite零配置、单文件、随拿随用,对验证方案来说是最合适的起点。

2.2 为什么选择Agent主动上报,而不是服务端主动拉取

这是方案推演时被问得最多的一个问题:既然可以远程SSH到各机器上执行命令收集信息,为什么还要每台机器装一个Agent?

三个原因比较关键:

  • 网络可达性:部分主机分布在不同网段或安全域内,服务端不一定能主动连过去。Agent主动上报只需要服务端开放一个HTTP端口,网络策略的开放难度小得多。
  • 采集深度与安全审计:Agent可以直接读取本机运行时的状态,包括进程列表、端口监听、系统日志、硬件DMI信息。通过SSH远程获取这些数据,往往需要额外配置命令白名单和提权策略,安全审计上更麻烦。
  • 后续扩展能力:Agent天然适合插件化扩展。以后想在主机本地做安全基线检查、采集业务日志、执行健康巡检,都可以在Agent框架上直接加,不需要推倒重来。

Agent模式也有它的代价:每台主机都需要安装维护一个常驻进程。但对于资产扫描这个目标来说,覆盖所有主机本来就是刚需,这点运维成本是可以接受的。

2.3 技术栈清单和版本约束

环境搭建前,先给出完整的技术栈清单。后面所有步骤都围绕这个清单展开,避免边写边试导致的混乱。

组件选型建议版本用途说明
操作系统Linux发行版(CentOS系或Ubuntu系)x86_64兼容性最稳,云主机和物理机都适用
PythonCPython3.10+部分新库已放弃对低版本的兼容
Web框架FastAPI0.110+异步性能好,自带接口文档
ASGI服务Uvicorn0.29+FastAPI的常用运行载体
采集库psutil5.9+跨平台系统信息采集核心库
硬件信息lshw / dmidecode系统自带通过命令行获取序列号、主板信息
ORMSQLAlchemy2.x统一数据访问,后续方便切换数据库
数据库SQLite(起步)/ MySQL(后期)3.x / 8.0先轻量起步,规模上来再迁移
权限管理sudoers系统自带给Agent免密执行指定命令

版本这块多说一句:生产环境最怕"新版库换了旧接口"。依赖文件里要锁定精确版本,至少锁到次版本号,不要用完全开放的latest。否则今天装能跑,三个月后到另一台机器装就跑不起来了。

3. 环境搭建全流程:从空机到最小数据链路

环境搭建是整个系列最容易被低估的环节。很多人觉得"装个Python、跑通个Demo"很简单,但真正在生产环境复现时会卡在各种细节上。下面按实际执行顺序完整走一遍。

3.1 准备干净的基础环境

建议准备两台机器:一台作为服务端,一台作为被采集端。如果资源紧张,也可以在同一台机器上同时启动服务端和Agent,但至少要用虚拟机隔离来验证链路是否真的走通了。

服务端机器建议不低于2核2G内存,磁盘20G以上。被采集端用普通虚拟机就可以,专门用来模拟真实主机。给CentOS系或Ubuntu系都行,但操作系统版本不要追求太新,用主流稳定版本即可。原因是新版本的内核或目录结构可能影响dmidecode等工具的默认路径,会增加不必要的排障成本。

基础环境准备按下面四步走:

  1. 安装操作系统时选择最小化安装,不带无关组件,最大限度减少干扰因素。
  2. 配置静态IP,或者确保DHCP分配的IP能长期稳定。Agent上报的IP字段需要真实可用,不能每次重启都变。
  3. 规划防火墙策略。服务端放通API端口,示例里用8000;Agent所在网络要能访问该端口。
  4. 创建专用系统用户。服务端用普通用户运行Web服务,不要用root;Agent采集硬件信息时通过sudoers白名单授权,而不是直接给root权限。

这里特别提醒一下:如果在云环境,除了操作系统防火墙,还要检查安全组的放通规则。如果服务端和被采集端跨网段,路由也要确认可达。环境搭建阶段最常见的失败原因就是"代码啥都没错,单纯网络不通"。

3.2 Python虚拟环境与依赖安装

操作系统的自带Python版本往往不能满足要求,推荐先手动安装Python 3.10,再用venv创建项目虚拟环境。以CentOS系为例,命令如下:

bash

安装编译依赖(注意sqlite-devel不能漏)

yum install -y gcc openssl-devel bzip2-devel libffi-devel sqlite-devel

下载Python源码并编译安装

wget https://www.python.org/ftp/python/3.10.12/Python-3.10.12.tgz tar -xf Python-3.10.12.tgz cd Python-3.10.12 ./configure --enable-optimizations make -j4 && make install

创建项目虚拟环境

python3.10 -m venv /opt/cmdb-scan/venv source /opt/cmdb-scan/venv/bin/activate

编译安装Python时如果缺少sqlite-devel,后面操作SQLite会报"No module named '_sqlite3'"。这个坑我踩过一次,排查半天才发现是编译依赖没装全。

虚拟环境激活后,安装核心依赖:

bash pip install --upgrade pip pip install fastapi==0.110.0 pip install uvicorn==0.29.0 pip install sqlalchemy==2.0.23 pip install psutil==5.9.8 pip install requests==2.31.0 pip install pydantic==2.6.0

装完后把依赖固化到文件,方便后续在其他机器上复现环境:

bash pip freeze > /opt/cmdb-scan/requirements.txt

提示:如果服务器访问外网较慢,可以在pip命令后追加"-i"参数指定镜像源。但锁定的版本号不要变,不同镜像源只是下载位置不同,不影响依赖本身。

3.3 数据库初始化与表结构设计

SQLite起步时不需要单独安装数据库服务,SQLAlchemy首次运行时会自动创建数据库文件。但表结构要提前想清楚,因为它直接决定资产模型的长相。

本阶段先设计两张核心表。第一张是主机资产表,字段包括:

  • host_uuid:主机唯一ID,由Agent根据系统特征生成,保证同一台机器多次上报能对应同一条记录。
  • hostname、os_name、os_version、kernel_version。
  • cpu_model、cpu_cores、mem_total、disk_total。
  • ip_address、mac_address、netmask、gateway。
  • 硬件序列号、主板厂商与型号。
  • 采集时间、上报Agent版本。

第二张是扫描记录表,每次上报生成一条新记录,记录采集状态和原始数据摘要,为后续变更比对留素材:

  • id、host_uuid、scan_time、status、raw_json。

表结构设计的原则是"先满足去重和变更识别,再考虑业务扩展"。不要在第一步就堆很多部门、项目、负责人字段,那些应该在资产建模阶段再补。数据库的schema一开始太复杂,后面每一轮迭代都要跟着改,会很痛苦。

3.4 项目目录组织与配置管理

项目管理遵循"服务端与Agent分离、配置与代码分离"的原则。推荐目录结构如下:

text /opt/cmdb-scan/ ├── server/ │ ├── app.py │ ├── models.py │ ├── config.py │ └── requirements.txt ├── agent/ │ ├── collector.py │ ├── sender.py │ └── agent_config.py ├── tests/ └── README.md

每个文件的职责要单一:服务端app.py负责HTTP接口,models.py负责数据库模型,config.py负责读取环境变量和配置文件;Agent的collector.py负责采集数据,sender.py负责上报。不要把所有逻辑都塞进一个文件,后面排障的时候,你会庆幸目录是拆开的。

配置管理方面,推荐用"环境变量+默认配置文件"结合的方式。服务端端口、数据库路径、Agent上报地址、上报间隔这些参数从代码里抽出来,放到配置文件里。换环境部署时只改配置不动代码,这是内部工具长期维护的基本要求。

4. 最小闭环验证:让第一份资产数据落库

环境搭建完成后,先不要急着写完整采集逻辑。先做一个最小闭环:Agent能采到几项关键数据,能通过HTTP上报到服务端,服务端能把数据写入SQLite,并能通过查询接口把数据读回来。这个链路一旦跑通,后面所有功能都是在它之上叠加。

4.1 先写一个足够简单的采集脚本

先用psutil写一个最小的采集脚本,验证基础环境可用。目标只采集五类信息:系统版本、主机名、CPU、内存、IP。

python import psutil import platform import socket import json import uuid

def get_base_info(): return { "host_uuid": uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname()).hex, "hostname": socket.gethostname(), "os_name": platform.system(), "os_version": platform.version(), "cpu_model": platform.processor(), "cpu_cores": psutil.cpu_count(logical=False), "mem_total": psutil.virtual_memory().total, "ip_address": socket.gethostbyname(socket.gethostname()) }

ifname== "main": print(json.dumps(get_base_info(), ensure_ascii=False, indent=2))

说明两点简化设计:host_uuid暂时用主机名生成UUID,只保证同一主机在主机名不变时ID一致;真实的唯一ID方案会在第2篇讲解,需要结合硬件序列号与网卡MAC生成,以保证主机名变化时ID依然稳定。IP字段这里直接用gethostbyname,真实场景下还需要处理多网卡和虚拟网卡的问题,后面采集篇会细化。

4.2 服务端写一个最小上报接口

服务端同样从最小实现开始。一个FastAPI应用,接收POST请求,把JSON数据写入SQLite表,再提供一个GET接口用于查询记录条数:

python from fastapi import FastAPI, Request from sqlalchemy import create_engine, Column, String, Integer, Float from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base import datetime

app = FastAPI() engine = create_engine("sqlite:///cmdb.db", connect_args={"check_same_thread": False}) Base = declarative_base()

class Asset(Base):tablename= "assets" id = Column(Integer, primary_key=True, autoincrement=True) host_uuid = Column(String(64), index=True) hostname = Column(String(128)) os_name = Column(String(64)) cpu_cores = Column(Integer) mem_total = Column(Float) ip_address = Column(String(64)) create_time = Column(String(64), default=str(datetime.datetime.now()))

Base.metadata.create_all(engine) Session = sessionmaker(bind=engine)

@app.post("/api/v1/report") async def report(req: Request): data = await req.json() session = Session() asset = Asset(**{k: data.get(k) for k in ["host_uuid", "hostname", "os_name", "cpu_cores", "mem_total", "ip_address"]}) session.add(asset) session.commit() session.close() return {"code": 0, "message": "ok"}

@app.get("/api/v1/count") async def count(): session = Session() total = session.query(Asset).count() session.close() return {"total": total}

这段代码故意写得朴素,字段写死、没有模型校验和异常处理。因为环境搭建阶段的目标是验证链路通不通,不是展示最佳实践。后面功能开发阶段会用Pydantic补上数据校验,用异常处理器统一返回错误格式。

4.3 冒烟测试执行顺序与预期结果

服务端启动和Agent执行建议按下面的顺序操作:

  1. 启动服务端:

bash cd /opt/cmdb-scan/server source ../venv/bin/activate uvicorn app:app --host 0.0.0.0 --port 8000

  1. 确认服务正常:浏览器访问 http://服务端IP:8000/docs,看到FastAPI自动生成的接口文档,说明服务端起来了。

  2. 执行采集脚本,本机先验证:

bash cd /opt/cmdb-scan/agent source ../venv/bin/activate python collector.py

输出JSON结构正常后,把它POST到服务端:

bash curl -X POST http://127.0.0.1:8000/api/v1/report
-H "Content-Type: application/json"
-d @result.json

  1. 验证落库结果:浏览器访问 http://服务端IP:8000/api/v1/count,返回total变成1,说明数据已经写入SQLite。

冒烟测试的预期结果可以对照这张表检查:

操作预期结果说明
启动uvicorn控制台无报错8000端口监听正常
访问 /docs显示接口文档FastAPI自动生成
执行collector.py输出合法JSON采集环境正常
POST /api/v1/report返回code=0数据写入成功
GET /api/v1/counttotal>=1数据可读回

这个最小闭环跑通后,环境搭建阶段就宣告完成。接下来第2篇在这个基础上扩展Agent采集逻辑,逐步覆盖硬件、网络、业务信息。

5. 环境阶段的高频坑与排查思路

这一部分把我实际搭建过程中遇到、以及预判读者可能遇到的高频问题整理出来。环境搭建阶段的坑大多集中在依赖、权限、网络、数据一致性这四个方面,提前知道能省不少时间。

5.1 依赖版本冲突:pydantic、FastAPI与Python版本

FastAPI对Pydantic的版本兼容性比较敏感。Pydantic的2.x和1.x在导入方式、模型校验API上有明显差异。如果你安装的是最新版Pydantic,而代码还按1.x风格写Optional字段和validator装饰器,启动时很容易报类型错误。

排查思路:当启动uvicorn出现类似"ImportError: cannot import name 'ModelField' from 'pydantic'"的错误,基本可以判定是版本错位。解决办法是把Pydantic锁定到和FastAPI版本配套的版本,而不是盲目追最新。最稳妥的就是直接采用前文列出的固定版本组合,不要用宽松范围。这一步看似简单,却能省下大量排障时间。

5.2 dmidecode权限与sudoers配置

采集硬件序列号、主板信息时,dmidecode命令需要root权限。如果Agent用普通用户运行,必须通过sudo方式调用。但直接sudo会卡在密码输入,自动化场景完全不可行。

正确做法是在sudoers里给Agent用户配置免密白名单:

bash

使用visudo编辑,添加如下行

cmdbuser ALL=(ALL) NOPASSWD: /usr/sbin/dmidecode

这里要强调,白名单尽量精确到具体命令和参数,不要写成"NOPASSWD: ALL"。否则Agent一旦被入侵,等于把整台主机的root权限都送出去了。另外,不同发行版dmidecode的路径可能不同,采集前先执行which dmidecode确认路径,再把实际路径写进配置。

5.3 虚拟网卡和多余IP带来的资产混乱

一台机器上往往存在多个网卡接口,除了物理网卡还有docker0、virbr0、vethxxx等虚拟网卡。如果不加过滤,Agent会采集到一堆虚拟IP。上报到服务端后,同一台主机在IP字段上出现多个值,后续按IP查资产时容易混淆。

解决方案是在采集网络信息时做两层处理:第一层,用psutil.net_if_addrs()拿到全部网卡信息后,按网卡名过滤掉以docker、br-、veth、virbr、lo开头的虚拟接口;第二层,优先选择默认路由对应的主IP,通过读取/proc/net/route或者调用ip route命令来确定。这个逻辑在采集篇会进一步细化,但环境验证阶段就建议在采集脚本里埋下过滤框架,否则后面数据一多再改就要洗一遍历史数据。

5.4 SQLite并发写锁与生产环境迁移

SQLite在并发写入场景下表现一般,两个进程同时写同一个文件时,会遇到database is locked错误。起步阶段数据量小不明显,但一旦Agent数量增多,集中上报会让服务端在某个时间点同时处理大量写请求,锁冲突就会暴露。

两个应对思路:一个是在Agent端做随机延迟,把上报时间打散。比如配置允许每台Agent采集完成后等待一个0到300秒的随机值再上报,减轻服务端瞬时压力;另一个是当数据量达到几百台以上时迁移到MySQL。SQLAlchemy这层数据访问抽象,让切换数据库时只需要改连接字符串,业务代码可以保持不变。

5.5 时间戳、时区与幂等判断

采集时间戳是后续变更比对的基准,但环境搭建初期很容易忽视时区问题。Agent所在机器和服务端机器如果时区不一致,或者没有统一以UTC存储,会导致"什么时候采集的"这个信息彻底失真。

处理建议:Agent在采集时间字段中使用UTC时间戳,比如time.time(),服务端展示层再按需转换为本地时区。同时服务端按host_uuid加采集时间去重,防止同一台机器因为网络重试等原因,产生多条看起来合理的重复记录。这个问题虽然不会让环境阶段立刻爆雷,但等到做变更比对时再回头修时区,成本会高得多,所以提前列进来。

我个人的体会是,环境搭建阶段折磨人的往往不是技术本身,而是各种底层细节的叠加:依赖库版本差一个次版本、宿主机时区没统一、防火墙漏放一个端口,哪怕只有一项出问题,第一个Demo就可能迟迟跑不起来。所以本篇写得很啰嗦,目的就是给后续系列省时间。最小闭环一旦通了,后面加采集字段、加接口、加变更比对都是顺水推舟的事。下一篇我会重点展开Agent采集器的完整实现,尤其是硬件信息获取和主机唯一ID方案,这两块是资产扫描里最容易踩坑、也最影响数据质量的核心环节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询