写点什么

千万级 QPS 下服务如何才能平滑启动

作者:HelloGeek
  • 2022 年 8 月 02 日
  • 本文字数:3631 字

    阅读完需:约 12 分钟

背景

无论在测试中还是在线上环境,服务系统刚启动后,第一个请求会比正常的请求响应时间慢很多,一般会在几百 ms~1 秒不等。如果我们的调用方服务设置了超时时间,那么在被调用方服务刚启动时,会有极大概率达到超时时间限制,从而发生超时异常。当流量非常大的时候,可能会发现服务一启动,因为响应时间较慢,立刻被高流量打死,导致服务无法启动,影响生产运行。

参考“阿里微服务治理白皮书”,下图 Spring Cloud 应⽤中第⼀次和第⼆次通过调⽤RestTemplate 调⽤远程服务的耗时对比情况:

原因分析

OpenJDK 使用了 JIT(Just-in-time) 即时编译技术,可以动态的把 Java 字节码编译成高度优化过机器码,提高执行效率,但在编译之前,Java 代码是以相对低效的解释器模式执行的。在应用启动完成后、业务流量刚进来的短时间内,容易出现的状况是大量 Java 方法开始被 JIT 编译,同时业务请求被较慢的解释器模式执行,最终的结果就是系统负载飙高,可能导致很多用户请求超时。Spring Cloud Ribbon 也是懒加载方式,首次请求也会加载大量功能代码,导致请求处理时长增加,从而增加请求处理超时的风险。

解决方案

  1. 第一步要保证系统顺利完全启动完成后,才能有流量访问;

  2. 第二步小流量预热,通过小部分流量让 JVM 虚拟机把高频的代码编译成机器码缓存到 JVM 缓存中,再次使用的时候不会触发临时加载;

  3. 第三步逐步放到流量到全量,当大流量请求使用热点代码时,不用每次都通过都进行解释,最终实现平滑启动。

系统完全启动

如何能确保服务系统完全启动后,流量才能打入呢?最简单最好用的是手工配置反向代理,当一个服务实例要下线时将其 IP 从代理服务器中摘除,上线后观察该节点日志是否启动完毕,再将该节点 IP 地址添加到代码服务器中即可,但需要运维成百上千个节点时,这个工作量就很大。一般我们会选择探活机制来保证系统完全启动后流量才会导入。

F5 探活

如果你是传统基于虚拟机的单体服务,可以在 F5 上配置一个探活端口,F5 可以通过 HTTP 或 TCP 协议,调用此业务指定端口。当调用失败次数超过配置闸值,F5 将不会调用此服务节点,当服务实例启动成功后,在成功调用次数大于设定闸值后,F5 会自动调用此服务实例流量。

注册中心注册

在微服务场景下,已 Eureka 注册中心为例,Provider 会现将信息注册到注册中心,注册完成后,Consumer 会定期从注册中心拉取并更新本地服务列表,通过 Ribbon 负载到对应的 Provider 实例上。

关闭 Ribbon 懒加载

对于中小公司而言,通过配置关闭 Ribbon 懒加载,让客户端随着容器启动而加载,在初次请求时就无需等待客户端配置类创建,减少请求处理时长,也是一个不错的简单粗暴解决方案。具体配置如下:

ribbon.eager-load.enabled=trueribbon.eager-load.clients=service_id1,service_id2
复制代码

探针探活

在容器环境下,特别是服务间使用 service 方式(Spring Boot+k8s Service)或 Service Mesh(如 Istio+Envoy)进行调用时,就需要使用到 Kubernetes 的探针机制。对这个概念不太熟悉的可以参考这里。简单理解 Liveness 是存活探针,检查容器是否还在运行(Running 状态),当探测到容器不健康,将会重启 Pod;Readiness 是就绪探针,判断服务是否可用(Ready 状态),当 Pod 达到 Ready 状态才能接收流量请求。

有两个 POD,podA 和 podB,他们 lablel 中 App=app1。通过 Label Selector:App=app1,使 Hollowapp Service 可以 Kubernetes 内部访问到他们,此时 pod A 的 Readiness 是 Failure,Kubernete 会将 Service 对应的 Endpoint 中关于 Pod A 的 IP 去掉,这样通过 Service 将只会访问到 Pod B,而不会访问到 Pod A。当 Pod A 中 Readiness Probe 变成 Success 后,调用服务将可以重新调用 Pod A 实例。

