爲什麼要用 Tair 來服務低延時場景 - 從購物車升級說起

「購物車升級」是今年雙十一的重要體驗提升項目,體現了大淘寶技術人 “用技術突破消費者和商家體驗天花板” 的態度。這是一種敢於不斷重新自我審視,然後做出更好選擇的存在主義態度。

「體驗提升」通常表現在以前需要降級的功能不降級,以前不夠實時的數據逐漸實時,以前調用鏈路的長耗時逐步降低——這通常是龐大的系統工程,需要涉及到的每一個環節(客戶端、應用、中間件、數據庫、網絡、容器、系統內核等組件)提供最強的產品能力來支撐。到數據庫這個環節,挑戰通常是訪問量和連接數暴漲的前提下,仍要保持延時穩定和成本可控。

**低延時是這些挑戰裏面的核心,是內存數據庫 Tair 提供的服務本質。**在高吞吐、大連接數、熱點請求、異常流量、複雜計算邏輯、彈性伸縮這些真實場景下保持穩定的低延時,是 Tair 能夠在低延時場景被選擇的關鍵因素。 作爲今年支撐購物車升級的核心產品,Tair 使用的內存 / SCM 混合存儲、水平擴展分區無鎖和 SQL 引擎等技術是在支撐十四次雙十一的過程中逐漸打磨完善的,在這些技術的基礎上 Tair 使用 Fast Path 執行 SQL 、執行器模式及算子適配等技術持續進行服務端優化。本文將圍繞 Tair 低延時這一本質特徵在構建時所採用的系統手段,藉此提出更多問題來探討,進一步打造更強大的內存數據庫。

Tair 在低延時場景下的服務能力

低延時的基石

存儲引擎的性能是數據庫低延時的基石。從功能上看,我們會關心存儲引擎提供的併發(線程安全、無鎖)、事務處理(MVCC、衝突識別、死鎖識別、操作原子性)、快照(標記數據集狀態、降低延遲、減少容量膨脹)等能力。這裏把併發放到後面論述,先看單次請求的延時,主要涉及到存儲介質和數據索引。

存儲介質

作爲內存數據庫,Tair 在絕大部分場景使用單次訪問延時在 ns 級別的內存 / SCM 作爲主要的存儲介質。以 Table 存儲爲例,服務端的常駐數據大概可以分爲 Tuple(可以認爲是表裏面的某一行)、String Pool、Index 三部分,這些數據都是存放在內存 / SCM 中,只有快照和日誌會存放在磁盤上。

除了存儲介質的延時,通常我們還需要關心的是介質的成本。成本一方面是從硬件上,Tair 是率先採用 SCM 的雲產品,相對於 DRAM,SCM 的密度更高能支持持久化,且成本更低。上面提到的三部分數據結構中, Tuple 和 String Pool 是主要佔用數據的空間,存放在空間更大的 SCM 上,Index 需要頻繁訪問且佔用空間更低,存放在空間較小延時更低的 DRAM 上。

另外一方面是從數據結構上去降低成本,這裏的技術手段包括,設計更友好的數據結構和碎片整理的機制、進行透明的數據壓縮。Tair 中會以 Page 爲單位來管理 Tuple,隨着數據的刪除,每個 Page 會有一些空閒的 Tuple,存儲引擎會按照空閒率來對 Page 分組,當整體的空閒率高於一定閾值(默認是 10%)時,就會試圖根據空閒率進行頁的合併。

索引

Tair 目前在使用的索引主要有 HashTable、SkipList、RBTree、RTree、Number Tree、Inverted index 等,分別應用於不同的場景。索引和需要服務的模型是相關聯的,比如如果服務的主要模型是 Key-Value,那麼主索引使用 HashTable 來達到 O(1)的時間複雜度,ZSet 涉及到數據排序和排名的獲取,所以 Zset 使用了一個可以在查找時同時獲取 Rank 的 Skiplist 作爲索引。排序場景使用 SkipList 作爲索引是內存數據庫中比較常見的方案,相較於 BTree 來說,由於沒有 Structure Modification,更易於實現併發和無鎖,當然,也會增加一些 Footprint。在 Table 存儲中,使用 RBTree 作爲排序索引,在數據量達到 10k 的場景下,RBTree 能夠提供更穩定的訪問延時和更低的內存佔用。

