有情怀,有干货,微信搜索【 三太子敖丙】关注这个不同的程序员。本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及个人系列文章。mysql
众所周知,MySQL普遍应用于互联网的OLTP(联机事务处理过程)业务系统中,在大厂开发规范中,常常会看到一条"不建议使用text大字段类型”。git
下面就从text类型的存储结构,引起的问题解释下为何不建议使用text类型,以及Text改造的建议方法。程序员
某歪有一个业务系统,使用RDS for MySQL 5.7的高可用版本,配置long_query_time=1s,添加慢查询告警,我第一反应就是某歪又乱点了。github
我经过监控看CPU, QPS,TPS等指标不是很高,最近恰好双十一全站都在作营销活动,用户量稍微有所增长。 某歪反馈有些本来不慢的接口变的很慢,影响了正常的业务,须要作一下troubleshooting。面试
我从慢查询告警,能够看到有一些insert和update语句比较慢,同时告警时段的监控,发现IOPS很高,达到了70MB/s左右,因为RDS的CloundDBA功能不可用,又没有audit log功能,troubleshooting比较困难,硬着头皮只能分析binlog了。sql
配置了max_binlog_size =512MB,在IOPS高的时段里,看下binlog的生成状况。数据库
须要分析为何binlog写这么快,最有可能缘由就是insert into request_log表上有text类型,request_log表结构以下(demo)后端
CREATE TABLE request_log (` `id bigint(20) NOT NULL AUTO_INCREMENT,` `log text,` `created_at datetime NOT NULL,` `status tinyint(4) NOT NULL,` `method varchar(10) DEFAULT NULL,` `url varchar(50) DEFAULT NULL,` `update_at datetime DEFAULT NULL,` `running_time tinyint(4) DEFAULT '0',` `user_id bigint(20) DEFAULT NULL,` `type varchar(50) DEFAULT NULL,` `PRIMARY KEY (id)` `) ENGINE=InnoDB AUTO_INCREMENT=4229611 DEFAULT CHARSET=utf8`
分析binlog:数组
$ mysqlbinlog --no-defaults -v -v --base64-output=DECODE-ROWS mysql-bin.000539|egrep "insert into request_log"
满屏幕都是看不清的内容,翻了半天没翻完。缓存
基本上已经肯定是写入request_log的log字段引发的,致使binlog_cache频繁的flush,以及binlog过分切换,致使IOPS太高,影响了其余正常的DML操做。
跟开发同窗沟通后,计划在下一个版本修复这个问题,再也不将request信息写入表中,写入到本地日志文件,经过filebeat抽取到es进行查询,若是只是为了查看日志也能够接入grayLog等日志工具,不必写入数据库。
文章最后我还会介绍几个MySQL 我踩过Text相关的坑,这介绍坑以前我先介绍下MySQLText类型。
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数据页由如下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支持四种行格式:
行格式 | Compact存储特性 | 加强的变长列存储 | 支持大前缀索引 | 支持压缩 | 支持表空间类型 |
---|---|---|---|---|---|
REDUNDANT | No | No | No | No | system, file-per-table, general |
COMPACT | Yes | No | No | No | system, file-per-table, general |
DYNAMIC | Yes | Yes | Yes | No | system, file-per-table, general |
COMPRESSED | Yes | Yes | Yes | Yes | file-per-table, general |
因为Dynamic是Compact变异而来,结构大同而已,如今默认都是Dynamic格式;COMPRESSED主要是对表和索引数据进行压缩,通常适用于使用率低的归档,备份类的需求,主要介绍下REDUNDANT和COMPACT行格式。
这种格式为了兼容旧版本MySQL。
行记录格式:
Variable-length offset list | record_header | col1_value | col2_value | ……. | text_value |
---|---|---|---|---|---|
字段长度偏移列表 | 记录头信息,占48字节 | 列1数据 | 列2数据 | ……. | Text列指针数据 |
具备如下特色:
其中变长类型是经过长度 + 数据的方式存储,不一样类型长度是从1到4个字节(L+1 到 L + 4),对于TEXT类型的值须要L Bytes存储value,同时须要2个字节存储value的长度。同时Innodb最大行长度规定为65535 Bytes,对于Text类型,只保存9到12字节的指针,数据单独存在overflow page中。
这种行格式比redundant格式减小了存储空间做为代价,可是会增长某些操做的CPU开销。若是系统workload是受缓存命中率和磁盘速度限制,compact行格式可能更快。若是你的工做负载受CPU速度限制,compact行格式可能更慢,Compact 行格式被全部file format所支持。
行记录格式:
Variable-length field length list | NULL标志位 | record_header | col1_value | col2_value | ……. | text_value |
---|---|---|---|---|---|---|
变长字段长度列表 | 记录头信息- | 列1数据 | 列2数据 | ……. | Text列指针数据 |
Compact首部是一个非NULL变长字段长度的列表,而且是按列的顺序逆序放置的,若列的长度小于255字节,用1字节表示;若大于255个字节,用2字节表示。变长字段最大不能够超过2字节,这是由于MySQL数据库中varchar类型最大长度限制为65535,变长字段以后的第二个部分是NULL标志位,表示该行数据是否有NULL值。有则用1表示,该部分所占的字节应该为1字节。
因此在建立表的时候,尽可能使用NOT NULL DEFAULT '',若是表中列存储大量的NULL值,一方面占用空间,另外一个方面影响索引列的稳定性。
具备如下特色:
[root@barret] [test]>create table user(id bigint not null primary key auto_increment, -> name varchar(20) not null default '' comment '姓名', -> age tinyint not null default 0 comment 'age', -> gender char(1) not null default 'M' comment '性别', -> info text not null comment '用户信息', -> create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立时间', -> update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间' -> ); Query OK, 0 rows affected (0.04 sec)
root@barret] [test]>insert into user(name,age,gender,info) values('moon', 34, 'M', repeat('a',1024*1024*3)); ERROR 1406 (22001): Data too long for column 'info' at row 1 [root@barret] [test]>insert into user(name,age,gender,info) values('sky', 35, 'M', repeat('b',1024*1024*5)); ERROR 1301 (HY000): Result of repeat() was larger than max_allowed_packet (4194304) - truncated
[root@barret] [test]>select @@max_allowed_packet; +----------------------+ | @@max_allowed_packet | +----------------------+ | 4194304 | +----------------------+ 1 row in set (0.00 sec)
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: This value by default is small, to catch large (possibly incorrect) packets. Must be increased if using large TEXT columns or long strings. As big as largest BLOB.
增长该参数的大小能够缓解报错,可是不能完全的解决问题。
公司每月都会作一些营销活动,有个服务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。
经过DMS登陆到数据库,看一下那个表最大,发现有张表push_log占用了100G+,看了下表结构,里面有两个text字段。
request text default '' comment '请求信息', response text default '' comment '响应信息' mysql>show table status like 'push_log';
发现Avg_row_length基本都在150KB左右,Rows = 78w,表的大小约为780000*150KB/1024/1024 = 111.5G。
insert into user(name,age,gender,info) values('thooo', 35, 'M', repeat('c',65535); insert into user(name,age,gender,info) values('thooo11', 35, 'M', repeat('d',65535); insert into user(name,age,gender,info) select name,age,gender,info from user; Query OK, 6144 rows affected (5.62 sec) Records: 6144 Duplicates: 0 Warnings: 0 [root@barret] [test]>select count(*) from user; +----------+ | count(*) | +----------+ | 24576 | +----------+ 1 row in set (0.05 sec)
作update操做并跟踪。
mysql> set profiling = 1; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> update user set info = repeat('f',65535) where id = 11; Query OK, 1 row affected (0.28 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> show profiles; +----------+------------+--------------------------------------------------------+ | Query_ID | Duration | Query | +----------+------------+--------------------------------------------------------+ | 1 | 0.27874125 | update user set info = repeat('f',65535) where id = 11 | +----------+------------+--------------------------------------------------------+ 1 row in set, 1 warning (0.00 sec) mysql> show profile cpu,block io for query 1; +----------------------+----------+----------+------------+--------------+---------------+ | Status | Duration | CPU_user | CPU_system | Block_ops_in | Block_ops_out | +----------------------+----------+----------+------------+--------------+---------------+ | starting | 0.000124 | 0.000088 | 0.000035 | 0 | 0 | | checking permissions | 0.000021 | 0.000014 | 0.000006 | 0 | 0 | | Opening tables | 0.000038 | 0.000026 | 0.000011 | 0 | 0 | | init | 0.000067 | 0.000049 | 0.000020 | 0 | 0 | | System lock | 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 | | query end | 0.033040 | 0.000000 | 0.000393 | 0 | 136 | | closing tables | 0.000046 | 0.000000 | 0.000043 | 0 | 0 | | freeing items | 0.000298 | 0.000000 | 0.000053 | 0 | 0 | | cleaning up | 0.000092 | 0.000000 | 0.000092 | 0 | 0 | +----------------------+----------+----------+------------+--------------+---------------+ 11 rows in set, 1 warning (0.00 sec)
能够看到主要耗时在updating这一步,IO输出次数16392次,在并发的表上经过id作update,也会变得很慢。
在业务开发当中,常常有相似这样的需求,须要根据每一个省份能够定点医保单位名称,一般实现以下:
select group_concat(dru_name) from t_drugstore group by province;
其中内置group_concat返回一个聚合的string,最大长度由参数group_concat_max_len(Maximum allowed result length in bytes for the GROUP_CONCAT())决定,默认是1024,通常都过短了,开发要求改长一点,例如1024000。
当group_concat返回的结果集的大小超过max_allowed_packet限制的时候,程序会报错,这一点要额外注意。
MySQL中的日志表mysql.general_log和mysql.slow_log,若是开启审计audit功能,同时log_output=TABLE,就会有mysql.audit_log表,结构跟mysql.general_log大同小异。
分别看一下他们的表结构
CREATE TABLE `general_log` ( `event_time` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `user_host` mediumtext NOT NULL, `thread_id` bigint(21) unsigned NOT NULL, `server_id` int(10) unsigned NOT NULL, `command_type` varchar(64) NOT NULL, `argument` mediumblob NOT NULL ) ENGINE=CSV DEFAULT CHARSET=utf8 COMMENT='General log'
CREATE TABLE `slow_log` ( `start_time` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `user_host` mediumtext NOT NULL, `query_time` time(6) NOT NULL, `lock_time` time(6) NOT NULL, `rows_sent` int(11) NOT NULL, `rows_examined` int(11) NOT NULL, `db` varchar(512) NOT NULL, `last_insert_id` int(11) NOT NULL, `insert_id` int(11) NOT NULL, `server_id` int(10) unsigned NOT NULL, `sql_text` mediumblob NOT NULL, `thread_id` bigint(21) unsigned NOT NULL ) ENGINE=CSV DEFAULT CHARSET=utf8 COMMENT='Slow log'
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中作统计分析。
在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分离,保证业务系统的稳定性。
好啦以上就是本期的所有内容了,我是敖丙,你知道的越多,你不知道的越多,咱们下期见!
敖丙把本身的面试文章整理成了一本电子书,共 1630页!
干货满满,字字精髓。目录以下,还有我复习时总结的面试题以及简历模板,如今免费送给你们。
连接:https://pan.baidu.com/s/1ZQEKJBgtYle3v-1LimcSwg 密码:wjk6
我是敖丙,你知道的越多,你不知道的越多,感谢各位人才的:点赞、收藏和评论,咱们下期见!
文章持续更新,能够微信搜一搜「 三太子敖丙 」第一时间阅读,回复【 资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub https://github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。