微服务架构
你当前正在浏览历史版本 Vert.x 4.5.35 的文档。 访问本页面的 最新版本。

Vert.x 服务发现(Service Discovery)

该组件提供了发布与发现各类资源(例如服务代理、HTTP 端点、数据源等)的基础设施。这些资源统称为 服务(Service)。服务 是一种可被发现的功能单元,可以通过其类型、元数据和位置来进行限定。因此,只要能够对其进行描述、发现并与之交互,服务 可以是数据库、服务代理、HTTP 端点以及你所能想象到的任何资源。它不一定非要是 Vert.x 实体,而可以是任意形式。每个服务都由一个 Record(服务记录)进行描述。

服务发现实现了面向服务计算(Service-Oriented Computing)中定义的交互模式,并在一定程度上提供了动态的面向服务计算交互能力。因此,应用程序可以对服务的上线(Arrival)与下线(Departure)做出响应。

服务提供者(Service Provider)可以:

  • 发布服务 Record

  • 注销(Un-publish)已发布的 Record

  • 更新已发布服务的状态(如 down、out of service 等)

服务消费者(Service Consumer)可以:

  • 查找服务

  • 绑定到所选服务(获取一个 ServiceReference 服务引用)并使用它

  • 在使用完毕后释放该服务

  • 监听服务的上线、下线及状态变更。

消费者的典型流程包括:1) 查找满足其需求的 Service Record;2) 获取用于访问该服务的 ServiceReference;3) 获取用于访问该服务的服务对象;4) 在使用完成后释放该服务对象。

该流程可以通过 服务类型(Service Type) 进一步简化——如果你已知其服务类型(如 HTTP 客户端等),则可以直接获取服务对象。

如上所述,提供者与消费者之间共享的核心信息载体就是 Record。

提供者与消费者必须创建各自的 ServiceDiscovery 实例。这些实例在后台相互协作(通过分布式数据结构),以保持服务集合的同步。

服务发现还支持 Bridge(网桥),用于向其他服务发现技术导入或导出服务。

使用服务发现

要在你的项目中使用 Vert.x Service Discovery,请将以下依赖添加到构建描述文件的 dependencies 部分:

  • Maven(在 pom.xml 中):

<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-service-discovery</artifactId>
<version>5.0.12</version>
</dependency>
  • Gradle(在 build.gradle 文件中):

compile 'io.vertx:vertx-service-discovery:5.0.12'

整体概念

服务发现机制基于本节介绍的几个核心概念。

服务记录(Service Record)

服务 Record 是描述由服务提供者发布之服务的对象。它包含名称、若干元数据(Metadata)以及一个位置对象(Location,描述服务所在的具体位置)。该 Record 是提供者(发布它)与消费者(通过查找获取它)之间共享的唯一对象。

元数据乃至位置格式均取决于具体的 服务类型(详见下文)。

当提供者准备好接受请求时发布 Record,并在服务提供者停止运行时将其注销。

服务提供者与发布者

服务提供者是提供 服务 的实体。发布者(Publisher)负责发布描述该提供者的 Record。发布者可以是提供者自身(自发布),也可以是一个独立的实体。

服务消费者

服务消费者在服务发现中搜索服务。每次查找会检索到 0..n 个 Record。消费者可以从这些 Record 中获取 ServiceReference,它代表了消费者与提供者之间的绑定关系。 该引用允许消费者获取 服务对象(用于使用服务),并在使用完毕后释放该服务。

及时释放服务引用非常重要,这样可以清理对象并更新服务的使用情况(Service Usage)。

服务对象

服务对象是提供对服务访问能力的对象。它可以表现为多种形式,例如代理(Proxy)、客户端(Client),对于某些服务类型甚至可以不存在。服务对象的形态取决于具体的服务类型。

请注意,由于 Vert.x 的多语言特性,从 Java、Groovy 或其他语言获取的服务对象可能会有所不同。

服务类型

服务本质上就是资源,且存在多种不同类型的服务。它们可以是功能性业务服务、数据库、REST API 等。Vert.x Service Discovery 引入了服务类型(Service Type)的概念来处理这种异构性。每种类型定义了:

  • 如何定位服务(URI、Event Bus 地址、IP / DNS 等)—— 位置(Location)

  • 服务对象的形态(服务代理、HTTP 客户端、消息消费者等)—— 客户端(Client)

部分服务类型已由服务发现组件内置提供,但你也可以添加自定义服务类型。

服务事件

每当有服务提供者发布或注销时,Event Bus 上都会触发一个事件。该事件包含被修改的 Record。

此外,为了追踪“谁在使用谁”,每当通过 getReference 获取引用,或者通过 release 释放引用时,Event Bus 上都会发出事件以追踪服务的使用情况。

有关这些事件的更多细节请参见下文。

后端(Backend)

