13.2 搭建服务注册中心

13.2 搭建服务注册中心

Spring Cloud是一个非常大的伞形项目,由多个独立的子项目组成,每个子项目都以某种形式支撑着微服务的开发。其中有一个子项目叫作Spring Cloud Netflix,它按照Spring的编码风格重新提供了Netflix的多个组件。在这些组件中包括了Netflix的服务注册中心Eureka。

Eureka赤裸裸的历史真相

Eureka这个词最初的含义是当人们找到或发现某件事情时所发出的欢呼。这使得Eureka非常适合用作服务注册中心的名称,微服务要借助注册中心实现彼此发现的功能。

据传说,Eureka最早是由希腊物理学家阿基米德发明的,他坐在浴缸里的时候发现了浮力的原理,于是他跳出浴缸,赤裸裸地跑回家,嘴里喊着“Eureka!”。

关于阿基米德是否真的光着身子跑回家并大喊“Eureka!”还有一些争论,但无论如何,这个故事非常有意思。话说回来,我们倒是可以衣冠整洁地使用Eureka服务注册中心。

在微服务应用中,Eureka会担当所有服务的注册中心。Eureka本身也可以视为一个微服务,只不过在整体应用中它的目的是让其他的服务能够互相发现。

鉴于它在微服务应用中的角色,在创建需要注册的服务之前,我们最好搭建一个Eureka服务注册中心。为了理解Eureka的运行原理,我们可以参见图13.1所述的流动过程。

当服务实例启动的时候,它会按照名称将自己注册到Eureka中。在图13.1中,服务的名称为“some-service”。“some-service”可能会有多个完全等价的实例,但是在Eureka注册时,它们的名称是相同的。

image-20211020204011914

图13.1 服务使用Eureka服务注册中心进行注册(这样其他的服务就能发现并消费它们了)

在某个时间点,另一个服务(图13.1中名为“other-service”)需要使用“some-service”的端点。在这里,“other-service”没有使用特定的主机和端口信息对“some-service”进行硬编码,而是根据名字从Eureka查找“some-service”。Eureka的回应中将会包含它所知道的“some-service”的所有实例。

现在,“other-service”需要做决策了。它该使用“some-service”的哪个实例呢?如果它们都是完全等同的,其实就没什么关系了。为了避免每次都选择同一个实例,最好用一些客户端负载平衡算法来分散请求。这就是Netflix的另一个项目Ribbon的用武之地了。

虽然“other-service”完全可以自行查找和选择“some-service”的实例,但在这里我们让它依赖Ribbon。Ribbon是一个客户端负载平衡器,会帮助“other-service”做出选择。Ribbon做完选择之后,剩下的就是让“other-service”向Ribbon选择的实例发出请求。

为何要使用客户端负载均衡器

通常,我们会认为负载均衡器是一个中心化的服务,它处理所有的请求并将请求分发到多个目标实例中。与之不同,Ribbon是一个客户端负载均衡器,它会在每个客户端上发起请求。

相对于中心化的负载均衡器,Ribbon作为客户端的负载均衡器会有很多额外的收益。因为有一个在客户端本地的负载均衡器,所以负载均衡器能够很自然地按照客户端的数量成比例伸缩。此外,每个负载均衡器都可以配置成最适合对应客户端的负载平衡算法,而不必对所有的服务都使用相同的配置。

如果你觉得它看上去有些复杂,那么不用担心,随后我们就会看到大多数功能都会以自动化、透明的方式来进行处理。在注册和消费服务之前,我们需要先启用Eureka服务器。

要开始使用Spring Cloud和Eureka,我们需要首先为Eureka本身创建一个全新的项目。最简单的方式是使用Spring Initializr,该项目可以使用任何名称,但是我一般会将其称为service-registry。在选择starter依赖的时候,我们只需要一项依赖:带有Eureka Server标签的复选框。在创建完新项目之后,在Initializr为我们生成的项目中,pom.xml将会包含如下依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

在pom.xml文件中,我们还可以看到名为spring-cloud.version的属性以及一个<dependencyManagement>区域,它们指定了Spring Cloud的发布版本。当我创建service-registry的时候,它引用的是Finchley train的第一个服务发布版本(SR1):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<properties>
...
<spring-cloud.version>Finchley.SR1</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>

如果你想要使用不同版本的Spring Cloud,只需要将spring-cloud.version属性修改为想要的版本即可。

在构建文件中添加完Eureka starter依赖之后,要启用Eureka服务器,我们还需要做一件事情,那就是打开应用的主引导类并为其添加@EnableEurekaServer注解:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceRegistryApplication.class, args);
}
}