在數據庫系統中,索引能力的增強還可以讓執行器對外暴露更強的算子,比如 Tair 中的 RBTree 提供了快速計算兩個值之間 Count 的能力,對外提供了 IndexCountOperator,這樣類似於 Select count(*) from person where age >= 8 and age <= 25 的查詢就可以直接使用 IndexCountOperator 來獲取結果,無需樸素地調用 IndexScanOperator -> AggregateOperator 對索引進行掃描才得出結果。

低延時的挑戰


合適的存儲介質和索引只是提供低延時的一個前提,要在真實環境提供低延時的內存數據庫服務,至少需要經歷高吞吐的磨練。剛纔我們關注了單個請求的延時,介質的延時和索引操作的時間複雜度會影響單個請求的延時。如果一個數據庫節點需要承擔每秒數十萬的請求,這些是不夠的,數據庫節點需要擁有良好的併發能力。如果吞吐近一步增長,帶來的 CPU、網絡消耗已經超過了單機的極限的時候,比如大促峯值時 Tair 某集羣每秒提供了數千萬的讀,這些讀會帶來數萬兆的網絡流量消耗,這時候就需要產品能夠支持水平擴展,“凡治衆如寡,分數是也”,把請求散落到不同的 Sharding,提供穩定的低延遲。

高併發

併發是低延時場景一個關鍵挑戰。解法通常分爲兩種,一種是在存儲引擎內部支持更細粒度的鎖或者無鎖的併發請求;還有一種是在存儲引擎外部來進行線程模型的優化,保證某一部分數據(一般來說是一個分區)只被一個線程處理,這樣就能夠在單線程引擎之上構建出高吞吐的能力。

早期的版本中,Tair 的鎖粒度是實例級別的,鎖開銷損耗較大。爲了提升單機的處理能力,Tair 引入了 RCU 無鎖引擎,實現內存 KV 引擎的無鎖化訪問,成倍提升了內存引擎的性能,相關工作發表在 FAST20 上:《HotRing: A Hotspot-Aware In-Memory Key-Value Store》。

在提升單機引擎的併發上,SQL 場景使用了另外一種解法,讓每一個 Partition 的數據由一個單獨的線程來進行處理,這樣能在引擎內部通過增加分區達到線性的擴展,而且無需使用前面提到的無鎖實現中常會使用的重試步驟。相對於上面的方案,這種方案的工程難度更低,且能夠天然地支持 Serializable 的事務隔離級別,某一特定時刻只有一個事務能夠運行在特定的分區上,增加 undo buffer 即可以保證事務的原子性。

但是使用這種方式需要滿足一些假設:對每個 Partition 的訪問是均衡的;跨 Partition 的訪問比較少。如果某個 Partition 存在熱點訪問,也就是明顯高出其它的 Partition,由於只有一個線程能處理這個 Partition 的數據,很容易造成這個 Parition 的請求堆積;如果出現跨 Partition 的訪問,就需要在各個 Partition 之間做同步,這樣也會造成等待並影響併發性能。目前支持的優惠、購物車場景都是用戶維度的,表中的 partition column 都是 buyer_id,所以單個請求基本是針對某一個特定分區的,數據鏈路不存在跨分區請求。數據統計調用的類似於 Select count(*) from table 這種請求,由於存儲引擎的支持,單分區內可以 O(1) 的時間返回,所以也不存在問題。當然,對於 delete * from table 這種跨分區的寫操作,目前會對請求造成秒級抖動,未來會加入 Lazy Free 的處理邏輯,降低對正常請求的影響。

水平擴展

