前言

通过源码分析可以得出这样一个负载均衡的源码结构图(基于TarsJava SpringBoot):

@EnableTarsServer注解:表明这是一个Tars服务;

  • @Import(TarsServerConfiguration.class):引入Tars服务相关配置文件;

    • Communcator:通信器;

      • getServantProxyFactory():获取代理工厂管理者;
      • getObjectProxyFactory():获取对象代理工厂;
        • createLoadBalance():创建客户端负载均衡调用器;

          • select():选择负载均衡调用器(有四种模式可以选择);

            • invoker:调用器;

              • invoke():具体的执行方法;

                • doInvokeServant():最底层的执行方法;
          • refresh():更新负载均衡调用器;
        • createProtocolInvoker():创建协议调用器;

注:在说明注解时,第一点加粗为注解中文含义,第二点为一般加在哪身上,缩进或代码块为示例,如:

@注解

  • 中文含义
  • 加在哪
  • 其他……
    • 语句示例
    //代码示例

1. Tars客户端启动

我们知道Tars应用可以分为客户端与服务端,而负载均衡逻辑一般在客户端,因此我们将只关注客户端的启动流程。

一个基础知识,SpringBoot应用入口在主启动类,Tars SpringBoot的主启动类是这样的:



可以发现它与普通SpringBoot应用的区别在于多了个@EnableTarsServer注解;

@EnableTarsServer

  • Tars服务;
  • 用在主启动类上;
  • 表名该服务是一个Tars服务,启用Tars功能;

我们从examples/tars-spring-boot-client的主启动类App.java(@EnableTarsServer注解)点进去,可以看到SpringBoot在启动时帮我们做了哪些Tars相关的配置:

@EnableTarsServer注解源码:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TarsServerConfiguration.class)
public @interface EnableTarsServer {
}

可以知道他帮我们引入了Tars服务配置类TarsServerConfiguration.class,我们点进去:

@Configuration
public class TarsServerConfiguration { private final Server server = Server.getInstance(); @Bean
public Server server() {
return this.server;
} @Bean
// 从通信器工厂注入通信器Communcator
public Communicator communicator() {
return CommunicatorFactory.getInstance().getCommunicator();
} @Bean
//通信器后置处理器
public CommunicatorBeanPostProcessor communicatorBeanPostProcessor(Communicator communicator) {
return new CommunicatorBeanPostProcessor(communicator);
} @Bean
//注入配置帮助器
public ConfigHelper configHelper() {
return ConfigHelper.getInstance();
} @Bean
//注入Servlet容器定制器
public ServletContainerCustomizer servletContainerCustomizer() {
return new ServletContainerCustomizer();
} @Bean
//Tars服务器启动生命周期
public TarsServerStartLifecycle applicationStartLifecycle(Server server) {
return new TarsServerStartLifecycle(server);
}
}

在这些容器中,可以看出最重要的是通信器Communicator,里面定义了代理方式、配置文件、负载均衡选择器等重要属性,下面我们来分析这个容器

2. Communicator通信器

通信器,最关键的容器

