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.1.8</version>
</dependency>
-
Gradle (
build.gradle
文件):
compile 'io.vertx:vertx-service-discovery:4.1.8'
概念总览
服务发现机制基于以下章节的几个概念。
服务记录
一个服务 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”
发布服务
一旦你拥有了服务发现实例,你可以发布服务。步骤如下:
-
为这个服务提供者创建一个服务记录
-
发布这个服务记录
-
持有这个服务记录,以便于后续的注销和更改操作
为了创建服务记录,你可以用 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)来选择服务。有两种方式来描述过滤器:
-
一个以
Record
为参数并以布尔类型(这是一个断言)作为返回值的函数 -
这个过滤器是一个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();
服务类型
如上所述,服务发现有 服务类型
的概念来管理不同类型的服务。
这些类型默认通过如下方式提供:
-
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 的方式来创建你自己的服务类型:
-
(可选) 创建一个public interface 继承
ServiceType
。这个interface仅仅用来提供辅助函数来简化你自定义类型的用法,例如createRecord
函数,getX
,X
是你获取到的服务对象的类型。可以查看HttpEndpoint
或者MessageSource
等接口例子来了解这种设计 -
创建一个类来实现
ServiceType
,或者实现你在第一步创建的类型。 这个类型有name
和一个为这个类型创建ServiceReference
的函数。这个name必须匹配Record
类的type
字段,这个record的type就是您自己定义的服务类型。 -
创建一个类继承
io.vertx.ext.discovery.types.AbstractServiceReference
,您可以参数化您将要返回的带有服务对象类型的类,你必须实现AbstractServiceReference#retrieve()
函数来创建服务对象。这个函数只能被调用一次。如果需要清除服务对象,也要重写AbstractServiceReference#onClose()
函数 -
在打包jar时在jar内创建一个
META-INF/services/io.vertx.servicediscovery.spi.ServiceType
文件。这个文件仅仅包含您在第二步创建的类的全名。 -
创建一个包含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
来释放服务对象,所以你不需要持有服务引用。
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步走:
-
用
MessageSource
创建一个服务对象。 -
推送消息
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步:
-
用
JDBCDataSource
创建record -
推送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步:
-
用
RedisDataSource
创建服务记录 -
推送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步:
-
用
MongoDataSource
创建服务记录 -
推送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
状态。
当服务发现组件停止,桥接器也随即停止。
注意:在集群当中,只需要注册服务记录当中一个成员的桥接器即可,因为所有的服务记录都是互通的。
其他的桥接器支持
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.1.8</version>
</dependency>
-
Gradle (
build.gradle
文件):
compile 'io.vertx:vertx-service-discovery-bridge-consul:4.1.8'
然后,在创建服务发现对象的时候,像下面这样注册桥接器:
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.1.8</version>
</dependency>
-
Gradle (
build.gradle
文件):
compile 'io.vertx:vertx-service-discovery-bridge-kubernetes:4.1.8'
然后,当创建服务发现时,按如下注册桥接器:
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
。
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.1.8</version>
</dependency>
-
Gradle(在您的
build.gradle
文件中):
compile 'io.vertx:vertx-service-discovery-bridge-zookeeper:4.1.8'
然后,在创建服务发现时按如下方式注册此桥:
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 桥接器可以从 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.1.8</version>
</dependency>
-
Gradle (
build.gradle
文件):
compile 'io.vertx:vertx-service-discovery-bridge-docker:4.1.8'
创建服务发现对象时,注册桥接器的示例:
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.1.8</version>
</dependency>
-
Gradle (
build.gradle
文件):
compile 'io.vertx:vertx-service-discovery-backend-redis:4.1.8'
需要注意的是,你只能在 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")
)));