StackOverflow 這麼大,它的架構是怎麼樣的?

【伯樂在線補充】:Nick Craver 是 StackOverflow 的軟件工程師 & 網站可靠性工程師。

這是「解密 Stack Overflow 架構」系列的第一篇,本系列會有非常多的內容。歡迎閱讀並保持關注。

爲了便於理解本文涉及到的東西到底都幹些了什麼,讓我先從 Stack Overflow 每天平均統計量的變化開始。下面的數據數來自 2013 年 11 月 12 日的統計:

  • 負載均衡器接受了148,084,833次HTTP請求
  • 其中36,095,312次是加載頁面
  • 833,992,982,627 bytes (776 GB) 的HTTP流量用於發送
  • 總共接收了286,574,644,032 bytes (267 GB) 數據
  • 總共發送了1,125,992,557,312 bytes (1,048 GB) 數據
  • 334,572,103次SQL查詢(僅包含來自於HTTP請求的)
  • 412,865,051次Redis請求
  • 3,603,418次標籤引擎請求
  • 耗時558,224,585 ms (155 hours) 在SQL查詢上
  • 耗時99,346,916 ms (27 hours) 在Redis請求上
  • 耗時132,384,059 ms (36 hours) 在標籤引擎請求上
  • 耗時2,728,177,045 ms (757 hours) 在ASP.Net程序處理上
  • 節選自@蔣生武 翻譯的《StackOverflow 這麼大,究竟用在什麼硬件設備?

下方數據是到 2016 年 2 月 9 日時,統計數字發生的變化,你可以比較一下:

  • 負載均衡器收到的 HTTP 請求:209,420,973 (+61,336,090)
  • 66,294,789 (+30,199,477) 其中的頁面加載數量
  • 發送的 HTTP 數據量:1,240,266,346,053 (+406,273,363,426) bytes (1.24 TB)
  • 總共接收的數據量:569,449,470,023 (+282,874,825,991) bytes (569 GB)
  • 總共發送的數據量:3,084,303,599,266 (+1,958,311,041,954) bytes (3.08 TB)
  • SQL 查詢(僅來自於 HTTP 請求):504,816,843 (+170,244,740)
  • Redis 緩存命中數:5,831,683,114 (+5,418,818,063)
  • Elastic 搜索數量:17,158,874 (not tracked in 2013)
  • 標籤引擎(Tag Engine)請求數:3,661,134 (+57,716)
  • 運行 SQL 查詢累計消耗的時間:607,073,066 (+48,848,481) ms (168 hours)
  • Redis 緩存命中消耗的時間:10,396,073 (-88,950,843) ms (2.8 hours)
  • 標籤引擎請求消耗的時間:147,018,571 (+14,634,512) ms (40.8 hours)
  • 在 ASP.Net 處理中消耗的時間:1,609,944,301 (-1,118,232,744) ms (447 hours)
  • 22.71 (-5.29) ms 49,180,275 個問題頁面平均的渲染時間(其中 19.12 ms 消耗在 ASP.Net 中)
  • 11.80 (-53.2) ms 6,370,076 個首頁的平均渲染時間(其中 8.81 ms 消耗在 ASP.Net 中)

你可能會好奇爲什麼 ASP.Net 在每天多處理6100萬次請求的情況下,處理時間卻減少了757個小時(相比於在2013 年)。這主要歸功於在 2015 年初的時候我們對服務器進行的升級,以及大量的應用內的性能優化工作。別忘了:性能依然是個賣點。如果你對具體的硬件配置細節更加好奇的話,別擔心,我很快就會在下一篇文章中以附錄的形式給出運行這些網站所用的服務器的具體硬件配置細節(到時候我會更新這個鏈接)。

所以這兩年來到底發生了哪些變化?不太多,只是替換掉一些服務器和網絡設備而已。下面是今天運行網站所用的服務器概覽(注意和 2013 年相比有什麼變化)

  • 4 臺 Microsoft SQL Server 服務器(其中 2 臺使用了新的硬件)
  • 11 臺 IIS Web 服務器(新的硬件)
  • 2 臺 Redis 服務器(新的硬件)
  • 3 臺標籤引擎服務器(其中 2 臺使用了新的硬件)
  • 3 臺 Elasticsearch 服務器(同上)
  • 4 臺 HAProxy 負載均衡服務器(添加了 2 臺,用於支持 CloudFlare)
  • 2 臺網絡設備(Nexus 5596 核心 + 2232TM Fabric Extender,所有設備都升級到 10Gbps 帶寬)
  • 2 臺 Fortinet 800C 防火牆(取代了 Cisco 5525-X ASAs)
  • 2 臺 Cisco ASR-1001 路由器(取代了 Cisco 3945 路由器)
  • 2 臺 Cisco ASR-1001-x 路由器(新的!)

