Tubi 是一个流媒体服务平台,有成千上万的用户每天通过这个平台免费观看他们喜爱的电影、电视剧。每当一位用户按下「播放」按钮,Tubi 的视频播放器就会向一个内部服务发送 API 请求以获取视频的 manifest 文件。这个内部服务会从 AWS S3 抓取 manifest 文件,并基于当前的请求参数对文件内容做一定的修改,最终将修改好的文件返回给视频播放器。

显然,这个 API 请求对用户的观看体验来说是至关重要的,我们希望保证这个 API 的性能。根据我们的内部数据,这个 API 服务的 P99 一直在 400 毫秒上下浮动。由于对文件的修改逻辑相对简单,我们怀疑这个 API 的性能瓶颈是发向 AWS S3 的网络请求(这个假设也被我们在本地开发机上的 benchmark 验证过了)。

因此,我们决定给这个服务加上一层 Redis Cache。

但是,当我们部署好 Redis,并为 50% 的访问流量启用了 Redis Cache 之后,P99 并没有如我们期望的那样降低,反而还升高了 100 毫秒。这显然不符合我们的预期,于是我们开始进一步研究。最终,我们修复了一个潜藏在 Elixir 代码库里 7 年的性能问题,将 API 的 P99 从 500 毫秒减少到了 10 毫秒。

这篇文章,就是对这一次有趣的 debug 经历的总结。从中你能了解到我们逐步定位到问题的过程、所用的工具、以及最终的解决方案。

深入调查

一开始,我们以为是 CPU 使用率的升高导致了 P99 升高。因为在我们 Cache 实现里,缓存的内容会在存储/读取时进行压缩/解压操作,以减少 Redis 的存储开销。这里我们在写入 Redis 时用到了 Erlang 的 :erlang.term_to_binary/2 函数,并传入 :compressed 参数进行压缩;在读取 Redis 时使用 :erlang.binary_to_term/1 自动解压。这个小技巧我们在之前的服务中就曾成功使用过。这些操作理论上是会消耗更多的 CPU 资源。而且我们也确实观察到 CPU 的使用率在启用 Cache 后从 ~30% 升高到了 ~40%。但是即使有 10% 的 CPU 损耗,以 BEAM 虚拟机强大的并发能力,我们依然不相信这就是导致 P99 升高 100 毫秒的原因。因此,我们继续探索其他的原因。

接下来,我们注意到一个很特别的现象:虽然请求延迟的 P99 和平均值增大了,但是延迟的中位数(P50)却减小了。这意味着在启用 Cache 之后,之前本就能快速处理的请求变快了,而之前处理速度慢的请求变得更慢了。这意味着请求 AWS S3 可能并不是我们服务的瓶颈,而是别的环节。

为了定位这个隐藏着的瓶颈,我们又做了一次测量分析。这一次,我们在有着真实请求的生产服务器上跑了一次eprof[1]。以下是这次测量的结果:

从这个结果我们可以看出:有 89.60% 的 CPU 时间都花在了这个 Regex.precompile_replacement/1 函数上。而且在每个 Erlang 进程的分析结果中, Regex.precompile_replacement/1 总是花费 CPU 时间最多的那个函数,占比从 30%~90% 不等。研究 Elixir 的 Regex 模块之后,我们发现这个函数只在 Regex.replace/4 里调用,而这也正是我们用来更新 manifest 文件内容的函数。

至此,P99 延迟和平均延迟升高,但中位数降低的谜团终于解开了:对某些不需要更新 manifest 文件内容的请求,它们并不用调用 Regex.replace/4 函数,因此 S3 的网络请求确实是它们的性能瓶颈。启用 Redis Cache 能够提高这些「快」请求的性能,降低延迟。而对某些需要更新 manifest 文件内容的请求,更新文件内容的时间远高于 S3 网络请求的时间,因此它们的性能瓶颈实则是对文件内容的更新操作。而启用 Redis Cache 导致了更新一些大文件内容的性能变慢了 100 毫秒,比 AWS S3 网络请求的几十毫秒多了一个量级,因此导致了 P99 和平均延迟的升高。

追根溯源

确定了 Regex.replace/4 正是慢请求的瓶颈之后,我们又开始了新一轮的优化。此时的我们还不知道,真正的性能问题藏在 Elixir 的 Regex 模块里。出于对 Elixir 项目本身的信任,我们以为 Regex.replace/4 的调用本身就是一个耗时的操作。因此,我们还只是打算通过其他的方式重写修改 manifest 文件内容的逻辑,避免调用 Regex.replace/4,以此达到优化性能的目的。

