Service Mesh在有赞的实践与发展

Service Mesh的概念自2017年初提出之后,受到了业界的广泛关注,作为微服务的下一代发展架构在社区迅速发酵,并且孵化出了诸如Istio等广受业界关注的面向于云原生(Cloud Native)的微服务架构。目前阿里、华为云、腾讯云都在Service Mesh上投入了大量精力进行研发和推广。阐述和讨论Service Mesh架构的文章目前网络上已经非常丰富,在此不再赘述。本文主要阐述Service Mesh架构在有赞是如何一步步发展和落地的,期望能够给读者带来一定的思考和借鉴意义,并对Service Mesh架构能够解决的问题和应用场景有进一步的了解。同时,有赞Service Mesh架构发展的过程也正是有赞微服务架构的演进过程,期待能够给正在进行微服务改造的团队带来一定的启发和思考。

缘起

有赞初期,使用的是Nginx+PHP-FPM,所有的业务逻辑代码都在一个叫做Iron的PHP代码仓库里,是一个典型的单体应用(Monolith),整体架构可以简单的表示成下图: 该架构在有赞初期,团队规模比较小,且业务逻辑相对比较简单的时候,很好的支撑和承载了有赞的核心业务。但是,随着有赞业务和团队规模的极速发展,单体应用的缺陷愈来愈凸显:

  • 耦合性高
  • 隔离性差
  • 团队协作性差

一次发布带来的故障往往需要几个业务团队的人坐在一起,花费数十分钟甚至几个小时才能定位究竟是哪处改动引发的。对单体应用进行微服务改造,势在必行。

综合当时团队和业务发展的实际情况,一方面,有赞选择了国内非常流行且具备良好生态的dubbo作为Java语言RPC框架;另一方面,考虑到团队中有相当数量PHP开发的同学,有赞内部孵化出了ZanPHP——使用PHP语言的纯异步RPC框架,并选择了ETCD作为服务注册和发现中心,开始搭建有赞服务化的整体架构。为了解决跨语言(Java与PHP语言之间)的RPC通信问题,有赞在facebook开源的thrift协议基础上进行了二次封装,开发了NOVA协议用以支持跨语言RPC调用。

综上所述,这一时期,整体的架构选型如下: 尽管将单体应用拆分成微服务能够带来一系列众所周知的收益,但任何业务迁移的过程都是痛苦的,同时,在迁移过程中必定会有相当长的一段时间新旧业务架构代码需要同时在线上运行。因而,现有承载了大量核心业务的单体PHP-FPM代码如何调用新拆分出来的Java或者ZanPHP的微服务,就成了首当其冲的问题。 由于PHP-FPM运行模式的特殊性:单个HTTP请求处理完成之后会释放所有的资源和内存,导致其很难实现最基本的微服务架构的需求。如,微服务架构需要调用端(consumer)长时间缓存服务发现结果,并能感知服务发现结果的变化。即使使用共享内存的方式实现,整体的实现成本也非常高,稳定性也难以得到很好的保证。 在这样的背景下,有赞PaaS团队在16年初使用golang开发并上线了服务化代理组件Tether 0.1版本。设计的主要功能为:

  • 实现对有赞内部跨语言RPC协议NOVA的解析
  • 对接有赞内部ETCD服务发现中心,解析并缓存服务发现数据
  • 通过本地端口接受NOVA RPC请求,并根据解析的NOVA请求信息和服务发现结果,将请求通过长链接转发至后端服务

进行总结就是:Tether 0.1版本是实现了代理、转发NOVA RPC请求至相应的服务提供方的本地Agent,简单的架构图如下: 从架构图上可以看出:

  • 对于PHP-FPM(现存的单体应用),只需要实现NOVA协议的编解码即可
  • 服务化整体架构的复杂度,包括:服务发现、负载均衡、后端服务的优雅下线等等,全部都下沉到Tether层处理。
  • Tether与有赞监控、日志平台对接,实现了对微服务间调用的监控和报警。

从功能和架构上可以很清晰地看出,Tether就是Service Mesh架构中的Sidecar,只不过在有赞初期的实践中,只有服务的调用方(consumer端)通过Sidecar发起RPC请求。

