Service Mesh 在有赞的实践与发展
閱讀本文約花費: 20 (分鐘)
一、缘起
有赞初期,使用的是 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 开发并上线了服务化代理组件 Tether0.1 版本。设计的主要功能为:
实现对有赞内部跨语言 RPC 协议 NOVA 的解析 对接有赞内部 ETCD 服务发现中心,解析并缓存服务发现数据
通过本地端口接受 NOVARPC 请求,并根据解析的 NOVA 请求信息和服务发现结果,将请求通过长链接转发至后端服务
进行总结就是:Tether0.1 版本是实现了代理、转发 NOVARPC 请求至相应的服务提供方的本地
Agent,简单的架构图如下:
从架构图上可以看出:
对于 PHP-FPM(现存的单体应用),只需要实现 NOVA 协议的编解码即可
服务化整体架构的复杂度,包括:服务发现、负载均衡、后端服务的优雅下线等等,全部都下沉到 Tether 层处理。
Tether 与有赞监控、日志平台对接,实现了对微服务间调用的监控和报警。
从功能和架构上可以很清晰地看出,Tether 就是 ServiceMesh 架构中的 Sidecar,只不过在有赞初期的实践中,只有服务的调用方 (consumer 端) 通过 Sidecar 发起 RPC 请求。
虽然在此阶段引入 Tether 作为 Sidecar 的出发点是为了解决 PHP-FPM 调用后端服务的问题,但是,ServiceMesh 架构带来的优势很快就体现了出来:整个微服务架构的复杂度都对应用隐藏了,架构的功能迭代和升级对业务应用完全透明,只需由运维升级 Sidecar,业务应用便具备了新的微服务功能特性。
以此为基础,有赞开始逐步将单体的 PHP-FPM 应用拆分成逻辑和业务上相对独立的微服务,逐步缩小 PHP-FPM 应用上所承载的业务逻辑,开启了有赞微服务架构的演进之路。
二、发?/p>
时间来到 2017 年年中,随着技术团队和业务的进一步发展,有赞的核心业务中台基本上都选择了 Java 与 Dubbo 的组合,为跨语言调用设计的 NOVA 协议以及 ZanPHP 框架逐渐式微。在这样的趋势和技术发展背景之下,有赞技术架构确定了新的发展方向:后端业务中台全部迁移至 JavaPHP-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…
在 ServiceMesh 架构落地过程中,我们欣喜的发现,除了最初的设计意图之外,Tether 作为 Sidecar 还能够实现其他非常有价值的功能,进而在一些项目中发挥至关重要的作用。
在实际项目中,我们遇到了这样一种场景:应用需要调用部署在另一个机房中的服务。由于调用发起方和服务提供方分属不同的微服务集群,服务发现无法发现部署在另一个机房中的服务提供方;同时,在跨机房调用的场景下,数据的安全性也必须得到高度的重视,跨机房调用必须经过严格的加密和鉴权。
有赞 PaaS 团队通过“服务伪装 (ServicePretender,以下简称 SP)”应用和 Tether 相结合,很巧妙的满足了跨机房服务发现和数据加密、鉴权的需求。整体的架构图如下:
值得注意的是,为了便于理解,图中只画出了 A 机房 Service0 调用 B 机房 Service1 的调用图示。实际上 A、B 机房是完全对称的,B 机房调用 A 机房的应用是完全一样的。对称的设计极大地简化了系统架构,降低了运维难度。SP 与服务注册中心、Tether 以及对端机房对称部署的 SP 进行交互,主要完成以下两个功能:
提供 HTTPS 服务,对端的 SP 应用可以通过相应的接口,获取到本机房服务注册中心 ETCD 上的应用元数据 (图示中,机房 A 的 SP 通过 HTTPS 协议从 B 机房的 SP 获取到注册在 B 机房 ETCD 上的 Service1 的元数据信息)
与指定的 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 请求的 ServiceMesh 架构还存在一定的距离。即使 Tether 的可靠性已经在生产环境得到了长时间的验证,在已然非常成熟的 dubbo 生态中推广使用,还存在非常多的困难和阻力。
有赞云,是有赞面向有技术研发能力的商家和开发者提供的实现自定义拓展和需求的“云平台”。作为“云平台”,有赞云需要为开发者提供一整套、包含完整功能的“微服务架构”,以便开发者快速搭建自己的应用、服务集群。按照有赞内部的经验,推动业务开发者升级应用框架是一个极其漫长而痛苦的过程;面对使用“有赞云”的外部开发者,可想而知,这个过程将会变得更加不可控。ServiceMesh 架构完美地解决了这个问题,通过将复杂的架构功能下沉到 Sidecar,应用框架将变得非常简单和轻薄。微服务层的功能迭代和升级,仅需静默升级 Sidecar 即可,无需任何业务开发者的参与和协同,极大地提升了整体架构的灵活性和功能迭代可控性。
与此同时,使用“有赞云”的开发者们必然不会都使用 Java 这一种开发语言,使用 ServiceMesh 架构避免了为每种支持的语言都开发微服务架构和进行后续功能迭代,在极大的减少开发工作量的同时保证了各个语言的微服务能力同步迭代。在上文中已经提及,目前有赞主站使用的 dubboRPC(默认使用 hessian 序列化) 协议与 Java 语言特性耦合严重,不适合于跨语言调用的场景。基于此,在“有赞云”场景中,我们设计了基于 HTTP1.1 和 HTTP/2 协议的可拓展的 RPC 协议,用于跨语言调用的场景。其中 HTTP/2 协议由于其标准化、异步性、高性能、语义清晰等特性,期待其成为未来跨语言调用的主流。
另外值得一提的是,“有赞云”已经全面拥抱开源社区,使用 Kubernetes 进行服务编排和应用管理,极大的提升了多用户场景下的运维效率,也为未来支持更多的开源特性和拓展打下了坚实的基础。在此基础上,有赞 PaaS 团队将 Istio 的服务发现组件 Pilot 也引入到了“有赞云”的 ServiceMesh 架构中,Pilot 直接对接 Kubernetes 为 TetherSidecar 提供服务发现能力。通过 Kubernetes 本身的服务编排能力,业务应用不再需要进行服务注册和服务保活;“有赞云”业务微服务集群也不再需要搭建独立的服务发现集群 (ETCD,Consul 等)。在简化整体架构、降低成本的同时,向开源社区更靠近了一步。
目前,基于 Kubernetes、IstioPilot、TetherSidecar 的 ServerMesh 架构已经在“有赞云”中全面落地,新特性和功能也在不断迭代中。期待为开发者提供更友好的开发、托管环境和更强大的功能支持。
五、展望
有赞主站的应用目前正在逐步向容器化和 Kubernetes 迁移,并且将在 19 年年内实现绝大多数应用的容器化。随着业务规模的不断增长,有赞微服务集群规模也随之水涨船高,有赞在服务化初期选择的 ETCDV2 服务发现在 18 年双十一期间已经遇到了瓶颈:应用发布时,全集群广播服务发现信息造成 ETCD 集群抖动。为了支持更大规模的微服务集群规模,同时对 dubbo 框架和 TetherSidecar 屏蔽实际服务发现数据的存储格式和存储系统,目前有赞内部也在将服务发现从 ETCD 迁移至 IstioPilot 组件,使用 Pilot 提供的 ADS(AggregatedDiscoveryService, 聚合发现服务) 接口获取服务发现数据。
在开源 Pilot 的基础上,为了满足有赞内部的需求。我们对 Pilot 进行了一系列的优化和改造,包括服务发现适配有赞 ETCD 的数据结构、对一定时间窗口内的服务集群变更事件进行聚合等。有赞内部还通过 Pilot 和 Istio 的路由规则,实现了 dubbo 请求的流量控制,通过具体的 RPC 流量控制,在上层包装出了“灰度发布”、“蓝绿发布”等产品。无论是 Pilot 的优化和改造,还是对 Istio 路由规则的使用,近期都会有专门的文章进行详细的介绍,这里不再展开。
未来,我们期望,在有赞主站应用完全容器化 +Kubernetes 之后,主站的整体服务化架构会向“有赞云”发展:Pilot 直接通过 Kubernetes 的服务编排能力获取服务发现数据,dubbo 框架不再需要进行服务注册和服务保活。
同时,有赞内部,部分网关型的 Java 应用需要调用大量不同的后端服务接口,为免去不断的构造调用句柄之苦,已经开始接入 TetherSidecar,并在生产环境进行使用;大量的其他 Java 应用也已经在 QA 环境接入和使用 TetherSidecar。期待在不久的将来,通过 ServiceMesh 加上容器化两把利刃,能够让架构升级和迭代的过程更加可控,免去业务开发者升级架构之苦的同时尽快享受到新的功能。