简介:这是一套面向运维工程师的服务器巡检工具包,基于 Python 与 Paramiko 实现 SSH 远程连接,支持多线程并发向多台服务器批量下发巡检命令,并能自动汇总结果,生成 Excel 报表与 Pyecharts 可视化图表,有效解决日常巡检中重复操作多、效率低的问题。压缩包共 10 个文件,容量约 40KB,内含 py 主脚本、cmd 巡检命令示例、xls 示例报表、log 运行日志、host/info 主机配置模板,以及 README、说明文件和附赠 docx 文档,结构清晰,便于按用途取用与学习。工具还提供简单界面,用户可配置巡检任务、运行参数和服务器列表,也附有主机清单模板和使用说明,初中级运维人员按文档即可快速部署。已有 147 人浏览学习。对于需要批量执行远程命令、自动生成运维报表的团队,以及想参考 Paramiko、多线程和 Pyecharts 结合思路的 Python 学习者,这套小工具都很具参考价值。
1. 项目整体设计与思路拆解
1.1 为什么选Paramiko而不是其他方案
做运维的人都有这种经历:月底要巡检几十台服务器,手动一台台SSH登录上去敲命令,运气好两个小时搞定,运气不好遇到几台密码过期、主机名对不上号的,一上午就搭进去了。我之前试过用Shell脚本批量搞,但密码分发和结果收集太痛苦,也试过直接上Ansible,反馈很直观,但对于只需要“跑几条命令然后把结果整理成表格”这个轻量场景来说,Ansible的学习成本和环境要求有点重了。后来换成Python加Paramiko,思路一下就顺了。
Paramiko是Python生态里最成熟的SSH协议客户端库,底层走的是SSH2协议,和你的服务器是否装了Agent完全无关。这意味着只要目标机器开放22端口、有账号密码或者密钥,你就能直接连上去执行命令。实际测试下来,由Paramiko拉起远程命令的延迟在几十毫秒级别,特别适合巡检这种“短命令、多机器、重收集”的场景——每条命令执行时间短,大量耗时其实在网络往返和命令排队上。项目中引入Paramiko还有一个关键优势:不需要在被管机器上部署任何Agent。对于跨网络区域的服务器巡检,这一点很实用,省去了Agent分发和权限审批的麻烦。
选择多线程并发而不是顺序执行,原因也很直观:如果一台一台依次跑命令,20台机器每台需要5秒,一轮巡检下来就是100秒,加上逐个连接、等待返回的耗时,最终可能要10到15分钟。这个时间看似不长,但一旦机器规模增长到50台甚至100台,耗时会线性增加到十几分钟,巡检窗口根本打不住。而用多线程把连接和命令执行并发起来,5到8个线程同时推进,20台机器的巡检时间可以压到20到40秒。这个项目里我用的是concurrent.futures.ThreadPoolExecutor来做线程池管理,而不是自己手工创建线程——原因是线程池能统一处理任务提交、结果获取、超时控制,以及最重要的“并发数限制”。
1.2 整体功能架构
这个巡检工具的完整流程可以概括成四个环节:
- 读取服务器清单,支持IP、端口、用户名、密码(或密钥路径)、分组、巡检命令等字段;
- 通过线程池并发执行
paramiko.SSHClient远程连接,批量执行预定义命令脚本; - 收集每条命令在每台机器上的标准输出、执行状态和耗时,统一封装成结构化数据;
- 用
openpyxl生成Excel巡检报表,用pyecharts生成可视化图表,并自动归档到按日期命名的报表目录。
把架构想清楚之后再动手写代码,会发现这个工具本质上只做两件事:并发执行远程命令,把结果整理成报表。但把这两件事做扎实,需要处理很多细节。比如:一个线程在执行命令时,另一个线程的连接超时了怎么处理?一台机器命令执行出错是直接终止整个巡检,还是记录错误继续跑后面的机器?生成的Excel报表是否需要按分组分Sheet?这些看起来不是核心逻辑的细节,恰恰是决定工具能否真正落地使用的关键。
我在实际开发中把服务器清单做成了Excel,用openpyxl读取。因为现网运维工程师对Excel的熟悉程度远高于JSON或YAML,用Excel当配置文件的优势是任何人拿到模板都能直接改,不需要打开文本编辑器去和括号逗号做斗争。每个Sheet代表一个分组(Web组、DB组、缓存组等),每一行是一台服务器,列分别是主机名、IP、SSH端口、认证方式、巡检命令列表。这样的配置方式在交付给别人用的时候,几乎没有学习成本。
2. 核心细节解析与实操要点
2.1 远程连接与命令执行:几个容易踩坑的地方
Paramiko的使用模式非常固定:创建SSHClient对象,设置自动添加策略(AutoAddPolicy),然后connect建立连接,用exec_command执行命令,最后读取stdout和stderr。但这几个基础操作里,隐藏着不少坑。
第一个坑是exec_command的默认行为是非阻塞的。什么意思?就是exec_command返回后,命令不一定已经执行完了。如果你立刻去读stdout,可能只读到空字符串。正确做法是调用stdout.read()或stdout.channel.recv_exit_status()来等待命令执行完成。recv_exit_status()会阻塞直到命令结束,并返回命令的退出码,这比简单调用read()更可靠,因为它能明确告诉你命令到底是成功还是失败。
第二个坑是超时。SSH连接的timeout参数影响的是TCP连接建立阶段的超时时间,但命令执行阶段如果服务器响应很慢,或者有命令卡住了,连接层的超时是管不到的。我最初写完代码测试时,碰到过好几台机器在某条命令上一直卡住,整个线程池的资源全被卡死的连接占用掉。后来在exec_command之后增加了sock.settimeout()的设置,给命令执行阶段也加上了超时控制。实践下来,单条命令的合理超时上限是30秒,超过这个时间基本可以判定命令异常或主机负载过高。
第三个坑是命令退出码。很多人判断命令是否成功只看stdout里有没有内容,这是在给自己埋雷。有的命令出错时也会往stdout里输出错误信息,有的命令执行成功但stdout为空。正确的判断逻辑是:先读recv_exit_status(),再根据退出码决定取stdout还是stderr。非零退出码表示命令执行失败,这时应该记录stderr内容;只有退出码为零时才记录stdout并解析正常输出。这个顺序搞反了,报表里就会出现大量误导信息。
2.2 多线程并发:不要一股脑全开
很多人一听说要“多线程并发”,第一反应是开几十个线程把所有机器一次性并发跑起来。这种思路在小规模场景下问题不大,但一旦机器数量超过20台,问题就来了:服务器侧的MaxStartups默认配置会限制并发SSH连接数,超过限制后新连接会被直接丢弃;被管服务器如果配置了MaxSessions限制,也会拒绝过多的会话。还有更常见的,就是你本机的文件描述符数量限制,线程一多,连接一多,程序直接报Too many open files崩掉。
所以并发数的控制思路应该是:线程数不是越多越好,而是够用就好。我实测的情况是,对于巡检这种轻量命令场景,并发数取5到8比较合适。低于5个,50台机器的巡检耗时在60到80秒左右;高于10个,耗时的下降幅度不明显,但报错的概率明显上升。20台机器用5个线程跑,基本可以把一轮远程命令执行的总耗时控制在25秒以内。这个并发数的选择可以根据被管机器的硬件配置做微调,如果服务器都是固态硬盘加高配CPU,可以适当调到10;如果是老旧的虚拟机环境,建议还是保守一点。
线程池用ThreadPoolExecutor还有一个隐藏的好处:配合as_completed()可以支持“结果完成一个就收集一个”的流式处理模式。这意味着不用等所有机器都跑完才开始生成报表,而是可以边执行边填充结果列表。对于长时间巡检场景,这种模式能让你实时看到哪些机器已经跑完了,哪些机器还在执行中。我在项目里加了一个简单的进度打印功能,每收集完一台机器的结果就打印一条包含主机名和耗时的日志,方便在命令行窗口直观监控整个巡检任务的推进状态。
2.3 配置文件与动态命令扩展
这个巡检工具的另外一个设计思路,是把“巡检项目”和“执行逻辑”解耦。也就是说,每台机器跑哪些命令,不是写死在代码里的,而是根据服务器分组动态决定的。比如Web组默认跑uptime、df -h、free -m、netstat -tlnp这几条,DB组额外增加mysqladmin status或redis-cli info memory之类的专用命令。在Excel配置里加一列“命令标识”,代码里维护一个命令标识到实际命令的映射表,这样新增一种巡检项时,不用改动代码逻辑,只需要在映射表里加一行。
这里有一个细节值得补充:命令脚本应该做成一个可配置的列表,而不是拼接成一个超长字符串用&&连起来。这样做的好处有三个。第一,单条命令失败不影响其他命令的执行,方便单独判断每条命令的状态;第二,命令执行耗时的统计粒度更细,报表里能看到是哪条命令慢;第三,后续如果想对某个特定命令做定向重试,按列表索引操作会非常方便。
3. 实操过程与核心环节实现
3.1 环境准备与依赖安装
先把环境搭起来。Python版本推荐3.8及以上,我用的是3.10,整个项目只依赖三个第三方库:paramiko、openpyxl、pyecharts。安装命令如下:
pip install paramiko openpyxl pyecharts如果你有多个Python环境,建议用pip install前先确认一下当前命令行环境下python --version输出的是不是你要用的那个版本。之前有人踩过坑,pip装到了系统Python上,代码却又在虚拟环境里跑,结果一直报ModuleNotFoundError。
项目目录结构上,我习惯这样组织:
inspection_tool/ ├── main.py # 主程序入口 ├── config/ │ └── servers.xlsx # 服务器清单Excel ├── core/ │ ├── ssh_executor.py # 远程命令执行模块 │ ├── report_generator.py # 报表生成模块 │ └── models.py # 数据模型定义 ├── output/ # 巡检结果输出目录 │ └── 20240615/ │ ├── report.xlsx │ └── charts.html └── requirements.txt3.2 远程命令执行的核心实现
ssh_executor.py里最核心的类大概是这样的逻辑:
import paramiko from concurrent.futures import ThreadPoolExecutor, as_completed class SSHExecutor: def __init__(self, max_workers=5, timeout=30): self.max_workers = max_workers self.timeout = timeout self.results = [] def _run_single(self, server, commands): """在单台服务器上执行命令列表,返回结构化结果""" host, port, user, pwd = server result = { "hostname": host, "ip": server["ip"], "group": server["group"], "status": "success", "error": "", "exec_time": 0, "cmd_details": [] } try: client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect( hostname=server["ip"], port=port, username=user, password=pwd, timeout=10, banner_timeout=10, auth_timeout=10 ) for cmd in commands: start = time.time() stdin, stdout, stderr = client.exec_command(cmd, timeout=self.timeout) exit_code = stdout.channel.recv_exit_status() output = stdout.read().decode("utf-8", errors="replace").strip() err_output = stderr.read().decode("utf-8", errors="replace").strip() result["cmd_details"].append({ "command": cmd, "exit_code": exit_code, "output": output if exit_code == 0 else err_output, "cost_sec": round(time.time() - start, 2) }) client.close() result["exec_time"] = sum(item["cost_sec"] for item in result["cmd_details"]) except Exception as e: result["status"] = "failed" result["error"] = str(e) return result def run_batch(self, servers, commands_by_group): """并发执行多台服务器的巡检""" tasks = [] with ThreadPoolExecutor(max_workers=self.max_workers) as executor: for server in servers: cmds = commands_by_group.get(server["group"], []) tasks.append(executor.submit(self._run_single, server, cmds)) for future in as_completed(tasks): res = future.result() self.results.append(res) return self.results有几个细节需要注意。banner_timeout和auth_timeout是我后来补上的——默认情况下Paramiko只有timeout这一个超时参数,但它覆盖不了某些网络环境下SSH握手阶段卡住的情况。加上这两个超时之后,连接异常时能更快抛错,线程不会长时间空转。
另外,注意解码时用了errors="replace"。为什么?因为远程机器上有些命令会输出GBK编码或包含非UTF-8字节的内容,直接按UTF-8解码会抛UnicodeDecodeError,整个线程就崩了。用errors="replace"可以让异常字节被替换成占位符,保证程序能继续往下跑。等到报表阶段,再去根据实际情况处理编码问题。
3.3 报表生成的实现:Excel与可视化图表
报表模块我想多花点篇幅说说,因为这是整个工具最直观的输出部分,也是用户感知最强的一部分。
Excel部分用的是openpyxl。我的设计是:工作簿里第一个Sheet放“汇总总览”,展示整体巡检结果,包括总机器数、成功数、失败数、平均耗时等指标;后续每个分组一个Sheet,详细列出每台机器每条命令的执行结果。这样做的好处是,汇报时给领导看总览Sheet就够了,自己排查问题时再翻详细Sheet。
单元格颜色我用了一套简单的规则:退出码为0且输出正常的记录,字体是黑色、填充是白色;非零退出码的记录,填充浅红色;连接失败的主机,整行标成橙色。另外,列宽设置很关键,如果执行结果超过160个字符,我会做截断处理,在Excel里加一个批注保存完整内容,否则表格会被长文本撑得没法看。
from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment def generate_excel_report(all_results, output_path): wb = Workbook() summary_ws = wb.active summary_ws.title = "巡检总览" # 统计指标 total = len(all_results) success = sum(1 for r in all_results if r["status"] == "success") failed = total - success # 写入总览数据并保存 wb.save(output_path)Pyecharts部分。这个库生成的是HTML可视化网页,可以直接在浏览器打开。我做两个核心图表:一个是“各分组巡检耗时对比”的柱状图,一个是“命令执行异常数量Top10”的横向条形图。前者能帮你看清哪个分组的服务器整体性能差,后者能暴露哪些命令高频失败、需要重点排查。
from pyecharts.charts import Bar from pyecharts import options as opts bar = Bar() bar.add_xaxis(group_names) bar.add_yaxis("平均耗时(秒)", avg_times) bar.set_global_opts(title_opts=opts.TitleOpts(title="分组巡检耗时对比")) bar.render("output/charts_group_cost.html")有个使用上的心得:Pyecharts生成的HTML默认是独立的,直接把图表和Excel报表放在同一个日期目录里即可,不需要塞进Excel里。一份详尽的Excel、一份可视化的HTML网页,两者是互补的——Excel适合数据整理和存档,HTML适合快速浏览趋势和定位异常。
4. 常见问题与排查技巧实录
4.1 问题速查表
这个问题速查表里的内容,基本都是从我自己实际跑这个工具的过程中踩坑踩出来的,逐条对应最常见的故障现象和处理方式。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 大批量机器报连接超时 | 服务器侧MaxStartups限制并发连接数 | 减少max_workers,从8降到5;检查目标服务器/etc/ssh/sshd_config |
| 个别机器一直卡住不返回 | 命令执行阶段无超时控制 | 给exec_command加timeout参数;检查是否有命令等待交互输入 |
| 输出内容乱码 | 远程输出编码和本地解码编码不一致 | 解码时指定正确的编码集,统一用errors="replace"兜底 |
| 报表里命令退出码非零但内容看起来正常 | 命令将警告信息输出到了stderr | 不要只根据stdout判读,必须以退出码为准;必要时合并stdout和stderr后统一展示 |
| 调度中心或本地报Too many open files | 线程数过多,文件描述符耗尽 | 增加本机ulimit上限,或降低并发数到合理范围 |
| Excel打开后显示“文件已损坏” | openpyxl保存对象未正确关闭 | 确保wb.close()被调用,或改用with上下文管理器 |
4.2 排查实战:一次巡检告警抖动
说一个我印象很深的实战案例。有一次跑巡检工具,报表里显示有3台机器报警,提示命令执行超时。单看现象很容易以为是网络问题或服务器负载过高,但仔细翻详细Sheet发现,超时的命令全都是同一个——df -h。这就很奇怪了,df -h理论上执行只要几十毫秒,为什么会在多台机器上同时超时?
后来排查发现,这3台机器挂载了一个NFS共享目录,而NFS服务端那台机器当时的网络状态异常,导致df -h在收集文件系统信息时阻塞在NFS挂载点上。在命令行手动SSH登录上去执行df -h,同样会卡住几十秒。这个案例说明了两个点:第一,巡检命令执行超时未必是SSH问题,也可能是命令本身在服务器上依赖了外部资源;第二,报表里把“单条命令耗时”单独列出来非常有用,它能帮你快速定位是哪条命令异常,而不是只看到整台机器巡检失败的结果。
4.3 进一步优化:敏感信息与故障重试
工具跑到后期,有两个进一步增强的点可以分享。
第一是敏感信息的处理。Excel里保存明文密码不是好习惯,虽然内网环境下暂时可用,但一旦配置表泄露,风险很大。一个简单的改进是改用密钥认证:把私钥路径配置到Excel里,代码里用paramiko.RSAKey.from_private_key_file()加载密钥连接,服务器上只需把公钥放到authorized_keys里即可。这样既支持批量部署,又不用在文件里保存明文口令。
第二是故障自动重试。巡检目标机器偶尔会有瞬时抖动,第一次连接超时不代表机器不可用。我给run_batch增加了一个简单的重试逻辑:连接或执行失败一次后,等待1到2秒再重试一次;重试仍然失败才标记为故障,并在报表中标注“重试后仍失败”的信息。这个小小的改进,把巡检的误报率降了一个量级,尤其是对网络状况不太稳定的混合云环境和跨机房场景,效果非常明显。
最后再分享一点我在实际使用中的体会:这种工具不要追求大而全,能每天稳定跑、报表能让不同角色的人看懂,比功能堆砌更重要。后续可以考虑扩展的方向有:把巡检结果推送到企业微信或钉钉机器人的Webhook、配合定时任务(Cron)做到每天自动巡检、把历史报表归档后做一个简易的指标趋势分析。每一步的扩展,都是在现有代码结构上增加模块,不需要推翻重来。
本文还有配套的精品资源,点击获取