从0到1全面探索淘宝流媒体架构改造与工程重建

原文标题:从0到1探索淘宝短视频流的架构再设计和工程重构

原文作者:阿里云开发者

冷月清谈:

**摘要**

随着淘宝视频流业务的蓬勃发展,老工程在架构设计和工程能力上逐渐暴露出诸多问题。为了解决这些问题,本文对流媒体老工程进行了从架构再设计到灰度放量的全面改造和工程重构。

重构成果

  • 架构重构:采用「纵向分层」和「横向模块化」的方式,实现架构解耦和快速迭代。
  • 代码质量提升:引入代码规范和静态扫描工具,保障代码质量。
  • 工程能力增强:建设工程工具和自动化测试,提升研发效率和代码稳定性。

实践要点

  • 小步重构:将重构分解为小的步骤,频繁运行测试,及时排查问题。
  • 安全重构:使用IDE的自动化重构功能,减少人为修改代码带来的风险。
  • 持续守护:采用ArchUnit等工具对代码进行约束,守护架构不受破坏。

灰度放量

  • 采用AABB方式,两个实验桶和两个对照桶相互比对验证。
  • 分阶段放量:先观察稳定性数据,再关注性能和业务数据,及时修复问题。

实践意义

对于大型业务工程的架构再设计和工程重构,本文提供了实践指引,可供业界参考借鉴。

相关文章

[12] Android Studio Lint:​​lint チェックによるコードの改善  |  Android Studio  |  Android Developers
[13] Sonar Lint:​​https://www.sonarsource.com/products/sonarlint/​​
[14] Sonar Qube:​​https://www-sonarsource-com.translate.goog/products/sonarqube/?_x_tr_sl=en&_x_tr_tl=zh-CN&_x_tr_hl=zh-CN&_x_tr_pto=sc​​




怜星夜思:


1、在架构重构中,为什么小步重构和持续守护架构至关重要?
2、在代码质量提升方面,除了引入代码规范和静态扫描工具,还有哪些有效的方法?
3、在工程能力增强方面,除了建设工程工具和自动化测试,还有什么可以改善工程效率和代码稳定性的方法?




原文内容



阿里妹导读


随着视频流业务的发展,业务的复杂性越来越高,视频流老工程在架构设计、代码质量、工程能力等方面的问题也逐渐凸显。本次重构是一次对大型业务工程进行架构再设计和重构的探索,本文是对这次探索的一次梳理与总结。

一、前言

随着视频流业务的发展,业务的复杂性越来越高,视频流老工程在架构设计代码质量工程能力等方面的问题也逐渐凸显。在这样的背景下我们开启了一次对老工程的大型重构。
本次重构是一次对大型业务工程进行架构再设计和重构的探索,本文是对这次探索的一次梳理与总结。
思维导图如下所示:


本文是实践篇,讲述如何从 0 到 1 对一个大型的业务工程开展重构。我们将按照定义架构解决的问题设计架构的实现方式重构前的准备、开始重构代码、新架构灰度放量的顺序来进行讲述,让我们开启重构之旅吧!

二、定义架构解决的问题

在设计新架构之前,我们需要先充分的了解业务以及业务背后的架构设计和实现,这个过程我们不仅要深入的去看代码,更重要的是和关联的业务同学和技术同学进行不断地对焦,向业务同学了解业务背景向技术同学了解架构设计意图以及技术开发痛点
我们可以通过以下方式加深我们的现有业务和工程的理解:
  • 找人:与相关干系人(技术,产品、设计、测试)沟通,对需求进行确认和答疑,深入的了解业务。这个是最有效的手段,可以带上一杯咖啡,找找对应的同学,聊聊业务的前世今生。

  • 看文档:看原有的需求文档、设计文档、测试用例、设计稿,帮助我们更好地去理解原有的需求。

  • 看代码:根据上述了解到业务功能,从最上层的 UI 页面代码看起,逐步根据代码的调用栈查看相关的逻辑。深入的了解代码。
在设计新架构的时候,我们需要考虑以下三个方面的问题。
  • 架构的问题:业务当前遇到的问题是什么,对应的业务工程遇到的问题是什么,哪些架构可以解决这些问题。

  • 团队的现状:团队当前的规模如何,双端的人员占比多少,团队的技术储备如何,新架构能否在这些方面和团队现状契合。新架构的引入会增加团队同学的理解成本吗。

  • 落地的成本:新架构相比老架构落地成本如何,落地路径如何,是一步到位,还是渐进式演进。
