目录


前言

本方案适合:无关业务的“日志数据”,但往往日志数据是最最占用我们的整体系统性能的,因此对这样的日志,我们是需要进行定期清理的。

如果你要说:业务数据也需要那么我告诉你,业务数据肯定用的是本方案中的场景2中的分场景2模式(只有这一条路),但是业务数据会暴发到你连本方案都无法覆盖的那一天的(很快的,如我上一家公司:几千万的会员生成业务流水),那么当本方案都失效时怎么办?

答案就是:垂直折分,hash一致算法,sharding sphere就要用上了,对于这一块涉及到的面太庞大了因此我需要写一段时间,当我写完也会分享给到大家。

开始进入正文:

如果你是单机,如果你是自己在家玩。你的数据库里有亿级数据,你来一条:

delete from user_behavior_logs;

然后你慢慢等个几小时,等到你的mysql暴了、硬盘被烧了都没事。

如果你在公司的生产环境,特别是在具有主从复制、1主多从甚至多主多从的环境下,你来一条delete命令,你知道会发生什么事吗?

如何在生产环境mysql删除亿万级数据解并且不影响数据库主从延迟的解决方案-LMLPHP

为什么在生产上主从环境情况下mySQL特别容易卡死

它的原理其实是:mysql上的delete语句首先会同步到各个从库上,delete语句会产生redo日志也会同步在各个从库上,然后是mysql本身数据的binlog也在同步。三条操作*总mysql库量*你删除的数据量产生的:

  • 网络io
  • CPU消耗
  • 磁盘读写
  • 等等等其它

导致了上面的“主从延迟”这样的一个问题。

当发生主从延迟时其实是不用怕的当生产环境读写操作频繁,总会发生一定概率的主从延迟。偶尔在大促季,一天发生个1-2次并且只要在主从延迟发生时,从库可以在5-10分钟内追平主库就不构成任何影响

但是,如果发生了主从延迟,这个从延尺不断的在加大时间,超过了20分钟,30分钟,往40、60分钟走时,此时的整个db群就是:读业务全部受影响,因为从还没执行完自己的任务还要去追主,但是主上不断的在写进大量的数据。一般为了让从能够追上主,你就必须“锁主库”。

我们都知道,在生产环境下是不能锁主库的,一锁,所有的订单或者相关的“写操作”都没法提交了。

那么就有人说了:让从慢慢追主吧。

但是,些时你的整体网站是读写分离的,从库追不平主库,整体的读业务又受影响。

这个痛苦啊,此时就会发生著名的“主从延迟土拨鼠之日”,这是一个悖论,即:

眼看着数据库里的日志越来越多、占用的磁盘越来越大、影响了日常的正常报表、运维工作,再不删,整体业务要严重受到影响。但是呢,当你要去删,就又出现了严重的主从延迟,一样影响业务。咽不下去也吐不出,活活被憋死

不要去怪设计不要去怪开发我们devops靠自己

“一千个观众眼中有一千个哈姆雷特”--《杀死比尔说的-哦,不是,是莎士比亚》

如何在生产环境mysql删除亿万级数据解并且不影响数据库主从延迟的解决方案-LMLPHP

可是,我们在生产db上删除记录并且又能不影响主从同步的话就只有“一种”方法,我们在说任何方法前先来一个感性的认识,即我们先用“人类”可以懂的语言来描述一下这件事到底该怎么做。它其实可以分为两个场景来做,每个场景有不同的做法:

场景一、当要被删除的数据量远大于保留的数据量的场景下的做法

场景二、当要被删除的数据量远小于保留的数据量的场景下的做法

下面,就让我们来展开这两个场景吧。

场景一、当要被删除的数据量远大于保留的数据量的场景下的做法

假设我们实际要执行的是下面这样的一条sql:

delete from user_behavior_logs where  created_datetime between '2016-08-10 17:20:00' and '2019-12-31 17:20:00';

这涉及到在生产的主库上:

删除:1700万条记录

实际需要保留的数据:30万条,条件: between '2020-01-01 17:20:00' and '2020-08-10 17:20:00',30万条数据。

那么我们的做法为:

1) 照着要被删除的table名建立一个完全一模一样名字带tmp_前缀的table名

