使用Eureka编写注册中心服务

引言

Spring Cloud Eureka是 Spring Cloud Netflix微服务套件的一部分,基于 Netflix Eurd做了二次封装,主要负责实现微服务架构中的服务治理功能。 Spring Cloud Eureka是一个基于REST的服务,并且提供了基于Java的客户端组件,能够非常方便地将服务注到Spring Cloud Eureka中进行统一管理。

注册中心带来的好处就是,不需要知道有多少提供方,你只需要关注注册中心即可,就像顺客不必关心有多少火车在开行,只需要去12306网站上看有没有票就可以了。为什么 Eureka比 Zookeeper更适合作为注册中心呢?主要是因为 Eureka是基于AP原则构建的,而 Zookeeper是基于CP原则构建的。在分布式系统领域有个著名的CAP定理,即C为数据一致性;A为服务可用性;P为服务对网络分区故障的容错性。这三个特性在任何分布式系统中都不能同时满足,最多同时满足两个。

Zookeeper有一个 Leader,而且在这个 Leader无法使用的时候通过 Paxos(ZAB)算法选举出一个新的 Leader.。这个 Leader的任务就是保证写数据的时候只向这个 Leader写入,Leader会同步信息到其他节点。通过这个操作就可以保证数据的一致性。

总而言之,想要保证AP就要用 Eureka,想要保证CP就要用 Zookeeper。Dubo中大部分都是基于 Zookeeper作为注册中心的。 Spring Cloud中当然首选 Eureka

——《spring cloud 微服务 入门、进阶与实战》第33页

开始

创建一个spring cloud 项目springcloud-eureka-server。(这里创建一个spring boot 再引入 spring cloud 的依赖也是一样)

  1. 这个是注册中心需要的依赖
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
  1. 这里也贴出spring boot 与 spring cloud 的依赖(之所以贴出来是因为我没有参照书中的添加,然后服务出错了启动不了,经过折腾发现 spring boot 与 spring cloud 的版本是有对应关系的)

    参照官网:

    https://spring.io/projects/spring-cloud

    image-20200428193338935.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<spring-cloud.version>Hoxton.SR4</spring-cloud.version>
</properties>

<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>

application.properties加入配置:

1
2
3
4
5
6
7
spring.application.name=eureka-server
server.port=8761

#由于该应用为注册中心,所以设置为false,代表不向注册中心注册自己
eureka.client.register-with-eureka=false
#由于注册中心的职责就是维护服务实例,它并不需要去检索服务,所以也设置成false
eureka.client.fetch-registry=false

启动类加上@EnableEurekaServer注解

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaServer
class SpringcloudEurekaServerApplication {

public static void main(String[] args) {
SpringApplication.run(SpringcloudEurekaServerApplication.class, args);
}
}

启动访问http://localhost:8761/ 可看到页面:

image-20200428201536367.png

这里只是一个空的注册中心,里边还没有服务。接下来我们来创建两个服务注册到服务中心去:

服务提供者

和上边一样创建一个spring cloud 项目 springcloud-eureka-server-Provider(引入上边spring boot 和spring cloud的依赖)再加上eureka 客户端的依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 与注册中心 不同的是 这里是 eureka-client -->

在启动类上加入@EnableEurekaClient 注解

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaClient
public class SpringcloudEurekaServerProviderApplication {

public static void main(String[] args) {
SpringApplication.run(SpringcloudEurekaServerProviderApplication.class, args);
}
}

我们在来编写一个测试的接口:

1
2
3
4
5
6
7
8
@RestController
public class UserController {

@GetMapping("/user/hello")
public String hello() {
return "hello";
}
}

application.properties

1
2
3
4
5
6
7
8
spring.application.name=eureka-client-user-service
server.port=8081
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

#采用IP注册
eureka.instance.preferIpAddress=true
#定义实例ID格式
eureka.instance.instance-id=${spring.application.name}:${spring.cloud.client.ip.address}:${server.port}

启动后在再刷新注册中心的页面可看到有了一个服务实例:

image-20200428203217712.png

服务消费者

首先与服务提供者一样建立一个spring cloud 项目 springcloud-eureka-server-Consumer,pom 依赖也一样

