Slack 架構設計

一、發展歷史

2017 年, Slack 的 CTO 卡爾 · 亨德森 (Cal Henderson ),暢銷書《構建可擴展的網站》的作者, 在接受 lifehacker 採訪時, 提到爲什麼要做 slack。Cal 從小癡迷於遊戲, 2009 年和 Stewart Butterfield,Eric Costello 和 Serguei Mourachov 一起離開雅虎 Flickr 公司, 創建了一個名爲 Glitch 的大型多人在線遊戲。該遊戲的開發者分散在三個不同的城市中, 爲了方便協作,團隊開發了一個 IM 工具,這就是 Slack 的原型。之後他們把這個工具分享給更多的朋友使用, 同時也意識到, 做 Slack 可能比做遊戲更有價值。

Cal 在啓動 Slack 後很快就認知到,無論一個組織的規模有多大,工作實際上都是由小團隊完成的——你每天與之交流的幾十個人。沒有人真正在工作中與 5000 人交流!人們能夠輕鬆地與核心團隊溝通是至關重要的,這也是 Cal 認爲找到 Slack 的最佳定位。

和 Cal 一同創業的 Stewart Butterfield 則成爲 Slack 的 CEO, 圖片分享服務網站 Flickr 也是他創建的。這是 Stewart 第二次將一個失敗的遊戲項目轉變爲一個成功的科技產品。2002 年,他的視頻遊戲工作室 Ludicorp 開始開發遊戲 “永無止境”,但從未推出。該遊戲的一些功能被用來創建 Flickr,他在 2004 年以 2500 萬美元的價格賣給了雅虎。2012 年,Stewart 關閉 Glitch,全心投入 Slack 開發工作。在開發時,這個軟件被稱之爲 Linefeed,Stewart 在上線時將它改名爲 Slack, 它代表 “Searchable Log of All Conversation and Knowledge”。

經過一年的私下測試,Slack 於 2014 年 2 月公開發布。市場立刻就接受了這一款軟件,第一天收到 8000 多個請求,第二週收到 15000 多個。Slack 不得不錯開發布時間,因爲它需要增加了更多的服務器容量來滿足需求。越來越多的組織開始使用 Slack,其中包括許多媒體組織,他們給出相當證明的評價, 口口相傳的結果是,Slack 以每週 5%~10% 的速度快速增長。2014 年底, Slack 被公認爲領域獨角獸,2015 年估值 28 億美元,翻了 3 倍。

Slack 最初的幾年是由該應用的用戶體驗引領的,與當時使用的另外兩個著名的在線聊天工具 Hipchat 或 Campfire 相比,它更容易、更現代。2016 年,Slack 推出了一系列新功能,包括應用程序和機器人生態系統,進一步鞏固其地位。機器人的使用使得經理們更容易將他們的大部分業務轉移到 Slack。經理可以跟蹤員工休假時間,發送調查,接收和轉發電子郵件,並通過該應用程序與客戶交談。業務工具,如谷歌驅動、GitHub、Asana、Zapier 和 Salesforce,也都集成到 Slack 中。一些人在 Slack 中構建了一個應用程序,讓用戶留在平臺上,而另一些人只是通知用戶文件的任何更改或更新。Slack 應用程序目錄上有 2000 多個應用程序和 750 個機器人。

然而 Slack 的好運氣並沒持續太久,2017 年,微軟推出了競爭產品 Teams。在 Office 365 平臺助力下,很快得就取得了對 Slack 競爭優勢。在此期間, Slack 的 DAU 從 600 萬增長到 2020 年的 1200 萬,但 Teams 更是領先一步,DAU 在 2020 年達到了 7500 萬。

2019 年 6 月,Slack 通過直接公開發行上市,市值達到 195 億美元。2020 年 12 月,Slack 被 Salesforce 以 277 億美元收購

以下爲 Slack 的一些統計數據:

和微信、WhatsApp 不同, Slack 是面向中小企業的協作工具, 可以說它是釘釘、飛書等國內同類企業軟件等鼻祖。潘亂在《飛書的前世今生》一文中也提到,飛書也從 Slack 中參考不少設計要點。而微軟在開發 Teams 的時候,則毫不掩飾地複製了 Slack 的大量功能。

二、產品特性

Slack 提供許多 IRC 風格的功能,包括按主題、私人羣組和直聊組織的持久聊天室(頻道)。包括文件、對話和人在內的所有內容都可以在 Slack 中搜索。用戶可以在他們的消息中添加表情符號按鈕,其他用戶可以點擊按鈕來表達他們對消息的反應。

