為什么不要問我DB極限QPS/TPS
背景 相信很多開發都會有這個疑問,DB到底可以支撐多大的業務量,如何去評估?對於這個很專業的問題,DBA也沒有辦法直接告訴你,更多的都是靠經驗提供一個看似靠譜的結果,這里主要說明數據庫容量評估的難點。
定性分析
借用學校時候做物理題的一個思考方法 -- 極限法;我們假設兩種極限場景: 極限場景一,所有SQL 都是主鍵等值查詢。極限場景二,所有SQL 都是走不上索引的全表掃描。這兩種場景下大家都能夠一眼看出數據庫的支撐能力,在場景一和場景二下會有很大的差別。當然,我們現實的業務場景,位於兩種極限場景之間,這個時候很難簡單粗暴的說當前實例可以支撐多少業務量,因為缺少信息輸入。DBA 同學一定會和研發同學進行詳細的溝通,確認數據庫運行SQL 的類型,以及不同類型SQL 的執行頻率,所涉及表的數據量情況,綜合評估一個可以支撐的性能區間,作為上線前的基本容量建設模型。
隨着系統上線后,數據庫系統就會一直處於一種變化的狀態。變化一,SQL 類型,隨着業務邏輯不斷豐富,運行SQL 開始逐漸變得更復雜,從最開始設計的幾類SQL 到 十幾類SQL 甚至 幾十類SQL,這就要求我們對新上線的SQL 書寫質量有一個保障機制,就是大家熟悉的SQL Review,當前有人工Review 和 IDB 自動Review 兩種保障機制。變化二,業務表的數據量也在不斷增長,針對小表很高效的SQL,隨着數據量的增長,運行效率會逐步下降,出現大促業務流量突增時,就是一個穩定性風險。DBA 針對這幾類問題都有相應的處理策略,這不是今天的重點,會在后續的系列文章中逐一和大家介紹。
常見影響性能案例
大規模數據導出功能
相信很多業務都遇到過數據導出,明細展示這方面的需求,sql基本上都是先求一個數據的總和然后,limit n,m分頁查詢,這樣的問題就在於,在掃描前面的數據時是不會有性能問題的,當n值越大,偏移量越多,掃描的數據就越多,這個時候就會產生問題,一個本來不的sql就會變成慢sql,導致DB性能下降。針對這種問題DBA都會建議開發將limit n,m改為id范圍的查詢,或者進行業務改造對於一些不必要的場景只展示前幾百條,只需要進行一次分頁即可。
類似sql模式:
select count(*) from table_name_1; select * from table_name_1 limit n,m;(n值越大性能越差) 建議改造成: select * from table_name_1 where id>? and id<?
ERP類系統使用聚合函數或者分組排序
類似倉庫內管理系統會需要展示很多統計信息,很多開發會選擇在DB端計算出結果直接展示,問題在於sum,max,min類的聚合函數在DB端執行會消耗到CPU資源,如果這個時候還遇到索引不合理的情況,往往會帶來災難性的后果。這種情況DB端除了增加索引,對CPU的消耗是無法優化的,所以DB性能必然下降。一般這種情況DBA會建議能在程序端計算的就不要放在DB端,或者直接接搜索引擎。
類似sql模式:
select sum(column_name) as column_1 from table_name_1; or select distinct cloumn_name from table_name_1 group by column_name_1 order by column_name_1;
錯誤使用子查詢
在DB端執行去重,join以及子查詢等操作的時候,mysql會自動創建臨時表。
DB自動創建臨時表的情況有如下幾種
1. Evaluation of UNION statements.
2. Evaluation of some views, such those that use the TEMPTABLE algorithm, UNION, or aggregation.
3. Evaluation of derived tables (subqueries in the FROM clause).(這個是本節關注的重點)
4. Tables created for subquery or semi-join materialization (see Section 8.2.1.18, “Subquery Optimization”).
5. Evaluation of statements that contain an ORDER BY clause and a different GROUP BY clause, or for which the ORDER BY or GROUP BY contains columns from tables other than the first table in the join queue.
6. Evaluation of DISTINCT combined with ORDER BY may require a temporary table.
7. For queries that use the SQL_SMALL_RESULT option, MySQL uses an in-memory temporary table, unless the query also contains elements (described later) that require on-disk storage.
8. Evaluation of multiple-table UPDATE statements.
9. Evaluation of GROUP_CONCAT() or COUNT(DISTINCT) expressions.
在mysql中,對於子查詢,外層每執行一次,內層子查詢要重復執行一次,所以一般建議用join代替子查詢。
下面舉一個子查詢引起DB性能問題的例子
Query1:select count(*) from wd_order_late_reason_send wrs left join wd_order_detail_late_send wds on wrs.store_code = wds.store_code;
下面是執行計划:
*************************<strong> 1. row </strong>***********************<strong> id: 1 select_type: SIMPLE table: wrs type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 836846 Extra: NULL </strong>***********************<strong> 2. row </strong>************************* id: 1 select_type: SIMPLE table: wds type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 670612 Extra: Using where; Using join buffer (Block Nested Loop)
Query2:select count(*) from (select wrs.store_code from wd_order_late_reason_send wrs left join wd_order_detail_late_send wds on wrs.store_code = wds.store_code) tb;
執行計划如下
*************************<strong> 1. row </strong>***********************<strong> id: 1 select_type: PRIMARY table: <derived2> type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 561198969752 Extra: NULL </strong>***********************<strong> 2. row </strong>***********************<strong> id: 2 select_type: DERIVED table: wrs type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 836846 Extra: NULL </strong>***********************<strong> 3. row </strong>************************* id: 2 select_type: DERIVED table: wds type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 670612 Extra: Using where; Using join buffer (Block Nested Loop)
這兩個sql結果相同,唯一不同的是第二條sql使用了子查詢。通過執行計划可以看出(排除沒有索引部分)兩個sql最大的差別就是第二個sql有derived table並且rows是561198969752,出現這個數值是因為在select count(*)每次計數的時候子查詢的sql都會執行一遍,所以最后是子查詢join的笛卡爾積。因為內存中用於進行join操作的空間有限,這個時候就會使用磁盤空間來創建臨時表,所以當第二種sql頻繁執行的時候會有磁盤被撐爆的風險。 想要了解更多關於子查詢的優化可以參考下面這個鏈接link
慢sql
這里我們所說的慢sql主要指那些由於索引使用不正確或沒有使用索引產生的,一般可以通過增加索引。一個合理的索引對一條sql性能的影響是非常巨大的。索引的主要目的是為了減少讀取的數據塊,也就是我們常說的邏輯讀,讀取的數據塊越少,sql效率越高。另外索引在一定程度上也可以減少CPU的消耗,例如排序,分組,因為索引本來就是有序的。
說到邏輯讀,對應的就會有物理讀,在mysql服務端是有buffer pool來緩存硬盤中的數據,但是這個buffer pool的大小跟磁盤中數據文件的大小是不等的,往往buffer pool會遠遠小於磁盤中數據的大小。buffer pool會有一個LRU鏈表,當從磁盤中加載數據塊到內存中(這個就是物理讀)發現沒有空間的時候會優先覆蓋LRU鏈表中的數據塊。當一條sql沒有合理的索引需要掃描大量的數據的時候,不光要掃描內存中的許多數據塊,還可能需要從磁盤中加載不同不存在的數據塊到內存中進行判斷,當這種情況頻繁發生的時候,sql性能就會急劇下降,因而也影響了DB實例的性能。
以下表格是訪問不同存儲設備的rt,由此可見一個合理的索引的重要性。
| 類別 | 吞吐量 | 響應時間 |
|---|---|---|
| 訪問L1 | Cache | 0.5ns |
| 訪問L2 | Cache | 7ns |
| 內存訪問 | 800M/s | 100ns |
| 機械盤 | 300M/s | 10ms |
| SSD | 300M/s | 0.1~0.2ms |
日志刷盤策略不合理
目前集團mysql大部分使用的都是innodb存儲引擎,因此在每條DML語句執行時不光會記如binlog還有記錄innodb特有的redo log和undo log。這些日志文件都是先寫入內存中然后在刷新到磁盤中。在server端有兩個參數分別控制他們的寫入速度。innodb_flush_log_at_trx_commit控制redo log寫入模式,sync_binlog控制binlog寫入模式。


