使用 Spring Cloud 和 Netflix Eureka 进行微服务注册与发现

工程 | Josh Long | 2015年1月20日 | ...

微服务架构风格与其说是关于构建独立的微服务,不如说是关于如何让服务之间的交互变得可靠且容错。虽然对这些交互的关注是新的,但这种关注的需求并非如此。我们很早就知道服务并非独立运行。即使在云计算经济出现之前,我们也明白——在实际世界中——客户端应该设计成能够免疫服务中断。云计算使得容量可以轻松地被视为短暂、流动的。管理这种固有的复杂性就落在了客户端的肩上。

在本文中,我们将探讨 Spring Cloud 如何通过 Eureka 和 Consul 等服务注册中心以及客户端负载均衡来帮助您管理这种复杂性。

云的电话簿

服务注册中心就像是您微服务的电话簿。每个服务都将自己注册到服务注册中心,并告知注册中心它的位置(主机、端口、节点名称)以及其他可能的服务特定元数据——这些信息可供其他服务用来对它做出明智的决定。客户端可以询问关于服务拓扑结构的问题(“是否有可用的‘fulfillment-services’,如果有,在哪里?”)和服务能力(“你能处理 X、Y 和 Z 吗?”)。您可能已经在使用某种具有集群概念的技术(如 Cassandra、Memcached 等),而这些信息最好存储在服务注册中心。

这里有几种流行的服务注册中心选项。Netflix 开发并开源了自己的服务注册中心 Eureka。另一个较新但越来越受欢迎的选项是 Consul。本文将主要关注 Spring Cloud 与 Netflix Eureka 服务注册中心的集成。

来自 Spring Cloud 项目页面:“Spring Cloud 为开发人员提供工具,可以快速构建分布式系统中的一些常见模式(例如,配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导者选举、分布式会话、集群状态)。分布式系统的协调会导致样板化模式,而使用 Spring Cloud,开发人员可以快速启动实现这些模式的服务和应用程序。它们将在任何分布式环境中良好运行,包括开发人员自己的笔记本电脑、裸机数据中心以及 Cloud Foundry 等托管平台。”

Spring Cloud 已支持 Eureka 和 Consul,尽管本文将重点介绍 Eureka,因为它可以在 Spring Cloud 的自动配置中自动引导。Eureka 是用 JVM 实现的,而 Consul 是用 Go 实现的。

安装 Eureka

如果您在类路径中包含了 org.springframework.boot:spring-cloud-starter-eureka-server,那么启动一个 Eureka 服务注册中心实例将非常简单。

package registry;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class Application {

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

我现在的标准 src/main/resources/application.yml 文件如下所示。

server:
  port: ${PORT:8761}

eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false
    server:
      waitTimeInMsWhenSyncEmpty: 0

如果 Cloud Foundry 的 VCAP_APPLICATION_PORT 环境变量不可用,则服务的端口将默认设置为众所周知的 8761。其余配置只是告诉此实例不要将自己注册到它找到的 Eureka 实例,因为那个实例就是它自己。如果您在本地运行,可以通过浏览器访问 https://:8761 来监视注册中心。

部署 Eureka

Spring Cloud 将通过其 Spring Boot 自动配置启动一个 Eureka 实例。部署 Eureka 时有几点需要考虑。首先,在生产环境中,您应该始终使用高可用配置。 Spring Cloud Eureka 示例展示了如何在生产环境中以高可用配置部署它。

客户端需要知道在哪里可以找到 Eureka 实例。如果您有 DNS,这可能是一个选项,前提是您没有污染过大的全局命名空间。如果您正在平台即服务 (PaaS) 中运行并拥抱 12-Factor App 风格的应用,那么后端服务的凭据就是配置,存在于应用程序外部,通常暴露为环境变量。不过,您可以通过使用 Cloud Foundry 的 cf CLI 创建用户提供的服务来立即获得 Eureka 服务的效果。

cf cups eureka-service -p '{"uri":"http://host-of-your-eureka-setup"}'

host-of-your-eureka-setup 指向您高可用 Eureka 设置的一个知名主机。我怀疑很快就会有一种方法可以在 Pivotal Cloud Foundry 上像创建 PostgreSQL 或 ElasticSearch 实例一样,将 Eureka 作为后端服务来创建。

现在 Eureka 已经运行起来了,让我们使用它来连接一些服务!

自我声明

基于 Spring Cloud 的服务有一个 spring.application.name 属性。它用于从配置服务器拉取配置,向 Eureka 标识服务,并在构建基于 Spring Cloud 的应用程序的许多其他上下文中可引用。这个值通常位于 src/main/resources/bootstrap.(yml,properties) 中,该文件在初始化过程中比正常的 src/main/resources/application.(yml,properties) 更早被加载。一个类路径中包含 org.springframework.cloud:spring-cloud-starter-eureka 的服务将通过其 spring.application.name 注册到 Eureka 注册中心。

我每个服务的 src/main/resources/boostrap.yml 文件如下所示,其中 my-service 是从服务到服务变化的实际服务名称。

spring:
  application:
    name: my-service

Spring Cloud 在服务启动时使用 bootstrap.yml 中的信息来发现 Eureka 服务注册中心,并注册服务及其 spring.application.name、主机、端口等。您可能对第一个部分感到好奇。Spring Cloud 尝试在众所周知的地址(http://127.0.0.1:)查找它,但您可以更改它。以下是我一个标准 Spring Cloud 微服务的 src/main/resources/application.yml,但 没有任何理由不能将其放在 Spring Cloud 配置服务器中。可能有很多实例将自己标识为 my-service;Eureka 会将进程的信息附加到同一 ID 的注册列表。



eureka:
  client:
    serviceUrl:
      defaultZone: ${vcap.services.eureka-service.credentials.uri:http://127.0.0.1:8761}/eureka/

---
spring:
  profiles: cloud
eureka:
  instance:
    hostname: ${APPLICATION_DOMAIN}
    nonSecurePort: 80

在此配置中,Spring Cloud Eureka 客户端知道连接到本地运行的 Eureka 实例如果 Cloud Foundry 的 VCAP_SERVICES 环境变量不存在或不包含有效凭据。

--- 分隔符下的配置部分是用于应用程序cloud Spring profile 下运行时。使用 SPRING_PROFILES_ACTIVE 环境变量可以轻松设置 profile。您可以在 manifest.yml 中配置 Cloud Foundry 环境变量,或者在 Cloud Foundry Lattice 上,在您的 Docker 文件中配置。

cloud profile 特定的配置明确地告诉 Eureka 客户端如何将服务注册到发现的 Eureka 注册中心。我这样做是因为我的服务不使用固定的 DNS。APPLICATION_DOMAIN 是我在部署脚本中设置的一个环境变量,它告诉服务其外部可引用的 URI 是什么。

约 30 秒后(截至本文撰写时),刷新 Eureka Web UI,您将看到您的 Web 服务已注册。

使用 Ribbon 进行客户端负载均衡

Spring Cloud 通过服务的 spring.application.name 值引用其他服务。在构建基于 Spring Cloud 的服务时,了解这个值在很多上下文中都很有用。

如您所知,目标是让客户端根据上下文信息(可能因客户端而异)决定它将连接到哪个服务实例。Netflix 有一个名为 Ribbon 的 Eureka 感知客户端负载均衡器,Spring Cloud 与之进行了广泛的集成。Ribbon 是一个内置软件负载均衡器的客户端库。让我们来看一个直接使用 Eureka 的示例,然后展示如何通过 Ribbon 和 Spring Cloud 集成来使用它。

package passport;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class)
                .web(false)
                .run(args);
    }
}

@Component
class DiscoveryClientExample implements CommandLineRunner {

    @Autowired
    private DiscoveryClient discoveryClient;

    @Override
    public void run(String... strings) throws Exception {
        discoveryClient.getInstances("photo-service").forEach((ServiceInstance s) -> {
            System.out.println(ToStringBuilder.reflectionToString(s));
        });
        discoveryClient.getInstances("bookmark-service").forEach((ServiceInstance s) -> {
            System.out.println(ToStringBuilder.reflectionToString(s));
        });
    }
}