水平擴展是應對高吞吐的有效手段之一。水平擴展爲分佈式系統帶來了應對高吞吐的能力,但同時相對於單節點的系統而言,也會帶來很多挑戰,比如:跨節點的請求如何保證事務性;如何彈性地進行節點增減;如何應對節點的失效等。在 Tair 的大部分場景而言,並不存在需要保證跨分區請求的原子性。Tair 的 SQL 引擎也支持跨節點的分佈式事務,但是這些分佈式事務一般不是常規的業務訪問,而是運維類的操作。

對於很多系統而言,分區和節點是 N 對 1 的關係(常見於 Hash 分區),採用固定的分區數,和動態的節點數是一種常見的解決方案,比如說 Redis Cluster,在這類系統中,彈性地進行節點增減的問題就轉換爲如何在節點前進行分區遷移的問題。也有一些系統的分區和節點是 1 對 1 的(常見於 Range 分區),比如 HBase,在這類系統中,彈性伸縮的問題就轉換爲分區分裂的問題了。

對於節點失效,涉及到判活和後續的數據處理兩類的問題。對於很多系統而言,冗餘和分片是分開的,比如 Redis Cluster、MongoDB、AnalyticDB Worker,即有一個 HA Group 的概念,HA Group 中的每一個節點,數據是完全一致的。不同系統處理的時候依然會有些區別,一些系統 HA Group 中的某一個節點所有分區都是 Leader,我們稱爲 Leader 節點,提供讀寫服務,其它節點只有冗餘數據的同步流量,稱爲 Follower 節點,比如 Redis Cluster,這類系統在調度系統不夠成熟的時期,有一個明顯的短板就是 Follower 節點所在的機器資源是有空餘的,通常是通過樸素的混布來提供資源的利用率,但也帶來了部署上的複雜度,所以在系統設計的時候就會有這樣的考量:能不能在 HA Group 內分散這些分區的 Leader 呢,於是就有了下面這些系統。一些系統 HA Group 的每一個節點都承擔部分分區的 Leader,這就是每個節點都會提供讀寫服務,比如 AnalyticDB Worker。這類有 HA Group 的系統,判活和後續數據處理一般只在 HA Group 內,即某個節點失效後,會把訪問流量轉移到 HA Group 內的其它節點,然後通過上層的調度在 HA Group 內補充新的節點。還是老問題,在調度系統還不夠成熟的時期,補充新的節點也會帶來運維的複雜度,那就會有新的考量:能不能跨越 HA Group 的限制,把冗餘的個數和系統內節點的個數解耦呢?於是就有了 Kudu 這類系統的架構,冗餘和分片是交織起來的,某一個節點失效之後,它上面的分區會由中心節點來調度到其它節點。在調度能力非常成熟的今天,數據庫系統自身的能力怎麼和數據庫相關的調度能力想結合,也會給系統架構帶來新的啓發。

超大連接數

連接數的限制是一個比較容易被忽略的約束。但在一個真實的系統中,連接數過多會給系統帶來巨大的壓力。比如說 Redis,即使在 6.0 支持了多 io 之後,能夠支持的連接數也是有限的。而目前直接訪問 Tair 的應用動輒有 100k 規模的容器數目,所以支持超多連接數是一個必選項。其中涉及到的技術主要是幾方面:a. 提高多線程 io 的能力,目前成熟的網絡框架基本都有這個能力;b. 把 io 線程和 worker 線程解耦,這樣可以獨立增強 worker 的處理能力,避免對 io 產生阻塞,當然這個策略取決於 worker 的工作負載,對於單次處理延時穩定較小的場景,支持無鎖併發後,整個鏈路使用 io 線程處理避免線程切換是更優的方案;c. 輕量化連接,把關聯到連接上的業務邏輯和 io 功能剝離開,可以更加靈活地做針對性的優化,一些系統中連接對資源的消耗較大,一個連接需要消耗 ~10M 的內存資源,這樣連接數就比較難以擴展了。

穩定的低延時


現在有了高效的存儲引擎和水平擴展,已經具備了提供低延時和高吞吐服務的能力,但是成爲一個健壯地提供低延時的數據庫系統還需要能夠應對一些異常的場景,比如說某一個分區有熱點訪問,比如說某個租戶的流量異常對其它租戶產生干擾,比如說某些慢請求消耗了大量的服務資源。本章節將介紹 Tair 是如何處理這些 “異常” 場景來提供穩定的低延時的。