通過以上表格可以了解到,在使用線上默認配置的情況下每次commit都會刷redo log到磁盤,也就是說每次寫入都會伴隨着日志刷盤的操作,需要消耗磁盤IO,所以在高TPS或者類似業務大促情況下,DBA可以調整這個參數,來提升DB支撐TPS的能力。
BP設置過小
前面已經提到sql在讀寫數據的時候不會直接跟磁盤交互,而是先讀寫內存數據,因為這樣最快。但是考慮到成本問題BP(buffer pool)大小是有限的,不可能跟數據文件同等大小,所以如果BP設置不合理就會導致DB的QPS TPS始終上不去。下面我們具體分析一下。
mysql buffer pool中包含undo page,insert buffer page,adaptive hash index,index page,lock info,data dictionary等等DB相關信息,但是這些page都可以歸為三類free page,clean page,dirty page.buffer pool中維護了三個鏈表:free list,dirty list,lru list
- free page:此page未被使用,此種類型page位於free鏈表中
- clean page:此page被使用,對應數據文件中的一個頁面,但是頁面沒有被修改,此種類型page位於lru鏈表中
- dirty page:此page被使用,對應數據文件中的一個頁面,但是頁面被修改過,此種類型page位於lru鏈表和flush鏈表中
當BP設置過小的時候,比如BP 10g 數據文件有200g 這個時候有大量的select或者dml語句,mysql就會頻繁的刷新lru list或者dirty list 到磁盤,大部分時間消耗在刷磁盤上,而不是業務sql處理上,這個時候就會導致業務TPS QPS始終上不去,伴隨着DB內存命中率降低。通常這個時候的解決辦法是需要DBA調整一下實例BP的大小。
硬件問題
就像生活中會有意外一樣,在排除了之前那些因素之后,還會存在因為硬件故障或者參數設置不合理導致DB性能抖動的情況,如果不能立即修復,DBA一般只能通過遷移實例的方式來消除影響。
寫在后面
經過上面幾個情景的描述,我們可以把影響線上DB性能的因素歸為三類:1、業務邏輯問題 2、DB端設置問題 3、硬件問題。因為硬件問題屬於小概率事件,所以影響線上DB性能的主要是前面兩類因素,也因此不同的業務場景下,DB的表現是天差地別的。
