此文章很大部分转载https://www.mrhelloworld.com/,博主均测试通过
什么是注册中心
注册中心可以说是微服务架构中的“通讯录”,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就到这里找到服务的地址,进行调用。
举个现实生活中的例子,比如说,我们手机中的通讯录的两个使用场景:
当我想给张三打电话时,那我需要在通讯录中按照名字找到张三,然后就可以找到他的手机号拨打电话。—— 服务发现
李四办了手机号并把手机号告诉了我,我把李四的号码存进通讯录,后续,我就可以从通讯录找到他。—— 服务注册
通讯录 —— ?什么角色(提示:服务注册中心)
总结:服务注册中心的作用就是服务的注册和服务的发现。
常见的注册中心
- Netflix Eureka
- Alibaba Nacos
- HashiCorp Consul
- Apache ZooKeeper
Eureka 介绍
Eureka 是 Netflix 开发的服务发现组件,本身是一个基于 REST 的服务。
Spring Cloud 将它集成在其子项目 Spring Cloud Netflix 中,实现 Spring Cloud 的服务注册与发现,同时还提供了负载均衡、故障转移等能力。
Eureka 注册中心三种角色
Eureka Server
通过 Register、Get、Renew 等接口提供服务的注册和发现。
Service Provider(Eureka Client)
服务提供方,把自身的服务实例注册到 Eureka Server 中。
Service Consumer(Eureka Client)
服务调用方,通过 Eureka Server 获取服务列表,消费服务。
Eureka 入门案例
eureka-demo
聚合工程。SpringBoot 2.2.4.RELEASE
、Spring Cloud Hoxton.SR1
。
创建项目
我们创建聚合项目来讲解 Eureka,首先创建一个 pom 父工程。
添加依赖pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- 项目坐标地址 --> <groupId>com.example</groupId> <!-- 项目模块名称 --> <artifactId>eureka-demo</artifactId> <!-- 项目版本名称 快照版本SNAPSHOT、正式版本RELEASE --> <version>1.0-SNAPSHOT</version> <!-- 继承 spring-boot-starter-parent 依赖 --> <!-- 使用继承方式,实现复用,符合继承的都可以被使用 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.4.RELEASE</version> </parent> <!-- 集中定义依赖组件版本号,但不引入, 在子工程中用到声明的依赖时,可以不加依赖的版本号, 这样可以统一管理工程中用到的依赖版本 --> <properties> <!-- Spring Cloud Hoxton.SR1 依赖 --> <spring-cloud.version>Hoxton.SR1</spring-cloud.version> </properties> <!-- 项目依赖管理 父项目只是声明依赖,子项目需要写明需要的依赖(可以省略版本信息) --> <dependencyManagement> <dependencies> <!-- spring cloud 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>
注册中心 eureka-server
创建项目
在刚才的父工程下创建 eureka-server
注册中心的项目。
添加依赖pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>eureka-server</artifactId> <version>1.0-SNAPSHOT</version> <!-- 继承父依赖 --> <parent> <groupId>com.example</groupId> <artifactId>eureka-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <!-- 项目依赖 --> <dependencies> <!-- netflix eureka server 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <!-- spring boot web 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- spring boot test 依赖 --> <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> </project>
配置文件application.yml
server:
port: 8761 # 端口
spring:
application:
name: eureka-server # 应用名称
# 配置 Eureka Server 注册中心
eureka:
instance:
hostname: localhost # 主机名,不配置的时候将根据操作系统的主机名来获取
client:
#如果该应用的角色是注册中心并是单节点的话,要关闭这两个配置项
register-with-eureka: true # 是否将自己注册到注册中心,默认为 true
fetch-registry: true # 是否从注册中心获取服务注册信息,默认为 true
service-url: # 注册中心对外暴露的注册地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
此时如果直接启动项目是会报错的,错误信息:
这是因为 Eureka 默认开启了将自己注册至注册中心和从注册中心获取服务注册信息的配置,如果该应用的角色是注册中心并是单节点的话,要关闭这两个配置项。
server:
port: 8761 # 端口
spring:
application:
name: eureka-server # 应用名称
# 配置 Eureka Server 注册中心
eureka:
instance:
hostname: localhost # 主机名,不配置的时候将根据操作系统的主机名来获取
client:
register-with-eureka: false # 是否将自己注册到注册中心,默认为 true
fetch-registry: false # 是否从注册中心获取服务注册信息,默认为 true
service-url: # 注册中心对外暴露的注册地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
启动类
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication // 开启 EurekaServer 注解 @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
访问http://localhost:8761/
高可用 Eureka 注册中心
注册中心 eureka-server
创建项目
在刚才的父工程下再创建一个 eureka-server02
注册中心的项目,如果是多机器部署不用修改端口,通过 IP 区分服务,如果在一台机器上演示需要修改端口区分服务。
添加依赖pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>eureka-server02</artifactId> <version>1.0-SNAPSHOT</version> <!-- 继承父依赖 --> <parent> <groupId>com.example</groupId> <artifactId>eureka-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <!-- 项目依赖 --> <dependencies> <!-- netflix eureka server 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <!-- spring boot web 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- spring boot test 依赖 --> <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> </project>
配置文件application.yml
集群配置下,注册中心需要相互注册实现信息的同步。
eureka-server 的 application.yml
server:
port: 8761 # 端口
spring:
application:
name: eureka-server # 应用名称(集群下相同)
# 配置 Eureka Server 注册中心
eureka:
instance:
hostname: eureka01 # 主机名,不配置的时候将根据操作系统的主机名来获取
client:
# 设置服务注册中心地址,指向另一个注册中心
service-url: # 注册中心对外暴露的注册地址
defaultZone: http://localhost:8762/eureka/
eureka-server02 的 application.yml
server:
port: 8762 # 端口
spring:
application:
name: eureka-server # 应用名称(集群下相同)
# 配置 Eureka Server 注册中心
eureka:
instance:
hostname: eureka02 # 主机名,不配置的时候将根据操作系统的主机名来获取
client:
# 设置服务注册中心地址,指向另一个注册中心
service-url: # 注册中心对外暴露的注册地址
defaultZone: http://localhost:8761/eureka/
启动类启动两个 server
访问http://localhost:8761/ 或者 http://localhost:8762/ 都出现如下图说明互相注册成功。
Status
显示方式为默认值,如果想要清晰可见每个服务的 IP + 端口需要通过以下配置来实现
显示 IP + 端口
一个普通的 Netflix Eureka 实例注册的 ID 等于其主机名(即,每个主机仅提供一项服务)。 Spring Cloud Eureka 提供了合理的默认值,定义如下:
${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}}
也就是:主机名:应用名:应用端口。我们也可以可以自定义进行修改:
eureka:
instance:
prefer-ip-address: true # 是否使用 ip 地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
服务提供者 service-provider
创建项目
在刚才的父工程下创建一个 service-provider
服务提供者的项目。
添加依赖pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>service-provider</artifactId> <version>1.0-SNAPSHOT</version> <!-- 继承父依赖 --> <parent> <groupId>com.example</groupId> <artifactId>eureka-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <!-- 项目依赖 --> <dependencies> <!-- netflix eureka client 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- spring boot web 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- lombok 依赖 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <!-- spring boot test 依赖 --> <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> </project>
配置文件application.yml
server:
port: 7070 # 端口
spring:
application:
name: service-provider # 应用名称(集群下相同)
# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true # 是否使用 ip 地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
service-url: # 设置服务注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
实体类Product.java
package com.example.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {
private Integer id;
private String productName;
private Integer productNum;
private Double productPrice;
}
编写服务ProductService.java
package com.example.service; import com.example.pojo.Product; import java.util.List; /** * 商品服务 */ public interface ProductService { /** * 查询商品列表 * * @return */ List<Product> selectProductList(); }
ProductServiceImpl.java
package com.example.service.impl; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.List; /** * 商品服务 */ @Service public class ProductServiceImpl implements ProductService { /** * 查询商品列表 * * @return */ @Override public List<Product> selectProductList() { return Arrays.asList( new Product(1, "华为手机", 2, 5888D), new Product(2, "联想笔记本", 1, 6888D), new Product(3, "小米平板", 5, 2666D) ); } }
控制层ProductController.java
package com.example.controller; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/product") public class ProductController { @Autowired private ProductService productService; /** * 查询商品列表 * * @return */ @GetMapping("/list") public List<Product> selectProductList() { return productService.selectProductList(); } }
该项目我们可以通过单元测试进行测试,也可以直接通过 url 使用 postman 或者浏览器来进行测试。
启动类ServiceProviderApplication.java
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication // 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解 //@EnableEurekaClient public class ServiceProviderApplication { public static void main(String[] args) { SpringApplication.run(ServiceProviderApplication.class, args); } }
注册中心
访问注册中心,可以看到用户服务已经注册至注册中心。
服务消费者 service-consumer
创建项目
在刚才的父工程下创建一个 service-consumer 服务消费者的项目。
添加依赖pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>service-consumer</artifactId> <version>1.0-SNAPSHOT</version> <!-- 继承父依赖 --> <parent> <groupId>com.example</groupId> <artifactId>eureka-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <!-- 项目依赖 --> <dependencies> <!-- netflix eureka client 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- spring boot web 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- lombok 依赖 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <!-- spring boot test 依赖 --> <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> </project>
配置文件application.yml
server:
port: 9090 # 端口
spring:
application:
name: service-consumer # 应用名称
# 配置 Eureka Server 注册中心
eureka:
client:
register-with-eureka: false # 是否将自己注册到注册中心,默认为 true
registry-fetch-interval-seconds: 10 # 表示 Eureka Client 间隔多久去服务器拉取注册信息,默认为 30 秒
service-url: # 设置服务注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
实体类
Product.java
package com.example.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @NoArgsConstructor @AllArgsConstructor public class Product implements Serializable { private Integer id; private String productName; private Integer productNum; private Double productPrice; }
Order.java
package com.example.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.List; @Data @NoArgsConstructor @AllArgsConstructor public class Order implements Serializable { private Integer id; private String orderNo; private String orderAddress; private Double totalPrice; private List<Product> productList; }
消费服务OrderService.java
RestTemplate
时添加 @LoadBalanced
负载均衡注解,表示这个 RestTemplate
在请求时拥有客户端负载均衡的能力。
package com.example.service; import com.example.pojo.Order; public interface OrderService { /** * 根据主键查询订单 * * @param id * @return */ Order selectOrderById(Integer id); }
对于服务的消费我们这里讲三种实现方式:
- DiscoveryClient:通过元数据获取服务信息
- LoadBalancerClient:Ribbon 的负载均衡器
- @LoadBalanced:通过注解开启 Ribbon 的负载均衡器
DiscoveryClient
Spring Boot 不提供任何自动配置的RestTemplate
bean,所以需要在启动类中注入 RestTemplate
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication // 开启 Eureka Client 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解 //@EnableEurekaClient public class ServiceConsumerApplication { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ServiceConsumerApplication.class, args); } }
OrderServiceImpl.java
package com.example.service.impl; import com.example.pojo.Order; import com.example.pojo.Product; import com.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.web.client.RestTemplate; import java.util.List; @Service public class OrderServiceImpl implements OrderService { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; /** * 根据主键查询订单 * * @param id * @return */ @Override public Order selectOrderById(Integer id) { return new Order(id, "order-001", "中国", 31994D, selectProductListByDiscoveryClient()); } private List<Product> selectProductListByDiscoveryClient() { StringBuffer sb = null; // 获取服务列表 List<String> serviceIds = discoveryClient.getServices(); if (CollectionUtils.isEmpty(serviceIds)) return null; // 根据服务名称获取服务 List<ServiceInstance> serviceInstances = discoveryClient.getInstances("service-provider"); if (CollectionUtils.isEmpty(serviceInstances)) return null; ServiceInstance si = serviceInstances.get(0); sb = new StringBuffer(); sb.append("http://" + si.getHost() + ":" + si.getPort() + "/product/list"); // ResponseEntity: 封装了返回数据 ResponseEntity<List<Product>> response = restTemplate.exchange( sb.toString(), HttpMethod.GET, null, new ParameterizedTypeReference<List<Product>>() {}); return response.getBody(); } }
LoadBalancerClient
OrderServiceImpl.java
package com.example.service.impl; import com.example.pojo.Order; import com.example.pojo.Product; import com.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; @Service public class OrderServiceImpl implements OrderService { @Autowired private RestTemplate restTemplate; @Autowired private LoadBalancerClient loadBalancerClient; // Ribbon 负载均衡器 /** * 根据主键查询订单 * * @param id * @return */ @Override public Order selectOrderById(Integer id) { return new Order(id, "order-001", "中国", 31994D, selectProductListByLoadBalancerClient()); } private List<Product> selectProductListByLoadBalancerClient() { StringBuffer sb = null; // 根据服务名称获取服务 ServiceInstance si = loadBalancerClient.choose("service-provider"); if (null == si) return null; sb = new StringBuffer(); sb.append("http://" + si.getHost() + ":" + si.getPort() + "/product/list"); // ResponseEntity: 封装了返回数据 ResponseEntity<List<Product>> response = restTemplate.exchange( sb.toString(), HttpMethod.GET, null, new ParameterizedTypeReference<List<Product>>() {}); return response.getBody(); } }
@LoadBalanced
启动类注入 RestTemplate
时添加 @LoadBalanced
负载均衡注解,表示这个 RestTemplate
在请求时拥有客户端负载均衡的能力。
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication // 开启 Eureka Client 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解 //@EnableEurekaClient public class ServiceConsumerApplication { @Bean @LoadBalanced // 负载均衡注解 public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ServiceConsumerApplication.class, args); } }
OrderServiceImpl.java
package com.example.service.impl; import com.example.pojo.Order; import com.example.pojo.Product; import com.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; @Service public class OrderServiceImpl implements OrderService { @Autowired private RestTemplate restTemplate; /** * 根据主键查询订单 * * @param id * @return */ @Override public Order selectOrderById(Integer id) { return new Order(id, "order-001", "中国", 31994D, selectProductListByLoadBalancerAnnotation()); } private List<Product> selectProductListByLoadBalancerAnnotation() { // ResponseEntity: 封装了返回数据 ResponseEntity<List<Product>> response = restTemplate.exchange( "http://service-provider/product/list", HttpMethod.GET, null, new ParameterizedTypeReference<List<Product>>() {}); return response.getBody(); } }
控制层OrderController.java
package com.example.controller; import com.example.pojo.Order; import com.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; /** * 根据主键查询订单 * * @param id * @return */ @GetMapping("/{id}") public Order selectOrderById(@PathVariable("id") Integer id) { return orderService.selectOrderById(id); } }
访问http://localhost:9090/order/1
接下来我们建第二个provider02
除了名称和provider模块相同
我们贴一下配置文件
server: port: 7071 # 端口 spring: application: name: service-provider # 应用名称(集群下相同) # 配置 Eureka Server 注册中心 eureka: instance: prefer-ip-address: true # 是否使用 ip 地址注册 instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port client: service-url: # 设置服务注册中心地址 defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
看注册中心
使用restTemplate调用的时候将@LoadBalanced 注释掉
@Bean //@LoadBalanced // 负载均衡注解,注入 RestTemplate 时添加 @LoadBalanced 负载均衡注解,表示这个 RestTemplate 在请求时拥有客户端负载均衡的能力。 public RestTemplate restTemplate() { return new RestTemplate(); }
访问http://localhost:9090/order/1,看结果
这里是因为有2个service-provider,他会报这个错,就是找不到host的异常
需要将@LoadBalanced 加上就正常了
Eureka 架构原理
- Register(服务注册):把自己的 IP 和端口注册给 Eureka。
- Renew(服务续约):发送心跳包,每 30 秒发送一次,告诉 Eureka 自己还活着。如果 90 秒还未发送心跳,宕机。
- Cancel(服务下线):当 Provider 关闭时会向 Eureka 发送消息,把自己从服务列表中删除。防止 Consumer 调用到不存在的服务。
- Get Registry(获取服务注册列表):获取其他服务列表。
- Replicate(集群中数据同步):Eureka 集群中的数据复制与同步。
- Make Remote Call(远程调用):完成服务的远程调用。
Eureka 自我保护
启动自我保护条件
一般情况下,服务在 Eureka 上注册后,会每 30 秒发送心跳包,Eureka 通过心跳来判断服务是否健康,同时会定期删除超过 90 秒没有发送心跳的服务。
有两种情况会导致 Eureka Server 收不到微服务的心跳
- 微服务自身的原因
- 微服务与 Eureka 之间的网络故障
自我保护模式
Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期,同时提示一个警告。
这种算法叫做 Eureka Server 的自我保护模式。
为什么要启动自我保护
- 因为同时保留"好数据"与"坏数据"总比丢掉任何数据要更好,当网络故障恢复后,这个 Eureka 节点会退出"自我保护模式”。
- Eureka 还有客户端缓存功能(也就是微服务的缓存功能)。即使 Eureka 集群中所有节点都宕机失效,微服务的 Provider 和 Consumer 都能正常通信。
- 微服务的负载均衡策略会自动剔除死亡的微服务节点。
如何关闭自我保护
eureka的server端配置
eureka:
server:
enable-self-preservation: false # true:开启自我保护模式,false:关闭自我保护模式
eviction-interval-timer-in-ms: 60000 # 清理间隔(单位:毫秒,默认是 60*1000)
eureka的client端配置
eureka:
instance:
leaseRenewalIntervalInSeconds: 10 #eureka客户需要多长时间发送心跳给eureka服务器,表明它仍然活着,默认为30 秒 (与下面配置的单位都是秒)
leaseExpirationDurationInSeconds: 30 #Eureka服务器在接收到实例的最后一次发出的心跳后,需要等待多久才可以将此实例删除,默认为90秒
Eureka 优雅停服
配置了优雅停服以后,将不需要 Eureka Server 中配置关闭自我保护。本文使用 actuator 实现。
添加依赖
服务提供者添加 actuator 依赖
<!-- spring boot actuator 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
配置文件
服务提供者service-provider配置度量指标监控与健康检查
# 度量指标监控与健康检查
management:
endpoints:
web:
exposure:
include: shutdown # 开启 shutdown 端点访问
endpoint:
shutdown:
enabled: true # 开启 shutdown 实现优雅停服
优雅停服
使用 POST 请求访问:http://localhost:7070/actuator/shutdown 效果如下
Eureka 安全认证
添加依赖
注册中心添加 security 依赖
<!-- spring boot security 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
配置文件
注册中心配置安全认证
spring:
# 安全认证
security:
user:
name: root
password: 123456
修改访问集群节点的 url
注册中心的配置文件
# 配置 Eureka Server 注册中心
eureka:
instance:
hostname: eureka01 # 主机名,不配置的时候将根据操作系统的主机名来获取
prefer-ip-address: true # 是否使用 ip 地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
# 设置服务注册中心地址,指向另一个注册中心
service-url: # 注册中心对外暴露的注册地址
defaultZone: http://root:123456@localhost:8762/eureka/
服务提供者的配置文件
# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true # 是否使用 ip 地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
service-url: # 设置服务注册中心地址
defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8762/eureka/
过滤 CSRF
Eureka 会自动化配置 CSRF 防御机制,Spring Security 认为 POST, PUT, and DELETE http methods 都是有风险的,如果这些 method 发送过程中没有带上 CSRF token 的话,会被直接拦截并返回 403 forbidden。
官方给出了解决的方法,具体可以参考 spring cloud issue 2754,里面有大量的讨论,这里提供两种解决方案。
首先注册中心配置一个 @EnableWebSecurity
配置类,继承 org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
,然后重写 configure
方法。
方案一
使 CSRF 忽略 /eureka/**
的所有请求
package com.example.config; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * 安全认证配置类 */ @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); // 加这句是为了访问 eureka 控制台和 /actuator 时能做安全控制 http.csrf().ignoringAntMatchers("/eureka/**"); // 忽略 /eureka/** 的所有请求 } }
方案二
保持密码验证的同时禁用 CSRF 防御机制
package com.example.config; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * 安全认证配置类 */ @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 注意,如果直接 disable 的话会把安全验证也禁用掉 http.csrf().disable().authorizeRequests() .anyRequest() .authenticated() .and() .httpBasic(); } }
访问
使用配置好的用户名和密码登录以后可看到注册中心界面,启动服务提供者和服务消费者,功能正常使用,至此 Eureka 注册中心所有的知识点就讲解结束了。