Categories
程式開發

Shopee 是如何進行數據庫選型的?


Shopee 於 2015 年底上線,是東南亞地區領先的電子商務平台,覆蓋東南亞和台灣等多個市場,在深圳和新加坡分別設有研發中心。

本文系 Shopee 的分佈式數據庫選型思路漫談。因為是『漫談』,可能不成體系,但會著重介紹一些經驗以及踩過的坑,提供給大家參考。

Shopee的數據庫使用情況

Shopee 在用哪些數據庫?

先說一下當前 Shopee 線上在用的幾種數據庫:

  • 在 Shopee,我們只有兩種關係數據庫:MySQL 和 TiDB。目前大部分業務數據運行在 MySQL 上,TiDB 集群的比重過去一年來快速增長中。
  • Redis 在 Shopee 各個產品線使用廣泛。從 DBA 的角度看,Redis 是關係數據庫的一種重要補充。
  • 內部也在使用諸如 HBase 和 Pika 等多種 NoSQL 數據庫,使用範圍多限於特定業務和團隊。不在本次討論範圍內。

數據庫選型策略

Shopee 是如何進行數據庫選型的? 1

過去的一年裡,我們明顯感覺到數據庫選型在 DBA 日常工作中的佔比越來越重了。隨著業務快速成長,DBA 每週需要創建的新數據庫較之一兩年前可能增加了十倍不止。我們每年都會統計幾次線上邏輯數據庫個數。上圖展示了過去幾年間這個數字的增長趨勢(縱軸表示邏輯數據庫個數,我們把具體數字隱去了)。

歷史數據顯示,邏輯數據庫個數每年都有三到五倍的增長,過去的 2019 年增長倍數甚至更高。每個新數據庫上線前,DBA 和開發團隊都需要做一些評估以快速決定物理設計和邏輯設計。經驗表明,一旦在設計階段做出了不當決定,後期需要付出較多時間和人力成本來補救。因此,我們需要製定一些簡潔高效的數據庫選型策略,確保我們大多數時候都能做出正確選擇。

我們的數據庫選型策略可以概括為三點:

  • 默認使用 MySQL。
  • 積極嘗試 TiDB。
  • 在必要的時候引入 Redis 用於消解部分關係數據庫高並發讀寫流量。

在使用 MySQL 的過程中我們發現,當單數據庫體量達到 TB 級別,開發和運維的複雜度會被指數級推高。 DBA 日常工作會把消除 TB 級 MySQL 數據庫實例排在高優先級。

“積極嘗試 TiDB”不是一句空話。 2018 年初開始,我們把 TiDB 引入了到 Shopee。過去兩年間 TiDB 在 Shopee 從無到有,集群節點數和數據體積已經達到了可觀的規模。對於一些經過了驗證的業務場景,DBA 會積極推動業務團隊採用 TiDB,讓開發團隊有機會獲得第一手經驗;目前,內部多數業務開發團隊都在線上實際使用過一次或者多次 TiDB。

關於借助 Redis 消解關係數據庫高並發讀寫流量,後面會展開講一下我們的做法。

分佈式數據庫選型參考指標

Shopee 是如何進行數據庫選型的? 2

