Shopee 在上線了 5 年後,已經成為了東南亞 WooCommerce 跨境電商的領頭羊,該平台的資料庫,值得不少的 WooCommerce 跨境電商平台方學習,他們是如何做選型的?Hishang 在這裏跟大家來講解一下吧。
Shopee 於 2015 年底上線,是東南亞 WooCommerce 地區領先的 WooCommerce 電子商務平台,覆蓋東南亞 WooCommerce 和台灣等多個市場,在深圳和新加坡分別設有研發中心。本文系 Shopee 的分散式資料庫選型思路漫談。因為是『漫談』,可能不成體系,但會著重介紹一些經驗以及踩過的坑,提供給大家參考。
1 Shopee 的資料庫使用情況
Shopee 在用哪些資料庫?
先説一下當前 Shopee 線上在用的幾種資料庫:
在 Shopee,我們只有兩種關聯式資料庫:MySQL 和 TiDB 。目前大部分業務資料執行在 MySQL 上,TiDB 叢集的比重過去一年來快速增長中。
Redis 在 Shopee 各個產品線使用廣泛。從 DBA 的角度看,Redis 是關聯式資料庫的一種重要補充。
內部也在使用諸如 HBase 和 Pika 等多種 NoSQL 資料庫,使用範圍多限於特定業務和團隊。不在本次討論範圍內。
資料庫選型策略
過去的一年裏,我們明顯感覺到資料庫選型在 DBA 日常工作中的佔比越來越重了。隨著業務快速成長,DBA 每週需要建立的新資料庫較之一兩年前可能增加了十倍不止。我們每年都會統計幾次線上邏輯資料庫個數。上圖展示了過去幾年間這個數字的增長趨勢 (縱軸表示邏輯資料庫個數,我們把具體數字隱去了) 。
歷史資料顯示,邏輯資料庫個數每年都有三到五倍的增長,過去的 2019 年增長倍數甚至更高。每個新資料庫上線前,DBA 和開發團隊都需要做一些評估以快速決定物理設計和邏輯設計。經驗表明,一旦在設計階段做出了不當決定,後期需要付出較多時間和人力成本來補救。因此,我們需要制定一些簡潔高效的資料庫選型策略,確保我們大多數時候都能做出正確選擇。
我們的資料庫選型策略可以概括為三點:
預設使用 MySQL 。
積極嘗試 TiDB 。
在必要的時候引入 Redis 用於消解部分關聯式資料庫高併發讀寫流量。
在使用 MySQL 的過程中我們發現,當單資料庫體量達到 TB 級別,開發和運維的複雜度會被指數級推高。 DBA 日常工作會把消除 TB 級 MySQL 資料庫例項排在高優先順序。
“積極嘗試 TiDB” 不是一句空話。 2018 年初開始,我們把 TiDB 引入了到 Shopee 。過去兩年間 TiDB 在 Shopee 從無到有,叢集節點數和資料體積已經達到了可觀的規模。對於一些經過了驗證的業務場景,DBA 會積極推動業務團隊採用 TiDB,讓開發團隊有機會獲得第一手經驗; 目前,內部多數業務開發團隊都線上上實際使用過一次或者多次 TiDB 。
關於藉助 Redis 消解關聯式資料庫高併發讀寫流量,後面會展開講一下我們的做法。
分散式資料庫選型參考指標
在制定了資料庫選型策略之後,我們在選型中還有幾個常用的參考指標:
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 構建 “不分庫分表” 的新型解決方案,並取得了一些進展。
2 MySQL 在 Shopee 的使用情況
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 線上資料庫中 80% 都低於 50GB; 此外,還有 2.5% 的資料庫體積超過 1TB 。上圖列出了部分 TB 級別資料庫的一個統計結果:平均體積是 2TB,最大的甚至超過 4TB 。
採用 MySQL 分庫分表方案和遷移到 TiDB 是我們削減 TB 級 MySQL 資料庫例項個數的兩種主要途徑。除此之外,還有一些辦法能幫助我們對抗 MySQL 資料庫體積膨脹帶來的負面效應。
舊資料歸檔。很多舊資料庫佔據了大量磁碟空間,讀寫卻不頻繁。換言之,這些舊資料很可能不是『熱資料』。如果業務上許可,我們通常會把舊資料歸檔到單獨的 MySQL 例項上。當然,應用程序需要把讀寫這些資料的流量改到新例項。新例項可以按年或按月把舊資料存入不同的表以避免單表體積過大,還可以開啓 InnoDB 透明頁壓縮以減少磁碟空間佔用。 TiDB 是非常理想的資料歸檔選項:理論上,一個 TiDB 叢集的容量可以無限擴充套件,不必擔心磁碟空間不夠用;TiDB 在計算層和儲存層皆可水平彈性擴充套件,我們得以根據資料體積和讀寫流量的實際增長循序漸進地增加服務器,使整個叢集的硬體使用效率保持在較為理想的水平。
硬體升級 (Scale-up) 。如果 MySQL 資料體積漲到了 1TB,磁碟空間開始吃緊,是不是可以先把磁碟空間加倍,記憶體也加大一些,為開發團隊爭取多一些時間實現資料庫橫向擴充套件方案? 有些資料庫體積到了 TB 級別,但業務上可能不太容易分庫分表。如果開發團隊能夠通過資料歸檔等手段使資料體積保持在一個較為穩定 (但仍然是 TB 級別) 的水準,那麼適當做一下硬體升級也有助於改善服務質量。
3 Redis 和關係型資料庫在 Shopee 的的配合使用
前文中我們提到,使用 Redis 來解決關聯式資料庫高併發讀寫流量的問題,下面我們就來講講具體的做法。
先寫快取,再寫資料庫
比較常用的一種做法是:先寫快取,再寫資料庫。應用程序前端直接讀寫 Redis,後端勻速非同步地把資料持久化到 MySQL 或 TiDB 。這種做法一般被稱之為 “穿透式快取”,其實是把關聯式資料庫作為 Redis 資料的持久化儲存層。如果一個系統在設計階段即判明線上會有較高併發讀寫流量,把 Redis 放在資料庫前面擋一下往往有效。
在 Shopee,一些偏社交類應用在大促時的峯值往往會比平時高出數十上百倍,是典型的 “效能優先型應用”(Performance-critical Applications) 。如果開發團隊事先沒有意識到這一點,按照常規做法讓程序直接讀寫關聯式資料庫,大促時不可避免會出現 “一促就倒” 的狀況。其實,這類場景很適合藉助 Redis 平緩後端資料庫讀寫峯值。
如果 Redis 叢集整體掛掉,怎麼辦? 一般來説,有兩個解決辦法:
效能降級:應用程序改為直接讀寫資料庫。效能上可能會打一個大的折扣,但是能保證大部分資料不丟。一些資料較為關鍵的業務可能會更傾向於採用這種方式。
資料降級:切換到一個空的 Redis 叢集上以儘快恢復服務。後續可以選擇從零開始慢慢積累資料,或者執行另一個程序從資料庫載入部分舊資料到 Redis 。一些併發高但允許資料丟失的業務可能會採用這種方式。
先寫資料庫,再寫快取
還有一種做法也很常見:先寫資料庫,再寫快取。應用程序正常讀寫資料庫,Shopee 內部有一箇中介軟件 DEC(Data Event Center) 可以持續解析 Binlog,把結果重新組織後寫入到 Redis 。這樣,一部分高頻只讀查詢就可以直接打到 Redis 上,大幅度降低關聯式資料庫負載。
把資料寫入 Redis 的時候,可以為特定的查詢模式定製資料結構,一些不太適合用 SQL 實現的查詢改為讀 Redis 之後反而會更簡潔高效。
此外,相較於 “雙寫方式”(業務程序同時把資料寫入關聯式資料庫和 Redis),通過解析 Binlog 的方式在 Redis 上重建資料有明顯好處:業務程序實現上較為簡單,不必分心去關注資料庫和 Redis 之間的資料同步邏輯。 Binlog 方式的缺點在於寫入延遲:新資料先寫入 MySQL 主庫,待其流入到 Redis 上,中間可能有大約數十毫秒延遲。實際使用上要論證業務是否能接受這種程度的延遲。
舉例來講,在新訂單實時查詢等業務場景中,我們常採用這種 “先寫資料庫,再寫快取” 的方式來消解 MySQL 主庫上的高頻度只讀查詢。為規避從庫延遲帶來的影響,部分關鍵訂單欄位的查詢須打到 MySQL 主庫上,大促時主庫很可能就不堪重負。歷次大促的實踐證明,以這種方式引入 Redis 能有效緩解主庫壓力。
4 TiDB 在 Shopee 的使用情況
講完 MySQL 和 Redis,我們來接著講講 TiDB 。
我們從 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 線上部署了二十多個 TiDB 叢集,約有 400 多個節點。版本以 TiDB 2.1 為主,部分叢集已經開始試水 TiDB 3.0 。我們最大的一個叢集資料量約有 30TB,超過 40 個節點。到目前為止,用户、商品和訂單等 WooCommerce 電商核心子系統都或多或少把一部分資料和流量放在了 TiDB 上。
TiDB 在 Shopee 的使用場景
我們把 TiDB 在 Shopee 的使用場景歸納為三類:
日誌儲存場景
MySQL 分庫分表資料聚合場景
程序直接讀寫 TiDB 的場景
第一種使用場景是日誌儲存。前面講到過,我們接觸 TiDB 的第一年裏上線的叢集以日誌類儲存為主。通常的做法是:前端先把日誌資料寫入到 Kafka,後端另一個程序負責把 Kafka 裏的資料非同步寫入 TiDB 。由於不用考慮分庫分表,WooCommerce 運營後台類業務可以方便地讀取 TiDB 裏的日誌資料。對於 DBA 而言,可以根據需要線性增加儲存節點和計算節點,運維起來也較 MySQL 分庫分表簡單。
第二種使用場景是 MySQL 分庫分表資料聚合。 Shopee 的訂單表和商品表存在 MySQL 上,並做了細緻的資料分片。為了方便其他子系統讀取訂單和商品資料,我們做了一層資料聚合:藉助前面提到的 DEC 解析 MySQL Binlog,把多個 MySQL 分片的資料聚合到單一 TiDB 彙總表。這樣,類似 BI 系統這樣的旁路系統就不必關注分庫分表規則,直接讀取 TiDB 資料即可。除此之外,訂單和商品子系統也可以在 TiDB 彙總表上執行一些複雜的 SQL 查詢,省去了先在每個 MySQL 分片上查一次最後再彙總一次的麻煩。
第三種就是程序直接讀寫 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:要適配,不要平移
把資料庫從 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 的特質和具體業務場景定製出適配的方案。
5 總結
本文回顧了 Shopee 在關聯式資料庫選型方面的思路,也附帶簡單介紹了一些我們在 MySQL 、 TiDB 和 Redis 使用方面的心得,希望能為大家提供一點借鑑。
簡單來説,如果資料量比較小,業務處於早期探索階段,使用 MySQL 仍然是一個很好的選擇。 Shopee 的經驗是不用過早的為分庫分表妥協設計,因為當業務開始增長,資料量開始變大的時候,可以從 MySQL 平滑遷移到 TiDB,獲得擴充套件性的同時也不用犧牲業務開發的靈活性。另一方面,Redis 可以作為關係型資料庫的很好的補充,用來加速查詢,緩解資料庫壓力,使得資料庫能夠更關注吞吐以及強一致場景。