Slack 的免費計劃限制用戶只能查看和搜索最近的 10,000 條消息。

  1. 團隊:社區、組或團隊可以通過 Slack 的團隊管理員或所有者發送的特定 URL 或邀請加入 “工作區 Workspace”。儘管 Slack 是面向組織的溝通而開發的,但它已被用作社區平臺,取代了留言板或社交媒體組。

  2. 消息傳遞:

  3. 公共頻道允許團隊成員在不使用電子郵件或羣發短信的情況下進行交流。公共頻道向工作區的每個人開放。實際上, Slack 目標之一就是替代電子郵件。

  4. 私有頻道 允許較小的子組之間的私人對話。這些私人渠道可以用來組織大型團隊。

  5. 私信(Direct messages)允許用戶向特定用戶而不是一組人發送私人消息。私信最多可以發給九個人。私信組也可以轉換成私有頻道。

  6. 應用集成:這是 Slack 的特性。Slack 可以集成了許多第三方服務,支持面向社區的集成,包括 Google Drive, Trello, Dropbox, Box, Heroku, IBM Bluemix, Crashlytics, GitHub, Runscope, Zendeskand Zapier。2015 年 12 月,Slack 推出了他們的軟件應用程序(“應用程序”)目錄,由用戶可以安裝 150 多個集成應用。2018 年 3 月,Slack 宣佈與財務和人力資本管理公司 Workday 建立合作伙伴關係。這種集成允許 Workday 客戶直接從 Slack 界面訪問 Workday 功能。

  7. API:Slack 爲用戶提供了一個 API 用於創建應用程序和自動化流程,例如根據人類輸入發送自動通知,在指定條件下發送警報,以及自動創建內部支持票證。Slack 的 API 的兼容性設計不錯,可以與各種類型的應用程序、框架和服務的兼容性。

三、技術架構

3.1 基本架構

Slack 實現了客戶端 - 服務器架構,其中客戶端(移動端、桌面端、Web 端和應用端)與兩個後端系統對話:

  1. WebApp 服務器,處理 HTTP 請求 / 響應週期,並與主數據庫和作業隊列等其他系統通信

  2. 實時消息服務器,它向客戶端發送消息、配置文件更改、用戶狀態更新和一系列其他事件

在頻道中發佈的新消息通過 API 調用發送到 webapp 服務器,在那裏它被髮送到消息服務器並保存在數據庫中。實時消息服務器接收這些新消息,遍歷頻道中的人員,通過 Web Socket 將其發送到連接的客戶端。

Slack 概要架構

3.2 技術棧

  1. 對於 Web 端,使用帶有 ReactJS 的 Javascript 和 ES6 作爲前端語言。ReactJS 是 Facebook 開發的流行 Javascript 框架之一。

  2. 桌面端,使用 Electron 以及 HTML、CSS、Javascript 以及 Chromium。Electron 是跨平臺的, Slack 桌面端支持 Windows、Mac、Linux。

  3. Android 端使用 Java 和 Kotlin。Kotlin 比 Java 更靈活,有助於構建高性能的應用程序。

  4. IOS 端使用 Objective C 和 Swift

  5. Slack 使用 PHP/HacklangJava 作爲後端編程語言。PHP/Hacklang 是 Facebook 開發的 HipHop 虛擬機(HHVM)的編程語言,它支持原生 PHP 不支持的動態類型和靜態類型。它類似於 Javascript 中的 TypeScript 支持。

  6. 在 Slack 中,最初,PHP 5 被用作後端,後來在 2016 年切換到 HHVM,這有助於更快地運行 PHP 代碼。Hack 類似 PHP 的超集,在 PHP 基礎上做了很多改進。

  7. 早期 Slack 使用 MysQL 做線上配置。之後,考慮到性能和縮放問題,MySQL 上引入了分片架構。後來爲了支持伸縮,採用 Vitess 數據庫, 2017 年 Slack 開始遷移到 Vitness,到現在已經全部遷移完成。Vitess 是一個數據庫集羣系統,用於水平擴容、部署和管理開源數據庫實例的大型集羣。Vitess 與 MySQL 完美配合。高可用性、可擴展性、可操作性、可擴展性和性能對於 Slack 至關重要,所以 Vitess 很適合它。今天,Slack 在世界各地的不同地理區域運行多個 Vitess 集羣。

  8. Slack 使用 Memcached,MCRouter 作爲緩存。

  9. Flannel 用於應用程序級邊緣緩存。Flannel 用於在加載 Slack、切換頻道和重新連接到 Slack 時減少連接時間。它是在應用程序級別緩存應用程序的服務。Flannel 在客戶端啓動時緩存用戶、頻道、bots 等的相關數據。然後,它按需向客戶端提供查詢 API,以快速提供結果。

  10. 對於搜索,Slack 使用 Solr。Solr 用於 Slack 中的全文搜索。Solr 在後臺使用了 LuceneJava 搜索庫。

  11. 對於實時消息傳遞,使用 Websocket。通過 Web API 提供歷史信息,通過 WebSocket 提供實時數據,以便人們獲得團隊中正在發生的事情的最新信息。

  12. 用戶和頻道首先將通過 Cloud Font 和 AWS ELB 從 Web API 中接收關於團隊的信息,並連接到 Web Socket 服務以接收最新信息。

  13. Slack 使用 Consul 來發現和配置服務。Consul 可以維護可靠和安全的連接,每個服務在其網絡中的位置的集中註冊表,即使引入或刪除新的服務節點,也可以降低從一個網絡應用或服務移動到另一個應用或服務的複雜性。當 Consul 與 HAProxy 結合使用時,負載均衡器配置就可以實現自動化。

  14. 服務器配置和管理的工具:

  15. Terraform 是一個開源的基礎架構代碼(IAC)工具,把基礎架構視爲代碼,由此實現對基礎架構全生命週期的安全、高效的管理。

  16. Chef 是一個開源雲架構體系自動化平臺,可以在任何環境(本地(私有)託管、虛擬託管或雲託管)和多個平臺(如 Windows、Ubuntu、Solaris 等)中輕鬆設置、配置、部署、測試和管理服務器。藉助它可以通過編碼來管理架構體系,而不是繁瑣的人工管理。

  17. Kubernetes 是一種虛擬機替代方案,可以自動管理資源,輕鬆實現對應用程序的闊渣。它允許開發人員與 IT 操作共享依賴關係和軟件,從而實現更快的代碼操作和交付。

  18. Kafka 和 Redis 用於異步任務排隊。

  19. Presto、Hive、Spark 是爲數不多的用於數據倉庫的工具,Presto 是專爲交互式查詢設計的分佈式 SQL 引擎。這是一個快速的方法來回答特別的問題和探索較小的數據集等。