在製定了數據庫選型策略之後,我們在選型中還有幾個常用的參考指標:

  • 1TB:對於一個新數據庫,我們會問:在未來一年到一年半時間裡,數據庫的體積會不會漲到 1TB?如果開發團隊很確信新數據庫一定會膨脹到 TB 級別,應該立即考慮 MySQL 分庫分錶方案或TiDB 方案。
  • 1000 萬行或 10GB:單一 MySQL 表的記錄條數不要超過 1000 萬行,或單表磁盤空間佔用不要超過 10GB。我們發現,超過這個閾值後,數據庫性能和可維護性上往往也容易出問題(部分 SQL 難以優化,不易做表結構調整等)。如果確信單表體積會超越該上限,則應考慮 MySQL 分錶方案;也可以採用 TiDB,TiDB 可實現水平彈性擴展,多數場景下可免去分錶的煩惱。
  • 每秒 1000 次寫入:單個 MySQL 節點上的寫入速率不要超過每秒 1000次。大家可能覺得這個值太低了;許多開發同學也常舉例反駁說,線上 MySQL 每秒寫入幾千幾萬次的實際案例比比皆是。我們為什麼把指標定得如此之低呢?首先,上線前做估算往往有較大不確定性,正常狀況下每秒寫入 1000 次,大促等特殊場景下可能會陡然飆升到每秒 10000 次,作為設計指標保守一點比較安全。其次,我們允許開發團隊在數據庫中使用Text 等大字段類型,當單行記錄長度增大到一定程度後主庫寫入和從庫複製性能都可能明顯劣化,這種狀況下對單節點寫入速率不宜有太高期待。因此,如果一個項目上線前就預計到每秒寫入速率會達到上萬次甚至更高,則應該考慮MySQL 分庫分錶方案或TiDB 方案;同時,不妨根據具體業務場景看一下能否引入Redis或消息隊列作為寫緩衝,實現數據庫寫操作異步化。
  • P99 響應時間要求是 1 毫秒,10 毫秒還是 100 毫秒?應用程序要求 99%的數據庫查詢都要在 1 毫秒內返回嗎?如果是,則不建議直接讀寫數據庫。可以考慮引入 Redis 等內存緩衝方案,前端直接面向 Redis 確保高速讀寫,後端異步寫入數據庫實現數據持久化。我們的經驗是,多數場景下,MySQL 服務器、表結構設計、SQL 和程序代碼等方面做過細緻優化後,MySQL有望做到 99% 以上查詢都在 10 毫秒內返回。對於 TiDB,考慮到其存儲計算分離和多組件協作實現 SQL 執行過程的特點,我們通常把預期值調高一個數量級到 100 毫秒級別。以線上某 TiDB 2.x 集群為例,上線半年以來多數時候 P99 都維持在 20 毫秒以內,偶爾會飆升到 200 毫秒,大促時抖動則更頻繁一些。 TiDB 執行 SQL 查詢過程中,不同組件、不同節點之間的交互會多一些,自然要多花一點時間。

要不要分庫分錶?

內部的數據庫設計評估清單裡包含十幾個項目,其中“要不要分庫分錶”是一個重要議題。在相當長時間裡,MySQL 分庫分錶方案是我們實現數據庫橫向擴展的唯一選項;把 TiDB 引入 Shopee 後,我們多了一個“不分庫分錶”的選項。

從我們的經驗來看,有幾種場景下採用 MySQL 分庫分錶方案的副作用比較大,日常開發和運維都要付出額外的代價和成本。 DBA和開發團隊需要在數據庫選型階段甄別出這些場景並對症下藥。

  • 難以準確預估容量的數據庫。舉例來講,線上某日誌數據庫過去三個月的增量數據超過了之前三年多的存量體積。對於這類數據庫,採用分庫分錶方案需要一次又一次做 Re-sharding,每一次都步驟繁瑣,工程浩大。 Shopee 的實踐證明,TiDB 是較為理想的日誌存儲方案;當前,把日誌類數據存入 TiDB 已經是內部較為普遍的做法了。
  • 需要做多維度複雜查詢的數據庫。以訂單數據庫為例,各子系統都需要按照買家、賣家、訂單狀態、支付方式等諸多維度篩選數據。若以買家維度分庫分錶,則賣家維度的查詢會變得困難;反之亦然。一方面,我們為最重要的查詢維度分別建立了異構索引數據庫;另一方面,我們也在TiDB 上實現了訂單匯總表,把散落於各個分片的訂單數據匯入一張TiDB 表,讓一些需要掃描全量數據的複雜查詢直接運行在TiDB 匯總表上。
  • 數據傾斜嚴重的數據庫。諸如點贊和關注等偏社交類業務數據,按照用戶維度分庫分錶後常出現數據分佈不均勻的現象,少數分片的數據量可能遠大於其他分片;這些大分片往往也是讀寫的熱點,進而容易成為性能瓶頸。一種常用的解法是Re-sharding,把數據分成更多片,盡量稀釋每一片上的數據量和讀寫流量。最近我們也開始嘗試把部分數據搬遷到 TiDB 上;理論上,如果 TiDB 表主鍵設計得高度分散,熱點數據就有望均勻分佈到全體 TiKV Region 上。

總體來說,MySQL 分庫分錶方案在解決了主要的數據庫橫向擴展問題的同時,也導致了一些開發和運維方面的痛點。一方面,我們努力在 MySQL 分庫分錶框架內解決和緩解各種問題;另一方面,我們也嘗試基於 TiDB 構建“不分庫分錶”的新型解決方案,並取得了一些進展。

MySQL 在 Shopee 的使用情況

Shopee 是如何進行數據庫選型的? 3