application.properties 基本一样,我们修改下服务名称和端口:

1
2
3
4
5
6
7
8
spring.application.name=eureka-client-article-service
server.port=8082
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

#采用IP注册
eureka.instance.prefer-ip-address=true
#定义实例ID格式
eureka.instance.instance-id=${spring.application.name}:${spring.cloud.client.ip.address}:${server.port}

再建立两个类:

1
2
3
4
5
6
7
@Configuration
public class BeanConfiguration {
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
1
2
3
4
5
6
7
8
9
10
@RestController
public class ArticleController {

@Autowired
private RestTemplate restTemplate;
@GetMapping("/article/callHello")
public String callHello(){
return restTemplate.getForObject("http://localhost:8081/user/hello",String.class);
}
}

启动后可看到配置中心有了两个实例

image-20200428211048055.png

我们来访问下消费者的接口得到返回:

image-20200428204803431.png

从代码我们可以看到。我们访问 http://localhost:8082/article/callHello 接口,其实在控制器里是通过restTemplate访问 服务提供者http://localhost:8081/user/hello接口返回数据的

restTemplate通过localhost:8081其实并没有通过配置中心来消费接口。

我们可以通过以下配置:

在BeanConfiguration中加入@LoadBalanced注解

1
2
3
4
5
6
7
8
@Configuration
public class BeanConfiguration {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}

restTemplate就可以通过实例的名称 eureka-client-user-service 来访问了

1
2
3
4
@GetMapping("/article/callHello")
public String callHello(){
return restTemplate.getForObject("http://eureka-client-user-service/user/hello",String.class);
}

这样才是利用了配置中心来消费接口

开启Eureka认证

Eureka自带了一个Web的管理页面,方便我们查询注册到上面的实例信息,但是有一个题:如果在实际使用中,注册中心地址有公网1P的话,必然能直接访问到,这样是不安全的。所以我们需要对 Eureka进行改造,加上权限认证来保证安全性改造我们的eureka- server,通过集成 Spring-security来进行安全认证在 pom. xml中添加Spring- Security的依赖包

——《spring cloud 微服务 入门、进阶与实战》第39页

在注册中心 springcloud-eureka-server 加入security依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

增加配置class文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
}
}

properties增加配置用户名和密码

1
2
spring.security.user.name=WuZhiYong
spring.security.user.password=123456

重新启动注册中心,访问htp:/ localhost:8761,此时浏览器会提示你输入用户名和密,人正确后才能继续访问 Eureka提供的管理页面。

在 Eureka开启认证后,客户端注册的配置也要加上认证的用户名和密码信息:

否则无法启动成功(不能注册到注册中心)

1
eureka.client.serviceUrl.defaultZone=http://WuZhiYong:123456@localhost:8761/eureka/

关闭自我保护

保护模式主要在一组客户端和 Eureka Server之间存在网络分区场景时使用。一且进入保护模式, Eureka Server将会尝试保护其服务的注册表中的信息,不再删除服务注册表中的数据。当网络故障恢复后,该 Eureka Server节点会自动退出保护模式。

——《spring cloud 微服务 入门、进阶与实战》第41页

访问注册中心页面的时候有时会出现这样的提示:即表示进入保护模式了

image-20200428213312803.png

在配置文件中我们可这样关闭保护模式

1
eureka.server.enable-self-preservation=false

在刷新页面会显示:

THE SELF PRESERVATION MODE IS TURNED OFF. THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.

(翻译:自我保存模式已关闭。在网络/其他问题的情况下,这可能无法保护实例到期)

自定义Eureka的实例(instanceId)ID

自定义实例id在配置文件中已做了注释说明。

不过我们前面在注册中心页面上看到的实例id的时候并没有象我们配置的那样呈现出了ip地址。实际上这是由于不同版本造成的。这里改成下面这样就好了。

1
2
#定义实例ID格式
eureka.instance.instance-id=${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}

再看发现正常显示ip了

image-20200428215931142.png

自定义实例跳转链接

我们通过配置实现了用IP进行注册,当点击 Instance ID进行跳转的时就可以用P跳转了,跳转的地址默认是IP+ Port/info。我们可以自定义这个跳转的地址

