Scaling Mastodon——What it takes to house 43,000 users

我的实例 mastodon.social 最近已经超过了 43,000 位用户。我不得不关掉注册以确保有足够的时间来审查 Mastodon 的基础架构,并且也是保证给已经注册的用户提供一个良好的体验(这个举动带来的更奇妙的结果是——Mastodon联邦为分布在500个以上的独立实例上、总计超过161,000人提供服务!

但是要运营一个为 43,000 位用户提供流畅且快速响应的服务是需要下点功夫的,而且其他的一些实例上的用户群体也在不断扩大,是时候分享一些提示和我在运营自己的实例时学习到的技巧了。

Mastodon 由两个扩展不同的部分组成:数据库,代码。数据库是纵向扩展。也就是说,让你的数据库跑在一台超厉害的计算机上是很容易也很有效的,要知道,这比起为了让数据库在多台计算机上运行而分片(sharding)或者复制(replication)真是简单多了。而 Mastodon 的代码呢,则是横向扩展——只要你想的话,你可以让它跑在任意多台机器上,还是并行的,并且为网络请求做负载均衡,这样你的服务总是可以保持在良好的状态。

首先,Mastodon 上的负载是从哪儿来的?

浏览和使用你的 Mastodon 实例站点需要响应来自用户的 HTTP 请求。每一个 Puma worker(WEB_CONCURRENCY)可以同时响应预定的 MAX_THREADS 个请求。如果每个 worker 的每一个线程都在忙着响应某个请求,那么新到的请求就必须等待了。如果它等得太久了的话,那么就会因为超时而取消响应。这即是说,为了满足更高的请求吞吐,你得有更多的 worker、更多的线程才行。

连接到流 API (Steaming API)意味着一个持续的从 nginx 到 流 API (Steaming API)的连接。我注意到,流 API (Steaming API)自身并没有因为大量的连接而产生压力,但是 nginx 需要一个更高限额的 可打开的文件数(worker_rlimit_nofile),和一个更高数量的 worker_connections 来保持这些连接。幸亏 nginx 即使是在这些参数都如此高的情况下,都保持着非常轻量的占用。

在站点中实际的活动,比如发送消息,关注或者取关某个人,以及其他更多的作为用户可以做的事,都会生成后台的任务,这些任务会由 Sidekiq 处理。如果它们没有被及时处理,那么它们就会在 backlog 中开始队列。当你发的 toot 在一个小时之后才被你的关注者看到的话,那就已经是一个值得关注的问题了。也就是,你需要更多的 Sidekiq 来处理越来越多的这些活动所产生的后台任务了。

这里是一些 Mastodon 在扩展时的基本准则。当然,实际中可能远不止这些。

每一次你横向扩展时,都会给数据库多来带一些压力,因为 web worker 和 background worker 以及流 API (Steaming API)都需要数据库连接。它们每个都使用连接池(connection pool)以为自己的每一个线程提供服务。这个很容易就上升到超过总计 200 个连接,而这是 PostgreSQL 所推荐的,在一台 16GB 内存的服务器上的最大连接数(max_connections)。当你到了这个点的时候,就意味着你需要 pgBouncer 了。pgBouncer 是一个 PostgreSQL 的透明代理,提供了基于数据库事务的池(pooling),而不是会话(sessions)。这比起让一个暂时没有数据库相关操作的线程拥有一个真正的数据库连接要好。Mastodon 支持 pgBouncer,你只需要简单的连接到它而不是 PostrgreSQL,并且设置环境变量 PREPARED_STATEMENTS=false 就行。

简单地将默认设置中推荐的 Sidekiq 数量调高也许不是让用户的活动得到及时处理的灵丹妙药。并不是所有的后台任务都是平等的!Sidekiq 要处理的任务可能来自不同的队列,带有不同的优先级。在 Mastodon 中,这些队列如下:

  • default: 负责将 toots 分发到本实例中每个关注者的时间线里
  • push: 在 toots 进入 default 队列前,将 toots 送到其他的服务器上、处理来自其他服务器的 toots
  • pull: 下载对话,用户头像、封面,个人资料信息
  • mailers: 通过 SMTP 服务器发送邮件

我将它们按重要程度排列了起来。Default 队列是最为重要的,因为它是直接且即时地影响着你的 Mastodon 实例上的用户体验。Push 队列也挺重要,因为它影响着你的关注者和来自其他地方的关联。Pull比起它们就稍微次一点,因为下载这些信息稍微等一下也无妨。最后呢,就是 Mailers 队列了,反正从 Mastodon 发出去的邮件也总不会太多。

当你有一个有着预定队列顺序的 Sidekiq 进程时,比如 -q default -q push -q pull -q mailers,它将首先检查第一个队列,如果第一个队列中没有任务的话,就检查下一个,以此类推。也就是,根据 -c (concurrency) 参数定义的每一个线程,都会做这样的检查。但是我认为你必须要想到这个问题——如果你突然间来了 100 个任务在 Default 队列中,100 个任务在 Push 队列中,而你仅有 25 个线程来处理这所有的任务的话,那么显然在 Sidekiq 处理时就会有一个巨大的延迟,哪怕先不管在 Push 队列中的那 100 个任务。

由于这个因素,我发现将这些队列分配到不同机器上的不同的 Sidekiq 中十分管用。一部分仅负责 Default 队列,一部分仅关心 Push 队列,一部分……按这种方法,你都不会让你的用户在进行任何用户相关的操作时感受到明显的延迟。

另一个大的点是,嘛~虽然是事后诸葛亮,就是在单一 Sidekiq 上设置一个高的并发数,比起用更多的 Sidekiq 进程、但设置的并发数小,前面那种方案实际上是降低了效率。事实上,对于 Puma workers 来说也是同样的——多 worker 少线程 比起 少 worker 多线程 的方案更快。这是因为 MRI Ruby 并没有使用 native 线程,所以它们并不是真正的并行运行,不管你有多少 CPU 核心。唯一的不足是:线程间可以共享一块内存,而不同的进程却不能。也就是说,越多进程意味着消耗越多的内存。但是如果你的机器上有空闲内存的话,那么你应该启动更多的 worker,以匹配 多 worker 少线程的方案。

当前 mastodon.social 的基础架构如下:

2x baremetal C2M (8 cores,16GB RAM) servers:

  • 1 running PostgreSQL (with pgBouncer on top) and Redis
  • 1 running 4x Sidekiq processes between 10–25 threads each

6x baremetal C2S (4 cores, 8GB RAM) servers:

  • 2 running Puma (8x workers, 2x threads each), Sidekiq (10 threads), streaming API
  • 1 running Nginx load balancer, Puma (8x workers, 2x threads each, Sidekiq (2 threads), streaming API
  • 2 running Sidekiq (20 threads)
  • 1 running Minio for file storage with a 150GB volume

这里面很多都是因为在互联网上备受关注之后添加的,在那之前,mastodon.social 仅仅服务 20,000 位用户(他们中多数,说实话,都不怎么活跃),就靠着 1 个数据服务器,2 个应用服务器,和一个 Minio 服务器。与此同时,Mastodon v.1.1.1 版本的发布囊括了大量的优化,至少比 Mastodon 在走向病毒式传播的那一天提升了两倍的请求吞吐量和后台任务处理。

在写下这篇的同时,mastodon.social 正有大约 6,000 个打开的连接,处理着约每分钟 3,000 个请求,平均响应时间是 200ms。

(原文来自Eugen Rochko @ Medium, Scaling Mastodon——What it takes to house 43,000 users,这里在不更改原文意思的情况下翻译,部分词语在中文中没有好的、能够表达出意味的翻译,故保留原文。翻译的目的是研究 Mastodon 使用的技术,这里正好是 Mastodon 开发者所写的技术类文章。)

声明: 本文翻译自:Scaling Mastodon What it takes to house 43,000 users喵~

Leave a Reply

Your email address will not be published. Required fields are marked *

two × five =