解密「云计算的太祖长拳」系列之三“功”:持续的特性交付与海量数据迁移 | U刻
  • 解密「云计算的太祖长拳」系列之三“功”:持续的特性交付与海量数据迁移

    栏目:技术分享

    编者按:UCloud 最新发布了名为“Sixshot”的可用区特性,用UCloud VP陈晓建的话说,“可用区就好比云计算的太祖长拳,看似平平淡淡,但要打得好着实不易。”太祖长拳属于南拳流派,共有四套拳路,讲求一胆、二力、三功、 四气、五巧、六变、七奸、八狠。有鉴于此,解密「云计算的太祖长拳」系列将在接下来的三篇内容里,详细介绍UCloud可用区项目的“一胆、二力、三 功”。

    本文是解密「云计算的太祖长拳」系列的第三篇(关注“细说云计算(CloudNote)”并回复“太祖长拳”可查阅该系列全部内容), 我们会和大家一起探讨一下我们对网络业务的后台服务(包括管理面程序management plane和控制面程序control plane)所做的重构工作。这里最重要的是如何对后端各个模块做正确的解耦从而使得我们可以做用户级别的灰度来可控地发布可用区这样一个涉及全局各个业 务的特性,并且将亿万条用户数据从老的非可用区架构平滑迁移到新的可用区架构上。云计算十年,架构师的地位越来越重要说明了什么?架构设计和数据迁移方见 真功夫,因此本篇选太祖长拳之“三功”为题。

    这是一个看似平淡沉闷但实则步步惊心的指尖之舞——如何尽最大可能保证用户的现网业务在切换过程中不受影响是我们最高优先级的任务。诚然,在整个可 用区灰度上线的过程中,我们还是遭遇了很多事先没有预料到的故障和挑战,这一程也远不是一马平川过来的。因此我们也在不断地总结和打磨我们的支撑系统和方 法论,但长期以来,以下的一些基本思想一直是整个研发和运维团队所秉承的底线:

    • 持续改进的能力高于一步到位的完美;
    • 早于客户探知和快速回滚的能力高于万无一失的程序逻辑;
    • 主动重构的团队意识高于一劳永逸的个人英雄主义。

    千里之任:后台管理程序的分拆

    UCloud的SDN网络服务的整个后台逻辑事实上是一个由20多个服务组成的大型的分布式系统。这些服务负责了SDN业务逻辑的方方面面,其中包括(只列举了主要的):

    我们在可用区项目进行的过程中,遇到的第一个大型的全局性问题就是我们发现由于之前广泛遵循的模式(design pattern),所有的manager的前端(Frontend)和后端(Backend)的逻辑都是在一个模块里实现的(当然部署的时候也是这一个模 块同时覆盖了Frontend和Backend的功能),如下图:

    这个架构的主要问题在于服务的前后端功能是耦合的。在服务程序逻辑相对简单,升级变更还相对轻量级的情况下,这里的矛盾还不是特别突出,我们通过严 密的监控、主动的运营(扩容或重启服务等)、及快速的回滚(升级验证失败的时候)等手段基本还是能控制整个系统的运行状态。但其实之前我们已经逐渐发现这 个不同关键路径程序逻辑间的耦合带来的问题了:一个十分典型的事例就是曾经发生过的一个现网事故——当时有用户反馈说从某个时间开始,控制台上的一些网络 服务的页面经常有间歇性超时从而无法显示数据的情况。

    如上图所示,我们的官网控制台是通过API Gateway来调用Manager上的接口的,因此我们的研发工程师自然而然地去排查了所有Frontend的运行日志,却发现在出现CPU飙高的时间 点,Frontend的日志里没有显示任何异常的行为。然后我们逐个排查了整条控制台到Manager调用路径上所有服务(API Gateway,Access层,包括控制台本身),都没有发现任何的问题。正当我们一筹莫展的时候,一个偶然的机会,另一位负责Manager中 Backend功能的同学提到,由于Backend每秒收到的请求(query per second)较高,所产生的日志也通常比较大,容易占用过多的磁盘空间。因此他提交了一个优化的逻辑,会随机地将一些旧的日志打包并送到远端的一个存储 空间里保存起来。而进一步分析Backend的运行日志后,我们发现所有控制台发生超时情况的时间点都是和Backend上这个日志打包逻辑的运行时间是 吻合的。这个变更对Backend的影响姑且不论,这里最大的问题在于:一个看似旁路的后端逻辑却直接影响了直接有损用户体验的前端服务,这是十分不应该 的。

    笔者之前在Amazon工作的时候,曾经听过一次内训的讲座,是当时Amazon的Global Payments部门的高级研发经理Thomas Vaughan所做的”Greatest Disaster in Amazon’s History”的演讲(这个是Amazon内训讲座中排名前三的一个演讲,可惜由于是内部资料,非Amazon的员工无法听讲)。Thomas在讲座中 总结了Amazon历史上最有教育意义的6次重大现网事故并从中总结了一系列大型分布式系统开发的原则和规范,其中第二条就是:

    Partition your critical use cases.

    也就是说:要注意解耦关键路径上程序逻辑。很难想象这样一个简单的原则却一直在不断地被违反(我们可以指出很多的原因,客观的,主观的),但事实就是如此。

    在可用区项目中,我们面临着一个相关的但更大的挑战,即:由于整个业务逻辑的复杂性,将一个用户从非可用区切换至可用区,整个操作流程需要经过许多 个业务组的配合联动,若强行要求前后端的业务同时操作不发生同步上的问题,那是代价巨大且不现实的,因此我们最终的灰度方案是后端控制面和管理面的逻辑先 进行切换,然后前端再将用户切换至新的可用区界面上(这样前后端的业务组不必一定要同时操作)。但如此我们必定要对Manager中耦合在一起的 Frontend和Backend的逻辑进行拆分才行:

    我们对大部分的后台Manager做了这样的重构。这样做也为我们今后打算进行的持续优化做了必要的铺垫。比如,之前整个Manager是用C++ 编写的,但拆分后,我们就可以为Frontend和Backend各自选择更适合它们特性的编程语言和框架 – 当前我们正在将整个Frontend用Go语言进行重构,对于编写一个基于REST风格的HTTP web service来说,用Go语言做开发,所能获得的工作效率上的提升是十分显著的(Go对RESTful风格的支持,对异步请求的处理等等,都要明显优于 C++)。

    分寸之间:控制面程序的灰度

    做完了Manager的Frontend和Backend的拆分后,我们分别对Frontend和Backend的代码做了大量的修改以支持可用区 相关的特性。其中,Frontend部分主要负责的是和前端控制台以及API交互的逻辑,也就是所谓管理面management plane的逻辑,这部分逻辑一般是用户直接可见或可操作的。Frontend模块的灰度能力是由上图中的API Gateway来提供,可以支持按用户/按API/按控制台版本等各个维度的转发调度。

    另一方面,Backend模块主要负责的是与controller交互下发规则的逻辑,也就是所谓控制面control plane的逻辑(或者说Backend模块 + controller共同组成了control plane,直接决定了整个底层SDN网络在数据转发面的行为逻辑)。此时对于Backend模块,我们还是缺乏一个灵活的灰度能力。在此之 前,Manager的发布流程是这样灰度的:

    也就是说,对于后端控制面,我们是按照宿主机的维度来灰度的:通过修改一台宿主机上controller的指向来决定它所运行的控制面逻辑。这个方 式也许在之前的场景下都还可以接受,但在面对可用区这样深入彻底的全局特性变更的时候,一台宿主机粒度的灰度能力还是不够精细。这是因为对于基于虚拟化的 云计算平台而言,一台宿主机上一般都是多租户(multi-tenant)的(除非是像主机私有专区之类的特殊主机产品),我们要求的灰度能力是可以按用 户的维度,一个用户一个用户地将他们切换到可用区的服务上。

    正如David Wheeler所言:

    All problems in computer science can be solved by another level of indirection.

    我们也为controller和各个Manager的Backend之间添加了一层转发代理(这里以Route Manager为例):

    在Controller Proxy里我们实现了如下能力:

    • SDN层面的ID到用户账户ID之间的映射(因为数据转发面上的overlay协议是不关心用户的账户信息的,因此也不可见);
    • 高并发的处理能力(因为数据转发面和控制面之间的交互通信是海量的);
    • 快速水平扩容的能力(其实就是通过ZooKeeper来添加节点)。

    如此,我们对整个后台服务灰度能力的改造就基本完成了。最后,我们还是想提一句,对于模块间耦合的重构一定要持续主动地进行。这次在可用区项目的过 程中对如此大量的服务程序做一次性的重构(可以说是“被迫的”),代价是很大的,整个研发团队顶着巨大的压力加班加点;从项目管理的角度来看,这不是一个 很理想的状态。而且在时间期限的压力下,很可能还会做出妥协而非最合理的架构决定,要知道,对于David Wheeler上面那句著名的引语,Kevlin Henney还给出了一句著名的推论:

    All problems in computer science can be solved by another level of indirection, except for the problem of too many layers of indirection.

    在下一节中,我们就来讨论一下,既然我们做了如此之多的变更和重构,我们又是如何保证这些新的逻辑既能实现新的功能,又不会破坏向前兼容性的?靠巨细靡遗的测试来保障吗?

    移山之术:管理面程序的灰度和亿万用户数据迁移

    UCloud的整个后台服务程序在升级到可用区逻辑的过程中经历了大量的改造,尤其是在SDN网络服务的部分,大量的overlay互联互通(内网 虚拟IP之间的,外网EIP和内网UHost之间的)的逻辑都发生了改变。更为重要的是,为了支持可用区的逻辑,后台业务信息数据库表的Schema都要 经过改造,我们面临的挑战是将海量的用户数据平行迁移到新的Schema下与新的后台程序的读写逻辑兼容并在这期间不中断客户的现网业务。

    首先,我们清醒地认识到:即使我们设计再复杂的测试用例,也无法真正覆盖到生产环境中错综复杂的用户场景;并且,如果我们单纯地依赖自身的测试,整 个项目的实施时间也会变得无法忍受之长。在飞速奔驰的生产环境的跑车上换引擎的问题我们已经在前面讨论过了,但在换之前,如何保证这个引擎和车的其他部分 是兼容的而不会一换上就将整辆车crash掉,这个是我们要解决的问题。

    我们必须在行进中的跑车上加一组轮胎让新的引擎去驱动,然后看看那组轮胎是不是运行正常,新的引擎和轮胎会接受同样的驾驶指令——方向盘、油门、排 挡等等——并做出它们的响应,但不会直接影响原车的行驶,我们只需观察它们响应驾驶指令时的行为是否与平行的原车的行为是一致的,如果有问题,我们可以做 停机修正然后再启动。

    这个思路的灵感是来自于一篇Twitch.tv工程师分享的技术文章《How we migrated over half a billion records without downtime》(注:Twitch.tv是北美第一大的游戏直播门户,2014年Amazon耗资10亿美元收购了它),我们首先来看一下总体的迁移思路:

    1. 对当前的数据库做一次全量的dump;
    2. 开启“双读双写”模式将全量的用户读写操作导到新的可用区程序逻辑上执行一遍;
    3. 将原数据库的全量dump导入新的数据库内;
    4. 将第1步和第2步之间“遗漏”的所有操作重新写入(replay)新的数据库中;
    5. 开始验证新的逻辑和新数据库里的信息适合和原数据库保持一致;
    6. 对于任何产生不一致的情况排查并修复根因;
    7. 当数据一致性达到100%并保持一定时间的时候,确信新的pipeline可以投入使用了;
    8. 按用户维度将流量切换到新的pipeline上。

    下图完整地呈现了我们整体思路的架构:

    当非可用区的API接到一个请求时,它会首先执行该请求,然后将一个message添加到我们后台的消息队列(AMQP协议)中去,这个message会包含下列信息:

    Message: [TimeStamp] [API Operation] [Input Parameter] [Result of the API Operation]

    其中“时间戳”的信息是必要的,因为我们在新的逻辑中执行这些操作的时候必须保证其时序是不变的。另外对于“读”操作,我们会在message中带 入原操作的结果,这样在新的逻辑上执行了同一操作后,我们可以实时地在线校验两个操作的返回结果是否一致。如果不一致的话,DCVE会立刻抛出一个异常告 警,然后研发团队就会介入排查原因了。对于“写”操作,由于一般不会直接返回操作后的新数据内容(有些API可能会),所以我们更多的是依赖离线对账,但 原理也是一样的。

    图中的DCVE是Data Consistency Validation Engine的缩写,它主要的任务就是从消息队列中读取包含API操作信息的消息,然后通过一张API映射表将同样的操作在新的可用区逻辑上执行一遍并比 对执行的结果。新的操作在执行后,我们通过在线校验或者离线对账的手段来验证两边的数据是否在语义上保持一致的,如果不是就会抛出异常告警。这样的机制使 我们能实时利用现网真实的用户操作来验证我们新编写的程序代码,不但省去了我们自己穷举测试用例的时间,也利用了“众包”的手段更好地复现了用户真实的操 作行为,帮助我们更快地定位到那些真正会对用户使用造成影响的程序bug:

    这样截图中显示了我们对其中一个Manager上两边的读写操作所做的校验结果。绿色的是不一致的结果,红色的是一致的结果。可以看到,前期还是有 一定量不一致的结果的,一般都会对应一个或数个程序逻辑里的bug。在经过一段时间的修复后,我们基本能达到一个稳定的状态了。我们发现的bug里比较有 代表性的包括:

    • 异步操作执行顺序不一致导致数据不一致;
    • 批量返回数据的时候由于数据是不排序的导致不一致;
    • 操作失败后的重试逻辑在新的服务里没有正确实现导致的不一致。

    下图是我们对每一个用户灰度过程中会遵循的一个标准化流程:

    简单解释一下每一个阶段我们后台逻辑工作的模式:

    大家可能注意到,在Stage 3中,我们还是将切换后的用户在新可用区下所做的操作同样“双写”回了老的非可用区的数据库里(通过调用非可用区对应的API)。这样做的原因在于如果我 们一旦发现可用区的逻辑有重大缺陷的时候,老的数据库中的信息还是和新的数据库保持了一致的,如此我们如果需要做紧急回退的话,也能够有条件完成。

    结语

    在本篇技术分享中,我们详细介绍了我们是如何验证并灰度发布可用区这个全局性的产品的。实现一个生产环境下的大型分布式系统,如果面对的问题数量级很小,通常很多矛盾都不会暴露出来。

    如果所有的新功能都能重起炉灶,也只是在规模达到一定量级前一个美好的理想状态。真正的困难往往就是在运营海量数据和保证现网服务不回归这两个前提 下才会集中爆发出来,而在这两个前提条件下稳定地迭代新的特性和功能,就犹如是给高速飞驰中的跑车更换引擎,是对一个系统和它背后的研发运营团队的真正挑 战。仅仅有用户至上的情怀恐怕还是不够的,要能拿出切实可行的技术方案和运营手段来。而这里的方法论、支撑系统、团队协同都需要经过大量的实战考验才能形 成。

    UCloud的团队在运营一个超大型的IaaS公有云平台上已经探索了4年时间,我们还是在不断地发现可以改进的地方,也在不断地总结和推广经验和 方法论,同时希望在这方面有实践的朋友们一起来讨论和切磋。感谢大家对这个“可用去特性技术内幕全面解析”系列的关注,期待听到您的批评和指正。

    1