本文由釘釘技術專家嘯臺、萬泓分享,為了獲得更好的閱讀效果,本文已對內容進行少修訂和重新排版。
1、引言
釘釘后端架構的單元化工作從2018年開始到今年,已經是第五個年頭了。五年的時間,釘釘單元化迭代了三個版本,從最初的毛頭小子,到達今年已經小有成就。
我們在進行單元化架構建設的過程中,除了網上能找到的屈指可數的文章外,可以直接使用的系統更是乏善可陳,使我們不得不從最基礎的系統開始造輪子,極大的影響建設效率。幸運的是,近幾年云原生技術的興起,讓我們能復用很多基礎設施,進而快速提升我們的單元化建設能力,助力釘釘的發展。
今天想借此文和大家分享我們在釘釘單元化架構實施過程中的心路歷程和一些最佳實踐。因涉及的技術和業務面太廣,本文的分享無法做到面面俱到,主要是想在同路人中形成共鳴,進而能復用一些架構或子系統的設計和實現思路。
學習交流:
(本文同步發布于:http://www.52im.net/thread-4122-1-1.html)
2、系列文章
本文是系列文章的第 10 篇,總目錄如下:
3、術語概念
本文內容中使用了一些專有的技術名詞,為了方便大家理解,我把關鍵的幾個術語概念的縮寫及其含義專門列出來,供大家參考。
主要是以下幾個:
- 1)Geo:釘釘專有化部署單位,解決數據合規需求,Geo間數據按需互通,并且互通數據在Geo內部做鏡像拷貝,解決兩化問題;
- 2)Unit: Geo內部資源物理分區隔離的最小單位,解決Geo內的容災和容量的問題;
- 3)L0:客戶端路由,決定了用戶客戶端接入釘釘服務器的所屬單元,用戶長連接所在的邏輯單元,起到連接加速作用。用戶接入單元;
- 4)L1:接入層路由,以用戶為維度進行調度,即用戶操作發生的單元。用戶歸屬單元;
- 5)L2:業務層路由,以業務資源為維度進行調度,大部分的業務資源所在單元應該和用戶調度單元一致,但一些業務無法按照用戶劃分單元,如IM的會話,音視頻的會議。 業務歸屬單元;
- 6)DMB:負責釘釘應用跨單元RPC調用的轉發,可以認為是釘釘單元化RPC路由中間件;
- 7)DMR:負責釘釘應用跨單元MQ消息的轉發,可以認為是釘釘單元化MQ路由中間件;
- 8)DTIM:釘釘IM系統。
4、單元化架構1.0版:合規驅動下的部署架構
2018年,部分大客戶出于法律政策、商業機密數據存儲的要求,要求釘釘的數據存儲、訪問接入、服務部署需要在其信任的區域內。既需要滿足其數據存儲私有化要求,同時需要滿足跨地區網絡的rt性能要求。
于是我們結合阿里云機房部署位置、物理距離、用戶數據安全等方面出發,釘釘在客戶的阿里云機房內建設了一個單元,將通訊錄、IM信息等企業數據單獨存儲在客戶機房。

我們通過一條專線,將兩個機房邏輯串聯到一起,內部通過DMB/DMR系統,實現了請求互通,這就是釘釘單元化架構的1.0版。
1.0版比較簡單,純粹是業務驅動,和支付寶單元化建設的初衷——“容災驅動”有較大區別。兩個站點通過UID分段,將用戶劃分為中心用戶和專有用戶。
上圖只是一個簡化的邏輯結構,內部實現遠比上圖復雜,但是1.0建設主要是從0到1,和大多數異地多活的系統較相似,這里就只簡單的和大家分享一下。
5、單元化架構2.0版:逼出來的容量架構
2020年是一個特殊的年份,由于疫情的原因,帶給大家非常多的改變,其中也包括釘釘。
由于在線辦公與教育流量的突增,開年第一天上班就給釘釘一個下馬威,平峰的流量已經和除夕跨年的持平,但是和除夕不同的是這個流量是持續的,即使節前準備了三倍容量,也抵擋不住流量對系統的沖擊。只能借助阿里云的能力,不斷的擴容。

