横看 Dubbo- 微服务治理之无损上线

微服务架构的典型特点是服务规模大,调用关系复杂,微服务间的调用如果发生异常可能导致严重的级联故障,而这种情况在微服务部署阶段会尤为严重,一方面新的实例可能未就绪,另一方面旧的实例被路由更多的流量,变得对异常较为敏感,所以在微服务部署阶段,需要着重设计方案以避免流量损失进而避免发布阶段的故障。
本文首先讨论无损上线模型及解决的问题,再说明 Dubbo 中如何实现无损上线,最后对 Dubbo 源码进行解析说明实现的原理。
无损上线模型
微服务架构
Dubbo 的服务框架如下图所示,可以看到涉及的主要角色包括注册中心、消费者、提供者及监控中心。

撇开监控中心,Dubbo 的服务架构运行阶段的逻辑可表述为:提供者通过注册中心暴露服务实例,消费者从注册中心订阅并监听提供者实例后对提供者发起调用。
此过程中会存在一些问题,我们进一步分析。
存在的问题
上述框架的逻辑可简化为:消费者监听到提供者后发起调用。
实例上线阶段的异常出现在调用时,如果没有调用,则上线阶段不会存在任何问题,而如果要做到有调用情况下无异常,需要做到:在调用之前,确保提供者是已经准备就绪的,可被调用的;在实际调用时,需要确保请求的流量在提供者实例可承受的范围,由此我们可澄清出上线阶段需解决的问题是:
1)设置提供者服务实例通过注册中心暴露的合适时机
2)消费者如何发起流量才不会压垮新起的提供者实例
服务实例暴露的时机
服务实例通过注册中心暴露,如果暴露过早,新实例未准备就绪的情况下接收流量可能会出现调用异常,具体而言,新实例可能正在进行 JIT 编译,CPU 使用率较高,新的流量可能是最后一根稻草;如果暴露过晚,则会影响新版本功能的验证。所以需要设置合适的暴露时机。
如何调用新服务实例
服务实例通过注册中心暴露后,消费者即可发起调用,一种极端的情况是所有流量均打到新的实例,此时新实例大概率是难以承担突发大流量的,导致实例宕机;另一种极端情况是不对新实例发起调用,但此时是起不到发布效果的。所以需要一种合适的流量负载策略。
解决方案
无损上线模型即是针对上述问题提出的解决方案。
具体而言,包括服务实例的延迟注册与小流量预测两个关键点。可用下图表示。

服务实例延迟注册
在实例注册阶段,需确保应用所依赖的中间件组件及服务自身的功能已就绪,才对外提供服务,可采用延迟注册的方式;
小流量预热新服务实例
对实例已注册的情况,设置预热时长及预热曲线,在预热时间范围内,逐步增大流量,保障新启动实例的稳定性。
接下来看 Dubbo 中如何实现上述逻辑及 Dubbo 支持无损上线的底层原理。分延迟注册与小流量预热两个阶段。
延迟注册
延迟注册的使用场景主要是在大规模分布式系统中,当服务提供者数量众多时,通过延迟注册可以有效降低启动时的负载压力,提高系统的稳定性和性能。
实现方法
配置文件方式
1)在服务提供者端的 Dubbo 配置文件中,添加以下配置:
<dubbo:provider delay="5000" />
上述配置中的 delay 属性表示延迟注册的时间,单位为毫秒。这里设置为 5000 毫秒,即 5 秒。
2)在注册中心的 Dubbo 配置文件中,添加以下配置:
<dubbo:registry check="false" />
通过将 check 属性设置为 false,可以禁用 Dubbo 在启动时检查和连接注册中心的行为。这样可以避免服务提供者在启动时立即注册到注册中心,从而实现延迟注册的效果。
dubbo-spring-boot-starter 中
1)设置延迟注册时间
dubbo.provider.delay=5000
上述配置中的 dubbo.provider.delay 表示延迟注册的时间,单位为毫秒。这里设置为 5000 毫秒,即 5 秒。
通过配置该属性,可以在应用启动后延迟一定时间再将服务提供者注册到注册中心,实现延迟注册的效果。
2)设置禁用启动时对注册中心的检查和连接行为
dubbo.registry.check=false
默认情况下,dubbo.registry.check 的值为 true,即 Dubbo 会在启动时检查和连接注册中心。这样可以确保服务提供者正常注册到注册中心,并且消费者可以通过注册中心发现可用的服务。
当 dubbo.registry.check 设置为 false 时,Dubbo 将禁用启动时对注册中心的检查和连接行为。这意味着服务提供者不会立即注册到注册中心,也不会尝试与注册中心建立连接。相反,它将等待消费者第一次调用该服务时才进行注册和连接。
通过将 dubbo.registry.check 设置为 false,可以实现延迟注册的效果。只有真正需要被消费者调用的服务才会被注册到注册中心,从而减轻了启动时的负载压力,提高了系统的性能和稳定性。
注:以上配置基于 2.7.X
了解使用方法后,我们再来看 Dubbo 在原理上是如何支持延迟注册的。
原理说明
分服务的解析、启动及注册三个模块。
服务的解析
2.7.8 版本后的服务解析逻辑由 AnnotationBeanDefinitionParser 迁移至 DubboNamespaceHandler,具体原因见:Spring XML meta-configuration https://github.com/apache/dubbo/issues/6174
在 DubboNamespaceHandler 类 init 函数中可看到 ServiceBean 类对象的解析。

