Python自动化运维脚本实战:从磁盘巡检到批量远程执行 深夜两点磁盘IO告警把我从睡梦中拉起来我手动登服务器、看日志、清缓存、重启服务折腾了四十分钟而同样的流程在这周已经出现了第三次。那一周我做了个决定把每天重复敲的那些命令写成Python自动化运维脚本。现在这台服务器的巡检、告警、日志检查全部由脚本完成我再也没有凌晨爬起来过。这篇内容我想写给两类人第一类是刚接触运维、还在手动敲命令的同行第二类是写了一些脚本但总觉得“能跑但不好用”的开发。我会从入门库的选择讲起再用三个实际脚本案例拆解编写思路最后把我这些年踩过的坑一并整理出来。不管你是想把巡检自动化还是想批量操作几十台服务器这篇都应该能帮到你。1. 先明确要解决什么问题自动化运维脚本的定位与边界1.1 值得写成脚本的永远是重复三次以上的事我个人判断一个操作要不要写脚本标准非常简单这件事我是不是已经重复做过三次了。如果只是偶尔登一次服务器查个进程不值得写脚本但如果你是每天早上都要登录五台服务器检查磁盘、看日志、确认服务状态那这件事就非常适合自动化。运维脚本本质上做的是两件事把“命令的重复执行”变成“程序的稳定执行”。手动敲命令的弊端很明显——容易漏、容易错、容易慢。比如你检查磁盘时习惯用df -h检查内存用free -m检查进程用ps aux | grep每一次都要登服务器、敲命令、看输出、做判断。脚本能把这些动作串起来输出一份结构化的结果甚至直接在指标异常时发出告警这才是自动化的价值。1.2 自动化运维脚本的三个层次我在不同阶段写过不同类型的脚本大致可以分成三个层次这也是我建议新手循序渐进的路线。第一层是单机巡检脚本跑在一台服务器上采集系统信息、检查磁盘和内存、分析日志。这类脚本通常由crontab定时触发结果写入文件或者直接输出。它解决的是“我不用每天手动登机器看状态”的问题。第二层是批量操作脚本通过SSH协议同时连接多台机器执行命令或者分发文件。这类脚本需要关注并发数控制、连接超时、错误重试和结果汇总技术含量比单机脚本高一个台阶。第三层是联动脚本脚本不光是执行命令还要和外部系统交互——调用接口获取数据、把运维结果写入数据库、触发告警通知、拉起或停止服务。这一层脚本已经能承担一部分基础运维平台的能力本质上是把重复性判断交给程序完成。热词里有很多人搜“ansible自动化运维”这是配置管理工具的范畴它确实比纯Python脚本更擅长批量配置和状态管理。但我的观点是如果只是几十台机器、几个固定操作纯Python脚本反而更轻、更可控。不需要引入额外的agent、不用学一套新的DSL维护成本低得多。等规模真正上来了再考虑上Ansible也不迟。1.3 有什么场景不应该写脚本写脚本前也要先学会判断边界不是所有操作都适合脚本化。我个人的经验是三类场景要谨慎一是需要人为确认的高危操作比如批量删库、清空日志、修改生产环境权限脚本一条命令下去没有任何确认机制出了问题你连止损都来不及。如果非要自动化至少要在脚本里加交互确认或者二次校验。二是已经非常成熟的工具能解决的问题比如全链路配置下发、服务编排、容器调度这类已经有ansible、k8s等成熟方案就不要硬用Python重新造轮子纯粹给自己增加维护负担。三是一次性的临时操作比如临时查一个端口连接数、手动导一份数据直接用命令行处理更快没必要为了“仪式感”写脚本。脚本是投资所有投资都要算回报不是所有自动化都值得做。2. 入门库到底怎么选标准库优先第三方库按需引入2.1 标准库是第一选择别一上来就装一堆包经常有人问做运维脚本应该学哪些库。我的答案和很多人不一样先学标准库。os、sys、subprocess、logging、argparse、smtplib、datetime这批标准库能覆盖日常运维脚本大约60%的需求。os负责路径拼接、文件遍历、获取环境变量比如遍历一个目录下的日志文件os.listdir和os.path.join组合起来非常顺手。subprocess是脚本里最核心的标准库它负责调用外部命令。运维脚本经常要执行一个shell命令然后拿到输出subprocess.run()就能完成。很多人有一个误区觉得标准库功能太弱不够用。实际上标准库的优点是零依赖、跨平台、不容易出现版本冲突。生产环境的机器往往不能随便装第三方包标准库是你最稳妥的底牌。我见过不少环境连外网都没有第三方库根本装不上这时候标准库就是唯一选择。用完标准库发现不够再去看第三方库也不迟。第三方库解决的是“通用命令行难办”的问题不是“查个磁盘信息”也要引依赖的问题。2.2 第三方库的经典组合按需引入不搞全家桶我常用的第三方库其实非常少每个库都对应一类明确的问题。远程执行命令用paramiko。这是自动化的主力库基于SSH协议封装了连接、认证、执行命令、上传下载文件的能力。网络设备批量下发配置、多台服务器批量执行它都能胜任。paramiko用起来有点像“用代码模拟SSH登录”所以学它之前最好先在命令行里亲手SSH连过机器知道一次会话是什么流程再对照代码就很容易理解。系统信息采集用psutil。这个库能拿到CPU、内存、磁盘、网络、进程的详细数据跨平台。写巡检脚本时我几乎不解析df和free命令的输出了直接用psutil.disk_usage(/)拿到的就是结构化的数据对象字段清晰还省去正则匹配的工作。HTTP接口调用用requests。运维脚本经常要调用内部系统的接口比如把告警信息推到企业微信、钉钉或者自建的告警平台requests几行代码就能完成。交互式命令处理用pexpect。如果服务器上有那种需要交互应答的脚本比如输入yes确认、输入密码pexpect可以模拟交互过程自动给出回应适合处理部分老旧系统的自动化场景。我把这些库的定位整理成了一张表方便对照使用场景场景推荐库为什么是它调用系统命令subprocess标准库零依赖、执行稳定、可直接拿返回码远程SSH执行paramiko成熟稳定支持大量主机复用连接系统指标采集psutil结构化数据免去解析命令输出HTTP接口调用requests简单直观还自带会话保持交互式命令行pexpect能模拟交互应答处理老旧系统必备日志输出logging标准库分级、写文件、轮转功能齐全命令行参数解析argparse标准库写一个带参数脚本的必备工具这个清单很克制但足够应对九成以上的运维场景。我一直建议新手不要照着一篇推荐文章把一堆库全部装上引入一个第三方库意味着引入一份依赖和维护成本按需引入才是健康的做法。2.3 库装不上怎么办环境管理与依赖隔离热词里关于“python安装numpy库”“python安装sklearn库”的搜索量很大可见“装库”本身就是一个大坑。我强烈建议所有运维脚本项目都建虚拟环境不要直接往系统Python里塞包。虚拟环境用venv就能创建不需要额外安装工具。进入项目目录后执行python -m venv venv然后在Linux下用source venv/bin/activate激活。激活后pip install装的包都只存在于这个项目目录内不会污染系统环境。安装库遇到问题的排查顺序也可以分享一下第一步看pip版本老版本pip在处理新版包时经常报错建议定期升级python -m pip install --upgrade pip第二步看是不是网络问题国内环境建议配置镜像源速度快很多第三步看是不是编译问题像numpy、scipy这类C扩展库优先安装官方wheel包不要轻易在Windows上用源码编译。依赖锁定的习惯我也建议早一点养成。项目里放一个requirements.txt把用到的第三方库和版本号写清楚换机器、拉新环境的时候一条pip install -r requirements.txt就能恢复全部环境。版本号一定要锁别用numpy1.20这种写法等两个月后新版本变了API脚本就悄悄坏了排查起来非常浪费时间。3. 实用指南三个最常见脚本的完整写法拆解3.1 服务器磁盘巡检脚本用psutil拿数据自定义阈值告警第一个脚本我选了磁盘巡检因为这是运维日常最频繁的操作逻辑简单但完整覆盖了“采集-判断-通知”三个阶段。先看用内置命令手动检查是什么体验登服务器敲df -h看哪个分区用了多少判断是否超过80%再决定要不要清理。这个过程的问题在于df输出的是给人看的表格脚本要处理它就必须做字符串解析既脆弱又啰嗦。换成psutil之后完全不一样。psutil.disk_partitions()能列出所有磁盘分区psutil.disk_usage(partition.mountpoint)能拿到每个分区的总量、已用、可用和利用率全是结构化的数值字段。核心逻辑变得非常干净import psutil def check_disk_usage(threshold80): alerts [] for part in psutil.disk_partitions(): try: usage psutil.disk_usage(part.mountpoint) except PermissionError: continue percent usage.percent if percent threshold: alerts.append(f[{part.mountpoint}] 使用率 {percent:.1f}%, 剩余 {usage.free / 1024**3:.1f} GB) return alerts if __name__ __main__: alerts check_disk_usage() if alerts: print(\n.join(alerts)) else: print(所有分区正常)这个脚本值得留意的点有两个。第一个是PermissionError的处理有些挂载点在当前用户下没有访问权限不处理这里脚本会直接崩溃我一开始就吃过这个亏。第二个是阈值参数化threshold80作为参数而不是写死在代码里这样以后想改成90就不用改逻辑了。我在实际用的时候还会加上邮件或Webhook通知逻辑就是alerts不为空的时候调用通知函数。运维脚本有一个隐含要求正常情况下尽量安静异常时一定要能“闹”起来。有异常却不通知的脚本比没有脚本更危险因为它会给你一种“一切正常”的错觉。3.2 批量远程执行脚本paramiko并发注意超时和重试第二个脚本是批量远程执行这也是很多人真正需要自动化的场景——几十台服务器一个操作反复登几十遍太痛苦了。来看一个简洁的版本给定主机列表和命令并发执行并汇总结果。这里用了paramiko建立SSH连接再用concurrent.futures.ThreadPoolExecutor做并发控制import paramiko from concurrent.futures import ThreadPoolExecutor, as_completed hosts [ {host: 192.168.1.10, port: 22, user: root, password: ******}, {host: 192.168.1.11, port: 22, user: root, password: ******}, ] def run_cmd(host_info, command): client paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: client.connect( host_info[host], porthost_info.get(port, 22), usernamehost_info[user], passwordhost_info[password], timeout10, ) stdin, stdout, stderr client.exec_command(command, timeout15) output stdout.read().decode(utf-8, errorsignore) error stderr.read().decode(utf-8, errorsignore) return {host: host_info[host], output: output, error: error} except Exception as e: return {host: host_info[host], error: str(e)} finally: client.close() if __name__ __main__: command df -h free -m with ThreadPoolExecutor(max_workers10) as executor: futures [executor.submit(run_cmd, h, command) for h in hosts] for future in as_completed(futures): result future.result() print(f {result[host]} ) if result.get(error): print(错误:, result[error]) else: print(result[output])这段代码里我特意加了几处生产环境必备的细节。第一是set_missing_host_key_policy(paramiko.AutoAddPolicy())不加的话连接全新主机时会因为未知主机密钥抛出异常第二是连接和命令执行都设置了timeout服务器卡死时脚本不会无限等下去第三是decode(utf-8, errorsignore)很多系统输出是GBK编码不加这个参数会直接乱码甚至崩溃。并发数控制在10是因为我实际观察下来的经验值。运维操作目标机器往往有共用网络设备和中间链路并发太高容易被限流甚至把交换打挂并发太低又体现不出批量执行的价值。10个连接同时跑大部分场景下都是安全且高效的。如果是几百台机器我会把最大并发控制在20到30之间且增加执行结果落盘记录而不是全部print出来。这个脚本唯一让我觉得需要特别提醒的是密码的安全问题。脚本里写明文密码只是示例生产使用一定要改成从环境变量、密钥文件或密钥管理系统中读取。SSH密钥登录永远好过密码登录用paramiko的时候配置一个key文件路径命令行下能做的事脚本里基本都能做。3.3 日志错误关键词告警脚本定时扫描文件结合正则做统计第三个脚本处理的是日志巡检最典型的场景服务日志里出现了特定错误关键字需要第一时间发现并告警。这类脚本核心有两个环节一是日志文件的读取二是关键字的匹配与统计。最简单可行的设计是记录上一次读取到日志文件的偏移量每次只扫描新增的部分避免每次启动都从头到尾读一次大文件。但在“正常够用”的标准下你也可以简化处理直接读取最近一段时间的日志过滤出错误级别以上的行再统计各类关键字的出现频率。import re from collections import Counter from pathlib import Path LOG_PATH /var/log/application/app.log KEYWORDS [ERROR, Exception, Timeout, Connection refused] def scan_log(log_path, keywords): counter Counter() error_lines [] with open(log_path, r, encodingutf-8, errorsignore) as f: for line in f: for keyword in keywords: if keyword in line: counter[keyword] 1 error_lines.append(line.strip()[:200]) break return counter, error_lines if __name__ __main__: counter, error_lines scan_log(LOG_PATH, KEYWORDS) for keyword, count in counter.items(): print(f{keyword}: {count} 次) if error_lines: print(最新错误示例:) for line in error_lines[:5]: print(line)这段脚本用到了几个值得借鉴的技巧。一是for line in f逐行读取而不是read()一次性全部读入内存。生产环境日志动辄几个GB一次性读入内存基本必挂逐行读取是日志处理的铁律。二是errorsignore很多应用日志包含特殊字符不处理编码问题脚本跑一会儿就会碰上异常中断。三是对错误行做了截断line.strip()[:200]避免告警信息太长人类可读性也更好。使用的正则表达式也需要注意。if keyword in line这种写法最简单但容易误匹配比如你想找“error”但“errorless”里也包含了它。更稳妥的做法是用正则边界匹配比如搜索独立的英文单词时加上\b词边界re.search(r\bERROR\b, line)。正则表达式可以很强大但也要有节制用错了反而误报比漏报更折磨人。实际部署时这个脚本一般配在crontab里每五分钟跑一次。它的判断逻辑非常简单但足够解决大部分监控缺失的问题。等规模大了、日志来源多了自然应该升级到ELK、Loki这类集中式日志平台但在那之前这样一个几十行的脚本完全不输给监控平台的告警效果。4. 实操中的代码打磨从“能跑”到“好用”的关键细节4.1 给脚本加上命令行参数与配置文件初期的脚本里主机列表、阈值、日志路径基本都是写死在自己的源码里。这在小规模场景没问题但脚本一旦要给别人用——哪怕是三个月后的你自己用——写死的参数就会变成大麻烦。换一台机器就得改代码改代码就有可能引入新错误。正确做法是引入两件事用argparse解析命令行参数用配置文件管理变动项。最典型的用法是这样的import argparse def parse_args(): parser argparse.ArgumentParser(description磁盘巡检脚本) parser.add_argument(-t, --threshold, typeint, default80, help磁盘使用率阈值) parser.add_argument(-H, --hosts, nargs, requiredTrue, help目标主机列表) parser.add_argument(--config, defaultconfig.yaml, help配置文件路径) return parser.parse_args() if __name__ __main__: args parse_args() print(args.threshold, args.hosts, args.config)argparse的参数还能关联环境变量关联到CI/CD平台里直接注入效果非常好。比如阈值不用写死每天跑巡检时通过参数传80换不同的业务组时传不同的值。配置文件的格式我喜欢用YAML或JSON。YAML可读性好适合人看人改JSON解析简单、不容易出错。无论用哪种原则是一样的把经常变动的、和环境相关的部分全部移出代码代码只负责读取和执行。4.2 日志与异常处理脚本本身也需要可观测性运维脚本负责保障业务系统的可观测性但如果脚本本身不可观测那就成了一个黑洞——你只知道它每天在跑但不知道它跑得好不好。我给自己的脚本定的最低标准是必须有日志输出。不要用print要用logging。logging支持级别控制、文件输出和格式自定义排查问题的时候体验天差地别。举个例子脚本跑完你看到一行ERROR xxx用print打印的就只能靠猜而logging能带上时间戳、模块名和线程信息几分钟就能定位。import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(name)s: %(message)s, filename/var/log/ops_scripts/disk_check.log ) logger logging.getLogger(disk_check) logger.info(磁盘巡检开始) try: do_check() except Exception as e: logger.exception(巡检过程出现异常: %s, e)日志文件还需要关注增长问题定义一个简单的按天轮转策略就够用。热词里有人搜“powersehell开机自启脚本”提醒一下如果一个计划任务脚本没输出日志、没有异常写出将来出问题排查原始信息时你会非常痛苦。好的标准库日志配置并不复杂几行代码就能获得完整的可观测性但很多人懒得加。我见过太多生产环境里的“神秘脚本”——pm2里挂着一堆脚本机器出问题时谁也不知道它们干过什么。多花几分钟写日志是对将来自己最大的善意。4.3 脚本的执行方式与部署规范写好的脚本要稳定运行还得放到正确的位置、以正确的方式调度。Linux环境下的首选调度工具是crontabWindows则是“任务计划程序”。一个常见的调度写法是*/5 * * * * /usr/bin/python3 /opt/scripts/disk_check.py /var/log/ops_scripts/disk_check.log 21这里要注意python3务必写绝对路径因为crontab执行时的PATH环境变量非常干净可能找不到你常用的python。我见过不止一次手动执行正常crontab一跑就报command not found原因就是PATH不完整。同样的原则也适用于脚本内部凡是调用的外部命令尽量写绝对路径。脚本也需要有稳定的存放目录。我习惯在/opt/scripts/下按项目建子目录每个脚本目录内包含源码、requirements.txt、配置文件和日志目录。这样一台机器上即使有十几个脚本也不会搅在一起。权限问题也需要留心。脚本如果是用root执行要遵守最小权限原则——能用普通用户跑就不要用root。尤其涉及清理文件、重启服务这类有副作用的行为权限最小化能显著降低风险。脚本目录还要控制写权限防止被别人或别的服务恶意篡改。4.4 写脚本最容易踩的代码逻辑坑我最后总结几个代码层面特别容易踩的坑每个都是真实掉进去过才明白的。第一个坑是“命令返回码”不等于“执行成功”。用subprocess调用外部命令时不要只看returncode是否为0。有些命令即使返回0输出里也可能有错误信息。比如用curl检查接口时返回码0但拿回来的可能是404页面脚本要做的是解析输出内容判断真实结果。第二个坑是“SHELLTrue”的注入风险。脚本里如果直接用字符串拼接用户输入去执行命令等于是给命令注入留了口子。比如拼接一个hostname去执行ping如果用户输入的是1.1.1.1; rm -rf /后果不堪设想。参数化传入、严格校验值得拓扑化这个习惯一开始就要养成。第三个坑是Windows和Linux的差异。日期时间格式解析、文件路径分隔符、换行符在两端都不完全一样。写跨平台脚本要用标准库统一约定不要写死/和\也不要有“全世界都跟我的xx服务器一样”的错觉。5. 常见问题排查实录与避坑指南5.1 命令找不到、无法识别八成是环境变量PATH的锅这几年在社区回答了很多“命令无法识别”的问题从“pnpm无法将项识别为cmdlet”到“python不是内部或外部命令”甚至“claude无法将项识别为cmdlet”本质都是同一个问题可执行文件的路径没有加进系统PATH环境变量或者加进PATH时没生效。PATH是什么简单理解就是操作系统查找命令的目录清单。你敲一个命令时系统会从PATH列出的目录里挨个找这个可执行文件找不到就报错。所以排查这个问题的第一步永远是先确认“这个命令的可执行文件到底装到了哪里”然后把它所在目录加进PATH。Windows上查这个事最快的命令是where加命令名Linux上是which加命令名。查到具体位置后再检查PATH配置。Windows下一般在“环境变量”里加路径然后新开一个终端窗口让配置生效Linux下改.bashrc、.zshrc文件用export PATH$PATH:/新路径再加source ~/.bashrc刷新。这些操作本身不难难的是很多新手卡在“我明明装了他就说找不到”这一步。大多数时候问题出在安装后没有重启终端窗口或在PATH里填了错误路径。记住一个原则先验证可执行文件真实存在再验证PATH生效两步走完基本都能解决。5.2 脚本一闪而过、控制台乱码先查编码和日志Windows上双击运行Python写好的.bat或.py脚本窗口一闪就没了这是几乎所有Windows新手都会碰到的问题。原因很简单程序跑完了或者崩溃了控制台窗口就关了你什么都看不到。解决方案也很简单。如果想让窗口停下来在脚本末尾加一段等待逻辑比如input(“按回车键退出”)。但这只是治标真正的问题是脚本可能早就报错退出了。我建议在调试阶段不要双击运行而是在终端里直接执行脚本这样所有输出和报错信息都会留在屏幕上。更规范的做法是给脚本加日志输出让所有信息落到文件里。编码乱码则基本是Python 2遗留下来的世纪老问题。Python 3默认源文件编码是UTF-8但Windows控制台默认可能是GBK打印中文时就可能出现UnicodeEncodeError或乱码。最简单的解决方案是在代码顶部声明# -*- coding: utf-8 -*-同时执行脚本时设置系统环境变量PYTHONIOENCODINGutf-8。文件读写时更是要明确指定编码不要依赖默认值。5.3 依赖装不上、版本冲突用虚拟环境锁版本号“装库失败”是热词里占比很高的一类问题。新手经常遇到的情况是pip install xxx卡住不动、进度条到一半报错、安装成功但一import就报错或者今天能跑明天就报版本冲突。这些问题的根源通常落在三处网络不稳定导致下载失败、编译环境缺失导致源码包装不了、以及没有虚拟环境导致全局依赖相互污染。网络问题在我这边的最优解是配置镜像源不展开多说把命令分享一下pip install -i https://pypi.tuna.tsinghua.edu.cn/simple numpy编译问题则要分平台来看。Linux下编译C扩展库需要gcc、python3-dev这些基础环境Windows下推荐直接安装官方编译好的wheel文件不要用源码构建。版本冲突问题最好的预防手段就是虚拟环境加锁版本。项目目录里建一个requirements.txt写清楚每个库的版本号这样无论谁、在哪台机器上重新安装得到的环境都是一致的。我见过很多线上事故的根源就是“我升级了一个库别的脚本就挂了”有了虚拟环境和锁定版本这类问题基本可以避免。5.4 脚本运行不稳定的隐性问题内存、超时与误杀日志处理类的脚本如果对大文件用了一次性读入的方式去处理内存直接被打满。这个问题在排查时非常隐蔽因为小日志文件上测试一切正常换到生产环境的GB级日志就立刻崩了。解决办法前面已经提过——逐行读取永远优于一次性读入。超时问题方面本地执行命令可能毫秒级返回但远程SSH连接在目标机器负载极高时可能几十秒没反应。不加超时限制的脚本可能在“假死”状态挂几个小时。我给自己的脚本统一的规则是凡是涉及网络请求、远程执行、外部等待的操作设置超时时间并且超时后要做降级处理——记录异常、继续执行、最后汇总报告。误杀问题则常出现在进程管理的脚本里。比如你写了一段脚本按名字找进程并kill掉结果同一台机器上存在多个同名进程脚本一股脑全清掉了包括不该清的业务进程。安全做法是先过滤条件列出将影响的进程人工确认后再执行kill。写脚本是在和机器打交道但每一步操作都应该对人负责。5.5 新手误区速查表把常见问题和解决思路汇总成一张表方便你在卡住的时候快速对照现象可能原因排查与解决双击脚本一闪而过程序已退出或异常崩溃终端里执行查看报错加日志输出python命令找不到PATH未配置用where/ which找到Python路径加入PATHpip安装报错网络、编译环境、版本冲突换镜像源、装wheel、建虚拟环境脚本打印中文乱码编码不一致指定UTF-8设置PYTHONIOENCODING大文件读入内存被打挂一次性读取整个文件改为逐行读取或分块读取远程命令长时间无响应连接超时未设置统一加timeout参数脚本定时任务不生效cron里PATH不完整所有命令和路径写成绝对路径这张表覆盖了我遇到过的九成问题。每次排查都是一个问题、一个原因、一个解决方案定位清楚了基本都能快速解决。在踩过足够多的坑之后我越来越觉得自动化运维脚本本质上不是“写代码”而是“把运维经验代码化”。一个成熟的脚本不只是能跑还要让看代码的人能理解当时的决策逻辑让半年后的自己还能轻松接手维护。保持脚本简洁、写清日志、锁好依赖、管理好配置文件这些习惯比任何技巧都重要。希望这篇内容能让你少踩几个我踩过的坑如果要用一句话总结我的经验那就是脚本的第一读者永远是三个月后的自己。