爲何阿里巴巴不建議MySQL使用Text類型?

有情懷,有乾貨,微信搜索【 三太子敖丙】關注這個不同的程序員。

本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及個人系列文章。mysql

前言

衆所周知,MySQL普遍應用於互聯網的OLTP(聯機事務處理過程)業務系統中,在大廠開發規範中,常常會看到一條"不建議使用text大字段類型」。git

下面就從text類型的存儲結構,引起的問題解釋下爲何不建議使用text類型,以及Text改造的建議方法。程序員

背景

寫log表致使DML慢

問題描述

某歪有一個業務系統,使用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類型。

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支持四種行格式:

行格式 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行格式。

Redundant行格式

這種格式爲了兼容舊版本MySQL。

行記錄格式:

Variable-length offset list record_header col1_value col2_value ……. text_value
字段長度偏移列表 記錄頭信息,佔48字節 列1數據 列2數據 ……. Text列指針數據

具備如下特色:

  • 存儲變長列的前768 Bytes在索引記錄中,剩餘的存儲在overflow page中,對於固定長度且超過768 Bytes會被當作變長字段存儲在off-page中。
  • 索引頁中的每條記錄包含一個6 Bytes的頭部,用於連接記錄用於行鎖。
  • 聚簇索引的記錄包含用戶定義的全部列。另外還有一個6字節的事務ID(DB_TRX_ID)和一個7字節長度的回滾段指針(Roll pointer)列。
  • 若是建立表沒有顯示指定主鍵,每一個聚簇索引行還包括一個6字節的行ID(row ID)字段。
  • 每一個二級索引記錄包含了全部定義的主鍵索引列。
  • 一條記錄包含一個指針來指向這條記錄的每一個列,若是一條記錄的列的總長度小於128字節,這個指針佔用1個字節,不然2個字節。這個指針數組稱爲記錄目錄(record directory)。指針指向的區域是這條記錄的數據部分。
  • 固定長度的字符字段好比CHAR(10)經過固定長度的格式存儲,尾部填充空格。
  • 固定長度字段長度大於或者等於768字節將被編碼成變長的字段,存儲在off-page中。
  • 一個SQL的NULL值存儲一個字節或者兩個字節在記錄目錄(record dirictoty)。對於變長字段null值在數據區域佔0個字節。對於固定長度的字段,依然存儲固定長度在數據部分,爲null值保留固定長度空間容許列從null值更新爲非空值而不會引發索引的分裂。
  • 對varchar類型,Redundant行記錄格式一樣不佔用任何存儲空間,而CHAR類型的NULL值須要佔用空間。

其中變長類型是經過長度 + 數據的方式存儲,不一樣類型長度是從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所支持。

行記錄格式:

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值,一方面佔用空間,另外一個方面影響索引列的穩定性。

具備如下特色:

  • 索引的每條記錄包含一個5個字節的頭部,頭部前面能夠有一個可變長度的頭部。這個頭部用來將相關連的記錄連接在一塊兒,也用於行鎖。
  • 記錄頭部的變長部分包含了一個表示null 值的位向量(bit vector)。若是索引中能夠爲null的字段數量爲N,這個位向量包含 N/8 向上取整的字節數。比例若是有9-16個字段能夠爲NULL值,這個位向量使用兩個字節。爲NULL的列不佔用空間,只佔用這個位向量中的位。頭部的變長部分還包含了變長字段的長度。每一個長度佔用一個或者2個字節,這取決了字段的最大長度。若是全部列均可覺得null 而且制定了固定長度,記錄頭部就沒有變長部分。
  • 對每一個不爲NULL的變長字段,記錄頭包含了一個字節或者兩個字節的字段長度。只有當字段存儲在外部的溢出區域或者字段最大長度超過255字節而且實際長度超過127個字節的時候會使用2個字節的記錄頭部。對應外部存儲的字段,兩個字節的長度指明內部存儲部分的長度加上指向外部存儲部分的20個字節的指針。內部部分是768字節,所以這個長度值爲 768+20, 20個字節的指針存儲了這個字段的真實長度。
  • NULL不佔該部分任何空間,即NULL除了佔用NULL標誌位,實際存儲不佔任何空間。
  • 記錄頭部跟着非空字段的數據部分。
  • 聚簇索引的記錄包含了因此用戶定於的字段。另外還有一個6字節的事務ID列和一個7字節的回滾段指針。
  • 若是沒有定於主鍵索引,則聚簇索引還包括一個6字節的Row ID列。
  • 每一個輔助索引記錄包含爲羣集索引鍵定義的不在輔助索引中的全部主鍵列。若是任何一個主鍵列是可變長度的,那麼每一個輔助索引的記錄頭都有一個可變長度的部分來記錄它們的長度,即便輔助索引是在固定長度的列上定義的。
  • 固定長度的字符字段好比CHAR(10)經過固定長度的格式存儲,尾部填充空格。
  • 對於變長的字符集,好比uft8mb3和utf8mb4, InnoDB試圖用N字節來存儲 CHAR(N)。若是CHAR(N)列的值的長度超過N字節,列後面的空格減小到最小值。CHAR(N)列值的最大長度是最大字符編碼數 x N。好比utf8mb4字符集的最長編碼爲4,則列的最長字節數是 4*N。

Text類型引起的問題

插入text字段致使報錯

建立測試表

[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.

增長該參數的大小能夠緩解報錯,可是不能完全的解決問題。

RDS實例被鎖定

背景描述

公司每月都會作一些營銷活動,有個服務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。

經過主鍵update也很慢

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,也會變得很慢。

group_concat也會致使查詢報錯

在業務開發當中,常常有相似這樣的需求,須要根據每一個省份能夠定點醫保單位名稱,一般實現以下:

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內置的log表

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中作統計分析。

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分離,保證業務系統的穩定性。

好啦以上就是本期的所有內容了,我是敖丙,你知道的越多,你不知道的越多,咱們下期見!

絮叨

敖丙把本身的面試文章整理成了一本電子書,共 1630頁!

乾貨滿滿,字字精髓。目錄以下,還有我複習時總結的面試題以及簡歷模板,如今免費送給你們。

連接:https://pan.baidu.com/s/1ZQEKJBgtYle3v-1LimcSwg 密碼:wjk6

我是敖丙,你知道的越多,你不知道的越多,感謝各位人才的:點贊收藏評論,咱們下期見!


文章持續更新,能夠微信搜一搜「 三太子敖丙 」第一時間閱讀,回覆【 資料】有我準備的一線大廠面試資料和簡歷模板,本文 GitHub https://github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。
相關文章
相關標籤/搜索