Ruby 思想在 Elixir 项目中的应用

编程入门 行业动态 更新时间:2024-10-06 20:37:03

Ruby <a href=https://www.elefans.com/category/jswz/34/1768679.html style=思想在 Elixir 项目中的应用"/>

Ruby 思想在 Elixir 项目中的应用

Tubi 是谁?一鸣是谁?

大家好,我是一鸣,很荣幸今年再次在 RubyConf China 和大家做分享,我是来自 Tubi 的 Elixir 工程师。和上一位演讲嘉宾 00 一样,我也在维护和开发 Tubi Multimedia Processing Platform 这个项目。

如果你对 Tubi 还不熟悉:

我们是一个为用户免费提供电影、电视剧、视频的流媒体服务,为了播放这些电影电视剧视频。我们需要从合作商那里拿到视频源文件,进行一系列处理后,将这些源文件切片、压缩成不同格式,放在不同的云存储后端上,将他们提供给用户。今天要分享的内容就来自于这个 Tubi Multimedia Processing Platform 项目,也是我从中学到的一些经验总结。

Ruby in Elixir? 

Elixir 是一门函数式的编程语言,而 Ruby 是一门面向对象的语言,要怎么在一个函数式语言中使用 Ruby 的思想呢?

在回答这个问题之前,让我们首先熟悉一下 Elixir 的语法。看下面这一段 Elixir 代码,我们可以看到 Elixir 和 Ruby 的语法非常相像:

希望这样的对比能减少你对 Elixir 语法的陌生,接下来我们也会围绕这一段函数展开。

要解决的问题与挑战

我们今天介绍的重点就是「如何把复杂的业务逻辑划分到不同的对象或模块中去」,然后根据业务需求将它们再次组合起来,去完成一个更加复杂的功能,最终达到降低组件维护成本、加强组合扩展性、加快开发速度的目的。

首先介绍一下我们要解决的问题与挑战:在不同的云存储服务之间进行大规模的文件迁移

如刚才所说,Tubi 是一个免费提供电影、电视剧、视频的流媒体服务。经过多年的积累,Tubi 有上千万个视频文件,文件大小从 KB 级别到 GB 级别不等,总共的文件大小达到 PB 级别,是非常大的数量级。每当我们配置一个新的云存储服务时,我们都要把如此大规模的文件迁移到新的云存储服务上。

>>> 这里的功能性需求非常直接:

首先,所有的文件都要按照文件夹分批迁移,保证最完整的迁移效果。

其次,我们要保证每一个文件自身的完整性,也就是说在迁移之后要进行数据的校验,保证在新的云存储后端的新文件和旧的云存储后端上的就文件是一致的,不会因为网络的波动或者其他原因引入数据上的不一致。

最后,在校验完成之后,我们还要在数据库里记录新文件的位置信息,才能在用户访问时为他们提供新云存储后端的版本。

>>> 除了功能性需求,我们还有一些非功能性需求,即在时间和金钱的开支上找到最佳的平衡:

要迁移这样 PB 级别的数据要花费的时间很久,数据传输上的开销也很大。而在不同的云存储后端之间迁移文件也有不同的方式,不同的方式在时间/金钱开支上有不同的取舍。有可能一个方式的传输速度快但是数据开销贵,而另一种方式的传输速度慢但数据开销更低。我们要灵活地替换不同的迁移方案,进行对比实验,找到性价比最高的一个解决方案。

项目成熟之后,我们甚至可以根据视频的特性采用不同的方案:比如一个比较火的电影,我们可以用速度快但是网络开销高的方案传输;而对于一个不是那么火的电视剧,我们可以接受相对较慢的迁移速度,以省下一些数据传输的费用。当然,项目的开发速度要快,要做到更换文件传输方案时不影响文件数据库更新部分的逻辑,也不会造成大量代码逻辑的改动、引入 bug。

接口驱动设计

为了完成这些功能性需求和非功能性需求,我们使用了「接口驱动设计」的思路去完成这个项目。顾名思义,接口驱动设计首先要定义一些接口,再定义一些不同功能的实现。

那么让我们先看一下接口的定义:

  • Mover.move(mover, source, destination)

  • Comparatorpare(comparator,source,destination)

在这里我们定义了两个接口:

>>> 第一个是 Mover ,也就是负责迁移数据的接口。

