作为 Service Mesh 领域最具权威的控制面,Istio 从 2017 年发布第一个版本后,就有着一个堪称“非常优雅”的架构设计。但在推出近 3 年后,其开发团队却“意外”推翻之前的架构,重新用上“复古的”单体应用设计。这里面究竟遇到什么不可逾越的鸿沟? 笔者从几个简单问题(WHY、WHAT、WHEN)出发,为大家揭开这次算是 Istio 诞生以来最大一次“自我革命”的来龙去脉。
背景
“Premature optimizationComplexity is the root of all evil or: How I Learned to Stop Worrying and Love the Monolith”
这是 istiod 长达 21 页设计文档的开篇引用语,原文出自 Donald Knuth 1974 年在 ACM Journal 上发表的文章《Structured Programming with go to Statements》,意思是在没有量化的性能测试检测出真正存在的性能问题前,各种在代码层面的“炫技式”优化,可能不仅提升不了性能,反而会导致更多 bug。
而 Istiod 的设计提出者把 “Premature optimization” 换成了 “Complexity”,并且补充一句,“How I Learned to Stop Worrying and Love the Monolith”,简单翻译过来就是 —— “复杂性是万恶之源,停止焦虑,爱上单体”。显然,今天我们要讨论的不是软件优化相关的事情,而是一个关于 Istio 架构调整的问题。
lstio 作为 Service Mesh(服务网格)领域最具权威的控制面,基本上提到服务网格,所有人都会不自觉地想到它,如网易杭州研究院的轻舟微服务平台,也是基于 lstio 提供服务网格的支持。
从 2017 年发布第一个版本以来,lstio 就有着一个堪称非常优雅的架构设计。服务网格整个系统分为数据面和控制面,前者通过同样是开源的智能代理组件 Envoy 负责进行流量处理。而之所以称之为智能,是因为 Envoy 相对比其它代理比如 Nginx 有着更丰富的治理能力和灵活的配置方式,并且支持各种插件可用于扩展流量治理能力;而控制面在网格系统里则根据功能职能的不同,被划分成以下 5 个核心组件:
1.Pilot
控制面的核心组件,负责对接 Envoy 数据面,也可以解析上层抽象出来的 lstio 配置,转换成数据面可以识别的 xDS 协议配置并分发到各个 Envoy;
2.Galley
为更好的解耦职责,它在 lstio 1.1 后由仅负责配置验证升级成了控制面的配置管理中心,可以对接不同注册中心,用于为服务网格提供配置输入能力;
3.Injector
在 K8s 体系里负责数据面的初始化相关工作,其中 lstio 的核心特性之一 Sidecar 自动注入正是依赖该组件;
4.Mixer
是 ilstio 里负责提供策略控制和遥测收集的组件,内部包含两个子组件 —— Telemetry 和 Policy,其中 Telemetry 前者负责监控相关的采集信息的数据聚合以用于对接各种监控后端,而 Policy 负责在服务相互调用过程中对请求进行策略检查,例如鉴权;
5.Citadel
负责服务网格里安全相关功能,为服务和用户提供认证和鉴权、管理凭据和 RBAC 等相关能力;
服务网格控制面各个组件被定义得清楚了然,设计之初就已经考虑到各种组件职责解耦、扩展性、安全性等,架构上看起来也非常清晰优雅。
那么,在这个架构设计中,究竟存在着什么样的难解之题,迫使 istio 开发团队在 istio 推出将近 3 年之时,决定推翻这个架构设计,重新用起“复古的”单体应用设计?
下面我从几个简单问题(WHY、WHAT、WHEN)出发,试图为读者分析这次可以算得上是 istio 从诞生以来最大的一次“自我革命”的来龙去脉,不足之处,欢迎指正。
WHY — 为什么要回归单体 ?
如果有人问 lstio 在回归单体架构设计后,谁最应该开香槟庆祝的话,我可能会不假思索的说是服务网格的运维人员,如果还要再加一类人,那必须算上 lstio 的开发人员。
长期以来,服务网格的运维人员饱受折磨,而这种难言之苦,估计也只有 lstio 的开发同学才能感同深受,试想一下如下场景:
正常非服务网格的环境下,当用户部署的一个应用出现调用异常时,只需要简单排查下这个应用自身以及被调用服务端即可,排除网络等基础组件异常的话,问题基本上跑不出这两个应用,原因自然也很容易被定位出来;
现在当用户的应用接入服务网格后,众所周知,在服务网格里的调用模型应该是如下图所示的:
此时,如果业务出现调用异常,由于接入服务网格,问题处理人员要确认 lstio 系统是否正常工作,还记得之前介绍过的控制面组件吗,首先需要检查 pilot 是否正常工作,配置是否能下发到 sidecar,然后可能还要检查 galley 组件是否正常同步到服务实例信息,也有可能是 sidecar 注入问题,还需要检查 injector 组件是否正常工作…… 这还只是控制面的排查,涉及到数据面还可能需要排查 Envoy 日志等,不过这不是这次关注的点,暂且先不展开介绍。
我猜你可能已经想到我要表达什么,lstio 控制面的各个组件异常都可能会导致数据面在发起请求调用时出现问题,而控制面组件越多,意味着在排查问题的时候要检查的故障点越多,当然过多的组件设计导致部署难度会增加这点也是毋庸置疑。
说得也许有点危言耸听。不过,对于将要或者正在使用服务网格的用户来说,大可不必太担心,这种由于服务网格本身异常导致数据面无法正常请求的情况一般只出现在服务网格的开发过程中,真正用于生产环境的肯定是经过充分测试的服务网格组件,对于业务用户而言可以当成是一个稳定的基础组件来用。
现在,Service Mesh 的优点已经逐渐被大家认可,比如开发运维解耦、集中式管理、开发语言无关等等,但这里有一个问题其实是被大家所忽视的,那就是 lstio 自身的控制面组件的运维难题,分析下来可能有这么三个方面:
第一是管理职责划分问题
lstio 控制面组件拆分设计的初衷是为了功能职责分离,以便不同的运维 / 开发人员可以单独管理,但现状是,大多数的 lstio 应用场景里,运维这件事往往是由一个人或者同一个团队来管理,这与当初的设计初衷是背道相驰的,存在着过度设计的可能;
第二是导致的部署复杂
拆分后的不同组件,在整个可运维性方面却是线性增长,比如刚才的排查问题场景,每个组件都有独立的部署文件,里面封装着各自特有的启动参数,虽然这部分可以直接封装成 K8s 的单一 yaml 文件,但是一旦出问题需要排查原因或者是社区版本的 lstio 功能不满足需要进行二次开发时,这么多复杂的配置和参数你就必须得关心了。对于开发或者运维来说,这无疑带来了巨大挑战;
而且,lstio 在设计之初非常理想化的提出控制面的各个组件都可以独立部署,在实际应用场景里却并非如此。
不管是出于简化运维或是节省资源的目的想要精简部署的话,你可能不得不做出“一些艰难选择”,任何一个组件的割舍都意味着你将失去很多核心能力……放弃 galley 意味着无法进行 API 校验,随意下发的错误配置可能会导致 envoy 拒绝配置而使得相关的配置也同时失效;放弃 mixer 意味着你将无法完成类似于流控、权限检查、监控采集等;放弃 injector 意味你无法使用自动注入功能……
第三是不必要的独立伸缩性和安全性考虑
lstio 拆分设计之后,按预先设想各个组件都拥有独立的伸缩能力,但值得思考的是,lstio 的各个拆分组件,真的需要各自不同的安全设计考虑以及独立的伸缩能力设计吗?
引用来自 Istiod 设计文档里的一段话:“目前看来,对于多数组件来说并非如此。而控制平面的成本由单一功能(xDS)决定。相对而言,其它所有组件的消耗微不足道,因此分离并无必要。”同样,在关于分离的安全性设计方面,文档是这么描述的:“在目前,Mutating Webhook、Envoy Bootstrap 以及 Pilot,这几个组件的安全级别和 Citadel 是基本持平的,对他们的滥用所引发的损失几乎是相同的。”
非常显然,分离设计并不会在安全性和扩展性方面带来实质性的提升,相反这种设计会给用户带来额外疑惑 ——“我究竟要给各个组件设置多少个副本或者资源才合理呢?“
看完上面的几点分析,你可能已经对 lstio 控制面的拆分架构设计有了新的看法,顺应了一句老话,“永远没有完美的架构,只有最合适的架构”。这个看似如此优雅的架构设计,却在用户落地过程中,对运维人员或者开发人员带来了意料之外的困难……
其实我们可以跳出这个架构设计来重新思考一个有趣的问题:
服务网格本身的设计目标就是号称下一代微服务架构,用于解决微服务之间的运维管理问题,而在服务网格的设计过程中,竟然又引入了一套新的微服务架构?
这岂不是“用一套微服务架构设计的系统来解决另一套微服务系统的服务治理问题?”那谁来负责解决 istio 系统本身的微服务架构问题呢?
微服务架构带来的运维成本是不可避免的,所以,这里问题的本质是“一套系统的运维职责由谁来负责?” lstio 作为一个开源项目,运维的职责无疑会落在各个想要使用网格的团队身上,对于非 istio 开发团队而言,这个使用门槛是比较高的,无疑存在着巨大的学习和使用成本,很多人因此望而却步。
WHAT — 什么是 Istiod ?
幸运的是,lstio 社区“非常及时”地发现这个问题,也许是因为 Github 社区里几乎每天都有各种花样、无穷无尽的部署相关 issue,也许是他们为了解决运维部署难题在尝试各种手段(比如 lstio operator、Istioctl 等工具在一定程度上是为解决部分运维部署易用性问题的)却未取得突破性的改善后,终于有一天,有个勇敢的人站出来说了句:“复杂性是万恶之源,停止焦虑,爱上单体” 吧!!!
这就是文章开头的这句合体“宣言”,召唤出了 Istiod …
在这这份公开的设计文档里,作者一开始便非常明确地提出 istiod 的设计目标:
降低安装复杂度。单一的二进制文件在安装部署时将会更加简单;
降低配置复杂度。之前的很大一部分配置文件是用于编排控制面组件,而单体化设计后这部分配置可以被移除,而且新版本的 Istiod 在配置上可能更精简,只需保留一个 mesh.yaml 文件,并且能提供最佳实践配置;
增加控制面可运维性。通过单体设计后的控制面在类似于金丝雀发布的多控制面场景里显得更加简单;不同的工作负载可以通过 namespace 或者 pod 上的标签设置(也可以是组合匹配)来选择对接不同的控制平面;
提高问题诊断能力。单个控制面组件意味着出了问题后无需在不同的组件间切换来切换去,排查各种问题,显然这有助于提升问题排查效率;
提高效率和响应速度。再也不用在各个组件间通过远程调用来传递数据,而且原本不同组件间需要共享的数据,现在也可以安全被共享,而且也会一定程度上加快控制面的启动速度;
消除不必要的耦合。通过把 Envoy 的启动配置生成移到控制面可以避免 pilot agent 的访问权限问题。(这个设计目标存在争议,有人提出 Envoy 的启动配置跟 pilot 或者 galley 没有直接关系应该单独出文档介绍,也有人认为作者的意图是指说将 injector 组件也合并入 Istiod 组件内)。
6 个设计目标,基本上都是在针对 Istio 的运维问题,而且也不难发现,这种单体式的设计理念,的确能降低整体的运维复杂度以及排查问题的难度。
那么在一体化的设计后,Istiod 又应该如何找准自己的定位,明确自己的工作职责呢?当然,Istio 的开发团队并不是说完全推翻之前的设计,其实只是将原有的多进程设计模式优化成单进程的形态,之前各个组件被设计成了 Istiod 的内部子模块而已,因此 Istiod 就需要承担所有的职责:
监听配置,接手了原来 galley 的工作,负责监听来自多种支持配置源的数据,比如 K8S api server,本地文件或者基于 MCP 协议的配置,包括原来 galley 需要处理的 API 校验和配置的转发也需要设计在内;
监听 Endpoint,监听来自本地或者远程集群的 endpoint 信息,在将来还计划允许 endpoint 复制在各个集群内,包括非 K8s 的注册中心例如 consul;
CA 根的生成,生成私钥和证书,目前 Citadel 的职责之一;
控制面身份标识,目前在内部控制面之间是通过 CA 根来生成一个 SPIFFE ID 用于识别身份,也同时用于 injector 组件的证书生成,可以参考另外一篇 proposal(simplified control-plane identity proposal)
证书生成,主要是为各个控制面之间的通讯生成私钥和证书来保证安全通讯;
自动注入,在 K8S 里需要为 Mutating Admission Controller 提供接口支持,从而可以在 pod 创建阶段修改资源文件来实现 sidecar 的自动注入;
CNI/CRI 注入,通过 CNI/CRI 作为 hook 来实现自动注入的另一种方式,目前还没使用;
Envoy 启动配置生成,上文目标中提到的,Envoy 的启动配置将由 istiod 来提供;
本地的 SDS Server,提供密钥发现服务的本地服务端;
中央 SDS Server,同上,是一个中央化的密钥发现服务端,一般用以对接第三方的密钥系统;
xDS 服务提供,之前 pilot 的核心能力,为所有的 Envoy 提供 xDS 下发的服务端;
如果将改造前的 Istio 控制前按功能简化整理成表格就是:
而新版本后的组件则非常精简,对应如下:
经过对比,可以很直观地看到,Istiod 是将原有的的其它组件统统塞入了 pilot,而架构调整后的新 pilot,即 Istiod 肩负了相比 pilot 更多的职责,单个二进制文件在部署方式上变得更加简单。
如果按照官方的部署一套 demo 的话,除了 ingress 和 egress 网关以及另外几个监控相关的组件,剩下的就只有一个 istiod 组件。
通过这种多进程到单进程的架构调整,Istio 的开发团队可以算是以最小的成本实现了整体运维方面的巨大收益:
运维配置变得更加简单,用户只需要部署或升级一个单独的服务组件;
更加容易排查错误,因为不需要再横跨多个组件去排查各种错误;
更加利于做灰度发布,因为是单一组件,可以非常灵活切换至不同的控制面;
避免了组件间的网络开销,因为组件内部可直接共享缓存、配置等,也会降低资源开销;
新的基于 Istiod 的架构变成下图:
相比之前的架构图,是不是感觉格外清爽,有种豁然开朗的感觉?
WHEN — 什么时候发布 ?
介绍完了 Istiod 的诞生初衷和设计理念,作为服务网格的开发人员或者正在观望的团队,想必都会对这个 Istiod 的新特性满怀期待,单体设计后的 Istio 管控平面简化部署流程,减少因为部署或者配置问题引发的不必要时间浪费,的的确确是在“save your life”。
网易轻舟微服务团队一直在关注 Istio 社区的最新动态,早在社区代码仓库的 release 1.5 开发分支中就发现了 Istiod 的身影,而近日正式发布的 1.5 版本,众望所归的 Istiod 自然包含在内,这意味着从这个版本开始,Istio 控制面部署形态将正式进入一个全新的时代,堪称是一次突破性的设计革新。
One More Thing,Istio 的官方博客在正式发布 1.5 的前几天,发布了一篇博文 ——《Istio in 2020 - Following the Trade Winds》,介绍了 Istio 社区 2020 年的一些“风向标”,其中就有提到一些非常令人兴奋的特性:
1.将会更整洁、更平滑、更快速
Istio 从诞生以来的第一天就提供了可扩展性方面的能力,这里面 mixer 组件扮演了非常重要的角色,它允许用户通过自定义适配器(adapter)的方式来开发扩展;在之前版本里,mixer 一直是一个进程外组件设计,新设计中针对一些常用场景比如鉴权,将会被 Istio 的认证模块取代,这样子设计好处是允许用户直接在 proxy 内部进行鉴权认证,其它的比如通用的指标监控也同时被移入了 proxy 内部。
这意味着之前一直遭人诟病的 Mixer 性能问题将会有非常大的改善,根据官方的 benchmark,通过新的 telemetry 模型设计可以取得业界领先的性能数据,相比之前减少约 50% 的延迟以及不少 CPU 消耗;
2.全新的扩展支持方式
这种全新的扩展支持架构相比之前提供了更好兼容性,也正是 Istio 社区的开发者主导开发了 Envoy 里的 WASM 特性支持,允许用户以超过 20 种开发语言来实现各种扩展插件。
令人感到兴奋的是,这些插件是可以在 envoy 处理流量过程中被动态的加载、重载的,这种动态的灵活性自然不言而喻,istio 社区也在和 envoy 社区共建来发现、分发更多的扩展,目的是为了让这些插件更加容易被用户安装或是在容器环境里运行;包括部分之前编写 mixer adapter 的开发者也正在“搬运”这些插件到 WASM 上,社区也在努力提供相关的文档以帮助更多的开发者上手 WASM;
Istio 1.5 确实可以称得上是一次突破性的版本发布,各种架构上或者是设计上的优化,都可以看的出来是在做减法,包括更精简的架构,更简单的扩展支持方式。
社区里提出的各种缺陷,都正在逐一被优化,比如可运维性、易用性、扩展性等方面,还有一直被人诟病的性能,也正在通过架构性的调整得以优化和改良,甚至这篇文章里还提到,之前把一大批非容器用户挡在门外的平台限制问题,在不久的将来也将成为一个历史,按照博客介绍的,社区正在努力 “Making it easier to run Istio without needing Kubernetes”!
结语
尽管之前一直被人抱怨存在各种问题,但 Istio 社区的开发脚步没有停歇,我们看到了一次又一次的版本发布从未间断,伴随着各种大大小小的功能更新和优化。就网易杭研而言,轻舟微服务将 Istio 引入生产环境也是极为审慎,事实上也曾遇到了运维和开发的困惑,而 istiod 架构设计的回归让我们彻底松了一口气,拥抱 Istio 实现服务网格的思路更加坚定。
就像当年的 kubernates 也曾受过质疑,现在却没人质疑它在容器编排领域的地位,相信在不久的未来,Istio 这艘小船在微服务的海洋里将会行驶得更加敏捷、轻快、平稳,而 Service Mesh,也终将不再是一个新鲜词。
参考资料:
Istio as an Example of When Not to Do Microservices
Istio in 2020 - Following the Trade Winds
作者介绍
翁扬慧,网易杭州研究院云计算技术部资深研发工程师,有多年微服务开发经验,目前主要负责网易轻舟服务框架和服务网格的核心设计以及业务落地工作,热爱编程,热衷于开源社区的技术交流和分享。
评论