@Component
class RestTemplateExample implements CommandLineRunner {

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public void run(String... strings) throws Exception {
        // use the "smart" Eureka-aware RestTemplate
        ResponseEntity<List<Bookmark>> exchange =
                this.restTemplate.exchange(
                        "http://bookmark-service/{userId}/bookmarks",
                        HttpMethod.GET,
                        null,
                        new ParameterizedTypeReference<List<Bookmark>>() {
                        },
                        (Object) "mstine");

        exchange.getBody().forEach(System.out::println);
    }

}

@Component
class FeignExample implements CommandLineRunner {

    @Autowired
    private BookmarkClient bookmarkClient;

    @Override
    public void run(String... strings) throws Exception {
        this.bookmarkClient.getBookmarks("jlong").forEach(System.out::println);
    }
}

@FeignClient("bookmark-service")
interface BookmarkClient {

    @RequestMapping(method = RequestMethod.GET, value = "/{userId}/bookmarks")
    List<Bookmark> getBookmarks(@PathVariable("userId") String userId);
}

class Bookmark {
    private Long id;
    private String href, label, description, userId;

    @Override
    public String toString() {
        return "Bookmark{" +
                "id=" + id +
                ", href='" + href + '\'' +
                ", label='" + label + '\'' +
                ", description='" + description + '\'' +
                ", userId='" + userId + '\'' +
                '}';
    }

    public Bookmark() {
    }

    public Long getId() {
        return id;
    }

    public String getHref() {
        return href;
    }

    public String getLabel() {
        return label;
    }

    public String getDescription() {
        return description;
    }

    public String getUserId() {
        return userId;
    }
}

DiscoveryClientExample bean 演示了如何使用 Spring Cloud 通用的 DiscoveryClient 来查询服务。结果包含每个服务的托管名称和端口等信息。

RestTemplateExample bean 演示了自动配置的 Ribbon 感知 RestTemplate 实例。请注意,URI 使用服务 ID 而不是实际的托管名称。URI 中的服务 ID 被提取出来并传递给 Ribbon,Ribbon 使用负载均衡器从 Eureka 中注册的实例中进行选择,最后,HTTP 调用被发送到一个实际的服务实例。

FeignExample bean 演示了如何使用 Spring Cloud Feign 集成。 Feign 是 Netflix 提供的一个方便的项目,它允许您通过接口上的注解以声明式的方式描述 REST API 客户端。在这种情况下,我们希望将对 bookmark-service 的调用的 HTTP 结果映射到 BookmarkClient Java 接口。这种映射是在代码页面顶部的 Application 类中配置的。

  @Bean
  BookmarkClient bookmarkClient() {
    return loadBalance(BookmarkClient.class, "http://bookmark-service");
  }

URI 是一个服务引用,而不是实际的托管名称。它会经过与上一个示例中传递给 RestTemplate 的 URI 相同的处理。

很酷,对吧?您可以使用更基本的 DiscoveryClient API 进行调用,或者使用 Ribbon 和 Eureka 感知的 RestTemplate 或 Feign 集成的客户端。

回顾

  • Spring Cloud 支持 Eureka 和 Consul 服务注册中心(可能还有更多!)。
  • DiscoveryClient API 可用于根据服务 ID 与 Eureka 进行交互式查询。
  • Ribbon 是一个客户端负载均衡器。
  • RestTemplate 可以在 URI 中用服务 ID 替换托管名称,并可以委托给 Ribbon 来选择服务。
  • Netflix Spring Cloud Feign 集成使得创建智能、Eureka 感知的 REST 客户端变得简单,这些客户端使用 Ribbon 进行客户端负载均衡来选择可用的服务实例。

下一步

我们只看了 Eureka 的服务发现和解析。我们在这里谈论的大部分内容也适用于 Consul,而且 Consul 确实具有一些 Netflix 没有的功能。

轮询负载均衡只是一种选择。您可能更需要某种领导者节点概念,以及领导者选举。Spring Cloud 也旨在支持这种协调。

服务注册和客户端负载均衡只是 Spring Cloud 为促进更具弹性的服务间调用所做的一项工作。我们尚未介绍它对单点登录和安全、分布式锁和领导者选举、断路器等可靠性模式的支持,以及更多内容。

示例代码在线提供,所以请随时在本地机器上查看示例,或者使用提供的 cf.sh 脚本和各种 manifest.yml 文件将其推送到 Cloud Foundry。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有