它接收三个参数,第一个参数是 mover,提供不同的实现,可以理解为 Ruby 里子类的对象。接着接收两个参数:source 和 destination,都是哈希 Map,分别包含了源文件路径和目标路径的信息,包含云存储后端和路径等元信息。

注意 Mover 迁移的数据不一定是文件本身,也可能是数据库记录等其他需要「迁移」的数据。如果迁移成功的话 Mover.move 函数会返回 :ok,如果迁移失败则会返回错误和失败的原因。

>>> 第二个接口是 Comparator,它也接收三个参数 comparator, source 和 destination。Comparaor 负责对比源路径和目标路径上的数据是否一致。

同样,这里对比的是「数据」的一致性,因此不止是文件数据的对比,也可能是数据库记录的对比,等等。如果对比结果一致的话, Comparatorpare 函数会返回 :ok,如果不一致,则会返回错误和不一致的原因。

有了这两个接口之后,我们就可以分别定义他们各自的实现了。在这个项目里,我们对 Mover 接口定义了这些实现:

  • FileMover

  • DBMover

  • ConcatMover

  • CompareAfterMover

FileMover 负责将文件从源路径迁移到目标路径上;DBmover 负责通过数据库的操作,拷贝并更改 source 对应的数据库记录,就可以得到destination上的数据库记录了。

>>> 另外,我们还可以有一些很抽象的 Mover:

像 ConcatMover,它可以接收两个其他类型的 Mover,比如一个 FileMover (mover1) 和一个 DBMover (mover2),在调用 Mover.move(concat_mover, source, destination) 时,它就会依次调用 Mover.move(mover1, source, destination) 和 Mover.move(mover2, source, destination),当两个 mover 都返回 :ok 时,ConcatMover 才会返回 :ok。也就是说它相当于一个承上启下的连接器,连接起两个 mover 实现。

>>> 我们还有一个 CompareAfterMover,这是一个比较有趣的 Mover。

它接收一个 mover 和一个 comparator ,在调用 Mover.move(compare_after_mover, source, destination) 时,它首先会调用 Mover.move(mover, source, destination) 让底层的 mover 去迁移数据。mover 将数据迁移完毕后,CompareAfterMover 又会调用 Comparatorpare(comparator, source, destination) 去对比 source 和 destination 的数据。

只有当 comparator 返回 :ok 时,这个 compare_after_mover 才会返回 :ok,否则会返回错误。也就是说,CompareAfterMover 将一个 mover 和 comparator 连接到了一起。在 mover 的基础上加了数据对比的步骤。

而 Comparator 同样有不同的实现。

比如,MD5Comparator 会对比 source 和 destination 上存储文件的 MD5 哈希值,只有当两个哈希值一致时,才会返回 :ok。同样我们也可以用其他哈希算法实现不同的 Comparator 对比文件的一致性。甚至可以设计一个 Comparator 来对比数据库记录是否已经存在。

接下来,我想分享在 Elixir 中如何使用 Protocol 这个特性定义一个接口。以 Mover 为例:

我们首先定义一个叫做 Mover 的 Protocol,它有一个 callback 函数 move。通过 Protocol 定义了接口之后,我们就可以定义接口所对应的实现了,这里我们用  ComparAafterMover 作为例子:

首先我们定义这个 CompareAfterMover 为一个 Struct,一个字段是 mover,一个是 comparator。同时这个模块提供了一个 new 函数,接收 mover 和 comparator,将他们保存在一个 CompareAfterMover 的 struct 里。接着我们就可以定义 CompareAfterMover 针对 Mover 的实现了:

在我看来,Protocol 也是 Elixir 相比 Ruby 的一个优势,它能够显式地定义一个接口是什么样子,接收什么样的参数,返回什么类型的值。

而在 Ruby 中更依靠测试和文档这些隐式的约定,提醒开发者要实现一个接口的话需要实现哪些方法,很容易被开发者错过或者绕过。

通过类似的方法定义了不同的接口实现之后,我们就可以将不同的实现组合在一起,最终构建一个方案:

再初始化一个 MD5Comparator,接着用 CompareAfterMover 将 FileMover 和 MD5Comparator 连在一起;然后是一个 DBMover,拷贝数据库的所有记录。最后用 ConcatMover 将 CompareAfterMover 和 DBMover 连在一起,再将这个最终的结构和 source, destination 一起传递给 Mover.move 方法。