2)选取要保留的数据 into tmp_table

3)rename table 原来的table名 to deleted_原来的table名

4)rename table tmp_table to 原来的table名

5)drop table deleted_原来的table名

它化成具体的操作就是以下这么几条sql(create table语句省略,因为这个太简单了)

insert into  tmp_user_behavior_logs
 (
  ak,gu,ln,st,os,
  ss,ip,bruser_behavior_logs,lan,fv,ifj,ifc,brs,cp,pn,pl,chn,sv,ev,et,pt,prn,created_datetime
  )
select
  ak,gu,ln,st,os,
  ss,ip,bruser_behavior_logs,lan,fv,ifj,ifc,brs,cp,pn,pl,chn,sv,ev,et,pt,prn,now()
from user_behavior_logs where created_datetime between '2020-01-01 17:20:00' and '2020-08-10 17:20:00';

rename table user_behavior_logs to deleted_user_behavior_logs;
rename table tmp_user_behavior_logs to user_behavior_logs;

drop table deleted_user_behavior_logs;

操作涉及数据量及环境

烂机器环境下的执行情况

以上的操作位于:base 1000万条记录,同时使用压力测试工具不断的往数据库中以每5秒进5000条数据的速度插入新数据,master slaver主从情况下,在4c cpu, 8gb ram,非ssd磁盘执行情况:

对于insert into ...select from...语句涉及到30万数据量的情况下,执行时间为:16s,执行期间有报主从同步,主从同步一开始值有点高为70s,这个报警持续了5分钟左右即消失;

对于rename与drop语句执行只用了1s,执行过程无任何主从同步报警;

结论

就算主从报警,为完全可接受范围内。

好机器环境下的执行情况

以上的操作位于:base 1000万条记录,同时使用压力测试工具不断的往数据库中以每5秒进5000条数据的速度插入新数据,master slaver主从情况下,在64c cpu, 256gb ram,ssd磁盘执行情况:

对于insert into ...select from...语句涉及到30万数据量的情况下,执行时间为:1.3s,执行期间,有报主从同步,主从同步一开始值为5s,这个报警持续了15s就消失了;

对于rename与drop语句执行只用了1s,执行过程无任何主从同步报警;

结论

就算主从报警也可以忽略不计。

场景二、当要被删除的数据量远小于保留的数据量的场景下的做法

分场景1、被删除的数据很小小到只会引起10分钟内的主从延迟-不建议

第1步:确定要被删除的id范围;

第2步:使用存储过程,分成小批量删除,每次删除的量不要超过(delete+where条件)万条。删除后停一下,再删下一批,全程最好有监控报警随时看着

操作涉及数据量及环境

烂机器环境下的执行情况

以上的操作位于:base 2.1亿条记录,总共:460gb,同时使用压力测试工具不断的往数据库中以每5秒进5000条数据的速度插入新数据,master slaver主从情况下,在4c cpu, 8gb ram,非ssd磁盘执行情况:

对于delete from user_behavior_logs where id between 1  and 5000; 每隔30秒我做一次这样的delete操作。

实际操作时间为:3.7s,主从延迟报警持续了:59s即告结束。

结论

就算主从报警,为完全可接受范围内。

好机器环境下的执行情况

以上的操作位于:base 1000万条记录,每5秒进5000条数据,master slaver主从情况下,在64c cpu, 256gb ram,ssd磁盘执行情况:

对于delete from user_behavior_logs where id between 1  and 5000; 每隔30秒我做一次这样的delete操作。

实际操作时间为:1s,主从延迟报警持续了:8s即告结束。

结论

就算主从报警,为完全可接受范围内。每次删除需要少数据量,频率不能太高,每次删完当中需要有一个30-60秒的间隔以让从尽量追上主库。

分场景2、被删除的数据不小,但是如果直接delete一定会引起15分钟以上的主从延迟

假设我们实际要执行的是下面这样的一条sql:

delete from user_behavior_logs where  created_datetime between '2020-04-07 09:00:00' and '2020-08-07 14:00:00';

这涉及到在生产的主库上:

删除:170万条记录

