Vert.x 服务发现

Vert.x 服务发现组件提供了一套用于对各种资源服务的发布、发现的基础组件,例如 服务代理、HTTP服务节点(HTTP endpoint)、数据源(data source)…​ 这些资源统称为 服务 。一个 服务 即是一个可被发现的功能性模块。它可以用类型、元数据、位置来区分,所以一个 服务 可以是一个数据库、服务代理、HTTP节点或者其他任何您能够描述、发现、交互的服务资源。它不一定是一个vert.x对象,它可以是任何组件。每个 服务 都被描述成一个 Record (即:下述 服务记录 )。

服务发现 组件实现了面向服务计算中定义的服务交互。此外,在某种程度上,还提供了动态的面向服务计算交互,这样应用程序可以对各种服务的上线、下线作出处理。

一个服务提供者可以做如下事情:

  • 发布一个服务的 服务记录

  • 下线一个已发布的 服务记录

  • 更新线上服务的状态 (下线、暂停服务…​)

一个服务消费者可以做如下事情:

  • 寻找服务

  • 选择绑定到某个服务(获取一个 ServiceReference) 并使用)

  • 当服务消费者停止后立即释放服务提供者资源

  • 监听服务的注册、注销、更新

服务消费者将:1)寻找符合自己需求的 服务记录; 2) 接收提供访问入口的 ServiceReference ;3) 获得一个提供访问入口的 服务 对象;4) 停止后立即释放服务对象

这个过程可以用 服务类型 (service type) 来简化,如果你知道服务的类型(JDBC client, Http client…​),那么你可以直接获取到这个服务对象。

综上所述,服务提供者和消费者之间共享的核心信息都存放在 records 当中。

提供者和消费者必须创建自己的 ServiceDiscovery 实例。这些实例在后台(分布式架构)进行协作,以使服务之间信息保持同步。

vert.x的服务发现通过桥接方式 支持对其他服务发现技术的导入和导出。

使用服务发现

要使用Vert.x 服务发现组件,需要将下列依赖加入到依赖配置中文件:

  • Maven (pom.xml 文件):

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

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

概念总览

服务发现机制基于以下章节的几个概念。

服务记录

一个服务 Record 是服务提供者发布的一个服务描述对象。它包含了名称、元数据、位置对象(描述服务发布在哪里)。这个 服务记录 是提供者(发布 服务记录 )和消费者(寻找服务时获取 服务记录 )之间仅有的共享对象。

元数据和位置格式由 服务类型(service type) 决定(见下述)。

服务提供方准备好之后,一个 服务记录 就被发布了,服务停止时 服务记录 被回收

提供者和发布者

服务提供者是一个提供 服务 的对象。发布者负责发布描述提供者的 服务记录 。它可能是同一个对象(提供者仅仅发布自己)也可能是个不同的对象。

服务消费者

服务消费者在服务发现模块中检索服务。每次检索都会得到 0..nRecord ,从这些 服务记录 当中,服务消费者可以获取到 ServiceReference ,这个 服务引用 绑定了提供方和消费方,它允许消费方获取到 服务对象(service object) (用来使用服务)或释放服务。

释放 服务引用 是一件很重要的事情,这清除了 服务引用 对象并更新了服务的使用状态

服务对象

服务对象 是一个提供了服务入口的对象。它可以是各种形式,例如 代理、客户端、甚至是一些不存在的服务类型。服务对象的性质取决于服务类型。

注意:因为Vert.x多语言的性质,所以如果你在java、groovy或其他语言获取的 服务对象 会不一样。

服务类型

服务仅仅是一些 资源(resource) 或不同类型的 服务。他们可以是功能服务组件、数据库、REST-Api等等。Vert.x服务发现模块定义 服务类型 来处理各类型的差异。每个类型定义了:

  • 服务是如何定位的(URI, event bus address, IP / DNS…​)- location

  • 服务对象的性质(service proxy, HTTP client, message consumer…​)client

服务发现组件提供了一些现成的服务类型,但是你可以添加你自己的类型。

服务事件

每当发布或注销一个服务,event-bus上就会触发一个事件(event),这个事件包含了被更新的服务记录。