我们首先用Benchee [2] 配置了一个 benchmark 脚本。这个脚本会用不同版本的实现去处理同一个 manifest 大文件,对比判断不同版本的性能优化效果。原始的版本 P99 大约为 100 毫秒。但巧合的是,我们第一次选取的 manifest 大文件中并没有任何需要替换更新的内容。按理说我们只需要遍历读取一个 40KB 的文本文件,不应该花费 100 毫秒。因此,一个很自然的优化就是在调用 Regex.replace/4 之前先调用 Regex.match?/2 确认是否存在需要替换的内容。仅仅应用了这个优化,我们就将这个函数的性能提高了几乎 100 毫秒。而对那些约有 50% 内容需要替换的 manifest 文件,性能提高了大约 50 毫秒。

至此,我们的优化之旅就算告一段落了。我们本打算用更复杂的解决方案(比如nimble_parsec [3]) 来解析、更新 manifest 文件。但是一个简单的 Regex.match?/2 检查就让这个 API 的性能达到了我们的预期,这些复杂的方案也就显得有点得不偿失了。

但还是有一些问题困扰着我们:为什么 Elixir 的 Regex 模块没有自带这个显而易见的优化呢?Erlang re 模块中的 :re.replace 函数会有类似的问题吗?于是我们又用 :re.replace 将相同的逻辑实现了一遍,其结果和带有 Regex.match?/2 优化的 Elixir 版本相同。也就是说 Elixir 的 Regex.replace/4 函数确实存在着性能问题,在不需要替换内容时会花费不必要的 CPU 时间。

研究了 Elixir 源码之后,我们发现 Elixir 确实检查了是否存在需要替换的内容。但是在检查之前,Elixir 又调用了 Regex.precompile_replacement/1[4] 这个函数,导致了不必要的 CPU 资源浪费。这跟我们使用 eprof 测量的结果是一致的。

而 Erlang 的 re 模块则没有类似 precompile_replacement 的逻辑 [5]。在没有需要替换的内容时,:re.replace 就不会做任何额外的操作消耗 CPU 时间。

在确认了这确实是一个 Elixir 的 bug 之后,我们马上提交了一个 PR 修复这个问题:Speed up Regex.replace/4 when there is no match by dsdshcym · Pull Request #10500 · elixir-lang/elixir [6].

(注意到这个 PR 在 10 分钟之内就被 merge 了。)

总结回顾

虽然最后的解决方案非常简单,但我们依然从这次有趣的 debug 经历中学到了很多:

  1. 在真实环境下用真实的数据测试得到的结果才是最可靠的我们最初测试这个 API 的性能时用的是我的 MacBook Pro。结果确实印证了我们一开始的假设:AWS S3 的网络请求是瓶颈。但是生产环境的情况和我本地的情况完全不同。如果我们没有在真实的服务器上跑 eprof 测试,我们很难发现性能问题的真正瓶颈。

  2. 真正的瓶颈往往要用最大的数据来测试才能发现我在本地测试时犯的第二个错误是使用了一个简单的 manifest 文件测试。而正如我们在 debug 过程中发现的:其实有两类请求,一类是 CPU 密集型,而另一类才是 I/O 密集型。真正的瓶颈藏在 CPU 密集型请求(复杂大文件)中。后期使用大文件配合 Benchee 来测试帮助我们迅速找到了问题所在。

  3. 贡献回上游的开源社区回到这个略显“标题党”的题目来说,这个 Regex.replace/4 的性能问题在这个函数被引进之初就存在了,也就是 7 年之前。我们也好奇为什么这个问题一直没有被修复。我们的猜测是还没有人在这样一个量级调用过 Regex.replace/4:在一次 API 请求周期里反复调用上千次。这就更说明了将这些发现、修复贡献回上游开源社区的重要性:其他人今后就不会被相同的问题影响到。而且,向 Elixir 贡献 PR 可能是一个开发者能享受到的最好的开源体验:10 分钟不到,你的代码就被合并进主干分支了。

如果使用 Elixir 去处理上千 QPS 的业务逻辑让你心动了,那就赶快加入 Tubi 吧!在 Tubi,你将和我们一起使用 Elixir 去保证千万用户的在线视频体验!

作者:Yiming Chen, Tubi Senior Tech Lead


加入 Tubi 和 Yiming 做同事吧!