熱點策略

熱點訪問是商品維度、賣家維度的數據常常會遇到的一個挑戰,熱點方案也是 Tair 能夠服務於低延時場景的關鍵能力。前面講了水平擴展之後,用戶的某個請求就會根據一定的規則(Hash、Range、List 等)路由到某一個分區上,如果存在熱點訪問,就會造成這一個分區的訪問擁塞。處理熱點有很多方案,比如二級散列,這種方案對於熱點的讀寫可以做進一步拆分的場景是有用的,比如現在我們有一個賣家訂單表,然後賣家 id 是分區列,則我們可以再以訂單 id 做一次二級散列,解決一個大賣家導致的熱點問題;目前淘寶大規模使用的 Tair 的 KV 引擎不滿足使用二級散列的前提,一般來說商品的信息映射到 Tair 內就是某一個 Value,更新和讀取都是原子的。所以 Tair 目前使用的方案是在一層進行散列,藉助於和客戶端的交互,將熱點數據分散到集羣當中的其它節點,共同來處理這個熱點請求,當然這種方案需要應用接受熱點在一定時間內的延遲更新。另外這種方案需要客戶端和服務端協同,需要應用升級到對應的客戶端才能使用。所以最新的 Tair 熱點策略在兼容社區 Redis 的服務時使用了不同的方案,應用能夠直接使用任一流行的開源客戶端進行訪問,因此需要在服務端提供獨立的熱點處理能力。目前的 Tair 熱點能力是由 Proxy 來提供的,相對於 Tair 之前的方案,這種方案擁有更強大的彈性和更好的通用性。

流控

服務於多租戶的數據庫系統,解決資源隔離的問題通常需要對進行容量或者訪問量的配額管理來保證 QoS。即使服務於單租戶的系統,也需要在用戶有突發異常流量時,保證系統的穩定性,識別出異常流量進行限制,保證正常流量不受影響,比如 Tair 中對於 慢 SQL 識別和阻斷。再退一步,即使面對無法識別的異常流量,如果判斷請求流量已經超過了服務的極限,按照正常的行爲進行響應會對服務端造成風險,需要進行 Fast Fail,並保證服務端的可用性,達到可用性防禦的目的,比如 Tair 在判斷有客戶端的 Output Buffer 超過一定內存閾值之後,就會強制 Kill 掉客戶端連接;在判斷目前排隊的請求個數或者回包占用的內存超過一定閾值之後,就會構造一個流控的回包並回復給客戶端。

流控一般包含以下幾部分內容:請求資源消耗的統計,這部分是爲流控策略和行爲提供數據支撐;流控的觸發,一般是給資源消耗設定一個閾值,如果超過閾值就觸發;流控的行爲,這部分各個系統根據服務的場景會有較大的不同;最後的流控的恢復,也是就是資源消耗到達什麼情況下解除流控。

執行流程優化

經典的 NoSQL 系統,提供的 API 都是和服務端的處理流程非常耦合的,比如說 Redis 提供了很多 API,光是 List 就有 20 多個接口。在服務端其實很多接口的執行過程中的步驟是比較類似的,比如說有一些 GenericXXX 的函數定義。我們再看看一般的 RDBMS 中的處理 SQL 的流程,一般是 解析(從 SQL 文本到 AST),然後是優化器編譯 (把 AST 編譯成算子,TableScan、Filter、Aggregate),然後是執行器來執行。類比到 Redis 中,用戶傳進來的就是 AST,且服務端已經預定了執行計劃,直接執行就行了。如果我想使用 SQL,不想學習這麼多 API,同時由於我的訪問場景是比較固定的,比如進行模板化之後,只有十多種 SQL 語句,且訪問的數據比較均衡,某一條特定的語句所有的參數用一條特定的索引就足夠了,有沒有辦法在執行過程中省去解析、編譯的開銷來提高運行的效率?有很多同學可能已經想到了存儲過程。是的,存儲過程很多場景是在擴充表達能力,比如多條語句組成的存儲過程,需要進行比較複雜的邏輯判斷,單條語句存儲過程本質上是在靈活性和性能上進行折衷。Tair 所有線上運行的 SQL 都是預先創建存儲過程的,這樣進行訪問就類似於調用 Redis 的一個 API 了,這是在複雜計算邏輯的場景下保證低延時的一種方案。

