模块四 展望微服务
33 | 下一代微服务架构Service Mesh
下一代微服务架构Service Mesh,在如今的微服务领域可谓是无人不知、无人不晓,被很多人定义为下一代的微服务架构。那么究竟什么是Service Mesh?Service Mesh是如何实现的?
什么是Service Mesh?
我认为是Service Mesh是一种新型的用于处理服务与服务之间通信的技术,尤其适用以云原生应用形式部署的服务,能够保证服务与服务之间调用的可靠性。在实际部署时,Service Mesh通常以轻量级的网络代理的方式跟应用的代码部署在一起,从而以应用无感知的方式实现服务治理。
从我的理解来看,Service Mesh以轻量级的网络代理的方式与应用的代码部署在一起,用于保证服务与服务之间调用的可靠性,这与传统的微服务架构有着本质的区别,在我看来这么做主要是出于两个原因。
1.跨语言服务调用的需要。在大多数公司内通常都存在多个业务团队,每个团队业务所采用的开发语言一般都不相同,以微博的业务为例,移动服务端的业务主要采用的是PHP语言开发,API平台的业务主要采用的是Java语言开发,移动服务端调用API平台使用的是HTTP请求,如果要进行服务化,改成RPC调用,就需要一种既支持PHP语言又支持支持Java语言的的服务化框架。在专栏第14期我给你讲解了几种开源的服务化框架,它们要么与特定的语言绑定,比如Dubbo和Spring Cloud只支持Java语言,要么是跟语言无关,比如gRPC和Thrift,得定义个IDL文件,然后根据这个IDL文件生成客户端和服务端各自语言的SDK,并且服务框架的功能比如超时重试、负载均衡、服务发现等,都需要在各个语言的SDK中实现一遍,开发成本很高。
2.云原生应用服务治理的需要。在专栏前面,我给你讲解了微服务越来越多开始容器化,并使用Kubernetes类似的容器平台对服务进行管理,逐步朝云原生应用的方向进化。而传统的服务治理要求在业务代码里集成服务框架的SDK,这显然与云原生应用的理念相悖,因此迫切需要一种对业务代码无侵入的适合云原生应用的服务治理方式。
在这种背景下,Buoyant公司开发的第一代Service Mesh产品Linkerd应运而生。从下图中你可以看到,服务A要调用服务B,经过Linkerd来代理转发,服务A和服务B的业务代码不需要关心服务框架功能的实现。为此Linkerd需要具备负载均衡、熔断、超时重试、监控统计以及服务路由等功能。这样的话,对于跨语言服务调用来说,即使服务消费者和服务提供者采用的语言不同,也不需要集成各自语言的SDK。
而对于云原生应用来说,可以在每个服务部署的实例上,都同等的部署一个Linkerd实例。比如下面这张图,服务A要想调用服务B,首先调用本地的Linkerd实例,经过本地的Linked实例转发给服务B所在节点上的Linkerd实例,最后再由服务B本地的Linkerd实例把请求转发给服务B。这样的话,所有的服务调用都得经过Linkerd进行代理转发,所有的Linkerd组合起来就像一个网格一样,这也是为什么我们把这项技术称为Service Mesh,也就是“服务网格”的原因。
Service Mesh的实现原理
Service Mesh实现的关键就在于两点:一个是上面提到的轻量级的网络代理也叫SideCar,它的作用就是转发服务之间的调用;一个是基于SideCar的服务治理也被叫作Control Plane,它的作用是向SideCar发送各种指令,以完成各种服务治理功能。下面我就来详细讲解这两点是如何实现的。
1.SideCar
我们首先来看一下,在传统的微服务架构下服务调用的原理。你可以看下面这张图,服务消费者这边除了自身的业务逻辑实现外,还需要集成部分服务框架的逻辑,比如服务发现、负载均衡、熔断降级、封装调用等,而服务提供者这边除了实现服务的业务逻辑外,也要集成部分服务框架的逻辑,比如线程池、限流降级、服务注册等。
而在Service Mesh架构中,服务框架的功能都集中实现在SideCar里,并在每一个服务消费者和服务提供者的本地都部署一个SideCar,服务消费者和服务提供者只管自己的业务实现,服务消费者向本地的SideCar发起请求,本地的SideCar根据请求的路径向注册中心查询,得到服务提供者的可用节点列表后,再根据负载均衡策略选择一个服务提供者节点,并向这个节点上的SideCar转发请求,服务提供者节点上的SideCar完成流量统计、限流等功能后,再把请求转发给本地部署的服务提供者进程,从而完成一次服务请求。整个流程你可以参考下面这张图。
我们可以把服务消费者节点上的SideCar叫作正向代理,服务提供者节点上的SideCar叫作反向代理,那么Service Mesh架构的关键点就在于服务消费者发出的请求如何通过正向代理转发以及服务提供者收到的请求如何通过反向代理转发。从我的经验来看,主要有两种实现方案。
- 基于iptables的网络拦截。这种方案请见下图,节点A上服务消费者发出的TCP请求都会被拦截,然后发送给正向代理监听的端口15001,正向代理处理完成后再把请求转发到节点B的端口9080。节点B端口9080上的所有请求都会被拦截发送给反向代理监听的端口15001,反向代理处理完后再转发给本机上服务提供者监听的端口9080。
- 采用协议转换的方式。这种方案请见下图,节点A上的服务消费者请求直接发给正向代理监听的端口15001,正向代理处理完成后,再把请求转发到节点B上反向代理监听的端口15001,反向代理处理完成后再发送给本机上的服务提供者监听的端口9080。
可见,这两种方案最大的不同之处在于,一个是通过iptables网络拦截实现代理转发的,一个是靠直接把请求发送给代理来转发的。基于iptables网络拦截的方式,理论上会有一定的性能损耗,但它的优点是从网络层实现调用拦截,能做到完全的业务无感知,所以适合云原生应用。而直接把请求发送给代理的方式,要求代理层加入业务逻辑,才能把请求转发给对应的服务提供者监听的端口。
2.Control Plane
既然SideCar能实现服务之间的调用拦截功能,那么服务之间的所有流量都可以通过SideCar来转发,这样的话所有的SideCar就组成了一个服务网格,再通过一个统一的地方与各个SideCar交互,就能控制网格中流量的运转了,这个统一的地方就在Sevice Mesh中就被称为Control Plane。如下图所示,Control Plane的主要作用包括以下几个方面:
- 服务发现。服务提供者会通过SideCar注册到Control Plane的注册中心,这样的话服务消费者把请求发送给SideCar后,SideCar就会查询Control Plane的注册中心来获取服务提供者节点列表。
- 负载均衡。SideCar从Control Plane获取到服务提供者节点列表信息后,就需要按照一定的负载均衡算法从可用的节点列表中选取一个节点发起调用,可以通过Control Plane动态修改SideCar中的负载均衡配置。
- 请求路由。SideCar从Control Plane获取的服务提供者节点列表,也可以通过Control Plane来动态改变,比如需要进行A/B测试、灰度发布或者流量切换时,就可以动态地改变请求路由。
- 故障处理。服务之间的调用如果出现故障,就需要加以控制,通常的手段有超时重试、熔断等,这些都可以在SideCar转发请求时,通过Control Plane动态配置。
- 安全认证。可以通过Control Plane控制一个服务可以被谁访问,以及访问哪些信息。
- 监控上报。所有SideCar转发的请求信息,都会发送到Control Plane,再由Control Plane发送给监控系统,比如Prometheus等。
- 日志记录。所有SideCar转发的日志信息,也会发送到Control Plane,再由Control Plane发送给日志系统,比如Stackdriver等。
- 配额控制。可以在Control Plane里给服务的每个调用方配置最大调用次数,在SideCar转发请求给某个服务时,会审计调用是否超出服务对应的次数限制。
总结
简单来说,Service Mesh思想的孕育而生,一方面出于各大公司微服务技术的普及,增加了对跨语言服务调用的需求;另一方面得益于微服务容器化后,采用Kubernetes等云平台部署的云原生应用越来越多,服务治理的需求也越来越强烈。Service Mesh通过SideCar代理转发请求,把服务框架的相关实现全部集中到SideCar中,并通过Control Plane控制SideCar来实现服务治理的各种功能,这种业务与框架功能解耦的思想恰好能够解决上面两个问题。
Service Mesh在诞生不到两年的时间里取得令人瞩目的发展,在国内外都涌现出一批具有代表性的新产品,最著名的莫过于Google、IBM领导的Istio,也是Service Mesh技术的代表之作,我会在下一期给你详细讲解。而国内在这一方面也不遑多让,秉承了Service Mesh的思想也走出了各自的实践之路,并且已经开始在线上的核心业务中大规模使用,比如微博的Weibo Mesh、华为公有云Service Mesh以及蚂蚁金服的SOFA Mesh等。
思考题
Service Mesh中SideCar的部署模式是在每个服务节点的本地,都同等部署一个SideCar实例,为什么不使用集中式的部署模式,让多个服务节点访问一个SideCar实例?
34 | Istio:Service Mesh的代表产品
随着技术发展,现在来看Linkerd可以说是第一代Service Mesh产品,到了今天当我们再谈到Service Mesh时,往往第一个想到的是Istio。为什么我认为Istio可以称得上是Service Mesh的代表产品呢?在我看来主要有以下几个原因:
- 相比Linkerd,Istio引入了Control Plane的理念,通过Control Plane能带来强大的服务治理能力,可以称得上是Linkerd的进化,算是第二代的Service Mesh产品。
- Istio默认的SideCar采用了Envoy,它是用C++语言实现的,在性能和资源消耗上要比采用Scala语言实现的Linkerd小,这一点对于延迟敏感型和资源敏感型的服务来说,尤其重要。
- 有Google和IBM的背书,尤其是在微服务容器化的大趋势下,云原生应用越来越受欢迎,而Google开源的Kubernetes可以说已经成为云原生应用默认采用的容器平台,基于此Google可以将Kubernetes与Istio很自然的整合,打造成云原生应用默认的服务治理方案。
现在我们一起走进Istio的架构,看看它各部分的实现原理。
Istio整体架构
如下图所示,Istio的架构可以说由两部分组成,分别是Proxy和Control Plane。
- Proxy,就是前面提到的SideCar,与应用程序部署在同一个主机上,应用程序之间的调用都通过Proxy来转发,目前支持HTTP/1.1、HTTP/2、gRPC以及TCP请求。
- Control Plane,与Proxy通信,来实现各种服务治理功能,包括三个基本组件:Pilot、Mixer以及Citadel。
下面我来详细分解Istio架构,看看每一个组件的作用和工作原理。
Proxy
Istio的Proxy采用的是Envoy,Envoy是跟上一期提到的Linkerd是同一代的产品,既要作为服务消费者端的正向代理,又要作为服务提供者端的反向代理,一般需要具备服务发现、服务注册、负载均衡、限流降级、超时熔断、动态路由、监控上报和日志推送等功能,它主要包含以下几个特性:
- 性能损耗低。因为采用了C++语言实现,Envoy能提供极高的吞吐量和极少的长尾延迟,而且对系统的CPU和内存资源占用也不大,所以跟业务进程部署在一起不会对业务进程造成影响。
- 可扩展性高。Envoy提供了可插拔过滤器的能力,用户可以开发定制过滤器以满足自己特定的需求。
- 动态可配置。Envoy对外提供了统一的API,包括CDS(集群发现服务)、RDS(路由发现服务)、LDS(监听器发现服务)、EDS(EndPoint发现服务)、HDS(健康检查服务)、ADS(聚合发现服务)等。通过调用这些API,可以实现相应配置的动态变更,而不需要重启Envoy。
Envoy是Istio中最基础的组件,所有其他组件的功能都是通过调用Envoy提供的API,在请求经过Envoy转发时,由Envoy执行相关的控制逻辑来实现的。
Pilot
Pilot的作用是实现流量控制,它通过向Envoy下发各种指令来实现流量控制,它的架构如下图所示。从架构图里可以看出,Pilot主要包含以下几个部分:
- Rules API,对外封装统一的API,供服务的开发者或者运维人员调用,可以用于流量控制。
- Envoy API,对内封装统一的API,供Envoy调用以获取注册信息、流量控制信息等。
- 抽象模型层,对服务的注册信息、流量控制规则等进行抽象,使其描述与平台无关。
- 平台适配层,用于适配各个平台如Kubernetes、Mesos、Cloud Foundry等,把平台特定的注册信息、资源信息等转换成抽象模型层定义的平台无关的描述。
那么具体来讲,Pilot是如何实现流量管理功能的呢?
1.服务发现和负载均衡
就像下图所描述的那样,服务B也就是服务提供者注册到对应平台的注册中心中去,比如Kubernetes集群中的Pod,启动时会注册到注册中心etcd中。然后服务A也就是服务消费者在调用服务B时,请求会被Proxy拦截,然后Proxy会调用Pilot查询可用的服务提供者节点,再以某种负载均衡算法选择一个节点发起调用。
除此之外,Proxy还会定期检查缓存的服务提供者节点的健康状况,当某个节点连续多次健康检查失败就会被从Proxy从缓存的服务提供者节点列表中剔除。
2.请求路由
Pilot可以对服务进行版本和环境的细分,服务B包含两个版本v1.5和v2.0-alpha,其中v1.5是生产环境运行的版本,而v2.0-alpha是灰度环境运行的版本。当需要做A/B测试时,希望灰度服务B的1%流量运行v2.0-alpha版本,就可以通过调用Pilot提供的Rules API,Pilot就会向Proxy下发路由规则,Proxy在转发请求时就按照给定的路由规则,把1%的流量转发给服务B的v2.0-alpha版本,99%的流量转发给服务B的v1.5版本。
3.超时重试
缺省状态下,Proxy转发HTTP请求时的超时是15s,可以通过调用Pilot提供的Rules API来修改路由规则,覆盖这个限制。比如下面这段路由规则,表达的意思是ratings服务的超时时间是10s。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:name: ratings
spec:hosts:- ratingshttp:- route:- destination:host: ratingssubset: v1timeout: 10s
除此之外,还可以通过修改路由规则,来指定某些HTTP请求的超时重试次数,比如下面这段路由规则,表达的意思就是ratings服务的超时重试次数总共是3次,每一次的超时时间是2s。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:name: ratings
spec:hosts:- ratingshttp:- route:- destination:host: ratingssubset: v1retries:attempts: 3perTryTimeout: 2s
4.故障注入
Istio还提供了故障注入的功能,能在不杀死服务节点的情况下,通过修改路由规则,将特定的故障注入到网络中。它的原理是在TCP层制造数据包的延迟或者损坏,从而模拟服务超时和调用失败的场景,以此来观察应用是否健壮。比如下面这段路由规则的意思是对v1版本的ratings服务流量中的10%注入5s的延迟。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:name: ratings
spec:hosts:- ratingshttp:- fault:delay:percent: 10fixedDelay: 5sroute:- destination:host: ratingssubset: v1
而下面这段路由规则意思是对v1版本的ratings服务流量中的10%注入HTTP 400的错误。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:name: ratings
spec:hosts:- ratingshttp:- fault:abort:percent: 10httpStatus: 400route:- destination:host: ratingssubset: v1
Mixer
Mixer的作用是实现策略控制和监控日志收集等功能,实现方式是每一次Proxy转发的请求都要调用Mixer,它的架构请见下图。而且Mixer的实现是可扩展的,通过适配层来适配不同的后端平台,这样的话Istio的其他部分就不需要关心各个基础设施比如日志系统、监控系统的实现细节。
理论上每一次的服务调用Proxy都需要调用Mixer,一方面检查调用的合法性,一方面要上报服务的监控信息和日志信息,所以这就要求Mixer必须是高可用和低延迟的,那么Mixer是如何做到的呢?下图是它的实现原理,从图中你可以看到Mixer实现了两级的缓存结构:
- Proxy端的本地缓存。为了减少Proxy对Mixer的调用以尽量降低服务调用的延迟,在Proxy这一端会有一层本地缓存,但由于Proxy作为SideCar与每个服务实例部署在同一个节点上,所以不能对服务节点有太多的内存消耗,所以就限制了Proxy本地缓存的大小和命中率。
- Mixer的本地缓存。Mixer是独立运行的,所以可以在Mixer这一层使用大容量的本地缓存,从而减少对后端基础设施的调用,一方面可以减少延迟,另一方面也可以最大限度减少后端基础设施故障给服务调用带来的影响。
那么Mixer是如何实现策略控制和监控日志收集功能呢?
1.策略控制
Istio支持两类的策略控制,一类是对服务的调用进行速率限制,一类是对服务的调用进行访问控制,它们都是通过在Mixer中配置规则来实现的。具体来讲,速率限制需要配置速率控制的yaml文件,每一次Proxy转发请求前都会先调用Mixer,Mixer就会根据这个yaml文件中的配置,来对调用进行速率限制。比如下面这段配置表达的意思是服务默认访问的速率限制是每秒5000次,除此之外还定义了两个特殊限制,第一个是v3版本的reviews服务请求ratings服务的速率限制是每5秒1次,第二个是其他服务请求ratings服务的速率限制是每10秒5次。
apiVersion: config.istio.io/v1alpha2
kind: memquota
metadata:name: handlernamespace: istio-system
spec:quotas:- name: requestcount.quota.istio-systemmaxAmount: 5000validDuration: 1soverrides:- dimensions:destination: ratingssource: reviewssourceVersion: v3maxAmount: 1validDuration: 5s- dimensions:destination: ratingsmaxAmount: 5validDuration: 10s
而访问控制需要配置访问控制的yaml文件,每一次Proxy转发请求前都会先调用Mixer,Mixer就会根据这个yaml文件中的配置,来对调用进行访问控制。比如下面这段配置表达的意思是v3版本的reviews服务调用ratings服务就会被拒绝。
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:name: denyreviewsv3
spec:match: destination.labels["app"] == "ratings" && source.labels["app"]=="reviews" && source.labels["version"] == "v3"actions:- handler: denyreviewsv3handler.denierinstances: [ denyreviewsv3request.checknothing ]
2.监控和日志收集
跟策略控制的实现原理类似,Mixer的监控、日志收集功能也是通过配置监控yaml文件来实现的,Proxy发起的每一次服务调用都会先调用Mixer,把监控信息发给Mixer,Mixer再根据配置的yaml文件来决定监控信息该发到哪。示例yaml文件可以参考这个链接。
Citadel
Citadel的作用是保证服务之间访问的安全,它的工作原理见下图,可见实际的安全保障并不是Citadel独立完成的,而是需要Proxy、Pilot以及Mixer的配合,具体来讲,
- Citadel里存储了密钥和证书。
- 通过Pilot把授权策略和安全命名信息分发给Proxy。
- Proxy与Proxy之间的调用使用双向TLS认证来保证服务调用的安全。
- 最后由Mixer来管理授权和审计。
总结
本文讲解了Istio的架构及其基本组件Proxy、Pilot、Mixer以及Citadel的工作原理,从Istio的设计和实现原理可以看出,它是采用模块化设计,并且各个模块之间高度解耦,Proxy专注于负责服务之间的通信,Pilot专注于流量控制,Mixer专注于策略控制以及监控日志功能,而Citadel专注于安全。正是这种高度模块化的设计,使得Istio的架构极具扩展性和适配性,如果你想加强流量控制方面的功能,可以在Pilot模块中定制开发自己的代码,而不需要修改其他模块;如果你想增加一种监控系统支持,可以在Mixer模块中添加对这个监控系统的适配器,就能接入Istio。除此之外,虽然Istio由Google和IBM主导,但也没有完全与Kubernetes平台绑定,你也可以在Mesos或者AWS上运行Istio,可见它的适配性极强,这也是Istio的强大之处,以至于它的竞争对手Linkerd也开始支持Istio,作为可选的Proxy组件之一。
思考题
Mixer的一个功能是实现服务调用的日志收集,假如某一个服务调用并发量很高,而每一次调用都经过Proxy代理请求Mixer,再由Mixer调用后端的日志系统的话,整个链路的网络延迟就会对服务调用的性能影响很大,你有什么优化建议吗?
35 | 微博Service Mesh实践之路(上)
由于Istio的设计理念非常新,并且它诞生在微服务容器化和Kubernetes云平台火爆之后,所以从设计和实现上,Istio都天生对云原生应用更友好。
但是现实是不是也是那么美好呢?对于一个已经上线运行多年的业务系统来说,要想从经典的微服务架构走上Istio这条看似完美的道路并不容易,各种内部基础设施的定制化以及业务稳定性优先准则等因素,都注定了大多数公司要走出一条自己的Service Mesh实践之路。今天我就来带你回顾下微博是如何一步步走向Service Mesh的。
跨语言服务调用的需求
微博的服务化框架采用的是自研的Motan,Motan诞生于2013年,出于微博平台业务单体化架构拆分为微服务改造的需求,在结合当时的开源服务化框架和自身实际的需求,选择了采用自研的方式。而且由于微博平台的业务采用的是Java语言开发,所以Motan早期只支持Java语言。后期随着微博业务的高速发展,越来越多的PHP业务开始登上舞台,于是在微博的流量体系中,主要是三股服务之间的相互调用:一个是Java与Java语言,一个是PHP和Java语言,一个是PHP和PHP语言。Java应用之间的调用采用的是Motan协议,而Java应用与PHP、PHP与PHP应用之间采用的都是HTTP协议。我回忆了一下当时一次PHP与Java之间的HTTP调用过程,大致需要经过DNS解析、四层LVS负载均衡、七层Nginx负载均衡,最后才能调用Java应用本身。
从上面这张图可以看出,一次HTTP调用的链路相当长,从我的实践来看,经常会遇到好几个问题。
第一个问题:中间链路损耗大。由于一次HTTP调用要经过DNS、LVS、Nginx这三个基础设施,每一层都会带来相应的损耗。我曾经在线上就碰到过因为DNS解析延迟、LVS带宽打满引起的网络延迟,以及Nginx本地磁盘写满引起的转发延迟等各种情况,造成接口响应在中间链路的损耗甚至超过了接口本身业务逻辑执行的时间。
第二个问题:全链路扩容难。由于微博业务经常要面临突发热点事件带来的流量冲击,所以需要能够随时随地动态扩缩容。其实在应用本身这一层扩容并不是难点,比较麻烦的是四七层负载均衡设备的动态扩缩容,它涉及如何评估容量、如何动态申请节点并及时修改生效等,要完成一次全链路扩容的话,复杂度非常高,所以最后往往采取的办法是给四七层负载均衡设备预备足够的冗余度,在峰值流量到来时,只扩容应用本身。
第三个问题:混合云部署难。专栏前面我讲过微博的业务目前采用的是混合云部署,也就是在内网私有云和公有云上都有业务部署,同样也需要部署四七层负载均衡设备,并且要支持公有云上的请求经过DNS解析后要能够转发到公有云上的负载均衡设备上去,避免跨专线访问带来不必要的网络延迟和专线带宽占用。
因此,迫切需要一种支持跨语言调用的服务化框架,使得跨语言应用之间的调用能够像Java应用之间的调用一样,不需要经过其他中间链路转发,做到直接交互,就像下图描述的那样。
Yar协议的初步尝试
为此,微博最开始考虑基于Motan框架进行扩展,使其支持PHP语言的Yar协议,下面是扩展后的架构图。这个架构的思路是PHP客户端的服务发现通过Nginx来支持,经过Nginx把PHP的Yar协议请求转发给服务端,由于Motan框架中了适配Yar协议,服务端会把PHP的Yar协议请求转换成Motan请求来处理,处理完后再转成Yar协议的返回值经过Nginx返回给客户端。
但这种架构主要存在两个问题。
第一个问题:Motan协议与Yar协议在基本数据结构和序列化方式的支持有所不同,需要经过复杂的协议转换。
第二个问题:服务调用还必须依赖Nginx,所以调用链路多了一层,在应用部署和扩容时都要考虑Nginx。
gRPC会是救命稻草吗
时间往后推演,gRPC横空出世,它良好的跨语言特性,以及高效的序列化格式的特性吸引了我们,于是便开始考虑在Motan中集成gRPC,来作为跨语言通信的协议。当时设计了下图的架构,这个架构的思路是利用gRPC来生成PHP语言的Client,然后在Motan框架中加入对gRPC协议的支持,这样的话PHP语言的Client就可以通过gRPC请求来调用Java服务。
但在我们的实际测试中,发现微博的业务场景并不适合gRPC协议,因为gRPC协议高度依赖PB序列化,而PHP对PB的兼容性不是很好,在微博的业务场景下一个接口返回值有可能超过几十KB,此时在PHP Client端PB数据结构解析成JSON对象的耗时甚至达到几十毫秒,这对业务来说是不可接受的。而且gRPC当时还不支持PHP作为Server对外提供服务,也不满足微博这部分业务场景的需要。
代理才是出路
考虑到PHP语言本身没有常驻内存控制的能力,在实现服务注册和发现以及其他各种服务框架功能时,仅靠PHP-FPM进程本身难以实现,因此需要一个统一常驻内存的进程来帮助完成服务框架的各种功能。一开始我们考虑过使用本地守护进程和OpenResty的Timer来实现服务发现,但其他服务框架的功能不太好实现,比如专栏前面提到的各种复杂的负载均衡策略、双发、熔断等。为此,我们希望通过一个Agent也就是代理,来帮助PHP进程来完成服务框架的各种功能,PHP进程本身只需要负责运行业务逻辑的代码,以及最简单的Motan协议解析。基于这个思路,当时我们设计了下面这个架构,它的思路就是在PHP进程的本地也部署一个Agent,PHP进程发出去的请求都经过Agent进行处理后,再发给对应的Java应用。
向Service Mesh迈进
2017年,就在我们开始采用Agent方案对业务进行改造,以支持PHP应用调用Java应用服务化的时候,Service Mesh的概念突然火热起来,并随着Istio的发布风靡业界。相信经过我前面对Service Mesh的讲解,你一定会发现这里的Agent不恰恰就是Service Mesh中的SideCar吗?没错,我们跨语言调用的解决方案竟然与Service Mesh的理念不谋而合。借鉴Service Mesh的思想,我们也对Agent方案进一步演化,不仅客户端的调用需要经过本地的Agent处理后再转发给服务端,服务端在处理前也需要经过本地的Agent,最后再由服务端业务逻辑处理,下面是它的架构图。如此一来,业务只需要进行集成最简单的Motan协议解析,而不需要关心其他服务框架功能,可以理解为业务只需要集成一个轻量级的Client用于Motan协议解析,而繁杂的服务框架功能全都由Agent来实现,从而实现业务与框架功能的解耦。
从上面的图中你可以看出,这个架构与上一期我们聊的Istio大体思路相同,但是区别还是很明显的,可以概括为以下几点:
- 都通过SideCar方式部署的代理来实现流量转发,Istio里使用的是Envoy,而Weibo Mesh采用的是自研的Motan-go Agent。这里有一个很明显的区别是,Weibo Mesh中业务代码还需要集成一个轻量级的Client,所以对业务有一定的倾入性;而Istio采用的是iptables技术,拦截网络请求给Envoy,所以业务无需做任何变更,更适合云原生应用。在微博的业务场景下,由于大部分业务并不是云原生应用,都是部署在物理机或者虚拟机集群之中的,所以需要根据自己的业务特点来决定SideCar的部署方式。而且Weibo Mesh中的轻量级Client除了实现基本的Motan协议的解析功能之外,还添加了一些业务需要的特性,比如为了防止Agent不可用,在本地保存了一份服务节点的本地快照,必要时Client可以访问本地快照获得节点的地址,直接向服务节点Server发起调用,而不需要经过Agent转发处理,只不过这个时候就丧失了Agent的服务治理功能。
- Weibo Mesh和Istio都具备服务治理功能,只不过Istio是通过Control Plane来控制Proxy来实现,并且Control Plane包括三个组件Pilot、Mixer以及Citedar,三者各司其职。而Weibo Mesh是通过统一的服务治理中心来控制Agent,从而实现服务治理的。这是因为微博本身的各种基础设施大部分是自研的,比如注册和配置中心是自研的Vintage、监控系统是自己基于Graphite改造的、容器平台DCP以及负责容量评估的Diviner也是自研的,为此需要一个统一的地方把这些基础设施串联起来。而Istio好像就为开源而生,设计之初就要考虑如何更好地集成并支持各类开源方案,比如专门抽象出Mixer组件来对接各种监控和日志系统。
总结
本文讲解了微博是如何一步步走向Service Mesh之路的,从这个过程你可以看出微博的Weibo Mesh并不是一开始就是设计成这样的,它是随着业务的发展,对跨语言服务调用的需求日趋强烈,才开始探索如何使得原有仅支持Java语言的服务化框架Motan支持多语言,在这个过程中又不断尝试了各种解决方案之后,才笃定了走Agent代理这条路,并实际应用到线上。而随着Service Mesh概念的兴起,微博所采用的Agent代理的解决方案与Service Mesh理念不谋而合,于是在Agent代理的方案中吸纳Service Mesh的思想,进一步演变成如今的Weibo Mesh。所以说一个可靠的架构从来都不是设计出来的,是逐步演进而来的。
思考题
如果要支持更多不同语言应用之间的相互调用,你觉得Weibo Mesh中的轻量级的Client需要做哪些工作?
36 | 微博Service Mesh实践之路(下)
Service Mesh主要由两部分组成,一部分是SideCar,负责服务之间请求的转发;一部分是Control Plane,负责具体的服务治理。从Weibo Mesh的实现方案来看,对应的SideCar采用的是自研的Motan-go Agent,服务治理则是通过统一服务治理中心来实现,这里面的一些思路还是和Control Plane有很大区别的。
今天我们就来聊聊Weibo Mesh实现的技术细节,看看它给业务带来了哪些收益,最后再谈谈Weibo Mesh下一步的发展方向。
Motan-go Agent
我们知道Weibo Mesh中使用的SideCar就是Motan-go Agent,考虑到Motan-go Agent要与PHP进程部署在一起,为了减少对本机资源的占用,这里Motan-go Agent采用了Go语言来实现,它包含的功能模块请看下图。
我们拆解一下图中Motan-go Agent主要的模块,看看它们的作用是什么。
Filter Chain模块是以请求处理链的组合方式,来实现AccessLog(请求日志记录)、Metric(监控统计)、CircuitBreaker(熔断)、Switcher(降级)、Tracing(服务追踪)、Mock(单元测试)、ActiveLimit(限流)等功能。
High Available模块是用来保证高可用性,默认集成了Failover、Backup Request等故障处理手段。
Load Balance模块负载均衡,默认集成了Random、Roundrobin等负载均衡算法。
EndPoint模块的作用是封装请求来调用远程的Server端,默认可以封装Motan请求和gRPC请求。
Serialize模块负责实现不同类型的序列化方式,默认支持Simple序列化。
Server模块实现不同类型的Server,要么是采用Motan协议实现,要么是采用gRPC协议。
Motan-go Agent每个模块都是功能可扩展的,你可以在Filter Chain模块加上自己实现的Trace功能,这样请求在经过Filter Chain处理时,就会自动加载你加上的Trace功能。当然,你也可以在High Available模块添加自己实现的故障处理手段,在Load Balance模块里实现自己的负载均衡算法,在EndPoint模块封装HTTP协议的请求,在Serialize模块添加PB序列化,在Server模块实现HTTP协议等。
另外Motan-go Agent之间的通信采用的是自定义的Motan2协议,它把请求中的Meta信息与请求参数信息进行了分离,更适合对请求进行代理转发,并且默认使用了Simple序列化来对不同语言的数据进行编码,以实现跨语言服务通信。
更多关于Motan2协议和Simple序列化的介绍,你可以点击这里查看。
统一服务治理中心
专栏上一期我给你讲过,在Weibo Mesh中是通过统一服务治理平台与Motan-go Agent交互来实现服务治理功能的。对着下面这张Weibo Mesh的架构图,我们一起看一下统一服务治理平台SGCenter具体是如何与Motan-go Agent交互,来实现服务治理的各项功能的。
1.动态服务注册与发现
首先来看下统一服务治理平台是如何实现服务注册与发现的。如下图所示,在Motan-go Agent中实现了具体的服务注册与发现的逻辑,Server端进程启动时,会通过Motan-go Agent向Vintage注册中心发起注册请求,把服务注册到Vintage中。Client端发起服务调用时,会经过Motan-go Agent转发,Motan-go Agent会调用Vintage查询该服务在Vintage中的注册信息,获取到服务节点列表后,按照某一种负载均衡算法选择一个服务节点,向这个服务节点发起调用。可以通过统一服务治理平台SGCenter,调用Vintage的管理接口,执行添加或者删除服务节点等操作,Motan-go Agent会感知到服务节点的变化,获取最新的服务节点。一般在业务开发或者运维人员需要手工扩容或者缩容一批服务节点时,才会执行这个操作。
2.监控上报
再看下面这张图,Client端发起的请求经过Motan-go Agent转发时,Motan-go Agent就会在内存中统计每一次调用的耗时、成功率等信息,并且每隔固定的时间间隔将这段时间内各个服务调用的QPS、平均耗时、成功率以及P999等metric信息发送给Graphite监控系统。这样的话,通过SGCenter调用Graphite的Web API就可以获取到服务调用的信息了。
3.动态流量切换与降级
动态流量切换与降级的过程请看下面这张图。Motan-go Agent在查询Vintage中某个服务节点信息的同时也会订阅该服务的变更,这样的话就可以通过SGCenter向Vintage下发服务的切流量或者降级指令,订阅了这个服务的Motan-go Agent就会收到变更通知,如果是切流量指令,比如把调用永丰机房服务的流量都切换到土城机房,那么Motan-go Agent就会把原本发给永丰机房的请求都发给土城机房;如果是降级指令,Motan-go Agent就会停止调用这个服务。
4.自动扩缩容
服务调用时Motan-go Agent会把Server端服务调用的监控信息上报给Graphite监控系统,同时Diviner容量评估系统会实时调用Graphite以获取服务在不同区间的QPS信息以计算服务池的水位线,然后SGCenter会每隔一段时间调用Diviner来获取各个服务池的冗余度以决定是否需要扩容。假如此时服务池的冗余度不足的话,SGCenter就会调用DCP容器运维平台给服务池进行扩容,DCP完成扩容后新的服务节点就会注册到Vintage当中,这样的话订阅了该服务的Motan-go Agent就会感知到服务节点的变化,从Vintage中获取最新的服务节点信息,这就是一个服务自动扩缩容的整个流程,你可以参考下面这张图。
Weibo Mesh的收益
Weibo Mesh是在微博的业务场景下,一步步进化到今天这个架构的,它给微博的业务带来的巨大的收益,总结起来主要有以下几点:
- 跨语言服务化调用的能力。Weibo Mesh发展之初最首要的目的,就是想让微博内部的Motan服务化框架能够支持PHP应用与Java应用之间调用,因而开发了Motan-go Agent,并在此基础上演变成今天的Weibo Mesh。支持多种语言之间的服务化调用,有助于统一公司内部业务不同语言所采用的服务化框架,达到统一技术体系的目的。
- 统一服务治理能力。以微博应对突发热点事件带来的峰值流量冲击为例,为了确保首页信息流业务的稳定性,我们有针对性的研发了自动扩缩容系统。而随着微博的不断发展,不断涌现出新的业务线,比如热门微博和热搜,也同样面临着突发热点事件带来的流量冲击压力。而开发一套稳定可用的自动扩缩容系统并非一朝一夕之事,如何能够把信息流业务研发的自动扩缩容系统推广到各个业务线,是个比较棘手的问题。因为信息流业务的后端主要采用了Java语言实现,而热门微博和热搜主要采用的是PHP语言,无法直接接入自动扩缩容系统。而Weibo Mesh可以支持多种语言,将热门微博和热搜业务进行服务化改造,就可以统一接入到自动扩缩容系统,实现了公司级的统一服务治理能力。
- 业务无感知的持续功能更新能力。采用Motan或者Dubbo类似的传统服务化框架,一旦服务框架功能有升级就需要业务同步进行代码升级,这对大部分业务来说都是一种不愿承受的负担。而采用Weibo Mesh,添加新功能只需要升级Motan-go Agent即可,业务代码不需要做任何变更,对于业务开发人员更友好。尤其是作为公司级的服务化框架时,服务框架的升级如果跟业务系统升级绑定在一起,从我的实践经验来看,将是一件耗时费力的工作,需要协调各个业务方配合才能完成。而Weibo Mesh可以看作是服务器上部署的基础组件,它的升级与维护不需要各个业务方的参与,这样才能具备作为公司级的服务化框架推广到各个业务线的前提。
Weibo Mesh的发展规划
在微博的业务场景下,存在大量服务对缓存、数据库以及消息队列等资源的调用,如果把资源也看作是一种服务,那么Weibo Mesh不仅可以管理服务与服务之间的调用,还可以管理服务与资源之间的调用,这样的话Weibo Mesh强大的服务治理能力也能延伸到对资源的治理上,对业务来说又将解决资源治理这一大难题。另一方面,随着Weibo Mesh治理的服务越来越多,收集的数据也越来越多,利用这些数据可以挖掘一些更深层次的东西,也是Weibo Mesh未来的发展方向之一。比如,引入机器学习算法,对采集的数据进行分析,进行监控报警的优化等。
总结
从Motan-go Agent和统一服务治理中心的具体实现这两个方面讲解了Weibo Mesh的技术细节,你可以看到很多都是微博基于自身业务特点定制化的解决方案。对于大部分中小团队来说,除非从一开始就采用了云原生应用的部署方式,否则Istio等开源方案并不能直接拿来就用,都需要从自身的业务特征和既有技术体系出发,选择一条适合自己的Service Mesh实践之路。Weibo Mesh也因为其紧贴业务,并没有脱离实际去设计,所以才能够在微博的业务中落地生根,被证明是行之有效的架构实践,使得微博服务化体系的统一成为可能,也坚定了我们在Weibo Mesh这条路上继续走下去。
思考题
Service Mesh中业务与服务框架解耦的优秀设计思想,除了能用于服务与服务之间相互调用的场景,你认为还能应用于哪些业务场景中?