微服务中的服务网格Sidecar代理与服务实例启动顺序协调与依赖启动机制
题目描述:
在微服务架构中,当服务实例与Sidecar代理以独立进程形式部署时(如Kubernetes中的Pod内容器模式),服务的启动、重启、扩容等场景会涉及Sidecar代理与服务实例进程之间的启动顺序协调问题。如何设计一种可靠的机制,确保Sidecar代理在完成必要的初始化(如连接控制平面、加载配置、建立监听)之后,服务实例再开始启动并处理请求,同时也要确保在Sidecar代理终止时,服务实例能被优雅地停止处理新请求,从而避免流量丢失、连接失败等问题。
知识背景:
- 在Service Mesh中,Sidecar代理通常负责流量拦截、路由、安全、可观测性等功能。
- 服务实例是实际的业务应用,它依赖Sidecar代理来接收和发送网络请求。
- 如果服务实例在Sidecar代理准备好之前启动并监听端口,它可能无法正确接收经过Sidecar代理的流量,或者向外发送的流量可能无法被Sidecar代理正确代理,导致请求失败。
- 反之,如果Sidecar代理终止时,服务实例还在处理请求,可能造成现有连接被强制中断,影响用户体验和数据一致性。
下面我将详细讲解如何设计这样一种协调与依赖启动机制,并解释其工作原理。
第一步:明确问题与核心需求
首先,我们把这个问题拆解为几个具体场景和需求:
-
启动场景:
- 当Pod(或类似部署单元)启动时,需要确保Sidecar代理先启动并完成初始化,包括:
- 与控制平面(如Istio的Pilot)建立连接,获取最新的路由规则、服务发现信息等。
- 配置好监听端口(如Envoy监听15001端口接收入站流量,监听15006端口接收出站流量转发等)。
- 加载所有必要的安全配置(如TLS证书)。
- 只有当Sidecar代理确认“就绪”后,服务实例容器才能启动。服务实例启动后,通常立即开始监听其业务端口(如8080)。
- 当Pod(或类似部署单元)启动时,需要确保Sidecar代理先启动并完成初始化,包括:
-
终止场景:
- 当Pod被终止(如滚动更新、缩容、故障)时,需要确保Sidecar代理在终止前,能先停止接收新的入站流量,并给服务实例一个“宽限期”处理完已有请求。
- 服务实例在处理完现有请求后,再优雅退出。
- 最后Sidecar代理再终止。
-
健康检查:
- 在运行期间,Sidecar代理和服务实例的健康状态需要独立监控,任何一方不健康都可能触发重启,但需要避免“级联故障”和“启动死锁”。
第二步:解决方案设计——生命周期钩子与探针协调
在容器编排平台如Kubernetes中,我们主要通过生命周期钩子(Lifecycle Hooks) 和探针(Probes) 来实现这种协调。整个机制可以分为启动顺序协调和终止顺序协调两部分。
Part A: 启动顺序协调(Sidecar优先启动)
-
使用Init容器进行Sidecar预配置(可选但推荐):
- 在Pod的容器定义中,可以定义一个
initContainer,它的唯一任务是生成Sidecar代理的初始配置文件(bootstrap config),或者从配置中心拉取必要的配置。 - 这个Init容器在Sidecar代理容器启动之前运行,确保Sidecar启动时有基本的配置可用,加速其初始化过程。
- 在Pod的容器定义中,可以定义一个
-
Sidecar容器的“就绪”探针(Readiness Probe):
- 这是协调启动的关键。在Sidecar代理容器(如Envoy)的定义中,配置一个
readinessProbe。 - 这个探针检查的是Sidecar代理的“管理端口”(如Envoy的15020端口)或一个特定的健康检查接口,该接口返回的状态表明:
- 与控制平面的连接已建立。
- 初始配置(监听器、集群、路由等)已加载完毕。
- 监听端口已打开。
- 例如,可以配置一个HTTP GET请求到
http://localhost:15020/ready,返回200 OK表示就绪。
- 这是协调启动的关键。在Sidecar代理容器(如Envoy)的定义中,配置一个
-
服务实例容器依赖Sidecar的就绪状态:
- Kubernetes Pod中的所有容器默认是并行启动的,为了实现顺序,我们需要让服务实例容器“等待”Sidecar就绪。
- 方法:利用PostStart钩子与共享检查点。这是一种更精细的控制方式,但更常见和标准化的做法是不直接定义顺序,而是让服务实例的启动逻辑自身具备“等待”能力。
- 服务实例启动脚本中主动等待:在服务实例的启动命令(Dockerfile的CMD或Kubernetes的command)中,最开始加入一段等待逻辑。例如,在启动Java应用前,先执行一个脚本,该脚本循环检查Sidecar的健康端点(如localhost:15020/ready),直到返回成功,再启动真正的应用进程。
- 这本质上是将启动顺序的依赖关系内嵌到了服务实例的启动过程中。
图解启动流程:
Pod启动
|
v
Init容器运行(生成Sidecar配置)
|
v
Sidecar容器进程启动
| \
| \-- 执行初始化(加载配置,连接控制平面,打开端口)
| \
| \-- 初始化完成,就绪探针开始返回成功
|
v
服务实例容器进程启动
|
v
服务实例启动脚本执行 -> [循环检测 localhost:15020/ready?]
| |
| (未就绪) (已就绪)
|--- 等待、重试 ---------------> 跳出循环
|
v
服务实例主进程(如Spring Boot应用)启动
|
v
服务实例监听业务端口(如8080),Pod进入Ready状态
Part B: 终止顺序协调(Sidecar最后终止)
-
Pod终止信号流:
- 当Pod被删除时,Kubernetes会向Pod中的每个容器发送
SIGTERM信号。 - 默认情况下,所有容器几乎同时收到信号,这不符合我们的需求。
- 当Pod被删除时,Kubernetes会向Pod中的每个容器发送
-
使用PreStop钩子实现优雅终止序列:
- 这是协调终止的关键。我们需要在服务实例容器上配置一个
lifecycle.preStop钩子。 - 服务实例的PreStop钩子:当Kubelet要终止服务实例容器时,会先执行这个钩子。在这个钩子中,我们可以做两件事:
- 移除就绪状态:让服务实例从服务发现中摘除(如果其自身注册了)或使其Kubernetes就绪探针失败。但更关键的是下一步。
- 静默等待:执行一个
sleep命令,等待一段时间(如20-30秒)。这段时间的目的是什么?- 在此期间,Sidecar代理依然在运行。
- Kubernetes在发送
SIGTERM给服务实例后,会同时发送SIGTERM给Sidecar容器。 - 但Sidecar代理收到
SIGTERM后,会进入“排空”(Drain)模式。在这个模式下:- 停止打开新的下游连接(不再接受新的入站请求)。
- 对已有的连接,继续处理直到完成或超时。
- 由于服务实例的PreStop钩子还在
sleep,它的主进程还没有收到SIGTERM,所以它可以继续处理Sidecar转发过来的、尚未完成的现有连接。
- Sidecar的PreStop钩子(可选但推荐):在Sidecar容器上也配置一个
preStop钩子,其内容可以是一个很短的sleep(如2-5秒)。这确保了Kubernetes在向Pod内所有容器发送SIGTERM时,Sidecar的SIGTERM会因为其preStop钩子的执行而略微延迟收到。这进一步加强了Sidecar后于服务实例收到终止信号的确定性。 - 等待期结束后,服务实例的
preStop钩子结束,Kubernetes向其主进程发送SIGTERM(如果还没发的话),服务实例开始优雅关闭(处理完当前请求,释放资源)。最后,服务实例容器终止。 - 由于Sidecar代理的排空模式也有超时设置,在所有连接处理完毕后或超时后,Sidecar代理也会退出,Sidecar容器终止。
- 这是协调终止的关键。我们需要在服务实例容器上配置一个
图解终止流程:
kubectl delete pod/myapp (或滚动更新触发)
|
v
Pod状态变为Terminating
|
v
1. Kubelet并行调用各容器的PreStop钩子(如果定义了)
- 服务实例容器: 执行PreStop钩子 (脚本: sleep 25s)
- Sidecar容器: 执行PreStop钩子 (脚本: sleep 5s)
|
v
2. PreStop钩子执行完毕后,Kubelet向容器主进程发送SIGTERM
- ***关键顺序***:
a) Sidecar的PreStop钩子短(5s),先结束 -> Sidecar收到SIGTERM -> 进入排空模式,停止接收新请求。
b) 服务实例的PreStop钩子长(25s),仍在sleep。其主进程尚未收到SIGTERM,继续运行。
|
v
3. Sidecar排空期间,将现有连接请求继续转发给服务实例处理。
|
v
4. 25秒后,服务实例PreStop钩子结束 -> 服务实例主进程收到SIGTERM -> 开始优雅关闭,处理完手头请求。
|
v
5. 服务实例进程退出 -> 容器终止。
|
v
6. Sidecar代理在排空超时(或所有连接关闭)后退出 -> Sidecar容器终止。
|
v
Pod删除完成。
第三步:实现细节与注意事项
-
就绪探针的设计:
- Sidecar的就绪探针检查需要真正反映其代理能力是否就绪,而不仅仅是进程存在。例如,Envoy的
/ready端点会在监听器就绪后才返回成功。 - 服务实例的就绪探针应该检查其业务逻辑健康,并且其检查路径必须通过Sidecar代理(即探测请求会先经过Sidecar再到达服务实例)。这样可以间接证明Sidecar到服务实例的路径是通的。
- Sidecar的就绪探针检查需要真正反映其代理能力是否就绪,而不仅仅是进程存在。例如,Envoy的
-
启动等待脚本示例:
# 在服务实例的启动命令中 # wait-for-sidecar.sh until curl -fs http://localhost:15020/ready; do echo "等待Sidecar代理就绪..." sleep 2 done echo "Sidecar已就绪,启动主应用..." exec java -jar /app/my-service.jar -
PreStop钩子配置示例(Kubernetes YAML):
apiVersion: v1 kind: Pod metadata: name: myapp spec: containers: - name: myapp-service # 服务实例容器 image: myapp:latest lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 25"] # 等待Sidecar先进入排空 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 - name: istio-proxy # Sidecar容器 image: istio/proxyv2:latest lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 5"] # 短暂延迟,确保先于业务容器收到SIGTERM readinessProbe: httpGet: path: /ready port: 15020 initialDelaySeconds: 3 periodSeconds: 3 -
排空时间设置:
- Sidecar代理(如Envoy)有自己的
drain_time参数,控制进入排空模式后等待多久强制关闭连接。这个时间应该略大于服务实例PreStop钩子中的sleep时间加上服务实例自身优雅关闭的最大耗时。例如,服务实例sleep 25s+ 优雅关闭5s= 30s,则Envoy的drain_time可设为35s。
- Sidecar代理(如Envoy)有自己的
-
与Kubernetes的terminationGracePeriodSeconds配合:
- Pod级别的
terminationGracePeriodSeconds定义了从发送SIGTERM到强制发送SIGKILL的总时间。 - 所有容器的PreStop钩子执行时间 + 优雅关闭时间必须在这个总时限内完成。例如,如果
terminationGracePeriodSeconds: 60,那么服务实例的sleep 25s+ 关闭时间,以及Sidecar的排空时间,总和应小于60秒。
- Pod级别的
总结:
微服务中Sidecar代理与服务实例的启动顺序协调,核心在于利用就绪探针和启动脚本内等待来保证启动时的依赖顺序;利用PreStop生命周期钩子制造一个时间差,配合Sidecar的排空(Drain)机制,来保证终止时的优雅顺序。这种机制确保了流量在服务生命周期的两端都不会“漏过”Sidecar代理,从而保障了服务网格的流量治理、安全策略在服务启动和关闭的关键时刻依然有效,是实现零停机部署和可靠性的重要基石。