缩容时gin服务未及时下线导致nginx仍转发请求,根本原因是gin收到sigterm后仅优雅关闭而未主动注销服务发现(如consul/k8s),且/healthz探针未反映draining状态,致使nginx持续将流量转发至已停止节点,引发502或连接拒绝。

缩容时Gin服务未及时下线,Nginx仍转发请求
这是最典型的连锁反应:后端Gin节点已停止,但Consul或Kubernetes未及时注销该实例,Nginx(或OpenResty)仍在其upstream中保留该地址。结果就是502 Bad Gateway或连接拒绝错误。
根本原因不是Gin本身,而是Gin服务在收到SIGTERM后未完成“主动注销”动作。它只做了优雅关闭(等待活跃请求结束),但没通知服务发现中心自己即将退出。
- 必须在
os.Signal监听到SIGTERM后,立即向Consul发起DELETE请求(如curl -X DELETE http://consul:8500/v1/agent/service/deregister/my-gin-service-123) - 若用Kubernetes,确保
preStop钩子中调用kubectl delete endpoints或依赖Endpoint Controller自动清理(需Pod readinessProbe通过后再注册) - 避免依赖“超时自动剔除”——Consul默认
ttl健康检查是10秒,期间流量仍会打过去
Gin的/healthz探针返回200,但实际已拒绝新请求
很多团队把/healthz写成无状态的静态响应,比如直接c.Status(200)。这导致缩容过程中,服务虽已进入draining状态、不再接受新连接,但健康检查仍持续通过,Nginx不会将其踢出负载池。
正确做法是让健康检查反映真实就绪/可服务状态:
- 维护一个内部原子变量
isDraining,在SIGTERM处理函数中设为true -
/healthzhandler中检查该标志:if isDraining { c.Status(503); return } - 配合Nginx的
max_fails=1 fail_timeout=1s,确保1秒内连续失败即摘除节点
缩容期间WebSocket长连接被强制中断
Gin默认HTTP服务器对长连接没有特殊保护;缩容时若仅靠srv.Shutdown()等待,可能因context超时(如默认30秒)而强行断开未完成的WebSocket握手或心跳帧。
关键不是延长超时,而是分阶段控制:
- 收到SIGTERM后,先关闭HTTP监听器(
srv.Close()),但不关闭WebSocket连接管理器 - 启动独立goroutine,每5秒扫描活跃WebSocket连接,对空闲>30秒的连接发
close帧并等待ACK - Nginx侧需配置
proxy_read_timeout 90、proxy_http_version 1.1、proxy_set_header Connection "",避免代理层提前断连
缩容后Gin残留本地缓存导致状态不一致
如果Gin服务用了sync.Map或lru.Cache缓存用户会话、权限策略等有状态数据,缩容时未清空或同步,新流量切到其他节点后就会读到过期或缺失数据。
这不是Gin的问题,而是架构约束被违反:
- 所有跨节点共享的状态必须外置——用Redis做session存储,用etcd存配置,用MySQL存业务主数据
- 若必须本地缓存,需加版本号+TTL,并在缩容前广播“缓存失效”事件(如通过Redis Pub/Sub)
- 上线时禁止从本地文件加载缓存,所有初始化数据必须走统一服务发现或配置中心拉取