服务发现使用 Vert.x 分布式数据结构来存储 Record。因此,集群中的所有成员都可以访问所有 Record。这是默认的后端实现。你可以通过实现 ServiceDiscoveryBackend SPI 来构建自己的后端。例如,我们提供了一个基于 Redis 的实现。

请注意,服务发现并不强制要求开启 Vert.x 集群模式。在单节点模式下,该数据结构是本地的。它可以通过各类 ServiceImporter 来填充。自 3.5.0 起,即使在集群模式下,你也可以通过将系统属性 vertx-service-discovery-backend-local 设置为 true(或将环境变量 VERTX-SERVICE-DISCOVERY-BACKEND-LOCAL 设置为 true)来使用本地数据结构。

创建服务发现实例

发布者与消费者必须创建各自的 ServiceDiscovery 实例才能使用服务发现基础设施:

ServiceDiscovery discovery = ServiceDiscovery.create(vertx);

// 自定义配置
discovery = ServiceDiscovery.create(vertx,
    new ServiceDiscoveryOptions()
        .setAnnounceAddress("service-announce")
        .setName("my-name"));

// 执行某些操作...

discovery.close();

默认情况下,通告地址(用于发送服务事件的 Event Bus 地址)为:vertx.discovery.announce。你还可以配置一个用于服务使用情况追踪的名称(参见服务使用情况章节)。

当你不再需要该服务发现对象时,别忘了将其关闭。关闭操作会关闭你配置的各类服务发现 Importer 和 Exporter,并释放服务引用。

你应该避免共享同一个服务发现实例,以便服务使用情况(Service Usage)能够准确反映各组件的真实使用状况。

发布服务

拥有服务发现实例后,你就可以发布服务了。整个流程如下:

  1. 为特定的服务提供者创建一个 Record

  2. 发布该 Record

  3. 保留已发布的 Record,以便后续注销服务或进行修改。

要创建 Record,你可以使用 Record 类,也可以使用服务类型提供的便捷方法。

Record record = new Record()
    .setType("eventbus-service-proxy")
    .setLocation(new JsonObject().put("endpoint", "the-service-address"))
    .setName("my-service")
    .setMetadata(new JsonObject().put("some-label", "some-value"));

discovery.publish(record).onComplete(ar -> {
  if (ar.succeeded()) {
    // 发布成功
    Record publishedRecord = ar.result();
  } else {
    // 发布失败
  }
});

// 根据服务类型创建 Record
record = HttpEndpoint.createRecord("some-rest-api", "localhost", 8080, "/api");
discovery.publish(record).onComplete(ar -> {
  if (ar.succeeded()) {
    // 发布成功
    Record publishedRecord = ar.result();
  } else {
    // 发布失败
  }
});

妥善保存返回的 Record 引用非常重要,因为该 Record 已被附加了 registration id(注册 ID)。

注销服务

要注销(撤回)一个 Record,可以使用:

discovery.unpublish(record.getRegistration()).onComplete(ar -> {
  if (ar.succeeded()) {
    // 成功
  } else {
    // 无法注销该服务,可能已被移除,或者该 Record 并未发布
  }
});

查找服务

本节介绍了检索服务的底层流程;每个服务类型都提供了聚合这些不同步骤的便捷方法。

在消费者端,首要任务就是查找 Record。你可以搜索单个 Record,也可以搜索所有匹配的 Record。在前一种情况下,将返回第一个匹配的 Record。

消费者可以传入过滤器(Filter)来筛选服务。描述过滤器有两种方式:

  1. 一个接收 Record 作为参数并返回布尔值的函数(即谓词 Predicate)

  2. 一个 JSON 对象过滤器。传入过滤器的每个条目都会与 Record 进行比对。所有条目必须与 Record 完全匹配。条目可以使用特殊的 * 值,表示仅对 Key 有要求,而对 Value 不做限制。

下面看一个 JSON 过滤器的示例:

{ "name" = "a" } => 匹配名称为 "a" 的 Record
{ "color" = "*" } => 匹配设置了 "color" 的 Record
{ "color" = "red" } => 仅匹配 "color" 设置为 "red" 的 Record
{ "color" = "red", "name" = "a"} => 仅匹配名称为 "a" 且 "color" 设置为 "red" 的 Record

如果未设置 JSON 过滤器(为 null 或空),则接受所有 Record。当使用函数时,要接受所有 Record,你必须忽略 Record 的具体内容直接返回 true。

以下是一些示例:

discovery.getRecord(r -> true).onComplete(ar -> {
  if (ar.succeeded()) {
    if (ar.result() != null) {
      // 找到了匹配的 Record
    } else {
      // 查找成功,但没有匹配的服务
    }
  } else {
    // 查找失败
  }
});

discovery.getRecord((JsonObject) null).onComplete(ar -> {
  if (ar.succeeded()) {
    if (ar.result() != null) {
      // 找到了匹配的 Record
    } else {
      // 查找成功,但没有匹配的服务
    }
  } else {
    // 查找失败
  }
});