实际需要保留的数据:9000w条记录,条件为created_datetime between '2020-04-07 09:00:00' and '2020-08-07 14:00:00';

那么我们的做法为:

第一步:mysqldump成一个文件;

第二步:把dump出去的文件导入到一个新的表中去

第三步:使用分场景2中的rename手法来

注意:这个手法只有在非业务时间段即一般在零晨去做这个事情,mysqldump回新表时,会造成不小的主从延迟,来看一下本人的实际操作情况。

烂机器环境下的执行情况

以上的操作位于:base 1亿条记录,总共:140gb,同时使用压力测试工具不断的往数据库中以每5秒进5000条数据的速度插入新数据,master slaver主从情况下,在4c cpu, 8gb ram,非ssd磁盘执行情况:

对于delete from user_behavior_logs where created_datetime between '2020-04-07 09:00:00' and '2020-08-07 14:00:00'; 要删除的数据多达:170w条,需要保留的有9900w条。

用mysqldump导出和恢复9900w条记录总计用了:6小时,造成了严重的主从同步,最后不得不锁主库,再用mysqldump追平从库,最后造成整个操作没法完成。

结论

该操作确实很耗时,在一般机器上很难模拟,也验证了,这种操作很耗资源。

好机器环境下的执行情况

以上的操作位于:base 1亿条记录,总共:140gb,同时使用压力测试工具不断的往数据库中以每5秒进5000条数据的速度插入新数据,master slaver主从情况下,在64c cpu, 256gb ram,ssd磁盘执行情况:

对于delete from user_behavior_logs where created_datetime between '2020-04-07 09:00:00' and '2020-08-07 14:00:00'; 要删除的数据多达:170w条,需要保留的有9000w条。

用mysqldump导出和恢复9900w条记录总计用了:3小时,从库每3分钟报一次主从同步,连续了3小时直到mysqldump把9900w条记录导入了新表才告终目。而后续的rename表名和drop都是秒级,期间无任务报警。

结论

这种手法,只有在非营业时间去做,并且这点时间是完全可以忍受的,但是这种需求只应该每半年或者季度发生一次。

最终对于生产mysql的日志清理策略的best practice

  • 策略一、如果需要删除的数据很多,多到比如说需要删除相当于原表数据内的50%,并且这个总量超过10个gb的话,都必须在非业务时间,有足够的空余时间(8小时内)才能去做这样的操作,操作前必须建立1v1数据库验证这个手法可以在8小时内完成,然后才可以去正式生产上做操作。并且这种操作视业务量,一般6个月或者最频繁3个月一次足以了;
  • 策略二、如果需要删除的数据远大于需要保留的数据,比如说需要保留的数据不过百万来条,10个gb以内,完全可以使用场景一中的“5步曲”去做这个操作;
  • 场景2中的分场景1,不建议原因有两点:1)你根本无法控制自动脚本的跑delete语句的准确率,特别是生产环境,你能确保定时触发的delete语句每次都删除的数据量是你规定的吗?2)如果在高并发环境下,为了确保被自动触发的delete语句永远是安全的你就必须去控制这个delete语句的数据范围,一般会控制在很少值,那么就是你删除的速度远远跟不上进入的数据,你的分小段delete清理日志手段或者在一开始业务量小的情况下有一定的效果,但是如果业务一旦爆增这种“涓涓溪流”的行为是毫无任何意义的
  • 无论采取的是策略一还是策略二,绝对不可以设成“自动脚本”,必须全程人为干涉和监控。就算用的是策略二、半年这么幸苦一晚上也是值得的

附录

自动监控mysql主从延迟报警shell脚本-behind_master.sh

#!/bin/bash
#desc:脚本
#通过从库监控Seconds_Behind_Master的值监控延迟情况。
#该值为null或着超过告警阈值会报错.
#本脚还通过mysql命令执行情况判定mysql服务可用状态。
#author:hahaxiao_mk
#date:2018/04/27
#source ~/.bash_profile
#source ~/.bashrc
#----Seconds_Behind_Master的值
v_sbm='NULL'
#----检测域值,单位s
v_threshold=1
#----机器标示
v_machine_mark=ymkmysql
MYSQL_HOME=/usr/local/mysql


