Spring Cloud - 使用 Eureka 进行服务发现


介绍

当应用程序作为微服务部署在云中时,服务发现是最关键的部分之一。这是因为对于任何使用操作,微服务架构中的应用程序可能需要访问多个服务以及它们之间的通信。

服务发现有助于跟踪服务地址和可以联系服务实例的端口。这里有三个组成部分 -

  • 服务实例- 负责处理传入的服务请求并响应这些请求。

  • 服务注册表- 跟踪服务实例的地址。服务实例应该向服务注册表注册其地址。

  • 服务客户端- 想要访问或想要发出请求并从服务实例获取响应的客户端。服务客户端联系服务注册表以获取实例的地址。

Apache Zookeeper、Eureka 和 Consul 是一些用于服务发现的著名组件。在本教程中,我们将使用 Eureka

设置 Eureka 服务器/注册表

为了设置 Eureka Server,我们需要更新 POM 文件以包含以下依赖项 -

<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-web</artifactId>
   </dependency>
</dependencies>

然后,使用正确的注释来注释我们的 Spring 应用程序类,即@EnableEurekaServer。

package com.tutorialspoint;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class RestaurantServiceRegistry{
   public static void main(String[] args) {
      SpringApplication.run(RestaurantServiceRegistry.class, args);
   }
}

如果我们想要配置注册表并更改其默认值,我们还需要一个属性文件。以下是我们将做出的改变 -

  • 将端口更新为 8900 而不是默认的 8080

  • 在生产中,为了实现高可用性,将有多个节点用于注册。这就是我们需要注册管理机构之间进行点对点通信的地方。当我们在独立模式下执行此操作时,我们可以简单地将客户端属性设置为false以避免任何错误。

所以,这就是我们的application.yml文件的样子 -

server:
   port: 8900
eureka:
   client:
      register-with-eureka: false
      fetch-registry: false

就是这样,现在让我们使用以下命令编译项目并运行程序 -

java -jar .\target\spring-cloud-eureka-server-1.0.jar

现在我们可以在控制台中看到日志 -

...
2021-03-07 13:33:10.156 INFO 17660 --- [ main]
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8900
(http)
2021-03-07 13:33:10.172 INFO 17660 --- [ main]
o.apache.catalina.core.StandardService : Starting service [Tomcat]
...
2021-03-07 13:33:16.483 INFO 17660 --- [ main]
DiscoveryClientOptionalArgsConfiguration : Eureka HTTP Client uses Jersey
...
2021-03-07 13:33:16.632 INFO 17660 --- [ main]
o.s.c.n.eureka.InstanceInfoFactory : Setting initial instance status as:
STARTING
2021-03-07 13:33:16.675 INFO 17660 --- [ main]
com.netflix.discovery.DiscoveryClient : Initializing Eureka in region useast-
1
2021-03-07 13:33:16.675 INFO 17660 --- [ main]
com.netflix.discovery.DiscoveryClient : Client configured to neither register
nor query for data.
2021-03-07 13:33:16.686 INFO 17660 --- [ main]
com.netflix.discovery.DiscoveryClient : Discovery Client initialized at
timestamp 1615104196685 with initial instances count: 0
...
2021-03-07 13:33:16.873 INFO 17660 --- [ Thread-10]
e.s.EurekaServerInitializerConfiguration : Started Eureka Server
2021-03-07 13:33:18.609 INFO 17660 --- [ main]
c.t.RestaurantServiceRegistry : Started RestaurantServiceRegistry in
15.219 seconds (JVM running for 16.068)

从上面的日志中我们可以看到,Eureka 注册表已经设置完毕。我们还获得了 Eureka 的仪表板(参见下图),该仪表板托管在服务器 URL 上。

尤里卡仪表板

设置 Eureka 客户端实例

现在,我们将设置将注册到 Eureka 服务器的服务实例。为了设置 Eureka Client,我们将使用单独的 Maven 项目并更新 POM 文件以包含以下依赖项 -

<dependencies>
   <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
</dependencies>

然后,使用正确的注释来注释我们的 Spring 应用程序类,即@EnableDiscoveryClient