// 按名称获取 Record
discovery.getRecord(r -> r.getName().equals("some-name")).onComplete(ar -> {
  if (ar.succeeded()) {
    if (ar.result() != null) {
      // 找到了匹配的 Record
    } else {
      // 查找成功,但没有匹配的服务
    }
  } else {
    // 查找失败
  }
});

discovery.getRecord(new JsonObject().put("name", "some-service")).onComplete(ar -> {
  if (ar.succeeded()) {
    if (ar.result() != null) {
      // 找到了匹配的 Record
    } else {
      // 查找成功,但没有匹配的服务
    }
  } else {
    // 查找失败
  }
});

// 获取与过滤器匹配的所有 Record
discovery.getRecords(r -> "some-value".equals(r.getMetadata().getString("some-label"))).onComplete(ar -> {
  if (ar.succeeded()) {
    List<Record> results = ar.result();
    // 如果列表不为空,说明找到了匹配的 Record
    // 否则,虽然查找成功,但没有匹配的服务
  } else {
    // 查找失败
  }
});


discovery.getRecords(new JsonObject().put("some-label", "some-value")).onComplete(ar -> {
  if (ar.succeeded()) {
    List<Record> results = ar.result();
    // 如果列表不为空,说明找到了匹配的 Record
    // 否则,虽然查找成功,但没有匹配的服务
  } else {
    // 查找失败
  }
});

你可以使用 getRecords 检索单个 Record 或所有匹配的 Record。 默认情况下,Record 查找仅包含 status 为 UP 的记录。这一行为可以被覆盖:

  • 使用 JSON 过滤器时,只需将 status 设置为你期望的值(或设为 * 以接受所有状态)

  • 使用函数时,将 getRecords 中的 includeOutOfService 参数设置为 true。

获取服务引用(Service Reference)

选定 Record 之后,你就可以获取 ServiceReference,进而获取服务对象:

ServiceReference reference1 = discovery.getReference(record1);
ServiceReference reference2 = discovery.getReference(record2);

// 然后获取服务对象,返回的具体类型取决于服务类型:
// 对于 HTTP 端点:
HttpClient client = reference1.getAs(HttpClient.class);
// 对于消息源:
MessageConsumer consumer = reference2.getAs(MessageConsumer.class);

// 使用完服务后
reference1.release();
reference2.release();

使用完毕后,请务必释放服务引用。

服务引用代表与服务提供者之间的绑定关系。

在获取服务引用时,你可以传入一个用于配置服务对象的 JsonObject。它可以包含有关服务对象的各类配置数据。有些服务类型无需额外配置,而有些则需要配置(例如数据源):

ServiceReference reference = discovery.getReferenceWithConfiguration(record, conf);

// 然后获取服务对象,返回的具体类型取决于服务类型:
// 对于 HTTP 端点:
HttpClient client = reference.getAs(HttpClient.class);

// 使用客户端执行某些操作...

// 使用完服务后
reference.release();

在前面的示例中,代码使用了 getAs。该参数是你期望获取的对象类型。如果在 Java 中,你可以直接使用 get。然而在其他语言中,你必须显式传入期望的类型。

服务类型

如前所述,服务发现具有服务类型(Service Type)概念,用以管理不同服务的异构性。

默认提供了以下服务类型:

  • HttpEndpoint:面向 REST API,其服务对象是配置了主机和端口的 HttpClient(位置为 URL)。

  • EventBusService:面向服务代理(Service Proxy),其服务对象是一个代理实例。其类型为代理接口(位置为地址)。

  • MessageSource:面向消息源(发布者),其服务对象是 MessageConsumer(位置为地址)。

  • RedisDataSource:面向 Redis 数据源,其服务对象是 Redis(客户端配置根据位置、元数据以及消费者配置计算得出)。

  • MongoDataSource:面向 Mongo 数据源,其服务对象是 MongoClient(客户端配置根据位置、元数据以及消费者配置计算得出)。

本节详细介绍了服务类型的常规概念,并阐述了如何使用默认的服务类型。

无特定类型的服务

某些 Record 可能没有类型(ServiceType.UNKNOWN)。无法直接为这些 Record 获取服务引用,但你可以根据 Record 的 location 和 metadata 自行构建连接详情。

使用此类服务不会触发服务使用情况(Service Usage)事件。

实现自定义服务类型

