古人学问无遗力，少壮工夫老始成。 纸上得来终觉浅，绝知此事要躬行。
DevOps落地翻车？90%团队都栽在这三件事上，附企业级避坑指南
公众号：耕云躬行录个人博客：躬行笔记干运维这么多年，我见过形形色色的"DevOps转型"。有的团队买了Jenkins就以为自己是DevOps了，结果流水线跑得比蜗牛还慢；有的团队搞了K8s集群，结果三天两头OOM，开发运维互相甩锅；还有的团队整天喊"敏捷协作"，但每次上线还是要走半个月的审批流程。说到底，大家对DevOps的理解还是太表面了。DevOps不是某个工具、某个平台，它本质上是一套思维方式+工程实践+文化变革的组合拳。光买工具不改变工作方式，那叫"新瓶装旧酒"，翻车是迟早的事。这篇文章我会把DevOps最核心的几件事掰开揉碎讲清楚：核心原则到底是什么、生产环境怎么部署才稳、出了故障怎么快速定位。全部是生产一线总结出来的经验，建议先收藏再慢慢看。DevOps的五大核心原则，别再被各种"框架"忽悠了讲原则之前先泼盆冷水。市面上讲DevOps原则的，一会儿CAMS、一会儿CALMS、还有什么"三步法""四象限"，听着挺唬人，其实核心就那么几件事。我把它们翻译成"人话"讲给你听。原则一：协作大于分工传统模式下，开发写完代码往运维那一扔，运维负责上线和背锅。出了事就是经典的"在我电脑上是好的啊"。DevOps要解决的就是这个——让开发对自己写的代码在生产环境的表现负责。我之前待过一个团队，引入SRE机制后，每个服务都有明确的Owner，开发要参与OnCall轮值，得背告警电话。这么一搞，开发写代码明显谨慎多了，因为他们知道半夜三点会被叫起来处理事故。这种"切身之痛"比任何培训都管用。实操建议：建立跨职能的"产品团队"（含开发、测试、SRE、产品），一个团队对服务全生命周期负责设立共享的OnCall值班表，开发和运维轮流上故障复盘（Blameless Postmortem）必须双方共同参与，重点是改进流程而不是追责原则二：自动化是一切的基础这里有个很反直觉的认知：自动化不是为了省事，而是为了减少人为错误。想象一下，你手动部署100次都没出问题，但第101次手抖打错了一个参数，半夜生产挂了，老板问你"为啥不自动化"，你怎么答？所以自动化不是可选项，是强制项。我现在的标准是：能脚本化的一律脚本化，能流水线化的一律流水线化。代码提交触发自动构建、自动测试、自动部署，整条链路全部留痕，谁改的、什么时候改的、改了什么，一查就知道。具体实践：CI（持续集成）：代码合并到主干自动触发构建和单元测试，推荐工具GitLab CI、Jenkins、GitHub ActionsCD（持续交付/部署）：构建产物自动部署到测试、预发环境，生产环境灰度发布IaC（基础设施即代码）：用Terraform或Pulumi管理云资源，用Ansible管理配置，杜绝手动改服务器GitOps：把环境配置也纳入Git管理，通过ArgoCD或Flux自动同步集群状态原则三：持续反馈，快速迭代这条原则很多人会忽略。DevOps的精髓在于"快速失败、快速学习"，不是"避免失败"。举个真实案例。我们之前有个支付服务，每次大促都会出问题。后来我们引入了混沌工程（Chaos Engineering），故意在生产环境注入故障，比如随机杀Pod、模拟网络延迟。刚开始团队都慌得不行，但跑了半年之后，大家发现系统的韧性提升了一大截，真实的故障率反而下降了很多。这就是反馈的价值。你不主动制造故障去测试系统，线上就会给你一个大惊喜。原则四：以终为始，价值导向很多团队搞DevOps陷入了"工具陷阱"，天天研究Jenkins怎么配、ArgoCD怎么玩，但忘了问自己：这些能给我们带来什么业务价值？DevOps的KPI不是"部署了多少次"，而是"多快把新功能交付给用户"和"多快修复生产问题"。DORA四大指标（部署频率、变更前置时间、变更失败率、MTTR）才是衡量DevOps成熟度的硬标准。Google那些顶级团队能做到一天部署几千次，但他们不是为了刷数字，而是这种能力让产品迭代速度甩开对手几条街。原则五：持续学习与改进这条是最虚但也最重要的。技术迭代太快了，三年前的主流方案到今天可能就过时了。团队必须建立持续学习的机制。我们团队的做法是每周一次技术分享，每人轮流讲一个技术点或踩坑经历。然后每月一次"故障复盘会"，把当月的生产事故摆出来，用"五问法"深挖根因。这么坚持一两年，整个团队的技术水位会有质的飞跃。生产环境部署方案：2024年企业级最佳实践讲完原则，接下来上硬菜——生产环境部署。很多团队一上来就搞蓝绿部署、灰度发布，方案看着高大上，但落地一塌糊涂。原因很简单：没有最完美的方案，只有最适合的方案。下面我按场景给你拆解。场景1：传统应用，单体架构对于这种场景，我建议滚动更新（Rolling Update）就够了，没必要过度设计。原理很简单：K8s的Deployment默认就是滚动更新策略，你只需要配置好maxSurge和maxUnavailable参数。比如10个Pod，你可以设置maxSurge=2，maxUnavailable=1，意思是更新过程中最多新建2个新Pod，最多有1个老Pod不可用。apiVersion: apps/v1 kind: Deployment metadata: name: web-app spec: replicas: 10 strategy: type: RollingUpdate rollingUpdate: maxSurge: 2 maxUnavailable: 1 template: spec: containers: - name: web image: web:v1.2.0场景2：微服务架构，对可用性要求极高这种场景推荐蓝绿部署或者金丝雀发布（Canary Release）。蓝绿部署的思路是：部署两套完全一样的环境（蓝和绿），当前流量走蓝环境，新版本部署到绿环境，验证没问题后一键切换流量。如果新版本出问题，切回蓝环境，回滚秒级完成。金丝雀发布更精细一点：先放5%的流量到新版本，观察一段时间没问题，再放到20%、50%、100%。这样即使新版本有bug，影响范围也只在5%的用户。我们用Argo Rollouts做金丝雀发布，配置大概是这样的：apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: name: web-app spec: replicas: 10 strategy: canary: steps: - setWeight: 5 - pause: {duration: 5m} - setWeight: 20 - pause: {duration: 5m} - setWeight: 50 - pause: {duration: 5m} - setWeight: 100每一步都暂停5分钟，这期间通过Prometheus和Grafana观察错误率、响应时间、P99延迟等关键指标。如果某一步指标异常，自动回滚；都OK就继续推进。场景3：金融、政企等强合规场景这种场景推荐GitOps + 审批流。所有环境变更都通过Git PR触发，PR必须经过代码审查和SRE审批，合并后ArgoCD自动同步到集群。整个过程完全可追溯、可审计，出了问题能精确到具体哪一行配置改坏了。我们的实践是：ArgoCD + SealedSecrets（管理敏感信息） + Kyverno（策略引擎）。这样既能保证效率，又能满足合规要求。部署方案的几个常见坑：没有健康检查。Pod启动就认为部署成功，结果应用还在初始化，流量打过来直接报错。一定要配置readinessProbe和livenessProbe，让流量控制器知道什么时候该把流量切过来。忽略数据库变更。应用可以无状态部署，但数据库schema变更怎么办？建议用Flyway或Liquibase做版本化迁移，并且先迁移数据库，再部署应用，避免新老版本schema不兼容。没设置资源限制。每个Pod必须设置resources.requests和resources.limits，不然一个Pod把节点资源吃光，整个集群跟着遭殃。缺乏回滚预案。发布前必须有"一键回滚"的预案，出了事能3分钟内回滚到上一个版本，而不是让运维在凌晨三点满头大汗地翻日志。生产环境故障排查：SRE实战方法论这部分是我最想讲的，因为生产故障是检验一个团队成色的唯一标准。讲排查之前，先讲一个重要的思维转变：故障不是异常，而是常态。分布式系统一定会出故障，区别只在于你能不能快速发现、快速定位、快速恢复。Google的SRE团队有个经典的理念叫"Error Budget（错误预算）"——如果这个月SLO是99.9%，那就有0.1%的故障时间预算。用完了这个预算，所有变更必须暂停，全团队集中精力提升稳定性。这种思维比"零故障"靠谱多了，因为"零故障"是不存在的，只会逼着团队掩盖问题。排查方法论：USE方法 + RED方法这是我从《SRE：Google运维解密》里学到的，配合Prometheus用非常顺手。USE方法（针对资源）：Utilization：资源使用率（CPU、内存、磁盘、网络）Saturation：资源饱和度（队列长度、等待时间）Errors：错误事件计数RED方法（针对服务）：Rate：请求速率Errors：错误率Duration：响应时间（特别是P95、P99）这两个方法配合起来，基本能覆盖80%的故障场景。具体的仪表盘在Grafana里都有现成的模板，导入就能用。真实排查案例讲个我们遇到的真实案例。有天晚上11点，告警群炸了，订单服务的错误率突然飙升到30%。我打开Prometheus一看，错误率是突增的，不是缓慢上升——这意味着不是资源耗尽（那种一般是渐进的），而是某个特定原因触发的。接下来按RED方法排查：Rate（请求量）：正常，没有流量激增Errors（错误率）：30%，主要错误是"500 Internal Server Error"Duration（响应时间）：P99从200ms飙升到5s然后查日志，发现错误日志里大量出现"connection timeout"和"connection refused"，指向下游的支付服务。接下来在Grafana里看支付服务的健康状态——果然，支付服务的某个Pod频繁重启，OOMKilled。最后深挖下去，发现是某个新上线功能有内存泄漏，长时间运行后内存被打满，Pod被系统杀掉。整个排查过程用了大概15分钟。这就是好的可观测性体系的价值：让运维在凌晨三点也能像白天一样清醒地定位问题。生产故障排查的工具链（2024最新版）：类别工具用途指标监控Prometheus + Grafana收集和展示时序指标日志收集Loki + Promtail 或 ELK集中式日志管理链路追踪Jaeger 或 Tempo分布式调用链追踪健康检查Blackbox Exporter主动探测服务可用性告警Alertmanager告警去重、分组、路由持续剖析Pyroscope 或 Parca定位性能瓶颈代码这里特别推荐一下OpenTelemetry（OTel）。这是CNCF的可观测性标准，可以统一指标、日志、追踪三种数据的采集，避免你被各种agent搞得头疼。未来的趋势一定是OTel一统江湖，建议新项目直接接入。混沌工程：主动制造故障前面讲过混沌工程，这里展开讲讲。我们用的是Chaos Mesh（K8s生态下比较成熟的工具），定期在测试环境做以下演练：随机杀Pod注入网络延迟（500ms）模拟CPU打满模拟磁盘IO异常模拟DNS故障每次演练都会生成一份报告，列出系统在压力下的表现，以及需要改进的点。半年下来，团队应对故障的能力会有质的提升。OnCall机制：让背锅变得有序OnCall不是"谁倒霉谁值班"，而是一个系统性的工程实践。我们的做法是：值班轮换表：SRE团队每周轮换，确保公平告警分级：P0（5分钟响应，影响核心业务）→ P1（30分钟响应）→ P2（工作时间处理）值班手册（Runbook）：每个常见告警都有对应的处理步骤，新人照着做也能搞定值班补贴：半夜处理事故有补贴，体现对值班人员的尊重强制休息：值班后第二天可以晚到或者调休，避免疲劳作战故障复盘：避免重复犯错每次P0/P1故障后，必须做Blameless Postmortem（无指责复盘）。格式大概是这样的：事故时间线：从告警到恢复的每一步根因分析：用5 Whys方法深挖影响范围：影响了多少用户、多少订单改进措施：每一条都要有明确的Owner和Deadline我们坚持了两年，重复发生的故障几乎降到了零。写在最后DevOps这条路没有银弹，没有一套方案能适用于所有公司。但核心思路是相通的：把人与人、人与系统、系统与系统之间的摩擦降到最低。文化层面，建立跨职能团队，让开发对生产负责；流程层面，自动化一切能自动化的环节；技术层面，建设完善的CI/CD、可观测性、混沌工程体系；心态层面，把故障当老师，把复盘当修炼。这套东西做下来，初期会很痛苦，但坚持半年到一年，你会明显感觉到团队战斗力的提升——部署不再心惊胆战，告警不再半夜惊醒，出了事故15分钟定位根因。这种感觉，谁用谁知道。如果这篇文章对你有帮助，欢迎转发给你身边做运维、做开发的朋友，大家一起避坑、一起进步。想第一时间看到更多一线运维实战经验、踩坑总结、企业级方案拆解，赶紧关注@耕云躬行录吧！后续会持续输出硬核内容，包括K8s深度实践、可观测性体系建设、SRE进阶路径等等。公众号：耕云躬行录个人博客：躬行笔记咱们江湖再见👋
TIOBE地址：https://www.tiobe.com/tiobe-index/干了这么多年运维，PHP这门语言我估计是绕不开的。你现在随便打开一个网站，大概率后端就跑着PHP。可你要是去TIOBE排行榜看一眼，会发现这哥们已经从当年的第一名跌到第14名了。这到底是PHP真的凉透了，还是说它只是换了一种方式"存在"？今天咱们就一起把这事掰扯清楚。我今天想聊三件事：一个是全世界到底有多少项目在用PHP、PHP到底过没过时；再一个是PHP-FPM配置文件的那些核心参数到底是什么意思、应该怎么调。这两块内容网上资料很多，但讲得都比较零散，特别是PHP-FPM配置这块，很多人都是照着网上的模板抄一遍，根本不知道每个参数到底干嘛的。我争取这次给大家讲明白。全世界有多少项目在用PHP？先说几个数字镇场子。根据W3Techs的统计，截至2023年7月，全世界77.4%的网站使用PHP。这个比例说实话相当恐怖，意思是每4个网站里有3个跑的都是PHP。这还没完，更早的2013年4月数据显示，PHP已经被安装在超过2.44亿个网站和210万台服务器上。这么多年过去了，这个数字只会更大不会更小。再来看看Composer包管理器的数据。截至2023年7月，每个月Composer包安装次数在2000万左右，Composer包的总量在37.5万个，不同版本加起来400万个。这生态规模，说它"凉了"我是不信的。再看版本分布情况。2024年有86%的PHP开发者已经在使用PHP 8，但截至2025年6月，PHP 8.3占了34.0%的份额，8.2占24.8%，8.4从2025年1月首次出现的5.1%涨到了6月份的13.7%。可以看出来大家都在慢慢往新版本迁移。那为什么TIOBE排行榜上PHP却跌到第13名了呢？我觉得这事儿得这么看。TIOBE反映的是"热度"和"讨论量"，而W3Techs反映的是"实际部署量"。两者统计维度完全不一样。PHP这两年讨论度确实不高，但谁让它是"闷声发大财"的代表呢。WordPress、Laravel、Symfony这些老牌项目依然在生产环境稳稳跑着。Slack和Tumblr这种大型平台的后端核心也都在用PHP，人家一个月150亿次页面浏览量可不是闹着玩的。所以结论是：PHP没过时，只是没以前那么"高调"了。PHP过时的讨论说到这个，我得引用一下JetBrains 2025开发者生态报告里的观点：PHP、Ruby和Objective-C正处于长期下滑趋势，而Rust、Go和Kotlin则继续保持缓慢且稳定的上升态势。下滑归下滑，但PHP的基数摆在那儿。我打个比方，这就好比一个曾经年级第一的学霸，现在成绩掉到了中游。你说他"凉了"吗？并没有，只是周围的人追上来了而已。而且PHP 8.0引入JIT之后，性能提升是实打实的，PHP 8.4又搞了属性钩子、非对称可见性这些新特性，PHP语言本身并没有停止进化。还有一点很重要，PHP基金会的存在。2021年11月JetBrains、Automattic、Laravel、Acquia等公司一起成立了PHP基金会，专职维护PHP语言的发展。这说明大厂们依然愿意为PHP的长期发展买单。所以我个人观点是：PHP在企业级Web开发、内容管理、电商平台这些领域依然非常能打；但在机器学习、实时高并发系统这些新兴领域，PHP确实不是最优选。工具是用来解决问题的，PHP能解决它擅长领域的问题，那就没"过时"这一说。PHP-FPM是什么？为什么要优化它？铺垫完了，接下来才是重头戏。PHP-FPM全称是PHP FastCGI Process Manager，说人话就是PHP的进程管理器。它在PHP 5.3.3之后被内置进了PHP源码，负责接收来自Nginx或Apache的请求，然后丢给PHP解释器去执行，最后把结果返回回去。为什么要优化它？很简单。一台服务器的资源是有限的，PHP-FPM如果配置得不好，要么就是浪费资源（进程开太多），要么就是响应慢甚至502（进程不够用）。这两个极端我都见过不少次。PHP-FPM的配置文件主要有三个层级：php-fpm.conf：全局配置www.conf（或其他pool文件）：进程池配置php.ini：PHP自身的配置（这个影响FPM里跑的所有PHP脚本）接下来我把这几个文件里最关键的参数一个个掰开讲。php-fpm.conf全局配置先说全局配置。这一块主要是控制FPM主进程和日志的行为。pid —— 用来指定PID文件的位置，默认是空的。一般会写/run/php-fpm/php-fpm.pid这种路径，方便后面用systemd管理服务时读取进程号。error_log —— FPM主进程的错误日志路径，默认是/usr/local/php/var/log/php-fpm.log。注意，子进程的错误日志一般通过php.ini里的error_log配置，别搞混了。log_level —— 日志级别，有alert、error、warning、notice、debug五种。生产环境一般用notice就够了，如果排查问题可以临时调到debug或warning。emergency_restart_threshold 和 emergency_restart_interval —— 这俩是搭配使用的。意思是如果FPM在emergency_restart_interval时间内，子进程收到超过emergency_restart_threshold次数的SIGSEGV或SIGBUS信号（说人话就是子进程频繁崩溃），FPM主进程就会自动重启。这是个保命参数，建议打开，比如emergency_restart_threshold = 10，emergency_restart_interval = 1m。process.max —— FPM能fork出来的最大进程数。用动态模式（后面会讲pm = dynamic）的时候，这个值是全局限制，也就是所有pool加起来不能超过这个数。用static模式的时候这个值是不生效的，所以一般保持默认的0就行。daemonize —— 是不是以守护进程方式跑。生产环境一般是yes，但如果用systemd管理的话建议设成no，这样systemd能直接管理主进程，出问题排查也方便。rlimit_files —— 主进程能打开的文件描述符数量限制。生产环境这个值可以调大一点，比如65535，避免出现"too many open files"的问题。进程池配置（pool配置）——这部分是重点这一块是性能调优的核心，生产环境80%的优化都集中在这里。pm 进程管理模式这个参数是灵魂，一共三个选项：static：固定子进程数，进程数量从头到尾都不变。适合流量稳定的场景，比如内部系统、API服务。dynamic：动态调整子进程数，根据流量自动扩缩容。适合流量有波峰波谷的场景，这是生产环境最常用的模式。ondemand：按需启动，平时没有请求时子进程会被回收掉，请求来了再启动。适合流量很小或者调试用的场景。我的建议是：大多数生产环境用dynamic就对了。pm.max_children 最大子进程数这是最重要的参数。它定义了这个pool最多能开多少个PHP-FPM子进程。每个子进程大约占用20-40MB内存（看你的PHP代码和加载的扩展），所以这个值的计算公式是：pm.max_children = 可用内存 * 0.7 / 单个进程平均占用内存举个例子，服务器有8GB内存，扣除系统、数据库、Redis等其他进程开销后剩4GB给PHP-FPM，单个进程按30MB算，那就是4 * 1024 / 30 ≈ 136。所以这个值一般设个100左右比较稳妥。别设太大，否则内存爆了直接OOM。pm.start_servers 启动时的子进程数只在dynamic模式下生效。意思是FPM启动的时候先创建多少个子进程。默认是(min_spare_servers + max_spare_servers) / 2。一般建议根据平峰流量来设置，避免流量一来还要临时fork进程导致响应慢。pm.min_spare_servers 和 pm.max_spare_servers 空闲进程上下限也是只在dynamic模式下生效。min_spare_servers：最小空闲进程数，如果空闲进程低于这个值，FPM会fork新进程。max_spare_servers：最大空闲进程数，如果空闲进程超过这个值，多余的进程会被回收。举个实际例子：min设10，max设30。意思就是空闲进程最少留10个，最多留30个，超出的杀掉。调优经验：min值要保证流量来的时候有足够"预备队"，max值不能太大，否则浪费内存。常见的组合是min=20、max=50、max_children=100这种。pm.max_requests 每个子进程最大请求数这个参数极其重要但很多新手会忽略。它定义了一个子进程在处理多少个请求后会被自动重启。为什么要重启？因为PHP进程在处理请求的过程中，内存可能会泄漏（特别是用了一些老旧扩展或框架）。如果不定期重启，进程占用的内存会越来越大，最终拖垮服务器。一般建议设成500-2000之间，具体看你应用的稳定性。如果应用代码质量好，可以设大点；代码一般的话就设小点保平安。设成0是禁用这个功能，生产环境千万别这么干。request_terminate_timeout 请求超时时间单个请求的最长执行时间，单位默认是秒。这个和php.ini里的max_execution_time有点像，但这个更狠——它会直接杀掉进程。一般设个30s或60s就行。如果超过这个时间请求还没完成，就直接干掉，释放进程资源给别的请求用。生产环境强烈建议打开，不然一个慢请求就能把进程卡住。request_slowlog_timeout 和 slowlog 慢请求日志这俩参数也是一对。如果一个请求的执行时间超过了request_slowlog_timeout设置的时间，FPM就会把这个请求的完整调用栈记录到slowlog指定的文件里。这是个排查性能问题的神器。比如你设成request_slowlog_timeout = 5s，slowlog = /var/log/php-fpm/www-slow.log。然后定期去翻这个慢日志文件，就能找到哪些接口、哪些代码执行得慢。listen 监听地址这个参数定义FPM监听在哪里接收请求。三种写法：TCP：listen = 127.0.0.1:9000Unix Socket：listen = /run/php-fpm/www.sock同机部署Nginx+PHP-FPM的话，强烈建议用Unix Socket，比TCP性能高一些，因为少了TCP协议栈的开销。listen.backlog 监听队列长度这个参数是listen(2)的backlog值，控制未完成连接队列的最大长度。默认值在Linux上是511。这个值的意思是：当FPM进程都在处理请求时，新来的连接请求会排队等。队列长度超过这个值，新连接就会被拒绝。如果你的Nginx配置用的是TCP方式连接FPM（比如fastcgi_pass 127.0.0.1:9000），这个值一定不能太小。否则高并发的时候客户端会报"connect to upstream: Connection refused"或者502错误。生产环境一般设到511或更高。同时要检查系统的somaxconn参数：sysctl -w net.core.somaxconn=4096，让内核也允许更大的队列。6.10 access.log 和 access.format 访问日志这两个参数用来开启FPM的访问日志。默认是关闭的。access.log = /var/log/php-fpm/www-access.log 指定日志文件路径。access.format 控制日志格式，默认是"%R - %u %t \"%m %r\" %s"。可以按需修改，比如加上响应时间%d、内存使用%M这些。注意：开启访问日志会有一定的IO开销，对性能有要求的环境慎开。php_admin_value 和 php_value —— 配置覆盖这个是PHP-FPM的"特殊功能"，可以在pool配置文件里覆盖php.ini里的设置。php_admin_value[memory_limit] = 256M php_flag[display_errors] = offphp_admin_value 和 php_value 的区别是：php_admin_value 设置的值，PHP代码里用ini_set()改不了（安全）php_value 设置的值，PHP代码里可以用ini_set()改（不推荐用于生产）生产环境建议用php_admin_value，避免被代码逻辑改回不安全的值。配置示例最后给一套生产环境推荐配置，大家可以参考：[www] user = www group = www listen = /run/php-fpm/www.sock listen.backlog = 511 listen.owner = www listen.group = www listen.mode = 0660 pm = dynamic pm.max_children = 100 pm.start_servers = 20 pm.min_spare_servers = 10 pm.max_spare_servers = 30 pm.max_requests = 1000 request_terminate_timeout = 60s request_slowlog_timeout = 5s slowlog = /var/log/php-fpm/www-slow.log php_admin_value[memory_limit] = 256M php_admin_value[error_log] = /var/log/php-fpm/www-error.log php_admin_flag[log_errors] = on php_admin_flag[display_errors] = off catch_workers_output = yes decorate_workers_output = no故障处理实战先把日志位置捋清楚排查之前，得先知道去哪看日志。PHP-FPM相关的日志主要有这么几个：日志类型默认位置作用FPM主进程错误日志/var/log/php-fpm/error.logFPM本身启动、运行报错Pool错误日志php-fpm.conf里error_log子进程执行PHP脚本时的错误慢请求日志pool里slowlog配置项执行超时的请求调用栈Nginx错误日志/var/log/nginx/error.log反代PHP-FPM失败的信息系统日志/var/log/messagesOOM、进程被杀等系统事件重要提醒：很多人会漏掉Nginx错误日志。其实Nginx作为反向代理，它把请求转发给PHP-FPM时，如果FPM那边出了问题，错误信息会先打到Nginx日志里。所以排查的时候两个日志要一起看。网站打不开的排查套路场景描述：用户反馈网站访问报502 Bad Gateway，或者一直转圈圈最后超时打不开。我把这个排查流程总结成一个四步法，照着走基本都能定位到问题。第一步：先看进程在不在ps aux | grep php-fpm正常情况下你应该能看到一个主进程 + N个子进程（N就是pm.max_children配置的数量）。如果主进程都没了，那说明FPM整个挂了，直接去看FPM的主错误日志。如果子进程数量=0但主进程在，那就是子进程都死光了，赶紧看子进程的错误日志。第二步：看监听端口或socket在不在# TCP方式 ss -tlnp | grep 9000 # Unix socket方式 ls -l /run/php-fpm/www.sock监听不上的常见原因有三个：FPM没启动或正在重启socket文件权限不对，Nginx用户读不到listen.backlog被占满，新连接进不来第三步：看Nginx错误日志tail -f /var/log/nginx/error.log重点关注这几类错误：connect() failed (111: Connection refused) while connecting to upstream —— FPM没起来或端口不通connect() to unix:/run/php-fpm/www.sock failed (2: No such file or directory) —— socket文件不存在upstream timed out (110: Connection timed out) —— FPM处理请求超时no live upstreams while connecting to upstream —— 所有FPM子进程都挂了第四步：看系统资源free -h # 内存 top -c # CPU df -h # 磁盘 ulimit -n # 文件描述符到这里基本能定位到80%的问题了。一个真实的502排查案例我之前遇到过一次比较典型的案例，分享一下。某天上午10点，业务突然开始502，监控告警一下子炸了。SSH连上去之后，我先看了下Nginx错误日志，满屏都是：connect() to unix:/run/php-fpm/www.sock failed (11: Resource temporarily unavailable) while connecting to upstream注意这个错误码，11 = EAGAIN，意思是socket队列被占满了。我马上意识到这是listen.backlog的问题。接着去看了下FPM状态：curl http://127.0.0.1:9000/status # 如果开了status页的话输出显示active processes = 100，正好等于pm.max_children。也就是说所有子进程都被占满了，新的请求根本排不上队。根因找到了：那天正好有个新功能上线，某个接口因为代码bug导致每个请求都要执行20多秒，直接把FPM的子进程全占满了。后来临时调大了pm.max_children（从100调到200），同时让开发紧急修复了那个慢接口，问题才彻底解决。但这只是临时止血。长期方案是开启request_terminate_timeout，让慢请求自动超时释放进程。这个参数我上一节讲过，生产环境一定打开。进程卡死怎么排查？这个比502还头疼。502好歹有个错误码，进程卡死就是请求一直挂着不返回。下面讲讲我的排查思路。场景一：单进程卡死，其他进程正常这种情况一般是某个请求陷入了死循环、阻塞IO、或者死锁。排查命令：# 找出执行时间最长的FPM子进程 ps -eo pid,etime,pcpu,pmem,cmd | grep php-fpm # 用strace跟踪这个进程在干啥 strace -p <PID> # 也能用lsof看进程打开了哪些文件/网络连接 lsof -p <PID>如果strace输出一直在显示futex、read、recvfrom之类的系统调用卡住，说明进程在等某个资源。这时候根据不同的系统调用能判断问题：卡在futex —— 死锁或者锁竞争卡在read —— 读文件或读网络慢卡在recvfrom —— 等某个外部接口返回场景二：所有进程都卡死这种情况一般是全局性的问题，比如：数据库挂了，所有请求都在等数据库连接池远程API挂了，所有请求都在等HTTP返回磁盘满了，写不了日志内存爆了，开始疯狂swap排查命令：# 看整体负载 uptime top -c # 看是不是OOM了 dmesg | grep -i "killed process" # 看磁盘和inode df -h df -idmesg这个命令特别重要。如果系统因为内存不够杀掉了进程，会在这里留下"Out of memory: Killed process xxxx (php-fpm)"的记录。我之前碰到过好几次FPM莫名奇妙消失，SSH一查dmesg就全明白了。慢日志的使用技巧上一节讲过slowlog这个参数，这一节讲讲怎么用好它。第一步：开启慢日志request_slowlog_timeout = 5s slowlog = /var/log/php-fpm/www-slow.log第二步：定期去翻这个文件# 找出出现频率最高的慢请求 awk '{print $NF}' /var/log/php-fpm/www-slow.log | sort | uniq -c | sort -rn | head -20第三步：看调用栈定位问题慢日志里会记录PHP的完整调用栈，类似这样：[0x00007f8b1c2a3638] file_get_contents() /var/www/html/app/Service/ApiService.php:42 [0x00007f8b1c2a35e0] ApiService->callThirdParty() /var/www/html/app/Controller/OrderController.php:78 [0x00007f8b1c2a3450] OrderController->create() /var/www/html/public/index.php:25一眼就能看出是ApiService->callThirdParty()这个方法慢，可能是调用了某个外部API。几个救命的小命令最后再分享几个生产环境救命的小命令，都是真用过的：1. 看FPM进程状态（如果开了pm.status_path）在www.conf里加上：pm.status_path = /statusNginx里配一下转发：location /status { fastcgi_pass unix:/run/php-fpm/www.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $fastcgi_script_name; allow 127.0.0.1; deny all; }然后就能看到这些关键信息：curl http://127.0.0.1/status输出里关注这几个值：accepted conn：累计接收的连接数active processes：当前活跃进程数idle processes：当前空闲进程数total processes：总进程数max active processes：历史最大活跃进程数slow requests：慢请求数2. 一键看FPM子进程都在干啥# 看每个子进程在处理什么请求 ps -ef | grep "php-fpm" | grep -v grep | awk '{print $2}' | xargs -I {} sh -c 'echo "PID: {}"; cat /proc/{}/cmdline 2>/dev/null; echo'如果开了pm.status_path+/status?full，还能看到每个子进程正在处理的请求URL。这在排查某个慢请求占着进程不放的时候特别有用。排查思路总结最后用我自己的经验把排查流程串一下：网站打不开 —— 先看进程在不在 → 看监听在不在 → 看Nginx错误日志 → 看系统资源网站慢 —— 看FPM状态页的活跃进程数 → 翻慢日志定位慢请求 → 抓典型慢请求的strace进程卡死 —— strace跟踪卡在哪 → 看是不是等数据库/外部API → 看是不是死锁进程消失 —— 第一件事去查dmesg看是不是被OOM Killer杀了记住这四条，99%的PHP-FPM问题都能搞定。剩下1%是那些玄学问题，比如磁盘满了、inode爆了、容器cgroup限制搞错了，那就要再具体问题具体分析了。如果觉得今天的内容对你有帮助，欢迎关注@运维躬行录，我会持续分享一线运维踩坑经验。咱不整那些虚的，全是生产环境摸爬滚打出来的干货。也可以访问我的个人博客 躬行笔记 看更多实战文章，觉得有用的话顺手转发给身边搞运维的朋友，咱们一起进步。咱们下期见。
一文讲透NAT：从家里路由器到公司防火墙，IP地址转换到底是怎么玩的
说真的，每次有人问我"运维是干什么的"，我都很头疼。这问题太大了，就像你问程序员"你怎么写代码"一样没法一句话答完。但要是有人问"你知道NAT是啥吗"，我反而能聊半天，因为这东西太常见了，常见到几乎每个搞网络的人都离不开它，但你真要问起来，很多人对它的理解就停留在"哦，就是地址转换嘛"这个层面。今天咱就把NAT这事掰开了揉碎了聊一聊，不整那些虚的，都是实打实的内容。NAT到底是个啥玩意儿NAT，全称Network Address Translation，网络地址转换。这名字听着挺唬人的，其实原理简单得要命。你想想看，现在全世界有多少设备要上网？手机、电脑、平板、智能音箱、监控摄像头...几十亿台设备对吧。但IPv4地址一共多少个？理论上是2的32次方，大概43亿个。听起来不少，但早就不够用了。我记得2019年时候，IANA就把最后一批IPv4地址分配完了，从此以后新申请的IPv4地址基本没有了。那这么多设备怎么上网？靠NAT啊。你可以这么理解：你们小区几百户人家，每家都有个门牌号（私有IP地址），但对外就一个小区大门（公网IP）。快递员（外部网络）只认小区大门，不认识你们每家每户的门牌号。门卫（NAT设备）就负责把外面的快递收下来，再分发给对应的住户；或者把住户寄出去的快递，标上小区大门的地址发出去。这就是NAT最核心的作用：让多个内部设备共享少量公网IP地址上网。我刚入行那会儿，在一个县城的小网吧干网管。网吧里几十台机器，就一根宽带，一个公网IP。那时候我才真正搞明白NAT是怎么回事——路由器把网吧内网的192.168.1.x网段全部转换成一个公网IP，端口号用来区分不同的内网机器。那个年代NAT基本就是救命的玩意儿，没它网吧都没法开。NAT都有哪些类型光知道NAT是地址转换还不够，你得知道它有几种玩法。不同场景用的类型不一样，效果也不一样。1. 静态NAT：一对一映射这种最好理解，一个私有IP对应一个公网IP，死绑定。打个比方，就像你们公司给每个高管配了专车，司机就固定服务一个人。实际应用场景：企业里有些服务器需要被外网访问，比如你们公司的官网服务器、邮件服务器。这种情况下就需要给服务器配置一个固定的公网IP，让外网用户能稳定访问到。我之前在一家电商公司，他们官网服务器就是用的静态NAT。内网服务器IP是10.0.1.100，对外映射成一个公网IP。客户访问官网的时候，DNS解析到的是那个公网IP，流量进来之后路由器一查NAT表，哦，这是要去找10.0.1.100的，转过去就行。这种方式的优点是稳定，缺点嘛...浪费公网IP啊。一个服务器就占一个公网IP，多奢侈的事儿。2. 动态NAT：池子里随便挑这种比静态NAT灵活一些。路由器维护一个公网IP地址池，内网设备需要上网的时候，临时从池子里分配一个公网IP给它用，用完了还回去。这就像共享单车，你扫码就骑，到了目的地锁车就走。路由器就像调度中心，负责把车（公网IP）分配给需要的人（内网设备）。不过说实话，动态NAT现在用得真不多了。为啥？因为它还是有上限的，池子就那么大，几十台机器同时上网就可能不够用。除非你们公司有大量公网IP地址可以挥霍，否则还是老老实实用下面的PAT吧。3. PAT（端口地址转换）：真·扛把子这个才是现在最常用的，名字也好几个，PAT、NAT Overload、NAPT都指的是这个东西。原理和前两种不太一样，它不仅转换IP地址，还转换端口号。多个内网设备共享一个公网IP，靠端口号来区分不同的连接。举个例子，公司里192.168.1.10和192.168.1.20两台电脑同时访问百度：192.168.1.10的源端口是5000，访问后变成 公网IP:50001192.168.1.20的源端口是5001，访问后变成 公网IP:50002百度返回数据的时候，看端口号就知道这数据包是给谁的。路由器里维护着一张NAT转换表，谁出去的时候用啥端口，回来的时候就按表查。你家里那个小路由器，99%用的就是这种方式。要不然一个公网IP怎么让手机、电脑、电视、平板同时上网？我刚毕业那会儿，住城中村租房，房东给拉了一根宽带，通过一个路由器分给好几户人用。十几号人的手机电脑都通过这一个公网IP上网，全靠PAT顶着。那时候我就琢磨，这技术真是妙啊。NAT都有啥优缺点凡事都有两面性，NAT也不例外。这东西虽然解决了IPv4地址枯竭的燃眉之急，但也不是完美的。优点第一，省IP。这是最直接的。 IPv4地址不够用是现实问题，NAT让一个公网IP可以带几十上百台内网设备上网，相当于把有限的公网IP资源放大了几十倍。我上家公司就一根专线，200多个员工全靠这根线上办公，没NAT根本玩不转。第二，天然带点安全性。 内网设备躲在NAT后面，外网没法直接主动连接进来。这不是说NAT是防火墙啊，但它确实让内网没那么"透明"。你想想，你的电脑是192.168.1.5，外网根本不知道这个地址，它只知道你们公司的公网IP。想从外网直接访问你的电脑？没门。除非你主动发起连接，或者在路由器上做了端口映射。第三，网络调整灵活。 你们公司内网架构调整了，服务器IP变了，只要NAT映射规则不变，对外服务就没影响。不用像以前一样，动个内网IP还得通知所有外部合作方。缺点第一，性能开销。 路由器得查NAT表，转换IP和端口，这肯定比直传数据要慢。高端路由器有专门的NAT硬件加速芯片，影响不大；但低端设备或者CPU软转发的，流量大的时候CPU会飙得很高。我之前遇到过一个坑，一个小企业用那种几百块钱的家用路由器当出口，带了40多台机器。结果高峰期CPU直接100%，网速慢得跟蜗牛一样。后来换了个企业级路由器，问题立马解决。第二，端到端通信被破坏。 互联网设计的初衷是端到端直连，每台设备都有独立IP，能直接通信。NAT把这个模型破坏了，外网设备没法主动向内网设备发起连接。这就导致一些P2P应用、视频会议、游戏联机之类的功能受影响。第三，NAT穿透问题。 因为上面那个问题，很多应用需要做NAT穿透。比如你用微信视频、视频会议，有些场景就是建立不起连接。这就需要STUN、TURN、UPnP、ICE这些技术来辅助。我之前做过一个IM项目，就为了解决NAT穿透折腾了好长时间，最后上了coturn这个TURN服务器才搞定。NAT在实际中怎么用家庭场景：你家路由器99%的家用路由器默认就是PAT模式。你登录路由器后台（一般是192.168.1.1或者192.168.0.1），看那个WAN口配置，显示的就是公网IP，LAN口配置就是192.168.1.x这种内网网段。有时候你想从外网访问家里的NAS、监控啥的，就得在路由器上做端口映射。比如NAS的Web端口是5000，你就在路由器上做个规则：外网访问公网IP的5000端口，转发到内网NAS的5000端口。这玩意儿我玩过不少次，刚开始的时候踩过不少坑。比如有些运营商封了80、443这些常用端口，你想做个网站就没法用；还有些运营商给你分配的根本不是公网IP，而是大内网IP（比如100.64.x.x），这种情况下你做端口映射也没用，外面根本访问不到。企业场景：防火墙和路由器企业级的NAT一般在防火墙或者出口路由器上做。配置比家用复杂一些，但原理一样。我之前在一家制造业公司做运维，他们有个分支办公室需要访问总部的ERP系统。分支办公室没有公网IP，但有个固定的公网IP可以分配。我就在出口防火墙上做了静态NAT，把ERP服务器的内网IP映射成一个公网IP，分支办公室通过这个公网IP访问ERP。还有些企业会用到源NAT（Source NAT）和目的NAT（Destination NAT）的组合。源NAT是内网访问外网时转换源IP，目的NAT是外网访问内网时转换目的IP。云环境中的NAT现在大家都上云了，AWS、阿里云、腾讯云这些云厂商都提供NAT网关服务。在云上，NAT的作用就更明显了。你的云服务器如果只有内网IP，想访问外网（比如更新系统、下载软件）就得通过NAT网关出去。云上的NAT网关基本都是PAT模式，很多云服务器共享一个或几个NAT公网IP出去。不过云上有个坑要注意：NAT网关是要收费的，而且按流量或者按小时收费。我之前帮一个创业公司优化云成本，发现他们NAT网关流量费特别高。后来一查，是因为有个测试环境的机器在疯狂拉外网流量。设置了个防火墙规则限制之后，成本立马降下来了。聊聊NAT的"坑"和那些年踩过的雷说几个我亲身经历过的真实案例吧，给大家提个醒。坑一：MTU问题导致部分网站访问不了我之前遇到过一次诡异现象：某些网站能访问，某些网站访问不了，有些网站能打开但图片加载不出来。折腾了一下午，最后发现是MTU的问题。因为NAT在转换IP的时候，IP头可能会变（IPv4转IPv4虽然头长度一样，但有些场景下会增加一些字段），导致数据包超过出口的MTU。然后又没有正确设置DF（Don't Fragment）标志和ICMP反馈，结果就是数据包被默默丢弃。解决方案：在路由器上调整TCP MSS，把MSS值设小一点（一般是MTU-40），这样TCP握手的时候双方就会按这个MSS来分片。坑二：FTP服务器的主动模式问题FTP有两种模式：主动模式（PORT）和被动模式（PASV）。在NAT环境下，被动模式能正常工作，主动模式经常出问题。因为FTP主动模式下，服务器会主动连接客户端的某个端口（通过PORT命令告诉服务器）。但客户端在内网，服务器在外网，服务器没法主动连接到内网的客户端。如果你遇到FTP连接慢、传不了文件，先看看是不是模式问题。让客户端用被动模式一般就能解决。坑三：视频会议连不上这个我前面提过，做IM项目的时候遇到过。两个内网用户之间视频通话，明明都登录了，但就是建立不了P2P连接。最后上了TURN服务器中转才解决。TURN服务器的作用就是当中继，两个客户端都把音视频流发给TURN服务器，TURN服务器再转发给对方。这种方式虽然增加了延迟和带宽成本，但能保证连通性。坑四：一些老旧协议不兼容有些老协议在设计的时候压根没考虑NAT的情况，比如一些工控协议、老的游戏协议。这些协议里直接硬编码了IP地址，NAT转换之后协议就乱了。遇到这种情况，要么换协议，要么用IPSec之类的隧道技术把NAT的影响绕过去，要么干脆用代理。NAT的现状和未来虽然IPv6号称能解决地址枯竭问题，每个设备都能拿到独立公网IP，不再需要NAT。但实际上，IPv6的普及是个漫长的过程。我写这篇文章的时候（2024年），国内IPv6的普及率虽然在快速提升，但很多场景下还是NAT IPv4为主。而且就算IPv6普及了，NAT也不会完全消失。因为NAT除了解决地址问题，还有安全防护、网络隔离这些作用。很多企业内网还是会用NAT来管理网络出口。另外，像云计算、SD-WAN这些新技术，NAT也在其中发挥重要作用。云上VPC之间的流量，很多场景下还是要通过NAT来访问公网。所以啊，NAT这门技术，运维不学不行，学了也真能解决不少实际问题。总结NAT这东西吧，说简单也简单，就是个地址转换；说复杂也复杂，各种类型、各种应用场景、各种坑。我见过不少人觉得"不就是地址转换嘛，有什么难的"，真上手配起来就抓瞎。其实不管是做运维还是搞网络开发，对NAT的理解都应该到位。它不只是一个概念，而是真真实实影响你日常工作的东西。从家里的WiFi到公司的网络，从传统机房到云环境，NAT无处不在。建议大家有空的时候，可以登录一下自己家的路由器后台，看看WAN口配置、NAT规则、端口映射这些选项。也可以用traceroute、tcpdump这些工具抓包看看实际的NAT转换过程。光看理论不动手，永远学不会。好了，今天就聊到这儿。如果你觉得这篇文章有点用，欢迎转发给身边需要的朋友。关注公众号「耕云躬行录」，我会持续分享运维路上的实战经验和踩坑故事。还有我的个人博客 躬行笔记，里面记录了更多详细的技术笔记和案例分析。咱们下期见！记得关注@运维躬行录，不迷路。
从0到1玩转夜莺监控：这篇实战指南我写了三天，建议收藏！
说起监控系统，你脑子里第一个蹦出来的是啥？Zabbix？Prometheus？Grafana？还是云厂商自带的那一套？说实话，这些年我用过不少监控工具，直到去年在一家公司接手了一个全新的运维项目，需要重新搭建监控体系。当时评估了一大圈，最后选择了夜莺（Nightingale）。用了一年半，体验是真的香，所以今天想把踩过的坑、填过的坑，一股脑儿全倒出来，给正在选型或者刚开始用夜莺的朋友一点参考。这玩意儿到底是个啥？文档地址：https://n9e.github.io/zh/docs/prologue/introduction/夜莺监控，听名字挺文艺，其实是个硬核的云原生监控分析工具。它的核心理念是"All-in-One"，啥意思呢？就是把数据采集、可视化、监控告警、数据分析这些事儿全给你包圆了，不用东拼西凑。说起它的出身还挺有来头。最早是滴滴公司开发并开源的，2022年5月11日，滴滴把它捐赠给了中国计算机学会开源发展委员会（CCF ODC），成了CCF ODC成立后接受的第一个开源项目 [3]。研发团队都是Open-Falcon那帮老炮儿，所以在告警引擎这块的功底，你完全可以放心。那它跟Grafana+Zabbix这套组合拳有啥区别？我个人用下来的感受是：Grafana强在可视化，夜莺强在告警。Grafana的图表是真的漂亮，但告警这块说实话做得一般。夜莺的告警引擎是它的看家本领，规则配置灵活、事件管道处理、告警自愈、20多种通知媒介（电话、短信、邮件、钉钉、飞书、企业微信这些都内置了），用起来是真的顺手 [3]。而且夜莺的定位很有意思——它可以做两件事：第一种用法：你已经有Prometheus+各类Exporter的监控体系了，那直接把Prometheus作为数据源接入夜莺，夜莺就只管告警这件事，可视化你继续用Grafana。第二种用法：用夜莺自带的Categraf采集器采集数据，然后夜莺作为数据转发网关，把数据写入后端时序库（Prometheus、VictoriaMetrics等），同时承担告警和可视化的角色。我们今天主要讲第二种用法，从零开始搭建一套完整的夜莺监控体系。架构这块儿，先整明白在动手部署之前，必须得把架构搞清楚，不然部署到一半你会懵。夜莺的核心进程叫n9e，这个名字来源于夜莺的英文Nightingale的前两位字母 [1]。n9e本身依赖两个存储：MySQL：存放用户、监控大盘、告警规则这类配置信息Redis：存放JWT Token、心跳信息（机器的CPU、内存、时间偏移、核数、操作系统、CPU架构这些）从v6版本开始，夜莺转型成了统一可观测性平台，不再只支持时序数据源，ES、Loki、Jaeger这些也都能接入了 [1]。部署架构上有两种典型场景：场景一：中心机房架构（适合单机房或者多机房专线稳定的公司）n9e部署在中心机房所有机器的Categraf直接上报数据给中心n9en9e把数据转发给后端时序库（Prometheus或VictoriaMetrics）部署多个n9e实例，前面挂Nginx做负载均衡即可实现高可用场景二：边缘下沉式混杂架构（适合多机房、网络链路不好的公司）中心机房部署n9e边缘机房部署n9e-edgen9e-edge不依赖MySQL、Redis，只依赖中心端的n9eCategraf在边缘机房把数据上报给n9e-edgen9e-edge再把数据写入边缘机房的时序库中心n9e通过配置数据源的方式去查询边缘机房的时序库我当时公司的情况是中心机房在南京，边缘机房在香港，之间网络时不时抽风，所以两种架构都用上了。下面我会分别讲讲怎么部署。环境准备我这次用的是Rocky Linux 8.5，大家用CentOS 7或者Ubuntu 20.04也都没问题，差别不大。服务版本说明n9e6.7.2夜莺服务端Categraf0.3.45夜莺客户端MySQL5.7.36存储配置信息Redis6.2.6缓存和心跳Prometheus2.37.0时序数据库这里有个坑要提一下，官方其实更推荐VictoriaMetrics作为时序库，性能确实比Prometheus好不少，单机版就能扛住每秒上百万数据点 [2]。但考虑到我们公司已经有一套Prometheus体系了，为了节省成本就直接用了Prometheus。如果你是新环境，强烈建议直接上VictoriaMetrics。中心机房架构部署实战MySQL和Redis先跑起来这两个我直接用Docker部署了，方便快捷。MySQL和Redis的具体部署过程我就不展开了，网上教程一大堆。重点是要建好库和授权。部署完成后，导入夜莺的初始化SQL：mysql -h 10.206.0.17 -u root -p'YourPassword' < /data/n9e/n9e.sql然后创建监控用户并授权：CREATE USER 'nightingale'@'%' IDENTIFIED BY 'YourStrongPassword'; GRANT ALL PRIVILEGES ON n9e_v6.* TO 'nightingale'@'%';Prometheus的坑你必须知道Prometheus的部署本身不难，但有个参数不注意会让人怀疑人生——--enable-feature=remote-write-receiver [1]。不加上这个参数启动Prometheus的话，夜莺往Prometheus写数据的时候会疯狂报404，原因是Prometheus默认没开启/api/v1/write接口的监听。我当时就是没加这个参数，排错排了半个小时才反应过来，气得我想把键盘摔了。下载Prometheus：wget https://github.com/prometheus/prometheus/releases/download/v2.45.2/prometheus-2.45.2.linux-amd64.tar.gz tar xf prometheus-2.45.2.linux-amd64.tar.gz -C /data/配置systemd管理：cat <<EOF >/etc/systemd/system/prometheus.service [Unit] Description="prometheus" Documentation=https://prometheus.io/ After=network.target [Service] Type=simple ExecStart=/data/prometheus-2.45.2.linux-amd64/prometheus \ --config.file=/data/prometheus-2.45.2.linux-amd64/prometheus.yml \ --storage.tsdb.path=/data/prometheus-2.45.2.linux-amd64/data \ --web.enable-lifecycle \ --enable-feature=remote-write-receiver \ --query.lookback-delta=2m Restart=on-failure SuccessExitStatus=0 LimitNOFILE=65536 StandardOutput=syslog StandardError=syslog SyslogIdentifier=prometheus [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable prometheus systemctl restart prometheus注意这里有个--web.enable-lifecycle参数，是为了支持热加载配置用的。--query.lookback-delta=2m则是查询时向前看的时间窗口，太小会查不到数据。访问http://yourIP:9090/能看到Prometheus的Web界面就说明启动成功了。部署夜莺核心n9en9e建议用二进制方式部署，systemd托管，后面升级维护也方便 [2]。Docker compose方式虽然简单，但社区里很多坑其实都是对Docker compose不熟导致的。Helm方式适合大规模K8s环境，但官方其实不太建议把夜莺服务端部署到K8s里——K8s挂了监控就挂了，监控系统作为P0级服务，依赖其他组件越少越好 [2]。下载安装包（去flashcat.cloud/download/nightingale/找最新版本）：mkdir /data/n9e tar xzf n9e-v6.7.2-linux-amd64.tar.gz -C /data/n9e/修改配置文件，主要是DB部分：cp etc/config.toml etc/config.toml.bak vim etc/config.toml找到[DB]部分，配置MySQL连接信息。然后找到[Pushgw.Writers]，配置时序库的地址：[Pushgw] LabelRewrite = true Pushgw.Writers Url = "http://127.0.0.1:9090/api/v1/write"这个配置的意思是：夜莺接收到Categraf上报的监控数据后，会转发给这个URL。如果你想把数据写入多个时序库，配置多个Pushgw.Writers就行。配置systemd：cat <<EOF >/etc/systemd/system/n9e.service [Unit] Description="n9e.service" After=network.target [Service] Type=simple ExecStart=/data/n9e/n9e WorkingDirectory=/data/n9e Restart=on-failure SuccessExitStatus=0 LimitNOFILE=65536 StandardOutput=syslog StandardError=syslog SyslogIdentifier=n9e.service [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl start n9e.service systemctl enable n9e.servicen9e默认监听17000端口，启动成功后netstat -lntp | grep 17000应该能看到。还有一点很重要：安装完成后一定要改默认密码！默认账号是root，密码是root.2020 [2]。我看到有公开数据显示，网上有上千个夜莺的公网站点，很多都没改默认密码，这不是明摆着让人白嫖吗？另外config.toml里的SigningKey也建议改成随机字符串，用来加密JWT token。部署采集器Categraf夜莺本身不负责数据采集，这事儿得交给agent。官方推荐的是Categraf，当然用telegraf、grafana-agent这些也都能对接 [1]。Categraf的安装包可以从GitHub的flashcatcloud/categraf仓库下载：tar xzf categraf-v0.3.45-linux-amd64.tar.gz -C /data/修改主配置conf/config.toml：[global] hostname = "" [n9e] url = "http://10.206.0.17:17000" # n9e服务端地址 [heartbeat] enable = true url = "http://10.206.0.17:17000/v1/n9e/heartbeat"Categraf的目录结构是这样的 [1]：conf/ ├── config.toml # 主配置 ├── logs.toml # logs-agent配置 ├── prometheus.toml # prometheus agent配置 ├── traces.yaml # trace-agent配置 └── input.*/ # 各种采集插件的配置目录Categraf的精妙之处在于它"约定优于配置"的设计——以input.开头的目录就是各种采集插件的目录，运行时自动加载。如果某个采集器不想用，把input.xx改成bak.input.xx就行，Categraf会自动忽略 [1]。基本采集项（CPU、内存、磁盘、磁盘IO、网络）默认就启用了，不用我们额外配置。systemd管理Categraf：cat <<EOF >/etc/systemd/system/categraf.service [Unit] Description="categraf.service" After=network.target [Service] Type=simple ExecStart=/data/categraf-v0.3.45-linux-amd64/categraf WorkingDirectory=/data/categraf-v0.3.45-linux-amd64 Restart=on-failure SuccessExitStatus=0 LimitNOFILE=65536 StandardOutput=syslog StandardError=syslog SyslogIdentifier=categraf.service [Install] WantedBy=multi-user.target EOF登录夜莺，第一眼该看啥服务都起来后，浏览器打开http://yourIP:17000，用默认账号root/root.2020登录。注意，登录后第一件事就是改密码！登录进去后你会发现，主机列表是空的——别慌，因为虽然Categraf已经在上报数据了，但主机还没有归组 [1]。这里就得理解夜莺的"业务组"概念了。业务组可以理解为资源的逻辑分组，比如你可以创建"web业务组"、"db业务组"、"中间件业务组"，然后把对应的主机放进去。不同业务组的用户只能看到自己业务组内的资源，权限隔离做得挺细。操作流程是这样的：创建用户：在系统管理-用户管理里创建用户创建团队：在权限管理-团队管理里创建团队，团队命名有讲究，像rab-dev这种带横线的写法是有层级关系的（rab是父团队，dev是子团队）创建业务组：在业务组管理里创建业务组把主机归到业务组：在主机列表里勾选主机，分配业务组把团队绑定到业务组：这样团队成员就能看到业务组内的主机了这套权限体系设计得挺合理，比直接给用户授权机器要灵活得多。配置数据源，让告警能跑起来到这里监控数据已经在采集了，但夜莺默认情况下还不知道Prometheus在哪。要让告警引擎能从时序库查询数据，需要配置数据源 [1]。操作路径：集成中心-数据源管理-新增数据源，类型选择Prometheus，URL填http://127.0.0.1:9090，测试通过后保存。配置完成后，告警规则才能查询到对应的指标数据。边缘机房部署：跨地域监控的救星说到这里就得提一下我们公司当时的情况了——南京中心机房和香港边缘机房之间网络时不时抽风。如果所有Categraf都直接上报到南京的n9e，香港那边网络一抖，数据就丢了，告警也漏了。边缘下沉式架构就是为了解决这个问题的。核心思想是：把时序库下沉部署到边缘机房，避免大量时序数据传输到中心机房带来的网络压力 [1]。n9e-edge的配置要点n9e-edge的安装包其实和n9e是同一个，解压后里面有n9e-edge这个二进制文件 [1]。mkdir -p /data/n9e-edge tar xzf n9e-v6.7.2-linux-amd64.tar.gz -C /data/n9e-edge/修改/data/n9e-edge/etc/edge/edge.toml，关键是配置中心端n9e的地址：[CenterApi] Addrs = ["http://119.45.140.59:17000"] BasicAuthUser = "user001" BasicAuthPass = "ccc26da7b9aba533cbb263a36c07dcc5" Timeout = 9000BasicAuthUser和BasicAuthPass对应的是中心端n9e的[HTTP.APIForService.BasicAuth]配置段 [1]。n9e-edge不依赖MySQL和Redis，只依赖中心端的n9e，所以即使边缘机房和中心机房的网络断了，边缘机房的告警功能还是能正常运转——这个特性在生产环境中是救命级别的。边缘Categraf的配置区别边缘机房的Categraf配置和中心机房有两个不同点 [1]：数据上报地址改为n9e-edge的地址（默认端口19000）心跳URL要改成/v1/n9e/edge/heartbeat，不能写/v1/n9e/heartbeat[n9e] url = "http://172.xx.xx.15:19000" [heartbeat] enable = true url = "http://172.xx.xx.15:19000/v1/n9e/edge/heartbeat"引擎名的坑n9e-edge默认的引擎名叫edge，中心端n9e的引擎名叫default，两个要区分开 [1]。如果你有多个边缘机房，每个边缘机房的n9e-edge的引擎名要不一样，不然会冲突。修改方法：[Alert.Heartbeat] IP = "" Interval = 1000 EngineName = "edge-hk" # 改成你自己的引擎名中心n9e添加边缘数据源在中心n9e的集成中心里，添加边缘机房的Prometheus作为数据源，选择告警引擎的时候要选对应边缘机房的引擎名（比如edge-hk），不能选default [1]。到这里边缘机房的数据就接入完成了，中心n9e可以直接查询边缘机房的时序库，告警规则也能正常触发。高可用怎么做？监控系统要是挂了，那就跟没监控一样糟糕。n9e的高可用其实很简单——多部署几个实例，前面挂Nginx或者LVS做负载均衡就行 [1]。边缘的n9e-edge同理，同一个edge集群的edge.toml里EngineName要一样，前面搞个负载均衡给agent上报数据用，高可用就完成了。几个我踩过的坑时间不同步：社区反馈的很多问题都是机器时间没校准导致的，监控系统对时间很敏感，服务端、时序库、被监控目标、浏览器所在机器的时间都要保持一致 [2]。我曾经因为一台机器时间快了5分钟，看不到监控数据，排错排了一晚上。Prometheus忘加参数：--enable-feature=remote-write-receiver这个参数一定要加上，不然数据写不进去。业务组权限：建好用户和团队后一定要把团队关联到业务组，不然用户看不到主机。公网暴露：夜莺千万别暴露在公网上，特别是没改默认密码的情况下 [2]。K8s部署需谨慎：不推荐把夜莺服务端部署到K8s里，K8s挂了监控就挂了 [2]。写在最后写到这里差不多4000字了，从夜莺是什么、怎么部署、怎么用、踩过哪些坑，都过了一遍。夜莺这套体系用起来是真的省心，特别是告警引擎这一块，比我用过的其他监控工具都要顺手。当然，工具好不好用，最终还是看适不适合你的业务场景。如果你只是在用云厂商的监控，夜莺可能不是必需品；如果你要自建监控体系，又希望告警能力强大、配置灵活，夜莺是个不错的选择。如果你觉得这篇文章对你有帮助，欢迎点赞、在看、转发三连，让更多运维兄弟看到！有任何问题欢迎评论区留言交流～关注公众号：耕云躬行录，获取更多运维实战经验分享！个人博客：躬行笔记我们下期见！👋
MHA退场，Orchestrator接棒：Debian 13 + MySQL 8.0高可用迁移全记录
讲真，搭这套环境的时候我头都大了。之前给公司搭的是经典MHA方案，跑在CentOS 7+MySQL 5.7上，好几年了稳如老狗。但今年IT架构升级，操作系统全部切到Debian 13，数据库统一升到MySQL 8.0.46，这下MHA就彻底歇菜了——倒不是它不能用，是真的"臣妾做不到了"。MHA这套东西是2016年作者Yoshinori Matsunobu在GitHub上停更的，到现在已经快十年没人维护。MySQL 8.0那一堆新特性它根本不认识，Perl依赖在新系统上也是各种打架，硬装上去不是缺这个模块就是那个版本冲突，搞得人头皮发麻。后来一狠心，决定上Orchestrator。这玩意儿是GitHub上一个明星项目，用Go语言写的，轻量、活跃、对MySQL 8.0支持好，配套还有个Web界面，拓扑图画得清清楚楚，比MHA那种纯命令行友好太多。用了几天之后，只想感叹一句：早干嘛去了。这次的环境先交代一下这次的舞台布景：管理机：一台Debian 13的机器，装Orchestrator主程序，用它自带的SQLite存拓扑元数据，轻量省事。MySQL集群：三台Rocky Linux 8的系统，跑MySQL 8.0.46，组个一主两从：主库 kafka-node1：192.168.198.201从库1 kafka-node2：192.168.198.202（开了super_read_only）从库2 kafka-node3：192.168.198.203（开了super_read_only）Orchestrator的部署其实不难，但MySQL 8.0有几个底层特性是真的会让人卡壳，这也是我今天重点想讲的部分。第一步：建账号，这一步差点翻车Orchestrator得有个账号能登到所有MySQL节点上去探活、查拓扑。听起来很常规对吧？但偏偏就这一步，差点把整个项目拖黄了。原因就是那两个从库开了super_read_only。在MySQL 8.0里，super_read_only=ON之后，连root账号都没法执行写操作了，包括创建用户和授权。我第一次直接登录从库跑CREATE USER的时候，控制台直接甩我一个ERROR 1290 (HY000): The MySQL server is running with the --super-read-only option so it cannot execute this statement，人都给我整麻了。正确做法是按顺序来：-- 先把只读保护临时关掉 SET GLOBAL super_read_only = 0; SET GLOBAL read_only = 0; -- 用兼容MySQL 8.0客户端的密码插件建账号 CREATE USER 'orchestrator_user'@'192.168.198.%' IDENTIFIED WITH mysql_native_password BY 'OrchPassword123!'; -- 给最小权限，别手贱多授 GRANT SUPER, PROCESS, REPLICATION SLAVE, RELOAD ON *.* TO 'orchestrator_user'@'192.168.198.%'; FLUSH PRIVILEGES; -- 搞完赶紧把只读保护打开 SET GLOBAL read_only = 1; SET GLOBAL super_read_only = 1;注意主库不要执行那两行开关只读的命令，从库才需要。三台机器都得执行一遍，账号密码保持一致。这里有个小细节，用mysql_native_password而不是默认的caching_sha2_password，是因为Orchestrator客户端兼容性还有些小问题，native_password更稳，踩过坑的老哥们应该都知道。第二步：在Debian 13上装OrchestratorDebian 13装东西是真舒服，包管理甩RPM几条街。Orchestrator官方提供了现成的.deb包，直接下下来装就行：wget https://github.com/openark/orchestrator/releases/download/v3.2.6/orchestrator-cli_3.2.6_amd64.deb wget https://github.com/openark/orchestrator/releases/download/v3.2.6/orchestrator_3.2.6_amd64.deb sudo apt install ./orchestrator_3.2.6_amd64.deb ./orchestrator-cli_3.2.6_amd64.deb -y但Debian有个让人又爱又恨的特性——unattended-upgrades服务默认开启，会自动跑包更新。如果运气不好撞上它正在工作，apt install就会卡在Could not get lock /var/lib/dpkg/lock-frontend这一步报错。解决思路很简单粗暴：# 找到占用锁的进程 ps -ef | grep apt # 干掉它 sudo kill -9 <PID> # 清理锁文件 sudo rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock # 然后重新装 sudo apt install ./orchestrator_*.deb -y这种坑就是典型的"基础操作"，但就是经常被人问起，所以单独提一下。第三步：配置文件是个大工程装完之后，配置文件才是真正的战场。Orchestrator的配置文件默认在/etc/orchestrator.conf.json，是用JSON格式写的，可读性比MHA的ini好不少。直接上我的配置：{ "Debug": true, "ListenAddress": ":3000", "MySQLTopologyUser": "orchestrator_user", "MySQLTopologyPassword": "OrchPassword123!", "MySQLTopologyCredentialsConfigFile": "", "BackendDB": "sqlite", "SQLite3DataFile": "/var/lib/orchestrator/orchestrator.db", "InstancePollSeconds": 5, "DiscoverByShowSlaveHosts": true, "ReasonableReplicationLagSeconds": 10, "ProblematicReplicationLagSeconds": 30, "FailMasterApprovalRequired": false, "RecoveryPeriodBlockSeconds": 60, "RecoverMasterClusterFilter": "*", "RecoverIntermediateMasterClusterFilter": "*", "SeedInjectedPool": "192.168.198.201:3306,192.168.198.202:3306,192.168.198.203:3306", "HostnameResolveMethod": "none" }几个关键配置我单独拎出来说："InstancePollSeconds": 5：Orchestrator每5秒去探一次活，这个频率我觉得比较合理，太快了浪费资源，太慢了检测不及时。"BackendDB": "sqlite"：用SQLite存元数据，单机够用了。如果Orchestrator要做高可用，那就得用MySQL后端。"DiscoverByShowSlaveHosts": true：让Orchestrator去通过SHOW SLAVE HOSTS命令发现从库，这是MySQL 8.0之后的推荐做法。"FailMasterApprovalRequired": false：自动故障转移，不需要人工审批。如果你们公司流程严格，建议改成true，等人确认了再切。"RecoverMasterClusterFilter": "*"：所有集群都允许自动恢复。重点来了——最下面那两个配置，是我这次能够"破案"的关键。"SeedInjectedPool"：直接告诉Orchestrator集群的种子IP有哪些，让它主动去探，而不是被动等主库汇报。"HostnameResolveMethod": "none"：禁掉主机名反查，因为MySQL 8.0的report_host经常出问题，这一关关了反而清爽。第四步：那两个让我熬到凌晨两点的坑配完之后启动服务，浏览器打开http://管理机IP:3000，一看拓扑图，差点一口老血喷在屏幕上——整个集群一片橙红，状态显示DeadMasterAndReplicas，意思就是主库是个孤岛，从库连不上。于是就有了接下来长达四个小时的破案之旅。坑一：克隆出来的虚拟机，Server_Id和UUID撞了这次的从库是用模板克隆出来的，我没改server-id就直接用了。结果在主库执行SHOW REPLICAS;的时候，从库记录老是一会儿有一会儿没有，状态完全不对。这就是经典的"克隆虚拟机没清UUID"的问题。Linux系统上用模板克隆虚拟机，MySQL的auto.cnf文件会被原样复制，导致所有从库的server_uuid完全一样，复制关系直接乱套。解决方法倒也简单粗暴：# 在每台机器的my.cnf里给server-id一个明确的值 [mysqld] server-id=101 # 主库 # server-id=102 # 从库1 # server-id=103 # 从库2然后去从库上把auto.cnf删掉重启MySQL：rm -f /var/lib/mysql/auto.cnf systemctl restart mysqld重启之后MySQL会自动生成一个新的UUID，复制关系也就恢复正常了。这事儿其实在MHA时代就经常遇到，不算新坑，但每次都能让人烦一下。坑二：主库上SHOW REPLICAS;的Host字段是空的server-id修好之后，复制是跑起来了，但Orchestrator的网页还是显示主库孤岛。我又跑去主库手敲SHOW REPLICAS;一看——这下数据是有了，两条从库记录安安静静躺在那，但Host那一列全是空字符串。这就是MySQL 8.0埋的一个不大不小的雷。8.0开始，MySQL默认不上报自己监听的物理IP了，report_host参数默认是空的。这在MHA时代影响不大，因为MHA可以靠SSH或者别的手段拿到主机的IP，但Orchestrator主要靠主库自己汇报，从库信息是从主库的SHOW REPLICAS;结果里拿的，Host字段一空，它就摸不到从库在哪儿了。正经的解决方案是在每台从库的my.cnf里加：report_host=192.168.198.202 # 写本机IP然后重启MySQL。但这种方法有一个前提：IP不能变。如果你们环境是DHCP或者经常漂，那就比较蛋疼。我当时图省事，直接用了一个更暴力的办法——在Orchestrator的配置里强行注入种子池："SeedInjectedPool": "192.168.198.201:3306,192.168.198.202:3306,192.168.198.203:3306", "HostnameResolveMethod": "none"加上这两行之后，Orchestrator就不再依赖主库的SHOW REPLICAS;汇报了，而是自己主动拿这三个IP去连、去探活。然后它内部会对每个节点跑SHOW SLAVE STATUS;，比对各个节点的Master_Log_File和Read_Master_Log_Pos，自己把拓扑关系拼出来。这个方案虽然土，但在内网环境、IP固定的情况下相当好用，不用每台机器都改my.cnf。第五步：收网验证配置改完，Orchestrator服务也得重启一下让它读新配置：sudo systemctl restart orchestrator然后主动触发一次拓扑发现：orchestrator -c discover -i 192.168.198.201:3306 orchestrator -c discover -i 192.168.198.202:3306 orchestrator -c discover -i 192.168.198.203:3306这一步其实就是告诉Orchestrator："兄弟，起来干活了，这三台机器给你。"最后再回到浏览器刷新http://管理机IP:3000——哗——整个拓扑图终于绿了！主库kafka-node1稳稳地待在左侧，下方两根绿色的连线分别牵引到kafka-node2和kafka-node3两个从节点，所有状态都是健康的绿色Health状态。之前那一堆橙红色的DeadMaster告警全部消失，图形化的集群状态一目了然。那一刻，真的有种"养了三个月的娃终于会叫爸爸了"的成就感（bushi）。写在最后整套方案折腾下来，最大的感受就是：MHA的时代真的结束了。不是说它不好用，是它真的老了，配套的东西都跟不上了。Orchestrator这种Go语言写的现代化工具，配合Web界面、JSON配置、对MySQL 8.0的原生支持，才是当下的最优解。这次踩到的几个坑，本质上都是MySQL 8.0相比5.7的一些"小脾气"——比如默认的super_read_only、report_host默认值变化、UUID机制等。这些小坑单个看都不大，但堆在一起就足以让人怀疑人生。如果你们公司也准备从MHA迁到Orchestrator，希望这篇文章能帮你少走点弯路。如果觉得有帮助，记得点赞、转发、在看走一波，这是对我最大的支持。有问题随时在公众号后台留言，看到都会回复的。想第一时间收到推送可以关注我的公众号：耕云躬行录，里面都是实打实的运维干货，没啥花里胡哨的东西。个人博客地址：躬行笔记（https://www.fuzhoupyy.work ），内容更系统一些，可以收藏起来慢慢看。下期准备写Orchestrator的故障切换实战，包括手动切换、自动化切换、切换过程中的数据一致性验证这些，有兴趣的可以先关注一下，别走丢啦~
用大模型搭一套最小化AIOps告警诊断系统：从告警风暴到自动归因，我只用了200行代码
凌晨3点，企业微信连续震动47次。你睁开眼，Prometheus告警群里全是红色——CPU高、Pod重启、接口超时、磁盘IO飙升……全是症状，没有一条告诉你：到底哪儿坏了，该先修什么。这就是大多数运维团队的日常。我们有几百条告警规则，却没有一条规则能回答："这一堆告警的根因是什么？"如果你也被告警风暴折磨过，这篇文章给你一个周末就能跑起来的最小化方案——用开源LLM + Alertmanager Webhook + 向量检索，搭一套能自动聚合告警、定位根因、给出修复建议的诊断系统。不讲概念、不画饼，全部可运行代码，踩坑点全标出来。一、为什么传统告警"越建越废"先看一组典型数据（来自我们团队的真实统计）：指标数值每周告警总量12,000+ 条真正需要人工介入的~180 条（1.5%）重复/关联告警占比78%on-call平均确认时间4.2分钟/条夜间告警误判导致的无效起夜每周3-4次痛点拆解：告警风暴：一个根因（比如DB主从切换）会触发上下游20+条告警，但每条都独立推送缺上下文：告警只有metric名+阈值，没有"这个服务最近改了什么""上次也这样是什么原因"经验锁在人脑里：老王知道"这三条一起出现=Redis集群脑裂"，但这个知识没沉淀规则越加越多，噪音越来越大：为了"不漏报"不断降阈值，结果告警疲劳传统方案是什么？加告警分组、加静默规则、加Escalation策略。有用吗？有，但治标不治本。你需要的不是更好的通知策略，而是一个能"看全局、做判断"的大脑。二、整体架构：200行代码的最小闭环┌─────────────────────────────────────────────────────┐ │ Prometheus / 各种监控源 │ └────────────────────────┬────────────────────────────┘ │ 告警触发 ▼ ┌─────────────────────────────────────────────────────┐ │ Alertmanager (webhook_configs) │ └────────────────────────┬────────────────────────────┘ │ POST /webhook ▼ ┌─────────────────────────────────────────────────────┐ │ AIOps Diagnosis Service (Python) │ │ │ │ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │ │ │ 告警聚合 │→│ 向量知识库检索 │→│ LLM根因分析 │ │ │ │(时间窗口) │ │(历史case匹配)│ │(prompt链) │ │ │ └──────────┘ └──────────────┘ └───────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────┐ │ │ │ 输出：根因+修复建议 │ │ │ │ → 企微/钉钉/Slack │ │ │ └──────────────────┘ │ └─────────────────────────────────────────────────────┘核心组件只有3个：告警聚合器：30秒时间窗口内的告警做聚合，按namespace/service分组向量知识库：存历史故障case + runbook，用embedding做相似检索LLM推理链：拿聚合后的告警 + 检索到的历史case，输出根因判断和修复建议三、动手：逐模块实现3.1 Alertmanager Webhook接收 + 时间窗口聚合# alert_receiver.py from fastapi import FastAPI, Request from collections import defaultdict import asyncio import time from datetime import datetime app = FastAPI() # 告警缓冲池：按 namespace/service 聚合 alert_buffer = defaultdict(list) WINDOW_SECONDS = 30 # 聚合窗口 @app.post("/webhook") async def receive_alert(request: Request): payload = await request.json() for alert in payload.get("alerts", []): labels = alert.get("labels", {}) # 聚合key：namespace + service（你可以按需调整粒度） group_key = f"{labels.get('namespace', 'default')}/" \ f"{labels.get('service', labels.get('job', 'unknown'))}" alert_buffer[group_key].append({ "alertname": labels.get("alertname"), "severity": labels.get("severity", "warning"), "instance": labels.get("instance", ""), "description": alert.get("annotations", {}).get("description", ""), "value": alert.get("annotations", {}).get("value", ""), "starts_at": alert.get("startsAt", ""), "labels": labels, }) return {"status": "ok"} async def flush_buffer(): """每30秒flush一次缓冲池，触发诊断""" while True: await asyncio.sleep(WINDOW_SECONDS) if not alert_buffer: continue # 取出当前buffer快照并清空 current_batch = dict(alert_buffer) alert_buffer.clear() for group_key, alerts in current_batch.items(): if len(alerts) >= 1: # 至少1条告警才触发诊断 asyncio.create_task(diagnose(group_key, alerts)) @app.on_event("startup") async def startup(): asyncio.create_task(flush_buffer())踩坑点 ⚠️：Alertmanager的webhook是同步阻塞的，如果你的接口响应慢（比如等LLM推理），Alertmanager会超时重试，导致告警重复。所以必须先收下来放buffer，异步处理。group_key 的粒度很关键：太粗（只按namespace）会把不相关的告警混在一起；太细（按pod）会丧失关联分析能力。建议从namespace/service开始，根据实际效果调整。3.2 向量知识库：让LLM有"记忆"裸LLM看告警只能做通用推理，效果很一般。真正的杀手锏是让它参考你们团队的历史故障case。# knowledge_base.py import chromadb from chromadb.config import Settings from sentence_transformers import SentenceTransformer # 用本地embedding模型，不依赖外部API embedder = SentenceTransformer("BAAI/bge-small-zh-v1.5") client = chromadb.PersistentClient(path="./chroma_db") collection = client.get_or_create_collection( name="incident_cases", metadata={"hnsw:space": "cosine"} ) def add_case(case_id: str, title: str, symptoms: str, root_cause: str, fix: str): """添加一条历史故障case""" # 把症状作为主要检索内容 text = f"症状：{symptoms}\n根因：{root_cause}\n修复：{fix}" embedding = embedder.encode(text).tolist() collection.upsert( ids=[case_id], documents=[text], embeddings=[embedding], metadatas=[{ "title": title, "root_cause": root_cause, "fix": fix, }] ) def search_similar_cases(alert_summary: str, top_k: int = 3): """根据当前告警摘要检索相似历史case""" query_embedding = embedder.encode(alert_summary).tolist() results = collection.query( query_embeddings=[query_embedding], n_results=top_k, ) cases = [] for i, doc in enumerate(results["documents"][0]): cases.append({ "document": doc, "metadata": results["metadatas"][0][i], "distance": results["distances"][0][i], }) return cases知识库冷启动怎么办？刚开始没有case数据很正常，这样填充：# 手动录入你们团队的Top 20高频故障 cases = [ { "id": "case-001", "title": "Redis集群脑裂导致缓存击穿", "symptoms": "redis_connected_clients飙升 接口超时P99>5s " "cache_hit_ratio下降 Pod内存OOM", "root_cause": "Redis哨兵网络分区导致脑裂，两个master同时写入，" "客户端连接数翻倍，缓存数据不一致触发击穿", "fix": "1. 确认哨兵状态 redis-cli sentinel masters\n" "2. 手动failover到正确master\n" "3. 清理脏数据 FLUSHDB on旧master\n" "4. 检查网络分区原因（通常是跨AZ带宽打满）" }, { "id": "case-002", "title": "DB慢查询引发连锁超时", "symptoms": "mysql_slow_queries突增 接口P99超时 " "thread_pool_active满 上游服务502", "root_cause": "某张大表全表扫描（新上线的报表SQL没加索引），" "占满DB连接池，上游服务拿不到连接全部超时", "fix": "1. SHOW PROCESSLIST找到慢SQL并KILL\n" "2. 添加缺失索引 ALTER TABLE ADD INDEX\n" "3. 临时扩连接池 max_connections\n" "4. 上线SQL审核卡点防止复发" }, # ... 继续添加你们团队的高频case ] for case in cases: add_case(**case)踩坑点 ⚠️：embedding模型选择：不要用OpenAI的text-embedding（延迟高+外网依赖），运维场景用bge-small-zh就够了，768维，本地跑延迟<50mscase的「症状」字段最重要，要用告警名+metric名的描述方式写，和实际告警格式对齐，检索命中率才高建议每次故障复盘后，安排值班人花5分钟把case录入知识库（做成复盘模板的一部分）3.3 LLM推理：Prompt工程是核心# diagnosis_engine.py import openai # 兼容接口，可以对接本地Ollama/vLLM # 对接本地部署的模型（推荐Qwen2.5-14B或DeepSeek-V2-Lite） client = openai.OpenAI( base_url="http://localhost:11434/v1", # Ollama api_key="ollama", # Ollama不需要真实key ) SYSTEM_PROMPT = """你是一个资深SRE工程师，专门做告警根因分析。 你的任务： 1. 分析一组同时出现的告警，判断它们之间的因果关系 2. 找出最可能的根因（Root Cause） 3. 给出具体的排查步骤和修复建议 分析原则： - 多条告警同时出现时，优先考虑"一个根因触发多个症状"的可能性 - 区分"因"和"果"：比如CPU高可能是果（慢查询导致），不是因 - 如果有历史case参考，优先匹配历史模式 - 给修复建议时，按紧急程度排序：先止血→再修复→最后防复发 输出格式： ## 根因判断 [一句话说清根因] ## 置信度 [高/中/低] + 理由 ## 告警关联分析 [哪些告警是因，哪些是果，因果链是什么] ## 修复建议 1. 【立即止血】... 2. 【根因修复】... 3. 【防复发】... ## 排查命令可直接执行的排查命令 """ async def diagnose(group_key: str, alerts: list): """核心诊断逻辑""" from knowledge_base import search_similar_cases # 1. 构造告警摘要 alert_summary = format_alerts(alerts) # 2. 检索相似历史case similar_cases = search_similar_cases(alert_summary, top_k=3) # 3. 构造prompt user_prompt = build_diagnosis_prompt(group_key, alerts, similar_cases) # 4. 调用LLM response = client.chat.completions.create( model="qwen2.5:14b", # Ollama模型名 messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ], temperature=0.1, # 诊断场景要低temperature，减少幻觉 max_tokens=2000, ) diagnosis = response.choices[0].message.content # 5. 推送结果 await send_notification(group_key, alerts, diagnosis) def format_alerts(alerts: list) -> str: """将告警列表格式化为可读文本""" lines = [] for a in alerts: lines.append( f"- [{a['severity']}] {a['alertname']} " f"| instance={a['instance']} " f"| {a['description']}" ) return "\n".join(lines) def build_diagnosis_prompt(group_key: str, alerts: list, similar_cases: list) -> str: """构造完整的诊断prompt""" alert_text = format_alerts(alerts) case_text = "" if similar_cases: case_text = "\n\n## 相似历史故障（供参考）\n" for i, case in enumerate(similar_cases, 1): if case["distance"] < 0.5: # 只用相似度够高的case case_text += f"\n### 历史Case {i}（相似度: " case_text += f"{1 - case['distance']:.0%}）\n" case_text += f"{case['document']}\n" prompt = f"""## 当前告警组 服务: {group_key} 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} 告警数量: {len(alerts)} 条 ## 告警详情 {alert_text} {case_text} 请分析以上告警，给出根因判断和修复建议。""" return prompt 踩坑点 ⚠️：temperature一定要低（0.1-0.2）。诊断场景不需要创造力，需要准确性。我们测试过temperature=0.7，会出现"编造不存在的服务名"的幻觉模型选择：14B参数量是性价比最优解。7B在因果推理上偏弱，70B对GPU要求太高。Qwen2.5-14B对中文运维场景表现最好一定要带历史case。裸模型对"你们团队特有的架构问题"几乎无法正确判断，有case参考后准确率从~30%飙到~75%相似度阈值0.5是经验值。太低会引入噪音case误导模型，太高可能匹配不到3.4 通知推送：结构化输出到IM# notifier.py import httpx WECOM_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" async def send_notification(group_key: str, alerts: list, diagnosis: str): """推送到企业微信""" severity_emoji = { "critical": "🔴", "warning": "🟡", "info": "🔵", } # 取最高severity max_severity = "info" for a in alerts: if a["severity"] == "critical": max_severity = "critical" break elif a["severity"] == "warning": max_severity = "warning" emoji = severity_emoji.get(max_severity, "⚪") content = f"""{emoji} **AIOps诊断报告** **服务**: {group_key} **告警数**: {len(alerts)} 条 **最高级别**: {max_severity} --- {diagnosis} --- _🤖 由AIOps系统自动诊断生成 | 如判断不准请反馈以优化模型_ """ async with httpx.AsyncClient() as client: await client.post(WECOM_WEBHOOK, json={ "msgtype": "markdown", "markdown": {"content": content} })四、部署：一个docker-compose搞定# docker-compose.yml version: "3.8" services: # AIOps诊断服务 aiops-diagnosis: build: . ports: - "8080:8080" environment: - OLLAMA_BASE_URL=http://ollama:11434 - WECOM_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx volumes: - ./chroma_db:/app/chroma_db # 知识库持久化 depends_on: - ollama # 本地LLM (Ollama) ollama: image: ollama/ollama:latest ports: - "11434:11434" volumes: - ollama_data:/root/.ollama deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] volumes: ollama_data:# Dockerfile FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . # 下载embedding模型（构建时就拉好，避免运行时下载） RUN python -c "from sentence_transformers import SentenceTransformer; \ SentenceTransformer('BAAI/bge-small-zh-v1.5')" CMD ["uvicorn", "alert_receiver:app", "--host", "0.0.0.0", "--port", "8080"]# requirements.txt fastapi==0.115.0 uvicorn==0.30.0 openai==1.40.0 chromadb==0.5.0 sentence-transformers==3.0.0 httpx==0.27.0部署步骤：# 1. 启动服务 docker-compose up -d # 2. 拉取LLM模型（首次需要，约8GB） docker exec -it ollama ollama pull qwen2.5:14b # 3. 配置Alertmanager webhook # alertmanager.yml 添加： # receivers: # - name: 'aiops' # webhook_configs: # - url: 'http://aiops-diagnosis:8080/webhook' # send_resolved: true # 4. 录入初始知识库 python init_knowledge_base.py # 5. 验证 curl -X POST http://localhost:8080/webhook \ -H "Content-Type: application/json" \ -d '{"alerts":[{"labels":{"alertname":"HighCPU","service":"order-api","severity":"warning"},"annotations":{"description":"CPU > 80% for 5min"}}]}'五、效果数据：上线一周后的对比指标接入前接入后变化on-call每次响应时间4.2min1.1min-74%夜间无效起夜3-4次/周0-1次/周-80%根因判断准确率人工100%（但慢）72%（秒级）可接受平均故障恢复时间(MTTR)23min11min-52%关键洞察：72%的准确率看起来不高，但在凌晨3点，一个70%准确的即时建议 >> 一个要等人醒来才能开始的100%人工判断。而且不准的时候，诊断报告里的排查命令依然有用——相当于帮你把runbook翻好了。六、进阶优化方向6.1 反馈闭环：让系统越用越准# feedback.py - 在诊断结果末尾加反馈按钮 # 值班人确认"准确/不准确"后： # - 准确：自动将本次告警+诊断结果存入知识库作为新case # - 不准确：记录真实根因，作为负样本优化prompt async def handle_feedback(diagnosis_id: str, is_accurate: bool, actual_root_cause: str = ""): if is_accurate: # 自动沉淀为新case add_case( case_id=diagnosis_id, title=f"自动沉淀-{diagnosis_id}", symptoms=cached_alerts[diagnosis_id], root_cause=cached_diagnosis[diagnosis_id], fix=cached_diagnosis[diagnosis_id], ) else: # 记录误判，后续用于prompt优化 log_misdiagnosis(diagnosis_id, actual_root_cause)6.2 拓扑感知：加入服务依赖关系告诉LLM服务之间的调用关系，根因定位准确率能再提升15-20%：# 从服务网格/APM提取拓扑，作为prompt上下文 SERVICE_TOPOLOGY = """ 服务依赖关系（A→B表示A调用B）： gateway → order-api → inventory-service → mysql-master gateway → user-api → redis-cluster order-api → payment-service → bank-gateway(外部) """ # 加入system prompt即可6.3 多模型协作：快慢结合告警进入 → 7B小模型快速分类（P0/P1/P2, 1秒） ↓ 仅P0/P1进入 → 14B模型深度诊断（5-10秒） ↓ 置信度<60%时 → 72B模型二次诊断（20-30秒）七、总结：给想搞AIOps的团队3条建议先搞知识库，再接模型。模型不是万能的，但模型+你们团队的历史case = 真正有用。知识库的丰富度决定了系统上限。从告警聚合开始，不要一步到位搞自愈。自动修复（auto-remediation）的风险很高，先做到"自动诊断+人工确认+辅助修复"，跑稳了再逐步放开。72%准确率就值得上线。不要追求完美，AIOps的价值不是取代人，而是把on-call的反应时间从分钟级压缩到秒级，把决策质量从"凌晨3点迷糊的人脑"提升到"清醒的AI+历史经验"。附：完整项目结构aiops-diagnosis/ ├── alert_receiver.py # Webhook接收 + 时间窗口聚合 ├── knowledge_base.py # 向量知识库（ChromaDB + BGE） ├── diagnosis_engine.py # LLM诊断引擎 ├── notifier.py # IM推送 ├── feedback.py # 反馈闭环 ├── init_knowledge_base.py # 初始case录入脚本 ├── docker-compose.yml ├── Dockerfile ├── requirements.txt └── chroma_db/ # 知识库持久化目录最小硬件要求：一张RTX 3090/4090（24GB显存跑14B模型），或者无GPU用CPU跑7B模型（慢一些但能用）。如果团队有A100/H100更好，可以直接跑70B效果拉满。全文代码已测试通过，基于 Python 3.11 + Qwen2.5-14B + ChromaDB 0.5。有问题欢迎评论区交流。
线上服务挂了2小时没日志可查！K8s日志采集的坑，我一个不落全踩了
凌晨1点47分，钉钉告警群炸了。订单服务5xx飙到30%，用户下单全部超时。我爬起来打开Kibana准备查日志——空的。切到Grafana看Fluent Bit指标——采集量断崖式归零。日志没了，线上故障等于在黑暗里排雷。最后这个故障花了2小时才定位根因，整个过程中日志系统的崩溃让排查效率暴降80%。事后复盘，我发现日志链路上埋了至少4个定时炸弹，而这些问题90%的K8s集群都存在，只是还没爆而已。事故现场时间：周三凌晨1:47告警：order-service 5xx rate > 20%，持续5分钟用户体感：下单接口超时，页面转圈后报错业务影响：核心交易链路中断，预估损失约12万/小时打开Kibana查order-service最近10分钟ERROR日志，索引里最后一条停在1:32——距离告警过去15分钟，之后一片空白。kubectl logs直接看Pod：$ kubectl logs order-service-7b8f9c6d4-xk2mn -n production --tail=100能看到实时日志。应用还在打日志，问题出在采集链路。排查踩坑——4步全错第一步：Fluent Bit挂了？$ kubectl get pods -n logging -l app=fluent-bit NAME READY STATUS RESTARTS AGE fluent-bit-4xk2n 1/1 Running 0 3d fluent-bit-7nm9p 1/1 Running 0 3d fluent-bit-bx5st 1/1 Running 0 3d全部Running，没重启。第二步：ES写不进去了？$ curl -s elasticsearch.logging.svc:9200/_cluster/health | jq . { "status": "green", "number_of_nodes": 3, "active_shards": 847 }集群健康，green状态。第三步：网络抖动？$ kubectl exec -it fluent-bit-4xk2n -n logging -- curl -s elasticsearch.logging.svc:9200通的，返回正常。到这里已经懵了——Agent在跑、ES正常、网络通畅，日志就是不进来。第四步：看Fluent Bit自身日志$ kubectl logs fluent-bit-4xk2n -n logging --tail=50关键报错出现了：[2025/07/10 01:33:15] [warn] [input:tail:tail.0] pausing tail, mem_buf_limit reached [2025/07/10 01:33:15] [warn] [input:tail:tail.0] pausing tail, mem_buf_limit reached内存缓冲区满了，停止读取日志文件。但缓冲区为什么会满？正常情况下日志从读取到写入ES只需几十毫秒，不可能积压。继续往上翻：[2025/07/10 01:30:42] [error] [output:es:es.0] HTTP status=429, Retry in 15 seconds [2025/07/10 01:30:57] [error] [output:es:es.0] HTTP status=429, Retry in 30 seconds [2025/07/10 01:31:27] [error] [output:es:es.0] HTTP status=429, Retry in 60 secondsES返回429（Too Many Requests），bulk写入被限流了。根因——4个炸弹同时引爆整个崩溃链路：应用日志量突增 → ES bulk队列打满返回429 → Fluent Bit重试堆积 → 内存缓冲区撑满 → 停止采集 → 日志断流日志量为什么突增？回头看监控，1:30有个CronJob触发数据同步任务，每秒打印上万行DEBUG日志，日志量瞬间打到平时50倍。4个问题叠加：问题本质CronJob没有日志级别控制生产环境跑DEBUGES写入队列太小bulk_queue_size默认200，一打满就拒绝Fluent Bit mem_buf_limit只有5MB下游阻塞立刻停采没有对高日志量Pod做采集隔离一个Pod暴增拖垮整个节点任何一个单独存在都不会炸，四个叠在一起就是连环爆。修复方案——配置直接拿走修复1：缓冲区扩容 + 文件缓冲兜底内存缓冲从5MB调到50MB，开启filesystem buffer，满了先落盘不丢弃：[SERVICE] Flush 1 Log_Level info HTTP_Server On HTTP_Listen 0.0.0.0 HTTP_Port 2020 storage.path /var/fluent-bit/storage/ storage.sync normal storage.checksum off storage.max_chunks_up 128 [INPUT] Name tail Path /var/log/containers/*.log Parser cri Tag kube.* Mem_Buf_Limit 50MB storage.type filesystem Skip_Long_Lines On Refresh_Interval 5 Rotate_Wait 30 DB /var/fluent-bit/tail-db/pos.db核心：storage.type filesystem 配合 storage.path，缓冲区满了日志写本地磁盘暂存，下游恢复后再发。这是防丢日志的关键配置。修复2：ES扩大写入队列# elasticsearch.yml thread_pool: write: queue_size: 1000 # 默认200 size: 8 # 写入线程数，按CPU核数调 indices: memory: index_buffer_size: 20% # 默认10%Fluent Bit output端加重试控制：[OUTPUT] Name es Match kube.* Host elasticsearch.logging.svc.cluster.local Port 9200 Logstash_Format On Logstash_Prefix k8s-logs Replace_Dots On Suppress_Type_Name On Buffer_Size 512KB Retry_Limit 5 workers 2workers 2 多线程并发写入提高吞吐，Retry_Limit 5 避免无限重试耗尽资源。修复3：高日志量Pod采集隔离 + 限流[FILTER] Name grep Match kube.* Exclude $kubernetes['labels']['logging-exclude'] true [FILTER] Name throttle Match kube.batch.* Rate 1000 Window 5 Interval 1s Print_Status trueCronJob Pod模板加标签：metadata: labels: logging-exclude: "true"修复4：Kyverno强制生产环境日志级别apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-log-level spec: validationFailureAction: Enforce rules: - name: check-log-level match: resources: kinds: - Pod namespaces: - production validate: message: "生产环境Pod必须设置LOG_LEVEL且不能为DEBUG" pattern: spec: containers: - env: - name: LOG_LEVEL value: "!DEBUG"架构改造——加Kafka缓冲层改造前：容器stdout → Fluent Bit → Elasticsearch（直连）改造后：容器stdout → Fluent Bit → Kafka（缓冲） → Logstash → Elasticsearch ↘ S3（审计日志冷存储）Kafka扛着，ES挂了日志也不丢。Fluent Bit的Kafka output配置：[OUTPUT] Name kafka Match kube.* Brokers kafka-0.kafka.svc:9092,kafka-1.kafka.svc:9092,kafka-2.kafka.svc:9092 Topics k8s-logs Timestamp_Key @timestamp rdkafka.log.connection.close false rdkafka.request.required.acks 1 rdkafka.message.max.bytes 1048576日志系统自身的监控——不能裸奔# Prometheus告警规则 groups: - name: logging-pipeline rules: - alert: FluentBitInputPaused expr: fluentbit_input_paused == 1 for: 2m labels: severity: critical annotations: summary: "Fluent Bit采集暂停！日志可能丢失" - alert: FluentBitOutputRetryHigh expr: rate(fluentbit_output_retries_total[5m]) > 10 for: 3m labels: severity: warning annotations: summary: "Fluent Bit写入重试频繁，下游可能阻塞" - alert: KafkaConsumerLag expr: kafka_consumer_group_lag{group="logstash-consumer"} > 100000 for: 5m labels: severity: warning annotations: summary: "日志消费积压超10万条"Fluent Bit开启metrics端点：[SERVICE] HTTP_Server On HTTP_Listen 0.0.0.0 HTTP_Port 2020Prometheus通过 :2020/api/v1/metrics/prometheus 拉取。三条血泪教训1. 日志系统必须有自己的监控和告警。 日志链路断了你根本不知道，等排查线上故障才发现没日志——已经晚了。2. 永远不要让一个Pod的日志暴增拖垮整个节点的采集。 做好隔离和限流，CronJob/Job这类批处理任务是重灾区。3. 内存缓冲区 ≠ 不丢日志。 Mem_Buf_Limit 到了就停采，必须配合文件缓冲或Kafka才能真正保障。如果你的K8s集群日志方案还是Fluent Bit直连ES、没有缓冲层、没有采集限流、没有日志系统自身的监控——趁没爆之前赶紧改，别等凌晨2点被教做人。觉得有用就转发一下，让更多人少踩这个坑 🙏公众号：耕云躬行录个人博客：躬行笔记
中小团队零成本自建监控体系，告警从200条/天降到5条，这套Prometheus+Loki+Grafana配置我用了3个月才跑通
每月云监控账单1.2万，告警消息一天200多条没人看，真出故障还是靠人肉SSH上去翻日志。50多个微服务跑在200个节点上，监控面板花里胡哨十几个Dashboard，值班的同学打开看一眼就关了——因为根本找不到重点。这就是我们去年底的现状。领导的原话是："你们这个监控，到底是摆设还是真能用？"我当时就觉得，这事不能再拖了。旧架构有多拉胯先说下当时的情况。公司业务增长快，微服务从年初的十几个膨胀到五十多个，峰值QPS大概1.2万，数据库TPS 8000左右。原来用的是某云厂商的监控服务，说实话刚开始还行，十来个服务的时候够用。但服务一多，问题就全暴露了：自定义指标加一个得提工单等两天告警规则只能选预置模板，想写个复杂点的PromQL？不支持日志查询，5分钟之内的还凑合，跨一小时直接超时最离谱的是计费——按时间线数量收费，服务一多时间线爆炸，账单跟着爆炸上个季度有一次核心服务OOM，从告警触发到有人真正看到处理，中间隔了22分钟。22分钟啊，那会儿客户投诉都打到客服那边去了。选型：为什么是这三件套ELK我之前搞过，ES的资源消耗是真的离谱。之前那套ES集群光机器成本每月2万多，日志量还没现在大。Datadog之类的SaaS方案更贵，而且敏感数据放外面领导不同意。最后选了Prometheus + Loki + Grafana：Prometheus — K8s生态下采集时序指标基本没对手，PromQL灵活度天花板Loki — 不做全文索引只索引label，存储成本能压到ES的1/10Grafana — 可视化天花板，而且这三个是同一个生态，集成度天然好选型容易，落地才是噩梦的开始。Prometheus部署：从安装到踩坑基础部署我们用的是kube-prometheus-stack这个Helm Chart，一把梭部署Prometheus、Alertmanager、Grafana和一堆exporter：helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update helm install monitoring prometheus-community/kube-prometheus-stack \ --namespace monitoring \ --create-namespace \ --set prometheus.prometheusSpec.retention=15d \ --set prometheus.prometheusSpec.resources.requests.memory=8Gi \ --set prometheus.prometheusSpec.resources.limits.memory=32Gi \ --set prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage=200Gi \ --set prometheus.prometheusSpec.replicas=2这里有几个参数说下为什么这么配：retention=15d：本地只存15天，长期存储交给Thanos内存request 8G、limit 32G：Prometheus吃内存大户，给少了OOM很常见replicas=2：双副本，一个挂了另一个顶上label基数爆炸问题跑了两周Prometheus内存飙到32G，查PromQL转圈。排查发现开发同学这么埋的点：# 错误示范！user_id是高基数值，绝对不能当label REQUEST_COUNTER.labels( service="order-service", method="POST", endpoint="/api/order", user_id=request.user_id, # 这行就是罪魁祸首 status_code=response.status ).inc()正确做法：# label只用低基数枚举值 REQUEST_COUNTER.labels( service="order-service", method="POST", endpoint="/api/order", status_code=response.status ).inc() # user_id这种高基数信息放到日志或trace里去怎么发现哪些指标有问题？用这个PromQL查时间线top10：topk(10, count by (__name__)({__name__=~".+"}))或者用Prometheus自带的TSDB状态页面 /tsdb-status，能直接看到哪些label的基数最高。我们砍掉不合理label后：时间线200万 → 40万，内存32G → 14G。Thanos高可用部署每个集群的Prometheus配Thanos Sidecar：# prometheus的额外配置，开启Sidecar需要的external_labels apiVersion: monitoring.coreos.com/v1 kind: Prometheus metadata: name: k8s namespace: monitoring spec: externalLabels: cluster: cluster-beijing-01 # 每个集群必须有唯一标识 region: cn-north-1 thanos: image: quay.io/thanos/thanos:v0.35.1 objectStorageConfig: key: thanos-storage.yaml name: thanos-objstore-config对象存储配置（S3兼容）：# thanos-storage.yaml type: S3 config: bucket: "prometheus-thanos-data" endpoint: "s3.cn-north-1.amazonaws.com.cn" access_key: "${AWS_ACCESS_KEY_ID}" secret_key: "${AWS_SECRET_ACCESS_KEY}"Thanos Query做跨集群聚合查询：apiVersion: apps/v1 kind: Deployment metadata: name: thanos-query namespace: monitoring spec: replicas: 2 template: spec: containers: - name: thanos-query image: quay.io/thanos/thanos:v0.35.1 args: - query - --store=dnssrv+_grpc._tcp.thanos-sidecar-beijing.monitoring.svc - --store=dnssrv+_grpc._tcp.thanos-sidecar-shanghai.monitoring.svc - --store=dnssrv+_grpc._tcp.thanos-sidecar-guangzhou.monitoring.svc - --store=dnssrv+_grpc._tcp.thanos-store-gateway.monitoring.svc - --query.replica-label=prometheus_replicaCompactor的坑——降低并发防OOM：thanos compact \ --data-dir=/tmp/thanos-compact \ --objstore.config-file=/etc/thanos/storage.yaml \ --retention.resolution-raw=14d \ --retention.resolution-5m=30d \ --retention.resolution-1h=90d \ --block-sync-concurrency=2 \ # 默认20太高，16G内存扛不住 --compact.concurrency=1Loki部署和优化Simple Scalable模式部署helm repo add grafana https://grafana.github.io/helm-charts helm install loki grafana/loki \ --namespace monitoring \ --set deploymentMode=SimpleScalable \ --set loki.storage.type=s3 \ --set loki.storage.s3.endpoint=s3.cn-north-1.amazonaws.com.cn \ --set loki.storage.s3.bucketnames=loki-logs-data \ --set loki.storage.s3.region=cn-north-1 \ --set loki.schemaConfig.configs[0].from="2024-01-01" \ --set loki.schemaConfig.configs[0].store=tsdb \ --set loki.schemaConfig.configs[0].object_store=s3 \ --set loki.schemaConfig.configs[0].schema=v13 \ --set loki.schemaConfig.configs[0].index.prefix=loki_index_ \ --set loki.schemaConfig.configs[0].index.period=24h关键性能配置Loki的values.yaml里这些参数很关键：loki: limits_config: max_query_series: 5000 # 限制单查询返回的series数 max_query_parallelism: 32 # 查询并发度 max_entries_limit_per_query: 10000 split_queries_by_interval: 1h # 大查询拆分成1h粒度并行 query_timeout: 5m chunk_store_config: chunk_cache_config: embedded_cache: enabled: true max_size_mb: 2048 # 给chunk缓存分2G内存 ingester: chunk_encoding: snappy # 换成snappy，写入性能+30% chunk_target_size: 1572864 # 1.5MB，比默认值大Alloy采集配置// alloy配置 - 采集K8s容器日志 discovery.kubernetes "pods" { role = "pod" } discovery.relabel "pod_logs" { targets = discovery.kubernetes.pods.targets rule { source_labels = ["__meta_kubernetes_namespace"] target_label = "namespace" } rule { source_labels = ["__meta_kubernetes_pod_name"] target_label = "pod" } rule { source_labels = ["__meta_kubernetes_pod_container_name"] target_label = "container" } rule { source_labels = ["__meta_kubernetes_pod_label_app"] target_label = "app" } } loki.source.kubernetes "pods" { targets = discovery.relabel.pod_logs.output forward_to = [loki.process.pipeline.receiver] } loki.process "pipeline" { // 提取JSON日志中的level字段作为label stage.json { expressions = { level = "level" } } stage.labels { values = { level = "" } } // 丢弃debug级别日志，减少存储 stage.drop { expression = ".*" source = "level" value = "debug" } forward_to = [loki.write.default.receiver] } loki.write "default" { endpoint { url = "http://loki-gateway.monitoring.svc:3100/loki/api/v1/push" } }告警治理：完整配置Alertmanager路由配置# alertmanager-config.yaml global: resolve_timeout: 5m route: receiver: 'default-webhook' group_by: ['alertname', 'cluster', 'namespace'] group_wait: 30s # 同一组告警等30s再发，合并通知 group_interval: 5m # 同一组5分钟内有新告警才重新发 repeat_interval: 4h # 同一条告警4小时重复一次 routes: - match: severity: critical receiver: 'phone-call' group_wait: 10s # P0级别等10s就发 repeat_interval: 10m - match: severity: warning receiver: 'wechat-oncall' - match: severity: info receiver: 'wechat-channel' # 抑制规则：底层告警抑制上层衍生告警 inhibit_rules: # 节点挂了，抑制该节点上所有pod告警 - source_match: alertname: NodeDown target_match_re: alertname: PodCrashLooping|PodNotReady|ContainerOOMKilled equal: ['node'] # 集群级别故障抑制服务级别告警 - source_match: severity: critical scope: cluster target_match: severity: warning equal: ['cluster'] receivers: - name: 'phone-call' webhook_configs: - url: 'http://alert-gateway.monitoring.svc:8080/api/v1/phone' send_resolved: true - name: 'wechat-oncall' webhook_configs: - url: 'http://alert-gateway.monitoring.svc:8080/api/v1/wechat/oncall' send_resolved: true - name: 'wechat-channel' webhook_configs: - url: 'http://alert-gateway.monitoring.svc:8080/api/v1/wechat/channel' - name: 'default-webhook' webhook_configs: - url: 'http://alert-gateway.monitoring.svc:8080/api/v1/wechat/default'几条核心告警规则# prometheus-rules.yaml apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: name: core-alerts namespace: monitoring spec: groups: - name: service-sla rules: # 错误率超5%持续2分钟 - alert: HighErrorRate expr: | ( sum(rate(http_requests_total{status=~"5.."}[5m])) by (namespace, service) / sum(rate(http_requests_total[5m])) by (namespace, service) ) > 0.05 for: 2m labels: severity: critical annotations: summary: "{{ $labels.service }} 错误率 {{ $value | humanizePercentage }}" # P99延迟超过2秒 - alert: HighLatencyP99 expr: | histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, namespace, service) ) > 2 for: 5m labels: severity: warning annotations: summary: "{{ $labels.service }} P99延迟 {{ $value }}s" - name: infrastructure rules: # 节点内存使用率超90% - alert: NodeMemoryHigh expr: | (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.9 for: 5m labels: severity: warning annotations: summary: "节点 {{ $labels.instance }} 内存使用率 {{ $value | humanizePercentage }}" # Pod频繁重启 - alert: PodCrashLooping expr: | increase(kube_pod_container_status_restarts_total[1h]) > 5 for: 5m labels: severity: warning annotations: summary: "{{ $labels.namespace }}/{{ $labels.pod }} 1小时内重启 {{ $value }} 次" - name: watchdog rules: # 监控系统自身的存活探测 - alert: Watchdog expr: vector(1) labels: severity: none annotations: summary: "Watchdog正常，如果这条告警消失说明Alertmanager挂了"告警自愈webhook示例# alert_handler.py - 接收Alertmanager webhook，执行自愈 from flask import Flask, request import subprocess import json app = Flask(__name__) @app.route('/api/v1/auto-heal', methods=['POST']) def auto_heal(): data = request.json for alert in data.get('alerts', []): alert_name = alert['labels'].get('alertname') namespace = alert['labels'].get('namespace') pod = alert['labels'].get('pod') if alert_name == 'PodCrashLooping' and alert['status'] == 'firing': # OOM频繁重启的pod，尝试重启deployment deploy = pod.rsplit('-', 2)[0] # 从pod名推断deployment cmd = f"kubectl rollout restart deployment/{deploy} -n {namespace}" result = subprocess.run(cmd.split(), capture_output=True, timeout=30) if result.returncode != 0: # 自愈失败，通知人工介入 notify_oncall(f"自愈失败: {namespace}/{deploy}, 需人工处理") elif alert_name == 'DiskSpaceHigh': node = alert['labels'].get('instance') # 清理Docker镜像缓存 cmd = f"ssh {node} docker system prune -f --filter until=72h" subprocess.run(cmd.split(), capture_output=True, timeout=60) return json.dumps({"status": "ok"}), 200Grafana Dashboard配置要点Dashboard按四层组织，用变量实现模板化：// Dashboard变量配置示例 { "templating": { "list": [ { "name": "namespace", "type": "query", "query": "label_values(kube_pod_info, namespace)", "refresh": 2 }, { "name": "service", "type": "query", "query": "label_values(http_requests_total{namespace=\"$namespace\"}, service)", "refresh": 2 } ] } }从指标跳转到日志的关键——Grafana Data Source配置里设置Derived Fields：# Grafana Loki数据源配置 derivedFields: - name: TraceID matcherRegex: "traceID=(\\w+)" url: "http://tempo.monitoring.svc:3200/api/traces/${__value.raw}" datasourceUid: tempo-datasource最终成果跑了3个多月，硬数据：指标改造前改造后月度监控成本1.2万~2000（机器+存储）日均告警数200+5条故障发现到响应15-22分钟3分钟内日志查询P99超时/15s+3s内监控覆盖率~60%服务100%服务+中间件整套系统资源占用：约60C100G（Prometheus双副本各16C32G + Thanos 8C16G + Loki三组件各4C8G + Grafana 2C4G）。成本降了80%，灵活度和故障响应效率都上了一个台阶。几条可复用的架构准则监控不是越多越好，是越准越好。200条告警没人看 = 没有监控。label设计决定了Prometheus和Loki 80%的使用体验，这个必须在接入前就规范好。告警分级+抑制规则是告警体系的灵魂，没有这两样，告警就是噪音制造机。先跑通核心链路再逐步完善——一步到位铺太大，很容易搞到一半没人维护。监控系统本身需要有独立的存活检测机制，不能只有一条监控链路。你们公司的监控体系现在是什么状态？是还在用云厂商服务，还是已经自建了？自建过程中踩过什么坑，评论区聊聊，互相避雷。觉得有用的话帮忙点个转发，让更多人少走弯路。关注我，后续还会持续分享云原生和SRE方向的实战干货。公众号：耕云躬行录个人博客：躬行笔记
K8S入门真不难，把这篇文章看完就够了
不少兄弟问我K8S怎么学，这事儿说来话长。之前我们公司搞大促，一个传统部署的Java服务突然挂了。运维同事手忙脚乱登上服务器，进程还在，但就是接不进请求。最后排查下来是机器负载太高了，想扩容？行，去申请机器、走审批流程、等IDC上架，一套下来活动都结束了。后来上了K8S，同样的场景再发生怎么办？HPA自动扩容，五分钟不到新Pod就起来了，流量切过去，稳稳当当。从那之后我才真正意识到，K8S这东西不是锦上添花，是雪中送炭。今天就把我这几年踩过的坑、总结的经验，毫无保留地分享给大家。把K8S当成"包工头"来理解很多新手一上来就去看官方文档，看到Pod、Deployment、Service这些名词直接就懵了。我跟你说，别那么正经地去学它，你就把它想象成一个"包工头"。包工头管什么呢？管工人（Pod）、管工地（Node）、管图纸（Deployment）、管门牌号（Service）。这么一想，是不是就接地气多了？具体到技术层面，K8S集群里有两种角色。控制节点（Master）相当于包工头的大脑，负责调度、决策、监控集群状态。一般包括API Server、etcd、Scheduler、Controller Manager这几个组件。etcd是存储集群所有数据的数据库，别小看它，集群挂了它都不能挂。工作节点（Node）是真正干活的工人，每个Node上跑着kubelet、kube-proxy、容器运行时（一般是containerd或者docker）。Pod就是在这个上面实际运行的。我们生产环境用的是3个Master节点 + 10个Worker节点的配置，高可用方面至少Master得保证3个，不然一个挂了集群就出问题了。环境搭建：先别上来就搭生产集群新手最常犯的错误是什么？一上来就要搭生产级K8S集群。兄弟，你连Pod是啥都没搞清楚，搭什么生产集群？我强烈建议，入门阶段就用 minikube 或者 kind 来练手。这两个工具能在你本机起一个单节点的K8S集群，特别适合学习。minikube安装（Mac为例）：brew install minikube minikube start --driver=docker kubectl get nodes看到节点状态变成Ready，恭喜你，你已经有了第一个K8S集群。kubectl是K8S的命令行工具，记住它，后面要天天用。建议配置上自动补全：source <(kubectl completion bash) echo "source <(kubectl completion bash)" >> ~/.bashrczsh用户就把bash换成zsh，这能给你省下大量时间。Pod：K8S里最小的调度单位很多人以为Pod就是容器，其实不完全对。Pod可以理解成一个"盒子"，里面可以装一个或多个紧耦合的容器。这些容器共享网络、存储，命运也连在一起——一个挂了，整个Pod都会被重启。举个真实的例子：我们有个业务是数据采集，需要一个主程序加一个日志收集的sidecar。这种场景下，把它们放在同一个Pod里就很合适。Sidecar模式也是K8S里非常推荐的做法。apiVersion: v1 kind: Pod metadata: name: my-app-pod spec: containers: - name: app image: nginx:1.21 ports: - containerPort: 80 - name: log-collector image: log-collector:v1 volumeMounts: - name: log-volume mountPath: /var/log/app volumes: - name: log-volume emptyDir: {}Deployment：管Pod的"包工头"光有Pod不够，因为Pod挂了就没了，不能自动恢复。Deployment就是来解决这个问题的——它负责保证你指定数量的Pod始终在运行。apiVersion: apps/v1 kind: Deployment metadata: name: web-deployment spec: replicas: 3 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: nginx image: nginx:1.21 ports: - containerPort: 80这个Deployment会保证集群里始终有3个nginx Pod在跑。Pod挂了？自动拉起新的。机器挂了？把Pod调度到其他机器上。我们生产上一个核心服务配置的是replicas=5，然后配合HPA，根据CPU使用率自动扩缩容，平时5个够用，大促的时候能自动扩到20多个，挺省心的。Service：给Pod一个稳定的"门牌号"Pod的IP是不固定的，今天是10.244.1.5，明天可能就变成10.244.1.8了。那别的服务怎么找到它？Service就是来解决这个问题的。最常用的Service类型是ClusterIP，只能在集群内部访问。如果要从外部访问，用NodePort或者LoadBalancer。云上一般用LoadBalancer，会自动创建SLB。apiVersion: v1 kind: Service metadata: name: web-service spec: type: ClusterIP selector: app: web ports: - port: 80 targetPort: 80这个Service会监听所有标签是app=web的Pod，然后把请求转发过去。Service的IP是固定的，Pod再怎么变，Service IP都不变。Namespace：给集群"分房间"当集群里跑了成百上千个服务时，全部堆在一起肯定乱。Namespace就是用来隔离资源的，相当于给集群分了不同的房间。我们生产上是这么分的：按环境分（dev、test、staging、prod），按业务线分（user、order、pay）。这样资源配额也能分开管理，避免某个环境的测试Pod把生产资源吃满。kubectl create namespace production kubectl get ns生产环境实战：一个完整的部署流程光说不练假把式。下面我带大家走一遍生产上部署一个服务的完整流程，包含我们踩过的坑。Dockerfile编写：FROM openjdk:11-jre-slim WORKDIR /app COPY target/myapp.jar /app/ EXPOSE 8080 ENTRYPOINT ["java", "-jar", "myapp.jar"]这里有个坑：基础镜像别用latest标签，版本要固定死。我们之前有个服务用openjdk:latest，结果某天构建的时候拉了最新版本，JDK 8和JDK 11不兼容，服务起不来了。血的教训。K8S部署文件：apiVersion: apps/v1 kind: Deployment metadata: name: myapp namespace: production labels: app: myapp spec: replicas: 3 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: registry.company.com/myapp:v1.0.0 ports: - containerPort: 8080 resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1000m" livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 env: - name: DB_HOST valueFrom: configMapKeyRef: name: app-config key: db.host重点说一下几个配置：resources这块，requests是K8S调度时给Pod分配的最低资源，limits是Pod能用的最大资源。不设limits的话，Pod可能把整个节点资源吃满，影响其他Pod。我们之前就遇到过这种情况，一个有内存泄漏的服务把整个节点搞挂了。健康检查也很关键，livenessProbe判断容器是不是活着，不健康就重启。readinessProbe判断容器能不能接流量，不健康就不接流量。这两个一定都要配，不然滚动更新的时候新版本可能还没起来流量就切过去了，导致服务短暂不可用。环境变量这块，配置和镜像分离，不要把配置写死在代码里。生产上我们用ConfigMap管理配置，用Secret管理敏感信息。应用配置并验证：kubectl apply -f deployment.yaml kubectl get pods -n production kubectl describe pod pod-name -n production kubectl logs -f pod-name -n production kubectl exec -it pod-name -n production -- /bin/bash如果Pod一直起不来，describe命令是你的救命稻草，它会把Pod的详细信息都列出来，包括事件、状态、配置等等。那次差点让我丢工作的故障讲个真实案例，希望大家能引以为戒。去年有个周五下午，我们上线一个新版本。开发那边说就改了个小功能，让运维直接发。流程是这样的：先灰度10%流量，观察10分钟，没问题再全量。结果灰度上去之后，监控系统告警了，错误率飙升到15%。我第一反应是版本有问题，赶紧回滚。回滚之后错误率下来了，但是！有用户反馈数据不一致。我慌了，赶紧看日志、看监控、看链路追踪。最后发现，回滚的时候，K8S是先创建新Pod再删除旧Pod的（滚动更新策略）。那10%灰度的Pod虽然被回滚了，但已经处理了一部分用户请求，这些请求触发的数据变更在新版本里有特殊处理逻辑（他们改的"小功能"），回滚之后这部分数据就出问题了。教训：任何发版都要有回滚预案。这次幸亏灰度比例小，要是全量发布，后果不堪设想。灰度发布要多观察一会儿。我们只观察了10分钟就全量了，有些问题在长时间运行后才会暴露。开发说"小改动"也要走完整测试流程。别问，问就是被坑过。后来我们调整了发布流程：灰度发布最低观察30分钟，配置回滚时自动保留新Pod数据快照，重要业务必须蓝绿发布。流程虽然复杂了，但再没出过类似的事故。入门到进阶，你还需要学这些K8S入门之后，往哪些方向深入？我列几个我们生产上用得最多的：Helm包管理工具必须学。手写YAML文件管理少量服务还行，服务一多就乱套了。Helm相当于K8S的"软件包管理器"，能让你像apt、yum一样管理K8S应用。我们生产上所有服务都用Helm管理，模板统一，配置抽离，省了大量重复劳动。ConfigMap和Secret是基本功。配置和代码分离是基本操作。ConfigMap存普通配置，Secret存敏感信息（密码、token等）。注意Secret默认是base64编码的，不是加密的，敏感信息一定要开启加密存储。持久化存储（PV/PVC）也得懂。Pod里的数据是临时的，Pod一删数据就没了。有状态服务（数据库、文件存储）需要持久化存储。我们用的是云厂商提供的块存储CSI插件，PVC声明存储需求，K8S自动创建PV。自动扩缩容（HPA/VPA）这个对业务很有用。HPA（Horizontal Pod Autoscaler）根据CPU、内存或者自定义指标自动调整Pod数量。VPA（Vertical Pod Autoscaler）自动调整Pod的资源配置。这两个配合用，效果更佳。监控和日志（Prometheus + Grafana + ELK）是生产必备。生产环境没有监控就是裸奔。Prometheus采集指标，Grafana做可视化，ELK（或者Loki）做日志聚合。我们自建了完整的监控告警体系，每个服务上线必须接入监控，否则不允许上线。服务网格（Istio）等有基础了再看。微服务多了之后，服务间调用关系复杂，流量管理、熔断限流、链路追踪这些需求自然就出来了。Istio就是解决这些问题的。不过Istio复杂度比较高，建议有一定K8S基础之后再学习。给新手的几条掏心窝子的话别光看文档，一定要动手。K8S是个实操性很强的东西，光看文档是学不会的。自己搭个环境，从部署一个Nginx开始，慢慢深入，遇到问题就去查、去解决，这个过程比看十遍文档都管用。先会用，再懂原理。很多人陷入了一个误区：非要搞懂K8S每个组件的原理才开始用。兄弟，先把集群搭起来，把服务跑起来，再回头看原理，事半功倍。我们公司新来的运维，都是先培训基本操作，再讲原理。生产环境慎之又慎。学习环境随便折腾，删库也没事。生产环境每一个操作都要三思而后行。我们生产操作都有严格的审批流程，重要操作要双人复核。多看官方文档，多逛社区。K8S官方文档（kubernetes.io）是最权威的学习资料，遇到问题先查文档。社区推荐CNCF、Stack Overflow、GitHub Issues。加入一个学习社群。一个人学容易放弃，找几个志同道合的兄弟一起学，互相分享经验，能走得更远。好了，今天的分享就到这里。这篇文章涵盖了我从生产事故中总结出来的K8S核心概念、环境搭建、部署流程、真实案例以及后续学习方向。K8S这个东西，会了不难，难了不会。关键是要迈出第一步，先把环境搭起来，把第一个Pod跑起来。跑起来了，你就有信心继续深入了。如果觉得这篇文章对你有帮助，欢迎点赞、在看、转发三连支持一波。你的支持是我持续创作的最大动力！后续会持续分享更多运维干货、生产实战经验、各种踩坑指南。咱们一起在运维这条路上走得更远、更稳。公众号：耕云躬行录个人博客：躬行笔记
DNS 解析出问题时，试试我的这套排查方法
半夜被叫起来处理故障，十有八九跟 DNS 有关。用户说网站打不开，你一查发现域名解析不出来；邮件收不到，一查发现 DNS 配置被人改过；新上线的系统别人访问不了，查了一圈是 DNS 缓存没清。这种事儿干这行的都遇到过，今天把我在生产环境里摸爬滚打总结出来的一套排查方法分享出来，Windows、Linux、Mac 三个系统怎么操作，nslookup、dig、traceroute 这些工具怎么用，全都讲清楚。网络到底通不通，先拿 IP 试一把很多人一看到网站打不开，第一反应就是敲 nslookup，其实不用这么急。DNS 的活儿是把域名翻译成 IP，如果网络本身就不通，DNS 翻译得再准确也没用。我的习惯是直接 ping 一个 IP 地址。ping 8.8.8.8，这是 Google 的公共 DNS，国内外基本都能通。或者 ping 114.114.114.114，国内 114DNS 的 IP。如果连这个都 ping 不通，问题在网络层，网线松了、WiFi 断了、网关挂了、出口防火墙拦了，一层层往上查去吧。反过来，IP 能 ping 通，但 ping www.baidu.com 不通，那才能确定是 DNS 的问题。Windows 直接 ping 8.8.8.8 就完事，默认四个包自动停。Linux 和 Mac 得加 -c 4，不然后面一直 ping 下去，ping -c 4 8.8.8.8。这一步不花时间，但能帮你省掉很多瞎忙活。一个域名不行还是所有都不行，先分清楚确认了是 DNS 的问题，接下来得判断是"全局瘫"还是"局部罢工"。百度能打开但公司内网系统进不去，跟所有网站都打不开，这是两码事。所有域名都不行的话，大概率是本机配的 DNS 服务器本身挂了，或者到 DNS 服务器的网络链路断了。你可以 ping 一下本机配置的 DNS 地址，看看能不能通。比如你配的是 8.8.8.8，就 ping -c 2 8.8.8.8 试一下。就一个域名不行的话，可能是这个域名自己的问题，权威 DNS 配置错了、域名过期被暂停了、TTL 没生效导致缓存不一致。这时候我喜欢用全球 ping 工具查一下各地解析情况。推荐三个我用着顺手的：ping.pe 界面简洁，全球节点覆盖全，输入域名就能看到每个节点的解析结果和响应时间。有一次我处理一个海外客户的工单，说域名在国内访问慢，打开 ping.pe 一看，欧美节点正常，亚洲几个节点解析出来的 IP 居然是旧的。一问才知道他们刚换了 DNS 服务商，但 TTL 设的是 86400，一天才过期，全球递归服务器还在用老记录。这种情况本地改 DNS 没用，只能等旧缓存过期，或者在权威端把 TTL 调小点。ping.cn 国内节点比较多，延迟数据更真实。host-tracker.com 老牌工具，也能看历史记录。说到 TTL，这玩意儿在生产环境里必须重视。想改域名解析记录的话，提前把 TTL 调小，比如改成 300 秒，等旧的 TTL 时间过了再改记录，改完确认没问题再调回去。不然你这边改完了，全球还有一堆递归服务器拿着旧数据，用户那边持续访问异常，短的持续一两个小时，长的能拖一两天。本机 DNS 配的啥，得先看清楚排查方向定了，就该看看本机具体用的哪个 DNS 服务器了。Windows 下打开命令行，输入 ipconfig /all，往下翻找到"DNS 服务器"那一行。如果显示 192.168.1.1 这种内网地址，说明 DNS 是路由器自动分配的；如果是 8.8.8.8、1.1.1.1 这种，就是手动改过的。企业内网有时候需要配特定的 DNS 服务器，比如公司域控的 IP，配错了内网域名全解析不了。临时改 DNS 的路径是：控制面板 → 网络和共享中心 → 更改适配器设置 → 右键网卡 → 属性 → IPv4 → 手动填 DNS。改完记得执行 ipconfig /flushdns 清缓存，不然可能还在用旧的解析结果。Linux 下配置文件位置看发行版。老系统直接看 /etc/resolv.conf，但很多新系统这个文件是个软链接，改了重启就没用。CentOS 7 以上用 nmcli dev show | grep DNS，Ubuntu 18 以上用 systemd-resolve --status。Mac 下 scutil --dns 能看到完整配置，或者系统偏好设置里图形界面操作也行。Mac 有个 mDNSResponder 服务，系统升级之后有时候会抽风，解析 .local 域名慢得要死，等好几秒才有反应。之前有台 Mac 升级到 Big Sur 之后这问题特别明显，重启一下服务就正常了：sudo killall -HUP mDNSResponder。nslookup 是入门工具，三大系统都能用这是最基础的 DNS 查询工具，Windows、Linux、Mac 都自带，不用装什么包，上来就能用。nslookup www.baidu.com默认用系统配置的 DNS 服务器查，返回结果里有几个关键信息：Server 显示实际用的 DNS 地址，Address 是解析出来的 IP，Non-authoritative answer 表示这个结果是从缓存里拿的，不是权威服务器直接给的。想指定 DNS 服务器查，加上参数就行：nslookup www.baidu.com 8.8.8.8 nslookup www.baidu.com 1.1.1.1同一个域名用不同 DNS 查出来的结果如果不一样，就有意思了。正常情况下应该是相同的，但如果你配的是运营商 DNS，可能被劫持了，返回的 IP 是广告或者乱七八糟的东西。8.8.8.8 和 114.114.114.114 结果一致但和本地 DNS 不一致，基本就是劫持没跑。查不同记录类型也方便：nslookup -type=mx baidu.com :: 邮件记录 nslookup -type=ns baidu.com :: 权威DNS nslookup -type=txt baidu.com :: SPF/DKIM验证用返回 NXDOMAIN 说明域名不存在，SERVFAIL 说明服务器出问题了，可能是 DNSSEC 配置错误或者权威服务器挂了。dig 信息更全，查问题用它dig 在 Linux 和 Mac 上自带，Windows 得装 BIND 或者用 WSL。功能比 nslookup 强大很多，输出信息也详细得多。dig www.baidu.com输出分好几段。QUESTION SECTION 是你查询的问题，ANSWER SECTION 是返回的答案，包括 A 记录、CNAME 链、TTL，SERVER 是实际查询的 DNS，Query time 是查询耗时。这个 Query time 容易被忽略，如果每次查询都要几百毫秒甚至超时，说明 DNS 服务器响应慢或者网络有问题。指定 DNS 服务器：dig @8.8.8.8 www.baidu.com dig @114.114.114.114 www.baidu.com查各类记录：dig www.baidu.com A :: A记录 dig baidu.com MX :: 邮件记录 dig baidu.com NS :: 权威DNS dig baidu.com TXT :: TXT记录 dig -x 8.8.8.8 :: 反向解析PTR只要 IP 地址的话：dig +short www.baidu.com这一行命令在脚本里用特别方便，不用解析一堆文本。dig +trace 是我排查 DNS 问题最常用的命令：dig +trace www.baidu.com它从根服务器开始一级一级往下查，能看到完整的递归过程：先问根服务器拿到 .com 的权威列表，再问 .com 的权威拿到 baidu.com 的权威，最后拿到具体 IP。如果哪一环卡住了或者返回错误，一眼就能看出来。之前有个项目域名突然解析失败，+trace 走到 .com 权威就停了，返回 SERVFAIL。查了半天发现是域名快过期了，注册商给暂停了解析。赶紧联系注册商续费才恢复。这种问题不 +trace 根本定位不到。dig 还能看 DNSSEC 验证结果：dig +dnssec www.baidu.com返回结果里如果有 RRSIG 记录，说明 DNSSEC 配置正常。如果 DNSSEC 验证失败，dig 会返回 SERVFAIL 并且带 validation disabled 或者 Bogus 字样。Windows 上没 dig 的话，PowerShell 有个替代品：Resolve-DnsName www.baidu.com Resolve-DnsName -Type MX baidu.com Resolve-DnsName -Server 8.8.8.8 www.baidu.com功能差不多，输出格式是 PowerShell 对象，方便脚本处理。traceroute 看网络路径，定位路由问题DNS 解析正常但访问慢或者不通，就得看看数据包走的是什么路了。Windows 用 tracert：tracert www.baidu.com tracert -d www.baidu.com加 -d 不解析主机名，纯显示 IP，看着快而且清楚。Windows 的 tracert 默认用 ICMP，有些运营商或者防火墙会限速 ICMP 包，看到全是星号不代表真断了，别被吓到。Linux 和 Mac 用 traceroute：traceroute www.baidu.comLinux 下我更推荐装个 mtr，yum install mtr 或者 apt install mtr。这个工具结合了 ping 和 traceroute 的功能，能持续监控每一跳的丢包率和延迟：mtr -r -c 100 www.baidu.com-r 是报告模式，-c 100 发 100 个包。输出重点看丢包率和延迟突变的那一跳，基本就是问题所在。之前处理过一个跨境业务慢的问题，mtr 一查发现从国内出去到新加坡那段丢包率 30%，延迟从 50ms 跳到 400ms。反馈给运营商，他们调整了路由策略才解决。hosts 文件和防火墙，两个容易被忽略的坑hosts 文件Windows：C:\Windows\System32\drivers\etc\hostsLinux/Mac：/etc/hosts这个文件优先级比 DNS 查询高，系统会先查 hosts 再查 DNS。测试环境经常改 hosts 做验证，改完忘了删或者不小心推到生产环境里，就容易出幺蛾子。我见过最离谱的一次，有个同事把测试环境的 hosts 映射同步到生产了，导致一个核心域名指向了测试服务器。那天所有用户都打不开正式系统，查了半天最后才想起来看 hosts 文件。改完 hosts 记得清 DNS 缓存：Windows：ipconfig /flushdnsLinux（nscd）：service nscd restartLinux（systemd-resolved）：systemd-resolve --flush-cachesMac：sudo killall -HUP mDNSResponder防火墙DNS 默认走 UDP 53，响应大了会切到 TCP 53。如果防火墙只放行了 UDP 53 但没放 TCP 53，大响应就失败，表现就是有些域名能解析有些不行，特别容易迷惑人。Linux 看 iptables 规则：iptables -L -n | grep 53Linux 看 firewalld：firewall-cmd --list-allWindows 看防火墙：netsh advfirewall show allprofiles云主机还得看安全组规则。阿里云、腾讯云、AWS 的安全组默认可能没开放 53 端口，自己搭 DNS 服务器的话记得加规则。DNS 缓存是个多层嵌套的坑浏览器有缓存，系统有缓存，JVM 有缓存，应用层也有缓存，改完 DNS 记录发现没生效，先别急着骂娘，看看是不是哪层缓存还在用旧的。浏览器缓存：Chrome 地址栏输入 chrome://net-internals/#dns，能看到缓存记录，点 Clear host cache 就能清。无痕模式可以跳过浏览器缓存，是个简单直接的验证方法。系统缓存：前面说过了，不重复。JVM 缓存：Java 应用会缓存 DNS 解析结果。networkaddress.cache.ttl 参数控制缓存时间，JDK 1.8 以后默认 30 秒，但老版本默认是 -1，永久缓存。域名换了 IP，Java 应用不重启就永远连不上新服务器。有个同事换服务器 IP 之后所有服务都正常，就一个 Java 微服务连不上，最后查出来就是 JVM 缓存的问题。Nginx 缓存：Nginx 启动时解析一次 upstream 域名，之后不会自动更新。upstream 的 IP 变了，Nginx 还连旧 IP。解决方案是用 resolver 指令配合变量，让 Nginx 每次请求动态解析。抓包是最后的底牌前面所有方法都试过了还是搞不定，那就 tcpdump 抓包吧，这是最后的手段。Linux/Mac 抓 DNS 包：sudo tcpdump -i any -n port 53实时显示所有 DNS 请求和响应。想保存下来分析：sudo tcpdump -i eth0 -n -s 0 port 53 -w dns.pcap用 Wireshark 打开 dns.pcap，过滤 dns 就能看到所有 DNS 包，dns.flags.response == 0 只看请求，dns.flags.response == 1 只看响应。有一次我遇到 DNS 查询间歇性超时，dig 查十次能有两三次失败。抓包一看，出去的 UDP 包确实发出去了，但回来的响应包时有时无。查了半天发现是出方向做了 SNAT，状态跟踪表偶尔没记录，回包被丢了。改成不 SNAT 或者强制用 TCP 53 就好了。这种问题不抓包根本定位不到。实战案例分享案例一：DNS 劫持客服说南方某省用户访问我们网站会跳到赌博网站。赶紧 dig @8.8.8.8 和 dig @114.114.114.114 对比，Google DNS 返回正常 IP，114 返回的是赌博网站 IP。确认是运营商 DNS 劫持。解决办法：引导用户换公共 DNS、投诉工信部、给网站加 HTTPS 让劫持后的页面无法正常显示。案例二：DNSSEC 配置错误有个客户新上了 DNSSEC，结果全球一半用户访问不了。dig 一查返回 SERVFAIL，dig +dnssec 也没 RRSIG 记录。查出来是 DNSSEC 签名过期了没续签，验证方直接拒绝解析。联系域名注册商续签签名，等全球传播之后就正常了。案例三：TTL 设置太长客户换 DNS 服务商，旧记录删了才发现很多节点还在用旧 DNS。TTL 设的 86400，要一天才过期。用户那边断服一整天。正确做法是提前把 TTL 调成 300 秒，等旧 TTL 时间过了再切换，出了问题还能切回来。案例四：内网 DNS 没配视图公司内网有个 oa.xxx.com，公网也有这个域名，但解析应该指向内网 IP。结果内网用户解析到公网 IP 了，根本打不开。查了一圈发现 DNS 服务器没配 split-horizon 视图，内网查询和公网查询返回了同样的结果。总结DNS 排查其实就那么几条路：先确认网络通不通，再看是单个域名还是所有域名，查本机 DNS 配置，用 nslookup 和 dig 诊断，看缓存和 hosts 文件有没有问题，检查防火墙规则，最后抓包定界。大多数问题几分钟就能定位。生产环境有几个建议：DNS 服务器一定要有备用的，不要只配一个；重要域名做好监控，IP 变了要告警；改 DNS 之前先调小 TTL；不要在 hosts 里留临时配置超过一天。遇到 DNS 问题别慌，按这套流程一步步查，基本都能搞定。实在搞不定的，重启一下试试——有时候物理重启能解决百分之九十的玄学问题。如果觉得这篇内容有帮助，欢迎关注我的公众号耕云躬行录，我会在上面分享更多生产环境的实战经验，都是干货，不整虚的。也可以来我的个人博客 躬行笔记 逛逛，上面有更多排障脚本和工具合集，说不定哪天就用上了。