但是每天將近30%的流量增幅,單純的擴容也能難保障服務的連續性,最終也遇到了擴無可擴的場景,張北機房沒有機位了,有機器資源但是沒有機位讓我們有力無處使。我們不得不不斷進行系統優化,同時借助限流、降級、雙推等措施,勉強抗住了流量的最高峰。
疫情之前,我們一直在做高可用,但是這個高可用主要集中在容災機制上,比如搭建容災單元。如同支付寶一樣,是因為當時光纖被挖斷;又比如銀行的兩地三中心架構,是擔心某一個地域由于天災或者戰爭導致數據丟失。疫情的流量給我們上了一課,僅僅關注容災是不夠的,特別是釘釘的DAU從千萬走向億級別之后,更需要在容量上做出提前規劃。
正因如此,我們認為“容量架構不是設計出來而是真真切切被逼出來的”,所以容量架構就成為我們單元化核心要素之一。
容量架構是將流量劃分到不同單元,每個單元承載各自的流量。容災架構是單元異常時,能保障核心的能力可用,也可以將流量動態調度到別的單元,實現服務的快速恢復。
因此釘釘單元化進入了2.0時代,專注于容量和容災的建設。

6、2.0版是基于什么維度進行流量劃分的?
要實現流量的劃分,必然要基于一個維度進行劃分,一部分到A單元,一部分到B單元。
釘釘單元化架構也是參考了淘系和支付寶的單元化架構,前兩者都是基于UID劃分,釘釘單元化的第一個版本其實也是一樣的,基于UID做拆分。
但是當我們設計容量架構時,發現基于UID劃分無法解決我們的容量問題。
以IM為例:一條消息其實屬于聊天雙方的,群聊亦是如此。用戶能和任意一個人聊天,這樣我們根本無法找到一個切入點來劃分流量,強行按照UID拆分,必然導致一個用戶的消息出現在N個單元,單元的自封閉就無法做了。
也有同學會說:為什么消息不按照每個人存儲,這不就能按照UID劃分了嗎?結論是不行。首先這個消息變成了寫擴散,持久化的時候會變成多單元寫,其次是成本翻倍,在DTIM這種過億規模的場景這條路走不通。這里可以多說一點,因為這個觀點來之不易,大家都知道,人是有慣性的,既然淘寶、支付寶甚至是微信都是UID劃分,為什么釘釘要特立獨行?當時我們團隊受到了絕大部分釘釘技術團隊的挑戰,持續長達將近一個月的技術選型的“爭吵”,最終還是達成了一致意見。
DTIM主要有3個維度,分別是UID、會話(CID)、消息。其中會話和消息是綁定的,而系統中最大量的是消息,按照第一性原則來看,一定要將消息劃分開來,才能做到將容量劃分開來的效果。
我們再來看看音視頻,是按照房間維度組織流量和數據的,和IM又完全不同。
同樣,文檔其實更適合按照企業維度來劃分。
不同的業務擁有不同的維度,因此我們認為:單元化最重要的找到自身“最大”的業務維度,將這個維護拆分,才能實現單元的橫向擴展,我們稱之為“業務路由”。
回頭來看:我們之前其實是進入了思考誤區,以為淘系和支付寶都是UID維度,我們也要這個維度,其實UID正是前者的業務維度,比如訂單,也是圍繞用戶,并不會有交集的情況,會話就是IM的劃分維度,因此做單元化之前要先找到屬于自己的業務維度。
7、2.0版是如何實現IM消息的全局路由能力的?
7.1概述
UID路由有個最大的好處,就是可以按照UID分段,能實現高效的靜態路由,也不用擔心多單元之間的一致性問題。但是這種分段路由局限性也比較明顯,需要預先分配,單元之間動態調度流量和數據成本極高,而且只能支持這種數值+順序的場景。
在釘釘的場景中,有會話維度、房間維度、企業維度等等,想簡單采用這種預分段機制難以滿足業務需求。因此我們需要構建一個業務路由系統(RoutingService),實現消息流量的精確路由。

