TiDB 在摩拜單車的深度實踐及應用

bluetooth發表於2021-09-09

作者介紹:呂磊,摩拜單車高階 DBA。

一、業務場景

摩拜單車 2017 年開始將 TiDB 嘗試應用到實際業務當中,根據業務的不斷髮展,TiDB 版本快速迭代,我們將 TiDB 在摩拜單車的使用場景逐漸分為了三個等級:

  • P0 級核心業務:線上核心業務,必須單業務單叢集,不允許多個業務共享叢集效能,跨 AZ 部署,具有異地災備能力。
  • P1 級線上業務:線上業務,在不影響主流程的前提下,可以允許多個業務共享一套 TiDB 叢集。
  • 離線業務叢集:非線上業務,對實時性要求不高,可以忍受分鐘級別的資料延遲。

本文會選擇三個場景,給大家簡單介紹一下 TiDB 在摩拜單車的使用姿勢、遇到的問題以及解決方案。

二、訂單叢集(P0 級業務)

訂單業務是公司的 P0 級核心業務,以前的 Sharding 方案已經無法繼續支撐摩拜快速增長的訂單量,單庫容量上限、資料分佈不均等問題愈發明顯,尤其是訂單合庫,單表已經是百億級別,TiDB 作為 Sharding 方案的一個替代方案,不僅完美解決了上面的問題,還能為業務提供多維度的查詢。

2.1 訂單 TiDB 叢集的兩地三中心部署架構

圖 1  兩地三中心部署架構圖

圖 1 兩地三中心部署架構圖

整個叢集部署在三個機房,同城 A、同城 B、異地 C。由於異地機房的網路延遲較高,設計原則是儘量使 PD Leader 和 TiKV Region Leader 選在同城機房(Raft 協議只有 Leader 節點對外提供服務),我們的解決方案如下:

  • PD 通過 Leader priority 將三個 PD server 優先順序分別設定為 5 5 3。
  • 將跨機房的 TiKV 例項通過 label 劃分 AZ,保證 Region 的三副本不會落在同一個 AZ 內。
  • 通過 label-property reject-leader 限制異地機房的 Region Leader,保證絕大部分情況下 Region 的 Leader 節點會選在同城機房 A、B。

2.2 訂單叢集的遷移過程以及業務接入拓撲

圖 2 訂單叢集的遷移過程以及業務接入拓撲圖

圖 2 訂單叢集的遷移過程以及業務接入拓撲圖

為了方便描述,圖中 Sharding-JDBC 部分稱為老 Sharding 叢集,DBProxy 部分稱為新 Sharding 叢集。

  • 新 Sharding 叢集按照 order_id 取模通過 DBproxy 寫入各分表,解決資料分佈不均、熱點等問題。
  • 將老 Sharding 叢集的資料通過使用 DRC(摩拜自研的開源異構資料同步工具 Gravity)全量+增量同步到新 Sharding 叢集,並將增量資料進行打標,反向同步鏈路忽略帶標記的流量,避免迴圈複製。
  • 為支援上線過程中業務回滾至老 Sharding 叢集,需要將新 Sharding 叢集上的增量資料同步回老 Sharding 叢集,由於寫回老 Sharding 叢集需要耦合業務邏輯,因此 DRC(Gravity)負責訂閱 DBProxy-Sharding 叢集的增量數放入 Kafka,由業務方開發一個消費 Kafka 的服務將資料寫入到老 Sharding 叢集。
  • 新的 TiDB 叢集作為訂單合庫,使用 DRC(Gravity)從新 Sharding 叢集同步資料到 TiDB中。
  • 新方案中 DBProxy 叢集負責 order_id 的讀寫流量,TiDB 合庫作為 readonly 負責其他多維度的查詢。

2.3 使用 TiDB 遇到的一些問題

2.3.1 上線初期新叢集流量灰度到 20% 的時候,發現 TiDB coprocessor 非常高,日誌出現大量 server is busy 錯誤。

