微服务之路(七)spring cloud netflix ribbon
前言
⒈Ribbon是什么?
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡工具。
Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。我们在配置文件中列出负载均衡所有的机器,Ribbon会自动的帮助我们基于某种规则(如简单轮询、随机连接等等)去连接这些机器。Ribbon客户端组件提供了一列完善的配置项(如连接超时、重试等等),我们也能很容易的使用Ribbon实现自定义的负载均衡算法。
⒉负载均衡概念
负载均衡(Load Balance,简称LB),在微服务或分布式集群中经常用到的一种功能,就是将用户的请求以某种规则平摊到多个服务器上,从而达到系统的高可用。
常见的负载均衡有软件例如Nginx、LVS等等,硬件F5等等。
相应的在中间件,例如Dubbo和Spring Cloud中均给我们提供了负载均衡,Spring Cloud的负载均衡算法可以自定义。
集中式负载均衡:即在服务的消费方和提供方之间使用独立的负载均衡设施(可以是硬件,如F5。也可以是软件,如Nginx),由该设施负责把请求通过某种策略转发至服务的提供方。
进程内负载均衡:将负载均衡逻辑集成到服务消费方,由消费方从服务注册中心获取有那些服务地址可用,然后消费方从这些地址中选择一个合适的服务器。(Ribbon属于进程内负载均衡,它只是一个类库,集成于服务消费方进程,消费方通过它来获取到服务提供方的地址)
主要议题
- Eureka高可用
- RestTemplate
- 整合Netflix Ribbon
- 问题总结
主体内容
一、Eureka高可用
- Eureka客户端高可用
- 高可用注册中心集群
- 获取注册信息时间间隔
- 实例信息复制时间间隔
- 实例Id
- 实例端点映射
- Eureka服务端高可用
1.Eureka客户端-高可用注册中心集群
(1)我们现在想要启动多个Eureka服务,这里调整一下启动参数。IDEA如下图:
(2)那么其实我们客户端的注册地址也可以变成两个,这是允许的,这就是高可用注册中心集群。
#原来的
#eureka.client.serviceUrl.defaultZone=http://localhost:${eureka.server.port}/eureka
#现在的
eureka.client.serviceUrl.defaultZone=http://localhost:9090/eureka,http://localhost:9091/eureka
但是最好的方式是用域名服务器,举个例子,nginx方向代理。一个端口映射多个端口。这样只要配置一个东西就好了。
(3)这时候我们启动客户端项目,你会发现Eureka 服务端9090注册了一个服务,但是9091却没有。是这样的,Eureka是先来先服务。如果Eureka客户端应用配置了多个Eureka注册服务器,那么默认情况只有第一台可用的服务器,存在注册信息;如果第一台可用的Eureka服务器Down掉了,那么Eureka客户端应用将会选择下一台可用的Eureka服务器。
我现在就把第一台Down掉,也就是我们第一台Eureka项目启动类关掉。你会发现客户端报了些错误,然后后面就不报错了。等待会儿发现9091端口竟然把这个服务自动注册上了,这个就是Eureka高可用。
这里补充个小知识:
这个端口我们可以采用随机数生成:${random.int[7070,7079]}。
(4)此时,整个项目结构图如下:
补充:这里补充一下Eureka客户端的配置源码,都在这个叫做EurekaClientConfigBean中,服务端则显而易见的在EurekaServerConfigBean中。
eureka.client.serviceUrl.defaultZone
映射的其实就是EurekaClientConfigBean中的serviceUrl字段,它是一个HashMap类型,Key为自定义,默认值“defaultZone";Value就是需要配置的Eureka注册服务器URL。构造函数如下:
public EurekaClientConfigBean() {
this.serviceUrl.put("defaultZone", "http://localhost:8761/eureka/");
...
}
value可以是多值字段,通过“,”分割。源码如下:
public List<String> getEurekaServerServiceUrls(String myZone) {
String serviceUrls = (String)this.serviceUrl.get(myZone);
if (serviceUrls == null || serviceUrls.isEmpty()) {
serviceUrls = (String)this.serviceUrl.get("defaultZone");
}
if (!StringUtils.isEmpty(serviceUrls)) {
String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
List<String> eurekaServiceUrls = new ArrayList(serviceUrlsSplit.length);
String[] var5 = serviceUrlsSplit;
int var6 = serviceUrlsSplit.length;
for(int var7 = 0; var7 < var6; ++var7) {
String eurekaServiceUrl = var5[var7];
if (!this.endsWithSlash(eurekaServiceUrl)) {
eurekaServiceUrl = eurekaServiceUrl + "/";
}
eurekaServiceUrls.add(eurekaServiceUrl.trim());
}
return eurekaServiceUrls;
} else {
return new ArrayList();
}
}
2.Eureka客户端-获取注册信息时间间隔
还记不记得上面配置项中提到的eureka.client.fetch-registry=false
这里面的fetch-registry
。Eureka客户端需要获取Eureka服务器注册信息,这个方便服务调用。我们看一下上一章写的例子中的UserServiceProxy类,它里面的:
private static final String PROVIDER_SERVER_URL_PREFIX="http://user-service-provider/";
为啥写个名称就能直接调用呢?其实我这个名称对应的是一个集群。
EurekaClient这个类关联着许多应用集合(Applications)->单个应用信息(Application)又关联着多个实例(InstanceInfos)->单个应用实例(InstanceInfo)
场景解释:那么当Eureka客户端需要调用具体某个服务时,比如user-service-consumer
调用user-service-provider
,user-service-provider实际对用对象是Application,关联了许多应用实例(InstanceInfo)。如果应用user-service-provider
的应用实例发生变化时,那么user-service-consumer是需要感知的。比如:user-service-provider
的机器从10台降到了5台,那么作为调用方的user-service-consumer
需要知道这个变化的情况。课时这个变化过程,可能存在一定的延迟,可以通过调整注册信息时间间隔来减少错误。
(1)配置项 EurekaClientBean#registryFetchIntervalSeconds(默认30秒)
那么根据以上场景,我们就需要在consumer的application.properties文件中添加获取注册信息时间间隔配置项。
#调整注册信息的获取周期,源码中默认值30秒
eureka.client.registry-fetch-interval-seconds=5
3.Eureka客户端-实例信息复制时间间隔
具体就是客户单信息的上报到Eureka服务器的时间。当Eureka客户端应用上报频率越高,Eureka服务器的应用状态管理一直性就越高。
(1)配置项 EurekaClientBean#instanceInfoReplicationIntervalSeconds(默认30秒)
同样的,我们就需要在consumer的application.properties文件中添加获取注册信息时间间隔配置项。
#调整客户端应用状态信息上报的周期
eureka.client.instanceInfoReplicationIntervalSeconds=5
Eureka的应用信息同步是:拉的模式。
Eureka的应用信息上报的方式:推的模式。
4.Eureka客户端-实例Id
从Eureka Server Dashboard里面可以看到具体某个应用中的实例信息,比如:
其中,他们的命名模式:${hostname}:{spring.application.name}😒{server.port}
(1)配置项EurekaInstanceConfigBean#instanceId
#Eureka应用实例的id
eureka.instance.instanceid=${spring.application.name}:${server.port}
Eureka这里有个bug,那就是更改实例名后,以前的实例名仍然在注册中心会存在。当我将在实例后加上xxx:aaaa,重启客户端consumer,发现它确实被改成xxx:aaa。当我再次将其aaa改成bbb,结果重启后两个居然都在,这里就是Eureka一致性不强的原因:
5.Eureka客户端-实例端点映射
我们可以配置Eureka客户端应用实例状态URL。当点击注册中心界面的Status地址时候,我们会发现它会跳转到我们配置的地址中去。
(1)配置项EurekaInstanceConfigBean#statuspageurlpath(示例,默认为/info)
#配置Eureka客户端应用实例状态URL
eureka.instance.status-page-url-path=/actuator/health
6.Eureka服务端高可用
那么这里示例图中的两个Eureka服务就需要进行双向同步,如图所示:
这里介绍一个概念,那就是peer,端点的意思。比如图中两个Eureka服务器就是两个端点。我们之前配置的自我注册和检索服务配置如下:
#取消服务器自我注册
eureka.client.register-with-eureka=false
#注册中心的服务器,没必要再去检索服务
eureka.client.fetch-registry=false
现在我们需要对其作出更改,这种更改就用Spring的profile来实现吧。
(1)在服务端resources下创建application-peer1.properties,内容复制于application.properties,对配置稍作修改。修改的注释如下:
#Eureka Server 应用名称
spring.application.name = spring-cloud-eureka-server
#实例hostname
eureka.instance.hostname=peer1
#Eureka Server服务端口
server.port=9090
#取消服务器自我注册
eureka.client.register-with-eureka=true
#注册中心的服务器,没必要再去检索服务
eureka.client.fetch-registry=true
#Eureka Server服务URL,用于客户端注册和服务发现
#当前Eureka 服务器向9091(Eureka服务器)复制数据
eureka.client.serviceUrl.defaultZone=http://localhost:9091/eureka
(2)同样的,我们需要在服务端resources创建另一个application-peer2.propertie,用于第二台Eureka服务器的配置。
#Eureka Server 应用名称
spring.application.name = spring-cloud-eureka-server
#实例hostname
eureka.instance.hostname=peer2
#Eureka Server服务端口
server.port=9091
#取消服务器自我注册
eureka.client.register-with-eureka=true
#注册中心的服务器,没必要再去检索服务
eureka.client.fetch-registry=true
#Eureka Server服务URL,用于客户端注册和服务发现
#当前Eureka 服务器向9090(Eureka服务器)复制数据
eureka.client.serviceUrl.defaultZone=http://localhost:9090/eureka
(3)现在如何启动项目呢,Idea右上角绿色run 旁边的Edit Configurations中,作出如图配置,采用spring的profile启动。这样就可以通过--spring.profiles.active=peer1和--spring.profiles.active=peer2分别激活Eureka Server1和Eureka Server2。
然后你会发现这里红框显示他们会互相同步信息:
二、RestTemplate
还记得上一章我们在客户端启动类加了一个这个东西:
@LoadBalanced//负载均衡
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
其实这个东西我们已经在consumer的代理类UserServiceProxy类中体现了,我们为啥能够通过这个地址:
//user-service-provider应用前缀
private static final String PROVIDER_SERVER_URL_PREFIX="http://user-service-provider/";
能够感知到下面这个服务调用呢?
@Override
public boolean save(User user) {
User returnValue = restTemplate.postForObject(PROVIDER_SERVER_URL_PREFIX+"/user/save",user,User.class);
return returnValue==null?false:true;
}
HTTP消息转换器 HttpMessageConvertor
自定义实现,请参考spring boot rest,也就是第一章。
HTTP Client适配工厂:ClientHttpRequestFactory
这个方面主要考虑大家使用HttpClient偏好:
- Spring实现
- SimpleClientHttpRequestFactory
- HttpClient实现
- HttpComponentsClientHttpRequestFactory
- OkHttp实现
- OkHttp3ClientHttpRequestFactory
......
这边就是简单列举一些代码来演示。
那么先认识一个接口ClientHttpRequestFactory,进入这个接口,你会发现
@FunctionalInterface
public interface ClientHttpRequestFactory {
ClientHttpRequest createRequest(URI var1, HttpMethod var2) throws IOException;
}
看看它都被谁实现了呢?(对着接口名IDEA中 Ctrl+Alt+B),发现OkHttpClient、RibbonClient,HttpClient等等都在其中。
那么怎么去做呢?就拿Http Client中HttpComponentsClientHttpRequestFactory类举例:在客户端consumer的test中写一个java示例:
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* @ClassName Http Client实现,这里比如使用Http Client ,那它对应的接口就是HttpComponentsClientHttpRequestFactory
* @Describe TODO
* @Author 66477
* @Date 2020/6/321:32
* @Version 1.0
*/
public class RestTemplateDemo {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
System.out.println(restTemplate.getForObject("http://localhost:8080/actuator/health", String.class));
}
}
跑起来控制台如下:
21:48:19.770 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "Connection: keep-alive[\r][\n]"
21:48:19.770 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "[\r][\n]"
21:48:19.770 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "f[\r][\n]"
21:48:19.770 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "{"status":"UP"}[\r][\n]"
21:48:19.773 [main] DEBUG org.apache.http.headers - http-outgoing-0 << HTTP/1.1 200
21:48:19.773 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Content-Type: application/json;charset=UTF-8
21:48:19.773 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Transfer-Encoding: chunked
21:48:19.773 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Date: Wed, 03 Jun 2020 13:48:19 GMT
21:48:19.773 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Keep-Alive: timeout=60
21:48:19.773 [main] DEBUG org.apache.http.headers - http-outgoing-0 << Connection: keep-alive
21:48:19.779 [main] DEBUG org.apache.http.impl.execchain.MainClientExec - Connection can be kept alive for 60000 MILLISECONDS
21:48:19.783 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
21:48:19.785 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json;charset=UTF-8"
21:48:19.786 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "0[\r][\n]"
21:48:19.786 [main] DEBUG org.apache.http.wire - http-outgoing-0 << "[\r][\n]"
21:48:19.788 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection [id: 0][route: {}->http://localhost:8080] can be kept alive for 60.0 seconds
21:48:19.788 [main] DEBUG org.apache.http.impl.conn.DefaultManagedHttpClientConnection - http-outgoing-0: set socket timeout to 0
21:48:19.788 [main] DEBUG org.apache.http.impl.conn.PoolingHttpClientConnectionManager - Connection released: [id: 0][route: {}->http://localhost:8080][total available: 1; route allocated: 1 of 5; total allocated: 1 of 10]
{"status":"UP"}
Process finished with exit code 0
以后根据习惯和团队习惯来使用适合自己的Http Client。
HTTP请求拦截器 ClientHttpRequestInterceptor
首先我们先找到ClientHttpRequestInterceptor这个接口,然后同样的Ctril+Alt+B看它的实现类,其中一个叫做LoadBalancerInterceptor,其中有个LoadBalancerClient,Ctril+Alt+B查看LoadBalancerClient的实现类RibbonLoadBalancerClient。我在这个类的下面图中方法处打了个断点:
然后我们重启下Eureka server,还是启动两个端口:9090和9091。(这里启动两是为了之前配置的server互相注册实现高可用控制台不报错)
接着启动客户端的provider(启动两个端口),第一个端口采用7070,第二个端口采用7071.具体操作还是使用--sever.port= 7071启动操作。
然后以debug启动客户端consumer。
此时3个服务状态如下:
那么这时候我们postman访问http://localhost:8080/user/save接口,保存一个用户至内存。
这时会触发断点,我们先给予放行,然后(IDEA F9)(Eclipse F8)直接全部放行。
这时浏览器输入http://localhost:8080/user/list,这时又会跳到断点,这时我们F8一步步向下,到图中位置发现这里红框里调用的是provider端口为7070端口;
到此为止直接(IDEA F9)(Eclipse F8)放行。然后再次刷新http://localhost:8080/user/list,重复上面的断点调试,到红框处,你会发现它调用的是provider的7071端口服务。
这就是负载均衡的源码过程。那么此时架构图如下:
三、整合Netflix Ribbon
实际请求客户端
- LoadBalanceClient
- RibbonLoadBalanceClient
负载均衡上下文
- LoadBalancerContext
- RibbonLoadBalancerContext
负载均衡器
- ILoadBalancer
- BaseLoadBalancer
- DynamicServerListLoadBalancer
- ZoneAwareLoadBalancer
- NoOpLoadBalancer
负载均衡规则
核心规则接口
- IRule
- 随机规则:RandomRule
- 最可用规则:BestAvailableRule
- 轮询规则:RoundRobinRule
- 重试实现:RetryRule
- 客户端配置:ClientConfigEnabledRoundRobinRule
- 可用性过滤规则:AvailabilityFilterRule
- RT权重规则:WeightedResponseTimeRule
- 规避区域规则:ZoneAvoidanceRule
PING策略
核心策略接口
- IPingStrategy
PING接口
- IPing
- NoOpPing
- DummyPing
- PingConstant
- PingUrl
Discovery Client实现
- NIWSDiscoveryPing
四、问题总结
1.为什么要用Eureka?
解答:目前业界比较稳定云计算的开发中间件,虽然有一些不足,基本上可用。
2.使用Eureka的话,每个服务api都需要eureka插件?
解答:需要使用Eureka客户端
3.eureka主要功能为啥不能用浮动ip代替呢?
解答:如果要使用浮动ip也是可以的,不过需要扩展。
4.这章内容是不是用eureka来解释负载均衡原理、转发规则计算?
解答:是的。
5.eureka可以替换为zookeeper和consul,那么这几个使用有什么差异?
解答:可以参考:https://www.consul.io/intro/vs
6.服务器高可用配置时,是两个eureka节点互相注册,为啥要把register-with-eureka和fetch-register都改为true?
解答:无解释。一般都是这么配置的。
7.通讯不是指注册到defaultZone配置的那个么?
解答:默认情况是往defaultZone注册。
8.如果服务注册中心都挂了,服务还是能够运行吧?
解答:服务调用还是可以运行,有可能数据会不及时,不一致。
9.Spring Cloud日志收集有解决方案吗?
解答:一般用HBase,或者TSDB.
10.Spring Cloud 提供了链路跟踪的方法吗?