SpringCloud:Eureka和负载均衡
什么是注册中心
在最初的架构体系中,集群的概念还不那么流行,且机器数量也比较少,此时直接使用 DNS + Nginx 就可以满足几乎所有服务的发现。相关的注册信息直接配置在 Nginx。但是随着微服务的流行与流量的激增,机器规模逐渐变大,并且机器会有频繁的上下线行为,这种时候需要运维手动地去维护这个配置信息是一个很麻烦的操作。所以开发者们开始希望有这么一个东西,它能维护一个服务列表,哪个机器上线了,哪个机器宕机了,这些信息都会自动更新到服务列表上,客户端拿到这个列表,直接进行服务调用即可。这个就是注册中心。
注册中心主要有三种角色:
- 服务提供者(Server):一次业务中,被其他微服务调用的服务。也就是提供接口给其他微服务。
- 服务消费者(Client):一次业务中,调用其他微服务的服务。也就是调用其他微服务提供的接口。
- 服务注册中心(Registry):用于保存 Server 的注册信息,当 Server 节点发生变更时,Registry 会同步变更。服务与注册中心使用一定机制通信,如果注册中心与某服务长时间无法通信,就会注销该实例。
他们之间的关系以及工作内容,可以通过两个概念来描述:
服务注册:服务提供者在启动时,向 Registry 注册自身服务,并向 Registry 定期发送心跳汇报存活状态。
服务发现:服务消费者从注册中心查询服务提供者的地址,并通过该地址调用服务提供者的接口。服务发现的一个重要作用就是提供给服务消费者一个可用的服务列表。

CAP理论
CAP理论主要包含三部分

- 一致性(Consistency):CAP 理论中的一致性指的是强一致性,即所有节点在相同时间具有相同的数据。
- 可用性(Availability)保证每个请求都有响应(响应结果可能不对)。
- 分区容错性(Partition Tolerance)当出现网络分区后,系统仍然能够对外提供服务。
假设一个部门在全国各地都有岗位,这时候总部下发了一个通知,由于通知需要开会周知全员,当有客户咨询时:
1. 所有成员对客户的回应结果都是一致的(一致性)。
2. 客户咨询时,一定有回应(可用性)。
3. 当其中一个成员休假时,这个部门的其他成员也可以对客户提供咨询服务(分区容错性)。
CAP 理论告诉我们:一个分布式系统不可能同时满足数据一致性、服务可用性和分区容错性这三个基本需求,最多只能同时满足其中的两个。
在分布式系统中,系统间的网络不能 100% 保证健康,服务又必须对外保证服务。因此,分区容错性是不可避免的。那就只能在一致性(C)和可用性(A)中选择一个。也就是说,分布式系统只能选择 CP(一致性 + 分区容错性)或者 AP(可用性 + 分区容错性)架构。
假设一个分布式系统中有两个节点,分别存储了数据的最新版本 V1 和旧版本 V0。当发生网络分区时:
CP 架构:系统会拒绝返回任何数据,直到所有节点能够同步并达成一致,确保返回的数据是最新且一致的。因此,部分请求可能会因为等待同步而超时或失败。
AP 架构:系统会选择返回旧版本 V0 的数据,以确保每个请求都能得到响应,即使这个数据可能不是最新的,但系统始终可用。
这两种架构各有优缺点,具体选择取决于系统的业务需求和对一致性和可用性的优先级。
常见注册中心
Zookeeper (CP)
Zookeeper 的官方并没有说它是一个注册中心,但是在国内的 Java 体系中,大部分的集群环境都是依赖 Zookeeper 来完成注册中心的功能。
Eureka (AP)
Eureka 是 Netflix 开发的基于 REST 的服务发现框架,主要用于服务注册、管理,负载均衡和服务故障转移。官方声明在 Eureka 2.0 版本停止维护,不建议使用。但是 Eureka 是 Spring Cloud 服务注册/发现的默认实现,所以目前还是有很多公司在使用。
Nacos (CP或AP,默认AP)
Nacos 是 Spring Cloud Alibaba 架构中重要的组件,除了服务注册、服务发现功能之外,Nacos 还支持配置管理、流量管理、DNS、动态 DNS 等多种特性。
Eureka介绍
搭建Eureka服务
引入依赖和项目构建工具
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins>
</build>
需要给启动类加上@EnableEurekaServer 注解,才能开启eureka注册中心服务
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {public static void main(String[] args) {SpringApplication.run(EurekaServerApplication.class, args);}
}
yml配置文件
# Eureka相关配置
# Eureka 服务
server:port: 10010
spring:application:name: eureka-server
eureka:instance:hostname: localhostclient:fetch-registry: false # 表示是否从Eureka Server获取注册信息,默认为true.因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,这里设置为falseregister-with-eureka: false # 表示是否将自己注册到Eureka Server,默认为true.由于当前应用就是Eureka Server,故而设置为false.service-url:# 设置与Eureka Server的地址,查询服务和注册服务都需要依赖这个地址defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/成功启动服务后访问127.0.0.1:10010就可以访问到Eureka客户端了