以IM為例:每次消息的發送,在單元化框架層面,會通過消息的會話(CID),查詢路由信息,如果是本單元,流量下行并持久化;如果是非本單元,路由到對應的單元中。
下圖是三個會話:分別是cid:1001、cid:1002、cid:1003,三個會話隸屬不同單元,不管用戶從哪個單元發送消息,都會路由到會話所在的單元。比如:用戶在Unit B的cid:1001 中發送消息,當消息進入Receiver之后,會先查詢此cid:1001所在的單元,發現是Unit A,路由框架將請求轉到A單元,消息在A單元持久化并通過A單元的同步協議,將數據推送到客戶端。


從上圖可知:每次消息發送,都要查詢路由服務,DTIM百萬的峰值,對路由必然會帶來超大的壓力,同時我們能發現,路由數據在多單元實現一致性是一個巨大的挑戰。
7.2邊緣計算:端到端路由
在DTIM的場景中,會話的路由信息幾乎不會變更,只有當我們決定將某些超大的會話或者企業騰挪到新單元時,才會發起路由的變更,因此會話的路由信息幾乎可以認為是恒定不變的。那么每次查詢路由服務端,效費比太低,是極大的浪費。
既然路由信息幾乎不可變,是否將路由信息緩存呢?最常見的是使用一個集中式的Cache系統,緩存Hot的會話,我們也是這么做的,但是這么做還是不夠,一旦Cache系統失效,DTIM還是會出現大面積故障,而且這個百萬級的請求對Cache也是一個極大的壓力。
考慮到釘釘有強大的客戶端,借用邊緣計算的思路,我們將用戶的會話數據緩存到客戶端。對于客戶端來說,也只用緩存用戶自身最熱的N會話路由數據,消息發送時,通過Header將路由數據攜帶到服務端,服務端路由SDK只要做合法性和續約即可,這樣就將路由流量降低了95%以上。當路由服務出現異常時,還可以繼續使用客戶端路由,將路由的可用性提升到一個新的高度。
SDK本地會依據上行請求的返回中是否有新的路由信息,進而更新客戶端路由。同時可以借助釘釘有主動下推的能力,通過同步協議將新的路由信息主動推送給客戶端,使會話遷移做到更平順。

7.3計算下沉:多單元一致性
對于新會話:比如小明要創建一個群聊,是應該創建在那個單元呢?
如果在A單元創建了,當會話消息來到B單元,系統怎么能第一時間知道會話已經在被綁定到A單元。
這里一般的方式有兩種:
- 1)單元之間的存儲系統采用類似DTS的機制進行異步同步,這種機制有秒級延遲;
- 2)在應用層主動同步,比如接入消息隊列。
這兩種方式由于都是異步的原因,都會出現不一致的問題,如果會話同時被綁定在兩個單元,邏輯上會導致用戶的歷史消息丟失,這個是不能接受的。
多地域(Region)數據同步其實是通用的技術挑戰,我們認為存儲系統提供是最好的方式,正如Google的Spanner一樣,這樣對我們上層才是最友好的方式。
因此我們找到了存儲的OTS、Nuwa團隊一起共建了GlobalTable。GlobalTable的核心原理還是借助Nuwa的一致性組,組分布在多個地域,采用多數派寫入成功即返回的原理,做到20ms以內的一致性寫。

8、2.0版的容災能力
釘釘單元化的容災能力是深度結合釘釘的業務層場景落地的,和淘系支付寶等有明確的區別。
以DTIM為例,最大的特點是當服務單元異常時,服務側仍能提供最核心的服務,保障最基本的能力。本質上是由于DTIM是最終一致性系統,可以短暫允許部分環節失敗。
可以看一下DTIM發送消息的容災場景。當某個單元完全不可用的情況下,用戶消息發送鏈路通過降級為local模式,在本地校驗非本單元會話數據通過之后直接做消息發送,processor遇到非本單元的會話消息數據可以做單元間投遞做數據回放,本地是否落庫可選,同步協議推送不必區分是否為本單元會話消息數據直接通過本單元的topic推送給客戶端,配合用戶無狀態快速遷移能力,單元間可以實現真正的分鐘級別容災切換能力。

