作者 | 阿里雲存儲
來源 | 凌雲時刻
背景
又一屆億萬買家和賣家參與的“雙 11”落下帷幕,“雙 11”這場消費盛宴離不開阿里雲產品技術團隊的大力支撐,阿里雲存儲團隊的表格存儲就是其中之一。
把時間向前撥動 10 個月,突如其來的疫情導致的線上辦公的剛性需求,讓釘釘對現有存儲系統在成本、可運維性、穩定性方面提出了極大的挑戰。經過一系列調研,釘釘最終選擇阿里雲存儲團隊的表格存儲 Tablestore 作為統一的存儲平臺。經過釘釘團隊和表格存儲團隊的緊密合作,釘釘 IM 系統、IMPaaS 多租戶平臺的所有消息同步數據和全量消息數據已經基本完成遷移工作。在剛剛過去的 2020 雙 11 購物節,Tablestore 第一次全面支持集團 IM(釘釘、手淘&天貓&千牛客服聊天、餓了麼等)並平穩度過,保障億萬買家和賣家之間更為順暢的交流。
本文將介紹釘釘 IM 存儲架構、表格存儲 Tablestore 為了滿足遷移在穩定性、功能、性能上做的一系列工作。
關於表格存儲 Tablestore 與釘釘消息系統
阿里雲表格存儲 Tablestore 是一款面向海量結構化數據存儲、搜索和分析一體的在線數據平臺,可擴展至單表 PB 級存儲規模,同時提供每秒億次訪問服務能力的產品。作為阿里雲智能自研產品,Tablestore 具有高性能、低成本、易擴展、全託管、高可靠、高可用、冷熱數據分層和靈活計算分析能力等特性,尤其是在元數據、大數據、監控、消息、物聯網、軌跡溯源等類型應用上有豐富的實踐經驗和積累。下圖展示了 Tablestore 的產品架構。
釘釘消息系統
釘釘消息系統包括:釘釘 IM 系統和 IMPaaS 系統,其中釘釘 IM 系統承載了釘釘 App 的 IM 業務,IMPaaS 作為多租戶 IM 平臺支持集團各團隊的接入,目前已經承載了釘釘、手淘&天貓&千牛客服聊天、餓了麼等、高德等集團主要 IM 業務。兩套系統在架構方面是統一的,所以本文後續中不會明顯區分兩套系統(為了方便,統一描述為 IM 系統)。該系統負責個人和群組消息的接收,存儲和轉發環節,同時也包括會話管理,已讀通知等狀態同步。一句話概括:負責端與端的溝通。
存儲架構升級
IM 系統已有架構使用的存儲系統比較多,主要包括 DB(InnoDB、X-Engine)和 Tablestore。其中會話和消息的存儲使用 DB,同步協議(負責消息同步推送)使用 Tablestore。在疫情期間,釘釘的消息量增長非常迅速,讓釘釘團隊意識到使用傳統的關係數據庫引擎來存儲流水型的消息數據侷限性較大,不管是在寫入性能、可擴展性、還是存儲成本上,基於 LSM 的分佈式 NoSQL 都是更佳的選擇。
上面是 IM 系統原來的架構圖。在這套系統中,一條消息發送後,會有兩次存儲,包括:
1. 全量消息存儲:如圖中第 4 步,將消息存儲到 DB 中持久化。該消息主要用於用戶主動拉和系統展現 App 的首屏信息。全量消息會永久保留。
2. 同步協議存儲:如圖第 5 步,將消息存儲到 Tablestore 中,然後讀取合併推送到用戶。同步協議存儲的數據只會保留若干天。
而在架構升級後,全量消息存儲去掉了對 DB 的依賴,把數據全部寫入 Tablestore 中,改造之後 IM 系統存儲只依賴 Tablestore,並且帶來如下收益:
1. 成本低:Tablestore 基於 LSM 存儲引擎的架構在成本上比分庫分表的架構有優勢,數據存儲打開 EC 功能,冷熱分層存儲、雲服務的按量收費,都能使得釘釘的成本更低。按照目前計費情況,存儲遷移到表格存儲後,釘釘存儲成本節約 60% 以上。
2. 系統彈性能力強,擴展性好:可以按需擴容任意數量機器,擴容速度快且對業務無影響,在機器準備完成(包括克隆系統)的情況下,分鐘級完成擴容。在扛過集群流量高峰之後,也可以快速縮容,節約資源。
3. 零運維:Tablestore 是全託管的雲服務產品,不需要用戶承擔任何運維工作,並且 Tablestore 也能做到運維期間業務無感知。另外,Tablestore是 schema free 的架構,用戶也不必為業務需求變化帶來的表結構調整而煩惱。
穩定性保障
穩定性是一切的根本,也是業務選型最大的顧慮。在複雜的分佈式系統中,每一個組件都有出問題的可能,大到機房挖斷光纜,小到網卡 bit 翻轉,每一個被漏掉的問題都可能導致災難性故障,甚至引發嚴重的輿情風險。穩定性工作的核心就是充分考慮這些可能性並儘可能的提供容錯能力。作為阿里雲智能的核心基礎產品,Tablestore 被部署在阿里雲全球所有的服務區,穩定性得到了很好的錘鍊,同時在服務釘釘的過程中,又專門做了穩定性的加強,具體包括:
主備雙集群容災
為了避免單機房故障引起的服務不可訪問,Tablestore 具備主備雙集群容災的能力。主備集群是兩個獨立的集群,數據通過後臺異步的方式來複制,因此這種容災方式並不能保證數據強一致,一般會有幾秒的延遲。在主集群單機房故障時,會將所有流量切換到備集群,利用備集群來提供服務,保證了服務的可用性。
3AZ 強一致容災
主備容災雖然能解決單機房的容災問題,但是在主集群非預期宕機情況下不能做到數據強一致,切換過程需要和業務配合(容忍一段時間內數據不一致,後續配合補數據等)等缺點。3AZ(Available Zone)的容災模式就能解決這些問題。
3AZ 的容災部署方式是將一套系統部署在 3 個物理機房之上,寫數據的時候,盤古會將數據的 3 個副本均勻分佈在 3 個機房,保證任何一個機房故障不可用,另外兩個機房都有全部數據,從而做到不丟數據。Tablestore 每次寫數據返回成功後,3 個副本都已經在 3 個機房落盤。所以 3AZ 的架構是保證數據強一致的。當其中一個機房故障時,Tablestore 就會將分區都調度到另外 2 個機房繼續對外提供服務。3AZ 的部署方式相比主備集群有如下優勢:
1. 成本低,底層盤古的 3 份副本均勻分佈在 3 個機房,而主備集群需要多一份數據複製;後續還可以採用 EC 繼續降低成本。
2. 機房間數據強一致(RPO = 0)。
3. 切換更順滑,由於數據是強一致,所以業務不需要在切換期間做額外的處理邏輯。
負載均衡系統
在表格存儲 Tablestore 內部,為了使表有高擴展能力,表的存儲在邏輯上分為很多分區,服務會根據分區鍵範圍對錶進行分區(range 分區),並將這些分區調度到不同的機器上對外服務。為了讓整個系統的吞吐和性能達到最優,應該讓系統內各個機器負載均衡,也就是需要分佈在各個機器上的訪問量和數據量實現負載均衡。
但實際上,在系統運行過程中,常常會出現由於設計問題或者業務問題帶來數據訪問和分佈不均衡的問題,常見的如數據傾斜、局部熱點分區等。這裡面上有兩個大的問題需要解決:1. 如何自動發現;2. 發現後如何自動處理。這裡強調自動主要是因為表格存儲 Tablestore 作為一個多租戶系統,上面運行的實例、表數量巨大,如果全靠人來反饋和處理,不僅成本巨大,實際也沒有可操作性。表格存儲 Tablestore 為了解決這個問題,設計了自己的負載均衡系統,其中要點:1. 收集分區級別詳細的信息統計,便於後續進行分析適用;2. 基於訪問量的分裂點計算,也就是發現熱點;3. 多維度精細的分組功能,主要是為了管理分區的分佈。在此基礎之上,負載均衡系統還開發了數十種策略分別來解決各種類型的熱點問題。
這套負載均衡系統能夠自動的發現並處理 99% 的集群中常出現的熱點問題,也提供了分鐘級自動化處理問題,分鐘級白屏化定位以及處理問題的能力,目前已經全域運行。
完善的流控體系
Tablestore 在流控方面提供了一整套完善的流控體系,大致如下:
1. 實例和表級別的全局流控:主要解決業務流量超過期望,防止打滿集群資源。如下圖,左側是流控架構,右側是流控模型,可以從實例以及表級別分別控制訪問流量。如果實例級的流量超限的話,那麼該實例的所有請求都會受到影響;也在表級別可以單獨設置表的流量上限,避免單表的流量超限對其他表產生影響。依賴這套系統,Tablestore 能夠精確的對指定實例、表、操作進行定向流控,提供多維度的個性化控制手段,保障服務的 SLA。在疫情期間,釘釘多次出現流量打滿集群的情況,全局流控就能比較好地解決該問題。
2. 分區主動流控:Tablestore 按照 range 對數據進行分區,在業務數據有傾斜的時候容易發生分區熱點讀寫。分區主動流控主要解決單分區熱點問題,防止單分區熱點影響其他分區。在進程中會統計每個分區的訪問信息,包括但不限於:訪問的流量、行數、cell 數等。
在上述流控體系的保障下,做到了多級的防禦,保障了服務的穩定。
極致性能優化
Tablestore 是典型的存儲計算分離架構,其依賴組件多,請求鏈路複雜,因此性能優化不能只侷限在某一個模塊,需要有一個全局視角,對請求鏈路上的各個組件協同優化,才能獲得比較好的全鏈路性能優化效果。
在釘釘場景中,經過一系列優化,在相同 SLA 以及同等硬件資源情況下,讀寫吞吐提升 3 倍以上,讀寫延遲下降 85%。讀寫性能的提升,也節省了更多機器資源,降低了業務成本。下面介紹一下主要的性能工作。
存儲/網絡優化
在 Tablestore 的核心數據鏈路上,包含 3 個主要的系統組件:
1. Tablestore Proxy:作為用戶請求的統一接入層,做請求鑑權、合法性檢驗、轉發;當所有前置的檢查操作完成後,會將請求轉發到 Table Engine 層。
2. Table Engine:表格存儲引擎以 LSM tree 為處理模型,提供分佈式表格能力。
3. 盤古:分佈式文件系統,用來承接表格存儲引擎在數據處理過程中的數據持久化工作。
在請求鏈路中,第一跳網絡(Tablestore Proxy 到 Table Engine)和第二跳網絡(Table Engine 到盤古)在原來的架構中,使用的是基於 kernel tcp 網絡框架,會有多次的數據拷貝以及 sys cpu 的消耗。因此在存儲網絡優化上,將第一跳使用 Luna(新一代的用戶態 RPC 框架)作為數據傳輸,降低 kernel 的數據拷貝以及 sys 的 cpu 消耗;在數據持久化的第二跳中,使用阿里雲分佈式雲存儲平臺——盤古提供的 RDMA 網絡庫,進一步優化數據傳輸延遲。
盤古作為存儲底座,新一代的盤古 2.0,面向新一代網絡和存儲軟硬件進行架構設計和工程優化,釋放軟硬件技術發展的紅利,在數據存儲鏈路上,獲得極大的性能提升,讀寫延遲進入 100 微秒級時代。基於存儲底座的架構優化,Table Engine 也升級了數據存儲通路,接入盤古 2.0,打通極致的 IO 通路。
迭代器優化
Tablestore 的表格模型支持非常豐富數據存儲語義,包括:
1. 數據列多版本
2. 列的 schema-free,寬表模型
3. 靈活的刪除語義:如行刪除,列多版本刪除,指定列版本刪除
4. TTL 數據過期
每一種策略在實現的過程中,為了快速迭代,都以火山模型方式實現迭代器體系(如下圖左半部)。這個方式帶來的一個問題就是隨著策略的複雜,迭代層次越來越深(即使不用到策略,也會被加到這個體系中);這個過程的動態多態造成的函數調用開銷大,並且從研發的角度看,很難形成統一視角,為後續的優化也帶來了阻礙。
為了優化數據迭代性能,將每種策略需要的上下文信息組裝成一個策略類,以一個全局的視角將策略相關的迭代器拍平,進而降低嵌套的迭代器層次在讀取鏈路上的性能損耗;在多路歸併上,使用列存提供的多列 PK 跳轉能力,降低多列歸併排序時的比較開銷,提升數據讀取性能。
鎖優化
除了存儲格式上的改進之外,數據在內存處理過程中,面臨的一個比較大的問題就是鎖競爭造成的性能損失。而在系統各個模塊中無處不在,除了顯式的用於同步或併發臨界區的鎖外,也有容易忽略的原子操作的樂觀鎖性能問題(如:全局的 metric 計數器,CAS 指令衝突等),另外像小對象的內存分配,也可能造成底層內存分配器的鎖衝突嚴重。在優化鎖結構上,主要在以下方面做了優化:
1. 將隱式事務中,互斥鎖轉換成讀寫鎖;只有顯示需要事務保證的請求,才通過寫鎖做串行化,對於常規路徑的無事務需求的請求,通過最大程度的讀鎖進行並行化。
2. 在讀路徑上,cache 的 lru 功能也引入了鎖做臨界區保護,在高併發讀取時,lru 的策略很容易造成鎖競爭,因此實現了一個無鎖的 lru cache 策略,提升讀性能。
基於實踐的功能創新
作為阿里自研 NoSQL 產品,Tablestore 在和釘釘合作過程中,充分發揮了自研的優勢,結合釘釘業務開發了多個新功能,進一步減少了 CPU 消耗,並且實現性能極致優化和業務層的無感知升級。
PK 自增+ 1 功能
釘釘同步協議使用了 Tablestore 的 PK 自增功能,PK 自增的功能是在用戶成功寫入一行數據後返回一個自增 ID,首先簡單介紹下釘釘利用自增 ID 實現消息同步推送的場景:
1. 用戶發送消息,應用服務器收到消息後,將消息寫入 Tablestore 中,寫入成功後,返回消息的自增 ID。
2. 應用服務器,根據上次已經推送的消息 ID,查詢兩次 ID 之間的所有消息。然後將查詢到的消息推送給用戶。
在上述場景中,有一種特殊的場景,就是該用戶只有一條未推送消息,如果保障返回消息的 ID 在保證自增的同時,還能保證自增+1,那麼應用服務器再寫入消息後,如果發現返回的 ID 和上次推送的 ID 只相差 1,那麼就不需要從 Tablestore 中查詢,直接將該消息推送給用戶即可。
PK 自增+1 功能上線後,減少了 40% 以上的應用端讀,減少了 Tablestore 服務器和業務自身服務器的 CPU 消耗,進一步節約了釘釘業務成本。
局部索引
在此之前,Tablestore 已經具備了異步的全局二級索引功能。但是在釘釘場景下,絕大部分查詢都是查詢某個用戶之下的數據,即索引只需在某個用戶自己的數據下生效即可。為此,我們開發了強一致的局部索引功能,並且支持存量數據的動態索引構建。
應用場景
如下圖中的場景,用戶可以直接找到所有@自己的消息。
對應到業務側,會有一張用戶消息表存儲該用戶的所有消息,然後在這張表上,建立一張最近@我的消息的局部索引表,那麼直接掃描該局部索引表,便可以得到 AT 該用戶的所有消息,兩張表結構分別如下圖:
消息主表
消息局部索引表
索引增量構建
對於增量索引構建,通過考察整個鏈路上的各個模塊,我們做了比較極致的性能優化,使一行索引的構建時間可以做到 10us 左右:
1. 通過分析表的 Schema,與當前用戶寫入的數據進行對比,來節省掉不必要的寫前讀。
2. 通過記錄數據更新之前的原始值到日誌,成倍的降低了索引構建成功後寫日誌的 IO 數據量。
3. 通過批量的索引構建,在一次構建過程中,便計算出所有索引表的索引行。
4. 通過在進入日誌之前便進行計算,充分利用了多線程並行的能力。
存量索引構建
對於存量數據的索引,在業界也是一個比較難處理的問題:局部二級索引要求強一致,即寫入成功,就可以從索引表中讀到,另外一方面,存量索引構建過程中,希望儘量的減少對在線寫入的影響(不能停寫)。
如下圖所示,傳統數據庫沒有很好的解決這個問題,在存量到增量的轉換過程中,需要鎖表,禁止用戶的寫入,造成停服,甚至 DynamoDB 直接不支持在表上進行存量局部二級索引的構建。
傳統數據庫的存量構建方式
Tablestore 存量索引的構建方式
Tablestore 利用自身引擎的特性,巧妙的解決了這個問題,同時進行存量數據和增量數據的索引構建,做到了存量數據構建局部二級索引完全不需要停止在線寫入流量,而且存量構建結束後,整個索引構建過程即結束,業務層完全無感知。
後記
數字化轉型過程不是“開著飛機換引擎”,而是“飛機加速時換引擎”。在業務快速發展過程中,如何保障存儲系統乃至整個業務系統實現平穩高效的升級將是每一位 CIO 需要解答的問題。
最後,歡迎加入釘釘公開群(釘釘號:11789671),一起探討技術,交流技術。