一. 优雅启动
- 什么是启动预热
启动预热就是让刚启动的服务,不直接承担全部的流量,而是让它随着时间的移动慢慢增加调用次数,最终让流量缓和运行一段时间后达到正常水平。 - 如何实现
首先对于调用方来说,我们要知道服务提供方的启动时间,这里有两种获取方法:
一种是服务提供方在启动的时候,主动将启动的时间发送给注册中心;
另一种就是注册中心来检测, 将服务提供方的请求注册时间作为启动时间。这两者时间会有一些差异, 但并没有关系, 因为整个预热过程的时间是一个粗略值,即使多个机器节点之间存在 1 分钟的误差也不会影响,并且在真实环境中机器都会开启 NTP 时间同步功能,来保证所有机器时间的一致性。
调用方通过服务发现,除了可以拿到 IP 列表,还可以拿到对应的启动时间。根据基于权重的负载均衡策略, 动态调整权重,随着时间的推移慢慢增加到服务提供方的调用次数。
- 通过这种机制, 对服务提供方进行降权,减少被负载均衡选择的概率,避免让应用在启动之初就处于高负载状态,从而实现服务提供方在启动后有一个预热的过程。
在Dubbo框架中也引入了"warmup"特性,核心源码是在com.alibaba.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance.java中:
protected int getWeight(Invoker<?> invoker, Invocation invocation) {
// 先得到Provider的权重
int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
if (weight > 0) {
// 得到provider的启动时间戳
long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);
if (timestamp > 0L) {
// provider已经运行时间
int uptime = (int) (System.currentTimeMillis() - timestamp);
// 得到warmup的值,默认为10分钟
int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP);
// provider运行时间少于预热时间,那么需要重新计算权重weight(即需要降权)
if (uptime > 0 && uptime < warmup) {
weight = calculateWarmupWeight(uptime, warmup, weight);
}
}
}
return weight;
}
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
// 随着provider的启动时间越来越长,慢慢提升权重weight
int ww = (int) ( (float) uptime / ( (float) warmup / (float) weight ) );
return ww < 1 ? 1 : (ww > weight ? weight : ww);
}
Dubbo2.7.3版本, 参考源码“org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance”
根据calculateWarmupWeight()方法实现可知,随着provider的启动时间越来越长,慢慢提升权重weight,且权重最小值为1,具体执行策略:
1)如果provider运行了1分钟,那么weight为10,即只有最终需要承担的10%流量;
2)如果provider运行了2分钟,那么weight为20,即只有最终需要承担的20%流量;
3)如果provider运行了5分钟,那么weight为50,即只有最终需要承担的50%流量;
二. 优雅关闭
- 为什么需要优雅关闭
对于调用方来说,服务关闭的时候可能会存在以下几种情况:
- 调用方发送请求时,目标服务已经下线。对于调用方来说,是可以立即感知的,并且在其健康列表里面会把这个节点挪掉,也就不会纳入负载均衡选中。
- 调用方发请求时,目标服务正在关闭中,但调用方并不知道它正处于关闭状态,而且两者之间的连接也没有断开,所以这个节点还会存在健康列表里面,所以这个节点仍一定概率会被调用, 从而导致调用失败问题。
- 如何实现优雅关闭
大家可能存有疑问,RPC 里面有服务注册与发现功能, 注册中心的作用就是用来管理服务的状态, 当服务关闭时, 会先通知注册中心进行下线, 然后通过注册中心移除节点信息,这样不就可以保障服务不被调用吗?
那我们来看下关闭的流程:
整个关闭过程中依赖了两次 RPC 调用,一次是服务提供方通知注册中心下线操作,一次是注册中心通知服务调用方下线节点操作。并且注册中心通知服务调用方都是异步的,并不能保证完全实时性,通过服务发现并不能做到应用的无损关闭。
有没有好的解决方案呢?
服务提供方已经进入关闭流程,那么很多对象已经被销毁了,这个时候我们可以设置一个请求“挡板”,挡板的作用就是告诉调用方,服务提供方已经开始进入关闭流程了,不能再处理其他请求了。
这就好比我们去超市结账,在交接班或者下班的时候, 收银员会放一个提示牌在柜台, 提示“该通道已关闭”,不能进行结账, 这个时候客户只能转移到其他可用的柜台上进行结账。
处理流程:
当服务提供方正在关闭,如果还收到了新的业务请求,服务提供方直接返回一个特定的异常给调用方。这个异常就是告诉调用方“我正在关闭,不能处理这个请求”,然后调用方收到这个异常响应后,RPC 框架把这个节点从健康列表挪出,并把其他请求自动重试到其他节点,因为这个请求是没有被服务提供方处理过,所以可以安全地重试到其他节点,这样就可以实现对业务几乎无损的处理。如果要更为完善, 我们还可以加上主动通知机制,这样既可以保证实时性,也可以避免客户端出现重试情况。
如何捕获关闭事件呢?
操作系统的进程的关闭,如果不是强制结束,进程会接收到一个结束信号,Java应用程序,在接收到结束信号时, 会调用Runtime.addShutdownHook 方法触发关闭钩子。 我们在 RPC服务启动的时候,提前注册关闭钩子,在里面添加处理程序,先开启挡板, 然后通知调用方服务已下线。当接收到新来的请求时,挡板会进行拦截,抛出特定异常。为了尽可能地完成正在处理的请求, 我们可以加入计数器机制,把剩余请求纳入计数器当中, 每处理完一个请求, 就减少一个计数, 将所有剩余请求处理完成之后, 再真正结束服务。
在Dubbo框架中, 在以下场景中会触发优雅关闭:
JVM主动关闭(
System.exit(int)
;JVM由于资源问题退出(
OOM
);应用程序接受到进程正常结束信号:
SIGTERM
或SIGINT
信号。
优雅停机是默认开启的,停机等待时间为10秒。可以通过配置dubbo.service.shutdown.wait
来修改等待时间。
基于ShutdownHook方式的优雅停机无法确保所有关闭流程一定执行完成,所以 Dubbo 推出了多段关闭的方式来保证服务完全无损。在关闭应用前,首先通过 QOS(在线运维命令) 的offline指令下线所有服务,然后等待一定时间确保已经到达请求全部处理完毕,由于服务已经在注册中心下线,当前应用不会有新的请求。这时再执行真正的关闭(SIGTERM 或SIGINT)流程,就能保证服务无损。
Dubbo优雅关闭的源码:
- DubboShutdownHook.register方法
注册关闭钩子:
/**
* 注册关闭钩子,在服务关闭时触发执行
*/
public void register() {
if (!registered.get() && registered.compareAndSet(false, true)) {
Runtime.getRuntime().addShutdownHook(getDubboShutdownHook());
}
}
- DubboShutdownHook.doDestroy方法
销毁所有相关资源:
/**
* 关闭注销所有资源, 包括注册器和协议处理器。
*/
public void doDestroy() {
if (!destroyed.compareAndSet(false, true)) {
return;
}
// 销毁所有注册器,包括Zookeeper、etcd、Consul等等。
AbstractRegistryFactory.destroyAll();
// 销毁所有协议处理器,包括Dubbo、Hessian、Http、Jsong等。
destroyProtocols();
}