服务空转排查指南:从“serving nothing”到定位链路断点
如果你在监控面板上看到一行状态:4 hours and 37 minutes of serving nothing,第一反应多半是“没流量而已,无所谓”。但真实情况往往没有这么简单:很多服务空转,并不是用户不访问,而是服务根本没有出现在用户能访问到的链路里。这 4 小时 37 分钟内,服务可能一直在空转,资源照常占用,日志一切正常,可请求就是到不了应用。
这不是一个“没流量”的问题,而是一个“链路断点”的信号。
这篇文章要讨论的,正是这种“服务看起来在运行,实际却没有服务任何请求”的现象。我会先讲清楚服务空转的几种可能含义,然后给出从网络入口、负载均衡、服务发现到应用进程的完整排查流程,再用几个最小示例模拟“端口在听、请求进不来”的场景,最后补充监控告警和生产环境治理建议。无论你是后端开发、运维还是 SRE,都应该能在里面找到对应的排查思路。
1. 这篇文章真正要解决的问题
先给结论:服务长时间没有请求,必须当作异常来处理,而不是当作“天然无流量”来接受。
serving nothing 这个词在不同场景下含义不同。在负载均衡器里,它可能表示“后端没有可用实例”;在应用监控里,它可能表示“QPS 持续为 0”;在访问日志里,它可能表示“没有任何请求记录”。但不管落在哪一层,它都意味着:这条服务链路上的某一环断了,或者流量被引到了别处。
真实环境里,一个服务空转 4 小时的原因可能非常隐蔽。常见的有几类:
- 路由配置错误:网关或负载均衡的后端列表里没有这个实例,或者把流量转发到了旧 IP。
- 服务注册失败:实例没有注册到注册中心,或者注册了错误的 IP、端口。
- 健康检查不通过:负载均衡探活失败,自动把实例摘除,于是服务一直在跑,但永远接不到流量。
- 应用层没有真正 accept 连接:进程在监听端口,但请求堆积在连接队列里,或者线程池耗死,外部看就是“服务无响应”。
- 监控数据本身挂了:指标采集、上报或查询链路有问题,面板显示 0,实际可能有流量。
这几种情况需要的处理方式完全不同。只靠“重启一下”大概率解决不了,甚至会掩盖真正的问题。
这篇文章适合以下读者:负责服务部署和上线的后端开发,维护 Kubernetes 或虚拟机集群的运维同学,以及需要建立监控告警体系的 SRE 工程师。读完你会得到一套可执行的“无流量服务排查清单”,也能理解为什么健康检查、服务注册地址和负载均衡后端状态比应用代码更值得优先检查。
2. 服务空转的核心概念与原理
要理解一个服务为什么“空转”,先要看清请求从客户端到后端服务的完整路径。简化来看,一次请求会经过以下链路:
在这条链路上,任何一跳断掉,服务都可能表现为“无请求”。但很多人排查时,只盯着最后一跳——应用进程本身,忽略了前面的路由、注册和探活环节。
2.1 “真无流量”和“假无流量”
先做一个很重要的区分:
| 类型 | 特征 | 常见原因 |
|---|---|---|
| 真无流量 | 用户确实没访问,所有链路正常 | 业务淡季、活动结束、入口关闭 |
| 假无流量 | 用户尝试访问,但流量没到应用 | 路由错误、注册错误、探活失败、监听未生效 |
判断真假无流量,不能只看应用侧 QPS。更可靠的方式是同时看入口流量、负载均衡流量、注册中心和应用日志。如果入口有流量,但某个后端实例没有任何请求,那基本可以断定问题发生在入口之后的链路。
2.2 服务发现与健康检查
在微服务架构里,一个实例要被外部调用,通常需要两步:先注册,再过健康检查。
- 服务注册:实例启动后,把自己的 IP 和端口报给注册中心,比如 Nacos、Eureka、Consul 或 etcd。如果注册信息错误,调用方拿到的是错误地址,请求自然打不到真实服务。
- 健康检查:负载均衡或注册中心会定期探测实例的存活状态。常见方式是 HTTP 探针,比如访问
/healthz、/actuator/health。如果探针返回非 200,或者连续失败达到阈值,这个实例就会被摘除或标记为不健康。
很多服务空转,根源就在这里:应用进程明明活着,但健康检查一直失败,于是负载均衡不发流量过来。从应用角度看,自己“什么都没收到”;从链路角度看,自己早就被移出可用列表了。
2.3 端口监听与连接队列
还有一个很常见的误区:进程在监听端口,不等于进程能正常服务。
在 TCP 连接建立过程中,内核会维护两个队列:SYN 队列和 accept 队列。应用调用 accept() 后,才会从 accept 队列里取连接出来继续处理。如果应用只监听了端口,但没有及时 accept,连接会堆积在队列里。外部客户端表现为“连接能建立,但请求一直没响应”。
这正好对应了标题里 serving nothing 的一种情况:端口在监听,但没有真正服务请求。稍后的示例部分,我会用一段代码模拟这个现象。
3. 环境准备与前置条件
接下来要进入实操。为了避免不同版本带来的干扰,本文采用“通用环境 + 最小示例”的方式,具体版本以你的实际项目为准,但排查思路和命令是通用的。
建议准备以下环境:
- 操作系统:Linux 环境,CentOS 7+、Ubuntu 20.04+ 等均可。
- 网络工具:
curl、ss、telnet、tcpdump,用于验证端口和流量。 - 容器工具:Docker,用于启动测试容器。
- 编排平台:Kubernetes 集群或本地 minikube,用于验证健康检查和 Service 转发问题。
- 应用运行环境:Python 3,用于跑最小示例服务。
- 监控查询工具:如果你有 Prometheus,可以用
curl直接查询指标接口;没有的话,用kubectl和ss也能完成大部分排查。
另外要提醒一点:在生产环境排查时,尽量使用只读命令,不要随意重启服务或修改配置。尤其是在负载均衡、注册中心这种公共组件上操作,必须有授权、有备份、有回滚方案。
4. 核心流程拆解:一条请求到底断在哪
遇到服务空转,不要一开始就去看业务日志。按链路从外到内逐层排查,效率最高。
4.1 第一跳:网络入口是否可达
首先确认服务暴露的入口是否正常。
- 如果是域名访问,先解析域名:
dig或nslookup看解析结果是否符合预期。 - 如果是 IP 访问,确认安全组、防火墙、iptables 规则是否放行了对应端口。
- 如果前面还有负载均衡,确认负载均衡监听器是否正常工作。
这一步的目的是排除“请求根本没进入内网”的可能。很多线上的“无流量”,其实是在安全组或防火墙层被拦截了。
常用命令:
4.2 第二跳:负载均衡和后端列表
如果入口可达,下一步看负载均衡。以 Nginx 为例,需要确认 upstream 中后端地址是否正确,实例是否被标记为 down,是否因为 max_fails 和 fail_timeout 被临时摘除。
常见现象是:后端服务明明启动成功,但 Nginx 返回 502,或者所有请求都落到另一台实例上。这时打开 Nginx 配置,检查 upstream 状态即可。
通过 curl -v 能直观看到响应状态码:
如果返回 502,说明 Nginx 自己认为没有可用后端,问题不在 Nginx,而在后端的健康状况或网络连通性。
4.3 第三跳:服务注册与服务发现
在微服务架构中,这一步非常关键。比较常见的排查方式:
- 登录注册中心控制台,查看目标服务是否在线。
- 查看实例列表里的 IP 和端口是否与该实例实际地址一致。
- 如果注册的 IP 是
127.0.0.1、0.0.0.0或错误的私有网段,调用方拿到这个地址后就会调用失败。
尤其是多网卡环境,很多服务会选错网卡地址并注册上去。这种问题很隐蔽,因为服务本身没报错,日志也正常,但外部就是调不通。
检查方式可以在注册中心侧完成,也可以在应用配置中确认是否显式设置了 ip 字段。建议在配置中直接固定使用指定网卡,或者运行时注入环境变量来保证 IP 正确。
4.4 第四跳:容器编排与健康检查
如果你的服务跑在 Kubernetes 里,这一步是排查重点。
先看 Pod 状态:
describe 的输出会直接给出探针失败原因。比如 Readiness probe failed: HTTP probe failed with statuscode: 404,说明 readinessProbe 配置的路径不存在,容器被标记为未就绪。
接着看 Service 是否有可用的 Endpoint:
如果 Endpoint 列表为空,说明 Service 的 selector 没有匹配到任何 Ready 的 Pod。流量进来后,kube-proxy 没有可转发的目标,最终表现为服务无响应或返回错误。
4.5 第五跳:应用进程与线程池
如果以上链路全部正常,才轮到应用本身。
进程是否活着、端口是否在监听、线程池是否耗尽、连接队列是否堆积,这些都需要在应用层排查。检查顺序如下:
- 进程状态:
ps -ef | grep java或对应语言进程。 - 端口监听:
ss -lntp | grep 8080 - 线程堆栈:Java 应用可用
jstack <pid>,定位是否有线程阻塞在accept或socketRead。 - 连接队列是否堆积:
ss -lntp输出的Recv-Q如果持续增长,说明 accept 速度跟不上。
很多“端口在听,但请求无响应”的问题,都出在这一层。应用代码可能阻塞在某个外部调用上,也可能线程池被慢请求占满,新请求全部排队等待。
5. 完整示例与代码实现
下面用几个最小示例,把上面提到的几类“服务空转”场景复现出来,并给出验证方法。
5.1 示例一:端口在监听,但没有 accept 连接
这个示例模拟“服务进程没挂,端口也在监听,但请求一直无响应”的情况。原因是没有调用 accept(),连接全部堆积在 accept 队列中。
创建一个 Python 文件 empty_server.py:
启动这个“故障版”服务:
然后用 curl 访问:
预期结果是连接建立后一直等待响应,直到超时。因为 TCP 握手可以完成,但应用没有 accept,连接进入了 accept 队列,curl 拿不到任何 HTTP 响应。
此时用 ss 看队列:
如果连接堆积,可以看到 Recv-Q 的值变成 1 或更高,而正常不积压时通常为 0。
再把代码改成“正常版”,补上 accept 和响应:
重启后再用刚才的 curl 命令,就能得到 HTTP/1.1 200 OK 和响应体 ok。
这个示例说明一个关键点:端口在监听,只代表 socket 在系统层面注册成功,不代表应用真的在“服务请求”。
5.2 示例二:Kubernetes 健康检查失败,Pod 持续 NotReady
第二个场景在 Kubernetes 环境下很常见:Deployment 和 Pod 都在,但 Pod 一直 0/1,Service 后端为空。
创建文件 demo-deployment.yaml:
同时创建一个 Service:
应用配置:
因为 nginx 镜像默认没有 /healthz 这个页面,readinessProbe 会一直返回 404,所以 Pod 永远不会进入 Ready 状态。
查看状态:
预期输出中,READY 列是 0/1。
再查看 Service 的端点:
预期输出为 ENDPOINTS 为空,说明 Service 没有可转发的后端。
修复方式有两种:
- 把探针路径改成
/,因为 nginx 默认首页可以访问。 - 给 nginx 增加一个
/healthz的静态页面。
改完后重新 apply,Pod 就会变为 1/1,Service 也会有 Endpoint。
这个示例直接说明了“服务在跑,但被健康检查摘除”的完整过程。遇到类似问题时,kubectl describe pod 里的探针失败信息,比业务日志更有排查价值。
5.3 示例三:注册中心里的 IP 是错误地址
第三种场景多见于微服务架构。应用注册到注册中心时,上报的 IP 不是外部可访问的地址,调用方拿到错误 IP 后无法发起有效请求。
以 Spring Cloud 接入 Nacos 为例,假设配置如下:
这段配置会把实例的注册 IP 设置为 127.0.0.1。如果这个服务部署在同一台机器上,调用方或许还能正常访问;如果部署在不同机器或容器里,调用方拿到 127.0.0.1 后,请求会打到自己本机而不是目标服务,最终导致调用失败或超时。
更隐蔽的情况是:实例确实注册到了注册中心,控制台上也能看到实例,但调用方拿到的地址不对,部分请求超时,部分请求直接连接拒绝。
排查方式:
- 登录注册中心控制台,查看该服务的实例列表。
- 对比实例 IP 是否等于该 Pod 或主机的实际 IP。
- 如果 IP 不对,通过环境变量注入正确地址,或者去掉显式 IP 配置,让框架自动选择正确的网卡。
修复后的配置可以这样:
注意:不同版本的 Nacos 客户端,配置字段命名可能有细微差异,但排查思路是一致的:注册到注册中心的地址,必须能被调用方真正访问到。
5.4 示例四:Nginx upstream 后端不可用
最后看一个负载均衡侧的例子。Nginx 配置了 upstream,但后端不可用时,服务也会表现为“没有请求被正常处理”。
如果 10.0.0.12:8080 上的应用实际监听在别的端口,或者网络不通,Nginx 会在连续失败 3 次后把该后端标记为不可用。之后即使后端恢复,也可能要等到 fail_timeout 超时才会被重新探测。
客户端侧的表现是:服务入口一直在,但返回 502 或 504,大量请求失败。
排查方式:
如果后端地址没问题,再检查 Nginx 是否把实例标记为 down:
配置中一个 down 标记,就能让一个健康的后端完全收不到流量。这种问题在排查时很容易被忽略。
6. 运行结果与效果验证
上面四个示例,各自有不同的验证方式。这里整理成一份“结果对照”,方便你排查时对照。
| 场景 | 现象 | 验证命令 | 判断标准 |
|---|---|---|---|
| 端口监听但未 accept | 请求超时,日志无输出 | ss -lntp 看 Recv-Q;curl --max-time 3 |
Recv-Q 持续增长,curl 超时 |
| 健康检查失败被摘除 | Pod 为 0/1,Endpoint 为空 | kubectl get pods;kubectl get endpoints |
READY 不足,Endpoint 列表为空 |
| 注册中心 IP 错误 | 控制台实例 IP 异常,调用超时 | 注册中心控制台查看实例元数据 | 实例 IP 与应用实际地址不一致 |
| Nginx 后端不可达 | 返回 502/504 | curl -I http://127.0.0.1/;tail error.log |
状态码 502,日志报 connection refused 或 timeout |
如果服务接了 Prometheus,还可以用 PromQL 查看一段时间的请求量:
返回 0 时,不代表没有流量,只代表“被采集到的请求量为 0”。所以指标查询只能作为第一步,真正确认问题,还要结合入口、负载均衡和注册中心的状态一起看。
7. 常见问题与排查思路
排障时,不同现象对应不同原因,下面表格列出高频问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 服务运行但监控 QPS 为 0 | 路由或负载均衡未指向实例 | 检查 Nginx/LB 后端列表 | 修正转发规则 |
| 端口在监听,请求超时 | 应用未 accept,线程阻塞 | ss -lntp、jstack |
修复连接处理逻辑,扩容线程池 |
| Pod 一直 0/1 Ready | readinessProbe 返回 404/超时 | kubectl describe pod |
修复探针路径或服务端状态 |
| 注册中心有实例但调用失败 | 注册 IP 是 127.0.0.1 或错误网段 | 注册中心控制台查看实例信息 | 显式配置正确 IP |
| 负载均衡有流量,但某个后端实例无流量 | 健康检查失败被摘除 | 查看 LB 探活日志 | 修复健康检查端点,调整阈值 |
| 接口返回成功但响应体为空 | 代理吞响应或上游空返回 | tcpdump 抓包确认数据 |
检查代理配置和后端逻辑 |
| 流量突然恢复,但此前空转数小时 | 探活超时触发熔断,自动恢复 | 查看 LB 状态历史 | 优化探活参数,增加告警 |
需要特别提醒:任何对生产环境的修改,都要先确认变更范围,做好备份和回滚方案。不要为了“快速恢复”直接重启服务,否则容易掩盖根因,下次还会复现。
8. 最佳实践与工程建议
服务空转不是一次性的偶发问题,而是系统设计缺陷的暴露。下面这些建议,可以在日常工作中落地。
8.1 每个服务都要有独立的健康检查端点
不要把业务接口当作健康检查接口。健康检查应该独立于业务逻辑,只反映服务是否具备“接受流量”的能力。比如 /healthz 返回 200,不代表数据库连接正常,只代表进程能响应 HTTP 请求。如果需要检查下游依赖,可以单独提供 /readyz 和 /livez,分别用于就绪和存活判断。
8.2 用监控指标代替“感觉”
判断服务有没有流量,不要只看“有没有报错”。要在应用层暴露请求量、错误数、耗时等指标,通过 Prometheus 等系统持续采集。这样当服务空转时,你能快速定位是从哪个时间点开始没有请求,再结合发布记录和配置变更记录缩小范围。
8.3 为“空转”设置告警
一张“QPS 为 0”的页面如果没有告警,等于不存在。建议针对核心服务设置空转告警,条件可以像这样:
持续 15 分钟以上触发警告。注意不要对所有服务都用同一个阈值,低频业务服务要单独排除,否则告警噪音会让大家失去耐心。
8.4 配置变更要有验证和回滚
无论是负载均衡、注册中心还是 Kubernetes 配置,变更前先确认影响面,变更后立刻观察流量。条件允许时,尽量先灰度一台实例,再逐步扩大。
8.5 定期做“断链演练”
生产环境最怕“从来没出过问题,一出问题就是大问题”。可以定期模拟健康检查失败、注册中心不可用、后端实例宕机等场景,验证监控告警和流量切换是否真的生效。
8.6 注册地址一定要显式声明
在多网卡或容器环境下,服务自动选出来的 IP 不一定是对的。建议在部署脚本或环境变量中显式指定注册地址,并设置监控校验“注册 IP 和实际监听 IP 是否一致”。
9. 总结与后续学习方向
回到标题:4 hours and 37 minutes of serving nothing。如果下次你在监控里看到这样的状态,不要简单接受为“没有流量”。按下面的顺序排查:
- 入口是否可达?域名解析、安全组、防火墙。
- 负载均衡后端列表是否包含该实例?
- 注册中心里的实例 IP 和端口是否正确?
- Pod 是否 Ready?Service 是否有 Endpoint?
- 应用是否真的在 accept 连接?线程池和队列是否正常?
这五步走完,绝大多数“服务空转”都能定位到具体断点。排查时要记住一个原则:不要只盯着应用日志,要顺着请求的完整链路从外到内一层层看。
后续可以把这三个方向作为深入重点:一是熟悉 Prometheus 指标体系和告警规则,让空转能被自动发现;二是学习分布式链路追踪,比如 OpenTelemetry 和 Jaeger,观察请求在不同服务间的完整路径;三是在容器化环境里多实践 Kubernetes 探针和 Service 转发机制,避免在健康检查这类基础能力上栽跟头。
服务空转最大的价值,不在于省下多少资源,而在于它提醒你:链路里的某个环节可能已经悄悄失效。早点发现,就能避免它在流量高峰时给你致命一击。建议收藏这篇文章,下次排查时直接对照着做。