package com.tutorialspoint;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class RestaurantCustomerService{
   public static void main(String[] args) {
      SpringApplication.run(RestaurantCustomerService.class, args);
   }
}

如果我们想要配置客户端并更改其默认值,我们还需要一个属性文件。以下是我们将做出的改变 -

  • 我们将在运行时提供端口,而在执行时提供 jar。

  • 我们将指定 Eureka 服务器运行的 URL。

所以,这就是我们的 application.yml 文件的样子

spring:
   application:
      name: customer-service
server:
   port: ${app_port}
eureka:
   client:
      serviceURL:
         defaultZone: http://localhost:8900/eureka

为了执行,我们将运行两个服务实例。为此,我们打开两个 shell,然后在一个 shell 上执行以下命令 -

java -Dapp_port=8081 -jar .\target\spring-cloud-eureka-client-1.0.jar

并在另一个 shell 上执行以下命令 -

java -Dapp_port=8082 -jar .\target\spring-cloud-eureka-client-1.0.jar

现在我们可以在控制台中看到日志 -

...
2021-03-07 15:22:22.474 INFO 16920 --- [ main]
com.netflix.discovery.DiscoveryClient : Starting heartbeat executor: renew
interval is: 30
2021-03-07 15:22:22.482 INFO 16920 --- [ main]
c.n.discovery.InstanceInfoReplicator : InstanceInfoReplicator onDemand
update allowed rate per min is 4
2021-03-07 15:22:22.490 INFO 16920 --- [ main]
com.netflix.discovery.DiscoveryClient : Discovery Client initialized at
timestamp 1615110742488 with initial instances count: 0
2021-03-07 15:22:22.492 INFO 16920 --- [ main]
o.s.c.n.e.s.EurekaServiceRegistry : Registering application CUSTOMERSERVICE
with eureka with status UP
2021-03-07 15:22:22.494 INFO 16920 --- [ main]
com.netflix.discovery.DiscoveryClient : Saw local status change event
StatusChangeEvent [timestamp=1615110742494, current=UP, previous=STARTING]
2021-03-07 15:22:22.500 INFO 16920 --- [nfoReplicator-0]
com.netflix.discovery.DiscoveryClient : DiscoveryClient_CUSTOMERSERVICE/
localhost:customer-service:8081: registering service...
2021-03-07 15:22:22.588 INFO 16920 --- [ main]
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081
(http) with context path ''
2021-03-07 15:22:22.591 INFO 16920 --- [ main]
.s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8081
2021-03-07 15:22:22.705 INFO 16920 --- [nfoReplicator-0]
com.netflix.discovery.DiscoveryClient : DiscoveryClient_CUSTOMERSERVICE/
localhost:customer-service:8081 - registration status: 204
...

正如我们从上面的日志中看到的,客户端实例已经设置完毕。我们还可以查看之前看到的 Eureka Server 仪表板。正如我们所看到的,Eureka 服务器知道有两个正在运行的“客户服务”实例 -

设置 Eureka 客户端实例

Eureka客户端消费者示例

我们的尤里卡服务器已经获得了“客户服务”设置的注册客户端实例。我们现在可以设置消费者,它可以向尤里卡服务器询问“客户服务”节点的地址。

为此,我们添加一个可以从 Eureka 注册表获取信息的控制器。该控制器将被添加到我们之前的 Eureka 客户端本身,即“客户服务”。让我们为客户端创建以下控制器。

