MySQL廣泛應用於互聯網的OLTP(聯機事務處理過程)業務系統中,在大廠開發規范中,經常會看到一條"不建議使用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支持四種行格式:
| 行格式 | 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 |
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分離,保證業務系統的穩定性。
