供应链计划性能优化解决方案-Clickhouse本地Join

作者:京东零售 姜波

前言

本文主要针对供应链计划业务发展过程中系统产生的瓶颈问题的解决方案进行阐述,并且分享一些问题解决过程中用到的一些工具方法,希望对其他业务同类问题提供启发,原理细节不着重介绍,如有兴趣欢迎一起探讨。

业务背景

供应链计划业务目前数据库主要使用了Tidb和Clickhouse,Tidb用于存储计划数据、维度数据、业务配置等数据,Clickhouse用于存储量级比较大的历史参考数据。随着业务发展,业务需要在某些场景下对这些历史数据做一些过滤或配置一些业务Tag,并且这些过滤条件和业务Tag需要支持更新、删除。最初我们的解决方案中部分配置的生效方案是将这些配置存在Tidb,然后通过离线抽数到离线大数据表,然后在离线大数据平台对历史数据进行处理后推送到Clickhouse再使用,但是这样业务的生效周期就是T+1,对业务使用的体验非常不友好。还有一些配置生效方案是将Tidb的业务配置和Clickhouse中的历史数据全部读取出来,在实例的内存中进行聚合处理,这种解决方案会导致实例的内存不够用,经常OOM,影响系统稳定性。而且在内存中处理这些数据的逻辑比较重,致使某些场景下的查询非常慢,个别情况一次查询响应时长会达到10秒以上,严重影响用户使用体验。

在这里插入图片描述

解决方案

针对以上生效周期长、聚合查询内存占用大、查询慢这些问题,我们在实验后发现,无论实在Tidb还是在Clickhouse中,通过sql聚合查询直接输出聚合结果,要比在内存中聚合要快很多,而且对db也不会造成很大的压力,其效果大概是在内存中执行5秒的逻辑,等比转化到sql中,大约300ms就可以输出结果,这中间涉及IO传输、DB的聚合优化、索引等,具体原理不做过多阐述,有兴趣自行网上查阅即可。 主要优化方向就是将业务配置数据同步到Clickhouse中一份,然后分别在Tidb和Clickhouse中join输出结果数据。

在这里插入图片描述

解决方案关键技术分享

1.Clickhouse ReplacingMergeTree建表及维表模式

最初我们查阅官方文档后,决定使用ReplacingMergeTree,然后在使用时使用final关键字保证数据去重,官方文档:https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replacingmergetree,官方文档的建表和查询示例:

在这里插入图片描述



官方文档的示例过于简略了,相当于“Hello Word”,并不能满足我们实际使用需求,想要实际在生产环境应用,需要建成下面这样:

1CREATE TABLE IF NOT EXISTS 库名.blacklist ON CLUSTER xx 2 ( 3 `dept_id_1` Int32 COMMENT '一级部门ID', 4 `dept_id_2` Int32 COMMENT '二级部门ID', 5 `dept_id_3` Int32 COMMENT '三级部门ID', 6 `saler` String COMMENT '销售erp', 7 `pur_controller` String COMMENT '采控erp', 8 `update_time` DateTime COMMENT '更新时间', 9 `is_deleted` UInt8 COMMENT '有效标识 0:标识未删除 1:表示已删除' 10 ) 11ENGINE = ReplicatedReplacingMergeTree('/clickhouse/xx/jdob_ha/sop_pre_mix/blacklist/{shard_dict}', '{replica_dict}', update_time) 12ORDER BY (dept_id_3, saler, pur_controller) 13SETTINGS storage_policy ='jdob_ha';

关于上面的建表语句,有几个要点需要解析一下

第一,ReplacingMergeTree需要写成ReplicatedReplacingMergeTree,这个参考了京东ck运营文档里的解释

在这里插入图片描述



第二,【'/clickhouse/xx/jdob_ha/库名/blacklist/{shard_dict}', '{replica_dict}',】 这个信息不用太关注,是京东存储元数据的key,符合范式即可;