Shopee 的母公司 SEA Group 成立於 2009 年。我們從一開始就使用 MySQL 作為主力數據庫,從早期的MySQL 5.1 逐漸進化到現在的 MySQL 5.7,我們已經用了十年 MySQL。

  • 我們使用 Percona 分支,當前存儲引擎以 InnoDB 為主。
  • 一主多從是比較常見的部署結構。我們的應用程序比較依賴讀寫分離,線上數據庫可能會有多達數十個從庫。一套典型的數據庫部署結構會分佈在同城多個機房;其中會有至少一個節點放在備用機房,主要用於定時全量備份,也會提供給數據團隊做數據拉取等用途。
  • 如果應用程序需要讀取 Binlog,從庫上會安裝一個名為 GDS(General DB Sync)的 Agent,實時解析 Binlog,並寫入 Kafka。
  • 應用程序透過DNS入口連接主庫或從庫。
  • 我們自研的數據庫中間件,支持簡單的分庫分錶。何為“簡單的分庫分錶”?只支持單一分庫分錶規則,可以按日期、Hash 或者某個字段的取值範圍來分片;一條SQL 最終只會被路由到單一分片上,不支持聚合或 Join 等操作。

如何解決TB 級MySQL數據庫的使用?

Shopee 是如何進行數據庫選型的? 4

根據我們的統計,Shopee 線上數據庫中 80% 都低於 50GB;此外,還有 2.5% 的數據庫體積超過 1TB。上圖列出了部分 TB 級別數據庫的一個統計結果:平均體積是 2TB,最大的甚至超過 4TB。

採用MySQL 分庫分錶方案和遷移到 TiDB 是我們削減 TB 級MySQL數據庫實例個數的兩種主要途徑。除此之外,還有一些辦法能幫助我們對抗 MySQL 數據庫體積膨脹帶來的負面效應。

  • 舊數據歸檔。很多舊數據庫佔據了大量磁盤空間,讀寫卻不頻繁。換言之,這些舊數據很可能不是『熱數據』。如果業務上許可,我們通常會把舊數據歸檔到單獨的 MySQL 實例上。當然,應用程序需要把讀寫這些數據的流量改到新實例。新實例可以按年或按月把舊數據存入不同的表以避免單表體積過大,還可以開啟 InnoDB 透明頁壓縮以減少磁盤空間佔用。 TiDB 是非常理想的數據歸檔選項:理論上,一個TiDB 集群的容量可以無限擴展,不必擔心磁盤空間不夠用;TiDB 在計算層和存儲層皆可水平彈性擴展,我們得以根據數據體積和讀寫流量的實際增長循序漸進地增加服務器,使整個集群的硬件使用效率保持在較為理想的水平。
  • 硬件升級​​(Scale-up)。如果 MySQL 數據體積漲到了 1TB,磁盤空間開始吃緊,是不是可以先把磁盤空間加倍,內存也加大一些,為開發團隊爭取多一些時間實現數據庫橫向擴展方案?有些數據庫體積到了 TB 級別,但業務上可能不太容易分庫分錶。如果開發團隊能夠通過數據歸檔等手段使數據體積保持在一個較為穩定(但仍然是TB級別)的水準,那麼適當做一下硬件升級也有助於改善服務質量。

Redis 和關係型數據庫在Shopee的的配合使用

前文中我們提到,使用Redis來解決關係數據庫高並發讀寫流量的問題,下面我們就來講講具體的做法。

先寫緩存,再寫數據庫

Shopee 是如何進行數據庫選型的? 5

比較常用的一種做法是:先寫緩存,再寫數據庫。應用程序前端直接讀寫 Redis,後端勻速異步地把數據持久化到 MySQL 或 TiDB。這種做法一般被稱之為“穿透式緩存”,其實是把關係數據庫作為 Redis 數據的持久化存儲層。如果一個系統在設計階段即判明線上會有較高並發讀寫流量,把 Redis 放在數據庫前面擋一下往往有效。

在 Shopee,一些偏社交類應用在大促時的峰值往往會比平時高出數十上百倍,是典型的“性能優先型應用”(Performance-critical Applications)。如果開發團隊事先沒有意識到這一點,按照常規做法讓程序直接讀寫關係數據庫,大促時不可避免會出現“一促就倒”的狀況。其實,這類場景很適合借助 Redis 平緩後端數據庫讀寫峰值。

如果 Redis 集群整體掛掉,怎麼辦?一般來說,有兩個解決辦法:

  • 性能降級:應用程序改為直接讀寫數據庫。性能上可能會打一個大的折扣,但是能保證大部分數據不丟。一些數據較為關鍵的業務可能會更傾向於採用這種方式。
  • 數據降級:切換到一個空的 Redis 集群上以盡快恢復服務。後續可以選擇從零開始慢慢積累數據,或者運行另一個程序從數據庫加載部分舊數據到 Redis。一些並發高但允許數據丟失的業務可能會採用這種方式。

