原文地址:HTTPS on Stack Overflow: The End of a Long Road
作者:Nick Craver
譯者: 羅晟 & 狄敬超
前文地址:【翻譯】Stack Overflow 的 HTTPS 化:漫漫長路的終點(一)
Cloudflare
我們評估了很多 CDN/DDoS 防護層供應(yīng)商。最終選擇了 Cloudflare,主要是考慮到他們的基礎(chǔ)設(shè)施怜姿、快速響應(yīng)、還有他們承諾的 Railgun援奢。那么我們?nèi)绾螠y試使用了 Cloudfalre 之后用戶的真實效果?是否需要部署服務(wù)來獲取用戶數(shù)據(jù)造虏?答案是不需要灶芝!
Stack Overflow 的數(shù)據(jù)量非常大:月 PV 過十億。記得我們上面講的客戶端耗時紀(jì)錄嗎奸晴?我們每天都有幾百萬的訪問了冤馏,所以不是直接可以問他們嗎?我們是可以這么做寄啼,只需要在頁面中嵌入 <iframe>
就行了逮光。Cloudflare 已經(jīng)是我們 cdn.sstatic.net(我們共用的無 cookie 的靜態(tài)內(nèi)容域)的托管商了。但是這是通過一條CNAME
DNS 紀(jì)錄來做的墩划,我們把 DNS 指向他們的 DNS涕刚。所以要用 Cloudflare 來當(dāng)代理服務(wù)的話,我們需要他們指向我們的 DNS乙帮。所以我們先需要測試他們 DNS 的性能杜漠。
實際上,要測試性能我們需要把二級域名給他們察净,而不是 something.stackoverflow.com
驾茴,因為這樣可能會有不一致的膠水記錄而導(dǎo)致多次查詢。明確一下氢卡,一級域名 (TLDs)指的是 .com
, .net
, .org
, .dance
, .duck
, .fail
, .gripe
, .here
, .horse
, .ing
, .kim
, .lol
, .ninja
, .pink
, .red
, .vodka
. 和 .wtf
锈至。 注意,這些域名尾綴都是译秦,我可沒開玩笑峡捡。 二級域名 (SLDs) 就多了一級,比如 stackoverflow.com
, superuser.com
等等筑悴。我們需要測的就是這些域名的行為及表現(xiàn)们拙。因此,我們就有了 teststackoverflow.com
阁吝,通過這個新域名砚婆,我們在全球范圍內(nèi)測試 DNS 性能。對一部分比例的用戶突勇,通過嵌一個 <iframe>
(在測試中開關(guān))装盯,我們可以輕松地獲取用戶訪問 DNS 的相關(guān)數(shù)據(jù)。
注意与境,測試過程最少需要 24 小時。在各個時區(qū)猖吴,互聯(lián)網(wǎng)的表現(xiàn)會隨著用戶作息或者 Netflix 的使用情況等發(fā)生變化摔刁。所以要測試一個國家,需要完整的一天數(shù)據(jù)海蔽。最好是在工作日(而不要半天落在周六)共屈。我們知道會有各種意外情況绑谣。互聯(lián)網(wǎng)的性能并不是穩(wěn)定的拗引,我們要通過數(shù)據(jù)來證明這一點借宵。
我們最初的假設(shè)是,多增加了的一個節(jié)點會帶來額外的延時矾削,我們會因此損失一部分頁面加載性能壤玫。但是 DNS 性能上的增加其實彌補了這一塊。比起我們只有一個數(shù)據(jù)中心來說哼凯,Cloudflare 的 DNS 服務(wù)器部署在離用戶更近的地方欲间,這一塊性能要好得多得多。我希望我們能有空來放出這一塊的數(shù)據(jù)断部,只不過這一塊需要很多處理(以及托管)猎贴,而我現(xiàn)在也沒有足夠多的時間。
接下來蝴光,我們開始將 teststackoverflow.com
放在 Cloudflare 的代理上做鏈路加速她渴,同樣也是放在 <iframe>
中。我們發(fā)現(xiàn)美國和加拿大的服務(wù)由于多余的節(jié)點而變慢蔑祟,但是世界其他地方都是持平或者更好趁耗。這滿足我們的期望。我們開始使用 Cloudflare 的網(wǎng)絡(luò)對接我們的服務(wù)做瞪。期間發(fā)生了一些 DDos 的攻擊对粪,不過這是另外的事了。那么装蓬,為什么我們接受在美國和加拿大地區(qū)慢一點呢著拭?因為每個頁面加載需要的時間僅為 200-300ms,哪怕慢一點也還是飛快牍帚。當(dāng)時我們認(rèn)為 Railgun 可以將這些損耗彌補回來儡遮。
這些測試完成之后,我們?yōu)榱祟A(yù)防 DDos 工作暗赶,做了一些其他工作鄙币。我們接入了額外的 ISP 服務(wù)商以供我們的 CDN/代理層對接。畢竟如果能繞過攻擊的話蹂随,我們沒必要在代理層做防護∈伲現(xiàn)在每個機房都有 4 個 ISP 服務(wù)商(譯者注:相當(dāng)于電信、聯(lián)通岳锁、移動绩衷、教育網(wǎng)),兩組路由器,他們之間使用 BGP 協(xié)議咳燕。我們還額外添置了兩組負(fù)載均衡器專門用于處理 CDN/代理層的流量勿决。
Cloudflare: Railgun
與此配套,我們啟用了兩組 Railgun招盲。Railgun 的原理是在 Cloudflare 那邊低缩,使用 memcached 匹配 URL 進行緩存數(shù)據(jù)。當(dāng) Railgun 啟用的時候曹货,每個頁面(有一個大小閾值)都會被緩存下來咆繁。那么在下一次請求時候,如果在這個 URL 在 Cloudflare 節(jié)點上和我們這里都緩存的話控乾,我們?nèi)匀粫?web 服務(wù)器最新的數(shù)據(jù)么介。但是我們不需要傳輸完整的數(shù)據(jù),只需要把傳輸和上次請求的差異數(shù)據(jù)傳給 Cloudflure蜕衡。他們把這個差異運用于他們的緩存上壤短,然后再發(fā)回給客戶端。這時候慨仿, gzip 壓縮 的操作也從 Stack Overflow 的 9 臺 Web Server 轉(zhuǎn)移到了一個 Railgun 服務(wù)上久脯,這臺服務(wù)器得是 CPU 密集型的——我指出這點是因為,這項服務(wù)需要評估镰吆、購買帘撰,并且部署在我們這邊。
舉個例子万皿,想象一下摧找,兩個用戶打開同一個問題的頁面。從瀏覽效果來看牢硅,他們的頁面技術(shù)上長得幾乎一樣蹬耘,僅僅有細(xì)微的差別。如果我們大部分的傳輸內(nèi)容只是一個 diff 的話减余,這將是一個巨大的性能提升综苔。
總而言之,Railgun 通過減少大量數(shù)據(jù)傳輸?shù)姆绞教岣咝阅芪徊怼.?dāng)它順利工作的時候確實是這樣如筛。除此之外,還有一個額外的優(yōu)點:請求不會重置連接抒抬。由于 TCP 慢啟動杨刨,當(dāng)連接環(huán)境較為復(fù)雜時候,可能導(dǎo)致連接被限流擦剑。而 Railgun 始終以固定的連接數(shù)連接到 Cloudflare 的終端妖胀,對用戶請求采用了多路復(fù)用可免,從而其不會受慢啟動影響。小的 diff 也減少了慢啟動的開銷做粤。
很可惜,我們由于種種原因我們在使用 Railgun 過程中一直遇到問題捉撮。據(jù)我所知怕品,我們擁有當(dāng)時最大的 Railgun 部署規(guī)模,這把 Railgun 逼到了極限巾遭。盡管我們花了一年追蹤各種問題肉康,最終還是不得不放棄了。這種狀況不僅沒有給我們省錢灼舍,還耗費了更多的精力『鸷停現(xiàn)在幾年過去了。如果你正在評估使用 Railgun骑素,你最好看最新的版本炫乓,他們一直在做優(yōu)化。我也建議你自己做決定是否使用 Railgun献丑。
Fastly
我們最近才遷到 Fastly末捣,因為我們在講 CDN/代理層,我也會順帶一提创橄。由于很多技術(shù)工作在 Cloudflare 那邊已經(jīng)完成箩做,所以遷移本身并沒有什么值得說的。大家會更感興趣的是:為什么遷移妥畏?畢竟 Cloudflare 在各方面是不錯的:豐富的數(shù)據(jù)中心邦邦、穩(wěn)定的帶寬價格、包含 DNS 服務(wù)醉蚁。答案是:它不再是我們最佳的選擇了燃辖。Flastly 提供了一些我們更為看中的特性:靈活的終端節(jié)點控制能力、配置快速分發(fā)馍管、自動配置分發(fā)郭赐。并不是說 Cloudflare 不行,只是它不再適合 Stack Overflow 了确沸。
事實勝于雄辯:如果我不認(rèn)可 Cloudflare捌锭,我的私人博客不可能選擇它,嘿罗捎,就是這個博客观谦,你現(xiàn)在正在閱讀的。
Fastly 吸引我們的主要功能是提供了 Varnish 和 VCL桨菜。這提供了高度的終端可定制性豁状。有些功能吧捉偏,Cloudfalre 無法快速提供(因為他們是通用化的,會影響所有用戶)泻红,在 Fastly 我們可以自己做夭禽。這是這兩家架構(gòu)上的差異,這種「代碼級別高可配置」對于我們很適用谊路。同時讹躯,我們也很喜歡他們在溝通、基礎(chǔ)設(shè)施的開放性缠劝。
我來展示一個 VCL 好用在哪里的例子潮梯。最近我們遇到 .NET 4.6.2 的一個超惡心 bug,它會導(dǎo)致 max-age 有超過 2000 年的緩存時間惨恭”螅快速解決方法是在終端節(jié)點上有需要的時候去覆蓋掉這個頭部,當(dāng)我寫這篇文章的時候脱羡,這個 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ù)據(jù)量還好)萝究,其余頁面都不設(shè)置。這是一個為解決緊急時間的非常便于部署的全局性解決方案锉罐。 我們很開心現(xiàn)在有能力在終端做一些事情糊肤。我們的 Jason Harvey 負(fù)責(zé) VCL 配置,并寫了一些自動化推送的功能氓鄙。我們基于一個 Go 的開源庫 fastlyctl 做了開發(fā)馆揉。
另一個 Fastly 的特點是可以使用我們自己的證書,Cloudflare 雖然也有這個服務(wù)抖拦,但是費用太高升酣。如我上文提到的,我們現(xiàn)在已經(jīng)具備使用 HTTP/2 推送的能力态罪。但是噩茄,F(xiàn)astly 就不支持 DNS,這個在 Cloudflare 那里是支持的「淳保現(xiàn)在我們需要自己解決 DNS 的問題了绩聘。可能最有意思的就是這些來回的折騰吧耗啦?
全局 DNS
當(dāng)我們從 Cloudflare 遷移到 Fastly 時候凿菩,我們必須評估并部署一個新的 DNS 供應(yīng)商。這里有篇 Mark Henderson 寫的 文章 帜讲。鑒于此衅谷,我們必須管理:
- 我們自己的 DNS 服務(wù)器(備用)
- Name.com 的服務(wù)器(為了那些不需要 HTTPS 的跳轉(zhuǎn)服務(wù))
- Cloudflare DNS
- Route 53 DNS
- Google DNS
- Azure DNS
- 其他一些(測試時候使用)
這個本身就是另一個項目了。為了高效管理似将,我們開發(fā)了 DNSControl获黔。這現(xiàn)在已經(jīng)是開源項目了蚀苛,托管在 GiHub 上,使用 Go 語言編寫玷氏。 簡而言之堵未,每當(dāng)我們推送 JavaScript 的配置到 git,它都會馬上在全球范圍里面部署好 DNS 配置盏触。這里有一個簡單的例子兴溜,我們拿 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ù)據(jù)。但是我們還需要測試所有部分都正常刨沦。
測試
客戶端響應(yīng)測試的追蹤可以方便我們做性能測試诗宣,但這個并不適合用來做配置測試∠胱纾客戶端響應(yīng)測試非常適合展現(xiàn)結(jié)果召庞,但是配置有時候并沒有界面,所以我們開發(fā)了 httpUnit (后來知道這個項目重名了 )来破。這也是一個使用 Go 語言的開源項目篮灼。以 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)時都有必要測一下送朱。我們必須保證我們的修改不會影響用戶訪問(先在預(yù)發(fā)布環(huán)境進行部署)娘荡。 httpUnit 就是我們來做集成測試的工具。
我們還有一個開發(fā)的內(nèi)部工具(由親愛的 Tom Limoncelli 開發(fā))驶沼,用來管理我們負(fù)載均衡上面的 VIP 地址 炮沐。我們先在一個備用負(fù)載均衡上面測試完成,然后將所有流量切過去回怜,讓之前的主負(fù)載均衡保持一個穩(wěn)定狀態(tài)大年。如果期間發(fā)生任何問題,我們可以輕易回滾玉雾。如果一切順利翔试,我們就把這個變更應(yīng)用到那臺負(fù)載均衡上。這個工具叫做 keepctl
(keepalived control 的簡稱)复旬,時間允許的話很快就會整理開源出來遏餐。
應(yīng)用層準(zhǔn)備
上面提到的只是架構(gòu)方面的工作。這通常是由 Stack Overflow 的幾名網(wǎng)站可靠性工程師組成的團隊完成的赢底。而應(yīng)用層也有很多需要完成的工作失都。這個列表會很長柏蘑,先讓我拿點咖啡和零食再慢慢說。
很重要的一點是粹庞,Stack Overflow 與 Stack Exchange 的架構(gòu) Q&A 采用了多租戶技術(shù)咳焚。這意味著如果你訪問 stackoverflow.com
或者 superuser.com
又或者 bicycles.stackexchange.com
,你返回到的其實是同一臺服務(wù)器上的同一個 w3wp.exe
進程庞溜。我們通過瀏覽器發(fā)送的 Host
請求頭來改變請求的上下文革半。為了更好地理解我們下文中提到的一些概念,你需要知道我們代碼中的 Current.Site
其實指的是 請求 中的站點流码。Current.Site.Url()
和 Current.Site.Paths.FaviconUrl
也是基于同樣的概念又官。
換一句話說:我們的 Q&A 全站都是跑在同一個服務(wù)器上的同一個進程,而用戶對此沒有感知漫试。我們在九臺服務(wù)器上每一臺跑一個進程六敬,只是為了發(fā)布版本和冗余的問題。
全局登錄
整個項目中有一些看起來可以獨立出來(事實上也是)驾荣,不過也同屬于整個大 HTTPS 遷移中的一部分外构。登錄就是其中一個項目。我首先來說說這個播掷,因為這比別它變化都要早上線审编。
在 Stack Overflow(及 Stack Exchange)的頭五六年里,你登錄的是一個個的獨立網(wǎng)站歧匈。比如垒酬,stackoverflow.com
、stackexchange.com
以及 gaming.stackexchange.com
都有它們自己的 cookies件炉。值得注意的是:meta.gaming.stackexchange.com
的登錄 cookie 是從 gaming.stackexchange.com
帶過來的伤溉。這些是我們上面討論證書時提到的 meta 站點。他們的登錄信息是相關(guān)聯(lián)的妻率,你只能通過父站點登錄乱顾。在技術(shù)上說并沒有什么特別的,但考慮到用戶體驗就很糟糕了宫静。你必須一個一個站登錄走净。我們用「全局認(rèn)證」的方法來「修復(fù)」了這個問題,方法是在頁面上放一個 <iframe>
孤里,內(nèi)面訪問一下 stackauth.com
伏伯。如果用戶在別處登錄過的話,它也會在這個站點上登錄捌袜,至少會去試試说搅。這個體驗還行,但是會有彈出框問你是否點擊重載以登錄虏等,這樣就又不是太好弄唧。我們可以做得更好的适肠。對了,你也可以去問問 Kevin Montrose 關(guān)于移動 Safari 的匿名模式候引,你會震驚的侯养。
于是我們有了「通用登錄」。為什么用「通用」這個名字澄干?因為我們已經(jīng)用過「全局」了逛揩。我們就是如此單純。所幸 cookies 也很單純的東西麸俘。父域名里的 cookie(如 stackexchange.com
)在你的瀏覽器里被帶到所有子域名里去(如 gaming.stackexchange.com
)辩稽。如果我們只二級域名的話,其實我們的域名并不多:
- 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)你登錄的時候,我們把這些域名都寫入一個 cookie嘲碧。我們是通過第三方的 cookie 和隨機數(shù)來做的稻励。當(dāng)你登錄其中任意一個網(wǎng)站的時候,我們在頁面上都會放 6 個 <img>
標(biāo)簽來往其它域名寫入 cookie愈涩,本質(zhì)上就完成了登錄工作望抽。這并不能在 所有情況 下都適用(尤其是移動 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('![](' + url + ')</img>'); });
});
}, 'json');
但是要做到這點,我們必須上升到賬號級別的認(rèn)證(之前是用戶級別)毁腿、改變讀取 cookie 的方式辑奈、改變這些 meta 站的登錄工作方式,同時還要將這一新的變動整合到其它應(yīng)用中已烤。比如說鸠窗,Careers(現(xiàn)在拆成了 Talent 和 Jobs)用的是另一份代碼庫。我們需要讓這些應(yīng)用讀取相應(yīng)的 cookies胯究,然后通過 API 調(diào)用 Q&A 應(yīng)用來獲取賬戶稍计。我們部署了一個 NuGet 庫來減少重復(fù)代碼。底線是:你在一個地方登錄裕循,就在所有域名都登錄臣嚣。不彈框净刮,不重載頁面。
技術(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穷吮,這件事情還簡單的。我們使用一個工具來設(shè)置開發(fā)者環(huán)境饥努,這個工具的名字叫「本地開發(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;
}
)
我把使用到的代碼放在了一個 gist 上:Register-Websites.psm1
漾狼。我們通過 host 頭來設(shè)置網(wǎng)站(通過別名添加),如果直連的話就給它一個證書(嗯饥臂,現(xiàn)在應(yīng)該把這個行為默認(rèn)改為 $true
了)逊躁,然后允許 AppPool 賬號來訪問數(shù)據(jù)庫,于是我們本地也在使用 https://
開發(fā)了隅熙。嗯志衣,我知道我們應(yīng)該把這個設(shè)置過程開源出來,不過我們?nèi)孕枞サ粢恍S械臉I(yè)務(wù)猛们。會有這么一天的念脯。
為什么這件事情很重要? 在此之前弯淘,我們從 /content
加載靜態(tài)內(nèi)容绿店,而不是從另一個域名。這很方便,但也隱藏了類似于跨域請求(CORS)的問題假勿。在同一個域名下用同一個協(xié)議能正常加載的資源借嗽,換到開發(fā)或者生產(chǎn)環(huán)境下就有可能出錯。「在我這里是好的转培《竦迹」
當(dāng)我們使用和生產(chǎn)環(huán)境中同樣協(xié)議以及同樣架構(gòu)的 CDN 還有域名設(shè)置時,我們就可以在開發(fā)機器上找出并修復(fù)更多的問題浸须。比如惨寿,你是否知道,從 https://
跳轉(zhuǎn)到 http://
時删窒,瀏覽器是不會發(fā)送 referer 的裂垦?這是一個安全上的問題,referer 頭中可能帶有以明文傳輸?shù)拿舾行畔ⅰ?/p>
「Nick 你就扯吧肌索,我們能拿到從 Google 拿到 referer 敖堵!!」確實诚亚。但是這是因為他們主動選擇這一行為晕换。如果你看一下 Google 的搜索頁面,你可以看到這樣的 <meta>
指令:
<meta content="origin" id="mref" name="referrer">
這也就是為什么你可以取到 referer。
好的,我們已經(jīng)設(shè)置好了,現(xiàn)在該做些什么呢?