在 k8s 平台测试自研 Service Mesh 方案时,发现更新服务时,会有少量请求耗时剧增。跟踪排查后确认是由于 Pod 被删除后,原先的 Pod 的 IP 不存在,客户端建立连接超时引起。
正常升级某个服务的 Deployment。
升级策略,先起一个新实例,再停一个旧实例:
type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0
实例停止前如果没有请求会立即退出,如果有请求则等待最多 60 秒,仍然没有结束时会被强制杀掉。
terminationGracePeriodSeconds: 60
升级过程中,发现服务响应时间的 98 值增长很多,95 值没有太大变化,看起来有少量请求被升级操作影响到了。
排查后,确认部分请求变慢的原因是因为和后端实例建立连接超时,由于使用的是 Go 的 DefaultTransport,所以连接超时时间为 30s,部分请求在超时 30s 后才被重试,从而导致响应时间的 98 值变慢。
为什么建立连接会超时?
原来在升级实例的过程中,实例被杀掉,对应的容器的虚拟 IP 就不存在了,而客户端建立连接时发送的 SYNC 包收不到回应,会一直重发,直到超时。
之所以客户端仍然会给该 IP 发送请求,是因为我们自研的 Service Mesh 方案的服务发现没有采用 k8s 默认的 DNS 轮询方式,而是自己开发的服务发现组件,为了能够更好地配合负载均衡的能力。网关是采用轮询的方式,每隔 10s 从 Discovery 组件同步一次数据,所以被杀掉的实例没有及时被同步到各网关。
为了更好解决问题,我们需要理解 k8s 中单个 Pod 停止的流程。
使用自定义的 Transport,内网的话超时时间可以减少为 1s,让请求尽快被重试,虽然不能解决问题,但是可以有效缓解问题。
思考了问题发生的原因,首先想到的就是能不能让实例先从服务发现中摘除,确认服务发现数据被同步到了各网关后,再杀实例。搜索了 k8s 的相关文档,发现通过 preStop 的 hook 机制,可以实现该功能。
示例配置如下:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: terminationGracePeriodSeconds: 90 containers: - name: nginx image: my-nginx:xxx lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 30"] ports: - containerPort: 80
重点就在于 lifecycle
的配置。对应实例停止时,会先将该实例的服务发现地址从 service
中移除,之后会调用我们给定的命令 sleep 30
,等待 30s 后,再给实例发送 SIGTERM 信号,如果实例超过 terminationGracePeriodSeconds
配置的时间后,会再给实例发送 SIGKILL 信号,强行杀掉实例。
我们服务发现数据同步间隔是 10s,留出 30s 的时间,所有网关的服务发现数据正常情况下已经全部同步完成,不会再有新的流量被路由到该实例上,也就不会出现新建连接超时的问题。
需要注意的是,由于我们延迟了 30s 停止实例,所以保险起见 terminationGracePeriodSeconds 也可以相应的增加 30s。
如果 k8s 自身就能通过 Deployment 参数配置实现上文中我们通过 preStop 实现的功能会更好一些,毕竟是一个比较取巧的方案,不一定完善。
涉及到 k8s 集群的网络解决方案,不一定所有的架构都能支持,需要进一步调研。