干货 | 携程Service Mesh可用性实践

作者简介

 

本文作者烧鱼、Shirley博,来自携程Cloud Container团队,目前主要从事Service Mesh在携程的落地,负责控制面的性能优化及可用性建设,以及推进各类基础设施服务的云原生化。

一、背景

近几年,国内各大公司大规模生产落地Kubernetes和Service Mesh,拉开了云原生革命的序幕。从2019年开始,团队开始在部分场景中落地Istio Gateway,积累Service Mesh经验;2020年中,我们开始与公司的框架部门合作着手Service Mesh在携程的落地,目前生产环境已有数百个应用接入,覆盖率还在持续的推进过程中。

Service Mesh作为一项新技术,相比传统的微服务框架有很多优势。但在生产环境落地的过程中,若无法保证可用性,出现大的故障,将会大大打击对新技术采用的信心,也会影响最终用户,造成对品牌的负面影响。显而易见,可用性是一切的基石。我们在落地Service Mesh的过程投入了大量的精力进行可用性的建设,避免出现单点故障,保证服务的高可用。Service Mesh在携程的落地并不是平地起高楼,公司内关于可用性已经有一套方法与模式,Service Mesh的可用性建设也必须考虑现有的高可用体系。

二、Service Mesh高可用

2.1 携程高可用架构简介

携程现有的高可用设计自上而下可以简化为:IDC级高可用、IDC内应用部署的高可用、IDC内基础设施的高可用。针对这三个层次的故障,分别有不同的设计,以下主要围绕IDC级及应用部署级可用性设计进行介绍。

2.1.1 IDC级灾备-同城双活

携程内部应用一般包含多个Group,Group是应用发布的最小单位,同时也是流量调度的单位。一个DR(容灾)组一般包含两个IDC,一个应用的多个Group会部署在DR组内的多个IDC,当某一个IDC出现故障时,迅速将流量切换到另一个IDC。其中核心应用多IDC部署,数据库跨IDC主备,流量调度层面也要支持跨IDC的管控。 

干货 | 携程Service Mesh可用性实践_第1张图片

2.1.2 IDC内高可用-应用多集群部署

随着容器化在携程内部大规模推进,绝大部分应用都跑在Kubernetes集群上。应用部署时,将同一个Group的实例,打散到多个Kubernetes集群上。然后,各个集群中的Operator与外部的注册中心交互,将本集群中的实例信息注册到对应的Group中。单个Kubernetes的控制面不可用,只会影响到当前集群实例的变更。

干货 | 携程Service Mesh可用性实践_第2张图片

可见网站的高可用设计就是通过划分故障域,进行故障隔离,切断故障的传导,保证故障时能够切换或控制故障范围。数据中心之间故障隔离,保证数据中心级的高可用;数据中心内部Kubernetes集群之间故障隔离,进而保证Kubernetes之上的可用性。在Service Mesh的高可用设计上也要遵守这些故障隔离的原则。

2.2 Service Mesh高可用设计

思路上,我们先梳理故障场景,制定目标,然后设计方案,再结合故障场景进行可用性分析,上线之后再通过故障演练进行验证。

2.2.1 故障场景

1)Service Mesh数据面故障,可能会导致应用请求异常,从而影响服务质量,大规模的数据面故障可能会导致数据中心级别的故障。

2)Service Mesh控制面故障,无法给数据面下发最新的配置,数据面可以根据现在有配置提供服务。故障期间,实例IP的变化无法下发,路由信息也无法更新,部分服务可能会受影响。

3)目前Service Mesh控制面还是以Kubernetes为基础的,所以Kubernetes控制面的故障也会导致Service Mesh控制面的故障。

2.2.2 目标制定

1)数据中心级故障隔离。考虑极端的故障场景,数据面大规模故障,控制面长时间无法恢复等,设计上需要将故障控制在单个数据中心内部,防止故障传导到其他数据中心,从而保证有其他数据中心可以提供服务,可以进行数据中心切换。

2)支持应用多集群部署架构。从设计上考虑,Service Mesh的高可用不应该仅仅局限于现在的应用多集群部署的架构,而是与应用部署架构解耦,支持多种部署架构。

