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..n 个 Record ,从这些 服务记录 当中,服务消费者可以获取到 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-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"));

// 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 来获取单条服务记录或者所有符合条件的服务记录。默认情况下,对于服务记录的查找仅仅包含 status 是 UP 的情况。这可以被重写:

  • 当使用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’来讲, 服务对象是一个由 host、port(位置是 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 对象的 location 和 metadata 来构建连接的具体信息。

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

实现您自己的服务类型

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

  1. (可选) 创建一个public interface 继承 ServiceType 。这个interface仅仅用来提供辅助函数来简化你自定义类型的用法,例如 createRecord 函数, getX , X 是你获取到的服务对象的类型。可以查看 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 属性是事件的类型,它有两个值:bind ,release

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

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

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

服务发现桥接器

桥接器 可以让你从其他服务发现机制(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 端点,如果服务带有值为 true 的 ssl (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")
)));