爲了支撐Stack Overflow的運行,我們需要些什麼?從 2013 年至今並沒有太多的變化,不過因爲優化以及上面提到的新硬件設備,我們現在只需要一臺 web 服務器了。我們已經無意中測試過這種情況了,成功了好幾次。請注意:我只是說這是可行的,我可沒說這是個好主意。不過每次發生這種情況的時候都還挺有意思的。

現在我們已經對服務器縮放的想法有了一些基線數字,來看看我們是如何製作這些炫酷網頁的。很少有系統是完全獨立存在的(當然我們的也不例外),如果沒有一個全局眼光能把這些部分集成在一起的話,架構規劃的意義就要大打折扣了。我們的目標,就是把握全局。後續會有很多文章深入到每個特定的領域中。本文只是一個關於重點硬件的邏輯結構概要,下一篇文章會包含這些硬件的具體細節。

如果你們想看看今天這些硬件到底長什麼樣子的話,這裏有幾張在 2015 年 2 月升級服務器的時候,我拍攝的機櫃A的照片(機櫃B和它是完全一樣的):

如果你想看更多這種東西的話,這裏是那一週升級過程中完整的 256 張照片的相冊(沒錯,這個數字就是故意的哈哈)。現在,讓我們來深入架構佈局。以下是現有的主要系統的邏輯架構概要:

(點擊看大圖)

基本原則

以下是一些通行的原則,不需要再依次介紹它們了:

  • 所有東西都有冗餘備份。
  • 所有的服務器和網絡設備之間都至少有兩個 10Gbps 帶寬的連接。
  • 所有服務器都有兩路電源,通過兩個 UPS 單元組、背後的兩臺發電機、兩臺電網電壓前饋來提供電力。
  • 所有服務器都有一個冗餘備份分別位於機櫃A和機櫃B中。
  • 所有服務器和服務都有雙份的冗餘備份,放在另外一個數據中心(位於科羅拉多),雖然這裏我主要是在介紹紐約的情況。
  • 所有東西都有冗餘備份。

互聯網

首先你得找到我們的網站,這是 DNS 的事兒。查找網站速度得快,所以我們現在把這事兒包給了 CloudFlare,因爲他們有遍佈在全球各個角落的 DNS 服務器。我們通過 API 來更新 DNS 記錄,他們負責“管理”DNS。不過以我們的小人之心,因爲還是有根深蒂固的信任問題,所以我們依然還是擁有自己的 DNS 服務器。當世界末日的時候——可能因爲 GPL、Punyon(譯註:Stack Overflow 團隊的一員)或者緩存問題——而人們依然想要通過編程來轉移注意力的話,我們就會切換到自己的 DNS 服務器。

你的瀏覽器找到了我們的藏身之所之後,來自我們四家網絡服務供應商(紐約的 Level 3、Zayo、Cogent 和 Lightower)的 HTTP 流量就會進入我們四臺先進的路由器之一。我們使用邊界網關協議(BGP,非常標準的協議)來對等處理來自網絡供應商的流量,以此來對其進行控制,並提供最高效的通路來訪問我們的服務。這些 ASR-1001 和 ASR-1001-X 路由器被分爲兩組,每組應都使用雙活的模式(active/active)來處理來自兩家網絡供應商的流量——在這裏是有冗餘備份的。雖然都是擁有同樣的物理 10Gbps 的帶寬,來自外部的流量還是和外部 VLAN 的流量獨立開來,分別接入負載均衡。在流量通過路由器之後,你就會來到負載均衡器了。

我想現在可能是時候提到我們在兩個數據中心之間擁有 10Gbps 帶寬的 MPLS,雖然這其實和網站服務沒什麼直接關係。我們使用這種技術來進行數據的異地複製和快速恢復,來應對某些突發情況。“不過 Nick,這裏面可沒有冗餘!”好吧,從技術角度上你說的沒錯(正面意義上的沒錯),在這個層面上它確實是單點故障。不過等等!通過網絡供應商,我們還額外擁有兩個 OSPF 故障轉移路由(MPLS是第一選擇,出於成本考慮這個是第二和第三選擇)。之前提到的每組設備都會相應地接入科羅拉多的數據中心,在故障轉移的情況下來對網絡流量進行負載均衡。當然我們本可以讓這兩組設備互相之間都連接在一起,這樣就有四組通路了,不過管它呢,讓我們繼續。

