服务治理主要用来实现各个微服务实例的自动化注册与发现。Spring Cloud Eureka 使用 Netflix Eureka 来实现服务注册和发现,它既包含服务端组件,也包含了客户端组件。
Eureka 服务端, 我们也称为服务注册中心。它同其他服务注册中心一样,支持高可用配置。它依托于强一致性提供良好的服务实例可用性, 可以应对多种不同的故障场景。如果 Eureka 以集群模式部署,当集群中有分片出现故障时,那么 Eureka 就转入自我保护模式。它允许在分片故障期间继续提供服务的发现和注册,当故障分片恢复运行时, 集群中的其他分片会把它们的状态再次同步回来。
Eureka 客户端,主要处理服务的注册与发现。在应用程序运行时,Eureka 客户端向注册中心注册自身提供的服务并周期性地发送心跳来更新它的服务租约。同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性地刷新服务状态。
快速入门
搭建服务注册中心(单节点)
1. 创建工程
使用 Spring Initializr 生成一个 Spring Cloud Eureka Server 的 Maven 项目
pom.xml 文件中已经引入下面的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
2. 添加 @EnableEurekaServer 注解
主类 Application 添加 @EnableEurekaServer 注解
@EnableEurekaServer
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3. 配置
在默认设置下,该服务注册中心也会将自己作为客户端来尝试注册它自己,所以我们需要禁用它的客户端注册行为,在 application.properties 中增加如下配置:
spring.application.name=eureka-server
server.port=9000
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
- eureka.client.register-with-eureka:由于该应用为注册中心,所以设置为 false,代表不向注册中心注册自己
- eureka.client.fetch-registry:由于注册中心的职责就是维护服务实例,它并不需要去检索服务,所以也设置为 false。
完成上面的配置后,启动应用并访问 http://localhost:9000。可以看到如下图所示的 Eureka 信息面板,其中
Instances currently registered with Eureka 栏是空的,说明该注册中心还没有注册任何服务。
注册服务提供者
1. 创建工程
使用 Spring Initializr 生成一个 Maven 项目,注册到 Eureka 服务注册中心。
pom.xml 文件中已经引入下面的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2. 新建接口
新建 /hello 接口,通过注入 DiscoveryClient 对象,在日志中打印出服务相关的内容
@RestController
public class HelloController {
private static final Logger LOGGER = LoggerFactory.getLogger(HelloController.class);
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/hello")
public String hello() {
LOGGER.info("**********************************");
discoveryClient.getInstances("eureka-provider").forEach(instance -> {
LOGGER.info("serviceId: " + instance.getServiceId() + ", host: " + instance.getHost() + ", port: " + instance.getPort());
});
LOGGER.info("**********************************");
return "eureka provider";
}
}
3. 添加 @EnableDiscoveryClient 注解
在主类 Application 中通过添加 @EnableDiscoveryClient 注解,自动创建 DiscoveryClient 接口的实例。
@EnableDiscoveryClient
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4. 配置
在 application.properties 中增加如下配置:
spring.application.name=eureka-provider
server.port=8000
eureka.client.serviceUrl.defaultZone=http://localhost:9000/eureka/
eureka.client.serviceUrl.defaultZone:用来指定服务注册中心的地址。
完成上面的配置后,启动应用。访问 Eureka 信息面板,在 Instances currently registered with Eureka 栏中看到服务的注册信息。
高可用注册中心
Eureka Server 的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心。
上面已经搭建了一个单节点的服务注册中心,现在再搭建一个 Eureka 服务应用,同时修改这两个服务的配置,实现相互注册。
1. 配置
修改 Eureka Server1 的配置,serviceUrl 指向 Server2:
spring.application.name=eureka-server
server.port=9000
eureka.instance.hostname=127.0.0.1
eureka.instance.prefer-ip-address=false
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.client.serviceUrl.defaultZone=http://localhost:9001/eureka/
修改 Eureka Server2 的配置,serviceUrl 指向 Server1:
spring.application.name=eureka-server
server.port=9001
eureka.instance.hostname=localhost
eureka.instance.prefer-ip-address=false
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:9000/eureka/
启动两个服务后,访问 Eureka Server1 http://localhost:9000/,可以看到 Instances currently registered with Eureka 中有两个服务,registered-replicas 中已经有 Eureka Server2 服务了。同样的,访问 Eureka Server2 http://localhost:9001/,registered-replicas 中已经有 Eureka Server1 服务了。
Eureka Server1 http://localhost:9000/:
Server2 http://localhost:9001/:
在设置了多节点的服务注册中心之后,服务提供者还需要修改 eureka.client.serviceUrl.defaultZone 配置,才能注册到 Eureka Server 集群中。
spring.application.name=eureka-provider
server.port=8000
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:9000/eureka/,http://localhost:9001/eureka/
启动该服务,通过访问 http://localhost:9000/ or http://localhost:9001/,可以观察到 eureka-provider 服务已经被注册到服务注册中心上了。
若此时断开 Eureka Server1,由于 eureka-provider 同时也向 Eureka Server2 注册了,因此在 Eureka Server2 上的其他服务依然能访问到 eureka-provider 服务,从而实现了服务注册中心的高可用。
2. 注意事项
1)Eureka 互相注册要求各个 Eureka 实例的 eureka.instance.hostname 不同,如果相同,则会被 Eureka 标记为 unavailable-replicas。所以 Eureka Server1 的 eureka.instance.hostname 设置为 127.0.0.1,Eureka Server2的 eureka.instance.hostname 设置为 localhost。
2)如果设置 eureka.instance.hostname 的值是 localhost 或者 127.0.0.1,则 eureka.instance.prefer-ip-address 一定要设置为 false。
3)可以设置 eureka.instance.hostname 的值为当前机器的 ip,Eureka Server1 和 Eureka Server2 的配置如下所示
Eureka Server1:
spring.application.name=eureka-server
server.port=9000
eureka.instance.hostname=${spring.cloud.client.ip-address}
eureka.instance.prefer-ip-address=true
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.client.serviceUrl.defaultZone=http://localhost:9001/eureka/
Eureka Server2:
spring.application.name=eureka-server
server.port=9001
eureka.instance.hostname=localhost
eureka.instance.prefer-ip-address=false
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.client.serviceUrl.defaultZone=http://10.8.1.102:9000/eureka/
Eureka Server2 中的 eureka.client.serviceUrl.defaultZone 的 ip 地址可以参考 Eureka 信息面板中的 Instance Info 内容。
注册服务消费者
构建一个服务消费者,主要完成两个目标,发现服务以及消费服务。其中服务发现的任务由 Eureka 的客户端完成,而服务消费的任务由 Ribbon 完成。Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡器。
1. 启动两个服务提供者
为了实现 Ribbon 的客户单负载均衡功能,启动两个不同端口的 Provider Server。启动成功后,在 Eureka 信息面板中可以看到名为 eureka-provider 的服务中出现了两个实例。
2. 创建工程
使用 Spring Initializr 生成一个 Maven 项目,注册到 Eureka 服务注册中心。
pom.xml 文件中已经引入下面的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
3. 修改主类
主类 Application 通过添加 @EnableDiscoveryClient 注解让该应用注册为 Eureka 客户端应用,已获得服务发现的能力。同时,在改主类中创建 RestTemplate 的 Spring Bean 实例,并通过 @LoadBalanced 注解开启客户端负载均衡。
@EnableDiscoveryClient
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
}
4. 新建接口
新建 /helloConsumer 接口,通过注入的 restTemplate 对象,远程调用服务提供者的接口。
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/helloConsumer")
public String helloConsumer() {
return restTemplate.getForEntity("http://eureka-provider/hello", String.class).getBody();
}
}
5. 配置
application.properties 配置内容和服务提供者差不多,只需要修改一下应用名称和端口号。
spring.application.name=传
server.port=7000
logging.level.root=info
#打开shutdown
management.endpoint.shutdown.enabled=true
#支持HTTP请求
management.endpoints.web.exposure.include=*
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:9000/eureka/,http://localhost:9001/eureka/
启动 eureka-consumer 应用,在 Eureka 信息面板上可以看到 eureka-consumer 服务。
通过向 http://localhost:7000/helloConsumer 发送请求,成功返回“eureka provider”,多次调用,观察服务提供者的后代日志,看到两个控制台会交替打印日志。
配置详解
在实际使用 Spring Cloud Eureka 的过程中,我们所做的配置内容几乎都是对 Eureka 客户端配置进行的操作,所以了解这部分的配置内容,对于用好 Eureka 非常有帮助。
Eureka 客户端的配置主要分为以下两个方面。
- 服务注册相关的配置信息,包括服务注册中心的地址、服务获取的间隔时间、可用区域等。
- 服务实例相关的配置信息,包括服务实例的名称、IP 地址、端口号、健康检查路径等。
Eureka 服务端大多数情况下,不需要修改它的配置信息。可以通过查看 org.springframework.cloud.netflix.eureka.server.EurekaServerConfigBean 类。
服务注册配置
关于服务注册类的配置信息,可以通过查看 org.springframework.cloud.netflix.eureka.EurekaClientConfigBean 类的源码。这些配置信息都以 eureka.client 为前缀。
指定注册中心
通过 eureka.client.serviceUrl 参数指定注册中心,该参数在源码中的定义如下所示,它的配置值存储在 HashMap 类型中,并且设置有一组默认值,默认值的 key 为 defaultZone、value 为 http://localhost:8761/eureka/。
private Map<String, String> serviceUrl = new HashMap<>();
{
this.serviceUrl.put(DEFAULT_ZONE, DEFAULT_URL);
}
public static final String DEFAULT_URL = "http://localhost:8761" + DEFAULT_PREFIX + "/";
public static final String DEFAULT_ZONE = "defaultZone";
由于之前实现的服务注册中心使用了9000端口,所以我们做了如下配置,来将应用注册到对用的 Eureka 服务端中。
eureka.client.serviceUrl.defaultZone=http://localhost:9000/eureka
当构建了高可用的服务注册中心集群时,我们可以为参数的 value 值配置多个注册中心的地址(通过逗号分隔)。
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:9000/eureka/,http://localhost:9001/eureka/
其他配置
整理了一些常用配置参数,这些参数均以 eureka.client 为前缀。
参数名 | 说明 | 默认值 |
---|---|---|
enabled | 启用 Eureka 客户端 | true |
registryFetchIntervalSeconds | 从 Eureka 服务端获取注册信息的间隔时间,单位为秒 | 30 |
instanceInfoReplicationIntervalSeconds | 更新实例信息的变化到 Eureka 服务端的间隔时间,单位为秒 | 30 |
initialInstanceInfoReplicationIntervalSeconds | 初始化实例信息到 Eureka 服务端的间隔时间,单位为秒 | 40 |
eurekaServiceUrlPollIntervalSeconds | 轮询Eureka服务端地址更改的间隔时间,单位为秒。当我们与Spring Cloud Config配合,动态刷新Eureka的serviceURL地址时需要关注该参数 | 300 |
eurekaServerReadTimeoutSeconds | 读取 Eureka Server 信息的超时时间,单位为秒 | 8 |
eurekaServerConnectTimeoutSeconds | 连接 Eureka Server 的超时时间,单位为秒 | 5 |
eurekaServerTotalConnections | 从 Eureka 客户端到所有 Eureka 服务端的连接总数 | 200 |
eurekaServerTotalConnectionsPerHost | 从 Eureka 客户端到每个 Eureka 服务端主机的连接总数 | 50 |
eurekaConnectionIdleTimeoutSeconds | Eureka 服务端连接的空闲关闭时间,单位为秒 | 30 |
heartbeatExecutorThreadPoolSize | 心跳连接池的初始化线程数 | 2 |
heartbeatExecutorExponentialBackOffBound | 心跳超时重试延迟时间的最大乘数值 | 10 |
cacheRefreshExecutorExponentialBackOffBound | 缓存刷新重试延迟事件的最大乘数值 | 10 |
cacheRefreshExecutorThreadPoolSize | 缓存刷新线程延迟时间的初始化线程数 | 2 |
useDnsForFetchingServiceUrls | 使用 DNS 来获取 Eureka 服务端的 serviceUrl | false |
registerWithEureka | 是否要将自身的实例信息注册到 Eureka 服务端 | true |
preferSameZoneEureka | 是否偏好使用处于相同 Zone 的 Eureka 服务端 | true |
filterOnlyUpInstances | 获取实例时是否过滤,仅保留 UP 状态的实例 | true |
fetchRegistry | 是否从 Eureka 服务端获取注册信息 | true |
服务实例类配置
关于服务实例类的配置信息,我们可以通过查看 org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean 的源码来获取详细内容,这些信息都以 eureka.instance 为前缀。
元数据
元数据是 Eureka 客户端在向服务注册中心发送注册请求是,用来描述自身服务信息的对象,其中包含了一些标准化的元数据,比如服务名称、实例名称、实例 IP、实例端口等用于服务治理的重要信息;以及一些用于负载均衡策略或是其他特殊用途的自定义元数据信息。
在使用 Spring Cloud Eureka 的时候,所有的配置信息都是通过 org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean 进行加载,但在真正服务注册的时候会把数据存储在 com.netflix.appinfo.InstanceInfo 对象发送给 Eureka 服务端。
其中,Map
我们可以通过 eureka.instance.
eureka.instance.metadataMap.zone=shanghai
实例名配置
实例名,即 InstanceInfo 中的 instanceId 参数, 它是区分同一服务中不同实例的唯一标识。在 Netflix Eureka 的原生实现中,实例名采用主机名作为默认值,这样的设置使得在同一主机上无法启动多个相同的服务实例。所以,Spring Cloud Eureka 的配置中 ,针对同一主机中启动多实例的情况,对实例名的默认命名做出了更为合理的扩展。默认规则如下:
${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}
对于实例名的命名规则,我们可以通过 eureka.instance.instanceId 参数来进行配置。如果设置 server.port=0 或者使用随机数 server.port=${random.int[10000,19999]} 来让 Tomcat 启动的时候采用随即端口。我们这个时候会发现注册到 Eureka Server 的实例名都相同的,这会使得只有一个服务实例能够正常提供服务。对于这个问题,我们可以通过设置实例名规则来轻松解决:
eureka.instance.instanceId=${spring.application.name}:${randmo.int}
虽然可以采用上面的方式设置实例名,但是不建议这么使用,server.port 还是建议手动指定。
端点配置
在 InstanceInfo 中,可以看到一些 URL 的配置信息,比如 homePageUrl、statusPageUrl、healthCheckUrl,它们分别代表了应用主页的 URL、状态页的 URL、健康检查的 URL。其中,状态页和监控检查的 URL 在 Spring Cloud Eureka 中默认使用了 spring-boot-actuator 模块提供的 /info 端点和 /health 端点。
为了服务的正常运作,必须确认 Eureka 客户端的 /health 端点在发送元数据的时候,是一个能够被注册中心访问的地址,否则服务注册中心不会根据应用的健康状态来更改状态(仅当开启了 healthcheck 功能时,以该端点信息作为健康检查标准)。而 /info 端点如果不正确的话,会导致在 Eureka 面板单击服务实例时,无法访问到服务实例提供的信息接口。
在一些特殊的情况下,比如,为应用设置了 context-path,这时,所有 spring-boot-actuator 模块的监控端点都会增加一个前缀。所以,我们就需要做类似如下的配置,为 /info 和 /health 端点也加上类似的前缀:
management.context-path=/hello
eureka.instance.statusPageUrlPath=${management.context-path}/info
eureka.instance.healthCheckUrlPath=${management.context-path}/health
另外,有时候为了安全考虑,也有可能会修改 /info 和 /health 端点的原始路径。这个时候,我们也需要做一些特殊配置,例如:
endpoints.info.pah=/appinfo
endpoints.health.path=/cheakHealth
eureka.instance.statusPageUrlPath=/${endpoints.info.pah}
eureka.instance.healthCheckUrlPath=/${endpoints.health.path}
上面实例使用的是相对路径。
由于 Eureka 的服务注册中心默认会以 HTTP 的方式来访问和暴露这些端点,因此当客户端应用以 HTTPS 的方式来暴露服务和监控端点时,相对路径的配置方式就无法满足要求了。所以,Spring Cloud Eureka 还提供了绝对路径的配置参数,例如:
eureka.instance.homePageUrl=https://${eureka.instance.homename}
eureka.instance.statusPageUrlPath=https://${eureka.instance.homename}/info
eureka.instance.healthCheckUrlPath=https://${eureka.instance.homename}/health
健康检测
默认情况下,Eureka 中各个服务实例的健康检查并不是通过 spring-boot-actuator 模块的 /health 端点来实现的,而是依靠客户端心跳的方式保持服务实例的存活,在 Eureka 的服务续约与剔除机制下,客户端的监控状态从注册到注册中心开始都会处于 UP 状态,除非心跳终止一段时间之后,服务注册中心将其剔除。默认的心跳实现方式可以有效检查客户端进程是否正常运作,但却无法保证客户端应用能够正常提供服务。由于大多数的应用都会有一些其他的外部资源依赖,比如数据库。缓存、消息代理等,如果应用与这些外部资源无法联通的时候,实际上已经不能提供正常的对外服务了,但此时心跳依然正常,所以它还是会被服务消费者调用,而这样的调用实际上并不能获得预期的结果。
在 Spring Cloud Eureka 中,我们可以通过简单的配置,把 Eureka 客户端的监控检查交给 spring-boot-actuator 模块的 /health 端点,以实现更加全面的健康状态维护。
详细步骤如下:
- 在 pom.xml 中加入 spring-boot-starter-actuator 模块的依赖。
- 在 application.properties 中增加参数配置 eureka.client.healthcheck.enabled=true。
其他配置
这些配置均以 eureka.instance 为前缀
参数名 | 说明 | 默认值 |
---|---|---|
preferIpAddress | 是否有限使用 IP 地址作为主机名的标识 | false |
leaseRenewalIntervalInSeconds | Eureka 客户端向服务端发送心跳的时间间隔,单位为妙 | 30 |
leaseExpirationDurationInSeconds | Eureka 服务端在收到最后一次心跳之后等待的时间上限,单位为秒。超过该时间之后服务端会将该服务实例从服务清单中剔除,从而禁止服务调用请求被发送到该实例上 | 09 |
nonSecurePort | 非安全的通信端口号 | 80 |
securePort | 安全的通信端口号 | 443 |
nonSecurePortEnabled | 是否启用非安全的通信端口号 | true |
securePortEnabled | 是否启用安全的通信端口号 | |
appname | 服务名,默认取 spring.application.name 的配置值,如果没有则为 unknown | |
hostname | 主机名,不配置的时候将根据操作系统的主机名来获取 |
跨平台支持
Eureka 的通信机制使用了 HTTP 的 REST 接口实现,这也是 Eureka 同其他服务注册工具的一个关键不同。由于 HTTP 的平台无关性,虽然 Eureka Server 通过 Java 实现,但是在其他微服务应用并不限于使用 Java 来进行开发。
目前除了 Eureka 的 Java 客户端之外,还有很多其他语言平台对其的支持,比如 eureka-js-client,python-eureka 等。
通信协议
默认,Eureka 使用 Jersey 和 XStream 配合 JSON 作为 Server 与 Client 之间的通信协议,也可以选择实现自己的协议来代替。
Jersey 是 JAX-RS 的参考实现,它包含三个主要部分。
- 核心服务器(Core Server):通过提供 JSR 311 中标准化的注释和 API 标准化,可以用直观的方式开发 RESTful Web 服务。
- 核心客户端(Core Client):Jersey 客户端 API 帮助你与 REST 服务轻松通信。
- 集成(Integration):Jersey 还提供乐意轻松集成 Spring、Guice、Apache Abdera 的库。
XStream 是用来将对象序列化成 XML(JSON)或反序列化为对象的一个 Java 类库。
JAX-RS 是将 Java EE 6 中引入的一种新技术。JAX-RS 即 Java API for RESTful Web Services,是一个 Java 编程语言的应用程序接口,支持按照表述性状态转移(REST)架构风格创建 Web 服务。JAX-RS 使用了 Java SE5 引入的 Java 标注来简化 Web 服务的客户端和服务端的开发和部署。
- @Path,标注资源类或者方法的相对路径。
- @GET、@PUT、@POST、@DELETE,标注方法是 HTTP 请求的类型。
- @Produces,标注返回的 MIME 媒体类型。
- @Consumes,标注可接受请求的 MIME 媒体类型。
- @PathParam、@QueryParam、@HeaderParam、@CookieParam、@MatrixParam、@FormParam, 标注方法的参数来自 HTTP 请求的不同位置,例如,@PathParam 来自 URL 的路径,@QueryParam 来自 URL 的参数查询,@HeaderParam 来自 HTTP 请求的头信息,@CookieParam 来自 HTTP 请求的 Cookie。
问题
问题一:本地调式服务注册中心的时候,基本上都会碰到这样一个问题,在服务注册中心的信息面板中出现红色警告信息。
解决方式有三种:
- 关闭自我保护模式(eureka.server.enable-self-preservation 设为 false),不推荐。
- 降低 renewalPercentThreshold 的比例(eureka.server.renewal-percent-threshold 设置为0.5以下,比如0.49),不推荐。
- 部署多个 Eureka Server 并开启其客户端行为(eureka.client.register-with-eureka 不要设为 false,默认为 true),推荐。
具体可以参考 Spring Cloud Eureka 自我保护机制
问题二:如何实现服务优雅的下线?
直接 kill 进程,服务注册中心很可能会进入自我保护模式,就是上面的情况。因为直接杀死客户端进程,客户端没有通知服务注册中心服务下线的操作,每分钟客户端的心跳少了,低于阈值,服务注册中心就进入自我保护模式了。
实现服务优雅下线:application.properties 配置开启 shutdown,同时支持 HTTP 请求。
POST 请求 http://localhost:8000/actuator/shutdown,就能实现服务的优雅下线。
客户端服务下线的过程中,会通知服务注册中心该客户端服务下线了。
参考
《Spring Cloud 微服务实战》
Eurake的自我保护机制
Spring Cloud Eureka 自我保护机制