PinalyticsDB:基于HBase的时间序列数据库

PinalyticsDB是Pinterest打造的一套专有时间序列数据库。在Pinterest,我们将PinalyticsDB作为后端以实现成千上万份时间序列报表的存储与可视化,例如下图所示案例(按国家与地区划分)。

我们在数年前以HBase为基础开发出PinalyticsDB,其利用实时map-reduce架构通过HBase协处理器实现聚合。但随着Pinterest业务的持续增长以及报表数量的不断提升,PinalyticsDB在处理庞大数据量及应用负载中开始遭遇一系列可扩展性挑战。

过去几个月以来,我们着手对PinalyticsDB进行重构,希望进一步提高其性能与可靠性水平。在本文中,我们将共同了解Pinterest面临的性能与可扩展性挑战,以及我们如何通过服务的重新设计构建出更为强大的PinalyticsDB新形态。

Hbase区域服务器热点

随着Pinterest内平台使用量的快速提升,热点问题也开始成为PinalyticsDB区域服务器面临的一大挑战。在以往的设计中,PinalyticsDB会为每份报表创建新的HBase表。

原有架构设计

原本的行键设计方案为:

原行键=前缀|日期|后缀

前缀=指标名称(字符串)

日期=YYYY-MM-DD格式,同样为字符串形式

后缀=由段号组成

行键由ASCII字符表示,其中“|”充当分隔符。

这套方案存在以下几个问题:

  • 需要为每份报表创建一个新表,但由于某些报表的访问频率远超其他报表,因此承载这些报表的区域服务器将面对更多往来流量。
  • 我们拥有成千上万份报表,因此HBase管理员基本不可能以手动方式监控报表并根据观察到的热点进行表拆分。
  • 在单一报表之内,某些指标的使用频率可能较高。由于指标名称为行键的第一部分,因此导致热点数量进一步增加。
  • 最近的数据使用趋势倾向于更频繁的访问操作,而日期则为指标后行键内容的组成部分,同样导致热点数量进一步增加。

以上提到的热点主要针对读取而言,但类似的热点在写入层面也同样存在,主要表现为某些报表表具有相对更高的写入次数,且指标数据始终会写入最新日期——这就导致各项指标的最新日期部分成为区域内的写入热点。

新的架构设计

我们通过改进行键架构与HBase表设计的方式解决了这个问题。我们利用以下行键设计创建出一份面向所有报表的表。

新的行键 = SALT | <报表-id> | <指标-id> | <日期-时间> | <段>

该行键以字节数组(byte[])形式表示,而不再使用字符串形式。

  • 键内的各个部分都有固定的长度,作为定义行键的固定结构,同时也支持模糊行过滤器。
  • 由于采用固定结构,因此我们不再需要“|”分隔符——这又进一步节约了空间资源。

对读取与写入操作的影响

Pinalytics DB V2 读取架构

如大家所见,由于采用salting逻辑,读取在整个集群当中得到良好分发。更重要的是,写入操作在这套架构中的分发效果同样令人满意。

改进协处理器性能

我们还计划通过修改请求结构与协处理器扫描行为的方式,进一步优化PinalyticsDB协处理器的性能。我们的优化举措使得区域服务器CPU利用率、RPC延迟以及JVM线程阻塞情况都得到显著改善。

我们的原始设计会针对发送至PinalyticsDB内每一项指标的对应请求创建Hbase扫描。Pinalytics会持续收到大量同类请求,从而引发大量扫描操作。通过将与同一报表及指标相关的聚合请求合并起来,并对与所请求段相关的FuzzyRowFilter进行单一扫描,我们显著减少了实际产生的Hbase扫描次数。

在使用Pinalytics时,用户通过会发出大批量请求,这些请求当中包含的不同细份指标往往数量有限。下图所示为跨美国多个州段请求某一样本指标。

这是一类非常常见的用例。相当一部分用户使用的仪表板中都包含这类图表。

在这个用例的启发下,我们尝试进行“多细分优化”,即利用协处理器对与同一指标相关联的PinalyticsRequest中的所有细分执行一次扫描(每区域salt)。

PinalyticsDB V2韩国人协处理器设计

  • Pinalytics Thrift Server将来自Pinalytics的全部请求按指标进行分组。接下来,协处理器会针对各个指标接收一条请求,其中包含与该指标相关的所有段FuzzyRowFilters。
  • 对于该协处理器区域内的各salt,协处理器会创建一条MUST_PASS_ONE扫描,并在单一FilterList的聚合请求中包含所有FuzzyRowFilters。
  • 该协处理器随后按照日期与FuzzyRowFilter对所有扫描结合进行聚合,并将响应结果发送回Thrift Server。 这意味着无论存在多少指向特定指标的不同段组合请求,我们都只需要处理一条指向该指标的聚合请求。

PinalyticsDB V2协处理器设计:对于每种salt,都只需要对指向同一指标的所有段创建一次扫描。

这一全新协处理器设计显著改善了区域服务器的CPU利用率、RPC延迟以及JVM线程阻塞情况。

注意:下图所示为部署多段优化数小时后得到的捕捉结果,因此无法准确反映系统的当前性能。但总体来看,这些设计举措仍然有助于提高协处理器的性能表现。

在部署新的协处理器后,区域服务器CPU利用率得到改善。

部署新的协处理器之后,区域服务器JVM线程阻塞情况得到改善。

部署新的协处理器之后,区域服务器RPG延迟得到改善。

大型报表元数据与Thrift Server OOM