对于当前的淘宝短视频业务来说
  • 架构的问题
    • 业务期望能够摆脱对发版的依赖,需要可以快速的进行迭代

    • 业务工程架构耦合度高。代码质量差。超大类(老架构 1000 行代码以上的类 6 个,其中视频流实例入口类 3000 行)。工程能力弱。没有标准化文档,缺乏工程脚本,新人上手工程难,问题排查手段单一,缺乏有效的线上监控等等。
  • 团队的现状
    • 团队规模逐渐变大,也分了多条业务线,面对耦合的模块,需求改动难度大,代码冲突多。

    • 团队需求多,人员紧张,新架构越简单越好,不要引入复杂的架构/技术增加团队成员的理解成本。
  • 落地的成本
    • 工程大,功能多,可以考虑先基于原工程重构,优先搬移大块的代码,解决整体耦合的问题。最后整体搬移到新工程。
基于上述的问题,我们定义出了新架构要解决的核心问题:”架构解耦&快速迭代“。

三、设计架构的实现方式

「定义架构解决的问题」章节中,我们定义了新架构要解决的核心问题:架构解耦&快速迭代。为此我们的架构需要向组件化架构演进,并且支持动态化发布。
在具体落地方案上我们采用了“纵向分层” +“ 横向模块化(微服务)”的方式。
纵向分层。代码分为业务层(面向业务需求)、框架层(面向容器功能)和基础层(面向基础代码)。核心解决架构代码和业务代码耦合,框架层无法独立于业务进行迭代,无法跨业务复用等问题


横向模块化(微服务)每个模块都是一个服务,一个服务的代码都在一个包下面,对外通过接口提供功能,核心解决不同功能之间的代码相互耦合,内聚性差,日常代码推送冲突率高,功能迭代和下线困难等问题。


“3 句话理解新架构的实现机制
  • FluidSDK (新架构命名)的基本功能单位是服务 ,每一个独立的功能就是一个服务。例如提供视图组装的容器服务,提供列表管理的列表服务,提供接口请求的数据服务等。

  • 服务通过服务注册表进行注册,通过服务注册表管理器进行管理,然后在视频流实例使用。

  • 服务通过视频流上下文进行获取,得到对应服务的接口,然后通过接口调用服务的能力。视频流实例实现了上下文接口。每个 TAB 都是一个视频流实例,例如关注/推荐/直播等。视频流实例是各种服务的具体承载者。
新架构虽然累计提交了 数万 行代码,但是核心架构代码只有 7 个类,合计约 800 行代码。这 800 行代码承载了其他 数万 行业务代码的运转。

四、重构前的准备

1)引入代码规范&静态扫描工具:重构前要确保代码的提交都要符合代码规范,代码静态扫描工具是落地代码规范&保障代码质量的重要手段,可以及时的发掘代码中的问题。
一般的编码问题我们都可以用 Android Studio Lint 扫描出来。
如果你想自定义扫描规则、支持更多语言、甚至搭建自己的静态扫描服务,推荐使用 Sonar Lint + Sonar Qube 这套方案。相比 Android Studio Lint ,Sonar Lint 扫描出问题以后还会提供解决建议。
2)建设工程工具:编写脚本工具,将研发中用到的各种功能精简成一行命令完成,减少人工成本,提升研发效率。
3)引入自动化测试:重构的过程中,出错几乎是难以避免的,如果知道出错了就成为一个很重要的问题。自动化测试便成为了侦查错误的重要手段。
  • 功能自动化测试:测试同学编写的测试脚本,验证业务的各种功能。

  • 稳定性自动化测试:测试同学编写的测试脚本,运行业务的各种功能,以验证程序的稳定性。

  • 性能自动化测试:测试同学编写的测试脚本,测试重构后的性能表现。

  • 单元测试:开发同学编写的测试用例,从代码角度,验证各个类/函数的功能。单元测试前期成本较高,可酌情按需落地。

五、开始重构代码