很多熟悉數據庫實現的同學對火山執行模型都不陌生,tuple-at-a-time 的執行方式會消耗比較多的 cpu cycle,對 cache locality 也不太友好。在分析場景,通常會引入 code-gen 技術來進行優化,比如 Snowflake、GreenPlum。Tair 中使用 Pipeline 執行模型,使用 Bulk Processing 更適合目前應用的 TP 場景。使用 Pipeline 執行模型對於算子的設計和執行計劃的生成更有挑戰,以 Scan 算子爲例,Scan 算子中內聯了 Filter、Aggregate 和 Projection,Scan 算子本身邏輯比較多,且在執行計劃編譯過程需要在邏輯優化階段進行算子內聯的轉換。

更多場景的低延時


從最早的 KV 到擴展的 Pkey-Skey-Value,再到 List、Zset,再到支持地理位置的 GIS,再到支持全文索引的 Search 和 Table 結構 的 SQL,Tair 早已不再是一個單純用來存儲熱數據的緩存,而是能夠把更多存儲上構建的計算能力方便地提供給業務使用的內存數據庫。這一章節介紹內存數據庫 Tair 在雙十一場景的應用。

購物車使用 Tair 支撐容量升級

提到 MySQL,開發者很容易想到 Table 模型,想到 SQL 查詢來進行過濾、排序、聚合等操作;想到 Redis,很容易想到高吞吐、低延時。使用 Redis 來進行讀加速的場景,都需要把 MySQL 中數據查詢出來之後,序列化到某一個 Value,加速場景直接獲取 Value 即可,無需再進行過濾、排序等操作。如果一個讀加速的場景不僅需要高吞吐低延時,也需要進行過濾等操作,Redis 還能夠滿足需求麼?更進一步,如果引入讀加速的過程中不希望改變數據模型,依然希望使用表模型,省去模型轉換的心智負擔,同時擁有高吞吐低延時,支撐 10w 級別的連接數,需要使用什麼產品呢?目前優惠查詢和購物車的場景的需求抽象出來就是這樣,這種需要關係型數據庫超級只讀的場景就需要引入 Tair 的 SQL 引擎,兼有 MySQL 和 Redis 優勢的產品。

銷量統計使用 Tair 提升實時計算

歷史上雙十一因無法解決銷量的實時計算問題對商家產生過很多困擾。爲應對 2022 年雙十一,Tair 銷量計數項目應運而生:利用已有的 Tair 非精確 “去重計數” 算子開發新的 “去重求和” 算子,解決用戶商品銷量計數慢而無法實時獲得銷量數據的痛點問題。通過對用戶的商品訂單消息進行原子地 “去重和銷量的實時求和” 能力,雙十一首次做到了 “買家訂單數不降級”、“商品月銷量不降級” 兩項大促核心體驗。同時,利用 Tair-PMem 底座進一步幫助用戶降低使用成本,提升數據持久化能力。相比於傳統的 AP 類數據庫,通過開發的獨特非精確計算算子,有效降低了單 QPS 的計算成本。

淘菜菜使用 Tair 進行賣家優惠券召回

淘菜菜是阿里社區電商對外的統一品牌,賣家維度的優惠券召回作爲一個重要的功能模塊,需要搜索系統滿足低成本、實時索引和低延遲的搜索能力。鑑於之前使用的搜索系統無法滿足需求,淘菜菜今年雙十一首次使用 TairSearch 能力實現賣家維度優惠券召回功能,Tair 以其高效實時的內存索引技術爲商家提供更加平滑友好的操作體驗。