你可以通过实现 ServiceType SPI 来创建自定义服务类型:

  1. (可选)创建一个继承自 ServiceType 的公共接口。该接口仅用于提供辅助方法,以简化该类型的使用,例如 createRecord 方法、getX(其中 X 是你所获取的服务对象类型)等。可以参考 HttpEndpoint 或 MessageSource 的实现示例。

  2. 创建一个实现 ServiceType 或步骤 1 中所创建接口的类。该类型具有一个 name,以及一个为此类型创建 ServiceReference 的方法。该名称必须与关联到你类型的 Record 的 type 字段相匹配。

  3. 创建一个继承自 io.vertx.ext.discovery.types.AbstractServiceReference 的类。你可以用要返回的服务对象类型对该类进行参数化。必须实现用于创建服务对象的 AbstractServiceReference#retrieve() 方法。该方法仅会被调用一次。如果你的服务对象需要清理,还需重写 AbstractServiceReference#onClose()。

  4. 在打包到 JAR 文件中的目录结构里创建 META-INF/services/io.vertx.servicediscovery.spi.ServiceType 文件。在该文件中,指定步骤 2 中创建的类的全限定名。

  5. 构建包含服务类型接口(步骤 1)、实现类(步骤 2 与步骤 3)以及服务描述符文件(步骤 4)的 JAR 包。将此 JAR 添加到应用程序的 Classpath 中。大功告成,你的自定义服务类型现已就绪。

HTTP 端点

HTTP 端点代表 REST API 或可通过 HTTP 请求访问的服务。HTTP 端点的服务对象是配置了主机、端口及 SSL 的 HttpClient。

发布 HTTP 端点

要发布 HTTP 端点,你需要一个 Record。你可以使用 HttpEndpoint.createRecord 来创建该 Record。

以下代码片段演示了如何从 HttpEndpoint 创建 Record:

Record record1 = HttpEndpoint.createRecord(
  "some-http-service", // 服务名称
  "localhost", // 主机地址
  8433, // 端口
  "/api" // 服务的根路径
);

discovery.publish(record1).onComplete(ar -> {
  // ...
});

Record record2 = HttpEndpoint.createRecord(
  "some-other-name", // 服务名称
  true, // 服务是否需要 HTTPS
  "localhost", // 主机地址
  8433, // 端口
  "/api", // 服务的根路径
  new JsonObject().put("some-metadata", "some value")
);

当你在容器或云环境中运行服务时,服务自身可能不知道其公网 IP 和公网端口,因此发布工作必须由掌握这些信息的其他实体来完成,通常是由 Bridge(网桥)来处理。

消费 HTTP 端点

一旦 HTTP 端点发布成功,消费者便可以检索它。服务对象是一个已配置好端口和主机地址的 HttpClient:

discovery.getRecord(new JsonObject().put("name", "some-http-service")).onComplete(ar1 -> {
  if (ar1.succeeded() && ar1.result() != null) {
    // 获取服务引用
    ServiceReference reference = discovery.getReference(ar1.result());
    // 获取服务对象
    HttpClient client = reference.getAs(HttpClient.class);

    // 你需要传入完整路径
    client.request(HttpMethod.GET, "/api/persons").compose(request ->
      request
        .send()
        .compose(HttpClientResponse::body))
      .onComplete(ar2 -> {
      // 别忘了释放服务
      reference.release();
    });
  }
});

你也可以使用 HttpEndpoint.getClient 方法,将查找和服务获取整合在单次调用中:

HttpEndpoint.getClient(discovery, new JsonObject().put("name", "some-http-service")).onComplete(ar -> {
  if (ar.succeeded()) {
    HttpClient client = ar.result();

    // 你需要传入完整路径
    client.request(HttpMethod.GET, "/api/persons").compose(request ->
      request
        .send()
        .compose(HttpClientResponse::body))
      .onComplete(ar2 -> {
        // 别忘了释放服务
        ServiceDiscovery.releaseServiceObject(discovery, client);
      });
  }
});

在第二种方式中,服务对象是通过 ServiceDiscovery.releaseServiceObject 释放的,因此你无需自行维护服务引用。

自 Vert.x 3.4.0 起,还提供了另一个客户端。这个名为 WebClient 的更高级别客户端往往更容易使用。你可以通过如下方式获取 WebClient 实例:

discovery.getRecord(new JsonObject().put("name", "some-http-service")).onComplete(ar -> {
  if (ar.succeeded() && ar.result() != null) {
    // 获取服务引用
    ServiceReference reference = discovery.getReference(ar.result());
    // 获取服务对象
    WebClient client = reference.getAs(WebClient.class);

    // 你需要传入完整路径
    client.get("/api/persons").send().onComplete(
      response -> {

        // ...

        // 别忘了释放服务
        reference.release();

      });
  }
});

如果你更偏好基于服务类型的方式:

HttpEndpoint.getWebClient(discovery, new JsonObject().put("name", "some-http-service")).onComplete(ar -> {
  if (ar.succeeded()) {
    WebClient client = ar.result();

    // 你需要传入完整路径
    client.get("/api/persons")
      .send().onComplete(response -> {

        // ...

        // 别忘了释放服务
        ServiceDiscovery.releaseServiceObject(discovery, client);

      });
  }
});

Event Bus 服务

