IstioCon 回顾 | 网易数帆的 Istio 推送性能优化经验 原创
在 IstioCon2022 上,网易数帆资深架构师方志恒从企业生产落地实践的视角分享了多年 Istio 实践经验,介绍了 Istio 数据模型,xDS 和 Istio 推送的关系,网易数帆遇到的性能问题和优化的经验,以及一些相关的 Tips。
数据模型
从推送的角度,Istio 所做的事情,以做菜的过程类比,大致分为以下几个部分:
首先是“备菜”。 Istio 会对接、转换、聚合各种服务注册中心,将不同的服务模型的数据统一转换为Istio内部的服务模型数据。早期的Istio实现里面这是有定义的接口,做代码级实现,使用者可以去做注册中心的实现和对接,但是这种方式对 Istio 的代码形成侵入,更便于开发者而不是使用者,所以 Istio 后续演进将其废弃,取而代之的是定义了一个数据模型,ServiceEntry 这种 API 的数据结构,以及对应的一个 MCP 的协议,如果有扩展和外部集成的需求,可以单独在外部组件里面实现这个协议,将服务模型数值转换之后再传输给 Istio,Istio 可以通过配置的方式对接到多个注册中心、服务中心。因为服务数据可以认为是整个服务网格里面最基本的要素,所以我们把这一步比作是备菜的过程。
其次是“加料、烹饪”。 这可能是使用者最为熟悉的部分,服务发现是服务网格的一个基本功能,但是 Istio 在整个服务网格中的优势更多的是通过它所支持的丰富、灵活的治理的能力来体现的,所以这其实是将 Istio 定义的治理规则给 apply 进去,我们可以把它比作是加料、烹饪的过程。
再次是“装盘、摆盘”。 以上我们已经得到了最终要推送的 xDS 数据,但 Istio 里面还有很多的代码是做各种部署、网络使用场景的适配,它会影响最终生成数据的一些特征,这类似装盘、摆盘的过程。
最后是“出菜”, 我们将最终的数据以 xDS 配置的方式推送给数据面,这是数据面能够理解的“语言”,这个过程我们把它比作是出菜的过程。
一般来说,我们端上餐桌的“菜”,相比之前准备好的那些“食材”已经面目全非了,你很难去了解它原来的样子。xDS 的配置,看过的同学都知道,其实是非常的复杂的。
另外一个例子,我想把它比作 Kubernetes 模型的 reconcile 过程,基本上是一个“watch-react-consistency”这样的循环,整个过程的复杂度,随着输入和输出资源的数量和种类,几乎是呈指数级别的增加的。
下面我整理了 Istio 上下游资源的关系,下游是指 xDS,上游是 Istio 自己定义出来的一些数据资源类型,用以支持丰富的治理能力以及场景适配。可以看到,每一种资源都会受到很多资源类型的影响,甚至是包括一些像 proxy 的特征,这种东西我们认为是一个非常运行时的数据,这些数据都会影响到最终生成的 CDS,所以说最终的效果应该是“千人千面”,也就是说从推送的视角来看,我们希望不同的特征影响到能获取哪些资源,而不是说获取到的同一个资源内容还不一样,后一种方式非常不利于推送和优化。
xDS 和 Istio 推送
简单看一下 xDS 协议,从推送的角度看说分成三类,第一类是 StoW(state-of-the-world),也是目前 Istio 默认的和主要的一个模式,它支持最终一致、实时计算(实际上是 Istio 使用它的一个姿势)和全量推送,特点是简单健壮,比较好维护,因为它每次都会去重新计算所有数据推送,所以我们在做功能开发的时候,不用太关心它的数据一致性或数据丢失,但是它也有一个不菲的代价,就是性能很差。
第二类是 delta xDS。 我们熟知的 delta,一般有一些前提条件,这样实现起来会比较方便一些。比方说将全局的各种资源版本化(如 GVK + NamespacedName + version),推送的时候根据 offset 来推送新增的差异部分,这是一个典型的 delta 的实现思路。但是对目前 Istio 的数据模型来说,是无法生成这种比较彻底的 delta 模式的。目前我了解到的社区的 delta 的实现,实际上是做了一个降级,还是为每个 proxy 去做实时计算,对可枚举的特征值进行缓存,如果两个 proxy 的特征完全相同,那么它们获取的配置数据应该是一样的。这个思路要求特征值是可枚举的,目前实际只有少数的资源类型(CDS、EDS)和场景(只有ServiceEntry变更)能够实现,否则会倒回到全量推送——delta xDS 在设计的时候,考虑到了这个场景,支持下发内容多于订阅内容。
最后一类是 on demand xDS, 目前 Istio 还没有开始使用。它大体的思路是,Envoy 在实际用到某个资源的时候才会去做请求,它有一个预设前提,就是 下发配置大部分 proxy 是不会用到的,这个前提是否能成立,我觉得不是特别的确定,而且它好像目前只支持 VHDS。
我们再回到 Istio 的视角,它的推送其实只有两种,一种叫 non-full push,一种叫 full push。non-full push 是只有 endpoint 发生变更的推送类型,只会做 EDS 推送,EDS 推送的特点,可以做到只推那些变更的 cluster,并且只推给那些对这些变更 cluster 做了watch 的 proxy。从这个角度来说,non-full push,或者说 EDS push 比较接近于理想的推送。但是有一个场景是说我们的 cluster 非常大,比如说有一万个 endpoint,这个时候我们只变了少数的 endpoint,我们还是会因为这些少数的 endpoint 的变更,去将这一万个 endpoint 的 cluster重新下发。
这个场景根据企业规模可能不是特别常见,而且它相比 full push 来说已经做得非常好了,因为后者所有类型的变更都会触发 full push,不仅重新推送所有类型的数据,每种类型的数据都是全量推送,而且是推送给所有的 proxy。当然这是它的最初形态,Istio 在不断的演进过程中也在不断优化,引入了一些 scoping 的机制,大体的思路是,它会尽量做到只把每个 proxy 需要的内容全量推送,也只推送给那些当前变更影响到的 proxy。这句话可能说起来比较简单,但实际上非常困难,因为前面我们也分析了,Istio 里面非常复杂或者灵活的那些能力,决定了它的上下游的配置的关系是非常难以理清楚的。
接下来讲 Istio 推送的 3W,第一个是 when,它其实是一个事件驱动类型,只有外部变更的场景它才会去做一些推送。思路是前面提到的最终一致、实时计算和全量推送,有少量的主动触发场景。
第二个是 who,控制面会尽量判断这个变更会影响到哪些 proxy,下面列出了在实现中 Istio 是通过哪些机制去判断变更的 config 或者是 service 会影响到哪些 proxy,可以通过类型的判断,可以通过具体的明确到资源的判断,还有上下游的类型。通过类型去判断是比较粗略的,而且需要一定的维护成本,因为某一个类型的推送,比如说 AuthorizationPolicy 这个 CR 的变更不会影响到 CDS。这是基于当前实现得出的结论,如果 Istio 在后续演进中引入新的特性,逻辑变更导致依赖关系发生变化,可能会漏掉,所以有一定的局限性。
另外一种明确到资源的依赖关系是更明确的,目前它其实是通过PushRequest.ConfigUpdated
去和SidecarScope.configDependencies
作比较,再决定 sidecar 是不是受到这个 configure 的影响,但它目前也有一些局限性,只支持 sidecar 类型的 proxy,而且只限于 sidecar 能够约束的 service、vs、dr、sidecar 等四种资源。网易数帆目前在做的另外一个事情,是把 configDependencies 能做到 proxy 级别,这样的话它几乎可以覆盖全部的资源类型。
最后一个是 what,就是控制面尽量判断出需要更新的内容,以及 proxy 是不是需要它,或者说是 proxy 需要的内容,因为 proxy 需要的内容是一个下游配置 xDS,需要更新的内容也是这个。这影响到的是推送的数据量,先确定哪些 proxy 是需要被影响到的,同时是需要推送的,再确定需要推送哪些,这是通过一些像资源和 workload 的依赖关系来描述,有一些资源支持 workload selector,还有一些资源是通过 Sidecar
约束的。
遇到的性能问题和优化经验
介绍完了背景,接下来主要分享网易数帆所经历过的性能问题,和对应的优化的经验。
MCP(over-xDS)性能问题
首先第一个问题是 MCP(over-xDS)性能问题,我们说 MCP 一般是指老版本的 MCP 协议,因为新版本的协议已经换成 xDS 了,只是保留了 MCP 协议里面的数据结构,但是称呼上一时半会儿改不过来。这个性能问题主要还是因为它的推送模式,因为它其实也是一个 xDS 协议,Istio 本身就实现了这个 MCP(over-xDS),也就意味着我们的一个 Istio Pilot,其实可以将另外一个 Istio Pilot 作为它的配置来源 。这可能是社区的一个有意的设计,它的模式是一致的,所以也会有 StoW 模式的问题——任何类型的资源的变更,哪怕只是一个变更,都会导致同类型资源的全量推送。对于我们的场景,ServiceEntry 和 VirtualService 都是五位数量级的,传输或者写放大也就是五位数量级的,所以开销是过大的。
我们的优化方式有两点,主要一点是支持增量推送,当然跟社区的思路也一样的,我们没有实现完全的 delta xDS,只是实现了一个对应的语义,但是最终的效果我们确实是做到了增量推送。它有两个要点,一是 MCP Server 要⽀持 ResourceVersion 的注解,这个时候它才能够把一些版本信息告诉 MCP Client ;二是 MCP Client 要强化对该注解的支持,目前社区的主干代码里面已经有一部分支持,但不够完整,我们后面做的一个事情就是当 ResourceVersion 没有变化的时候,我们会跳过他的更新,这相当于是社区思路的一个延续。
另外一点是我们的 MCP Server 支持了 Istio Revision 的资源隔离,因为当前社区 Revision 的一个思路是 client side 的过滤,就是说我先收到了所有的数据,再根据我的 Revision 做一个过滤,这样传输的数据量还是比较大的。我们在做的这个增强之后,MCP Server 会根据 client 的 Revision 来做一个过滤,从而降低传输的数据量。
ServiceEntryStore 的数据处理性能问题
第二个是 ServiceEntryStore 的数据处理性能问题。简单地说,它里面有一个步骤,会全量更新实例的索引,这意味着如果我的 service 有一个设备发生变化了,它会去更新全部 service 的索引,这也是一个量级非常大的写放大,优化的思路也比较接近,我们没有去硬改 Istio 的主流程,而是在原来的refreshIndexe
基础上,对它的索引更新操作做了一个聚合,优化的效果非常明显。这块社区的代码其实是有一些重构的,具体的效果我们还没有确认。
还有一个是 servicesDiff
没有略过 CreateTime、Mutex 字段,这两个字段的值是经常不一样的,这就导致结果不准确,有些时候只有 endpoints变更,但是这样比较出来的结果,就会导致它升级为 service 变更,从⽽ non-full push 也会被升级为 full-push。这主要是一个演进的问题,社区最新代码 Service 中这两个字段要么删除要么没赋值,所以此问题可以忽略。
启动时数据初始加载的“雪崩效应”
第三个问题是一个比较大的问题,简要的说是 Istio 在做大量的配置变更,尤其是做初始加载的时候,会加载所有的数据,每一个新的数据都会被认为是一个变更,可能导致雪崩的效应。雪崩效应一般都是由正反馈带来的,这里有两个场景的正反馈,第一个是每个服务变更都会去刷新全部服务的缓存,我们实际的服务是一个一个地装载进来的,缓存刷新量就是 O(n^2),计算量非常大。优化方式前面也有提到,就是做一个聚合。
第二个场景比较复杂,背景是前面提到的,服务变更配置变更都会触发 full-push,全称是 full-config-update,包含 full-update 和 full-push 两个阶段,full-update 做 Istio 内部的数据更新, full-push 拿所有的被影响到的 proxy,然后对每个 proxy 做数据生成,再推送给每一个 proxy,这两步的开销都是非常大的。Istio 对这个场景其实是做了一个抑制抖动,如果发现有高频的变更,它会对这些变更做一个抑制,效果是比较不错的,但是有一个问题,抑制抖动主要是抑制的是瞬时和集中的大量变更,而这个大量变更如果因为某种原因被拖慢了,持续时间更长了,就会导致抑制抖动的设计失效。我们如果有这样的数据装载的时候,以及有其他的雪崩逻辑导致计算量放大的时候,会出现非常严重的 CPU 争用,因为 Istio 的内容变更触发的更新和推送都是一个强 CPU 性加上强 I/O 性,如果它有 proxy 需要推的话,CPU 争用会使流程变得更慢。这两点是互为正反馈的,会导致更多的 full update,就会更慢。整个流程还不止于此,这个时候如果有 proxy 连上来,我们还要去叠加一个正比于 proxy 数量的 full-push 开销,整个情况会更加恶化,配置装载就会持续更长时间。另外严重瓶颈的时候对 proxy 的 push 会超时,proxy 又会做断链重连,这个过程会继续恶化。最后一个是业务效果,加载时间特别长意味着整个数据加载完毕的时间比较晚,而 proxy 如果在这之前连接上来,会拿到不完整的配置,这样会造成业务有损,因为这个 proxy 可能是重连,它之前是有完整的配置的。
以网易数帆的数据量级来说,如果上述的优化都没有做,我们需要一二十分钟甚至更长的时间才能达到稳定,但在这期间可以认为系统是不可用的。优化的思路大概有几点:一是前面提到过的优化 endpoint index 更新的写放大;二是优化整个系统对 ready 的判断的逻辑,因为 Istio 本身有一个逻辑,只有当它认为组件都已经 ready 的时候,才会把自己标记为 ready,但这其实是有问题的;三是最重要的一点,我们是引入了一个对变更的管理,原先单纯的抑制已经不足以去覆盖这种场景了,所以我们做了一个类似于推送状态的管理器,在抑制的基础上再加上了一些启停的控制。最终的效果是,我们优化后同等规模的启动时间大概是 14 秒,并且中间是没有状态误判,也没有业务受损的。
运行时的大量服务/配置变更
还有一个关联问题,就是如果运行时有大量的服务变更会怎么样?运行时虽然没有这么大的概率会出现,但是它有一些条件也不一样,比如此时我们的 Pilot 就是 ready 的,Envoy 就是连上来了,这个时候挑战也是比较大的,它的大规模变更一般来说会有三种场景,一种场景是业务有一些非规范的发布,比如短时间内新增大量的服务,或者新增大量的配置,此时我们需要保证自己的健壮性;第二是上游的 bug,比如说配置来源的一些 bug导致的频繁变更、推送,比较少见;第三个是上游的 MCP Server 的重启,对于 Kubernetes 里面的资源来说,它是带版本号的,可以基于版本的检查只推送增量部分,但是对于转换出来的 ServiceEntry,它没有持久化的版本号,可能会导致大量的假更新。
对应的思路,是一条一条地针对性地处理。网易数帆引入了几种优化方式,一是做有条件的批处理,场景比如 MCP,它的交互方式是一次性推送同一个类型的所有的数据,这就意味着变更量是很大的,我们就模拟一个事务提交的机制,我们会在事务周期内禁用推送。再一个场景,如果本身是一个非批处理,我们会加入一个防抖动机制,把连续的变更转换成批处理。另外一点就是把资源版本化,生成的资源通过某种方式去引入一个持久化版本号,这样可以减少不必要的变更。
proxy 接入先于系统 ready
还有一个场景,前面提到在我们系统没有 ready 的时候 sidecar 会接入进来,这是因为目前 Istio 内部判断组件是否 ready 的设计,它可能判断得不够精确,在大规模的数据和高压的情况下会被放大出来。用一句话来概括,就是中间有一个异步处理的过程,这个过程在正常情况下是比较快的,时序上的不足不会体现出来,但是高压力的情况下,CPU 争用严重,这个异步过程的时间 gap 就会被放大,导致 Istio 错误地提前认为整个系统已经 ready。
优化的方式,一是优化性能,减少这种高压力的场景,另外就是引入更多的组件 ready check 的机制。比如我们上游的 MCP,它之前也有类似的场景,没有严谨地判断是否 ready 就提前做数据下发,这个时候我们也是做额外的检查。
密集变更问题
最后一个 case,就是我们会遇到一个密集的变更,比方说初始化加载,这时候 CPU 的高水位会使得 https 的 health check 失败,从而 liveness probe 失败乃至 pod 重启,它的默认值只有 1 秒,https 的协商阶段对 CPU 其实是有一定要求的,就会导致持续抢不到时间片,或者是调度不上,就会出现这种状况。优化方式比较简单粗暴,我们直接把超时改成 10 秒以上。
“⾃动”服务依赖管理
后面讲一下场景无关的优化,是一个老生常谈的自动化服务依赖管理。前面也提到缩小下发给 proxy 的数据量是非常重要的事情,一个是少推一点数据给它,另外一个是无关的数据发生变更的时候不要推送给它,所以依赖关系非常重要。
Istio 目前提供了一个 sidecar 的 API,这个 API 人工维护是不太现实的,我认为这是把基础设施上的能力不足转换成使用者的操作风险,所以就有一个对自动化的刚性需求,我认为也是网格的一个必备组件。它核心的思路是通过实际的调用数据来生成和更新依赖的关系,同时在依赖关系完备之前我们要正确地兜底流量处理。
这方面大家也可以去了解一下我们开源的 Slime lazyload
,现在我们在它的动态依赖关系维护的基础上,也支持了比较灵活的半静态的依赖关系描述,类似于条件匹配,可以用来实现一些高阶的特性,所以我们现在更愿意把它叫做 servicefence
,在这个组件里面,我们也沉淀了比较多的生产实践的经验。
一些 Tips
最后讲几个 Tips,不一定每个场景大家都会遇得到,但是会有一些思路可以借鉴。比方说我们有一些场景是连接不均衡,原生的是有一个用于自我保护的限流,如果有大量的连接或者请求发往单个节点,单个 pod 的控制面的时候,它就会去做一个限流的保护,但是这并不能使得不均衡的情况重新均衡,所以我们有一个组件会去做均衡的调配,也已经开源。
另外一个场景比较特殊,如果我们有海量的 endpoint,它的内存使用是一个比较大的问题,这个时候我们可以考虑对可枚举的内容用字符串池优化,尤其像 label 这种重复度比较高的。
还有前面提到的,如果我有超大的 cluster,这时候 EDS 推送甚至都会成为问题啊,解决思路是大资源拆小资源,曾经有一个方案叫 EGDS,在社区有有过一些讨论,虽然说最终是没有进到社区主干,但是可以作为一个解决问题的思路参考,Kubernetes 其实也是有类似的思路。
最后一个是说我们如果是有超大规模的服务注册中心,我们的控制面已经无法承担所有的配置数据或者服务数据了。这时候我们可以考虑去做控制面的分组,让 sidecar 去基于依赖关系的亲和性,来让每一个控制面只处理一部分的配置。
结语
我今天要分享的内容就是这些。最后分享一下我个人做了几年服务网格优化的感受,Istio 的很多设计确实比较符合当下软件工程的一个务实路径,先采用一个高可靠的方式来做快速演进,当然曾经欠下的东西,在后面的实践中还是都要补回来的。谢谢!
相关链接
作者简介
方志恒,网易数帆云原生技术专家,负责轻舟 Service Mesh,先后参与多家科技公司 Service Mesh 建设及相关产品演进。从事多年基础架构、中间件研发,有较丰富的 Istio 管理维护、功能拓展和性能优化经验。