第三,后面跟的update_time ORDER BY,这个有点意思,以上面那个表举例,就是在用final查询 或者 Optimize手工合并时,用ORDER BY中的dept_id_3, saler, pur_controller作为唯一业务主键,用update_time排序,保留最后一条

第四,当前建表方式属于维表模式,维表模式简单说就是每个节点存储一份全量数据。对于维表,如果使用分布式表使用join会有remote查询,节点之间的通讯会增加sql耗时。后面说为什么选择使用维表模式。



2.Clickhouse本地join

今天我们主要说一说本地Join,为什么主要说它呢,第一因为它好,第二因为这次我用了😋



本地join的优势

在Clickhouse中,与本地join对应的是Global Join,就拿最简单的两表join来说,Clickhouse的执行流程是:左表,先在每个分布式节点查一次,然后将查询结果远程传输到聚合节点上,右表,也是先在每个分布式节点查一次,然后将查询结果远程传输到聚合节点上,再在聚合节点上join两份结果,最终输出;本地join是在每个分布式节点上进行join,然后将每个节点的join结果远程传输到聚合节点上,合并结果最终输出;两者对比,本地join的性能和资源开销都远超Global Join。

本地join对于数据散列方式的要求

如果是两张分布式表,那就要保证分布函数要完全一致,举个例子,我们平时建分布式表的语句如下:

1-- 分布式表 2CREATE TABLE IF NOT EXISTS sop_pre_mix.history ON CLUSTER xx AS sop_pre_mix.history_local ENGINE = Distributed 3( 4 'xx', 5 'sop_pre_mix', 6 'history_local', 7 rand() 8) ;

分布函数就是这个rand(),如果想要对两个分布式表使用本地join,就要保证这个分布函数对于维度相同的数据,算出来完全一致的结果,rand()肯定是不能用,可以参考一致性hash算法。

我们本次使用的场景,左表是一个分布式表,是纯散列的,右表我建成了维表,维表上面我们也写了它的形式,每个节点存储一份全量数据,刚好命中可以使用本地join的场景。



查询语法的要求

另外,本地join对于sql语法上也有要求,我就踩了一下语法的这个坑

在京东Clickhouse运维文档里摘录了一下精华,本地 Join是用 a.dis join b.local,对于左表a(分布式表)的过滤条件需要写成 select * from a.dis join b.local where a.cond1(千万不能写成子查询的 如 select * from (select * from a.dis where a.cond1 )join b.local ),这样没法正确的执行本地join,使得查询结果不正确,对于b 本地表的过滤条件则需要放到子查询中,那么正确的样式应该是select * froma.dis join (select * from b.local where b.cond2) where a.cond1.

语法踩坑:低版本引擎的Clickhouse,本地join时,维表需要带库名前缀,不然执行算子会报找不到表的错误,对应下面sql片段【sop_pre_mix.sop_sale_plan_rule_core_dim】,这里必须写上sop_pre_mix



展示一下本地join的最终呈现

左表的建表语句(分布式表模式),数据量约2.9亿

1CREATE TABLE IF NOT EXISTS sop_prod_mix.sop_sale_history_week_local ON CLUSTER xx 2( 3 dept_id_1 Int32 COMMENT '一级部门id', 4 dept_name_1 String COMMENT '一级部门名称', 5 ……一堆维度指标字段,略 6 sale_amount_lunar_sp Decimal(20, 2) COMMENT '同期自营销售出库金额', 7 dt String COMMENT '数据日期' 8) 9ENGINE = ReplicatedMergeTree 10 ( 11 '/clickhouse/LFRH_CK_Pub_115/jdob_ha/sop_prod_mix/sop_sale_history_week_local/{shard}', 12 '{replica}' 13 ) 14PARTITION BY dt 15ORDER BY (dept_id_1,dept_id_2,dept_id_3,saler,pur_controller,cate_id_3,ym,ymw) 16SETTINGS storage_policy = 'jdob_ha', 17 index_granularity = 8192; 18 19 20-- 分布式表 21CREATE TABLE IF NOT EXISTS sop_prod_mix.sop_sale_history_week ON CLUSTER xx AS sop_prod_mix.sop_sale_history_week_local ENGINE = Distributed 22 ( 23 'xx', 24 'sop_prod_mix', 25 'sop_sale_history_week_local', 26 rand() 27 ) ;