小步安全的重构
  • 小步:将整个重构分解为小的步骤,例如一次方法提取,一次方法移动。每一次小的重构后可以通过版本管理工具进行保存,这样方便我们及时将代码进行回滚。

  • 频繁运行测试:每当有一次小的重构完成后都需要频繁执行测试,如果这个时候测试有异常,就证明我们的重构破坏了原有的功能,需要进行排查。通过这样的反馈,我们可以在更早期发现问题并及时处理。

  • 使用 IDE 的安全重构功能:使用自动化重构可以有效减少人为修改代码带来的风险,并且效率也会更高。

5.1 如何安全的重构


重构代码的常见手段,IDE(IDEA/Android Studio)都为我们提供了支持,”非不要不要手动移动代码,减少出错的可能性“。
  • 提取(Extract):重构的重用手段,大化小,繁化简,提升代码的可读性。

  • 内联(Inline):独立的函数、变量没有对提升代码可读性有帮助,可以进行消除,内联到对应的调用位置上。

  • 封装(Encapsulate):封装是实现高内聚的有效手段,“对不变的部分进行封装,为变化的部分提供扩展”。

  • 重命名(Rename):函数/变量的重命名有助于提升代码的可读性,如果一个函数/变量无法用一个合适的单词表达,说明这个函数/变量违反了“单一职责”,需要重新进行设计。

  • 移动(Move):移动函数/变量,把负责同一个职责的函数/变量放在一起,提升代码的内聚。
