使用 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 FoundryVCAP_APPLICATION_PORT 环境变量不可用,则服务的端口默认为众所周知的 8761。其余配置只是告诉此实例不要向它找到的 Eureka 实例注册自身,因为该实例是…它自己。如果您在本地运行它,则可以将浏览器指向 https://127.0.0.1:8761 并从那里监控注册中心。

部署 Eureka

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

客户端需要知道在哪里找到 Eureka 实例。如果您有 DNS,那么这可能是一个选项,如果您没有污染过大的全局命名空间。如果您在 Platform-as-a-Service 中运行并采用 12-Factor 应用程序风格的应用程序,那么后端服务凭据就是配置,并且存在于应用程序外部,通常以环境变量的形式公开。不过,您可以立即获得拥有 Eureka 服务的效果,方法是使用 Cloud Foundry 的 cf CLI 创建用户提供的服务

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

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

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

cloud 配置文件特定的配置专门告诉 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 可以用服务 ID 替换 URI 中的主机名,并且可以委托给 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 社区中所有即将举行的活动。

查看全部