到目前为止,我们仅仅在本地机器上部署了我们的微服务。当我们部署一个微服务在云端会发生什么?大多数云平台提供了让你部署和运行更容易的服务。伸缩能力、负载均衡是常见特性,这些是与部署响应式微服务尤其相关的。在这一章节,我们将看到这些特性怎样被用来开发和部署响应式微服务。
为了展示这些优势,我们将使用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绿色部分)方面扩展它,比如提供开箱即用的滚动更新、金丝雀部署、持续交付管道。
OpenShift有一些简单的实体(Entity),如图5-2所描述,投入到工作之前我们需要理解他们:
构建配置
构建是创建容器镜像的过程,镜像被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):
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所显示的那样:
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所显示的:
你也可以用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所示:
当容器就绪时,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)