2.2.3 方案设计

多个数据中心之间隔离,每个数据中心有独立的控制面,将管理的资源收敛到每个数据中心内部。应用跨数据中心访问时需要走Gateway,通过ServiceEntry的方式导入其他集群的Gateway,由各个数据中心的Gateway收敛对外的服务入口,以数据中心为单位隔离故障。

在数据中心内部,控制面部署到独立的Kubernetes集群部署作为Primary Cluster,应用所在的Kubernetes集群作为Remote Cluster,Remote Cluster上的Sidecar共用同一个Service Mesh控制面。通过将Service Mesh控制面与应用部署的Kubernetes的集群拆分开的方式,可以灵活应对其他的应用多集群部署架构。

干货 | 携程Service Mesh可用性实践_第3张图片

2.2.4 可用性分析

1)当某一个数据中心出现故障时,需要将流量切换到另一个数据中心,与现有架构保持一致。

2)当Service Mesh数据面出现故障时,应用可服务的实例数会减少。但是基于Envoy FailOver的能力,随着健康的实例数的减少,也会逐渐将流量转到其他数据中心的Gateway上,服务可用性不会有太大影响。

3)Primary Cluster出现故障时(包括Primary Kubernetes集群故障和Service Mesh控制面故障),无法通过控制面下发新的配置。如果此时该数据中心有服务异常不可用,数据面也会自动FailOver其他数据中心,短时间内没有太大影响。而数据面访问其他数据中心的服务时,因为其他数据中心的服务都是收敛到了Gateway上,所以并不感知具体的实例信息,即使其他数据中心服务实例信息变化,也不会因为配置无法下发,导致数据面的访问出现异常的问题。

4)在数据中心内部,Remote Cluster出现问题时,只会影响应用的HPA和发布,在Kubernetes的高可用层面解决,Service Mesh层面没有影响。

2.2.5 故障演练

1)确认是否按照设计预期的方式应对故障。将某一个数据中心内的服务,置成不可用状态,观察数据面是否按照预期执行了FailOver,以及数据面请求的成功率和延迟,是否在预期范围内。

2)分析演练的效果,挖掘是否存在隐藏的问题点。当某一个数据中心内,服务不可用时,数据面会FailOver到其他数据中心的Gateway上,此时需要重点检查是否存在环路,防止请求在多个数据中心的Gateway上循环。

三、Service Mesh自身的可用性提升

宏观层面的高可用的架构,通常是作为应对服务故障的兜底措施,可以应对不同级别的灾难,故障,但是数据中心级别的切换影响面较大,不能作为常规武器,服务自身也要加强可用性建设。Service Mesh可用性建设还要围绕使用场景,深度完善可观察性指标,从而迭代优化提升可用性。

3.1 场景/目标

1)控制面运行时,需要支撑大量客户端的数据面连接,同时需要快速的将配置推送到数据面。

2)故障恢复,由于Node故障或者服务自身异常,控制面需要快速启动提供服务。

3)发布场景,控制面和数据面的新版本发布,需要快速灰度和快速回滚的能力。

3.2 确定xDS推送指标

干货 | 携程Service Mesh可用性实践_第4张图片

梳理配置下发的流程,apply到Kubernetes,到控制面处理该事件,并触发xDS的推送,最后数据面ack回来。主要是控制面内部的流程比较复杂,社区的版本中针对内部的各个阶段都有一些指标衡量,但是缺少一个描述整个下发流程耗时的指标。针对这个问题,内部已有相应的计划,初步验证了可行性。

以控制面接收到Event的时间做为起始时间,中间多个事件合并时取最小值,数据面的ack为结束时间。最终就可以得到控制面推送的耗时分布,从而进行针对性优化。

3.3 xDS推送的可靠性保证