四、核心模塊設計

4.1 工作區 Workspace 設計

從 2014 年 Slack 推出開始,這個核心系統架構就圍繞着 “工作空間” 的概念來設計。從邏輯上講,工作區包含了系統中的所有其他對象,包括用戶、頻道、消息、文件、表情符號、應用程序等。它們還提供了實現訪問控制的管理和安全邊界,以及策略和可見性首選項。

以工作空間爲邊界,可以建立分片系統來分散負載,使得服務的性能提升就變得非常方便。實現上, 當一個工作空間創建之後, 它就被指定到某個特定的數據庫分片、消息服務器分片和搜索服務分片上。這種設計支持 Slack 可以很容易的通過增加更多的服務器來實現水平擴展, 承載更多的工作空間。這樣在應用中如果需要訪問某個內容,則首先要找到這個內容所在的工作空間,之後通過工作空間上下文來訪問特定的數據庫或者服務器分片。本質上, 工作空間是多租戶服務中的租戶單元,用來實現對數據的分片。

這種設計非常簡單有效:要查找數據或發送消息,我們的代碼只需要查找對應的工作空間所在分片,並將請求路由到那裏,以驗證請求用戶是否可以訪問給定的頻道。多年來,越來越多的應用程序代碼和服務圍繞着數據活在在工作空間中的核心假設進行開發,進一步鞏固了這種封閉邊界的設定。

4.2 共享頻道:跨組織的溝通設計

共享通道是連接兩個獨立組織的通道。不再需要在外部電子郵件和內部 Slack 渠道之間來回穿梭,也不需要向 Slack 工作區提供無窮無盡的外部聯繫人:共享頻道爲兩家公司的人員創建了一個高效的溝通空間。然而,共享頻道的設計挑戰了 Slack 的基本假設,即工作空間是劃分客戶數據的原子單元。因爲共享頻道使用戶能夠_跨工作空間邊界_訪問一些頻道、消息和文件。這需要對 Slack 的基本權限、可見性和數據分片功能進行改造,其中有不少設計挑戰和權衡。

共享頻道要解決的主要問題是消息應該如何在工作區之間流動。如何分發和存儲消息,以便兩個工作區的成員可以加入頻道、發送消息並正常使用 Slack。

如果繼續假設用戶在 Slack 上與之交互的所有內容都要放在用戶的工作區,這意味着兩個工作區在各自的數據庫分片中都有共享頻道的副本,消息將被寫入發送用戶的分片和接收用戶的分片上。這樣的優點在於底層的邏輯保持不變,但是缺點也很明顯。考慮到 Slack 每週發送超過 10 億條消息,爲兩個工作區複製數據將限制共享頻道的擴展能力。這不僅僅侷限於消息——它還包括 pin、reactor 和其他特定於頻道的信息。我們還希望寫入數據儘可能實時和一致,但是寫入多個數據庫和多個實時消息服務器可能會導致寫入時間不一致,從而導致頻道數據不一致。

Slack 採用了單副本的方案。引入了新的 shared_channels 數據庫,用於橋接共享頻道中的不同的工作空間。

Slack 的所有頻道,包括共享和非共享的, 都記錄在對應的 workspace 工作區分片上 channels 表中。在這個案例中,如果 Ben 發起了共享頻道,則會在 Ben 所在的 workspace 工作區分片上 channels 表中記錄這個頻道信息,同時,在這個工作區分片上的 shared_channels 中記錄共享頻道的擴展信息,包括:

  1. 頻道 ID,即對應 channels 表的外鍵

  2. 工作區 ID,當前工作區 ID,用於分片路由。

  3. 源和目標工作區 ID,即發起共享和加入共享的人所在的工作區。

  4. 頻道名稱,主題和隱私,這三個字段意味着不同工作空間的人可以爲頻道設置不同的名稱和主題。

  5. 另外和這個頻道相關的其他信息,如消息、reactions、pins 等,也都會記錄到發起人 Ben 的這個工作空間中。