我们的Thrift Server还存在OOM频繁崩溃的问题,如果这时用户尝试加载相关图表,那么就会发生Web应用程序超时的状况。这是因为Thrift Server的jvm没有设置XX:+ExitOnOutOfMemoryError,因此Thrift Server无法退出OOM,而继续调用只能产生超时。快速的解决办法是添加XX:+ExitOnOutOfMemoryError标记,以便在OOM生产Thrift Server上自动进行重启。

为了调试这个问题,我们将jconsole指向其中一台生产Thrift Server,使其能够捕捉到其他Thrift Server的崩溃情况。以下图表所示为总heap、上一代与新一代架构的运行情况。

Thrift Server的总heap内存为8G。 请注意,内存使用量从低于4G突然提升至8G以上,引发问题的根源正是OOM。

用于PinalyticsDB Thrift Server的上一代CMS。

同样的,旧一代架构的内存使用量会突然猛增至4G以上,直接超出了系统极限。峰值出现得太快,CMS收集器根本无暇介入,甚至连full GC都来不及出现。

用于PinalyticsDB Thrift Server的Eden空间。

我们通过负载测试在开发环境当中重现这类问题,并最终确定问题根源与我们的报表元数据存储与读取机制有关。对于大部分报表而言,元数据一般仅为数KB。但对于某些大型报表,元数据可能超过60 MB,甚至高达120 MB。这是因为此类报表中可能包含大量指标。

报表元数据结构

以下为单一报表中的元数据。报表元数据存储在一个专门的二级索引表当中。

#!/usr/bin/python
# -*- coding: utf-8 -*-
ReportInfo(
   tableName=u’growth_ResurrectionSegmentedReport’,
   reportName=u’growth_ResurrectionSegmentedReport’,
   segInfo={
       u’segKey2': u’gender’,
       u’segKey1': u’country’,
       u’segKeyNum’: u’2',
       },
   metrics={u’resurrection’: MetricMetadata(name=u’resurrection’,
       valueNames=None),
       u’5min_resurrection’:MetricMetadata(name=u’5min_resurrection’
       , valueNames=None)},
   segKeys={
       1: {
         u’1': u’US’,
         u’2': u’UK’,
         u’3': u’CA’,
…
       }
   )

优化表元数据的存储与检索使用

报表元数据以序列化blob的形式存储在HBase的二次索引表中。因此,可以判断问题的根源在于报表元数据的体量太大,而我们每次都在加载完整元数据——而非仅加载我们需要使用的部分。在高负载情况下,jvm leap可能会很快被填满,导致jvm无法处理汹涌而来的full GC。

为了从根本上解决问题,我们决定将报表元数据内容分发至报表行键下的多个列族与限定符当中。

经过增强的PinalyticsDB V2报表行键结构,分布在多个列族与限定符之间。

行键采用原子更新,因此所有列族都将在单一PUT操作时以原子化方式更新。

我们还创建了一种新的report-info获取方法。

getReportInfo(final String siTableName, final String reportName, List<String> metrics)

此方法会返回所有seg-info与seg-keys数据,且仅返回“metrics”列族中的相关指标数据。由于报表的大部分内容基于指标数据,因此我们只需要返回几kb数据,而非完整的100 mb以上数据。

Thrift Server轮循池

我们还对Thrift Server做出另一项变更,旨在实现可扩展性。每个Thrift Server都具有hbase org.apache.hadoop.hbase.client.Connection的单一实例。

hbase.client.ipc.pool.size=5
hbase.client.ipc.pool.type=RoundRobinPool

每个区域服务器默认只拥有1个连接。这样的设置增加了并发连接数,且有助于我们进一步扩展每个服务器的请求数量。

缺点与局限

通过以上设计,我们的业务体系运行得更为顺畅。但我们也意识到,其中仍存在一定局限性。

虽然横向扩展架构带来了均匀的读取写入分布,但却会对可用性造成影响。例如:任何区域服务器或者Zookeeper问题,都会影响到全部读取与写入请求。我们正在设置一套具有双向复制能力的备份集群,从而在主集群发生任何问题时实现读写机制的自动故障转移。

由于各个段属于行键的一部分,因此包含多个段的报表必然占用更多磁盘存储空间。在创建报表后,我们也无法对报表内的细分内容进行添加或删除。此外,尽管能够快速转发FuzzyRowwFilter,但对于基数很高的报表及大量数据而言,整个过程仍然非常缓慢。相信通过在协处理器中添加并发机制,从而并发执行对每种salt的扫描(甚至按日期进行分区扫描),能够有效解决这个问题。

这套架构利用协处理器执行读取,但不支持利用协处理器复制读取内容。接下来,我们可以通过将结果存储在高可用性表中实现对缓存层的聚合,从而在一定程度上弥补这种协处理器复制能力缺失问题。另外,我们还会在无法从主区域处读取数据时,执行副本读取操作(利用常规Hbase扫描,不涉及协处理器)。

我们计划在下一次迭代当中解决一部分局限性问题。此外,我们还计划添加对前N项、百分位、按…分组以及min/max等函数的支持。

鸣谢:

感谢Rob Claire、Chunyan Wang、Justin Mejorada-Pier以及Bryant Xiao为PinalyticsDB支持工作做出的贡献。同样感谢分析平台与存储/缓存团队为Pinalytics Web应用以及HBase集群提供的宝贵支持。

原文链接:

https://medium.com/pinterest-engineering/pinalyticsdb-a-time-series-database-on-top-of-hbase-946f236bb29a

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/zi4qpxakG2feGvWQRIhS
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券