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

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

微服务架构风格与其说是构建独立的各个服务,不如说是让服务之间的交互变得可靠和容错。虽然对这些交互的关注是新的,但对其的需求并不是。我们早就知道服务不是在真空中运行的。即使在云经济之前,我们就知道——在实际世界中——客户端应该设计成能够抵御服务中断。云使得将容量视为短暂、流动的变得容易。管理这种内在复杂性的负担落在了客户端身上。

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

云的电话簿

服务注册中心就像您微服务的电话簿。每个服务都向服务注册中心注册自己,并告诉注册中心它在哪里(主机、端口、节点名称),或许还有其他服务特定的元数据——其他服务可以使用这些信息来做出明智的决策。客户端可以询问服务拓扑结构的问题(“是否有任何可用的 '履行服务',如果有,它们在哪里?”)和服务能力的问题(“你能处理 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 FoundryVCAP_APPLICATION_PORT 环境变量不可用,服务的端口默认设置为周知端口 8761。其余配置只是告诉这个实例不要向它找到的 Eureka 实例注册自己,因为那个实例就是... 它自己。如果您在本地运行它,可以将浏览器指向 http://localhost: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 高可用设置的一个周知主机。我猜我们很快就会看到一种将 Eureka 创建为后端服务的方式,就像您在 Pivotal Cloud Foundry 上创建 PostgreSQL 或 ElasticSearch 实例一样。

现在 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 客户端知道如果 Cloud Foundry 的 VCAP_SERVICES 环境变量不存在或不包含有效凭据,则连接到在 localhost 上运行的 Eureka 实例。

--- 分隔符下的配置部分是当应用程序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 有一个感知 Eureka 的客户端负载均衡客户端,名为 Ribbon,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 社区所有即将举行的活动。

查看全部