跟进 DubboBeanDefinitionParser 类,可以看到 ServiceBean 的解析过程。

ServiceBean 类继承至 ServiceConfig,而服务的启动和注册则是通过 ServiceConfig 的 export 方法。

在 ServiceConfig 的 export 方法中可以看到启动和注册的实际的执行函数是 doExportUrlsFor1Protocol 方法。
此处值得关注的是 getDelay(),正是此方法控制了服务上线阶段的延迟注册的具体时间。
注:shouldDelay()调用 getDelay(),通过判断是否大于 0,返回布尔值。

doExportUrlsFor1Protocol 方法中通过 Protocol 类对象完成服务的启动和注册。

其中 Protocol 的实现类正是 RegistryProtocol,这里也可以看到 Dubbo SPI 扩展点机制的应用。
此处值得关注的是 REGISTER_KEY,正是此变量控制了服务上线阶段的延迟注册的逻辑。

export 函数实现了启动和注册的逻辑,我们接下来分别看具体是如何实现的。
服务的启动
从 doLocalExport 函数入手。

调用 protocol.export(invokerDelegate),而此处的实现类为 DubboProtocol,其中的核心方法即为 openServer。

同样的套路,先从缓存中按需加载,如果不存在,才真正创建,对应的函数为 createServer。

而 createServer 函数中的具体创建方法则为 Exchangers.bind。

继续跟进 bind 函数,发现 Exchanger 的获取也是通过 Dubbo SPI。

其中的 Exchanger 为 HeaderExchanger。

注意到 HeaderExchanger 初始化对象时传入了 Transporters.bind 对象,同样的,用 Dubbo SPI 方式获取类。

通过 SPI 注解发现默认的 Transporter 即为 NettyTransporter。

继续看 NettyTransporter 的具体实现,发现仅返回了 NettyServer 对象。

那么服务是如何启动的?

注意到 NettyServer 构造函数中调用了父类的构造函数。
进入父类 AbstractServer,发现在对象初始化时会执行 doOpen 方法,而 NettyServer 实现了此方法,所以最终会执行到 NettyServer 的 doOpen。

而 NettyServer 的 doOpen 方法调用了 Netty 的 API 启动了 Socket 服务。

至此,服务的启动过程解析结束。
服务的注册
在‘服务的解析’小节中提到,服务的注册逻辑位于 RegistryProtocol 类 export 函数中,注册代码如下。

注册过程可分解为 Registry 获取与服务注册两步,分别来看。
Registry 获取
跟进 getRegistry 函数可以看到,Registry 是通过 registryFactory 获取到的。

那么 registryFactory 对象是什么?
可以看到 RegistryFactory 采用了自适应扩展点机制,通过获取 URL 中的 protocol 参数决定使用哪种具体的实现。

假设在运行阶段 URL 中的 protocol 参数是"zookeeper",那么对应的 RegistryFactory 实现类是 ZookeeperRegistryFactory,而返回的对象是 ZookeeperRegistry。

服务注册
在实际注册阶段会调用 FailbackRegistry.register 进行注册,此处是模板方法,真正逻辑执行在 ZookeeperRegistry 类中的 doRegister 方法。

而 ZookeeperRegistry 的 doRegister 方法是通过 zk 客户端将服务信息写到 Zookeeper 注册中心。