举例说明,在应用的滚动发布过程中实例的IP都发生了变化,老的实例已经被删除,但最新的实例信息可能没有及时推送到数据面,可能导致数据面出现访问异常。根本问题在于实例删除的动作和配置下发是异步的,无法保证在实例删除之前,所有的数据面已经获取到了该实例的下线信息将流量摘除。在大规模动态扩缩,或者临界场景时,这种异步和非确定性的方式极易导致大面积的服务不可用。

因此我们需要一个保证实例变更流程可靠的机制。

干货 | 携程Service Mesh可用性实践_第5张图片

1)在我们内部使用方式上,Pod会有一个对应的WorkloadEntry,然后通过ServiceEntry注册成为服务。内部kubelet扩展了finalizer机制,使得finalizer不权阻塞对象的删除,还会阻塞kubelet对Pod的Kill操作。Pod创建时,会被对应的Operator打上finalizer,所以删除Pod时,Pod会因为Finalizer的存在而继续存活,此时还可以提供服务。

2)由内部的Operator感知Pod变化,更新WorkloadEntry的label,使之被更新为不可用状态,不会被任何ServiceEntry选中。此时WorkloadEntry的generation会+1为X。

3)然后控制面watch到这个事件,会更新内部的ledger写入WorkloadEntry的NamespacedName以及generation,并将此时RootHash作为xDS的Nonce推送给数据面,当数据面ack时,控制面会记录该数据面最新的ack的Nonce。此时可以根据Nonce以及NamespacedName去ledger中查询已经推送下去的资源的generation。

4)再由Operator调用我们的聚合服务,去查询所有的控制面中数据面的WorkloadEntry的generation,与X对比,如果都大于等于X,则认为这个WorkloadEntry都下发成功了。如果是推送未完成,Operator也会进行重试,等待推送成功。

5)确认推送成功之后,Operator会去删掉WorkloadEntry,去除Pod finalizer,真正的删除Pod。

3.4 ServiceEntry/WorkloadEntry事件处理耗时优化

实践上,我们通过ServiceEntry和WorkloadEntry将部署到虚拟机和物理机上的服务导入到Mesh中,但是发现一个namespace下5千ServiceEntry和1万WorkloadEntry的场景下,事件处理的耗时都在分钟级,多个应用发布时,延迟可能会到小时级别,根本无法满足上线的需求。

深入研究发现istio 1.7,当一个ServiceEntry/WorkloadEntry发生变化时,会触发maybeRefreshIndexes这个方法,方法中会进行遍历所有的ServiceEntry和WorkloadEntry进行匹配,重新生成内存的Map,因此会执行5000*10000=5千万次workloadLabels.IsSupersetOf()。

func (s *ServiceEntryStore) maybeRefreshIndexes() {
    ...
    wles, err := s.store.List(gvk.WorkloadEntry, model.NamespaceAll)// 10000
    for _, wcfg := range wles {
        ...
        entries := seWithSelectorByNamespace[wcfg.Namespace]
        for _, se := range entries { // 5000
            workloadLabels := labels.Collection{wle.Labels}
            if !workloadLabels.IsSupersetOf(se.entry.WorkloadSelector.Labels) { // 5000 * 10000
                continue
            }
        }
    }
}

解决问题的思路肯定是全量变增量,当WorkloadEntry发生变化时,只要遍历所在namespace的ServiceEntry,那么循环的次数就下降到了原来的万分之一,事件处理耗时也得到极大优化,下降到毫秒级。

具体实现上,处理事件的过程中需要更新多个Map,为了保证数据的准确性,还是沿用了原有的全局锁。所以目前也只是完成从全量到增量的优化,但是事件还是串行处理方式,在大规模变更的场景下延迟会被逐步放大,最后的变更推送下去需要很长的时间。目前也在尝试拓展一个新的CRD,实现上使用Sync.Map分段式锁,替换原有的全局锁,通过并发处理的方式来提升事件处理的效率。

熟悉Kubernetes和istio的同学也会发现,istio在处理event时是并没有用Kubernetes Controller Runtime的方式编程,以WorkloadEntry和ServiceEntry为例,当WorkLoadEntry变化时,应该去查找关联的ServiceEntry,然后触发ServiceEntry的Reconcile。目前内部也在尝试通过引入Controller Runtime来处理ServiceEntry,并且调大MaxConcurrentReconciles,让控制面支持并发的处理事件,进一步提升推送的时效。