Event Bus 服务本质上是服务代理(Service Proxy)。它们在 Event Bus 之上实现了异步 RPC 服务。从 Event Bus 服务获取服务对象时,你将获得一个正确类型的服务代理实例。你可以使用 EventBusService 中的辅助方法。

请注意,服务代理(服务实现和服务接口)是用 Java 开发的。

发布 Event Bus 服务

要发布 Event Bus 服务,你需要创建一个 Record:

Record record = EventBusService.createRecord(
    "some-eventbus-service", // 服务名称
    "address", // 服务地址
    "examples.MyService", // 字符串形式的服务接口
    new JsonObject()
        .put("some-metadata", "some value")
);

discovery.publish(record).onComplete(ar -> {
  // ...
});

你也可以将服务接口作为 Class 对象传入:

Record record = EventBusService.createRecord(
"some-eventbus-service", // 服务名称
"address", // 服务地址
MyService.class // 服务接口
);

discovery.publish(record).onComplete(ar -> {
// ...
});

消费 Event Bus 服务

要消费 Event Bus 服务,你可以先检索 Record 再获取引用,也可以使用 EventBusService 接口在单次调用中合并这两项操作。

使用引用时,代码大致如下:

discovery.getRecord(new JsonObject().put("name", "some-eventbus-service")).onComplete(ar -> {
if (ar.succeeded() && ar.result() != null) {
// 获取服务引用
ServiceReference reference = discovery.getReference(ar.result());
// 获取服务对象
MyService service = reference.getAs(MyService.class);

// 别忘了释放服务
reference.release();
}
});

使用 EventBusService 类时,你可以按如下方式获取代理:

EventBusService.getProxy(discovery, MyService.class).onComplete(ar -> {
if (ar.succeeded()) {
MyService service = ar.result();

// 别忘了释放服务
ServiceDiscovery.releaseServiceObject(discovery, service);
}
});

消息源(Message Source)

消息源是向 Event Bus 上特定地址发送消息的组件。消息源的客户端是 MessageConsumer。

消息源服务的 位置(Location) 就是发送消息的目标 Event Bus 地址。

发布消息源

与其他服务类型一样,发布消息源分为两个步骤:

  1. 使用 MessageSource 创建 Record

  2. 发布该 Record

Record record = MessageSource.createRecord(
    "some-message-source-service", // 服务名称
    "some-address" // Event Bus 地址
);

discovery.publish(record).onComplete(ar -> {
  // ...
});

record = MessageSource.createRecord(
    "some-other-message-source-service", // 服务名称
    "some-address", // Event Bus 地址
    "examples.MyData" // 负载类型
);

在第二个 Record 中,还指明了负载的类型。该信息是可选的。

在 Java 中,你可以使用 Class 参数:

Record record1 = MessageSource.createRecord(
"some-message-source-service", // 服务名称
"some-address", // Event Bus 地址
JsonObject.class // 消息负载类型
);

Record record2 = MessageSource.createRecord(
"some-other-message-source-service", // 服务名称
"some-address", // Event Bus 地址
JsonObject.class, // 消息负载类型
new JsonObject().put("some-metadata", "some value")
);

消费消息源

在消费者端,你可以获取 Record 和引用,或者使用 MessageSource 类在单次调用中获取服务。

使用第一种方法,代码如下:

discovery.getRecord(new JsonObject().put("name", "some-message-source-service")).onComplete(ar -> {
  if (ar.succeeded() && ar.result() != null) {
    // 获取服务引用
    ServiceReference reference = discovery.getReference(ar.result());
    // 获取服务对象
    MessageConsumer<JsonObject> consumer = reference.getAs(MessageConsumer.class);

    // 挂载消息处理器
    consumer.handler(message -> {
      // 消息处理器
      JsonObject payload = message.body();
    });
  }
});

当使用 MessageSource 时,代码变为:

MessageSource.<JsonObject>getConsumer(discovery, new JsonObject().put("name", "some-message-source-service")).onComplete(ar -> {
  if (ar.succeeded()) {
    MessageConsumer<JsonObject> consumer = ar.result();

    // 挂载消息处理器
    consumer.handler(message -> {
      // 消息处理器
      JsonObject payload = message.body();
    });
    // ...
  }
});

Redis 数据源

Redis 数据源是专门针对 Redis 持久化数据库的服务类型。 Redis 数据源服务的客户端是 Redis。

发布 Redis 服务

发布 Redis 数据源分为两个步骤:

  1. 使用 RedisDataSource 创建 Record

  2. 发布该 Record

Record record = RedisDataSource.createRecord(
  "some-redis-data-source-service", // 服务名称
  new JsonObject().put("url", "localhost"), // 位置信息
  new JsonObject().put("some-metadata", "some-value") // 元数据
);

discovery.publish(record).onComplete(ar -> {
  // ...
});

location 是一个简单的 JSON 对象,应提供用于访问 Redis 数据源的字段(url、port 等)。