另外,为了追踪谁调用谁,每当调用 getReference 则获取reference或者 调用 release 释放reference的时候,事件都在event-bus上被发出 用以跟踪服务的使用情况。

关于事件的更多详细信息如下。

后台

服务发现模块使用了Vert.x的分布式数据结构来存储 服务记录 。所以所有的集群成员都能获取到所有的 服务记录 。这是后台默认的实现。你可以实现 ServiceDiscoveryBackend SPI 来实现自己的Backend。例如,Vert.x提供了一个基于Redis的基础实现。

注意:服务发现不要求必须是Vert.x集群。在单节点模式下,这个数据结构存储于本地。它可以用 ServiceImporter 实现。从3.5.0版本开始,你可以在集群模式下用本地结构的event,这可以设置系统参数 vertx-service-discovery-backend-localtrue (或者设置环境变量 VERTX-SERVICE-DISCOVERY-BACKEND-LOCALtrue) 来实现。

创建服务发现实例

提供者和消费者必须创建他们自己的 ServiceDiscovery 实例来使用服务发现组件:

ServiceDiscovery discovery = ServiceDiscovery.create(vertx);

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

// Do something...

discovery.close();

默认情况下,公告地址(发送事件的event-bus地址)是 vertx.discovery.announce。你也可以为service usage(见service usage章节)配置一个名称。

当你再也不需要服务发现对象时,不要忘记去关闭它。它关闭了您已配置的发现导入器和导出器,并释放服务引用。

您应该避免共享服务发现实例,因此service usage 将代表正确的“usage”

发布服务

一旦你拥有了服务发现实例,你可以发布服务。步骤如下:

  1. 为这个服务提供者创建一个服务记录

  2. 发布这个服务记录

  3. 持有这个服务记录,以便于后续的注销和更改操作

为了创建服务记录,你可以用 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, ar -> {
  if (ar.succeeded()) {
    // 发布成功
    Record publishedRecord = ar.result();
  } else {
    // 发布失败
  }
});

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

持有服务记录的引用是很重要的事情,因为服务记录中拥有一个 注册 id

回收服务

要回收(下线)一个record,使用:

discovery.unpublish(record.getRegistration(), ar -> {
  if (ar.succeeded()) {
    // Ok
  } else {
    // 无法下线服务,可能是因为已经被移除或者 record根本没有被发布
  }
});

寻找服务

本章解释了获取服务的底层原理,每个服务类型都提供了便捷的方式来整合各个的步骤

在消费方,第一件事就是要寻找服务记录。您可以检索单个服务记录或者所有符合条件的服务记录。第一种情况,第一个符合条件的服务记录被返回。

消费方可以提供一个过滤器(filter)来选择服务。有两种方式来描述过滤器:

  1. 一个以 Record 为参数并以布尔类型(这是一个断言)作为返回值的函数

  2. 这个过滤器是一个JSON-obejct。每一个给出的filter过滤条件都会检查record,所有的过滤条件都必须满足record。过滤条件可以用 * 通配符来表示对key的要求,而不是精准匹配。

我们来看看JSON过滤器的一个例子:

{ "name" = "a" } => 筛选出名称是"a"的record
{ "color" = "*" } => 筛选出存在"color"字段的record
{ "color" = "red" } => 筛选出"color"字段是"red"的record
{ "color" = "red", "name" = "a"} => 筛选出"color"字段是"red" 且 "name"字段是"a"的record

如果没有设置JSON过滤器(null 或 empty) ,则筛选出所有的服务记录。当用函数来过滤时,如果你想获取到所有的服务记录,那无论是什么样的服务记录,你必须要返回 true

示例如下:

discovery.getRecord(r -> true, ar -> {
  if (ar.succeeded()) {
    if (ar.result() != null) {
      // 获取到一个record
    } else {
      // 寻找成功但是没有符合条件的服务
    }
  } else {
    // 查找失败
  }
});

discovery.getRecord((JsonObject) null, ar -> {
  if (ar.succeeded()) {
    if (ar.result() != null) {
      // 获取到一个record
    } else {
      // 寻找成功但是没有符合条件的服务
    }
  } else {
    // 查找失败
  }
});


