Spring|Spring Cloud学习day98(Eureka的原理和Ribbon负载均衡)

一、Eureka注册中心架构原理:
1.Eureka架构图: Spring|Spring Cloud学习day98(Eureka的原理和Ribbon负载均衡)
文章图片
示例

操作 名称 作用
Register 服务注册 把自己的 IP 和端口注册给 Eureka
Renew 服务续约 发送心跳包,每 30 秒发送一次。告诉 Eureka 自己还活着
Cancel 服务下线 当 provider 关闭时会向 Eureka 发送消息,把自己从服务列表中删除。防止 consumer 调用到不存在的服务。
Get Registry 获取服务注册列表 获取其他服务列表
Replicate 集群中数据同步 eureka 集群中的数据复制与同步
Make Remote Call 远程调用 完成服务的远程调用
2.基于分布式 CAP 定理,Eureka 与 Zookeeper 的区别:
  • 什么是CAP原则?
CAP 原则又称 CAP 定理,指的是在一个分布式系统中,Consistency (一致性)、Availability (可用性)、Partition tolerance (分区容错性),三者不可兼得。
  • 分布式系统CAP定理:
分布式系统 P CAP 定理
C:数据一致性( Consistency) 也叫做数据原子性系统在执行某项操作后仍然处于一致的状态。在分布式系统中,更新操作执行成功后所有的用户都应该读到最新的值,这样的系统被认为是具有强一致性的。等同于所有节点访问同一份最新的数据副本。
A:服务可用性( Availablity) 每一个操作总是能够在一定的时间内返回结果,这里需要注意的是"一定时间内"和"返回结果"。一定时间内指的是,在可以容忍的范围内返回结果,结果可以是成功或者是失败。
P :分区容错性( Partition-torlerance) 在网络分区的情况下,被分隔的节点仍能正常对外提供服务(分布式集群,数据被分布存储在不同的服务器上,无论什么情况,服务器都能正常被访问)
  • CAP定律:
任何分布式系统只可同时满足二点,没法三者兼顾。
定律
CA,放弃P 如果想避免分区容错性问题的发生,一种做法是将所有的数据(与事务相关的)都放在一台机器上。虽然无法 100%保证系统不会出错,单不会碰到由分区带来的负面效果。当然这个选择会严重的影响系统的扩展性。
CP:放弃 A 相对于放弃"分区容错性"来说,其反面就是放弃可用性。一旦遇到分区容错故障,那么受到影响的服务需要等待一定时间,因此在等待时间内系统无法对外提供服务。
AP:放弃 C 这里所说的放弃一致性,并不是完全放弃数据一致性,而是放弃数据的强一致性,而保留数据的最终一致性。以网络购物为例,对只剩下一件库存的商品,如果同时接受了两个订单,那么较晚的订单将被告知商品告罄。
  • Zookeeper与Eureka的区别:
对比项 Zookeeper Eureka
CAP CP AP
Dubbo 集成 支持 ~
Spring Cloud 集成 支持 支持
kv服务 支持 ~
使用接口 提供客户端 http多语言
watch支持 支持 支持
集群监控 ~ metrics
3.Eureka的自我保护:
  • 什么是自我保护?
一般情况下,微服务在 Eureka 上注册后,会每 30 秒发送心跳包,Eureka 通过心跳来判断服务时候健康,同时会定期删除超过 90 秒没有发送心跳服务。
  • 导致Eureka接收不到心跳的原因:
    (1)微服务自身的原因。
    (2)微服务与 Eureka 之间的网络故障:
通常(微服务的自身的故障关闭)只会导致个别服务出现故障,一般不会出现大面积故障,而(网络故障)通常会导致 Eureka Server 在短时间内无法收到大批心跳。考虑到这个区别,Eureka 设置了一个阀值,当判断挂掉的服务的数量超过阀值时,Eureka Server 认为很大程度上出现了网络故障,将不再删除心跳过期的服务。
  • 阀值是多少?
15 分钟之内是否高于 85%;
Eureka Server 在运行期间,会统计心跳失败的比例在 15 分钟内是否高于 85%
这种算法叫做 Eureka Server 的自我保护模式。
  • 为什么要启动自我保护?