分类
操作子项
子项演示
提取(Extract)
提取函数(Extract Method:将一段代码提取成一个函数。拆分&精简大函数的常用手段。
提取变量(Extract Variable):将一段难以理解的长表达式提取成一个变量,提升代码的可读性。
提取类(Extract Class):提取类是拆分超大类的有效手段,按照类责任的划分,将属于一个职责的代码提取到一个类里面去。
提取超类(Extract SuperClass):如果发现两个类在做相似的事情,可以把它们相似之处提取成超类,复用该部分代码。
内联(Inline)
内联函数(Inline Method):内联函数通常用在函数的拆分不合理,先用内联把多个小函数合并成一个大函数,再进行函数拆分和提取。
内联变量(Inline Variable):变量并不比表达式更具有表现力,可以直接通过内联消除改变量。
内联类(Inline Class):如果一个类不在承担足够的责任,不再有单独存在的理由。可以把通过内联把这个类移除。
封装(Encapsulate)
封装变量(Encapsulate Variable):对 public 变量进行封装,设置为 private,缩小可见范围,同时严格控制暴露 set 方法。
封装集合(Encapsulate Collection):避免外部可以直接访问和修改集合本身,对集合内部数据提供单独的 get 和 put 方法。如果确实需要返回集合对象,考虑使用保护性拷贝,返回集合对象的一个副本。

重命名(Rename)
修改函数声明(Changed Signature):修改函数声明包括修改函数名字,修改函数参数。一个好名字可以一眼看出函数的用途。合理的函数入参设置,可以降低函数调用者的理解成本。切忌在函数里面堆砌参数。
变量改名(Rename Variable):好的命名是整洁编程的核心,变量可以很好的解释一段程序在干什么。
移动(Move)
移动函数(Move Function):每个函数都有自己的上下文环境,在面向对象语音中,类一般就是函数的上下文环境,如果一个函数频繁的其他类的函数/变量交互,说明它不适合放在这个类里,把它移动到它应该去的类里面去。
移动变量(Move Field)和函数类似,变量存在的上下文也是类,一般谁为变量赋值谁就负责管理这个变量。变量的移动一般发生在函数移动之后。
移除无用代码(Remove Unused Code):无用代码包括编译期和运行期没有被调用代码,编译期未被调用的代码通过 IDE 就可以安全移除,运行期没有被调用的代码则需要通过代码覆盖率工具进行统计。
函数/变量上移(Pull Memebers Up):函数/变量上移是提升代码复用性的有效手段,在处理继承关系时,如果两个子类的两个函数/字段做了同样的事情,可以考虑将其上移。
函数/变量下移(Pull Memebers Download):如果父类中的一个函数/变量只和它其中的一个子类有关系,那么这样的函数/变量就需要下移到这个子类中。
以代理取代继承(Replace extend with Delegate)
继承虽然可以实现代码复用,但是也带来了两个问题:
  • 继承一般只是单继承(只能继承一个类),如果想处理两种不同行为的抽象,继承无法实现。
  • 继承给父类和子类引入了非常紧密的关系,父类的任何改动都可能会影响子类。

因此我们可以把不同行为的抽象委托给代理类(Delegate)去处理。我们平时耳熟能详的“对象组合优于继承”,以及设计模式中的状态(State)模式、策略(Strategy)模式,也有异曲同工之妙。


5.2 如何持续守护新架构

一套新架构设计并落地后,我们还需要有效的手段守护架构不被后续的修改破坏。这个手段需要落到具体的工具上,单纯的文档规范无法做到这一点。这个时候架构守护 ArchUnit 就派上用场了。

ArchUnit 是一个免费、简单且可扩展的库,它可以用 Java 单元测试框架来检查 Java 代码的架构,包括检查包和类、层和片之间的依赖关系,可以作为“架构的守护门禁”。如下所示:
ArchUnit 可以从以下几个方面对代码进行约束。
约束
示意图
约束包之间的依赖关系
图片
约束类之间的依赖关系
图片
约束包和类的包含关系
图片
约束类之间的继承关系
图片
约束类的注解
图片
约束层之间的依赖关系
约束循环依赖


六、新架构灰度放量

在灰度放量之前,要先做好数据工作,包括数据埋点以及数据可视化,监控的指标包括:
  • 新架构数据大盘
    • 新架构生效率(分钟表/天表)

    • 新架构版本大盘(分钟表/天表)

    • 新架构错误码大盘(分钟表/天表)

    • 新架构性能大盘(分钟表/天表)

    • ...
  • 稳定性:Java Crash 、ANR 等

  • 性能:首帧、帧率、内存等

  • 舆情
在灰度放量开始之后,又分为两个小阶段:
  • 放量到 10% 之前:这个阶段主要观察稳定性数据,有没有 Java Crash,ANR 等。

  • 放量到 10% 之后:这个阶段主要观察性能数据业务数据,有没有线上舆情等。
放量的时候,可以采用 AABB 的方式,两个实验桶,两个对照桶,相互比对验证,记录每天的性能数据、业务数据的变化,及时修复问题。

七、新架构落地效果

新架构重构累计 Commit 400 余次,代码变动行数 数万 行。新架构落地以后,在 架构设计 代码质量 工程能力 等方面取得了比较好的效果。

7.1 架构设计

7.2 代码质量

7.3 工程能力

好了,本次的工程重构的分享到这里就结束了,不知道读者在平时的工作中有没有遇到重构工程的情况呢,有什么心得体会,欢迎评论区留言交流~
参考资料:

[12] Android Studio Lint

https://developer.android.com/studio/write/lint?hl=zh-cn

[13] Sonar Lint

https://www.sonarsource.com/products/sonarlint/

[14] Sonar Qube

https://www-sonarsource-com.translate.goog/products/sonarqube/?_x_tr_sl=en&_x_tr_tl=zh-CN&_x_tr_hl=zh-CN&_x_tr_pto=sc

培养良好的编码习惯,遵循代码风格指南和最佳实践,从源头上减少代码质量问题。

建立持续集成和持续交付管道,自动化代码构建、测试和部署,及时发现和修复代码问题。

使用自动化单元测试和集成测试,确保代码在不同条件下都能正常运行。

小步重构可以避免大刀阔斧的改动带来的风险,同时也能方便回滚。持续守护架构可以让架构不会在后续的修改中被破坏,保持其稳定性和可维护性。

规范项目结构和命名约定,让工程师更容易查找和维护代码。

小步重构就像盖房子,一点点筑基,以免地基不稳导致整个建筑倒塌。持续守护架构就像雇个保安,时刻盯紧架构,防止不法分子(随意修改)破坏它。

建立完善的文档和 wiki,帮助工程师快速了解项目和代码。

定期进行代码审查,让其他开发人员检查代码并提出改进建议。

小步重构可以把重构的任务分解成一个个小目标,降低了重构的难度和风险,而且通过频繁的测试可以及时发现和解决问题。持续守护架构就像给架构加了一道防火墙,可以防止不规范的修改破坏架构的稳定性和可维护性。这两个做法都非常重要,可以帮助我们安全高效地进行架构重构。