概要:天猫双11对于零售通团队来说也是全年最大的一场战役,数据响应需要更实时,但也会相应增加更多的个性化指标,业务面临的挑战也会更大。本文将会讲述阿里巴巴零售通数据平台如何优化Hologres实时数仓,达到性能提升10倍+的效果,完美支撑双11营销活动、实时数据大屏等核心场景。也希望通过此文对Hologres新用户起到一定的帮助作用,通过合理的数仓设计实现事半功倍的性能效果。
作者:
曹泰铭(潇铭) 阿里巴巴数据技术产品部-B系数据业务-零售通数据-高级数据工程师
汪宇(旋宇) 阿里巴巴数据技术产品部-B系数据业务-零售通数据-高级数据工程师
背景
阿里巴巴零售通团队是阿里巴巴B2B事业群针对线下零售小店(便利店/小超市)推出的一个为城市社区零售店提供订货、物流、营销、增值服务等的互联网一站式进货平台,实现互联网对线下零售业的升级,同时也为有志于线上线下零售业的创业群体提供创业平台。
整个平台的架构图如下所示,是一个流批分离且以离线为主的架构。零售通数据团队负责对离线MaxCompute数仓和实时Flink数仓的建设和维护,对内部小二和外部生态伙伴提供决策支持,数据服务和数据产品建设。
因为Hologres与MaxCompute有着极好的兼容性,并且能够对MaxCompute毫秒级加速,财年初零售通数据团队作为业务数据中台基于Hologres在业务上进行了大量的尝试,包括实时数仓公共层,冷热混合计算,查询加速等场景,因为Hologres的加入,相同的需求量,以前的架构需要2天才能完成,而现在在2小时内就能完成,大大提升了开发效率,得到了研发们的一致好评。
天猫双11对于零售通团队来说也是全年最大的一场战役,数据响应需要更实时,但也会相应增加更多的个性化指标,业务面临的挑战也会更大。基于Hologres日常在业务的优秀表现,团队也决定使用Hologres作为双11核心开发产品,应用于双11核心场景包括营销活动中心,实时数据大屏等。
在10月份对全链路进行了几次压测,Hologres在100%压测压力下CPU和内存资源在100%线上徘徊,属于压线通过。本以为能顺利通过大促考验,但在2020-11-01这天,在大量查询QPS和高并发的数据写入峰值下,Hologres的RT延迟一度达到90秒,没有达到预期。在Hologres团队的紧急支持下,通过整体结构的调整以及相关性能调优,整体性能提升了10倍+,顺利扛住2020-11-01的流量洪峰,同时通过此次调整也平稳的支持了整个双11期间的流量洪峰(包括2020-11-11当天的波峰),为双11划下了圆满句号。
事后我们针对整个事件做了全链路复盘,发现主要问题还是出在对于Hologres的原理了解不够深入,包括技术原理、Table Group、表结构设计等,导致某些用法还不是最优,这也导致在实际业务场景中,Hologres的性能没有发挥到最大化。
我们将会通过此文讲诉阿里巴巴零售通团队如何根据业务场景合理的设计Hologres实时数仓,包括表结构、Table Group设计等,以到达更优的性能表现。也希望通过此文对Hologres新用户起到一定的帮助作用,通过合理的资源利用实现事半功倍的性能效果。
11月1日现场还原
首先我们先来还原一下11月1日的现场反馈,以更直观的方式来看看当时Hologres承受的压力和相关表现(注:监控页面为Hologres在阿里内部的监控页面,与公有云的监控页面和指标项略有不同):
查询QPS: 每秒完成query的数量,一般代表数据库性能和压力,23:30后的增长代表压力负荷增长(业务方开始大量关注/查询报表),00:15分的骤降代表性能降低,1点之后的降低代表业务水位下降。
查询延迟(RT):15分左右陡升到25s,大概持续15分钟;Hologres超负荷运作,性能下降。
CPU使用率:代表正在运转的core数量。我们的实例有几百个core用于计算,但在峰值严重超负载50%+;
Woker CPU使用率:Woker的CPU负载情况,和CPU使用率基本一致,峰值超负载50%+。
这两个指标代表大量Query将会无法及时处理,在队列中等待,延迟将会较大增加,实例也有停摆的风险。
Blink写入RPS:代表每秒实时数据的写入量,0点是实时订单写入的峰值。当时有较多数据导入,对Worker产生较大的负荷,影响实例性能(比如RT,CPU使用率等),所以对部分blink写入脚本采取紧急降级操作,峰值过后1:10恢复降级,产生第二波较大的写入。
问题定位和优化手段
在Hologres团队的帮助下,经过反复的排查,发现问题主要有以下几个原因:
1)主要问题: Table Group&Shards数设置不合理,在高并发读取和写入峰值时,造成集群资源浪费和性能下降。
零售通Hologres实例总计有几百core的计算资源,几TB的内存资源,和十几TB的存储资源。在实例创建后会根据实例规格生成默认数量的Table Group,然后每个Table Group会自动创建对应的Shard数(按照现有的规格,Shard数有300个),实例中存储的表都会被默认分发至这些Shard中。
开发视角的Table Group和Shard
首先,先从开发者视角来理解一下Table Group和Shard的相关概念和原理:
在Hologres中,1个DB包含多个Table Group,每个Table Group包含多个Table,每个Table只能属于一个Table Group。一个Table Group唯一对应一组Shard,由这组Shard来负责其中表的数据存储和查询,其包含的Shard个数称为Shard Count,Table Group一旦建立,Shard数不可调整。
一个Table Group拥有的Shard数量(即Shard Count,后同)是它的一个重要属性。Shard数多的Table Group,其数据写入和查询分析处理可以得到更大的并行度,一定范围内,增大Shard数可以加快数据写入和查询分析的速度,但Shard数也并非越多越好,更多Shard数需要更多的节点间通信资源、计算资源以及内存资源,在资源不满足的时候,或者Query很小时可能会导致适得其反的效果。
再结合具体的业务场景,当一个双表Join的SQL执行时,按照现有Shard数,执行计划会产生一个300*300 Shard的笛卡尔积(两张表都被分散300份),这在shuffle阶段对CPU和内存产生巨大的压力,也就意味着需要更多的节点间通信资源、计算资源以及内存资源。除此之外,单表的数据随机分散到300个Shard的过程中容易出现数据倾斜的问题,如下图所示
设置Table Group和Shard建议
零售通团队的业务场景大都是数据量偏少、表大小也非常不均匀,对于保障的优先级也不一致,所以上面将的所有表都放在一个300个Shard的Table Group中对实际业务并不适用,这就导致当流量增大时,没有办法有效利用Hologres的Local Join能力,导致CPU的开销剧增。
正确的做法是按照使用场景、Join频次、表大小,分裂设计成不同的Table Group中存放数据;一方面可以提升集群性能提高计算速度,另一方面可以节省资源同时一定程度上实现资源合理分配隔离。
新建Table Group的原则:
-
数据量过大,可新建独立的较大Shard数的Table Group。当察觉写入性能变慢 or 读取200万行一个Shard时建议新建Table Group;
-
有大量数据量很小的表,可适当独立出一个小Shard数的Table Group,减小Query启动开销;
-
需要Join的表,尽量放在同一个Table Group;
为Table Group设置合理的Shard数:Shard数不是越大越好,过大的Shard数会造成资源的浪费&负载过高,过小的Shards数会导致大数据量下读写性能不足以及不能抵挡较大的并发,下面也是经验总结出来的Shard数设置(仅供参考,实际需要根据实例规格和业务要求来设置)
-
查询性能要求:若是业务对于查询要求较高,Shard数的设置是表在SQL中扫描的分区范围内的总行数的均值/200万 = 1 Shard
-
写入性能需求:Shard数和数据写入性能呈一定的正相关性,单个Shard的写入能力是相对固定的,Shard越多,写入的并发越多,写入的吞吐越高。因此,如果表有较高RPS的写入需求,需要增大Shard数。
具体完整的计算方法参考 如何选择合适的Shard数,也可以咨询Hologres技术小哥。
Table Group重构设计
针对Table Group的设计,零售通对当前Hologres实例的期待是作为分析型OLAP实时数据库,在财年不扩充资源的情况下对现有表和业务需求梳理后有以下诉求:
-
存放数仓中公共明细层,公共汇总层,维表;并能对公共层较明细的数据进行快速分析&开发(Local Join);表单分区一般在数千万行量级
-
营销活动分析数据产品的接口层存储,表格行数较少一般在单分区3000行以下,表格数量较少(10个以下)的Local Join,并发较高但不太会和外部表格频繁关联
-
经营管理中心数据产品的接口层存储,表格行数较大,单分区可以到4亿行,不需要in,一般做灵活多维度汇总,并发较低
-
商品评估中心数据产品的接口层存储,表格行数单分区千万行,一般不需要Join,不需要汇总,但需要根据条件在where中进行明细筛选
最后决定设立4个Table Group:
-
公共层TG: 存放维表,明细表,公共汇总表,Local Join 以及汇总,分配较多资源
-
营销+大屏TG:大屏及营销活动应用,数据量较少,主要用于历史对比,实时应用并发读写较大;采用20个Shard, 之前300个Shard是极大的资源浪费
-
经营管理中心TG:经营管理中心各明细粒度表格,设置50个Shard
-
商品or行业TG:存放商品和类目结果数据,设置40个Shard
2)次要问题1:表结构设计
数仓建设中,最重要的一个环节就是合理的设计表结构,包括表的数据类型、表的索引等。尤其是索引,合理的索引设计将会提高几倍甚至几十倍的性能。通过重新梳理表结构,发现业务并没有合理的设置表索引,这是导致性能不符合预期的原因之一,于是我们也对索引进行了改造。值得注意的是,当前和数据布局有关的索引的建立必须要在建表初期完成,后面不可以更改/新增,独立于数据布局的索引,比如bitmap,可以后面再按需修改。所以需要提前根据场景设计好表结构,以免做重复工作。
distribution_key
-
如果创建了Primary Key索引(也是唯一性约束,用于数据更新),默认为distribution_key。Distribution_key如果为空,默认是随机分发。
-
如果distribution_key设置不合理,数据会不均匀分布于Shard中。计算过程中会产生Redistribute Motion算子数据重新分布打散,带来冗余的网络开销。 如设置合理,则可以避免这种情况。
-
通常设置关联(Join)的列或Group by的列或分散更随机的列作为distribution_key,来尽量打散数据到不同的Shard。请注意这里选择单列作为distribution_key即可。
Segment_key
分段键,用于文件块的边界划分,查询时基于Segment_key可以快速定位数据所在文件块,选择与写入时间戳相关的字段在查询时有加速的效果。一般用于时间戳这样的时序数据,Segment_key通常只用一列,遵循左对齐原则。Segment_key使用的限制比较多,要求文件在向Hologres写入时是按照Segment_key的顺序排序完成后再写入,即select后按照Segment_key进行order_by再写入,才会生效;一般适用于纯实时写入的自增/类自增字段(e.g.下单时间)。
Clustering_key
聚簇索引,是文件内的排序列,用于范围查询(RangeQuery)的快速过滤。与MySQL的聚簇索引不同,Hologres用来布局数据,不是布局索引,因此修改Clustering_key,需要重新数据导入,且只有一组Clustering_key,一般Clustering_key不超过2列。通常建议将where条件里面的筛选列设置为Clustering_key
Bitmap
位图索引,对于等值过滤场景有明显的优化效果,多个等值过滤条件,通过向量高效计算; 适用于哑变量(基数低)的列,相当于哑变量一列变多列的实现。
Dictionary_encoding
字典编码列索引,可以将字符串的比较转为数字的比较,对于字符串类型可以有效压缩,特别是基数低的列,达到加速Group by,Filter的效果。Hologres在建表时会自动给text类型加上Bitmap索引和字典编码列索引,以实现更优的性能,但是需要注意的是,在不满足需要的场景下需要根据业务场景添加或删除相应的索引,因为dictionary_encoding会消耗编码解码的资源。
下图是Hologres索引匹配原则,可以通过该图了解一下索引的执行原理:
- 可以通过执行analyze参数,来获取表的统计信息,帮助Hologres在读取计算时将执行计划优化。
analyze tablename;
下面结合具体的示例展示怎样优化表结构:
table1是一个数千万到亿行的明细表,对其他表(维表)有频繁Local Join的需求,和较大的并发写入;
1)根据业务查询和写入需求,将表放在公共层的Table Group中并分配60个Shard来满足读写需求。
2)因为是明细表,有大量的关联和等值/筛选场景,添加了较为全面索引配置:
- 分析场景应用较多,有大量聚合,Group by操作,选择列存。
- distribution_key:正常来讲应该满足常用Join的列+能尽量分散的列作为distribution_key;这张表作为明细表,很多列都会用于关联,所以不太好选一个key出来,选多个key的话反而会造成性能下降(要全部key都被使用才有效),最后决定选择较符合条件的id3作为distribution_key;
- 这里像id1和id2是哑变量字段,适合同时配置Bitmap索引和字典编码列索引,方便Group by 和等值查询;
- 对于日期(ds)设置为分区字段。明细表在查询和使用时日期都是必不可免的字段,通过设置分区,可以有效缩小每次查询的扫描范围;另一方面也可以较安全的进行运维和排查问题。
- 当前表不适用Segment_key,因数据离线/实时两种插入模式,排序成本较高,暂不设置。
- 对于Clustering_key,按照使用频次,目前的选择是id1+id2。
- 最终DDL如下(因涉及业务敏感数据,只展示部分DDL):
BEGIN;
CREATE TABLE public.table1 (
"stat_date" text,
"id0" text,
.....,
"id3" text,
"id2" text,
"name2" text,
...,
"id1" text,
....,
"ds" text NOT NULL
)
PARTITION BY LIST (ds);
--如果是用来新建TableGroup,则需要下面第一句,已有TableGroup则不需要
call set_table_property('table1', 'shard_count', '60');
CALL set_table_property('table1', 'distribution_key', 'id3');
CALL SET_TABLE_PROPERTY('public.table1', 'orientation', 'column');
CALL SET_TABLE_PROPERTY('public.table1', 'clustering_key', 'id1,id2');
CALL SET_TABLE_PROPERTY('public.table1', 'bitmap_columns', '...,id1,id2,...');
CALL SET_TABLE_PROPERTY('public.table1', 'dictionary_encoding_columns', '...,id1,id2,...');
CALL SET_TABLE_PROPERTY('public.table1', 'time_to_live_in_seconds', '7776000');
COMMIT;
3)次要问题2:应用缓存
对于重要高频报表添加合适的缓存来缓解数据库压力,离线报表可以设置时间较长的缓存,实时报表可以考虑在应用端增加 5s, 10s, 30s,1min等多个档位的缓存。
4)次要问题3:不合理压测计划
在之前几次全链路压测中,对于Hologres实例进行读和查的多方面压测,虽然压测读的量到位了,但是没有同步压测数据库写入峰值,在实际场景中读的性能会受到写入数据洪流的压力和影响;尤其Hologres存在两种主要的写入方式(外表同步内表,实时写入内表);在压测和实际使用的过程中需要特别注意读写峰值一起压测。
优化后业务效果
通过优化后,在双11这天0点的流量高峰期,在0点写入和Query读取同时达到业务峰值的情况下,Hologres支持的数据产品的RT平均响应时间稳定在100ms左右,为使用数据产品的业务同学/分析同学,在双11提供稳定毫秒级的实时OLAP决策数据支持。同时也非常平稳的支持了营销活动中心&实时大屏等核心的高并发业务产品,以及BI同学实时取数分析等场景,CPU水位稳定在30%以下,内存水位也稳定在50%以下。
同时通过本次天猫双11,我们也发现Hologres作为实时数据存储,在分析方面有巨大的潜力,在满足写入性能的同时,一方面可以和现有离线数据关联分析,另一方面是能支持高性能的OLAP分析数据。这也团队后续使用Hologres作为数据团队新实时数仓架构的核心组件奠定了基础。
后续规划
经过双11之后,研发团队下个阶段将利用Hologres进行更大范围的实时数仓改造:
- Hologres作为行存实时公共层(替代之前timetunnel作为新中间件)开放下游数据库订阅, 保持对内整个架构和对外多个架构的数据一致性,以及解决实时结果数据在timetunnel中不可见,二次操作成本高的痛点。
- 下游应用层订阅公共层实时数据,应用层数据按照保障级别和local_Join的需要进行实例级别分割,资源隔离。
举例:以渠道数据化实例为例,这部分数据大部分对外开放给CRM系统和生态三方合作伙伴,对一致性,及时性和并发都有较高的要求,容易出现数据故障;在数据层面上也会有较频繁的Local Join诉求;综合来看作为单独实例分割,包给予充足的资源保障。