好的,这样就可以了!如果此时启动应用,Eureka服务注册中心就会运行起来并监听8080端口。如果此时在浏览器上访问http://localhost:8080 ,将会看到如图13.2所示的Web界面。

image-20211020204747659

图13.2 Eureka基于Web的dashboard

Eureka还对外暴露了REST API,借助它们服务可以自行进行注册,也可以发现其他的服务。你可能不会直接使用REST API,但是你会发现“/eureka/apps”端点非常有意思。它会列出注册中心所有服务实例的细节。此时,我们没有注册任何服务,它的响应如下所示。在注册完服务之后,我们还会研究这个端点:

1
2
3
4
<applications>
<versions__delta>1</versions__delta>
<apps__hashcode></apps__hashcode>
</applications>

你会发现,在Eureka的日志中,每隔大约30秒就会打印出一些异常。不用担心,Eureka正在运行,而且完全符合我们的预期。但是,这些异常表明我们还没有完全配置好服务注册中心。接下来,我们添加一些配置属性来消除这些异常。

13.2.1 配置Eureka

Eureka不喜欢独自工作,并相信数量多会更安全的理念,希望能够成为Eureka服务器集群的一部分。如果有多个Eureka服务器,其中有一个遇到问题,就不会出现单点故障。因此,Eureka的默认行为是与其他Eureka服务器建立关联,尝试获取其他Eureka服务器的服务注册中心,甚至还会将自身注册为其他Eureka服务器的服务。

在生产环境中,Eureka的高可用是非常有价值的。但是,对于开发阶段来说,启动多个Eureka服务器既不方便也没有必要。为了达到开发的目的,有一个单独的Eureka服务器就足够了。除非我们正确配置了Eureka服务器,否则它会以日志文件中异常的形式每隔30秒就抱怨孤独状态。这是因为,每隔30秒,Eureka服务器就会尝试与另外的Eureka服务器建立关联,以注册自己并共享其注册中心中的信息。

我们需要做的就是配置Eureka使其接受当前的孤独状态。为了实现这一点,我们需要在application.yml中设置一些属性,代码片段如下所示:

1
2
3
4
5
6
7
8
eureka:
instance:
hostname: localhost
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka

首先,我们将eureka.instance.hostname属性设置为localhost。这会告诉Eureka它正运行在哪个主机(host)上。这个属性是可选的,如果我们不指定它,那么Eureka会尝试通过环境变量确定它的主机。明确设置这个属性能够让我们更加确定它的值。

接下来的两个属性是eureka.client.fetch-registry和eureka.client.register-with-eureka。在其他的微服务中,我们可能会通过这两个属性告诉它们该如何与Eureka服务器进行交互。但是,不要忘了,Eureka也是一个微服务,所以这些属性也可以用到Eureka服务器上,以便于告诉它该如何与其他Eureka服务器进行交互。

这两个属性的默认值都是true,表明Eureka应该从其他的Eureka实例获取注册信息,并且应该将自身注册为其他Eureka服务器中的服务。因为在开发模式下并没有其他的Eureka服务器,所以我们将它们设置为false,这样Eureka将不会尝试与其他的Eureka服务器建立关联。

最后,我们还设置了eureka.client.service-url属性。这个属性包含了zone名称与该zone下一个或多个Eureka服务器之间的映射关系。defaultZone是一个特殊的key,如果客户端(在本例中,也就是Eureka本身)没有指定所需的zone,就将会使用这个zone。因为我们只有一个Eureka,映射到默认zone的URL就是Eureka服务器本身,所以这里使用了占位符变量,由其他属性填充它的值。

指定Eureka的服务器端口

尽管不一定是强制要求,但是我们可能想要修改默认的服务器端口。虽然Eureka非常乐意监听8080端口,但是在开发代码的时候我们可能会在本地机器同时运行多个应用(微服务),也就无法让所有的应用均监听8080端口。因此,在本地开发的时候,设置server.port属性通常是一个比较好的做法:

1
2
server:
port: 8761

在这里,我们将端口设置成了8761,这是Eureka客户端(我们将会在13.3节中进行讨论)默认监听的端口。

禁用自我保护模式

另外一个我们需要考虑设置的属性是eureka.server.enable-self-preservation。如果我们启动Eureka服务器并让它空闲一分钟以上,可能就会在Eureka UI上看到一个非常吓人的错误信息,如图13.3所示。

image-20211020205004645

图13.3 在自我保护模式下,Eureka会在dashboard显示信息