到这里,完成服务注册的流程解析。
小流量预热
小流量预热实现的功能是服务实例启动阶段逐步增大流量,确保在高并发场景下,服务提供者能够平稳的处理流量,避免突增流量压垮新启动的实例。
实现方法
配置文件方式
1)在服务提供者端的 Dubbo 配置文件中,添加以下配置项:
<dubbo:provider warmup="10000" />
上述配置中的 warmup 属性表示预热时间,单位为毫秒。这里设置为 10000 毫秒,即 10 秒。
2)可选的:还可以对服务进行更详细的配置,如设置每个服务的最大请求数和预热策略等。以下是一个示例:
<dubbo:service interface="com.example.UserService" ref="userService">
<dubbo:method name="getUserInfo" warmup="5000" warmupRequests="100" />
</dubbo:service>
在上述示例中,warmup 属性指定了该方法在系统启动后的预热时间为 5000 毫秒(5 秒),warmupRequests 属性指定了在预热期间处理的请求数为 100 个。
通过以上配置,Dubbo 会在系统启动后,逐渐增加服务的流量,使其平滑过渡到正常的运行状态。
dubbo-spring-boot-starter 中
1)配置预热时长
dubbo.provider.warmup=10000
上述配置中的 dubbo.provider.warmup 表示预热时间,单位为毫秒。这里设置为 10000 毫秒,即 10 秒。
2)可选的:还可以对具体的服务方法进行更详细的配置。以下是一个示例:
dubbo:
service:
com.example.UserService:
warmup: 5000
methods:
getUserInfo:
warmup: 5000
warmupRequests: 100
在上述示例中,针对 com.example.UserService 接口的 getUserInfo 方法,设置了预热时间为 5000 毫秒(5 秒),预热期间处理的请求数为 100 个。
可以看到,Dubbo 的预热配置中仅有预热时间,即多长时间结束预热,但并无预热曲线的定义,即流量以怎样的方式增长,是线性方式还是多阶曲线方式,此处先跳过预热曲线的逻辑,待分析 Dubbo 的预热原理后再来看预热曲线在 Dubbo 中可以如何实现。
原理说明
我们知道 Dubbo 的函数远程调用实际是通过代理类发出的。
执行的核心逻辑是 org.apache.dubbo.rpc.proxy.InvokerInvocationHandler#invoke。

其中的 invoker 对象在 org.apache.dubbo.config.ReferenceConfig#init 阶段设置,通常是 FailoverClusterInvoker,在执行 invoke 函数时,调用的是其父类 AbstractClusterInvoker 中的方法:

此处可以看到,在发出调用前,通过 LoadBalance 筛选出了待调用的服务端实例,跟进具体实现类 doInvoke 可以看到 LoadBalance 是如何通过权重筛选出具体的服务端实例。

跟进 select 函数,可以看到筛选 Invoker 的逻辑最终是在 LoadBalance 中实现的。

在 META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.LoadBalance 文件中的实现类如下:

以 RandomLoadBalance 为例,可以看到在选择 Invoker 时用到了 getWeight 方法。

所以 getWeight 是如何实现的?
在 AbstractLoadBalance 中可以看到 Invoker 的权重(weight)实际上是实例启动时间的二次曲线,如果启动时间在预热时间范围内。

通过此算法即可实现随着启动时间,新实例分配的流量越来越大,从而完成小流量预热功能。
接下来看遗留的问题,如何实现预热曲线。
预热曲线
通过上一小节小流量预热的原理说明,可以看到 Dubbo(2.7.x)中预制的默认预热曲线是二阶的,可满足大部分场景。同时未将预热曲线作为参数暴露,所以无法指定。
如果需要实现其他阶次的预热曲线,可通过扩展负载均衡策略的方式来实现。
要实现自定义负载均衡器,可通过以下步骤:
1.编写自定义 LoadBalance

2.配置自定义 LoadBalance
src/main/resources/META-INF/dubbo 下创建 org.apache.dubbo.rpc.cluster.LoadBalance 文件,添加 LoadBalance 对象:myLoadBalance=xx.MyLoadBalance
3.使用自定义 LoadBalance
1)通过 XML 文件
配置全局:
<dubbo:consumer loadbalance="myLoadBalance" />
配置某个服务:
<dubbo:reference id="demoService" loadbalance="myLoadBalance" interface="xx.DemoService" />
2)dubbo-spring-boot-starter 中
配置全局:
dubbo:
consumer:
loadbalance: your-load-balance-strategy
配置某个服务:

以上。
如果本文对您有帮助,欢迎关注[木语云行]公众号并转发~
声明:本文言论仅代表个人观点。
版权声明: 本文为 InfoQ 作者【Karl】的原创文章。
原文链接:【http://xie.infoq.cn/article/1b2aba25ba873c79b25cc3162】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论