批量插入更新 MySQL 服务优化

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

1 原执行步骤

  1. 从Hive中拉数据产出数据文件
  2. 主进程读文件,按子进程数切分
  3. 根据待插入集合,使用 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)
  1. 根据 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 原执行步骤性能现状

在这里插入图片描述

  1. 执行速度慢,220万,执行3个小时(线上每小时500万,分业务刷入)
  2. 内存占用大,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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值