TairSearch 是 Tair 自主研發的高性能、低延時、基於內存的實時搜索特性,不但增強了 Tair 在實時計算領域的能力,還和現有的其他數據結構一起爲用戶提供一站式的數據解決方案。Tair 採用了和 ElasticSearch(下文稱之爲 ES)相似的基於 JSON 的查詢語法,滿足了靈活性的同時還兼容 ES 用戶的使用習慣。Tair 除了支持 ES 常用的分詞器,還新增 JIEBA 和 IK 中文分詞器,對中文分詞更加友好。Tair 支持豐富的查詢語義和聚合能力,並且支持索引實時更新和局部更新。Tair 可以通過 msearch 方案實現索引的分片和搜索能力,並通過讀寫分離架構實現搜索性能的水平擴展。

判店場景使用 Tair 解決熱點商家判店

隨着同城購業務的興起,商戶判店場景越來越流行,判店就是商家給自己的一個門店圈出來一個銷售範圍,可以是行政區域,也可以是不規則形狀,或者按照半徑圈選,如果消費者在這個銷售範圍內就認爲門店對該消費者可售,如果不在消費範圍內則不可售,抽象此模型則是:點和多邊形包含關係的判斷。

傳統的判店架構使用 MySQL 或者 PostGis 數據庫,雖然其對 GIS 相關能力有專業的支持,API 也比較完備,但是由於其本身磁盤存儲的特性,查詢速度較慢,特別是數據量較大的場景下,產生多次磁盤讀 IO,導致業務查詢超時。

新一代判店系統,依託 Tair 的 Gis 能力,底層使用 RTree 結構,支持常見的 Contains, Within, Intersects 等關係判斷,可以在 ms 級別返回查詢數據,目前已經在淘菜菜、天貓超市、淘鮮達、盒馬、同城購等多個業務使用。

TairGis 的新一代判店系統

互動場景使用 Tair 多種高性能數據結構快速支撐業務

雙十一主互動場景一直是技術挑戰最大的場景之一,一方面參與活動的用戶數量大,在活動時間集中活躍,帶來的大量的訪問請求對數據庫層面的衝擊尤其巨大;另一方面要求活動體驗不降級,對延時的要求更高。今年主互動活動 -- 猜價格,使用了 Tair 單一數據庫的模式支撐了整個互動活動。在主互動場景中,Tair 作爲 KV 數據庫支撐幾乎所有的數據存儲和讀寫,後端無 DB 兜底,是唯一的數據源,除了要求讀寫的低延遲、高併發以外,還要求數據的絕對安全無丟失。

TairHash 提供的高併發寫入能力確保了千萬級用戶的答案提交順暢;TairZSet 提供的有序數據結構,幫助應用在 10s 內計算出千萬體量的用戶分數排行榜,並支撐快速查詢;帶有二級 Key 主動過期的 TairHash 爲業務設計復活卡、拉新、錦鯉抽獎等多種玩法提供了方便而強大的技術支撐。

Tair 已經在這一類需要低延遲、高併發、數據安全、快速開發的業務場景中表現出了強大的能力,還將持續追求更高性能、更易用、更安全。

寫在最後


在產品力上,Tair 提供了遠遠不止以上圍繞着低延時來打造的產品能力,比如數據多副本管理、全球多活、任意時間點恢復、審計日誌等等。同時 Tair 在兼容 Redis 之外,提供了豐富的數據處理能力和基於不同存儲介質的混合引擎來提升性價比。

2022 年還有一些其它的事情在發生:Tair 的論文發表在數據庫領域頂會 VLDB ,雲原生內存數據庫 Tair 獨立產品上線阿里雲官網,Tair 全自研 Redis 兼容內核在公共雲所有 Region 上線等等。有一些成績,也有很多挑戰,還有更多機會。Tair 會將已經具備的能力建設得更通用,並在新的領域尋求新的突破,在更豐富的低延時場景承擔起更重要的責任,爲客戶創造更多價值。

參考鏈接:

Tair 獨立產品:https://www.aliyun.com/product/apsaradb/kvstore/tair

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/kQ-U9eGHmeFiwLf_BO-ykA