使用vertx构建响应式微服务-第四章 系统

上一章的重点是构建 microservices, 但本章是关于构建系统的。一个微服务不能叫做系统,系统由许多微服务组成。管理越多的微服务系统越复杂。
首先, 我们将学习如何使用服务发现来解决位置透明性和移动性问题。
然后, 我们将讨论弹性和稳定性模式, 如超时, 断路器, 和故障转移。

服务发现

当你有一套微服务,第一个问题就是如果让他们发现彼此?为了和另一个微服务通信,需要知道它的地址。

正如上章所见,我们可以硬编程地址(事件总线地址,RUL,路径,等)在代码中或者放在配置文件中。然而这个解决方案没有移动性。你的应用将会相当僵硬,每一部分都不能移动,这与我们想用微服务做的背道而驰。

客户端和服务器端服务发现


Microservices 需要移动, 但可寻址。消费者需要能够与 microservice 通信, 而不知道它的确切位置, 特别是因为这个位置可能会随着时间的推移而改变。位置透明性提供了弹性和活力: 使用循环策略的用户可以调用 microservice 的不同实例, 在两个调用之间 microservice 可能已经移动或更新。

位置透明度可以通过称为服务发现的模式来解决。每个 microservice 应该宣布如何调用它及其特性, 包括它的位置, 以及其他元数据 (如安全策略或版本)。这些公告存储在服务发现基础结构中, 通常是执行环境提供的服务注册表。microservice 还可以决定从注册表中撤回其服务。寻找其他服务的 microservice 还可以搜索此服务注册表以查找匹配的服务, 选择最好的一个 (使用任何类型的标准), 然后开始使用它。


可以使用两种类型的模式来消耗服务。使用客户端服务发现时, 使用者服务会根据服务注册表中的名称和元数据查找服务, 选择匹配的服务并使用它。从服务注册表检索到的引用包含指向 microservice 的直接链接。由于 microservices 是动态实体, 因此服务发现基础结构必须不仅允许提供商发布其服务和消费者来查找服务, 还提供有关服务的到达和离职的信息。使用客户端服务发现时, 服务注册表可以采取各种形式, 如分布式数据结构、专用基础结构 (如领事) 或存储在诸如 Apache 动物园或 Redis 等库存服务中。


或者, 您可以使用服务器端服务发现, 并让负载平衡器、路由器、代理或 API 网关管理您的发现 (图 4-2)。使用者仍然根据其名称和元数据查找服务, 但检索虚拟地址。当使用者调用服务时, 请求被路由到实际实现。您可以在 Kubernetes 或使用 AWS 弹性负载平衡器时使用此机制。

Vert.x服务发现


vert.x 提供了一个可扩展的服务发现机制。您可以使用相同的 API 在客户端或服务器端服务发现。vert.x 服务发现可以从许多类型的服务发现基础结构 (如Consul 或 Kubernetes) 中导入或导出服务 (图 4-3)。它也可以在没有任何专用服务发现基础结构的情况下使用。在这种情况下, 它使用在vert.x 群集上共享的分布式数据结构。


您可以按类型检索服务, 以便能够使用已配置的服务客户端。服务类型可以是 HTTP 端点、事件总线地址、数据源等。例如, 如果要检索我们在上一章中实现的名为 hello 的 HTTP 端点, 请编写以下代码:

// We create an instance of service discovery
ServiceDiscovery discovery = ServiceDiscovery.create(vertx);
// As we know we want to use an HTTP microservice, we can
// retrieve a WebClient already configured for the service
HttpEndpoint
  .rxGetWebClient(discovery,
    // This method is a filter to select the service     
rec -> rec.getName().endsWith("hello")
  )
  .flatMap(client ->
    // We have retrieved 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")); 

服务发现是 microservice 基础结构中的一个关键组件。它实现了动态性、位置透明性和机动性。在处理一小组服务时, 服务发现可能看起来很麻烦, 但当您的系统增长时, 它是必需的。无论使用何种基础结构和服务发现类型, vert.x 服务发现都为您提供了唯一的 API。但是, 当您的系统增长时, 还有另一个变量会以指数形式出现故障。

稳定性和韧性模式

在处理分布式系统时, 失败是一流的公民, 您必须与他们一起生活。您的 microservices 必须知道, 他们调用的服务可能由于许多原因而失败。microservices 之间的每一个互动最终都会以某种方式失败, 你需要为失败做好准备。失败可以采取不同的形式, 从各种网络错误到语义错误不等。

响应式微服务的错误管理

反应 microservices 负责在本地管理故障。他们必须避免把失败传播到另一个 microservice。换言之, 你不应该把烫手山芋委派给另一个 microservice。因此, 反应性 microservice 的代码认为失败是头等公民。


Vert.x 开发模型使故障成为一个中心实体。使用回调开发模型时, 处理程序通常以参数的方式接收 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 时, 可以在订阅方法中进行故障管理:

client.get("/").as(BodyCodec.jsonObject())
    .rxSend()
    .map(HttpResponse::body)
    .subscribe(
        json -> { /* success */ },         
err -> { /* failure */ }     );

如果发生错误,错误处理会被调用。我们也可以更早的处理错误。

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.x event 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 new JsonObject().put("message", "D'oh! Timeout");
  })
  .subscribe(
    json -> System.out.println(json.encode())   );