先寫數據庫,再寫緩存

Shopee 是如何進行數據庫選型的? 6

還有一種做法也很常見:先寫數據庫,再寫緩存。應用程序正常讀寫數據庫,Shopee 內部有一個中間件 DEC(Data Event Center)可以持續解析 Binlog,把結果重新組織後寫入到 Redis。這樣,一部分高頻只讀查詢就可以直接打到 Redis上,大幅度降低關係數據庫負載。

把數據寫入 Redis 的時候,可以為特定的查詢模式定制數據結構,一些不太適合用 SQL 實現的查詢改為讀 Redis 之後反而會更簡潔高效。

此外,相較於“雙寫方式”(業務程序同時把數據寫入關係數據庫和Redis),通過解析Binlog 的方式在Redis 上重建數據有明顯好處:業務程序實現上較為簡單,不必分心去關注數據庫和Redis 之間的數據同步邏輯。 Binlog 方式的缺點在於寫入延遲:新數據先寫入 MySQL 主庫,待其流入到 Redis 上,中間可能有大約數十毫秒延遲。實際使用上要論證業務是否能接受這種程度的延遲。

舉例來講,在新訂單實時查詢等業務場景中,我們常採用這種“先寫數據庫,再寫緩存”的方式來消解  MySQL 主庫上的高頻度只讀查詢。為規避從庫延遲帶來的影響,部分關鍵訂單字段的查詢須打到 MySQL 主庫上,大促時主庫很可能就不堪重負。歷次大促的實踐證明,以這種方式引入 Redis 能有效緩解主庫壓力。

TiDB 在 Shopee 的使用情況

講完MySQL和Redis,我們來接著講講TiDB。

Shopee 是如何進行數據庫選型的? 7

我們從 2018 年初開始調研 TiDB,到 2018 年 6 月份上線了第一個 TiDB 集群(風控日誌集群,版本 1.0.8)。 2018 年 10 月份,我們把一個核心審計日誌庫遷到了 TiDB上,目前該集群數據量約 7TB,日常 QPS 約為 10K ~ 15K。總體而言,2018 年上線的集群以日誌類存儲為主。

2019 年開始我們嘗試把一些較為核心的線上系統遷移到 TiDB 上。 3 月份為買家和賣家提供聊天服務的 Chat 系統部分數據從 MySQL 遷移到了 TiDB 。最近的大促中,峰值 QPS 約為 30K,運行平穩。今年也有一些新功能選擇直接基於 TiDB 做開發,比如店鋪標籤、直播彈幕和選品服務等。這些新模塊的數據量和查詢量都還比較小,有待持續觀察驗證。

