RPC预热转发等高级特性一网打尽|8月更文挑战

513 阅读5分钟

图片

之前的文章中,我们详细阐述了RPC的调用过程,分析了其耗时组成,为我们日常性能调优提供了理论支持。有兴趣的可以点击《详解RPC的一次调用过程》浏览查看。

为了更好的体验和更优的性能,其实RPC悄悄的做了很多工作,本篇就带大家来看下RPC的一些高级特性和其背后的原因。(还是以开源的dubbo和sofa为例来说明)

Part1 RPC为了性能做了哪些努力

1.1 Provider分组和直连

路由寻址,负载均衡是很好,可以保证流量均匀从而保护服务节点稳定。

但是,我们有的时候其实不希望我们的请求乱跑,最好能打到指定的机器上。比如联调和测试的时候,直连功能就显得很重要了。

只有经历过多方合作联调时请求到处乱跑的痛,才知道分组和直连的功能对开发是多么的友好。

//以sofa为例
@Extension(value = "directUrl", order = -20000)
@AutoActive(consumerSide = true)
public class DirectUrlRouter extends Router {
  //...
}

我们可以看到直连路由策略的order属性,被赋予了一个极小的值,变成了优先级最高的路由策略,所以只要配置的直连列表,则会优先走配置中的列表地址。

图片

摘自:www.sofastack.tech

1.2 异步调用

图片

Future异步调用

异步调用对服务性能和并发的支持起到很大的作用。

一般异步调用有Futurn和callback等方式,这里我们说下Future的原理:

调用下游之后,先返回一个Future,上游通过Future.get()方法对结果进行获取,如果结果未返回则会让出CPU资源进入等待,直到结果到达或超时后触发回调方法才被唤醒。由于篇幅问题,Future的核心逻辑的相关注释就不放了,之前的消息消费顺序保障的文章中也有叙述,有兴趣的同学可以看下~

1.3 本地优先、远程优先

很多时候,我们会遇到消费端和服务端可能都是自己的情况。这个时候,在常规的路由寻址之外,又提供给我们一种调用的可能性,就是直接调用当前服务器上的程序,这样做的好处比较明显,省去了网络传输等时间损耗,效率更高。

List<ProviderInfo> localProviderInfo = new ArrayList<ProviderInfo>();
// 解析IP,看是否和本地一致
for (ProviderInfo providerInfo : providerInfos) { 
    if (localhost.equals(providerInfo.getHost())) {
        localProviderInfo.add(providerInfo);
    }
}
// 命中本机的服务端
if (CommonUtils.isNotEmpty(localProviderInfo)) { 
    return super.doSelect(invocation, localProviderInfo);
} else { 
  // 没有命中本机上的服务端
   return super.doSelect(invocation, providerInfos);
}

当然,也需要看业务和内部服务路由的实际情况,比如在阿里的单元化部署下,需要根据用户ID路由到对应的zone进行处理,如果还是优先本机,那就可能在操作数据库的时候涉及到跨zone调用,比走远程rpc更加耗时。因此这种情况下就需要禁用本机优先策略。

1.4 延迟暴露

很多时候,我们的服务需要依赖一些其他内容才可以正常提供服务,比如缓存预热、线程池预热等等,所以,在服务真正就绪之后再注册到配置中心是很有必要的。

//服务注册之前,先延迟
public void export() {
    // 根据配置延迟加载
    if (providerConfig.getDelay() > 0) { 
        Thread thread = factory.newThread(new Runnable() {
         @Override
         public void run() {
             try {
                  Thread.sleep(providerConfig.getDelay());
             } catch (Throwable ignore) { 
             }
              //真正的服务注册逻辑
              doExport();
         }
      });
      thread.start();
   } else {
       doExport();
   }
}

1.5 粘滞连接

问: 我们需要每次都进行路由寻址和负载均衡来确定服务地址么?
答: 大部分情况是有利的,不过有些特殊的场景,更希望多次请求连接到同一台服务器。

比如,有状态的服务(很多带数据功能的服务都是有状态的,比如很久之前的带登陆session的Tomcat服务、存储集群服务等),其实希望每次请求都连接到相同的服务器。

这就用到了粘滞连接功能。

protected ProviderInfo select(...)throws SofaRpcException {
    // 判断isSticky 粘滞连接配置
    if (consumerConfig.isSticky()) {
        //如果最后一次使用的provider不为空,则使用
        if (lastProviderInfo != null) {
            ProviderInfo providerInfo = lastProviderInfo;        
            //获取对应连接
            ClientTransport lastTransport = connectionHolder.getAvailableClientTransport(providerInfo);
            if (lastTransport != null && lastTransport.isAvailable()) {
               checkAlias(providerInfo, message);
               return providerInfo;
            }
        }
    }
    ...
}

1.6 预热转发

前面扯了那么多,其实,这个才是我们今天想说的重点。

预热转发是针对服务节点的负载均衡来说的。因为在服务刚启动的时候,如果请求过多可能会影响机器性能和正常业务,如果将处于预热期的机器的请求转发到集群内其它机器,过了预热期之后再恢复正常,则可以保证服务节点的性能和服务整体的可用性。

那么这个功能是怎么实现的呢?--带权重的随机负载均衡。

图片

摘自sofastack:权重随机的原理

 //累加总权重totalWeight,代码忽略。。。
 
 //在总权重内随机得到一个值
 int offset = random.nextInt(totalWeight);
 
 //确定随机值落在哪个片断上
 for (int i = 0; i < size; i++) {
     offset -= getWeight(providerInfos.get(i));
     if (offset < 0) {
        providerInfo = providerInfos.get(i);
        break;
     }
}

配置示例:

core_proxy_url=weightStarting:0.2,during:60,weightStarted:0.2,address:x.x.x.x,uniqueId:core_unique

如上,预热权重20%,预热持续时长60s。这样,按照上述计算方式,权重小的服务节点被选到的几率就相对小,以此达到权重随机的效果。

推荐阅读:

3. 高并发架构优化:从BAT实际案例看消息中间件的妙用

4. 高并发存储优化:细说数据库索引原理及其优化策略

5. 高并发存储优化:许是史上最详尽分库分表文章之一

6. 高并发存储优化:数据库索引优化Explain实战

7. 高并发存储番外:阿里数据中间件源码不完全解析

8. 高并发存储优化:诸多策略,缓存为王

9. 高并发存储番外:redis套路,一网打尽

10. 高并发服务优化:浅谈数据库连接池

11. 高并发服务优化:详解RPC的一次调用过程

参考资料

[1]

JIT 编译器如何优化代码: "www.ibm.com/docs/zh/sdk…"