問題分析:

  • 訂單資料單表超過 100 億行,每次查詢涉及的資料分散在 1000+ 個 Region 上,根據 index 構造的 handle 去讀表資料的時候需要往這些 Region 上傳送很多 distsql 請求,進而導致 coprocessor 上 gRPC 的 QPS 上升。
  • TiDB 的執行引擎是以 Volcano 模型執行,所有的物理 Executor 構成一個樹狀結構,每一層通過呼叫下一層的 Next/NextChunk() 方法獲取結果。Chunk 是記憶體中儲存內部資料的一種資料結構,用於減小記憶體分配開銷、降低記憶體佔用以及實現記憶體使用量統計/控制,TiDB 2.0 中使用的執行框架會不斷呼叫 Child 的 NextChunk 函式,獲取一個 Chunk 的資料。每次函式呼叫返回一批資料,資料量由一個叫 tidb_max_chunk_size 的 session 變數來控制,預設是 1024 行。訂單表的特性,由於資料分散,實際上單個 Region 上需要訪問的資料並不多。所以這個場景 Chunk size 直接按照預設配置(1024)顯然是不合適的。

解決方案:

  • 升級到 2.1 GA 版本以後,這個引數變成了一個全域性可調的引數,並且預設值改成了 32,這樣記憶體使用更加高效、合理,該問題得到解決。

2.3.2 資料全量匯入 TiDB 時,由於 TiDB 會預設使用一個隱式的自增 rowid,大量 INSERT 時把資料集中寫入單個 Region,造成寫入熱點。

解決方案:

  • 通過設定 SHARD_ROW_ID_BITS,可以把 rowid 打散寫入多個不同的 Region,緩解寫入熱點問題:ALTER TABLE table_name SHARD_ROW_ID_BITS = 8;

2.3.3 異地機房由於網路延遲相對比較高,設計中賦予它的主要職責是災備,並不提供服務。曾經出現過一次大約持續 10s 的網路抖動,TiDB 端發現大量的 no Leader 日誌,Region follower 節點出現網路隔離情況,隔離節點 term 自增,重新接入叢集時候會導致 Region 重新選主,較長時間的網路波動,會讓上面的選主發生多次,而選主過程中無法提供正常服務,最後可能導致雪崩。

問題分析:

  • Raft 演算法中一個 Follower 出現網路隔離的場景,如下圖所示。

圖 3  Raft 演算法中,Follower 出現網路隔離的場景圖

圖 3 Raft 演算法中,Follower 出現網路隔離的場景圖
  • Follower C 在 election timeout 沒收到心跳之後,會發起選舉,並轉換為 Candidate 角色。
  • 每次發起選舉時都會把 term 加 1,由於網路隔離,選舉失敗的 C 節點 term 會不斷增大。
  • 在網路恢復後,這個節點的 term 會傳播到叢集的其他節點,導致重新選主,由於 C 節點的日誌資料實際上不是最新的,並不會成為 Leader,整個叢集的秩序被這個網路隔離過的 C 節點擾亂,這顯然是不合理的。

解決方案:

  • TiDB 2.1 GA 版本引入了 Raft PreVote 機制,該問題得到解決。
  • 在 PreVote 演算法中,Candidate 首先要確認自己能贏得叢集中大多數節點的投票,才會把自己的 term 增加,然後發起真正的投票,其他節點同意發起重新選舉的條件更嚴格,必須同時滿足 :
    • 沒有收到 Leader 的心跳,至少有一次選舉超時。
    • Candidate 日誌足夠新。PreVote 演算法的引入,網路隔離節點由於無法獲得大部分節點的許可,因此無法增加 term,重新加入叢集時不會導致重新選主。

三、線上業務叢集(P1 級業務)

線上業務叢集,承載了使用者餘額變更、我的訊息、使用者生命週期、信用分等 P1 級業務,資料規模和訪問量都在可控範圍內。產出的 TiDB Binlog 可以通過 Gravity 以增量形式同步給大資料團隊,通過分析模型計算出使用者新的信用分定期寫回 TiDB 叢集。

圖 4  線上業務叢集拓撲圖

圖 4 線上業務叢集拓撲圖

四、資料沙盒叢集(離線業務)