對於加入頻道的人,比如 Jerry,在其工作空間中的 channels 和 shared_channels 表也分別有一條共享頻道記錄,不一樣的是當前頻道是共享頻道的目標頻道。這樣通過 shared_channels 表的源工作空間 id 和源頻道 id 就可以找到源頻道的信息。

這種設計很好的解決隱私和性能上的問題。

4.3 角色和權限體系設計

類似 Slack 這樣系統的角色和權限體系設計並不容易,挑戰在於如何平衡易用性和權限管理的精細度。如 對頻道管理員授權時,如何避免管理員執行超過預期的範圍和操作,避免其查看儀表盤等和管理無關的事情。當前 Slack 中內置的角色包括:

  1. 訪客:使用 Slack 的能力受到限制,並且只允許查看一個或多個授權的頻道。

  2. 會員:這是基本類型的用戶,不具有任何特定的管理功能,但對本單位的 Slack 工作區具有基本訪問權限。如需管理功能,需要找管理員或者所有者來處理。

  3. 管理員:Slack 內組織的基本管理員, 能夠在 Slack 內部進行各種管理變更,如重命名頻道、對頻道做存檔、設置首選項和各種策略、邀請新用戶、安裝應用程序等。管理員能夠執行團隊內的絕大部分管理任務。

  4. 業主(所有者):能夠執行包括管理員在那的各種管理工作, 此外還能執行合規性的功能, 比如設置數據丟失預防(DLP)。

  5. 主業主(所有者, Primary Owner):該組織的首席管理員,能夠執行任何管理操作。

這種設計導致管理員的權限過大, 需要一個細粒度的角色系統來分解管理員的核心能力。Slack 採用基於角色的訪問控制系統, 這樣用戶可以被授予一個或多個角色,這些角色被授予與這些角色相關的權限。Slack 需要能夠在組織級別(對於我們的企業網格客戶層)或工作區級別來設置這些角色。

當用戶執行操作時,先檢查該操作所需的權限。如果用戶已通過其分配的角色委託了這些權限,則允許他們執行該操作。如果他們沒有顯式地擁有這些權限,將返回到預定義的角色來確定他們是否有能力執行該操作。在客戶端,默認都會有一個無權限的用戶可以看到的界面。如果由於 Flannel 的緩存導致用戶權限更新不及時,則會默認顯示這個界面。

角色權限管理是一個獨立的模塊,使用 Go 語言開發, 和 Slack 的 WebApp 是分離的, 並通過 gRPC 來調用。新的架構中增加了三個角色:

  1. 頻道管理:這種類型的用戶有權存檔頻道、重命名頻道、創建私人頻道以及將公共頻道轉換爲私人頻道。

  2. 用戶管理:這種類型的用戶能夠從工作區中添加和刪除用戶,以及查看組織的用戶組。

  3. 角色管理:這種類型的用戶能夠管理角色,並將用戶賦予其關聯的角色。

角色信息保存在 Vitess 數據庫中, 通過 user_id 來分片,這樣就可以根據用戶 id 來高效查詢。

舉個例子,在客戶端,如果 Bob 是頻道管理員,並希望對頻道歸檔。爲了確認是否可以執行這個操作, 客戶端首先從服務器端獲取基本的權限設置, 並保存得 redux 中並緩存一段時間。當管理員 Bob 重新分配一個角色時,客戶端會收到一個實時消息,其中包含相關的其他權限。系統將顯示一個更新操作,並將所有的 UI 組件做對應的更新。

4.4 Vitess:存儲架構設計

Slack 一開始就使用 MySQL 作爲數據存儲引擎,採用雙活架構。2017 年開始遷移到 Vitess,到 2020 年底已經完成了所有遷移工作。這裏重點分析這個架構調整的設計考慮因素和技術挑戰。

數據存儲層的可用性、性能和可伸縮性對於 Slack 至關重要。舉個例子,在 Slack 中發送的每條消息在通過實時 WebSocket 發送並顯示給頻道的其他成員之前都是需要持久化。這意味着存儲訪問需要_非常_快速和_非常_可靠。在 2020 年底,Slack 在高峯期的 QPS 是 230 萬,其中 200 萬讀, 30 萬寫。查詢延遲中位數是 2ms,P99 查詢延遲是 11ms。這是採用 Vitess 之後的數據。

早期 Slack 採用 LAMP 技術棧,所有數據存儲在三個 MySQL 主集羣上:

  1. 分片:存儲 Slack 所有業務數據,如消息、頻道和 DMs,數據按照工作區 ID 做分區。

  2. 元數據集羣:元數據集羣用作查找表,將工作區 id 映射到基礎分片 id。這意味着要找到工作區中特定 Slack 域的分片,我們必須首先在這個元數據集羣中查找記錄。

  3. 下水道集羣:這個集羣存儲了所有其他沒有綁定到特定工作空間的數據,但這仍然是重要的 Slack 功能。一些示例包括第三方應用目錄。任何沒有與工作區 ID 關聯的記錄的表都將進入此羣集。

