古人学问无遗力，少壮工夫老始成。 纸上得来终觉浅，绝知此事要躬行。
Python 打包到底怎么选？我把 6 种主流方式全跑了一遍，优缺点一次说透
最近在给团队做内部工具的时候，又被 Python 打包这事折腾了一遍。说真的，这玩意看起来简单，真要让你选个合适的方案，没个三五天根本摸不清门道。我团队那帮人，java、go、PHP 的都有，每次看我搞 Python 部署就笑话我。"你这啥玩意儿？打个包搞一下午？" 你还别说，真就一下午，还不一定能搞利索。所以这次我决定把市面上主流的 Python 打包方式都跑一遍，每个都写写感受，说说坑，给大家一个真实参考。写之前我没看任何官方文档，全凭自己动手踩坑，这样写出来的东西才有烟火气。先说结论：没有银弹，只有适合。不同场景用不同方案，强行用错工具只会让自己加班。那些年，我被 Python 打包支配的恐惧先聊聊为啥要写这篇文章。事情是这样的，上个月接了个活，要把公司一个数据处理脚本做成可执行文件，给运营同事用。他们电脑是 windows，没装 Python，也不会装。我当时心想，pyinstaller 一把梭呗，结果搞了一下午。为啥？因为这脚本依赖了 pandas、numpy、requests，还有一堆奇奇怪怪的库。打包出来 300 多 M，运营同事传个文件发邮件都不方便。更要命的是，第一次在他电脑上跑，直接报错说缺什么 dll，我人都麻了。后来换了几种方式，发现每种都有它自己的脾气。今天就把我试过的几种全讲讲。不过别期待太高，Python 打包这个领域，本身就是"能用"和"好用"之间的巨大鸿沟。你要是想找完美方案，那真的没有。PyInstaller：这个老朋友，我又爱又恨先说 PyInstaller，估计 90% 的 Python 开发者都用过它。用起来真的很简单。一行命令搞定：pyinstaller -F your_script.py-F 是单文件模式，打包出来就一个 exe，用户拿着就能跑。对开发者友好度这块，PyInstaller 真的没得说。我第一次用的时候，感觉这玩意简直是神器。我写了个小工具，打包成 exe 发给同事，他双击就能用，那种成就感，懂的人都懂。但坑也多。最典型的就是体积问题。我那个数据处理脚本，打包出来 280M。同事问我："你这啥程序啊，怎么比英雄联盟客户端还大？" 我无言以对。为啥这么大？因为 PyInstaller 是把整个 Python 解释器和所有依赖库都塞进去了。pandas、numpy 这些库本身就大，再加上 PyInstaller 的运行时，动辄上百 M 起步。还有更恶心的兼容性问题。我那次在 Windows 10 打包的 exe，拿到 Windows 7 上跑不起来。报什么 "api-ms-win-crt" 错误，查了半天是缺 VC++ 运行库。还有在某些精简版系统上，连 tkinter 都跑不起来，要手动指定 hidden import。pyinstaller --hidden-import=tkinter your_script.py这种坑，第一次遇到真的能让人崩溃。不过话说回来，PyInstaller 还是我最常用的。因为它跨平台，Windows、Linux、macOS 都能打，社区也活跃，遇到问题基本搜得到。对于一些内部小工具，不在乎体积的情况下，PyInstaller 仍然是首选。有一次我做个小爬虫工具，就一个 py 文件，依赖只有 requests 和 beautifulsoup4，打包出来才 30M，体验就很好。所以体积这事，看你依赖什么库。纯 Python 库小，带 numpy、pandas 这些科学计算库就炸。cx_Freeze：跨平台的另一种选择cx_Freeze 这玩意，我接触得比 PyInstaller 少，但也有故事。当时做项目要在 Linux 服务器上跑一个 Python 脚本，但服务器是个精简版系统，连 pip 都没有。我想着搞个绿色版，直接拷过去就能用。cx_Freeze 生成的目录结构比较合理，library 和可执行文件分开。看着比 PyInstaller 整洁一些。但这玩意用起来比 PyInstaller 麻烦。要写 setup.py：from cx_Freeze import setup, Executable setup( name="myapp", executables=[Executable("your_script.py")] )然后 python setup.py build，出一堆文件。配置项多，新手容易懵。优点是跨平台做得不错，生成的结构清晰，库和可执行文件分离，对于一些需要定制部署的场景比较友好。缺点就是配置复杂，文档也不如 PyInstaller 详细。我那次配置参数搞了半天，最后还是放弃了，转头用了 PyInstaller。要是你项目有特殊需求，比如要打进一些自定义的 .so 文件或者 dll，cx_Freeze 的配置反而更灵活。但对普通用户来说，没那必要折腾。Nuitka：把 Python 编译成 C，性能起飞这个是我最近才认真试的，用完直接说一句：真香。Nuitka 的思路不一样，它不是把 Python 解释器和代码一起打包，而是把 Python 代码编译成 C 代码，然后再编译成机器码。对，你没看错，是真的编译，不是那种伪编译。第一次跑的时候，我在 Linux 上测试一个数据处理脚本，编译完跑起来，速度直接快了一倍。我以为是我眼花了，又跑了几次，确实快了不少。nuitka --standalone --onefile your_script.py参数和 PyInstaller 类似，但生成的二进制文件运行效率高很多。体积方面也很惊艳。同样的脚本，PyInstaller 打包 280M，Nuitka 编译出来 80M。差距不是一点半点。但！坑也来了。Nuitka 的编译速度是真的慢。我那个 300 行的脚本，编译花了 4 分钟。PyInstaller 几秒钟搞定。Nuitka 几分钟后才有结果。还有就是兼容性。Nuitka 对一些冷门库的支持不如 PyInstaller 好。我有个脚本用了 pyexecjs，Nuitka 怎么都搞不定，最后还是退回 PyInstaller。所以我的建议是：如果你的项目对性能有要求，Nuitka 真的值得一试。但要是项目复杂、依赖多，PyInstaller 还是更稳。Nuitka 还有个加分项，它支持 Python 3.6 以上的所有版本，而且对一些 C 扩展库的支持做得不错，至少 numpy、pandas 这种常见的没问题。但我得提醒一句，Nuitka 安装本身就是个坑。在 Linux 上还好，pip 直接装就行。在 Windows 上，你要先装 C 编译器，Visual Studio Build Tools，几 G 的东西。不像 PyInstaller 那样 pip install 就能用。源码包与 wheel 包：最 Pythonic 的方式说完编译型的，再说说 Python 圈子最传统的打包方式——源码包和 wheel 包。这个就是用 setuptools 那一套，写个 setup.py 或者 pyproject.toml，然后 python setup.py sdist bdist_wheel。这玩意严格说不算"打包成可执行文件"，但它在 Python 生态里太重要了，不得不提。python setup.py sdist # 源码包 python setup.py bdist_wheel # wheel 包wheel 包的好处，装起来快。因为是预编译的格式，pip 安装 wheel 包的时候不需要本地编译，直接解压就行。我之前在树莓派上装过一个库，等编译等了 20 分钟。换 wheel 包，几秒钟。这种体验差异真的巨大。wheel 包还有个隐藏好处：可以指定平台和 Python 版本。xxx-cp39-cp39-linux_x86_64.whl，这名字一看就知道是哪个平台哪个版本用的。源码包（sdist）则更通用，里面就是 Python 源代码和 setup.py。pip 装了之后会自动编译，但有些纯 Python 包就直接拷贝了。这两种方式适合的场景：你要发布的是库，而不是应用。用户拿到你的包后还会 import 使用，而不是直接运行。我自己的项目，如果是写库给别人用，就用这种方式。如果是写工具给自己或者非技术人员用，就用 PyInstaller 或者 Nuitka 编译成可执行文件。Docker：容器化才是终极方案？说完传统的打包方式，再聊聊现在云原生时代的玩法——Docker。严格说 Docker 不是 Python 打包工具，但用 Docker 部署 Python 应用真的太香了，我现在新项目基本都是这套。Dockerfile 写起来简单：FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "your_script.py"]这一套下来，生成的镜像扔到任何装了 Docker 的机器上都能跑。不用管宿主机的 Python 版本，不用管系统库依赖，不用管这个那个环境变量。优点太多了：环境隔离得彻底。每个容器都是独立的 Python 环境，互不干扰。部署一致性。开发环境测试过了，生产环境肯定一样。这点对运维来说简直是救命的。资源利用高效。一个服务器可以跑几十上百个容器，密度比虚拟机高多了。但也有代价：镜像体积不小。最小的 python:3.9-slim 镜像也有几十 M，加点依赖就上百 M。不过相比 PyInstaller 的 280M，还是能接受的。需要懂 Docker。不是说门槛多高，但要是团队没人用过 Docker，学起来还是要点时间。启动速度比直接跑可执行文件慢。不过这点在生产环境一般不是问题。我个人现在的做法：所有线上服务都走 Docker，本地开发也尽量用 Docker。桌面端的小工具才用 PyInstaller 这种。Docker 还有一个隐藏的好处，可以多阶段构建。编译阶段用一个完整镜像，运行阶段用一个精简镜像，镜像体积能小很多。比如：FROM python:3.9 AS builder WORKDIR /app COPY requirements.txt . RUN pip install --user -r requirements.txt FROM python:3.9-slim WORKDIR /app COPY --from=builder /root/.local /root/.local COPY . . CMD ["python", "your_script.py"]这样最终的镜像能小一半不止。PyOxidizer：Rust 写的打包工具，性能怪兽这个是最近挖到的宝，PyOxidizer。它是用 Rust 写的，思路和 Nuitka 类似，但更激进——它把 Python 解释器嵌入到一个 Rust 二进制里。听起来很硬核是吧？用起来其实没那么复杂：# pyoxidizer.bzl python_run_file path = "your_script.py"然后 pyoxidizer build，搞定。这个工具的亮点：启动速度比 PyInstaller 快得多。Nuitka 我已经觉得很厉害了，PyOxidizer 比它还快。我做了个简单测试，启动一个简单的 Flask 应用：PyInstaller 打包：启动 2.3 秒Nuitka 编译：启动 1.1 秒PyOxidizer：启动 0.4 秒这差距真的不是一点半点。而且它的单文件模式更彻底。PyInstaller 的 -F 模式本质上还是先解压到临时目录再运行，PyOxidizer 是真的把解释器和代码都嵌入到一个可执行文件里，没有解压过程。但缺点也很明显：首先，这玩意还比较新，文档不算完善，很多功能要翻 GitHub Issues 才能搞清楚。其次，对 Windows 的支持还在完善中。我在 Windows 上用踩了几个坑，最后还是退回 Linux 环境。还有就是，配置用的是 Starlark 语法（一种 Python 风格的配置语言），和 Python 本身有点区别，新手要适应一下。我的评价是：PyOxidizer 是打包工具的未来，但现在还不够成熟。要是你项目允许用 Linux，对启动速度有极致要求，可以试试。实战案例：我是怎么给运营做工具的光说理论太空了，给大家讲讲我最近做的真实项目。需求是这样的：运营部门每周要处理一批 Excel 文件，提取数据生成报表。原来是人工处理，一人一天。现在让我搞个自动化工具。第一个版本，我用 PyInstaller：写了个 Python 脚本，用 pandas 处理 Excel，openpyxl 写报表，tkinter 做了个简单界面。PyInstaller 打包出来 280M，运营同事收到邮件当天就吐槽："这啥玩意这么大？"第二个版本，我换了 Nuitka：编译出来 80M，运行速度还快了不少。运营同事勉强接受了。但有个问题，他们公司电脑有些是老款 Windows 7，Nuitka 编出来的二进制文件跑不起来，要装 VC++ 运行库，IT 部门不太愿意装。第三个版本，我妥协了，用 Docker：在内部服务器上起了个 Web 服务，运营同事用浏览器访问。镜像大小 200M 左右，但在服务器上运行，稳定得很。运营同事也不用装任何东西，打开浏览器就能用。最后选了 Docker 方案。不是因为它最好，而是因为最适合运营同事的使用场景。这个案例给我的最大启发：打包方式的选择，不是看技术多牛，而是看用户场景。给技术人员用和给非技术人员用，完全是两个思路。选哪个？给你个参考说了这么多，到底选哪个？我列个表给你参考：场景推荐方案理由内部小工具，分发给同事PyInstaller简单，跨平台对性能有要求Nuitka编译型，启动快桌面应用，要给非技术人员PyInstaller 或 Web 化学习成本低发布 Python 库setuptools + wheel生态标配线上服务Docker部署一致性云原生环境Docker标准化极致启动速度PyOxidizer启动飞快我个人的优先级：Docker > Nuitka > PyInstaller > 源码包 > PyOxidizer日常开发基本都 Docker，性能要求高的脚本用 Nuitka，分发小工具才用 PyInstaller。这些坑你别再踩了最后说几个常见的坑，新手一定要避开：第一，不要在打包环境里瞎装东西。我见过有人在打包机器上装了一堆开发工具，结果打出来的包巨大，依赖混乱。最好用干净的虚拟环境打包，pip install 完依赖就打包，别多装。第二，注意隐藏依赖。有些库是动态导入的，PyInstaller 静态分析找不到，要在 spec 文件里手动指定 hidden import。这事不复杂，但很多人栽在这里。第三，路径问题。打包后的程序，__file__ 这个变量的行为会变。在 PyInstaller 单文件模式下，__file__ 指向的是临时解压目录，不是你的 exe 所在目录。要用 sys._MEIPASS 获取真实路径。第四，不要忽略杀毒软件。PyInstaller 打包的 exe 经常被杀软误报，尤其是国内各种管家。我之前有个工具，运营同事的电脑装了 360，直接给拦截了，还给我归到"高危"。这种情况要么加白名单，要么换签名，要么用 Web 方案。第五，不要忽视更新机制。PyInstaller 打包的 exe 没法自动更新，要重新打包让用户下载。Docker 容器可以拉取最新镜像，Web 应用根本不用管。这点对工具的长期维护影响很大。写在最后Python 打包这事，说简单也简单，说复杂也复杂。简单是因为大部分情况下 PyInstaller 就能搞定，复杂是因为真要做得完美，需要考虑的场景太多。我没有最好的方案给你，只有最适合的方案。选择之前想清楚三件事：用户是谁？技术人员还是非技术人员？部署环境是什么？Windows、Linux、还是云端？维护成本怎么算？频繁更新还是一锤子买卖？想清楚这三个问题，答案自然就有了。Python 打包这领域，未来肯定会越来越好。PyOxidizer 让我看到了一些希望，Nuitka 也在快速迭代。但现阶段，要学会"将就"着用，别追求完美。行了，今天就聊这么多。下次有机会讲讲 Python 项目结构怎么组织，那个也是个大坑。如果觉得这篇文章对你有帮助，点个在看、转发给身边搞 Python 的朋友。整理这些内容花了我不少时间，都是真实踩坑经验，不是网上抄的。日常分享运维、Python、效率工具的干货，关注我，少踩坑，少加班。公众号：耕云躬行录个人博客：躬行笔记
别再用 for 循环串行执行了！Python asyncio 异步编程实战（运维自用版）
凌晨 4 点，我盯着屏幕上那个跑了快 20 分钟的 Python 脚本，咖啡喝了三杯，困得要死。脚本干啥呢？检查 80 台服务器的网络连通性，每台超时设 5 秒，串行跑的。当时我就想，这要是哪天服务器数量翻到 200 台，我不得跑到天亮？后来同事看了我的代码，就说了句：你咋不用 asyncio？我一愣，asyncio 是啥？查了一晚上资料，代码重构之后，同样的活，40 秒干完了。提速 30 倍。从那之后，我就把异步编程当成自己写脚本的"标配"了。不管是批量查 API、并发执行 SSH 命令、还是拉一堆监控数据，只要涉及到"等待"，我第一反应就是能不能异步。今天这篇文章不讲那些虚头巴脑的理论（什么事件循环原理、底层 epoll 机制），就讲怎么用、用在哪、踩过什么坑。看完你也能把自己脚本的效率拉起来，少加几个小时的班。运维写脚本的痛，谁写谁知道搞运维的，估计都干过这事：写个脚本批量处理一堆任务，逻辑也不复杂，就是循环里调个 API 或者连一下服务器，结果等得花儿都谢了。常见的场景我列一下，你看看中了几条：批量 ping 一堆机器看通不通调云厂商 API 拉几十台 ECS 的监控数据并发执行 SSH 命令收集日志定时任务里要调 N 个外部接口拿数据爬内部系统页面抓信息这些场景有个共同点：每个任务都要"等"。等网络返回、等 IO 完成、等对端响应。而 Python 默认的同步代码，就是一个任务完成再做下一个。我之前那个检查连通性的脚本，每台 5 秒超时，80 台理论最差要等 400 秒。但实际上很多机器不通要等满 5 秒，所以最后跑了将近 20 分钟。如果用异步呢？所有请求"同时"发出去，谁先回来先处理谁，总时间就取决于最慢的那一个，而不是所有加起来。这就是异步编程的核心价值：把"等待"的时间利用起来。异步不是新概念，只是 Python 终于有了"正经"语法在说代码之前，先聊点背景，避免你看着看着懵。异步这个概念早就有了，从最早的 Node.js 到 Go 语言，异步都是标配。Python 因为历史原因（GIL、全局解释器锁），多线程一直被诟病，所以异步这个特性迟迟没搞起来。直到 Python 3.4 出了 asyncio 模块，3.5 加入了 async/await 语法，异步编程才在 Python 这边真正能用。简单理解 asyncio 的几个关键概念：协程（Coroutine）：可以暂停的函数。用 async def 定义的函数就是协程函数，调用它不会立即执行，而是返回一个协程对象，得用 await 或者 asyncio.run() 之类的方式驱动它。事件循环（Event Loop）：asyncio 的核心，负责调度所有协程。你可以理解成"大管家"，所有协程都注册到它上面，它来决定什么时候执行哪个。任务（Task）：对协程的进一步封装，让协程能被事件循环调度执行。asyncio.create_task() 就是干这个的。Future：表示一个还没完成的结果，通常我们不用直接操作它。这玩意有点像餐厅服务员。同步就是你点完菜干等着，菜上完才点下一道；异步就是你点完菜服务员给你个号牌，你接着点下一道，菜好了服务员喊你。听起来挺玄乎，下面看代码就明白了。async/await 基础语法最基础的异步函数长这样：import asyncio async def hello(): print("Hello") await asyncio.sleep(1) # 模拟 IO 等待 print("World")就这么简单。async def 定义异步函数，await 后面接"可等待对象"（通常是另一个协程）。调用的时候不能直接 hello()，那只会返回一个协程对象，函数体不会执行。得这么干：asyncio.run(hello()) # Python 3.7+ 推荐写法asyncio.run() 会创建事件循环，运行协程，然后关闭循环。一句话搞定。但这有啥用？单个协程和同步没啥区别。真正的威力在并发。真正的提速：gather 和 create_taskasyncio 里两个最常用的并发工具：asyncio.gather 和 asyncio.create_task。先说 gather，最直观的并发方式：import asyncio import aiohttp # 异步 HTTP 库 async def fetch(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: return await response.text() async def main(): urls = [ "https://api1.example.com", "https://api2.example.com", "https://api3.example.com", ] results = await asyncio.gather(*[fetch(url) for url in urls]) print(results)asyncio.gather 接受一堆协程，并发执行，等所有都完成返回一个结果列表。这代码如果同步执行，每个请求假设 0.5 秒，三个就 1.5 秒。异步并发，几乎就是 0.5 秒多一点。再说 create_task，这个更灵活：async def main(): task1 = asyncio.create_task(fetch("https://api1.example.com")) task2 = asyncio.create_task(fetch("https://api2.example.com")) task3 = asyncio.create_task(fetch("https://api3.example.com")) result1 = await task1 result2 = await task2 result3 = await task3create_task 把协程包装成任务，立即扔到事件循环里调度。这样你可以在等待的时候干别的事。gather vs create_task 的区别：gather 更简单，自动收集所有结果create_task 更灵活，可以单独控制某个任务（取消、查状态）实际写脚本我 90% 的场景用 gather，剩下 10% 用 create_task。够用了。实战案例：80 台服务器健康检查光说不练假把式，我拿自己用过的真实场景讲讲。需求是这样的：检查 80 台服务器的网络连通性，每台用 ping 或者 TCP 端口检测，要支持超时控制，最后输出哪些不通。同步版本（不推荐）：import socket def check_server(host, port, timeout=3): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) sock.connect((host, port)) sock.close() return True except: return False def main(): servers = [...] # 80 台服务器 failed = [] for host, port in servers: if not check_server(host, port): failed.append((host, port)) print(f"不通的: {failed}")这版本最坏情况要等 80 * 3 = 240 秒。异步版本：import asyncio async def check_server(host, port, timeout=3): try: # asyncio.open_connection 是异步的 reader, writer = await asyncio.wait_for( asyncio.open_connection(host, port), timeout=timeout ) writer.close() await writer.wait_closed() return True except: return False async def main(): servers = [...] # 80 台服务器 tasks = [check_server(host, port) for host, port in servers] results = await asyncio.gather(*tasks) failed = [servers[i] for i, r in enumerate(results) if not r] print(f"不通的: {failed}") asyncio.run(main())这版本总时间接近 3 秒（最慢的那台的超时时间），不是 240 秒。asyncio.wait_for 是干嘛的？ 给协程加超时。如果超时还没完成，就取消掉，避免一直卡着。asyncio.open_connection 干啥？ 异步建立 TCP 连接。底层用的是非阻塞 IO，所以不会卡住事件循环。看到区别了吧？同步是"排队买票"，异步是"同时开 N 个窗口一起办"。异步里最常踩的 5 个坑这玩意看着简单，但用起来一堆坑。我把自己踩过的列出来，你绕着走。第一个坑：在 async 函数里调用同步的阻塞 IO这是最常见的错误。举个例子：import asyncio import requests # 同步 HTTP 库 async def fetch(url): response = requests.get(url) # 错误！这会阻塞整个事件循环 return response.textrequests 是同步库，会阻塞。阻塞期间，事件循环啥也干不了，等于异步失效。解决办法：换成异步 HTTP 库，比如 aiohttp、httpx。import httpx # 支持异步的 HTTP 库 async def fetch(url): async with httpx.AsyncClient() as client: response = await client.get(url) return response.text第二个坑：忘记 awaitasync 函数返回的是协程对象，必须 await 才会真正执行。少写一个 await，程序就静悄悄不工作了。async def main(): result = fetch("https://api.example.com") # 没 await，协程没执行 print(result) # 输出的是协程对象，不是结果新手经常栽这上面，代码跑起来没报错，但结果不对。第三个坑：asyncio.run() 只能调用一次asyncio.run() 会创建新的事件循环，结束后关闭。所以一个进程里只能调一次。如果你在 Jupyter 或者已经有事件循环的环境里用，会报 RuntimeError。解决办法：普通脚本：用 asyncio.run()Jupyter：用 await main()（IPython 内核已经有事件循环了）已有事件循环：用 asyncio.create_task()第四个坑：CPU 密集型任务别用 asyncioasyncio 适合 IO 密集型（网络请求、文件读写），不适合 CPU 密集型（计算、加密、压缩）。为啥？因为异步的本质是"等待的时候干别的"，如果任务是纯计算，根本没有"等待"，异步就退化成了同步。CPU 密集型用 multiprocessing（多进程）或者 concurrent.furures.ProcessPoolExecutor。第五个坑：超时控制别用 sleep有些同学想"等 N 秒"，这么写：await asyncio.sleep(N) # 正确 time.sleep(N) # 错误！会阻塞事件循环time.sleep 是同步的，会卡住整个循环，所有其他任务都得等。进阶用法：信号量控制并发度实战中还有个常见问题：并发数太高会把对方服务打挂。比如批量请求某个 API，对方限流 100 QPS，你一下发 1000 个请求过去，轻则被封 IP，重则把人家服务搞挂。asyncio 提供 Semaphore（信号量）来控制并发度：import asyncio semaphore = asyncio.Semaphore(10) # 最多 10 个并发 async def fetch(url): async with semaphore: # 获取信号量 async with httpx.AsyncClient() as client: response = await client.get(url) return response.textSemaphore(10) 意味着最多同时 10 个协程在跑，其他都得等。简单粗暴但有效。类似的还有 asyncio.Queue，用于生产者-消费者模式。这个我后面专门写一篇文章讲，太长了今天塞不下。异步文件 IO 怎么搞有人问，文件操作能用异步吗？能。Python 3.4+ 的 aiofiles 库：import aiofiles async def read_file(path): async with aiofiles.open(path, 'r') as f: content = await f.read() return content但说实话，文件 IO 异步化收益不大。因为：本地磁盘 IO 本身就快异步文件库生态不完善，很多库还是同步的如果是网络文件（NFS、对象存储），那是另一回事所以运维场景里，文件操作我一般还是同步。除非你读写的是 S3 之类的远程存储，那可以用 aioboto3（异步版 AWS SDK）。协程 vs 线程 vs 进程，一张表说清楚这三种并发方式经常被搞混，我做了个对比表，方便你根据场景选：维度协程 (asyncio)线程 (threading)进程 (multiprocessing)适合场景IO 密集型IO 密集型CPU 密集型切换开销极小中等大内存占用小中等大编程复杂度较高中等中等受 GIL 影响否（通过 IO 释放）是否适用并发量上万几百几十简单说：网络请求多 → asyncio等 IO + 需要兼容老库 → threading大量计算 → multiprocessing我们运维写脚本，90% 是网络 IO，所以 asyncio 几乎是首选。我常用的异步库清单分享下我自己常用的库，你按需取用：HTTP 客户端：httpx（推荐，同时支持同步和异步）aiohttp（纯异步，老牌库）Web 框架：FastAPI（异步框架，写接口特别爽）aiohttp（也能写服务端）数据库：asyncpg（PostgreSQL 异步）aiomysql（MySQL 异步）motor（MongoDB 异步）sqlalchemy[asyncio]（ORM 异步版）Redis：aioredis（已合并到 redis-py 4.0+）SSH：asyncssh（异步 SSH 客户端）paramiko（同步，需要线程包装）文件：aiofiles（异步文件 IO）消息队列：aio-pika（RabbitMQ）aiokafka（Kafka）把这些库装上，基本能满足运维 90% 的脚本需求。真实案例：监控数据采集脚本最后来个综合点的案例。需求：每隔 30 秒从云厂商 API 拉取 50 台 ECS 的 CPU、内存、磁盘数据，写到本地文件。import asyncio import httpx import json from datetime import datetime # 信号量控制并发 semaphore = asyncio.Semaphore(20) async def fetch_metric(instance_id, client): async with semaphore: try: url = f"https://api.cloud.example.com/metric/{instance_id}" response = await client.get(url, timeout=5) data = response.json() return { "instance_id": instance_id, "cpu": data.get("cpu"), "memory": data.get("memory"), "disk": data.get("disk"), "timestamp": datetime.now().isoformat() } except Exception as e: return { "instance_id": instance_id, "error": str(e), "timestamp": datetime.now().isoformat() } async def main(): # 读机器列表 with open("instances.txt") as f: instances = [line.strip() for line in f if line.strip()] async with httpx.AsyncClient() as client: tasks = [fetch_metric(iid, client) for iid in instances] results = await asyncio.gather(*tasks, return_exceptions=True) # 写结果 with open(f"metrics_{datetime.now():%Y%m%d_%H%M%S}.json", "w") as f: json.dump(results, f, indent=2) print(f"采集完成: {len(results)} 台") while True: asyncio.run(main()) asyncio.sleep(30) # 注意：这里该用 asyncio.sleep，但外层是同步这个脚本同步版本要跑 2-3 分钟（50 台 * 单台 3 秒），异步版本 5-10 秒搞定。注意几个细节：asyncio.gather(..., return_exceptions=True) 让单个任务失败不影响整体用 Semaphore 控制并发，避免打挂云厂商 APIhttpx.AsyncClient 用 async with 管理，自动关闭连接调试异步代码的小技巧异步代码调试比同步麻烦，因为栈帧不是线性的。几个小技巧：1. 用 logging 而不是 printimport logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) async def fetch(url): logger.info(f"开始请求 {url}") ...2. 启用 asyncio debug 模式asyncio.run(main(), debug=True)debug 模式会打印慢回调、未等待的协程、未处理的异常等警告。3. 用 aiomonitor 看运行时状态import aiomonitor async def main(): monitor = aiomonitor.start_aiomonitor() ...aiomonitor 起一个交互式 shell，可以查看当前所有任务、事件循环状态，还能执行 Python 代码。调试神器。写在最后写到最后，回头看看，其实 asyncio 没那么神秘。它就是 Python 给我们的一个工具，让我们写 IO 密集型脚本时能更高效。我个人建议的学习路径：先用 async/await 写个简单的并发请求学会 gather 和 wait_for理解 Semaphore 控制并发接触异步库（aiohttp、httpx）进阶：异步上下文、异步生成器这玩意 80% 的场景用 20% 的特性就够，别一上来就啃源码、看实现。少加点班，从写好脚本开始。如果这篇文章对你有帮助，别忘了点赞、转发，让更多运维兄弟看到。下期我打算写写 asyncssh 批量执行 SSH 命令的实战，以及怎么用异步改造我们组那套老掉牙的部署脚本。感兴趣的话，关注公众号「耕云躬行录」，第一时间收到更新。需要文中代码合集的，公众号后台回复 asyncio 领取。我的个人博客上也整理了一份完整的 asyncio 速查表，地址放下面了，欢迎常来逛逛：👉 躬行笔记关注 @耕云躬行录，回复关键词拿资料，踩过的坑、趟过的雷，都给你掰开揉碎讲清楚。咱们下篇见。
凌晨3点被502叫醒后，我把排查全流程写成了手册
睡梦中手机震个不停，迷迷糊糊摸过来一看，好家伙，告警群里已经炸了：核心业务接口大面积502，客服那边电话快被打爆了。我盯着天花板愣了两秒，然后爬起来开电脑，远程连上跳板机那一刻，说实话手都是抖的。这种场景做运维的应该都不陌生吧？大晚上被叫起来，脑子还没转过弯来，屏幕上全是红色告警，脑子里只有一个念头：这锅今天是不是得我来背？后来那次故障复盘会上，领导问我一个问题：502到底是怎么来的？你下次能不能10分钟之内定位清楚？我当时没敢接话。但事后我花了整整一周时间，把502的所有可能场景、排查路径、命令清单、根因分类全部整理了一遍。今天这篇文章，就是我整理出来的成果。不敢说100%覆盖，但生产环境里能踩的坑，基本都在里面了。你要是看完能少熬两个夜，少背几次锅，这文章就没白写。502到底是个什么东西在开干之前，得先搞清楚502到底是什么，不然排查的时候就是瞎子摸象。502的全称是Bad Gateway，翻成人话就是：网关或者代理服务器在往上游拿数据的时候，没拿到，或者拿到的东西不对，于是就给你返回了一个502。这句话听起来简单，但里面的信息量很大。第一个关键词：网关。也就是说502基本不会出现在直连后端的场景里，它一定是有中间层的情况下才会出现。Nginx、Apache、Haproxy、SLB、CDN、F5这些都是常见的网关。第二个关键词：上游。502是网关在告诉客户端：我没拿到上游的正常响应。至于是没连上、超时了、还是上游给我返回了一个乱七八糟的东西，那就是后面要具体排查的内容了。所以你记住一句话：502的锅，永远不在用户和网关之间，一定是网关到上游这段路出了问题。这也是为什么很多新手排查502的时候，一上来就去看后端服务的日志，看半天发现服务其实跑得好好的，于是就懵了——其实方向就不对。排查前的三件准备工作讲具体排查步骤之前，先说几个准备工作，这几个步骤能帮你少走一半弯路。第一，先确认影响范围。是所有接口都502，还是只有部分接口？是所有用户都中招，还是只有某个地区、某个运营商的用户？这个信息决定了你是S级故障还是A级故障，也决定了你需要叫多少人起来一起干。我之前就吃过亏，大半夜一看到告警就埋头排查，结果搞了半天才发现是某个边缘节点的局部问题，影响范围非常小，但当时我慌得一批，根本没先评估范围。第二，先抓现场。故障窗口期是金贵的，你排查过程中产生的所有数据，最好都保留下来。日志、监控截图、网络包、命令输出，能留的都留。为啥？因为等你查完已经天亮了，领导问你"你确定是这个问题？"的时候，你得有东西给他看。空口无凭的故障复盘，谁都不会买账。第三，先沟通。发现故障的第一时间，别自己一个人闷头干。在群里吼一声，说"现在线上有502问题，正在排查，影响范围XXX"，这个动作比排查本身还重要。沟通到位了，出了问题大家一起扛；沟通不到位，最后就是你的锅。这不是甩锅，这是基本的职业素养。好，准备工作说完，进入正餐。Nginx返回502的几种典型场景生产环境里502最多的场景，还是Nginx作为反向代理的时候。所以我重点讲这块。Nginx的error.log是排查502的第一现场，基本所有502的原因都能在这个日志里找到蛛丝马迹。所以排查的第一步，永远是去看Nginx的error.log。一般路径是 /var/log/nginx/error.log，tail一下最近的错误，基本就能看出个大概。下面是我这些年遇到的高频错误形态。connect() failed (111: Connection refused)这种错误的意思是：Nginx去连上游的时候，连接被拒了。翻译成普通话就是：上游服务根本没在监听那个端口，或者服务挂了。排查思路很直接：上游服务是不是活着？直接curl一下上游的IP+端口，看能不能连上上游服务的端口是不是变了？有时候发版的时候改了端口，Nginx配置没同步上游服务是不是只监听了127.0.0.1？这种情况下外部根本连不进去我之前有个项目，后端用的是Spring Boot，默认配置下服务只监听127.0.0.1，本地测试一切正常，一上线就502。后来排查了半天，定位到是这个原因，加上server.address配置才解决。这种情况其实挺常见的，尤其是开发环境和生产环境配置不一致的时候，特别容易出这种幺蛾子。connect() failed (110: Connection timed out)这个比Connection refused更恶心，因为它意味着连接请求发出去之后，对方一直没回应，直到超时。这种情况一般是网络层面的问题，比如：上游服务器宕机或者网络断了防火墙拦截了请求安全组规则没放行上游服务卡死了，没法accept新连接排查这种问题的时候，telnet是最直接的工具。telnet 上游IP 上游端口要是telnet都连不上，基本可以确认是网络问题。这时候你要做的事情是：在Nginx所在的机器上ping一下上游IPtraceroute一下看路由路径检查iptables规则检查云上的安全组配置有一回我遇到一个特别坑爹的情况：上游服务器明明在跑，本地telnet也能通，但Nginx就是连不上。最后查出来是两台机器之间的MTU不一致，大的包被丢了。改完MTU立马就好了。这种问题排查起来特别费时间，因为从表面看一切正常，但就是不通。upstream prematurely closed connection这个错误的意思是：Nginx刚和上游建立连接，上游就主动把连接关了，连响应都没给。这种场景的根因基本都在上游服务，比如：上游服务crash了上游服务的worker被kill了上游服务处理请求太慢，超过了Nginx的超时时间上游服务在做优雅停机这种问题光看Nginx的日志是不行的，必须配合上游服务的日志一起看。我的习惯是定位到上游机器之后，tail -f 应用日志，同时在Nginx那边复现请求，然后两边对照着看。有一次遇到一个诡异的问题：上游是Java应用，每次Nginx打过来的请求处理到一半就被中断了，Nginx报prematurely closed。查了两天才发现是JVM的某个参数配错了，导致Socket被异常关闭。504 Gateway Time-out虽然这个错误码是504不是502，但很多同学会混淆，而且它们的排查思路其实是相通的，所以也放在一起讲。504的意思是：Nginx和上游建立了连接，但上游在规定时间内没返回响应。这种场景基本都是上游服务处理能力的问题。排查思路：看上游服务的QPS、RT、错误率看上游服务的CPU、内存、线程数看上游是否有慢SQL、死锁、FullGC等情况看上游依赖的下游服务是否正常我处理过的最典型的一次504，是数据库出现了一条慢SQL，单条查询要跑30秒，拖垮了整个服务池。这种情况下Nginx报错，后端服务看起来也没崩，但请求就是处理不过来。经验之谈：遇到504的时候，不要只盯着Nginx的timeout配置调大，调大只是掩盖问题，根因还在上游。no live upstreams这个错误比较少见，但遇到了就很头疼。它的意思是Nginx配置的upstream组里，所有服务器都被标记为不可用了。这种情况一般是健康检查配置有问题，或者上游真的全挂了。排查思路：检查upstream配置里有没有启用健康检查看健康检查的阈值配得是否合理手动测试upstream里的每台机器是否都还活着我之前有个项目，配了比较激进的健康检查策略，结果服务稍微一抖动就被标记为不可用，后来调整了阈值才好。SSL握手失败如果你的Nginx是HTTPS的，上游也是HTTPS的，那中间还有个SSL握手的过程。SSL握手失败也会导致502。这种场景的错误日志一般是SSL_do_handshake() failed之类的。排查思路：检查证书是否过期检查证书链是否完整检查SSL协议版本和加密套件是否兼容检查上游是否要求客户端证书生产环境里证书过期导致的502，简直是运维的噩梦。尤其是Let's Encrypt的免费证书，90天就要续期一次，没自动化的话很容易忘记。Nginx自身资源耗尽还有一种容易被忽略的情况：Nginx自己扛不住了。Nginx的worker_connections是有上限的，默认是1024。要是连接数打满了，新的请求就处理不了，表现出来也可能是502。排查命令：# 查看Nginx当前的连接数 ss -n | grep :80 | wc -l # 查看Nginx的worker进程状态 ps aux | grep nginx这种情况一般是遭受了CC攻击，或者上游回包太慢导致大量连接被占用。完整排查路径图上面讲的是单点场景，现在串起来讲一个完整的排查流程。第一步：看监控，确认影响范围和持续时间Grafana、Zabbix、Prometheus都行，先看一下：502的QPS和占比影响的接口范围故障开始的时间点是否有发布、配置变更、网络调整等动作同步发生第二步：看Nginx错误日志tail -f error.log，观察错误的具体形态。这一步至关重要，能帮你快速定位到是上面几种场景里的哪一种。第三步：测试上游连通性从Nginx机器上直接curl上游：curl -v http://上游IP:端口/health注意要用-v参数，能看到详细的连接过程。这一步能验证最基本的网络连通性。第四步：检查上游服务状态进程是否在跑端口是否在监听资源使用情况应用日志有没有异常第五步：缩小问题范围根据前三步的结果，基本能定位到是网络问题、上游服务问题、还是Nginx配置问题。然后针对性地深入排查。第六步：根因定位 + 临时止血找到根因之后，先做临时止血，比如重启服务、回滚配置、切流量。然后再彻底解决。第七步：事后复盘故障恢复之后，写复盘文档，把时间线、根因、改进措施都写清楚。几个真实案例案例一：某天晚上业务高峰期突然大量502那次的error.log显示是connect() failed (110: Connection timed out)。我第一反应是上游挂了，结果telnet测试一切正常。继续查，发现是云厂商的负载均衡SLB和后端服务器之间的健康检查出了问题，SLB认为后端不健康，所以把流量都打到了同一台机器上，那台机器扛不住就超时了。解决方法：在SLB上调整健康检查的阈值，并且增加后端机器数量。案例二：发版之后部分接口502，部分正常这种就很典型了，肯定是发布相关的问题。我查了一下Nginx配置，果然是上游upstream的地址没改，新的服务在新的端口，Nginx还在打老端口。教训：发布流程里一定要有Nginx配置同步这个环节，最好做配置变更的自动化检查。案例三：CDN回源502CDN回源502的排查和直接Nginx的502类似，但要更复杂一些，因为中间多了CDN这一层。排查思路：先确认是不是全网502，还是只有某些节点通过修改hosts的方式直连源站测试查看CDN的回源配置CDN的问题很多时候是回源Host配置不对，或者源站有针对CDN的特殊限制（比如限流、白名单）。502排查的避坑清单聊几个我踩过的坑，新司机认真看。不要只看Nginx的配置不看上游。很多时候问题真的不在Nginx，在上游。不要忽略keepalive配置。Nginx和上游之间的长连接配置不当，会导致大量连接重建，性能急剧下降。不要在生产环境直接改配置再reload。一定要在测试环境验证通过。不要忽略时间点的对齐。把监控时间点、Nginx日志时间点、上游日志时间点对齐到秒，往往能发现一些隐藏的关联。不要只查HTTP状态码。要看完整的请求和响应，包括header、body，这些信息对定位问题非常关键。502的预防比排查更重要写到最后，说点预防的事情，因为最好的故障处理就是不让它发生。做好健康检查。Nginx自带的健康检查比较弱，建议用Tengine或者自己写脚本来做主动健康检查。做好超时配置。connect_timeout、send_timeout、read_timeout这些参数要根据业务特点合理设置，不要照搬默认值。做好监控告警。502的错误率一定要有监控，阈值要合理，既不能太敏感导致告警风暴，也不能太迟钝导致故障扩散。做好容量评估。定期做压测，了解系统的极限在哪里，提前扩容或者优化。做好变更管理。所有的配置变更、发布操作都要有记录，有回滚方案，有审批流程。做好预案。针对502这类常见故障，要有标准化的处理流程，新人来了也能照着做。写在最后做运维这几年，最大的感受就是：故障是不可避免的，但好的运维能让故障的影响降到最低。502这个错误码看着简单，背后牵扯到的东西可不少。网络、服务、配置、架构，任何一个环节出问题都可能导致它。但只要你按照一个标准化的流程去排查，有完整的工具链和监控体系支持，定位它其实并不难。我今天写的这些，都是我这些年线上实战踩过的坑、走过的弯路。有一些看起来很基础，但恰恰是这些基础的东西，在关键时刻能救你一命。建议你把这篇文章收藏起来，下次遇到502的时候拿出来对照着排查。说不定能帮你省下一个通宵。如果觉得这篇内容对你有帮助，记得点个在看，转发给你身边做运维的朋友。关注公众号「耕云躬行录」，后台回复"502"，可以领取我整理的《502排查命令清单》和《线上故障应急手册》PDF。我的个人博客上也会有更多运维实战的干货内容，地址放这里了：躬行笔记，欢迎常来逛逛。咱们下一篇接着聊线上故障那些事，不见不散。
服务器上线就被扫？一套完整的Linux安全加固方案，照着做能少踩80%的坑（建议收藏）
你新买了一台云服务器，装好系统、部署完应用，觉得万事大吉了。但你打开/var/log/auth.log看一眼就知道，从你服务器暴露在公网那一刻起，全世界的扫描器已经在排队敲门了。一天几万条暴力破解记录，不是夸张，是常态。这篇文章我把每一步加固操作写到具体命令级别。不讲道理，直接上手。系统账户和认证这块，是重灾区大部分入侵事件的入口就是SSH暴力破解。你去看任何一台暴露在公网的Linux机器，/var/log/auth.log或者/var/log/secure里面一定有大量失败登录记录。禁止root直接SSH登录这是最基础的一步，但很多人懒得改。vim /etc/ssh/sshd_config找到这一行，改成no：PermitRootLogin no然后重启sshd：systemctl restart sshd改之前记得先建好一个普通用户并加入sudo组，不然你改完自己也登不上去了。我见过有人改完直接断连，机房又不在身边，只能提工单让机房人员接显示器重置——这种低级错误犯一次就够了。useradd -m -s /bin/bash devops passwd devops usermod -aG sudo devops # Debian/Ubuntu # 或者 usermod -aG wheel devops # CentOS/RHEL修改SSH默认端口22端口是所有扫描器的第一目标。改成一个高位端口，比如58222，虽然不能完全防住定向攻击，但能过滤掉99%的自动化扫描。# /etc/ssh/sshd_config Port 58222改端口之后有个坑：如果你开了firewalld或者ufw，一定要先放行新端口再重启sshd。顺序反了你又得去机房了。# ufw的情况 ufw allow 58222/tcp systemctl restart sshd # firewalld的情况 firewall-cmd --permanent --add-port=58222/tcp firewall-cmd --reload systemctl restart sshd强制使用密钥登录，禁用密码认证这一步做完，暴力破解基本就废了。本地机器上生成密钥对：ssh-keygen -t ed25519 -C "your-server-name"把公钥传到服务器上：ssh-copy-id -p 58222 devops@your-server-ip确认密钥能登录之后，再禁用密码：# /etc/ssh/sshd_config PasswordAuthentication no PubkeyAuthentication yes重启sshd生效。这里有个细节很多教程不提：如果你用的是CentOS 8或者更新的系统，sshd_config里可能有个Include /etc/ssh/sshd_config.d/*.conf，子配置文件里的设置会覆盖主文件。我就踩过这个坑，明明改了主文件，密码登录还是能用，最后发现是子配置里有一行PasswordAuthentication yes在生效。grep -r "PasswordAuthentication" /etc/ssh/sshd_config.d/检查一下有没有冲突配置。登录失败锁定用fail2ban来做。装完就能用，配置也简单：apt install fail2ban # Debian/Ubuntu yum install fail2ban # CentOS systemctl enable fail2ban systemctl start fail2ban默认配置已经能防SSH暴力破解了，但我一般会调整一下参数。创建一个本地配置文件：cat > /etc/fail2ban/jail.local << 'EOF' [sshd] enabled = true port = 58222 maxretry = 3 bantime = 3600 findtime = 600 EOF意思是：10分钟内失败3次，封禁1小时。生产环境我会把bantime调到86400（一天）甚至更长。systemctl restart fail2ban查看当前封禁状态：fail2ban-client status sshd防火墙策略：默认拒绝，按需放行很多人的防火墙配置是反的——默认放行，然后去堵某些端口。正确做法是默认拒绝所有入站流量，只开放你确实需要的端口。UFW（Ubuntu/Debian推荐）ufw default deny incoming ufw default allow outgoing ufw allow 58222/tcp comment 'SSH' ufw allow 80/tcp comment 'HTTP' ufw allow 443/tcp comment 'HTTPS' ufw enablefirewalld（CentOS/RHEL）# 先看当前zone firewall-cmd --get-default-zone # 设置默认拒绝 firewall-cmd --set-default-zone=drop # 放行需要的端口 firewall-cmd --permanent --add-port=58222/tcp firewall-cmd --permanent --add-port=80/tcp firewall-cmd --permanent --add-port=443/tcp firewall-cmd --reload有个地方容易踩坑：如果你的服务器在云上（AWS、阿里云之类的），云平台本身有安全组。安全组和系统防火墙是两层独立的东西，两边都要配。我遇到过的情况是：系统防火墙开了端口，安全组没开，排查半天以为是应用问题。反过来也一样。还有一种情况，服务器需要访问外部API或者拉取更新，出站规则一般不用太限制。但如果是高安全要求的环境，出站也要白名单控制，防止反弹shell之类的情况。进阶玩法：只暴露443，管理端口全部走Nginx域名转发这是我目前在用的方案，比改SSH端口还彻底。思路很简单：服务器对外只开443端口，SSH、管理面板、监控这些全部藏在Nginx后面，通过不同的域名来区分和转发。扫描器扫到的只有一个HTTPS端口，连SSH端口都看不到。具体怎么做：防火墙只放行443：# ufw ufw default deny incoming ufw default allow outgoing ufw allow 443/tcp ufw enable # 如果SSH还需要保留一个应急入口（比如VPN内网段） ufw allow from 10.0.0.0/8 to any port 22 proto tcpNginx配置不同域名转发到不同的后端服务。比如你有这几个管理需求：ssh.yourdomain.com → 转发到本地SSHmonitor.yourdomain.com → 转发到Grafana（3000端口）admin.yourdomain.com → 转发到管理后台（8080端口）SSH走Nginx Stream模块（四层转发）：# /etc/nginx/nginx.conf 主配置中加载stream模块 stream { map $ssl_preread_server_name $backend { ssh.yourdomain.com 127.0.0.1:22; } server { listen 8443; # 内部stream监听端口 ssl_preread on; proxy_pass $backend; } }HTTP类管理服务走正常的反向代理：# /etc/nginx/conf.d/monitor.conf server { listen 443 ssl; server_name monitor.yourdomain.com; ssl_certificate /etc/letsencrypt/live/monitor.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/monitor.yourdomain.com/privkey.pem; # IP白名单，只允许你的办公网络访问 allow 203.0.113.0/24; deny all; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这套方案有几个好处：攻击面极小，nmap扫描只能看到443端口，SSH端口完全不可见管理域名可以加IP白名单或者套Cloudflare的Access策略，多一层认证所有管理入口都有TLS加密，不存在明文传输集中管理，加个新服务就加个nginx配置文件，不用动防火墙有个坑要注意：SSH走Nginx Stream转发的话，你本地ssh连接要改成连443端口（或者你stream监听的端口）。配置~/.ssh/config：Host my-server HostName ssh.yourdomain.com Port 443 User devops IdentityFile ~/.ssh/id_ed25519还有就是万一Nginx挂了，你SSH也进不去了。所以建议保留一个VPN内网段能直连22端口的应急通道，或者云平台的VNC控制台得确保能用。内核参数加固这部分经常被忽略，但对防御网络层攻击很有效。cat >> /etc/sysctl.conf << 'EOF' # 防止SYN Flood net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_max_syn_backlog = 2048 net.ipv4.tcp_synack_retries = 2 # 禁止IP转发（非路由器/网关场景） net.ipv4.ip_forward = 0 # 忽略ICMP重定向 net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 # 禁止源路由 net.ipv4.conf.all.accept_source_route = 0 net.ipv4.conf.default.accept_source_route = 0 # 开启反向路径过滤 net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 # 记录可疑数据包 net.ipv4.conf.all.log_martians = 1 # 忽略ping广播 net.ipv4.icmp_echo_ignore_broadcasts = 1 EOF sysctl -p说个实际场景：之前有台Web服务器被SYN Flood攻击，连接队列被打满，正常用户全部超时。加了tcp_syncookies和调大tcp_max_syn_backlog之后，配合前端的负载均衡限流，扛住了后面几次同样规模的攻击。文件权限和关键目录保护Linux下有些文件权限设置得太松，会成为提权的跳板。检查SUID/SGID文件SUID位的文件会以文件所有者的权限执行。如果一个root所有的文件被设置了SUID，任何用户执行它都会获得root权限。find / -perm -4000 -type f 2>/dev/null find / -perm -2000 -type f 2>/dev/null看看输出里有没有不该出现的东西。正常的SUID文件比如/usr/bin/passwd、/usr/bin/sudo这些是系统需要的。但如果你发现/tmp目录下有个SUID文件，那八成有问题了。关键配置文件权限chmod 600 /etc/shadow chmod 644 /etc/passwd chmod 700 /root chmod 600 /etc/ssh/sshd_config/tmp目录加固/tmp是很多攻击的中转站，因为默认所有用户都能写。如果条件允许，给/tmp单独分区并挂载时限制执行权限：# /etc/fstab 中对/tmp分区加参数 tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev 0 0noexec禁止执行，nosuid忽略SUID位，nodev禁止设备文件。这样即使攻击者把恶意脚本丢到/tmp，也没法直接执行。重新挂载让配置生效：mount -o remount /tmp自动化安全更新漏洞被公开之后到你打补丁之间的这段时间窗口，是最危险的。很多入侵事件利用的都是已知漏洞，补丁早就发布了但服务器没更新。Debian/Ubuntuapt install unattended-upgrades dpkg-reconfigure -plow unattended-upgrades这样系统会自动安装安全更新。配置文件在/etc/apt/apt.conf.d/50unattended-upgrades，可以精细控制只更新安全补丁还是全部更新。我的建议是：安全补丁自动装，但功能更新和内核更新手动来。自动更新内核如果出问题可能导致重启后起不来，这个风险在生产环境承担不起。# /etc/apt/apt.conf.d/50unattended-upgrades Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}-security"; // "${distro_id}:${distro_codename}-updates"; 注释掉非安全更新 }; // 自动移除不需要的依赖 Unattended-Upgrade::Remove-Unused-Dependencies "true"; // 更新后如果需要重启，自动重启（谨慎开启） // Unattended-Upgrade::Automatic-Reboot "true"; // Unattended-Upgrade::Automatic-Reboot-Time "04:00";CentOS/RHELyum install yum-cron systemctl enable yum-cron编辑/etc/yum/yum-cron.conf，把apply_updates = no改成yes。审计和日志出了事之后要能溯源，这就依赖完整的日志。auditd审计框架apt install auditd # Debian/Ubuntu yum install audit # CentOS systemctl enable auditd systemctl start auditd添加一些关键审计规则：cat > /etc/audit/rules.d/hardening.rules << 'EOF' # 监控passwd和shadow文件的修改 -w /etc/passwd -p wa -k identity -w /etc/shadow -p wa -k identity -w /etc/group -p wa -k identity -w /etc/gshadow -p wa -k identity # 监控sudoers修改 -w /etc/sudoers -p wa -k sudoers -w /etc/sudoers.d/ -p wa -k sudoers # 监控SSH配置修改 -w /etc/ssh/sshd_config -p wa -k sshd_config # 监控crontab修改 -w /etc/crontab -p wa -k cron -w /etc/cron.d/ -p wa -k cron -w /var/spool/cron/ -p wa -k cron # 监控系统调用 - 可执行文件的权限修改 -a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -k perm_mod EOF # 重新加载审计规则 augenrules --load日志集中管理单机日志如果机器被入侵了，攻击者会清理日志。把日志实时同步到远程日志服务器是基本操作。最简单的方式是用rsyslog转发：# /etc/rsyslog.conf 或 /etc/rsyslog.d/remote.conf *.* @@log-server-ip:514@@是TCP转发，@是UDP。生产环境用TCP，不会丢日志。条件好的话上ELK或者Loki，日志检索和告警都方便很多。服务最小化装完系统默认会跑一些你根本不需要的服务。每多一个监听端口就多一个攻击面。# 查看当前监听端口 ss -tlnp看看输出里有没有你不认识的东西。常见的可以关掉的：# 如果不需要邮件服务 systemctl disable postfix systemctl stop postfix # 如果不需要打印服务 systemctl disable cups systemctl stop cups # 如果不需要avahi（mDNS） systemctl disable avahi-daemon systemctl stop avahi-daemon还有一个点：确认/etc/hosts.allow和/etc/hosts.deny（TCP Wrappers）。虽然这玩意儿比较老了，但某些服务还在用。可以做个兜底：# /etc/hosts.deny ALL: ALL # /etc/hosts.allow sshd: 10.0.0.0/8 192.168.0.0/16意思是默认拒绝所有TCP Wrapper管控的服务，只对内网IP放行SSH。实战案例：一次挖矿木马的排查和清理去年有台服务器CPU突然飙到100%，业务响应变得很慢。当时排查过程是这样的：发现异常监控告警CPU持续100%。登上去top看了一下，有个叫kworkerds的进程吃满了CPU。这名字伪装得挺像内核线程的，但真正的内核线程在top里显示会带方括号（比如[kworker/0:0]），没有方括号的就是冒牌货。top -c # 找到异常进程PID ls -la /proc/<PID>/exe/proc/<PID>/exe指向了/tmp/.ice-unix/下的一个二进制文件。这个路径就很经典了，挖矿木马最爱藏这里。查找入侵路径# 查看最近被修改的文件 find / -mtime -3 -type f 2>/dev/null | grep -v proc | head -50 # 查看定时任务 crontab -l cat /var/spool/cron/root ls -la /etc/cron.d/果然在crontab里发现了一条每分钟执行的任务，从一个外部地址下载脚本并执行。/etc/cron.d/下也被塞了一个文件。再看auth.log，发现几天前有一个IP暴力破解成功了——密码太弱了，就是一个简单的admin123。清理过程# 杀进程 kill -9 <PID> # 删除恶意文件 rm -rf /tmp/.ice-unix/ rm -f /etc/cron.d/malicious_cron # 清理crontab crontab -r # 清空当前用户的crontab，之后重新添加合法任务 # 检查有没有其他后门 # 查看是否有异常的authorized_keys cat /root/.ssh/authorized_keys cat /home/*/.ssh/authorized_keys # 检查是否被修改了系统命令 rpm -Va # CentOS，验证所有包的文件完整性 debsums -c # Debian/Ubuntu最后发现攻击者还在/root/.ssh/authorized_keys里加了自己的公钥。如果只杀进程不检查这个，人家随时能回来。事后加固把这篇文章前面说的那些全做了一遍：改端口、禁密码登录、上fail2ban、firewall只开必要端口。另外密码策略也收紧了，所有服务器统一用密钥认证，彻底告别密码。一份可以直接用的加固检查清单怕你看完就忘，我把关键项整理成清单。新机器上线前过一遍：序号加固项状态1创建普通用户，禁止root SSH登录☐2SSH端口改为高位非标端口☐3强制密钥认证，禁用密码登录☐4安装配置fail2ban☐5防火墙默认拒绝，白名单放行☐6云安全组与系统防火墙双重配置☐7内核参数加固（SYN防护/禁转发/反向过滤）☐8检查并清理SUID/SGID文件☐9/tmp挂载noexec,nosuid,nodev☐10配置自动安全更新☐11安装auditd，配置关键文件审计☐12日志转发到远程日志服务器☐13关闭不必要的服务和端口☐14定期检查crontab和authorized_keys☐几个容易忽略的坑DNS配置很多人只关注TCP端口，忘了DNS。如果你的/etc/resolv.conf里配的是公共DNS（比如8.8.8.8），而你的内网有些域名需要走内部DNS解析，搞错了会导致内网服务访问异常。而且DNS查询也是可以被劫持的，有条件就用DoH或者DoT。NTP时间同步日志分析和审计溯源都依赖准确的时间。如果各台服务器时间不一致，你在日志里对不上事件的先后顺序，排查的时候会很痛苦。# 确认NTP同步状态 timedatectl status # 或者 chronyc trackingHistory命令记录默认的bash history有大小限制，而且关了终端可能丢失。加几行配置让命令记录更完整：cat >> /etc/profile << 'EOF' export HISTSIZE=10000 export HISTFILESIZE=20000 export HISTTIMEFORMAT="%Y-%m-%d %H:%M:%S " shopt -s histappend EOF这样每条命令都有时间戳，出事了能查到谁在什么时候执行了什么。总结服务器安全加固这件事，说白了就是把攻击面压到最小。SSH加固堵住入口，防火墙控制流量，内核参数防御网络攻击，文件权限防止提权，自动更新堵住已知漏洞，审计日志提供溯源能力。每一项单独看都不复杂，但组合在一起就是一道靠谱的防线。我见过太多"先上线再说安全"的情况，最后出了事才来亡羊补牢。补救的成本比预防高十倍不止。花半天时间做好加固，换来的是之后你能踏实睡觉，不用半夜被告警叫醒之后发现是被人搞了。安全不是一次性的事，定期巡检、持续更新、关注漏洞公告，这些习惯要养成。如果这篇文章对你有用，帮忙点个在看或者转发给你的运维同事，说不定能帮他们避开一次事故。我是运维躬行录，一个在生产环境里踩坑踩出来的运维人。关注我，持续分享运维实战经验，少走弯路少加班。公众号：耕云躬行录个人博客：躬行笔记
我们团队踩了2年坑，才总结出这套企业级Jenkins CI/CD搭建方案（建议收藏）
200多个微服务同时构建失败，线上发版卡死，开发堵在办公室门口骂娘。我盯着Jenkins那台破服务器，磁盘100%，内存飘红，连后台都进不去。那一刻我才真正明白：Jenkins不是装上就能用的，企业级的CI/CD，是一套体系，不是一个工具。装上Jenkins到用好Jenkins，中间隔着一百个坑。今天把这两年的踩坑经验全掏出来，从架构到Pipeline，从插件选型到高可用，从权限管控到性能调优，掰开了讲。看完你直接能落地，少走半年弯路。从一次深夜故障说起事情是这样的。那天搞活动，所有业务线都在赶发版。我们的Jenkins集群突然就崩了，Master机器CPU直接100%，整个构建系统卡死。排查过程堪称教科书式的狼狈。先看top，发现Java进程吃满了CPU。再看Jenkins的thread dump，发现大量线程卡在Git操作上。查构建队列，积压了400多个任务，整个Master被拖到喘不过气。根因查出来让人哭笑不得：有个老项目没做Git LFS，把一堆二进制文件全塞进Git仓库了，单次拉取要20多分钟。每次构建全量拉取，整个Master被拖垮。怎么解决的：第一步，立刻扩容Master机器到8核16G临时顶住，让发版能继续走。第二步，强制清理那个20多G的仓库，二进制文件迁移到Nexus。第三步，加了Git仓库大小监控，超过500M的报警。第四步，重要Agent节点限流，配置合理的并发数。这个案例让我彻底明白一个道理：CI/CD系统的稳定性，取决于最弱的那个环节。你Jenkins装得再高端，架构设计得再完美，一个失控的Git仓库就能搞死你。先搞清楚Jenkins到底是个啥很多人对Jenkins的认知停留在"装上就能用的工具"，但企业用起来，会发现完全不是那么回事。Jenkins本质上是个事件驱动的执行引擎。它监听代码变更、定时任务、webhook这些事件，然后调度Master和Agent去干活。Master负责调度和界面展示，Agent负责实际构建。这套Master/Agent架构听起来简单，但企业用起来，全是坑。它和GitLab CI、GitHub Actions最大的区别在于插件生态。Jenkins有1500多个官方插件，几乎能集成所有你能想到的工具。这种开放性是双刃剑，好处是扩展性强，坏处是插件质量参差不齐，装多了容易出兼容性问题。而Jenkins Pipeline，是它区别于传统CI工具的核心特性。它是一组插件，让你可以用Groovy DSL把整个构建流程写成代码，然后签入到Git仓库里管理。这就是所谓的"Pipeline as Code"——把构建流水线当成代码一样去版本控制、审查、迭代。说实话，我见过太多团队还在用Freestyle Job建项目，建了几百个Job，改个参数要改到手断。企业级必须上Pipeline，这是血泪教训。架构选型：别一上来就堆K8s我们第一版架构就吃了亏：上来就搞Jenkins on K8s，动态Agent拉满，觉得这就是最佳实践。结果网络问题、PV挂载问题、调度问题全冒出来，团队天天救火。后来才悟出来：架构要匹配团队规模，不要为了用新技术而用新技术。10人以下的团队，单Master就够，搞那么多花活纯属给自己挖坑。装个Jenkins，配几个Agent，先把流程跑通再说。50人左右的团队，Master+静态Agent够用。搭个Nginx反向代理做高可用，Agent按语言分组管理，这套架构能扛住绝大部分中型公司的构建压力。200人以上的大团队，才需要考虑K8s动态Agent。K8s的好处是弹性伸缩，构建高峰期自动扩容，低谷期自动释放。但运维成本也上去了，需要有专门的SRE团队维护。我们现在的架构跑了两年，稳得很：2台Master做主备，用Nginx+Keepalived漂移VIP，构建Agent按语言分组（Java一组、Python一组、前端一组），构建产物推Nexus，镜像推Harbor。这套扛了日均2000+次构建，稳定性99.9%以上。环境准备：这些坑提前避开Jenkins的安装本身不复杂，但环境准备阶段有几个坑，新手必踩。第一个坑：运行用户。Jenkins千万别用root启动，但目录权限一定要给足。我们当时chown改权限，改完一堆任务报错，查了半天才发现是.ssh目录和known_hosts的问题。jenkins用户的SSH密钥、known_hosts文件都要正确配置，不然Agent通信会出问题。第二个坑：JDK版本。Jenkins 2.346以后的版本必须JDK11，我们当时还在用JDK8，启动直接报错。升级JDK又牵扯到一堆老插件兼容性，又是半天折腾。所以选Jenkins版本前，先查清楚对应的JDK要求。第三个坑：磁盘空间。Jenkins的workspace会越积越多，构建历史、构建产物、日志文件，全吃空间。我们后来挂了块单独的SSD专做workspace，IO瓶颈直接消失。JENKINS_HOME目录建议放在独立分区，最好是SSD。第四个坑：基础工具。Maven、Git、Docker这些基础工具必须预装到Agent上。别指望Jenkins自己装，它装不靠谱。Agent的镜像要做标准化，新Agent拉起来就能用。第五个坑：时区和时间同步。Jenkins的定时任务、构建日志、构建历史全靠时间戳。Agent时区不对，构建历史会乱套；时间不同步，分布式构建会出各种诡异问题。所有机器必须配置NTP时间同步。Pipeline才是企业级的核心前面说过，Freestyle Job是玩具，Pipeline才是企业级。但Pipeline写起来也有讲究。声明式Pipeline vs 脚本式Pipeline。新手推荐用声明式（Declarative Pipeline），语法更严格，写法更规范，不容易出岔子。老手可以用脚本式（Scripted Pipeline），更灵活，能写复杂逻辑。我们团队规范是能用声明式就用声明式，复杂场景才用脚本式。标准的声明式Pipeline长这样：pipeline { agent any options { timestamps() // 日志加时间戳 timeout(time: 30, unit: 'MINUTES') // 构建超时 disableConcurrentBuilds() // 禁止并发构建 buildDiscarder(logRotator(numToKeepStr: '30')) // 保留30次构建 } parameters { string(name: 'BRANCH', defaultValue: 'main', description: '构建分支') choice(name: 'ENV', choices: ['dev', 'staging', 'prod'], description: '部署环境') } stages { stage('Checkout') { steps { checkout scm } } stage('Build') { steps { sh 'mvn clean package -DskipTests' } } stage('Unit Test') { steps { sh 'mvn test' junit '**/target/surefire-reports/*.xml' } } stage('Deploy') { when { branch 'main' } steps { sh "./deploy.sh ${params.ENV}" } } } post { always { cleanWs() // 清理workspace } success { echo '构建成功' } failure { emailext to: 'team@company.com', subject: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}", body: "构建日志: ${env.BUILD_URL}" } } }这套模板能解决80%的构建场景。剩下的复杂场景，用参数化构建、多分支Pipeline、并行执行这些特性扩展。这里有个细节很多人忽略：Jenkinsfile要放在项目根目录。这样每个项目自带Jenkinsfile，谁提交的代码谁负责维护Pipeline，权责清晰。我们团队有条规定：PR里改了Jenkinsfile，必须有高级工程师Review。插件选型：别装太多，挑重点Jenkins插件是它的灵魂，也是它最容易崩的地方。我们之前图新鲜装了一百多个插件，升级的时候一半不兼容，Jenkins直接起不来，折腾了一整天。企业级必装的插件就这些：Pipeline流水线核心，必装。Git/GitHub/GitLab根据代码仓库选一个。Maven Integration做Java构建必备。Docker Pipeline构建镜像必备。SSH Agent远程执行脚本。Role-based Authorization Strategy权限管理，这个没装会出大事。Timestamper日志加时间戳，排查问题必备。Workspace Cleanup清理workspace。Localization中文支持，新手友好。这些插件谨慎装：第三方小众插件，维护可能跟不上。老旧插件长期不更新，容易出安全漏洞。功能重复的插件装一个就行，装多了冲突。插件管理要做白名单制度。新插件上线前必须在测试环境跑一遍，确认兼容性再上生产。我们吃过亏，某次升级装了个小众插件，结果和Pipeline插件冲突，整个Master起不来。另外，Jenkins升级和插件升级要分开做。Jenkins核心版本升级前，一定先在测试环境验证。插件升级也要错开，别一次性全升。权限管控：出事才知道重要我们出过这么个事：一个新来的实习生权限没控制好，把生产环境的Job配置改了，导致全公司发版中断了2小时。权限这东西，事前不设防，事后火葬场。基于角色的权限管理是必须的。Role-based Authorization Strategy这个插件一定要装。我们的角色设计参考：管理员有所有权限，能改全局配置。开发只能看和触发自己项目的Job，不能改配置。测试可以触发但不能改配置，能看所有项目的构建日志。运维可以改所有Job配置，但管理权限要单独申请。访客只读，连触发构建都不行。每个项目组成员加到对应角色，Job级别也要分配。千万别图省事给所有人admin权限。出事的时候追责都追不到人。权限配置还有个坑：不要在Job配置里写死用户信息。用Jenkins的Credentials管理敏感信息（SSH密钥、Kubernetes Token、Docker Registry密码等），Job里只引用Credentials ID。这样人员变动的时候不用改Job配置。高可用怎么做Master高可用我们有套标准方案，跑了两年没出过问题。两台Master机器，配置完全一致（CPU、内存、磁盘、JDK版本、插件版本都要一致）。JENKINS_HOME挂NFS共享存储，所有配置、插件、构建历史全在共享目录。Nginx做反向代理，upstream配置两台后端。Keepalived做VIP漂绑，一台Master挂了另一台自动接管。Agent节点按业务分组，每组至少2台机器。一台挂了另一台能顶上。关键业务的Agent一定要做冗余，不能只有一台。构建产物要分离。Jenkins只负责调度，产物存到Nexus（Java包）或S3（通用文件）。Jenkins本身不存大的构建包，磁盘压力会小很多。镜像统一推Harbor，Jenkins不做镜像存储。JENKINS_HOME的NFS一定要用高可用的存储方案。普通NFS单点故障，Master切换了NFS没切过来就完蛋。我们用的是NFS+DRBD双主方案，后来上了CephFS，更稳了。监控告警不能少没有监控的Jenkins就是在裸奔，故障定位全靠猜。Jenkins本身有Prometheus插件，暴露metrics接口。我们接了Prometheus+Grafana，重点监控这些指标：队列积压任务数，这个是核心指标。积压超过50就要警惕，说明Agent不够用或者有慢任务。Master JVM内存，Jenkins是Java应用，内存泄漏是常见问题。内存持续增长不释放就要排查。Agent在线状态，Agent掉线是常有的事，特别是K8s动态Agent。构建成功率，按项目分组统计，能看出哪些项目代码质量差。平均构建时长，突然变长说明有问题，可能是依赖变更或者环境问题。告警规则要分级：紧急告警（电话）：Master宕机、队列积压超过200、构建失败率超过30%。重要告警（IM）：Agent离线超过5分钟、内存超过80%、单个项目连续失败3次。一般告警（邮件）：构建时长超过基线2倍、磁盘使用率超过70%。构建时长基线这个概念很重要。每个项目第一次构建成功后记录时长，作为基线。后续构建超过基线2倍就告警，能提前发现很多问题——依赖下载变慢、测试用例变多、镜像拉取问题，全能反映在构建时长上。性能调优实战Jenkins用久了总会变慢，这里面有几个调优的硬核技巧。JVM调优是基础。Jenkins的JVM参数要调整，默认配置太保守。我们生产环境的配置是：堆内存设为机器内存的50%-70%，新生代用G1垃圾回收器，MaxGCPauseMillis设为200ms。具体配置在JENKINS_JAVA_OPTIONS环境变量里设置。export JENKINS_JAVA_OPTIONS="-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/jenkins/"Master和Agent分工要明确。Master只做调度，不做实际构建。所有的Maven、Docker、Shell执行都放Agent。千万别在Master上跑构建任务，Master一旦卡住整个集群就完蛋。并发数控制是关键。Master的并发执行数（# of executors）建议设为0，让Master只做调度。Agent的并发数按机器配置来，4核8G的机器建议设2-3个executor，别贪多。workspace管理容易被忽略。构建完成后及时清理workspace，不然磁盘会爆。我们用Workspace Cleanup插件，在post阶段的always里调用cleanWs()。重要构建产物在构建中就归档到Nexus，不留在workspace。构建缓存能大幅提升速度。Maven的本地仓库挂载到独立目录，Agent之间共享。npm、pip这些也有类似的缓存机制。Docker构建用BuildKit的缓存，挂载到持久化存储。插件精简前面说过，但还要强调一下。每多装一个插件，Jenkins启动时间就多一秒。我们的Master启动时间从1分钟优化到15秒，就是靠砍掉30多个不用的插件。避坑清单：这些坑千万别踩不要直接在生产改配置。所有配置变更先在测试环境验证，配置变更要走变更流程，重要变更要双人复核。我们出过生产环境JVM参数改错，Master直接OOM的事故。不要无监控上线。新Job上线前必须配监控和告警，不然故障发生了都不知道。不要只看CPU不看load、上下文切换。有时候CPU不高但load很高，原因是IO阻塞或者线程争抢。不要省略复盘。每次故障都要复盘，写清楚现象、原因、解决、预防。不复盘的故障一定会再发生。不要把Jenkins当部署工具。Jenkins的核心是构建和调度，部署应该用专业的工具（Ansible、Argo CD、Spinnaker）。Jenkins可以调用这些工具，但不要自己造轮子。不要在Jenkins里存敏感信息。密码、Token、密钥这些必须用Credentials管理，不要明文写在脚本里。定期轮换Credentials，特别是有人员变动的时候。不要忽视构建历史清理。构建历史会一直保留，时间长了Jenkins会越来越慢。配置logRotator，定期清理旧的构建历史。写在最后Jenkins这东西，说简单也简单，装上就能跑。说难也难，企业级的稳定性、性能、权限、安全，处处是坑。我见过太多团队上来就照着博客搭一套，三个月后又推翻重来。企业级CI/CD不是一蹴而就的，是踩着坑迭代出来的。希望我今天总结的这些经验，能让你少踩几个坑。这套方案我们在生产环境跑了2年，扛住了日均2000+次构建，没出过大的稳定性问题。核心就三点：架构匹配规模、Pipeline as Code、监控全覆盖。如果觉得这篇内容对你有帮助，点赞、在看、转发三连支持一下，这是对我最大的鼓励。也可以关注我的公众号耕云躬行录，回复"CI"领取这份企业级CI/CD搭建的完整方案文档、Jenkinsfile模板合集、以及Jenkins调优配置脚本。下期我会讲讲Jenkins与Kubernetes深度集成实战，包括动态Agent、PV挂载那些让人头大的问题怎么解。关注我，别错过。有问题欢迎评论区留言，看到必回。—— 来自一个被Jenkins折磨过无数次的运维老兵公众号：耕云躬行录个人博客：躬行笔记关注回复「CI」获取完整方案资料
为什么光会 strace 远远不够先说个大实话。很多刚入行的兄弟一提排查就上 strace，觉得这玩意儿万能。strace 确实好用，追系统调用和信号一把好手。可它有个致命毛病——性能开销太大了。你在一个每秒几万请求的高并发服务上直接 strace，好家伙，进程直接被你拖慢好几倍，本来没崩的可能被你 strace 崩了。我早年就干过这种蠢事，线上抓 strace 把一个本来只是慢的服务直接搞到超时雪崩，被组长在群里点名，社死现场。而且 strace 只能看系统调用。程序卡在用户态的库函数里、卡在锁上、卡在某个死循环里，strace 是看不出来的，它只会显示进程一直卡在某个 read 或者 futex 上，然后你就懵了。排查故障的核心逻辑就一句话：先定位是哪一类资源出了问题，再用对应的专项工具往下钻。 别一上来就闷头抓包、闷头 strace，那是没头苍蝇。下面我按场景拆开讲。追踪剖析类：strace 的那些「兄弟」ltrace —— 看库函数调用strace 追内核调用，ltrace 追用户态的动态库函数，俩是互补的。比如你怀疑程序疯狂 malloc 内存、或者在某个字符串处理函数里死循环，strace 是看不见的，这时候 ltrace 就派上用场了：ltrace -p 12345 # attach 到进程 ltrace -c ./your_app # 统计各库函数调用次数和耗时那个 -c 参数特别有用，能给你统计出哪个库函数被调用了几十万次、占了多少时间，一眼就能看出程序在哪儿磨蹭。不过说句实话，ltrace 在生产上我用得也少，开销跟 strace 半斤八两，一般是测试环境或者能接受短暂影响的时候才上。eBPF 全家桶（bpftrace / bcc-tools）—— 生产环境的救命稻草重点来了，这个我要多说两句。前面提到我凌晨那次故障，最后就是靠 eBPF 里的工具定位的。eBPF 这东西现在是真香，基于内核的 BPF 虚拟机跑，性能损耗极低，低到你可以在生产高并发环境直接开，几乎感觉不到影响。bcc-tools 里一堆现成脚本，拿来就用：execsnoop # 实时追踪谁在启动新进程，排查异常启动、定时任务乱跑神器 opensnoop # 追踪谁在打开哪些文件 ext4slower 10 # 揪出超过 10ms 的慢磁盘 I/O，直接告诉你哪个进程哪个文件慢 biolatency # 磁盘 I/O 延迟直方图 tcpconnect # 追踪 TCP 主动连接我那次就是用 ext4slower 发现某个进程在疯狂读一个小文件，一秒好几万次，磁盘 await 被它顶爆了。后来查出来是代码里配置读取没做缓存，每个请求都去磁盘捞一次配置文件。bpftrace 更灵活，能写单行脚本自定义追踪，比如统计某个系统调用的分布：bpftrace -e 'tracepoint:syscalls:sys_enter_read { @[comm] = count(); }'这行意思是统计各进程调用 read 的次数，跑几秒 Ctrl+C，直接给你排好序。学会 bpftrace，很多以前要写复杂脚本才能干的事，一行搞定。安装嘛，yum install bcc-tools 或者 apt install bpfcc-tools，脚本一般在 /usr/share/bcc/tools/ 下面。内核版本得 4.9 以上，太老的内核用不了，这是唯一的门槛。perf —— CPU 热点分析的标准答案CPU 飙高但你不知道飙在哪个函数上，perf 就是干这个的。perf top # 实时看当前占 CPU 最多的函数 perf record -F 99 -p 12345 -g -- sleep 30 # 采样 30 秒 perf report # 看结果perf 最强的搭配是火焰图（FlameGraph）。把采样数据生成火焰图，一张图上哪个函数占 CPU 一目了然，越宽的越占。我强烈建议每个运维都把火焰图这套流程练熟，定位 CPU 问题效率翻好几倍：perf record -F 99 -p 12345 -g -- sleep 30 perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > cpu.svg生成的 svg 用浏览器打开，能交互，点哪块放大哪块。gdb —— 程序卡死了就靠它服务卡死、疑似死锁，进程还在但就是不响应，这时候 gdb attach 上去看堆栈：gdb -p 12345 (gdb) bt # 看当前调用栈 (gdb) thread apply all bt # 看所有线程的栈thread apply all bt 这个命令排查死锁贼好用，能把所有线程卡在哪儿全打出来，你一眼就能看到两个线程互相等对方的锁。不过 gdb attach 会短暂暂停进程，生产上慎用，最好在能接受短暂卡顿的时候操作。系统资源全景：先搞清楚是谁在闹排查第一步，永远是先定性——到底是 CPU 满了、内存爆了、还是 I/O 堵了。别急着钻细节。htop / glances / dstattop 那界面看着累，我平时都用 htop，彩色的、能鼠标操作、能看每个核的负载。glances 更全，CPU 内存磁盘网络进程一屏全给你。dstat 我个人最爱，因为它能把好几类指标横着排一行滚动输出，特别适合观察趋势：dstat -tcmndy 1 # 时间 CPU 内存 网络 磁盘 系统状态，一秒刷一次vmstat —— 上下文切换和 swap 的照妖镜vmstat 我几乎每次排查都要跑一下，因为它能看到几个 top 看不到的关键指标：vmstat 1重点盯这几列：cs：上下文切换次数。这个如果异常高，可能是线程太多、锁竞争激烈in：中断数，网络流量大或者硬件问题会飙si / so：swap 换入换出。这俩只要不是 0，你的内存基本就出问题了，机器慢得像卡了我碰到过一次机器莫名其妙巨慢，CPU 内存看着都正常，最后 vmstat 一看 si/so 一直在跳，原来是某进程内存泄漏把物理内存吃光开始用 swap 了，一用 swap 性能直接崩盘。pidstat —— 精确到进程和线程前面工具告诉你「系统级」哪块有问题，pidstat 帮你锁定「具体哪个进程」：pidstat 1 # 各进程 CPU pidstat -d 1 # 各进程磁盘 I/O，谁在刷盘一目了然 pidstat -r 1 # 各进程内存 pidstat -t -p 12345 1 # 细化到线程级别那个 pidstat -d 1 我用得最多，磁盘 I/O 高的时候直接告诉你是哪个进程在读写，比翻半天日志快多了。磁盘 I/O：慢和满是两回事iostat —— 磁盘忙不忙iostat -x 1两个指标必看：%util：磁盘繁忙程度，接近 100% 说明磁盘快到极限了await：I/O 平均响应时间，这个飙高说明磁盘扛不住了不过提醒一句，现在很多 SSD 和 NVMe 盘，%util 到 100% 也不一定是瓶颈，因为它们能并发处理多个 I/O，这个指标在新硬件上要结合 await 一起看，别被单一指标骗了。iotop —— 谁在疯狂读写跟 top 一样的操作逻辑，按 I/O 排序：iotop -oP-o 只显示真正有 I/O 的进程，-P 按进程显示。找刷盘元凶就靠它。lsof —— 「磁盘满了但找不到大文件」的经典解法这个场景我敢说八成运维都遇到过：df -h 显示磁盘满了，du 一顿找却找不到大文件，急得跳脚。原因十有八九是——有进程在写一个大文件，然后这个文件被删了，但进程句柄还占着，空间没释放。这种「幽灵文件」du 看不到，得用 lsof：lsof | grep deleted找到那个 deleted 但还占着空间的文件，对应的进程 kill 掉或者重启，空间瞬间回来。我第一次遇到这问题查了俩小时，知道这招之后再也没被难住过。lsof 还能查端口占用、查进程打开了哪些文件：lsof -i :8080 # 谁占了 8080 端口 lsof -p 12345 # 进程打开了哪些文件fuser —— 强制解除占用想卸载某个目录结果提示 device is busy，就是有进程还占着：fuser -m /data # 谁在用这个挂载点 fuser -k /data # 直接把占用的进程干掉四、网络排查：从连接到抓包ss —— 别再用 netstat 了真心建议把 netstat 忘了，用 ss。ss 直接读内核，netstat 是遍历 /proc，连接数一多 netstat 慢得要死，ss 快好几个数量级。ss -s # TCP 连接状态统计总览 ss -tan | awk '{print $1}' | sort | uniq -c # 统计各状态连接数 ss -tnp state time-wait # 看 TIME_WAIT 连接重点关注两个状态：TIME_WAIT 过多：通常是短连接太多，可以调内核参数优化CLOSE_WAIT 过多：这个更危险，一般是程序 bug，连接没正常关闭，会把文件句柄耗尽。看到 CLOSE_WAIT 堆积赶紧查代码tcpdump —— 抓包大杀器网络层面的疑难杂症，最终都得靠抓包说话。tcpdump 负责在服务器上抓，抓完丢给 Wireshark 可视化分析：tcpdump -i eth0 host 10.0.0.1 and port 8080 -w capture.pcap抓包记得加过滤条件，不然一秒能抓几个 G，硬盘扛不住。抓完把 pcap 文件下载到本地用 Wireshark 打开，追踪 TCP 流、看握手挥手、看重传，问题基本无所遁形。nethogs —— 哪个进程在偷跑流量带宽被打满，想知道是哪个进程干的：nethogs eth0按进程显示实时带宽占用，一目了然，抓「偷跑流量」的进程神器。ip / tc —— 配置查看和故障模拟ifconfig 也建议退休了，现在都用 iproute2 里的 ip 命令：ip a # 看网卡地址 ip r # 看路由tc 更高级，能模拟网络延迟丢包，测试系统在弱网下的表现：tc qdisc add dev eth0 root netem delay 100ms loss 5%这条命令给网卡加 100ms 延迟和 5% 丢包，测完记得删掉，别忘了，不然自己坑自己。curl / nc / telnet —— 连通性快速验证排查连通性，这三个够用了：curl -v http://xxx # 看 HTTP 响应头和完整过程 nc -zv 10.0.0.1 8080 # 测端口通不通 telnet 10.0.0.1 8080 # 老办法测端口curl -v 我用得最勤，加个 -w 还能看各阶段耗时，判断是 DNS 慢、连接慢还是响应慢。五、内核和底层：日志里藏着真相dmesg / journalctl -k —— OOM 和硬件报错第一现场服务莫名其妙被杀了、进程凭空消失了，第一件事就是看内核日志：dmesg -T | tail -50 # -T 显示可读时间 journalctl -k --since "10 min ago"最常见的就是 OOM Killer。内存不够了，内核会挑一个进程杀掉腾内存，被杀的往往是占内存最多的那个,也就是你的核心服务。dmesg 里会明明白白写着「Out of memory: Killed process xxx」。我见过太多人服务被 OOM 杀了还一脸懵，到处找日志找不到,其实答案就在 dmesg 里躺着。除了 OOM，网卡 Link Down、磁盘变只读、硬件报错，全在这儿能看到。numactl / numastat —— NUMA 架构的坑这个偏冷门，但在多路服务器上很关键。NUMA 架构下 CPU 访问本地内存快、访问其他节点内存慢，如果内存分配不均，会出现 CPU 不忙但性能就是上不去的诡异情况：numastat # 看各 NUMA 节点内存分配 numactl --hardware # 看 NUMA 拓扑数据库这类内存敏感的服务，跑在多路机器上时特别要注意 NUMA 绑定，不然性能可能白白损失一大截。一份排查避坑清单踩过的坑总结成几条，你直接记住：别在高并发生产环境无脑上 strace / ltrace，开销太大能把服务拖崩，优先用 eBPF别只看 CPU 使用率，vmstat 的上下文切换、swap 换页同样致命磁盘满找不到文件，先 lsof | grep deleted，别傻乎乎地 du 半天%util 100% 别急着下结论，SSD/NVMe 上要结合 await 判断CLOSE_WAIT 堆积一定是程序 bug，别指望调内核参数解决tcpdump 抓包务必加过滤条件，不然硬盘分分钟被打满tc 模拟故障测完记得删规则，别把自己坑了服务被杀先看 dmesg，八成是 OOM写在最后排查故障说到底，拼的不是你会背多少命令，而是你脑子里有没有一套「分场景往下钻」的思路。CPU 有问题就 perf、火焰图；I/O 有问题就 iostat、iotop、lsof；网络有问题就 ss、tcpdump；服务莫名死了先看 dmesg。工具是死的，思路是活的。我把这些工具按场景整理成了一张速查表和一份火焰图部署脚本，需要的兄弟在公众号后台回复 「排查工具」 就能领。以后再遇到线上故障，对着表挨个排，比抓瞎强一百倍。下一期我打算专门讲讲 eBPF 实战，从零搭一套生产可用的动态追踪体系,感兴趣的记得追更。觉得有用的话，点个赞、转发到你们运维群里,说不定哪天就帮兄弟们少熬一个通宵。有你自己压箱底的排查神器，也欢迎评论区甩出来，咱们互相学习。公众号：耕云躬行录个人博客：躬行笔记
Kafka 监控这块我踩了两年坑，今天把从 JMX 到 Grafana 的全套方案掏给你
我把这两年折腾 Kafka 监控的经验整理出来，从 JMX 指标怎么暴露、Exporter 怎么选、Prometheus 怎么抓、Grafana 面板怎么配，到告警规则怎么写，全流程给你讲清楚。看完你照着搭一套，基本能覆盖 90% 的 Kafka 线上问题。建议先收藏，搭的时候对着看。先说清楚，Kafka 到底该监控啥很多人一上来就问用啥工具，其实工具是最后一步。你得先知道 Kafka 出问题的时候，都是哪些指标先变脸。我自己踩下来，最关键的就这么几类：消费延迟（Consumer Lag），这是重中之重。Lag 就是消息生产了但还没被消费的数量。它一涨，要么是消费者处理不过来，要么是消费者挂了。我开头那次事故，就是 Lag 没监控。Broker 的存活和吞吐。哪个 broker 挂了、每秒进出多少字节、请求处理耗时多少，这些直接反映集群健不健康。Under Replicated Partitions（副本不同步分区数）。正常情况这个值应该是 0。一旦大于 0，说明有分区的副本没跟上，可能是某个 broker 压力大或者网络抖动，这是集群不稳的前兆。Controller 数量。整个集群应该只有一个 Controller，如果监控显示 0 个或者 2 个，那你集群基本要出大事了，脑裂了。磁盘、堆内存、GC。Kafka 是重度依赖磁盘和 PageCache 的，磁盘满了直接写不进去；堆内存和 GC 卡顿会让 broker 响应变慢。先记住这几个，后面配面板和告警都围绕它们来。JMX 是一切的源头Kafka 是 Scala/Java 写的，它的内部指标全部通过 JMX（Java Management Extensions）暴露出来。你想拿到任何一个指标，本质上都是从 JMX 里读的。默认情况下 Kafka 的 JMX 端口是不开的，你得手动开。在启动 Kafka 之前设置一个环境变量：export JMX_PORT=9999或者直接改 kafka-server-start.sh，在里面加上：export KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote \ -Dcom.sun.management.jmxremote.authenticate=false \ -Dcom.sun.management.jmxremote.ssl=false \ -Djava.rmi.server.hostname=你的服务器IP \ -Dcom.sun.management.jmxremote.port=9999 \ -Dcom.sun.management.jmxremote.rmi.port=9999"这里有个坑我提醒一下，java.rmi.server.hostname 这个一定要写对，写成 127.0.0.1 或者不写，远程连 JMX 就会连不上，报一堆莫名其妙的 RMI 错误。我第一次搭的时候在这卡了俩小时，就是因为没配这个 hostname，本地能连远程死活连不上。配好之后重启 Kafka，你可以用 jconsole 或者 jmxterm 连上去验证一下端口通不通。生产环境我一般会写个简单的检查：echo "beans" | java -jar jmxterm.jar -l localhost:9999 -n | grep kafka能看到一堆 kafka.server 开头的 bean，就说明 JMX 正常暴露了。Exporter 怎么选，我给你说实话JMX 数据 Prometheus 是直接读不了的，得有个中间转换的东西，这就是 Exporter。市面上主要有两条路：第一条：JMX Exporter。这是官方 Prometheus 出的通用 JMX 转 Prometheus 工具。它有两种跑法，一种是作为 javaagent 挂在 Kafka 进程里跑，另一种是单独起个进程去连 JMX 端口。我强烈推荐用 javaagent 模式，因为它性能好，直接在进程内读，不用走 RMI。配置方式是在 Kafka 启动参数里加：export KAFKA_OPTS="-javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=7071:/opt/jmx_exporter/kafka.yml"这里 7071 是暴露给 Prometheus 抓取的端口，kafka.yml 是配置文件，里面写你要转换哪些 JMX 指标。这个配置文件是个大工程，因为 JMX 的 bean 名字特别长特别乱，你得写正则把它们映射成规范的 Prometheus 指标名。好消息是官方仓库里有现成的 kafka-2_0_0.yml 配置，直接拿来改改就能用，不用自己从头写。我贴一小段让你有个概念：lowercaseOutputName: true rules: - pattern: kafka.server<type=(.+), name=(.+)PerSec\w*><>Count name: kafka_server_$1_$2_total type: COUNTER - pattern: kafka.server<type=(.+), name=(.+), topic=(.+)><>Count name: kafka_server_$1_$2_total labels: topic: "$3" type: COUNTER第二条：Kafka Exporter（danielqsj 那个）。这个专门针对 Kafka，最大的好处是它能直接算出 Consumer Lag，而且不依赖 JMX，它是通过 Kafka 协议直接连集群拉元数据的。我的实战经验是：两个都上。JMX Exporter 负责 broker 的各种细粒度指标（GC、请求耗时、副本状态），Kafka Exporter 专门管消费延迟。两者互补，谁也替代不了谁。别偷懒只上一个。Kafka Exporter 跑起来很简单：./kafka_exporter --kafka.server=kafka1:9092 --kafka.server=kafka2:9092 --web.listen-address=:9308它默认在 9308 端口暴露指标，其中最重要的就是 kafka_consumergroup_lag，这个指标带着 consumergroup、topic、partition 三个 label，粒度细到每个分区，特别好用。Prometheus 把数据抓过来Exporter 起好了，接下来让 Prometheus 定时去抓。改 prometheus.yml：scrape_configs: - job_name: 'kafka-broker' static_configs: - targets: - 'kafka1:7071' - 'kafka2:7071' - 'kafka3:7071' labels: cluster: 'prod-kafka' - job_name: 'kafka-exporter' static_configs: - targets: ['kafka-exporter-host:9308'] labels: cluster: 'prod-kafka'注意我这里给每个 job 打了个 cluster 的 label，这个习惯建议你养成。如果你公司有多套 Kafka 集群（生产、测试、大数据专用），后面在 Grafana 里就能用这个 label 做集群切换，不然指标全混一块儿你根本分不清哪个是哪个。改完 reload 一下 Prometheus，去它的 Web UI 里 Status → Targets 看看，那几个 target 都是 UP 状态就成了。然后在查询框里敲个 kafka_consumergroup_lag，能出数据，说明整条链路通了。Grafana 面板别自己从零画到 Grafana 这步，很多人喜欢自己一个个 panel 拖，我劝你别。Grafana 官方 dashboard 市场里有大量现成的 Kafka 模板，直接导入 ID 就能用。我常用的几个 ID 给你：7589：Kafka Exporter Overview，专看消费延迟的，简洁实用721：Kafka 集群整体监控，配合 JMX Exporter 用11962：比较全面的 Kafka 大盘导入方式：Grafana 左边 Dashboard → Import → 填 ID → 选你的 Prometheus 数据源 → 完事。导进来之后你八成会发现有些 panel 显示 No Data，这很正常，因为模板里的指标名和你 JMX Exporter 转出来的名字对不上。这时候别慌，点开那个 panel 编辑，看它的 PromQL 用的是啥指标，再去 Prometheus 里搜相近的名字替换掉就行。这个过程有点烦，但比你从零画快多了。我自己最后是拿这几个模板拼了个组合大盘，把最关心的几块单独拎出来放在最顶上：Consumer Lag、Under Replicated Partitions、Active Controller Count、每个 broker 的 Bytes In/Out。一眼就能看出集群状态，不用往下翻。告警规则，这才是能救命的部分面板画得再漂亮，你也不可能 24 小时盯着屏幕。真正能让你少熬夜的是告警。我把这两年沉淀下来的核心告警规则给你，直接抄进 Prometheus 的 rules 文件里。groups: - name: kafka-alerts rules: # 消费延迟过高 - alert: KafkaConsumerLagHigh expr: sum(kafka_consumergroup_lag) by (consumergroup, topic) > 100000 for: 5m labels: severity: warning annotations: summary: "消费组 {{ $labels.consumergroup }} 延迟过高" description: "topic {{ $labels.topic }} 积压已超过 10 万，当前 {{ $value }}" # 副本不同步 - alert: KafkaUnderReplicatedPartitions expr: kafka_server_replicamanager_underreplicatedpartitions > 0 for: 3m labels: severity: critical annotations: summary: "Broker {{ $labels.instance }} 存在副本不同步分区" # Controller 数量异常 - alert: KafkaControllerCountAbnormal expr: sum(kafka_controller_kafkacontroller_activecontrollercount) != 1 for: 1m labels: severity: critical annotations: summary: "集群 Controller 数量异常，当前 {{ $value }}" # Broker 掉线 - alert: KafkaBrokerDown expr: up{job="kafka-broker"} == 0 for: 1m labels: severity: critical annotations: summary: "Kafka Broker {{ $labels.instance }} 掉线"这里有几个我踩过坑的点说一下。Lag 告警的阈值别写死一个数拉倒。我一开始所有 topic 都用 10 万这个阈值，结果有个大流量 topic 平时 Lag 就在几万浮动，隔三差五误报，被半夜吵醒好几次。后来我针对不同 topic 分了级别，高吞吐的放宽，核心业务的收紧。你也可以用变化率来判断，比如 Lag 持续上涨且 5 分钟内涨了多少，这个比绝对值更能反映真实问题。Controller 那条告警别偷懒不配。Controller 数量不等于 1，要么是没有（选主中，短暂正常），要么是脑裂（多个，灾难）。所以我给它 for: 1m，短暂的没有不告警，持续异常才报。up == 0 这个 Broker 掉线告警看着简单，但它是最实在的兜底。哪怕别的都没配，这条你也得有。一次真实的排查，告诉你监控上齐了有多爽上个月又出了一次类似的事，但这次结局完全不一样。那天下午三点多，钉钉群里 KafkaConsumerLagHigh 告警响了，说某个消费组 Lag 在涨。我打开 Grafana 大盘，Lag 曲线确实是一条往上的斜线，但涨得不算特别快。我第一时间看了 Under Replicated Partitions，是 0，Broker 全在线，Controller 也是 1 个，说明集群本身没问题，问题出在消费端。然后我切到 Kafka Exporter 那个面板，按 partition 拆开看，发现只有几个特定分区的 Lag 在涨，其他分区正常。这就基本锁定了——不是消费者整体挂了，是消费能力分布不均，某几个分区的消费实例出问题了。我去看对应消费实例的 JVM 监控（这个我们也接进了同一套 Prometheus），果然发现有个实例 Full GC 频率异常，堆内存快满了，处理速度掉下来了。重启那个实例，Lag 曲线十几分钟就降回去了。整个过程从收到告警到定位根因，也就十来分钟，全程对着面板看，一条命令没敲。这跟我开头那次熬到四点的经历比，差距就是有没有一套能说话的监控。几个千万别做的事搭这套东西的时候，有几个坑我用血泪总结的，你千万别踩：别只监控 Broker 不监控 Consumer Lag。Broker 一切正常但消费积压，业务照样炸，Lag 才是业务最直接的感受。别把 JMX 端口对公网开着。JMX 默认不带认证，暴露公网等于把服务器门敞开了，一定要用防火墙锁死或者只在内网开。别所有 topic 用一个告警阈值，误报多了大家就麻木了，狼来了喊多了真出事没人理。别忘了监控 Exporter 本身。Exporter 挂了你的监控就瞎了，得给 Exporter 也配个 up 告警。别只看 Lag 绝对值忽略趋势，有时候 Lag 高但在稳定下降是正常追赶，误告警反而添乱。最后唠两句Kafka 监控这套东西，说复杂也不复杂，核心就是 JMX 出数据、Exporter 转格式、Prometheus 存和抓、Grafana 展示、Alertmanager 告警，这么一条链路。难点不在工具，在于你知不知道该盯哪些指标、阈值怎么定得既灵敏又不烦人。这些东西没有捷径，都是一次次事故堆出来的经验。我自己的建议是，别等出了事故才想起来搭监控。你现在手头的 Kafka 集群，哪怕就先把 Consumer Lag 和 Broker 存活这两块监控上了，也比裸奔强一百倍。剩下的慢慢完善。这套配置我整理成了完整的文件，包括 JMX Exporter 的 yml、Prometheus 的 scrape 和 rules、还有那几个 Grafana 大盘的 JSON。需要的话在公众号后台回复「kafka监控」，我打包发你，省得你自己一点点抠。如果这篇对你有用，帮我点个赞、转发给身边还在裸奔跑 Kafka 的兄弟，也算是帮他少熬几个夜。下一期我打算讲讲 Kafka 消费积压后怎么快速追平以及分区扩容的那些坑，想看的评论区扣个 1。公众号：耕云躬行录个人博客：躬行笔记
一次 Kafka 集群磁盘写满后，我把这些底层原理全捋了一遍
上个月某天凌晨，告警群里突然炸了：Kafka 集群某台 broker 磁盘使用率 95%，马上要撑爆。我赶紧爬起来处理，登上去一看，日志目录里堆了一堆 .log 文件，大的有几十 G，小的也有几个 G。当时心里挺懵的，Kafka 不是号称"高吞吐、零丢失"吗，怎么磁盘会爆？后来花了半宿才把问题搞定，顺带把 Kafka 底层存储这块东西翻了个底朝天。今天把这次踩坑的收获，还有我整理的一些核心原理全部分享出来，能让你少走不少弯路。Kafka 到底是咋工作的先把 Kafka 的运行机制理一遍，后面讲的所有东西都跟这个有关。Kafka 是个分布式消息系统，由几类角色组成：Producer（生产者）发消息，Consumer（消费者）收消息，Broker 是 Kafka 服务本身，集群就是多台机器起多个 broker 进程。消息按 Topic（主题）分类，一个 Topic 可以分成多个 Partition（分区），分区分布在不同 broker 上。Consumer 启动后加入 Consumer Group（消费组），组内每个 Partition 只被一个 Consumer 消费。早期版本依赖 ZooKeeper 存元数据、协调集群，新版本（2.8+）开始支持 KRaft 模式，可以脱离 ZooKeeper 跑。Kafka 既能当消息队列（解耦、削峰、异步），也能当存储系统（消息持久化、多副本），还能当流处理平台的数据源。不过生产上最常见的用法还是第一种。明白了这些，再看下面的内容就不费劲了。目录结构：分区和分段存储Kafka 的存储是分层的，几个核心概念得搞明白。主题（Topic）是逻辑上的概念，下面分为多个分区（Partition），分区是物理存储的基本单位。每个分区对应磁盘上一个文件夹，文件夹名字一般是 主题名-分区编号，比如 order-topic-0。分区下面不是一个大文件，而是分成了多个日志分段（LogSegment），每个 LogSegment 包含三类文件：.log 文件：实际的消息数据.index 文件：偏移量索引.timeindex 文件：时间戳索引比如你看到这样的目录结构：order-topic-0/ ├── 00000000000000000000.log ├── 00000000000000000000.index ├── 00000000000000000000.timeindex ├── 00000000000000123456.log ├── 00000000000000123456.index └── 00000000000000123456.timeindex每个 LogSegment 默认是 1G（由 log.segment.bytes 控制），当一个 .log 文件写满了，就会滚动生成新的 LogSegment，文件名就是第一条消息的 offset。这就解释了为什么我那台机器磁盘会爆：分区里 .log 文件一个接一个，副本数又是 1，没法通过副本分散压力。消息顺序性的保证：按 Key 路由很多人问 Kafka 怎么保证消息顺序，严格来说，Kafka 只能保证单个分区内的消息是有序的。生产者发消息的时候，如果没有指定分区，会走一个分区器（Partitioner）。Kafka 默认的分区策略是这样的：List<PartitionInfo> partitions = cluster.partitionsForTopic(topic); return Math.abs(key.hashCode()) % partitions.size();也就是说，同一个 Key 的消息一定会进同一个分区，同一个分区内的消息是顺序写、顺序读的。我之前遇到过一个生产事故，某个业务把消息的 Key 随机生成（用的 UUID），结果消息被分散到所有分区，消费者并行消费的时候，消息顺序就乱了。后来把 Key 改成业务主键 ID，问题才解决。所以如果你对消息顺序有要求，一定要保证 Key 是固定的业务标识，比如订单 ID、用户 ID。生产者客户端的两个线程Kafka 的生产者客户端跑得这么溜，靠的是两个线程的协作：主线程和 Sender 线程（也叫发送线程）。主线程负责把消息经过拦截器、序列化器、分区器之后，放到消息累加器（RecordAccumulator）里。注意这个累加器不是简单地把消息扔进去，它内部是按分区分组的，每个分区对应一个双端队列，队列里是一批一批的消息（Batch）。Sender 线程在后台不断轮询，从累加器里把攒够的 Batch 发送到 broker。为什么要攒批？减少网络 IO 次数啊，一批发几十条肯定比一条一条发快得多。这里面有几个关键参数：batch.size：攒批的大小，默认 16KBlinger.ms：最长等待时间，默认 0（不等待）compression.type：压缩算法，默认 none我之前调优过一个项目，把 batch.size 调到 64KB，linger.ms 调到 10ms，吞吐量直接翻了 3 倍。代价是延迟会增加那么一点点，对异步业务来说完全可以接受。消息丢失和重复消费：坑过才知道痛运维最怕的就是消息丢失和重复消费，这两个问题我都被坑过。重复消费的常见原因有这么几个：Rebalance 的时候。比如一个消费者正在处理一条消息，还没处理完，这时候组里加了新消费者，触发 Rebalance，那条消息就被新消费者拿到又处理了一遍。先消费后提交 offset。如果消费完消息，offset 还没提交，服务挂了，重启后这条消息会再被消费一次。生产者重试。生产者发送消息没收到 ACK，会重试，broker 端如果没做幂等控制，就会收到重复消息。消息丢失更可怕，常见原因有：acks 没设置为 all。如果 acks=1，leader 副本写入后就返回 ACK，这时候 follower 副本还没同步，leader 挂了，消息就丢了。消费者先提交 offset 再消费。offset 提交了但消息还没处理完，这时候消费者挂了，消息就丢了。生产者是 fire-and-forget 模式。只管发不管结果，失败了就丢了。针对消息丢失，Kafka 引入了幂等性机制，每个生产者有个 PID（Producer ID），每条消息有个序列号，broker 端会校验，重复的就丢弃。如果还不够用，就上事务，能保证"消费-生产-提交 offset"这三步是原子的。我那次故障之后，就把核心业务的生产者 acks 全改成了 all，retries 调到很大，再加上幂等性，基本上就不怕丢了。ISR、HW、LEO：副本同步原理Kafka 副本同步这块有几个关键概念，新手很容易搞混。每个 Partition 可以配置多个副本（replication），分布在不同 broker 上。副本里有个特殊的角色叫 leader，处理所有读写请求，其他副本叫 follower，只负责从 leader 同步数据。AR（Assigned Replicas） 是分区分配的所有副本，ISR（In-Sync Replicas） 是和 leader 保持同步的副本集合，OSR 是 Out-of-Sync Replicas，就是掉队的那些。leader 副本负责维护 ISR 集合，有个关键参数 replica.lag.time.max.ms，默认 10 秒。如果 follower 副本超过 10 秒没追上 leader，就会被踢出 ISR。HW（High Watermark） 是高水位，消费者只能消费 HW 之前的消息。LEO（Log End Offset） 是下一条要写入的消息位置。打个比方，HW 就是水库的水位线，消费者只能喝到水位线以下的水，LEO 就是当前水库的入水口位置。ISR 集合里所有副本里最小的 LEO，就是整个分区的 HW。这里有个坑，如果 ISR 里的副本都掉线了，Kafka 默认不允许从 OSR 里选 leader（unclean.leader.election.enable=false），这时候分区就不可用了。但如果你把这个参数改成 true，虽然能恢复可用性，但可能会丢数据。高性能的秘密：这几个设计缺一不可Kafka 能扛这么高的吞吐量，靠的是几个关键设计：顺序写盘很多人以为 Kafka 是内存存储，其实它是磁盘存储。但 Kafka 用的是顺序写盘，机械磁盘顺序写盘的速度比随机写内存还快。.log 文件的写入是 append-only 模式，永远在文件末尾追加。零拷贝传统的数据传输要走：磁盘 → 内核缓冲区 → 用户缓冲区 → Socket 缓冲区 → 网卡。Kafka 用 sendfile() 系统调用，数据直接从磁盘到网卡，少了用户态和内核态的来回拷贝，速度快很多。页缓存操作系统会把磁盘上的数据缓存在内存里（这就是页缓存），Kafka 充分利用了这一点。它不维护进程内的缓存，而是依赖操作系统的页缓存，这样重启后缓存还在。批量发送前面说过的攒批机制，一批消息一次网络 IO 搞定。端到端压缩生产者压缩一批消息，broker 存的是压缩后的数据，消费者拿到的也是压缩数据，整个链路不解压，省 CPU 也省带宽。我之前做的性能测试，单 broker 用普通机械盘，顺序写也能达到几百 MB/s 的吞吐量。换成 SSD 更是直接起飞。日志清理策略：磁盘满了不慌回到最开始那个磁盘撑爆的问题，其实和日志清理策略有很大关系。Kafka 有两种日志清理策略，由 log.cleanup.policy 控制：delete（默认）：按时间或大小删除过期日志可以配置这几个参数：log.retention.hours：保留时间，默认 168 小时（7 天）log.retention.bytes：分区最大大小，默认 -1（不限制）log.segment.bytes：单个日志分段大小，默认 1GBcompact：日志压缩，相同 key 只保留最新 value这个适合那种"只关心最新状态"的场景，比如用户配置信息。每个 key 对应的旧版本消息会被清理掉，只保留最新的那条。我当时那个故障，根本原因是没设置 log.retention.bytes，导致分区可以无限增长。设个上限，比如 log.retention.bytes=107374182400（100GB），就保险多了。消费组协调那点事Kafka 的消费者是以消费组（Consumer Group）为单位管理的。一个分区只能被同一个消费组内的一个消费者消费，但可以被多个不同的消费组消费。如果消费者数超过分区数，多出来的消费者就分配不到任何分区，闲在那儿。协调消费者和分区分配的是两个组件：GroupCoordinator：运行在 broker 上，管理消费组ConsumerCoordinator：运行在客户端，和 GroupCoordinator 通信Rebalance 的过程有四个阶段：FIND_COORDINATOR：找到消费组对应的 GroupCoordinatorJOIN_GROUP：所有消费者加入组，选举 leaderSYNC_GROUP：leader 把分区分配方案同步给所有人HEARTBEAT：消费者定期发心跳，保持成员关系Rebalance 期间所有消费者都会停止消费，所以这个过程要尽量快。别随便加消费者，也别让消费者频繁挂掉。我之前有个项目，消费者用的是短链接方式（处理完一条就断开重连），结果 Rebalance 频繁触发，整个消费组基本处于瘫痪状态。后来改成常驻进程，稳稳当当。多线程消费的正确姿势KafkaConsumer 是非线程安全的，一个实例不能被多个线程同时用。有两种多线程消费方案：方案一：每个线程一个 KafkaConsumer 实例最简单也最稳，每个线程自己消费自己的分区，彼此隔离。适合消费逻辑比较重的场景。方案二：单线程拉取 + 线程池处理一个线程负责从 Kafka 拉消息，放到内存队列里，线程池负责处理消息。这样消费逻辑可以并行，但拉取还是单线程的。我一般用方案一，简单粗暴不容易出问题。方案二性能更好，但要处理消息的顺序性、内存队列溢出等问题，复杂度高不少。写在最后Kafka 这东西，上手容易，玩精通难。底层涉及到的东西太多，光是副本同步、存储结构、消费者协调这几块就够啃上一阵。这次磁盘撑爆的事故给我最大的教训是：任何中间件上线前，都要把容量、清理策略、监控这些想清楚。不然半夜被叫起来处理故障，那种感觉太酸爽了。下期我打算把 Kafka 的监控指标体系讲一下，怎么用 JMX 抓数据，怎么配告警规则，这些实战经验全是坑里趟出来的，感兴趣的可以留意。如果你也是踩过 Kafka 的坑，或者对某个点有疑问，欢迎在评论区交流。我是@运维躬行录，专注分享运维、监控、自动化踩坑经验。公众号：耕云躬行录（回复 kafka 领这份《Kafka 运维实战手册》）个人博客：躬行笔记关注我，下期讲《Kafka 监控告警体系搭建：从 JMX 到 Grafana 实战》
tcpdump抓包分析实战：从入门到精通，运维必备的"网络显微镜"
凌晨2点，线上服务突然告警，业务反馈接口超时。监控显示服务正常，防火墙没动，配置也没改。你登服务器查了半天，越查越懵——网络明明是通的，业务就是跑不起来。这种场景我太熟悉了，ping、telnet、netstat 这些老伙计轮番上场，问题依旧悬而未决。最后往往是抓个包一看，才发现是某个TCP重传、或者某个SYN被默默丢弃了。今天这篇，把tcpdump的实战用法彻底讲清楚。文章比较长，建议先收藏，需要的时候翻出来对照着练。认识tcpdump：为什么它这么重要tcpdump是Linux下最经典的网络抓包工具，它工作在网络层，能捕获所有流经网卡的数据包。和wireshark相比，tcpdump是命令行工具，更适合在服务器上直接使用，尤其是在没有图形界面的生产环境中，几乎是唯一选择。我一直把它比作"网络显微镜"——ping只能告诉你通不通，telnet只能告诉你端口开没开，而tcpdump能让你看到每一个数据包的真实面貌。这在排查疑难网络问题时，往往是决定胜负的关键。基础环境准备开始之前，先准备两台虚拟机，配置如下：主机IP系统rocky-web（服务端）192.168.159.167Rocky Linux 9rocky-client（客户端）192.168.159.168Rocky Linux 9安装tcpdump很简单：# CentOS/RHEL/Rocky yum install -y tcpdump # Ubuntu/Debian apt-get install -y tcpdump必会的10个核心命令1. 抓取指定网卡的包最基础的用法，指定网卡开始抓包：tcpdump -i ens160执行后会一直抓取该网卡上的所有流量，按Ctrl+C停止。刚接触tcpdump的同学会一脸懵——屏幕上刷刷刷地滚，根本看不清啥是啥。别急，下面这些过滤参数会帮你精准定位。2. 抓取指定数量的包抓几个包看看就行：tcpdump -c 5 -i ens160-c 5 表示只抓5个包就停止，避免一直刷屏看不清。3. 不解析域名和端口（强烈推荐）tcpdump -nn -i ens160nn参数告诉tcpdump不要把IP解析成域名、把端口解析成服务名。这一条在生产环境几乎必加，原因有两个：一是提升性能，避免DNS查询拖慢抓包；二是显示更直观，看到的端口号就是真实的端口号。4. 显示完整时间戳tcpdump -tttt -i ens160默认时间显示是11:34:19.760275这种只有时分秒的格式，加了-tttt后会变成2025-06-03 13:53:09.891524，排查问题时能精确到毫秒。5. 指定协议类型# 只抓ARP包 tcpdump -i ens160 arp # 只抓TCP包 tcpdump -i ens160 tcp # 只抓UDP包 tcpdump -i ens160 udp # 只抓ICMP包（ping用的就是这个协议） tcpdump -i ens160 icmp排查不同类型的问题，用不同的协议过滤，能极大减少干扰信息。6. 指定端口# 抓取80端口的包 tcpdump -i ens160 port 80 # 抓取多个端口 tcpdump -i ens160 'port 80 or port 443' # 抓取端口范围 tcpdump -i ens160 portrange 8000-9000注意多条件要用引号包起来，避免shell解析出错。7. 指定主机（最常用）# 抓取和特定IP相关的所有包 tcpdump -i ens160 host 192.168.159.168 # 只抓源IP是该IP的包 tcpdump -i ens160 src host 192.168.159.168 # 只抓目标IP是该IP的包 tcpdump -i ens160 dst host 192.168.159.1688. 组合过滤（实战核心）单个条件往往不够用，需要组合：# 抓取从168到167的80端口请求 tcpdump -i ens160 src host 192.168.159.168 and dst host 192.168.159.167 and port 80 # 抓取特定网段 tcpdump -i ens160 net 192.168.159.0/24 # 排除某IP的流量 tcpdump -i ens160 not host 192.168.159.1支持的逻辑运算符有三个：and、or、not，组合起来能应对绝大多数场景。9. 保存到文件（必备技能）# 保存到pcap文件 tcpdump -i ens160 -w capture.pcap # 抓100个包就停止 tcpdump -i ens160 -c 100 -w capture.pcap # 指定端口保存 tcpdump -i ens160 port 80 -w http.pcap-w参数把原始数据包保存成pcap格式的文件，这个文件可以下载到本地用wireshark打开分析。pcap是抓包分析领域的通用格式，相当于网络流量的"录像"。生产环境抓包时，建议先保存到文件，不要直接在终端看。原因很简单：终端刷屏太快根本看不清，而且文件可以反复回看。10. 读取pcap文件tcpdump -r capture.pcap tcpdump -r capture.pcap host 192.168.159.168-r参数用于读取之前保存的pcap文件，可以加上过滤条件做精确分析。这个功能在排查历史问题、回溯故障时特别有用。进阶用法：看懂的"钥匙"光会抓还不够，还要看得懂抓出来的内容。下面我用一个真实场景，把包的结构讲透。场景：抓取HTTP请求在167上部署nginx，修改index.html： Test! Hello the cruel world. 在168上发起请求：while true; do curl 192.168.159.167; sleep 3; done在167上抓包：tcpdump -i ens160 -nn -tttt -vvv dst 192.168.159.168 and port 80参数说明：-nn：不解析域名和端口-tttt：显示完整时间戳-vvv：显示最详细的信息抓出来的包长这样：2025-06-03 16:41:03.735170 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60) 192.168.159.167.80 > 192.168.159.168.48312: Flags [S.], cksum 0xc0cf (incorrect -> 0xf152), seq 1443628272, ack 853270288, win 31856, options [mss 1460,sackOK,TS val 1359281735 ecr 197104783,nop,wscale 7], length 0我来拆解一下这段信息到底在说啥：时间戳：2025-06-03 16:41:03.735170协议类型：IP（IPv4）TOS字段：0x0（服务类型）TTL：64（生存时间，每经过一个路由器减1）包ID：0标志位：[DF] 表示不分片协议：TCP (6)包长度：60字节源地址到目标地址这一段：192.168.159.167.80 > 192.168.159.168.48312意思是167的80端口，发往168的48312端口。> 代表数据流向。Flags [S.] 是最关键的部分——这是TCP标志位：S = SYN（建立连接）. = ACK（确认）P = PSH（推送数据）F = FIN（关闭连接）R = RST（重置连接）[S.] 表示这是SYN+ACK包，是TCP三次握手的第二阶段。后面跟着 seq 1443628272（序列号）、ack 853270288（确认号）、win 31856（窗口大小）等，这些是TCP协议的核心字段。抓取完整HTTP内容想看到HTTP请求和响应的明文内容，用-X参数：tcpdump -i ens160 -nn -tttt -XX dst 192.168.159.168 and port 80-X参数会以十六进制和ASCII两种形式显示包内容，能直接看到HTTP协议头：HTTP/1.1 200 OK Server: Tengine/3.1.0 Date: Tue, 03 Jun 2025 08:41:03 GMT Content-Type: text/html Content-Length: 121 Last-Modified: Tue, 03 Jun 2025 08:05:59 GMT Connection: keep-alive是不是很神奇？连HTTP响应头都能完整看到。HTTPS是加密的看不到，但HTTP的明文内容能看个底朝天——这也是为什么HTTP不适合传输敏感信息。实战案例：TCP三次握手分析理解TCP三次握手是看包的基础。我把一个完整的HTTP请求包序列拆解一下：1. 192.168.159.167.80 > 192.168.159.168.48312: Flags [S.], seq X, ack Y+1 # 第二次握手 2. 192.168.159.167.80 > 192.168.159.168.48312: Flags [.], ack Y+1 # 第三次握手 3. 192.168.159.167.80 > 192.168.159.168.48312: Flags [P.], seq 1:239 # 发送数据 4. 192.168.159.167.80 > 192.168.159.168.48312: Flags [F.], seq 360 # 关闭连接注意这里没有第一次握手，因为用的是dst 192.168.159.168，过滤的是发给168的包，第一次握手是168发给167的。完整的TCP连接生命周期：SYN → 客户端请求建立连接SYN+ACK → 服务端同意建立ACK → 客户端确认PSH+ACK → 传输数据FIN+ACK → 关闭连接故障信号识别：只有SYN没有SYN+ACK → 服务端无响应（端口未开/防火墙拦截）大量SYN+无ACK → 可能是SYN Flood攻击大量TCP重传（Retransmission）→ 网络丢包严重RST包异常增多 → 端口被强制关闭高级过滤：按标志位抓包按TCP标志位过滤是排查特定问题的高级技巧：# 只抓SYN包（排查SYN Flood） tcpdump -i ens160 'tcp[tcpflags] & tcp-syn != 0' # 只抓SYN+ACK包 tcpdump -i ens160 'tcp[tcpflags] & (tcp-syn|tcp-ack) == (tcp-syn|tcp-ack)' # 只抓RST包（排查异常重置） tcpdump -i ens160 'tcp[tcpflags] & tcp-rst != 0' # 抓SYN但排除SYN+ACK tcpdump -i ens160 'tcp[tcpflags] & tcp-syn != 0 and not tcp[tcpflags] & tcp-ack'syn_flood攻击的特征就是短时间内大量SYN包但没有对应的ACK，用这个过滤条件能快速识别。真实故障案例：防火墙导致服务不通某天收到反馈，访问Web服务提示"无法连接服务器"，但服务端进程正常。常规检查全部正常，就是连不上。用tcpdump在服务端抓包：tcpdump -i eth0 port 8080 -w web_capture.pcap下载到本地用wireshark分析，发现客户端SYN包已经到了服务端，但服务端没有任何响应。同步在客户端抓包，看到客户端一直在重传SYN（1s → 2s → 4s 指数退避），最终超时。最后查防火墙规则，发现有人加了这条：iptables -A INPUT -p tcp --dport 8080 -j DROP防火墙把8080端口的包默默丢弃了，连个RST都不回，所以客户端只能干等。临时删除规则后，TCP三次握手瞬间正常。这个案例给我的启示是：用tcpdump能"看到"网络层到底发生了什么，比靠猜要靠谱得多。ping和telnet只能告诉你表象，抓包才能看到真相。配合Wireshark做深度分析tcpdump在服务器上抓包很方便，但分析起来不如wireshark直观。最佳实践是：用tcpdump抓包保存成pcap文件，下载到本地用wireshark分析。wireshark的几个神仙功能：1. Follow TCP Stream（追踪TCP流）右键 → Follow → TCP Stream，能把一次TCP会话的所有数据按顺序完整展示出来，不用自己拼接。2. 过滤器语法wireshark的过滤器比tcpdump更强大，比如：# 只看重传 tcp.analysis.retransmission # 只看丢包 tcp.analysis.lost_segment # 只看乱序 tcp.analysis.out_of_order # 只看RST tcp.flags.reset == 13. Statistics → Conversations看所有会话的统计信息，包括包数、字节数、重传率，一眼就能定位异常会话。生产环境抓包的注意事项最后说几个容易踩的坑，都是我用血泪换来的经验：1. 抓包会消耗CPU高流量场景下抓包非常消耗CPU资源，建议在低峰期抓，或者用-c限制包数量。2. 磁盘空间要预留一个高流量的pcap文件可能几分钟就几个G，抓包前先df -h看看磁盘。我曾经因为没注意磁盘，pcap文件把根分区撑爆，机器直接挂了。3. 多网卡机器要指定网卡服务器一般有多块网卡（业务网卡、管理网卡、存储网卡），用-i指定正确的网卡，不然可能抓到一堆无关流量。4. 权限问题tcpdump需要root权限或CAP_NET_RAW能力。普通用户执行会报permission denied。5. 容器内抓包要特殊处理在K8s容器里抓包比较麻烦，常见做法是用kubectl debug注入一个带tcpdump的临时容器，官方文档有详细的操作步骤。6. 大文件用-W和-C切割# 每个文件100MB，最多保留10个文件 tcpdump -i eth0 -w capture.pcap -C 100 -W 10-C 100表示每个文件100MB，-W 10表示最多保留10个文件，避免磁盘被撑爆。7. 性能优化参数# 增大缓冲区到2MB tcpdump -i eth0 -B 2048 port 80 # 抓完整包（不截断） tcpdump -i eth0 -s 0 port 80默认-s 0只抓每个包的前68字节，看不到完整内容。用-s 0抓完整包，虽然更耗资源，但能看到所有细节。我的tcpdump常用命令速查最后整理一份我日常用得最多的命令清单，建议保存：# 1. 基础抓包 tcpdump -i eth0 # 抓所有包 tcpdump -i eth0 -nn # 不解析域名端口 tcpdump -i eth0 -c 100 # 只抓100个包 tcpdump -i eth0 -tttt # 显示完整时间戳 # 2. 按条件过滤 tcpdump -i eth0 host 192.168.1.1 # 指定主机 tcpdump -i eth0 port 80 # 指定端口 tcpdump -i eth0 tcp # 指定协议 tcpdump -i eth0 'port 80 and host 192.168.1.1' # 组合条件 # 3. 文件操作 tcpdump -i eth0 -w file.pcap # 保存到文件 tcpdump -r file.pcap # 读取文件 tcpdump -i eth0 -w file.pcap -C 100 -W 5 # 文件切割 # 4. 高级过滤 tcpdump -i eth0 'tcp[tcpflags] & tcp-syn != 0' # SYN包 tcpdump -i eth0 'tcp[tcpflags] & tcp-rst != 0' # RST包 # 5. 显示详细信息 tcpdump -i eth0 -v # 详细 tcpdump -i eth0 -vv # 更详细 tcpdump -i eth0 -vvv # 最详细 tcpdump -i eth0 -X # 显示包内容（十六进制+ASCII） tcpdump -i eth0 -s 0 -X # 显示完整包内容结语tcpdump看着简单，但真要用好，需要大量的实战积累。建议大家搭个测试环境，亲手抓一下各种场景的包：正常HTTP请求的包长啥样TCP三次握手的完整过程四次挥手的包序列连接被重置时的RST包网络丢包时的重传包看多了，下次遇到问题就能一眼看出包里的异常。抓包分析能力，是运维工程师的核心竞争力之一，也是面试的高频考点。如果觉得本文有用，欢迎点赞、在看、转发三连，让更多运维兄弟看到。关注公众号【耕云躬行录】，回复"tcpdump"领取我整理的tcpdump速查手册（含常用命令、过滤表达式、故障排查清单）。个人博客：躬行笔记
网络出问题时，我是怎么用流量监控一步步把"凶手"揪出来的
大半夜被电话吵醒那种滋味，干过运维的兄弟都懂。上周五凌晨两点多，我正睡得香，一个电话直接把我从梦里拽出来——某业务系统卡得不行，研发那边查了一通没找到原因，怀疑是网络问题，让我赶紧看看。说实话，接到这种电话第一反应肯定是烦躁，但活儿来了还是得干。我揉着眼睛打开电脑，先远程登录到核心交换机上瞄了一眼端口状态，乍一看没啥问题，端口也没down。但业务反馈就是慢，这就很烦了——肉眼看不到的问题最难搞。先别急着抓包，确定范围很重要很多新手一遇到网络问题就想着抓包分析，这个思路不能说错，但很多时候效率很低。流量分析也好，抓包也好，都是有成本的，尤其是大流量的生产环境，你一上来就在核心链路上开抓包，可能会把链路打满，搞得更糟。我一般会先做一件事：缩小范围。这个业务慢，到底是一个人慢，还是一个部门慢，还是全公司都慢？如果只是某个人慢，那大概率是终端或者他接入的那一段问题；如果是整个部门都慢，那问题可能在上游的汇聚交换机或者防火墙；如果全公司都慢，那基本就是核心设备或者出口链路的问题了。问了一圈下来，发现是研发部整层楼都慢，行政那边正常，市场部也正常。这就基本可以判断问题在研发部那一段的接入层往上。范围一下子缩小了很多。用什么工具看流量？别一上来就上Wireshark说到流量监控工具，很多人第一个想到的就是Wireshark。Wireshark确实强大，协议分析做得非常细致，但它有个问题——太重了。在生产环境尤其是高流量场景下，你不可能在每一台设备上都跑Wireshark实时抓包，那对性能影响很大。我自己的习惯是先从轻量级的开始。比如交换机上看看端口计数，看看有没有CRC错误、丢包、冲突这些指标。大部分硬件厂商的交换机都有这个功能，华为的display interface、華三的display interface brief、思科的show interface都能看到。配置低的设备可能数据不准确，但参考一下还是可以的。这次我先登录到研发部接入层的交换机，敲了display interface，发现有一个端口的input errors和CRC都明显比别的端口高。这个端口下面接的是一台汇聚交换机，基本上可以判断问题点就在这段链路或者这台汇聚设备上。接着我又看了下设备的CPU和内存，CPU飙到90%多，内存也快满了。这就是典型的"交换机累趴下了"的表现。设备一旦CPU高，丢包是必然的，因为报文处理不过来了。抓到证据：上sFlow或者NetFlow看具体流量光看端口计数还是不够细致的。要想知道到底是哪些流量在搞事情，我一般会开sFlow或者NetFlow。这两个东西原理差不多，都是把流量的统计信息（不是原始报文）发到采集器，对设备性能影响很小，特别适合长时间监控。我这边的环境是华为交换机，用的是NetStream（华为对NetFlow的实现）。配置其实不复杂：system-view netstream export ip 192.168.1.100 9995 // 指定采集器地址 netstream export version 9 // 用v9版本 interface GigabitEthernet0/0/24 netstream inbound netstream outbound然后在采集器上我用ntopng来收，这个工具是开源的，部署也简单，装在一台Linux机器上就能用。配置完等个五到十分钟，数据就开始往上报了。打开ntopng的web界面一看，好家伙，有个IP的流量占了整个接口的60%多。再点进去看详情，这个IP发出去的包大部分都流向了一个外部地址。看了下目的端口，是8443。研发那边主要用8443做啥呢？想了想，应该是某个内部系统的接口调用，但这个流量明显不正常，比平时高了几十倍。继续深挖：必要时还是得上Wireshark到这里其实已经基本能定位了，但为了搞清楚到底发的什么内容，我还是抓了个包。抓包我一般不在问题设备上抓，而是在它上游或者下游抓，这样对问题链路的影响小一些。用tcpdump在采集器上抓了几分钟：tcpdump -i eth0 -w /tmp/cap.pcap host 10.1.2.100把抓包文件拖到Wireshark里一分析，发现这个IP在疯狂地往一个外部域名发请求，而且请求的频率高得离谱，1秒钟上百次。正常业务调用不可能这么频繁。再仔细看HTTP头（虽然是8443但内部走了HTTP），这哥们儿居然在循环重试，每次都报504超时。问题就很明显了：业务代码死循环调用某个接口，但接口本身一直超时返回，导致请求越积越多，最终把网络带宽和交换机CPU都吃满了。定位到具体设备，解决问题到这里我基本可以下结论了：研发部业务系统调用外部接口失败代码进入死循环重试逻辑产生大量重复请求把接入层交换机CPU打满导致整个研发部网络卡顿临时解决也很简单——直接在交换机上把这个IP对应的MAC或者接口给shutdown了（或者用ACL deny掉），网络立刻恢复正常。但这只能算止血，真正的根因还得研发那边去查代码逻辑。后来了解到是某个新上的功能，代码里catch了异常之后没有break或者return，直接retry，搞了个死循环。这种问题其实上线前的压测应该能发现，但有时候就是会被漏掉。我常用的几个流量监控工具盘点一下经过这么多年的折腾，我手头常用的一些工具基本固定下来了，给大家列一下：Wireshark：协议分析的王者，没有之一。功能太强大了，过滤器语法用熟了之后简直如虎添翼。比如常用的过滤条件：ip.addr == 10.1.2.100：看特定IP的流量tcp.port == 80 && tcp.analysis.retransmission：看TCP重传tcp.analysis.lost_segment：看丢包http.request.uri contains "api"：看特定URI的请求不过Wireshark对新手不太友好，三次握手那个图就能把很多人看懵。建议初学者先把TCP/IP协议栈搞清楚，再用Wireshark会顺手很多。ntopng：基于NetFlow/sFlow的流量分析工具，我个人非常喜欢它的一点是web界面做得漂亮，能直观看到各种协议的占比、Top Talkers、Top Destinations。部署也简单：# Ubuntu/Debian apt-get install ntopng # 修改配置 /etc/ntopng/ntopng.conf --community // 社区版 --interface eth0 // 监听网卡 --http-port 3000然后浏览器打开 http://服务器IP:3000 就能看到界面了。tcpdump：Linux下的命令行抓包工具，适合在服务器上抓包分析。没有图形界面，所以得配合过滤条件用。常用的：tcpdump -i eth0 -nn -s 0 -c 1000 host 10.1.2.100-i：指定网卡-nn：不解析主机名和端口名-s 0：抓完整报文-c：抓多少包就退出Smokeping：监控网络延迟和丢包的神器，画出来的图非常直观。它会定期往目标IP发包，记录延迟和丢包率。用来判断"链路质量"特别合适，比起手动ping好用太多。配置好后看哪个时间段有尖刺，丢包了，就一目了然。Snort：开源的入侵检测系统，偏向安全方向。它能对流量做实时的模式匹配，发现异常行为就报警。比如SQL注入、扫描行为、异常协议使用等等。我一般在出口或者DMZ区部署一份，能及时发现很多攻击行为。网络排障的一些经验之谈干了这么多年，我发现网络排障这个事情，技术是一方面，思路更重要一些。先全局后局部。遇到问题别一上来就钻细节，先看大面的状态。设备CPU、内存、端口状态、路由表——这些信息看一眼可能就帮你排除了80%的问题。养成记录的好习惯。每解决一个问题，把过程和原因都记下来。下次再遇到类似的问题，搜索一下可能就能直接找到答案。我自己有个文档库，分门别类记各种case，时间长了这就是你最大的财富。善用对比。网络出问题往往伴随着各种指标的变化，但你不一定知道什么是"正常"。所以平时就要把基线数据记录下来，比如交换机的CPU平时都在20%以下，端口流量峰值大概多少，链路延迟多少毫秒。出问题的时候一对比，异常就显而易见了。不要相信单一信息源。比如交换机显示一切正常，但业务就是慢，这时候要怀疑是不是监控本身有问题，或者业务确实没问题但用户访问路径上有别的问题。多源验证是排障的黄金法则。保留现场。在动手改任何配置之前，先把当前状态记录下来（配置、日志、状态信息）。改坏了还能回退，没记录就完蛋了。我有血泪教训，曾经改ACL把整个公司网络搞瘫过半小时，从此之后再也不敢不备份就动手。多跟研发和业务沟通。很多网络问题其实是应用层的问题，但表象在网络。就像我这次遇到的情况一样，如果只盯着网络设备看，永远也找不到根因。多了解业务的变化，比如是不是上了新功能、是不是升级了版本、是不是数据量突然增加了，这些信息对定位问题非常有帮助。写在最后网络排障这件事说难也难，说简单也简单。难的是面对一个完全没见过的场景，从零开始分析；简单的是大部分问题其实都是那些常见的坑，经验丰富的人一眼就能看出大概方向。流量监控是我们做网络排障最重要的武器之一，但工具只是工具，真正的核心还是你对网络协议的理解、对业务流程的理解、以及分析问题的逻辑能力。希望今天分享的这些经验能帮到大家，也欢迎在评论区说说你们遇到过的奇葩网络问题，咱们一起交流学习。如果你觉得这篇内容有点东西，点个在看和转发给身边同样被网络问题折磨的兄弟吧，这也是我持续输出干货的最大动力。关注@运维躬行录，后面会继续分享各种实战排障案例和工具使用技巧，咱们江湖再见！公众号：耕云躬行录个人博客：躬行笔记