3.5 控制面冷启动

在我们实践过程中也发现控制面ready之后,没有及时给连接上来的数据面推送最新的配置,导致数据面无法获取到最新的实例信息,从而影响了请求的转发。

深入分析发现,控制面ready时只等待Kubernetes的informer完成sync,但此时还有很多事件阻塞在内部队列中,导致控制面在ready之后的一段时间内,无法处理最新的事件,影响数据面下发最新的配置。在集群规模较大或者是istio资源较多的时候,尤为明显。对此我们增强了控制面启动流程,阻塞控制面ready,等待内部的队列被清空,从而保证可以尽快处理后续的事件。虽然增加了控制面的启动耗时,但相比于服务的可靠性的提升,这个代价还是值得的。

我们也在1.10中引入了DiscoveryNamespacesFilter,在控制面上忽略一些不需要关心的namespace,加快事件的处理。控制面冷启动的过程中,也发现使用DiscoveryNamespacesFilter存在潜在的风险,也向istio社区提交了PR https://github.com/istio/istio/pull/36628,目前已经合入。

func (c *Controller) SyncAll() error {
    c.beginSync.Store(true)
    var err *multierror.Error
+    err = multierror.Append(err, c.syncDiscoveryNamespaces())
    err = multierror.Append(err, c.syncSystemNamespace())
    err = multierror.Append(err, c.syncNodes())
    err = multierror.Append(err, c.syncServices())
    return err
}


+func (c *Controller) syncDiscoveryNamespaces() error {
+    var err error
+    if c.nsLister != nil {
+        err = c.opts.DiscoveryNamespacesFilter.SyncNamespaces()
+    }
+    return err
+}

3.6 灰度发布

内部在不停的优化控制面和数据面,所以也需要经常发布。随着应用接入的规模变大,更多的核心的应用接入的,传统的滚动更新也无法满足需求,更细力度的灰度发布也变得尤为重要。虽然每次发布都会经过内部多个环境的验证,最终才上线生产,但是也存在不可控的因素,变更就会带来风险,所以需要做到可灰度可快速回滚。

Canary发布

1)在同一个集群中部署一组Canary的控制面。

2)然后调整Sidecar注入策略,将部分Sidecar接入Canary的控制面。

3)通过自动化的方式进行验证,需要提前梳理测试的场景。

Canary之后会再灰度发布

4)创建一组控制面实例,逐步扩容,逐步接入流量,开始观察。

5)此时回滚就是直接缩容新实例,数据面会快速重新连接到老版本控制面上,从而快速回滚。

6)观察稳定之后,逐步缩容老的服务。

四、未来展望

Service Mesh在流量管控领域具有重大的意义,不仅可拓展性强,还可以统一流量管理模型,统一多语言的异构系统。

未来我们还会持续投入,围绕可观察性,进一步提升服务可靠性,优化xDS的推送性能,支持内部大规模落地。同时增加与社区的沟通,期待与大家共同成长。

团队招聘信息

Cloud Container&Service团队主要关注在推动云原生技术和理念在携程的落地,包括混合多云场景下的K8s、ServiceMesh、Scheduling&Autoscaling、Infra Definition&Automation等技术,使技术体系更加标准化、自动化、弹性和灵活,提升整体的效率、质量、可迁移性和交付治理能力,助力业务全球化。

团队长期期待云原生相关领域人才的加入,欢迎砸简历给我们。简历投递邮箱:[email protected],邮件标题:【姓名】-【携程Cloud Container&Service】。

【推荐阅读】

  • 容器成本降低50%,携程在AWS Spot上的实践

  • 携程Service Mesh性能优化实践

  • 携程酒店AWS实践

  • 如何构建系统优化成本,携程出海云原生实践


干货 | 携程Service Mesh可用性实践_第6张图片

 “携程技术”公众号

  分享,交流,成长

你可能感兴趣的:(大数据,分布式,kubernetes,数据库,人工智能)