// 通过名称获取record
discovery.getRecord(r -> r.getName().equals("some-name"), ar -> {
  if (ar.succeeded()) {
    if (ar.result() != null) {
      // 获取到一个record
    } else {
      // 寻找成功但是没有符合条件的服务
    }
  } else {
    // 查找失败
  }
});

discovery.getRecord(new JsonObject().put("name", "some-service"), ar -> {
  if (ar.succeeded()) {
    if (ar.result() != null) {
      // 获取到一个record
    } else {
      // 寻找成功但是没有符合条件的服务
    }
  } else {
    // 查找失败
  }
});

// 获取所有符合过滤器条件的record
discovery.getRecords(r -> "some-value".equals(r.getMetadata().getString("some-label")), ar -> {
  if (ar.succeeded()) {
    List<Record> results = ar.result();
    // 如果获取到非空list,那么我们获取到了record
    // 否则说明寻找成功但是没有符合条件的服务
  } else {
    // 查找失败
  }
});


discovery.getRecords(new JsonObject().put("some-label", "some-value"), ar -> {
  if (ar.succeeded()) {
    List<Record> results = ar.result();
    // 如果获取到非空list,那么我们获取到了record
    // 否则说明寻找成功但是没有符合条件的服务
  } else {
    // 查找失败
  }
});

我们可以用 getRecords 来获取单条服务记录或者所有符合条件的服务记录。默认情况下,对于服务记录的查找仅仅包含 statusUP 的情况。这可以被重写:

  • 当使用JSON过滤器时,设置 status 为你的期望值(或者 * 来接收所有的状态)

  • 当使用函数时,在 getRecords 函数 设置 includeOutOfService 参数为 true

获取服务引用

一旦你选择了 Record ,你可以获取一个 ServiceReference服务对象 :

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

// 获取到service object,返回服务引用的类型取决于 service type
// Http 节点
HttpClient client = reference1.getAs(HttpClient.class);
// 消息源
MessageConsumer consumer = reference2.getAs(MessageConsumer.class);

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

切记处理完毕之后释放服务引用资源

一个服务引用代表了一个对服务提供者的绑定关系。

获取服务引用时,你可以传一个包含了各种数据的 JsonObject 用来配置 服务对象 。某些服务类型不需要额外的配置,如下是一些必要的配置(数据源为例):

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

// 获取到service object,返回服务引用的类型取决于 service type
// JDBC 节点
JDBCClient client = reference.getAs(JDBCClient.class);

// Do something with the client...

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

在前述例子当中,代码用了 getAs 函数,其参数是你期望得到的服务类型。如果你用java语言,你可以用 get 函数。然而其他语言 你必须传一个你期望的类型值。

服务类型

如上所述,服务发现有 服务类型 的概念来管理不同类型的服务。

这些类型默认通过如下方式提供:

  • HttpEndpoint - 对于 REST API’来讲, 服务对象是一个由 hostport(位置是 url 参数)配置的 HttpClient .

  • EventBusService - 对于服务代理,服务对象是一个proxy。它的类型是`proxies interface`(服务所在位置是地址)

  • MessageSource - 对于消息源(发送者),服务对象是一个 MessageConsumer (服务所在位置是地址)。

  • JDBCDataSource - 对于 JDBC 数据源, 服务对象是一个 JDBCClient (客户端配置从 location,metadata和消费方配置来解析)。

  • RedisDataSource - 对于 Redis 数据源, 服务对象是一个 Redis (客户端配置从 location,metadata和消费方配置来解析).

  • MongoDataSource - 对于 Mongo 数据源, 服务对象是一个 MongoClient (客户端配置从 location,metadata和消费方配置来解析).

本节总体上给出有关服务类型的详细信息,并介绍如何使用默认服务类型。

无类型的服务

一些record也许没有类型(ServiceType.UNKNOWN)。所以不可能从这些record里面获取到服务对象,但是你可以通过 Record 对象的 locationmetadata 来构建连接的具体信息。

使用这些服务不会触发 service usage 事件

实现您自己的服务类型