9、2.0版的成果與突破

以上是釘釘單元化2.0提供給應用的核心能力,在滿足容災和容量設計需求之后,釘釘單元化給應用帶來了更多的能力和想象空間。
比如:
- 1)快速遷移:當某一地域資源不足時,釘釘單元化可以將業務快速的從A單元遷移到B單元;
- 2)常態化切流:比如新建的教育會話,可以放到獨立的單元;
- 3)熱點治理:當前某一個會話過熱,特殊時期可以遷移到獨立集群;
- 4)SLA:滿足不同的VIP客戶需求,基于不同的SLA和售賣價格,將VIP客戶放到對應地單元。
核心還是我們擁有單元化能力之后,實現了多單元流量的快速調度,為業務解決了后顧之憂。
10、2.0版在新時代面臨的新挑戰
10.1魚和熊掌不可兼得
2022年對釘釘來說是成本之年,成本的壓力不光落到了團隊,還落到了每個人身上。
正如存儲的CAP理論是一樣的,我們同時只能滿足兩個維度,對于流量(性能P)、成本(C)、體驗(E)也是一樣,在流量不可預知和干預的情況下,選擇成本必然導致體驗受損,反之選擇體驗,必然導致成本升高。進入下半年,疫情反復帶來流量的反復,為了實現可控的教育成本,只能在高峰期降級部分能力,這又導致體驗受損,這段時間的工單量可以窺見一斑。
流量是用戶側觸發的,我們無法干預,只能在成本和體驗之間尋求平衡。和前面提及的一樣,為了減小成本的消耗這就導致我們在擴容和縮容之間疲于奔命,反應不及時甚至有故障的危險,這種機制不可取也不可持續。到底是要流量與成本,還是要流量與體驗,給我們技術團隊帶來了巨大的挑戰和矛盾。
10.2商業化路在何方
當前釘釘為支持大客戶提供了多種解決方案,專業釘釘、專屬存儲與打包、專有釘釘。
專屬釘釘通過APP專屬化以及部分專屬功能,比如為一個企業定制一個擁有獨立Logo的APP,能滿足一般的中大型客戶的業務訴求。
對于大型以及超大型客戶,我們提供專有釘釘,提供專有化輸出,完全隔離的方案,比如浙政釘。
伴隨著釘釘的商業化進入深水區,客戶對釘釘提出了新的訴求,特別是數據安全與歸屬、互聯互通、完整的能力棧等訴求,當前釘釘輸出產品形態都無法同時地滿足以上需求。
前幾年互聯網上出現的幾起數據安全事件,數據丟失與泄露,未經客戶授權私自訪問客戶數據,讓大多數客戶不信任服務提供商,即使服務商的安全能力已經是業界一線能力。其實這個是可以理解的,數據即客戶的生命線,數據無法在自身可控范圍內,特別是對于很多特殊行業,這是無法接受的,自身性命豈能假手于人。專屬釘釘在面臨這種客戶時,前線售賣同學是無能為力。
那么很多同學肯定會提“如果專屬釘釘滿足不了需求,我們專有釘釘不是能解決這些問題嗎?”,其實單單從訴求來看,專有釘釘場景是切合客戶的業務訴求,提供完全獨立運行環境、可控的數據安全。但是專有釘釘由于其獨特的架構帶來高昂的售價以及后期的運維代價,對于超大型的客戶來說也難以承擔如此高的成本。對于釘釘自身來說,從研發到后續運維,維護一套獨立體系也難以在客戶側大面積推廣。

