微博云原生技术的思考与实践
本文由新浪微博架构师陈飞撰写,因见解深刻,故在此转载
现在越来越多的企业开始全面拥抱云计算,开始关注云原生技术。从管理物理数据中心到使用云主机,我们不用再关心基础运维。从云主机到 Kubernetes容器,我们不用再关心机器的管理。云上抽象层级越高,就越少人需要关心底层问题,企业就能够节省大量的人力成本与资源投入。云原生技术就是更高一层的抽象, CNCF对云原生技术的定义是:
有利利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展应用。通过容器器、 服务网格、微服务、不不可变基础设施和声明式API等技术,构建容错性好、易易于管理和便于观察的松耦合系统。
例如FaaS架构,开发者可以完全不用考虑服务器,构建并运行应用程序和服务。还有面向开源架构的的云原生技术,与提供 MySQL, Redis 云服务类似,提供基于Spring Cloud、Dubbo、HSF 等开源微服务架构的应用管理服务,开发者无需考虑部署、监控、运维的问题。
微博也一直在致力于推动基础设施云原生化,我们围绕Kubernetes构建面向容器器的云原生基础设施,形成了了物理数据中心加多个公有云的混合云 Kubernetes平台,提供秒级伸缩能力。构建开箱即用的CI/CD体系,依托云原生伸缩能力,保证大量的Job稳定运行,让开发人员摆脱代码发布泥沼。接下介绍这几方面的实践经验。
物理数据中心Kubernetes化
面向单机器的基础设施架构已经无法发挥云的大优势。把容器按照服务颗粒度进行管理,每个服务对应一组虚拟机,虽然基础运维通过 IaaS 层抽象得到了极大简化,但是业务的运维成本依然很高,业务SRE需要维护复杂的设备配置脚本,管理不同服务设备配置的差异性,需要7*24小时对故障设备进行干预。而且资源利用率无法大化,服务池是按设备划分,一个新设备添加到服务池后只能被这个服务使用,它的冗余的计算能力并不能为其他服务使用。另外不同业务容器运行在不同的机器上,容器网络架构更关注性能而非隔离性,通常会采用Host模式,这也提高了服务混合部署的运维成本。
基础设施只有形成集群,才能大程度发挥容器的良好隔离、资源分配与编排管理的优势。目前Kubernetes已经容器编排系统的事实标准,提供面向应用的容器集群部署和管理系统,消除物理(虚拟)机,网络和存储基础设施的负担。同时CNCF推出一致性认证,推动各公有云厂商提供标准的 Kubernetes服务,这就确保通过Kubernetes部署的应用在不不同云厂商之间具有可迁移性,避免被厂商锁定。
之前提到微博的容器会独占物理机的网络协议栈,虽然能够做到网络效率的大化,但是会导致多容器部署时出现端口冲突,无法满足Kubernetes动态编排的需求。为了了解决端口冲突问题,我们首先测试了了vxlan网络架构,因为其数据平面需要进行封装、解封操作,网络性能损耗超过5%,并不不满足微博后端服务对网络性能的要求。后我们评估可行的网络方案有两种 MacVlan和Calico BGP。
其中 MacVlan 成熟稳定,通过机房上联交换机改为Vlan Trunk模式,在物理机上创建MacVlan网卡子接口,通过CNI插件将虚拟网卡插入Pause容器中,实现容器网络与物理网络打通。容器的网络通信直接通过MacVlan物理子接口,发出的报文在网卡上打VlanTag,数据平面基本没有性能损耗。控制平面因需要对所有上联交换机进行Vlan Trunk改造,工作量量较大,所以这个方案仅针对高配物理机所在网络进行了改造。
Calico BGP是可以同时实现数据平面0损耗与控制平面自动化的容器网络解决方案。与MacVlan实现的扁平二层网络不同,Calico在每个节点上部署 BGP Client与Reflector实现了一个扁平的三层网络,每个节点发布的路由状态由 Felix 维护。不过由于Felix采用iptables实现路路由ACLs功能,对性能存在一定影响。因为物理数据中心不面向外部用户开放,所以ACLs功能对微博是可以去除的,我们对 Calico 进行了优化,去除iptables依赖。
微博也主动回馈Kubernetes社区,也包括为Kubernetes代码库做贡献,例例如修复多租户下网络隔离TC资源泄露问题。
之前的运维是面向物理机的,所以物理机上存在很多运维工具,如日志推送、域名解析、时钟同步、定时任务等。业务通过Kubernetes编排后,以上的功能都需要进行容器化改造。例如在容器中使用systemd会涉及到提权问题,在实践过程中发现用systemd如果权限控制不当会造成容器器被Kill的情况。所以我们单独开发了兼容linux crontab语法的定时任务工具gorun,把这个工具集成在了了运维容器里面。
因为业务容器会产生大量日志,出于I/O性能考虑,同时为了方便快速定位,日志会存储于本地PVC中,支持配额管理,避免一个容器把磁盘写满。运维基础设施容器通过监听文件,对老旧日志进行压缩清理,性能Profile日志会在本地进行统计计算后通过UDP协议推送到Graphite或Prometheus。对于关键日志,会通过Flume推送到Kafka集群,而且支持失败重传,保证日志的一致性。
通过对运维容器化后,所有业务Pod都具备相同的运维能力,形成标准化的监控报警、运维决策、流量切换、服务降级,异常封杀、日志查询的服务保障体系,服务可运维性大幅度提升。
容器编排
Kubernetes的Deployment支持Pod自我修复,滚动升级和回滚,扩容和缩容,这些特性都是云原生基础设施必备的。但是Kubernetes设计原则中对集群的管理尤其是服务升级过程中保持“无损”升级,对Deployment进行行滚动升级,会创建新Pod替换老Pod,以保证Deployment中Pod的副本数量。原有里面的IP地址和滚动升级之前的IP地址是不会相同的。而如果集群够大,一次滚动发布就会导致负载均衡变更(集群副本数/滚动发布步长)次。对于微博服务来说,频繁变更会导致这个负载均衡管辖下的后端实例的接口不不稳定。
微博实现了常备Pod的In-place Rolling Updates功能,根据业务冗余度及业务实际需要来调整上线的步长,上线过程中保持容器的IP不变,减少在上线过程中业务的抖动。因为业务的启动需要一定时间,不能按照容器启停来做步长控制,我们利用Kubernetes容器生命周期管理的liveness/readiness probe 实现容器提供服务的状态,避免了上线过程中容器大面积重启的问题。同时优化了了Kubernetes的postStar的原生实现,因为原生里面只调用一次,不管成功与否都会杀掉容器,改成不成功会按照指定的次数或时间进行重试。IP的静态分配使用Calico CNI实现:
Kubernetes的编排策略相对灵活,分为三个阶段,初筛阶段用于筛选出符合基本要求的物理机节点,优选阶段用于得到在初筛的节点里面根据策略略来完成选择优节点。在优选完毕之后,还有一个绑定过程,用于把Pod和物理机进行绑定,锁定机器上的资源。这三步完成之后,位于节点上的 kubelet才开始创建Pod。在实际情况中,把物理机上的容器迁移到 Kubernetes,需要保持容器的部署结构尽量一致,例如一个服务池中每台物理机上分配部署了wb_service_a和wb_service_b两个容器,可以通过 podAffinity来完成服务混部的编排:
一些比较复杂的,运维复杂的集群,通过Kubernetes Operator进行容器编排。Operator是由CoreOS 开发的,用来扩展Kubernetes API,特定的应用程序控制器,它用来创建、配置和管理复杂的有状态应用,如数据库、缓存和监控系统。Operator基于Kubernetes的资源和控制器概念之上构建,但同时又包含了了应用程序特定的领域知识。Operator 可以将运维人员对软件操作的知识给代码化,同时利用Kubernetes强大的抽象来管理大规模的软件应用。例如CacheService的运维是比较复杂的,需要资源编排,数据同步,HA结构编排,备份与恢复,故障恢复等等。通过实现 CacheService Operator可以让开发通过声明式的Yaml文件即可创建、配置、管理复杂的Cache集群。CacheService Operator支持:
1. 创建/销毁:通过Yaml声明CacheService规格,即可通过Kubernetes一键部署,删除
2. 伸缩:可以修改Yaml中声明的副本数量,Operator实现扩容,配置主从结构,挂载域名等操作
3. 备份:Operator根据Yaml中声明的备份机制,实现自动的备份功能,例例如定期备份,错峰备份等
4. 升级:实现不停机版本升级,并支持回滚
5. 故障恢复:单机故障时,自动HA切换,同时恢复副本数量,并自动恢复主从结构
复杂的应用在Kubernetes上部署,服务数量众多,服务间的依赖关系也比较复杂,每个服务都有自己的资源文件,并且可以独立的部署与伸缩,这给采用Kubernetes做应用编排带来了诸多挑战:
1. 管理、编辑与更新大量的Yaml配置文件,
2. 部署一个含有大量配置文件的复杂Kubernetes应用,例如上面提到的 CacheService Operator
3. 参数化配置模板支持多个环境
Helm可以解决这些问题。Helm把Kubernetes资源(如Pods, Deployments, Services等) 打包到一个 Chart中,实现可配置的发布是通过模板加配置文件,动态生成资源清单文件。
弹性伸缩
在云时代,弹性已经成为新常态。而且微博的社交媒体属性,不可提前预期的突发峰值是家常便饭,所以基础设施不但需要具备弹性,而且需要具备在短时间内提供足够资源的能力。Kubernetes基于容器技术在启动时间方面比虚拟机更具优势,省去了虚拟机创建、环境初始化、配置管理等诸多环节,直接拉起业务 Pod, 扩容时间可以从分钟级缩短到秒级。
而且峰值流量突发时,运维、开发同学可能是在吃饭、睡觉、休假,这个时候靠人为干预肯定是来不及的,所以系统需要自动做出扩容决策。对于复杂的分布式系统,实现自动决策需要解决两个问题,一个是容量量决策,一个是依赖关系。Kubernetes的HPA(Horizontal Pod Autoscaling)可以根据 Metric自动伸缩一个Deployment 中的 Pod 数量。HPA由一个控制循环实现,循环周期由horizontal-pod-autoscaler-sync-period标志指定(默认是 30 秒)。在每个周期内,查询HPA中定义的资源利利用率。并且在扩容前会有一个冷静期,一般是5分钟(可通过horizontal-pod-autoscaler-downscale-stabilization参数设置),然后通过下面的公式进行扩缩容:
但是这种容量决策存在两个问题。因为突发峰值流量上涨迅速,上述扩容机制次扩容往扩不不到位,触发连续多次扩容,导致服务在流量上涨期间一直处于过载状态,影响服务SLA。另一个问题是冷静期问题, 如果冷静期过长,会导致峰值流量无法得到及时扩容,冷静期过短会错把抖动当做峰值,造成不必要的成本浪费。第三个问题是复杂的业务系统依赖关系复杂,每个服务根据各自指标进行伸缩,由于上面还未伸缩流量被挡在了了上游,下游这时感知不到准确流量趋势,从整体应用角度看很容易出现上游泄洪下游被淹的问题。
微博整体的弹性伸缩架构是基于混合云的架构,内网私有云,公有云虚机,云Kubernetes,一体化Kubernetes弹性集群,实现快速自动化资源调度,解决了跨IDC调度、定制的调度算法与策略、容量评估、服务间扩容依赖关系等,构建了全链路路,压测,指标,报警,干预多维度的能力:
1. 全链路路是构建一个应用整体的容量决策体系,各服务不再独自判定容量,而是根据全链路路容量指标作出一致性扩容决策
2. 压测可以帮助了解目前部署的冗余情况,合理的设定扩容公式,避免多次重复性扩容
3. 指标体系是要从成千上万个Metric中抽象出可以作为决策的依据,打通负载均衡,Web服务,数据库资源等多维度指标
4. 报警及时多路径触达,避免单点
5. 干预不但要支持快速伸缩,还应支持快速优雅降级,为服务扩容争取时间
CI/CD
云计算技术的普及,研发流程也随之变化,越来越多的组织和团队开始接受 DevOps 理念。持续集成(CI) 和持续交付(CD)是 DevOps 的基石。但是 CI/CD 在实际落地过程中存在诸多困难,导致实际效果不不理想。以 CI为例,开发同学应该对“顺利利的话,会有大约100个失败的测试”这种情形并不不陌生。由于开发环境与测试环境并不一致等诸多因素,CI经常出现不相干的偶发失败,长此以往开发同学会默认选择忽略CI环节的报错警告,终导致CI/CD沦为一句口号。
利用云原生的声明性基础架构,可以将应用系统的和应用程序存放在 Git 的版本控制库中,每个开发人员都可以提交拉取请求代码,轻松在Kubernetes 上部署应用程序和运维任务,开发人员可以更高效地将注意力集中在创建新功能而不是运维相关任务上。基于Git的持续交付流水线,有诸多优势和特点:
1. 版本控制的声明性容器器编排,Kubermetes作为一个云原生的工具,可以把它的“声明性”看作是“代码”,声明意味着配置由一组事实而不不是一组指令组成,例如,“有十个redis服务器”,而不是“启动十个redis服务器,告诉我它是否有效”
2. Git作为事实的真实来源,任何能够被描述的内容都必须存储在Git库中,包括系统相关的:策略, 代码,配置,甚至监控事件
3. 与其他工具相结合,例如监控系统可以方便地监控集群,以及检查比较实际环境的状态与代码库上的状态是否一致
目前大多数CI/CD工具都使用基于推送的模型。基于推送的流水线意味着代码从CI系统开始,通过一系列构建测试等终生成镜像,后手动使用 “kubectl” 将部署到 Kubernetes 集群。程序员是不喜欢开发流程被打断,多个系统间的切换会极大影响程序员的开发效率。所以我们通过CI和 IDE结合,把CI流程融入到开发自测环节中,让程序员可以进行面向CI的测试驱动开发,提高对交付代码质量的信心。
CI/CD流水线是围绕程序员经常使用的GitLab构建,程序员可以对Merge Request的CI结果一目了然,避免了在多个系统间来回切换。每次代码提交都要执行基于分支的完整CI流程,借助云原生的弹性能力和共享存储,解决了大量并发的Job的计算资源瓶颈,同时缓解了Job间共享数据的带宽压力以及网络传输延时。
持续部署要比持续集成更加复杂。部署流程中依赖人工的环节非常多,例如灰度是由运维部署到生产环境部分机器,验证需要依靠开发和运维同学经验检查新版本各项指标是否正常,滚动发布与回滚也需要运维同学全程干预。金丝雀部署可以有效规避风险,在生产环境的基础设施中小范围的部署新的应用代码,如果没有错误,新版本才逐渐推广到整个服务,而不用一次性从老版本切换到新版本。不过如何验证没有错误是比较有挑战的,微服务依赖复杂、部署范围广、指标维度多,是易出错,耗时的环节。我们针对这个问题,开发了智能时序数据异常识别服务,覆盖操作系统,JVM,资源 SLA,业务 SLA 的上千维度指标。它不不但可以自动准确识别异常识别,性能衰减等人工经验能够发现的问题,也能够识别如资源不不合理访问等人工很难察觉的问题。现在的CD流程包含部署、集成测试、金丝雀验证、滚动发布、回滚自动化环节。
Weibo Mesh
Service Mesh并不是什么新的技术,它所关注的高性能、高可用、服务发现和治理等有服务化的一天就已经存在,社区也不不乏这方面的佳实践。不不过之前主要是两种方式,一种是微服务RPC框架形式,例如Motan, gRPC, Thrift, Dubbo等。传统微服务框架有诸多弊端:
1. 升级困难,框架、SDK 的与业务代码强绑定
2. 多语言问题,各种语言的服务治理能力天差地别,服务质量体系难以统一
还有一种是集中式Proxy形式,例如Nginx, Twemproxy, SQL Proxy等。虽然Proxy的形式一定程度上解决了胖客户端的问题,没有了升级问题,多语言可以统一接入。但是在性能方面的损耗,对于耗时较长的请求来说还可以接受,但这在服务间调用这种毫秒级请求时,性能是不能容忍的,而且服务的拆分势必导致整个体系内耗时随着微服务规模的扩大而剧增,而且Proxy本身很容易成为整个系统中的瓶颈点。所以经常可以看到后端服务是同时使用 Proxy和RPC的情况。
而Cloud Native会催生出如此火爆的Service Mesh,主要的因素是 Kubernetes使基础设施的标准化,大家发现之前这些很重的RPC框架可以抽离出来,原本需要增加维护的复杂性被Kubernetes解决掉了,跨语言、服务治理等收益凸显出来。而且Mesh的SideCard形式,相比Proxy在请求耗时方面优势也相当明显。
微博将Motan RPC胖客户端实现的治理功能下沉到Agent上,服务注册和发现依赖微博自研Vintage命名和配置服务,对服务的订阅和发现来建立服务间依赖的逻辑网络。业务与的通信协议保持一致,Agent支持HTTP和RPC的调用,业务只需把原有的调用指向Agent即可,不需要改造业务代码。在跨语言通信协议设计方面,Google的Protocol Buffers(pb)序列化能够提供的跨语言序列化能力,但是在一是老旧HTTP迁移到pb 协议的改造成本过高,二是部分语言(例如 PHP)在做复杂pb对象序列化时性能比较差,甚至比json序列化还要慢3倍左右。微博实现了全新语言无关的通信协议 Motan2和跨语言友好的数据序列化协议Simple来应对跨语。
除了代理Service的能力外,Mesh体系提供了缓存、队列等服务化代理,业务方可以与依赖缓存、队列资源治理解耦的能力。可以大幅提高那些治理能力比较薄弱的业务和语言的架构水平。随着云原生技术的日趋完善,会有越来越多的基础设施从原有的 SDK 中抽象出来。未来数据库访问会以 Database Mesh形式提供访问,封装数据分片、读写分离、从库负载均衡、熔断、链路路采集能力,例如Google Cloud SQL提供本地Proxy,业务方无需将IP地址列入白名单或配置SSL,即可安全地访问Cloud SQL。
相关文章