你可以通过实现 ServiceType SPI 的方式来创建你自己的服务类型:

  1. (可选) 创建一个public interface 继承 ServiceType 。这个interface仅仅用来提供辅助函数来简化你自定义类型的用法,例如 createRecord 函数, getXX 是你获取到的服务对象的类型。可以查看 HttpEndpoint 或者 MessageSource 等接口例子来了解这种设计

  2. 创建一个类来实现 ServiceType ,或者实现你在第一步创建的类型。 这个类型有 name 和一个为这个类型创建 ServiceReference 的函数。这个name必须匹配 Record 类的 type 字段,这个record的type就是您自己定义的服务类型。

  3. 创建一个类继承 io.vertx.ext.discovery.types.AbstractServiceReference ,您可以参数化您将要返回的带有服务对象类型的类,你必须实现 AbstractServiceReference#retrieve() 函数来创建服务对象。这个函数只能被调用一次。如果需要清除服务对象,也要重写 AbstractServiceReference#onClose() 函数

  4. 在打包jar时在jar内创建一个 META-INF/services/io.vertx.servicediscovery.spi.ServiceType 文件。这个文件仅仅包含您在第二步创建的类的全名。

  5. 创建一个包含service type interface(步骤1)的jar,实现类(步骤2,步骤3)和服务描述文件(步骤4)。把这个jar放在你应用的classpath下,然后您的服务类型现在就已经可用了。

HTTP 节点

一个HTTP节点代表一个REST API或者可用HTTP请求访问的服务。HTTP节点服务对象是一个由host、port、ssl所配置的 HttpClient 对象

发布一个HTTP节点

要发布一个HTTP节点,你需要一个 Record ,你可以用 HttpEndpoint.createRecord 来创建服务记录。

下面一段阐述如何用 HttpEndpoint 创建 Record

Record record1 = HttpEndpoint.createRecord(
  "some-http-service", // 服务名称
  "localhost", //  host
  8433, // port
  "/api" // 服务的根路由
);

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

Record record2 = HttpEndpoint.createRecord(
  "some-other-name", // 服务名称
  true, // 是否要求 HTTPs
  "localhost", // host
  8433, // port
  "/api", // 服务的根路由
  new JsonObject().put("some-metadata", "some value")
);

当你在容器或者云启动你的服务时,也许并不知道服务的公网IP和端口,所以发布操作必须要通过另一个对象来获取这个信息,通常它是一个 桥接对象bridge )。

消费一个HTTP服务节点

一旦HTTP节点发布,一个消费者可以获取到它。这个服务对象是一个配置了host和port的 HttpClient

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

    // 定义完整的path
    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"), ar -> {
  if (ar.succeeded()) {
    HttpClient client = ar.result();

    // 你需要提供完整的path
    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"), ar -> {
  if (ar.succeeded() && ar.result() != null) {
    // 获取服务引用
    ServiceReference reference = discovery.getReference(ar.result());
    // 获取服务对象
    WebClient client = reference.getAs(WebClient.class);

    // 你需要提供完整的path
    client.get("/api/persons").send(
      response -> {

        // ...

        // 不要忘记释放服务资源
        reference.release();

      });
  }
});

另外,如果你更倾向于用服务类型的方式:

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

    // 你需要提供完整的path
    client.get("/api/persons")
      .send(response -> {

        // ...

        // 不要忘记释放服务资源
        ServiceDiscovery.releaseServiceObject(discovery, client);

      });
  }
});

Event bus 服务

Event bus服务是服务代理,它基于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, ar -> {
  // ...
});

你也可以传服务接口类:

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

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

消费event bus服务

要消费一个event bus服务,你可以先获取服务记录,再通过服务记录获取服务引用;或者用 EventBusService 接口通过一次调用来完成这两个操作。

当使用服务引用是,你应该这样做:

discovery.getRecord(new JsonObject().put("name", "some-eventbus-service"), 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, ar -> {
if (ar.succeeded()) {
MyService service = ar.result();

// 不要忘记释放服务资源
ServiceDiscovery.releaseServiceObject(discovery, service);
}
});

消息源服务

消息源是一个往event bus地址发送消息的组件,消息源客户端是 `MessageConsumer`类。

消息被发送给到eventBus的 location 或 消息源服务上。