11、單元化架構3.0版:混合云架構
11.1概述
釘釘單元化經過四年的發展,在容災和容量上做出一定的積淀,同時完成了一些核心技術的積累。
當整體架構成熟之后,我們也在思考,單元化能否從技術架構升級為業務架構,比如搭建獨立的高可用單元,按照售賣的SLA提供給VIP客戶,支持釘釘商業化的發展。
同時我們在云原生逐步發力,將部分核心應用放到云上,經過這一年多的運行,遇到了新的挑戰,但更獲得云下無法獲得的計算彈性能力,云上的彈性對云下是一個降維打擊,從一個新的方向解決計算問題。
如上文提到的兩個核心挑戰,釘釘單元化同樣面臨這個問題,在持續的發展中找到了一個合適的架構方向。
基本思路是:
- 1)云下作為基本盤,保障核心流量的問題,畢竟云下經過集團多年的打磨,不管是穩定性還是流程的合理性都有保障;
- 2)云上應對高漲異常的流量,比如和疫情正相關的教育流量,既保證了服務的穩定性,又能充分利用云上彈性能力,在提供完整能力的前提下做到一個相對較低的成本。
其次是升級Geo概念:
- 1)將Geo作為一個獨立的業務域,實現Geo級別完全獨立部署,分布式云模式;
- 2)同時Geo之間按需互通,從研發體系上能做到一套代碼。

因此,釘釘單元化來到了3.0版本,我們稱之為釘釘單元化混合云架構。
混合云主要是從兩個維度來看:
- 第一:是云上云下,我們認為云上云下并不是取代的關系,而是相互補充的關系,是一個長期的狀態,正如很多大客戶隨著規模的持續擴張,最終依賴的部分核心能力必然走向自研道理一樣,這能做成本的進一步降低,所以架構是一個混合云架構;
- 第二:業務架構上也是混合云架構,通過不同的Geo,將不同的業務邏輯上聚合到一起,構建起一張釘釘的大網,不同Geo按需互通,實現了業務架構的混合。
3.0從系統架構上相對于2.0,最大的區別就是云原生技術的運用和互通網關的建立。
11.2云原生技術 :抵抗系統架構熵增的有效手段
近幾年,互聯網圈最火的技術莫過于以Docker為代表的云原生技術最為火熱,各大云廠商也都在不遺余力的推廣云原生技術以及對應的產品。同時釘釘服務過億DAU的客戶,面對各種可靠性、服務連續性、并發、容災等技術挑戰,也都走到了現有技術的邊界。
所以我們也在不斷吸收新的技術和架構,希望從體系與架構上降低我們的技術復雜度,以抵抗熵增。
我們在2021年底啟動了云原生升級戰略,升級云原生技術并不是為了技術而升級,而是切實面臨巨大的技術挑戰。
1)首先我們面臨多語言的挑戰:
我們以IM為例,IM的核心邏輯都是使用C++構建,但是我們常用的中間件三大件:存儲、緩存、異步隊列,其中緩存和異步隊列在C++客戶端上長期建設不足,導致IM長期在使用低版本。
低版本由于長時間缺乏維護,經常會出現異常,比如隊列假死、消費不均等,導致我們自己不得不親自上陣修改SDK的代碼,以致最后難以使用到產品的新能力,阻礙IM服務能力的提升。
2)其次是多產品多云的挑戰:
我們以阿里云為例,數據庫類目下的產品,從類別上就有關系數據庫、NoSQL數據庫、數倉等等,還有存儲也是一樣。
對于我們上層業務,其實絕大部分服務都只依賴了底層的CURD,這么多產品,每次對接一個產品都要開發一輪。
配置系統也是一樣,彈內有Diamond,云上有Nacos、Mse,K8s有自己的Configmap等,而且這些配置系統不像數據庫有標準,而是百花齊放,但是這樣卻苦了我們使用者。
這些內容不是我們的核心路徑,浪費大把時間在各種產品接口的適配上,明顯拖累了釘釘的發展。
3)最后就是通用的流量治理挑戰:
釘釘很多系統都是最終一致的系統,IM就是典型的最終一致系統,這類系統和強同步系統在架構設計有一個明顯的區別,強一致系統如果遇到失敗,必須要持續重試直到成功,所以一般編程上都是重試+退避。
但是最終一致系統不是,這類系統允許部分節點失敗,不要阻礙其他流程,失敗的流量通過一個異步回旋的隊列,將數據逐步回放回來即可。這種回旋需要借助異步隊列,而且要設計各種消費機制,比如限速、比如丟棄等等,這是一個通用的邏輯,但是每個業務方或多或少都在實現自己的回旋系統,重復的造輪子。又比如各種故障注入,單元化路由流量等等,要想擁有這個能力,團隊不得不投入人力研發。
在對付架構復雜度上,我們主要從兩個維度來屏蔽復雜度。
首先代碼層面我們選擇了DDD模式,我們使用DDD分層核心是把對外系統的依賴全部收攏到Infrastructure這一層,全部采用純虛函數(Interface)對外提供接口。屏蔽底層中間件差異和細節。
在架構上采用Sidecar的模式,類似于Dapr的思想,通過標準的GRPC和PB實現應用與中間件解耦。Sidecar中集成了各種中間件、配置系統、灰度系統等,等價實現了應用和中間件的解耦。上文中提到的不管是多語言挑戰、多云多產品的挑戰、重復造輪子等問題,都能很好的解決。