消费 Redis 服务

正如前文所述,如何访问数据源取决于数据源本身。要构建 Redis,你可以合并多方配置:Record 位置、元数据以及消费者提供的 JSON 对象:

discovery.getRecord(
  new JsonObject().put("name", "some-redis-data-source-service")).onComplete(ar -> {
    if (ar.succeeded() && ar.result() != null) {
      // 获取服务引用
      ServiceReference reference = discovery.getReference(ar.result());

      // 获取服务实例
      Redis client = reference.getAs(Redis.class);

      // ...

      // 使用完成后
      reference.release();
    }
  });

你也可以使用 RedisDataSource 类,在单次调用中完成查找与获取:

RedisDataSource.getRedisClient(discovery,
  new JsonObject().put("name", "some-redis-data-source-service")).onComplete(ar -> {
    if (ar.succeeded()) {
      Redis client = ar.result();

      // ...

      // 别忘了释放服务
      ServiceDiscovery.releaseServiceObject(discovery, client);

    }
  });

Mongo 数据源

Mongo 数据源是专门针对 MongoDB 数据库的服务类型。 Mongo 数据源服务的客户端是 MongoClient。

发布 Mongo 服务

发布 Mongo 数据源分为两个步骤:

  1. 使用 MongoDataSource 创建 Record

  2. 发布该 Record

Record record = MongoDataSource.createRecord(
  "some-data-source-service", // 服务名称
  new JsonObject().put("connection_string", "some mongo connection"), // 位置信息
  new JsonObject().put("some-metadata", "some-value") // 元数据
);

discovery.publish(record).onComplete(ar -> {
  // ...
});

location 是一个简单的 JSON 对象,应提供用于访问 Mongo 数据源的字段(url、port 等)。

消费 Mongo 服务

正如前文所述,如何访问数据源取决于数据源本身。要构建 MongoClient,你可以合并多方配置:Record 位置、元数据以及消费者提供的 JSON 对象:

discovery.getRecord(
  new JsonObject().put("name", "some-data-source-service")).onComplete(ar -> {
    if (ar.succeeded() && ar.result() != null) {
      // 获取服务引用
      ServiceReference reference = discovery.getReferenceWithConfiguration(
        ar.result(), // Record 记录
        new JsonObject().put("username", "clement").put("password", "*****")); // 额外的元数据

      // 获取服务对象
      MongoClient client = reference.get();

      // ...

      // 使用完成后
      reference.release();
    }
  });

你也可以使用 MongoDataSource 类,在单次调用中完成查找与获取:

MongoDataSource.getMongoClient(discovery,
  new JsonObject().put("name", "some-data-source-service"),
  new JsonObject().put("username", "clement").put("password", "*****") // 额外的元数据
).onComplete(ar -> {
    if (ar.succeeded()) {
      MongoClient client = ar.result();

      // ...

      // 别忘了释放服务
      ServiceDiscovery.releaseServiceObject(discovery, client);

    }
  });

监听服务的上线与下线

每当有提供者发布或移除时,都会在 vertx.discovery.announce 地址上发布一个事件。 该地址可以通过 ServiceDiscoveryOptions 进行配置。

接收到的 Record 包含一个 status 字段,用于指示 Record 的新状态:

  • UP:服务可用,你可以开始使用它

  • DOWN:服务已不可用,你不应再使用它

  • OUT_OF_SERVICE:服务当前未运行,你不应再使用它,但它可能在稍后恢复。

监听服务的使用状态

每当检索服务引用(bind)或释放服务引用(release)时,都会在 vertx.discovery.usage 地址上发布一个事件。该地址可以通过 ServiceDiscoveryOptions 进行配置。

这使你能够监听服务的使用情况并映射服务绑定关系。

接收到的消息是一个 JsonObject,包含:

  • record 字段中的 Record

  • type 字段中的事件类型。取值为 bind 或 release

  • id 字段中的服务发现 ID(其名称或节点 ID)

该 id 可在 ServiceDiscoveryOptions 中进行配置。默认情况下,单节点配置下为 "localhost",集群模式下为节点 ID。

你可以通过 setUsageAddress 将使用地址设置为 null 来禁用服务使用情况追踪支持。

服务发现 Bridge

Bridge(网桥)允许你从其他发现机制(如 Docker、Kubernetes、Consul 等)导入或向其导出服务。 每个 Bridge 自行决定如何导入和导出服务。这并不一定是双向的。

你可以通过实现 ServiceImporter 接口来提供自定义 Bridge,并使用 registerServiceImporter 进行注册。

第二个参数可以为该 Bridge 提供可选配置。

当 Bridge 注册后,系统将调用 start 方法。 它允许你对 Bridge 进行配置。当 Bridge 配置完毕、就绪且已导入 / 导出初始服务后,必须完成给定的 Future。 如果 Bridge 的启动方法是阻塞的,它必须使用 executeBlocking 结构,并完成给定的 Future 对象。

