1总结eureka微服务创建的流程
1、 搭建注册中心eureka
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
2.0.0.RELEASE
配置 yml文件
server:
port: 7001
eureka:
instance:
hostname: localhost
client:
#声明自己是个服务端
registerWithEureka: false #false表示不向注册中心注册自己
fetchRegistry: false #false表示自己就是注册中心,职责是维护实例,不参加检索
serviceUrl: #设置eureka server的交互地址,即对外暴露的地址
defaultZone: http://
e
u
r
e
k
a
.
i
n
s
t
a
n
c
e
.
h
o
s
t
n
a
m
e
:
{eureka.instance.hostname}:
eureka.instance.hostname:{server.port}/eureka/
2 、注册服务到服务中心
server: port: 8001 #指定注册中心地址 eureka: client: serviceUrl: defaultZone: http://localhost:7001/eureka/ #服务的名称 spring: application: name: product-service
1
1:pom.xml
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
2:application.yml
server:
port: 8001
指定注册中心地址
eureka:
client:
service-url:
defaultZone: http://eureka7001/eureka
#定义服务名称
spring:
application:
name: cloud-payment-service
3 、 微服务要引入接口调用jar 实现注解式编程
org.springframework.cloud
spring-cloud-starter-openfeign
在使用Feign的时候,要注意使用requestBody,应该使用@PostMapping
1、执行流程
总到来说,Feign的源码实现的过程如下:
1
(1)首先通过@EnableFeignCleints注解开启FeignCleint
(2)根据Feign的规则实现接口,并加@FeignCleint注解
(3)程序启动后,会进行包扫描,扫描所有的@ FeignCleint的注解的类,并将这些信息注入到ioc容器中。
(4)当接口的方法被调用,通过jdk的代理,来生成具体的RequesTemplate
(5)RequesTemplate在生成Request
(6)Request交给Client去处理,其中Client可以是HttpUrlConnection、HttpClient也可以是Okhttp
(7)最后Client被封装到LoadBalanceClient类,这个类结合类Ribbon做到了负载均衡。
2、Feign和Ribbon比较优点
(1) feign本身里面就包含有了ribbon,只是对于ribbon进行进一步封装
(2) feign自身是一个声明式的伪http客户端,写起来更加思路清晰和方便
(3) fegin是一个采用基于接口的注解的编程方式,更加简便
4、网关搭建
导入网关jar
server:
port: 6001
#服务的名称
spring:
application:
name: zuul-gateway
#指定注册中心地址
eureka:
client:
serviceUrl:
defaultZone: http://localhost:7001/eureka/
#/order-service/api/v1/order/save?user_id=2&product_id=1
#自定义路由映射
zuul:
routes:
order-service: /apigateway/order/**
product-service: /apigateway/product/**
#统一入口为上面的配置,其他入口忽略
ignored-patterns: /*-service/**
#忽略整个服务,对外提供接口
ignored-services: order-service
2.如何搭建eureka集群
端口分别是7001,7002,7003。然后我们分别在三个配置文件中修改hostname
在这里插入图片描述
域名修改完了,我们需要通过挂载defaultZone把三个服务的地址关联起来,配置文件变成这样,三个eureka的配置文件做相应修改即可。
在这里插入图片描述
修改完之后三个eureka成功关联,达到下图的效果
在这里插入图片描述
每个微服务注册三个eureka
这个很好理解,在需要被注册的微服务配置文件中更改注册内容
在这里插入图片描述
测试
1
以此启动7001,7002,7003,在eureka的管理界面上可以看到,三者进行了管理,如果这时你把刚才要注册到3个eureka的微服务启动,你会发现3个eureka都可以发现dept服务。
在这里插入图片描述
3.RestTemplate如何使用?
一:restTemplate简介
1.1:restTemplate的类结构
可以看出它继承自HttpAccessor这个统一的处理器,然后再继承自InterceptingHttpAccessor,这个拦截转换器,最终RestTemplate实现了封装httpClient的模板工具类
1.2:restTemplate的方法
Spring用于同步客户端HTTP访问的中心类。它简化了与HTTP服务器的通信,并执行RESTful原则。它处理HTTP连接,使应用程序代码提供URL,使用可能的模板变量,并提取结果。
注意:默认情况下,RestTemplate依赖于标准的JDK来建立HTTP连接。你可以切换使用不同的HTTP库,如Apache HttpComponents,Netty和OkHttp通过setRequestFactory属性。 内部模板使用HttpMessageConverter实例将HTTP消息转换为POJO和从POJO转换。主要MIME类型的转换器是默认注册的,但您也可以注册其他转换器通过setMessageConverters
以下是http方法和restTempalte方法的比对映射,可以看出restTemplate提供了操作http的方法,其中exchange方法可以用来做任何的请求,一般我们都是用它来封装不同的请求方式。
二:restTemplate的配置方法
2.1:在springboot中的配置,springboot是一款简化传统xml配置式的开发方式,主要采用注解的方式来代替传统繁琐的xml配置,接下来我们就用springboot提供的注解来配置restTemplate:
@Configuration
public class RestTemplateConfig {
private static final Logger logger= LoggerFactory.getLogger(RestTemplateConfig.class);
@Bean
public RestTemplate restTemplate() {
// 添加内容转换器,使用默认的内容转换器
RestTemplate restTemplate = new RestTemplate(httpRequestFactory());
// 设置编码格式为UTF-8
List<HttpMessageConverter<?>> converterList = restTemplate.getMessageConverters();
HttpMessageConverter<?> converterTarget = null;
for (HttpMessageConverter<?> item : converterList) {
if (item.getClass() == StringHttpMessageConverter.class) {
converterTarget = item;
break;
}
}
if (converterTarget != null) {
converterList.remove(converterTarget);
}
HttpMessageConverter<?> converter = new StringHttpMessageConverter(StandardCharsets.UTF_8);
converterList.add(1,converter);
LOGGER.info("-----restTemplate-----初始化完成");
return restTemplate;
}
@Bean
public ClientHttpRequestFactory httpRequestFactory() {
return new HttpComponentsClientHttpRequestFactory(httpClient());
}
@Bean
public HttpClient httpClient() {
// 长连接保持30秒
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
//设置整个连接池最大连接数 根据自己的场景决定
connectionManager.setMaxTotal(500);
//同路由的并发数,路由是对maxTotal的细分
connectionManager.setDefaultMaxPerRoute(500);
//requestConfig
RequestConfig requestConfig = RequestConfig.custom()
//服务器返回数据(response)的时间,超过该时间抛出read timeout
.setSocketTimeout(10000)
//连接上服务器(握手成功)的时间,超出该时间抛出connect timeout
.setConnectTimeout(5000)
//从连接池中获取连接的超时时间,超过该时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
.setConnectionRequestTimeout(500)
.build();
//headers
List<Header> headers = new ArrayList<>();
headers.add(new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36"));
headers.add(new BasicHeader("Accept-Encoding", "gzip,deflate"));
headers.add(new BasicHeader("Accept-Language", "zh-CN"));
headers.add(new BasicHeader("Connection", "Keep-Alive"));
headers.add(new BasicHeader("Content-type", "application/json;charset=UTF-8"));
return HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.setDefaultHeaders(headers)
// 保持长连接配置,需要在头添加Keep-Alive
.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
//重试次数,默认是3次,没有开启
.setRetryHandler(new DefaultHttpRequestRetryHandler(2, true))
.build();
}
}
首先解释以下@configuration,它的主要作用就是在spring容器启动的时候,初始化IOC,使用了这个注解,那么该类就会在spring启动的时候,把@Bean注解标识的类进行依赖注入。@Bean理解的话,就好比在配置文件中配置.接下来就是在restTemplate的构造方法中添加httpRequest的工厂,使用连接池来优化http通信,默认使用长连接时间为30秒,再设置路由让http连接定向到指定的IP,然后设置并发数。再就是设置请求配置的超时时间,为了防止请求时间过长而引起资源的过渡浪费。如果在超过设置的timeout还没有数据返回,就直接断开连接。headers是添加默认的请求头,这里设置了传送的格式为json,语言为中-英等等属性。HttpClientBuilder.create设置请求头到HttpClient,然后在设置保持的时间,重试的次数,注入给httpClient进行封装。
在bean中的HttpMessageConverter,就是http信息转换器,它的主要作用就是转换和解析返回来的json数据,restTemplate默认使用jackson来作为底层的解析工具,而其它的比如Gson,fastjson等等第三方开源库放在headers这个list中,如果要使用,可以通过以下代码进行改变:
this.restTemplate.getMessageConverters().clear();
final List<HttpMessageConverter<?>> myHttpMessageConverter = new ArrayList<HttpMessageConverter<?>>();
//自己实现的messgeConverter
HttpMessageConverter<Object> messageConverter = new MyHttpMessageConverter<Object>();
myHttpMessageConverter.add(messageConverter);
this.restTemplate.setMessageConverters(myHttpMessageConverter);
三:restUtil工具类
restUtil就是通过包装restTemplate暴露出面向外界的方法,通过高度封装,可以隐藏内部细节,简单使用,在使用它的时候,我们只需要传入请求的url和对应的参数,然后就可以取到结果了。参数一般有两种形式,一种是直接传入json,另一种是key、value形式的,key/value形式的,可以直接使用execute方法,传入url和请求的方法类型就可以了。在开头看到了restTemplate基本上是支持所有http请求的,接下来的工具类就介绍一下post和get请求的主要封装方法
@Component
public class RestUtil {
@Autowired
private RestTemplate restTemplate;
//一些自定义的请求头参数
public static final String supplierID="";
public static final String interfacekey= "";
/**
* DLT专用执行方法
* @param param 请求参数:可以添加一些常量请求值
* @param url 访问的url
* @param method 请求的方法
* @return
*/
public String execute(Map<String,Object> param, String url, HttpMethod method){
HttpHeaders headers = this.getDefaultHeader();
Map<String,Object> requestor = this.getDefaultParam();
param.put("requestor",requestor);
param.put("supplierID",supplierID);
HttpEntity<Map<String,Object>> requestEntity = new HttpEntity<>(param, headers);
ResponseEntity<String> response = restTemplate.exchange(url,method, requestEntity, String.class);
return response.getBody();
}
/**
* 获取默认的头请求信息
* @return
*/
public HttpHeaders getDefaultHeader(){
String timestamp = ""+System.currentTimeMillis();
String signature = EncoderByMd5(supplierID + timestamp + interfacekey);
HttpHeaders headers = new HttpHeaders();
headers.add("signature", signature);
headers.add("timestamp", timestamp);
return headers;
}
/**
* 获取默认的参数
* @return
*/
public Map<String,Object> getDefaultParam(){
Map<String,Object> defParam = new HashMap<>();
defParam.put("invoker","xx");
defParam.put("operatorName","xx");
return defParam;
}
/**
* 通过MD5加密
* @param str
* @return
*/
public static String EncoderByMd5(String str){
if (str == null) {
return null;
}
try {
// 确定计算方法
MessageDigest md5 = MessageDigest.getInstance("MD5");
BASE64Encoder base64en = new BASE64Encoder();
// 加密后的字符串
return base64en.encode(md5.digest(str.getBytes("utf-8"))).toUpperCase();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
return null;
}
}
/**
* get请求
* @param url 请求的url
* @param jsonData 请求的json
* @return
*/
public String restGet(String url,String jsonData){
return request(url, jsonData,HttpMethod.GET);
}
/**
* @param url 请求的url
* @param jsonData json数据
* @param httpMethod
* @return
*/
private String request(String url, String jsonData,HttpMethod httpMethod) {
ResponseEntity<String> response=null;
try {
if (Check.isEmpty(url)) {
throw new IllegalArgumentException();
}
HttpEntity<String> requestEntity = new HttpEntity<String>(jsonData);
response = restTemplate.exchange(url, httpMethod, requestEntity, String.class);
}catch (Exception ex){
ex.printStackTrace();
return "";
}
return response.getBody().toString();
}
/**
* Get请求获取实体类
* @param url 请求的url
* @param responseType 返回的类型
* @param parms 不限定个数的参数
* @param <T> 泛型
* @return
*/
public <T> T getForEntity(String url,Class<T> responseType,Object... parms){
return (T) restTemplate.getForEntity(url,responseType,parms);
}
/**
* Get请求
* @param url
* @param parm
* @return
*/
public String get(String url,Map<String,Object> parm){
return restTemplate.getForEntity(url,String.class,parm).getBody();
}
}
四:使用示例
4.1:首先我们用springBoot来搭建一个简单的rest请求链接,我们来模拟一个请求,传入年龄和性别、身高,计算出标准体重的接口,这段代码比较简单,我只给出示范代码:
@SpringBootApplication
@RestController
public class HealApplication {
@RequestMapping(value = “weight”, method = RequestMethod.GET)
public ResultModel getWeight(@RequestParam(value = “height”, required = false) Integer height, @RequestParam(value = “sex”, required = false) Integer sex, @RequestParam(value = “age”, required = false) Integer age) {
if (height == null || age == null || sex == null || (!sex.equals(0) && !sex.equals(1))) {
return new ResultModel(400, "缺少请求参数或者参数错误", 0d);
}
double condition = getStandardWeight(sex, age, height);
return new ResultModel(200, "请求成功", condition);
}
/**
* 获取标准体重
*
* @param sex 性别 1:男 2:女
* @param age 年龄
* @param height
* @return 体重(单位:kg)
*/
public double getStandardWeight(int sex, int age, int height) {
double weight = 0.0;
switch (sex) {
//男性
case 1:
if (age < 12 && age > 2) {
weight = age * 2 + 12;
} else if (age > 12) {
weight = (height - 150) * 0.6 + 50;
}
break;
case 0:
if (age < 12 && age > 2) {
weight = age * 2 + 12;
} else if (age > 12) {
weight = (height - 100) * 0.6 + 50;
}
break;
default:
weight = 0;
break;
}
return weight;
}
可以看到我们的控制器有个映射weight请求的方法,通过传入年龄、身高、性别,就可以计算出标准体重,我们来启动springBoot,先试着用浏览器访问一下,可以看出如下结果:
4.2:为了表明接口是通的,我们再用postman来试一下,可以看到返回结果正确:
4.3:在springboot里引入testNg单元测试类,测试一下访问这个链接的结果:
public class TestRestManager extends OrderProviderApplicationTests {
@Autowired
private RestUtil restUtil;
/**
* 请求方法为GEt
* @return
*/
@Test
private void requestGet(){
String url="http://localhost:8080/weight?age={age}&sex={sex}&height={height}";
//组装请求参数
Map<String,Object> parmMap =new HashMap<String,Object>();
parmMap.put("age",35);
parmMap.put("sex",1);
parmMap.put("height",178);
String result = restUtil.get(url, parmMap);
System.out.println(result);
}
}
4.简述eureka的自我保护模式? 如何配置其自我保护模式
一、Eureka的自我保护模式
如果在Eureka Server的首页看到以下这段提示,则说明Eureka已经进入了保护模式:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
产生原因:Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server会将当前的实例注册信息保护起来,同时提示这个警告。保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。 一般出现此模式时,服务返回错误。即如果真实的服务已经Down掉,但在注册中心界面服务却一直存在,且显示为UP状态。
详见:https://github.com/Netflix/eureka/wiki/Understanding-Eureka-Peer-to-Peer-Communication
解决方法:设置enableSelfPreservation:false
配置心跳检测时长,下线leaseRenewalIntervalInSeconds: 2
如何处理服务挂掉后或者手动关闭服务后,Ribbon负载均衡还是一直调用这个服务,然后调用@HystrixCommand断路器注解的方法:利用Hystrix,在error callback方法中可以shutdown指定的server
1
2
3
4
ZoneAwareLoadBalancer lb = (ZoneAwareLoadBalancer) springClientFactory.getLoadBalancer(“CLOUD-SERVICE”);
Server server = lb.chooseServer();
System.out.println(“error->” + server.getHostPort());
lb.markServerDown(server);
另外在Camden.SR3中可以配置Ribbon请求重试,可以参考DD大神的新作:http://blog.didispace.com/spring-cloud-ribbon-failed-retry/
二、指定Eureka的Environment
1
eureka.environment: 指定环境
参考文档:https://github.com/Netflix/eureka/wiki/Configuring-Eureka
三、指定Eureka的DataCenter
1
eureka.datacenter: 指定数据中心
5. 什么是CAP理论? cp ap原则的含义
想必大多数的IT从业者,都听过CAP理论,但是听过和理解并熟练应用又是两码事,笔者也看了几篇文章,就想在这片里浅谈一下我的理解。
CAP理论,是指在一个互相连接且共享数据的分布式系统中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition tolerance)三者中的两个,三者不可兼得。
这里有个坑,就是什么样的分布式系统才满足CAP理论呢,就是互相连接并且共享数据的分布式系统,大家都知道,什么是分布式系统,但是分布式系统不一定会互相连接和共享数据呢。举个例子,memcache集群,各节点存储的数据都不一样,所以这类分布式系统就不满足CAP理论,而类似mysql集群,需要互联进行数据复制,就满足CAP理论。
CAP理论关注的是数据的读写操作,而不是分布式系统的所有功能。像分布式系统的负载均衡、容错等都不是CAP理论的讨论范围。
故,CAP理论针对的是互相连接并且共享数据的分布式系统的数据的读写操作。
下面我们先分别解释一下CAP理论里每个理论的概念。
一致性(Consistence),对指定的客户端来说,读操作保证能够返回最新的写操作的结果。举个例子,mysql在事务执行过程中,系统就是一个不一致的状态,不同的节点很可能对应的数据不一致,数据提交前后数据是不一致的,客户端只能读取事务提交成功后的数据,对应客户端来说,这就是符合一致性的状态。
可用性(Availability)非故障节点在合理的时间内返回合理的相应。这里说的合理的相应,肯定不是超时或者错误,只要合理即可。举个例子,查询本应是返回A,结果返回的是B,就是满足了可用性,但是显然,没满足一致性。
分区容错性(Partition tolerance),当网络分区后,系统可以继续正常的工作。网络分区指的是网络设备出现的丢包、阻塞、超时等问题。
在分布式系统中,网络不是100%可靠的,所以我们必须选择P。但为什么这三个要素不能同时满足呢?当分布式系统满足P时,如果网络分区,为了保证C,我们会禁止写入,而为了保证A,我们又不能返回error,这就冲突了,所以在分布式系统里,我们只能选择CP或者AP架构。
CP架构,当节点1与节点2之间出现网络分区,节点1与节点2之间不能进行数据交换,假设节点1的数据是最新的,这时client访问节点2,为保证一致性(读操作保证能够返回最新的写操作的结果),节点2只能返回error,违背了A,故这种架构属于CP。
AP架构,同样的情况,当节点1与节点2之间出现网络分区,节点1与节点2之间不能进行数据交换,client访问节点,因需要满足A,故节点2需要给client响应,返回的是b,而不是最新的数据a,虽然不是最新的数据,但是却是合理的,违背了C(对指定的客户端来说,读操作保证能够返回最新的写操作的结果),故这种架构属于AP。
上面介绍了CAP理论中每个要素的含义,以及CP/AP架构的简单的例子。当我们的系统,没有发生网络分区的时候,是会同时满足CAP的,没有必要放弃A/P,而我们系统绝大多数时间都是稳定的,我们只需要考虑网络分区是需要选择AP/CP即可,没有发生网络分区是,我们需要考虑如何保证CA。
所以舍弃A/P,只是在网络分区时才考虑,在网络正常之后,我们要选择适当的补偿机制,来保证之后系统的CA,不能让影响扩大。
选择CP/AP其实一个很大的难题。一个系统里有很多个功能和业务,而对于不同的业务和功能,我们可以选择不同的模型。举个例子,我们做一个秒杀系统,查询商品的时候我们可以选择AP架构,短时间的数据不一致,保证用户可以查询出列表,但购买商品的时候,我们要选择CP架构,没有库存,就不能进行购买。但是整个秒杀系统,不是AP也不是CP,而是AP和CP的混合。
所以在实际的设计和开发中,我们要灵活运用CAP理论,灵活选择AP或CP架构。
6. eureka 和zookeeper consul的区别?
主要区别的话,看CAP选择,大部分注册中心,就是在这个定理去选择的,具体怎么选择,看下文
CAP定理: 指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可同时获得。
一致性(C): 在分布式系统中的所有数据备份,在同一时刻是否同样的值。(所有节点在同一时间的数据完全一致,越多节点,数据同步越耗时)
可用性(A): 负载过大后,集群整体是否还能响应客户端的读写请求。(服务一直可用,而且是正常响应时间)
分区容错性(P): 分区容忍性,就是高可用性,一个节点崩了,并不影响其它的节点(100个节点,挂了几个,不影响服务,越多机器越好)
再进一步解释CAP理论: 就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。所以我们只能在一致性和可用性之间进行权衡
C A 满足的情况下,P不能满足的原因:
数据同步©需要时间,也要正常的时间内响应(A),那么机器数量就要少,所以P就不满足
CP 满足的情况下,A不能满足的原因:
数据同步©需要时间, 机器数量也多§,但是同步数据需要时间,所以不能再正常时间内响应,所以A就不满足
AP 满足的情况下,C不能满足的原因:
机器数量也多§,正常的时间内响应(A),那么数据就不能及时同步到其他节点,所以C不满足
使用场景 就是楼主的注册中心选择:
Zookeeper和Consul :CP设计,保证了一致性,集群搭建的时候,某个节点失效,则会进行选举行的leader,或者半数以上节点不可用,则无法提供服务,因此可用性没法满足
Eureka:AP原则,无主从节点,一个节点挂了,自动切换其他节点可以使用,去中心化
结论:分布式系统中P,肯定要满足,所以只能在CA中二选一
没有最好的选择,最好的选择是根据业务场景来进行架构设计
如果要求一致性,则选择zookeeper、Consul,如金融行业
如果要去可用性,则Eureka,如电商系统
7. 使用ribbon进行负载均衡的步骤
ribbon的作用是负载均衡,这篇博客就来分析一下这里面的请求流程,里面贴的源码会比较多,如果看不惯的话,可以直接看最后的总结
一般来说,使用原生ribbon而不搭配feign的话,使用的都是RestTemplate,通过这个RestTemplate 来访问其他的服务,看起来是这样的
@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
RestTemplate本身并没有负载均衡的功能,只是一个单纯的http请求组件而已,通过上面的代码,我们可以发现多了一个@LoadBalanced注解,这个注解就是ribbon实现负载均衡的一个入口,我们就从这里开始看
/**
- Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
- @author Spencer Gibb
*/
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
乍一眼看过去,这个注解好像是没啥东西,这个时候就需要一些技巧了,一般的Spring Boot项目都会有一个XXXAutoConfigration类作为自动配置类,这里面都会提供一些有用的信息,在同一个包下稍微找找就能发现一个类叫做LoadBalancerAutoConfiguration,我们接着往里面看
在这个类里面,最重要的就是给RestTemplate 添加了一个拦截器,那么这个拦截器的作用是什么呢?其实这个拦截器就是将请求交给了ribbon来处理,之后的负载均衡就由ribbon全权负责了
@Configuration
@ConditionalOnMissingClass(“org.springframework.retry.support.RetryTemplate”)
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
// 将拦截器加入到restTemplate中
restTemplate.setInterceptors(list);
}
};
}
}
接下来就是看看这个拦截器具体在做些什么,首先进入这个拦截器的类,发现里面有一个intercept方法
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
// 获取请求url
final URI originalUri = request.getURI();
// 获取服务名称
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
拦截器最后又调用了loadBalancer的execute方法,那就接着往下看吧
@Override
public T execute(String serviceId, LoadBalancerRequest request) throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer);
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);
}
这里的loadBalancer默认是ZoneAwareLoadBalancer,下面的方法就是getServer方法,光从方法名也可以猜出来这个方法就会根据多个服务实例负载均衡出来一个机器出来,那么在此之前就有一个问题了,我们是如何取到所有服务实例的信息的呢?
这就得依靠服务注册中心了,因为服务实例的信息都注册到了服务注册中心中了,这里以Eureka为例,那么ribbon是如何从Eureka中获取到服务实例信息呢?
这里的奥秘就在ZoneAwareLoadBalancer中
public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule,
IPing ping, ServerList serverList, ServerListFilter filter,
ServerListUpdater serverListUpdater) {
super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
}
在ZoneAwareLoadBalancer的构造函数中,我们发现其实就是调用了父类(DynamicServerListLoadBalancer)的构造方法,接着往下走
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
ServerList serverList, ServerListFilter filter,
ServerListUpdater serverListUpdater) {
super(clientConfig, rule, ping);
this.serverListImpl = serverList;
this.filter = filter;
this.serverListUpdater = serverListUpdater;
if (filter instanceof AbstractServerListFilter) {
((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
}
restOfInit(clientConfig);
}
重点在restOfInit方法中
void restOfInit(IClientConfig clientConfig) {
boolean primeConnection = this.isEnablePrimingConnections();
// turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
this.setEnablePrimingConnections(false);
enableAndInitLearnNewServersFeature();
updateListOfServers();
if (primeConnection && this.getPrimeConnections() != null) {
this.getPrimeConnections()
.primeConnections(getReachableServers());
}
this.setEnablePrimingConnections(primeConnection);
LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
}
enableAndInitLearnNewServersFeature方法我们之后再说,先来看updateListOfServers方法,很明显这个方法就是在更新服务实例列表的信息,可以直接理解为从Eureka中获取服务实例注册表中的信息
@VisibleForTesting
public void updateListOfServers() {
List servers = new ArrayList();
if (serverListImpl != null) {
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug(“List of Servers for {} obtained from Discovery client: {}”,
getIdentifier(), servers);
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
updateAllServerList(servers);
}
serverListImpl.getUpdatedListOfServers()这段代码就是从Eureka中获取服务注册信息,走得是DiscoveryEnabledNIWSServerList的getUpdatedListOfServers方法,具体这边就不再展开细讲了,反正这里就获取到了所有的服务实例信息,以供后面的负载均衡算法来进行选择
回过头再看看之前跳过的enableAndInitLearnNewServersFeature方法
public void enableAndInitLearnNewServersFeature() {
LOGGER.info(“Using serverListUpdater {}”, serverListUpdater.getClass().getSimpleName());
serverListUpdater.start(updateAction);
}
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
};
发现这个方法里面还是调用的updateListOfServers方法,这里其实就是一个线程,每隔30秒再去Eureka同步一下最新的服务注册信息
如果你还有印象的话,我们之前就是分析到了获取负载均衡的算法的地方,也就是getServer方法
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer(“default”); // TODO: better handling of key
}
chooseServer就是实际进行负载均衡的地方,这里会根据你使用的负载均衡算法从服务实例中选择一台机器来发送请求,跳过中间的代码跳转,直接来分析一下默认的RoundRobinRule,轮询算法
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn(“no load balancer”);
return null;
}
Server server = null;
int count = 0;
// count 在这里其实是一个重试的次数
while (server == null && count++ < 10) {
// 所有启动的服务实例
List<Server> reachableServers = lb.getReachableServers();
// 通过Eureka获取的服务实例
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
// 没有可用服务实例的话返回null
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
// 轮询算法的核心,也很好理解,递增并根据服务实例数量取模
int nextServerIndex = incrementAndGetModulo(serverCount);
// 从所有服务实例中取出选择的那台机器
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
// 服务实例是可用的话则返回它
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
到这里为止就已经分析完了整个ribbon负载均衡的流程,之后就可以根据选择的服务实例,去发送我们的请求了
通过这张图来总结一下整个ribbon负载均衡的流程
发送请求,被LoadBalancerInterceptor拦截器拦截,请求被交给ribbon来处理
拦截器拦截请求,交给了RibbonLoadBalancerClient的execute方法(下面的逻辑都是包含在这个方法中)
在进行负载均衡之前首先得知道有哪些服务实例信息,所以通过DynamicServerListLoadBalancer的updateListOfServers方法从注册中心(Eureka)那里获取到了所有的服务实例信息,并且会定时更新
使用负载均衡算法(默认轮询算法)从所有的服务实例信息中选择一台机器出来
将请求发送给负载均衡选择出来的服务实例上去
8.ribbon负载均衡的策略有哪些?
IRule
这是所有负载均衡策略的父接口,里边的核心方法就是choose方法,用来选择一个服务实例。
AbstractLoadBalancerRule
AbstractLoadBalancerRule是一个抽象类,里边主要定义了一个ILoadBalancer,就是我们上文所说的负载均衡器,负载均衡器的功能我们在上文已经说的很详细了,这里就不再赘述,这里定义它的目的主要是辅助负责均衡策略选取合适的服务端实例。
RandomRule
看名字就知道,这种负载均衡策略就是随机选择一个服务实例,看源码我们知道,在RandomRule的无参构造方法中初始化了一个Random对象,然后在它重写的choose方法又调用了choose(ILoadBalancer lb, Object key)这个重载的choose方法,在这个重载的choose方法中,每次利用random对象生成一个不大于服务实例总数的随机数,并将该数作为下标所以获取一个服务实例。
RoundRobinRule
RoundRobinRule这种负载均衡策略叫做线性负载均衡策略,也就是我们在上文所说的BaseLoadBalancer负载均衡器中默认采用的负载均衡策略。这个类的choose(ILoadBalancer lb, Object key)函数整体逻辑是这样的:开启一个计数器count,在while循环中遍历服务清单,获取清单之前先通过incrementAndGetModulo方法获取一个下标,这个下标是一个不断自增长的数先加1然后和服务清单总数取模之后获取到的(所以这个下标从来不会越界),拿着下标再去服务清单列表中取服务,每次循环计数器都会加1,如果连续10次都没有取到服务,则会报一个警告No available alive servers after 10 tries from load balancer: XXXX。
RetryRule
看名字就知道这种负载均衡策略带有重试功能。首先RetryRule中又定义了一个subRule,它的实现类是RoundRobinRule,然后在RetryRule的choose(ILoadBalancer lb, Object key)方法中,每次还是采用RoundRobinRule中的choose规则来选择一个服务实例,如果选到的实例正常就返回,如果选择的服务实例为null或者已经失效,则在失效时间deadline之前不断的进行重试(重试时获取服务的策略还是RoundRobinRule中定义的策略),如果超过了deadline还是没取到则会返回一个null。
WeightedResponseTimeRule
WeightedResponseTimeRule是RoundRobinRule的一个子类,在WeightedResponseTimeRule中对RoundRobinRule的功能进行了扩展,WeightedResponseTimeRule中会根据每一个实例的运行情况来给计算出该实例的一个权重,然后在挑选实例的时候则根据权重进行挑选,这样能够实现更优的实例调用。WeightedResponseTimeRule中有一个名叫DynamicServerWeightTask的定时任务,默认情况下每隔30秒会计算一次各个服务实例的权重,权重的计算规则也很简单,如果一个服务的平均响应时间越短则权重越大,那么该服务实例被选中执行任务的概率也就越大。
ClientConfigEnabledRoundRobinRule
ClientConfigEnabledRoundRobinRule选择策略的实现很简单,内部定义了RoundRobinRule,choose方法还是采用了RoundRobinRule的choose方法,所以它的选择策略和RoundRobinRule的选择策略一致,不赘述。
BestAvailableRule
BestAvailableRule继承自ClientConfigEnabledRoundRobinRule,它在ClientConfigEnabledRoundRobinRule的基础上主要增加了根据loadBalancerStats中保存的服务实例的状态信息来过滤掉失效的服务实例的功能,然后顺便找出并发请求最小的服务实例来使用。然而loadBalancerStats有可能为null,如果loadBalancerStats为null,则BestAvailableRule将采用它的父类即ClientConfigEnabledRoundRobinRule的服务选取策略(线性轮询)。
PredicateBasedRule
PredicateBasedRule是ClientConfigEnabledRoundRobinRule的一个子类,它先通过内部定义的一个过滤器过滤出一部分服务实例清单,然后再采用线性轮询的方式从过滤出来的结果中选取一个服务实例。
ZoneAvoidanceRule
ZoneAvoidanceRule是PredicateBasedRule的一个实现类,只不过这里多一个过滤条件,ZoneAvoidanceRule中的过滤条件是以ZoneAvoidancePredicate为主过滤条件和以AvailabilityPredicate为次过滤条件组成的一个叫做CompositePredicate的组合过滤条件,过滤成功之后,继续采用线性轮询的方式从过滤结果中选择一个出来。
9.如何自定义负载均衡
1.现在我们不使用Ribbon默认的负载均衡策略,自定义一个负载均衡策略。
修改microcloudservice-consumer-dept-80的主程序类,加入注解
@RibbonClient。
2.
现在我们自定义一个Ribbon负载均衡的策略MyRule。这个自定义的配置类不能放在@ComponentScan所扫描的包下一级子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,也就是说我们达不到特殊化定制的目的。
package com.gwolf.config;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyRule {
@Bean
public IRule myRule() {
return new RandomRule();
}
}
3.在启动该微服务的时候就能去加载我们的自定义的Ribbon配置类,从而使配置生效。
在启动类上自定义Ribbon负载均衡。
@RibbonClient(name= “MICROCLOUDSERVICE-PROVIDER-DEPT”, configuration = MyRule.class)
4.@RibbonClient(name = “MICROCLOUDSERVICE-PROVIDER-DEPT”, configuration = MyRule.class)这个注解中的name代表的是自定义负载均衡的是哪个提供者,configuration 代表使用的是哪个自定义的负载均衡类。
5.现在我们程序有一个报错:Invalid bean definition with name ‘myRule’ defined in com.gwolf.config.MyRule: Bean name derived from @Bean method ‘myRule’ clashes with bean name for containing configuration class; please make those names unique!
这个是因为我们在ConfigBean中定义了一个相同的Bean名字。
6.更改我们自定义MyRule返回的Bean的名字。
7.现在我们重新启动这个部门消费者,查看是否是随机访问的策略模式。
10 服务提供方集群如何搭建
1、创建cloud-provider-payment8002
仿照cloud-provider-payment8001创建cloud-provider-payment8002
2、服务提供者注册进Eureka集群
在cloud-provider-payment8001和cloud-provider-payment8002中分别将注册地址改成集群地址
eureka:
client:
register-with-eureka: true #表示是否将自己注册进EurekaServer默认为True
fetch-registry: true #是否愿意从Eureka抓取字的注册信息,默认为true.单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka
并且为了方便查看服务信息,用端口区分
在Controller里修改
@Value("${server.port}")
private String port;
@PostMapping("/payment/create")
public CommonResult create(@RequestBody Payment payment){
int result=paymentService.create(payment);
log.info("******插入结果"+result);
if(result>0){
return new CommonResult(200,"插入数据库成功,ServerPort"+port,result);
}else {
return new CommonResult(444,"插入数据库失败",null);
}
}
@GetMapping("/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id")Long id){
payment=paymentService.getPaymentById(id);
log.info("******查询到的"+payment);
if(payment!=null){
return new CommonResult(200,"查询成功,ServerPort"+port,payment);
}else {
return new CommonResult(444,"没有对应记录,查询ID"+id,null);
}
}
3、消费者向集群注册中心拿服务
此时8001和8002组成了服务提供者集群
因此拿服务的地址应该改成服务提供者注册到Eureka的名称
public static final String PAYMENT_URL=“http://CLOUD-PAYMENT-SERVICE”;
4、开启负载均衡
开启RestTemplate的负载均衡特别简单
在Config上加上@LoadBalanced注解即可
5、启动Eureka集群,启动服务提供者集群,启动80消费者。
在Eureka的面板上能看见所有信息
image.png
并且通过80消费端口去访问发现我们的集群也已经实现了简单的负载均衡的轮询