分片是在 Slack 的單體應用 webapp 來管理和控制, 其中包括在特定工作空間中檢索元數據,之後創建到底層數據庫分片鏈接的邏輯。這樣從數據集分佈的視角來看, 這是一個工作空間分片模型, 每個數據庫分片包含數千個工作區及其所有數據,包括消息和通道。從架構體系的角度來看,所有這些集羣都是由一個或多個分片組成的,其中每個分片都配置了位於不同數據中心的至少兩個 MySQL 實例,並使用異步複製相互備份。下圖顯示了原始數據庫體系結構的概述。

這種主動 - 主動配置有許多優點,很容易實現服務的升級:

隨着越來越多用戶和公司使用 Slack,越來越多的產品團隊來參與開發 Slack 功能,此時這個方案的缺點就顯現出來了:

到 2016 年秋天,生產環境的 MySQL 的 QPS 達到數十萬,MySQL 分片達到數千個。應用程序性能團隊經常遇到縮放和性能問題,需要爲工作空間分片架構的侷限性設計新的架構。現在問題是: 在原架構上演進,還是另起爐竈?Slack 團隊這裏做出了一個教科書版的選擇:

  1. 將消息數據的分片依據從工作區 ID 調整爲 頻道 ID, 這樣可以更均勻分散負載。

  2. MySQL 的使用應該繼續,應用中已經大量使用了 MySQL 特定的查詢;團隊在 MySQL 的部署、數據持久性、備份、數據倉庫 ETL、合規性等操作方面有大量的經驗和積累。

這樣, 首先排除了類似 DynamoDB 或 Cassandra 這樣的非關係型數據庫數據存儲,以及像 Spanner 或 CockroachDB 這樣的 NewSQL。這一點是和 WhatsApp 不一樣的地方。後者使用 Cassandra,而且是很早就開始用了。對 Slack 來說,這個調整已經來不及了。基於上述因素,Vitess 的優勢就顯露出來了。Vitess 的核心是提供了一個數據庫集羣系統,用於 MySQL 的水平擴容,它滿足團隊上述所有需求:

這樣自 2017 年開始,三年時間, 基本完成了 MySQL 流量到 Vitess 的遷移。Slack 在世界各地不同的地理區域運行多個 Vitess 集羣,擁有數十個 keyspaces。單體 webapp 以及其他的服務都遷移到 Vitess 上。每個 kyespace 都是一個邏輯數據集合,並按照某種因素來伸縮:用戶數、團隊或者頻道,而不是僅僅按照工作空間來分片。熱點問題也隨之得到解決。非常幸運, Slack 在新冠流行之前引入了 Vitess,2019 年冠狀病毒病襲擊美國,Slack 的使用量空前增加。在數據存儲方面,僅在一週內,查詢率提高了 50%。Vitess 非常好的應對了這一次使用量的井噴 。Vitess 部署簡要架構如下:

Vitess 運行時架構

4.5 Flannel:邊緣緩存設計


Slack 遇到的第一個挑戰是連接到 Slack 的速度很慢。當用戶加入越來越多、越來越大的團隊時,連接到 Slack 的速度變得很慢。我們可以分析 Slack 啓動流程來發現瓶頸所在:

  1. 客戶端向服務器發送一個 HTTP 請求。

  2. 服務器驗證發送過來的 token,回覆用戶所在的羣組的快照。

  3. 建立一個 WebSocket 連接。在該連接上,實時事件被髮送到客戶端, 羣組信息就被更新到最新狀態。

WebSocket 連接是基於 TCP 的雙層通信協議。用戶連接時間是從發送第一個 HTTP 請求到 WebSocket 連接建立爲止。這時候客戶端就準備就緒了。這個流程的瓶頸在第二個步驟, 即發送羣組快照。

羣組快照包含如下內容:

  1. 團隊成員;

  2. 用戶所在的頻道列表;

  3. 頻道中的用戶。

一旦涉及到有幾千個用戶或者頻道, 這些信息動不動就增長到幾十兆字節。爲什麼客戶端在啓動時需要這些用戶和頻道信息?這是早期做出的架構決策, 會讓後面的功能實現更方便,用戶體驗也會更好。從當前 Slack 用戶數量來看, 這個設計就不太合適了。Slack 做了一些迭代來試圖壓縮快照的大小,比如從初始有效負載中刪除一些字段,或者改變字段的格式以節省空間。不管怎麼做, 快照的大小就無法減少, 所有有必要從架構上做優化。

實現策略也很簡單, 就是引入懶加載方案:減少啓動時加載的數據量;推遲到按需加載。這意味着客戶端需要重寫他們的數據訪問層。它不能假設所有數據都在本地可用。

當遠程獲取數據時,會有網絡往返時間消耗。所以 Slack 決定建立一個查詢服務, 支持緩存, 就近部署,由此來保證數據訪問速度。這個服務被稱爲 Flannel 服務。Slack 分兩階段實現 Flannel 服務。

