背景
在信息时代时代,数据量呈指数级增长,高效的数据处理和分析能力已经成为企业竞争力的关键。在实际业务中,用户会基于不同的产品分别构建实时数仓和离线数仓。其中,实时数仓强调数据能够快速入库,且在入库的第一时间就可以进行分析,低时延的返回分析结果。而离线数仓强调复杂任务能够稳定的执行完,需要更好的内存管理。
在联机分析处理(OLAP)领域,ClickHouse是一个成熟开源的列式数据库管理系统(DBMS),它有着高性能、可扩展性、数据压缩、SQL支持的特征,广泛应用于数据分析商业智能、日志分析等场景。
但是ClickHouse也有它自身的局限性,比如扩缩容成本高、复杂查询性能受限,鉴于这个原因,字节跳动基于ClickHouse内核开发并开源了ByConity。
ClickHouse从设计之初是面向OLAP(在线分析)场景,无论是列存、索引还是执行向量化的优化,他们都有效地应对大宽表的聚合计算。
针对复杂查询,尤其是数据仓库中典型的ETL任务来说,ClickHouse则并不擅长。结构复杂、耗时较长的数据加工作业,通常需要复杂的调优过程。典型的问题如下:
- 重试成本高:对于运行时长在分钟级甚至小时级的ETL作业,如果运行过程中出现失败,ClickHouse只能进行query级别的重试。从头重试不仅造成大量的资源浪费,也对加工任务的SLA提出了挑战。
- 资源占用巨大:由于缺少迭代计算和有效的任务拆分,在查询数据量大、计算复杂的情况下,通常要求节点有充足的内存进行处理。
- 并发控制:当多个查询同时运行时,ClickHouse并不会根据资源的使用情况进行调度。任务之间相互挤压会导致失败(通常是Memory limit错误)。叠加重试机制的缺乏,通常会引起雪崩效应。
针对以上问题,ByConity在ClickHouse高性能计算框架的基础上,增加了对bsp模式的支持:可以进行task级别的容错;更细粒度的调度;支持资源感知的调度。带来的收益有:
- 当query运行中遇到错误时,可以自动重试当前的task,而不是从头进行重试。大大减少重试成本。
- 当query需要的内存巨大,甚至大于单机的内存时,可以通过增加并行度来减少单位时间内内存的占用。只需要调大并行度参数即可,理论上是可以无限扩展的。
- 可以根据集群资源使用情况有序调度并发ETL任务,从而减少资源的挤占,避免频繁失败。
ByConity官网:byconity.github.io/zh-cn/docs/…
ByConity测试
测试环境
| 版本 | 配置 | |
| ByConity v1.0.1 | 集群规格 | Worker:4 x 16core 64G Server:1 x 16core 64G TSO:1 x 4core 16G Daemon Manager:1 x 4core 16G Resource Manager:1 x 8core 32G 存储:对象存储 TOS FoundationDB:3 x 4core 16G |
测试准备
- 打开终端,输入
ssh -p 23 <提供的用户名>@<ECS服务器IP地址>,输入密码; -
- 为避免使用时超时自动断开连接,请运行
tmux new -s $user_id(如tmux new -s user0001)命令创建一个新的tmux会话,其中$user_id是可以自定义的会话名称。(后续重新登录时,使用tmux a -t $user_id)。
- 为避免使用时超时自动断开连接,请运行
-
- 执行
clickhouse client --port 9010命令进入客户端。如果后续输入 SQL 会被截断,在此处可以执行clickhouse client --port 9010 -mn,此后 SQL 后需要加;作为结束。
- 执行
- 使用测试用数据库 test_elt:
use test_elt - 由于TPC-DS定义的查询语法为标准 SQL,设置数据库会话的方言类型为 ANSI:
set dialect_type = 'ANSI'
查询测试
基础联合查询
从数据表中找出在 2000 年有退货记录、退货总金额超过同店铺其他客户平均退货金额 1.2 倍且店铺位于田纳西州的前 100 个客户的编号,并按客户编号升序排列返回结果。SQL语句如下:
with customer_total_return as
(
select
sr_customer_sk as ctr_customer_sk,
sr_store_sk as ctr_store_sk
,sum(sr_return_amt) as ctr_total_return
from store_returns, date_dim
where sr_returned_date_sk = d_date_sk and d_year = 2000
group by sr_customer_sk,sr_store_sk)
select c_customer_id
from customer_total_return ctr1, store, customer
where ctr1.ctr_total_return > (
select avg(ctr_total_return) *1.2
from customer_total_return ctr2
where ctr1.ctr_store_sk = ctr2.ctr_store_sk
)
and s_store_sk = ctr1.ctr_store_sk
and s_state = 'TN'
and ctr1.ctr_customer_sk = c_customer_sk
order by c_customer_id
limit 100;
执行效果如下:
查询速度还是很快的。
大内存查询
with ws as
(select d_year AS ws_sold_year, ws_item_sk,
ws_bill_customer_sk ws_customer_sk,
sum(ws_quantity) ws_qty,
sum(ws_wholesale_cost) ws_wc,
sum(ws_sales_price) ws_sp
from web_sales
left join web_returns on wr_order_number=ws_order_number and ws_item_sk=wr_item_sk
join date_dim on ws_sold_date_sk = d_date_sk
where wr_order_number is null
group by d_year, ws_item_sk, ws_bill_customer_sk
),
cs as
(select d_year AS cs_sold_year, cs_item_sk,
cs_bill_customer_sk cs_customer_sk,
sum(cs_quantity) cs_qty,
sum(cs_wholesale_cost) cs_wc,
sum(cs_sales_price) cs_sp
from catalog_sales
left join catalog_returns on cr_order_number=cs_order_number and cs_item_sk=cr_item_sk
join date_dim on cs_sold_date_sk = d_date_sk
where cr_order_number is null
group by d_year, cs_item_sk, cs_bill_customer_sk
),
ss as
(select d_year AS ss_sold_year, ss_item_sk,
ss_customer_sk,
sum(ss_quantity) ss_qty,
sum(ss_wholesale_cost) ss_wc,
sum(ss_sales_price) ss_sp
from store_sales
left join store_returns on sr_ticket_number=ss_ticket_number and ss_item_sk=sr_item_sk
join date_dim on ss_sold_date_sk = d_date_sk
where sr_ticket_number is null
group by d_year, ss_item_sk, ss_customer_sk
)
select
ss_sold_year, ss_item_sk, ss_customer_sk,
round(ss_qty/(coalesce(ws_qty,0)+coalesce(cs_qty,0)),2) ratio,
ss_qty store_qty, ss_wc store_wholesale_cost, ss_sp store_sales_price,
coalesce(ws_qty,0)+coalesce(cs_qty,0) other_chan_qty,
coalesce(ws_wc,0)+coalesce(cs_wc,0) other_chan_wholesale_cost,
coalesce(ws_sp,0)+coalesce(cs_sp,0) other_chan_sales_price
from ss
left join ws on (ws_sold_year=ss_sold_year and ws_item_sk=ss_item_sk and ws_customer_sk=ss_customer_sk)
left join cs on (cs_sold_year=ss_sold_year and cs_item_sk=ss_item_sk and cs_customer_sk=ss_customer_sk)
where (coalesce(ws_qty,0)>0 or coalesce(cs_qty, 0)>0) and ss_sold_year=2000
order by
ss_sold_year, ss_item_sk, ss_customer_sk,
ss_qty desc, ss_wc desc, ss_sp desc,
other_chan_qty,
other_chan_wholesale_cost,
other_chan_sales_price,
ratio
LIMIT 100;
综合分析 2000 年店铺销售(store_sales)与网络销售(web_sales)、目录销售(catalog_sales)之间的销售数量、成本、价格等数据关系,筛选出有对应其他渠道销售记录(网络或目录)的店铺销售记录,并按照特定顺序展示前 100 条结果,帮助了解不同销售渠道在该年份针对相同商品和客户的业务情况对比。
执行时报错:
错误原因是内存超限。 接下来利用ByConity的bsp模式执行,Sql语句后面增加下面语句:
SETTINGS bsp_mode = 1, distributed_max_parallel_size = 12;
执行结果如下:
ByConity 增加了 bsp 模式:可以进行 task 级别的容错;更细粒度的调度;基于资源感知的调度。通过 bsp 能力,把数据加工(T)的过程转移到 ByConity 内部,能够一站式完成数据接入、加工和分析。
这就是bsp的魅力,当query需要的内存巨大,甚至大于单机的内存时,可以通过增加并行度来减少单位时间内内存的占用。只需要调大并行度参数即可,理论上是可以无限扩展的。
模拟内存限制问题
with cs_ui as
(select cs_item_sk
,sum(cs_ext_list_price) as sale,sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit) as refund
from catalog_sales
,catalog_returns
where cs_item_sk = cr_item_sk
and cs_order_number = cr_order_number
group by cs_item_sk
having sum(cs_ext_list_price)>2*sum(cr_refunded_cash+cr_reversed_charge+cr_store_credit)),
cross_sales as
(select i_product_name product_name
,i_item_sk item_sk
,s_store_name store_name
,s_zip store_zip
,ad1.ca_street_number b_street_number
,ad1.ca_street_name b_street_name
,ad1.ca_city b_city
,ad1.ca_zip b_zip
,ad2.ca_street_number c_street_number
,ad2.ca_street_name c_street_name
,ad2.ca_city c_city
,ad2.ca_zip c_zip
,d1.d_year as syear
,d2.d_year as fsyear
,d3.d_year s2year
,count(*) cnt
,sum(ss_wholesale_cost) s1
,sum(ss_list_price) s2
,sum(ss_coupon_amt) s3
FROM store_sales
,store_returns
,cs_ui
,date_dim d1
,date_dim d2
,date_dim d3
,store
,customer
,customer_demographics cd1
,customer_demographics cd2
,promotion
,household_demographics hd1
,household_demographics hd2
,customer_address ad1
,customer_address ad2
,income_band ib1
,income_band ib2
,item
WHERE ss_store_sk = s_store_sk AND
ss_sold_date_sk = d1.d_date_sk AND
ss_customer_sk = c_customer_sk AND
ss_cdemo_sk= cd1.cd_demo_sk AND
ss_hdemo_sk = hd1.hd_demo_sk AND
ss_addr_sk = ad1.ca_address_sk and
ss_item_sk = i_item_sk and
ss_item_sk = sr_item_sk and
ss_ticket_number = sr_ticket_number and
ss_item_sk = cs_ui.cs_item_sk and
c_current_cdemo_sk = cd2.cd_demo_sk AND
c_current_hdemo_sk = hd2.hd_demo_sk AND
c_current_addr_sk = ad2.ca_address_sk and
c_first_sales_date_sk = d2.d_date_sk and
c_first_shipto_date_sk = d3.d_date_sk and
ss_promo_sk = p_promo_sk and
hd1.hd_income_band_sk = ib1.ib_income_band_sk and
hd2.hd_income_band_sk = ib2.ib_income_band_sk and
cd1.cd_marital_status <> cd2.cd_marital_status and
i_color in ('purple','burlywood','indian','spring','floral','medium') and
i_current_price between 64 and 64 + 10 and
i_current_price between 64 + 1 and 64 + 15
group by i_product_name
,i_item_sk
,s_store_name
,s_zip
,ad1.ca_street_number
,ad1.ca_street_name
,ad1.ca_city
,ad1.ca_zip
,ad2.ca_street_number
,ad2.ca_street_name
,ad2.ca_city
,ad2.ca_zip
,d1.d_year
,d2.d_year
,d3.d_year
)
select cs1.product_name
,cs1.store_name
,cs1.store_zip
,cs1.b_street_number
,cs1.b_street_name
,cs1.b_city
,cs1.b_zip
,cs1.c_street_number
,cs1.c_street_name
,cs1.c_city
,cs1.c_zip
,cs1.syear
,cs1.cnt
,cs1.s1 as s11
,cs1.s2 as s21
,cs1.s3 as s31
,cs2.s1 as s12
,cs2.s2 as s22
,cs2.s3 as s32
,cs2.syear
,cs2.cnt
from cross_sales cs1,cross_sales cs2
where cs1.item_sk=cs2.item_sk and
cs1.syear = 1999 and
cs2.syear = 1999 + 1 and
cs2.cnt <= cs1.cnt and
cs1.store_name = cs2.store_name and
cs1.store_zip = cs2.store_zip
order by cs1.product_name
,cs1.store_name
,cs2.cnt
,cs1.s1
,cs2.s1;
这段 SQL 语句先是通过公共表达式定义了两个具有特定筛选和聚合逻辑的数据子集(cs_ui 和 cross_sales),然后在主查询中基于 cross_sales 表进行自连接并再次筛选,最终选取特定字段并按照一定顺序排序,目的可能是对比分析同商品在相邻年份(1999 年和 2000 年)、同店铺下的各项业务指标数据变化情况以及它们之间的相互关系,帮助进行业务决策或者数据分析等相关工作。
这是一段复杂查询,但是可以正常查询出结果:
这里我们给sql增加SETTINGS max_memory_usage=40000000000;限制,模拟内存问题,当我们限制到40000000000的70%,即28000000000后查询出错:
继续配置bsp模式后:
查询出正确结果:
总结
ByConity 增加的 BSP(Bulk Synchronous Parallel)模式是一个重要的功能更新,非常好用:
- 通过 distributed_max_parallel_size 参数控制分布式查询中表扫描的并行度。通过调整这个参数,用户可以根据集群的资源情况和查询的需求来优化查询性能。
- max_memory_usage 参数用于限制单个查询在执行过程中可以使用的最大内存量。通过设置这个参数,可以防止单个查询占用过多内存资源,影响其他查询的执行和系统的稳定性。
通过合理调整 distributed_max_parallel_size 和 max_memory_usage 的值,用户可以在保证查询性能的同时,避免资源过度消耗和查询失败的风险。ByConity非常推荐,原生数据仓库搭建,ByConity你值得拥有。