小蚂蚁说:
2018年上半年,蚂蚁金服决定基于 Istio 订制自己的 ServiceMesh 解决方案,并在6月底正式对外公布了 SOFAMesh 。
在 SOFAMesh 的开发过程中,针对遇到的实际问题,我们给出了一套名为 x-protocol 的解决方案,本文将会对这个解决方案进行详细的讲解,后面会有更多内容,欢迎持续关注本系列文章。
前言
x-protocol 的定位是云原生、高性能、低侵入性的通用 Service Mesh 落地方案,依托 Kubernetes 基座,利用其原生的服务注册和服务发现机制,支持各种私有 RPC 协议低成本、易扩展的接入,快速享受 Service Mesh 所带来的红利。
具体解决的问题包括:
-
多通讯协议支持问题,减少开发工作量,简单快捷的接入新协议
-
尽量提升性能,提供更灵活的性能与功能的平衡点选择,满足特定高性能场景
-
兼容现有 SOA 体系,提供通过接口进行访问的方式,实现不修改业务代码也能顺利接入 Service Mesh
-
支持单进程多服务的传统 SOA 程序,可以在微服务改造之前,先受益于 Service Mesh 带来的强大功能
在本系列文章中,我们将对此进行详细的讲解,首先是“DNS通用寻址方案”。
SOFA 开源网站:
背景和需求
SOA的服务模型
在 SOFAMesh 计划支持的 RPC 框架中,SOFARPC、HSF、Dubbo 都是一脉相承的 SOA 体系,也都支持经典的SOA服务模型,通常称为”单进程多服务”,或者叫做”单进程多接口”。(备注:由于服务一词使用过于频繁,下文都统一称为接口以便区分)
SOA 标准的服务注册,服务发现和调用流程如下:
-
在单个 SOA 应用进程内,存在多个接口
-
服务注册时,以接口为单位进行多次独立的服务注册
-
当客户端进行调用时,按照接口进行服务发现,然后发起调用
当我们试图将这些 SOA 架构的应用搬迁到 ServiceMesh 时,就会遇到服务模型的问题:微服务是单服务模型,也就是一个进程里面只承载一个服务。以 k8s 的服务注册为例,在单进程单服务的模型下,服务名和应用名可以视为一体,k8s 的自动服务注册会将应用名作为服务注册的标示。
这就直接导致了 SOA 模型和微服务模型的不匹配问题:
-
SOA 以接口为单位做服务注册和服务发现,而微服务下是服务名
-
SOA 是”单进程多接口”,而微服务是”单进程单服务”
一步接一步的需求
-
先上车后补票
最理想的做法当然是先进行微服务改造,实现微服务拆分。但是考虑到现有应用数量众多,我们可能更愿意在大规模微服务改造之前,先想办法让这些应用可以运行在 ServiceMesh 下,提前受益于 ServiceMesh 带来的强大功能。因此,我们需要找到一个合适的方案,让 ServiceMesh 支持没有做微服务改造依然是”单进程多接口”形式的传统 SOA 应用,所谓”先上车后补票”。
-
不修改代码
考虑到原有的 SOA 应用,相互之间错综复杂的调用关系,最好不要修改代码,即保持客户端依然通过接口名来访问的方式。当然,SOA 架构的客户端 SDK 可能要进行改动,将原有的通过接口名进行服务发现再自行负载均衡进行远程调用的方式,精简为标准的 ServiceMesh 调用(即走Sidecar),因此修改SDK依赖包和重新打包应用是不可避免。
-
支持带特殊字符的接口名
k8s 的服务注册,Service 名是不能携带”.“号的。而 SOA 架构下,接口名有时出于管理方便,有可能是加了域名前缀,如“com.alipay.demo.interface-2”。为了实现不修改原有代码,就只能想办法支持这种带特殊字符的接口名。
参考 Kubernetes 和 Istio
在进一步讨论解决方案之前,我们先来看一下kubernetes 和 istio 中的标准请求寻址方式。
(备注:过程稍显复杂,涉及到k8s/istio的一些底层细节。但是了解这个过程对后续的理解非常重要,也可以帮助大家了解k8s和k8s的工作原理,强烈推荐阅读。)
k8s下的DNS寻址方式
在k8s下,如图所示,假定我们部署了一个名为 userservice 的应用,有三个实例,分别在三个 pod 中。则应用部署之后,k8s 会为这个应用分配 ClusterIP 和域名,并在DNS中生成一条DNS记录,将域名映射到ClusterIP:
当部署在 k8s 下的某个充当客户端的应用发起请求时,如图中的 HTTP GET请求,目标 URL 地址为 “http://userservice/id/1000221" 。 请求的寻址方式和过程如下:
-
首先进行域名解析,分别尝试解析“userservice”/“userservie.default.svc.cluster.local”等域名,得到 ClusterIP
-
然后客户端发出请求的报文,目标地址为ClusterIP,源地址为当前客户端所在的 pod IP(简单起见,端口先忽略)
-
请求报文随即被 kube-proxy 拦截,kube-proxy 根据 ClusterIP,拿到ClusterIP 对应的多个实际服务实例所在的 pod ip,取其中一个,修改目标地址为这个pod IP
-
请求报文最终就被发送到服务实例所在的pod IP
应答回来的方式类似,userservice发出的应答报文会被 kube-proxy 拦截并修改为发送到客户端所在的 pod IP。
我们详细看一下请求和应答全称的四个请求包的具体内容(简单起见继续忽略端口):
重点关注请求和应答报文的源地址和目标地址:
-
客户端发出的请求,为“客户端到 ClusterIP”
-
kube-proxy 拦截到请求后,将请求修改为“客户端到服务器端”
-
服务器端收到请求时,表现为“客户端到服务器端”,ClusterIP 被kube-proxy 屏蔽
-
服务器端发送应答,因为收到的请求看似来自客户端,因此应答报文为”服务器端到客户端”
-
应答报文被 kube-proxy 拦截,将应答修改为 “ClusterIP到服务器端”
-
客户端收到应答,表现为“ClusterIP 到服务器端”,服务器端 IP 被 kube-proxy 屏蔽
kube-proxy 在客户端和服务器端之间拦截并修改请求和应答的报文,联通两者,但各自屏蔽了一些信息:
-
在客户端看来它是在和 ClusterIP 交互,userservice 的具体服务器端实例对客户端是无感知的
-
在服务器端看来,客户端是直接在和它交互,ClusterIP 的存在对服务器端是无感知的
更深入一步,看 kube-proxy 在两个拦截和修改报文中的逻辑处理关系,即kube-proxy是如何在收到应答时正确的找回原有的 ClusterIP:
-
在拦截并修改请求报文之后,kube-proxy 会保存报文修改的5元组对应关系(5元组指源 IP 地址,源端口,协议,目的地 IP 地址,目的地端口)
-
在收到应答报文后,根据应答报文中的5元组,在保存的5元组对应关系中,找到对应信息,得到原有的 ClusterIP 和端口,然后修改应答报文
总结,通过上述k8s下的寻址方式,客户端只需发送带简单寻址信息的请求(如 “http://userservice/id/1000221" 中的“userservice” ),就可以寻址到正确的服务器端。这期间有两个关注点:
-
通过 DNS,建立了域名和 ClusterIP 的关系。
对于客户端,这是它能看到的内容,非常的简单,域名、DNS 是非常容易使用的。
-
而通过 kube-proxy 的拦截和转发,又打通了ClusterIP 和服务器端实际的Pod IP
对于客户端,这些是看不到的内容,不管有多复杂,都是k8s在底层完成,对客户端,或者说使用者透明。
以客户端的视角看来,这个DNS寻址方式非常的简单直白:
Istio的 DNS 寻址方式
Istio的请求寻址方式和普通 kubernetes 非常相似,原理相同,只是 kube-proxy被 sidecar 取代,然后 sidecar 的部署方式是在 pod 内部署,而且客户端和服务器端各有一个 sidecar。其他基本一致,除了图中红色文本的部分:
-
iptables 在劫持流量时,除了将请求转发到 localhost 的 Sidecar 处外,还额外的在请求报文的 TCP options 中将 ClusterIP 保存为 original dest。
-
在 Sidecar (Istio 默认是 Envoy)中,从请求报文 TCP options 的 original dest 处获取 ClusterIP
通过TCP options 的 original dest,iptables就实现了在劫持流量到Sidecar的过程中,额外传递了 ClusterIP 这个重要参数。Istio为什么要如此费力的传递这个 ClusterIP 呢?
看下图就知道了,这是一个 Virtual Host 的示例, Istio 通过 Pilot 将这个规则发送给 Sidecar/Envoy ,依靠这个信息来匹配路由请求找到处理请求的cluster:
domains中,除了列出域名外,还有一个特殊的IP地址,这个就是 k8s 服务的 ClusterIP!因此,Sidecar可以通过前面传递过来的 ClusterIP 在这里进行路由匹配(当然也可以从报文中获取 destination 然后通过域名匹配)。
总结,Istio 延续了 k8s 的寻址方式,客户端同样只需发送带简单寻址信息的请求,就可以寻址到正确的服务器端。这期间同样有两个关注点:
-
通过DNS,建立了域名和ClusterIP的关系。
-
通过 ClusterIP 和 Pilot 下发给 Virtual Host 的配置,Sidecar 可以完成路由匹配,将 ClusterIP 和目标服务器关联起来
同样,对于客户端,这些是看不到的内容。
因此,以客户端的视角看来,Isito的这个DNS寻址方式同样的简单直白!
DNS通用寻址方案具体介绍
解决问题的思路
在详细讲述了 k8s 和 istio 的 DNS 寻址方案之后,我们继续回到我们的主题,我们要解决的问题:
如何在不修改代码,继续使用接口的情况下,实现在 Service Mesh _上运行现有的 __Dubbo/HSF/SOFA _等传统 __SOA 应用?
这里有一个关键点:k8s 的服务注册是以基于 Service 或者说基于应用(app name),而我们的客户端代码是基于接口的。因此,在 Virtual Host 进行路由匹配时,是不能通过域名匹配的。当然,这里理论上还有一个思路,就是将接口注册为 k8s Service。但是,还记得要支持接口特殊字符的需求吗?带点号的接口名,k8s 是不能接受它作为 Service Name 的,直接堵死了将接口名注册到 k8s Service 的道路。
这样,我们就只有一条路可以走了:效仿 istio 的做法,通过 ClusterIP 匹配!
而要将接口名(如”com.alipay.demo.interface-1”)和 ClusterIP 关联,最简单直接的方式就是打通DNS :
只需要在DNS记录中,增加接口到 ClusterIP 的映射,然后就可以完全延续Istio的标准做法!其他的步骤,如域名解析到ClusterIP,iptables拦截并传递ClusterIP,sidecar读取ClusterIP并匹配路由,都完全可以重用原有方案。
具体实现方案
实现时,我们选择了使用 CoreDNS 作为 k8s 的 DNS 解决方案,然后通过 Service Controller 操作 CoreDNS 的记录来实现 DNS 解析。
为了收集到 SOA 应用的接口信息,我们还提供了一个 Register Agent 给 Service Controller 收集信息。
详细的实现方案,不在本文中重复讲述,请参阅我们之前的分享文章 SOFAMesh 的通用协议扩展 中的 DNS 寻址方案一节。
(备注:暂时修改 CoreDNS 记录的方式是直接修改 CoreDNS 的底层数据,不够优雅。未来将修改为通过 CoreDNS 的 Dynamic updates API 接口进行,不过 CoreDNS 的这个API还在开发中,需要等待完成。)
单进程多接口问题的解决
上面的解决方案,在解决通过接口实现访问的同时,也将”单进程多接口”的问题一起解决了:
-
原 SOA 应用上 k8s 时,可以注册为标准的 k8s Service,获取 ClusterIP。此时使用应用名注册,和接口无关。
-
通过操作 CoreDNS,我们将该 SOA 应用的各个接口都添加为 DNS 记录,指向该应用的 ClusterIP
-
当客户端代码使用不同的接口名访问时,DNS解析出来的都是同一个 ClusterIP,后续步骤就和接口名无关了
欠缺微服务改造带来的限制
需要特别指出的是,DNS 通用寻址方案虽然可以解决使用接口名访问和支持单进程多接口的问题,但是这种方案只是完成了“寻址”,也就是打通端到端的访问通道。由于应用没有进行微服务改造,部署上是依然一个应用(体现为一个进程,在 k8s 上体现为一个 Service)中包含多个接口,本质上:
-
服务注册依然是以应用名为基础,对应的 k8s service 和 service 上的 label也是应用级别
-
因此提供的服务治理功能,也是以 k8s 的 Service 为基本单位,包括灰度,蓝绿,版本拆分等所有的 Vesion Based Routing 功能
-
这意味着,只能进行应用级别的服务治理,而不能继续细分到接口级别
这个限制来源于应用没有进行微服务改造,没有按照接口将应用拆分为多个独立的微服务,因此无法得到更小的服务治理粒度。这也就是我在2018年上半年,蚂蚁金服决定基于 Istio 订制自己的 ServiceMesh 解决方案,在6月底对外公布了 SOFAMesh,详情请见之前的文章: 大规模微服务架构下的Service Mesh探索之路 。
DNS通用寻址方案总结
我们将这个方案称为“DNS通用寻址方案”,是因为这个方案真的非常的通用,体现在以下几个方面:
-
对使用者来说,通过域名和 DNS 解析的方式来访问,是非常简单直白而易于接受的,同时也是广泛使用的,适用于各种语言、平台、框架。
-
这个方案延续了 k8s 和 istio 的做法,保持了一致的方式方式,对用户提供了相同的体验
-
这个寻址方案,不仅仅可以用于 Dubbo、SOFA、HSF 等 RPC 框架往 Service Mesh 的迁移,也可以适用于基于 HTTP/REST 协议的 SOA 应用,甚至最传统的 web 应用(例如 tomcat 下部署多个 war 包)迁移到Service Mesh
-
我们也在考虑在未来的 Serverless 项目中,将 Function 的寻址也统一到这套方案中,而无需要求每个 Function 都进行一次服务注册
概括的说,有了这套 DNS 通用寻址方案,不管需要寻址的实体是什么形态,只要它部署在 Service Mesh 上,满足以下条件:
-
有正常注册为 k8s Service,分配有 ClusterIP
-
为实体(或者更细分的子实体)分配域名或子域名,然后添加到 DNS,解析到 ClusterIP
那么我们的 DNS 通用寻址方案,就可以工作,从而将请求正确的转发到目的地。而在此基础上,Service Mesh 所有的强大功能都可以为这些实体所用,实现我们前面的目标:在不修改代码不做微服务改造的情况下,也能提前受益于 Service Mesh 带来的强大服务治理功能。
长按关注,获取分布式架构干货
欢迎大家共同打造 SOFAStack https://github.com/alipay