通过源码分析,我们可以知道这个容器里有通信器相关初始化initCommunicator()、关闭shutdown()、获取容器idgetId()等基础方法,此外,有几个比较关键的方法:

  1. getCommunicatorConfig:获取客户端协调器的配置文件。该配置文件里做了一些超时、线程数等相关配置;

  1. getServantProxyFactory:获取代理工厂管理者。管理者的主要作用是管理ObjectProxyFactory,如果缓存有就从缓存中取,没有就生产;

    public <T> Object getServantProxy(Class<T> clazz, String objName, String setDivision, ServantProxyConfig servantProxyConfig,
    LoadBalance loadBalance, ProtocolInvoker<T> protocolInvoker) {
    //获取管理者的键
    String key = setDivision != null ? clazz.getSimpleName() + objName + setDivision : clazz.getSimpleName() + objName;
    //通过键从缓存中获取管理者的值
    Object proxy = cache.get(key);
    if (proxy == null) {
    lock.lock();
    try {
    proxy = cache.get(key);
    if (proxy == null) {
    //创建管理者
    ObjectProxy<T> objectProxy = communicator.getObjectProxyFactory().getObjectProxy(
    clazz, objName, setDivision, servantProxyConfig, loadBalance, protocolInvoker);
    //将管理者放进缓存
    cache.put(key, createProxy(clazz, objectProxy));
    proxy = cache.get(key);
    }
    } finally {
    lock.unlock();
    }
    }
    return proxy;
    }
  2. getObjectProxyFactory:获取对象代理工厂。该工厂的作用是生产对象代理ObjectProxy,包括创建Servant服务的配置信息与更新服务端点等:

    //生产对象代理ObjectProxy
    public <T> ObjectProxy<T> getObjectProxy(Class<T> api, String objName, String setDivision, ServantProxyConfig servantProxyConfig,
    LoadBalance<T> loadBalance, ProtocolInvoker<T> protocolInvoker) throws ClientException {
    //如果容器里没有服务代理相关配置,则生成默认配置;如果容器里有服务代理相关配置,说明用户自定义了用户配置了服务代理,则读取用户配置文件进行自定义配置(SpringBoot的核心思想之一)
    if (servantProxyConfig == null) {
    servantProxyConfig = createServantProxyConfig(objName, setDivision);
    } else {
    servantProxyConfig.setCommunicatorId(communicator.getId());
    servantProxyConfig.setModuleName(communicator.getCommunicatorConfig().getModuleName(), communicator.getCommunicatorConfig().isEnableSet(), communicator.getCommunicatorConfig().getSetDivision());
    servantProxyConfig.setLocator(communicator.getCommunicatorConfig().getLocator());
    addSetDivisionInfo(servantProxyConfig, setDivision);
    servantProxyConfig.setRefreshInterval(communicator.getCommunicatorConfig().getRefreshEndpointInterval());
    servantProxyConfig.setReportInterval(communicator.getCommunicatorConfig().getReportInterval());
    } //更新服务端点
    updateServantEndpoints(servantProxyConfig); //【重要】创建客户端负载均衡调用器
    if (loadBalance == null) {
    loadBalance = createLoadBalance(servantProxyConfig);
    } //创建协议调用器
    if (protocolInvoker == null) {
    protocolInvoker = createProtocolInvoker(api, servantProxyConfig);
    }
    return new ObjectProxy<T>(api, servantProxyConfig, loadBalance, protocolInvoker, communicator);
    } …… //创建Servant服务的配置信息
    private ServantProxyConfig createServantProxyConfig(String objName, String setDivision) throws CommunicatorConfigException {
    ……
    } …… //更新服务端点:通过ObjectName判断是有设置了服务器节点,如果有(本地只连接),如果没有那就从tars管理中获取服务器节点。放在ServantCacheManager管理起来。
    private void updateServantEndpoints(ServantProxyConfig cfg) {
    CommunicatorConfig communicatorConfig = communicator.getCommunicatorConfig();
    ……
    }

通过上面的客户端启动流程源码分析,我们找到第一个核心点: 客户端的负载均衡调用器LoadBalance

*除了创建了一个负载均衡调用器LoadBalance,还创建了一个协议调用器protocolInvoker,该协议调用器里分别对同步与异步调用方法、Tars与Http协议请求处理、以及过滤器等相关配置,但我们的重点不在这,下面将着重分析LoadBalance

3. 客户端的负载均衡调用器LoadBalance

我们点进去查看原有负载均衡逻辑,发现这是一个接口,里面定义了两个方法,都是与负载均衡调用器相关的:

public interface LoadBalance<T> {

    /**
* 选择负载均衡调用器
* @param 调用的上下文
* @return
* @throws 无负载均衡调用器 - 异常
*/
Invoker<T> select(InvokeContext invokeContext) throws NoInvokerException; /**
* 刷新本地负载均衡调用器
* @param 负载均衡调用器
*/
void refresh(Collection<Invoker<T>> invokers);
}