比如配置跳转到我的博客(在非注册中心的项目里配置)

1
2
#配置跳转链接
eureka.instance.status-page-url=https://wu_zhiyong.gitee.io/myblog/

点击实例ID后就会跳转到我的博客

快速移除已失效的服务信息

在实际开发过程中,我们可能会不停地重启服务,由于 Eureka有自己的保护机制,故节点下线后,服务信息还会一直存在于 Eureka中。我们可以通过增加一些配置让移除的速度更快一点,当然只在开发环境下使用,生产环境下不推荐使用。

——《spring cloud 微服务 入门、进阶与实战》第43页

首先在 eureka-server 注册中心添加配置:

1
2
3
4
#别是关闭自我保护
eureka.server.enable-self-preservation=false
#清理间隔 默认60000
eureka.server.eviction-interval-timer-in-ms=5000

然后在具体的服务中配置:(本例中就是前面说的服务提供者和消费者两个服务)

1
2
3
4
5
6
7
#开启健康检查
eureka.client.healthcheck.enabled=true
# 表示注册的服务向eureka配置中心发送心跳的频率 默认30秒
eureka.instance.lease-renewal-interval-in-seconds=5
# 表示eureka配置中心向注册的服务们自上一次心跳后等待下一次心跳的超时时间,
# 在这个过程中没有收到则移除实例 默认90秒
eureka.instance.lease-expiration-duration-in-seconds=5

同时加上健康检查的依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

更多的配置信息可参考EurekaInstanceConfigBean 和 EurekaServerConfigBean

Eureka高可用搭建

高可用原理

前面我们搭建的注册中心只适合本地开发使用,在生产环境中必须搭建一个集群来保证高可用。 Eureka的集群搭建方法很简单:每一台 Eureka只需要在配置中指定另外多个Eureka的地址就可以实现一个集群的搭建了。

——《spring cloud 微服务 入门、进阶与实战》第39页

搭建步骤

简单说明:

如果有主、从两个注册中心

将 主 注册->到 从

将 从 注册->到 主

如果有主、从1、从2三个注册中心(以此类推)

将 主 注册->到 从1、从2

将 从1 注册->到 主、从2

将 从2 注册->到 主、从1

以搭建主、从注册中心为例:

参照eureka-server建立一个项目(或复制一个项目)项目名我们就叫做 springcloud-eureka-server-cluster.

至此我们现在有四个项目了

image-20200429132054476.png

简单说明下:

1
2
3
4
5
6
7
8
#注册中心服务
springcloud-eureka-server
#注册中心服务的复制版(用于高可用)
springcloud-eureka-server-cluster
#服务消费者 注册到注册中心
springcloud-eureka-server-consumer
#服务提供者 注册到注册中心
springcloud-eureka-server-provider

我们先看eureka-server的application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server.port=8761
spring.application.name=eureka-server
#指向从节点的注册中心
eureka.client.serviceUrl.defaultZone=http://WuZhiYong:123456@localhost:8762/eureka/

#由于该应用为注册中心,所以设置为false,代表不向注册中心注册自己
eureka.client.register-with-eureka=false
#由于注册中心的职责就是维护服务实例,它并不需要去检索服务,所以也设置成false
eureka.client.fetch-registry=false

spring.security.user.name=WuZhiYong
spring.security.user.password=123456
#别是关闭自我保护
eureka.server.enable-self-preservation=false
#清理间隔 默认60000
eureka.server.eviction-interval-timer-in-ms=5000

再看看eureka-server-cluster的application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server.port=8762
spring.application.name=eureka-server-cluster
#指向主节点的注册中心
eureka.client.serviceUrl.defaultZone=http://WuZhiYong:123456@localhost:8761/eureka/

#由于该应用为注册中心,所以设置为false,代表不向注册中心注册自己
eureka.client.register-with-eureka=false
#由于注册中心的职责就是维护服务实例,它并不需要去检索服务,所以也设置成false
eureka.client.fetch-registry=false

spring.security.user.name=WuZhiYong
spring.security.user.password=123456
#别是关闭自我保护
eureka.server.enable-self-preservation=false
#清理间隔 默认60000
eureka.server.eviction-interval-timer-in-ms=5000

