性能监控Dashboard
一、 业务背景
作为一个日活过亿的APP,随着新特性的不断迭代,包越来越大,谁也无法预料哪个Dev会发布一个bug,导致手机掉电飞快或卡到怀疑人生。所以软件性能的监控是非常重要的一环。毕竟用户离开的时候会抱不留情。常见的监控指标,一般有CPU、温度等,手机硬件数据采集都是通过埋点上报,每5秒频率。同时性能团队也会做AB实验,来跟踪实验效果,再决定是否推全。
二、问题恶化
随着用户增加和性能策略迭代,任务运行时间由3小时上升为7小时,性能团队要T-2才能回收实验效果。
我们可以预估一下数据量: 每日用户手机端上报的数据量约200亿,但是这里要关联另一张表:用户实验关系表,关联下来日级增量数据就是2000亿。
三、定位分析
数据量大到无法呼吸,而且业务需求还有100个指标要计算,只能硬着头皮扫描一下核心脚本,这里只截取一小段,实际脚本大概2K行左右。
Grouping sets:对分组集中指定的组表达式的每个子集执行group by。
group by A,B
grouping sets(A,B)就等价于
group by A union group by B
其中A和B也可以是一个集合,比如group by A,B,C grouping sets((A,B),(A,C))
1. 查看执行计划
这里用到了grouping sets,所以膨胀2倍数据也就是2000亿 x 2 = 4000亿。
通过SparkUI数据也可以发现。
目前来看膨胀2倍其实还好,大不了处理的慢一点,但报错却有点诡异。
# -java.lang.OutOfMemoryError: Java heap space
# -XX:OnOutOfMemoryError="kill %p"
# -Executing /bin/sh -c "kill 4209"...
2. OOM暴力堆资源
确定是OOM带来的问题,为了先把任务跑成功,给下游有个交代。所以先给executor的内存放大,当然目前内存已经很大了,但还是暴力加资源。通常处理OOM的要关注几方面,我的经验整理在《常见资源分配策略》的文章里,大家可以看看。
Before
--driver-memory 12G
--executor-memory 12G
--executor-cores 2
--num-executors 100
After
--driver-memory 12G
--executor-memory 18G
--executor-cores 2
--num-executors 200
3. OOM参数配置
重新调起运行后,依旧OOM,再次调整memoryOverhead的参数。调大这个参数观察效果,一般我们会选择memoryOverhead的大小为spark.executor.memory的25%,如果OOM还存在,则要进一步检查代码的原因,毕竟代码不是我写的,有一些业务背景不熟悉,不敢随便修改代码。
spark.executor.memoryOverhead = 4G
继续看SparkUI的内容,输入和shuffle的数据膨胀了近40倍!!!!!居然不是2倍
4. 业务代码理解
默默的回去看源代码
count ( distinct exp_id)
居然还有N个子查询带着count distinct,然后和上面的片段进行full join,把这个天杀的给拖出去砍了。熟悉count distinct操作的应该知道,它也是靠expand来实现的,也就是至少这个代码里还藏着10个count distinc,这意味着数据将膨胀10倍。。。。
果不其然,真正的罪魁祸首在这里卡着。都还没有到最开始的千亿数据量的那张表,首先在这里就卡住了。
这里刚好是stage 1.0的位置,回去看它的task metrics
5. 降低Input Size
明明文件才89M左右,但数据量很大,已经76610100条数据,这里让我不由自主的想要缩小一下read的数据量,可以使用这2个参数。
spark.sql.files.maxPartitionBytes = 20971520
spark.sql.files.openCostInBytes = 10485760
以为这个时候把read的数据量降下来,那问题应该就解决了,但没想到还是OOM了。百思不得其解。继续看SparkUI的统计指标。
虽然task增加了,但是task的inputsize的中位数是52.2KB,想必75分位的15.1M而言,妥妥的有问题,我们称之为:大部分任务在空跑。
Google了一下,发现这个是一个已知的问题,说的是这2个参数只对csv或text文件生效,对已经压缩的parquet效果并没有什么作用。
6. 上游表重分区
我们把读取的底层表再重新repartition一下,生成更小的文件,这将导致小文件也会增加。虽然知道可行,而且改造成本有点儿高,暂时换一条路再试试。
7. 业务代码改写
我们目的是为了避免expand的代价,但业务要求又要使用count distinct。那就用我们最习惯的group by来粗糙演示。
这里隐藏了额外的判断:当state=succ才计算,那改成max和sum,新增按照event_id进行打标和间接去重,最外层sum只会把tag=1的数据进行加总。
改写完后,重新运行,expand的情况就避免了,数据也正常产出,至少给业务有了一个交代。
四、后续改造
-
方案一:
我们跟业务方沟通后,其实这个性能数据少一部分都没关系,毕竟最后看的是分位数分布,那其实随机采样也是不影响整体数据形状。那其实可以这样做:
-
方案二:
这个SQL脚本太大大大大大大大大大,完全可以拆分多个任务进行处理,而且非常适合预聚合一轮之后,再进行汇总,但前任大侠为何没有做这些操作,估计有可能大概率他也是接的别人吧。
五、结语
整体优化也花了一天的时间,主要这个任务太大,改动一些参数要看效果,也得等到5小时以后,解决问题的过程和思路还是值得复制在其他地方,借此以飨读者。
如果它对您有一些价值,可以关注一下,原创不易,需要支持。
参考阅读
[1] “Partition” in “Apache Spark”:
https://oindrila-chakraborty88.medium.com/introduction-to-partition-in-apache-spark-66e005c6e15d
[2] Spark Memory Overhead
https://stackoverflow.com/questions/63561233/spark-memory-overhead
标签:memory,group,OOM,SQL,--,千亿,数据量,executor From: https://blog.csdn.net/lssilu/article/details/145094390