在本系列的前面两篇文章, 在Webpay中使用Linkerd作为服务网格代理 以及 Sidecar和DaemonSet: 容器模式之争 中,我们深入探讨了一些服务网格的细节,分别是服务网格代理( Linkerd)和这些代理的容器模式。
在本系列的第三部分中,我们将站在一个更高层面来看待服务网格系统。具体来说,我们将会一起来看看服务网格系统的健康状况,并讲述该如何使用各组数据来定义WePay基础架构中服务网格架构的高可用。
和我们在本系列中前面讨论的服务网格设定一样,我们将一起来看看一个跑在 Google Kubernetes Engine (GKE)的 Kubernetes 集群上的高可用和模块化的服务网格。
我们在此之前经历了几次服务网格架构设计的迭代,而支持模块化的那个最能满足我们的需求和需要。在我们的模块化设计中,服务网格的数据面板或者说代理( Linkerd )以及服务发现或者说控制面板( Namerd )被分成两个独立的模块,以便维护和监控。在这样的服务网格架构中,服务之间可以使用Namerd相互发现,并且可以借助Linkerd将请求路由到其他服务,此外它还提供负载均衡和监控指标。
图1:服务网格模块即Namerd服务和Linkerd代理
图1展示了Namerd和Linkerd作为Webpay基础设施中实现服务网格的模块,还有将 Webpay的Sensu设定 作为核心,让我们能够在出现任何问题时查看服务网格系统的状态,并且在某些地方无法正常工作时发出告警。
正如前面所提到的那样,在这篇文章中,我们将会把重点放在如何通过监控工具简化并实现一个高可用的服务网格系统。我们还将详细介绍监控系统涉及到的一些内容,一起来看看在一个系统里什么操作被认为是正常的,什么则是异常。
上一节介绍了我们是如何通过明确定义的模块来简化服务网格的架构。使用该架构时,我们把一些时间花在了确定系统中每个模块的监控方式,随后是整个系统。我们只想简单的回答这样一个问题,“应该怎样监控系统才能在遇到问题时能够拿到第一手资料,还有,我们的告警对象是谁?”
通过不断解答这些问题,我们发现并改进了整个系统的各个部分。例如,在我们的POC阶段,我们最开始使用的一个解决方案,其中将Namerd当作Kubernetes中Linkerd的sidecar。一旦我们认识到这两个服务拥有各自的生命周期和健康检查定义会是一个更好的做法时,我们将Namerd迁移到了 HAProxy 后面的集群,后端由 Google Compute Engine 实例组成。在多次迭代后,我们把Namerd重构成了Kubernetes的service,在那里它通过一个 Kubernetes load balancer 暴露给Linkerd代理。
图2:通过K8S的namer发现的服务网格栈
在图2中,Namerd会在Kubernetes集群中做复制同步,Linkerd代理通过它的Kubernetes load balancer与Namerd通信,它会以proxy的形式运行Kubernetes proxy来监视每个Kubernetes集群的更改,而每个Kubernetes代理会向它的master上报集群更改的信息,反馈到Namerd。
这只是一个示例,其中每个模块的适用范围的明确极大地简化了服务网格系统里服务的监控和生命周期管理。在确定了每个模块的适用范围后,我们根据系统中模块的高可用要求对它们进行了测试:
下面部分介绍了每个Namerd和Linkerd模块的一些重要的监控单元。我们还会详细介绍我们开发的一些工具,针对每个Google Cloud项目和Kubernetes集群,分别在监控和告警方面实现外部和内部的可见性。
服务发现是服务网络系统的重要组成部分,对我们来说,它是基础设施里的一项关键服务,它能给出在每个数据中心里发现了多少服务。服务发现如果中断可能会影响路由,如果中断延长到几分钟,那么可能会让服务网格里发现彼此的所有服务的路由停止工作。 因此,如何监控namerd的服务发现取决于Namerd本身的心跳,以及proxy的运行状况(如图2所示),它会将服务端点的更改上报给Namerd。
作为服务网格的发现服务,Namerd内部的健康状况通过监控它的每个实例实现。我们将Namerd迁移到Kubernetes的主要原因之一是为了得到更好的自愈能力和监控,这和我们所监控和观察跑在数据中心的所有微服务的方式一致。
我们再仔细研究一下运行在Kubernetes环境中的Namerd的副本,图3显示了集群中该服务的N个副本。 Kubernetes的调度程序会监视每个副本,以确保每时每刻存在N个实时的副本。此外,每个副本配置成在一个很短的时间间隔内使用 Kubernetes的health scheduler 进行ping操作,比如每隔几秒钟,这样来衡量它的响应能力。如果这些检查中有任何一次不成功或是无法响应,那么Kubernetes会重启受影响的副本。这样一来,副本将会重新启动然后重新绑定之前配置的名称。
此外,我们可能会遇到所有副本同时受影响的情况。在这种情况下,我们会通过设置一个更高级别的心跳检测获悉Namerd完整的停机时间。这个心跳检测配置在Kubernetes的外部,并且它会在Namerd的 Kubernetes load balancer 后面寻找至少一个健康且可用的后端实例。我们通过在数据中心里使用Sensu系统的检查运行器来实现这一目标。
Sensu的检查器会每隔一个很短的时间间隔(每10秒左右)运行一次,以确保始终至少有一个后端实例是可用的。这个Sensu的检查即是针对Kubernetes中Namerd的load balancer做简单的HTTP ping。在图4中,Sensu服务端向其中一个检查运行程序发出信号,让它去ping Namerd,然后它会向配置好的通知系统上报结果。如果心跳连续多次执行失败,则会向通知服务发送告警,便于运维团队进一步调查。
我们让每个副本彼此相对独立地运行,对Namerd服务进行一个完整的健康测试,这主要是通过将每个实例的名称专用于该副本来实现的。在该模式下,每个副本都是服务本身的一个完整表达,而除了其配置之外没有其他的依赖项。此外,我们还解决了在服务需要横向扩展来适应高交易量情况下的可扩展性。
现在可以通过监控Namerd的操作来发现任何可能存在的运行时问题,我们可以把精力专注在识别可能存在的服务发现问题,即图4中的发现检查。这类监控的目的是测试Namerd使用的不同类型的代理或 namer 。
我们实现了一个自定义的Sensu检查,它针对被测试的发现类型设置了对应的 dtab配置 。这些dtab配置定义了一个特定的范畴,也称为 命名空间 :
GET /api/1/dtabs/<namespace> HTTP/1.1 HTTP/1.1 200 OK { { "prefix":"/srv/default", "dst":"/#/io.l5d.k8s/prefix/portName" }, { "prefix":"/svc", "dst":"/srv" } }
代码示例1:从Namerd获取实时的dtab配置
命名空间的一个例子就是用于HTTP1协议的出向linkerd代理的dtab配置。参考代码示例1中的一个命名空间配置,其中包含一个微服务名称和命名空间,要求Namerd解析该名称来发现该微服务的检查尝试次数:
POST /dtab/delegator.json HTTP/1.1 { "dtab":<dtab_configuration>, "namespace":<namespace_name>, "path":"/prefix/service" } HTTP/1.1 200 OK { "type":"delegate", "path":"/prefix/service", "delegate":{ "type":"alt", "path":"/srv/prefix/service", "dentry":{ "prefix":"/svc", "dst":"/srv" }, "alt":[ { "type":"neg", ... }, { "type":"leaf", "path":"/#/io.l5d.k8s/prefix/portName/service", "dentry":{ "prefix":"/srv/default", "dst":"/#/io.l5d.k8s/prefix/portName" }, "bound":{ "addr":{ "type":"bound", ... }, "id":"/%/io.l5d.k8s.daemonset/mesh/...", "path":"/" } }, ... ] } }
代码示例2:通过Namerd解析一个服务的名字
在代码示例2的API调用中可以看到,命名空间A发现了Service X,因此在Namerd的响应主体中返回了 “type”:“leaf”
对象。 在同一请求中,所有其他的发现路由都返回了 “type”:“neg”
,根据API调用中的请求主体没有识别出到Service X的路径。
该检查用到的每个命名空间都和协议及路由器类型,入向/出向,设置等有关。比如,HTTP/1.1协议有一条用于发送方路由的dtab,以及用于接收路由的另一个dtab,而且为了简单起见,这还只是考虑了在一个可发现域范围的情况,其中不包括外部服务或实体。
由于发现是每个微服务环境的核心,所有发现的检查都被视为对整体服务发现的健康状况至关重要,如果问题在短时间内无法自我纠正,将会相应地触发告警。
就像监控Namerd的发现一样,监控和测试生产环境里使用Linkerd做代理的数据面板包含两个维度,每个维度都为我们提供了有关服务网格代理如何运行的不同视角。一个维度是使用一个外部的watcher监控代理运行的健康状况,在这里即 Kubernetes的health scheduler 。另一方面,如果在给定代理是健康的条件下,它们是否可以成功地将请求路由到同一集群或是跨集群的其他节点上的代理呢?
检查代理的运行健康状况的目标是发现那些可以通过重启有问题的容器解决的问题。因此,我们需要配置一个健康检查,确保经过代理的一个完整循环,它的响应码可以被转换成Kubernetes里的对象,即响应码是200表明状态是healthy,而非200的响应码则会将容器标记成unhealthy:
GET /admin/ping HTTP/1.1 HTTP/1.1 200 OK pong
代码示例3:针对每个代理做简单的健康检查
这些健康检查可以根据基础设施环境自定义任意的复杂度,但是一般在基础设施里,只需要做基本的健康检查就能发现潜在的问题,作为监控检查的一部分,代码示例3展示了对代理容器的简单ping探测。
图5:带有Kubernetes健康检查探测的Linkerd sidecar代理
图5展示了基础设施中每个代理在一个很短的时间间隔里发生的ping,无论每个代理组使用什么 容器模式 。
当我们开始查看本节开头提到的第二个维度,即检查路由的内部运行状况时,检查代理会变得更有趣。换句话说,如果路由情况从外面看起来很好的话,我们就得看看代理是否健康并且是否能够路由到不同的域:
图6:两个Kubernetes集群之间内部和外部的探测
要做更明确一点的探测测试的话,我们需要在每个域里种下一个服务,在这里即是两个不同的Kubernetes集群,这样一来便可以按需从内部探测任何微服务。在图6中,在集群A中启动探测,指示探测服务A向其代理发送探测目标服务1的请求。要在这一设置下实现全域覆盖的话,我们可以在集群B中使用探测服务B启动一个相同的探测任务,其中探测本身是从集群B中发起,但是最终是以集群A的服务1为目标服务。相同的模式可以扩展到任意数量的集群,使用我们的探测服务的按需功能进行任意数量的迭代。
此外,一套基础设施环境中存在多种协议的情况也是适用的,比如,同时有 HTTP/1.1
或 HTTP/2
,可以将Probe Service配置成在单次探测检查中使用这些协议中的任意一种来探测目标:
图7:使用探测服务探测REST及gRPC应用
就像发现检查那样,任何路由检查都是至关重要的,如果问题没有自愈,那么会触发通知或告警。如果在服务网格的环境里某些东西不能正常工作,我们可以通过这样端到端的监控形式,弥补基础设施中可能出错的地方。
服务网格是一个技术栈,它将控制面板,数据流和负载平衡三块独立开来,而且通过一套高可用的设定,我们得以始终获得它所能带来的所有好处,并提供更好的保证。
在我们的高可用设定中,我们:
由于从高可用的服务网格设定源源不断获得的信心,我们将越来越多的微服务迁移到了服务网格,而且利用了服务网格附带的所有功能。因此,在本系列的下一篇文章中,我们将深入探讨我们的应用程序,REST或gRPC应用,是如何利用服务网格的,以及我们如何在WePay的基础架构中管理服务网格的生命周期。