# 每天处理 1 亿个像素-少量竞争会导致大规模问题

> 原文： [http://highscalability.com/blog/2013/1/21/processing-100-million-pixels-a-day-small-amounts-of-content.html](http://highscalability.com/blog/2013/1/21/processing-100-million-pixels-a-day-small-amounts-of-content.html)

![](img/5088f2340fb872cea42ebbdd8c5dbfc1.png)

*这是 [Gordon Worley](http://www.linkedin.com/pub/gordon-worley/39/977/257) 的来宾帖子， [Korrelate](http://korrelate.com/) 的软件工程师在其中将在线购买与离线购买相关联（查看他们在这里做了什么）。*

几周前，我们一天早上来到办公室，发现所有服务器警报都响了。 像素日志处理落后了 8 个小时，并且没有取得进展。 查看日志后，我们发现有一个大客户在夜间上线，为我们提供的流量比最初告诉我们的要多 10 倍。 我不会说我们感到惊慌，但办公室肯定比平时更加​​紧张。 但是，在接下来的几个小时中，由于有远见和快速的思考，我们得以扩大规模以处理增加的负载并清除积压的日志，以使日志处理恢复到稳定状态。

在 Korrelate，我们部署了[跟踪像素](http://en.wikipedia.org/wiki/Web_bug)（也称为信标或网络错误），我们的合作伙伴将其用于向我们发送有关其用户的信息。 这些微小的 Web 对象不包含可见内容，但可以包含透明的 1 乘 1 gif 或 Javascript，并允许我们的合作伙伴在有人看到广告或采取与广告相关的操作时通知我们，而无需向我们发送广告服务器日志。 例如，当我们的一个合作伙伴展示广告时，他们通过将展示跟踪像素作为图像包含在 iframe 中或嵌入以我们的像素为源的脚本标签来触发我们的展示跟踪像素。 他们的广告会动态生成像素的网址，因此可以在查询字符串中向我们传递有关所展示广告的信息。 对于选择接受第三方 cookie 的用户，我们还可以在用户上设置和读取 Korrelate 用户 ID cookie，以便我们可以跟踪他们的活动，并在[聚合和匿名化](http://korrelate.com/privacy/)之后使用它，以提供我们的 分析产品。

在早期，甚至在 Korrelate 成为 Korrelate 之前，我们就知道有一天我们需要每天摄取数十亿个跟踪像素。 因此，当我们开始编写像素对数处理器时，我们进行了架构选择，使其可以扩展。

最初，日志处理器是用 Java servlet 编写的，但是事实证明这很难在服务器上进行管理，而且我们都不满意使用 Java 进行编程。 寻找替代方案，因为当时我们使用 [Pentaho](http://www.pentaho.com/) 分析工具套件来生成有关原始数据的报告，所以我们转向了[水壶](http://kettle.pentaho.com/)（俗称 Pentaho 数据集成） 数据。

Kettle 是在 JVM 上运行的提取转换加载工具，可以充分利用多线程来快速处理大量数据。 它还具有一个称为 Spoon 的易于使用的 GUI 工具，用于设计 Kettle 程序，该程序需要大量的配置，而编程却相对较少。 我们非常享受使用 Kettle 创建和部署日志处理的速度。 随着时间的流逝，我们意识到了水壶的局限性。

通过 Kettle 作业运行大量数据需要大量内存（在我写这篇文章时，日志处理器需要 8 GB 内存来处理 250,000 条记录的文件）。 在开发方面，使用 Spoon 易用性的缺点是源文件仅是人类可编辑的 XML，因此我们无法利用 Git 正常工作流来轻松合并并发开发分支，从而迫使我们 就像有人处理日志处理器时源被锁定一样。 但是尽管有这些限制，我们仍继续使用 Kettle，因为它正在工作，我们还有很多其他工作要做，并且我们知道我们可以在需要时扩大规模，即使它会很昂贵。

几个月前，我们不得不开始同时运行两个日志处理器，以适应我们的负载。 这是一个很好的体验，因为它有助于揭示日志处理中的问题区域。

当只有一个日志处理器运行时，我们遇到的唯一性能问题与过程的各个部分有关，需要很长时间才能完成。 例如，我们发现对数据库中表的更新花费了很长时间，因此我们几乎将所有存储像素数据的表都转换为仅追加，以便快速插入。 某些表不能仅作追加操作，因此要与我们创建的加载表一起使用，日志处理将快速将数据插入到表中，然后稍后再回过头来将加载表与数据库中的主表进行同步 比我们执行 upsert 的速度快。

调出第二个日志处理器会使我们面临新的问题。 尽管由于对仅追加表的非阻塞写入而使我们能够快速写入数据库，但是需要与加载表同步的少数几个表引起了足够的争用，两个日志处理器在运行一个日志处理器时几乎没有任何收益。 为了解决这个问题，我们将日志处理分为两部分：仅写到仅追加表的部分和需要插入堆表的部分。 这样，我们就可以打开仅追加日志处理器的两个实例（仅是堆表日志处理器之一），并获得良好的吞吐量，因为堆表从每个需要插入或更新的日志文件接收的数据相对较少，而追加- 只有表从每个日志文件接收很多数据。

因此，在像素大量涌入的早晨，我们认为我们已经做好了扩大规模的准备。 我们在几个小时之内启动了运行其他附加仅日志处理器实例的其他服务器，并开始处理日志（堆表日志处理器自行继续足够快地运行以保持运行状态）。 但是，我们很快发现，日志处理器中仍然存在争用。

为了跟踪日志处理的方式，我们将一些基本统计信息写到了几个审计表中，这些统计信息涉及日志处理需要多长时间以及处理多少像素。 为了收集这些数据，我们使用了 Kettle 的内置日志记录功能将信息写入表格，然后将其合并为对我们有用的摘要形式。 事实证明，Kettle 的编写方式要求在审计表上请求排他表级锁以将其写入。 而且由于在日志处理器实例的每次运行中都会发生数十次，因此对于相同的表，我们有数百个请求彼此等待。 每个请求的清除速度都很快，但是加了一点点延迟，导致日志处理器实例在应在 2 中完成时需要花费 15 分钟的时间来运行。

我们关闭了 Kettle 的内置日志记录功能，将其替换为我们自己的一些不需要表级锁定的代码，并且日志处理器之间的数据库争用消失了。 我们以艰难的方式学到了我们经常听到的智慧，但显然还没有被内在化：**少量争用可能成为大规模的问题**。

通过从日志处理中完全消除争用，我们能够快速启动十多个日志处理器实例，并使用它们来快速处理积压的事务，然后将其调节回稳定状态。 在短短 24 小时内，一切恢复正常，节省了一些额外的硬件和日志处理器。

尽管日志处理器现在可以处理高级别的并发性，但仍需要对其进行重建以处理更多的像素，而无需当前日志处理器的高昂成本。 调出当前日志处理器的新实例意味着添加具有大量 RAM 的服务器（通常大约需要 24 GB，以便我们在每台服务器上运行两个日志处理器，以及用于其他进程的额外 RAM），这是很昂贵的。 而且，当前的日志处理器在与数据库的有限连接上仍然面临潜在的争用。 为了解决这个问题，我们需要寻找减少需要向数据库传递数据的进程数量的方法。

我们还希望从 Kettle 转移到一个使代码管理更容易的系统。 为此，我们已开始使用 [Storm](http://storm-project.net/) 构建新的日志处理器，该处理器提供了用于创建自定义实时流处理工作流的灵活框架，类似于 [Hadoop](http://hadoop.apache.org/) 提供了灵活框架的方式 用于批处理。 尽管仅 Storm 本身提供的内置功能要比 Kettle 少得多，但也不必这样做，因为 Storm 工作流可以用任何语言编写，也可以使用我们想要的任何现有库。 由于我们的大多数代码已经在 Ruby 中，因此我们能够利用现有的代码库在 Ruby 中构建 Storm 工作流。 基于其他使用 Storm 的[其他人](https://github.com/nathanmarz/storm/wiki/Powered-By)，我们希望看到我们的 Storm 日志处理器每天可以扩展到数十亿像素。