服务注册
在需要注册到Eureka的服务项目里添加依赖(博主这里是product-service )
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
product-service服务yml文件配置
spring:application:name: product-service
eureka:client:service-url:defaultZone: http://127.0.0.1:10010/eureka成功启动两个服务之后就可以在Eureka客户端界面看到了

服务发现
现在再使用一个服务order-service,通过Eureka进行远程调用拉取product-service里的信息(因为刚刚product-service已经注册到Eureka里了所以现在其他服务就可以使用product-service提供的一些方法),博主这里是order-service
引入依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>yml配置文件
spring:application:name: order-service
eureka:client:service-url:defaultZone: http://127.0.0.1:10010/eureka远程调用
@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate DiscoveryClient discoveryClient;@Autowiredprivate RestTemplate restTemplate;private List<ServiceInstance> instances;@PostConstructpublic void init(){//应用在启动之初就会从eureka中读取,就不会发生变化了instances = discoveryClient.getInstances("product-service");//拿到的是一个数组}public OrderInfo selectOrder(Long id){//查询orderOrderInfo orderInfo = orderMapper.selectOrder(id);//通过http获取设置productinfo//String url = "http://127.0.0.1:8081/product/" + orderInfo.getProductId();List<ServiceInstance> instances = discoveryClient.getInstances("product-service");//拿到的是一个数组String uri = instances.get(0).getUri().toString();//ip地址+端口号String url = uri + "/product/" + orderInfo.getProductId();log.info("URL: " + url);ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);//设置productinfoorderInfo.setProductInfo(productInfo);return orderInfo;}启动服务之后就可以看到了

Eureka和Zookeeper的区别
Eureka 和 Zookeeper 都是用于服务注册和发现的工具,它们的区别如下:
开源背景:Eureka 是 Netflix 开源的项目,而 Zookeeper 是 Apache 开源的项目。
CAP 原则:Eureka 基于 AP 原则,保证高可用性;Zookeeper 基于 CP 原则,保证数据一致性。
节点角色:Eureka 的每个节点都是均等的,而 Zookeeper 的节点区分 Leader 和 Follower 或 Observer。正因为这个原因,如果 Zookeeper 的 Leader 发生故障时,需要重新选举,选举过程中集群会有短暂时间的不可用。
负载均衡
刚刚的远程调用代码通过List<ServiceInstance> 获取服务实例列表,再从列表中选择一个服务实例
List<ServiceInstance> instances = discoveryClient.getInstances("productservice");
//服务可能有多个,
获取第⼀个
EurekaServiceInstance instance = (EurekaServiceInstance) instances.get(0);如果有多个服务那么能不能保证负载均衡呢?
在上面的情况下,一般只会一直访问同一个服务,不过可以通过修改服务端口号的方式实现顺序访问不同的服务
private AtomicInteger count = new AtomicInteger();//计数器,原子性的操作
public OrderInfo selectOrder(Long id){//查询orderOrderInfo orderInfo = orderMapper.selectOrder(id);//通过http获取设置productinfo//String url = "http://127.0.0.1:8081/product/" + orderInfo.getProductId();//计算轮流的实例indexint index = count.getAndIncrement() % instances.size();//保证每次的下标都递增//获取实例String uri = instances.get(index).getUri().toString();//ip地址+端口号//拼接urlString url = uri + "/product/" + orderInfo.getProductId();log.info("URL: " + url);ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);//设置productinfoorderInfo.setProductInfo(productInfo);return orderInfo;}什么是负载均衡
负载均衡(Load Balance,简称 LB)是高并发、高可用系统必不可少的关键组件。当服务流量增大时,通常会采用增加机器的方式进行扩容,负载均衡就是用来在多个机器或者其他资源中,按照一定的规则合理分配负载。
一个团队最开始只有一个人,后来随着工作量的增加,公司又招聘了几个人。负载均衡就是:如何把工作量均衡地分配到这几个人身上,以提高整个团队的效率。
SpringCloudLoadBalancer
Spring Cloud从2020.0.1版本开始,移除了Ribbon组件,使用Spring Cloud LoadBalancer组件来代替Ribbon实现客户端负载均衡。
使用的方法也很简单给RestTemplate这个Bean添加 @LoadBalanced 注解就可以。
@Configuration
public class BeanConfig {@Bean@LoadBalancedpublic RestTemplate restTemplate(){return new RestTemplate();}
}之后修改IP端口号为服务名称
public OrderInfo selectOrder(Long id){//查询orderOrderInfo orderInfo = orderMapper.selectOrder(id);//通过http获取设置productinfo//SpringCloudLoadBalanced,会自动解析urlString url = "http://product-service/product/" + orderInfo.getProductId();log.info("URL: " + url);ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);//设置productinfoorderInfo.setProductInfo(productInfo);return orderInfo;
}负载均衡策略
负载均衡策略是一种思想,无论是哪种负载均衡器,它们的负载均衡策略都是相似的。Spring Cloud LoadBalancer仅支持两种负载均衡策略:轮询策略和随机策略。
1. 轮询:轮询策略是指服务器轮流处理用户请求。这是一种实现最简单、也最常用的策略。生活中也有类似的场景,比如学校轮流值日,或者轮流打扫卫生。
2. 随机选择:随机选择策略是指随机选择一个后端服务器来处理新的请求。
LoadBalancer原理
LoadBalancer的实现主要是通过LoadBalancerInterceptor,这个类会对RestTemplate的请求进行拦截,然后从Eureka根据服务ID获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务ID。
下面是源码:
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {//...public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {URI originalUri = request.getURI();String serviceName = originalUri.getHost();Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));}
}可以看到这里的intercept方法,拦截了用户的HttpRequest请求,然后做了几件事:
1. request.getURI():从请求中获取URI
2. originalUri.getHost():从URI中获取路径的主机名,也就是服务ID,例如product-service。
3. loadBalancer.execute:根据服务ID,进行负载均衡,并处理请求。
点进去继续跟踪
public class BlockingLoadBalancerClient implements LoadBalancerClient {public <T> T execute(String serviceId, LoadBalancerRequest<T> request)throws IOException {String hint = this.getHint(serviceId);LoadBalancerRequestAdapter<T, TimedRequestContext> lbRequest = newLoadBalancerRequestAdapter(request, this.buildRequestContext(request,hint));Set<LoadBalancerLifecycle> supportedLifecycleProcessors = this.getSupportedLifecycleProcessors(serviceId);supportedLifecycleProcessors.forEach((lifecycle) -> {lifecycle.onStart(lbRequest);});//根据serviceId,和负载均衡策略, 选择处理的服务ServiceInstance serviceInstance = this.choose(serviceId, lbRequest);if (serviceInstance == null) {supportedLifecycleProcessors.forEach((lifecycle) -> {lifecycle.onComplete(new CompletionContext(Status.DISCARD,lbRequest, new EmptyResponse()));});throw new IllegalStateException("No instances available for " +serviceId);} else {return this.execute(serviceId, serviceInstance, lbRequest);}}/*** 根据serviceId,和负载均衡策略, 选择处理的服务 **/public <T> ServiceInstance choose(String serviceId, Request<T> request) {//获取负载均衡器 ReactiveLoadBalancer<ServiceInstance> loadBalancer =this.loadBalancerClientFactory.getInstance(serviceId);if (loadBalancer == null) {return null;} else {//根据负载均衡算法, 在列表中选择⼀个服务实例 Response<ServiceInstance> loadBalancerResponse =(Response) Mono.from(loadBalancer.choose(request)).block();return loadBalancerResponse == null ? null :(ServiceInstance)loadBalancerResponse.getServer();}}
}