(1)因为同时保留"好数据"与"坏数据"总比丢掉任何数据要更好,当网络故障恢复后,这个 Eureka 节点会退出"自我保护模式"。
(2)Eureka 还有客户端缓存功能(也就是微服务的缓存功能)。即便 Eureka 集群中所有节点都宕机失效,微服务的 Provider 和 Consumer都能正常通信。
(3)微服务的负载均衡策略会自动剔除死亡的微服务节点。
  • Eureka自我保护的开关:
#自我保护的开关:true 为开启自我保护,false 为关闭自我保护 eureka.server.enableSelfPreservation=false #清理间隔(单位:毫秒,默认是 60*1000) eureka.server.eviction.interval-timer-in-ms=60000

Spring|Spring Cloud学习day98(Eureka的原理和Ribbon负载均衡)
文章图片
示例 4.如何优雅停服?
停止服务的另一种方式,不需要在Eureka Server 中配置关闭自我保护。
  • 在服务中添加Actuator.jar包:
修改POM文件中Eureka的依赖。
org.springframework.cloud spring-cloud-starter-eureka-server

  • 修改配置文件:
#启用 shutdown endpoints.shutdown.enabled=true #禁用密码验证 endpoints.shutdown.sensitive=false

  • 发送一个关闭服务的URL请求:
package com.bjsxt.springboothelloworld; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; public class HttpClientUtil {public static String doGet(String url, Map param) {// 创建Httpclient对象 CloseableHttpClient httpclient = HttpClients.createDefault(); String resultString = ""; CloseableHttpResponse response = null; try { // 创建uri URIBuilder builder = new URIBuilder(url); if (param != null) { for (String key : param.keySet()) { builder.addParameter(key, param.get(key)); } } URI uri = builder.build(); // 创建http GET请求 HttpGet httpGet = new HttpGet(uri); // 执行请求 response = httpclient.execute(httpGet); // 判断返回状态是否为200 if (response.getStatusLine().getStatusCode() == 200) { resultString = EntityUtils.toString(response.getEntity(), "UTF-8"); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (response != null) { response.close(); } httpclient.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; }public static String doGet(String url) { return doGet(url, null); }public static String doPost(String url, Map param) { // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 创建Http Post请求 HttpPost httpPost = new HttpPost(url); // 创建参数列表 if (param != null) { List paramList = new ArrayList<>(); for (String key : param.keySet()) { paramList.add(new BasicNameValuePair(key, param.get(key))); } // 模拟表单 UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList,"utf-8"); httpPost.setEntity(entity); } // 执行http请求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "utf-8"); } catch (Exception e) { e.printStackTrace(); } finally { try { response.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }return resultString; }public static String doPost(String url) { return doPost(url, null); }public static String doPostJson(String url, String json) { // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 创建Http Post请求 HttpPost httpPost = new HttpPost(url); // 创建请求内容 StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON); httpPost.setEntity(entity); // 执行http请求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "utf-8"); } catch (Exception e) { e.printStackTrace(); } finally { try { response.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }return resultString; }public static void main(String[] args) { String url ="http://127.0.0.1:9090/shutdown"; //该url必须要使用dopost方式来发送 HttpClientUtil.doPost(url); } }

5.加强Eureka注册中心的安全验证:
  • 在Server的POM文件中添加Security依赖:
org.springframework.boot spring-boot-starter-security

  • 修改Eureka Server的配置文件:
#开启 http basic 的安全认证 security.basic.enabled=true security.user.name=user security.user.password=123456

Spring|Spring Cloud学习day98(Eureka的原理和Ribbon负载均衡)
文章图片
低版本和高版本的区别
  • 修改访问集群节点的url:
eureka.client.serviceUrl.defaultZone=http://user:123456@eureka2:8761/eureka/

  • 修改微服务的配置文件添加注册中心的用户和密码:
#设置服务注册中心地址,指向另一个注册中心 eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/,http://user:123456@eureka2:8761/eureka/ #启用 shutdown endpoints.shutdown.enabled=true #禁用密码验证 endpoints.shutdown.sensitive=false

Spring|Spring Cloud学习day98(Eureka的原理和Ribbon负载均衡)
文章图片
示例 二、Ribbon负载均衡
1.Ribbon在微服务中的作用:
  • 什么是Ribbon?
(1)Ribbon 是一个基于 Http 和 TCP 的客服端负载均衡工具,它是基于 Netflix Ribbon 实 现的。
(2)它不像 spring cloud 服务注册中心、配置中心、API 网关那样独立部署,但是它几乎 存在于每个 spring cloud 微服务中。包括 feign 提供的声明式服务调用也是基于该 Ribbon 实现的。
(3)ribbon 默认提供很多种负载均衡算法,例如 轮询、随机 等等。甚至包含自定义的负 载均衡算法。
  • Ribbon解决了什么问题:
解决并提供了微服务的负载均衡的问题。
2.负载均衡的解决方案分类:
  • 集中式负载均衡:
即在 consumer 和 provider 之间使用独立的负载均衡设施(可 以是硬件,如 F5, 也可以是软件,如 nginx), 由该设施负责把 访问请求 通过某种策略转发 至 provider。
  • 进程内负载均衡:
进程内负载均衡,将负载均衡逻辑集成到 consumer,consumer 从服务注册中 心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的 provider。
  • Ribbon的负载均衡:
Ribbon 就属于后者,它只是一个类库,集成于 consumer 进程,consumer 通过它来获取 到 provider 的地址。
  • 两种负载均衡方式结构图:

    Spring|Spring Cloud学习day98(Eureka的原理和Ribbon负载均衡)
    文章图片
    集中式
    Spring|Spring Cloud学习day98(Eureka的原理和Ribbon负载均衡)
    文章图片
    进程内
3.Ribbon中常见的负载均衡:
策略名称 对应的类名 实现原理
轮询策略 RoundRobinRule 轮询策略表示每次都顺序取下一个 provider,比如一共有 5 个provider,第 1 次取第 1 个,第 2次取第 2 个,第 3 次取第 3 个,以此类推
权重轮询策略 WeightedResponseTimeRule 特点:根据每个provider 的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性越低。 ——————原理:一开始为轮询策略,并开启一个计时器,每 30 秒收集一次每个 provider 的平均响应时间,、当信息足够时,给每个 provider附上一个权重,并按权重随机选择
随机策略 RandomRule 从 provider 列表中随机选择一个provider
最少并发数策略 BestAvailableRule 选择正在请求中的并发数最小的 provider,除非这个provider 在熔断中。
在“选定的负载均 衡策略”基础上进行重 试机制 RetryRule “选定的负载均衡策略”这个策略是轮询策略RoundRobinRule 。该重试策略先设定一个阈值时间段,如果在这个阈值时间段内当选择provider 不成功,则一直尝试采用“选定的负载均衡策略:轮询策略”最后选择一个可用的provider
可用性敏感策略 AvailabilityFilteringRule 过滤性能差的 provider,有 2种。第一种:过滤掉在 eureka 中处于一直连接失败 provider。 第二种:过滤掉高并发的 provider
区域敏感性策略 ZoneAvoidanceRule 以一个区域为单位考察可用性,对于不可用的区域整个丢弃,从剩下区域中选可用的provider 。如果这个 ip 区域内有一个或多个实例不可达或响应变慢,都会降低该 ip 区域内其他 ip 被选中的权重
4.Ribbon默认的轮询策略:
  • 在Consumer中添加打印语句:
@Service public class UserService {@Autowired private LoadBalancerClient loadBalancerClient; // ribbon 负 载均衡器 public List getUsers() { // 选择调用的服务的名称 // ServiceInstance 封装了服务的基本信息,如 IP,端口 ServiceInstance si = this.loadBalancerClient.choose("eureka-provider"); // 拼接访问服务的 URL StringBuffer sb = new StringBuffer(); // http://localhost:9090/user }sb.append("http://").append(si.getHost()).append(":").appen d(si.getPort()).append("/user"); //添加输出URL语句 System.out.println(sb.toString()); //springMVC RestTemplate RestTemplate rt = new RestTemplate(); ParameterizedTypeReference> type = new ParameterizedTypeReference>() {}; //ResponseEntity:封装了返回值信息 ResponseEntity> response = rt.exchange(sb.toString(),HttpMethod.GET, null, type); List list =response.getBody(); return list; }}

  • 修改Consumer的配值文件:
spring.application.name=eureka-consumer server.port=9091 #设置服务注册中心地址,指向另一个注册中心 eureka.client.serviceUrl.defaultZone=http://user:123456@eur eka1:8761/eureka/,http://user:123456@eureka2:8761/eureka/

  • 部署Provider的集群:
将Provider打包上传到两台Linux服务器上。
  • 添加server.sh启动脚本:
修改:JAR_NAME="项目名称";
SPRING_PROFILES_ACTIV="";
设置权限:chmod -R 755 server.sh
cd `dirname $0` CUR_SHELL_DIR=`pwd` CUR_SHELL_NAME=`basename ${BASH_SOURCE}` JAR_NAME="项目名称" JAR_PATH=$CUR_SHELL_DIR/$JAR_NAME #JAVA_MEM_OPTS=" -server -Xms1024m -Xmx1024m -XX:PermSize=128m" JAVA_MEM_OPTS="" #SPRING_PROFILES_ACTIV="-Dspring.profiles.active=配置文件变量名称" SPRING_PROFILES_ACTIV="" LOG_DIR=$CUR_SHELL_DIR/logs LOG_PATH=$LOG_DIR/${JAR_NAME%..log echo_help() { echo -e "syntax: sh $CUR_SHELL_NAME start|stop" } if [ -z $1 ]; then echo_help exit 1 fi if [ ! -d "$LOG_DIR" ]; then mkdir "$LOG_DIR" fi if [ ! -f "$LOG_PATH" ]; then touch "$LOG_DIR" fi if [ "$1" == "start" ]; then # check server PIDS=`ps --no-heading -C java -f --width 1000 | grep $JAR_NAME | awk '{print $2}'` if [ -n "$PIDS" ]; then echo -e "ERROR: The $JAR_NAME already started and the PID is ${PIDS}." exit 1 fi echo "Starting the $JAR_NAME..." # start nohup java $JAVA_MEM_OPTS -jar $SPRING_PROFILES_ACTIV $JAR_PATH >> $LOG_PATH 2>&1 & COUNT=0 while [ $COUNT -lt 1 ]; do sleep 1 COUNT=`ps--no-heading -C java -f --width 1000 | grep "$JAR_NAME" | awk '{print $2}' | wc -l` if [ $COUNT -gt 0 ]; then break fi done PIDS=`ps--no-heading -C java -f --width 1000 | grep "$JAR_NAME" | awk '{print $2}'` echo "${JAR_NAME} Started and the PID is ${PIDS}." echo "You can check the log file in ${LOG_PATH} for details." elif [ "$1" == "stop" ]; then PIDS=`ps --no-heading -C java -f --width 1000 | grep $JAR_NAME | awk '{print $2}'` if [ -z "$PIDS" ]; then echo "ERROR:The $JAR_NAME does not started!" exit 1 fi echo -e "Stopping the $JAR_NAME..." for PID in $PIDS; do kill $PID > /dev/null 2>&1 done COUNT=0 while [ $COUNT -lt 1 ]; do sleep 1 COUNT=1 for PID in $PIDS ; do PID_EXIST=`ps --no-heading -p $PID` if [ -n "$PID_EXIST" ]; then COUNT=0 break fi done done echo -e "${JAR_NAME} Stopped and the PID is ${PIDS}." else echo_help exit 1 fi

  • 启动Consumer和Provider集群:

    Spring|Spring Cloud学习day98(Eureka的原理和Ribbon负载均衡)
    文章图片
    示例
5.指定其他负载均衡策略:
  • 在启动类中添加负载均衡策略对象的方法:
@EnableEurekaClient @SpringBootApplication public classConsumerApplication{ @Bean public RandomRule createRule() { return new RandomRule(); }public static void main(String[] args) { SpringApplication.run( ConsumerApplication .class, args); } }

  • 修改Consumer配置文件更换负载均衡策略:
#设置负载均衡策略 eureka-provider 为调用的服务的名称 eureka-provider.ribbon.NFLoadBalancerRuleClassName=策略全限定类名

6.Ribbon 的点对点直连 :
【Spring|Spring Cloud学习day98(Eureka的原理和Ribbon负载均衡)】在不需要通过Eureka注册中心而是直连某个服务。
  • 修改配置文件与 Eureka 相关的配置,添加新配置项 :
spring.application.name=eureka-consumer server.port=9091 #禁用 eureka ribbon.eureka.enabled=false #指定具体的服务实例清单 eureka-provider.ribbon.listOfServers=192.168.226.130:9090

  • 修改启动类去除@EnableEurekaClient注解:
@SpringBootApplication public classConsumerApplication{ public static void main(String[] args) { SpringApplication.run( ConsumerApplication .class, args); } }

    推荐阅读