爲什麼叫 Flannel?根據 Slack 工程師介紹,在項目啓動的那天,首席工程師碰巧穿了一件法蘭絨襯衫。這就是原因_。_

第一階段,中間人策略。將 Flannel 放在 WebSocket 連接上,這樣 Flannel 也可以接收到所有事件並轉發給客戶端, 將其中一些事件用來更新其緩存。客戶端可以向向 Flannel 發送查詢請求。Flannel 根據緩存中的數據回覆這些查詢。緩存按團隊來組織。當團隊中的第一個用戶連接時,Flannel 將團隊數據加載到其緩存中。只要團隊中有一個用戶保持連接,Flannel 將保持緩存爲最新狀態。當最後一個用戶斷開連接時,Flannel 將卸載緩存。這種設計的優點在於它不需要修改後端系統的其他部分。在這個階段,Slack 還推出了一個即時標記功能。這是對客戶端的優化。Flannel 預測客戶端下一步可能查詢的對象,並主動將對象推送給客戶端。

舉個例子, 假設團隊中的用戶 Alice 發送了一條消息。消息被廣播到所有通道成員。每個客戶端都需要用戶 Alice 的信息來呈現消息。Flannel 維護一個 ARU 緩存來跟蹤每個客戶端最近查詢的對象是什麼。它檢測客戶端 A 最近沒有查詢 Alice。所以它可能沒有數據。所以它將帶有消息的用戶 Alice 對象推送到客戶端。對於客戶端 B 和 C,他們最近查詢了用戶 Alice,所以 Flannel 只是發送了一條消息。經過改進,用戶連接流程的瓶頸被消除了。最大團隊的用戶連接時間下降到 1/10。Flannel 緩存處於進程內存中。Flannel 被部署到多個敏捷的位置。它離用戶很近,所以接近提供了更快的數據訪問。客戶端使用基於地理的 DNS 來確定他們應該連接到哪個區域。

Flannel 是以團隊親和來組織的, 這意味着同一個團隊中的所有用戶都連接到同一個 Flannel 主機。Flannel 前端設置了一個代理, 它使用一致的哈希根據團隊 ID 來路由流量。在第一階段實現完成之後,有幾個需要改進的地方。

  1. 首先,作爲一箇中間人,Flannel 留在每一個 WebSocket 連接上。對於許多事件,它們被廣播給團隊的所有成員或頻道中的所有成員,所以有很多重複的事件。讀取和處理此消息需要花費大量的 CPU。

  2. 其次,緩存更新與 WebSocket 連接綁定。當第一個用戶到來時,Flannel 加載緩存。當最後一個用戶離開時,它卸載緩存。這意味着對於團隊中的第一個用戶來說,它總是會命中代碼緩存。對於團隊中的第一個用戶來說,這不是一個理想的體驗。

第二階段改進重點解決上述問題,核心是將 Pub/Sub 引入系統。引入實時消息 API, Flannel 可以訂閱團隊和頻道列表的用戶連接事件。這種設計帶來的另一個好處是緩存更新不再與 WebSocket 連接綁定。Flannel 甚至可以在第一個用戶上線之前預熱緩存。在 Pub/Sub 之前,Flannel 需要處理的事件達到了每 10 秒 50 萬次的峯值。在 Pub/Sub 之後,它只達到了 1000 次。減少了 500 倍。頻道成員資格查詢接口的 P99 延遲在遷移到 Flannel 後從 2000 毫秒下降到 200 毫秒。

這個實現有兩個架構上的問題:

  1. 爲什麼不使用 Memache 來做緩存?原因是 Flannel 需要服務於自動完成查詢,所以 Flannel 將索引保存在內存中,並且索引並不真正適合 Memache 的鍵值模式。

  2. Pub/Sub 選型。Slack 使用自研的 Pub/Sub 系統, 很早就建立起來的,穩定,性能不錯。更換成 Kafka 的 ROI 不高。

4.6 EnvoyProxy:負載均衡設計


2020 年,Hollywood 的前一天, 在 2 分鐘內, 由於 Flannel 的 Bug, Slack 突然掉了 160 萬 WebSocket 鏈接。一些 Flannel 主機崩潰了。重新連接的流量流向了健康的主機。它們超載,崩潰,然後故障蔓延到整個集羣。團隊花了 135 分鐘纔將流量恢復到正常水平。這個故障暴露了幾個問題。

首先發生故障的是負載均衡器 EOB。它是託管在 AWS 上的網絡層的前端。重新連接的流量淹沒了 EOB 集羣。整個集羣變得無法訪問之後, 團隊花了 45 分鐘時間來擴展另一個 EOB 集羣,並將流量指向它。同時做了嚴格的限流措施。團隊設置了幾種限流配置文件,但大家對限流參數如何設置卻拿不準,只能邊測試邊驗證。這在事故發生時,要調整一組合適參數就更困難了。這也是導致事故恢復時間變長的一個原因。