一个潜藏在 Elixir 代码库里 7 年的性能问题相关推荐

  1. 微软打造了全球最大的Git代码库

    丹棱君有话说:今年 2 月,微软宣布将用 Git 管理 Windows 源代码.随后,Visual Studio 宣布开发 "Git 虚拟文件系统(GVFS)",并将在终极项目和超 ...

  2. 丰富自己的代码库-快速排序

    一些常用的代码,和demo一定要保留,对于一个程序员的成长就是不断 学习,实践,积累,除了少数的天才外, 绝大多数程序员的发展都要经历的几步,这里重点说一下积累,看到一些好的代码活着功能复杂的算法,方 ...

  3. All-In-One Code Framework [一站式示例代码库] 【转】

    All-In-One Code Framework [一站式示例代码库] 2010 对一站式示例代码库,对奋战在一站式示例代码库上的每一位工程师来说都是不同寻常的一年. 在我们共同努力和开发社区的支持 ...

  4. c v语言 小数后20位,V语言学习笔记-30集成C代码库

    集成C代码库 优势 V的代码库很多都直接调用C标准库函数来实现,对C标准库的依赖还是很重的 由于V代码编译后生成的是C代码,然后再调用C编译器编译成可执行文件 这样的机制决定了V语言可以很方便地调用C ...

  5. Nx 介绍: 基于插件的单一代码库(Monorepo)构建系统

    文章目录 前言 一.Nx 设计理念 二.Nx 核心概念 1. 项目图 - Project graph 2.元数据驱动 - Metadata driven 3. 任务图 - Task graph 4.受 ...

  6. 【PySlowFast】Facebook开源算法代码库PySlowFast,轻松复现前沿视频理解模型

    关注上方"深度学习技术前沿",选择"星标公众号", 资源干货,第一时间送达! 在近些年的视频理解研究中,Facebook AI Research 贡献了许多精彩 ...

  7. Facebook开源算法代码库,轻松复现前沿视频理解模型

    在近些年的视频理解研究中,Facebook AI Research 贡献了许多精彩的工作.近日,FAIR视频团队在 ICCV 相关研讨会上开源了视频识别检测代码库 PySlowFast,并同时发布了预 ...

  8. 多少个没收到会收敛_三分历史纪录2973个,库里2483个,库里生涯结束三分会是多少个?...

    小球时代的"主旋律"是攻防转换的速度,还是位置分化的模糊,或者是中锋的凋零?都不是,小球时代的主旋律,是三分球. 三分球从21世纪的第二个10年开始,在各支球队进攻中扮演的角色越来 ...

  9. php项目数据库控制器代码_如何为大型代码库组织Express控制器

    php项目数据库控制器代码 by Alexandre Levacher 亚历山大·莱瓦彻(Alexandre Levacher) 如何为大型代码库组织Express控制器 (How To Organi ...

最新文章

  1. python源码精要(2)-C代码规范
  2. Tomcat新版本旧版本下载(Windows和Linux)
  3. bzoj 1687: [Usaco2005 Open]Navigating the City 城市交通(BFS)
  4. ubuntu添加PPA(个人软件包)源
  5. 第十五周项目3-在OJ上玩指针
  6. 经过事件还是箭头 html,箭头函数不合适什么场景?
  7. 空间留言软件_锦州教育智慧云平台登录个人空间
  8. 打印机上的一款驱动-惠普LaserJet1020Plus打印机驱动提供下载
  9. 李永乐2021线代讲义练习题答案
  10. 爱看小说程序源码+4W条数据全站打包
  11. Android常用布局-02
  12. python查询12306余票_使用 Python 在 12306 查询火车票余票
  13. PS调色类插件哪家强
  14. cpu平均负载高的几种情况
  15. 海门开发区机器人项目_点赞!海门“经洽会”现场签约10亿元以上项目21个
  16. HTML5 案例学习笔记
  17. HTML、CSS、JavaScript学习总结
  18. 面试问题记录 三 (JavaWeb、JavaEE)
  19. SpringCloud2020学习笔记13——SpringCloud Stream消息驱动
  20. 计算机网络mtu值设置,应该如何设置mtu值才可以让网速达到最快-电脑自学网

热门文章

  1. git idea 如何删除本地分支_git删除本地分支和删除远程分支
  2. 制作Appstore预览视频并上传
  3. 快速制作一张炫酷可视化报表
  4. 【机器人操作系统(ROS)中的机械臂仿真】
  5. LR安装遇到 Cannot save the license information because access to the registry is denied
  6. 故障频出的摩拜单车,背后是野蛮生长的原罪
  7. spring aop拦截自定义注解的切入点表达式
  8. iOS11以上版本和cocoapods版本不匹配问题
  9. 【Python】**kwargs
  10. Centos 7 crontab重启命令