推送消息

就像其他类型的服务,推送一个消息分2步走:

  1. MessageSource 创建一个服务对象。

  2. 推送消息

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

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

record = MessageSource.createRecord(
    "some-other-message-source-service", // 服务名
    "some-address", // event bus 地址
    "examples.MyData" // 消息体类型
);

在上述第二个record当中,我们同时指明了消息体(payload)的类型,这个参数是可选的。

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

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

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

消费消息服务

在消费者端,你可以获取服务记录从而获取服务引用,或者用 MessageSource 通过一次调用合两步为一。

通过第一种方式,代码如下:

discovery.getRecord(new JsonObject().put("name", "some-message-source-service"), 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"), ar -> {
  if (ar.succeeded()) {
    MessageConsumer<JsonObject> consumer = ar.result();

    // 指定消息处理器
    consumer.handler(message -> {
      // 消息处理器逻辑
      JsonObject payload = message.body();
    });
    // ...
  }
});

JDBC数据源

数据源代表数据库或数据仓储。而JDBC数据源特指用JDBC驱动访问数据库。JDBC数据源客户端是 JDBCClient 类。

发布JDBC服务

类似其他类型的服务,发布JDBC服务需要2步:

  1. JDBCDataSource 创建record

  2. 推送record

Record record = JDBCDataSource.createRecord(
    "some-data-source-service", // 服务名
    new JsonObject().put("url", "some jdbc url"), // 服务地址
    new JsonObject().put("some-metadata", "some-value") // 元数据
);

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

因为JDBC数据源可以使用很多数据库,并且访问方式经常不一样,所以服务记录是没有标准结构定义的,location 是访问数据库配置而提供的一个通用JSONObject属性,用于访问数据源(JDBC url,username…​)。其他字段的定义依赖于数据库以及所用连接池决定。

消费一个JDBC服务

由前所述,如何获取数据源取决于数据源本身。要创建 JDBCClient ,你可以同时提供:record location,元数据和一个有消费方提供的一个Json object:

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

        // 获取service object
        JDBCClient client = reference.getAs(JDBCClient.class);

        // ...

        // 完毕
        reference.release();
      }
    });

你可以用 JDBCClient 通过一次调用来发现和获取服务。

JDBCDataSource.<JsonObject>getJDBCClient(discovery,
    new JsonObject().put("name", "some-data-source-service"),
    new JsonObject().put("username", "clement").put("password", "*****"), // 一些额外的元数据
    ar -> {
      if (ar.succeeded()) {
        JDBCClient client = ar.result();

        // ...

        // 不要忘记释放服务资源
        ServiceDiscovery.releaseServiceObject(discovery, client);

      }
    });

Redis 数据源

Redis数据源是Redis持久性数据库的一种特殊实现。

Redis数据源客户端是 Redis

发布Redis服务

发布Redis服务需要2步:

  1. RedisDataSource 创建服务记录

  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, ar -> {
  // ...
});

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

消费Redis服务

由前所述,如何访问数据源决定于数据源本身。创建一个 Redis 对象,你可以同时提供:record地址,元数据和由消费方提供的Json object。

discovery.getRecord(
  new JsonObject().put("name", "some-redis-data-source-service"), 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"),
  ar -> {
    if (ar.succeeded()) {
      Redis client = ar.result();

      // ...

      // 不要忘记释放服务资源
      ServiceDiscovery.releaseServiceObject(discovery, client);

    }
  });

Mongo数据源

Mongo数据源是MongoDB数据库的专用化实现。 Mongo数据源服务的客户端是 MongoClient

发布Mongo服务

发布Mongo服务需要2步:

  1. MongoDataSource 创建服务记录

  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, ar -> {
  // ...
});

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

消费Mongo服务

由前所述,如何访问数据源取决于数据源本身。创建 MongoClient ,你可以同时提供:record location ,元数据和consumer提供的json object:

discovery.getRecord(
  new JsonObject().put("name", "some-data-source-service"),
  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.<JsonObject>getMongoClient(discovery,
  new JsonObject().put("name", "some-data-source-service"),
  new JsonObject().put("username", "clement").put("password", "*****"), // 一些额外的元数据
  ar -> {
    if (ar.succeeded()) {
      MongoClient client = ar.result();

      // ...

      // 不要忘记释放服务资源
      ServiceDiscovery.releaseServiceObject(discovery, client);

    }
  });

监听服务的注册和注销

每当服务提供方被发布或移除, vertx.discovery.announce 地址上会推送一个事件。 这个地址可以在 ServiceDiscoveryOptions 配置

收到的record会有一个 status 字段,它描述了record的新状态:

  • UP : 服务可获取,您可以开启并使用它

  • DOWN : 服务不可获取,你不应该再使用它

  • OUT_OF_SERVICE : 服务不在运行状态,你不该再用它,但是可能过一会可能会恢复

监听服务使用状况

每当获取或释放服务引用时 vertx.discovery.usage 地址上会推送一个event。这个地址可以由 ServiceDiscoveryOptions 配置。

这会让您监听服务的使用情况并映射服务的绑定情况。

收到的消息是一个包含如下信息的 JsonObject

  • record 属性指服务记录对象

  • type 属性是事件的类型,它有两个值:bindrelease

  • id 属性是服务发现的id(名称或者node id)

这个 id 可以通过 ServiceDiscoveryOptions 配置。默认情况下 单节点是"localhost",集群模式下是node id。

你可以禁用服务使用情况的功能,这可以通过 setUsageAddress 设置 usage addressnull 来实现。

服务发现桥接器

桥接器 可以让你从其他服务发现机制(Docker,Kubernetes,Consul…​)里导入导出服务 每个桥接器决定了服务如何导入导出。这不一定是双向操作。

您可以实现 ServiceImporter 接口,并用 registerServiceImporter 来注册的方式,为自己提供桥接器。

第二个参数是可选的,它可以配置桥接器。

当桥接器被注册时,start 函数被调用。 它让您可以配置桥接器。当桥接器被配置时,准备就绪并导入/导出初始服务后,它让给定的 Future 处于 completed 状态;如果bridge starts函数正在阻塞,则它必须使用 executeBlocking 构造,并设置给定的future对象为 completed 状态。

当服务发现组件停止,桥接器也随即停止。

调用 close 函数会清除资源,移除已经导入/导出的服务。这个函数必须设置返回的 Future 对象为 completed 状态,目的在于提醒调用者处理完成事件。

注意:在集群当中,只需要注册服务记录当中一个成员的桥接器即可,因为所有的服务记录都是互通的。

其他的桥接器支持

Vert.x 服务发现组件除了支持桥接器机制以外,还提供了一些现成的桥接器。

Consul 网桥

Consul服务发现网桥从 Consul 导入服务到Vert.x服务发现。这个网桥链接了 Consul agent 服务 并周期性扫描以下服务:

  • 新导入的服务

  • 移除 处于 maintenance 模式的服务或已经从consul中被移除的服务

这个桥接器使用的是 Consul 的HTTP API接口。它不能将服务导出到Consul,并且也不支持服务的修改。

服务的类型是通过 tags 推断出来的,如果有一个 tag 和已知的服务类型一样,那么就使用这种服务类型,如果没有匹配的,那么服务导入后将标记为unknown类型。目前暂时只支持http-endpoint类型。

桥接器的使用

要使用该服务发现桥接器,需要将如下的依赖包加入到依赖配置文件中:

  • Maven (pom.xml 文件):

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

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

然后,在创建服务发现对象的时候,像下面这样注册桥接器:

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 属性,配置 agent 的访问控制令牌,默认值是 null

  • scan-period 属性,配置扫描的频率,扫描的单位是毫秒(ms),默认是 2000 ms

Kubernetes 桥接器

Kubernetes 桥接器可以从Kubernetes(或者 Openshift v3)中导入服务到Vert.x的服务发现组件中。 Kubernetes的所有服务,都将映射为一条 Record ,目前桥接器只支持将服务从Kubernetes中导入到Vert.x中(反过来不行)。

Kubernetes中的服务,在导入到Vert.x后都会创建对应的 Record ,服务类型是通过 service-type 标签推断出来,或者通过服务暴露的端口推断出来。

桥接器的使用

要使用该服务发现桥接器,需要将如下的依赖包加入到依赖配置文件中:

  • Maven (pom.xml 文件):

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

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

然后,当创建服务发现时,按如下注册桥接器:

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

桥接器的配置

桥接器的配置项有:

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

  • 服务搜索的命名空间(默认是`default`)

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

服务记录的映射

服务记录按照如下的步骤进行创建:

  • service.type 中推断出服务类型;如果没有设置,那么服务类型被设置为 unknown

  • 服务记录的名称就是服务的名称

  • 服务的标签(label)都被映射为服务记录的元数据

  • 此外还会加上:kubernetes.uuid , kubernetes.namespace , kubernetes.name

  • location 信息将从服务的第一个端口推断出来

对于 HTTP 端点,如果服务带有值为 truessl (https) 标签的话,那么服务记录的 ssl (https)属性将被设置为 true

动态性

Kubernetes 桥接器将会在启动(start)的时候导入所有的服务,在停止(stop)的时候移除所有的服务。在运行期间,它将监听 Kubernetes 的服务,并且动态地导入新加入的服务,移除被删除的服务。

支持的类型

桥接器使用 service-type 标签来指定类型。另外,它还检查服务的端口,支持如下:

  • 80,443,8080-9000: HTTP端点

  • 5432,5433:JDBC数据源(PostGreSQL)

  • 3306,13306:JDBC数据源(MySql)

  • 6379: Redis 数据源

  • ports 27017, 27018 和 27019: MongoDB 数据源

如果 service-type 存在,那么将覆盖端口的默认配置

Zookeeper 桥

该发现桥将服务从 Apache Zookeeper 导入到 Vert.x 服务发现中。 该桥使用 用于服务发现的 Curator 扩展

服务描述可被读取为 JSON 对象(该对象合并在 Vert.x 服务记录元数据中)。 可通过阅读 service-type 从该描述中推断出服务类型。

使用该桥

要使用这个 Vert.x 发现桥, 请将以下依赖项添加到构建描述符的 dependencies 部分:

  • Maven(在您的 pom.xml):

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

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

然后,在创建服务发现时按如下方式注册此桥:

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

只有 connection 配置是强制的。它是 Zookeeper 服务器连接的 字符串

此外,您亦可配置:

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

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

  • basePath:存储服务的 Zookeeper 路径。默认为`/discovery`。

  • connectionTimeoutMs:以毫秒为单位的连接超时。默认为 1000。

  • canBeReadOnly:后端是否支持 只读 模式(默认为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")
    );

Docker Links 桥接器可以从 Docker Links 中导入服务到 Vert.x 的服务发现组件中。当你将一个Docker容器与另外一个Docker容器链接在一起(link)的时候,Docker将会注入一组环境变量。该桥接器将分析这些环境变量,并且针对每个链接(link),生成一个服务记录。服务记录的类型从 service.type 属性中推断;如果没有设置,那么服务类型将被设置为 unknown 。目前暂时只支持 http-endpoint 服务类型。

由于Docker容器只在启动的时候创建链接,所以这个桥接器只会在启动的时候导入服务记录,然后此后就都不改变了。

桥接器的使用

要使用该服务发现桥接器,需要将如下的依赖包加入到依赖配置文件中:

  • Maven (pom.xml 文件):

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

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

创建服务发现对象时,注册桥接器的示例:

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

这种桥接器不需要进一步的配置。

其他后台支持

除了此库支持的后台之外,Vert.x服务发现还提供了其他后台以供您在自己的应用程序中使用。

Redis backend

服务发现组件通过实现 ServiceDiscoveryBackend SPI提供了一种可插拔的存储后端扩展机制,这是以Redis为基础的SPI的实现。

使用 Redis 存储后端

要使用 Redis 存储后端,需要将如下的依赖包加入到依赖配置文件中:

  • Maven ( pom.xml 文件):

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

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

需要注意的是,你只能在 classpath 中指定一个SPI的实现;如果没有指定,那么将使用默认的存储后端。

配置

Redis存储后端是基于 vertx-redis-client 实现的,这个配置是客户端配置以及在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")
)));