作者维 多利亚-梅尔尼科娃,Evil Martians的商业开发部和奥尔加-鲁萨科娃,Evil Martians的作家
我们已经讲述了关于我们如何创建FEED社交移动应用程序的故事。而在这篇文章中,我们决定揭示一下由于这个项目我们能够深入研究的技术--从Swift和TimescaleDB到最新的机器学习发现和其他复杂的前沿解决方案。
全面的方法
像往常一样,FEED项目从一个有针对性的设计冲刺开始。我们和FEED的创意团队一起设计了应用程序的主要屏幕的第一个版本,并从一开始就为设计带来了一致性。
设计概念
FEED的创意团队带来了他们自己的设计概念,包括所有的主要屏幕和关键机制的细节。他们决定该应用程序将以用户的兴趣为中心,以视频为基础。他们塑造了整体的主题、主要的颜色和界面的氛围,围绕着休闲、娱乐、俱乐部等主题,并以明亮的颜色为特色。
我们与FEED的创意团队合作,改进了iOS的界面模式、设计一致性和导航方案。我们一起融入了他们的颜色,试图抓住正确的氛围,组装了第一个版本,并在此过程中想出了几个更有趣的东西。例如,用户在观看事件提要时,可以对事件卡应用iOS的捏合到放大的手势,看到该事件与其他事件的关系。
我们在设计导航系统时准备的第一张草图
该系统
为了给设计带来一致性,FEED和Evil Martians的设计师使用组件组装了所有的布局,并在各处重复使用,以创造一个统一的体验。所有的图标都有相同的边框,可以互换。FEED团队还创建了一个单一的调色板和文本风格,我们帮助他们简化了所有的屏幕。后来,FEED的设计师们改变了用户界面的颜色,使整个主题更加平静和方便。
这种合作助长了一些关于系统性方法的想法:例如,一个屏幕模板表明了iOS安全区域的位置,以及一个在每个屏幕上显示视频卡行为的模板(因为视频有固定比例,必须正确缩放)。
iOS安全区域
移动开发
在移动领域,用户需要能够处理媒体、视频和声音,并且能够通过3G和LTE下载所有的媒体,使用高效率的格式来保证视频质量,同时控制数据大小。在实际使用SwiftUI时,我们很有眼光,只在最适合的地方使用它。我们还在整个应用程序中采用了一些清洁架构的想法,包括SwiftUI和UIKit组件。
我们对我们帮助创建的复杂和先进的功能水平感到相当自豪。让我们回顾一下其中的一些功能。
视频合成器
这个视频制作功能只是该应用程序的关键部分之一,它本来可以成为一个成熟的、独立的市场产品。它允许用户录制视频并对视频片段进行大量的操作:添加各种过滤器和重新录制。视频合成器是该应用最复杂的功能之一,我们在其工程上投入了大量的精力--进行了许多优化和用户行为研究。
视频合成器
电影
电影功能是一个集合了出现在某个feed中的最受欢迎的视频。我们实现了对视频的统计--我们需要看到浏览量、跳过率、重播次数等等。然后,根据这些数据,我们能够确定最受欢迎的视频,并编写一个六分钟的影片,用户可以与朋友分享。
创建影片
入职培训
作为用户入职过程的一部分,我们建立了注册、登录、电话号码输入和国际电话代码选择器等屏幕。我们还实现了注册状态的持久性,当用户关闭和打开应用程序时,保存和恢复点,以便从同一个地方继续注册过程。
注册和入职过程
饲料导航
这个功能允许用户在一个Feed中的不同视频或由该Feed制作的影片之间进行切换。
应用程序的后端
我们涵盖了FEED的所有API和应用程序在后端的架构。在这里,我们继续植入模块化的架构(又称Rails引擎)。所有的业务逻辑都分布在关键领域:核心、授权、事件、聊天、机器学习集成和管理仪表板。它们之间的通信是通过事件驱动架构,使用发布-订阅模式来减少组件的耦合。
后台需要大量的应用程序和数据库优化,为即将到来的高负载做准备,并组织增量的扩展。
将视频压缩到最后一个字节的性能
我们被赋予了一个有趣的后端技术任务,以实现定制的6分钟的用户影片:我们需要应用程序从不同的视频剪辑中组成,并将覆盖物自然嵌入视频中。此外,我们需要在视频之间应用效果和过渡。这是一个耗时的操作:影片编纂可能需要长达30分钟。在使用FFmpeg建立自动编译功能时,我们尝试了大量不同的编解码器和预设,然后选择了支持Android和iOS的完美组合,并提供完美的压缩视频文件。
我们对视频尺寸进行了实验,并找到了一个合适的组合,使我们在最小的文件尺寸下获得最佳的分辨率。首先,我们选择了具有最佳文件大小的全高清H.265(HEVC)。但它并不符合技术要求,而且压缩视频的速度很慢。因此,我们选择了960x540的H.264,以获得更好的兼容性,并做了大量的微调,以获得高质量,同时保持较小的文件大小。
我们增加了一个流程,提前创建尺寸小、分辨率降低的视频,这样连接不好的用户也可以观看。
到处都是API
任何移动应用程序都必须与后端数十个API打交道,因此移动后端开发的很大一部分是由实现这些API组成的。例如,用户需要它们来选择他们的基本兴趣。使用Interests API,我们根据这些兴趣在主馈送中显示多达70%的内容;用户即使在刚登录的时候也能立即看到与他们相关的内容。用户可以通过书签API将饲料或电影添加到他们的 "收藏夹",以便以后观看。通过查看影片的API,用户可以在他们的档案中监控他们还没有看过的影片。
在这一过程中,我们还必须解决API的各种问题。例如,通过日志的API,让用户在他们的档案中看到其他用户的事件。最棘手的问题是划分时间和对用户进行分组。由于世界各地时区的参与者以不同的频率贡献内容,有时我们需要在一周,甚至一个月的范围内编纂日志。此外,我们还需要考虑观看事件的用户所在的时区--一个时区的下午可能是另一个时区的第二天。因此,我们创建了一个复杂的SQL查询,其中有许多分析性的汇总,尽管其规模很大,但工作速度很快。
另一项复杂的任务是通知用户他们的朋友已经在应用程序中注册,并显示建议关注他们。为此,我们需要将用户手机中的联系人上传到服务器上(当然要经过他们的同意)。这需要一些API工作,因为我们必须同时提供隐私和安全,比如包括禁止在其他人的联系人中搜索电话号码的选项。
TimescaleDB
为了根据事件浏览量来估计用户评分,我们使用TimescaleDB来计算一个用户在过去两周内创建了多少个事件。此前,我们使用的是每个事件的每周浏览量的滚动,这个功能已经内置于Timescale中。为了计算每个事件的整个时间的统计数据,我们需要把它收到浏览量的所有星期加起来。一般来说,这对时间序列数据库来说是一个非典型的任务。但我们迁移到了新的Timescale 2.0,可以根据用户行动功能编写我们自己的卷积,内置的调度系统根据特定的时间表启动。
弹性搜索(Elasticsearch
主feed中的排名算法是一个关键的功能,所以我们在这方面做了大量的工作,我们也对其进行了几次修改,以确保实施质量。主要的挑战是显示一个同样由新的、流行的和针对用户兴趣的内容组成的feed。为了扩大feed的范围,我们还需要15%的feed来自于不在用户兴趣列表中的主题。
我们已经在这个项目中使用Elasticsearch来搜索feed和用户,我们当时决定使用它的高级功能--Learning to Rank插件来过滤/排名搜索结果。
我们还创建了一个更先进的公式来平等地推广新鲜和流行的饲料。我们增加了探索页面的大小,以适应多达576个feeds,这样新鲜或流行的内容就有机会获得更多的动力。
机器学习
在这个项目中,我们建立了主要的机器学习系统--使用Pytorch、OpenCV、ONNX、Core ML和其他库和工具。这使我们能够实现更多很酷、更智能、更复杂的功能,同时,产品对用户来说变得更顺畅、更智能、更安全。这里只是其中的几个。
口语识别和分类
这个功能至关重要,因为核心团队设计了一个有趣的广告策略:根据对话的主题,在应用程序的流程中自然地嵌入目标受众的广告(例如,如果人们正在谈论某种饮料,我们将在饲料中显示一个相应的广告)。
为了确定人们在公共对话中谈论的内容(所有私人内容都被排除在这个过程之外),我们需要按照某些话题对言论进行分类,并突出关键词。由于我们首先计划在大型市场推出,我们最初的语言是俄语和英语(后来,我们将增加更多语言)。
我们在Mozilla的一个开放的多语言数据集上建立了这个解决方案。我们在其小的音频片断上训练口语分类器--所产生的模型出来的速度很快,所以即使在生产环境中,我们也不需要有GPU的服务器来处理数据。
语言识别之后,我们需要获得文本转录。我们使用了Vosk--一个在Apache2许可下提供的闪电般的开源语音转文字系统,这使我们可以将软件用于业务开发。他们预先训练好的模型适用于不同的语言,包括我们需要的语言。
之后,我们需要根据主题对文本进行分类--无论是IT、购物,还是饮料。这是一个复杂的问题,因为只有少数数据集被准备用来训练这种模型。你可以很容易地找到一个俄语数据集,因为他们有一个非常强大的数据科学社区来处理这类文本,但对于其他语言来说,要做到这一点并不容易。
由于使用了这么多语言,我们有很多复杂的模型,难以训练和维护。我们最终使用了Facebook的开源LASER库,它将文本翻译成嵌入向量(它将文本表示为N维空间的向量,因此不同语言的相同文本在该空间中会非常接近)。他们的模型将文本转化为多维向量,这对多语言分类器来说是完美的。他们支持98种语言,而且它在定义小块文本方面相当出色由于我们有15秒的视频文件,需要将其归入一百个类别中,所以任务很简单。我们为我们的工程改造了这个库:我们写了代码,包装了它,简化了它,并最终得到了适当的解决方案。
NSFW识别
我们不想在应用程序中处理任何 "对工作不安全 "的内容,所以我们使用了一个神经网络来提前判断。我们设法重用了人脸识别管道,它快速处理了视频流,切割了帧,缩小了它们的尺寸,进行了转换,并将其集成到网络中,以检查内容。如果一切合法,我们会将视频标记为 "已检查"。同时,它将把可疑的内容发送给版主。如果神经网络相当确定存在限制性内容,它就会阻止视频,将其从流中隐藏起来,标记用户,并向审核小组发出紧急请求,以检查相应的用户和视频。
音频去噪
我们采用了一个先进的C语言库Rnoise,用于音频去噪,并将其内置到我们的流程中,因为它非常小而且性能非常好。
优化
我们创建的机器学习系统是可自动再训练的、可部署的和可扩展的。我们 "压缩 "了模型的大小,以便在视频内容处理方面有足够的质量和速度,而且不需要显卡--用4个处理器运行几个小型的Kubernetes pods就足以处理一切。
为了在我们的服务中使用这些模型,我们首先将它们包装在一个网络服务中。然而,我们很快意识到,通过网络发送的数据太多了。然后我们决定使用Faktory,在一个强大的引擎上作为后台任务运行模型。这是一个类似于Rails世界中使用的Sidekick的后台任务处理产品,它是由同一个作者建立的。它与我们的Rails代码和Python编写的机器学习功能完美契合。
ML系统的可扩展性
Faktory让我们把所有东西都包在一个Docker容器里,然后部署到Kubernetes集群上。我们因此获得了可扩展性,因为我们可以通过增加pod的数量来创建我们需要的工人。而且,由于FEED应用在谷歌云上运行,我们还可以利用更多的资源进行工作,因此,我们可以并行处理大量的视频。
再培训
我们将收集到的所有数据保存到数据库中(用于重新识别的人脸向量、中间训练结果等)。每周一次,数据被收集到CSV(逗号分隔的数值)文件中,上传到谷歌云存储(GCS),我们在Faktory工作者中运行一个再训练任务。来自GCS的文件被放到训练堆栈中(这与生产堆栈使用的技术不同:后者使用权重,以ONNX格式压缩,训练堆栈运行模型训练PyTorch框架)。
训练堆栈下载数据,并检查它是否有变化(因为一些数据被交给了主持人来审查正确的分类)。然后我们训练模型,并使用最终验证指标来检查它们与这个模型以前的版本的性能。我们将其与之前的进行比较,只有在指标得到改善时才将更新的模型加载到S3。我们用升高的模型启动了连续的容器。
基础设施优化和部署
下一个阶段是项目的生产。显然,像视频社交网络这样的应用程序在处理能力需求方面容易出现极端的波动,而且它还必须是可扩展的--在自动扩展🤖。因此,我们必须使用云基础设施,还有什么比使用Kubernetes更好、更可控的方式呢?我们选择了谷歌云平台(GCP)的云和其管理的谷歌Kubernetes引擎(GKE)服务。
除了管理的数据库,所有东西都在Kubernetes上部署和运行。
Kubernetes是一个复杂的平台,需要深厚的实践经验。虽然GitOps并非没有缺点,但它仍然是管理基础设施的较好方法之一。我们依靠Terragrunt来管理GCP本身的对象。对于管理Kubernetes集群的对象,我们使用ArgoCD设置。我们默认的ArgoCD设置带来了一个项目可能需要的所有基本要素:一个精心打磨的Prometheus监控设置(它也准备处理应用程序监控)、日志聚合、几个Ingress控制器和一些操作员。
负载测试的负荷
有趣的挑战是负载测试。这里有三个子挑战:前面提到的视频处理,应用程序的压力测试,以及处理网络延迟。我们之前提到过自动缩放吗?这里是它真正有机会发光的地方!这是准备和测试应用程序的可扩展性的完美时刻。谷歌云可以扩大和减少你的节点池,但它不能对你的应用程序的Pod做同样的事情。由于Kubernetes的Horizontal Pod Autoscaling,它可以在眨眼间处理这个问题。尽管如此,还是需要一个真正的工程师(在我们的案例中是火星人)来决定什么指标是最值得依赖的。
顺便说一下,你是否意识到,如果你不是作为一个 "广为人知 "的人或服务来运作,你可能--从字面上看--遇到限制?云供应商对不同种类的云资源有实际配额:节点、CPU、内存、存储等。这些不仅因地区而异,而且因客户而异。这就是为什么我们建议在负载测试和实际可扩展性测试达到配额的情况下要小心,因为谷歌可以在默认配额增加程序中拒绝你。例如,我们与谷歌云团队进行了多次长时间的视频通话谈判,以验证我们为什么需要这么多节点。
云计算供应商有很多节点类型的选择,其中很多是共享的,所以你总是有可能不时地从一个虚拟节点上得不到全部性能。但我们必须确定我们的视频处理时间是可控的,而且是快速的,必须在不像火箭燃料那样烧钱的情况下做到这一点。我们做了一些压力测试,以确定哪种配置是最好的,并且是基于预算的最可预测的。
一旦项目开始进入预发布状态,我们就在Grafana实验室的k6帮助下开始了压力测试过程。这样做是为了确保我们的应用程序和我们建立的基础设施能够应对新用户的涌入。初次发布的目标是处理至少300,000 RPM。这样的测试没有银弹,但我们投入了大量的时间来创建一个现实的用户行为档案来产生负载。
这时,网络延迟和更多的调整出现了。FEED是一个社交应用,所以它的要求很高。我们注意到一些PostgreSQL和Redis的请求比预期的要花更多的时间来完成。在这里,我们不得不深入研究代码,并对不同节点类型之间的网络速度和延迟进行彻底的基准测试。事实证明,在两个方向上都有改进的余地。
解决数据库的问题
在这个过程中,我们还碰到了一个成本与价值的问题。TimescaleDB。它实际上是我们在Kubernetes内托管的唯一数据库。为什么会这样呢?快速的答案是,TimescaleDB的管理版本对于我们的负载情况来说太昂贵了。更长和更有趣的答案涉及到讨论TimescaleDB的管理版本是如何配置的。它的连接数非常有限,即使有相当大的节点类型,但我们的项目已经准备好在任何时候运行数以百计的节点和Faktory工作者。因为我们是如何处理TimescaleDB数据的,我们需要数百个连接,而不是数百GB的内存。因此,我们决定将TimescaleDB托管给我们自己。结果发现TimescaleDB团队做得很好,他们制作并发布了一个Helm图表,就是为了这个目的--为他们点赞!
这个项目对我们的团队来说是又一次很好的学习经历。我们相信,它再次证明了--只要有才华的开发者和集中的努力--即使一个项目利用了复杂的技术,也不意味着它必须有一个复杂的或口译的架构,一个困难的和漫长的工程过程,或昂贵的解决方案。由于FEED项目,我们已经表明我们可以快速实施一个解决方案,并考虑到成本效益。
我们所取得的所有成就,加上FEED核心团队的辛勤工作,在一个真正新鲜的概念基础上,创造了最引人注目的社交应用程序之一。到目前为止,FEED已经在35个国家的App Store上推出,包括美国、加拿大和欧洲大部分地区。
如果你想建立一个具有尖端技术的移动应用,你知道在哪里可以找到我们。