几个月前,我致力于提高“完整”索引器的性能。我觉得这种改进足以分享这个故事。完整索引器是 Box 从头开始创建搜索索引的过程,从 hbase 表中读取我们所有的文档并将文档插入到 Solr 索引中。
我们根据 id 对索引文档进行分片,同样的文档 id 也被用作 hbase 表中的 key。我们的 Solr 分片公式是 id % number_of_shards。mapreduce 作业扫描 hbase 表,通过上述分片公式计算每个文件的目标分片,并将每个文档插入相应的 solr 分片中。这是在过去几年中为我们提供良好服务的初始模型的示意图:
所有 mapreduce 作业都与所有分片对话,因为每个分片的数据分布在所有 hbase 区域中。该作业是仅地图作业,没有减少作业。hbase 表扫描以及更新请求都在映射器中完成。
在每个映射器中,都有一个批处理作业的共享队列;和一个 http 客户端共享池,它们从队列中获取作业并将其发送到相应的分片。每个单独的文档都不会直接插入到队列中。相反,需要在同一个分片上索引的文档在插入队列之前会一起批处理(当前默认值为 10)。队列是有界的,当它已满时,文档生产者必须等待才能扫描更多行。
如果所有 Solr 分片继续以一致且一致的速度*摄取文档,则该系统以稳定的速度运行。但是,Solr 时不时地会将内存中的结构刷新到文件中,这种 I/O 可能会导致一些索引操作暂时变慢。在这个阶段,集群不提供查询服务,所以这不是问题。
如果分片的总数为 n,并且给定分片的间歇性慢索引速率的概率为 p,则:
- P(至少 n 个分片中的一个很慢)= P(恰好一个分片很慢) P(正好两个分片很慢) ... P(所有 n 个分片都很慢)
要么
- P(至少一个分片很慢)= 1 - P(没有一个分片很慢)
- P(n 个分片中至少有 1 个很慢)= 1 — (1-p)ⁿ
如果我们假设对于给定的时间间隔 p = 0.01,这是 P 的图表(集群中至少有一个分片很慢):
这意味着要在更多分片上获得良好的索引性能,我们需要隔离一个分片的瓶颈,以免影响其他分片的索引。我的第一个尝试是增加工作人员池,这样如果一些工作人员由于速度慢而被卡在一个分片上,那么其余工作人员可以继续处理队列。这有所帮助,但仍然有可能让所有或许多工人在选择工作时陷入困境,这些工作会间歇性地进入缓慢的分片。在这种情况下,文档生产者线程将不会创建新文档,因为队列已满,并且所有工作人员都无法继续进行,因为他们正在等待缓慢的工作完成。在我的第二次尝试中,我为每个分片(在每个映射器上)创建了单独的队列和工作人员,这确保了如果一些分片很慢,那么其余分片不必闲置,因为他们的工作人员将继续阅读队列中的作业并将它们发送以进行索引。最终,正在呼吸的碎片将再次开始更快地索引,而其他一些碎片可能会开始缓慢响应等等。这极大地改善了系统的总流量。
这是具有较旧并发模型的 39 台主机的图表。该作业在运行三天后崩溃。即使在崩溃之前,它的表现也不一致。此外,分片的平均索引速度低于我们过去看到的总分片较少的情况。
这是在具有新并发模型的同一组主机上执行的相同工作,它的性能要好得多且更一致:
y 轴上的单位是每秒读取次数。它增加了一倍多。Box 拥有近 500 亿份文档**,通过改进,完整索引器能够在不到两天的时间内完成此索引阶段。 但是,这种新模型也有其缺点,例如:
- 此模型在针对同一分片的工作人员之间没有通信。因此,当一个分片响应缓慢时,来自其他并行运行的映射器的工作人员继续向它发送请求(并且失败,然后重试),即使一个或多个工作人员(在其他映射器中)已经确定该分片很慢。
- 由于每个映射器为每个分片分配一个固定长度的队列,因此设计不会扩展到超过一定数量的分片;因为队列的内存需求将超过映射器的堆大小。
更具可扩展性的模型将涉及映射器和 Solr 分片之间的队列。并且应该有特定于分片的客户端,它们可能运行在分片的主机上,它将从队列中读取分片的文档并发送到 Solr 进行索引(通过 REST API 或 SolrJ)。 * Hbase 表扫描和文档生成器不是我们的瓶颈,因此我在这里只提到 Solr 索引性能。