我们Ctrl+H一下即可发现该接口有四个实现类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KJyHqhl5-1627523692062)(https://lexiangla.com/assets/3963f704ee0411ebbe94aee286d18512 "负载均衡调用器实现类")]

分别是:

  • ConsistentHashLoadBalance:一致hash选择器;
  • HashLoadBalance:hash选择器;
  • RoundRobinLoadBalance: 轮询选择器;
  • DefaultLoadBalance:默认的选择器(由源码可知先ConsistentHashLoadBalance,HashLoadBalance,RoundRobinLoadBalance);

需要注意实现类有四个,选择器有三个。这四个选择器都是一个构造方法+实现接口的两个方法,比较相近。下面我们只分析RoundRobinLoadBalance的select方法:

@Override
public Invoker<T> select(InvokeContext invocation) throws NoInvokerException { //静态权重缓存器列表
List<Invoker<T>> staticWeightInvokers = staticWeightInvokersCache; //使用权重轮询
if (staticWeightInvokers != null && !staticWeightInvokers.isEmpty()) {
//【体现轮询】根据index获取一个调用器,规则是:获取“静态权重顺序递增值”的绝对值后对“静态权重缓存器数”取余?
Invoker<T> invoker = staticWeightInvokers.get((staticWeightSequence.getAndIncrement() & Integer.MAX_VALUE) % staticWeightInvokers.size());
//如果调用器存活则直接返回
if (invoker.isAvailable()) return invoker; //判断存活:先根据调用器的url获取“调用器活动状态”,判断:状态的“上次重试时间”+“尝试重启时间间隔” < “系统当前时间”,存活则将系统当前时间设置为“上次重启时间”
ServantInvokerAliveStat stat = ServantInvokerAliveChecker.get(invoker.getUrl());
if (stat.isAlive() || (stat.getLastRetryTime() + (config.getTryTimeInterval() * 1000)) < System.currentTimeMillis()) {
logger.info("try to use inactive invoker|" + invoker.getUrl().toIdentityString());
stat.setLastRetryTime(System.currentTimeMillis());
return invoker;
}
} //无权重轮询,抛出异常
List<Invoker<T>> sortedInvokers = sortedInvokersCache;
if (CollectionUtils.isEmpty(sortedInvokers)) {
throw new NoInvokerException("no such active connection invoker");
} List<Invoker<T>> list = new ArrayList<Invoker<T>>();
for (Invoker<T> invoker : sortedInvokers) {
//如果调用器挂了
if (!invoker.isAvailable()) { //尝试救回调用器:先根据调用器的url获取“调用器活动状态”,判断:状态的“上次重试时间”+“尝试重启时间间隔” < “系统当前时间”,存活则加入到list中,挂了就不加入
ServantInvokerAliveStat stat = ServantInvokerAliveChecker.get(invoker.getUrl());
if (stat.isAlive() || (stat.getLastRetryTime() + (config.getTryTimeInterval() * 1000)) < System.currentTimeMillis()) {
list.add(invoker);
}
} else {
//调用器存活则将调用器添加到list里
list.add(invoker);
}
}
//TODO When all is not available. Whether to randomly extract one
if (list.isEmpty()) {
throw new NoInvokerException(config.getSimpleObjectName() + " try to select active invoker, size=" + sortedInvokers.size() + ", no such active connection invoker");
} //随机获取一个调用器?
Invoker<T> invoker = list.get((sequence.getAndIncrement() & Integer.MAX_VALUE) % list.size()); //如果调用器不存活,则将当前系统时间设置为该调用器的上次重启时间
if (!invoker.isAvailable()) {
//Try to recall after blocking
logger.info("try to use inactive invoker|" + invoker.getUrl().toIdentityString());
ServantInvokerAliveChecker.get(invoker.getUrl()).setLastRetryTime(System.currentTimeMillis());
}
return invoker;
}

可以看出select方法重点还是在于“怎样”找到一个负载均衡调用器,只不过实现的方法不同,有的采用轮询的方法、有的根据hash值,而我们关注的是给负载均衡方法做扩展(增添路由规则),因此这里也不是重点。但为我们指明了一个方向,就是上面源码里反复提到的invoker调用器(invoker老眼熟了,SpringBoot里的controller参数处理里也有它)。

我们来看看Tars里的invoker,它也是一个接口,只有一个实现类,

public interface Invoker<T> {

    //获取uil
Url getUrl(); //获取api
Class<T> getApi(); //判断是否存活
boolean isAvailable(); //执行方法
Object invoke(InvokeContext context) throws Throwable; //销毁方法
void destroy();
}

通过对这几个实现类的源码阅读,我们发现invoke方法就是对doInvokeServant底层方法进行层层封装。

通过对TarsInvoker的源码阅读,我们还可以知道TarsInvoker有四个属性config、api、url、clients,对应前面提到的getXXX对应方法;还可以设置是否存活,对应前文对是否存活的判断。在doInvokeServant里最核心的操作流程是try里面的语句:

public class TarsInvoker<T> extends ServantInvoker<T> {

    final List<Filter> filters;

    public TarsInvoker(ServantProxyConfig config, Class<T> api, Url url, ServantClient[] clients) {
super(config, api, url, clients);
filters = AppContextManager.getInstance().getAppContext() == null ? null : AppContextManager.getInstance().getAppContext().getFilters(FilterKind.CLIENT);
} @Override
public void setAvailable(boolean available) {
super.setAvailable(available);
} @Override
protected Object doInvokeServant(final ServantInvokeContext inv) throws Throwable {
final long begin = System.currentTimeMillis();
int ret = Constants.INVOKE_STATUS_SUCC;
try {
//根据api获取将要执行的方法
Method method = getApi().getMethod(inv.getMethodName(), inv.getParameterTypes()); //如果是异步调用
if (inv.isAsync()) {
//执行异步方法
invokeWithAsync(method, inv.getArguments(), inv.getAttachments());
return null;
//如果是承诺未来???
} else if (inv.isPromiseFuture()) {
return invokeWithPromiseFuture(method, inv.getArguments(), inv.getAttachments());// return Future Result
} else {
//执行同步方法
TarsServantResponse response = invokeWithSync(method, inv.getArguments(), inv.getAttachments());
ret = response.getRet() == TarsHelper.SERVERSUCCESS ? Constants.INVOKE_STATUS_SUCC : Constants.INVOKE_STATUS_EXEC;
if (response.getRet() != TarsHelper.SERVERSUCCESS) {
throw ServerException.makeException(response.getRet(), response.getRemark());
}
return response.getResult();
}
} catch (Throwable e) {
if (e instanceof TimeoutException) {
ret = Constants.INVOKE_STATUS_TIMEOUT;
} else if (e instanceof NotConnectedException) {
ret = Constants.INVOKE_STATUS_NETCONNECTTIMEOUT;
} else {
ret = Constants.INVOKE_STATUS_EXEC;
}
throw e;
} finally {
if (inv.isNormal()) {
setAvailable(ServantInvokerAliveChecker.isAlive(getUrl(), config, ret));
InvokeStatHelper.getInstance().addProxyStat(objName)
.addInvokeTimeByClient(config.getMasterName(), config.getSlaveName(), config.getSlaveSetName(), config.getSlaveSetArea(),
config.getSlaveSetID(), inv.getMethodName(), getUrl().getHost(), getUrl().getPort(), ret, System.currentTimeMillis() - begin);
}
}
} ……
}

而try语句里主要做的是执行的调用方法(异步、同步、承诺未来),由于我们要扩展的路由功能与调用方法无关,这里就不深入分析了。

由此我们可以分析得出负载均衡设计的底层结构图:

@EnableTarsServer注解:表明这是一个Tars服务;

  • @Import(TarsServerConfiguration.class):引入Tars服务相关配置文件;

    • Communcator:通信器;

      • getServantProxyFactory():获取代理工厂管理者;
      • getObjectProxyFactory():获取对象代理工厂;
        • createLoadBalance():创建客户端负载均衡调用器;

          • select():选择负载均衡调用器(有四种模式可以选择);

            • invoker:调用器;

              • invoke():具体的执行方法;

                • doInvokeServant():最底层的执行方法;
          • refresh():更新负载均衡调用器;
        • createProtocolInvoker():创建协议调用器;

最后

新人制作,如有错误,欢迎指出,感激不尽!
欢迎关注公众号,会分享一些更日常的东西!
如需转载,请标注出处!

Tars | 第2篇 TarsJava SpingBoot启动与负载均衡源码初探的更多相关文章

  1. Tars | 第4篇 Subset路由规则业务分析与源码探索

    目录 前言 1. Subset不是负载均衡 1.1 任务需求 1.2 负载均衡源码结构图 1.3 负载均衡四种调用器 1.4 新增两种负载均衡调用器 1.5 Subset应该是"过滤&quo ...

  2. CentOS 6.5 + Nginx 1.8.0 + PHP 5.6(with PHP-FPM) 负载均衡源码安装 之 (一)Nginx安装篇

    CentOS 6.5 minimal安装不再赘述 Nginx源码安装 1.安装wget下载程序 yum -y install wget 2.安装编译环境:gcc gcc-c++ automake au ...

  3. CentOS 6.5 + Nginx 1.8.0 + PHP 5.6(with PHP-FPM) 负载均衡源码安装 之 (二)PHP(PHP-FPM)安装篇

    编译安装PHP及内置PHP-FPM nginx本身不能处理PHP,它只是个web服务器,当接收到请求后,如果是php请求,则发给php解释器处理,并把结果返回给客户端(浏览器). nginx一般是把请 ...

  4. 14 微服务电商【黑马乐优商城】:day02-springcloud(理论篇四:配置Robbin负载均衡)

    本项目的笔记和资料的Download,请点击这一句话自行获取. day01-springboot(理论篇) :day01-springboot(实践篇) day02-springcloud(理论篇一) ...

  5. Eureka源码探索(一)-客户端服务端的启动和负载均衡

    1. Eureka源码探索(一)-客户端服务端的启动和负载均衡 1.1. 服务端 1.1.1. 找起始点 目前唯一知道的,就是启动Eureka服务需要添加注解@EnableEurekaServer,但 ...

  6. Tars | 第7篇 TarsJava Subset最终代码的测试方案设计

    目录 前言 1. SubsetConf配置项的结构 1.1 SubsetConf 1.2 RatioConfig 1.3 KeyConfig 1.4 KeyRoute 1.5 SubsetConf的结 ...

  7. Tars | 第8篇 TarsJava Subset最终代码的执行流程与原理分析

    目录 前言 1. SubsetConf配置项的结构 1.1 SubsetConf 1.2 RatioConfig 1.3 KeyConfig 1.4 KeyRoute 1.5 SubsetConf的结 ...

  8. Spring IOC容器启动流程源码解析(一)——容器概念详解及源码初探

    目录 1. 前言 1.1 IOC容器到底是什么 1.2 BeanFactory和ApplicationContext的联系以及区别 1.3 解读IOC容器启动流程的意义 1.4 如何有效的阅读源码 2 ...

  9. 手把手教你调试SpringBoot启动 IoC容器初始化源码,spring如何解决循环依赖

    授人以鱼不如授人以渔,首先声明这篇文章并没有过多的总结和结论,主要内容是教大家如何一步一步自己手动debug调试源码,然后总结spring如何解决的循环依赖,最后,操作很简单,有手就行. 本次调试 是 ...

随机推荐

  1. Java8新特性(二)之函数式接口

    .subTitle { background: rgba(51, 153, 0, 0.66); border-bottom: 1px solid rgba(0, 102, 0, 1); border- ...

  2. Lingoes安装词典和语音库

    安装词典: 选项->词典,出现"词典管理"窗体,点"安装",从磁盘上选择要安装的词典文件(扩展名为ld2的文件),勾选"添加到索引组" ...

  3. Build a Beautiful oh-my-zsh Themes

    Selection Criteria double line; provide username, hostname, current directory; provide information o ...

  4. cmseasy&内网渗透 Writeup

    某CTF内网渗透 题目:www.whalwl.site:8021 目录 cmseasy 内网横向渗透 cmseasy 简单看一下网站架构 Apache/2.4.7 (Ubuntu) PHP/5.5.9 ...

  5. S3C2440—1.熟悉裸机开发板

    文章目录 一.板载资源介绍 二.安装驱动及上位机 1.USB的驱动及上位机 2.eop驱动安装 3.安装烧录软件oflash 三.烧写开发板 1.预备知识 2.烧写裸板 3.使用u-boot烧写程序 ...

  6. innodb是如何存数据的?yyds

    前言 如果你使用过mysql数据库,对它的存储引擎:innodb,一定不会感到陌生. 众所周知,在mysql8以前,默认的存储引擎是:myslam.但mysql8之后,默认的存储引擎已经变成了:inn ...

  7. noip33

    T1 第一个猎人死的轮数等于在1号猎人之前死的猎人数+1,如果当前这个人没死,那么他死在一号猎人之前的概率为 \(\frac{w_{i}}{w_{1}+w_{i}}\),因为每死一个就会造成1的贡献, ...

  8. Vamware没有卸载干净,导致无法重装,无法删除VMware旧版本,请与技术小组联系

    原因:注册表没有清理干净!!! 问题:把文件夹清理了n遍,却无法重装VMware,报错如标题. 原因:相关注册表没删完. 解决办法: - 1.创建一个.txt文本: - 2.将下面的内容复制到.txt ...

  9. SpringSession(redis)

    pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="htt ...

  10. 接口和包--Java学习笔记

    接口 定义及基础用法 interface定义:没有字段的抽象类 interface person{ void hello(); String getName(); } /*接口本质上就是抽象类 abs ...