2022-10-27 299
前言
众所周知,MySQL广泛应用于互联网的OLTP(联机事务处理过程)业务系统中,在大厂开发规范中,经常会看到一条“不建议使用text大字段类型”。
下面就从text类型的存储结构,引发的问题解释下为什么不建议使用text类型,以及text改造的建议方法。
背景
写log表导致DML慢
1)问题描述
某歪有一个业务系统,使用RDS for MySQL 5.7的高可用版本,配置long_query_time=1s,添加慢查询告警,我第一反应就是某歪又乱点了。
我通过监控看CPU, QPS,TPS等指标不是很高,最近刚好双十一全站都在做营销活动,用户量稍微有所增加。某歪反馈有些原本不慢的接口变的很慢,影响了正常的业务,需要做一下troubleshooting。
2)问题分析
我从慢查询告警,可以看到有一些insert和update语句比较慢,同时告警时段的监控,发现IOPS很高,达到了70MB/s左右,由于RDS的CloundDBA功能不可用,又没有audit log功能,troubleshooting比较困难,硬着头皮只能分析binlog了。
配置了max_binlog_size =512MB,在IOPS高的时段里,看下binlog的生成情况。
需要分析为什么binlog写这么快,最有可能原因就是insert into request_log表上有text类型,request_log表结构如下(demo)
CREATETABLErequest_log(` `idbigint(20)NOTNULLAUTO_INCREMENT,` `logtext,` `created_atdatetimeNOTNULL,` `statustinyint(4)NOTNULL,` `methodvarchar(10)DEFAULTNULL,` `urlvarchar(50)DEFAULTNULL,` `update_atdatetimeDEFAULTNULL,` `running_timetinyint(4)DEFAULT'0',` `user_idbigint(20)DEFAULTNULL,` `typevarchar(50)DEFAULTNULL,` `PRIMARYKEY(id)` `)ENGINE=InnoDBAUTO_INCREMENT=4229611DEFAULTCHARSET=utf8`
分析binlog:
$mysqlbinlog--no-defaults-v-v--base64-output=DECODE-ROWSmysql-bin.000539|egrep"insertintorequest_log"
满屏幕都是看不清的内容,翻了半天没翻完。
基本上已经确定是写入request_log的log字段引起的,导致binlog_cache频繁的flush,以及binlog过度切换,导致IOPS过高,影响了其他正常的DML操作。
3) 问题解决
跟开发同学沟通后,计划在下一个版本修复这个问题,不再将request信息写入表中,写入到本地日志文件,通过filebeat抽取到es进行查询,如果只是为了查看日志也可以接入grayLog等日志工具,没必要写入数据库。
文章最后我还会介绍几个MySQL 我踩过text相关的坑,这介绍坑之前我先介绍下MySQL text类型。
MySQL中的text
text类型
text是一个能够存储大量的数据的大对象,有四种类型:TINYTEXT, TEXT, MEDIUMTEXT,LONGTEXT,不同类型存储的值范围不同,如下所示:
Data Type | Storage Required |
---|---|
TINYTEXT | L + 1 bytes, where L < 2**8 |
TEXT | L + 2 bytes, where L < 2**16 |
MEDIUMTEXT | L + 3 bytes, where L < 2**24 |
LONGTEXT | L + 4 bytes, where L < 2**32 |
其中L表是text类型中存储的实际长度的字节数。可以计算出TEXT类型最大存储长度2**16-1 = 65535 Bytes。
InnoDB数据页
Innodb数据页由以下7个部分组成:
内容 | 占用大小 | 说明 |
---|---|---|
File Header | 38Bytes | 数据文件头 |
Page Header | 56 Bytes | 数据页头 |
Infimun 和 Supermum Records | 伪记录 | |
User Records | 用户数据 | |
Free Space | 空闲空间:内部是链表结构,记录被delete后,会加入到free_lru链表 | |
Page Dictionary | 页数据字典:存储记录的相对位置记录,也称为Slot,内部是一个稀疏目录 | |
File Trailer | 8Bytes | 文件尾部:为了检测页是否已经完整个的写入磁盘 |
说明:File Trailer只有一个FiL_Page_end_lsn部分,占用8字节,前4字节代表该页的checksum值,最后4字节和File Header中的FIL_PAGE_LSN,一个页是否发生了Corrupt,是通过File Trailer部分进行检测,而该部分的检测会有一定的开销,用户可以通过参数innodb_checksums开启或关闭这个页完整性的检测。
从MySQL 5.6开始默认的表存储引擎是InnoDB,它是面向ROW存储的,每个page(default page size = 16KB),存储的行记录也是有规定的,最多允许存储16K/2 – 200 = 7992行。
InnoDB的行格式
Innodb支持四种行格式:
由于Dynamic是Compact变异而来,结构大同而已,现在默认都是Dynamic格式;COMPRESSED主要是对表和索引数据进行压缩,一般适用于使用率低的归档,备份类的需求,主要介绍下REDUNDANT和COMPACT行格式。
Redundant行格式
这种格式为了兼容旧版本MySQL。
行记录格式:
具有以下特点:
其中变长类型是通过长度 + 数据的方式存储,不同类型长度是从1到4个字节(L+1 到 L + 4),对于TEXT类型的值需要L Bytes存储value,同时需要2个字节存储value的长度。同时Innodb最大行长度规定为65535 Bytes,对于text类型,只保存9到12字节的指针,数据单独存在overflow page中。
Compact行格式
这种行格式比redundant格式减少了存储空间作为代价,但是会增加某些操作的CPU开销。如果系统workload是受缓存命中率和磁盘速度限制,compact行格式可能更快。如果你的工作负载受CPU速度限制,compact行格式可能更慢,Compact 行格式被所有file format所支持。
行记录格式:
Compact首部是一个非NULL变长字段长度的列表,并且是按列的顺序逆序放置的,若列的长度小于255字节,用1字节表示;若大于255个字节,用2字节表示。变长字段最大不可以超过2字节,这是因为MySQL数据库中varchar类型最大长度限制为65535,变长字段之后的第二个部分是NULL标志位,表示该行数据是否有NULL值。有则用1表示,该部分所占的字节应该为1字节。
所以在创建表的时候,尽量使用NOT NULL DEFAULT ”,如果表中列存储大量的NULL值,一方面占用空间,另一个方面影响索引列的稳定性。
具有以下特点:
每个辅助索引记录包含为群集索引键定义的不在辅助索引中的所有主键列。如果任何一个主键列是可变长度的,那么每个辅助索引的记录头都有一个可变长度的部分来记录它们的长度,即使辅助索引是在固定长度的列上定义的;
text类型引发的问题
插入text字段导致报错
1)创建测试表
[root@barret][test]>createtableuser(idbigintnotnullprimarykeyauto_increment, ->namevarchar(20)notnulldefault''comment'姓名', ->agetinyintnotnulldefault0comment'age', ->genderchar(1)notnulldefault'M'comment'性别', ->infotextnotnullcomment'用户信息', ->create_timedatetimeNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间', ->update_timedatetimeNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'修改时间' ->); QueryOK,0rowsaffected(0.04sec)
2)插入测试数据
root@barret][test]>insertintouser(name,age,gender,info)values('moon',34,'M',repeat('a',1024*1024*3)); ERROR1406(22001):Datatoolongforcolumn'info'atrow1 [root@barret][test]>insertintouser(name,age,gender,info)values('sky',35,'M',repeat('b',1024*1024*5)); ERROR1301(HY000):Resultofrepeat()waslargerthanmax_allowed_packet(4194304)-truncated
3)错误分析
[root@barret][test]>select@@max_allowed_packet; +----------------------+ |@@max_allowed_packet| +----------------------+ |4194304| +----------------------+ 1rowinset(0.00sec)
max_allowed_packet控制communication buffer最大尺寸,当发送的数据包大小超过该值就会报错,我们都知道,MySQL包括Server层和存储引擎,它们之间遵循2PC协议,Server层主要处理用户的请求:连接请求—>SQL语法分析—>语义检查—>生成执行计划—>执行计划—>fetch data;存储引擎层主要存储数据,提供数据读写接口。
max_allowed_packet=4M,当第一条insert repeat(‘a’,1024*1024*3),数据包Server执行SQL发送数据包到InnoDB层的时候,检查数据包大小没有超过限制4M,在InnoDB写数据时,发现超过了text的限制导致报错。第二条insert的数据包大小超过限制4M,Server检测不通过报错。
引用AWS RDS参数组中该参数的描述:
max_allowed_packet:Thisvaluebydefaultissmall,tocatchlarge(possiblyincorrect)packets.MustbeincreasedifusinglargeTEXTcolumnsorlongstrings.AsbigaslargestBLOB.
增加该参数的大小可以缓解报错,但是不能彻底的解决问题。
RDS实例被锁定
1)背景描述
公司每个月都会做一些营销活动,有个服务apush活动推送,单独部署在高可用版的RDS for MySQL 5.7,配置是4C8G 150G磁盘,数据库里也就4张表,晚上22:00下班走的时候,rds实例数据使用了50G空间,第二天早晨9:30在地铁上收到钉钉告警短信,提示push服务rds实例由于disk is full被locked with —read-only,开发也反馈,应用日志报了一堆MySQL error。
2)问题分析
通过DMS登录到数据库,看一下那个表最大,发现有张表push_log占用了100G+,看了下表结构,里面有两个text字段。
requesttextdefault''comment'请求信息', responsetextdefault''comment'响应信息' mysql>showtablestatuslike'push_log'; 发现Avg_row_length基本都在150KB左右,Rows=78w,表的大小约为780000*150KB/1024/1024=111.5G。
3)通过主键update也很慢
insertintouser(name,age,gender,info)values('thooo',35,'M',repeat('c',65535); insertintouser(name,age,gender,info)values('thooo11',35,'M',repeat('d',65535); insertintouser(name,age,gender,info)selectname,age,gender,infofromuser; QueryOK,6144rowsaffected(5.62sec) Records:6144Duplicates:0Warnings:0 [root@barret][test]>selectcount(*)fromuser; +----------+ |count(*)| +----------+ |24576| +----------+ 1rowinset(0.05sec)
做update操作并跟踪。
mysql>setprofiling=1; QueryOK,0rowsaffected,1warning(0.00sec) mysql>updateusersetinfo=repeat('f',65535)whereid=11; QueryOK,1rowaffected(0.28sec) Rowsmatched:1Changed:1Warnings:0 mysql>showprofiles; +----------+------------+--------------------------------------------------------+ |Query_ID|Duration|Query| +----------+------------+--------------------------------------------------------+ |1|0.27874125|updateusersetinfo=repeat('f',65535)whereid=11| +----------+------------+--------------------------------------------------------+ 1rowinset,1warning(0.00sec) mysql>showprofilecpu,blockioforquery1; +----------------------+----------+----------+------------+--------------+---------------+ |Status|Duration|CPU_user|CPU_system|Block_ops_in|Block_ops_out| +----------------------+----------+----------+------------+--------------+---------------+ |starting|0.000124|0.000088|0.000035|0|0| |checkingpermissions|0.000021|0.000014|0.000006|0|0| |Openingtables|0.000038|0.000026|0.000011|0|0| |init|0.000067|0.000049|0.000020|0|0| |Systemlock|0.000076|0.000054|0.000021|0|0| |updating|0.244906|0.000000|0.015382|0|16392| |end|0.000036|0.000000|0.000034|0|0| |queryend|0.033040|0.000000|0.000393|0|136| |closingtables|0.000046|0.000000|0.000043|0|0| |freeingitems|0.000298|0.000000|0.000053|0|0| |cleaningup|0.000092|0.000000|0.000092|0|0| +----------------------+----------+----------+------------+--------------+---------------+ 11rowsinset,1warning(0.00sec)
可以看到主要耗时在updating这一步,IO输出次数16392次,在并发的表上通过id做update,也会变得很慢。
4)group_concat也会导致查询报错
在业务开发当中,经常有类似这样的需求,需要根据每个省份可以定点医保单位名称,通常实现如下:
selectgroup_concat(dru_name)fromt_drugstoregroupbyprovince;
其中内置group_concat返回一个聚合的string,最大长度由参数group_concat_max_len(Maximum allowed result length in bytes for the GROUP_CONCAT())决定,默认是1024,一般都太短了,开发要求改长一点,例如1024000。
当group_concat返回的结果集的大小超过max_allowed_packet限制的时候,程序会报错,这一点要额外注意。
5)MySQL内置的log表
MySQL中的日志表mysql.general_log和mysql.slow_log,如果开启审计audit功能,同时log_output=TABLE,就会有mysql.audit_log表,结构跟mysql.general_log大同小异。
分别看一下他们的表结构。
CREATETABLE`general_log`( `event_time`timestamp(6)NOTNULLDEFAULTCURRENT_TIMESTAMP(6)ONUPDATECURRENT_TIMESTAMP(6), `user_host`mediumtextNOTNULL, `thread_id`bigint(21)unsignedNOTNULL, `server_id`int(10)unsignedNOTNULL, `command_type`varchar(64)NOTNULL, `argument`mediumblobNOTNULL )ENGINE=CSVDEFAULTCHARSET=utf8COMMENT='Generallog'
CREATETABLE`slow_log`( `start_time`timestamp(6)NOTNULLDEFAULTCURRENT_TIMESTAMP(6)ONUPDATECURRENT_TIMESTAMP(6), `user_host`mediumtextNOTNULL, `query_time`time(6)NOTNULL, `lock_time`time(6)NOTNULL, `rows_sent`int(11)NOTNULL, `rows_examined`int(11)NOTNULL, `db`varchar(512)NOTNULL, `last_insert_id`int(11)NOTNULL, `insert_id`int(11)NOTNULL, `server_id`int(10)unsignedNOTNULL, `sql_text`mediumblobNOTNULL, `thread_id`bigint(21)unsignedNOTNULL )ENGINE=CSVDEFAULTCHARSET=utf8COMMENT='Slowlog'
mysql.general_log记录的是经过MySQL Server处理的所有的SQL,包括后端和用户的,insert比较频繁,同时argument mediumblob NOT NULL,对MySQL Server性能有影响的,一般我们在dev环境为了跟踪排查问题,可以开启general_log,Production环境禁止开启general_log,可以开启audit_log,它是在general_log的基础上做了一些filter,比如我只需要业务账号发起的所有的SQL,这个很有用的,很多时候需要分析某一段时间内哪个SQL的QPS,TPS比较高。
mysql.slow_log记录的是执行超过long_query_time的所有SQL,如果遵循MySQL开发规范,slow query不会太多,但是开启了log_queries_not_using_indexes=ON就会有好多full table scan的SQL被记录,这时slow_log表会很大,对于RDS来说,一般只保留一天的数据,在频繁insert into slow_log的时候,做truncate table slow_log去清理slow_log会导致MDL,影响MySQL稳定性。
建议将log_output=FILE,开启slow_log, audit_log,这样就会将slow_log,audit_log写入文件,通过Go API处理这些文件将数据写入分布式列式数据库clickhouse中做统计分析。
text改造建议
使用ES存储
在MySQL中,一般log表会存储text类型保存request或response类的数据,用于接口调用失败时去手动排查问题,使用频繁的很低。可以考虑写入本地log file,通过filebeat抽取到ES中,按天索引,根据数据保留策略进行清理。
使用对象存储
有些业务场景表用到TEXT,BLOB类型,存储的一些图片信息,比如商品的图片,更新频率比较低,可以考虑使用对象存储,例如阿里云的OSS,AWS的S3都可以,能够方便且高效的实现这类需求。
总结
由于MySQL是单进程多线程模型,一个SQL语句无法利用多个cpu core去执行,这也就决定了MySQL比较适合OLTP(特点:大量用户访问、逻辑读,索引扫描,返回少量数据,SQL简单)业务系统,同时要针对MySQL去制定一些建模规范和开发规范,尽量避免使用text类型,它不但消耗大量的网络和IO带宽,同时在该表上的DML操作都会变得很慢。
另外建议将复杂的统计分析类的SQL,建议迁移到实时数仓OLAP中,例如目前使用比较多的clickhouse,里云的ADB,AWS的Redshift都可以,做到OLTP和OLAP类业务SQL分离,保证业务系统的稳定性。
原文链接:https://77isp.com/post/10240.html
=========================================
https://77isp.com/ 为 “云服务器技术网” 唯一官方服务平台,请勿相信其他任何渠道。
数据库技术 2022-03-28
网站技术 2022-11-26
网站技术 2023-01-07
网站技术 2022-11-17
Windows相关 2022-02-23
网站技术 2023-01-14
Windows相关 2022-02-16
Windows相关 2022-02-16
Linux相关 2022-02-27
数据库技术 2022-02-20
抠敌 2023年10月23日
嚼餐 2023年10月23日
男忌 2023年10月22日
瓮仆 2023年10月22日
簿偌 2023年10月22日
扫码二维码
获取最新动态