package com.tutorialspoint;
import java.util.List;
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;
@RestController
class RestaurantCustomerInstancesController {
   @Autowired
   private DiscoveryClient eurekaConsumer;
   @RequestMapping("/customer_service_instances")

请注意注释 @DiscoveryClient,这是 Spring 框架提供的用于与注册表通信的注释。

现在让我们重新编译我们的 Eureka 客户端。为了执行,我们将运行两个服务实例。为此,我们打开两个 shell,然后在一个 shell 上执行以下命令 -

java -Dapp_port=8081 -jar .\target\spring-cloud-eureka-client-1.0.jar

并在另一个 shell 上执行以下命令 -

java -Dapp_port=8082 -jar .\target\spring-cloud-eureka-client-1.0.jar

两个 shell 上的客户端启动后,现在让我们点击我们在控制器中创建的 http://localhost:8081/customer_service_instances。此 URL 显示有关两个实例的完整信息。

[
   {
      "scheme": "http",
      "host": "localhost",
      "port": 8081,
      "metadata": {
         "management.port": "8081"
      },
      "secure": false,
      "instanceInfo": {
         "instanceId": "localhost:customer-service:8081",
         "app": "CUSTOMER-SERVICE",
         "appGroupName": null,
         "ipAddr": "10.0.75.1",
         "sid": "na",
         "homePageUrl": "http://localhost:8081/",
         "statusPageUrl": "http://localhost:8081/actuator/info",
         "healthCheckUrl": "http://localhost:8081/actuator/health",
         "secureHealthCheckUrl": null,
         "vipAddress": "customer-service",
         "secureVipAddress": "customer-service",
         "countryId": 1,
         "dataCenterInfo": {
            "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
            "name": "MyOwn"
         },
         "hostName": "localhost",
         "status": "UP",
         "overriddenStatus": "UNKNOWN",
         "leaseInfo": {
            "renewalIntervalInSecs": 30,
            "durationInSecs": 90,
            "registrationTimestamp": 1616667914313,
            "lastRenewalTimestamp": 1616667914313,
            "evictionTimestamp": 0,
            "serviceUpTimestamp": 1616667914313
         },
         "isCoordinatingDiscoveryServer": false,
         "metadata": {
            "management.port": "8081"
         },
         "lastUpdatedTimestamp": 1616667914313,
         "lastDirtyTimestamp": 1616667914162,
         "actionType": "ADDED",
         "asgName": null
      },
      "instanceId": "localhost:customer-service:8081",
      "serviceId": "CUSTOMER-SERVICE",
      "uri": "http://localhost:8081"
   },
   {
      "scheme": "http",
      "host": "localhost",
      "port": 8082,
      "metadata": {
         "management.port": "8082"
      },
      "secure": false,
      "instanceInfo": {
      "instanceId": "localhost:customer-service:8082",
      "app": "CUSTOMER-SERVICE",
      "appGroupName": null,
      "ipAddr": "10.0.75.1",
      "sid": "na",
      "homePageUrl": "http://localhost:8082/",
      "statusPageUrl": "http://localhost:8082/actuator/info",
      "healthCheckUrl": "http://localhost:8082/actuator/health",
      "secureHealthCheckUrl": null,
      "vipAddress": "customer-service",
      "secureVipAddress": "customer-service",
      "countryId": 1,
      "dataCenterInfo": {
         "@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo",
         "name": "MyOwn"
      },
      "hostName": "localhost",
      "status": "UP",
      "overriddenStatus": "UNKNOWN",
      "leaseInfo": {
         "renewalIntervalInSecs": 30,
         "durationInSecs": 90,
         "registrationTimestamp": 1616667913690,
         "lastRenewalTimestamp": 1616667913690,
         "evictionTimestamp": 0,
         "serviceUpTimestamp": 1616667913690
      },
      "isCoordinatingDiscoveryServer": false,
      "metadata": {
         "management.port": "8082"
      },
      "lastUpdatedTimestamp": 1616667913690,
      "lastDirtyTimestamp": 1616667913505,
      "actionType": "ADDED",
      "asgName": null
     },
     "instanceId": "localhost:customer-service:8082",
     "serviceId": "CUSTOMER-SERVICE",
     "uri": "http://localhost:8082"
   }
]

尤里卡服务器API

Eureka Server 为客户端实例或服务提供各种 API 进行通信。其中很多 API 都是抽象的,可以直接与我们之前定义和使用的 @DiscoveryClient 一起使用。需要注意的是,它们的 HTTP 对应项也存在,并且对于 Eureka 的非 Spring 框架使用非常有用。

事实上,我们之前使用的API,即获取有关运行“Customer_Service”的客户端的信息,也可以通过浏览器使用http://localhost:8900/eureka/apps/customer-service来调用,如图所示这里 -

<application slick-uniqueid="3">
   <div>
      <a id="slick_uniqueid"/>
   </div>
   <name>CUSTOMER-SERVICE</name>
   <instance>
         <instanceId>localhost:customer-service:8082</instanceId>
         <hostName>localhost</hostName>
         <app>CUSTOMER-SERVICE</app>
         <ipAddr>10.0.75.1</ipAddr>
         <status>UP</status>
         <overriddenstatus>UNKNOWN</overriddenstatus>
         <port enabled="true">8082</port>
         <securePort enabled="false">443</securePort>
         <countryId>1</countryId>
         <dataCenterInfo
class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
               <name>MyOwn</name>
         </dataCenterInfo>
         <leaseInfo>
            <renewalIntervalInSecs>30</renewalIntervalInSecs>
            <durationInSecs>90</durationInSecs>
            <registrationTimestamp>1616667913690</registrationTimestamp>
            <lastRenewalTimestamp>1616668273546</lastRenewalTimestamp>
            <evictionTimestamp>0</evictionTimestamp>
            <serviceUpTimestamp>1616667913690</serviceUpTimestamp>
         </leaseInfo>
         <metadata>
            <management.port>8082</management.port>
         </metadata>
         <homePageUrl>http://localhost:8082/</homePageUrl>
         <statusPageUrl>http://localhost:8082/actuator/info</statusPageUrl>
   <healthCheckUrl>http://localhost:8082/actuator/health</healthCheckUrl>
         <vipAddress>customer-service</vipAddress>
         <secureVipAddress>customer-service</secureVipAddress>
         <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
         <lastUpdatedTimestamp>1616667913690</lastUpdatedTimestamp>
         <lastDirtyTimestamp>1616667913505</lastDirtyTimestamp>
         <actionType>ADDED</actionType>
   </instance>
   <instance>
         <instanceId>localhost:customer-service:8081</instanceId>
         <hostName>localhost</hostName>
         <app>CUSTOMER-SERVICE</app>
         <ipAddr>10.0.75.1</ipAddr>
         <status>UP</status>
         <overriddenstatus>UNKNOWN</overriddenstatus>
         <port enabled="true">8081</port>
         <securePort enabled="false">443</securePort>
         <countryId>1</countryId>
         <dataCenterInfo
class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
            <name>MyOwn</name>
         </dataCenterInfo>
         <leaseInfo>
               <renewalIntervalInSecs>30</renewalIntervalInSecs>
               <durationInSecs>90</durationInSecs>
               <registrationTimestamp>1616667914313</registrationTimestamp>
               <lastRenewalTimestamp>1616668274227</lastRenewalTimestamp>
               <evictionTimestamp>0</evictionTimestamp>
               <serviceUpTimestamp>1616667914313</serviceUpTimestamp>
         </leaseInfo>
         <metadata>
            <management.port>8081</management.port>
         </metadata>
         <homePageUrl>http://localhost:8081/</homePageUrl>
         <statusPageUrl>http://localhost:8081/actuator/info</statusPageUrl>
   <healthCheckUrl>http://localhost:8081/actuator/health</healthCheckUrl>
         <vipAddress>customer-service</vipAddress>
         <secureVipAddress>customer-service</secureVipAddress>
         <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
         <lastUpdatedTimestamp>1616667914313</lastUpdatedTimestamp>
         <lastDirtyTimestamp>1616667914162</lastDirtyTimestamp>
         <actionType>ADDED</actionType>
   </instance>
</application>

其他有用的 API 很少有:

行动 应用程序编程接口
注册新服务 POST /eureka/apps/{appIdentifier}
注销服务 删除 /eureka/apps/{appIdentifier}
有关服务的信息 GET /eureka/apps/{appIdentifier}
有关服务实例的信息 GET /eureka/apps/{appIdentifier}/ {instanceId}

有关编程 API 的更多详细信息,请访问https://javadoc.io/doc/com.netflix.eureka/eureka-client/latest/index.html

尤里卡 – 高可用性

我们一直在独立模式下使用 Eureka 服务器。然而,在生产环境中,理想情况下我们应该运行多个 Eureka 服务器实例。这保证了即使一台机器宕机了,另一台Eureka服务器的机器仍能继续运行。

让我们尝试以高可用性模式设置 Eureka 服务器。对于我们的示例,我们将使用两个实例。为此,我们将使用以下application-ha.yml来启动 Eureka 服务器。

注意事项-