資料沙盒,屬於離線業務叢集,是摩拜單車的一個資料聚合叢集。目前執行著近百個 TiKV 例項,承載了 60 多 TB 資料,由公司自研的 Gravity 資料複製中心將線上資料庫實時彙總到 TiDB 供離線查詢使用,同時叢集也承載了一些內部的離線業務、資料包表等應用。目前叢集的總寫入 TPS 平均在 1-2w/s,QPS 峰值 9w/s+,叢集效能比較穩定。該叢集的設計優勢有如下幾點:

  • 可供開發人員安全的查詢線上資料。

  • 特殊場景下的跨庫聯表 SQL。

  • 大資料團隊的資料抽取、離線分析、BI 報表。

  • 可以隨時按需增加索引,滿足多維度的複雜查詢。

  • 離線業務可以直接將流量指向沙盒叢集,不會對線上資料庫造成額外負擔。

  • 分庫分表的資料聚合。

  • 資料歸檔、災備。

圖 5  資料沙盒叢集拓撲圖

圖 5 資料沙盒叢集拓撲圖

4.1 遇到過的一些問題和解決方案

4.1.1 TiDB server oom 重啟

很多使用過 TiDB 的朋友可能都遇到過這一問題,當 TiDB 在遇到超大請求時會一直申請記憶體導致 oom, 偶爾因為一條簡單的查詢語句導致整個記憶體被撐爆,影響叢集的總體穩定性。雖然 TiDB 本身有 oom action 這個引數,但是我們實際配置過並沒有效果。

於是我們選擇了一個折中的方案,也是目前 TiDB 比較推薦的方案:單臺物理機部署多個 TiDB 例項,通過埠進行區分,給不穩定查詢的埠設定記憶體限制(如圖 5 中間部分的 TiDBcluster1 和 TiDBcluster2)。例:

[tidb_servers]
tidb-01-A ansible_host=$ip_address deploy_dir=/$deploydir1 tidb_port=$tidb_port1 tidb_status_port=$status_port1
tidb-01-B ansible_host=$ip_address deploy_dir=/$deploydir2 tidb_port=$tidb_port2 tidb_status_port=$status_port2  MemoryLimit=20G 
複製程式碼

實際上 tidb-01-Atidb-01-B 部署在同一臺物理機,tidb-01-B 記憶體超過閾值會被系統自動重啟,不影響 tidb-01-A

TiDB 在 2.1 版本後引入新的引數 tidb_mem_quota_query,可以設定查詢語句的記憶體使用閾值,目前 TiDB 已經可以部分解決上述問題。

4.1.2 TiDB-Binlog 元件的效率問題

大家平時關注比較多的是如何從 MySQL 遷移到 TiDB,但當業務真正遷移到 TiDB 上以後,TiDB 的 Binlog 就開始變得重要起來。TiDB-Binlog 模組,包含 Pump&Drainer 兩個元件。TiDB 開啟 Binlog 後,將產生的 Binlog 通過 Pump 元件實時寫入本地磁碟,再非同步傳送到 Kafka,Drainer 將 Kafka 中的 Binlog 進行歸併排序,再轉換成固定格式輸出到下游。

使用過程中我們碰到了幾個問題:

  • Pump 傳送到 Kafka 的速度跟不上 Binlog 產生的速度。

  • Drainer 處理 Kafka 資料的速度太慢,導致延時過高。

  • 單機部署多 TiDB 例項,不支援多 Pump。

其實前兩個問題都是讀寫 Kafka 時產生的,Pump&Drainer 按照順序、單 partition 分別進行讀&寫,速度瓶頸非常明顯,後期增大了 Pump 傳送的 batch size,加快了寫 Kafka 的速度。但同時又遇到一些新的問題:

  • 當源端 Binlog 訊息積壓太多,一次往 Kafka 傳送過大訊息,導致 Kafka oom。

  • 當 Pump 高速大批寫入 Kafka 的時候,發現 Drainer 不工作,無法讀取 Kafka 資料。

和 PingCAP 工程師一起排查,最終發現這是屬於 sarama 本身的一個 bug,sarama 對資料寫入沒有閾值限制,但是讀取卻設定了閾值:github.com/Shopify/sar…

