简介
Spring Cloud Eureka是Spring Cloud Netflix微服务套件中的一部分,它基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务治疗功能。Spring Cloud为Eureka增加了Spring Boot风格的自动化配置,我们只需通过简单引入依赖和注解配置就能让Spring Boot构建的微服务应用轻松与Eureka服务治理体系进行整合。
服务治理
服务治理可以说是微服务架构中最为核心和基础的模块,它主要用来实现各个微服务的自动化注册与发现。
服务注册与服务发现
服务注册:在服务治理框架中,通常都会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机和端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按照服务名分类组织服务清单。
服务发现:由于在服务治理框架下运作,服务间的调用不再通过指定具体的实例地址来实现,而是通过服务名发起请求调用实现。
构建服务注册中心
下面我们来构建一个简单示例,学习如何使用Eureka构建注册中心以及进行注册与发现服务。
创建服务注册中心
首先,构建一个基础的Spring Boot工程,命名为eureka-server,并在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>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>2.0.1.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>Finchley.RC1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-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>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
通过@EnableEurekaServer注解启动一个服务注册中心提供给其他应用进行对话。这一步非常简单,只需在一个普通的SpringBoot应用中添加这个注解就能开启此功能,比如下面的例子:
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
在默认设置下,该服务注册中心也会将自己作为客户端来尝试注册自己,所以需要禁用它的客户端注册行为,只需在application.properties中增加如下配置:
server.port=1111
spring.application.name=eureka-server
eureka.instance.hostname=localhost
#注册中心不注册自己
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka
在完成了上面的配置后,启动应用并访问http://localhost:1111
可以看到如下所示的Eureka信息面板,其中Instances currently registered with Euraka栏是空的,说明该注册中心还没有注册任务服务。
注册服务提供者
在完成了服务注册中心的搭建后,接下来我们创建一个SpringBoot应用加入Eureka的服务治理体系。
构建一个基础的Spring Boot工程,命名为eureka-demo,并在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>eureka-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>eureka-demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.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>Finchley.RC1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-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>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
接着实现一个请求处理接口,通过注入DiscoveryClient对象
@RestController
public class HelloController {
private final Logger logger=Logger.getLogger(getClass());
@Autowired
private DiscoveryClient client;
@Autowired
private Registration registration;
@RequestMapping(value="/hello",method=RequestMethod.GET)
public String index() {
ServiceInstance instance=serviceInstance();
logger.info(MessageFormat.format("/hello,host:{0},service_id:{1}", instance.getHost(),instance.getServiceId()));
return "Hello world";
}
}
然后在主类中通过加入@EnableDiscoveryClient注解,激活Eureka中的DiscoveryClient实现,才能实现上述Controller中对服务信息的输出。
@EnableDiscoveryClient
@SpringBootApplication
public class EurekaDemoApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaDemoApplication.class, args);
}
}
最后,我们需要在application.properties配置文件中,通过spring.application.name属性来为服务命名,再通过eureka.client.serviceUrl.defaultZone属性来指定服务注册中心的地址。
server.port=1112
spring.application.name=hello-service
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka
然后就可以启动应用,我们可以发现在eureka-server的控制台中可以发现hello-service已经被注册成功了,信息面板也可以看到hello-service的注册信息。
高可用注册中心
在微服务架构这样的分布式环境中,我们需要考虑发送故障的情况,所以在生产环境中必须对各个组件进行高可用部署,对服务注册中心也是一样。
在Eureka的服务治理设计中,所有节点既是服务提供方,也是服务消费方,服务注册中心也不例外。在之前的eureka-server中我们设置过下面两个参数,让服务注册中心不能注册自己
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
Eureka Server的高可用实际就是将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心,以实现服务清单的互相同步,达到高可用的效果。
下面我们就来尝试搭建高可用服务注册中心的集群。
启动两个eureka-server服务注册中心,application.properties分别为以下内容
server.port=1110
spring.application.name=eureka-server
eureka.instance.hostname=localhost
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:1111/eureka
server.port=1111
spring.application.name=eureka-server
eureka.instance.hostname=localhost
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:1110/eureka
启动服务注册中心后,可以发现两个服务注册中心分别注册了对方的服务。
接下来让我们修改一下hello-service的配置
eureka.client.serviceUrl.defaultZone=http://localhost:1110/eureka,http://localhost:1111/eureka
然后我们启动hello-service应用,访问http://localhost:1110 和http://localhost:1111 , hello-service服务同时被注册到了以上两个服务注册中心上,如果再断开其中一个服务注册中心,hello-service仍然可以在另一台注册中心上被发现,从而实现了服务注册中心的高可用。
服务发现与消费
通过上面的示例我们已经完成了服务注册中心与服务提供者,下面我们来尝试构建一个服务消费者,它主要完成两个目标,发现服务以及消费服务。其中,服务的发现由Eureka的客户端完成,而服务的消费由Ribbon完成。
Ribbon是一个基于HTTP和TCP的客户端负载均衡器,它可以在通过客户端中配置的ribbonServerList服务端列表去轮询访问以达到负载均衡的作用。当Ribbon与Eureka联合使用时,Ribbon的服务实例清单RibbonServerList会被DiscoveryEnableNIWSServerList重写,扩展成从Eureka注册中心中获取服务端列表。
在此,我们就不对Ribbon做详细介绍,只需要知道它在Eureka服务发现的基础上,实现了一套对服务实例的选择策略,从而实现对服务的消费。
首先,我们做一些准备工作。启动之前实现的注册中心eureka-server以及hello-service服务,为了实验Ribbon的客户端负载均衡功能,我们用两个不同的端口2220和2221分别启动hello-service服务。
成功启动两个hello-service服务后,从Eureka信息面板中可以看到名为HELLO-SERVICE的服务中出现了两个实例,如下图所示
接下来创建一个Spring Boot的基础工程来实现服务消费者,取名为ribbon-consumer,并在pom.xml中引入如下的依赖内容,比较之前的hello-service,我们新增了Ribbon模块的依赖Spring-cloud-starter-ribbon。
<?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>ribbon-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>ribbon-consumer</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.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>Finchley.RC1</spring-cloud.version>
</properties>
<dependencies>
<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-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
<type>jar</type>
<version>15.0</version>
</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>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
创建应用主类RibbonConsumerApplication,通过@EnableDiscoveryClient注解让该应用注册为Eureka客户端应用,以获得服务发现能力,同时,在该主类中创建RestTemplate的Spring bean实例,并通过@LoadBalanced注解开启客户端负载均衡。
@EnableDiscoveryClient
@SpringBootApplication
public class RibbonConsumerApplication {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(RibbonConsumerApplication.class, args);
}
}
创建ConsumerController类实现/ribbon-consumer接口,在该接口中,通过在上面创建的RestTemplate来实现对HELLO-SERVICE服务提供的/hello接口进行调用。
@RestController
public class ConsumerController {
@Value(value="${spring.request.hello.service}")
private String requestUrl;
@Autowired
RestTemplate restTemplate;
@Autowired
HelloService helloService;
/**
* GET
* @return
*/
@RequestMapping(value="/ribbon-consumer",method=RequestMethod.GET)
public String helloConsumer() {
return restTemplate.getForEntity(requestUrl+"hello", String.class).getBody();
}
}
在application.properties中配置Eureka服务注册中心的位置,需要与之前的HELLO-SERVICE一样,不然是发现不了该服务的,同时设置该消费者的端口为3333,不能与之前启动的应用端口冲突。
spring.application.name=ribbon-consumer
server.port=3333
eureka.client.serviceUrl.defaultZone=http://localhost:1110/eureka,http://localhost:1111/eureka
spring.request.hello.service=http://HELLO-SERVICE/
在这里我们可以看到访问的地址是服务名HELLO-SERVICE,而不是一个具体的地址。
启动ribbon-consumer应用后,在Eureka信息面板中看到下图的服务列表
通过向http://localhost:3333/ribbon-consumer 发送GET请求,成功返回了“Hello World”。
再尝试发送几次请求,并观察启动的两个HELLO-SERVICE的控制台,可以看到两个控制台会交替打印下面的日志,这就说明了ribbon-consumer对HELLO-SERVICE的调用实现了负载均衡。
Eureka详解
基础框架
- 服务注册中心:Eureka提供的服务端,提供服务注册于发现的功能。
- 服务提供者:提供服务的应用,可以是Spring Boot应用,也可以是其他技术平台且遵循Eureka通信机制的应用。它将自己提供的服务注册到Eureka,以供其他应用发现。
- 服务消费者:消费者应用从服务注册中心获取服务列表,从而使消费者可以知道去何处调用其所需要的服务。
很多时候客户端既是服务提供者也是服务消费者。
服务治理机制
以下图为例,有几个这样的重要元素:
- “服务注册中心-1”和“服务注册中心-2”,它们互相注册组成了高可用集群。
- “服务提供者”启动了两个实例,一个注册到“服务注册中心-1”上,另外一个注册到“服务注册中心-2”上。
- 还有两个“服务消费者”,它们也都分别只指向了一个注册中心。
根据上面的结构,下面我们来了解一下从服务注册开始带服务调用,及各个元素所涉及的一些重要通信行为。
服务提供者
服务注册
“服务提供者”在启动的时候会通过发送REST请求的方式将自己注册到的Eureka Server上,同时带自身服务的一些元数据信息,Eureka Server接收到这个这个REST请求之后,将元数据信息存储在一个双层结构Map中,其中第一层的可以是服务名,第二层的key是具体服务的实例名。
服务同步
如架构图中所示,这里的两个服务提供者分别注册到了两个不同的服务注册中心上,也就是说,它们的信息分别被两个服务注册中心所维护。此时,由于服务注册中心之间因为互相注册服务,当服务提供者发送注册请求到一个服务注册中心时,这个服务注册中心会将该请求转发给集群中相连的其他注册中心,从而实现注册中心之间的服务同步,通过服务同步,两个服务提供者的服务信息就可以通过这两台服务注册中心中的任意一台获取到。
服务续约
在注册完服务后,服务提供者会维护一个心跳用来持续告诉Eureka Server自身的存活状态,以防止Eureka Server的“剔除任务”将该服务实例从服务列表中排除出去,我们称该操作Wie服务续约。
服务消费者
获取服务
在服务注册中心已经注册了一个服务,并且该服务有两个实例。当我们启动服务消费者的时候,它会发送一个REST请求服务注册中心,来获取上面注册的服务请求。为了性能考虑,Eureka Server会维护一份只读的服务清单来返回客户端,同时该缓存清单会每隔30秒更新一次。
服务调用
服务消费者在获取服务清单后,通过服务名可以获取具体提供服务的实例名和该实例的元数据信息。通过这些服务实例的详细信息,客户端可以根据自己的需要决定具体调用哪个实例,在Ribbon中会默认采用轮询的方式进行调用,从而实现客户端的负载均衡。
服务下线
在客户端程序中,当服务实例进行正常的关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心“我要下线了”。服务端接收到请求后,将该服务状态置为下线(DOWN),并把该下线事件传播出去。
服务注册中心
失效剔除
有些时候,我们的服务实例并不一定会正常下线,可能由于内存溢出、网络故障等原因使得服务不能正常工作,而服务注册中心并未收到“服务下线”的请求。为了从服务列表中将这些无法提供服务的实例剔除,Eureka Server在启动的时候会创建一个定时任务,默认每隔一段时间(默认为60s)将当前清单中超时(默认为90s)没有续约的服务剔除出去。
自我保护
我们之前在本地调试Eureka的程序时,有时会在服务注册中心的信息面板中出现红色的警告信息
实际上,这个警告就是触发了Eureka Server的自我保护机制。服务注册到Eureka Server之后,会维护一个心跳连接,告诉Eureka Server自己还活着。Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server会将当前的实例注册信息保护起来,让这些实例不会过期,尽可能保护这些注册信息,但是,在这段保护期间内实例若出现问题,那么客户很容易拿到实际已经不存在的服务实例,会出现调用失败的情况,所以客户端必须要有容错机制,比如可以使用请求重试,断路器等机制。
由于本地调试很容易触发注册中心的保护机制,这会使得注册中心维护的服务实例不那么准确,所以我们在本地进行开发的时候,可以使用以下配置关闭保护机制
eureka.server.enable-self-preservation=false
好了,本次的Eureka就介绍到这了。