最后一行才是工作的真正开始。组合出来的新对象根据 ConcatMover 以及其内部的编排最终实现我们想要的效果。

这样的实现优点是,它的实现每一步都是分离的,每一个组件都负责自己的那部分功能,可以很灵活地替换不同的实现,只需要替换组件即可。并且这种组合是在运行时发生的,我们可以加上新的 PopularityMover 动态地根据电影的热度决定要使用哪一种性价比的 FileMover;另外,这种方法易于测试,我们甚至可以单元测试 ConcatMover 这种抽象的 Mover。

但是缺点就是逻辑比较分散,整个项目的逻辑被分散在零零散散的组件中,因此前期理解时需要一定的成本,需要比较好的文档和测试帮助新人很快地接收这种模式。当然上手之后,这种方案的优势就会凸显出来,可以很快的研发和迭代。

对 Rubyist 的启发与思考

这样的开发思路对于 Ruby 开发者有哪些启发和思考呢?

>>> 第一,在初始化对象时就传递它的依赖。

我在之前主要写 Ruby 的时候,一直有一个疑惑:在初始化对象时到底要传递哪些参数?以 Mover 为例,要传递 source, destination 吗,还要传递 mover 和 comparator 吗,还是四个都要传递呢?在经过这个项目之后,我明确了在初始化对象时,我们只传递这个对象所依赖的其他对象。以 CompareAfterMover 为例:

初始化时接收的就是 mover 和 comparator,这样我们就得到了一个对象和它的依赖关系,这个对象依赖关系是不变的。

在接下来的方法调用中我们可以传递不同的 source 和不同的 destination,对象本身可以重复地使用。

>>> 第二,组合优于继承的设计思想。

通过组合的方法,可以在运行时构建一个灵活的解决方案,就像我们刚才看到的那样,最终的 mover 是通过不同的 mover 和 comparator 组合在一起的;

相对地,如果要用继承实现的话,在定义类的时候就要定义清楚它继承的是哪个类,比较死板。

即使我们可以通过 super 这种方法去重用父类的逻辑,但是我们仍然要在运行时之前就规定好所有逻辑,很难构建一个动态的方案,更不用说根据视频热度来决定使用哪一种组件这种灵活的方式了。

>>> 第三,接口驱动设计的设计思路。

我们可以规定相同的接口,但是不同的实现,正如 FileMover 和 DBMover 那样。这样理解起来就会容易很多,因为他们的接口都是同一个,而底层的实现却很不一样,一个是操纵底层的文件,一个是操纵数据库。也就是说,这个接口的深度是动态变化的,我们可以通过 ConcatMover 和 CompareAterMover 将不同的 mover 串联、并联在一起,让接口的深度在运行时动态地变化。

这样一来,我们在开发和测试的环境中,mover 接口的深度比较浅,容易理解、测试、除错。而在生产环境中组件的组合层次可以有任意多层,达到扩展接口深度的目的。

最后我们可以通过 mock 来测试接口的调用,提供一个 ok mover 或者 error mover 就可以很简单地测试数据迁移是否成功。

>>> 第四,同样的接口驱动设计思路可以应用到微服务的设计上。

Tubi Mulmedia Processing Platform (TMPP) 就是这样一个例子。

我们规定了 controller, parsing, transcording, packaging, breakfinding, checking 这些不同的微服务以及他们之间的接口,最重要的是 controller 和这些不同的 stage 之间的接口。这样我们可以很容易地增加新的 stage,或者对不同的stage分别进行优化。只要它们和 controller 之间的接口不发生改变,整个的 TMPP 服务都可以保持正常运转。我们就可以将 TMPP 做的越来越好,并且将复杂程度拆分到不同的微服务上。

以上就是我今天的分享,主要希望大家可以了解到我们如何通过组合的方式在 Elixir 或 Ruby 中将复杂的业务逻辑拆分到不同的子模块中。同时,能将这种的思路应用到微服务上。

很遗憾今年的 RubyConf China 又是在线上进行,希望明年我们可以在线下见面,一起吃自助餐,同时讨论 Elixir 或 Ruby 里这些有趣的项目设计。

作者:Yiming CHEN,Tubi Senior Tech Lead


欢迎加入 Tubi TMPP 团队!

更多推荐

Ruby 思想在 Elixir 项目中的应用

本文发布于:2024-02-07 07:08:03,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1754455.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:思想   项目   Ruby   Elixir

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!