最後的解決方案是給 Pump 和 Drainer 增加引數 Kafka-max-message 來限制訊息大小。單機部署多 TiDB 例項,不支援多 Pump,也通過更新 ansible 指令碼得到了解決,將 Pump.service 以及和 TiDB 的對應關係改成 Pump-8250.service,以埠區分。

針對以上問題,PingCAP 公司對 TiDB-Binlog 進行了重構,新版本的 TiDB-Binlog 不再使用 Kafka 儲存 binlog。Pump 以及 Drainer 的功能也有所調整,Pump 形成一個叢集,可以水平擴容來均勻承擔業務壓力。另外,原 Drainer 的 binlog 排序邏輯移到 Pump 來做,以此來提高整體的同步效能。

4.1.3 監控問題

當前的 TiDB 監控架構中,TiKV 依賴 Pushgateway 拉取監控資料到 Prometheus,當 TiKV 例項數量越來越多,達到 Pushgateway 的記憶體限制 2GB 程式會進入假死狀態,Grafana 監控就會變成圖 7 的斷點樣子:

圖 6 監控拓撲圖

圖 6 監控拓撲圖

圖 7 監控展示圖

圖 7 監控展示圖

目前臨時處理方案是部署多套 Pushgateway,將 TiKV 的監控資訊指向不同的 Pushgateway 節點來分擔流量。這個問題的最終還是要用 TiDB 的新版本(2.1.3 以上的版本已經支援),Prometheus 能夠直接拉取 TiKV 的監控資訊,取消對 Pushgateway 的依賴。

4.2 資料複製中心 Gravity (DRC)

下面簡單介紹一下摩拜單車自研的資料複製元件 Gravity(DRC)。

Gravity 是摩拜單車資料庫團隊自研的一套資料複製元件,目前已經穩定支撐了公司數百條同步通道,TPS 50000/s,80 線延遲小於 50ms,具有如下特點:

  • 多資料來源(MySQL, MongoDB, TiDB, PostgreSQL)。
  • 支援異構(不同的庫、表、欄位之間同步),支援分庫分表到合表的同步。
  • 支援雙活&多活,複製過程將流量打標,避免迴圈複製。
  • 管理節點高可用,故障恢復不會丟失資料。
  • 支援 filter plugin(語句過濾,型別過濾,column 過濾等多維度的過濾)。
  • 支援傳輸過程進行資料轉換。
  • 一鍵全量 + 增量遷移資料。
  • 輕量級,穩定高效,容易部署。
  • 支援基於 Kubernetes 的 PaaS 平臺,簡化運維任務。

使用場景:

  • 大資料匯流排:傳送 MySQL Binlog,Mongo Oplog,TiDB Binlog 的增量資料到 Kafka 供下游消費。
  • 單向資料同步:MySQL → MySQL&TiDB 的全量、增量同步。
  • 雙向資料同步:MySQL ↔ MySQL 的雙向增量同步,同步過程中可以防止迴圈複製。
  • 分庫分表到合庫的同步:MySQL 分庫分表 → 合庫的同步,可以指定源表和目標表的對應關係。
  • 資料清洗:同步過程中,可通過 filter plugin 將資料自定義轉換。
  • 資料歸檔:MySQL→ 歸檔庫,同步鏈路中過濾掉 delete 語句。

Gravity 的設計初衷是要將多種資料來源聯合到一起,互相打通,讓業務設計上更靈活,資料複製、資料轉換變的更容易,能夠幫助大家更容易的將業務平滑遷移到 TiDB 上面。該專案 已經在 GitHub 開源,歡迎大家交流使用。

五、總結

TiDB 的出現,不僅彌補了 MySQL 單機容量上限、傳統 Sharding 方案查詢維度單一等缺點,而且其計算儲存分離的架構設計讓叢集水平擴充套件變得更容易。業務可以更專注於研發而不必擔心複雜的維護成本。未來,摩拜單車還會繼續嘗試將更多的核心業務遷移到 TiDB 上,讓 TiDB 發揮更大價值,也祝願 TiDB 發展的越來越好。

TiDB 在摩拜單車的深度實踐及應用

相關文章