前言

通过源码分析可以得出这样一个负载均衡的源码结构图(基于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. 7年老Android收到阿里offer,跟领导提离职被怼:为年薪百万不做兄弟?

    在当今社会,钱就是衡量一个人价值的标准,如果你在一家公司,领导再怎么重用你,但是薪资待遇却很低,这样根本是很难留住人,毕竟工作就是为了赚钱,要是连工资都满足不了,谈其他根本就是扯淡. 最近在职业论坛看 ...

  2. 害...原来阿里面试Redis最常问的是它呀

    一提到Redis缓存,我们不得不了解的三个问题就是:缓存雪崩.缓存击穿和缓存穿透.这三个问题一旦发生,会导致大量的请求直接请求到数据库层.如果并发压力大,就会导致数据库崩溃.那p0级的故障是没跑了. ...

  3. 把对象交给spring管理的3种方法及经典应用

    背景 先说一说什么叫把对象交给spring管理.它区别于把类交给spring管理.在spring里采用注解方式@Service.@Component这些,实际上管理的是类,把这些类交给spring来负 ...

  4. CVE-2020-2883漏洞复现&&流量分析

    CVE-2020-2883漏洞复现&&流量分析 写在前面 网上大佬说CVE-2020-2883是CVE-2020-2555的绕过,下面就复现了抓包看看吧. 一.准备环境 靶机:win7 ...

  5. SpringBoot-AOP记录操作日志

    package com.meeno.inner.oa.extend.operaterecord.aop; import com.alibaba.fastjson.JSONArray; import c ...

  6. Spring-Boot-Bean的使用,@Repository,@Service,@Controller,@Component

    前言 在Spring MVC的时候,我们使用xml来配置bean,如今的Spring boot推荐我们使用元注解的发生,那就听Spring Boot的推荐,下面我就为大家来介绍下Spring Boot ...

  7. 【mysql】explain性能分析

    1. explain的概念 使用EXPLAIN 关键字可以模拟优化器执行SQL 查询语句,从而知道MySQL 是如何处理你的SQL 语句的.分析你的查询语句或是表结构的性能瓶颈. 用法: Explai ...

  8. 七:使用Session进行会话管理

    一.Session简单介绍 在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下).因此,在需要保存用户数据时,服务 ...

  9. [ASP.NET MVC]@Partial 和@RenderPartial的区别

    @Partial 和@RenderPartial的区别 Html.partial和RenderPartial的用法与区别 Html.partial和RenderPartial都是输出html片段,区别 ...

  10. 关于通用Mapper new Example使用记录

    关于通用MapperExample使用记录 环境准备 需要集成 mybatis 的 generator 插件,方便自动生成 实体类和 mapper 类,还可以生成xml,不过一般我们都不用 xml b ...