1 原执行步骤
- 从Hive中拉数据产出数据文件
- 主进程读文件,按子进程数切分
- 根据待插入集合,使用 ON DUPLICATE KEY UPDATE 批量更新或插入 table1(假设supply_id,order_id为联合唯一索引) 和 table2
INSERT INTO table1
(supply_id,order_id,num,end_time,status)
VALUES
(50441900,111,0.0,'2019-09-17 23:59:59',1),
(50441900,112,0.0,'2019-09-17 23:59:59',1)
ON DUPLICATE KEY UPDATE
supply_id=VALUES(supply_id),order_id=VALUES(order_id),num=VALUES(num),end_time=VALUES(end_time),status=VALUES(status)
INSERT INTO table2
(product_id,city_id,supply_id)
VALUES
(50,441900,50441900),
(50,442000,50442000)
ON DUPLICATE KEY UPDATE
product_id=VALUES(product_id),city_id=VALUES(city_id)
- 根据 table1 中supply_id下的列表集合,对在相同supply_id下order_id不在插入更新列表中且状态为1的记录设为无效
UPDATE table1 SET status='0'
WHERE status='1' AND supply_id='50441900'
AND order_id NOT IN (111,112)
2 原执行步骤性能现状

- 执行速度慢,220万,执行3个小时(线上每小时500万,分业务刷入)
- 内存占用大,220万,9×6.2%=55.8%(60.8G),500万数据总共近700M
3 性能剖析及优化
优化整体性能提升:500万数据,速度5h->80s,内存消耗量为优化前的2.2%
3.1 优化点1:无效数据模式优化为前后两次PT数据对比取数
性能剖析:
根据 supply_id 列表集合,对在相同supply_id下order_id不在插入更新列表中且状态为1的记录设为无效,语句按条件无效,拼接时两层遍历,速度奇慢(1万条数据,耗时5分钟);且依赖supply_id分片,造成数据倾斜(如:一批5w,一批100w)
优化:
解耦对supply_id分片的依赖,使用上一时段pt数据与当前pt数据的差集,直接用HiveSQL把无效数据拉出,同时无效的SQL使用效率更高的(INSERT INTO ON DUPLICATE KEY UPDATE)批量插入
无效数据和有效数据解耦,可均匀分片,避免数据倾斜;同时解决supply_id分片,对分片中间supply_id无法无效的问题
3.2 优化点2:hive取数加速
性能剖析:
在优化点1的基础上,HiveSQL由原模式的一条SQL变为两条,串行执行较慢,需加速
优化:
两条SQL并发执行,并加入HiveSQL提速配置
--hiveconf hive.execution.engine=tez
--hiveconf hive.exec.parallel=true
--hiveconf hive.exec.parallel.thread.number=8
3.3 优化点3:并发数、批量数合理设置
性能剖析:
原并发写库为8进程,两张表串行执行,批量执行数量为20
优化:
由于两张表可并发写库,可增加进程数,两张表的写库在不同进程间处理
而进程数的设置需要考虑线上库所在机器的CPU核数,考虑线上服务稳定运行,进程数要小于CPU核数
批量插入条数,要考虑到速度和主从延迟,如果批量插入数据过多,会导致主从延迟明显
针对公司机器40核,设置并发数为11,第一个进程写入table2,其它10进程并发写入table1,批量数设置为1000
3.4 优化点4:使用耗内存更少的数据格式
性能剖析:
在读数据使使用了pandas,处理过程中把dataframe转换为List,而该形式会存储字段名称,相同数据量,根据下面两次内存占用分析,可知该数据格式是datafrme内存占用的16倍
内存占用分析(100万数据,主进程切分数据,使用List形式):
Line # Mem usage Increment Line Contents
================================================
19 91.2 MiB 91.2 MiB @profile
20 def get_data1_list():
33 91.2 MiB 0.0 MiB try:
34 91.2 MiB 0.0 MiB result_supply_list = list()
35 91.2 MiB 0.0 MiB result_detail_list = list()
36 91.2 MiB 0.0 MiB sids_list = list()
37
41 91.2 MiB 0.0 MiB columns = ['product_id', 'city_id', 'supply_id', 'order_id', 'num', 'end_time', 'status']
42 266.9 MiB 175.8 MiB df = pd.read_table('data.txt', sep='\t', header=None, names=columns, dtype='str')
43
44 328.0 MiB 61.1 MiB data_supply = df[['product_id', 'city_id', 'supply_id']]
47 328.1 MiB 0.0 MiB data_supply['status'] = str(1)
48 290.3 MiB 0.0 MiB data_supply = data_supply.drop_duplicates()
49
50 389.5 MiB 99.2 MiB data_detail = df[['supply_id', 'order_id', 'num', 'end_time', 'status']]
53 401.2 MiB 11.8 MiB data_detail = data_detail.drop_duplicates()
54
55 403.5 MiB 2.3 MiB detail_supply_id = list(set(data_detail['supply_id']))
56 403.5 MiB 0.0 MiB len_detail = len(detail_supply_id)
57 403.5 MiB 0.0 MiB avg_len = len_detail / conf.MULTI_CNT
58
59 2602.2 MiB 0.0 MiB for i in range(1, conf.MULTI_CNT + 1):
60 2602.2 MiB 0.0 MiB if i != conf.MULTI_CNT:
61 2602.2 MiB 0.2 MiB s_ids = detail_supply_id[(i-1) * avg_len: i * avg_len]
62 else:
63 2351.8 MiB 0.0 MiB s_ids = detail_supply_id[(i - 1) * avg_len: i * len(detail_supply_id)]
64 2605.4 MiB 12.6 MiB detail = data_detail.loc[data_detail['supply_id'].isin(s_ids)]
65 2440.8 MiB 39.2 MiB tmp_detail = detail.to_json(orient='records')
66 2602.2 MiB 354.4 MiB detail_list = pd.json.loads(tmp_detail)
67 2602.2 MiB 0.0 MiB result_detail_list.append(detail_list)
68 2602.2 MiB 0.0 MiB sids_list.append(s_ids)
内存占用分析(100万数据,主进程切分数据,直接使用dataframe):
Line # Mem usage Increment Line Contents
================================================
19 77.6 MiB 77.6 MiB @profile
20 def get_data1_list():
33 77.6 MiB 0.0 MiB try:
34 77.6 MiB 0.0 MiB result_supply_list = list()
35 77.6 MiB 0.0 MiB result_detail_list = list()
36 77.6 MiB 0.0 MiB sids_list = list()
37
38 77.6 MiB 0.0 MiB columns = ['product_id', 'city_id', 'supply_id', 'order_id', 'num', 'end_time', 'status']
39 257.2 MiB 179.6 MiB df = pd.read_table('data.txt', sep='\t', header=None, names=columns, dtype='str')
40
41 310.6 MiB 53.5 MiB data_supply = df[['product_id', 'city_id', 'supply_id']]
42 272.9 MiB 0.0 MiB data_supply = data_supply.drop_duplicates()
43 272.9 MiB 0.0 MiB data_supply['status'] = str(1)
44
45 364.4 MiB 91.6 MiB data_detail = df[['supply_id', 'order_id', 'num', 'end_time', 'status']]
46 375.9 MiB 11.5 MiB data_detail = data_detail.drop_duplicates()
47
48 375.9 MiB 0.0 MiB len_detail = data_supply.shape[0]
49 375.9 MiB 0.0 MiB avg_len = len_detail / conf.MULTI_CNT
50
51 473.8 MiB 0.0 MiB for i in range(1, conf.MULTI_CNT + 1):
52 469.2 MiB 0.0 MiB if i != conf.MULTI_CNT:
53 458.0 MiB 0.0 MiB s_ids = data_supply['supply_id'][(i-1) * avg_len: i * avg_len]
54 else:
55 469.2 MiB 0.0 MiB s_ids = data_supply['supply_id'][(i - 1) * avg_len:]
56 473.8 MiB 14.7 MiB detail = data_detail.loc[data_detail['supply_id'].isin(s_ids)]
57 473.8 MiB 0.0 MiB result_detail_list.append(detail)
58 473.8 MiB 0.0 MiB sids_list.append(s_ids)
优化:
弃用List形式,直接使用dataframe,不做转换
内存占用分析(500万数据,主进程切分数据,直接使用dataframe):
内存占用从220万(9×6.2%=55.8%,60.8G)降为500万(9×1.5%=13.5%,14.3G),整体内存耗用量为之前的12%

