本文来自。
项目背景
蜂巢计费系统为(网易蜂巢)提供整体的计费服务,业务范围涵盖完整的产品售卖流程,包含定价、订单、支付、计费、结算、优惠、账单等主体功能,支持十几种不同产品的售卖,产品形态上贯穿了IaaS、PaaS和SaaS类别。同时,计费方式还提供了了按量、包年包月、资源包等多种方式。该项目的业务范围之广,玩法种类之多,数据要求之严注定了它将成为一个烫手的山芋,而且还是一个吃力不讨好的工作。
该项目在人员上已经几经易手,就我所知,已经换过两拨完整的开发和测试团队了,而且已经全部离职。不得不说,该项目已经变得令人谈之色变,让人敬而远之。在这样的背景下,后期接手的开发和QA不得不硬着头皮上,踩着雷过河,小心翼翼的应对着不断涌来的业务需求。随之而来的是高居不下的bug率,越来越难以维护的代码,无法扩展的架构问题,我们开始意识到这样下去是不行的。于是我们从8月份开始了漫漫的架构升级之路。
重新出发
在我们开始优化架构之前,我们重新梳理了计费系统完整的业务,得到了如下图所示的业务领域:
梳理以后发现,计费系统承载了太多非计费的业务,包含订单、账单、结算和代金券等,这些业务代码散落在各处,没有严格地业务边界划分,而是“奇迹般”的融合在了一个工程里面。造成这个局面的原因在于计费系统初版设计时,根本没有考虑到这些问题,当然也不可能考虑到,而在后面逐步地迭代过程中,也未能去及时地调整架构,架构腐化不是一天内完成的。当然,这方面有部分技术的原因,也有部分人为的原因所在,因为当时负责计费系统的开发就只有一人,还是刚毕业的同学。目前看来,也是难为这位同学了。
技术债务的问题不是小事,千里之堤毁于蚁穴。既然我们找到了问题的症结所在,那么解决的方式也就显而易见了,一个字:拆!我们分析了所有的业务,订单是最大也是最复杂的一个业务,而结算和账单考虑到后期有可能迁移到云支付团队,我们决定优先把订单系统拆分出去!
拆分的阵痛
订单拆分说起来容易,做起来难。套用一句业界常说的话,就是开着飞机换轮胎。因为在我们拆分的同时,不断地有新的业务需求进来,还有一些bug需要处理,所以不太可能让我们专门进行拆分的工作。因此,为了不影响正常的业务迭代,我们决定拉出独立分支进行开发。我们分出两人专门处理拆分的工作。
为了最小化风险,订单拆分我们分了两步进行:一,模块独立;二:系统独立。
模块独立
模块独立是将订单的代码首先在工程内部独立出来,我们采用独立Module的形式,将订单独立成了一个Order的模块。它拥有完全独立的服务层、业务层以及持久化层。其他模块可以依赖Order,而Order不能依赖除公共模块外的其他业务模块。整体的模块划分如下图所示。模块的拆分过程中我们也发现了原先很多不合理的地方,例如:其他服务直接操作订单的持久化层(DAO)、模块直接依赖关系混乱、Service所在的Pacakge不合理、存在大量无用的代码和逻辑、随意的命名等。我们边拆分边重构,虽然进度比预期要缓慢一些,但整体上在向着合理的方向进行。
模块独立的过程中我们遇到了业务层级关系的问题。由于订单模块不再依赖于其他业务模块,而又有一些业务逻辑是由订单触发的,需要在计费模块完成,我们又不能直接调用计费模块的Service。针对这个问题,我们采用了领域事件
的方式来解耦,简单来说就是订单通过发布事件的方式来与其他模块进行通信,当时实现的代码其实也相当简单。
我们并没有独立拆分web层,因为系统还没有独立,web层作为统一的打包入口也承载着订单的流量。而且,Controller层的逻辑相对比较简单,完全可以在系统独立时再做。通过大家的努力,8月底订单已独立模块的方式上线了,一切正常。
系统独立
模块拆分完成后,仅接着就是系统独立,此时我们需要将订单系统独立部署。这里一个关键的问题是,独立部署意味着单独提供服务,而依赖订单系统的业务方非常之多,包含前端、主站、大部分的PaaS业务和计费,都有需要直接依赖订单接口的地方,冒然独立风险很大。针对这个问题,我们采用使用haproxy七层转发代理来将流量分发到不同的vip来解决。虽然,在上线过程中遇到了一些坎坷,但最终还是成功了。现在看来这个选择是非常对的,因为这样可以在业务方无感知的情况下平滑升级。但长远来看,最终我们还是以独立的vip对外保留服务。
订单和计费直接我们采用RabbitMQ来完成主体通信,关于采用MQ还是HTTP调用我们内部还进行了一番争论。之所以最终还是采用MQ来进行通信,是因为我们发现很多业务流程并不需要计费系统立即响应(大部分流程都是订单触发的),也就是我们常说的弱依赖。另外,职责上计费系统的响应的质量也不应影响到订单的主体流程,举个例子:用户支付了一个云主机的订单,如果计费系统此时无法响应,业务上相对来说可以接受过一小会儿计费再处理,而不是把订单直接退款给用户。MQ的引入在技术和职责层面都将订单和计费分的更开了。当然,强依赖的服务是我们无法避免的,其中之一就是结算模块还留在计费中,订单需要通过接口调用结算服务来完成支付。
前期,我们在模块独立时采用事件解耦的方式,在此时也获得了收获。我们通过一个统一的转化层,将那些事件直接转化层RabbitMQ可以识别的消息,这样代码的改造工作就大大减少了。
系统独立后一个直接的表象就是每个系统的代码行数大大降低了。独立前,整体的代码行数已经达到了12W行以上(包含配置文件),独立后,计费系统降低到了10W以下,订单维持在4W以下。代码行数的降低将直接提高系统的可维护性。个人认为如果一个工程里的代码超过10W行,那么维护性将大大降低,除非是那些有着严格自律意识的团队,否则,我建议还是尽量降低代码行数。
经过大家一个月的努力,订单系统终于已独立的姿态提供服务了。过程很艰辛,但是收获良多。
拆分的收获
订单独立后,一个直接的好处就是我们能独立的思考问题了,这在以前是很难做到的一件事情,因为大家不得不小心翼翼的处理那些依赖,做事会畏手畏脚的。另外一个好处就是,我们的工作可以有侧重点的进行了。订单业务可以说是产品最为关注的业务,也是计费对外暴露的主要入口。 下图就是我们在拆分后规划订单的业务架构,大家对后期的订单规划充满期待。
多Region的挑战
公有云产商面临的一大挑战就是多Region环境的支持。普通的互联网行业出于高可用的考虑,往往会把核心系统部署到多个机房,然后根据自己的实际应用场景选择冷备、双活甚至三活。我们经常听到的“两地三中心”、“三地五中心”等等高大上的名词就是代多机房高可用的缩影。这些行业做多机房部署的主要目的是为了提高系统的可用性,不是其业务的必须属性。换句话说,他们不做多机房部署也可以,做了当然更好。而公有云产商不一样,多Region部署就是其行业属性之一。如果哪个云产商不提供多region产品的支持,那么它肯定是不完整的。不得不承认,我们在这方面的经验是比较欠缺的,在多Region的支持上走了一些弯路。
摸着石头过河
今年上半年的时候,蜂巢开始计划启动北京Region,预计年中交付,当时对我们横向业务提出了很大地技术挑战。一是在于横向系统设计之初并没有考虑到对Region环境的支持,我们很被动;二是我们并没有跨Region系统设计的经验,我们很着急。计费系统面临的问题更加严重,因为它对数据的一致性要求更高,而且出错地影响范围也更大。而且当时计费的技术债务已经很高了,产品的需求列表也拍了很长,套用一句很形象的话说,“留给我们的时间不多了”。
在这种情况下,我们“胆战心惊”的给出了第一版的多Region设计方案,主体架构如下所示:
因为当时计费系统还没有拆分,所有的业务都在一个系统中完成的,就是我们常说的“大泥球”系统。这种情况下我们很难做到多Region部署,订单和账单其实只有在一个Region部署就可以了,而计费的数据采集和请求分发是要下沉到各个Region的,而计算过程可以集中完成。采用"双主"同步复制的方案实则是无奈之举。数据库的同步只能基于实例级别,而无法细分到表,我们各Region中计费数据库中存在资源的计量表,这个数据需要同步到杭州Region来完成。为了避免“脑裂”的问题,我们特别将该表的主键采用UUID的形式。存量表因为无法做大规模修改,我们通过限制北京MySQL用户的权限来避免写入和修改全局表。
这个设计很糟糕,但是当时的条件限制,我们也拿不出更好的设计了。虽然上线的过程有些曲折,当这个架构还是成功运行了,这是令我们最为欣慰的事情。因为为了适配这个架构,团队的小伙伴做了很多工作。不可否认,这个架构存在诸多弊端,其中最大的隐患就在于数据库的“双主”同步,这就像一颗随时会爆的炸弹萦绕在我们心头。当时专线还没有搭建好,所有的流量均通过外网隧道代理,糟糕的网络质量无疑放大了这个风险。为此,DBA们向我们吐槽了好久,幸好我们抗打击能力很强。
涅槃重生
在做完双Region的支持以后,计费团队就继续做产品需求了,因为架构调整导致需求列表已经很长了。而且当时也说的是,短期内(至少今年)不会再有第三个Region了,我们也想着快点做完,多花点精力投入到重构中。但是计划赶不上变化,9月底我们被通知到第三个Region来了,而且已经被提高到第一优先级支持了。
有了第一版双Region的经验,这一次我们淡定了很多。当然,我们不可能在沿用第一版的设计了,因为DBA就会跟我们拼命的。回过头来梳理多Region支持面临的问题时,我发现一开始我们就自己给自己挖了一个坑,然后往里面跳。横向支撑系统显然都需要对所有Region提供支持,但这并不代表其需要在各个Region内部署(我还与团队其他的小伙伴分享了这方面的想法,网上应该还能找到这一次分享的ppt——《跨Region实践初探》)。因为公有云产商经常会提供多个Region的服务,有得甚至达到几十个Region,如果横向支持系统每个Region都要全量部署的话,那么我们花在运维上的精力就可以拖垮我们,更不要说还有最为困难的数据的一致性问题。
其实多Region的支持的问题我们总结出主要表现在一下两个方面,一是应用层面的接口互通;二是底层数据库的同步。
我们先说底层数据库的同步,对计费系统而言,数据的一致性是至关重要的,但多机房部署是在挑战CAP定律。是不是就没有了这样的数据库方案了呢,有,那就是Google的Spanner,号称可以在全球做到强一致的数据库。但是我们没有这样的数据库。其实我们也考虑使用NoSQL数据库——Cassandra,但是这个数据库运维起来太复杂,我们也没有这方面的经验,也就放弃了。还是回归到MySQL,受限于传统关系型数据库在扩展性方面的问题,我们不可能把整个库在各个Region都同步一份。但是计费原始数据又必须在各个Region内收集,于是我们决定——拆,把计费拆层两个部分,分为bill-agent(数据采集)和bill-central(数据计算)两个部分。
- Bill-Agent负责Region内日志的收集和简单聚合。
- Bill-Central负责日志收集外的全局事务处理。
通过这样的拆分,架构就清晰多了。再多加Region,我们只需要部署Bill-Agent就可以了。Bill-Agent将处理过的计费数据写入本地库的一张资源表,利用NDC(马进在网上分享过关于这个中间件的介绍)将资源表单向同步到Bill-Central的中央库,然后Bill-Central统一在对计费数据进行处理。有意思的是,这张资源表就是我们在第一版设计中新建的资源表,因为我们将主键修改为UUID,所有使用NDC同步表的方案是相当顺利的。当然,NDC在我们其他项目的跨Region支持上也发挥了重要作用,比如:跨机房缓存更新的问题。这一版的数据库方案在技术评审时大家都比较满意,DBA也肯定了我们的方案。
现在再来看跨Region调用的问题。在多Region的横向系统中,我们发现或多或少的存在着机房间的接口调用问题。这些问题有可能是某些Region的库不能写需要路由到主库来写导致的,也有可能是全局缓存的问题,还有就是Global业务向Region内服务发送指令。计费属于最后一种场景,我们有一些业务场景需要由杭州Region触发,然后调用各个Region内的服务的接口。在第一版的实现中,计费系统自己实现了跨Region代理部分,但是实现的不是很好,代码的可维护性比较差,加重了调试的难度。这一版的设计中,我们决定把跨Region接口代理单独拿出来重新做,结合多Region的应用场景,然后封装一些非功能性的特性,这就成了后面我们很重要的一个组件——RegionProxy。
RegionProxy最开始是为了解决跨Region调用的非功能性问题,简化应用系统处理的成本。但是设计上经历了比较大的调整。最开始的设计我们是希望Region内所有跨Region的HTTP调用都能通过RegionProxy来代理,RegionProxy之间能够发现对方并且相互通信,那么Region内的应用系统就只需要与本Region的RegionProxy通信就可以调到任意一个Region的应用系统了。但是在方案评审的过程中,我们发现如果都用RegionProxy代理,可能会导致跨Region调用多出一跳或者两跳,调试可能会比较困难。后来,我们放弃了这个方案。再后来,我们发现ServiceMesh的方案和我们最初RegionProxy的方案是十分相似的。
在RegionProxy的设计上我们进行了简化处理,我们将所有Region的业务系统录入到一个全局的配置中心(我们自己开发的ConfigCenter)中,然后通过一个自己开发的一个HttpProxy的Java库来与ConfigCenter通信来完成跨Region的调用。这样做的好处就是使用方用起来比较轻量,但是在网络连通性方面我们需要与所有Region的系统做到互通。在开发Proxy库的时候,我们不仅对跨Region的HTTP调用进行了封装,而且对普通的HTTP调用也加入了非功能性的封装,这样系统可以通过Proxy库完成所有的HTTP调用请求,极大的简化了代码的维护成本。后面,我们使用RegionProxy来代理请求后,确实删除了很多以前的无用代码,整体流程上也清晰了许多。
多Region的感悟
经过两版多Region的改造,我们确实收货了很多宝贵的经验,非常难得。实际上,在多Region的支持上,大家需要清晰地认识到为什么要支持多Region,以何种方式去支持多Region,多Region支持与高可用的关系等基本问题。如果这些问题回到不好,或者不清楚,那么很容易就会掉到陷阱中去。另外一个感悟就是结合业务的实际场景,第二版的多Region架构我们之所以能够这么设计,就在于计费系统不需要实时出账,我们完全可以把数据保存下来,离线计算以后再出账,这是可以接受的。但这并不适用与所用情况,有些性能要求很高的横向业务就不适合这种场景。
拿来主义
前面提过几次技术债务的问题,有些问题是可以通过工具来解决了,有些只能通过内部重构来解决。左耳朵耗子曾经说过一句话对我感触很大,大意是说有些公司在解决问题时偏流程,有些公司偏技术。我想我们既然是技术团队,在解决问题时能通过技术方式解决的就应该尽量用技术解决,流程和人都是不可靠的。
难以管理的配置文件
计费项目面临的诸多问题之一就有配置文件的管理,因为业务流程的原因,计费系统有着大量的各种各样的配置。以前我们把配置文件放到工程里面,通过自动化部署平台来指定使用不同的配置文件。这样做的一个显著问题就是代码和配置耦合起来了,每次修改什么配置都得提交代码,而我们提交又有着一套严格地流程,导致整体效率不高。另外一个问题就是可视化的问题。往往QA在线下环境测试都是通过的,而上线以后出了问题,基本上都是配置导致的问题。针对这几个的问题,我们决定使用来管理我们的配置,通过整合Apollo,我们的两个项目(订单和计费)都做到了工程零配置,所有的配置都放到Apollo上进行管理,好处良多。
替换定时任务框架
计费系统严重依赖于定时任务,有许多流程需要通过定时任务来推动。以前我们使用QUARTZ+MYSQL来作为我们分布式定时任务框架,但是这种做法的可维护性太差,而且对数据库侵入很高,对测试也不友好。在QA的不断吐槽中,我们决定替换掉现有的定时任务框架。在调研开源的定时任务框架后我们决定使用来作为我们的分布式定时任务框架。目前,我们的两个项目的所有定时任务(除bill-agent外)都已迁移到Elastic-Job上来了。
抽象化设计
如果你要问我做蜂巢计费最困难的地方是什么?我的回答肯定是业务太复杂了。这种复杂性不是因为我们架构设计的不好导致的复杂,而是业务本身就是十分复杂的。现在计费系统需要支持十几种产品的售卖形式,涵盖IaaS、PaaS和SaaS的绝大部分产品,同时各个产品的售卖和计费模式都存在或多或少的差异,这让我们很难通过一个统一的模型就涵盖所有的场景。我们找到了一条缓解这个问题的方式——抽象化。
横向系统或者支持系统如果需要服务多个产品,那么抽象化设计是不可或缺的一个缓解。如果越早进行抽象化,那么后期对接和维护的成本也就会越低,还能把系统的边界划分得更清晰。计费系统早期的设计在抽象化方面没有过多的规划,在后期的对接方面又处于比较弱势的一方,导致计费系统出现了大量的特化代码。这些特化代码对一个服务十几个产品的支持系统无疑是伤害巨大的。现在我们已经意识到了问题的严重性,也着手在做这方面的重构工作了。但是挑战依然很大,因为业务的复杂性是无法通过技术手段就能降低的,这方面我们只有和产品、运营和销售各方面一起努力,打造一个合理、灵活、稳定的新计费。
抽象化设计因为我们还在进行中,后期有机会再分享。
总结
从八月底加入计费团队以来,收获良多,无论是在技术上,还是在对业务的理解上,都有了许多新的认识。最为给力的还是团队的小伙伴们,因为计费本身的需求非常多,处理这些需求的人都只刚刚够。后来我们又做了两版跨Region改造、订单拆分、框架替换、抽象化等优化工作,迭代周期从两周一次压缩到了一周一次,开发和QA的小伙伴也都是任劳任怨。当然,大家能在这个过程中有所收获才是最关键的。
计费系统可以说是我接触过的最为复杂的一个系统,越是复杂的系统越需要清晰的头脑和良好的设计。云计算产商的博弈已经到了白热化阶段了,大家拼的不光光是每个产品的质量和体验,还有整个云平台的内功。公有云平台本身就是一个庞大、复杂的系统,如何把这个系统建设好,用户体验做好、服务质量提高、稳定性得到保障,这本身就是极为有难度的一件事情。计费系统作为公有云平台一个重要的组成部分,可以说扮演着一个极为关键的角色。做得好可以对整个平台提供助力,而做的差则会拖慢整体的发展进程。我们已经找到了适合自己的一条道路,相信会走上正轨!
网易深度整合了 IaaS、PaaS 及容器技术,提供弹性计算、DevOps 工具链及微服务基础设施等服务,帮助企业解决 IT、架构及运维等问题,使企业更聚焦于业务,是新一代的云计算平台。
本文来自网易云社区,经作者蒋文康授权发布。
原文: