# 更简单，更便宜，更快：Playtomic 从.NET 迁移到 Node 和 Heroku

> 原文： [http://highscalability.com/blog/2012/10/15/simpler-cheaper-faster-playtomics-move-from-net-to-node-and.html](http://highscalability.com/blog/2012/10/15/simpler-cheaper-faster-playtomics-move-from-net-to-node-and.html)

![](img/f9ab322d6775cffd84a15e52504cfff4.png)

*这是 [Playtomic](https://playtomic.com/) 首席执行官 Ben Lowry 的特邀帖子。 Playtomic 是一项游戏分析服务，每天大约有 2 千万人在大约 8000 种移动，网络和可下载游戏中实施。*

*这是 [Ben Lowry 在 Hacker News](http://news.ycombinator.com/item?id=4458124) ：*上的一个很好的摘要语录

> 昨天有超过 2 千万的人点击了我的 API 700,749,252 次，玩了我的分析平台集成的大约 8,000 款游戏，总播放时间不到 600 年。 就是昨天 有许多不同的瓶颈在等着人们大规模经营。 在我的用例中，Heroku 和 NodeJS 最终以非常便宜的价格缓解了很多。

Playtomic 始于几乎唯一的 Microsoft.NET 和 Windows 体系结构，该体系结构保持了 3 年，然后被 NodeJS 完全重写所取代。 在整个生命周期中，整个平台从单个服务器上的共享空间发展为完全专用，然后扩展到第二专用，然后将 API 服务器卸载到 VPS 提供程序和 4-6 个相当大的 VPS。 最终，API 服务器安装在 Hivelocity 的 8 台专用服务器上，每个服务器都具有超线程+ 8gb ram +运行 500 或 3 个 API 堆栈实例的双 500gb 磁盘的四核。

这些服务器通常为 30,000 至 60,000 个并发游戏玩家提供服务，每秒最多接收 1500 个请求，并通过 DNS 轮询实现负载平衡。

7 月，整个服务器群被 Heroku 托管的 NodeJS 重写所取代，以节省大量资金。

## 使用 NodeJS 扩展 Playtomic

迁移包括两个部分：

1.  **专用于 PaaS** ：优势包括价格，便利性，利用其负载平衡和降低总体复杂性。 缺点包括没有用于 NodeJS 的 New Relic，非常小的崩溃以及通常不成熟的平台。
2.  **.NET 到 NodeJS** ：将具有本地 MongoDB 实例和服务预处理事件数据的 ASP.NET/C#体系结构切换到本地，然后将其发送到集中式服务器以完成操作； 到 Heroku + Redis 上的 NodeJS 以及 SoftLayer 上的预处理（请参见 Catalyst 程序）。

## 专用于 PaaS

复杂性的降低是显着的； 我们在托管合作伙伴 Hivelocity 拥有 8 台专用服务器，每台服务器运行 3 或 4 个 API 实例。 每个人都运行一小套软件，其中包括：

*   MongoDB 实例
*   日志预处理服务
*   监视服务
*   带有 api 网站的 IIS

部署是通过 FTP 脚本完成的，该脚本将新的 api 站点版本上载到所有服务器。 服务更讨厌部署，但很少更改。

MongoDB 对于在预处理和发送日志之前临时保存日志数据是一个糟糕的选择。 它提供了最初只写内存的巨大速度优势，这意味着写请求几乎立即就“完成”了，这远远优于 Windows 上的常见消息队列，但是它从未回收已删除数据留下的空间，这意味着 db 大小会膨胀到 如果不定期压缩，则超过 100 GB。

PaaS 提供商的优势是众所周知的，尽管它们似乎最成熟并且拥有广泛的技术支持，但对 Heroku 和 Salesforce 的信心最容易，尽管看上去最相似。

过渡到 PaaS 的主要挑战是，人们像在专用服务器上那样，可以与网站一起运行辅助软件的想法已经动摇。 大多数平台都提供了一些可以利用的后台工作线程，但这意味着您需要通过 3 rd 第三方服务或服务器来路由 Web 线程中的数据和任务。

我们最终选择在 Softlayer 上的大型服务器上运行了十二个特定目的的 Redis 实例和一些中间件，而不是后台工作程序。 Heroku 不对出站带宽收费，而 Softlayer 不对入站带宽收费，这巧妙地避免了所涉及的大量带宽。

## 从.NET 切换到 NodeJS

在服务器端使用 JavaScript 是一种混合体验。 一方面，缺乏形式和样板正在解放。 另一方面，没有 New Relic，也没有编译器错误，这使所有事情都变得比原本困难。

有两个主要优点，这使得 NodeJS 对于我们的 API 极为有用。

1.  **后台工作程序**与 Web 服务器位于相同的线程和内存中
2.  **与 Redis 和 mongodb 的持久共享连接**

### 后台工作者

NodeJS 具有非常有用的功能，可以独立于请求继续工作，允许您预取数据和其他操作，这些操作可以让您非常早地终止请求，然后完成对它的处理。

对于我们而言，将整个 MongoDB 集合复制到内存中（定期刷新）是特别有利的，这样，整个工作类都可以访问当前数据，而不必使用外部数据库或本地/共享缓存层 。

在以下条件下，我们每秒总共节省 100 至 1000 的数据库查询：

*   主 api 上的游戏配置数据
*   数据导出 api 上的 API 凭据
*   GameVars，开发人员用来存储配置或其他数据以将其热加载到他们的游戏中
*   排行榜得分表（不包括得分）

基本模型是：

> var cache = {};
> 
> module.exports = function（request，response）{
> response.end（cache [“ x”]）;
> }
> 
> 函数 refresh（）{
> 
> //从数据库中获取更新的数据，存储在缓存对象
> 中。cache [“ x”] =“ foo”;
> setTimeout（refresh，30000）;
> }
> 
> refresh（）;

这样做的优点是，您可以与后端数据库建立单个连接（每个 dyno 或实例），而不是每个用户建立一个连接，并且具有非常快的本地内存缓存，该缓存始终具有新数据。

注意事项是您的数据集必须很小，并且此线程与其他所有线程都在同一线程上运行，因此您需要意识到阻塞线程或执行过多的 CPU 工作。

### 持久连接

NodeJS 通过.NET 为我们的 API 提供的另一个巨大好处是持久性数据库连接。 在.NET（等）中进行连接的传统方法是打开您的连接，进行操作，然后将您的连接返回到池中，以便在短期内不再使用或不再使用时可以重新使用。

这是很常见的，除非达到很高的并发性，否则它将正常工作。 并发率很高，因此连接池无法足够快地重用连接，这意味着它会生成新连接，数据库服务器必须扩展这些连接才能处理。

在 Playtomic，我们通常有数十万并发游戏玩家正在发送事件数据，这些事件数据需要被推回到不同数据中心中的 Redis 实例，而使用.NET 则需要大量 连接数量–这就是为什么我们在每台旧专用服务器上本地运行 MongoDB 的原因。

使用 NodeJS，每个 dyno /实例具有单个连接，该连接负责推送特定 dyno 接收的所有事件数据。 它位于请求模型之外，如下所示：

> var redisclient  = redis.createClient(….);
> 
> module.exports = function（request，response）{
> 
> var eventdata =“ etc”;
> 
> redisclient.lpush（“ events”，eventdata）;
> 
> }

### 最终结果

**高负载：**

最后一刻的要求

* * *

_exceptions：75 （0.01％） _ 失败：5 （0.00％） 总计：537,151 （99.99％） data.custommetric.success：1,093 （0.20％） data.levelaveragemetric.success ：2,466 （0.46％） data.views.success：105 （0.02％） events.regular .invalid_or_deleted_game＃2：3,814 （0.71％） events.regular.success：527,837 （98.25％） gamevars.load.success：1,060 （0.20％） geoip.lookup.success：109 （0.02％）  Leaderboards.list.success：457 （0.09％） Leaderboards.save.missing_name_or_source＃201：3 （0.00％） 排行榜。保存。成功：30 （0.01％） 。
排行榜.saveandlist。成功：102 （0.02％） playerlevels.list.success：62 （0.01％） playerlevels.load.success：13 （0.00％）

* * *

此数据来自在每个实例的后台运行的一些负载监控，将计数器推送到 Redis，然后将其汇总并存储在 MongoDB 中，您可以在 [上查看它们的运行情况 https://api.playtomic.com/load.html](https://api.playtomic.com/load.html) 。

该数据中有几种不同的请求类别：

*   **事件** 从 MongoDB 检查游戏配置，执行 GeoIP 查找（开源的非常快速的实现，网址为 https://github.com/benlowry/node-geoip-native） ，然后推送到 Redis
*   **GameVars ， 排行榜，玩家级别** 都从 MongoDB 中检查游戏配置，然后再检查相关的 MongoDB 数据库
*   **数据** 查找被代理到 Windows 服务器，因为 NodeJS 对存储过程的支持不佳

The result is 100,000s of concurrent users causing spectactularly light Redis loads fo 500,000 – 700,000 lpush’s per minute (and being pulled out on the other end):

 1  [||                                                                                      1.3%]     Tasks: 83; 4 running
 2  [|||||||||||||||||||                                                                    19.0%]     Load average: 1.28 1.20 1.19
 3  [||||||||||                                                                              9.2%]     Uptime: 12 days, 21:48:33
 4  [||||||||||||                                                                           11.8%]
 5  [||||||||||                                                                              9.9%]
 6  [|||||||||||||||||                                                                      17.7%]
 7  [|||||||||||||||                                                                        14.6%]
 8  [|||||||||||||||||||||                                                                  21.6%]
 9  [||||||||||||||||||                                                                     18.2%]
 10 [|                                                                                       0.6%]
 11 [                                                                                        0.0%]
 12 [||||||||||                                                                              9.8%]
 13 [||||||||||                                                                              9.3%]
 14 [||||||                                                                                  4.6%]
 15 [||||||||||||||||                                                                       16.6%]
 16 [|||||||||                                                                               8.0%]
 Mem[|||||||||||||||                                                                 2009/24020MB]
 Swp[                                                                                    0/1023MB]

 PID USER     PRI  NI  VIRT   RES   SHR S CPU% MEM%   TIME+  Command
12518 redis     20   0 40048  7000   640 S  0.0  0.0  2:21.53  `- /usr/local/bin/redis-server /etc/redis/analytics.conf
12513 redis     20   0 72816 35776   736 S  3.0  0.1  4h06:40  `- /usr/local/bin/redis-server /etc/redis/log7.conf
12508 redis     20   0 72816 35776   736 S  2.0  0.1  4h07:31  `- /usr/local/bin/redis-server /etc/redis/log6.conf
12494 redis     20   0 72816 37824   736 S  1.0  0.2  4h06:08  `- /usr/local/bin/redis-server /etc/redis/log5.conf
12488 redis     20   0 72816 33728   736 S  2.0  0.1  4h09:36  `- /usr/local/bin/redis-server /etc/redis/log4.conf
12481 redis     20   0 72816 35776   736 S  2.0  0.1  4h02:17  `- /usr/local/bin/redis-server /etc/redis/log3.conf
12475 redis     20   0 72816 27588   736 S  2.0  0.1  4h03:07  `- /usr/local/bin/redis-server /etc/redis/log2.conf
12460 redis     20   0 72816 31680   736 S  2.0  0.1  4h10:23  `- /usr/local/bin/redis-server /etc/redis/log1.conf
12440 redis     20   0 72816 33236   736 S  3.0  0.1  4h09:57  `- /usr/local/bin/redis-server /etc/redis/log0.conf
12435 redis     20   0 40048  7044   684 S  0.0  0.0  2:21.71  `- /usr/local/bin/redis-server /etc/redis/redis-servicelog.conf
12429 redis     20   0  395M  115M   736 S 33.0  0.5 60h29:26  `- /usr/local/bin/redis-server /etc/redis/redis-pool.conf
12422 redis     20   0 40048  7096   728 S  0.0  0.0 26:17.38  `- /usr/local/bin/redis-server /etc/redis/redis-load.conf
12409 redis     20   0 40048  6912   560 S  0.0  0.0  2:21.50  `- /usr/local/bin/redis-server /etc/redis/redis-cache.conf

and very light MongoDB loads for 1800 – 2500 crud operations a minute:

insert  query update delete getmore command flushes mapped  vsize    res faults locked % idx miss %     qr|qw   ar|aw  netIn netOut  conn       time
    2      9      5      2       0       8       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0     3k     7k   116   01:11:12
    1      1      5      2       0       6       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0     2k     3k   116   01:11:13
    0      3      6      2       0       8       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     3k     6k   114   01:11:14
    0      5      5      2       0      12       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0     3k     5k   113   01:11:15
    1      9      7      2       0      12       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0     4k     6k   112   01:11:16
    1     10      6      2       0      15       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     1|0     4k    22k   111   01:11:17
    1      5      6      2       0      11       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     3k    19k   111   01:11:18
    1      5      5      2       0      14       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0     3k     3k   111   01:11:19
    1      2      6      2       0       8       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     3k     2k   111   01:11:20
    1      7      5      2       0       9       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0     3k     2k   111   01:11:21
insert  query update delete getmore command flushes mapped  vsize    res faults locked % idx miss %     qr|qw   ar|aw  netIn netOut  conn       time
    2      9      8      2       0       8       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     4k     5k   111   01:11:22
    3      8      7      2       0       9       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     4k     9k   110   01:11:23
    2      6      6      2       0      10       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     3k     4k   110   01:11:24
    2      8      6      2       0      21       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     4k    93k   112   01:11:25
    1     10      7      2       3      16       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     4k     4m   112   01:11:26
    3     15      7      2       3      24       0  6.67g  14.8g  1.23g      0      0.2          0       0|0     0|0     6k     1m   115   01:11:27
    1      4      8      2       0      10       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     4k     2m   115   01:11:28
    1      6      7      2       0      14       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     4k     3k   115   01:11:29
    1      3      6      2       0      10       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0     3k   103k   115   01:11:30
    2      3      6      2       0       8       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     3k    12k   114   01:11:31
insert  query update delete getmore command flushes mapped  vsize    res faults locked % idx miss %     qr|qw   ar|aw  netIn netOut  conn       time
    0     12      6      2       0       9       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     4k    31k   113   01:11:32
    2      4      6      2       0       8       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0     3k     9k   111   01:11:33
    2      9      6      2       0       7       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0     3k    21k   111   01:11:34
    0      8      7      2       0      14       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     4k     9k   111   01:11:35
    1      4      7      2       0      11       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     3k     5k   109   01:11:36
    1     15      6      2       0      19       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0     5k    11k   111   01:11:37
    2     17      6      2       0      19       1  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     6k   189k   111   01:11:38
    1     13      7      2       0      15       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     1|0     5k    42k   110   01:11:39
    2      7      5      2       0      77       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     2|0    10k    14k   111   01:11:40
    2     10      5      2       0     181       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0    21k    14k   112   01:11:41
insert  query update delete getmore command flushes mapped  vsize    res faults locked % idx miss %     qr|qw   ar|aw  netIn netOut  conn       time
    1     11      5      2       0      12       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     0|0     4k    13k   116   01:11:42
    1     11      5      2       1      33       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     3|0     6k     2m   119   01:11:43
    0      9      5      2       0      17       0  6.67g  14.8g  1.22g      0      0.1          0       0|0     1|0     5k    42k   121   01:11:44
    1      8      7      2       0      25       0  6.67g  14.8g  1.22g      0      0.2          0       0|0     0|0     6k    24k   125   01:11:45

## 相关文章

*   [新的 API 服务器：Heroku 和 NodeJS 与专用和.NET](https://playtomic.com/blog/post/86-the-new-api-server-heroku-an)
*   [日志处理，从无到有，每天发生十亿个事件](https://playtomic.com/blog/post/68-log-processing-from-nothing)
*   [每天在预算范围内实时处理超过 3 亿个事件](https://playtomic.com/blog/post/53-handling-over-300-million-ev)
*   [回顾 2010 年-从每月 4000 万事件到每天 3 亿](https://playtomic.com/blog/post/46-looking-back-on-2010)
*   [四种让玩家爱上您的游戏的方式](http://www.gamasutra.com/blogs/BenLowry/20111116/8914/Four_ways_to_keep_players_in_love_with_your_game.php)
*   [已发布 InkTd Flash 游戏源代码](http://www.emanueleferonato.com/2012/04/19/inktd-flash-game-source-code-released/)
*   [黑客新闻主题](http://news.ycombinator.com/item?id=4615799)的相关评论
*   [Heroku 成功案例页面](http://success.heroku.com/playtomic)
*   [关于黑客新闻](http://news.ycombinator.com/item?id=4655718)

Redis 和 MongoDB 嗯...。 好文章

这是一篇很棒的文章，感谢您的分享。

因此，您切换了三件事：体系结构，部署/操作模型和运行时。
多汁的标题指的是第三部分，它可能是更改的最不重要的部分，而成本最高的部分（因为它需要完全重写）。

不要误会我的意思-我爱我一些 node.js，并且我是 Heroku 的忠实拥护者，但是.NET impl 中指出的所有警告很容易（或不太容易，但比完全重写要容易得多） ）可寻址。 因此，您*可以*通过优化现有代码库（而不是重写）并迁移到具有 AppHarbor，Azure 的易于部署的模型（或在 Heroku 上运行 Mono）来获得类似的结果。 而且您还会有 New Relic :)

很想听听您对完整重写的成本收益平衡的看法

写得好！，我们曾经做过类似的事情。 当我们的集合太大时，Node GC 将暂停很长一段时间，并最终会因 JS 内存不足错误（大约 2 Gig）而崩溃。 我们最终创建了一个 NodeJS C ++插件来存储 V8 堆之外的数据，这实际上使我们可以将驻留在内存中的缓存扩展到超过 10 个 Gig。

我对 Node.JS 不太了解，但是 devdazed 的评论提出了我无法解决的问题。 在哪个星球上，NodeJS 内置的 GC 比 JVM（或.NET）更好？

我猜不能与成功争辩-这对他们是有用的。 但是很难相信在 JVM 或.NET 环境中无法轻松实现同一目标，在这些环境中，您不会遇到与 GC 相关的陷阱。 或者至少您发现的那些是众所周知的。

这是一本不错的书，但是我没有得到一些要点：
1.后台工作者。 通过创建一个新线程，Asp.net 可以轻松地做到这一点。 唯一应注意的事情是不会在该线程中引发未处理的异常。
2.持久连接。 为什么不仅仅拥有一个静态的 MongoServer 实例（它是线程安全的类）呢？ MongoCollection 类也是线程安全的。 所以我们甚至可以有一个静态的集合实例

究竟是什么使您用 Node.js 取代.NET？ ASP.NET 完全支持异步。 通过从.NET 切换到节点，您还可以获得“沉重”（如 3-10 倍）的性能。 如果您的应用切换到 node.js 之后变得更快，那么您首先在.net 方面做错了什么。

您也可以随意处理数据库连接。 您可以使用[ThreadStatic]或其他某种机制使每个线程保持一个打开状态。

@Dmitry：

并不是说我是该领域的专家（也不认为 Node 是灵丹妙药），但也许我会说一些话来为您澄清一下：

1.我不会依赖 ASP.NET 来执行后台任务，因为该实现通常非常脆弱。 ASP.NET 根本不是为此目的而设计的，我想如果您想通过 ASP.NET 应用程序上下文中的线程 API 实现后台工作程序，则需要一个非常非常好的理由。 我宁愿选择 WCF 服务作为 Windows 服务托管，它更可靠。

2.的确如此，拥有静态的 MongoServer / MongoDatabase 实例将使您在应用程序域的整个生命周期中都具有持久的连接。 最后一部分很重要，因为 ASP.NET 应用程序可以出于多种原因（计划的应用程序池回收，web.config 或应用程序文件夹更改等）重新启动。 而且我认为 Node 在这方面更可靠。

但总的来说，我同意其他人的观点，即完全没有必要完全重写（以及移至 Node）（但我认为他们也知道自己在做什么）。

What on earth made you replace .NET with Node.js? ASP.NET supports asynchrony to the fullest. You also take a *heavy* (like 3-10x) perf hit by switching to node from .NET. If your app got faster after switching to node.js you did something wrong on the .net side in the first place.

You are also free to handle your DB connections however you want. You could have kept one open per thread for example with [ThreadStatic] or some other mechanism.

最近所有的 node.js 文章都有什么？

是的，node.js 速度很快，但是它缺少太多基本功能，因此当您重写所有模块时，“神奇”的速度优势将不复存在，而且整个代码库将成为一堆不断调用自身的回调 ，进行简单的更改需要您分析整个过程，就像穿针一样。

Node.js 令人头疼。

我们正在将 StatsD + Graphite 与我们的企业 Node 应用一起使用。 有了它，您可以轻松免费地免费获得 New Relic。

哇，这家伙真的很喜欢过度设计！ 在 API 客户端中使用 boost（并迫使 android 用户使用自定义 SDK 进行编译，这对于使用 malkade skd 的用户而言更加痛苦）。