TiDB 3.0 GA 後,新的 Titan (https://github.com/tikv/titan) 存儲引擎吸引了我們。在Shopee,我們允許 MySQL 表設計中使用 Text 等大字段類型,通常存儲一些半結構化數據。但是,從 MySQL 遷移到 TiDB 的過程中,大字段卻可能成為絆腳石。一般而言,TiDB 單行數據尺寸不宜超過 64KB,越小越好;換言之,字段越大,性能越差。 Titan 存儲引擎有望提高大字段的讀寫性能。目前,我們已經著手把一些數據遷移到 TiKV 上,並打開了 Titan,希望能探索出更多應用場景。

集群概況

Shopee 是如何進行數據庫選型的? 8

目前 Shopee 線上部署了二十多個 TiDB 集群,約有 400 多個節點。版本以 TiDB 2.1 為主,部分集群已經開始試水 TiDB 3.0。我們最大的一個集群數據量約有 30TB,超過 40 個節點。到目前為止,用戶、商品和訂單等電商核心子系統都或多或少把一部分數據和流量放在了 TiDB 上。

TiDB 在 Shopee 的使用場景

我們把 TiDB在 Shopee 的使用場景歸納為三類:

  • 日誌存儲場景
  • MySQL 分庫分錶數據聚合場景
  • 程序直接讀寫 TiDB 的場景

Shopee 是如何進行數據庫選型的? 9

第一種使用場景是日誌存儲。前面講到過,我們接觸 TiDB 的第一年裡上線的集群以日誌類存儲為主。通常的做法是:前端先把日誌數據寫入到 Kafka,後端另一個程序負責把 Kafka 裡的數據異步寫入 TiDB。由於不用考慮分庫分錶,運營後台類業務可以方便地讀取 TiDB 裡的日誌數據。對於 DBA 而言,可以根據需要線性增加存儲節點和計算節點,運維起來也較MySQL分庫分錶簡單。

Shopee 是如何進行數據庫選型的? 10

第二種使用場景是 MySQL 分庫分錶數據聚合。 Shopee 的訂單表和商品表存在 MySQL上,並做了細緻的數據分片。為了方便其他子系統讀取訂單和商品數據,我們做了一層數據聚合: 借助前面提到的 DEC 解析 MySQL Binlog,把多個 MySQL 分片的數據聚合到單一 Ti​​DB 匯總表。這樣,類似BI系統這樣的旁路系統就不必關注分庫分錶規則,直接讀取 TiDB 數據即可。除此之外,訂單和商品子系統也可以在 TiDB 匯總表上運行一些複雜的 SQL 查詢,省去了先在每個 MySQL 分片上查一次最後再匯總一次的麻煩。

Shopee 是如何進行數據庫選型的? 11

第三種就是程序直接讀寫 TiDB。像前面提到的 Chat 系統,捨棄了 MySQL,改為直接讀寫 TiDB。優勢體現在兩個方面:不必做分庫分錶,應用程序的實現相對簡單、直接;TiDB 理論上容量無限大,且方便線性擴展,運維起來更容易。

前面提到過,在 Shopee 內部使用GDS(General DB Sync)實時解析 MySQL Binlog,並寫入 Kafka 提供給有需要的客戶端消費。 TiDB 上也可以接一個 Binlog 組件,把數據變化持續同步到 Kafka 上。需要讀取 Binlog 的應用程序只要適配了 TiDB Binlog 數據格式,就可以像消費 MySQL Binlog 一樣消費 TiDB Binlog 了。

從MySQL遷移到TiDB:要適配,不要平移

Shopee 是如何進行數據庫選型的? 12

把數據庫從 MySQL 搬到 TiDB 的過程中,DBA 經常提醒開發同學:要適配,不要平移。關於這點,我們可以舉一個案例來說明一下。

線上某系統最初採用 MySQL 分錶方案,全量數據均分到 1000 張表; 遷移到 TiDB 後我們去掉了分錶,1000 張表合為了一張。應用程序上線後,發現某個 SQL 的性能抖動比較嚴重,並發高的時候甚至會導致整個 TiDB 集群卡住。分析後發現該 SQL 有兩個特點:

  • 該 SQL 查詢頻度極高,佔了查詢高峰時全部只讀查詢的 90%。
  • 該 SQL 是一個較為複雜的掃表查詢,不易通過添加索引方式優化。遷移到TiDB 之前,MySQL 數據庫分為1000 張表,該SQL 執行過程中只會掃描其中一張表,並且查詢被分散到了多達二十幾個從庫上;即便如此,隨著數據體積增長,當熱數據明顯超出內存尺寸後,MySQL 從庫也變得不堪重負了。遷移到 TiDB 並把1000 張表合為一張之後,該 SQL 被迫掃描全量數據,在 TiKV 和 SQL 節點之間會有大量中間結果集傳送流量,性能自然不會好。

判明原因後,開發團隊為應用程序引入了 Redis,把 Binlog 解析結果寫入 Redis,並針對上述 SQL 查詢定制了適當的數據結構。這些優化措施上線後,90% 只讀查詢從 TiDB 轉移到了 Redis 上,查詢變得更快、更穩定;TiDB 集群也得以削減數量可觀的存儲和計算節點。

TiDB 高度兼容  MySQL 語法的特點有助於降低數據庫遷移的難度;但是,不要忘記它在實現上完全不同於 MySQL,很多時候我們需要根據 TiDB 的特質和具體業務場景定制出適配的方案。

總結

本文回顧了 Shopee 在關係數據庫選型方面的思路,也附帶簡單介紹了一些我們在MySQL、TiDB 和 Redis 使用方面的心得,希望能為大家提供一點借鑒。

簡單來說,如果數據量比較小,業務處於早期探索階段,使用 MySQL 仍然是一個很好的選擇。 Shopee 的經驗是不用過早的為分庫分錶妥協設計,因為當業務開始增長,數據量開始變大的時候,可以從MySQL平滑遷移到TiDB,獲得擴展性的同時也不用犧牲業務開發的靈活性。另一方面,Redis 可以作為關係型數據庫的很好的補充,用來加速查詢,緩解數據庫壓力,使得數據庫能夠更關注吞吐以及強一致場景。

作者介紹:

劉春輝,Shopee DBA,TiDB User Group Ambassador

相關閱讀:

分佈式前沿技術專題