尽管这里使用了红色字体和大写字母,但是这条信息并不像看上去那么严重。Eureka希望服务实例能够注册上来,并且每隔30秒向它发送一次注册更新的请求。通常,如果Eureka在3个更新周期(或者说90秒)内没有收到服务的更新请求,就会将该服务注销。在本例中,Eureka假定出现了网络问题,进入自我保护模式,所以不会注销服务实例。

在生产环境中,自我保护模式是很好的,可以防止在出现网络故障时更新请求无法发送至Eureka所导致的活跃服务被注销。但是,在我们第一次启动Eureka并且还没有注册任何服务时候,出现这样的告警会让人产生疑虑。我们可以将eureka.server.enable-self- preservation属性设置为false,从而禁用自我保护模式:

1
2
3
4
eureka:
...
server:
enable-self-preservation: false

这个属性在开发环境中是非常有用的。在开发环境中,基于各种原因,Eureka可能会收不到更新请求。在这种环境下,我们可能会频繁地启动或关闭服务实例,自我保护模式会将已停止服务的注册项保留下来,另一个服务访问已经不可用的服务时就会产生问题。禁用自动保护模式将会防止这种诡异的问题。然而,我们付出的代价就是会看到另一条恐怖的红色信息(见图13.4)。

image-20211020205040204

图13.4 禁用自我保护模式时,提示自我保护模式已禁用

虽然我们在开发环境可以禁用自我保护模式,但是在投入生产环境时需要将其启用。

13.2.2 扩展Eureka

在开发环境中,单个Eureka实例会更加便利;但是在将应用投入生产环境时,我们可能至少需要两个Eureka实例,以实现高可用性。

生产环境可用的Spring Cloud Services

在将微服务部署到生产环境时,有许多需要考虑的因素。Eureka的高可用性和安全性在开发阶段可能并不太重要,但是在生产环境中就非常关键了。如果你是PivotalCloud Foundry或Pivotal Web Services的客户,就可以让他们来关心这些事情了。

Spring Cloud Services提供了一个Eureka实现,同时还包含了配置服务器和断路器dashboard。我们所需要做的就是从marketplace请求一个p-service-registry服务,然后将自己的微服务绑定到该服务上。在marketplace中,配置服务器和断路器dashboard(我们将会在接下来的两章中讨论它们)的名称分别为p-config-server和p-circuit-breaker-dashboard。

配置两个(或更多)Eureka实例最简单直接的方式就是在application.yml中使用Spring profile,然后针对两个profile各启动一次。例如,程序清单13.1中的配置项会将两个Eureka服务器设置为彼此对等的端。

配置两个(或更多)Eureka实例最简单直接的方式就是在application.yml中使用Spring profile,然后针对两个profile各启动一次。例如,程序清单13.1中的配置项会将两个Eureka服务器设置为彼此对等的端。

程序清单13.1 使用Spring profile将Eureka配置成两个对等的端

eureka:
  client:
    service-url:
      defaultZone: http://${other.eureka.host}:${other.eureka.port}/eureka
---
spring:
  profiles: eureka-1
  application:
    name: eureka-1
server:
  port: 8761
eureka:
  instance:
    hostname: eureka1.tacocloud.com
other:
  eureka:
    host: eureka2.tacocloud.com
    port: 8761
---
spring:
  profiles: eureka-2
  application:
    name: eureka-2
server:
  port: 8762
eureka:
  instance:
    hostname: eureka2.tacocloud.com
other:
  eureka:
    host: eureka1.tacocloud.com
    port: 8762

在默认的profile中(位于程序清单13.1顶部),我们用占位符变量来设置eureka.client. service-url.defaultZone属性,这些占位符都是在每个profile特定的配置中设置的。

在默认的profile之后,我们配置了两个profile,分别为eureka-1和eureka-2。每个profile都按照自己的配置需要指定了端口和eureka.instance.hostname。随后,我们设置了两个略显牵强的other.eureka.host和other.eureka.port属性,在每个profile中它们都指向了其他的Eureka实例。这两个属性与框架本身是没有关系的,但是在默认profile的占位符中会引用它们。

注意,我们在这里没有设置eureka.client.fetch-registry或eureka.client.register-with-eureka。它们的默认值为true,因此能够确保每个Eureka服务器都会向对方进行注册,并且能够从其他Eureka服务器上获取注册信息。

目前,Eureka服务注册中心已经启动并处于运行状态了。但是,它现在就像一个没有人查阅的空电话本。只有让服务开始在注册中心注册,并让其他服务查找和调用它们才行,否则我们的工作都是徒劳的。接下来,我们看一下如何让微服务成为Eureka的客户端。