Phoenix 200 万 Websocket 连接之路
发布于 2015 年 11 月 3 日,作者 Gary Rennie
如果你最近关注 Twitter,你可能已经注意到一些关于 Phoenix web 框架可以处理的并发连接数量不断增长的数字。这篇文章记录了一些用于执行基准测试的技术。
如何开始
几周前,我试图对连接数量进行基准测试,并在我的本地机器上获得了 1000 个连接。我不相信这个数字,所以我发布到 IRC,看看是否有人对 Phoenix channels 进行过基准测试。结果发现还没有人这样做,但核心团队的一些成员发现我提供的 1000 个数字低得可疑。这就是这段旅程的开始。
如何运行基准测试
服务器
为了对可以同时打开的 websocket 数量进行基准测试,首先需要一个 Phoenix 应用程序来接受这些 socket。对于这些测试,我们使用了一个稍微修改过的版本 chrismccord/phoenix_chat_application,该版本可在 Gazler/phoenix_chat_example 中找到 - 主要区别在于
- 在
after_join
钩子中,广播用户已加入频道的内容已被移除。在测量并发连接时,我们希望限制发送的消息数量。将来会对这方面进行基准测试。
大多数这些测试是在 Rackspace 15 GB I/O v1 上进行的 - 这些机器拥有 15GB 内存和 4 个核心。 Rackspace 友好地让我们免费使用 3 台这样的服务器进行我们的基准测试。他们还让我们使用了一台 OnMetal I/O,它拥有 128GB 内存,并在 htop 中显示了 40 个核心。
您可能需要做的一个额外更改是在 conf/prod.exs
中删除 check_origin
- 这意味着无论使用什么 IP 地址/主机名,都可以连接到应用程序。
要启动服务器,只需 git clone
它并运行
-
MIX_ENV=prod mix deps.get
-
MIX_ENV=prod mix deps.compile
-
MIX_ENV=prod PORT=4000 mix phoenix.server
您可以通过访问 YOUR_IP_ADDRESS:4000
来验证是否正常工作。
客户端
对于运行客户端,我们使用了 Tsung。Tsung 是一款开源的分布式负载测试工具,可以轻松地对 websocket(以及许多其他协议)进行压力测试。
Tsung 在分布式时的工作方式是使用主机名。在我们的示例中,第一台机器名为“phoenix1”,它在 /etc/hosts
中分配了相应的 IP 地址。其他机器“phoenix2”和“phoenix3”也应该在 /etc/hosts
中。
重要的是在与 Phoenix 应用程序不同的机器上运行客户端进行基准测试。如果两者都在同一台机器上运行,结果将无法真实地反映情况。
Tsung 使用 XML 文件进行配置。您可以在 文档 中阅读有关特定值的详细信息。以下是我们使用的配置文件(但数字已降低以反映此处客户端的数量,对于我们更大的测试,我们使用了 43 个客户端)。它以每秒 1000 个连接的速度启动,最多可达到 100,000 个连接。对于每个连接,它会打开一个 websocket,加入“rooms:lobby”主题,然后休眠 30,000 秒。
我们使用了较长的休眠时间,因为我们希望保持连接打开,以查看在所有客户端连接后应用程序的响应速度。我们会手动停止测试,而不是在配置中关闭 websocket(您可以使用 type="disconnect"
来完成此操作)。
<?xml version="1.0"?>
<!DOCTYPE tsung SYSTEM "/user/share/tsung/tsung-1.0.dtd">
<tsung loglevel="debug" version="1.0">
<clients>
<client host="phoenix1" cpu="4" use_controller_vm="false" maxusers="64000" />
<client host="phoenix2" cpu="4" use_controller_vm="false" maxusers="64000" />
<client host="phoenix3" cpu="4" use_controller_vm="false" maxusers="64000" />
</clients>
<servers>
<server host="server_ip_address" port="4000" type="tcp" />
</servers>
<load>
<arrivalphase phase="1" duration="100" unit="second">
<users maxnumber="100000" arrivalrate="1000" unit="second" />
</arrivalphase>
</load>
<options>
<option name="ports_range" min="1025" max="65535"/>
</options>
<sessions>
<session name="websocket" probability="100" type="ts_websocket">
<request>
<websocket type="connect" path="/socket/websocket"></websocket>
</request>
<request subst="true">
<websocket type="message">{"topic":"rooms:lobby", "event":"phx_join", "payload": {"user":"%%ts_user_server:get_unique_id%%"}, "ref":"1"}</websocket>
</request>
<for var="i" from="1" to="1000" incr="1">
<thinktime value="30"/>
</for>
</session>
</sessions>
</tsung>
第一个 1000 个连接
Tsung 在端口 8091
提供一个 Web 界面,可用于监控测试状态。对于这些测试,我们真正感兴趣的唯一图表是并发用户。所以,我第一次在自己的机器上运行 Tsung 时,是在本地运行 Tsung 和 Phoenix 聊天应用程序。这样做时,Tsung 经常会崩溃 - 当这种情况发生时,您将无法看到 Web 界面 - 这意味着没有图表可以显示,但它只是令人印象深刻的 1000 个连接。
再次尝试第一个 1000 个连接!
我远程设置了一台机器,并尝试再次进行基准测试。这次我获得了 1000 个连接,但至少 Tsung 没有崩溃。造成这种情况的原因是系统范围内的资源限制已达到。为了验证这一点,我运行了 ulimit -n
,它返回了 1024
,这解释了为什么我只能获得 1000 个连接。
从那时起,我们一直使用以下配置。此配置将我们带到了 200 万个连接。
sysctl -w fs.file-max=12000500
sysctl -w fs.nr_open=20000500
ulimit -n 20000000
sysctl -w net.ipv4.tcp_mem='10000000 10000000 10000000'
sysctl -w net.ipv4.tcp_rmem='1024 4096 16384'
sysctl -w net.ipv4.tcp_wmem='1024 4096 16384'
sysctl -w net.core.rmem_max=16384
sysctl -w net.core.wmem_max=16384
第一个真正的基准测试
我在 IRC 中谈论 Tsung 时,Chris McCord(Phoenix 的创建者)联系我,让我知道 RackSpace 为我们设置了一些实例供我们用于基准测试。我们开始设置 3 台服务器,并使用以下配置文件:https://gist.github.com/Gazler/c539b7ef443a6ea5a182
在我们启动并运行后,我们专用于一台机器运行 Phoenix,两台机器运行 Tsung。我们的第一个真正的基准测试最终获得了大约 27,000 个连接。
在上图中,图表上有两条线,上面的线标注为“用户”,下面的线标注为“已连接”。用户根据到达率增加。对于大多数这些测试,我们使用的到达率为每秒 1000 个用户。
结果一出来,José Valim 就开始着手处理 此提交
这是我们的第一个改进,也是一个重大的改进。从此,我们获得了大约 50,000 个连接。
观察变化
在我们的第一个改进之后,我们意识到自己是在盲目地进行。如果有一种方法可以让我们观察正在发生的事情就好了。幸运的是,Erlang 附带了 observer,并且可以远程使用它。我们使用来自 https://gist.github.com/pnc/9e957e17d4f9c6c81294 的以下技术来打开远程观察器。
Chris 能够使用 observer 按邮箱大小对进程进行排序。 :timer
进程的邮箱中大约有 40,000 条消息。这是因为 Phoenix 每 30 秒进行一次心跳,以确保客户端仍然处于连接状态。
幸运的是,Cowboy 已经处理了这个问题,所以在进行 此提交 后,结果看起来像
我实际上是在这张图片中使用 observer 杀掉了 pubsub supervisor,这解释了最后 100,000 个连接的下降。这是第二个 2 倍的性能提升。使用 2 台 Tsung 机器,最终获得了 100,000 个并发连接。
我们需要更多机器
上图有两个问题。一个是我们没有达到完整的客户端数量(大约 15,000 个客户端超时),另一个是我们实际上只能为每个 Tsung 客户端(技术上说,每个 IP 地址)生成 40,000 到 60,000 个连接。对于 Chris 和我来说,这还不够好。除非我们可以生成更多负载,否则我们无法真正看到限制。
在这个阶段,RackSpace 给我们提供了 128GB 的机器,所以我们实际上可以再使用一台机器,将如此强大的机器用作 Tsung 客户端,限制在 60,000 个连接可能看起来是一种浪费,但总比让机器闲置好!Chris 和我分别设置了另外 5 台机器,这又增加了 300,000 个可能的连接。
我们再次运行了基准测试,获得了大约 330,000 个已连接客户端。
最大的问题是大约 70,000 个客户端实际上没有连接到机器。我们无法找出原因。可能是硬件问题。我们决定尝试在 128GB 的机器上运行 Phoenix。当然,这样就没有任何问题可以达到我们的连接限制,对吧?
错了。这里的结果与上面的结果几乎相同。Chris 和我当时认为 330,000 个连接已经很不错了。Chris 在 Twitter 上发布了结果,我们就收工了。
我们放弃了最大限度地使用 Channels 的尝试 - 333,000 个客户端。要实现这一点,需要在 8 台服务器上最大化端口使用量,剩余内存 40%。我们没有更多服务器了!
— Chris McCord (@chris_mccord) 2015 年 10 月 24 日
了解您的 ETS 类型
在达到 330,000 个连接并获得了 2 个相当容易的性能提升后,我们不确定是否还能获得同样规模的性能提升。我们错了。当时我并不知道,但我在 VoiceLayer 的同事 Gabi Zuniga (@gabiz) 在周末一直在研究这个问题。他的提交为我们带来了迄今为止最大的性能提升。您可以在 pull request 上看到差异。为了方便起见,我也会在这里提供它。
- ^local = :ets.new(local, [:bag, :named_table, :public,
+ ^local = :ets.new(local, [:duplicate_bag, :named_table, :public,
这 10 个额外的字符使图表看起来像这样
它不仅增加了并发连接的数量。它还使我们能够将到达率提高 10 倍。这使得随后的测试速度快得多。
bag
和 duplicate_bag
之间的区别在于 duplicate_bag
允许对同一个键使用多个条目。由于每个 socket 只能连接一次,并且只能拥有一个 pid,因此使用重复的 bag 对我们来说没有任何问题。
这达到了大约 450,000 个连接。此时,16GB 的机器内存不足。我们现在已经准备好对更大的机器进行真正的测试了。
我在 channels 基准测试中过早地放弃了,通过 @gabiz 的优化,我们现在在 4 核/15GB 的机器上实现了 450,000 个客户端的最大连接数!
— Chris McCord (@chris_mccord) 2015 年 10 月 25 日
我们需要更多的机器
Justin Schneck (@mobileoverlord) 在 IRC 中告诉我们,他和他的公司 Live Help Now 将在 RackSpace 上为我们设置一些额外的服务器。确切地说,是 45 台额外的服务器。
我们设置了几台机器,并将 Tsung 的阈值设置为 100 万个连接。这轻松地被 128GB 的机器实现了,这是一个新的里程碑。
在一个更大的 @Rackspace 机器上,我们在一台服务器上实现了 100 万个 Phoenix channel 客户端!快速截屏演示:https://t.co/ONQcVWWdy1
— Chris McCord (@chris_mccord) 2015 年 10 月 26 日
当 Justin 完成了所有 45 台机器的设置时,我们确信可以实现 200 万个连接。不幸的是,事实并非如此。在 130 万个连接时出现了一个新的瓶颈!
就这样。130 万个连接已经足够好了,对吧?错了。当我们达到 130 万个订阅者时,我们开始在请求订阅单个 pubsub 服务器时出现定期超时。我们还注意到广播时间的显著增加,广播到所有订阅者需要超过 5 秒。
Justin 对物联网(物联网)很感兴趣,并想知道我们是否可以针对 130 万以上的订阅者优化广播,因为他看到这些级别存在实际的用例。他有一个想法,可以通过对订阅者进行分片并并行化广播工作来分片广播。我们试用了这个想法,它将广播时间缩短至 1-2 秒。但是,我们仍然有那些讨厌的订阅超时问题。我们已经达到了单个 pubsub 服务器和单个 ets 表的极限。因此,Chris 开始着手将 pubsub 服务器汇集到一个池中,我们意识到可以将 Justin 的广播分片与 pubsub 服务器和 ets 表池结合起来。因此,我们根据订阅者的 pid 对订阅者进行分片,并将其分配到一个 pubsub 服务器池中,每个服务器管理其自身的 ets 表,每个表对应一个分片。这使我们能够在没有超时的情况下达到 200 万个订阅者,并保持 1 秒的广播。更改已在 此提交 中,该提交在合并到主分支之前正在不断完善。
Phoenix 频道基准测试在 40 核/128GB 机器上的最终结果。200 万个客户端,受 ulimit 限制#elixirlang pic.twitter.com/6wRUIfFyKZ
— Chris McCord (@chris_mccord) 2015 年 10 月 28 日
就这样。200 万个连接!每次我们认为没有更多优化空间时,都会有人提出另一个想法,从而导致性能大幅提升。
我们对 200 万这个数字感到满意。但是,我们并没有完全利用机器的性能,而且我们还没有努力减少每个套接字处理程序的内存使用量。此外,我们还会进行更多基准测试。这一组基准测试专门针对同时打开的套接字数量。一个拥有 200 万用户的聊天室很棒,尤其是当消息能够如此快速地广播时。但这并不是一个典型的用例。以下是一些未来的基准测试想法:
- 一个通道,x 个用户发送 y 条消息
- x 个通道,每个通道 1000 个用户发送 y 条消息
- 在多个节点上运行 Phoenix 应用程序
- 一个模拟,发送随机数量的消息,用户随机加入和离开,以模拟真实的聊天室
在本基准测试中发现的改进将在 Phoenix 的即将发布的版本中提供。请继续关注有关未来基准测试的信息,Phoenix 将继续突破现代 Web 的界限。