虽然在此阶段引入Tether作为Sidecar的出发点是为了解决PHP-FPM调用后端服务的问题,但是,Service Mesh架构带来的优势很快就体现了出来:整个微服务架构的复杂度都对应用隐藏了,架构的功能迭代和升级对业务应用完全透明,只需由运维升级Sidecar,业务应用便具备了新的微服务功能特性。

以此为基础,有赞开始逐步将单体的PHP-FPM应用拆分成逻辑和业务上相对独立的微服务,逐步缩小PHP-FPM应用上所承载的业务逻辑,开启了有赞微服务架构的演进之路。

发展

时间来到2017年年中,随着技术团队和业务的进一步发展,有赞的核心业务中台基本上都选择了Java与Dubbo的组合,为跨语言调用设计的NOVA协议以及ZanPHP框架逐渐式微。在这样的趋势和技术发展背景之下,有赞技术架构确定了新的发展方向:

  • 后端业务中台全部迁移至Java
  • PHP-FPM中与页面拼装和渲染相关的逻辑全部迁移至Node.js

随着Node.js的引入,同样的问题再次出现:Node.js作为业务编排和模版渲染层,如何调用部署在复杂服务化架构中的Java中台应用?

首先,是RPC协议和编码的选择问题。对于跨语言调用业界一般选择使用IDL来描述接口定义,并来通过工具自动生成的桩代码序列化、反序列化数据和编码、解析RPC请求包,grpc和thrift都是通过这种方式实现的多语言支持。这是笔者比较推崇的方式,IDL通常仅保留各个编程语言公共的特性,而避免引入与特定语言相关的特性,进而对跨语言调用有着非常好的支持。同时IDL本身就是良好的接口描述文档,能够在相当程度上减少沟通、协作成本,也便于开发者养成先设计接口再进行开发的习惯。

虽然有赞初期就设计了NOVA协议,通过编写IDL生成桩代码的方式实现跨语言RPC,但是使用dubbo框架的Java开发同学已经习惯了直接编写接口就能实现Java服务之间相互调用的开发模式,导致在推广NOVA协议的时候遇到了不少的阻力——存在更便捷的调用方式时,为何还要学习具有一定上手门槛的IDL?摆在面前的困境:使用Java的后端开发同学已经不情愿、甚至抵触编写IDL,向Node.js的开发同学推广IDL也可能遇到同样的问题。

紧接着是Node.js框架与有赞服务化架构的整合问题。我们调研了业界已有的开源方案dubbo2.js。虽然Node.js具有实现请求编解码、服务发现、长链接保持、请求负载均衡的能力;但是,业务前台是否有必要引入如此的复杂度?服务化调用的监控、路由策略、限流、熔断等特性,Node.js是否需要全部都实现一遍?若有赞后续业务需要使用新的编程语言:C#、Python等,那是否这些编程语言又要再实现一遍这些特性?

dubbo2.js对我们面临的第一个问题提供了一定的思路:通过显示的指定调用的后端Java接口的参数类型,dubbo2.js实现了dubbo协议的编解码,达到了不通过IDL实现跨语言调用的目的。但是,让Node.js的开发同学去感知后端的Java类型系统真的合理吗?

基于以上的思考,我们想到了早已接入有赞微服务框架的Sidecar产品:Tether。同时,在传统的Sidecar上进行了创新:为了贴近Node.js同学的开发模式和习惯,并最大程度的隐藏后端服务化架构的复杂度,我们设计了简单的HTTP+Json的接口用以Node.js与Tether之间的调用,由Tether实现HTTP协议与微服务调用的dubbo协议之间的相互转换。整体架构如下: 值得注意的是,图中的泛化调用并不是开源版本dubbo的“泛化调用”,而是有赞内部仿照开源版本的“泛化调用”针对性实现的参数使用json编码(有赞内使用dubbo默认的hessian2序列化方式编码参数)的“跨语言泛化调用”,该接口调用返回的也是hessian编码的json串。