Flannel 引入了熔斷和限流機制來避免被打垮。Flannel 監控內存使用, 一旦超過閾值,就拒絕流量。直到內存下降到正常水平。另一個方法是使用斷路器。當 Flannel 檢測到這些上游服務的故障率開始上升時,它就開始拒絕流量。它使用反饋迴路來控制它向後端服務發送多少請求。所以這給了後端服務一個恢復的機會。

解決鏈接風暴的關鍵在於如何避免流量被一下子全部被負載均衡服務切斷。Slack 一開始是使用 HAProxy 來做負載均衡,但 HAProxy 對熱重啓的支持是有問題的。

在 Slack,更改後端服務接入點列表是常見事件(由於實例被添加或循環刪除)。HAProxy 提供了兩種方法來支持接入點變更後的配置參數修改:

  1. 使用 HAProxy 運行時 API,這種方式會導致鏈接雪崩,在 Slack 的可怕、可怕、不好、非常糟糕的一天一文中有詳細描述。

  2. 在服務接入點變更後,直接修改 HAProxy 的配置文件,重新加載 HAProxy。這個機制應用在 WebSocket 的負載均衡上。

HAProxy 在重新加載後,新建的鏈接會使用新的配置來加載;老的鏈接線程還不能中斷,繼續保持運行,這樣 WebSocket 的長鏈接不被中斷。這樣會導致有大量的長鏈接仍然使用老的配置信息。爲了解決這個問題, Slack 選擇了 Envoy Proxy。Envoy 支持是以哦能夠動態配置的集羣和連接點。如果連接點列表發生變更,無需重新加載。如果代碼或者配置發生變更, Envoy 可以熱重啓,並不會丟棄任何鏈接。Envoy 使用 iNotify 監視配置文件的變更。在熱重啓時,Envoy 會從父進程中複製統計信息到子進程,這樣各種計數器也都不會被重置。這一切都大大減少了使用特使的操作開銷,並且不需要額外的服務來管理配置更改或重啓。

Envo 提供了幾個高級負載平衡功能,例如:

爲此,在 2019 年,Slack 的入口負載均衡就從 HAProxy 逐步遷移到 EnvoyProxy。

4.7 Solr:搜索設計

平均來說,一個知識工作者一天的 20% 時間都花在尋找完成工作所需的信息上,也就是一週的工作有一整天的時間花在搜索上。Slack 的搜索、學習和智能團隊把工作重點放在提升搜索結果的質量上。建立了一個個性化相關性排序機制和一個名爲 “頭部_結果”_ 的搜索模塊,在一個視圖中顯示個性化的、最新的結果。

和百度、谷歌爲代表的 Web 搜索不同, Slack 關注於內部搜索。Slack 用戶可以訪問不同的文檔, 文檔內容還經常會變好。Web 搜索大量使用的數據聚合技術在 Slack 搜索中無法使用,但它有自己的特性:

  1. Slack 更瞭解用戶與 Slack 中其他用戶、頻道、消息和用戶界面元素的交互歷史。

  2. Slack 不需要處理垃圾郵件或遊戲搜索引擎優化。

  3. 雖然文本語料庫的總大小很大,但每個團隊的語料庫相對較小,因此允許在排名期間爲每條消息投入更多的計算資源。

  4. 不僅可以控制搜索界面,還控制目標文檔的呈現和結構

Slack 提供了兩種搜索策略:最近_和_相關。_最近_的搜索找到匹配所有關鍵詞的消息,並按相反的時間順序呈現它們。如果用戶試圖回憶剛剛發生的事情,最近搜索是一個有效的處理方式。

_相關_搜索放鬆了時間限制,並考慮了文檔的 Lucene 分數——它與查詢關鍵詞的匹配程度。使用約 17% 的時間,_相關_搜索表現略差於_最近搜索。這是按照如下方法度量出來的:_每次搜索的點擊次數和在搜索結果前幾個位置的點擊率。我們認識到,_相關_搜索可以受益於使用用戶與頻道及其他用戶的交互歷史——他們的 “工作地圖”

舉個例子,假設你正在搜索 “路線圖”。你很可能在尋找你團隊的路線圖。如果您的團隊成員在您經常閱讀和撰寫消息的渠道中共享了包含“路線圖” 一詞的文檔,則此搜索結果應該比另一個團隊的 2017 年路線圖更相關。通過將用戶的工作地圖合併到_相關_搜索中,搜索結果點擊頻率增加了 9%, 搜索結果第一條的點擊增加了 27%。

此外,文檔作者和搜索者之間的熟悉程度、頻道的參與度、消息本身的特性等,都可以有助於提升搜索結果的命中率。這樣在實現上, 對搜索處理就引入兩階段的方法:

  1. 利用 Solr 的自定義搜索排序功能, 提取一組消息,僅按照一些選定的特性來排序, 這樣便於 Solr 執行搜索。

  2. 在應用層,對這些搜索結果按照全部特性重新排序,並賦予適當的權重。