当服务发现停止时,Bridge 也会停止。 系统将调用 close 方法,为你提供清理资源、移除导入 / 导出的服务等操作的时机。 该方法必须完成给定的 Future 以通知调用方已完成。

需要注意的是,在集群环境中,只需要一个成员注册该 Bridge 即可,因为所有成员都可以访问这些 Record。

更多 Bridge

除了本库直接支持的 Bridge 之外,Vert.x Service Discovery 还提供了可以在应用程序中使用的额外 Bridge。

Consul Bridge

此服务发现 Bridge 将服务从 Consul 导入到 Vert.x Service Discovery 中。该 Bridge 连接到 Consul Agent(服务端)并定期扫描服务:

  • 导入新服务

  • 移除处于维护模式或已从 Consul 中删除的服务

该 Bridge 使用 Consul 的 HTTP API。它不会将服务导出到 Consul,也不支持修改服务。

服务类型由 tags 推导得出。如果某个 tag 匹配已知的服务类型,则使用该服务类型。 否则,该服务将作为 unknown 导入。目前仅支持 http-endpoint。

使用该 Bridge

要使用此 Vert.x 服务发现 Bridge,请将以下依赖添加到构建描述文件的 dependencies 部分:

  • Maven(在 pom.xml 中):

<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-service-discovery-bridge-consul</artifactId>
 <version>4.5.35</version>
</dependency>
  • Gradle(在 build.gradle 文件中):

compile 'io.vertx:vertx-service-discovery-bridge-consul:4.5.35'

然后,在创建 Service Discovery 时,按如下方式注册该 Bridge:

ServiceDiscovery.create(vertx)
    .registerServiceImporter(new ConsulServiceImporter(),
        new JsonObject()
            .put("host", "localhost")
            .put("port", 8500)
            .put("scan-period", 2000));

你可以配置以下内容:

  • 通过 host 属性配置 Agent 主机地址,默认为 localhost

  • 通过 port 属性配置 Agent 端口,默认为 8500

  • 通过 acl_token 属性配置 ACL Token,默认为 null

  • 通过 scan-period 属性配置扫描周期。单位为毫秒(ms),默认为 2000 ms

Kubernetes Bridge

此服务发现 Bridge 将服务从 Kubernetes(或 OpenShift v3)导入到 Vert.x Service Discovery 中。 Kubernetes 服务会被映射为 Record。此 Bridge 仅支持将服务从 Kubernetes 导入到 Vert.x 中(不支持反向操作)。

Record 是根据 Kubernetes Service 创建的。服务类型由 service-type 标签或该服务所暴露的端口推导得出。

使用该 Bridge

要使用此 Vert.x 服务发现 Bridge,请将以下依赖添加到构建描述文件的 dependencies 部分:

  • Maven(在 pom.xml 中):

<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-service-discovery-bridge-kubernetes</artifactId>
 <version>4.5.35</version>
</dependency>
  • Gradle(在 build.gradle 文件中):

compile 'io.vertx:vertx-service-discovery-bridge-kubernetes:4.5.35'

然后,在创建 Service Discovery 时,按如下方式注册该 Bridge:

JsonObject defaultConf = new JsonObject();
serviceDiscovery.registerServiceImporter(new KubernetesServiceImporter(), defaultConf);

配置该 Bridge

该 Bridge 使用以下内容进行配置:

  • OAuth Token(默认使用 /var/run/secrets/kubernetes.io/serviceaccount/token 的内容)

  • 搜索服务的命名空间(默认为 default)。

请注意,应用程序必须具备访问 Kubernetes 的权限,并且能够读取所选的命名空间。

服务到 Record 的映射

Record 的创建规则如下:

  • 服务类型由 service.type 标签推导得出。如果未设置该标签,则服务类型设为 unknown

  • Record 的名称为服务名称

  • 服务的标签被映射为元数据(Metadata)

  • 额外添加的元数据包括:kubernetes.uuid、kubernetes.namespace、kubernetes.name

  • 位置(Location)由服务的第一个端口推导得出

对于 HTTP 端点,如果该服务具有值为 true 的 ssl 标签,则 ssl(https)属性将被设为 true。

动态性

该 Bridge 在 start 时导入所有服务,并在 stop 时将其移除。在运行期间,它会监视 Kubernetes 服务,添加新服务并移除已删除的服务。

支持的类型

该 Bridge 使用 service-type 标签推导类型。此外,它还会检查服务的端口。所支持的端口如下:

  • 80、443 端口以及 8080 到 9000 端口:HTTP 端点

  • 5432 与 5433 端口:JDBC 数据源(PostgreSQL)

  • 3306 与 13306 端口:JDBC 数据源(MySQL)

  • 6379 端口:Redis 数据源

  • 27017、27018 与 27019 端口:MongoDB 数据源

