Elixir 应用优化之旅
摘要
一位开发者分享了优化 Elixir 应用的经验与教训,重点介绍了针对 Postgres 连接池工具 Ultravisor 的性能改进。文章涵盖了使用火焰图、调用追踪等性能分析技术,以及 eFlambè 和 tprof 等工具。
<p><a href="https://lobste.rs/s/7mqzck/journey_optimising_elixir_application">评论</a></p>
查看缓存全文
缓存时间: 2026/04/21 02:58
# Hauleth 的博客 - Scotty,我需要在三分钟内达到曲速
来源:https://hauleth.dev/post/things-about-elixir-you-probably-will-never-need/
在我上一份较大的工作中,我参与了一个非常迷人的项目——用 Elixir 编写的 Postgres 连接池(https://github.com/supabase/supavisor)。不幸的是,由于各种原因,这个项目让我彻底精疲力竭。然而,那些没有击垮你的破事(毕竟也不是什么致命武器)反而能成为宝贵的学习经历。
我在这个项目中的大部分成就都与性能有关。这个项目包含了一个**非常**紧凑的循环,以查询处理器的形式存在,需要每秒在每个用户连接上运行数十万次。这意味着这些函数*非常*敏感,哪怕最轻微的性能变化都会影响到它。而这就是我的任务——寻找潜在的改进空间,让代码库运行得更快。
在离开 Supabase 之后,我非常喜欢这个项目(主要是作为学习平台),于是创建了自己的分支。在那里,不受项目所有业务方面的束缚,我可以纯粹专注于尽可能压榨性能。这个项目现在以 Ultravisor(https://github.com/Ultravisor/ultravisor)的形式存在——它离我希望的完成状态还很远,但我仍会时不时回去继续研究,寻找潜在的性能优化点。
这是关于我在那段旅程中所做所学的故事。
> **注意**:这是一篇回顾,所以在某些地方我的记忆可能不是最准确的。
这里我需要先做一些解释,说明 Ultravisor 如何处理数据库连接。它提供了两种运行模式:
- `session`(会话模式)——用户到 Ultravisor 的每个连接都会从 Ultravisor 到数据库的连接池中获取一个连接。它在连接开始时获取一次,然后一直保持到连接结束;
- `transaction`(事务模式)——连接建立时什么都不做。客户端连接到 Ultravisor 后可以无限期保持该连接,而完全不会打扰数据库。只有当用户发出请求时才会*获取*数据库连接,并且一旦查询结果返回、数据库准备好处理下一个请求时,连接就会立即归还到连接池。
虽然 `session` 模式与其他 Postgres 连接池实现不相上下,但 `transaction` 模式才是性能不足的地方,也是主要的优化重点。在整篇文章中(除非另有说明),我将讨论 Ultravisor 的 `transaction` 模式。
## 教训:火焰图和调用追踪必不可少(https://hauleth.dev/post/things-about-elixir-you-probably-will-never-need/#lesson-flame-graphs-and-call-tracing-is-essential)
这很明显,但对任何性能优化工作来说仍是宝贵的经验。这要非常感谢 Trevor Brown(https://github.com/Stratus3D)以及他出色的项目 eFlambè(https://github.com/Stratus3D/eflambe)。它在追踪运行代码中的热点时帮了大忙。
不幸的是,这个项目最近似乎不太活跃,并且缺少一些功能,比如按给定持续时间而非函数调用次数进行监听(https://github.com/Stratus3D/eflambe/issues/48)。这可以通过简单地监听给定次数的 `handle_event/4` 函数调用,然后运行 `cat \*.bggg` 将所有文件拼接成更大的追踪文件来部分解决。这有缺点,但至少可以在 Speedoscope(https://speedoscope.app/)中使用,我也强烈推荐给任何需要进行此类优化的人。
虽然火焰图很强大,但用 eFlambè 收集它们是有代价的——它会严重影响性能。幸运的是,Erlang 有一些内置工具对性能的影响更小,其中"最现代"的是 `tprof`(https://erlang.org/doc/man/tprof.html)。这个工具非常容易使用,但不如 eFlambè 详细。不过即使有这个限制,它也能对影响性能最大的部分提供极佳的洞察,并且由于它是异步工作的,所以更容易处理长时间运行的进程,你可以"手动"决定想要追踪进程多长时间。
**总结:** 知道瓶颈在哪里对性能优化至关重要。
## 教训:少做一点可以提升性能(https://hauleth.dev/post/things-about-elixir-you-probably-will-never-need/#lesson-doing-less-can-improve-performance)
有个显而易见的道理需要说明——什么都不做比做点什么更快。使用 `:inet.getstat/2` 调用提取给定 socket 上发送的数据量很快
相似文章
使用 Egg 编写 SQL 优化器(2023)
本文介绍了如何使用 Rust 中的 Egg 框架构建 SQL 优化器,涵盖常量折叠、谓词下推、连接重排序等技术,并采用基于规则和基于成本的优化,代码量不到一千行。
我(至今)在使用 Elixir 和 Swift 构建在线小游戏中学到的东西
一位开发者反思了使用 Elixir 与 Phoenix 以及 Swift 与 SpriteKit 构建在线小游戏应用 Migo Games 的经历,强调了 AI 编码辅助的作用以及 Elixir 进程模型的可扩展性优势。
为变更优化,而非应用性能
本文指出,软件团队常常过度优化微性能基准测试,却牺牲了开发者体验和工程吞吐量,而这两者才是长期交付速度与可维护性的真正瓶颈。
为Bluesky DataPlane选择Elixir:我们未曾预料的抉择
这篇博客文章详细说明了为什么团队选择Elixir而不是Go、Rust或Node来构建高性能的Bluesky DataPlane,利用BEAM的并发性处理热路径,并将计算密集型操作卸载到Rust NIF上。
未充分利用的性能
一篇技术博客文章,演示了如何通过LLVM的配置文件引导优化(PGO)在标准-O3和LTO之外显著提升二进制性能,以SQLite作为基准测试。