原文地址:HTTPS on Stack Overflow: The End of a Long Road
作者:Nick Craver
譯者: 羅晟 & 狄敬超
前文地址:【翻譯】Stack Overflow 的 HTTPS 化:漫漫長路的終點(diǎn)(一)
Cloudflare
我們評估了很多 CDN/DDoS 防護(hù)層供應(yīng)商。最終選擇了 Cloudflare,主要是考慮到他們的基礎(chǔ)設(shè)施、快速響應(yīng)、還有他們承諾的 Railgun。那么我們?nèi)绾螠y試使用了 Cloudfalre 之后用戶的真實(shí)效果?是否需要部署服務(wù)來獲取用戶數(shù)據(jù)?答案是不需要!
Stack Overflow 的數(shù)據(jù)量非常大:月 PV 過十億。記得我們上面講的客戶端耗時(shí)紀(jì)錄嗎?我們每天都有幾百萬的訪問了,所以不是直接可以問他們嗎?我們是可以這么做,只需要在頁面中嵌入 <iframe>
就行了。Cloudflare 已經(jīng)是我們 cdn.sstatic.net(我們共用的無 cookie 的靜態(tài)內(nèi)容域)的托管商了。但是這是通過一條CNAME
DNS 紀(jì)錄來做的,我們把 DNS 指向他們的 DNS。所以要用 Cloudflare 來當(dāng)代理服務(wù)的話,我們需要他們指向我們的 DNS。所以我們先需要測試他們 DNS 的性能。
實(shí)際上,要測試性能我們需要把二級(jí)域名給他們,而不是 something.stackoverflow.com
,因?yàn)檫@樣可能會(huì)有不一致的膠水記錄而導(dǎo)致多次查詢。明確一下,一級(jí)域名 (TLDs)指的是 .com
, .net
, .org
, .dance
, .duck
, .fail
, .gripe
, .here
, .horse
, .ing
, .kim
, .lol
, .ninja
, .pink
, .red
, .vodka
. 和 .wtf
。 注意,這些域名尾綴都是,我可沒開玩笑。 二級(jí)域名 (SLDs) 就多了一級(jí),比如 stackoverflow.com
, superuser.com
等等。我們需要測的就是這些域名的行為及表現(xiàn)。因此,我們就有了 teststackoverflow.com
,通過這個(gè)新域名,我們在全球范圍內(nèi)測試 DNS 性能。對一部分比例的用戶,通過嵌一個(gè) <iframe>
(在測試中開關(guān)),我們可以輕松地獲取用戶訪問 DNS 的相關(guān)數(shù)據(jù)。
注意,測試過程最少需要 24 小時(shí)。在各個(gè)時(shí)區(qū),互聯(lián)網(wǎng)的表現(xiàn)會(huì)隨著用戶作息或者 Netflix 的使用情況等發(fā)生變化。所以要測試一個(gè)國家,需要完整的一天數(shù)據(jù)。最好是在工作日(而不要半天落在周六)。我們知道會(huì)有各種意外情況?;ヂ?lián)網(wǎng)的性能并不是穩(wěn)定的,我們要通過數(shù)據(jù)來證明這一點(diǎn)。
我們最初的假設(shè)是,多增加了的一個(gè)節(jié)點(diǎn)會(huì)帶來額外的延時(shí),我們會(huì)因此損失一部分頁面加載性能。但是 DNS 性能上的增加其實(shí)彌補(bǔ)了這一塊。比起我們只有一個(gè)數(shù)據(jù)中心來說,Cloudflare 的 DNS 服務(wù)器部署在離用戶更近的地方,這一塊性能要好得多得多。我希望我們能有空來放出這一塊的數(shù)據(jù),只不過這一塊需要很多處理(以及托管),而我現(xiàn)在也沒有足夠多的時(shí)間。
接下來,我們開始將 teststackoverflow.com
放在 Cloudflare 的代理上做鏈路加速,同樣也是放在 <iframe>
中。我們發(fā)現(xiàn)美國和加拿大的服務(wù)由于多余的節(jié)點(diǎn)而變慢,但是世界其他地方都是持平或者更好。這滿足我們的期望。我們開始使用 Cloudflare 的網(wǎng)絡(luò)對接我們的服務(wù)。期間發(fā)生了一些 DDos 的攻擊,不過這是另外的事了。那么,為什么我們接受在美國和加拿大地區(qū)慢一點(diǎn)呢?因?yàn)槊總€(gè)頁面加載需要的時(shí)間僅為 200-300ms,哪怕慢一點(diǎn)也還是飛快。當(dāng)時(shí)我們認(rèn)為 Railgun 可以將這些損耗彌補(bǔ)回來。
這些測試完成之后,我們?yōu)榱祟A(yù)防 DDos 工作,做了一些其他工作。我們接入了額外的 ISP 服務(wù)商以供我們的 CDN/代理層對接。畢竟如果能繞過攻擊的話,我們沒必要在代理層做防護(hù)?,F(xiàn)在每個(gè)機(jī)房都有 4 個(gè) ISP 服務(wù)商(譯者注:相當(dāng)于電信、聯(lián)通、移動(dòng)、教育網(wǎng)),兩組路由器,他們之間使用 BGP 協(xié)議。我們還額外添置了兩組負(fù)載均衡器專門用于處理 CDN/代理層的流量。
Cloudflare: Railgun
與此配套,我們啟用了兩組 Railgun。Railgun 的原理是在 Cloudflare 那邊,使用 memcached 匹配 URL 進(jìn)行緩存數(shù)據(jù)。當(dāng) Railgun 啟用的時(shí)候,每個(gè)頁面(有一個(gè)大小閾值)都會(huì)被緩存下來。那么在下一次請求時(shí)候,如果在這個(gè) URL 在 Cloudflare 節(jié)點(diǎn)上和我們這里都緩存的話,我們?nèi)匀粫?huì)問 web 服務(wù)器最新的數(shù)據(jù)。但是我們不需要傳輸完整的數(shù)據(jù),只需要把傳輸和上次請求的差異數(shù)據(jù)傳給 Cloudflure。他們把這個(gè)差異運(yùn)用于他們的緩存上,然后再發(fā)回給客戶端。這時(shí)候, gzip 壓縮 的操作也從 Stack Overflow 的 9 臺(tái) Web Server 轉(zhuǎn)移到了一個(gè) Railgun 服務(wù)上,這臺(tái)服務(wù)器得是 CPU 密集型的——我指出這點(diǎn)是因?yàn)?,這項(xiàng)服務(wù)需要評估、購買,并且部署在我們這邊。
舉個(gè)例子,想象一下,兩個(gè)用戶打開同一個(gè)問題的頁面。從瀏覽效果來看,他們的頁面技術(shù)上長得幾乎一樣,僅僅有細(xì)微的差別。如果我們大部分的傳輸內(nèi)容只是一個(gè) diff 的話,這將是一個(gè)巨大的性能提升。
總而言之,Railgun 通過減少大量數(shù)據(jù)傳輸?shù)姆绞教岣咝阅?。?dāng)它順利工作的時(shí)候確實(shí)是這樣。除此之外,還有一個(gè)額外的優(yōu)點(diǎn):請求不會(huì)重置連接。由于 TCP 慢啟動(dòng),當(dāng)連接環(huán)境較為復(fù)雜時(shí)候,可能導(dǎo)致連接被限流。而 Railgun 始終以固定的連接數(shù)連接到 Cloudflare 的終端,對用戶請求采用了多路復(fù)用,從而其不會(huì)受慢啟動(dòng)影響。小的 diff 也減少了慢啟動(dòng)的開銷。
很可惜,我們由于種種原因我們在使用 Railgun 過程中一直遇到問題。據(jù)我所知,我們擁有當(dāng)時(shí)最大的 Railgun 部署規(guī)模,這把 Railgun 逼到了極限。盡管我們花了一年追蹤各種問題,最終還是不得不放棄了。這種狀況不僅沒有給我們省錢,還耗費(fèi)了更多的精力。現(xiàn)在幾年過去了。如果你正在評估使用 Railgun,你最好看最新的版本,他們一直在做優(yōu)化。我也建議你自己做決定是否使用 Railgun。
Fastly
我們最近才遷到 Fastly,因?yàn)槲覀冊谥v CDN/代理層,我也會(huì)順帶一提。由于很多技術(shù)工作在 Cloudflare 那邊已經(jīng)完成,所以遷移本身并沒有什么值得說的。大家會(huì)更感興趣的是:為什么遷移?畢竟 Cloudflare 在各方面是不錯(cuò)的:豐富的數(shù)據(jù)中心、穩(wěn)定的帶寬價(jià)格、包含 DNS 服務(wù)。答案是:它不再是我們最佳的選擇了。Flastly 提供了一些我們更為看中的特性:靈活的終端節(jié)點(diǎn)控制能力、配置快速分發(fā)、自動(dòng)配置分發(fā)。并不是說 Cloudflare 不行,只是它不再適合 Stack Overflow 了。
事實(shí)勝于雄辯:如果我不認(rèn)可 Cloudflare,我的私人博客不可能選擇它,嘿,就是這個(gè)博客,你現(xiàn)在正在閱讀的。
Fastly 吸引我們的主要功能是提供了 Varnish 和 VCL。這提供了高度的終端可定制性。有些功能吧,Cloudfalre 無法快速提供(因?yàn)樗麄兪峭ㄓ没模瑫?huì)影響所有用戶),在 Fastly 我們可以自己做。這是這兩家架構(gòu)上的差異,這種「代碼級(jí)別高可配置」對于我們很適用。同時(shí),我們也很喜歡他們在溝通、基礎(chǔ)設(shè)施的開放性。
我來展示一個(gè) VCL 好用在哪里的例子。最近我們遇到 .NET 4.6.2 的一個(gè)超惡心 bug,它會(huì)導(dǎo)致 max-age 有超過 2000 年的緩存時(shí)間??焖俳鉀Q方法是在終端節(jié)點(diǎn)上有需要的時(shí)候去覆蓋掉這個(gè)頭部,當(dāng)我寫這篇文章的時(shí)候,這個(gè) VCL 配置是這樣的:
sub vcl_fetch {
if (beresp.http.Cache-Control) {
if (req.url.path ~ "^/users/flair/") {
set beresp.http.Cache-Control = "public, max-age=180";
} else {
set beresp.http.Cache-Control = "private";
}
}
這將給用戶能力展示頁 3 分鐘的緩存時(shí)間(數(shù)據(jù)量還好),其余頁面都不設(shè)置。這是一個(gè)為解決緊急時(shí)間的非常便于部署的全局性解決方案。 我們很開心現(xiàn)在有能力在終端做一些事情。我們的 Jason Harvey 負(fù)責(zé) VCL 配置,并寫了一些自動(dòng)化推送的功能。我們基于一個(gè) Go 的開源庫 fastlyctl 做了開發(fā)。
另一個(gè) Fastly 的特點(diǎn)是可以使用我們自己的證書,Cloudflare 雖然也有這個(gè)服務(wù),但是費(fèi)用太高。如我上文提到的,我們現(xiàn)在已經(jīng)具備使用 HTTP/2 推送的能力。但是,F(xiàn)astly 就不支持 DNS,這個(gè)在 Cloudflare 那里是支持的。現(xiàn)在我們需要自己解決 DNS 的問題了??赡茏钣幸馑嫉木褪沁@些來回的折騰吧?
全局 DNS
當(dāng)我們從 Cloudflare 遷移到 Fastly 時(shí)候,我們必須評估并部署一個(gè)新的 DNS 供應(yīng)商。這里有篇 Mark Henderson 寫的 文章 。鑒于此,我們必須管理:
- 我們自己的 DNS 服務(wù)器(備用)
- Name.com 的服務(wù)器(為了那些不需要 HTTPS 的跳轉(zhuǎn)服務(wù))
- Cloudflare DNS
- Route 53 DNS
- Google DNS
- Azure DNS
- 其他一些(測試時(shí)候使用)
這個(gè)本身就是另一個(gè)項(xiàng)目了。為了高效管理,我們開發(fā)了 DNSControl。這現(xiàn)在已經(jīng)是開源項(xiàng)目了,托管在 GiHub 上,使用 Go 語言編寫。 簡而言之,每當(dāng)我們推送 JavaScript 的配置到 git,它都會(huì)馬上在全球范圍里面部署好 DNS 配置。這里有一個(gè)簡單的例子,我們拿 askubuntu.com 做示范:
D('askubuntu.com', REG_NAMECOM,
DnsProvider(R53,2),
DnsProvider(GOOGLECLOUD,2),
SPF,
TXT('@', 'google-site-verification=PgJFv7ljJQmUa7wupnJgoim3Lx22fbQzyhES7-Q9cv8'), // webmasters
A('@', ADDRESS24, FASTLY_ON),
CNAME('www', '@'),
CNAME('chat', 'chat.stackexchange.com.'),
A('meta', ADDRESS24, FASTLY_ON),
END)
太棒了,接下來我們就可以使用客戶端響應(yīng)測試工具來測試?yán)玻?a href="#preparing-for-a-proxy-client-timings" target="_blank">上面提到的工具可以實(shí)時(shí)告訴我們真實(shí)部署情況,而不是模擬數(shù)據(jù)。但是我們還需要測試所有部分都正常。
測試
客戶端響應(yīng)測試的追蹤可以方便我們做性能測試,但這個(gè)并不適合用來做配置測試??蛻舳隧憫?yīng)測試非常適合展現(xiàn)結(jié)果,但是配置有時(shí)候并沒有界面,所以我們開發(fā)了 httpUnit (后來知道這個(gè)項(xiàng)目重名了 )。這也是一個(gè)使用 Go 語言的開源項(xiàng)目。以 teststackoverflow.com
舉例,使用的配置如下:
[[plan]]
label = "teststackoverflow_com"
url = "http://teststackoverflow.com"
ips = ["28i"]
text = "<title>Test Stack Overflow Domain</title>"
tags = ["so"]
[[plan]]
label = "tls_teststackoverflow_com"
url = "https://teststackoverflow.com"
ips = ["28"]
text = "<title>Test Stack Overflow Domain</title>"
tags = ["so"]
每次我們更新一下防火墻、證書、綁定、跳轉(zhuǎn)時(shí)都有必要測一下。我們必須保證我們的修改不會(huì)影響用戶訪問(先在預(yù)發(fā)布環(huán)境進(jìn)行部署)。 httpUnit 就是我們來做集成測試的工具。
我們還有一個(gè)開發(fā)的內(nèi)部工具(由親愛的 Tom Limoncelli 開發(fā)),用來管理我們負(fù)載均衡上面的 VIP 地址 。我們先在一個(gè)備用負(fù)載均衡上面測試完成,然后將所有流量切過去,讓之前的主負(fù)載均衡保持一個(gè)穩(wěn)定狀態(tài)。如果期間發(fā)生任何問題,我們可以輕易回滾。如果一切順利,我們就把這個(gè)變更應(yīng)用到那臺(tái)負(fù)載均衡上。這個(gè)工具叫做 keepctl
(keepalived control 的簡稱),時(shí)間允許的話很快就會(huì)整理開源出來。
應(yīng)用層準(zhǔn)備
上面提到的只是架構(gòu)方面的工作。這通常是由 Stack Overflow 的幾名網(wǎng)站可靠性工程師組成的團(tuán)隊(duì)完成的。而應(yīng)用層也有很多需要完成的工作。這個(gè)列表會(huì)很長,先讓我拿點(diǎn)咖啡和零食再慢慢說。
很重要的一點(diǎn)是,Stack Overflow 與 Stack Exchange 的架構(gòu) Q&A 采用了多租戶技術(shù)。這意味著如果你訪問 stackoverflow.com
或者 superuser.com
又或者 bicycles.stackexchange.com
,你返回到的其實(shí)是同一臺(tái)服務(wù)器上的同一個(gè) w3wp.exe
進(jìn)程。我們通過瀏覽器發(fā)送的 Host
請求頭來改變請求的上下文。為了更好地理解我們下文中提到的一些概念,你需要知道我們代碼中的 Current.Site
其實(shí)指的是 請求 中的站點(diǎn)。Current.Site.Url()
和 Current.Site.Paths.FaviconUrl
也是基于同樣的概念。
換一句話說:我們的 Q&A 全站都是跑在同一個(gè)服務(wù)器上的同一個(gè)進(jìn)程,而用戶對此沒有感知。我們在九臺(tái)服務(wù)器上每一臺(tái)跑一個(gè)進(jìn)程,只是為了發(fā)布版本和冗余的問題。
全局登錄
整個(gè)項(xiàng)目中有一些看起來可以獨(dú)立出來(事實(shí)上也是),不過也同屬于整個(gè)大 HTTPS 遷移中的一部分。登錄就是其中一個(gè)項(xiàng)目。我首先來說說這個(gè),因?yàn)檫@比別它變化都要早上線。
在 Stack Overflow(及 Stack Exchange)的頭五六年里,你登錄的是一個(gè)個(gè)的獨(dú)立網(wǎng)站。比如,stackoverflow.com
、stackexchange.com
以及 gaming.stackexchange.com
都有它們自己的 cookies。值得注意的是:meta.gaming.stackexchange.com
的登錄 cookie 是從 gaming.stackexchange.com
帶過來的。這些是我們上面討論證書時(shí)提到的 meta 站點(diǎn)。他們的登錄信息是相關(guān)聯(lián)的,你只能通過父站點(diǎn)登錄。在技術(shù)上說并沒有什么特別的,但考慮到用戶體驗(yàn)就很糟糕了。你必須一個(gè)一個(gè)站登錄。我們用「全局認(rèn)證」的方法來「修復(fù)」了這個(gè)問題,方法是在頁面上放一個(gè) <iframe>
,內(nèi)面訪問一下 stackauth.com
。如果用戶在別處登錄過的話,它也會(huì)在這個(gè)站點(diǎn)上登錄,至少會(huì)去試試。這個(gè)體驗(yàn)還行,但是會(huì)有彈出框問你是否點(diǎn)擊重載以登錄,這樣就又不是太好。我們可以做得更好的。對了,你也可以去問問 Kevin Montrose 關(guān)于移動(dòng) Safari 的匿名模式,你會(huì)震驚的。
于是我們有了「通用登錄」。為什么用「通用」這個(gè)名字?因?yàn)槲覀円呀?jīng)用過「全局」了。我們就是如此單純。所幸 cookies 也很單純的東西。父域名里的 cookie(如 stackexchange.com
)在你的瀏覽器里被帶到所有子域名里去(如 gaming.stackexchange.com
)。如果我們只二級(jí)域名的話,其實(shí)我們的域名并不多:
- askubuntu.com
- mathoverflow.net
- serverfault.com
- stackapps.com
- stackexchange.com
- stackoverflow.com
- superuser.com
是的,我們有一些域名是跳轉(zhuǎn)到上面的列表中的,比如 askdifferent.com。但是這些只是跳轉(zhuǎn)而已,它們沒有 cookies 也無需登錄。
這里有很多細(xì)節(jié)的后端工作我沒有提(歸功于 Geoff Dalgas 和 Adam Lear),但大體思路就是,當(dāng)你登錄的時(shí)候,我們把這些域名都寫入一個(gè) cookie。我們是通過第三方的 cookie 和隨機(jī)數(shù)來做的。當(dāng)你登錄其中任意一個(gè)網(wǎng)站的時(shí)候,我們在頁面上都會(huì)放 6 個(gè) <img>
標(biāo)簽來往其它域名寫入 cookie,本質(zhì)上就完成了登錄工作。這并不能在 所有情況 下都適用(尤其是移動(dòng) Safari 簡直是要命了),但和之前比起來那是好得多了。
客戶端的代碼不復(fù)雜,基本上長這樣:
$.post('/users/login/universal/request', function (data, text, req) {
$.each(data, function (arrayId, group) {
var url = '//' + group.Host + '/users/login/universal.gif?authToken=' +
encodeURIComponent(group.Token) + '&nonce=' + encodeURIComponent(group.Nonce);
$(function () { $('#footer').append('</img>'); });
});
}, 'json');
但是要做到這點(diǎn),我們必須上升到賬號(hào)級(jí)別的認(rèn)證(之前是用戶級(jí)別)、改變讀取 cookie 的方式、改變這些 meta 站的登錄工作方式,同時(shí)還要將這一新的變動(dòng)整合到其它應(yīng)用中。比如說,Careers(現(xiàn)在拆成了 Talent 和 Jobs)用的是另一份代碼庫。我們需要讓這些應(yīng)用讀取相應(yīng)的 cookies,然后通過 API 調(diào)用 Q&A 應(yīng)用來獲取賬戶。我們部署了一個(gè) NuGet 庫來減少重復(fù)代碼。底線是:你在一個(gè)地方登錄,就在所有域名都登錄。不彈框,不重載頁面。
技術(shù)的層面上看,我們不用再關(guān)心 *.*.stackexchange.com
是什么了,只要它們是 stackexchange.com
下就行。這看起來和 HTTPS 沒有關(guān)系,但這讓我們可以把 meta.gaming.stackexchange.com
變成 gaming.meta.stackexchange.com
而不影響用戶。
本地 HTTPS 開發(fā)
要想做得更好的話,本地環(huán)境應(yīng)該盡量與開發(fā)和生產(chǎn)環(huán)境保持一致。幸好我們用的是 IIS,這件事情還簡單的。我們使用一個(gè)工具來設(shè)置開發(fā)者環(huán)境,這個(gè)工具的名字叫「本地開發(fā)設(shè)置」——單純吧?它可以安裝工具(Visual Studio、git、SSMS 等)、服務(wù)(SQL Server、Redis、Elasticsearch)、倉庫、數(shù)據(jù)庫、網(wǎng)站以及一些其它東西。做好了基本的工具設(shè)置之后,我們要做的只是添加 SSL/TLS 證書。主要的思路如下:
Websites = @(
@{
Directory = "StackOverflow";
Site = "local.mse.com";
Aliases = "discuss.local.area51.lse.com", "local.sstatic.net";
Databases = "Sites.Database", "Local.StackExchange.Meta", "Local.Area51", "Local.Area51.Meta";
Certificate = $true;
},
@{
Directory = "StackExchange.Website";
Site = "local.lse.com";
Databases = "Sites.Database", "Local.StackExchange", "Local.StackExchange.Meta", "Local.Area51.Meta";
Certificate = $true;
}
)
我把使用到的代碼放在了一個(gè) gist 上:Register-Websites.psm1
。我們通過 host 頭來設(shè)置網(wǎng)站(通過別名添加),如果直連的話就給它一個(gè)證書(嗯,現(xiàn)在應(yīng)該把這個(gè)行為默認(rèn)改為 $true
了),然后允許 AppPool 賬號(hào)來訪問數(shù)據(jù)庫,于是我們本地也在使用 https://
開發(fā)了。嗯,我知道我們應(yīng)該把這個(gè)設(shè)置過程開源出來,不過我們?nèi)孕枞サ粢恍S械臉I(yè)務(wù)。會(huì)有這么一天的。
為什么這件事情很重要? 在此之前,我們從 /content
加載靜態(tài)內(nèi)容,而不是從另一個(gè)域名。這很方便,但也隱藏了類似于跨域請求(CORS)的問題。在同一個(gè)域名下用同一個(gè)協(xié)議能正常加載的資源,換到開發(fā)或者生產(chǎn)環(huán)境下就有可能出錯(cuò)。「在我這里是好的。」
當(dāng)我們使用和生產(chǎn)環(huán)境中同樣協(xié)議以及同樣架構(gòu)的 CDN 還有域名設(shè)置時(shí),我們就可以在開發(fā)機(jī)器上找出并修復(fù)更多的問題。比如,你是否知道,從 https://
跳轉(zhuǎn)到 http://
時(shí),瀏覽器是不會(huì)發(fā)送 referer 的?這是一個(gè)安全上的問題,referer 頭中可能帶有以明文傳輸?shù)拿舾行畔ⅰ?/p>
「Nick 你就扯吧,我們能拿到從 Google 拿到 referer 啊!」確實(shí)。但是這是因?yàn)樗麄?em>主動(dòng)選擇這一行為。如果你看一下 Google 的搜索頁面,你可以看到這樣的 <meta>
指令:
<meta content="origin" id="mref" name="referrer">
這也就是為什么你可以取到 referer。
好的,我們已經(jīng)設(shè)置好了,現(xiàn)在該做些什么呢?