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 中找到 - 主要区别在于

  1. 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 它并运行

  1. MIX_ENV=prod mix deps.get
  2. MIX_ENV=prod mix deps.compile
  3. 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 上发布了结果,我们就收工了。

了解您的 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 倍。这使得随后的测试速度快得多。

bagduplicate_bag 之间的区别在于 duplicate_bag 允许对同一个键使用多个条目。由于每个 socket 只能连接一次,并且只能拥有一个 pid,因此使用重复的 bag 对我们来说没有任何问题。

这达到了大约 450,000 个连接。此时,16GB 的机器内存不足。我们现在已经准备好对更大的机器进行真正的测试了。

我们需要更多的机器

Justin Schneck (@mobileoverlord) 在 IRC 中告诉我们,他和他的公司 Live Help Now 将在 RackSpace 上为我们设置一些额外的服务器。确切地说,是 45 台额外的服务器。

我们设置了几台机器,并将 Tsung 的阈值设置为 100 万个连接。这轻松地被 128GB 的机器实现了,这是一个新的里程碑。

当 Justin 完成了所有 45 台机器的设置时,我们确信可以实现 200 万个连接。不幸的是,事实并非如此。在 130 万个连接时出现了一个新的瓶颈!

就这样。130 万个连接已经足够好了,对吧?错了。当我们达到 130 万个订阅者时,我们开始在请求订阅单个 pubsub 服务器时出现定期超时。我们还注意到广播时间的显著增加,广播到所有订阅者需要超过 5 秒。

Justin 对物联网(物联网)很感兴趣,并想知道我们是否可以针对 130 万以上的订阅者优化广播,因为他看到这些级别存在实际的用例。他有一个想法,可以通过对订阅者进行分片并并行化广播工作来分片广播。我们试用了这个想法,它将广播时间缩短至 1-2 秒。但是,我们仍然有那些讨厌的订阅超时问题。我们已经达到了单个 pubsub 服务器和单个 ets 表的极限。因此,Chris 开始着手将 pubsub 服务器汇集到一个池中,我们意识到可以将 Justin 的广播分片与 pubsub 服务器和 ets 表池结合起来。因此,我们根据订阅者的 pid 对订阅者进行分片,并将其分配到一个 pubsub 服务器池中,每个服务器管理其自身的 ets 表,每个表对应一个分片。这使我们能够在没有超时的情况下达到 200 万个订阅者,并保持 1 秒的广播。更改已在 此提交 中,该提交在合并到主分支之前正在不断完善。

就这样。200 万个连接!每次我们认为没有更多优化空间时,都会有人提出另一个想法,从而导致性能大幅提升。

我们对 200 万这个数字感到满意。但是,我们并没有完全利用机器的性能,而且我们还没有努力减少每个套接字处理程序的内存使用量。此外,我们还会进行更多基准测试。这一组基准测试专门针对同时打开的套接字数量。一个拥有 200 万用户的聊天室很棒,尤其是当消息能够如此快速地广播时。但这并不是一个典型的用例。以下是一些未来的基准测试想法:

  • 一个通道,x 个用户发送 y 条消息
  • x 个通道,每个通道 1000 个用户发送 y 条消息
  • 在多个节点上运行 Phoenix 应用程序
  • 一个模拟,发送随机数量的消息,用户随机加入和离开,以模拟真实的聊天室

在本基准测试中发现的改进将在 Phoenix 的即将发布的版本中提供。请继续关注有关未来基准测试的信息,Phoenix 将继续突破现代 Web 的界限。