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

一、業務場景

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

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

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

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

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

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

圖 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 訂單集群的遷移過程以及業務接入拓撲圖

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

  • 新 Sharding 集群按照 order_id 取模通過 DBproxy 寫入各分表,解決數據分布不均、熱點等問題。
  • 將老 Sharding 集群的數據通過使用 DRC(摩拜自研的開源異構數據同步工具 Gravity github.com/moiot/gravit)全量+增量同步到新 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 (github.com/pingcap/docs),可以把 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 出現網路隔離的場景圖
  • 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 在線業務集群拓撲圖

四、數據沙盒集群(離線業務)

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

  • 可供開發人員安全的查詢線上數據。
  • 特殊場景下的跨庫聯表 SQL。
  • 大數據團隊的數據抽取、離線分析、BI 報表。
  • 可以隨時按需增加索引,滿足多維度的複雜查詢。
  • 離線業務可以直接將流量指向沙盒集群,不會對線上資料庫造成額外負擔。
  • 分庫分表的數據聚合。
  • 數據歸檔、災備。
圖 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/sara

最後的解決方案是給 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 監控就會變成下圖的斷點樣子:

圖 6 監控拓撲圖
圖 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 開源,歡迎大家交流使用github.com/moiot/gravit

五、總結

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

更多案例閱讀:

案例?

www.pingcap.com
圖標

推薦閱讀:
相关文章