每個人的電腦上都有一個被數百個進程使用的文件系統。但是,如果要讓成千上萬的用戶同時操作數億個文件,這些文件又包含了 PB 級的數據,那就困難了。一個是磁盤空間不足,一個是無法進行彈性擴展,一個是如果運行一些 IO 密集型的進程或者與其他多個用戶協作就會出現瓶頸。在這篇文章裏,我們把場景轉到一個有數百萬個付費用戶的雲原生文件系統上,你將看到我們如何使出渾身解數來擴展這個系統,讓它支持持續增長的用戶以及 SLA 需求,同時還能提供類似本地文件系統那樣的體驗。
Egnyte 是一個安全的內容協作和數據治理平臺,創立於 2007 年。當時,谷歌還沒有推出 Google Drive,AWS S3 的價格還非常昂貴。我們唯一能做的就是擼起袖子,開發了一個最基礎的雲文件系統。隨着時間的推移,S3 和 GCS 的成本降到了可接受的範圍,Egnyte 的存儲插件架構也漸漸成熟,我們的客戶可以選擇他們想要的存儲後端。在過去幾年中,爲了幫助客戶管理持續增長的數據,我們重新設計了很多核心組件。在這篇文章裏,我將分享我們當前的架構、在擴展系統過程中學到的東西以及一些在未來可改進的地方。
注:本文第一、二部分主要枚舉了 Egnyte 公司的技術選型方案,包括雲平臺、編程語言、數據庫、存儲、服務器、負載均衡、部署管理、開發環境等方面。第三部分是更爲詳細的採訪細節部分。
Egnyte Connect
Egnyte Connect 是一個內容協作和數據管理平臺,組件包括 CFS(Cloud File System,雲文件系統)、EOS(Egnyte Object Store)、內容安全、事件同步、搜索服務、基於用戶行爲的推薦服務。
Egnyte Connect 的 3 個數據中心負責處理來自全球數百萬個用戶的請求。爲了保證彈性、可靠性和持久性,這些數據中心通過安全的 Google Interconnect Network 連接到谷歌雲平臺。
Egnyte Connect 的服務網格將我們的數據中心與谷歌雲平臺的多個服務連通起來:
協作
- 文檔存儲
- 預覽
- 視頻轉碼
- 分享(鏈接和權限)
- 標籤
- 註解
- 任務
- 推薦
混合同步
-
本地數據處理
-
大文件或低帶寬
-
離線訪問
-
邊緣緩存
基礎設施優化
- 遷移到雲端
- 優化本地冷存儲成本
- 合併存儲庫
- Egnyte Connect 的架構會基於不同的級別來分片和緩存數據:
- 數據量
- 數據獨立性
- 併發讀
- 併發寫
Egnyte Connect 的技術棧
雲平臺
- 谷歌雲
- Azure
- 託管數據中心
編程語言
對象存儲
- Egnyte Object Store
- GCS
- AWS S3
- Azure
應用程序服務器
- Tomcat
數據庫
- MySQL
- Redis
- BigTable
- DataStore
- Elasticsearch
緩存
- Memcached
- Redis
- Nginx
負載均衡器和反向代理
- HAProxy
- Nginx
消息隊列
- 谷歌 PubSub
- RabbitMQ
- Scribe
- Redis
部署管理
- Puppet
- Docker
- Ansible
- Jenkins
- GitLab
- Kubernetes
分析
- New Relic
- OpenTSDB/bosun
- Grafana
- MixPanel
- Tableau
- BigQuery
其他
- ZooKeeper
- Nagios
- Apache FTP 服務器
- Kong
- ReactJS/Backbone/Marionette/JQuery/NPM/Nightwach
- Rsync
- PowerDNS
- Mashery
- 基於 REST API 的 SOA 架構
- 使用 Java 開發核心文件系統代碼
- 使用 Python 開發面向客戶端的代碼,包括微服務、遷移腳本、內部腳本
- 原生 Android 和 iOS App
- 原生桌面和服務器端託管的客戶端,可以混合同步訪問整個用戶空間
數據統計
- 3 個主區域,其中一個在歐洲,通過 Google Interconnect 連接到 GCP
- 500 多個 Tomcat 實例
- 500 多個存儲節點(Tomcat/Nginx 提供支持)
- 100 多個 MySQL 節點
- 100 多個 Elasticsearch 節點
- 50 多個文本抽取服務實例(可以自動伸縮)
- 100 多個 HAProxy 實例
- 其他類型的服務實例
- 數十 PB 數據,保存在我們自己的服務器以及 GCS、S3 和 Azure Blobstore 上
- 數 TB 被索引到 Elasticsearch 中的內容
- 數百萬個客戶端與雲端同步文件,用於離線訪問
- 數百萬個客戶端通過交互式方式訪問文件
開發環境
- 服務器安裝了 Ubuntu
- UI 團隊使用的是 Windows/Mac,他們連接到本地 Ubuntu 虛擬機或者共享的 QA 服務器進行 REST API 測試
- Eclipse/IDEA
- 構建環境在 AWS 上
- Maven
- Docker
- GitLab
- Jenkins
- Confluence
- JIRA
- 谷歌辦公套件
- Slack
以下摘錄了與技術和架構相關的問答
你們的系統是用來做什麼的?
Egnyte Connect 有數百萬用戶,他們把它作爲安全的內容平臺,用來管理他們的文檔。平臺提供了各種各樣的文件服務,並支持已有的雲端文件系統。用戶可以通過多種方式訪問平臺,比如 FTP、WebDAV、移動客戶端、公共 API、瀏覽器。平臺還提供了強大的審計和安全組件。
你們爲什麼要開發這個系統?
2007 年,我們的業務模式朝着分佈式方向發展,用戶通過多個設備訪問他們的文件,這就有必要爲用戶提供一種順暢的訪問體驗。於是,我們開發了 Egnyte Connect,一個分佈式文件系統,支持混合同步,滿足各種內容協作需求。隨着本地數據和雲端數據的碎片化以及 GDPR 的推出,我們開發了 Egnyte Protect,幫助用戶實現數據的合規和治理。
你們的系統有多大?
我們的系統保存着數十億個文件,數十 PB 的數據。Egnyte Connect 每秒鐘處理 1 萬個 API 請求,平均響應時間小於 60 毫秒。用戶可以從三個不同的區域訪問我們的系統。Egnyte Protect 提供了持續的內容監控能力,保證數據的合規和安全。
你們系統的架構是怎樣的?
我們採用了基於 REST 的 SOA 架構,可以獨立伸縮每一個服務,還可以將後端服務部署在雲端。所有服務都是無狀態的,它們使用了數據庫或者我們自己開發的對象存儲。
Egnyte Connect 服務概覽:
請求流程概覽:
搜索架構概覽:
你們在設計、架構和實現系統時遇到了哪些特別的挑戰?
較大的一些架構挑戰包括:
- 文件存儲的伸縮
- 元數據訪問的伸縮
- 與桌面客戶端實時同步
- 帶寬優化
- 故障隔離
- 緩存
- 發佈新特性
你們是如何解決這些問題的?
- 在存儲方面,我們開發了自己的存儲系統和插件架構,可以支持公共雲存儲,如 S3、GCS、Azure……
- 在元數據伸縮方面,我們改用 MySQL,並啓用了分片。
- 在實時同步方面,我們修改了同步算法,就像 Git 那樣,讓客戶端接收增量事件,並最終保持與雲端狀態一致。
- 在發佈新特性方面,我們開發了一個自定義配置服務,提供了功能開關。這樣就可以在睡眠模式下運行和收集數據,用戶可以自己啓用新功能,或者由某個 POD 或某個數據中心爲一羣用戶啓用新功能。
- 還有很重要的一點是監控。如果沒有監控數據,就無法進行優化。有時候,我們監控的東西太多,以至於不知道該把重點放在哪裏。所以,我們不得不轉移注意力,使用其他工具(比如 New Relic、bosun、ELK、OpenTSDB 和自定義報告)來檢測異常。
你們是如何演化系統來應對這些挑戰的?
- 第一個版本:用 Lucene 索引文件元數據,把文件保存在 DRBD 中,通過 NFS 掛載,然後在 Lucene 中搜索。問題:Lucene 的更新不是實時的,所以需要被替換掉。
- 第二個版本:用 Berkeley DB 保存文件元數據,把文件保存在 DRBD 中,通過 NFS 掛載,然後在 Lucene 中搜索。問題:達到 NFS 的限制,需要被替換成 HTTP。
- 第三個版本:用 Berkeley DB 保存文件元數據,把文件保存在 EOS 中,通過 HTTP 連接,然後在 Lucene 中搜索。問題:在大流量壓力下,啓用了分片的 Berkeley DB 仍然會出現瓶頸,而且一旦數據庫發生崩潰,需要幾個小時才能恢復,所以需要被替換掉。
- 第四個版本:用 MySQL 保存文件元數據,把文件保存在 EOS 中,通過 HTTP 連接,然後在 Lucene 中搜索。問題:公有云變得越來越便宜了。
- 第五個版本:用 MySQL 保存文件元數據,把文件保存在 EOS/GCS/S3/Azure 中,通過 HTTP 連接,然後在 Lucene 中搜索。問題:搜索遇到瓶頸,需要被替換掉。
- 第六個版本:用 MySQL 保存文件元數據,把文件保存在 EOS/GCS/S3/Azure 中,通過 HTTP 連接,然後在 Elasticsearch 中搜索。這是目前的架構。
- 第七個版本(未來):把所有的計算移到雲端,拆分出更多的服務,實現故障隔離,使用動態資源池更好地管理資源。
你們使用了哪些很酷的技術或者算法嗎?
-
對於服務間調用,我們使用了指數回退,實現了迴路斷路器,避免出現故障雪崩。
-
核心服務節點資源使用了公平共享的分配方式,接收到的請求被打上標籤並分組。每一組都有一定的容量,假設有一個客戶每秒鐘發送 1000 個請求,其他客戶每秒發送 10 個請求,系統可以保證其他客戶不會因爲這個客戶發送太多請求而“捱餓”。這裏的訣竅在於,當只有一個用戶在使用系統時,它可以開足馬力,隨着用戶越來越多,它們可以共享容量。對於大客戶,我們創建了專門的資源池來保證一致的響應時間。
-
一些有 SLA 要求的核心服務使用了單獨的 POD,保證不讓壞客戶影響了整個數據中心。
-
我們使用了基於事件的同步機制,當服務器端發生了事件,事件被推送給桌面客戶端,客戶端在本地重放這些事件。
-
我們採用了大規模數據過濾算法,讓大集羣的客戶端與雲端文件系統進行同步。
-
對於不同的問題,我們使用了不同的緩存技術,包括:
傳統的緩存技術。
簡單的內存緩存,包括可變對象和大數據集合。
複雜的內存緩存,比如高容量可變數據集合。
基於磁盤的緩存。
你們有哪些獨到的東西是值得別人學習的?
初創公司要把注意力放在覈心技術能力上,如果遇到了技術難題,需要自己開發一些東西,那麼就擼起袖子幹吧。有很多東西可以學的,其中存儲層、基於事件的同步機制最值得一看,更多細節可以參看:
你們總結了哪些經驗?
-
儘可能收集系統相關信息,先優化經常被使用的部分。
-
在剛開始引入新技術時不要太過激進,不要奢望一開始就爲手頭的問題找到完美的工具。如果引入了太多技術,代碼寫起來雖容易,但維護工作(比如部署、運維、學習曲線)會變得困難。隨着規模的增長,需要把系統拆分成多個服務。保留一兩個服務作爲微服務模板,儘量不要使用不同的技術棧來開發不同的微服務。
-
作爲初創公司,你需要小步快跑。先引入當前最爲合適的方案,然後一步步加以改善。
-
找到單點故障點,並毫不留情地把它們一網打盡。付出額外的努力去解決那些讓你夜不能寐的問題,並儘快從防守轉爲進攻。
-
在 SOA 架構中,儘早使用迴路斷路器來減少負載,如果服務達到瓶頸,可以發送 503 錯誤碼給客戶端。與其拒絕處理每一個請求,不如看看是否可以公平地分配資源,只拒絕那些濫用資源的請求。
-
給服務消費者添加自動修復功能。服務器可能會發生阻塞,桌面客戶端或其他服務消費者可以通過指數回退降低服務器端的壓力,並在服務恢復時自動修復。
-
始終可用:使用服務端迴路斷路器和消費端迴路斷路器。例如,如果通過 WebDAV 或 FTP 訪問文件系統存在性能問題,並且需要 4 個小時來修復,那麼在這 4 個小時中,可以在網關或防火牆上禁用 FTP/WebDAV,並要求客戶端使用 Web UI 或其他方式。類似地,如果一個客戶端的異常行爲阻塞了系統,就暫時禁用這個客戶端或者相應的服務,並在問題修復後重新啓用。我們在這方面使用了功能開關和迴路斷路器。
-
對於高度可伸縮的服務,讓它們運行在 Java 進程之外需要付出很高的成本,甚至放在 Memcache 或 Redis 中也一樣,所以我們在內存中緩存了一些使用率很高的數據結構,如訪問控制計算、功能開關、路由元數據等,並設定了不同的 TTL。
-
在處理大數據集時,我們發現了一些模式。無休止地優化代碼可能是徒勞的,那樣只會讓代碼變複雜。通常情況下,最簡單的解決方案來自於最初的想法:
減少所需的數據集;
重新規劃數據在內存或磁盤上的存儲方式;
反規範化數據,避免使用表連接;
使用基於時間的過濾器,比如歸檔舊數據;
使用多租戶數據結構來創建更小的分片;
通過事件來更新緩存,而不是進行全量更新。
-
保持簡單:每個月都會有新人加入,所以我們的目標是讓他們能夠在第一週就上手,而只有簡單的架構才能實現這個目標。
你們的開發流程是怎樣的?
我們實行 Scrum,雲文件系統團隊每週發佈一次版本。我們使用了 Git Flow 的一個變體,對於每一個 ticket,我們克隆代碼庫,併爲每一個合併請求執行自動化測試。一個合併請求需要由兩名工程師確認,之後相應的 JIRA ticket 纔算解決。ticket 被解決之後會進入後續的發佈流程,包括自動化 REST API 測試和一些手動冒煙測試。
代碼在發佈前的 2 到 3 天進入 UAT 環境,繼續發現在自動化測試中沒有被發現的問題。我們每週三正式發佈版本,每天生成異常報告。我們把發佈時間選在這個時間點,主要考慮到了生活和工作的平衡,因爲一旦發佈出了問題,所有的工程師都在。
你們的服務是怎麼劃分的?
我們採用了 SOA 架構,根據服務的類型來分配服務器,其中頂級服務包括:
- 元數據
- 存儲
- 對象服務
- Web UI
- 索引
- 同步
- 搜索
- 審計
- 內容智能
- 實時事件傳遞
- 文本提取
- 集成
- 縮略圖生成
- 反病毒
- 垃圾郵件
- 預覽 / 縮略圖
- 遠程備份
- API 網關
- 計費
- FTP/SFTP
- 其他
你們是怎麼分配服務器的?
大部分服務運行在虛擬機中,只有一小部分運行在物理機上,比如 MySQL、Memcached 和存儲節點。我們使用了第三方基於模板的服務器分配工具。不過,我們已經着手把所有東西都遷移到雲端,所以最後會使用 Kubernetes。我們面臨的最大挑戰是如何在不宕機的情況下做這些事情。
你們使用什麼數據庫?
MySQL 和 Redis。之前我們也使用了其他數據庫,比如 Berkeley DB、Lucene、Cassandra,然後出於工程和運維方面的考慮,逐步改成 MySQL。
在某些地方我們還使用了 OpenTSDB、BigTable、Elasticsearch。
你們的存儲策略是什麼?
剛開始我們自己組建服務器,在一臺機器上使用儘可能多的磁盤,因爲那個時候 AWS 的價格還很昂貴。我們也嘗試了 GlusterFS,但它的伸縮性滿足不了我們的需求。當 S3 變得越來越便宜,GCS 和 Azure 也開始出現,我們重構了存儲層,支持插拔,用戶可以選擇他們想要的存儲引擎(Egnyte、S3、GCS、Azure 等)。現在,我們在雲端和數據中心分別保存一個副本,最終會使用數據中心的副本作爲直通緩存,因爲雲服務雖然便宜,但帶寬仍然很貴。
你們是如何規劃容量的?
我們有半自動化的容量規劃工具,根據 New Relic、Grafana 和其他統計信息來規劃容量。我們每個季度都會基於監控報告的關鍵指標進行容量規劃,並預留一些容量。一些服務部署到了雲端,可以根據隊列大小進行自動伸縮。
你們的數據庫架構是怎樣的?主副?分片?其他?
對於大部分數據庫,我們採用了主副複製的模式,並帶有自動失效備援機制。有些變動頻繁的數據庫需要進行手動切換,因爲複製延遲會導致在切換時應用程序數據不一致,所以我們需要通過重構部分核心文件系統邏輯來修復這個問題。
你們是怎麼解決負載均衡問題的?
我們根據用戶訪問系統時使用的 DNS IP 將流量路由到相應的數據中心,然後根據 HAProxy 將流量路由到相應的 POD,在 POD 裏也是根據 HAProxy 進行路由。
你們的系統提供了標準的 API 了嗎?如果有,你們是怎麼實現的?
我們的 API 分爲 3 類:
- 公共 API:爲第三方 App 開發者、集成團隊和我們自己的移動 App 提供的 API。
- 客戶端 API:爲我們自己的客戶端提供的 API。
- 內部服務 API:數據中心內部使用的 API,服務之間通過這些 API 相互通信,外部無法調用這些 API。
你們的對象和內容緩存策略是怎樣的?
我們保存着數 PB 的數據,所以不可能緩存所有的東西。不過,如果一個用戶有 5 千萬個文件,那麼在 15 天內他可能只會使用其中的 1 百萬個。我們有基於 LRU 算法的緩存過濾器節點,可以彈性地增加或減少節點數量。上傳速度是一個大問題,該如何保證從世界各地將文件快速上傳到 Egnyte?爲此,我們構建了特殊的網絡連接點(PoP)。
我們使用 Memcached 和 Redis 來緩存元數據,Memcached 緩衝池用來保存長期使用的靜態數據和文件系統元數據。核心文件系統的元數據很大,Memcached 節點放不下,爲此我們使用了三種緩存池,並在應用層決定該從哪裏獲取需要的數據。不同類型的數據具有不同的過期時間。對於某些請求(如列出文件內容),將一些經常用到的數據(如客戶信息或分片映射信息)放在 Memcached 中反而會讓速度變慢,爲此,我們將這些數據放在 JVM 的內存中,並基於 TTL 或者發佈訂閱機制來沖刷它們。
緩存方面存在的兩個最大的問題是權限數據和事件數據。對於權限數據,我們已經進行了多次重構,最近開發了 TRIE 來緩存它們。
我們把事件數據緩存在 Memcached 中,但在某些情況下會出問題。例如,我們可能在夜間爲一個用戶發佈 10 萬個事件,到了早上 9 點,有 3 萬個用戶打開了他們的電腦,每個用戶都需要接收着 10 萬個事件。也就是說,我們需要在短短的 15 分鐘內處理 300 億個事件,而且只能將事件發送給有權限訪問這些事件的用戶。因爲事件是不可變的,我們可以將它們保存在 Memcached 中 12 個小時,但多次下載事件數據仍然存在網絡方面的壓力。最終,我們將事件緩存在內存中一小段時間,並調整了 GC 設置。我們還將這些節點放到更快的網段中。不過,這個問題還沒有完全解決……
你們是如何檢測全局可用性以及如何模擬終端用戶性能的?
我們的節點分佈在不同的 AWS 區域,以此來測試帶寬性能。我們還基於內部 HAProxy 報告來調整上傳 / 下載速度,並使用特殊的網絡 PoP 和其他策略來加速數據包的傳輸。
你們是如何進行服務器和網絡可用性檢測的?
我們使用了 Nagios、Grafana、New Relic 和一些內部異常分析工具。
你們是如何可視化服務器統計信息和趨勢的?
我們使用了 Grafana、Kibana、Nagios 和 New Relic。
你們是如何測試系統的?
Selenium、JUnit、Nose、Nightwatch 和手動測試。我們會進行單元測試、功能測試、集成測試和性能測試。
你們是如何分析性能的?
我們使用 New Relic 來監控應用程序性能。我們還使用自己開發的框架收集了很多應用程序指標。我們也使用了 Grafana、Nagios、Kibana 和一些內部工具來監控系統其他部分的性能。
你們是如何處理安全問題的?
我們有專門負責安全的團隊,他們在每次發佈版本之前都會運行自動化的安全基準測試。我們在生產環境中持續運行自動化滲透測試。我們還推出了 bug 獎勵計劃。一些客戶藉助第三方工具自己進行安全測試。
你們是怎樣備份和還原系統的?
我們使用 Percona XTraBackup 來備份 MySQL。Elasticsearch 會被複制 3 份。用戶的文件也會被複制 3 分,其中一份保存在雲端。如果一個副本無法還原,我們就將其放棄,並加入新的副本。對於某些用戶,我們會額外地複製一份到他們選擇的存儲廠商。對於選擇 S3、Azure 或 GCS 作爲存儲的用戶,我們會確保爲它們複製了一個副本,防止數據丟失。
你們是如何處理數據庫結構變更的?
不同的服務使用了不同類型的數據庫,它們的升級方式也不一樣。
- EOS 保存了對象元數據,增長速度很快。我們對它進行了分片,並不斷加入新分片。
- MDB 增長速度更快,我們也對它進行了分片,並不斷加入新分片。
- dc_central 是一個 DNS 數據庫,相對比較穩定。出於伸縮性方面的考慮,我們複製了多個副本。
- pod_central 保存頻繁變化的數據,但每張表不會超過 20M。出於伸縮性方面的考慮,我們也複製了多個副本。
- 每個數據庫的 schema 都保持向前和向後兼容,也就是說,我們不會在同一個版本中丟棄數據庫列和相關的代碼。例如,在版本 1 中部署新代碼,這些代碼不再使用某些數據庫列,然後在版本 2 中才將不用的列移除。
- 不分片的數據庫每週都會進行升級。在生產環境中我們使用腳本進行升級,在 QA 環境使用 Liquibase,後續會逐步在生產環境中也使用這個工具。
- 分片數據庫使用自動化腳本來修改表結構。
- 分片數據庫的遷移是個痛點,因爲我們有 1 萬 2 千多個分片,而且還在不斷增長,一次升級沒一個小時是搞不定的。
- 我們通過 schema 檢查報告來確保在升級之後所有數據中心的數據庫 schema 是一致的。
英文原文
Egnyte Architecture: Lessons Learned In Building And Scaling A Multi Petabyte Content Platform