高并发大流量的Java系统策略揭秘,千万级QPS服务如何平滑启动

背景

无论在测试中还是在线上环境,服务系统刚启动后,第一个请求会比正常的请求响应时间慢很多,一般会到达几百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将不会调用此服务节点(如单体服务实例2),当服务实例启动成功后,在成功调用次数大于设定闸值后,F5会自动调用此服务实例流量。

注册中心注册

在这里插入图片描述
在微服务场景下,已Eureka注册中心为例,只有Provider完全启动后,注册到注册中心,Consumer从注册中心拉取到Provider的信息如IP和状态,才能调用到Provider。

探针探活

在容器环境下,特别是服务间使用service方式(Spring Boot+k8s Service)或Service Mesh(如Istio+Envoy)进行调用时,就需要使用到Kubernetes的探针机制。如下图有两个POD,podA和podB,他们lablel中App=app1,通过Label Selector:App=app1,使Hollowapp Service可以访问到他们,此时pod A的Readiness是Failure,Kubernete会将Service对应的Endpoint中关于Pod A的IP去掉,这样通过Service将只会访问到Pod B,而不会访问到Pod A。
在这里插入图片描述

spring boot 在2.3已经支持

    spec:
      containers:
        - name: *****
          image: *****
          tty: true
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8090
            initialDelaySeconds: 5
            failureThreshold: 10
            timeoutSeconds: 10
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8090
            initialDelaySeconds: 5
            timeoutSeconds: 10
            periodSeconds: 5

Ribbon懒加载

Ribbon的一些客户端配置,并不是随着容器启动而加载。而是在使用时,才进行加载。所以第一次请求时,由于客户端配置的一些类才开始创建,就会导致请求处理时长增加,从而增加请求处理超时的风险。
通过配置关闭ribbon懒加载,让客户端随着容器启动而加载。在初次请求时就无需等待客户端配置类创建,减少请求处理时长。

ribbon.eager-load.enabled=true
ribbon.eager-load.clients=service_id1,service_id2

但是这种配置有个问题,会使启动时长更长。

延迟注册

对于初始化过程需要异步加载资源的复杂应⽤启动过程,由于注册通常与应⽤初始化过程同步
进⾏,从⽽出现应⽤还未完全初始化就已经被注册到注册中⼼供外部消费者调⽤,此时直接调
⽤由于资源未加载完成可能会导致请求报错。通过设置延迟注册,可让应⽤在充分初始化后再
注册到注册中⼼对外提供服务。

通过小流量预热JVM

在这里插入图片描述
在微服务启动时,通过微服务SDK将当前启动时间startTime和预热时间warmUpTime作为元数据Metadata注册到注册中心上。
在微服务SDK的负载均衡策略上进行增强,在计算微服务节点的权重时,可以拿到被调用方的启动时间和预热时间。根据以下的公式来计算权重(currentTime指的是当前时间):
warmUpWeight = weight * ( (currentTime - starTime) / warmUpTime )
下图是调用方计算被调用方单个节点权重的流程图:
在这里插入图片描述
开源 Dubbo 所实现的⼩流量服务预热模型计算如下公式所示:
在这里插入图片描述
模型中应用 QPS 对应的 f(x) 随调用时刻 x 线性变化,x 表示调用时刻的时间,startTime 是应
用开始时间,warmupTime 是用户配置的应用预热时长,k 是常数,一般表示各实例的默认权
重。

总结

在应对高并发大流量情况下的Java系统平滑启动时,我们需要采取综合的策略。通过探活、懒加载、延迟注册和小流量预热,来实现Java系统在高负载情况下的稳定性启动。

参考文章
优雅启动:如何避免流量打到没有启动完成的节点?
《阿里微服务治理白皮书》
掌握SpringBoot-2.3的容器探针:实战篇
Kubernetes Liveness and Readiness Probes

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值