大数据分析技术里最坑的一课:同一条SQL,白天8秒,凌晨40分钟
凌晨两点十七分,电话响了。值班的同事说日报SQL跑了四十分钟还没出结果,调度队列已经堵成一锅粥。
我点开页面看了一眼。这条SQL我熟。白天在测试集群上,它8秒。
同一条语句,同一份底表。白天8秒,深夜40分钟。干这行久了,你会有种说不清的麻木——每次都说“这次肯定不是数据问题”,每次最后都是数据问题。
别急着加机器,先看它到底在等谁
遇到慢查询,很多人的本能反应是扩资源。加节点、加内存、把并行度调大。这动作本身没错,但大多数时候没用,甚至会更快地把集群拖垮。
原因在于,分布式计算的耗时,八成不在“算”上,在“等”上。
一条典型的分析SQL大概长这样:从HDFS或者对象存储读一堆文件,按某个键把数据重新分发到各个节点,再分组、聚合、排序,最后写回。中间那个“重新分发”的动作,行业里叫shuffle。你可以把它想成一个食堂打饭的场景——
三百个人均匀排在八个窗口前,两分钟就散场了。可要是其中两千人不知为什么全挤到了三号窗口,另外七个师傅干站着,那三号窗口前面能排出食堂大门。
机器没变,人也没变。变的是分布。
Spark作业Task耗时分布倾斜监控界面
数据倾斜这东西,藏得比你想的深
我们那次出事,是个IoT场景。两亿多条设备心跳日志,按设备ID关联一张维表,算每个设备某天的活跃时长。
逻辑简单到无聊。问题是,那两亿条记录里,有将近七成挂在了同一个设备ID上。
不是脏数据,也不是程序写错了。那是个网关设备。它的ID被下发给了一大批下游终端,等于所有终端都用同一个身份在说话。业务上合理,计算上致命。
那个join键,正好就是这个网关ID。于是shuffle之后,一个task分到了六千多万条,剩下的199个task各自分到几十万,早早跑完,在内存里干等。
你在监控上看到的画面就特别典型:199个task早就绿了,最后一个卡在那里,进度条一动不动,CPU占用不高,网络IO也不高。它不是在算,它是在扛一个巨型哈希表,扛到内存开始往磁盘溢写,然后一遍遍读写。
处理办法其实不复杂。给这个键撒点盐——随机加个0到9的后缀,把它拆成十份,先局部聚合一遍,再把盐洗掉做第二次聚合。代价是多一轮计算,换来的是把一个人的活分给十个人。
另一种情况是维表不大,几百兆以内,那干脆别join了,直接广播到每个节点,在map端就把关联做完。省掉整个shuffle环节,那40分钟能直接砍掉大半。
但我想说的不是这两种技巧。技巧网上一搜一大把。
我想说的是——你得像排查水管漏水那样,先知道水从哪儿来。别一上来就调参数。
比倾斜更阴的,是过期三个月的统计信息
还有一种慢,慢得没道理,慢得让人怀疑人生。
明明两个表都不大,join起来却跑了十几分钟。你去翻执行计划,会发现优化器给了一个特别离谱的策略——它把小表当成了大表,把大表当成了小表。
为什么?因为它不知道表到底有多大。
现在的查询引擎大多带一个基于代价的优化器,简称CBO。它做决策的唯一依据,就是元数据里那张统计表:行数、列的基数、最大值最小值、直方图。
这张表不会自动更新。你不去手动跑一次收集统计信息的命令,它就一直停在上次那个时间点上。
我遇到过最夸张的一次,是某张订单表统计信息显示四百万行,实际十二亿行。差了三百倍。优化器看着四百万,觉得小意思,选了广播策略,结果每个节点都在广播十二亿行数据。
Hive EXPLAIN执行计划各阶段算子输出解读
所以现在我看慢查询,流程基本固定:先看执行计划,再看统计信息的时间戳,然后才轮到看资源。
还有个小东西容易被人忽略——小文件。
HDFS上一条路径下挂着二十万个5MB的文件,这种事在流式写入的场景里太常见了。每个文件对应一个读取任务,二十万个任务光是启动、调度、关停的开销,就能吃掉几十分钟。而数据总量摊开还不到1TB。
合并小文件,很多时候比换引擎立竿见影得多。
几个我亲眼见过的翻车现场
几个我亲眼见过的翻车现场
说几个具体的,都是真人真事。
有人为了算去重后的用户数,直接上count distinct。数据量上亿,内存扛不住,溢写、重试、再溢写。换成两阶段聚合,先按用户ID分组去重再计数,资源消耗掉到原来的一个零头。
有人把日期存成了字符串,查询时写 where dt between '2024-01-01' and '2024-01-31'。听着没问题对吧?可分区字段没做裁剪,因为写法没触发优化器的分区识别,于是全表扫描。改成 dt >= '2024-01-01' and dt < '2024-02-01',扫描量从几个T掉到几十个G。
还有人,用 select * 把结果拉到一个16G内存的笔记本上,塞进pandas做透视。他就是想看看数据长什么样。结果电脑风扇狂转十分钟,然后进程被系统杀了。
这些都是很基础的东西。基础到什么程度呢——你知道了会觉得“这还用说”。但在真实的项目里,它们每天都在发生。
它也有够不着的地方
它也有够不着的地方
聊到这儿,得说点不那么讨喜的话。
大数据分析技术不是万能的。它擅长的场景很明确:数据量大到单机放不下,或者计算逻辑复杂到需要横向扩展。数据量只有几百万行、几千万行的时候,你在一台机器上用polars或者duckdb跑,往往比搭一整套集群快得多,也省心得多。
搭集群这件事本身是有成本的。运维、调优、故障排查、人员培训,全都要钱。很多人低估了这部分。
另一个边界是实时性。批处理给你的是准确但延迟的结果,流处理给你的是及时但可能有误差的结果。这两者之间没有免费的午餐,只能按业务需求去选。给管理层看的日报,晚两个小时没人介意;风控拦截,晚两秒钟就是事故。
还有个更根本的问题——数据质量。
引擎再快,也快不过一堆脏数据带来的返工。字段口径不统一、上游写数据没有约束、同一个指标三个部门算出三个数……这些问题加多少机器都解决不了。它们不在技术层面,在协作层面。
另外,涉及个人信息的数据,现在的合规要求越来越严。分析之前该脱敏就脱敏,该做权限隔离就做隔离。别等到出事才想起来翻日志。
回到那个凌晨两点十七分。
加盐改造之后,40分钟变成90秒。收工,睡觉。
但躺下之后我一直在想,真正的问题其实不是倾斜。真正的问题是——没有任何人知道,那个网关设备的ID会在那个月被下发给几十万个终端。数据分布在变,业务在变,而我们的监控里,没有一条告警是关于“分布”的。
这才是大数据分析里最难的部分。不是把SQL写对,是知道它什么时候会不对。