用Java构建响应式微服务4-构建响应式微服务系统
前一章节聚焦在构建微服务,这一章节全部是关于构建系统。一个微服务不能成为一个服务系统。当你拥抱微服务构架,你将有成打的微服务。管理两个微服务,正如上一章节我们所做的,是容易的。你用的微服务越多,应用就会变得更复杂。
首先,我们将学习服务发现怎样被用来实现透明的、移动的地址定位。然后,我们将讨论可恢复性及其常见模式比如超时、熔断器以及故障转移。
服务发现
当你有一组微服务器,你不得不回答的第一个问题是:这些微服务彼此怎样定位?为了与另一个通讯,一个微服务需要知道另一个的地址。正如我们在前一章节所做的,我们能够在代码中硬编码地址(事件总线地址,URL,位置描述等等),或者放到一个外部的配置文件中。然而,这种解决方式不能够移动。你的应用将会相当固化,不同的部分不能够移动,这与我们用微服务的意图相抵触。
客户端和服务端的服务发现
微服务需要可移动但是能定位。一个消费者需要在事先不知道一个微服务的确切地址的前提下能够与之通讯,特别是微服务的地址会随着时间而变化。位置透明提供了弹性和活力:消费者能够用轮询策略请求微服务的不同实例,两次请求之间微服务可能发生移动或更新。
位置透明定位通过一个称为服务发现的模式实现。每一个微服务应该声明它能够怎样被请求,它的特征,当然包括它的位置,而且包括一些别的元数据比如安全策略和版本。这些声明被存储在服务发现基础设施里,通常通过执行环境来提供服务注册器。一个微服务也能够决定从服务注册器里撤消服务。一个微服务搜索服务注册器来发现匹配的服务、选择最佳的一个(用某种标准),并开始使用它。图4-1描述这些交互:
图4-1 与服务注册器的交互消费服务有两种模式。当使用客户端服务发现时,消费者基于名称和元数据在服务注册器中查找服务,选择一个匹配的服务并使用它。从服务注册器获得的引用包括目标微服务的直接链接。因为微服务是动态实体,服务发现基础设施必须不仅允许提供者发布服务、消费者查找服务,而且也提供关于服务的可达以及弃离信息。当使用客户端服务发现时,服务注册器能够提供多种形式比如分布式数据结构,一个专用的基础设施比如Consul,或者存储在存储服务中,比如Apache Zookeeper或Redis。
或者,你能够使用服务端服务发现,让一个负载均衡器、一个路由器、一个代理或者一个API Gateway为你管理发现(图4-2)。消费者仍然基于名称和元数据查找服务,但是得到的是一个虚拟地址。但消费者请求服务时,请求被路由到真实的地址,当你使用Kubernetes或者AWS弹性负载均衡器时你就是使用了这种机制。
图4-2 服务端服务发现Vert.X服务发现
Vert.X提供了一个可扩展的服务发现机制。用同样的API,你能够使用客户端或者服务端服务发现。Vert.X能够从许多类型的服务发现基础设施(比如Consul或者Kubernetes)导入或导出服务(图4-3)。它也能够在没有任何服务发现基础设施的情况下使用,在这种情形下,它使用在Vert.X集群中共享的一个分布式的数据结构。
图4-3 从其他服务发现机制中导入服务、导出服务到其他服务发现机制你可以通过类型查找服务,获得一个就绪的服务来使用。服务类型可以是一个http端点(endpoint)、一个事件总线地址、一个数据源等等。举一个例子,如果你想查找我们在前面章节实现的命名为hello的http端点,你将写下面的代码:
// We create an instance of servicediscovery
ServiceDiscovery discovery =ServiceDiscovery.create(vertx);
// As we know we want to use an HTTPmicroservice, we can
// retrieve a WebClient already configuredfor the service
HttpEndpoint.rxGetWebClient(discovery,
// This methodis a filter to select the service
rec ->rec.getName().endsWith("hello")
)
.flatMap(client ->
// We haveretrieved the WebClient, use it to call the service
client.get("/").as(BodyCodec.string()).rxSend()
)
.subscribe(response -> System.out.println(response.body()));
获得的WebClient是被配上了服务的位置,这意味着你可以立刻用它来调用服务。如果你的环境是采用客户端发现,被配上的URL指向一个指定的服务实例;如果你采用的是服务端发现,客户端使用的是一个虚拟的URL。
取决于你的运行时基础设施,你可能不得不注册你的服务。但是当使用服务端服务发现时,你通常不必做这个,因为当服务被部署时,你声明了你的服务。否则,你需要显示地发布你的服务。为了发布一个服务,你需要创建一个记录,包含服务名、位置和元数据:
// We create the service discovery object
ServiceDiscovery discovery =ServiceDiscovery.create(vertx);
vertx.createHttpServer()
.requestHandler(req ->req.response().end("hello"))
.rxListen(8083)
.flatMap(
// Once the HTTP server is started (we are ready to serve)
// we publish the service.
server -> {
// We create a record describing the service and its
// location (for HTTP endpoint)
Record record = HttpEndpoint.createRecord(
"hello", // the name of the service
"localhost", // the host
server.actualPort(), // the port
"/" // the root of the endpoint
);
// We publish the service
return discovery.rxPublish(record);
}
)
.subscribe(rec ->System.out.println("Service published"));
在微服务基础设施里,服务发现是一个关键组件。它能够动态、透明定位、可移动。当处理少量服务时,服务发现可能看起来显得笨重,但是,当你的系统不断增长时,它是必须的。Vert.X服务发现提供你统一的API,与你所采用的基础设施、服务发现类型不相关。然而,当你的系统增长时,会有另一种可变因素呈指数增长---失败。
稳定性和可恢复模式
当处理分布式系统时,失败是头第公民,你不得不与他们打交道。你的微服务必须知道他们请求的服务可能因为很多种原因而失败。每个微服务间的交互将以某种方式失败,你需要对这些失败有所准备。失败能够以不同的形式,从多种网络错误到语义错误。
在响应式微服务里管控失败
响应式微服务是有责任管控本地的失败的。他们必须避免传播失败到别的微服务。换言之,你不应该传递烫手山竽给别的微服务。因此,响应式微服务把失败作为头等公民。
Vert.X开发模型把失败作为一个重要的内容。当使用回调开发模型时,处理器(Handler)常常接收一个AsyncResult作为参数,这个结构封装了异常操作的结果:成功时,你可以得到结果;失败时,它包含一个Throwable描述失败:
client.get("/").as(BodyCodec.jsonObject())
.send(ar -> {
if (ar.failed()) {
Throwable cause = ar.cause();
// You need to manage the failure.
} else {
// It's a success
JsonObject json = ar.result().body();
}
});
当使用RxJava API时,失败管理能够在subscribe方法里做:
client.get("/").as(BodyCodec.jsonObject())
.rxSend()
.map(HttpResponse::body)
.subscribe(
json -> { /* success */ },
err -> { /* failure */ }
);
如果失败产生于一个被订阅的流,错误处理器会被调用。你也可以更早地处理失败、以避免subscribe方法里的错误处理器:
client.get("/").as(BodyCodec.jsonObject())
.rxSend()
.map(HttpResponse::body)
.onErrorReturn(t -> {
// Called if rxSend produces a failure
// We can return a default value
return new JsonObject();
})
.subscribe(
json -> {
// Always called, either with the actual result
// or with the default value.
}
);
管控错误不好玩但它必须做。当面对失败的时候,响应式微服务的代码有责任做出合乎需要的决定。它也需要有所准备,清楚它对其他微服务请求可能会失败。
使用超时
当处理分布式交互时,我们经常使用超时。超时是一个简单的机制,允许你停止等待响应,一旦你认为响应不会到来时。恰当的超时提供了失败隔离,确保失败被限制在它所影响的微服务、允许你处理超时继续以降级模式执行。
client.get(path)
.rxSend() // Invoke the service
// We need to be sure to use the Vert.xevent loop
.subscribeOn(RxHelper.scheduler(vertx))
// Configure the timeout, if no response,it publishes
// a failure in the Observable
.timeout(5, TimeUnit.SECONDS)
// In case of success, extract the body
.map(HttpResponse::bodyAsJsonObject)
// Otherwise use a fallback result
.onErrorReturn(t -> {
// timeout or another exception
return new JsonObject().put("message", "D'oh!Timeout");
})
.subscribe(
json -> {
System.out.println(json.encode());
}
);
超时经常和重试一起使用,当一个超时发生时,我们可以重试一次。失败之后立刻重试一个操作有多种作用,但是仅仅一些是有益的。如果操作失败是因为在请求微服务方面显著的问题,立刻重试很可能再一次失败。一些暂时的失败能够用重试来解决,特别是网络失败比如丢失消息。你可以像下面这样来决定是否重试操作:
client.get(path)
.rxSend()
.subscribeOn(RxHelper.scheduler(vertx))
.timeout(5, TimeUnit.SECONDS)
// Configure the number of retries
// here we retry only once.
.retry(1)
.map(HttpResponse::bodyAsJsonObject)
.onErrorReturn(t -> {
return newJsonObject().put("message", "D'oh! Timeout");
})
.subscribe(
json ->System.out.println(json.encode())
);
这是重要的,记住超时并不表明操作失败。在分布式系统,存在很多失败原因。让我们看一个例子,你有两个微服务,A和B,A正发送一个请求给B,但是响应没有及时过来、A获得一个超时,在这个情况里,三种类型的失败可能发生:
[if !supportLists]1) [endif]A至B之间的消息丢失了:操作没有执行;
[if !supportLists]2) [endif]B的操作失败了:操作没有完成它的执行;
[if !supportLists]3) [endif]B至A的响应消息丢失了:操作被成功执行了,但是A没有收到响应。
最后一种case经常被忽略,它可能是有害的。在这种情况下,结合超时和重试可能破坏了系统的完整性。重试仅仅可以用于幂等操作:这种操作你可以调用多次也不会改变最初调用的结果。使用重试之前,总是检查确定你的系统能够优雅地处理重试操作。
重试也会使消费者等待更长时间获得响应,这也不是一件好事情。相比重试许多次,返回一个回退(fallback)通常是更好的。另外,持续地请求一个失败的服务,可能给跟踪带来不便。这两个关系可被另一个可恢复模式所管控:熔断器。
熔断器(Circuit Breaker)
熔断器是一种模式,用来处理重复的失败。它保护微服务反复请求一个失败的服务。熔断器是管理交互的三种状态自动流转(图4-4)。它开始是关闭状态,在这种状态下,熔断器正常地执行操作,如果交互成功,没有任何事情发生,如果失败,熔断器记下了一次失败,一旦失败的次数(或者失败的频度)超过了阀值,熔断器切换到打开状态,在这种状态下,请求熔断器立刻失败、根本不执行交互。代替执行操作,熔断器可能执行回退(fallback)、提供一个缺省的结果。过了设置的时间后,熔断器判断操作有成功的可能,因此它进入半开状态,在这种状态下,下一个请求将执行交互,取决于这个请求的结果,熔断器恢复到关闭状态、或者返回到打开状态直到另一次超时。
图4-4熔断器状态用Java实现的最有名的熔断器是Hystrix(https://github.com/Netflix/Hystrix)。当你在Vert.X微服务里使用Hystrix时,你需要显示地切换到Vert.X事件轮询器、执行不同的回调。或者,对于异常操作你可以用Vert.X内置的熔断器,强化Vert.X非阻塞的异步开发模型。
让我们想象一个脆弱的hello微服务。消费者应该保护与这个服务的交互、象下面这样使用熔断器:
CircuitBreaker circuit =CircuitBreaker.create("my-circuit",
vertx,
new CircuitBreakerOptions()
.setFallbackOnFailure(true) // Call thefallback
// on failures
.setTimeout(2000) // Set the operationtimeout
.setMaxFailures(5) // Number of failuresbefore
// switching to
// the 'open' state
.setResetTimeout(5000) // Time beforeattempting
// to reset
// the circuit breaker
);
// ...
circuit.rxExecuteCommandWithFallback(future->
client.get(path)
.rxSend()
.map(HttpResponse::bodyAsJsonObject)
.subscribe(future::complete, future::fail),
t -> new JsonObject().put("message", "D'oh!Fallback")
).subscribe(
json -> {
// Get the actual json or the fallback value
System.out.println(json.encode());
}
);
在这段代码里,http交互被熔断器保护。当失败次数达到配置的阀值时,熔断器将停止请求微服务、取而替之调用一个回退。一段时间后,熔断器将让一个请求通过来检查跟踪微服务是否恢复。这个例子使用一个web客户端,任何交互都能够被熔断器管理,以保护脆弱的服务、异常以及其它的失败。
运维团队需要监控熔断器切换到open状态,Hystrix和Vert.X熔断器都有监控能力。
健康检查和故障转移
超时和熔断器允许消费者在它们那一侧处理失败,崩溃呢?面对崩溃,故障转移策略重启失败的部分。但是实现这个之前,我们必须能够检测微服务死掉了。
健康检查是微服务提供的一个指示它的状态的API。它告诉请求者服务是否是健康的。调用常常使用http交互但不是必须的。一套检查被执行、整体状态被计算和返回。当一个微服务被检测出是不健康时,它不应当再被请求,因为结果很可能是失败。注意请求一个健康的微服务也不确保成功。健康检查仅仅表明服务正在运行,它不保证精确地处理请求、网络传递响应。
取决于你的环境,你可能有不同层级的健康检查。举例来说,你可能有就绪检查(readiness check),用于部署时确定微服务是否就绪可以接收请求(一切都被正确地初始化);活着检查(liveness check)用于检查不正常、表明微服务是否能够成功地处理请求。当目标微服务不能响应、活着检查不能被执行时,微服务很可能崩溃了。
在Vert.X应用里,这有几种方式实现健康检查。你可以简单地实现route返回状态,或者甚至使用一个真实的请求。在你的应用中你也可能使用Vert.X健康检查模块去实现几种健康检查、合成不同的结果。下面的代码提供了一个提供两个层级的健康检查的例子:
Router router = Router.router(vertx);
HealthCheckHandler hch =HealthCheckHandler.create(vertx);
// A procedure to check if we can get adatabase connection
hch.register("db-connection",future -> {
client.rxGetConnection()
.subscribe(c -> {
future.complete();
c.close();
},
future::fail
);
});
// A second (business) procedure
hch.register("business-check",future -> {
// ...
});
// Map /health to the health check handler
router.get("/health").handler(hch);
// ...
完成健康检查之后,你可以实现失败转移策略。一般地,策略是仅仅重启系统死掉的部分。另外,失败转移常常是被你的运行时基础设施提供的。Vert.X提供了一个内置的失败转移,当集群中的一个节点死掉时会被触发。使用内置的Vert.X故障转移,你不需要一个传统的健康检查比如周期性地ping Vert.X集群节点。当Vert.X失去对一个节点的跟踪时,Vert.X挑选集群中一个健康的节点来重新部署死掉的部分。
失败转移保证你的系统运行但是不解决根本原因---这是你的工作。当应用意外地死掉时,应该做一下剖析。
小结
这一章节定位于当你的微服务系统增长时你需要面对的几个关键问题。正如我们所学的,在任何微服务系统里,服务发现是必须有的,确保位置透明。然后,因为失败是不可避免的,我们讨论了一组模式来提升系统的可恢复性和稳定性。
Vert.X包含了一个可插拔的服务发现设施,它能够用同样的API处理客户端服务发现和服务端服务发现。Vert.X服务发现也能够从其它服务发现基础设施中导入服务、导出服务到其它服务发现基础设施。Vert.X包含了一套恢复模式比如超时、熔断器和失败转移。我们看到了这些模式的不同例子。不幸地,处理失败,我们仍不得不做部分工作。
在下一章节,我们将学习怎样部署响应式微服务到OpenShift,解释怎样做服务发现、熔断器,失败转移能够让你的系统几乎是”防弹的”。这些课题是特别重要的,不要低估处理微服务时需要处理的其他方面,比如安全、部署、聚合日志、测试等等。
关于这些课题,如果你想了解更多,查看下面的资源:
. 响应式微服务构架(https://info.lightbend.com/COLL-20XX-Reactive-Microservices-Architecture-RES-LP.html)
. Vert.X服务发现文档(http://vertx.io/docs/vertx-service-discovery/java/)
. 发布它!设计和部署生产就绪的软件(http://shop.oreilly.com/product/9780978739218.do)
. Netflix Hystrix(https://github.com/Netflix/Hystrix)
. Vert.X服务熔断器文档(http://vertx.io/docs/vertx-circuit-breaker/java/)