至此,一劳永逸地解决了多语言接入有赞服务化架构的问题,由于HTTP+Json协议的通用性,任何其他语言都可以很方便地通过Tether调用核心Java服务的dubbo接口,而不用关心服务发现、监控等复杂问题。

目前,该架构在有赞生产环境中已经运行了一年半多,通过Tether的请求占到有赞微服务总流量的20%+,是有赞微服务整体架构的重要组成部分。

More…

在Service Mesh架构落地过程中,我们欣喜的发现,除了最初的设计意图之外,Tether作为Sidecar还能够实现其他非常有价值的功能,进而在一些项目中发挥至关重要的作用。

在实际项目中,我们遇到了这样一种场景:应用需要调用部署在另一个机房中的服务。由于调用发起方和服务提供方分属不同的微服务集群,服务发现无法发现部署在另一个机房中的服务提供方;同时,在跨机房调用的场景下,数据的安全性也必须得到高度的重视,跨机房调用必须经过严格的加密和鉴权。

有赞PaaS团队通过“服务伪装(Service Pretender,以下简称SP)”应用和Tether相结合,很巧妙的满足了跨机房服务发现和数据加密、鉴权的需求。整体的架构图如下: 值得注意的是,为了便于理解,图中只画出了A机房Service0调用B机房Service1的调用图示。实际上A、B机房是完全对称的,B机房调用A机房的应用是完全一样的。对称的设计极大地简化了系统架构,降低了运维难度。

SP与服务注册中心、Tether以及对端机房对称部署的SP进行交互,主要完成以下两个功能:

  1. 提供HTTPS服务,对端的SP应用可以通过相应的接口,获取到本机房服务注册中心ETCD上的应用元数据(图示中,机房A的SP通过HTTPS协议从B机房的SP获取到注册在B机房ETCD上的Service1的元数据信息)
  2. 与指定的Tether保持心跳检查,若Tether心跳正常,则将对应的Tether使用1中获取的服务元数据信息,注册到本机房的服务注册中心。即将Tether伪装成对端机房的相应应用(图示中,机房A的SP将同机房的Tether伪装成了B机房的Service1应用)

如此,当机房A中的Service0想要调用机房B中的Service1时,根据服务发现的结果,Service0会请求本机房的出口网关TetherA,TetherA收到请求后会直接转发至B机房的入口网关TetherB,再由TetherB根据本机房内实际服务发现的结果,将调用请求路由至实际需要请求的Service1。同时在网关型的TetherA与TetherB之间,使用了TLS双向验证、加密来满足数据加密和鉴权的需求。

在架构和功能上,SP与Tether完全解耦,可以各自独立迭代和升级。由于使用了dubbo框架服务发现的特性,网关Tether可以方便地进行水平扩容;当需要进行版本升级之时,也可以通过调整服务发现的权重,方便的进行小流量灰度。加上Tether早已对接有赞监控系统,所有跨机房的服务化调用都得到了很好的监控,具备很强的可观测性。所有这一切,保障了整体架构的稳定和可靠。

通过如上的设计,在无需任何业务和框架改造的前提下,有赞基础保障部门实现了应用的跨机房拆分和服务化调用。整个过程,基本上做到了无需业务方参与和改造。

当下

正如上文所述,在有赞主站,Tether目前主要是作为跨语言调用场景下Consumer端的Sidecar,以及跨机房调用时服务化流量的出入口网关。虽然在跨机房调用场景下,Tether实际上同时托管了Consumer端和Provider端的流量,但离真正Sidecar托管全部RPC请求的Service Mesh架构还存在一定的距离。即使Tether的可靠性已经在生产环境得到了长时间的验证,在已然非常成熟的dubbo生态中推广使用,还存在非常多的困难和阻力。

有赞云,是有赞面向有技术研发能力的商家和开发者提供的实现自定义拓展和需求的“云平台”。作为“云平台”,有赞云需要为开发者提供一整套、包含完整功能的“微服务架构”,以便开发者快速搭建自己的应用、服务集群。按照有赞内部的经验,推动业务开发者升级应用框架是一个极其漫长而痛苦的过程;面对使用“有赞云”的外部开发者,可想而知,这个过程将会变得更加不可控。Service Mesh架构完美地解决了这个问题,通过将复杂的架构功能下沉到Sidecar,应用框架将变得非常简单和轻薄。微服务层的功能迭代和升级,仅需静默升级Sidecar即可,无需任何业务开发者的参与和协同,极大地提升了整体架构的灵活性和功能迭代可控性。

