Spring Cloud Eureka是Spring Cloud Netflix 微服务套件中的一部分,它基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务治理功能。Spring Cloud 通过为Eureka增加了Spring Boot风格的自动化配置,我们只需通过引入依赖和注解配置就能让Spring Boot构建的微服务应用轻松的与Eureka服务治理体系进行整合。
服务治理:
服务治理可以说是微服务架构中最为核心和基础的模块,主要用来实现各个微服务实例的自动化注册与发现。
使用服务治理的原因:在服务引用并不算多的时候,可以通过静态配置来完成服务的调用,但随着业务的发展,系统功能越来越复杂,相应的微服务也不断增加,此时静态配置会变得越来越难以维护。并且面对不断发展的业务,集群规模,服务的位置、服务的命名等都有可能发生变化,如果还是通过手工维护的方式,极易发生错误或是命名冲突等问题。同时,也将消耗大量的人力来维护静态配置的内容。为了解决微服务架构中的服务实例维护问题,就产生了大量的服务治理框架和产品。这些框架和产品的实现都围绕着服务注册与服务发现机制来完成对微服务应用实例的自动化管理。
服务注册:
在服务治理框架中,通常都会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。比如:有两个提供服务A的进程分别运行于192.168.0.100:8000 和192.168.0.101:8000 位置上,还有三个提供服务B的进程分别运行于192.168.0.100:9000、192.168.0.101:9000、192.168.0.102:9000位置上。当这些进程都启动,并向注册中心注册自己的服务之后,注册中心就会维护类似下面的一个服务清单。另外,注册中心还需要以心跳的方式去监测清单中的服务是否可用,若不可用需要从服务清单中剔除,达到排除故障服务的效果。
服务发现:
在服务治理框架的运作下,服务间的调用不再通过指定具体的实例地址来实现,而是通过向服务名发起请求调用实现。所以,服务调用方在调用服务提供方接口时,并不知道具体的服务实例位置。因此,调用方需要向注册中心咨询服务,并获取所有服务的实例清单,以实现对具体服务实例的访问。比如:以上述服务为例,有服务C希望调用服务A,服务C就向注册中心发起咨询请求,服务注册中心就会将服务A的位置清单返回给服务C,当服务C要发起调用时,便从该清单中以某种轮询策略取出一个位置来进行服务调用(客户端负载均衡)。
Netflix Eureka
Spring cloud Eureka ,使用 Netflix Eureka 来实现服务注册与发现,它即包含了服务端组件,也包含了客户端组件,并且服务端和客户端均采用Java编写,所以 Eureka 主要适用于通过 Java实现的分布式系统,或是与JVM兼容语言构建的系统。但是,Eureka服务端的服务治理机制提供了完备的RESTful API,所以也支持将非 Java语言构建的微服务应用纳入Eureka 的服务治理体系中来。只是在使用其他语言平台时,需要自己来实现Eureka的客户端程序。
Eureka服务端:也称为服务注册中心。它和其他服务注册中心一样,支持高可用配置。
Eureka客户端:主要处理服务的注册与发现。客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中,在应用程序运行时,Eureka客户端向注册中心注册自身提供的服务并周期性的发送心跳来更新它的服务租约。同时也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性的刷新服务状态。
搭建服务注册中心
首先,创建spring boot 工程,命名为eureka-server,并在pom.xml 中引入必要的依赖内容(也可以通过spring initializer 快速构建项目):
<?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>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>eureka-server</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Dalston.SR2</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <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> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
通过@EnableEurekaServer 注解启动一个服务注册中心提供给其他应用进行对话
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @EnableEurekaServer @SpringBootApplication public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
在默认配置下,该服务注册中心也会将自己作为客户端来尝试注册它自己,所以需要禁用它的客户端注册行为,在application.properties文件中增加如下配置:
server.port=8082 eureka.instance.hostname=localhost # 向注册中心注册服务 eureka.client.register-with-eureka=false # 检索服务 eureka.client.fetch-registry=false eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
完成配置后,启动应用并访问http://localhost:8082/。可以看到以下Eureka信息面板,其中Instances currently registered with Eureka 栏是空的,表示该注册中心还没有注册任何服务。
注册服务提供者
完成服务注册中心的搭建后,就可以添加一个既有的spring boot应用到Eureka的服务治理体系中去。
新建项目名为eureka-client的spring boot应用,将其作为一个微服务应用向服务注册中心发布自己。首先在pom.xml中增加spring cloud eureka 模块的依赖。
<?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-client</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>eureka-client</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Dalston.SR2</spring-cloud.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</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <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> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
接着,新建RESTful API,通过注入DiscoveryClient对象,在日志中打印出服务的相关内容。
package com.example.demo.web; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author lxx * @version V1.0.0 * @date 2017-8-9 */ @RestController public class HelloController { private final Logger logger = Logger.getLogger(getClass()); @Autowired private DiscoveryClient client; @RequestMapping(value = "/index") public String index(){ ServiceInstance instance = client.getLocalServiceInstance(); logger.info("/hello:host:"+instance.getHost()+" port:"+instance.getPort() +" service_id:"+instance.getServiceId()); return "hello world!"; } }
然后在主类中添加 @EnableDiacoveryClient 注解,激活Eureka 中的DiscoveryClient 实现(自动化配置,创建DiscoveryClient接口针对Eureka客户端的EurekaDiscoveryClient实例),才能实现上述对服务信息的输出。
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication public class EurekaClientApplication { public static void main(String[] args) { SpringApplication.run(EurekaClientApplication.class, args); } }
最后修改application.properties文件,通过spring.application.name属性为服务命名,再通过eureka.client.service-url.defaultZone 属性来指定服务注册中心的地址,地址和注册中心设置的地址一致:
server.port=2222 spring.application.name=hello-service eureka.client.service-url.defaultZone=http://localhost:8082/eureka/
下面分别启动服务注册中心以及服务提供方,在hello-service服务控制台中,DiscoveryClient对象打印了该服务的注册信息:
2017-08-09 17:17:27.635 INFO 8716 --- [ main] c.example.demo.EurekaClientApplication : Started EurekaClientApplication in 9.844 seconds (JVM running for 10.772) 2017-08-09 17:17:27.797 INFO 8716 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_HELLO-SERVICE/chanpin-PC:hello-service:2222 - registration status: 204
在注册中心控制台可以看到hello-service的注册信息:
2017-08-09 17:17:27.786 INFO 10396 --- [nio-8082-exec-1] c.n.e.registry.AbstractInstanceRegistry : Registered instance HELLO-SERVICE/chanpin-PC:hello-service:2222 with status UP (replication=false) 2017-08-09 17:17:47.792 INFO 10396 --- [a-EvictionTimer] c.n.e.registry.AbstractInstanceRegistry : Running the evict task with compensationTime 0ms
此处的输出内容为HelloController中注入的DiscoveryClient接口对象,从服务注册中心获取的服务相关信息。
高可用注册中心
在微服务架构这样的分布式环境中,需要充分考虑发生故障的情况,所以在生产环境中必须对各个组件进行高可用部署,对于微服务如此,对于服务注册中心也一样。
Eureka Server 的设计一开始就考虑了高可用问题,在Eureka的服务治理中,所有节点既是服务提供方,也是服务消费方,服务注册中心也一样。
Eureka Server 的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心,以实现服务清单的相互同步,达到高可用的效果。下面尝试搭建一个高可用服务注册中心的集群。在之前的服务注册中心的基础上进行扩展,构建一个双节点的服务注册中心集群。
- 创建 application-peer1.properties,作为peer1 服务中心的配置,并将serviceUrl指向peer2:
spring.application.name=eureka-server server.port=1111 eureka.instance.hostname=peer1 eureka.client.service-url.defaultZone=http://peer2:1112/eureka/
- 创建 application-peer2.properties,作为peer2 服务中心的配置,并将serviceUrl指向peer1:
spring.application.name=eureka-server server.port=1112 eureka.instance.hostname=peer2 eureka.client.service-url.defaultZone=http://peer1:1111/eureka/
- 在C:\Windows\System32\drivers\etc\hosts 文件中添加对peer1 和 peer2 中的转换,让上面配置的host形式的serviceURL能在本地正确访问到;
127.0.0.1 peer1 127.0.0.1 peer2
- 通过spring.profiles.active 属性来分别启动peer1 和 peer2(打开两个terminal进行启动,在一个terminal中先启动的peer1 会报错,但不影响,是因为它所注册的服务peer2 还未启动,在另外个terminal中把peer2 启动即可,不用启动主类) :
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1 java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2
此时访问peer1的注册中心 http://localhost:1111/ 可以看到,registered-replicas 中已经有 peer2 节点的eureka-server了。同样的访问peer2 的注册中心 http://localhost:1112/ 也可以看到registered-replicas 中有 peer1 节点, 并且这些节点在可用分片(available-replicase)之中。当关闭了peer1 节点后,刷新peer2 注册中心,可以看到 peer1 的节点变成了不可用分片(unavailable-replicas)。
- 在设置了多节点的服务注册中心之后,服务提供方还需要做一些简单的配置才能将服务注册到Eureka Server 集群中。以hello-service为例,修改配置文件如下:
server.port=2222 spring.application.name=hello-service eureka.client.service-url.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/
主要是将eureka.client.service-url.defaultZone 的注册中心指向之前搭建的peer1 和 peer2。
下面启动该服务,通过访问 http://localhost:1112/ 或者 http://localhost:1111/ 可以看到 hello-service 服务同时被注册到了peer1 和 peer2 上。
若此时断开 peer1 ,由于 hello-service 同时也向peer2 上注册了,因此在peer2 上的其他服务依然能访问到hello-service,从而实现了服务注册中心的高可用。
如果不想使用主机名来定义注册中心的地址,也可以使用IP地址的形式,但是需要在配置文件中增加配置参数 eureka.instance.prefer-ip-address=true,该值默认为false。
服务发现与消费
通过上面的内容介绍与实践,已经搭建起微服务架构中的核心组件——服务注册中心(包括单节点模式和高可用模式)。同时,还通过简单的配置,将hello-service服务注册到Eureka注册中心上,成为该服务治理体系下的一个服务。现在已经有了服务注册中心和服务提供者,下面就构建一个服务消费者,它主要完成两个目标,发现服务和消费服务。其中,服务发现的任务由Eureka客户端完成,而服务消费的任务由Ribbon完成。Ribbon是一个基于HTTP和TCP的客户端负载均衡器,它可以在通过客户端中配置的ribbonServerList服务端列表去轮询访问以达到均衡负载的作用。当Ribbon与Eureka联合使用时,Ribbon的服务实例清单RibbonServerList会被DiscoveryEnabledNIWSServerList重写,扩展成从Eureka注册中心获取服务端列表。同时也会用NIWSDiscoveryPing来取代IPing,它将职责委托给Eureka来确定服务端是否已经启动。
- 准备工作:启动之前实现的服务注册中心eureka-server以及hello-service服务,为了实验Ribbon的客户端负载均衡功能,我们通过java -jar 命令行的方式来启动两个端口不同的hello-service,具体如下:
- 修改配置文件:
server.port=2222 spring.application.name=hello-service eureka.client.service-url.defaultZone=http://localhost:8082/eureka/
- 再将hello-service应用打包:mvn clean package
- 通过下列命令启动应用程序:
java -jar eureka-client-0.0.1-SNAPSHOT.jar --server.port=8011 java -jar eureka-client-0.0.1-SNAPSHOT.jar --server.port=8012
- 成功启动两个服务后,可以在注册中心看到名为HELLO-SERVICE的服务中出现两个实例单元:
- 创建一个Spring boot项目来实现服务消费者,取名为ribbon-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>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>demo</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Dalston.SR2</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <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> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
- 在主类中通过@EnableDiscoveryClient注解让该应用注册为Eureka客户端应用,以获取服务发现的能力,同时,在该主类中创建RestTemplate的Spring Bean实例,并通过@LoadBalanced 注解开启客户端负载均衡。
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableDiscoveryClient @SpringBootApplication public class DemoApplication { @Bean @LoadBalanced RestTemplate restTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
- 创建ConsumerController类并实现/ribbon-consumer接口。在该接口中,通过上面创建的RestTemplate 来实现对HELLO-SERVICE 服务提供的 /hello 接口进行调用。此处的访问地址是服务名 HELLO-SERVICE ,而不是一个具体的地址,在服务治理框架中,这是一个重要特性。
package com.example.demo.web; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; /** * @author lxx * @version V1.0.0 * @date 2017-8-9 */ @RestController public class ConsumerController { @Autowired RestTemplate restTemplate; @RequestMapping(value = "ribbon-consumer", method = RequestMethod.GET) public String helloConsumer(){ return restTemplate.getForEntity("http://HELLO-SERVICE/index", String.class).getBody(); } }
- 在application.properties中配置Eureka服务注册中心的位置,需要与之前的HELLO-SERVICE一样,同时设置该消费者的端口为3333,不与之前启动的应用端口冲突即可。
server.port=3333 spring.application.name=ribbon-consumer eureka.client.service-url.defaultZone=http://localhost:8082/eureka/
- 启动ribbon-consumer应用后,可以在Eureka信息面板中看到,除了HELLO-SERVICE外,还多了实现的RIBBON-CONSUMER服务。
- 通过向 http://localhost:3333/ribbon-consumer 发起访问, 成功返回字符串 “hello world”。在消费者控制台中打印出服务列表情况。
- 多发送几次请求,可以在服务提供方hello-service的控制台中看到一些打印信息,可以看出两个控制台基本是交替访问,实现了客户端的负载均衡。
Eureka详解
基础架构(核心三要素)
- 服务注册中心:Eureka提供的服务端,提供服务注册与发现的功能,即之前的eureka-server。
- 服务提供者:提供服务的应用,可以是spring boot应用,也可以是其他技术平台且遵循Eureka通信机制的应用。它将自己提供的服务注册到Eureka,以供其他应用发现。即之前的HELLO-SERVICE.
- 服务消费者:消费者从服务注册中心获取服务列表,从而使消费者可以知道去何处调用其所需要的服务,在上一节中使用了Ribbon来实现服务消费,后续还会介绍使用Feign的消费方式