11.3互通網關 :混合架構的基石
云上云下互通,或者說多個云賬戶VPC之間的互通,我們常見的有兩種方案:
- 1)其一是VPC直接打通,讓多個VPC之間形成一個大的局域網,RealServer實現點對點互通;
- 2)其一是中間搭建一個負載均衡器,通過暴露EIP實現互通。
兩個方案都有自己的優缺點。
對于方案一:打通的VPC涉及到IP規劃,如果前期沒有合理規劃,后續很難打通;還有這種方案有水桶短板安全問題,一旦一個VPC被攻破,這張網也被攻破;但是對于內部的應用來說架構就比較簡單,可以僅僅借助K8s DNS service就能做到服務發現。
對于方案二:最大的缺點就是中間有一個集中式的負載均衡,需要申請獨立的LB才可訪問;但是這種方案隔離性好。
對于釘釘單元化來說,涉及N個業務方,N * M個應用,對應X個VPC,要想VPC之間打通,幾乎沒有可能性,而且VPC打通,還面臨應用之間的安全性問題。要實現Geo之間互通,環境之間的隔離性是基本要求,與此同時,我們也要考慮到系統的可擴展性,所以我們必須要構建一套獨立的流量網關,實現流量加密、尋址、轉發等通用能力。
釘釘互通網關是構建在Envoy之上的系統,雙向Ingress和Egress,支持GRPC和釘釘自研協議。具備流量管理、傳輸加密、單元尋址等能力。釘釘單元化借助互通網關的能力,再配合全局流控系統,我們可以在多單元之間實現精確的流量控制和調度。
12、寫在最后
伴隨著專屬集群的持續輸出,客戶對專屬的場景需求會越來越多,需要我們投入更多的人力持續的建設。
比如:
- 1)在架構側:首先是Sidecar持續強化,支持更多的中間件和環境,提供不同維度的安全能力,滿足客戶和應用的安全需求;
- 2)在運維側:我們需要構建多Geo管理能力,完善Geo和單元之間流量快速調度能力,提供自動化的自檢系統等;
- 3)在交付側:如果實現快速交付,比如是否能做到新應用一周完成單元化改造,新Geo一天部署完成。這些挑戰都是接下來我們要重點投入的方向。
對于標準釘釘來說,這個是我們的基本盤,一個穩定可靠且低成本的釘釘是我們持之以恒的目標,接下來我們會加大云上流量的占比,充分的借助云上彈性能力,實現可控的成本。
今天我們只是站在釘釘的角度上拋了一個“磚”,希望在異地多活這個領域激起一層浪花,歡迎大家一起討論。
13、相關資料
[1] 現代IM系統中聊天消息的同步和存儲方案探討
[2] 企業級IM王者——釘釘在后端架構上的過人之處
[3] 深度解密釘釘即時消息服務DTIM的技術設計
[4] 釘釘——基于IM技術的新一代企業OA平臺的技術挑戰(視頻+PPT)
[5] 企業微信的IM架構設計揭秘:消息模型、萬人群、已讀回執、消息撤回等
[6] IM系統的MQ消息中間件選型:Kafka還是RabbitMQ?
[7] 深度揭密RocketMQ在釘釘IM系統中的應用實踐
(本文同步發布于:http://www.52im.net/thread-4122-1-1.html)