3.5 优化点5:子进程分片读取数据
性能剖析:
从上面内存占用分析可知,主进程、子进程内存占用相同,原因是python子进程是对主进程内存的直接复制,在主进程中读取数据全集,子进程内存占用也为数据全集
考虑python多线程,虽然多线程可以共用内存,但只能使用1个CPU,不利于加速
尝试从python内存管理方面解决,被引用变量不会被内存回收,主动删除(del)已赋值的大dataframe,无明显内存占用下降
优化点5:
换个思路,主进程不读入整个数据全集,传入子进程切分数据位置,在子进程读入需要数据,所有子进程内存耗用量加和即为整个数据集大小
子进程内存耗用量从1.5%->0.3%,主进程几乎不耗费内存
加上优化4,整体内存消耗量优化为之前的2.2%(原内存耗用是优化后的46.5倍)
# 按分片读取数据集
df = pd.read_table(conf.DATA_PATH, sep='\t', header=None, names=columns, dtype='str', skiprows=detail_batch_size * process_num, nrows=detail_batch_size)

3.6 优化点6:SQL拼接使用更快的方式
性能剖析:
SQL拼接为遍历, 数据量大时拼接SQL速度异常慢
优化:
SQL拼接直接用dataframe处理,dataframe内部矩阵处理,CPU占用率从一直95%,降到偶尔5%
df_query = "(" \
+ df_detail["product_id"].map(str) \
+ "," + df_detail["city_id"].map(str) \
+ "','" + df_detail["supply_id"].map(str) \
+ "'," + df_detail["order_id"].map(str) \
+ "," + df_detail["num"].map(str) \
+ ",'" + df_detail["end_time"].map(str) \
+ "'," + df_detail["status"].map(str) \
+ ")"
query_values_str = df_query.str.cat(sep=",")
# sql执行语句
query_str = "INSERT INTO " + self.table1.get_table_name() + query_header_str + " VALUES " + query_values_str + " ON DUPLICATE KEY UPDATE " + query_update_str
self.eng.execute(query_str)
--------------------------文档信息--------------------------
版权声明:本文为博主原创文章,未经博主允许不得转载
署名(BY) :dkjkls(dkj卡洛斯)
文章出处:http://blog.youkuaiyun.com/dkjkls

通过解耦无效数据模式、加速Hive取数、合理设置并发数、采用高效数据格式及子进程分片读取等策略,将500万数据处理速度从5小时提升到80秒,内存消耗降至优化前的2.2%。
1522

被折叠的 条评论
为什么被折叠?