#-----发送告警信息函数
function f_send_msg()
{
   echo "准备发送主从迟告警:${1} ${2}" >> /home/appadmin/behind_master.log
   python /home/appadmin/send_alert_msg.py ${1}$2 101 1
   #调用alert告警${v_java_home_bin}/java -jar /opt/config/inf/alarm.jar 1 $1 $2
}
#-----判定mysql服务状态
starttime=$(date +%Y-%m-%d\ %H:%M:%S)
v_mysql_status=`mysql -uroot  -phaha -P3306 -h10.0.0.1 -e "show slave status\G"|grep Seconds_Behind_Master`
echo "开发库10.0.0.1于 ${starttime} -> 主从延迟目前为:${v_mysql_status}" >> /home/appadmin/behind_master.log
if [ $? -eq 1 ]
then
    v_err_msg="mysql is not available! "
    # f_send_msg ${v_mobile} ${v_err_msg}
    echo ${v_err_msg}
    f_send_msg ${v_err_msg}
    exit
fi
#------判定延迟情况
v_sbm=`echo ${v_mysql_status}|awk -F ":" '{print $2}'`
if [ "${v_sbm}" = " NULL" ]
then
     v_err_msg="开发库10.0.0.1于 ${starttime} -> 发生主从延迟为: ${v_sbm}!"
     # f_send_msg ${v_mobile} ${v_err_msg}
     #echo ${v_err_msg}
     f_send_msg ${v_err_msg}
elif [ ${v_sbm} -gt ${v_threshold} ]
then
     v_err_msg="开发库10.0.0.1发生主从延迟${v_sbm}s!"
     echo ${v_err_msg} >> /home/appadmin/behind_master.log
     #f_send_msg ${v_mobile} ${v_err_msg}
     #echo ${v_err_msg}
     f_send_msg ${v_err_msg}
fi

使用CentOS的crontab设置监控脚本每5s运行一次写法

crontab -e

然后把下面这一陀复制进去吧(crontab的最小条件为分钟,因此要做成秒必须化解成以下的语句,这是一个实用技巧

* * * * * sh /home/appadmin/behind_master.sh
* * * * * sleep 5; sh /home/appadmin/behind_master.sh
* * * * * sleep 10; sh /home/appadmin/behind_master.sh
* * * * * sleep 15; sh /home/appadmin/behind_master.sh
* * * * * sleep 20; sh /home/appadmin/behind_master.sh
* * * * * sleep 25; sh /home/appadmin/behind_master.sh
* * * * * sleep 30; sh /home/appadmin/behind_master.sh
* * * * * sleep 35; sh /home/appadmin/behind_master.sh
* * * * * sleep 40; sh /home/appadmin/behind_master.sh
* * * * * sleep 45; sh /home/appadmin/behind_master.sh
* * * * * sleep 50; sh /home/appadmin/behind_master.sh

自动发送告警信息到企业微信接口(aldi-cupidmq)的python脚本

#!/usr/bin/python
import re
import requests
import time
import json
import sys

url='http://localhost:9081/alertservice/sendMsg'
if (len(sys.argv)>1):
  inputedmsg=sys.argv[1]
  msgtype=sys.argv[2]
  modelId=sys.argv[3]
  print('input message->'+inputedmsg+' input msgtype->'+msgtype+' modelId->'+modelId)
  currentTime=time.strftime('%Y/%m/%d %H:%M:%S',time.localtime(time.time()))
  print 'current time is ', currentTime
  if(msgtype=='101'):
    wechatmsg='Issue happened on ' +currentTime +':\n'+ inputedmsg
    wechatcontent={"modelId": modelId, "content": wechatmsg}
    wechatheaders = {"content-type": "application/json; charset=UTF-8", "type": "101"}
    req = requests.post(url, data=json.dumps(wechatcontent),headers=wechatheaders)
    print(req.text)
  elif(msgtype=='102'):
    print('send mail msg')
  else:
    print('inputed msgtype required 101|102')
else:
  print('inputed msg can not be null')

企业微信收到主从延迟后的展示效果

如何在生产环境mysql删除亿万级数据解并且不影响数据库主从延迟的解决方案-LMLPHP

09-03 05:12