用Java构建响应式微服务5-OpenShift上部署响应式微服务

到目前为止,我们仅仅在本地机器上部署了我们的微服务。当我们部署一个微服务在云端会发生什么?大多数云平台提供了让你部署和运行更容易的服务。伸缩能力、负载均衡是常见特性,这些是与部署响应式微服务尤其相关的。在这一章节,我们将看到这些特性怎样被用来开发和部署响应式微服务。

为了展示这些优势,我们将使用OpenShift(https://www.openshift.org/)。然而,大多数现代的云平台包含我们在这儿所用的这些特性。这一章的最后,你将看到云是怎样让响应式变得容易。


OpenShift是什么?

RedHat OpenShift v3是一个开源的容器平台。用OpenShift部署运行在容器中的应用,OpenShift使得构建和管理变得容易。OpenShift构建在Kubernetes(https://kubernetes.io/)之上。

Kubernetes(图5-1蓝色部分)是一个项目,拥有在大规模Linux容器里运行微服务集群的许多功能。Google打包十多年的容器经验到Kubernetes。OpenShift构建在这个经验之上,在构建和部署自动化(图5-1绿色部分)方面扩展它,比如提供开箱即用的滚动更新、金丝雀部署、持续交付管道。


图5-1 OpenShift容器平台 

OpenShift有一些简单的实体(Entity),如图5-2所描述,投入到工作之前我们需要理解他们:


图5-2 OpenShift实体 

构建配置

构建是创建容器镜像的过程,镜像被OpenShift用来实例化构成应用的不同的容器。OpenShift构建可以使用不同的策略:

. Docker: 从dockerfile文件构建一个镜像

. 源代码到镜像(S2I):基于OpenShift构建镜像(builder

image),从应用源代码构建一个镜像

. Jenkins管道:用Jenkins管道(https://jenkins.io/doc/book/pipeline)构建一个镜像,潜在地包含多个步骤比如构建、测试和部署

构建配置能够被git push自动地触发,配置变化或者依赖的镜像发生更新;显示地,手工触发。


部署配置

部署配置了构建生成的镜像的实例化,它定义了哪一个镜像被用来创建容器、需要保持活着的实例的数量。它也描述了什么时候部署应该被触发。一个部署也作为一个复制控制器,负责保持容器活着。为了达到这个目的,你设定了期望的实例数量。期望的实例数量能随时间或者基于负载波动而调整(自动伸缩)。部署也能够指定健康检查、管理滚动更新、监测死容器。


Pods

一个Pod是包含一个或更多容器的容器组,然而,通常是一个单一的容器构成。Pod的编排、计划、管理被委托给Kubernetes。Pods是可代替的,能够在任何时候被另一个实例所代替。举个例子,如果容器崩溃,另一个实例将被生成。


服务和路由

因为Pod是动态实体(实例的数量随时间而变化),我们不能依赖他们直接的IP地址(每个pod有它自己的IP地址)。服务允许我们和Pod通讯,不依赖于Pod的地址、而是使用service虚拟地址。一个服务作为一组Pods的前端代理,它也实现了负载均衡策略。

运行在OpenShift上的别的应用能够用服务访问Pods提供的功能,但是OpenShift外面的应用需要一个路由。一个路由暴露一个服务在一个象www.myservice.com这样的主机名上,因此外面的客户端能够通过主机名访问它。

在你的机器上安装OpenShift

这些是足够抽象的概念。现在是时候动手了。我们将在你的机器上安装MiniShift(https://github.com/minishift/minishift)。或者,你可以用OpenShiftOnline(https://www.openshift.com/devpreview/),或者RedHat容器开发套件V3(https://developers.redhat.com/products/cdk/download/)。

安装MiniShift(https://github.com/minishift/minishift#installation)需要hypervisor来运行容纳OpenShift的虚拟机。取决于你的主机的操作系统,你可以选择hypervisor,查看MiniShift安装向导以了解细节。

为了安装MiniShift,仅仅从MiniShift发布页(https://github.com/minishift/minishift/releases)下载最近的适合你操作系统的压缩包,解压它到你指定的位置,加minishift执行目录到你的PATH环境变量。一旦安装完成,启动MiniShift:

minishift start

一旦启动,你应该能够访问https://192.168.64.12:8443连接到你的OpenShift实例。你可能不得不确认SSL认证。用developer/developer登录。

我们还需要OpenShift客户端oc,一个命令行工具用来与你的OpenShift实例交互。从https://github.com/openshift/origin/releases/latest下载最近OpenShift客户端版本。解压它到你指定的位置,加oc执行目录到你的PATH环境变量。

然后,连接你的OpenShift实例:

oc login https://192.168.64.12:8443 -u developer -pdeveloper

OpenShift有一个命名空间的概念称之为project。为了创建我们打算部署的例子的project,执行:

oc new-project reactive-microservices

oc policy add-role-to-user admin developer –n reactive-microservices

oc policy add-role-to-user view -n reactive-microservices-z default

用你的浏览器,打开https://192.168.64.12:8443/console/project/reactive-microservices/。你应该能够看到这个project,这时它是没什么东西的,因为我们还没有部署任何东西(图5-3):


图5-3 

OpenShift部署微服务

是时候部署一个微服务到OpenShift。我们打算部署的代码放在代码仓库的openshift/hello-

microservice-openshift目录。Verticle是与我们前面部署的hello微服务很接近:

package io.vertx.book.openshift;

import io.vertx.core.AbstractVerticle;

import io.vertx.core.http.HttpHeaders;

import io.vertx.core.json.JsonObject;

import io.vertx.ext.web.*;

public class HelloHttpVerticle extends AbstractVerticle {

static finalString HOSTNAME = System.getenv("HOSTNAME");

@Override

public voidstart() {

Router router =Router.router(vertx);

router.get("/").handler(this::hello);

router.get("/:name").handler(this::hello);

vertx.createHttpServer()

.requestHandler(router::accept)

.listen(8080);

}

private voidhello(RoutingContext rc) {

String message= "hello";

if(rc.pathParam("name") != null) {

message += " " + rc.pathParam("name");

}

JsonObject json= new JsonObject()

.put("message",message)

.put("served-by",HOSTNAME);

rc.response()

.putHeader(HttpHeaders.CONTENT_TYPE,"application/json")

.end(json.encode());

}

}

代码没有依赖特定的OpenShift API或者是结构。它与你部署在你的机器的应用一样。Java代码与部署选择分离必须是一个深思熟虑的设计选择,让代码能够在任何云平台上运行。

我们将手工地创建所有OpenShift实体,但是让我们使用Fabric8提供的Maven插件(https://maven.fabric8.io/),一个为Kubernetes提供的端到端的的开发平台。如果你打开pom.xml文件,你将看到这个插件被配置在openshift profile,与Vert.X Maven插件协作一起创建OpenShift实体。

打包和部署微服务到OpenShift,执行:

mvnfabric8:deploy –Popenshift

这个命令与你用oc登录的OpenShift实例交互,创建一个构建(用源代码到镜像策略)并触发它。首次构建会花一些时间因为它需要获得builder镜像。不用担心---一旦被缓存,构建将很快被创建。构建的输出(镜像)被部署配置使用,部署配置也是被Fabric8 Maven插件创建,缺省地,它创建一个Pod。一个Service也被这个插件创建。你可以在OpenShift仪表盘上找到这些信息,象图5-4所显示的那样:


图5-4 

Fabric8 Maven插件缺省地并不创建路由(route)。然而,我们从它的定义文件(src/main/fabric8/

route.yml)中创建一个。

如果你在你的浏览器打开:http://hello-microservice-reactive-microservices.192.168.64.12.nip.io/Luke

你应该看到像这样的结果:

{"message":"helloLuke","served-by":"hello-microservice-1-9r8uv"}

hello-microservice-1-9r8uv即是为这个请求提供服务的pod的主机名。


服务发现

现在,我们部署了hello微服务,让我们用一个微服务消费它。在这一小节里我们将要部署的代码放在代码仓库的openshift/hello-microservice-consumer-openshift目录。

为了消费一个微服务,我们首先不得不找到它。OpenShift提供了一个服务发现机制。服务查找能够用环境变量、DNS或者Vert.X服务发现来实现,这们这里用Vert.X的服务发现。

项目的pom.xml配置了引入Vert.X服务发现,Kubernetes服务引入,一个服务端的服务发现。你不必在提供者侧显示地注册服务,象Fabric8 Maven声明一个服务那样。消费者将得到OpenShift服务而不是Pods。

@Override

public voidstart() {

Router router = Router.router(vertx);

router.get("/").handler(this::invokeHelloMicroservice);

// Create the service discovery instance

ServiceDiscovery.create(vertx, discovery -> {

// Look for an HTTP endpoint named "hello-microservice"

// you can also filter on 'label'

Single single =HttpEndpoint.rxGetWebClient(discovery,

rec -> rec.getName().equals("hello-microservice"),

new JsonObject().put("keepAlive", false)

);

single.subscribe(client -> {

// the configured client to call the microservice

this.hello = client;

vertx.createHttpServer()

.requestHandler(router::accept)

.listen(8080);

},

err -> System.out.println("Oh no, no service")

);

});

}

在start方法里,我们用服务发现来找到hello微服务。然后,如果服务可获得,我们启动http server、保持一个得到的web客户端的引用。我们也传递了一个配置给web客户端、keep-alive设为false(一会儿我们将明白原因)。在调用hello微服务里,我们没必要象前面所做的那样传递端口和主机给rxSend方法,实际上,web客户端被配置为目标是hello服务:

HttpRequestrequest1 = hello.get("/Luke").as(BodyCodec.jsonObject());

HttpRequestrequest2 = hello.get("/Leia").as(BodyCodec.jsonObject());

Singles1 = request1.rxSend().map(HttpResponse::body);

Singles2 = request2.rxSend().map(HttpResponse::body);

// ...

在终端控制台,切换至openshift/hello-microservice-consumer-openshift目录,构建和部署这个消费者:

mvnfabric8:deploy –Popenshift

在OpenShift仪表盘上,你应该看到第二个服务和路由。如果你打开与hello消费者服务关联的路由,你应该会看到:

{

"luke": "hello Luke hello-microservice-1-sa5pf",

"leia": "hello Leia hello-microservice-1-sa5pf"

}

你可能看到503错误页,因为pod仍然没有起来。仅仅刷新直到你得到正确的页面。到现在为止,没有什么令人吃惊的。显示的served-by值总是指向同一个pod(因为仅有一个)。


伸缩

如果我们正在用一个云平台,主要是因为可伸缩性的原因。我们希望能够根据负载增加/减少我们的应用的实例数量。在OpenShift仪表盘上,我们可以调节pods的数量的多少,正如图5-5所显示的:


图5-5

你也可以用oc命令行来设置副本的数量:

# 增加至2个副本

oc scale --replicas=2 dc hello-microservice

# 减少到0个副本

oc scale --replicas=0 dc hello-microservice

让我们创建hello微服务的第二个实例。然后,等到第二个微服务实例正确地起来(等待是令人厌烦的,后面我们将解决这个问题),在浏览上返回至hello消费者页,你应该看到像这样:

{

"luke" : "hello Luke hello-microservice-1-h6bs6",

"leia" : "hello Leia hello-microservice-1-keq8s"

}

如果你刷新几次,你将看到OpenShift在两个实例间均衡负载。你还记得keep-alive设置为false? 当http连接使用一个keep-alive连接时,OpenShift转发请求到同一个pod。注意在实践中,keep-alive是非常值得有的头,因为它允许重用连接。


在前面的情形里存在一个小问题。当我们伸展(scale

up)时,OpenShift开始分发请求到新的pod,并没有检查应用是否就绪能够服务这些请求。因此,消费者可能请求了一个还没有就绪的微服务、得到了一个失败。解决这个有两种方式:

[if !supportLists]1)      [endif]在微服务里面用健康检测;

[if !supportLists]2)      [endif]在消费者代码里准备应对失败。


健康检查和失败转移

在OpenShift里面你能够定义两种类型的检测。就绪检测(Readiness Check)用来避免更新一个微服务的时候出现停机。在滚动更新下,OpenShift直到新版本就绪才停掉前一个版本,它ping新版本微服务的就绪检测点直到它就绪、验证微服务被成功地初始化。活着检测(Liveness Check)用来判定一个容器是否活着,OpenShift周期地向活着检测点发请求,如果一个容器没有正确地应答,它将会被重启。活着检测聚焦在微服务所需求的关键资源上。在下面的例子,两个检测我们将使用同样的检测点,然而,最好是使用不同的检测点。

这个例子的代码放在openshift/hello-microservice-openshift-health-checks目录。如果你打开verticle,你将看到验证http服务是否起来的健康检测处理器:

privateboolean started;

@Override

publicvoid start() {

Router router = Router.router(vertx);

router.get("/health").handler(

HealthCheckHandler.create(vertx)

.register("http-server-running",

future ->future.complete(started ? Status.OK() : Status.KO())

)

);

router.get("/").handler(this::hello);

router.get("/:name").handler(this::hello);

vertx.createHttpServer()

.requestHandler(router::accept)

.listen(8080, ar -> started =ar.succeeded());

}

Fabric8

Maven插件被配置为使用/health作为就绪和活着健康检测。一旦这个版本的hello微服务被部署,所有后续的部署将使用就绪检测来避免出现停机,下如图5-6所示:


图5-6 滚动更新(Rolling Update)

当容器就绪时,OpenShift路由请求到这个容器、停掉老版本的容器。当我们扩展时(scale up),OpenShift不会路由请求到一个尚未就绪的容器。


使用熔断器

尽管健康检测避免了请求一个未就绪、重启死掉的微服务,我们仍然需要从别的失败比如超时、网络中断、微服务的bug等等中保护自己,在这一小节我们打算用熔断器来保护hello消费者,这一小节的代码放在openshift/hello-microservice-consumer-openshift-circuit-breaker目录。

在verticle里,我们用一个简单的熔断器来保护对hello微服务的两个请求,下面的代码使用这个设计,然而,这仅仅是大量可行的途径中的一种,比如每个请求独立地用一个熔断器、而不是用一个简单的熔断器保护两个请求:

privatevoid invokeHelloMicroservice(RoutingContext rc) {

circuit.rxExecuteCommandWithFallback(

future -> {

HttpRequest request1= hello.get("/Luke").as(BodyCodec.jsonObject());

HttpRequest request2= hello.get("/Leia").as(BodyCodec.jsonObject());

Single s1 = request1.rxSend().map(HttpResponse::body);

Single s2 = request2.rxSend().map(HttpResponse::body);

Single.zip(s1, s2, (luke, leia) -> {

// We have the result of both requestin Luke and Leia

return new JsonObject()

.put("Luke",luke.getString("message") + " " +luke.getString("served-by"))

.put("Leia",leia.getString("message") + " " +leia.getString("served-by"));

})

.subscribe(future::complete,future::fail);

},

error -> newJsonObject().put("message", "hello (fallback, "+circuit.state().toString() + ")")

).subscribe(

x ->rc.response().end(x.encodePrettily()),

t ->rc.response().end(t.getMessage())

);

}

在error情况下,我们提供一个回退(fallback)消息,指示熔断器的状态。这将帮助我们理解发生了什么。部署这个工程:

mvnfabric8:deploy –Popenshift

现在让我们收缩(scale

down)hello微服务到0,做这个,我们可以在OpenShift Web控制台上点击容器旁边的向下箭头或者运行:

oc scale--replicas=0 dc hello-microservice

现在如果你刷新消费者页面(http://hello-consumer-reactive-microservices.192.168.64.12.nip.io/),你应该看到回退(fallback)消息。前面3个请求显示:

{

"message": "hello (fallback, CLOSED)"

}

一旦失败次数达到阀值,它会返回:

{

"message": "hello (fallback, OPEN)"

}

如果你恢复hello微服务的副本(replicas)到1:

oc scale--replicas=1 dc hello-microservice

一旦微服务就绪你应该会获得正常的输出。


等等,我们是响应式的么?

是的,我们是响应式的了。让我们看看为什么。

所有的交互是异步的,使用异步的、非阻塞的http请求和响应。另外,感谢OpenShift的service,我们发送请求到一个虚拟地址,这使得有弹性。Service在一组容器中均衡负载。我们能够很容易扩展或收缩,通过调整容器的数量或者使用自动伸缩。我们也有了可恢复性。感谢健康检测,我们有了失败转移机制来确保总是有正常数量的容器在运行。在消费者一侧,我们能够使用几种恢复模式比如超时、重试、或者熔断器来从失败中保护微服务。因此,当处于负载且面对失败的情况下,我们的系统能够及时地处理请求,我们是响应式的!

任何使用非阻塞http、在云端提供负载均衡和可恢复特性的系统是响应式的吗?是的,但是不要忘记了成本。Vert.X使用事件轮询器(event loop)实现用少数线程来处理大量并发请求,展示了云的重要本质。当使用依赖于线程池的途径时,你需要:1)调整线程池找到合适的大小;2)在你的代码里处理并发,这意味着调试死锁、竞争、瓶颈;3)监控性能。云环境是基于虚拟机的,当你有很多线程时,线程安排可能变成一个大问题。

有许多非阻塞技术,并不是所有的用同样的执行模式来处理异步特性,我们可以把这些技术归为三大类:

[if !supportLists]1.      [endif]在后台使用一个线程池的途径---然后面临着调整,安排,运维时不断变化的负荷的并发挑战;

[if !supportLists]2.      [endif]使用回调线程的途径---你仍然需要管理你代码的线程安全,避免死锁和瓶颈;

[if !supportLists]3.      [endif]用同一个线程的途径,比如Vert.X---使用少数线程,从调试死锁中解放出来。

我们可以在云端使用消息系统来实现响应式微服务系统么?当然可以。在OpenShift里面我们能够用Vert.X事件总线(event bus)来构建我们的响应式微服务,但是这将不会展示虚拟服务地址、OpenShift提供的负载均衡,而是Vert.X它自己来处理。这里我们决定用http,无限选择中一种设计。按你所想的方式打造你的系统吧!


小结

在这一章节,我们在OpenShift里部署了微服务,看到了Vert.X和OpenShift怎样组合来构建响应式微服务。组合异步的http服务端和客户端,OpenShift Services、负载均衡、失败转移以及消费侧可恢复性给了我们响应式特性。

这本书聚焦在响应式。然而,当构建一个微服务系统,许多其它方面需要被管理比如安全、配置、日志等等。大多数云平台,包括OpenShift,提供了处理这些方面的服务。

关于这些topic,如果你想了解更多,查看下面的资源:

.OpenShift官网(http://openshift.org/)

.OpenShift核心概念(https://docs.openshift.com/enterprise/3.0/architecture/core_concepts/)

.Kubernetes官网(https://kubernetes.io/)

.OpenShift健康检测文档(https://docs.openshift.com/enterprise/3.0/dev_guide/application_health.html)

推荐阅读更多精彩内容