配置也基本相同。只是端口和注册中心的地址不一样。

好的。我们再把server-provider和server-consumer的application.properties里的注册中心指向上面的两个:

1
eureka.client.serviceUrl.defaultZone=http://WuZhiYong:123456@localhost:8761/eureka/,http://WuZhiYong:123456@localhost:8762/eureka/

好了,高可用的配置已经完成,我们分别启动主从注册中心,然后再启动 server-provider和server-consumer。

分别访问http://localhost:8762/与http://localhost:8761/都能看到里边俩个服务。

image-20200429133958776.png

image-20200429134015432.png

然后我们后台任意停止一个注册中心。我们访问server-consumer的接口时,依然能正确调用到server-provider的服务并返回数据。

扩展使用

Eureka REST API

Eureka 提供了一些API用于获取某个服务的注册信息

官方文档:

https://github.com/Netflix/eureka/wiki/Eureka-REST-operations

以上面我们启动的服务为例调用api

http://localhost:8761/eureka/apps/eureka-client-user-service

localhost:8761 是注册中心的地址

eureka-client-user-service 是服务实例的名称

如果配置中心没有加入security认证机制直接访问上面的地址就可以得到xml格式的数据响应。

如果 Eureka开启了认证,记得添加认证信息,用户名和密码必须是Base64编码过的Authorization: Basic用户名:密码,其余的接口就不做过多讲解了,大家可以自己去尝试。Postman直接支持了 Basic认证,将选项从 Headers切换到 Authorization,选择认证方式为Basic Auth就可以填写用户信息了

image-20200429135803033.png

如果想返回json需在headers 中添加:

1
2
Content-Type:application/json
Accept:application/json

image-20200429135941147.png

配置好后点击发送

image-20200429140121377.png

响应的json

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
{
"application": {
"name": "EUREKA-CLIENT-USER-SERVICE",
"instance": [
{
"instanceId": "eureka-client-user-service:192.168.0.106:8081",
"hostName": "192.168.0.106",
"app": "EUREKA-CLIENT-USER-SERVICE",
"ipAddr": "192.168.0.106",
"status": "UP",
"overriddenStatus": "UNKNOWN",
"port": {
"$": 8081,
"@enabled": "true"
},
"securePort": {
"$": 443,
"@enabled": "false"
},
"countryId": 1,
"dataCenterInfo": {
"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
"name": "MyOwn"
},
"leaseInfo": {
"renewalIntervalInSecs": 5,
"durationInSecs": 5,
"registrationTimestamp": 1588138533939,
"lastRenewalTimestamp": 1588140045949,
"evictionTimestamp": 0,
"serviceUpTimestamp": 1588138533939
},
"metadata": {
"management.port": "8081"
},
"homePageUrl": "http://192.168.0.106:8081/",
"statusPageUrl": "https://wu_zhiyong.gitee.io/myblog/",
"healthCheckUrl": "http://192.168.0.106:8081/actuator/health",
"vipAddress": "eureka-client-user-service",
"secureVipAddress": "eureka-client-user-service",
"isCoordinatingDiscoveryServer": "false",
"lastUpdatedTimestamp": "1588138533939",
"lastDirtyTimestamp": "1588138533909",
"actionType": "ADDED"
}
]
}
}

元数据使用

Eureka的元数据有两种类型,分别是框架定好了的标准元数据和用户自定义元数据标准元数据指的是主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。自定义元数据可以使用 eureka. Instance metadatamap进行配置。

自定义元数据说得通俗点就是自定义配置,我们可以为每个 Eureka Client定义一些属于自己的配置,这个配置不会影响 Eureka的功能。自定义元数据可以用来做一些扩展信息,比如灰度发布之类的功能,可以用元数据来存储灰度发布的状态数据, Ribbon转发的时候就可以根据服务的元数据来做一些处理。当不需要灰度发布的时候可以调用 Eureka提供的 REST API将元数据清除掉。

——《spring cloud 微服务 入门、进阶与实战》第47页

下面我们来自定义一个简单的元数据,在属性文件中配置如下

1
2
#配置元数据(不同版本有细微的差别,可通过提示来选择)
eureka.instance.metadata-map.create-user=WuZhiYong

