Spring Cloud Ribbon服务路径的解析(一)
一、RestTemplate
1.1简介
spring框架提供的RestTemplate类可用于在应用中调用rest服务,它简化了与http服务的通信方式,统一了RESTful的标准,封装了http链接, 我们只需要传入url及返回值类型即可。相较于之前常用的HttpClient,RestTemplate是一种更优雅的调用RESTful服务的方式。
在Spring应用程序中访问第三方REST服务与使用Spring RestTemplate类有关。RestTemplate类的设计原则与许多其他Spring *模板类(例如JdbcTemplate、JmsTemplate)相同,为执行复杂任务提供了一种具有默认行为的简化方法。
RestTemplate默认依赖JDK提供http连接的能力(HttpURLConnection),如果有需要的话也可以通过setRequestFactory方法替换为例如 Apache HttpComponents、Netty或OkHttp等其它HTTP library。
考虑到RestTemplate类是为调用REST服务而设计的,因此它的主要方法与REST的基础紧密相连就不足为奇了,后者是HTTP协议的方法:HEAD、GET、POST、PUT、DELETE和OPTIONS。例如,RestTemplate类具有headForHeaders()、getForObject()、postForObject()、put()和delete()等方法。
1.2、实现
首先建两个项目
RestTemplate包含以下几个部分:
- HttpMessageConverter 对象转换器
- ClientHttpRequestFactory 默认是JDK的HttpURLConnection
- ResponseErrorHandler 异常处理
- ClientHttpRequestInterceptor 请求拦截器
spring-cloud-server的配置
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
application.properties
spring.application.name=spring-cloud-serverserver.port=8080
RestTemplateServer.class
@RestController public class RestTemplateServer { @Value("${server.port}") private int port; @GetMapping("/orders") public String getAllOrder(){ System.out.println("port:"+port); return "测试成功"; } }
启动项目访问结果如下
spring-cloud-user的配置文件
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
server.port=8088
业务代码RestTemplateUser.class
@RestController public class RestTemplateUser { @Autowired RestTemplate restTemplate; //因为RestTemplate不存在所以要注入 @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } @GetMapping("/user") public String findById(){ return restTemplate.getForObject("http://localhost:8080/orders",String.class); } }
启动项目访问可得到8080服务的结果
这样我们初步完成了两个独立项目的通信,如果不想在通过new的方式创建RestTemplate那也可以通过build()方法创建,修改后如下
@RestController public class RestTemplateUser { @Autowired RestTemplate restTemplate; //因为RestTemplate不存在所以要注入 // @Bean // public RestTemplate restTemplate(){ // return new RestTemplate(); // } @Bean public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){ return restTemplateBuilder.build(); } @GetMapping("/user") public String findById(){ return restTemplate.getForObject("http://localhost:8080/orders",String.class); } }
但是现在很多服务架构都是多节点的,那么我们就要考虑多节点负载均衡的问题,这时最先想到的是Ribbon,修改代码
修改cloud-cloud-user的pom.xml文件,增加
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> <version>2.2.3.RELEASE</version> </dependency>
为演示负载均衡,启动两个spring-cloud-server节点,再配置一个节点并启动
修改完后,再修改spring-cloud-user配置文件
server.port=8088 spring-cloud-server.ribbon.listOfServers= localhost:8080,localhost:8081
这样玩后有心的人就发现了,业务再用return restTemplate.getForObject("http://localhost:8080/orders",String.class);访问另一个项目就不合适了,更改RestTemplateUser.class类
@RestController public class RestTemplateUser { @Autowired RestTemplate restTemplate; //因为RestTemplate不存在所以要注入 // @Bean // public RestTemplate restTemplate(){ // return new RestTemplate(); // } @Bean public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){ return restTemplateBuilder.build(); } @Autowired LoadBalancerClient loadBalancerClient; @GetMapping("/user") public String findById(){ ServiceInstance serviceInstance=loadBalancerClient.choose("spring-cloud-server"); String url=String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort()+"/orders"); return restTemplate.getForObject(url,String.class); //通过服务名称在配置文件中选择端口调用 // return restTemplate.getForObject("http://localhost:8080/orders",String.class); } }
访问下面地址,多点几次
说到 了这里那我们现在就要来看下Ribbon了
二、Ribbon简介
③ 当部署多个相同微服务时,如何实现请求时的负载均衡?
实现负载均衡方式1:通过服务器端实现负载均衡(nginx)
实现负载均衡方式2:通过客户端实现负载均衡
Ribbon工作时分为两步:第一步选择Eureka Server,它优先选择在同一个Zone且负载较少的Server;第二步再根据用户指定的策略,再从Server取到的服务注册列表中选择一个地址。其中Ribbon提供了很多策略,例如轮询round robin、随机Random、根据响应时间加权等。
为了更好的了解Ribbon后面肯定是要进入源码,在进入源码之前做个铺垫,我再来改造上面的代码,引入@LoadBalanced注解,修改下
@RestController public class RestTemplateUser { @Autowired RestTemplate restTemplate; //因为RestTemplate不存在所以要注入 // @Bean // public RestTemplate restTemplate(){ // return new RestTemplate(); // } // @Bean // public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){ // return restTemplateBuilder.build(); // } @Bean @LoadBalanced public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){ return restTemplateBuilder.build(); } // @Autowired // LoadBalancerClient loadBalancerClient; @GetMapping("/user") public String findById(){ // ServiceInstance serviceInstance=loadBalancerClient.choose("spring-cloud-server"); // String url=String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort()+"/orders"); // return restTemplate.getForObject(url,String.class); //通过服务名称在配置文件中选择端口调用 return restTemplate.getForObject("http://spring-cloud-server/orders",String.class); } }
启动项目后会发现@LoadBalanced也能实现负载均衡,这里面我们就应该进入看下@LoadBalanced到底做了啥,在没用@LoadBalanced之前getForObject只能识别ip的路径,并不能识别服务名进行负载均衡,所以我们要看下@LoadBalanced是怎么实现的负载均衡
在看码源前先剧透下,之前某人说我写的东西不好看懂,那我这次多花点时间画图,restTemplate.getForObject("http://spring-cloud-server/orders",String.class);这个方法他调用的是一个服务器名称,我们知道,如果要访问一个服务器我们一个具体的路径才能访问,那么@LoadBalanced是怎么做到的由一个服务名得到一个具体的路径呢,这就要说到拦截器,他在调用真实路径前会有拦截器拦截服务器名,然后拿到服务器去解析然后拼接得到一个真实的路径名称,然后拿真实路径去访问服务,详细的步骤在源码讲解中具体分析。
我们点击@LoadBalanced进入如下图
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface LoadBalanced { }
我们会发现有一个叫@Qualifier的东西,其实这玩意就是一个标记的作用,但为了后面的源码分析,这里还是说明下@Qualifiler的用法
我们在spring-cloud-user项目中新建一个Qualifier包,在包中建三个类
public class QualifierTest { private String name; public QualifierTest(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
//@Configuration用于定义配置类,可替换xml配置文件, // 被注解的类内部包含有一个或多个被@Bean注解的方法, // 这些方法将会被AnnotationConfigApplicationContext或 // AnnotationConfigWebApplicationContext类进行扫描, // 并用于构建bean定义,初始化Spring容器。 @Configuration public class QualifierConfiguration { @Qualifier @Bean("QualifierTest1") QualifierTest QualifierTest1(){ return new QualifierTest("QualifierTest1"); } @Qualifier @Bean("QualifierTest2") QualifierTest QualifierTest2(){ return new QualifierTest("QualifierTest2"); } }
@RestController public class QualifierController { //@Qualifier作用是找到所有申明@Qualifier标记的实例 @Qualifier @Autowired List<QualifierTest> testClassList= Collections.emptyList(); @GetMapping("/qualifier") public Object test(){ return testClassList; } }
启动项目访问接口结果如下
除掉QualifierConfiguration.class中其中一个@Qualifier后刷新接口,会发现结果如下,这两个结果对比可以证明@Qualifier其实就是一个标记的作用
有了这个概念后我们进入LoadBalancerAutoConfiguration.class这个自动装配类中会发现有和我刚刚演示一样的代码,其实我就是从这个装配类中抄的,哈哈;
看到这里相信大家就明白了,因为红框的内容加了@LoadBalanced注解就能使RestTemplate生效是因为@Qualifier注解,有了这个概念接着往下走,在上图这个自动装配类中会加载注入所有加了@LoadBalanced注解的RestTemplate,这一步很关键,因为后面的拦截器加载跟这一步有关联;竟然我们来到了LoadBalancerAutoConfiguration,这个自动装配类来了,那就聊聊这里面的Bean装配,下面这个图是Bean的自动装配过程
首先看自动装配类拦截器LoadBalancerInterceptor
@Configuration(proxyBeanMethods = false) @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") static class LoadBalancerInterceptorConfig { //定义一个Bean @Bean public LoadBalancerInterceptor ribbonInterceptor( LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) { return new LoadBalancerInterceptor(loadBalancerClient, requestFactory); } //将定义的Bean作为参数传入 @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return restTemplate -> { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); //设置拦截器 list.add(loadBalancerInterceptor); //设置到restTemplate中去 restTemplate.setInterceptors(list); }; } }
@Bean public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated( final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) { return () -> restTemplateCustomizers.ifAvailable(customizers -> { //对restTemplates进行for循环,对每一个restTemplate加一个包装叫RestTemplateCustomizer //这个包装的意义是可以对restTemplate再加一个自定义的拦截 for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) { for (RestTemplateCustomizer customizer : customizers) { customizer.customize(restTemplate); } } }); }
有了上面的包装,才有下面的拦截的加强
@Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return restTemplate -> { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; }
说到这里再将时序图画一下,我最初是通过@LoadBalanced注解进入到他的装配类LoadBalancerAutoConfiguration,然后在LoadBalancerAutoConfiguration装配类中找到拦截器的加载和增强的,根据这个逻辑画出的时序图如下
之前在开篇中还讲到过用下面这种方式进行负载均衡访问,其实针对LoadBalancerClient是一样的,他里面有一个RibbonAutoConfiguration
@Autowired LoadBalancerClient loadBalancerClient;
在RibbonAutoConfiguration装配类中会找到一个代码如果下,他在装配类中对LoadBalancerClient进行初始化
@Bean @ConditionalOnMissingBean(LoadBalancerClient.class) public LoadBalancerClient loadBalancerClient() { return new RibbonLoadBalancerClient(springClientFactory()); }
我们看头文件,会发现加载了LoadBalancerAutoConfiguration
这时补充下时序图如下,这就是Bean的加载过程,经过这一过程拦截器就算是加载进去了
有了拦截器后,下一步要看的话肯定就是来看下拦截器到底做了啥,进入LoadBalancerInterceptor拦截器,会发现他会最终进入如下方法
@Override public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { final URI originalUri = request.getURI(); String serviceName = originalUri.getHost(); Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); //将拦截委托给loadBalancer进行实现 return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); }
跟进loadBalancer看下做了啥(LoadBalancerClient注入是在RibbonAutoConfiguration配置类中完成的),跟踪进去发现最终还是调用了RibbonLoadBalancerClient
进入execute方法,会发现里面只做了两件事
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException { //获得负载均衡器 ILoadBalancer loadBalancer = getLoadBalancer(serviceId); //根据负载均衡器返回Server,这个Server返回是指定的某一个地址,其实负载的解析在这里就完成了 Server server = getServer(loadBalancer, hint); if (server == null) { throw new IllegalStateException("No instances available for " + serviceId); } RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server)); return execute(serviceId, ribbonServer, request); }
进入getLoadBalancer看看他做了啥,在看之前先看下他的类关系图
ILoadBalancer接口:定义添加服务,选择服务,获取可用服务,获取所有服务方法
AbstractLoadBalancer抽像类:定义了一个关于服务实例的分组枚举,包含了三种类型的服务:ALL
表示所有服务,STATUS_UP
表示正常运行的服务,STATUS_NOT_UP
表示下线的服务。
BaseLoadBalancer:
1):类中有两个List集合,一个List集合用来保存所有的服务实例,还有一个List集合用来保存当前有效的服务实例
2):定义了一个IPingStrategy,用来描述服务检查策略,IPingStrategy默认实现采用了SerialPingStrategy实现
3):chooseServer方法中(负载均衡的核心方法),调用IRule中的choose方法来找到一个具体的服务实例,默认实现是RoundRobinRule
4):PingTask用来检查Server是否有效,默认执行时间间隔为10秒
5):markServerDown方法用来标记一个服务是否有效,标记方式为调用Server对象的setAlive方法设置isAliveFlag属性为false
6):getReachableServers方法用来获取所有有效的服务实例列表
7):getAllServers方法用来获取所有服务的实例列表
8):addServers方法表示向负载均衡器中添加一个新的服务实例列表
DynamicServerListLoadBalancer:主要是实现了服务实例清单在运行期间的动态更新能力,同时提供了对服务实例清单的过滤功能。
ZoneAwareLoadBalancer:主要是重写DynamicServerListLoadBalancer中的chooseServer方法,由于DynamicServerListLoadBalancer中负责均衡的策略依然是BaseLoadBalancer中的线性轮询策略,这种策略不具备区域感知功能
NoOpLoadBalancer:不做任何事的负载均衡实现,一般用于占位(然而貌似从没被用到过)。
有了这个概念后我们下面就来重点看BaseLoadBalancer,在唠唠之前先补充下时序图
点击getLoadBalancer进入如下代码
在向下写前,先提前说下ILoadBalancer这个类里面会帮我们做一件事,他会根据负载均衡的一个算法进行一个负载的选择,但是在负载之前他会有一个类的初始化过程,在选择完成后ILoadBalancer实现返回,然后将ILoadBalancer做为参数传给Server server = getServer(loadBalancer, hint);在ILoadBalancer中他有一个实现会去调用BaseLoadBalancer.chooseServer,它会调用rule.choose(),rule的初始化是在ZoneAvoidanceRule中完成的,所以接下来看要分两部分,ILoadBalancer做为一个负载均衡器,然后getServer会把这个负载均衡器会传过去后进行一个负载的计算,这个流程说完后可能很多人还在懵逼状态,那接下来我们就通过代码来看他的实现,首先看ILoadBalancer的实现是谁
接着上图来,点击getLoadBalancer
然后点击getInstance
@Override public <C> C getInstance(String name, Class<C> type) { //这里面通过传送一个name和一个type得到一个实例,这里面是一个工厂模式,我们点击getInstance选择它的NamedContextFactory实现进去 C instance = super.getInstance(name, type); if (instance != null) { return instance; } IClientConfig config = getInstance(name, IClientConfig.class); return instantiateWithConfig(getContext(name), type, config); }
public <T> T getInstance(String name, Class<T> type) { //工厂模式会加载一个context AnnotationConfigApplicationContext context = getContext(name); if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) { return context.getBean(type); } return null; }
getContext方法里面是用spring写的,比较复杂,点击getContext后如下图,这里面是有个默认缓存的,如果没有会用createContext(name)根据名称创建一个缓存
回退到AnnotationConfigApplicationContext context = getContext(name);
public <T> T getInstance(String name, Class<T> type) { AnnotationConfigApplicationContext context = getContext(name); if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) { 通过type得到一个Bean return context.getBean(type); } return null; }
再回退到C instance = super.getInstance(name, type);进行打debug看下他返回的是什么类型的ILoadBalancer
从上图可以看到返回的是一个ZoneAwareLoadBalancer的ILoadBalancer,然后就拿着ILoadBalancer传入getServer(loadBalancer, hint);中,这时的时序图就如下了
到了这一步获取负载均衡器这一过程就完成了,下面就是来完成过程2.通过负载均衡器中配置的默认负载均衡算法选一个合适的Server,我们进入
Server server = getServer(loadBalancer, hint);的getServer方法,点击进去如下,这里面其实进行的就是针对一个服务节点的选择,其中loadBalancer.chooseServer(hint != null ? hint : "default");就是一种算法的选择,我们这里面没有选择算法,所以采用默认算法BaseLoadBalancer
进入默认算法截图如下
然后他会调用rule.choose(key);方法,我们可以在进入方法前先看下IRule是啥,通过下图我们可以很清楚的看到IRule里面所有的实现,之所以在这里提到IRule是因为IRule是Ribbon中实现负载均衡的一个很重要的规则,他实现了重置规则、轮询规则、随机规则及客户端是否启动轮询的规则;在后面我看机会说其中一到两种比较常用的算法说明下
我们这里rule.choose(key);采用的是轮询算法,选择PredicateBasedRule,进去后截图如下
@Override public Server choose(Object key) { ILoadBalancer lb = getLoadBalancer(); //根据我们的过滤规则过滤之后会根据轮询去进行筛选,其中lb.getAllServers是获取一个静态的服务列表 Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key); if (server.isPresent()) { return server.get(); } else { return null; } } }
我们进入chooseRoundRobinAfterFiltering,下面的轮询比较简单,他先把节点数量eligible.size()传进去,然后通过incrementAndGetModulo方法获取一个下标
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) { //得到我们所有的配置信息 List<Server> eligible = getEligibleServers(servers, loadBalancerKey); //配置数量 if (eligible.size() == 0) { return Optional.absent(); } //进行轮询计算 return Optional.of(eligible.get(incrementAndGetModulo(eligible.size()))); }
可以进入incrementAndGetModulo方法看下
private int incrementAndGetModulo(int modulo) { for (;;) { //获取下一个节点的当前值 int current = nextIndex.get(); //根据这个值进行取模运算 int next = (current + 1) % modulo; //设置下一个值 if (nextIndex.compareAndSet(current, next) && current < modulo) return current; } }
上面就是轮询算法的实现,这个算法的实现比较简单,下面再来看一个随机算法的实现
public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { return null; } Server server = null; while (server == null) { if (Thread.interrupted()) { return null; } List<Server> upList = lb.getReachableServers(); //得到所有节点信息 List<Server> allList = lb.getAllServers(); int serverCount = allList.size(); if (serverCount == 0) { /* * No servers. End regardless of pass, because subsequent passes * only get more restrictive. */ return null; } //传入节点数量,然后随机取值,如果有人想看怎么取的点击这个chooseRandomInt就可以看到,它实现就一句话,就是把数量传进去得到一个随机值 int index = chooseRandomInt(serverCount); server = upList.get(index); if (server == null) { /* * The only time this should happen is if the server list were * somehow trimmed. This is a transient condition. Retry after * yielding. */ Thread.yield(); continue; } if (server.isAlive()) { return (server); } // Shouldn't actually happen.. but must be transient or a bug. server = null; Thread.yield(); } return server; }
随机实现聊完后,再回到我们跟踪的代码 return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));通过算法得到具体的节点后eligible.get就可以得到 对应下标的服务列表,这时就得到了什么localhost:8082的具体端口号了,这一步完成后其实Server server = getServer(loadBalancer, hint);的活就做完了,下面的活就是拿着具体端口去重构了,更新下时序图
项目中所有例子源码:https://github.com/ljx958720/spring-cloud-Ribbon-1-.git