Spring Cloud 简明教程

Spring Cloud - Service Discovery Using Eureka

Introduction

当应用程序作为云中的微服务部署时,服务发现是其中最关键的部分之一。这是因为,对于任何使用操作,微服务架构中的应用程序可能需要访问多个服务,并进行相互通信。

服务发现有助于跟踪服务地址以及可以联系到服务实例的端口。这里有三个组件在发挥作用 -

  1. Service Instances - 负责处理服务传入的请求并响应这些请求。

  2. Service Registry - 跟踪服务实例的地址。服务实例应该向服务注册中心注册其地址。

  3. Service Client - 想要访问或想要放置请求并从服务实例获得响应的客户端。服务客户端联系服务注册中心以获取实例的地址。

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

Setting up Eureka Server/Registry

要设置 Eureka 服务器,我们需要更新 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);
   }
}

如果我们想要配置注册表并更改其默认值,还需要一个 properties file 。以下是我们将做出的一些更改:

  1. 将端口更新为 8900,而不是默认的 8080

  2. 在生产中,注册表将具有多个节点以实现高可用性。这就是我们需要注册表之间的点对点通信的位置。由于我们在独立模式中执行此操作,因此我们可以简单地将客户端属性设置为 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 上。

dashboard for eureka

Setting up Eureka Client for Instance

现在,我们将设置将注册到 Eureka 服务器的服务实例。为了设置 Eureka 客户端,我们将使用一个单独的 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);
   }
}

如果我们想要配置客户端并更改其默认值,还需要一个 properties file 。以下是我们将做出的一些更改:

  1. 我们将在执行时通过 jar 包提供端口。

  2. 我们将指定运行 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 服务器仪表板。正如我们所见,Eureka 服务器了解到有正在运行的两个“CUSTOMER-SERVICE”实例:

setting up eureka client for instance

Eureka Client Consumer Example

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

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

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 上启动后,我们现在进入控制器中创建的 [role="bare"] [role="bare"]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"
   }
]

Eureka Server API

Eureka 服务器为客户端实例或服务相互通信提供各种 API。这些 API 中有很多是抽象的,可以直接与我们之前定义和使用的 @DiscoveryClient 一起使用。需要注意的是,它们也有 HTTP 对应项,可用于 Eureka 的非 Spring 框架用法。

实际上,我们之前使用的 API,即获取有关运行“Customer_Service”的客户端的信息,也可以通过浏览器使用 [role="bare"] https://javadoc.io/doc/com.netflix.eureka/eureka-client/latest/index.html 调用,如下所示:

<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 包括:

Action

API

Register a new service

POST /eureka/apps/{appIdentifier}

Deregister the service

DELTE /eureka/apps/{appIdentifier}

Information about the service

GET /eureka/apps/{appIdentifier}

服务实例信息

GET /eureka/apps/{appIdentifier}/ {instanceId}

有关编程 API 的更多详细信息可以在此处找到 https://javadoc.io/doc/com.netflix.eureka/eureka-client/latest/index.html

Eureka – High Availability

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

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

Points to note -

  1. 我们对端口进行参数化,以便我们可以使用相同的配置文件启动多个实例。

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

  3. 我们正在将该应用命名为“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 上的仪表盘:

dashboard on eureka server1

以下是 Eureka 服务器 2 的仪表盘:

dashboard on eureka server2

因此,正如我们所看到的,我们有两个正在运行且处于同步状态的 Eureka 服务器。即使一台服务器宕机,另一台服务器也将持续工作。

我们还可以更新服务实例应用程序,以便通过逗号分隔的服务器地址为两个 Eureka 服务器提供地址。

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

Eureka – Zone Awareness

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 客户端项目。为了执行,我们将运行两个服务实例。为此,让我们打开两个外壳,然后在一个外壳上执行以下命令 −

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 服务器是否注册了服务区域。在下图中看到,我们有两个可用区,而不是迄今为止所见的 1 个。

eureka server

现在,任何客户端都可以查看它所处的区域。比如,如果客户端在美国,它将选择美国的实例服务。它可以从 Eureka 服务器获取区域信息。