在訓練數據集上,搜索團隊使用 SparkML 的內置 SVM 算法訓練了一個模型,該模型推導出如下用於搜索排序的特性:

  1. 該消息的存續時間。

  2. Lucene 在該查詢中給出的分數。

  3. 搜索者對消息作者的_親和力_(即該用戶閱讀另一個用戶消息的傾向)

  4. 包含消息作者的搜索者 DM 頻道的優先級分數

  5. 消息出現的頻道的搜索者優先級分數

  6. 消息作者是否與搜索者相同

  7. 信息是否被 pin、標星或者有 emoji 反饋。

  8. 搜索者從消息出現的頻道點擊其他消息的傾向

  9. 消息內容的各個方面,例如字數計數、換行符的存在、表情符號和格式。

值得注意的是,除了 Lucene“匹配” 分數之外,目前還沒有在模型中引入消息本身的任何其他語義特徵。

根據搜索團隊發佈的結果來看, 相關搜索有明顯的提升。搜索點擊量增加了 9%;在至少獲取一次點擊的搜索中, 搜索結果第一位的點擊量增加了 27%。

4.8 本地化

目前 slack 支持 8 種語言,本地化一直都是不容易的事情。Slack 的本地化第一步是將代碼庫中的字符串進行本地化。雖然移動平臺爲本地化提供了清晰的狂減,並實現了代碼和字符串的分離, 但 Web 端和桌面端的代碼和字符串卻是混在一起的,嵌入到 HTML 模版和業務邏輯中。這也爲本地化實現增加了難度。Slack 本地化團隊採用的兩步走的策略:第一步是創建一個框架來表示使用的各種編程語言中的字符串。之後修改代碼中的字符串。這個過程被稱之爲 “包裝字符串”,每個字符串都被包裝在一個塊或者函數調用中。

Slack 選擇 ICUMessageFormat 語法來表示字符串。和 gettext 相比,其處理複雜字符串的能力,特別是在選擇和複數處理方面,更勝一籌。此外其對 JavaScript 和 PHP 的支持更靈活和健壯。開發團隊爲模版語言、 Handlebars 和 Smarty 創建了 ICU 的助手程序。但還有一個問題,現有的翻譯管理系統( Translation Management System ,TMS)基本都不支持 ICU,因而團隊需要自己實現 TMS 的大部分內置的翻譯解析和驗證功能。

常見的本地化處理有兩種方案:

  1. 類似 Android 的 strings.xml 的鍵值對,將代碼中涉及到的字符串替換爲索引鍵;

  2. 類似 iOS 的 NSLocalizedString 方案, 採用英文字符串作爲索引鍵;

Slack 採用第二種方案,其優勢是可讀性強,但也意味着英文字符串的任何改變都需要重新翻譯。

下面是我們的通知電子郵件模板中本地化之前的字符串示例:

這個 {t} 塊有兩個目的

  1. 在靜態分析中尋找它來提取字符串,上傳到的翻譯管理系統。

  2. 在運行時(或構建時),它散列字符串,查找其翻譯,並使用 ICUMessageFormat 庫來渲染它。

按照這個策略, Slack 需要在 2000 個文件中對大約 20,000 個字符串進行此操作,這是一項艱鉅的任務。最終的目標也很明確:Slack 應該在每種語言中擁有一致的聲音和高質量的翻譯,本地化應該融入每個團隊的工作流程,所有新功能都應該在發佈時翻譯。爲此 Slack 僱傭了一個全職翻譯團隊,他們爲每種語言編寫詞彙表和風格指南,並與承包商合作翻譯所有的單詞。此外,團隊還對 Slack 的代碼審覈工具 Linting 增強了驗證 ICU 每個源字符串的語法的正確性,並確保傳遞正確的參數。加上培訓和代碼審查,這些工具有助於避免開發過程中的本地化問題。

Slack 的網絡代碼庫每天更新並持續部署 100 多次。本地化團隊構建了額外的工具,以確保新功能發佈和副本更改不會導致用戶在其他翻譯體驗中看到英文字符串。

對於功能發佈,Slack 使用功能標誌系統,這允許我們儘早將新功能引入代碼,並對大多數有條件塊的用戶保持禁用。功能只能在開發環境中啓用,或者僅爲我們自己的 Slack 團隊啓用,或者向一定比例的團隊推出。本地化團隊通過開發自動化工具來標識每個字符串所在的條件語句集,以便確定該字符串是否對生產中的用戶可見。在前端遷移到 React 框架後, 這工作難度就更大了,需要對 PHP、Smarty、JavaScript、Handlebar 和 React/JSX 等語言進行識別。然而,帶來的收益也是巨大的。這個工具允許我們添加一個檢查,這樣在所有字符串都被翻譯之前,該功能就不能開放給用戶。該工具還帶有一個儀表板,顯示與功能相關的所有字符串及其翻譯狀態。以下是本地化團隊總結的一些經驗總結:

Slack 架構一直在演進中。如上是從現有資料中分析的 WhatsApp 的架構, 僅代表現階段的 Slack 的架構設計。對本文有興趣的同學,可以評論下留言,歡迎交流。

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