背景
账号登录系统,作为游戏发行平台最重要的应用之一,在当前的发行平台的应用架构中,主要承载的是用户的账号注册、登录、实名、防沉迷、隐私合规、风控等职责。合规作为企业经营的生命线,同时,账号登录作为在线链路转化的第一站,因此账号登录系统的稳定性,一直面临极高的要求。
出于稳定性需要,游戏发行平台在很早期就实践了两地三中心的多活架构。目前以公司公司机房为中心,同时在华东公有云和华南公有云,实现了两地三中心部署方案。依托公有云的主要考量因素在于,早期公有云提供的快速弹性和按量付费的能力,能够高效的承接游戏业务方的发展诉求;其次,对于华南地区的选择,也是优先考虑重要合作方所处的地理位置。
基于稳定性、效率、成本的多方考量,最终实现了公司机房、华东公有云A、华南公有云B的混合云架构。混合云架构带来便利的同时,也存在普遍的挑战,其中最显著在于两个方面:
其一、数据架构,数据在云端存储的存在的泄露风险。在具体实施层面,数据架构的差异化,将会导致底层的领域服务所能交付的API必定存在差异,主要差异体现在API的能力范围。比如,bilibili授权登录等能力,云上机房是无法做到实际支持的,服务端只能将相关请求转发至公司机房处理。
其二、PaaS平台的差异。公司基础架构在数据库管理平台、KV数据管理平台、消息队列等产品支撑已相当成熟,但早期在公有云的应用部署只能依托云原生的能力。这导致在底层的依赖上,原本需要建立一个标准防腐层,屏蔽具体的实现差异,但因为业务进度的需要,导致有所折中欠缺。
由于存在以上两个显著的挑战,项目进展等因素的考虑,最终演变的应用架构如下,也因此账号登录系统,在公司机房和公有云机房演变出了两套代码仓库(login-idc-api / login-cloud-api)。
游戏账号登录应用的两套代码,迭代至今已近7年。当前应用,虽然在主观上理解已经迭代趋于稳定,但是基于最近一年完成的迭代版本统计,全年迭代版本超过10次,版本平均耗时接近4周,包括设计、开发、测试、上线。双代码仓库导致的设计、开发、测试,部署平均耗时增加了30%~40%。其次,在双代码仓库,且依赖Spring等核心组件版本较低的情况下,对齐公司基础架构的各项产品,可预见工作量的前提之下,该系统的重构工作,已经到了不得不行的地步。
挑战
为稳定性诉求极高、迭代长久的系统发起重构,需要巨大勇气,得失于转瞬之间。
重要性:承载千万MAU,L0不可降级链路;支撑113个SDK版本(采样周期:2024/02/15 - 2024/03/15)
稳定性:重构过程中SLO 4个9的约定,必须在执行之前规划完成执行细节和过程,全面又不失细致;
复杂度:业务本身内在的复杂度,7年来的高速发展,积累的大量技术债。
价值
1.效率:开发效率提升50%,迭代效率提升30%-40%
2.成本:节省人力(预期当前系统的生命周期至少5年)
3.质量:
代码圈复杂度:降低40%;
内网调用去SLB依赖,核心链路接口RT 99.9Line 提升31.5%
4.文化:践行“极致执行”的价值观。
实践:如何实现重构?
战略方向
方案一、放弃现有的两地三中心,账号登录全部统一到公司标准,不依赖云厂商。该方案作为未来的长期方向,完全回公司部署符合公司长期战略规划,难点在于发行平台的应用架构,目前绝大多数服务还是重点依赖公有云,短期内不具备独立完成的可行性。
方案二、推动主站账号团队,打通公司和公有云的数据架构,实现领域能力的对等,但存在数据安全风险,考虑到项目收益和跨团队的工作成本,并非当前最高ROI的选择。
方案三、将双代码仓库中,有关于数据架构和PaaS的差异,以重构的办法实现兼容,达到最终的代码仓库统一。
思考过程
重构的前提
回顾Martin Flower有关重构的著作《重构 改善既有代码的设计》一文中,将”重构“以名词、动词两种方式定义如下:
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
这本关于重构的经典,在两种定义方式中,提到了3个关键概念,”调整“、”结构“、”不改变软件可观察行为“。这3个关键词组合起来,约等于行动指南。”不改变软件的可观察行为“作为前提,如何理解可观察行为,Martin并未在技术和业务上做过明确的定义,因此在这个概念的理解上,不免要多一些执行者的个人理解和思考。
如果把应用架构视为当前的结构,对于“可观察行为”的理解,从以下几个角度尝试去思考:
1.系统职责,前后端分离的应用架构之下,交付这些职责的API是一类可观察的行为;
2.系统依赖项,是一类可观察的行为,包括两个大部分:
bilibili账号的领域服务、游戏发行的领域服务、第三方服务(比如:极验服务提供商)
PaaS基础架构(公司的数据库、KV数据系统、消息队列等等;公有云的云MySQL、Redis、Kafka等等)
3.系统的横向服务。从分层架构的层次上来说,当前定位是应用服务层BFF,本不应该出现较大的横向影响,但是过往高速演进的过程中,承载了部分领域服务的职责(比如:查询游戏信息、查询活跃游戏、查询用户信息等),因此姑且对于这一类概括为横向的影响。
形形色色的差异
当前应用架构下,双仓库在逻辑模块、应用内分层(J2EE的3层模型)的特点,展示如图:
在不可变的前提之下,试图通过对齐双仓库代码差异,以求得统一的思路,单从Service这一层来说(其依赖的bilibili账号服务API超过80个),理解起来已足够让人头大,其变更风险也是极其巨大,更何况还有JDBC ORM、Redis在技术选型和使用约束上的差异。如此风险再加上即将投入较大的工作量,势必提案困难重重。基于静态的模块和应用内代码分层来看,这巨大的差异看似是无法“暴力”抹平。
消失的复杂度
柳暗花明时刻,来源于应用运行时的一个事实:“生产环境多次高可用切流”。这个事实不仅实现了对SDK上游异地多活的承诺,而且经过了生产环境的多次验证。简而言之,双仓库在Controller、Service这两个最业务逻辑最复杂,技术债积累最多的分层,虽各有千秋,但殊途同归。
由此推理,重构过程中的最大的认知注意力负担,可以直接从这两层忽视。这两层注意力的移除,同时也利好DB和Redis的兼容工作量,作为基础架构中最重要的两个组件,完全无需再关注各类RedisTemplate/Jedis,Key命名、JdbcTemplate/Mybatis 、SQL的之间的逻辑差异,因为他们只是Controller和Service最终呈现出来的一部分。
在混沌中聚焦
SDK API链路:由于Controller、Service复杂度的消失,那么剩余的不可变的可观察行为,就只集中在DAO。DAO差异的本质来源于混合云部署架构的挑战(数据架构、PaaS差异),那么实现策略也就清晰了,在DAO层基于运行时ZONE来标识依赖的bilibili账号API调用即可。参考六边形架构,对于外部依赖和业务逻辑的隔离方式,实现核心业务逻辑对于外部API和PaaS解耦,以此降低变更侵入的影响面。
PaaS的差异在业务逻辑层Service(或者少量的Controller)已经被消化,因此无需再考虑逻辑上的差异,只需关注框架和数据库的版本兼容性。
领域类和通知类API链路:这条链路虽然不是核心,但这是混合云数据架构差异的体现。生产环境的调用集中在公司机房,能力”较小“的公有云应用,向公司应用融合靠拢,也正因如此,统一之后的代码仓库是以公司机房代码仓为基准。
转发类API链路:这条链路差异化的本质也是来源于混合云部署的数据架构差异,在当前的融合方案中无法解决,因此只能沿用现有的转发策略,但转发后续可向上移交至API GW实现,这个已在计划当中。
具体实现
公司机房原仓库为基准,在公有云A/B可用区部署,可用区选择与当前两地三中心保持一致。依赖的DB Schema在三机房原本就相同,因此可直接复用已有的DB实例。Redis,因其均有读写操作,避免灰度过程对于线上应用的数据污染,因此部署单独的Redis集群用于数据隔离,待完成灰度之后,一并下线原应用和原Redis集群。
数据验证
产品视角
基于用户维度,验证比对新老集群的数据写入和查询;
基于游戏维度,验证比对新老集群的数据写入和查询;
生产环境SDK,基于测试域名,实现核心SDK产品回归验证,覆盖全量用例;
其他非SDK API,实现全量的单测覆盖,基于接口实现验收。
数据视角
数据库:基于Job任务,实现源与目标数据源内容比对;
缓存:纯缓存场景,灰度期间隔离部署,回滚后不影响业务逻辑。缓存未命中直接远程调用;
埋点/报表:基于灰度过程观察游戏维度的报表趋势,并与过往数据进行比对。
发布方案
发布计划设计,严格遵守公司安全生产要求:可灰度、可观测、可恢复。
可灰度
灰度过程一共分为两个大步:
Step1
在公司机房执行灰度部署,分批部署导入生产流量,发现异常立即回滚。
Step2
在公有云A执行全量发布,但未接流;通过SLB规则配置,基于域名、规则的重要性、API的量级(调用量/周)制定规则级的SLB发布计划。公有云B部署同样重复此步骤。
可观测
观测的主要维度:业务和SLO、日志、性能
观测1:原公有云A应用流量切流后分布和成功率
观测2:新公有云A应用流量切流后分布和成功率
观测3:新公有云A应用错误码分布
观测4:新公有云A应用API性能
观测5:新公有云A应用JVM性能
可恢复
1.公司机房应用执行灰度部署,如遇异常立即回滚,且不产生脏数据
2.新的公有云A/B应用,通过SLB基于规则分批发布,如遇异常立即回滚,回滚后流量指向原有对应机房服务集群,Redis写产生的脏数据不影响回滚后业务逻辑
3.新的公有云可用区,申请单独的Redis实例,用于隔离不同代码仓库,不同Key命名风格的数据,避免回滚过程中脏数据,对于原可用区服务的影响。