Netflix eureka 简单使用笔记Eureka 是 Netflix 开发的服务发现框架. 它本身是一个基于 REST 的服务, 主要用于定位中间层服务, 同时也有负载均衡和分区容灾的目的 Spring cloud 将它集成在其子项目 spring-cloud-netflix 中以实现 Spring cloud 的服务发现功能
注册中心简介 微服务架构中的通讯录, 记录服务和服务地址的映射关系
常见的注册中心
Netflix Eureka
Alibaba Nacos
HashiCorp Consul
Apache Zookeeper
CoreOS etcd
CNCF CoreDNS
CAP
Consistency(一致性)
Availability(可用性)
Partition tolerance(分区容错性)
注册中心中的角色
Server(注册中心)
Consumer(消费者)
Provider(生产者)
stateDiagram-v2
Provider --> Server: Register(注册)/Renew(心跳)/Cancel(下线)
Consumer --> Server: Get registry(拉取实例列表)
Consumer --> Provider: Remote call(远程调用)
配置 注册中心实例需要在 Spring boot 启动类添加 @EnableEurekaServer 注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 server: port: 9998 spring: application: name: eureka-server eureka: server: enable-self-preservation: false eviction-interval-timer-in-ms: 60000 instance: hostname: eureka prefer-ip-address: true instance-id: ${spring.cloud.client.ip-address}:${server.port} client: fetch-registry: true register-with-eureka: true registry-fetch-interval-seconds: 10 service-url: defaultZone: http://127.0.0.1:8761/eureka/, http://127.0.0.1:8762/eureka/
eureka.client.service.defaultZone 中如需添加多个注册中心地址, 尽量用逗号分隔, 使用数组形式时服务发现可能会不进行 basichttp 验证
eureka.client.service.defaultZone 中地址尽量以 / 结尾, 否则某些版本会报错
远程调用 实现方式
DiscoverClient 通过元数据获取服务信息
LoadBalancerClient 通过负载均衡器获取服务信息
@LoadBalanced RestTemplate远程调用时自动拉取服务实例信息, 添加@LoadBalanced注解后前两种方法将的 RestTemplate 不能使用 IOC 中的 bean
代码 Config.java
1 2 3 4 5 6 7 8 9 10 11 12 import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.client.RestTemplate;@Configuration public class Config { @Bean @LoadBalanced RestTemplate restTemplate () { return new RestTemplate (); } }
ControllerTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 import org.springframework.cloud.client.ServiceInstance;import org.springframework.cloud.client.discovery.DiscoveryClient;import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.Mapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;import java.util.List;@Controller @RestController @RequestMapping("/consumer") public class ControllerTest { private final RestTemplate restTemplate; private final DiscoveryClient discoveryClient; private final LoadBalancerClient loadBalancerClient; public ControllerTest (RestTemplate restTemplate, DiscoveryClient discoveryClient, LoadBalancerClient loadBalancerClient) { this .restTemplate = restTemplate; this .discoveryClient = discoveryClient; this .loadBalancerClient = loadBalancerClient; } @RequestMapping("/discoveryClient") public String discoveryClient () { StringBuilder stringBuilder = new StringBuilder ("<h1>discoveryClient:</h1><br>" ); List<String> serviceIds = discoveryClient.getServices(); if (serviceIds.isEmpty()) { stringBuilder.append("serviceIds: empty<br>" ); return stringBuilder.toString(); } else { stringBuilder.append("serviceIds: " ).append(serviceIds).append("<br>" ); } List<ServiceInstance> serviceInstances = discoveryClient.getInstances("eureka-provider" ); if (serviceInstances.isEmpty()) { stringBuilder.append("eureka-provider: null<br>" ); return stringBuilder.toString(); } else { stringBuilder.append("eureka-provider:" ).append(serviceInstances).append("<br>" ); } ServiceInstance serviceInstance = serviceInstances.get(0 ); stringBuilder.append("remote call result: " ); String remoteUrl = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/provider/test" ; String remoteCallResult = restTemplate.getForEntity(remoteUrl, String.class).getBody(); stringBuilder.append(remoteCallResult); return stringBuilder.toString(); } @RequestMapping("/LoadBalancerClient") public String LoadBalancerClient () { StringBuilder stringBuilder = new StringBuilder ("<h1>LoadBalancerClient:</h1><br>" ); ServiceInstance serviceInstance = loadBalancerClient.choose("eureka-provider" ); if (null == serviceInstance) { stringBuilder.append("serviceInstance: null <br>" ); return stringBuilder.toString(); } else { stringBuilder.append("serviceInstance: " ).append(serviceInstance).append("<br>" ); } stringBuilder.append("remote call result: " ); String remoteUrl = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/provider/test" ; String remoteCallResult = restTemplate.getForEntity(remoteUrl, String.class).getBody(); stringBuilder.append(remoteCallResult); return stringBuilder.toString(); } @RequestMapping("/LoadBalanced") public String LoadBalanced () { StringBuilder stringBuilder = new StringBuilder ("<h1>LoadBalanced:</h1><br>" ); ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://eureka-provider/provider/test" , String.class); return stringBuilder.append("remote call result: " ).append(responseEntity.getBody()).toString(); } }
自我保护模式 Eureka 发现实例的心跳比例在 15 min 内低于 85% 时触发. Eureka会将实例保护起来不会过期, 并发出警告. 当网络故障恢复后Eureka将解除自我保护模式 Eureka客户端具有缓存功能, 所有注册中心实例都下线时, 其他实例也可根据缓存通信 负载均衡策略会自动剔除下线实例
Eureka 常用的 API
请求名称
请求方式
HTTP地址
请求描述
注册新服务
POST
/eureka/apps/{appID}
传递JSON或者XML格式参数内容,HTTP code为204时表示成功
取消注册服务
DELETE
/eureka/apps/{appID}/{instanceID}
HTTP code为200时表示成功
发送服务心跳
PUT
/eureka/apps/{appID}/{instanceID}
HTTP code为200时表示成功
查询所有服务
GET
/eureka/apps
HTTP code为200时表示成功,返回XML/JSON数据内容
查询指定appID的服务列表
GET
/eureka/apps/{appID}
HTTP code为200时表示成功,返回XML/JSON数据内容
查询指定appID&instanceID
GET
/eureka/apps/{appID}/{instanceID}
获取指定appID以及InstanceId的服务信息,HTTP code为200时表示成功,返回XML/JSON数据内容
查询指定instanceID服务列表
GET
/eureka/apps/instances/{instanceID}
获取指定instanceID的服务列表,HTTP code为200时表示成功,返回XML/JSON数据内容
变更服务状态
PUT
/eureka/apps/{appID}/{instanceID}/status?value=DOWN
服务上线、服务下线等状态变动,HTTP code为200时表示成功
变更元数据
PUT
/eureka/apps/{appID}/{instanceID}/metadata?key=value
HTTP code为200时表示成功
查询指定IP下的服务列表
GET
/eureka/vips/{vipAddress}
HTTP code为200时表示成功
查询指定安全IP下的服务列表
GET
/eureka/svips/{svipAddress}
HTTP code为200时表示成功
心跳 实例默认每 30s 会向 Eureka 发送一次心跳 Eureka会剔除 90s 未发送心跳的实例
健康检测 步骤
导入 actuator 的 jar 包1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency >
配置application.yml 文件 1 2 3 4 5 6 7 8 management: endpoints: web: exposure: include: shutdown endpoint: shutdown: enabled: true
如需开启 shutdown 端点必须同时将 management.endpoint.shutdown.enabled 设置为 true
访问 /actuator URI即可查看该实例状况
远程停服 开启 shutdown 端点后 POST 访问 /actuator/shutdown URI 即可将服务远程关闭,返回信息:
1 2 3 { "message" : "Shutting down, bye..." }
整合Spring Security 问题 Spring boot 3.0.0 + Spring cloud 2022.0.0-RC2 版本组合会周期性触发空指针异常, 日志信息为
1 2 3 4 5 6 7 2022-12-10T16:01:33.097+08:00 WARN 5620 --- [get_127.0.0.1-0] c.n.eureka.util.batcher.TaskExecutors: Discovery WorkerThread error java.lang.NullPointerException: Cannot invoke "String.toLowerCase()" because the return value of "java.lang.Throwable.getMessage()" is null at com.netflix.eureka.cluster.ReplicationTaskProcessor.maybeReadTimeOut(ReplicationTaskProcessor.java:196) ~[eureka-core-2.0.0-rc.4.jar:2.0.0-rc.4] at com.netflix.eureka.cluster.ReplicationTaskProcessor.process(ReplicationTaskProcessor.java:95) ~[eureka-core-2.0.0-rc.4.jar:2.0.0-rc.4] at com.netflix.eureka.util.batcher.TaskExecutors$BatchWorkerRunnable.run(TaskExecutors.java:190) ~[eureka-core-2.0.0-rc.4.jar:2.0.0-rc.4] at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
版本降为 Spring boot 2.7.6 + Spring cloud 2021.0.5 后问题没有复现
配置 eureka.client.service-url.defaultZone 需改为 http://${spring.security.user.name}:${spring.security.user.password}@${Host}:${Port}/eureka 的形式
CSRF 处理 通常有两种简单的处理方式
使 CSRF 忽略 /eureka/** 的所有请求
关闭 CSRF
使 CSRF 忽略 /eureka/** 的所有请求: Config.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.web.SecurityFilterChain;@Configuration public class Config { @Bean SecurityFilterChain filterChain (HttpSecurity http) throws Exception { return http .authorizeHttpRequests() .anyRequest().authenticated() .and() .formLogin() .and() .httpBasic() .and() .csrf().ignoringRequestMatchers("/eureka/**" ) .and().build(); } }
关闭 CSRF Config.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.web.SecurityFilterChain;@Configuration public class Config { @Bean SecurityFilterChain filterChain (HttpSecurity http) throws Exception { return http .formLogin() .and().httpBasic() .and() .authorizeHttpRequests() .anyRequest().authenticated() .and() .csrf().disable().build(); } }
负载均衡 主流的负载均衡方案分为服务器负载均衡(集中式负载均衡)和客户端负载均衡(进程内负载均衡) 高版本 Spring cloud 默认使用 LoadBalance 代替 Ribbon 执行负载均衡
Ribbon Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具
问题 Spring boot 2.7.6 + Spring cloud 2021.0.5 整合 Ribbon 会出现负载均衡无法获取注册实例情况,经过调试发现, Ribbon 可用从 Eureka 获取所有实例详细, 但最终没有保存下来.
版本降为 Spring boot 2.2.4.RELEASE + Spring cloud Hoxton.SR1 后问题没有复现
依赖 高版本的 Spring cloud 默认负载均衡变为 LoadBalance ,使用 Ribbon 时需要手动导入 jar 包
1 2 3 4 5 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-ribbon</artifactId > <version > ${spring-cloud-starter-netflix-ribbon.version}</version > </dependency >
策略
策略名称
对应类名
原理
轮询
RoundRobinRule
按默认顺序每次调用按序取 provider
权重随机
WeightedResponseTimeRule
根据每个 provider 响应时间分配权重. 响应时间越长, 权重越小刚开始时为轮询策略, 同时开启计时器, 每 30 秒计算一次各个 provider 的平均响应时间, 之后按权重随机选择 provider
随机
RandomRule
随机选择 provider
最少并发
BestAvailableRule
选择请求并发数量最小的可用的 provider
重试
RetryRule
轮询策略的服务不可用时不做处理, 重试策略的服务不可用时会重新尝试连接其它节点
可用性敏感
AvailabilityFilteringRule
过滤性能差的 provider过去一段时间内始终连接失败的 provider 处于高并发状态的 provider
区域敏感性
ZoneAvoidanceRule
以区域为单位, 过滤不可用的区域 当一个区域内有服务不可用或者响应变慢时, 降低该区域中服务的权重
Ribbon 的默认负载均衡策略是 ZoneAvoidanceRule
高版本的 Spring boot 需要配置spring.cloud.loadbalancer.cache.enabled = false
或
1 2 3 4 5 <dependency > <groupId > com.github.ben-manes.caffeine</groupId > <artifactId > caffeine</artifactId > <version > ${caffeine.version}</version > </dependency >
配置 全局配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import com.netflix.loadbalancer.IRule;import com.netflix.loadbalancer.RandomRule;import org.springframework.cloud.client.loadbalancer.LoadBalanced;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.client.RestTemplate;@Configuration public class Config { @Bean IRule randomRule () { return new RandomRule (); } }
局部配置 配置文件 application.yml 中配置
1 2 3 eureka-provider-ribbon: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
eureka-provider-ribbon 为具体的实例名称, 表示对该实例的调用采取的负载均衡策略 com.netflix.loadbalancer.RoundRobinRule 是策略全类名
Ribbon 点对点直连 使用 Ribbon 点对点直连时, 需要屏蔽 Eureka (去除 Eureka 依赖)
配置 配置文件 application.yml 中配置
1 2 3 4 5 6 7 8 eureka-provider-ribbon: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule listOfServers: http://127.0.0.1:9996, http://127.0.0.1:9997 ribbon: eureka: enable: false
eureka-provider-ribbon 为具体的实例名称