Kong网关请求超时排查:Docker DNS解析与连接池调优实战 这周的排查是从一个线上告警开始的。我们团队维护的某电商平台订单核心链路所有入口流量都经过 Kong 网关部署形态是 Docker Compose 拉起的 4 个服务容器Kong、Kong 依赖的 Postgres、Redis、以及真正提供业务能力的订单服务。用户反馈 APP 订单列表一直转圈部分请求要 8 秒以上才返回甚至直接超时。一开始我以为是后端数据库慢查询结果查下来Kong 的 CPU、内存都正常Postgres 和 Redis 的压力也不高这就有点意思了——网关没挂后端也没挂但用户体验就是卡死。整个排查从下午 3 点持续到凌晨中间绕了不少弯路最后定位到 Docker 网络里的 DNS 解析问题顺手还把压测时暴露出来的限流提示英文问题一起做了汉化。这篇文章把这几天踩过的坑完整记录下来包括现象、排查手段、修复配置、以及我自己的教训。如果你也在用 Docker 跑 Kong或者你的 API 网关在上游连接时请求经常超时这篇应该能帮你省下至少一个通宵。1. 故障现场Kong 网关请求卡死服务没挂却像挂了1.1 现象描述与第一波排查故障的表现非常奇怪只有一部分请求会卡住不是全挂。我用压测工具简单打了一下 100 并发发现差不多 15% 的请求耗时超过 5 秒而正常请求 30 毫秒就返回了。p99 延迟从平时的 50ms 直接飙到 8 秒以上。更诡异的是当请求卡住时Kong 容器里没有明显的 CPU 或内存占用飙升网络连接数也只是缓慢增长。我第一反应是 Kong 的缓存同步出问题了因为 Kong 依赖 Postgres 存储配置如果数据库连接池满了路由缓存刷新就会卡住。但看了 Kong 的错误日志没有发现任何数据库连接失败。再看 Postgres 的慢查询日志也很干净。那就把注意力放到上游连接上。Kong 的日志里开始出现大量类似这样的记录2025/xx/xx 15:21:33 [error] 123#0: *45678 upstream timed out (110: Connection timed out) while connecting to upstream, client: 172.28.0.100, server: kong, request: GET /api/order/list HTTP/1.1, upstream: http://order-service:8080关键词是while connecting to upstream说明请求在建立到上游连接的那一步就超时了并不是上游处理得慢。也就是说请求根本没送到订单服务手里而是卡在 Kong 和订单服务之间的网络通路。但这就有问题了订单服务容器明明活着Kong 也活着两个容器还在同一个 Docker 网络里怎么会出现连接超时1.2 从日志到链路锁定 upstream 连接阶段为了确认问题范围我在 Kong 容器里直接手动请求订单服务的健康检查接口docker exec -it kong-gateway curl -v http://order-service:8080/health第一次执行慢悠悠转了两三秒返回了 200。连续执行 10 次有 3 次明显延迟超过 1 秒。这就说明问题不在 Kong 的业务逻辑而是 Kong 容器到 order-service 这个服务名之间的解析或连接链路有性能问题。我再对订单服务的 IP 直连测试发现只要绕过服务名、直接用容器 IP 访问响应速度就非常稳定。到这里基本可以断定问题出在 DNS 解析这一层。接着在 Kong 容器里反复执行nslookup order-service结果确实出现了偶发超时间隔几次就会出现一次不能立即返回结果的情况。当时的直觉是 Docker 内置的 DNS 服务在某种条件下变得不稳定但还不确定是最根本原因。真正的深坑是在翻看容器网络配置时一层一层剥出来的。2. 定位 Docker 网络深坑问题根源比想象中更隐蔽2.1 深坑一容器重启导致 IP 漂移旧连接和缓存全变废先看整个部署的网络模型。我们用的是 docker-compose 默认创建的 bridge 网络容器从网段里自动获取 IP。这种模式下容器的 IP 不是固定的只要容器重启DHCP 分配结果就可能变化。订单服务在故障前 20 分钟恰好因为发布重启过一次它的 IP 很可能已经变了。Kong 内置的负载均衡器对 upstream 域名做解析时解析结果会缓存一段时间。如果缓存里留着旧 IP而订单服务已经在新的 IP 上运行那么 Kong 发往旧 IP 的连接全部会超时。日志里出现大量Connection timed out就说得通了。这里要说明一个容易混淆的点Kong 在upstream里配置的是域名order-service它不像普通 Nginx 那样只在启动时解析一次而是会在请求过程中根据 TTL 重新解析。但重新解析依赖的是容器内/etc/resolv.conf指向的 DNS 服务器。在 Docker 环境下这个 DNS 服务器就是 127.0.0.11也就是 Docker 的嵌入式 DNS。如果这个 DNS 查询慢或者返回结果不及时所有新建连接都会卡在解析阶段。2.2 深坑二Docker 内置 DNS 在高并发下会掉链子Docker 的 127.0.0.11 是一个内部 DNS 代理作用是做容器名和服务名的解析。它本身并不是一个高并发的 DNS 服务器实际处理能力非常有限。当容器内大量进程同时发起 DNS 查询尤其当查询走 UDP 时这个内置 DNS 在压力下会出现响应丢弃和延迟。Kong 证书校验、连接池重建、健康检查等都会产生 DNS 查询。而我们当时上线了大促前的压测脚本瞬间涌入的流量让 Kong 创建了大量新连接这就触发了大量并发 DNS 查询。一旦 127.0.0.11 响应不及时Nginx 的请求就会卡在解析阶段表现就是部分请求长超时。还有一个我没有马上意识到的因素容器里的/etc/resolv.conf中往往带上了宿主机配置的 search domain。这样当 Kong 尝试解析order-service时系统会依次尝试拼接 search domain 去查询比如先查order-service.search-domain失败后才会查完整的order-service。在 DNS 无响应的情况下这一步会成倍放大等待时间。你可以进容器查看docker exec -it kong-gateway cat /etc/resolv.conf如果看到search行就说明存在这个问题。默认情况下order-service会自动带上宿主机域名后缀再查一次白白多等一个超时周期。2.3 keepalive 配置不当放大了问题第三个隐蔽因素来自 Kong 的连接池参数。Kong 默认的 upstream keepalive 连接池大小不算大当并发量上来之后连接池里的连接被用完Kong 就只能频繁创建新连接。每创建一个新连接就要重新经历一次 DNS 解析和 TCP 建连。如果 keepalive 池够大大量请求可以复用已有连接就不需要频繁触发 DNS 查询问题可能被推迟甚至避免。但恰恰我们没有针对网关型流量调过这个参数默认配置在高并发面前就像一个小水池洪水一冲就满了后续全部走“新建连接”这条慢路径。也就是说IP 漂移是引子Docker DNS 波动是关键keepalive 池不足是放大器。三个因素叠在一起才出现了那种“服务都没挂但请求卡死”的诡异现场。2.4 最终修复方案不再让 DNS 成为不稳定因素针对上述三个深坑最终修复方案分四步走核心思路是尽可能让 Kong 在访问上游时减少对 DNS 的依赖第一步给订单服务分配静态 IP。在 docker-compose 里通过ipam配置固定网段并给订单服务指定固定地址从根本上避免重启造成的 IP 漂移。第二步在 Kong 容器里配置extra_hosts直接把order-service指向固定的 IP。这样 Kong 容器内的/etc/hosts里就写死了映射关系优先级高于 DNS 查询彻底绕过 Docker 内置 DNS 的不稳定性。第三步调大 Kong 的 upstream keepalive 连接池减少新建连接频率也就减少了触发 DNS 查询的次数。第四步开启 Kong 的 DNS 容错机制设置适当的缓存过期时间让 DNS 查询失败时可以短暂复用旧记录而不是立即超时。3. 限流提示汉化拯救用户体验的小工程3.1 问题来源默认 429 响应全是英文修复了卡死问题之后压测继续跑。为了给下游订单服务更充足的保护我们在 Kong 上开启了 rate-limiting 限流插件策略用的 Redis保证多副本下计数一致。结果压测一跑前端同学立刻反馈请求被限流后弹出的错误提示是英文的用户体验不行必须汉化。Kong 限流插件默认返回的响应只有一行英文{message:API rate limit exceeded}如果团队面向的是国内用户这种提示放到公网环境里显然不合格。而且光改 message 还不够前端同学还想拿到code字段和Retry-After响应头里的等待时长方便在界面上做倒计时提示。3.2 方案一用自定义插件改写 429 响应体如果你用的 Kong 版本比较老或者希望有一个完全可控、不依赖版本差异的方案我推荐直接写一个极简插件在响应阶段对 429 状态码的响应体做统一替换。下面代码可以直接用。先创建插件目录mkdir -p ./plugins/ratelimit-response-cnhandler.lua文件内容local cjson require cjson.safe local RateLimitResponseCN { PRIORITY 1000, VERSION 0.1.0, } function RateLimitResponseCN:header_filter(conf) if ngx.status 429 then kong.response.set_header(Content-Type, application/json; charsetutf-8) end end function RateLimitResponseCN:body_filter(conf) if ngx.status 429 then local retry_after tonumber(ngx.header[Retry-After]) or 0 local body cjson.encode({ code 429, message conf.message, retry_after retry_after, }) ngx.arg[1] body ngx.arg[2] true end end return RateLimitResponseCNschema.lua文件内容return { name ratelimit-response-cn, fields { { config { type record, fields { { message { type string, default 请求过于频繁请稍后再试, } }, }, }, }, }, }关键点拆解一下header_filter阶段主要保证响应头里的 Content-Type 带charsetutf-8否则中文 body 可能被某些客户端解析成乱码。body_filter阶段做了两件事一是用自定义结构拼出新的 JSON二是设置ngx.arg[2] true。这个true表示响应体到此结束如果不设置Nginx 可能还会追加一段原始 body导致返回内容拼接错乱。部署的时候需要把插件目录挂载进 Kong 容器并重新声明插件加载列表volumes: - ./plugins/ratelimit-response-cn:/usr/local/share/lua/5.1/kong/plugins/ratelimit-response-cn environment: KONG_PLUGINS: bundled,ratelimit-response-cn注意这里必须是bundled,ratelimit-response-cn不能只写自定义插件名。bundled代表 Kong 自带的所有插件一旦漏掉内置插件会全部失效。重启 Kong 之后通过 Admin API 把插件挂到对应服务上curl -X POST http://localhost:8001/services/order-service/plugins \ --data-urlencode nameratelimit-response-cn \ --data-urlencode config.message请求过于频繁请稍后再试这里有一个非常容易踩的坑传给 Admin API 的message是中文如果用普通的-d参数直接拼curl 可能会因为编码问题导致表单解析失败。正确做法是使用--data-urlencode做 URL 编码。3.3 方案二如果版本够新直接用 error_template如果你用的 Kong 3.1 以上版本官方已经开始支持在插件配置里自定义错误模板那不写 Lua 也是一种可行路线。以 rate-limiting 插件为例可以在插件配置中增加一个error_template字段内容是一段 JSON 字符串写法大致如下{ name: rate-limiting, config: { minute: 60, policy: redis }, error_template: {\code\:429,\message\:\请求过于频繁请稍后再试\,\retry_after\:\$(headers[Retry-After][1])\} }需要提醒的是不同小版本之间的模板语法有调整实际使用务必以对应版本官方文档为准。我自己的经验是如果只是为限流提示汉化自写插件更稳因为不依赖具体版本也不容易踩模板语言的坑。如果还要改动好几个插件的默认报错那再考虑error_template统一处理。3.4 汉化过程中的两个小坑第一个坑就是刚才提到的 charset。只改响应体内容不把 Content-Type 里加上charsetutf-8大概率会导致中文在 Chrome 和部分 App 的 WebView 里变乱码。自定义插件里已经在header_filter阶段处理了这里就不用再重复设置。第二个坑是Retry-After的处理。Kong 默认限流响应头里的Retry-After是数字字符串表示需要等待多少秒。前端拿到这个字段后通常要做倒计时如果你在响应体里输出的是字符串而不是数字某些前端组件会出现类型判断问题。所以我特意在插件里用tonumber做了转换这一点建议照抄。4. 最终配置方案与验证把坑彻底填平4.1 docker-compose 完整配置参考把排查和修复过程中涉及到的关键配置汇总成一个完整可用的 docker-compose 参考。这里的核心思路是静态 IP 保证不漂移extra_hosts 保证不依赖 DNSkeepalive 保证不频繁建连限流插件保证服务稳定。version: 3.8 networks: kong-net: driver: bridge ipam: config: - subnet: 172.28.0.0/24 services: kong-gateway: image: kong:3.6 container_name: kong-gateway depends_on: - kong-db - order-service environment: KONG_DATABASE: postgres KONG_PG_HOST: kong-db KONG_PG_USER: kong KONG_PG_PASSWORD: kong KONG_PLUGINS: bundled,ratelimit-response-cn KONG_DNS_ORDER: LAST,A,CNAME KONG_DNS_STALE_TTL: 30 KONG_DNS_CACHE_SIZE: 10000 KONG_UPSTREAM_KEEPALIVE_POOL_SIZE: 512 KONG_UPSTREAM_KEEPALIVE_MAX_REQUESTS: 1000 extra_hosts: - order-service:172.28.0.10 volumes: - ./plugins/ratelimit-response-cn:/usr/local/share/lua/5.1/kong/plugins/ratelimit-response-cn ports: - 8000:8000 - 8001:8001 networks: kong-net: ipv4_address: 172.28.0.2 kong-db: image: postgres:13 environment: POSTGRES_DB: kong POSTGRES_USER: kong POSTGRES_PASSWORD: kong networks: kong-net: ipv4_address: 172.28.0.5 redis: image: redis:7 networks: kong-net: ipv4_address: 172.28.0.6 order-service: image: order-service:v1.2.3 container_name: order-service networks: kong-net: ipv4_address: 172.28.0.10这里用到的KONG_DNS_STALE_TTL是 Kong 提供的容错机制当 DNS 查询失败时允许继续使用过期缓存记录一定的时间。在 Docker DNS 偶尔抖动的情况下这个参数非常有用等于给请求上一道保险。KONG_DNS_ORDER设置为LAST,A,CNAME意思是优先用最后一条记录、优先 A 记录避免不必要的额外查询。KONG_UPSTREAM_KEEPALIVE_POOL_SIZE从默认值调大到 512足以覆盖压测场景下的连接复用需求。4.2 验证命令与压测数据对比修完之后不能直接宣布完事必须把之前的故障场景重新压一遍。我用压测工具分别测了修复前后的数据命令大致如下ab -n 5000 -c 100 http://localhost:8000/api/order/list修复前同样 100 并发下p99 到了 8 秒以上还有大量 5xx 超时。修复后p99 回落到 45ms 左右没有出现任何超时请求。同时监控 Kong 的/status接口可以看到connections_waiting稳定在合理区间不再出现连接堆积。再验证一下限流响应是否正常。连续快速请求 60 次之后触发限流curl -i http://localhost:8000/api/order/list返回内容变为HTTP/1.1 429 Too Many Requests Content-Type: application/json; charsetutf-8 Retry-After: 60 {code:429,message:请求过于频繁请稍后再试,retry_after:60}前端同学反馈提示正常倒计时也能正确读取。4.3 监控建议DNS 和连接池指标要盯紧排查完这次问题后我把之前监控视角做了一次更新。以前只盯着 CPU、内存、QPS这次之后加上了三个此前被忽略的观察点第一Kong /status接口里的连接状态。重点关注connections_writing和connections_waiting。如果 writing 持续走高说明上游连接建立速度跟不上请求速度需要查看 DNS 和连接池。第二Kong 的 Prometheus 插件。启用之后可以通过指标看到每个 upstream 的连接分布和健康检查状态。比如kong_upstream_target_health指标能直接看出目标节点是否被标记为不健康。第三容器网络层面的 DNS 波动。Docker 没有现成的 DNS 延迟指标但可以通过定时脚本对关键服务名做nslookup采集耗时连续采集几次就能发现潜在抖动。我自己写了一个 10 秒间隔的检查脚本用来观察 Docker 内置 DNS 的稳定性实测在故障发生前就提前看到了延迟爬升的趋势。5. 排查速查表与避坑清单5.1 症状与解决方案速查表把这次遇到的问题整理成一张速查表后面再遇到类似情况对着查就行。症状可能原因推荐处理部分请求卡死日志报 upstream timed out容器 IP 漂移旧连接指向失效地址固定静态 IP配置 extra_hosts 映射服务名压测时大量请求超时CPU 正常Docker 内置 DNS 并发处理能力不足减少 DNS 查询频率开启 stale_ttl调大 keepalive容器内 nslookup 偶发超时容器 DNS 转发压力大或 search domain 干扰检查 resolv.conf必要时用 hosts 绕过 DNS连接数持续增长请求堆积upstream keepalive 池过小调大KONG_UPSTREAM_KEEPALIVE_POOL_SIZE限流返回英文无法自定义文案Kong 默认错误响应模板自定义插件改写响应体或使用 error_template限流返回中文乱码缺少 charsetutf-8设置 Content-Type 为 application/json; charsetutf-85.2 容器网络避坑清单经历过这次折腾我对容器网络有了几个必须遵守的原则。写在这里算是给团队也给自己立个规矩不要在 bridge 网络里依赖自动分配的容器 IP。凡是作为上游被别的服务访问的容器固定 IP 是第一优先项这能省掉非常多莫名其妙的网络故障。不要高估 Docker 内置 DNS 的能力。容器之间的服务发现尽量用固定 IP 加 hosts 映射或者干脆让服务名直接落到/etc/hosts里。DNS 只在动态扩缩容场景下才值得依赖。不要忽略 keepalive 配置对网关的影响。对于网关这类需要高吞吐转发的基础组件连接池参数不是“性能优化”而是“可用性保障”默认值在大流量下就是不够用。不要把 429 响应当成小事。用户体验往往就毁在这些不起眼的地方一个中文提示虽然简单但直接影响前端对接和用户体感。最后再分享一个小技巧。如果你在排查容器间调用卡死问题最直接的办法是进入调用方容器用nslookup反复查目标服务名同时配合curl -v直连和域名访问做对比。只要两次结果差距明显基本就可以把问题锁定在 DNS 解析链路上不用在业务代码里瞎猜。我个人实际体会是容器里 90% 的“服务没挂但请求卡死”最后都能落到网络层的问题上早点查网络少走很多弯路。