【Spring Cloud-Eureka】服务治理
为什么需要服务治理
微服务架构下,会有n个服务在运行,服务于服务之间可能存在相互调用的关系,随着服务数量的增加,需要对所有服务进行管理,即服务提供者往注册中心注册,服务消费者从注册中心获取服务列表然后调用,另外如果涉及到动态的容量调整,那么就需要自动的服务发现和服务注销
注册中心的基本模型
访问注册中心的客户端程序一般会嵌入在服务提供者和服务消费者内部。
在服务启动时,服务提供者通过内部的注册中心客户端程序自动将自身注册到注册中心,
服务消费者的注册中心客户端程序则可以从注册中心中获取那些已经注册的服务实例信息。
注册中心的基本模型参考下图
路由缓存表:为了提高服务路由的效率和容错性,服务消费者配备l了缓存机制以加速服务路由。更重要的是,当服务注册中心不可用时,服务消费者可以利用本地缓存路由实现对现有服务的可靠调用
EureKa高可用集群搭建
- 准备
准备2个节点部署eureka,也可以单机部署
修改本机host文件(路径:C:\Windows\System32\drivers\etc),绑定一个主机名,单机部署时使用ip地址会有问题
127.0.0.1 euk1.com
127.0.0.1 euk2.com
- 配置文件
- 配置文件1
eureka:
client:
#是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息
register-with-eureka: true
#是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false
fetch-registry: true
#设置服务注册中心的URL,用于client和server端交流
service-url:
#带有zone的原因是最初该项目在亚马逊上部署,因此有部分配置沿用了早期的配置,配置多个地址的话就可以向多个节点去注册
defaultZone: http://euk2.com:7002/eureka/
instance:
hostname: euk1.com
management:
endpoint:
shutdown:
enabled: true
server:
port: 7001
spring:
application:
name:EurekaServer
- 配置文件2
eureka:
client:
#是否将自己注册到Eureka Server,默认为true,由于当前就是server,故而设置成false,表明该服务不会向eureka注册自己的信息
register-with-eureka: true
#是否从eureka server获取注册信息,由于单节点,不需要同步其他节点数据,用false
fetch-registry: true
#设置服务注册中心的URL,用于client和server端交流
service-url:
#带有zone的原因是最初该项目在亚马逊上部署,因此有部分配置沿用了早期的配置,配置多个地址的话就可以向多个节点去注册
defaultZone: http://euk1.com:7001/eureka/
instance:
hostname: euk2.com
management:
endpoint:
shutdown:
enabled: true
server:
port: 7002
spring:
application:
name:EurekaServer
- 启动类加入@EnableEurekaServer注解来标识这是一个Eureka服务器
@EnableEurekaServer
@SpringBootApplication
public class SpringEurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringEurekaServerApplication.class, args);
}
}
- 用两个配置文件分别启动两个服务,如下图就是集群搭建成功
注册中心原理
- Register(服务注册)
想要参与服务注册发现的实例首先需要向Eureka服务器注册信息,注册在第一次心跳发生时提交
- Renew(续租、心跳)
由客户端向服务端发起,每30秒一次心跳来完成续租,告诉服务端这个客户端还活着,如果服务端在90秒之内没有看到心跳,就把这个客户端从注册列表中移除
- Fetch Registry(获取服务列表信息)
Eureka客户端从服务器获取注册表信息并将其缓存在本地。之后,客户端使用这些信息来查找其他服务。
通过获取上一个获取周期和当前获取周期之间的增量更新,可以定期(每30秒)更新此信息。
节点信息在服务器中保存的时间更长(大约3分钟),因此获取节点信息时可能会再次返回相同的实例。Eureka客户端自动处理重复的信息。
在获得增量之后,Eureka客户机通过比较服务器返回的实例计数来与服务器协调信息,如果由于某种原因信息不匹配,则再次获取整个注册表信息。
- Cancel(下线通知)
Eureka客户端在关闭时向Eureka服务器发送取消请求。这将从服务器的实例注册表中删除实例,从而有效地将实例从通信量中取出。
- Time Lag(同步时间延迟)
来自Eureka客户端的所有操作可能需要一段时间才能反映到Eureka服务器上,然后反映到其他Eureka客户端上。这是因为eureka服务器上的有效负载缓存,它会定期刷新以反映新信息。Eureka客户端还定期地获取增量。因此,更改传播到所有Eureka客户机可能需要2分钟。
- Communication mechanism
通讯机制是Http协议下的Rest请求,默认情况下Eureka使用Jersey和Jackson以及JSON完成节点间的通讯
服务提供方注册
- web应用引入eureka-client支持
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 启动类加入@EnableEurekaClient注解,该注解用于表明当前服务就是一个 Eureka 客户端,这样该服务就可以自动注册到 Eureka 服务器
@EnableEurekaClient
@SpringBootApplication
public class EurekastarterApplication {
public static void main(String[] args) {
SpringApplication.run(EurekastarterApplication.class, args);
}
}
- 配置文件
eureka:
client:
service-url:
#注册中心的访问地址
defaultZone: http://euk1.com:7001/eureka/
server:
port: 80
# 注意多个相同应用的name要保持一致
spring:
application:
name: provider
服务消费方注册
- web应用引入eureka-client支持
- 配置文件
eureka:
client:
service-url:
#注册中心的访问地址
defaultZone: http://euk1.com:7001/eureka/
server:
port: 90
spring:
application:
name: consumer
如何理解服务者和消费者
对于 Eureka 而言,微服务的提供者和消费者都是它的客户端,其中服务提供者关注服务注册、服务续约和服务下线等功能,而服务消费者关注于服务信息的获取。同时,对于服务消费者而言,为了提高服务获取的性能以及在注册中心不可用的情况下继续使用服务,一般都还会具有缓存机制
极简版远程服务调用
/**
* 极简版本远程服务调用,通过eureka服务器
*
* @return
*/
@GetMapping("/callService")
public Object callService() {
//获取服务元数据信息
List<InstanceInfo> ins = eurekaClient.getInstancesByVipAddress("provider", false);
if (ins.size() > 0) {
InstanceInfo info = ins.get(0);
//拼装访问路径
if (info.getStatus() == InstanceInfo.InstanceStatus.UP) {
String url = "http://" + info.getHostName() + ":" + info.getPort() + "/gethi";
//发起服务调用
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(url, String.class);
System.out.println(response);
}
}
return "xxoo";
}
负载均衡式远程服务调用
/**
* 通过负载均衡的方式获取服务列表,并调用接口
* @return
*/
@GetMapping("/loadBlance")
public Object loadBlance() {
//通过loadBlance拿到的服务列表,会自动把状态已经DOWN的服务筛选掉,返回的服务全部是可用的
//choose有负载均衡的策略
ServiceInstance instance = loadBalancerClient.choose("provider");
String url = "http://" + instance.getHost() + ":" + instance.getPort() + "/gethi";
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(url, String.class);
return response;
}
自我保护机制
Eureka在CAP理论当中是属于AP , 也就说当产生网络分区时,Eureka保证系统的可用性,但不保证系统里面数据的一致性
默认开启,服务器端容错的一种方式,即短时间心跳不到达仍不剔除服务列表里的节点,在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在一定时间内,没有接收到某个微服务心跳,会将某个微服务注销(90S)。但是当网络故障时,微服务与Server之间无法正常通信,上述行为就非常危险,因为微服务正常,不应该注销。
Eureka Server通过自我保护模式来解决整个问题,当Server在短时间内丢失过多客户端时,那么Server会进入自我保护模式,会保护注册表中的微服务不被注销掉。当网络故障恢复后,退出自我保护模式。
思想:宁可保留健康的和不健康的,也不盲目注销任何健康的服务。
自我保护触发
客户端每分钟续约数量小于客户端总数的85%时会触发保护机制
自我保护机制的触发条件: (当每分钟心跳次数( renewsLastMin ) 小于 numberOfRenewsPerMinThreshold 时,并且开启自动保护模式开关( eureka.server.enable-self-preservation = true ) 时,触发自我保护机制,不再自动过期租约。) numberOfRenewsPerMinThreshold = expectedNumberOfRenewsPerMin * 续租百分比( eureka.server.renewalPercentThreshold, 默认0.85 ) expectedNumberOfRenewsPerMin = 当前注册的应用实例数 x 2 为什么乘以 2: 默认情况下,注册的应用实例每半分钟续租一次,那么一分钟心跳两次,因此 x 2 。
服务实例数:10个,期望每分钟续约数:10 * 2=20,期望阈值:20*0.85=17,自我保护少于17时 触发。
关闭自我保护
eureka.server.enable-self-preservation=false
EureKa手动配置服务上下线
在client端配置:将自己真正的健康状态传播到server。
eureka:
client:
healthcheck:
enabled: true
Client端配置Actuator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
控制健康状态的Service
@Service
public class HealthStatusService implements HealthIndicator {
private Boolean status = true;
public void setStatus(boolean status) {
this.status = status;
}
public Boolean getStatus() {
return this.status;
}
@Override
public Health health() {
if (status) {
return new Health.Builder().up().build();
}
return new Health.Builder().down().build();
}
}
改变健康状态的Controller
@RestController
public class ServiceHealthController {
@Autowired
HealthStatusService healthService;
/**
* 手动控制服务上下线
*
* @param status
* @return
*/
@RequestMapping("/setServiceHealth")
public Boolean setServiceHealth(@RequestParam("status") Boolean status){
healthService.setStatus(status);
return healthService.getStatus();
}
}
健康检查Actuator
<!-- 上报节点信息 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
开启所有端点
在application.yml中加入如下配置信息
*代表所有节点都加载
#开启所有端点
management:
endpoints:
web:
exposure:
include: "*"
开启Eureka服务安全验证
- 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
- 设置账号密码
spring:
security:
user:
name: zhangyibing
password: zyb123456
- 如果服务注册报错,是默认开启了防止跨域攻击 ,需要手动关闭
Root name 'timestamp' does not match expected ('instance') for type [simple
在服务端增加配置类
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
super.configure(http);
}
}
RestTemplate整合Ribbon服务注册与服务负载均衡消费
在启动类中加入@Bean和@LoadBalanced注解
@Bean
@LoadBalanced//自动负载均衡
RestTemplate getRestTemplate(){
return new RestTemplate();
}
使用范例:
/**
* 通过负载均衡的方式获取服务列表,并自动处理URL,并远程调用服务接口
* 在初始化restTemplate的时候给@LoadBalanced的注解
* @return
*/
@GetMapping("/autoBuildUrl")
public Object autoBuildUrlLoadBalanced() {
//自动处理URL,其中provider是服务名,gethi是接口名
String url = "http://provider/gethi";
String response = restTemplate.getForObject(url, String.class);
return response;
}
远程服务调用拦截器
注意:这个拦截器在服务消费端
自定义拦截器
/**
* 特别说明:这里的拦截器拦截的是调用远程服务的请求,并不是打到consumer的请求
*/
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
System.out.println("拦截啦!!!");
/**
* 这里面可以根据业务需要做一些处理
*/
System.out.println(request.getURI());
ClientHttpResponse response = execution.execute(request, body);
System.out.println(response.getHeaders());
return response;
}
}
加入拦截器
@EnableEurekaClient
@SpringBootApplication
public class EurekaConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaConsumerApplication.class, args);
}
@Bean
@LoadBalanced//自动负载均衡
RestTemplate getRestTemplate(){
RestTemplate restTemplate = new RestTemplate();
//这里可以添加多个拦截器,只需要自定义一个新的拦截器,并在这里添加即可
restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor());
return restTemplate;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理