协程、Lua 队列、带宽打满

1、问题现象

在生产现场,早上 6 点,网关出现与所有节点连接断连的异常,具体表现为:

  • 网关所有进程与南北向、东西向的节点全部断开连接;
  • 日志显示与所有的节点都心跳超时;
  • 6 点异常必现

受影响的节点包括:

  • 客户端
  • 后端微服务
  • ZooKeeper
  • Redis(哨兵)
  • RabbitMQ

2、初步分析与验证思路

  1. 现场确认
    与现场确认这两个时间段是否有特殊操作,答复:无。

  2. 日志分析

    • 获取并分析 error.logaccess.log 以及相关组件日志。
    • 发现 9 个进程中,8 个 worker 与客户端、后端服务节点全部心跳超时(HTTP 及私有协议均受影响)。
    • manage 进程与 ZooKeeper、Redis Sentinel、RabbitMQ 也出现连接拒绝或重连。
    • 后端微服务日志显示与网关心跳超时, 但是和zookeeper不超时
  3. 关键现象

    • 网关 ↔ zookeeper 异常

    • 网关 ↔ 微服务 异常

    • 微服务 ↔ zookeeper未见明显异常

      三者对比,于是重点就在于“网关或网络”可能存在问题。

3、前置假设

基于现象,列出网关出问题的可能性:

正向理由(怀疑网关)

  • 两台网关与所有组件心跳超时,而组件之间的交互正常。
  • 现场曾更换过机器,且网关独立部署,依然存在问题,所以和机器本身没关系,原因指向网关应用。

反向理由(怀疑环境/网络)

  • 该系统在多家基金上线运行多年,历史上无类似问题。
  • 网关进程运行正常,持续记录日志,且无异常运行日志,无core生成
  • 日志显示在某一段时间内,与所有节点的连接都心跳超时,日志信息清晰明了,且其余时间段无任何异常

可能原因假设

  • 网关 Bug 导致假死,无法及时处理心跳。
  • 网络质量差,流量高时丢包抖动严重。

4、抓包分析

分为2步,抓包+流量监控

在网关、zookeeper、后端服务三端进行同步抓包。

结果:

  • 三端均能看到心跳包发出,但在网关侧存在大量延迟与重传,且超时集中在 网关 ↔ 各组件 的交互方向。
  • 同时查看网络监控,发现异常时段网关机器的入口带宽飙升至接近万兆上限。

5、问题定位

高带宽来源

  • 网络报文分析显示,异常时段的高流量主要是网关与 Redis 的数据交互。

  • 异常高流量

  • 数据类型为某类公共数据,单份数据体积约 20 MB。

  • 在 6:00~7:00,会进行集中登录,单小时登录量可达 2000+。

  • 登录过程会写入redis:

    • 某类数据(0.5M,每次登录一份)
    • 公共数据(每个进程总共一份)

公共数据采用的是本地cache、redis、权限数据接口的三级缓存。照理来说,公共数据,如果本地cache、redis数据不存在,会调用接口会去获取一份数据,然后写到redis,后续其他进程就可以从redis获取到数据,写入cache,最后就可以直接从cache获取,达到三级缓存的效果。

但是看日志,以及端口,确认nginx从redis获取了32次该类数据,照理8个进程最多获取8次,明显有异常。

代码分析

首先cache整个队列只设置了100,即存储100个key-value,lru的机制。所以怀疑是不是队列被持续打满,导致不停的从redis获取数据。对集中登录的场景进行复现,8个进程的效果并不明显,切换为32个进程,马上可以复现全部超时的场景。

在高并发时,会出现一笔请求从redis获取公共数据,发起网络io调用时,下一笔请求又来临,在cache查询不到公共数据(此时上一笔请求还没有从redis获取到数据,写到本进程的cache),又去redis获取数据,导致重复从redis获取数据。

同时,每笔请求都会进行权限校验,即会从cache get公共数据,应当一直保持为热点数据,就算队列只有100,也应该不会替换。

那么此时,底层技术问题就分为2个部分:

  • openresty实现的协程-cosocket
  • cache的lru机制

原理分析

实际上,公共数据是预加载的,所以redis的数据是一直存在,否则如果redis的数据也不存在,那么直接会打穿缓存到权限数据接口。而对于打满带宽,实际还是打穿了cache,到了redis。

缓存机制

  • 网关缓存结构为:LRU 本地缓存(每进程) + Redis 缓存
  • 本地 LRU 容量默认 100(key-value 对)。
  • 公共数据由于登录阶段几乎不被访问,LRU 认为它是“非热点”数据,容易被淘汰。
  • 大量并发登录写入其他数据,触发 LRU 淘汰,将公共数据移除。
  • 当随后的鉴权请求需要公共数据时,本地缓存命中失败 → Redis 拉取。
  • 协程机制 → 出现并发多次拉取相同大数据的情况。

最终后果

  • Redis 出口带宽被大量大包传输占满(接近万兆上限)。
  • Redis 返回包堵在链路上,阻塞了:
    • 网关 ↔ Redis 的正常交互。
    • 微服务响应网关的心跳包(因为走相同带宽出口)。
  • 心跳包超时 → 网关判定连接断开 → 南北向、东西向链路全部中断。

6、优化

协程的问题无法解决,也不可能加锁来保证时序,影响网关的高性能,因此重点还在于保证数据不被淘汰。那么对缓存进行优化,将cache替换为共享内存,多个进程共享一份,渐少拉取次数,同时共享内存也是LRU的机制,但不是队列,而是内存单位M,最终缓存调整为

  • Worker 使用lua_shared_dict 共享缓存,减少跨 Worker 重复拉取
  • 共享内存可以减少公共数据被淘汰的概率,预留足够大小,几乎不会被淘汰

协程、Lua 队列、带宽打满
https://zjfans.github.io/2025/08/27/协程、Lua 队列、带宽打满/
作者
张三疯
发布于
2025年8月27日
许可协议