与此同时,使用“有赞云”的开发者们必然不会都使用Java这一种开发语言,使用Service Mesh架构避免了为每种支持的语言都开发微服务架构和进行后续功能迭代,在极大的减少开发工作量的同时保证了各个语言的微服务能力同步迭代。在上文中已经提及,目前有赞主站使用的dubbo RPC(默认使用hessian序列化)协议与Java语言特性耦合严重,不适合于跨语言调用的场景。基于此,在“有赞云”场景中,我们设计了基于HTTP1.1和HTTP/2协议的可拓展的RPC协议,用于跨语言调用的场景。其中HTTP/2协议由于其标准化、异步性、高性能、语义清晰等特性,期待其成为未来跨语言调用的主流。

另外值得一提的是,“有赞云”已经全面拥抱开源社区,使用Kubernetes进行服务编排和应用管理,极大的提升了多用户场景下的运维效率,也为未来支持更多的开源特性和拓展打下了坚实的基础。在此基础上,有赞PaaS团队将Istio的服务发现组件Pilot也引入到了“有赞云”的Service Mesh架构中,Pilot直接对接Kubernetes为Tether Sidecar提供服务发现能力。通过Kubernetes本身的服务编排能力,业务应用不再需要进行服务注册和服务保活;“有赞云”业务微服务集群也不再需要搭建独立的服务发现集群(ETCD, Consul等)。在简化整体架构、降低成本的同时,向开源社区更靠近了一步。

目前,基于Kubernetes、Istio Pilot、Tether Sidecar的Server Mesh架构已经在“有赞云”中全面落地,新特性和功能也在不断迭代中。期待为开发者提供更友好的开发、托管环境和更强大的功能支持。

展望

有赞主站的应用目前正在逐步向容器化和Kubernetes迁移,并且将在19年年内实现绝大多数应用的容器化。随着业务规模的不断增长,有赞微服务集群规模也随之水涨船高,有赞在服务化初期选择的ETCD V2服务发现在18年双十一期间已经遇到了瓶颈:应用发布时,全集群广播服务发现信息造成ETCD集群抖动。为了支持更大规模的微服务集群规模,同时对dubbo框架和Tether Sidecar屏蔽实际服务发现数据的存储格式和存储系统,目前有赞内部也在将服务发现从ETCD迁移至Istio Pilot组件,使用Pilot提供的ADS(Aggregated Discovery Service, 聚合发现服务)接口获取服务发现数据。

在开源Pilot的基础上,为了满足有赞内部的需求。我们对Pilot进行了一系列的优化和改造,包括服务发现适配有赞ETCD的数据结构、对一定时间窗口内的服务集群变更事件进行聚合等。有赞内部还通过Pilot和Istio的路由规则,实现了dubbo请求的流量控制,通过具体的RPC流量控制,在上层包装出了“灰度发布”、“蓝绿发布”等产品。无论是Pilot的优化和改造,还是对Istio路由规则的使用,近期都会有专门的文章进行详细的介绍,这里不再展开。

未来,我们期望,在有赞主站应用完全容器化+Kubernetes之后,主站的整体服务化架构会向“有赞云”发展:Pilot直接通过Kubernetes的服务编排能力获取服务发现数据,dubbo框架不再需要进行服务注册和服务保活。

同时,有赞内部,部分网关型的Java应用需要调用大量不同的后端服务接口,为免去不断的构造调用句柄之苦,已经开始接入Tether Sidecar,并在生产环境进行使用;大量的其他Java应用也已经在QA环境接入和使用Tether Sidecar。期待在不久的将来,通过Service Mesh加上容器化两把利刃,能够让架构升级和迭代的过程更加可控,免去业务开发者升级架构之苦的同时尽快享受到新的功能。

欢迎关注我们的公众号