如果设置了 service-type 标签,它将覆盖基于端口的推导规则。

ZooKeeper Bridge

此服务发现 Bridge 将服务从 Apache ZooKeeper 导入到 Vert.x Service Discovery 中。该 Bridge 使用了 Curator 的服务发现扩展。

服务描述以 JSON Object 的形式读取(合并到 Vert.x Service Record 的元数据中)。通过读取该描述中的 service-type 来推导服务类型。

使用该 Bridge

要使用此 Vert.x 服务发现 Bridge,请将以下依赖添加到构建描述文件的 dependencies 部分:

  • Maven(在 pom.xml 中):

<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-service-discovery-bridge-zookeeper</artifactId>
 <version>4.5.35</version>
</dependency>
  • Gradle(在 build.gradle 文件中):

compile 'io.vertx:vertx-service-discovery-bridge-zookeeper:4.5.35'

然后,在创建 Service Discovery 时,按如下方式注册该 Bridge:

ServiceDiscovery.create(vertx)
    .registerServiceImporter(new ZookeeperServiceImporter(),
        new JsonObject()
            .put("connection", "127.0.0.1:2181"));

只有 connection 配置是必填的。它是 ZooKeeper 服务端的连接字符串(string)。

此外,你还可以配置以下内容:

  • maxRetries:连接重试次数,默认为 3

  • baseSleepTimeBetweenRetries:重试之间的等待毫秒数(指数退避策略),默认为 1000 ms

  • basePath:存储服务的 ZooKeeper 路径,默认为 /discovery

  • connectionTimeoutMs:连接超时时间(毫秒),默认为 1000

  • canBeReadOnly:后端是否支持只读(read-only)模式(默认为 true)

ServiceDiscovery.create(vertx)
    .registerServiceImporter(new ZookeeperServiceImporter(),
        new JsonObject()
            .put("connection", "127.0.0.1:2181")
            .put("maxRetries", 5)
            .put("baseSleepTimeBetweenRetries", 2000)
            .put("basePath", "/services")
    );

此服务发现 Bridge 将服务从 Docker Links 导入到 Vert.x Service Discovery 中。当你将一个 Docker 容器链接到另一个 Docker 容器时,Docker 会注入一组环境变量。该 Bridge 会分析这些环境变量, 并为每个链接导入相应的 Service Record。服务类型由 service.type 标签推导得出。如果未设置, 则将该服务作为 unknown 导入。目前仅支持 http-endpoint。

由于链接是在容器启动时创建的,因此导入的 Record 会在该 Bridge 启动时创建,且之后不会发生改变。

要使用此 Vert.x 服务发现 Bridge,请将以下依赖添加到构建描述文件的 dependencies 部分:

  • Maven(在 pom.xml 中):

<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-service-discovery-bridge-docker</artifactId>
 <version>4.5.35</version>
</dependency>
  • Gradle(在 build.gradle 文件中):

compile 'io.vertx:vertx-service-discovery-bridge-docker:4.5.35'

然后,在创建 Service Discovery 时,按如下方式注册该 Bridge:

ServiceDiscovery.create(vertx)
    .registerServiceImporter(new DockerLinksServiceImporter(), new JsonObject());

该 Bridge 不需要任何进一步的配置。

更多后端

除了本库直接支持的后端之外,Vert.x Service Discovery 还提供了可以在应用程序中使用的额外后端。

Redis 后端(Backend)

Service Discovery 拥有一个基于 ServiceDiscoveryBackend SPI 的可插拔后端。这是该 SPI 基于 Redis 的实现。

使用 Redis 后端

要使用 Redis 后端,请将以下依赖添加到构建描述文件的 dependencies 部分:

  • Maven(在 pom.xml 中):

<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-service-discovery-backend-redis</artifactId>
 <version>4.5.35</version>
</dependency>
  • Gradle(在 build.gradle 文件中):

compile 'io.vertx:vertx-service-discovery-backend-redis:4.5.35'

请注意,你的 classpath 中只能包含该 SPI 的一个实现。如果未包含任何实现,则使用默认后端。

配置

该后端基于 vertx-redis-client。 其配置既包含客户端配置,也包含用于指定 Record 在 Redis 中存储位置的 key。

示例如下:

ServiceDiscovery.create(vertx, new ServiceDiscoveryOptions()
    .setBackendConfiguration(
        new JsonObject()
            .put("connectionString", "redis://localhost:6379")
            .put("key", "records")
    ));

需要特别注意的是,后端配置是通过 setBackendConfiguration 方法传递的(如果使用 JSON,则是 backendConfiguration 条目):

ServiceDiscovery.create(vertx,
  new ServiceDiscoveryOptions(new JsonObject()
    .put("backendConfiguration",
      new JsonObject().put("connectionString", "redis://localhost:6379").put("key", "my-records")
)));