然后再次通过postman 来请上面的rest api 地址

我们定义的元数据已经呈现出来了

image-20200429141503793.png

EurekaClient 使用

当我们项目中集成了Eureka之后,可以通过EurekaClient来获取一些我们想要的数据,比如上面的元数据,我们就可以直接通过EurekaClient来获取。

首先我们在server-cunsumer 的controller 中加入下面代码并重启

1
2
3
4
5
6
7
8
@Qualifier("eurekaClient")
@Autowired
private EurekaClient eurekaClient;

@GetMapping("/article/info")
public Object serviceUrl(){
return eurekaClient.getInstancesByVipAddress("eureka-client-user-service",false);
}

然后通过postman请求可以得到相同的结果:(server-cunsumer没有认证所以这里直接访问即可)

image-20200429142503537.png

健康检查

默认情况下, Eureka客户端是使用心跳和服务端通信来判断客户端是否存活,在某些场景下,比如 Mongodb出现了异常,但你的应用进程还是存在的,这就意味着应用可以继续通过心跳上报,保持应用自己的信息在 Eureka中不被剔除掉。

Spring Boot Actuator提供了/ actuator/ health端点,该端点可展示应用程序的健康信息,当 MONGODB异常时,/ actuator/health端点的状态会变成DOWN,由于应用本身确实处于存活状态,但是 Mongodb的异常会影响某些功能,当请求到达应用之后会发生操作失败的

情况。

在这种情况下,我们希望可以将健康信息传递给 Eureka服务端。这样 Eureka中就能及时将应用的实例信息下线,隔离正常请求,防止出错。通过配置如下内容开启健康检查

eureka. client healthcheck. enabled-true

我们可以通过扩展健康检查的端点来模拟异常情况,定义一个扩展端点,将状态设置为DOWN

——《spring cloud 微服务 入门、进阶与实战》第49页

我们在server-consumer application.properties中开启健康检查:

1
2
#开启健康检查(可以让注册中心获取检查状态)
eureka.client.healthcheck.enabled=true

当然前提需要添加依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

自定义类:

1
2
3
4
5
6
7
@Component
public class MyHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
builder.down().withDetail("status",false);
}
}

重启后访问注册中心

image-20200429144244592.png

服务上下线监控

在某些特定的需求下,我们需要对服务的上下线进行监控,上线或下线都进行邮件通知, Eureka中提供了事件监听的方式来扩展。

目前支持的事件如下:

  • EurekalnstanceCanceledEvent服务下线事件。

  • EurekalnstanceRegisteredEvent服务注册事件。

  • EurekalnstanceRenewedEvent服务续约事件。

  • EurekaRegistry AvailableEvent Eureka注册中心启动事件。

  • EurekaServerStartedEvent Eureka Server启动事件。

基于 Eureka提供的事件机制,可以监控服务的上下线过程,在过程发生中可以发送邮

——《spring cloud 微服务 入门、进阶与实战》第50页

我们在注册中心Eureka-server的项目里加上下面代码:(集群就每个集群项目里加上)

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
@Component
public class EurekaStateChangeListener {

@EventListener
public void listen(EurekaInstanceCanceledEvent event) {
//发送邮件。。。
//发送短信。。。
System.err.println(event.getServerId() + "\t" + event.getAppName() + " 服务下线");
}

@EventListener
public void listen(EurekaInstanceRegisteredEvent event) {
InstanceInfo instanceInfo = event.getInstanceInfo();
System.err.println(instanceInfo.getAppName() + "进行注册");
}

@EventListener
public void listen(EurekaInstanceRenewedEvent event) {
System.err.println(event.getServerId() + "\t" + event.getAppName() + " 服务进行续约");
}

@EventListener
public void listen(EurekaRegistryAvailableEvent event) {
System.err.println("注册中心 启动");
}

@EventListener
public void listen(EurekaServerStartedEvent event) {
System.err.println("Eureka Server 启动");
}

}

注意

注在 Eureka集群环境下,每个节点都会触发事件,这个时侯需要控制下发送通知的行为,不控制的话每个节点都会发送通知。

重启后控制台:

image-20200429152545740.png