  • 我们已经参数化了端口,以便我们可以使用相同的配置文件启动多个实例。

  • 我们添加了地址,再次参数化,以传递 Eureka 服务器地址。

  • 我们将该应用程序命名为“Eureka-Server”。

spring:
   application:
      name: eureka-server
server:
   port: ${app_port}
eureka:
   client:
      serviceURL:
         defaultZone: ${eureka_other_server_url}

现在让我们重新编译我们的 Eureka 服务器项目。为了执行,我们将运行两个服务实例。为此,我们打开两个 shell,然后在一个 shell 上执行以下命令 -

java -Dapp_port=8900 '-Deureka_other_server_url=http://localhost:8901/eureka' -
jar .\target\spring-cloud-eureka-server-1.0.jar --
spring.config.location=classpath:application-ha.yml

并在另一个 shell 上执行以下命令 -

java -Dapp_port=8901 '-Deureka_other_server_url=http://localhost:8900/eureka' -
jar .\target\spring-cloud-eureka-server-1.0.jar --
spring.config.location=classpath:application-ha.yml

我们可以通过查看仪表板来验证服务器是否已启动并在高可用性模式下运行。例如,这是 Eureka 服务器 1 上的仪表板 -

Eureka 服务器 1 上的仪表板

这是 Eureka 服务器 2 的仪表板 -

Eureka Server 2 仪表板

因此,正如我们所看到的,我们有两个正在运行且同步的 Eureka 服务器。即使一台服务器出现故障,另一台服务器也会继续运行。

我们还可以通过使用逗号分隔的服务器地址来更新服务实例应用程序,以拥有两个 Eureka 服务器的地址。

spring:
   application:
      name: customer-service
server:
   port: ${app_port}
eureka:
   client:
      serviceURL:
         defaultZone: http://localhost:8900/eureka,
http://localhost:8901/eureka

尤里卡 – 区域意识

Eureka 还支持区域意识的概念。当我们拥有跨不同地理位置的集群时,区域意识作为一个概念非常有用。比如说,我们收到一个传入的服务请求,我们需要选择应该为该请求提供服务的服务器。与其在距离较远的服务器上发送和处理该请求,不如选择位于同一区域的服务器更为有效。这是因为,网络瓶颈在分布式应用程序中很常见,因此我们应该避免它。

现在让我们尝试设置 Eureka 客户端并让它们感知区域。为此,我们添加application-za.yml

spring:
   application:
      name: customer-service
server:
   port: ${app_port}
eureka:
   instance:
      metadataMap:
         zone: ${zoneName}
   client:
      serviceURL:
         defaultZone: http://localhost:8900/eureka

现在让我们重新编译我们的 Eureka 客户端项目。为了执行,我们将运行两个服务实例。为此,我们打开两个 shell,然后在一个 shell 上执行以下命令 -

java -Dapp_port=8080 -Dzone_name=USA -jar .\target\spring-cloud-eureka-client-
1.0.jar --spring.config.location=classpath:application-za.yml

并在另一个 shell 上执行以下命令 -

java -Dapp_port=8081 -Dzone_name=EU -jar .\target\spring-cloud-eureka-client-
1.0.jar --spring.config.location=classpath:application-za.yml

我们可以返回仪表板来验证 Eureka Server 是否注册了服务的区域。如下图所示,我们有两个可用区,而不是到目前为止我们看到的 1 个可用区。

尤里卡服务器

现在,任何客户端都可以查看它所在的区域。假设客户端位于美国,它会更喜欢美国的服务实例。并且它可以从Eureka Server获取区域信息。