負載均衡(HAProxy

負載均衡通過 HAProxy 1.5.15 實現,運行在 CentOS 7 上(我們最喜歡的 Linux 版本)。並在HAProxy上加入TLS(SSL)安全傳輸協議。我們還在密切關注 HAProxy 1.7,它馬上就會提供對 HTTP/2 協議的支持。

和其他擁有雙路 10Gbps LACP 網絡連接的服務器不同,每臺負載均衡都擁有兩套 10Gbps 的連接:其中一套對應外部網絡,另一套對應 DMZ。這些服務器擁有 64GB 或者更多的內存,來更有效地處理 SSL 協議層。當我們可以在內存中緩存和重用更多的 TLS 會話的時候,在連接到同一個客戶端時就會少消耗一些計算資源。這意味着我們能夠以更快、更便宜的方式來還原會話。內存是如此廉價,所以這是個很容易做出的抉擇。

負載均衡本身搭建起來很容易。我們在多個不同的 IP(主要出於證書和 DNS 管理的考慮)上監聽不同的網站,然後將流量路由到不同的後端(主要基於host header)。我們在這裏做的唯一值得一提的事情就是限速和抓取部分 header 信息(來自 web 層)記錄到 HAProxy 的系統日誌消息中,通過這種方式我們可以記錄每個請求的性能指標。我們會在後面詳細提到這一點

Web 層(IIS 8.5、ASP.Net MVC 5.2.3 和 .Net 4.6.1)

負載均衡將流量分配到 9 臺我們所謂的主 web 服務器(01-09)中和 2 臺開發 web 服務器(10-11,我們的測試環境)。主服務器運行着 Stack Overflow、Careers 以及所有的 Stack Exchange 網站,除此之外的 meta.stackoverflow.com 和 meta.stackexchange.com在是運行在另外兩臺服務器上的。主要的 Q&A 應用本身就是多租戶(multi-tenant)形式的,也就是說一個單獨應用處理了所有 Q&A 網站的請求。換句話說,我們可以在一臺服務器的一個應用程序池上,運行整個的 Q&A 應用。其它的應用比如 Careers、API v2、Mobile API 等等,都是獨立的。下面是主服務器和開發服務器的 IIS 中看到的內容:

下面是在 Opserver(我們內部的監控儀表板)中看到的 Stack Overflow 的 web 層分佈情況:

(點擊查看大圖)

還有下面這個是這些 web 服務器的資源消耗情況(譯註:不是說好的 11 臺麼):

(點擊查看大圖)

我會在後續的文章中詳細提到爲什麼我們過度提供了這麼多資源,重點在於:滾動構建(rolling build)、留有餘地、冗餘。

服務層(IIS、ASP.Net MVC 5.2.3、.NET 4.6.1 和 HTTP.SYS)

緊挨着web層的是服務層。它們同樣運行在 Windows 2012R2 的 IIS 8.5 之上。這一層運行一些內部服務,對生產環境的 web 層和其他內部系統提供支持。兩個主要的服務包括:“Stack Server”,其中運行着標籤引擎,是基於 http.sys的(背後並非是 IIS);Providence API(基於IIS)。一個有趣的事實:我不得不對着兩個進程進行相關性設置,讓它們連接到不同的 socket 上,因爲 Stack Server 在以兩分鐘爲間隔刷新問題列表的時候,會非常頻繁的訪問 L2 和 L3 級緩存。

運行這些服務的機器對於標籤引擎和後端的 API 有着舉足輕重的意義,因此它們必須是冗餘的,不過並不需要 9 倍的冗餘。舉例來說,我們會每隔 n 分鐘(目前是兩分鐘)就從數據庫中加載所有文章及其標籤,這個操作消耗並不低。我們可不想在 web 層把這個加載操作重複 9 次,3 次對我們來說就足夠安全了。我們同樣會對這些服務器採用不同的硬件配置,以便針對標籤引擎和 elastic 索引作業(同樣運行在這一層中)的計算和數據加載的特徵進行更好的優化。“標籤引擎”本身就是一個相對複雜的話題,會在專門的文章中進行介紹。基本的原理是:當你訪問地址 /questions/tagged/java 的時候,你會訪問標籤引擎來獲取與之匹配的問題。該引擎處理了除 /search 之外的所有標籤匹配工作,所以包括新的導航在內的所有地方都是通過這個服務來獲取數據的。

緩存 & 發佈/訂閱(Redis

我們在一些地方使用了 Redis,它擁有堅如磐石般地穩定性。儘管每個月的操作有 1600 億次之多,每個實例的 CPU 也不會超過 2%,通常會更低:

(點擊查看大圖)

我們藉助 Redis 用於 L1/L2 級別的緩存系統。“L1”級是 HTTP 緩存,在 web 服務器或者任何類似的應用程序中起作用。“L2”級則是當上一級緩存失效之後,通過 Redis 獲取數據。我們的數據是以 Protobuf 格式儲存的,通過 Marc Gravell 編寫的 protobuf-dot-net實現。對於 Redis 客戶端,我們使用了 StackExchange.Redis 庫,這是一個內部開發的開源庫。如果一臺 web 服務器在 L1 和 L2 緩存中都沒有命中,它就會從其數據源中獲取數據(數據庫查詢、API 調用等等),然後將結果保存到本地緩存和 Redis 中。下一臺服務器在獲取同樣數據的時候,可能會在 L1 緩存中缺失,但是它會在 L2/Redis 中獲取到數據,省去了數據庫查詢或者 API 調用的操作。

我們同樣運行着很多 Q&A 站點,每個站點都有其自己的 L1/L2 緩存:在 L1 緩存中使用 key 作爲前綴,在 L2/Redis 緩存中使用數據庫 ID。我們會在未來的文章中深入探討這個話題。

除了運行着所有站點實例的兩臺主要的 Redis 服務器(一主一從)之外,我們還利用另外兩臺專用的從服務器搭建了一個用於機器學習的的實例(主要出於內存考慮)。這組服務器用來提供首頁上的問題推薦、做出更優的工作職位匹配等服務。這個平臺稱爲 Providence,Kevin Montrose 曾撰文描述過它

主要的 Redis 服務器擁有 256GB 內存(大約使用了 90GB),Providence 服務器擁有 384GB 內存(大約使用了 125GB)。

Redis 並非只用來做緩存,它同樣擁有一套發佈和訂閱機制,一臺服務器可以發佈一條消息,其他的訂閱服務器可以收到該消息(包括 Redis 從服務器上的下游客戶端)。我們利用這個機制來清除其他服務上的 L1 緩存,用來保持 web 服務器上的緩存一致性。不過它還有另外一個重要的用途:websocket。

Websockets(NetGain

我們使用 websocket 向用戶推送實時的更新內容,比如頂部欄中的通知、投票數、新導航數、新的答案和評論等等。

socket 服務器本身在 web 層上運行,使用原生的 socket。這是一個基於我們的開源庫實現的非常小型的應用程序:StackExchange.NetGain。在高峯時刻,我們大約有 50 萬個併發的 websocket 連接,這可是一大堆瀏覽器。一個有趣的事實:其中一些瀏覽器已經打開超過 18 個月了,得找人去看看那些開發者是不是還活着。下面這張圖是本週 websocket 併發量的模式:

(點擊查看大圖)

爲什麼用 websocket?在我們這個規模下,它比輪詢要有效率得多。通過這種方式,我們可以簡單地使用更少資源來推送更多數據,而且對用戶而言實時性也更高。不過這種方式也並非沒有問題:臨時端口、負載均衡上的文件句柄耗盡,都是非常有趣的問題,我們稍後會提到它們

搜索(Elasticsearch

劇透:這裏沒多少讓人興奮的東西。web層使用了Elasticsearch 1.4 ,並實現了超輕量級、高性能的 StackExchange.Elastic 客戶端。和大多數東西不同的是,我們並沒有計劃把這部分內容開源,簡單來說,是因爲它只暴露了非常少量的我們需要使用的 API 的子集。我確信把它公開出來是得不償失的,只會讓開發者感到困惑。我們在這些地方用到了 elastic:/search、計算相關問題、提問時給出相關建議。

每個 Elastic 集羣(每個數據中心各有一個)包含 3 個節點,每個站點都擁有各自的索引。Careers 站點還有一些額外的索引。在 elastic 圈子裏,我們的配置中稍微不那麼標準的地方是,我們 3 臺服務器的集羣比通常的配置要更強大一些:每臺服務器都使用了 SSD 存儲、192GB 內存、雙路 10Gbps 帶寬的網絡。

在 Stack Server 的同一個應用程序域(沒錯,我們在這個地方被 .Net Core 折騰慘了)裏面還宿主着標籤引擎,它同樣使用了 Elasticsearch 進行連續索引。這裏我們用了些小花招,比如使用 SQL Server(數據來源)中的 ROWVERSION 和 Elastic 中的“最後位置”文檔進行比較。因爲從表觀上看它是順序的,這樣如果內容在最後一次訪問後被修改的話,我們就很容易對其進行抓取和索引了。

我們使用 Elasticsearch 代替如 SQL 全文檢索這類技術的主要原因,就是它的可擴展性和性價比。SQL 的 CPU 相對而言非常昂貴,而 Elastic 則便宜得多,並且最近有了非常多的新特性。爲什麼不用 Solr?我們需要在整個網絡中進行搜索(同時有多個索引),在我們進行決策的時候 Solr 還不支持這種場景。我們還沒有使用 2.x 版本的原因,是因爲 2.x 版本中類型(types)有了很大的變化,這意味着想要升級的話我們得重新索引所有內容。我只是沒有足夠的時間來制定需求變更和遷移的計劃。

數據庫(SQL Server)

我們使用 SQL Server 作爲單一的數據源(single source of truth)。Elastic 和 Redis 中的所有數據都來自 SQL Server。我們有兩個 SQL Server 集羣,並配置了 AlwaysOn 可用性組。每個集羣都在紐約有一臺主服務器(承擔了幾乎全部負載)和一臺副本服務器,此外還有一個在科羅拉多(我們的災備數據中心)的副本服務器。所有的複製操作都是異步的。

第一個集羣是一組 Dell R720xd 服務器,每臺擁有 384GB 內存,4TB 空間的 PCIe SSD,和兩個 12 核 CPU。它包含了 Stack Overflow、Sites(這是個壞名字,稍後我會解釋它)、PRIZM 以及 Mobile 的數據庫。

第二個集羣是一組 Dell R730xd 服務器,每臺擁有 768GB 內存,6TB 空間的 PCIe SSD,和兩個 8 核 CPU。這個集羣包含了所有其它數據庫,包括 CareersOpen IDChat異常日誌,以及其他的 Q&A 網站(比如 Super UserServer Fault 等)。

在數據庫層上,我們希望讓 CPU 利用率保持在一個非常低的級別,不過實際上在一些計劃緩存問題(我們正在排查)發生的時候,CPU 佔用率會稍高一些。目前,NY-SQL02 和 04 是主服務器,01 和 03 是副本服務器,我們今天因爲 SSD 升級剛剛重啓過它們。以下是它們在過去 24 小時內的表現:

(點擊查看大圖)

我們對 SQL 的使用非常簡單。簡單就意味着快速。雖然有些查詢語句會很變態,我們對 SQL 本身的交互還是通過相當原生的方式進行的。我們有一些遺留的 Linq2Sql,不過所有新開發的內容都使用了 Dapper,這是我們開源的微型 ORM 框架,使用了 POCO。讓我換一種方式來解釋一下:Stack Overflow 的數據庫中只有一個存儲過程,而且我打算把這個最後殘留的存儲過程也幹掉,換成代碼。

好吧讓我們換個思路,這裏是更直接能幫到你的東西。在前面我已經提到過一些了,不過我會給出一個列表,其中包含了很多由我們維護的、大家都在使用的開源 .Net 類庫。我們把它們開源,因爲其中並不涉及到核心的商業價值,但是可以幫助世界上的開發者們。我希望你們如今能用到它們:

  • Dapper (.Net Core) – 高性能的微型 ORM 框架,用於 ADO.Net
  • StackExchange.Redis – 高性能的 Redis 客戶端
  • MiniProfiler – 輕量的分析探查器(profiler),我們在每個頁面上都使用了它(同樣支持 Ruby、Go 和 Node)
  • Exceptional – 用於 SQL、JSON、MySQL 等的錯誤日誌記錄
  • Jil – 高性能的 JSON 序列化和反序列化器
  • Sigil – .Net CIL 生成幫助器(在 C# 不夠快的時候使用)
  • NetGain – 高性能的 websocket 服務器
  • Opserver – 監控儀表板,可以直接輪詢大多數系統,並且可以從 Orion、Bosun 或 WMI 中獲取信息
  • Bosun – 後臺的監控系統,使用 Go 編寫

下一篇內容:目前我們所使用硬件的詳細配置清單。再之後,我們會按照列表依次撰文。敬請期待。

發佈了7 篇原創文章 · 獲贊 14 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章