右表的建表语句(维表模式),数据量约4500+条

1CREATE TABLE IF NOT EXISTS sop_pre_mix.sop_dim_blacklist ON CLUSTER xx 2 ( 3 `dept_id_1` Int32 COMMENT '一级部门ID', 4 `dept_id_2` Int32 COMMENT '二级部门ID', 5 `dept_id_3` Int32 COMMENT '三级部门ID', 6 `saler` String COMMENT '销售erp', 7 `pur_controller` String COMMENT '采控erp', 8 `update_time` DateTime COMMENT '更新时间', 9 `is_deleted` UInt8 COMMENT '有效标识 0:标识未删除 1:表示已删除' 10 ) 11ENGINE = ReplicatedReplacingMergeTree('/clickhouse/xx/jdob_ha/sop_pre_mix/sop_dim_blacklist/{shard_dict}', '{replica_dict}', update_time) 12ORDER BY (dept_id_3, saler, pur_controller) 13SETTINGS storage_policy ='jdob_ha';

命中本地join的查询sql

此sql只是动态sql生成的其中一种,实际项目中动态场景很多,主要依赖mybatis动态sql实现

1SELECT 2 a.ymw AS ymw, 3 a.dept_id_2 AS dept_id_2, 4 a.dept_id_3 AS dept_id_3, 5 a.week AS week, 6 a.cold_type AS cold_type, 7 a.year AS YEAR, 8 a.net_type AS net_type, 9 a.saler AS saler, 10 a.pur_controller AS pur_controller, 11 a.dept_id_1 AS dept_id_1, 12 a.month AS MONTH, 13 a.ym AS ym, 14 CASE 15 WHEN a.dept_id_1 != c.dept_id_1 16 OR c.dept_id_1 IS NULL 17 THEN - 100 18 ELSE c.core_dim_id 19 END AS core_dim_id, 20 CASE 21 WHEN a.dept_id_1 != c.dept_id_1 22 OR c.dept_id_1 IS NULL 23 THEN - 100 24 ELSE c.core_dim_id 25 END AS brand_id, 26 SUM(initial_inv_amount) AS initial_inv_amount, 27 ……一堆指标,略 28 SUM(gmv_lunar_sp) AS gmvLunarSp 29FROM 30 sop_pur_history_week a 31LEFT JOIN 32 ( 33 SELECT 34 dept_id_2, 35 dept_id_3, 36 pur_controller, 37 saler 38 FROM 39 sop_pre_mix.sop_dim_blacklist final 40 WHERE 41 is_deleted = 0 42 AND dept_id_3 IN(12345, 23456,……) 43 ) 44 b 45ON 46 a.dept_id_3 = b.dept_id_3 47 AND a.pur_controller = b.pur_controller 48 AND a.saler = b.saler 49LEFT JOIN 50 ( 51 SELECT 52 dept_id_1, 53 dept_id_2, 54 dept_id_3, 55 pur_controller, 56 saler, 57 core_dim_id 58 FROM 59 sop_pre_mix.sop_sale_plan_rule_core_dim final 60 WHERE 61 is_deleted = 0 62 AND dept_id_3 IN(12345, 23456,……) 63 AND core_dim_id IN(12310) 64 AND plan_dim = 'brand' 65 ) 66 c 67ON 68 a.dept_id_1 = c.dept_id_1 69 AND a.dept_id_2 = c.dept_id_2 70 AND a.dept_id_3 = c.dept_id_3 71 AND a.saler = c.saler 72 AND a.pur_controller = c.pur_controller 73 AND a.brand_id = c.core_dim_id 74WHERE 75 dt = '2023-12-16' 76 AND a.dept_id_3 IN(12345, 23456,……) 77 AND a.brand_id IN(12310) 78 AND 79 ( 80 a.dept_id_3 != b.dept_id_3 81 OR b.dept_id_3 IS NULL 82 ) 83GROUP BY 84 a.ymw, 85 a.dept_id_2, 86 a.dept_id_3, 87 a.week, 88 a.cold_type, 89 a.year, 90 a.net_type, 91 a.saler, 92 a.pur_controller, 93 a.dept_id_1, 94 a.month, 95 a.ym, 96 c.dept_id_1, 97 core_dim_id

