Quora 的流量涉及大量閱讀而非寫入,一直致力於優化讀和數據量而非寫。 0 資料庫負載的主要部分 讀取 數據量 寫入 1 優化讀取 1.1 不同類型的讀需要不同優化 ① 複雜查詢,如連接、聚合等 在查詢計數已成為問題的情況下,它們在另一個表中構建了計數,以便它們可以直接讀取計數值而非計算計數。 ② ...
Quora 的流量涉及大量閱讀而非寫入,一直致力於優化讀和數據量而非寫。
0 資料庫負載的主要部分
- 讀取
- 數據量
- 寫入
1 優化讀取
1.1 不同類型的讀需要不同優化
① 複雜查詢,如連接、聚合等
在查詢計數已成為問題的情況下,它們在另一個表中構建了計數,以便它們可以直接讀取計數值而非計算計數。
② 大型掃描
他們使用 LIMIT 改變它或使用分頁
③ 模式與查詢之間不匹配
若:
- 無很好的索引
- 或索引沒有足夠的列
- 或索引中的列順序對查詢來說不是最佳
則查詢可能很慢,可能對資料庫造成很大負載。
這種情況下,通常會修改索引以對查詢進行優化。 有時查詢也可修改以對索引進行優化。如:
- 刪除 select 子句中不必要的列(特別是索引中不存在的列)
- 刪除 order by 子句,改為在客戶端上排序(MySQL CPU 一般比客戶端 CPU 更寶貴)
- 若該查詢提供的功能不再重要,可完全刪除查詢
1.2 高 QPS 查詢
即使使用了優化的 SQL 和良好的模式,高 QPS查詢也給資料庫帶來很大負載。有時可能表示緩存效率低下(甚至沒緩存)。
① 低效的緩存是否導致了高 QPS 查詢?
緩存通常用於減少資料庫 QPS。緩存鍵的選擇可以極大地影響緩存的效率:
- 若緩存鍵過於具體或狹窄,可能導致資料庫出現高 QPS
- 若緩存鍵太寬泛,每次查詢都會從資料庫中拉取大量數據
② 對用戶語言表的查詢
我們有一個表跟蹤用戶使用的語言信息。通常會查詢資料庫以查看用戶 U 是否使用語言 L。使用(uid,language_id)作為緩存鍵看起來合理。如緩存未命中,將為該 uid 和 language_id 查詢資料庫表。
因此,將緩存鍵更改為僅使用 uid 確實有意義,緩存值將是有關用戶使用的所有語言的信息。
以上述方式更改緩存鍵,會增加從庫表中每次查詢獲取的數據量,但它將 QPS 減少超過 90%。大多數用戶只使用一或幾種語言。 因此,大多數情況,新的查詢並沒有拉取比以前更多的數據,這是一個顯然的優化!
③ 查詢 A2A(ask to answer)表
這裡我們處理 3 個實體間的關係,即用戶(誰提問或關註問題)、問題和回答者,這比 2 個實體之間的關係更不常見。
通常產品邏輯是查詢:
- 用戶已請求過的所有回答者,使用緩存鍵 (question_id, user_id)
- 請求過相同回答者回答一個問題的所有用戶,使用緩存鍵 (question_id, answerer_id)
綜上,A2A 表的 QPS 非常高,這意味著上述緩存效果並不明顯。上述兩個緩存都在使用 2 個實體作為緩存鍵question_id 和 user_id(可以是提問者或回答者)。
潛在緩存鍵數量巨大,因為它是問題數和用戶數的乘積,其中只有很少的組合實際上在表中有數據。所以它可看作一個稀疏的數據集,有2維。
大多數問題的 A2A 請求數量相對較少,但有少數問題的 A2A 數量要多得多。因此,添加額外緩存,該緩存包含問題的 A2A,最多限製為 N 個,以便我們捕獲大多數問題。 該緩存的鍵只是 question_id。 如緩存列表大小小於N,我們知道緩存是完整的。 否則,緩存不完整,我們不會使用緩存。
這額外緩存幫助顯著減少 A2A 表上的 QPS(在 50% 到 66% 的範圍內)。 還對產品邏輯進行了其他更改,以提高效率,但 QPS 的減少大部分來自額外緩存。
1.3 一維數據集中的稀疏數據
Quora 在緩存方面經常遇到的另一個問題是:稀疏一維數據集。如可能需要查詢資料庫,看某問題是否需重定向到另一問題(如同一個問題被重新發佈,就可能發生這種情況)。
絕大多問題不需要重定向,所以 Quora 只會獲取幾個“重定向”,而大量“不重定向”。
當他們只是緩存了 question_id ,緩存中就會填滿不用,只有幾個重定向。 這在緩存中占用大量空間,且由於“重定向”數量如此稀疏,也會導致大量緩存未命中。
相反,他們開始緩存範圍。 如 question id 123–127的任一問題都沒重定向,那麼他們會將該範圍緩存為所有問題均為 No,而不是緩存每個單獨的 question id。
這大大降低此類查詢的資料庫負載,QPS 下降 90%。
2 優化表占用空間
由於以下幾個原因,表大小很重要:
- 存儲更多數據的成本更高
- 隨表增長,適應資料庫緩衝池的數據百分比會變小,即IO會逐漸增加,性能會逐漸下降
- 備份和恢復時間會隨表大小線性增長。雖然備份是從 MySQL 副本完成的,但我們也會從副本讀數據。在備份期間,MySQL副本性能略有下降
- 隨表增長,備份大小也在增長,導致備份存儲成本隨時間增長
顯然,對不需要永久存儲的數據,制定最佳保留策略有助減少表大小 —— 使用 MyRocks 減少表大小
- 有一些表對於表所有者來說無法接受任何數據的刪除。為此研究使用 MyRocks 來減小空間使用
- MySQL 中的表可能使用更複雜的模式和查詢。 所以他們希望謹慎使用 MyRocks。 作為分片項目的一部分,已對 MySQL 中最大的表進行分片,這是在 MySQL 在 Quora 的分片中記錄
- 此表是基於自增列範圍進行分片的,與基於時間的分片接近,因為自增列值隨時間增加
- 大多數查詢訪問最近的分片。 包含 18 個月以上舊數據的較舊分片對日常業務相對不太關鍵
因此,他們決定按如下方式將較舊的分片移至 MyRocks。 有個工具可將 MySQL 表從一個 MySQL 主伺服器移動到另一個主伺服器。 每個分片實際上是一個 MySQL 表。 他們能夠使用該工具按如下方式將包含舊數據的 MySQL 分片轉換為 MyRocks 分片:
- 在 MyRocks 主伺服器上使用相同的模式創建一個新的空表,但使用 RocksDB 存儲引擎
- 使用該工具複製數據並從 MySQL 主伺服器重放binlog(二進位日誌)到 MyRocks 主伺服器。 (該工具已被修改為跳過在目標主機上創建表,因為它已經在前一步中創建過。)
- 執行陰影讀取測試以驗證 MyRocks 分片返回的結果與 MySQL 分片的結果相同。
- 將流量切換到 MyRocks 分片。 (這類似於我們在將 MySQL 表從一個 MySQL 主伺服器移動到另一個 MySQL 主伺服器時執行的切換。 源主機上的表被重命名以停止新寫入,然後在重放趕上後,該表的流量會切換到目標主機。)
- 對於非鍵值存儲表使用 MyRocks 是我們的一個重大舉措。 根據表的不同,空間使用量的減少也有差異。 對於上面提到的第一個表,我們看到每個已移動的分片使用的空間減少了 80% 以上! 對於第二個表,我們看到每個已移動的分片使用的空間只減少了約 50-60%。
3 優化寫入
有時複製延遲警報,因為 MySQL複製預設情況下會在副本上串列重放主伺服器上的併發寫。在主伺服器上並行寫入而在副本上串列重放寫入並不適合擴展寫入,特別是如果他們使用帶多核 CPU 的機器。
MySQL 提供兩種方法實現這點,如下所述。兩種方法中都需使用 slave_parallel_workers
配置並行度。
- slave_parallel_type=LOGICAL_CLOCK(從 MySQL 8.0.26 開始為 replica_parallel_type)
- MySQL 5.7開始可用。即使所有表都在同一邏輯資料庫中,它也可以在副本上並行執行寫。
- slave_parallel_type=DATABASE(從 MySQL 8.0.26 開始為 replica_parallel_type)
- 這需要表位於多個邏輯資料庫中才能並行執行寫
- 增強存儲在 zk 中的資料庫配置,以跟蹤表所在的邏輯資料庫。將此信息保存在 zk 而非代碼庫或靜態配置中,允許動態更改現有表的邏輯資料庫。大多數表都位於預設邏輯資料庫,因此只需要為不在預設邏輯資料庫中的表保留此信息
- MySQL alter table 語句可用於更改表的邏輯資料庫,如
alter table <logical_db1>.table rename <logical_db2>.mytable
。 它不複製數據,只是將底層 ibd 文件從一個目錄移動到另一個目錄,速度很快。移動表後,我們還會在 zk 更新資料庫配置,以便應用程式可找到該表 - 他們將一個表移動到其自己的邏輯資料庫並啟用並行複製。有助減少包含該表的 MySQL 副本上的複製延遲。
4 結論
學習了世界級大廠如何使用各種技術的組合來優化資料庫中的讀取、寫入和空間使用。你們公司如何優化的呢?歡迎和我一起交流。
參考:
本文由博客一文多發平臺 OpenWrite 發佈!