为进一步加强技术交流,推进云原生生态共建,6 月 28 日下午,首届云原生实践者大会在线上线下同步举办。来自作业帮、知乎、转转、58、同程等多家科技企业的数十名研发人员,以及中国信通院云大所的专家共同参与了此次技术研讨沙龙。
研讨会上,知乎核心架构平台开发工程师谢楚瑜讲述了“知乎的 Istio 之旅”,与参会者分享了知乎如何在大规模集群下使用 Istio 升级微服务架构,具体包括如何使 Istio 管理的服务和现存服务之间可以互相通信、知乎的 Istio 迁移都遇到和解决过哪些问题、Service Mesh 如何为业务提供了帮助,以及知乎如何在大规模集群中优化性能指标等。
以下,是谢楚瑜的分享。
大家好,我是谢楚瑜,这次主要是和大家分享下知乎是如何进行 Service Mesh 改造的、在改造期间有遇到了哪些问题,以及随着规模地不断扩大,我们又解决了哪些新的问题。
现状
先看一下 Service Mesh 在知乎目前的情况。
目前,我们的核心业务以及数百个周边业务都已经完成迁移。在每天的高峰期,Service Mesh 中的流量可以达到百万级别的 rps。在指标维度也已经达到了百万级别。我们服务治理的各种功能,如压测、鉴权、限流和断路等也通过 Service Mesh 完成了兼容和重构。
然后说下迁移的兼容方案。知乎在早期的时候就已经完成了容器化改造。那时候,我们其实还没有用上 K8s,是自己做了一套基于负载均衡以及服务发现的服务通信发现。因此,在迁移 Service Mesh 时,我们必须首先提供一套方案即可以兼容以前的服务通信,又兼容旧方案的服务治理功能,同时迁移过程对业务无感知,如果有大规模故障还可以快速回滚到旧方案。
为了满足上述需求,我们首先做的就是梳理现有的服务调用流程。
以前,客户端会直接通过注册中心目标服务的负载均衡地址进行访问。但在云原生环境的做法应该是通过 coreDNS 查询它的一个 ServiceIP,通过这个 ServiceIP 进行服务间通信。如果是在 Service Mesh 里的话,访问 serviceIP 时会直接通过 Envoy 发现一个合适的服务端实例,直接从客户端打到服务端。
考虑到对旧方案的兼容,我们决定对服务发现组件进行改造,增加一个服务发现的代理。如果是上了 Mesh 的服务访问没上 Mesh 的服务,那么它其实和以前是一样的,仍然通过旧的负载均衡方案去调用。而对于客户端、服务端都是在 Mesh 环境下服务调用,这个代理将会返回 ServiceIP,这样一来就不需要做 SDK 层面的修改,可以直接兼容旧的负载均衡方案和新的 Service Mesh 调用了。这样有一个好处,如果我们需要切换到旧方案,比如紧急回滚的情况,只需要把这个代理上面的配置修改下就可以直接切回过往的一套流量方式。
除此之外,关于鉴权和限流这些功能,我们通过一些 Controller 实现了一套同步方案,然后通过 ServiceMesh 的能力实现了兼容。然后关于从零上 Mesh 其实还有很多改造的点,比如限流。我们上一次 IstioCon 的分享有比较详细的描述,这次暂时不赘述了。
在迁移期间,我们有遇到过很多的问题。就像我前面提到的,我们当时有一种集中式的代理方案,在切换到 Mesh 以后,Mesh 其实会给每个业务容器实例后加上一个 Sidecar,这样相当于说,它的连接方式从以前一种集中式的代理访问,变成一种点对点的模式。
这会直接导致服务端收到并发数发生很明显的变化,很多性能比较敏感的服务也会发生改变。关于这个问题,我们通过一些指标去做连接池的适配,对于延时变化非常敏感的服务,我们会做一些人工处理。
然后是连接管理带来的一些差异。比如我们以前那一套负载均衡方案,会在客户端断开连接以后,仍然保持跟服务端的连接。然而在 Istio 中,如果客户端连接断开,那么它和服务端连接也就断了,这导致我们的业务会在监控上看到一些不一样的错误,如 Golang 服务会看到更多的 Context Cancel 这类错误。对于这种情况,我们是通过 SDK 对异常的采集做适配来解决。
还有一个比较核心的点。在 Istio 中,每次读取指标文件都可能在内存里面同时写两份完整的指标配置。当一个实例的指标文件达到上百兆的时候,对 Sidecar 的性能影响是非常大的。我在后面会具体提到指标这块我们是怎么解决的。
业务应用
在做好了基础的兼容和适配以后,剩下就是考虑怎么样让业务应用起来。
考虑到 Istio 对象的复杂度非常高,一上来如果直接看 VS、DR,还有 Authpolice、Service Entry 这些东西的话,很难直接用起来。另外,每一个配置都可能会因为需求的复杂而进行重复配置,比如一个 vs 的配置里面有时可能会加一个故障注入改一下,然后又加一个重试规则又改一下。
基于这个问题,我们实现了一个类似于 Envoyfilter 一样的 Crd、Istiofilter。通过这个 crd,我们可以对 Istio 应用一种 overlay 形式配置。这样我们可以通过配置多个 Istiofilter 同时管理同一个 vs 配置。如果中间某些配置不要了,比如不要流量镜像了,就只需要删掉对应的 IstioFilter,随后 vs 上面就只会减少这一个 mirror 的配置,其他配置仍然可以正常使用。目前这一套 Istiofilter 也是有开源版本的,可以直接在 GitHub 上找到。
除此之外,我们 Mesh 的所有能力目前都是直接面向业务研发开放使用的。目前大部分功能都是基于接口这一种粒度进行配置。他们可以通过指标或者自己手动在接口配置界面上配置他们的一些接口定义,在完善接口配置以后就可以在上面使用一些故障注入和限流功能。
考虑到自身业务的特殊性,我们并没有全面使用 Istio 原生提供的功能。比如,我们自己实现了流量镜像功能,它其实可以支持超过 100%这种比例的配置,这样就可以拿线上的流量进行压测,也可以将压力都集中在需要测试的服务端实例上,避免影响其他的客户端应用。
前面提到我们大概有百万级的 Rps,同时也已经有 10 万+的实例运行在上面,这样就会有一些新的问题。比如像 Ipvs 上如果 Service 数量太多,一些采集工作会导致它在机器上面软中断时间变长,进而从业务角度观察会有一些网络延迟。最终我们是通过livepatch关闭了特性来解决的这个问题。
然后是做了 DNS 优化。前面提到,我们当时是用了自己的一套服务发现体系,没有用 coreDNS。然后我们在切到 Mesh 的时候发现,当时的 coreDNS 比较难以支撑我们的性能需求。于是我们自己搭建了一套 local Dns,每个 K8s 节点放一个 DNS 实例,同时结合这一个 Istio 的 smartdns,减少了由于服务域名没有填写完整而重复查询 DNS 的次数,这样尽量优化 DNS 在服务调用中的时间开销。
关于 Istio 的参数配置,首先由于集群规模较大、服务更新频繁,对于一些服务来说可能配置推送会非常频繁,推送内容也非常比较大,这样的话推送配置非常容易超时。所以,我们根据集群规模调了配置,包括减少了配置推送的量,只针对某个位去推送所需的配置。我们做了一个自动的服务配置范围(sidecar crd)的适配,这样确保每个服务都按需加载配置。
除此之外,我们还使用了 Istio 的 DiscoverySelector 减少 Istio 采集集群信息的范围。因为我们的集群中除了常规的业务容器外,还有很多永远不会参与服务间通信的容器。但是 DiscoverySelector 有个问题:它里面的接口可能和我们集群的 K8s 接口不兼容,Istio 版本如果低于 1.13 的话,可能会因为 panic 导致 istiod 偶发重启。更多细节我们以前也在知乎专栏上做过很详细的分享。
指标方面,前面提到我们指标的维度已经比较大,总维度在百万以上,这对我们的指标系统确实存在一个挑战。我们以前其实只有一个 victoriametrics 集群,随着指标规模扩大,我们将指标集群分成了两个:一个是短期存储,只存大概一个星期左右的全量指标;另一个是长期存储,它会把短期存储指标里面的 pod 信息去掉,然后把剩下聚合过的指标存三个月。这样,我们可以做到在大部分情况都能较快地查询到监控数据。
还有一个维度的问题,比如最常见的 Istio request total 指标,会把某一个客户端的版本信息,以及服务的版本信息全部都带上。但这会遇到一个问题,比如某一个服务端一直不更新、客户端一直更新的情况,会导致服务端有特别多不会增长的指标堆积在里面。可惜 Envoy 也不支持指标过期的功能,导致每一次拉取指标时我们会多拉很多冗余的指标。
最终,我们决定在服务端去除客户端的版本信息、客户端的指标中去除掉服务端的版本信息,通过这种方式优化指标维度。这样做了之后,我们的指标系统压力有了很大程度的下降,个别服务的 duration 指标维度甚至下降了 20 万。通过这些操作,我们集群的监控指标维度基本趋于稳定。
在知乎的 Mesh 化过程中,Mesh 一方面带来了很多新的问题,比如性能上、运营上的问题。但是另一方面,我们利用 Mesh 节约了很多过往需要在 sdk 层面反复实现的功能,也利用 Mesh 弥补了我们在监控与测试链路方面的许多缺失。Service Mesh 是一项充满魅力的技术,而知乎的 Mesh 之路也将继续下去。
评论