走本地join和不走本地join对资源开销的差别 在这里插入图片描述

很明显能看到走本地join的查询行数和内存占用是小了很多的,我们测试用的这个集群是9分片18节点的,就节约了一倍多的资源……分片越多效果越明显

在这里插入图片描述

****

3.Tidb到Clickhouse准实时同步链路

这部分见我另外一篇神灯文章,TiCDC接入JDQ实践: http://sd.jd.com/article/41284?shareId=54243&isHideShareButton=1



最终优化效果

首先,解决了常规查询情况下实例经常OOM的问题;

其次,对于查询性能也有了稳定的提升



下面是我在测试环境对比的一些实验数据:(线上实际性能比图中都要好)

 在这里插入图片描述

点赞
收藏

评论区

加载中...

相关推荐

VOP 消息仓库演进之路|如何设计一个亿级企业消息平台

VOP作为京东企业业务对外的API对接采购供应链解决方案平台,一直致力于从企业采购数字化领域出发,发挥京东数智化供应链能力,通过产业链上下游耦合与链接,有效助力企业客户的成本优化与资产效能提升。本文将介绍VOP如何通过亿级消息仓库系统来保障上千家企业KA客户与京东的数据交互。

AI、IoT、区块链、自主系统、下一代计算五大技术引领未来供应链发展

!(https://static001.geekbang.org/infoq/5d/5d0dc4e3f7593f32ec3000855c80f546.webp)京东推出《技术重构社会供应链未来科技趋势白皮书》,秉承京东一贯专注技术聚焦业务的务实风格,京东对前沿技术洞察紧密围绕京东集团供应链业务的主阵营,解析数智化科技助力下,如何达成数智化社会供

定时任务原理方案综述 | 京东云技术团队

本文主要介绍目前存在的定时任务处理解决方案。业务系统中存在众多的任务需要定时或定期执行,并且针对不同的系统架构也需要提供不同的解决方案。京东内部也提供了众多定时任务中间件来支持,总结当前各种定时任务原理,从定时任务基础原理、单机定时任务(单线程、多线程)、分布式定时任务介绍目前主流的定时任务的基本原理组成、优缺点等。希望能帮助读者深入理解定时任务具体的算法和实现方案。

京东门详一码多端探索与实践 | 京东云技术团队

本文主要讲述京东门详业务在支撑过程中遇到的困境,面对问题我们在效率提升、质量保障等方向的探索和实践,在此将实践过程中问题解决的思路和方案与大家一起分享,也希望能给大家带来一些新的启发

分拣平台API安全治理实战 | 京东物流技术团队

导读本文主要基于京东物流的分拣业务平台在生产环境遇到的一些安全类问题,进行定位并采取合适的解决方案进行安全治理,引出对行业内不同业务领域、不同类型系统的安全治理方案的探究,最后笔者也基于自己在金融领域的经验进行了关于API网关治理方案的分享。写在前面随着互

2024了,我不想再用AOP收集业务操作日志了 | 京东云技术团队

0.背景在近期的项目中,系统涉及到针对系统的业务操作日志统计功能,由于本系统位于业务链路的中心环节,负责接收上游系统的数据,并将基于用户操作产生的数据传递至下游系统,鉴于业务链路的复杂性和操作场景的多样性,我们计划通过对核心业务数据进行全生命周期的日志记录