目前 spring boot 在 2.3 以后已经支持了存活和就绪探针,在 actuator 新增了两个地址:/actuator/health/liveness 和/actuator/health/readiness,具体配置如下:

    <--省略无用信息-->    spec:      containers:        - name: *****          image: *****          livenessProbe:            httpGet:              path: /actuator/health/liveness              port: 8090            initialDelaySeconds: 5   # 容器启动后多久开始探测            failureThreshold: 10     # 连续探测10次失败表示失败            timeoutSeconds: 10       # 表示容器必须在10s内做出相应反馈给probe,否则视为探测失败            periodSeconds: 5         # 探测周期,每5s探测一次          readinessProbe:            httpGet:              path: /actuator/health/readiness              port: 8090            initialDelaySeconds: 10    # 容器启动后多久开始探测      			timeoutSeconds: 2          # 表示容器必须在2s内做出相应反馈给probe,否则视为探测失败      			periodSeconds: 30          # 探测周期,每30s探测一次      			successThreshold: 1        # 连续探测1次成功表示成功      			failureThreshold: 3        # 连续探测3次失败表示失败    <--省略无用信息-->
复制代码

刚兴趣的可以学习掌握SpringBoot-2.3的容器探针:实战篇,这里有比较详细的说明。

小流量预热

在线上发布场景下,很多时候刚启动的冷系统直接处理⼤量请求,可能由于系统内部资源初始化不彻底从⽽出现⼤量请求超时、阻塞、报错甚⾄导致刚发布应⽤宕机等线上发布事故出现。一般我们会使用⼩流量预热⽅法来解决此类问题。

⼀般情况下,刚发布微服务应⽤实例跟其他正常实例⼀样⼀起平摊线上总 QPS。⼩流量预热⽅法通过在服务消费端结合提供者实例的启动时间计算权重+载均衡算法,逐渐递增流量到正常⽔平,这样可以帮助刚启动运⾏进⾏预热,详细 QPS 随时间变化曲线如图所示。

开源 Dubbo

实现的⼩流量服务预热过程原理:

  1. 服务提供端在向注册中⼼注册服务的过程中,将⾃身的预热时⻓ WarmupTime、服务启动时间 StartTime 通过元数据的形式注册到注册中⼼中;

  2. 服务消费端在注册中⼼订阅相关服务实例列表,调⽤过程中根据 WarmupTime、StartTime 计算个实例所分批的调⽤权重;

  3. 刚启动 StartTime 距离调⽤时刻差值较⼩的实例权重下,从⽽实现对刚启动应⽤分配更少流量实现对 其进⾏⼩流量预热。

由于篇幅原因,就不在过多介绍,详情可以下载《阿里微服务治理白皮书》

开源美团 OCTO-RPC

原理也是一样,核心代码如下:


Serivce Mesh Isito-proxy

使用 Service Mesh 的话,Envoy 也提供了相关的预热机制 Slow start mode,目前是支持 Round Robin and Least Request 两种负载聚合,也提供了一套小流量预热模型:

Envoy 默认情况下不开启预热功能,如需开启可以参考如下配置:

  message SlowStartConfig {    // Represents the size of slow start window.    // If set, the newly created host remains in slow start mode starting from its creation time    // for the duration of slow start window.    google.protobuf.Duration slow_start_window = 1;
// This parameter controls the speed of traffic increase over the slow start window. Defaults to 1.0, // so that endpoint would get linearly increasing amount of traffic. // When increasing the value for this parameter, the speed of traffic ramp-up increases non-linearly. // The value of aggression parameter should be greater than 0.0. // By tuning the parameter, is possible to achieve polynomial or exponential shape of ramp-up curve. // // During slow start window, effective weight of an endpoint would be scaled with time factor and aggression: // `new_weight = weight * time_factor ^ (1 / aggression)`, // where `time_factor=(time_since_start_seconds / slow_start_time_seconds)`. // // As time progresses, more and more traffic would be sent to endpoint, which is in slow start window. // Once host exits slow start, time_factor and aggression no longer affect its weight. core.v3.RuntimeDouble aggression = 2; }
复制代码

总结

无损上线看似简单,在虚机和容器环境下,解决方案各不相同,但思路都是一样的,关键包括三大部分:

  1. 实例启动,包括 JVM、Spring 等初始化和一些微服务组件如配置中心的初始化,值得注意的是资源未完全加载前不要注册到注册中心中,可以通过设置延迟注册,让应⽤在充分初始化后再注册到注册中⼼对外提供服务;

  2. 小流量预热,通过客户端负载和权重算法,使刚启动的实例节点流量成线性增长,最终达到正常水平;

  3. 全量运行,需要查看 JVM、接口相应时间指标监控和日志,保证新启动实例上线没问题。


参考文章

优雅启动:如何避免流量打到没有启动完成的节点?

掌握SpringBoot-2.3的容器探针:实战篇

Kubernetes Liveness and Readiness Probes

Configure Liveness, Readiness and Startup Probes

config.cluster.v3.Cluster.SlowStartConfig

envoy slow start mode

《阿里微服务治理白皮书》

发布于: 刚刚阅读数: 3
用户头像

HelloGeek

关注

还未添加个人签名 2018.07.09 加入

还未添加个人简介

评论

发布
暂无评论
千万级QPS下服务如何才能平滑启动_微服务架构_HelloGeek_InfoQ写作社区