你需要记住非常重要的一点是,超时并不一定是程序错误。在一个分布式系统中,有很多原因导致错误。我们来看一个示例。你有两个微服务,A和B。A发一个请求给B.但是B没搭理A,A的请求超时。在这个例子中,可能有三种原因。

1.A给B的消息丢失了,操作没有执行。

2.B中的操作失败了--B的操作没有完成

3.B返回给A的消息丢失了--B的操作执行了,但是A没有收到响应。

最后一种情况经常被忽略而且很有害。这个时候发起重试会破坏系统的完整性(B被执行了两遍)。重试只能用于幂等运算(在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同)。在发起重试前,请确保该操作是幂等的。重试还会导致响应时间变长。因此返回回退经常比重试更好。此外不断的向发生错误的服务器发送请求也不利于他们恢复正常。下面有请断路器出场。

断路器

断路器可以防止我们重复调用一个错误的操作。当调用一个操作错误时,断路器会记录,当这个记录超出一定次数时,断路器打开,以后的请求将会被断路器切断,不会去执行那个错误的操作,而是由断路器返回一个结果。但是断路器也不是永远的打开了,经过我们配置的时间后,它觉得那个操作可以成功了,于是下次请求会去调用那个操作,如果那个操作成功了,它会自动关闭,如果失败了,他会继续打开,直到我们配置的时间到了会去再试一次,这样周而复始。

CircuitBreaker circuit = CircuitBreaker.create("my-circuit",
    vertx, new CircuitBreakerOptions()
        .setFallbackOnFailure(true) // Call the fallback
                                    // on failures
        .setTimeout(2000)           // Set the operation timeout
        .setMaxFailures(5)          // Number of failures before
                                    // switching to
                                    // the 'open' state
        .setResetTimeout(5000)      // Time before attempting
                                    // 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交互受到断路器保护,当失败到一定次数时,断路器阻止继续调用,在一个周期时间里,断路器会允许一次调用通过,来试探操作是否正常了。

健康检查和故障转移

虽然超时和断路器可以让消费者处理错误,但是崩溃呢?当崩溃时,故障转移策略将重启崩溃的部分。

但是在做到这些之前,我们需要知道什么时候微服务崩了。微服务提供了api来检查它自己的状态。它会告诉调用者他是否正常。调用通常使用HTTP交互,但不是必须的。经过调用,一系列的检查被执行,并返回状态。当一个服务被发现不正常时它将不会再被调用,因为就算调用也很可能会失败。就算调用正常的服务也不能保证成功。建康检查仅仅表明那个服务在运行,而不能确定它能给你想要的结果。

根据你的环境,你可能需要不同级别的健康检查。比如启动时执行准备检查,确保服务已经初始化,并准备好接受请求,运行时接受服务检查,确保服务可以正常返回结果,如果没有返回结果,说明它崩了。

在Vert.x中有多种方法来实现健康检查。你可以仅仅用返回状态的路由,或者发一个真实请求。你还可以使用Vert.x健康检查模块来实现多种健康检查,并返回不同结果。下边的代码演示了如果在一个应用中使用两种级别的健康检查。

Router router = Router.router(vertx);
HealthCheckHandler hch = HealthCheckHandler.create(vertx);
// A procedure to check if we can get a database 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丢失一个节点的连接,Vert.x会在集群中选择另一个健康的节点,并重新部署崩掉的那个。

故障转移可以保证你的系统运行,但不能解决根本问题,发生故障需要你来做分析并解决。

总结

本章讨论了你的微服务系统增长时你将面临的几个问题。

正如我们所学的,服务发现是必须做的在微服务系统中,来确保位置的公开。接着,由于不可避免的错误,我们讨论了几个模式,来提高你的系统的容错性和稳定性。

Vert.x可以用相同的API来处理客户端服务发现和服务的服务发现。Vert.x服务发现可以导入导出到其他服务发现架构。Vert.x包含了一套容错模式例如超时,断路器,故障转移。我们也看到了这些示例。处理错误虽然很枯燥,但我们不得不做。

下一章,我们将学习如何发布Vert.x 响应式微服务到OpenShift和演示如果使用服务发现,断路器,故障转移来让你的系统坚不可摧。虽然这些主题非常重要,但不要低估其他问题的重要性。例如安全,部署,汇总日志,测试等等。

本章完撒花奏乐。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值