服务治理:Spring cloud eureka
spring cloud eureka是spring cloud netfix微服务套件中的一部分,它基于netfix eureka做了二次封装,主要负责完成服务架构中的服务治理功能。核心内容包括:
- 构建服务注册中心
- 服务注册和服务发现
- eureka的基础架构
- eureka的服务治理机制
- eureka的配置
服务治理
服务治理可以说是微服务架构中最为核心和基础的模块,它主要用来实现各个微服务实例的自动化注册和发现。
在最初开始构建微服务系统的时候可能服务并不多,我们可以通过做一些静态配置来完成服务的调用。但随着业务的发展,我们的静态配置就会变得越来越难以维护。因此产生了大量服务治理的框架和产品,这些框架和产品的实现都是围绕服务注册和服务发现机制来完成服务应用实例的自动化管理。
服务注册
在服务治理中,通常都会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。
服务注册中心还需要以心跳的方式去监测清单里的服务是否可用,若不可用,需要从清单中剔除,达到排查故障服务的效果。
服务发现
服务间的调用不再通过具体的实例地址来实现,而是通过服务名发起请求调用发现。所以服务调用方需要向注册中心咨询服务,并获得所有服务的实例清单,以实现具体事务实例的访问。比如服务C向调用服务A,那么服务C就需要向注册中心发起咨询服务请求,服务注册中心就会将服务A的位置清单返回给服务C,服务C发起调用的时候,便从清单里以某种轮询策略取出一个位置来进行服务调用。
以上只是举例一种简单的服务治理逻辑,实际的框架为了性能等因素,不会采用每次都向注册中心获取服务的方式,并且不同的应用场景在缓存和服务剔除等机制上也会有一些不同的实现策略。
Netflix Eureka
Spring Cloud Eureka,使用Netfix eureka来实现服务注册于发现,它既包含了服务端组件,也包含了客户端组件,并且服务端与客户端均采用Java编写,所以Eureka主要适用于通过Java实现的分布式系统,或者与JVM兼容语自构建的系统。但是,由于Eurcka 服务端的服务治理机制提了完备的RESTful API,所以它也支持将非Java语自构建的微服务应用纳入Eureka的服务治理体系中来。只是在使用其他语言平台的时候,需要自己来实现Eureka的客户端程序,不过庆幸的是,在目前儿个较为流行的开发平台上,都已经有了一些针对Eureka 注册中心的客户端实现框架,比.NET平台的Steeltoe、 Node.js 的cureka-js-client等.
Eureka服务端,我们也称为服务注册中心。它同其他服务注册中心一样,支持高可用配置。它依托于强一致性提良好的服务实例可用性,可以应对多种不同的故障场景。如果Eureka以集群模式部署,当集群中有分片出现故障时,那么Eureka就转入自我保护模式,它允许在分片故障期间维续提供服务的发现和注册,当故障分片恢复运行时,集群中的其他分片会把它们的状态再次同步回来。
Eureka客户端,主要处理服务的注册与发现。客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中,在应用程序运行时,Eureka客户端向注册中心注册自身提供的服务并周期性地发送心跳来更新它的服务租约。同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性地刷新服务状态。
搭建服务注册中心
引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
配置@EnableEurekaServer注解
@EnableEurekaServer
@SpringBootApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
}
配置properties配置文件
#服务名
spring.application.name=eureka-server
#服务端口号
server.port=1111
#服务本地主机名称参数
eureka.instance.hostname=localhost
#设置为false,代表不向注册中心注册自己
eureka.client.register-with-eureka=false
#由于注册中心的职责就是维护服务实例,它并不需要去检索服务,使用设置为false
eureka.client.fetch-registry=false
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
访问信息面板
访问http://localhost:1111/。其中Instances currently registered with Eureka为空,说明注册中心还没有注册任何服务。
注册服务提供者
引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
注入DiscoveryClient对象
往HelloController注入DiscoveryClient对象,在日志中打印服务的具体内容
@RestController
public class HelloController {
private final Logger logger = Logger.getLogger(getClass());
@Autowired
private DiscoveryClient client;
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String hello() throws Exception {
ServiceInstance instance = client.getLocalServiceInstance();
logger.info("/hello, host:" + instance.getHost() + ", service_id:" + instance.getServiceId());
return "Hello World";
}
}
配置应用主类
在主类上增加@EnableDiscoveryClient注解,激活EurekaDiscoveryClient实现(自动化配置,创建DiscoveryClient接口针对Eurek端的EurekaDiscoveryClient实例),才能实现上述Controler中对服务信息的输出。
@EnableDiscoveryClient
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
配置properties配置文件
spring.application.name=hello-service
server.port=8001
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
测试注册是否成功
1、启动spring-boot服务时:
控制台输出了
2、访问Eureka信息面板时(http://localhost:1111/)
3、访问http://localhost:8001/hello时
控制台输出了
高可用注册中心
eureka server的高可用实际上就是讲自己作为服务向其他服务注册中心注册自己,这样就可以形成一组相互注册的服务注册中心,以实现服务清单的相互同步,达到高可用的效果。下面我们来尝试搭建高可用服务注册中心的集群,构建一个双节点的服务注册中心集群。
创建两种环境的properties配置文件
创建part1环境的配置文件,并将serviceUrl指向part2
spring.application.name=eureka-server-part1
server.port=1112
eureka.instance.hostname=part1
eureka.client.serviceUrl.defaultZone=http://part2:1113/eureka/
创建part2环境的配置文件,并将serviceUrl指向part1
spring.application.name=eureka-server-part2
server.port=1113
eureka.instance.hostname=part2
eureka.client.serviceUrl.defaultZone=http://part1:1112/eureka/
添加part1、part2的地址转换
在C:\Windows\System32\drivers\etc\hosts文件中增加以下内容,让part1、part2指向本地主机地址:
127.0.0.1 part1
127.0.0.1 part2
分别启动part1,part2下的应用
在项目jar包的所在目录中,分别启动两条cmd命令:
java -jar eureka-server-1.0.0.jar --spring.profiles.active=part1
java -jar eureka-server-1.0.0.jar --spring.profiles.active=part2
查看多节点注册中心
访问http://localhost:1112/的第一个注册中心时,我们发现part2为其DS Replicas(副本)
访问http://localhost:1113/的第二个注册中心时,我们发现part1为其DS Replicas(副本)
将服务注册到eureka server集群中
修改hello-service的配置文件,主要对eureka.client.serviceUrl.defaultZone做了修改,将注册中心指向了之前我们搭建的part1,和part2。
eureka.client.serviceUrl.defaultZone=http://part1:1112/eureka//,http://part2:1113/eureka/
然后启动hello-service应用。
再次查看多节点注册中心
访问http://localhost:1112/和http://localhost:1113/的两个个注册中心时,我们发现hello-service服务同时注册到了part1和part2。
若断开part1注册中心,在part2上的服务也能访问到hello-service。从而实现了服务注册中心的高可用。
服务发现与消费
通过上面的内容介绍和实践,我们已经搭建了微服务架构中的核心组件——服务注册中心。同时,我们也配置了hello-server服务。现在我们已经有了服务注册中心和服务提供者。
接下来,我们尝试构建一个服务消费者,服务发现的任务由eureka的客户端完成,而消费服务的任务由Ribbon完成。ribbon是一个基于http和tcp协议的客户端负载均衡器,它可以在通过客户端中配置的ribbonServerList服务端列表去轮询访问以达到负载均衡的作用。
当Ribbon与Eureka联合使用时,Ribbon的服务实例清单RibbonServerList被DiscoveryEnabledNIWsServerList重写,扩展成从Eureka注册中心中获取服务端列表。同时它也会用NIWSDiscoveryPing来取代IPing,它将职责委托给Eureka 来确定服务端是否已经启动。在本章中,我们对Ribbon不做详细的介绍,读者只需要理解它在Eureka服务发现的基础上,实现了一套对服务实例的选择策略,从而实现对服务的消费。
导入依赖
创建ribbon-consumer模块。添加一下依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
配置应用主类
- 增加@EnableDiscoveryClient注解,获得服务发现能力
- 注入RestTemplate类并添加@LoadBalanced注解,开启客户端负载均衡
@EnableDiscoveryClient
@SpringBootApplication
public class Application {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
配置properties配置文件
这里配置的eureka服务注册中心需要与服务提供者的注册中心一致。
spring.application.name=ribbon-consumer
server.port=9000
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
创建调用服务的controller
创建ConsumerController类并实现/ribbon-consumer接口。在该接口中,通过在上面创建的RestTemplate 来实现对HELLO-SERVICE 服务提供的/hello接口进行调用。可以看到这里访问的地址是服务名HELLO-SERVICE,而不是一个具体的地址,在服务治理框架中,这是一个非常重要的特性。
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@RequestMapping(value = "ribbon-consumer",method = RequestMethod.GET)
public String helloConsumer(){
return restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
}
}
测试
查看注册中心面板http://localhost:1111/,服务提供者与消费者都已经注册在注册中心里。
通过访问http://localhost:9000/ribbon-consumer,响应成功
测试刷新几次http://localhost:9000/ribbon-consumer地址,可以发现两个HELLO-SERVICE交替打印以下内容:
Eureka详解
基础架构
整个Eurcka 服务治理基础架构的三个核心要素:
- 服务注册中心
- 服务提供者
- 服务消费者
服务治理机制
准备工作
服务注册中心-1”和“服务注册中心-2”,他们相互注册组成高可用集群。
java -jar eureka-server-1.0.0.jar --spring.profiles.active=part1
java -jar eureka-server-1.0.0.jar --spring.profiles.active=part2
“服务提供者”启动了两个实例,一个注册到“服务注册中心-1”,另外一个注册到“服务注册中心-2”上。
java -jar spring-boot-1.0.0.jar --spring.profiles.active=server1
java -jar spring-boot-1.0.0.jar --spring.profiles.active=server2
还有两个“服务消费者”,他们也都分别指向了一个注册中心。
java -jar ribbon-consumer-0.0.1-SNAPSHOT.jar --spring.profiles.active=consumer1
java -jar ribbon-consumer-0.0.1-SNAPSHOT.jar --spring.profiles.active=consumer2
最终效果如下:
服务提供者
服务注册
服务提供者在启动的时候会通过发送REST请求的方式将自己注册到Eureka Server上,同时带上了自身服务的一些元数据信息。
Eureka Server 接收到这个REST请求之后,将元数据信息存储在一个双层结构 Map中,其中第一层的 key是服务名,第二层的key是具体服务的实例名。
eureka.client.register-with-eureka=true:启动注册操作,该值默认为true.若设置为false将不会启动注册操作。
服务同步
如架构图中所示,这里的两个服务提供者分别注册到了两个不同的服务注册中心上,也就是说,它们的信息分别被两个服务注册中心所维护。此时,由于服务注册中心之间因互相注册为服务,当服务提供者发送注册请求到一个服务注册中心时, 它会将该请求转发给集群相连的其他注册中心,从而实现注册中心之间的服务同步。通过服务同步,两个服务提供者的信息就可以在这两台注册中心中的任意一台获得到。
服务续约
与心跳检测一个意思。
eureka.instance.lease-renewal-interval-in-seconds=30:用于定义服务续约任务的调用间隔时间,默认30秒
eureka.instance.lease-expiration-duration-in-seconds=90:用于定义服务失效的时间,默认为90秒
服务消费者
获取服务
到这里,在服务注册中心已经注册了一个服务,并且该服务有两个实例。当我们启动服务消费者是,它会发一个REST请求给注册中心,来获取上面注册的服务清单。为了性能考虑,Eureka Server会维护一份只读的服务清单来返回给客户端,同时该缓存清单会每隔30s更新一次
获取服务是消费者的基础,所以要确保eureka-client-fetch-registery=true参数有没有被修改成false,该值默认为true。若想修改缓存清单的更新时间,可以通过eureka.client.registry-fetch-interval-seconds参数来进行修改,该值默认为30s
服务调用
服务消费者在获取服务清单后,通过服务名可以获得具体提供服务的实例名和该示例的元数据信息。因为有这些服务示例的详细信息,所以客户端可以根据自己的需要决定具体需要调用的实例,在Rinbbon中默认采用轮询的方式进行调用,从而实现客户端的负载均衡。
对于访问实例的选择,Eureka中有Region和Zone的概念,一个Regionzhong keyi baohan duoge Zone.每个服务客户端需要被注册到一个Zone中,所以每个客户端对应一个Region和一个Zone.在进行服务调用的时候,优先访问同一个Zone中的服务提供方,若访问不到就访问其他的Zone。
服务下线
在系统运行过程中必然会棉铃关闭或重启服务的某个实例的情况,在服务关闭期间我们自然不希望客户端会继续电泳关闭了的实例,所以在客户端程序中,当服务实例进行正常的关闭操作时,他会出发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”,服务端会在接收到请求之后将该服务状态置为下线(DOWN),并把该下线的时间传播出去。
服务注册中心
失效剔除
当一些外部原因,如内存溢出、网络故障等导致服务实例非正常下线,而服务注册中心并未收到“服务下线”的请求。为了从服务列表中将这些无法提供服务的实例剔除,Eureka Server在启动的时候会创建一个定时任务,默认每隔一段时间(默认60s)将当前清单中超时(默认90s)没有续约的服务剔除出去。
自我保护
eureka.server.enable-self-preservation=false:关闭保护机制(确保注册中心可以将不可用的实例正确剔除)
当我们在本地调试基于 Eureka 的程序时,基本上都会在服务注册中心的信息面板上出现类似下面的红色警告信息:
实际上,该警告是触发了Eureka Server的自我保护机制。之前介绍过,服务注册到Eureka Server之后,会维护一个心跳连接,告诉Eureka Server 自己还活着。Eureka Server 在运行期间,会统计心跳失败的比例在15分钟之内低于85%,如果出现低于的情况,Eureka Server 会将当前的实例信息保护起来,让这些实例不会过期,尽可能保护这些注册信息。但是,在保护期间内实例若出现问题,那么客户端很容易拿到实际已经不存在的服务实例,会出现调用失败的情况,所以客户端必须要有容错机制,比如可以使用请求重试、断路器等机制。
由于在本地调试很容易触发注册中心的保护机制,使得注册中心维护的服务实例不那么准确。可以在本地进行开发时,使用eureka-server.enable-self-preservation=false 参数来关闭保护机制,确保注册中心将不可用的实例正确剔除。
源码分析
配置详解
在实际使用Spring Cloud Eureka的过程中,我们所做的配置内容几乎都是对Eureka客户端配置进行操作,所以了解这部分配置内容,对于用好Eureka非常有用。
Eureka客户端的配置主要分下面两个方面。
- 服务注册相关配置信息,包括服务注册中心的地址、服务获取的间隔时间、可用区域等。
- 服务实例相关配置信息,包括服务实例名称、IP地址、端口号、健康检查路径等
关于服务注册的配置信息,我们看看org.springframework.cloud.netflix.eureka.EurekaClientConfigBean的源码可获得比官方文档更为详尽的说明,这些配置内容都以Eureka.client为前缀。
服务注册类配置
指定注册中心
通过在配置文件中配置eureka.client.serviceUrl参数来实现。
该参数定义如下,它的配置值存储在HashMap类型中,并且设置了一组默认值,默认值的key为defaultZone、value为http://localhost:8761/eureka/
当构建了高可用的服务注册中心集群时,我们可以为参数的value值配置多个注册中心的地址(通过逗号分隔)。
比如下面例子:
eureka.client.serviceUrl.defaultZone=http://part1:1112/eureka//,http://part2:1113/eureka/
另外,为了服务注册中心的安全考虑,很多时候我们都会为服务注册中心加入安全校验。这个时候,在配置serviceUrl时,需要在value值的URL中加入相应的安全校验信息,比如:
eureka.client.serviceUrl.defaultZone=http://<username>:<password>@localhost:1112/eureka
其中为安全校验信息的用户名,为该用户的密码。
其他配置
这些配置都是以eureka.client为前缀。
服务实例类配置
关于服务实例类的配置信息,可以通过org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean的源码来获取详细内容,这些配置信息都是以eureka.instance为前缀。
元数据
那么什么是服务实例的元数据呢?它是Eureka客户端向服务注册中心发送请求时,用来描述自身服务信息的对象,其中包含了一些标准的元数据;以及一些用于负载均衡策略或是其他用途的自定义元数据信息。
可以通过eureka.instance.=的格式对标准定义元数据直接进行配置,其中就是EurekaInstanceConfigBean对象中的成员变量名。而对于自定义的元数据,可以通过eureka.instance.metadataMap.=的格式进行配置,比如:
#自定义的元数据
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来启动,但是这样还是比较麻烦。实际上,我们可以直接通过设置server.port=0或者使用随机数server.port=${random.int[10000,19999]}来让Tomcat启动时候采用随机端口。但是这个时候我们发现注册到Eureka Serve的实例名都是相同的,这会使得只有一个服务实例能够正常提供服务。对于这个问题,我们就可以通过设置实例名规则来轻松解决:
eureka.instance.instanceId=${spring.application.name}:${random.int}
通过上面的配置,利用应用名加随机数的方式来区分不同的实例,从而实现在同一主机上,不指定端口就能启动多个实例的效果。
端点配置
在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端点,以实现更加全面的健康状态维护。
详细步骤如下:
1 在pom.xml中加入spring-boot-starter-actuator模块的依赖。
2 在application.properties中增加参数配置eureka.client.healthcheck.enabled=true
其他配置
这些配置均以eureka.instance为前缀