原文做者:Nick Craver
翻譯做者:羅晟 & 狄敬超(滬江工程師)
原文地址:nickcraver.com/blog/2017/0…
本文爲原創翻譯文章,已經得到原做者受權,轉載請註明做者及出處。javascript
今天,咱們默認在 Stack Overflow 上部署了 HTTPS。目前全部的流量都將跳轉到 https://
上。與此同時,咱們也會在接下去的幾周內更改Google 連接。啓用 HTTPS 的過程自己只是舉手之勞,但在此以前咱們卻花了好幾年的時間。到目前爲止,HTTPS 在咱們全部的 Q&A 網站上都默認啓用了。php
在過去的兩個月裏,咱們在 Stack Exchange 全網維持發佈 HTTPS。Stack Overflow 是最後,也是迄今最大的的一個站點。這對咱們來講是一個巨大里程碑,但決不意味着是終點。後文會提到咱們仍有不少須要作的事情,但如今咱們總算能看得見終點了,耶!css
友情提示:這篇文章講述的是一個漫長的旅程。很是漫長。你可能已經注意到你的滾動條如今很是小。咱們遇到的問題並非只在 Stack Exchange/Overflow 纔有,但這些問題的組合還挺罕見。我在文章中會講到咱們的一些嘗試、折騰、錯誤、成功,也會包括一些開源項目——但願這些細節對大家有所幫助。因爲它們的關係錯綜複雜,我難以用時間順序來組織這篇文章,因此我會將文章拆解成架構、應用層、錯誤等幾個主題。html
首先,咱們要提一下爲何咱們的處境相對獨特:前端
因爲這篇文章實在太長,我在這裏先列出連接:java
咱們早在 2013 年就開始考慮在 Stack Overflow 上部署 HTTPS 了。是的,如今是 2017 年。因此,到底是什麼拖了咱們四年?這個問題的答案放在任何一個 IT 項目上都適用:依賴和優先級。老實說,Stack Overflow 在信息安全性上的要求並不像別家那麼高。咱們不是銀行,也不是醫院,咱們也不涉及信用卡支付,甚至於咱們每一個季度都會經過 HTTP 和 BT 種子的方式發佈咱們大部分的數據庫。這意味着,從安全的角度來看,這件事情的緊急程度不像它在其餘領域裏那麼高。而從依賴上來講,咱們的複雜度比別人要高,在部署 HTTPS 時會在幾大領域裏踩坑,這些問題的組合是比較特殊的。後文中會看到,有一些域名的問題仍是一直存在的。git
容易踩坑的幾個領域包括:github
那咱們到底是爲何須要 HTTPS 呢?由於數據並非惟一須要安全性的東西。咱們的用戶中有操做員、開發者、還有各個級別的公司員工。咱們但願他們到咱們站點的通訊是安全的。咱們但願每個用戶的瀏覽歷史是安全的。某些用戶暗地裏喜歡 monad 卻又懼怕被人發現。同時,Google 會提高 HTTPS 站點的搜索排名(雖然咱們不知道能提高多少)。golang
哦,還有性能。咱們熱愛性能。我熱愛性能。你熱愛性能。個人狗熱愛性能。讓我給你一個性能的擁抱。很好。謝謝。你聞起來很香。web
不少人喜歡情人包,因此咱們來一場快速問答(咱們喜歡問答!):
讓咱們先聊聊證書,由於這是最容易被誤解的部分。很多朋友跟我說,他安裝了 HTTPS 證書,所以他們已經完成 HTTPS 準備了。呵呵,麻煩你看一眼右側那個小小的滾動條,這篇文章纔剛剛開始,你以爲真的這麼簡單麼?我有這個必要告訴大家一點人生的經驗 :沒這麼容易的。
一個最多見的問題是:「爲什麼不直接用 Let’s Encrypt?」
答案是:這個方案不適合咱們。 Let's Encrypt 的確是一個偉大的產品,我但願他們可以長期服務於你們。當你只有一個或少數幾個域名時,它是很是出色的選擇。可是很惋惜,咱們 Stack Exchange 有數百個站點,而 Let’s Encrypt 並不支持通配域名配置。這致使 Let's Encrypt 沒法知足咱們的需求。要這麼作,咱們就不得不在每上一個新的 Q&A 站點的時候都部署一個(或兩個)證書。這樣會增長咱們部署的複雜性,而且咱們要麼放棄不支持 SNI 的客戶端(大約佔 2% 的流量)要麼提供超多的 IP——而咱們目前沒這麼多的 IP。
咱們之因此想控制證書,還有另一個緣由是咱們想在本地負載均衡器以及 CDN / 代理提供商那邊使用完成相同的證書。若是不作到這個,咱們沒法順暢地作從代理那裏作失效備援(failover)。支持 HTTP 公鑰固定(HPKP)的客戶端會報認證失敗。雖然咱們仍在評估是否使用 HPKP,可是若是有一天要用的話咱們得提早作好準備。
不少朋友在看見咱們的主證書時候會嚇得目瞪口呆,由於它包含了咱們的主域名和通配符子域名。它看上去長成這樣:
爲何這麼作?老實說,是咱們讓 DigiCert 替咱們作的。這麼作會致使每次發生變化的時候都須要手動合併證書,了 咱們爲何要忍受這麼麻煩的事呢?首先,咱們指望可以儘量讓更多用戶使用咱們產品。這裏麪包括了那些還不支持 SNI 的用戶(好比在咱們項目啓動的時候 Android 2.3 勢頭正猛)。另外,也包括 HTTP/2 與一些現實問題——咱們過會兒會談到這一塊。
Stack Exchage 的一個設計理念是,針對每一個 Q&A 站點,咱們都有一個地方供討論。咱們稱之爲 「second place」。好比 meta.gaming.stackexchange.com
用來討論 gaming.stackexchange.com
。這個有什麼特別之處呢?好吧,並無,除了域名:這是一個 4 級域名。
我以前已經說過這個問題,但後來怎麼樣了呢?具體來講,如今面臨的問題是 *.stackexchange.com
包含 gaming.stackexchange.com
(及幾百個其它站點),但它並不包含 meta.gaming.stackexchange.com
。RFC 6125 (第 6.4.3 節) 寫道:
客戶端 不該該 嘗試匹配一個通配符在中間的域名(好比,不要匹配
bar.*.example.net
)
這意味着咱們沒法使用 meta.*.stackexchange.com
,那怎麼辦呢?
meta.*
這種形式的域名配置額外的 DNS 詞條
*.meta.stackexchange.com
咱們部署了 全局登陸系統,而後將子 meta 域名用 301 重定向到新地址,好比 gaming.meta.stackexchange.com。作完這個以後咱們才意識到,由於這些域名*曾經*存在過,因此對於 HSTS 預加載來講是個很大的問題。這件事情還在進行中,我會在文章最後面討論這個問題。這類問題對於 meta.pt.stackoverflow.com
等站點也存在,不過還好咱們只有四個非英語版本的 Stack Overflow,因此問題沒有被擴大。
對了,這個方案自己還存在另外一個問題。因爲將 cookies 移動到頂級目錄,而後依賴於子域名對其的繼承,咱們必須調整一些其餘域名。好比,在咱們新系統中,咱們使用 SendGrid 來發送郵件(進行中)。咱們從 stackoverflow.email
這個域名發郵件,郵件內容裏的連接域名是 sg-links.stackoverflow.email
(使用 CNAME 管理),這樣你的瀏覽器就不會將敏感的 cookie 發出去。若是這個域名是 links.stackoverflow.com
,那麼你的瀏覽器會將你在這個域名下的 cookie 發送出去。 咱們有很多雖然使用咱們的域名,但並不屬於咱們本身的服務。這些子域名都須要從咱們受信的域名下移走,不然咱們就會把大家的 cookie 發給非咱們自有的服務器上。若是由於這種錯誤而致使 cookie 數據泄露,這將是件很丟人的事情。
咱們有試過經過代理的方式來訪問咱們的 Hubspot CRM 網站,在傳輸過程當中能夠將 cookies 移除掉。可是很不幸 Hubspot 使用 Akamai,它會斷定咱們的 HAProxy 實例是機器人,並將其封掉。頭三次的時候還挺有意思的……固然這也說明這個方式真的無論用。咱們後來再也沒試過了。
你是否好奇爲何 Stack Overflow 的博客地址是 stackoverflow.blog/?沒錯,這也是出於安全目的。咱們把博客搭在一個外部服務上,這樣市場部門和其餘團隊可以更便利地使用。正由於這樣,咱們不能把它放在有 cookie 的域名下面。
上面的方案會牽涉到子域名,引出 HSTS 預加載 和 includeSubDomains
命令問題,咱們一會來談這塊內容。
好久以前,你們都認爲 HTTPS 更慢。在那時候也確實是這樣。可是時代在變化,咱們說 HTTPS 的時候再也不是單純的 HTTPS,而是基於 HTTPS 的 HTTP/2。雖然 HTTP/2 不要求加密,但事實上倒是加密的。主流瀏覽器都要求 HTTP/2 提供加密鏈接來啓用其大部分特性。你能夠來講 spec 或者規定上不是這麼說的,但瀏覽器纔是你要面對的現實。我誠摯地指望這個協議直接更名叫作 HTTPS/2,這樣也能給你們省點時間。各瀏覽器廠商,大家聽見了嗎?
HTTP/2 有不少功能上的加強,特別是在用戶請求以前能夠主動推送資源這點。這裏我就不展開了,Ilya Grigorik 已經寫了一篇很是不錯的文章。我這裏簡單羅列一下主要優勢:
咦?怎麼沒提到證書呢?
一個不多人知道的特性是,你能夠推送內容到不一樣的域名,只要知足如下的條件:
讓咱們看一下咱們當前 DNS 配置:
λ dig stackoverflow.com +noall +answer
; <<>> DiG 9.10.2-P3 <<>> stackoverflow.com +noall +answer
;; global options: +cmd
stackoverflow.com. 201 IN A 151.101.1.69
stackoverflow.com. 201 IN A 151.101.65.69
stackoverflow.com. 201 IN A 151.101.129.69
stackoverflow.com. 201 IN A 151.101.193.69
λ dig cdn.sstatic.net +noall +answer
; <<>> DiG 9.10.2-P3 <<>> cdn.sstatic.net +noall +answer
;; global options: +cmd
cdn.sstatic.net. 724 IN A 151.101.193.69
cdn.sstatic.net. 724 IN A 151.101.1.69
cdn.sstatic.net. 724 IN A 151.101.65.69
cdn.sstatic.net. 724 IN A 151.101.129.69複製代碼
嘿,這些 IP 都是一致的,而且他們也擁有相同的證書!這意味着你能夠直接使用 HTTP/2 的服務器推送功能,而無需影響 HTTP/1.1 用戶。 HTTP/2 有推送的同時,HTTP/1.1 也有了域名共享(經過 sstatic.net
)。咱們暫未部署服務器推送功能,但一切都盡在掌握之中。
HTTPS 是咱們實現性能目標的一個手段。能夠這麼說,咱們的主要目標是性能,而非站點安全性。咱們想要安全性,但光是安全性不足以讓咱們花那麼多精力來在全網部署 HTTPS。當咱們把全部因素都考慮在一塊兒的時候,咱們能夠評估出要完成這件事情須要付出的巨大的時間和精力。在 2013 年,HTTP/2 尚未扮演那麼重要的角色。而如今形勢變了,對其的支持也多了,最終這成爲了咱們花時間調研 HTTPS 的催化劑。
值得注意的是 HTTP/2 標準在咱們項目進展時還在持續發生變化。它從 SPDY 演化爲 HTTP/2,從 NPN 演化爲 ALPN。咱們這裏不會過多涉及到這部分細節,由於咱們並無爲其作太多貢獻。咱們觀望並從中獲准,但整個互聯網卻在推動其向前發展。若是你感興趣,能夠看看 Cloudflare 是怎麼講述其演變的。
咱們最先在 2013 年開始在 HAProxy 中使用 HTTPS。爲何是 HAProxy 呢?這是歷史緣由,咱們已經在使用它了,而它在 2013 年 的 1.5 開發版中支持了 HTTPS,並在 2014 年發佈了正式版。曾經有段時間,咱們把 Nginx 放置在 HAProxy 以前(詳情看這裏)。可是簡單些老是更好,咱們老是想着要避免在鏈路、部署和其餘問題上的複雜問題。
我不會探討太多細節,由於也沒什麼好說的。HAProxy 在 1.5 以後使用 OpenSSL 支持 HTTPS,配置文件也是清晰易懂的。咱們的配置方式以下:
HAProxy 比較簡單,這是咱們使用一個 SSL 證書來支持 :433 端口的第一步。過後看來,這也只是一小步。
這裏是上面描述狀況下的架構圖,咱們立刻來講前面的那塊雲是怎麼回事:
我對 Stack Overflow 架構的效率一直很自豪。咱們很厲害吧?僅用一個數據中心和幾個服務器就撐起了一個大型網站。不過此次不同了。儘管效率這件事情很好,可是在延遲上就成了個問題。咱們不須要那麼多服務器,咱們也不須要多地擴展(不過咱們有一個災備節點)。這一次,這就成爲了問題。因爲光速,咱們(暫時)沒法解決延遲這個基礎性問題。咱們據說有人已經在處理這個問題了,不過他們造的時間機器好像有點問題。
讓咱們用數字來理解延遲。赤道長度是 40000 千米(光繞地球一圈的最壞狀況)。光速在真空中是 299,792,458 米/秒。不少人用這個數字,但光纖並非真空的。實際上光纖有 30-31% 損耗,因此咱們的這個數字是:(40,075,000 m) / (299,792,458 m/s * .70) = 0.191s,也就是說最壞狀況下繞地球一圈是 191ms,對吧?不對。這假設的是一條理想路徑,而實際上兩個網絡節點的之間幾乎不多是直線。中間還有路由器、交換機、緩存、處理器隊列等各類各樣的延遲。累加起來的延遲至關可觀。
這些和 Stack Overflow 有什麼關係呢?雲主機的優點出來了。若是你用一家雲供應商,你訪問到的就是相對較近的服務器。但對咱們來講不是這樣,你離服務部署在紐約或丹佛(主備模式)越遠,延遲就越高。而使用 HTTPS,在協商鏈接的時候須要一個額外的往返。這仍是最好的狀況(使用 0-RTT 優化 TLS 1.3)。Ilya Grigorik 的 這個總結 講的很好。
來講 Cloudflare 和 Fastly。HTTPS 並非閉門造車的一個項目,你看下去就會知道,咱們還有好幾個項目在並行。在搭建一個靠近用戶的 HTTPS 終端(以下降往返時間)時,咱們主要考慮的是:
開始正式啓用終端鏈路加速以前,咱們須要有性能測試報告。咱們在瀏覽器搭好了一整套覆蓋全鏈路性能數據的測試。 瀏覽器裏能夠經過 JavaScript 從 window.performance
取性能耗時。打開你瀏覽器的審查器,你能夠親手試一下。咱們但願這個過程透明,因此從第一天開始就把詳細信息放在了 teststackoverflow.com 上。這上面並無敏感信息,只有一些由頁面直接載入的 URI 和資源,以及它們的耗時。每一張記錄下來的頁面大概長這樣:
咱們目前對 5% 的流量作性能監控。這個過程沒有那麼複雜,可是咱們須要作的事情包括:
最終的結果是咱們有了一份來自於全球真實用戶的很好的實時彙總。這些數據可供咱們分析、監控、報警,以及用於評估變化。它大概長這樣:
幸虧,咱們有持續的流量來獲取數據以供咱們決策使用,目前的量級是 50 億,而且還在增加中。這些數據概覽以下:
OK,咱們已經把基礎工做準備好了,是時候來測試 CDN/代理層供應商了。
咱們評估了不少 CDN/DDoS 防禦層供應商。最終選擇了 Cloudflare,主要是考慮到他們的基礎設施、快速響應、還有他們承諾的 Railgun。那麼咱們如何測試使用了 Cloudfalre 以後用戶的真實效果?是否須要部署服務來獲取用戶數據?答案是不須要!
Stack Overflow 的數據量很是大:月 PV 過十億。記得咱們上面講的客戶端耗時紀錄嗎?咱們天天都有幾百萬的訪問了,因此不是直接能夠問他們嗎?咱們是能夠這麼作,只須要在頁面中嵌入 <iframe>
就好了。Cloudflare 已是咱們 cdn.sstatic.net(咱們共用的無 cookie 的靜態內容域)的託管商了。可是這是經過一條CNAME
DNS 紀錄來作的,咱們把 DNS 指向他們的 DNS。因此要用 Cloudflare 來當代理服務的話,咱們須要他們指向咱們的 DNS。因此咱們先須要測試他們 DNS 的性能。
實際上,要測試性能咱們須要把二級域名給他們,而不是 something.stackoverflow.com
,由於這樣可能會有不一致的膠水記錄而致使屢次查詢。明確一下,一級域名 (TLDs)指的是 .com
, .net
, .org
, .dance
, .duck
, .fail
, .gripe
, .here
, .horse
, .ing
, .kim
, .lol
, .ninja
, .pink
, .red
, .vodka
. 和 .wtf
。 注意,這些域名尾綴都是,我可沒開玩笑。 二級域名 (SLDs) 就多了一級,好比 stackoverflow.com
, superuser.com
等等。咱們須要測的就是這些域名的行爲及表現。所以,咱們就有了 teststackoverflow.com
,經過這個新域名,咱們在全球範圍內測試 DNS 性能。對一部分比例的用戶,經過嵌一個 <iframe>
(在測試中開關),咱們能夠輕鬆地獲取用戶訪問 DNS 的相關數據。
注意,測試過程最少須要 24 小時。在各個時區,互聯網的表現會隨着用戶做息或者 Netflix 的使用狀況等發生變化。因此要測試一個國家,須要完整的一天數據。最好是在工做日(而不要半天落在週六)。咱們知道會有各類意外狀況。互聯網的性能並非穩定的,咱們要經過數據來證實這一點。
咱們最初的假設是,多增長了的一個節點會帶來額外的延時,咱們會所以損失一部分頁面加載性能。可是 DNS 性能上的增長其實彌補了這一塊。比起咱們只有一個數據中心來講,Cloudflare 的 DNS 服務器部署在離用戶更近的地方,這一塊性能要好得多得多。我但願咱們能有空來放出這一塊的數據,只不過這一塊須要不少處理(以及託管),而我如今也沒有足夠多的時間。
接下來,咱們開始將 teststackoverflow.com
放在 Cloudflare 的代理上作鏈路加速,一樣也是放在 <iframe>
中。咱們發現美國和加拿大的服務因爲多餘的節點而變慢,可是世界其餘地方都是持平或者更好。這知足咱們的指望。咱們開始使用 Cloudflare 的網絡對接咱們的服務。期間發生了一些 DDos 的攻擊,不過這是另外的事了。那麼,爲何咱們接受在美國和加拿大地區慢一點呢?由於每一個頁面加載須要的時間僅爲 200-300ms,哪怕慢一點也仍是飛快。當時咱們認爲 Railgun 能夠將這些損耗彌補回來。
這些測試完成以後,咱們爲了預防 DDos 工做,作了一些其餘工做。咱們接入了額外的 ISP 服務商以供咱們的 CDN/代理層對接。畢竟若是能繞過攻擊的話,咱們不必在代理層作防禦。如今每一個機房都有 4 個 ISP 服務商(譯者注:至關於電信、聯通、移動、教育網),兩組路由器,他們之間使用 BGP 協議。咱們還額外添置了兩組負載均衡器專門用於處理 CDN/代理層的流量。
與此配套,咱們啓用了兩組 Railgun。Railgun 的原理是在 Cloudflare 那邊,使用 memcached 匹配 URL 進行緩存數據。當 Railgun 啓用的時候,每一個頁面(有一個大小閾值)都會被緩存下來。那麼在下一次請求時候,若是在這個 URL 在 Cloudflare 節點上和咱們這裏都緩存的話,咱們仍然會問 web 服務器最新的數據。可是咱們不須要傳輸完整的數據,只須要把傳輸和上次請求的差別數據傳給 Cloudflure。他們把這個差別運用於他們的緩存上,而後再發回給客戶端。這時候, gzip 壓縮 的操做也從 Stack Overflow 的 9 臺 Web Server 轉移到了一個 Railgun 服務上,這臺服務器得是 CPU 密集型的——我指出這點是由於,這項服務須要評估、購買,而且部署在咱們這邊。
舉個例子,想象一下,兩個用戶打開同一個問題的頁面。從瀏覽效果來看,他們的頁面技術上長得幾乎同樣,僅僅有細微的差異。若是咱們大部分的傳輸內容只是一個 diff 的話,這將是一個巨大的性能提高。
總而言之,Railgun 經過減小大量數據傳輸的方式提升性能。當它順利工做的時候確實是這樣。除此以外,還有一個額外的優勢:請求不會重置鏈接。因爲 TCP 慢啓動,當鏈接環境較爲複雜時候,可能致使鏈接被限流。而 Railgun 始終以固定的鏈接數鏈接到 Cloudflare 的終端,對用戶請求採用了多路複用,從而其不會受慢啓動影響。小的 diff 也減小了慢啓動的開銷。
很惋惜,咱們因爲種種緣由咱們在使用 Railgun 過程當中一直遇到問題。據我所知,咱們擁有當時最大的 Railgun 部署規模,這把 Railgun 逼到了極限。儘管咱們花了一年追蹤各類問題,最終仍是不得不放棄了。這種情況不只沒有給咱們省錢,還耗費了更多的精力。如今幾年過去了。若是你正在評估使用 Railgun,你最好看最新的版本,他們一直在作優化。我也建議你本身作決定是否使用 Railgun。
咱們最近才遷到 Fastly,由於咱們在講 CDN/代理層,我也會順帶一提。因爲不少技術工做在 Cloudflare 那邊已經完成,因此遷移自己並無什麼值得說的。你們會更感興趣的是:爲何遷移?畢竟 Cloudflare 在各方面是不錯的:豐富的數據中心、穩定的帶寬價格、包含 DNS 服務。答案是:它再也不是咱們最佳的選擇了。Flastly 提供了一些咱們更爲看中的特性:靈活的終端節點控制能力、配置快速分發、自動配置分發。並非說 Cloudflare 不行,只是它再也不適合 Stack Overflow 了。
事實勝於雄辯:若是我不承認 Cloudflare,個人私人博客不可能選擇它,嘿,就是這個博客,你如今正在閱讀的。
Fastly 吸引咱們的主要功能是提供了 Varnish) 和 VCL。這提供了高度的終端可定製性。有些功能吧,Cloudfalre 沒法快速提供(由於他們是通用化的,會影響全部用戶),在 Fastly 咱們能夠本身作。這是這兩家架構上的差別,這種「代碼級別高可配置」對於咱們很適用。同時,咱們也很喜歡他們在溝通、基礎設施的開放性。
我來展現一個 VCL 好用在哪裏的例子。最近咱們遇到 .NET 4.6.2 的一個超噁心 bug,它會致使 max-age 有超過 2000 年的緩存時間。快速解決方法是在終端節點上有須要的時候去覆蓋掉這個頭部,當我寫這篇文章的時候,這個 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 分鐘的緩存時間(數據量還好),其他頁面都不設置。這是一個爲解決緊急時間的很是便於部署的全局性解決方案。 咱們很開心如今有能力在終端作一些事情。咱們的 Jason Harvey 負責 VCL 配置,並寫了一些自動化推送的功能。咱們基於一個 Go 的開源庫 fastlyctl 作了開發。
另外一個 Fastly 的特色是能夠使用咱們本身的證書,Cloudflare 雖然也有這個服務,可是費用過高。如我上文提到的,咱們如今已經具有使用 HTTP/2 推送的能力。可是,Fastly 就不支持 DNS,這個在 Cloudflare 那裏是支持的。如今咱們須要本身解決 DNS 的問題了。可能最有意思的就是這些來回的折騰吧?
當咱們從 Cloudflare 遷移到 Fastly 時候,咱們必須評估並部署一個新的 DNS 供應商。這裏有篇 Mark Henderson 寫的 文章 。鑑於此,咱們必須管理:
這個自己就是另外一個項目了。爲了高效管理,咱們開發了 DNSControl。這如今已是開源項目了,託管在 GiHub 上,使用 Go 語言編寫。 簡而言之,每當咱們推送 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)複製代碼
太棒了,接下來咱們就能夠使用客戶端響應測試工具來測試啦!上面提到的工具能夠實時告訴咱們真實部署狀況,而不是模擬數據。可是咱們還須要測試全部部分都正常。
客戶端響應測試的追蹤能夠方便咱們作性能測試,但這個並不適合用來作配置測試。客戶端響應測試很是適合展示結果,可是配置有時候並無界面,因此咱們開發了 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"]複製代碼
每次咱們更新一下防火牆、證書、綁定、跳轉時都有必要測一下。咱們必須保證咱們的修改不會影響用戶訪問(先在預發佈環境進行部署)。 httpUnit 就是咱們來作集成測試的工具。
咱們還有一個開發的內部工具(由親愛的 Tom Limoncelli 開發),用來管理咱們負載均衡上面的 VIP 地址 。咱們先在一個備用負載均衡上面測試完成,而後將全部流量切過去,讓以前的主負載均衡保持一個穩定狀態。若是期間發生任何問題,咱們能夠輕易回滾。若是一切順利,咱們就把這個變動應用到那臺負載均衡上。這個工具叫作 keepctl
(keepalived control 的簡稱),時間容許的話很快就會整理開源出來。
上面提到的只是架構方面的工做。這一般是由 Stack Overflow 的幾名網站可靠性工程師組成的團隊完成的。而應用層也有不少須要完成的工做。這個列表會很長,先讓我拿點咖啡和零食再慢慢說。
很重要的一點是,Stack Overflow 與 Stack Exchange 的架構 Q&A 採用了多租戶技術。這意味着若是你訪問 stackoverflow.com
或者 superuser.com
又或者 bicycles.stackexchange.com
,你返回到的實際上是同一臺服務器上的同一個 w3wp.exe
進程。咱們經過瀏覽器發送的 Host
請求頭來改變請求的上下文。爲了更好地理解咱們下文中提到的一些概念,你須要知道咱們代碼中的 Current.Site
其實指的是 請求 中的站點。Current.Site.Url()
和 Current.Site.Paths.FaviconUrl
也是基於一樣的概念。
換一句話說:咱們的 Q&A 全站都是跑在同一個服務器上的同一個進程,而用戶對此沒有感知。咱們在九臺服務器上每一臺跑一個進程,只是爲了發佈版本和冗餘的問題。
整個項目中有一些看起來能夠獨立出來(事實上也是),不過也同屬於整個大 HTTPS 遷移中的一部分。登陸就是其中一個項目。我首先來講說這個,由於這比別它變化都要早上線。
在 Stack Overflow(及 Stack Exchange)的頭五六年裏,你登陸的是一個個的獨立網站。好比,stackoverflow.com
、stackexchange.com
以及 gaming.stackexchange.com
都有它們本身的 cookies。值得注意的是:meta.gaming.stackexchange.com
的登陸 cookie 是從 gaming.stackexchange.com
帶過來的。這些是咱們上面討論證書時提到的 meta 站點。他們的登陸信息是相關聯的,你只能經過父站點登陸。在技術上說並無什麼特別的,但考慮到用戶體驗就很糟糕了。你必須一個一個站登陸。咱們用「全局認證」的方法來「修復」了這個問題,方法是在頁面上放一個 <iframe>
,內面訪問一下 stackauth.com
。若是用戶在別處登陸過的話,它也會在這個站點上登陸,至少會去試試。這個體驗還行,可是會有彈出框問你是否點擊重載以登陸,這樣就又不是太好。咱們能夠作得更好的。對了,你也能夠去問問 Kevin Montrose 關於移動 Safari 的匿名模式,你會震驚的。
因而咱們有了「通用登陸」。爲何用「通用」這個名字?由於咱們已經用過「全局」了。咱們就是如此單純。所幸 cookies 也很單純的東西。父域名裏的 cookie(如 stackexchange.com
)在你的瀏覽器裏被帶到全部子域名裏去(如 gaming.stackexchange.com
)。若是咱們只二級域名的話,其實咱們的域名並很少:
是的,咱們有一些域名是跳轉到上面的列表中的,好比 askdifferent.com。可是這些只是跳轉而已,它們沒有 cookies 也無需登陸。
這裏有不少細節的後端工做我沒有提(歸功於 Geoff Dalgas 和 Adam Lear),但大致思路就是,當你登陸的時候,咱們把這些域名都寫入一個 cookie。咱們是經過第三方的 cookie 和隨機數來作的。當你登陸其中任意一個網站的時候,咱們在頁面上都會放 6 個 <img>
標籤來往其它域名寫入 cookie,本質上就完成了登陸工做。這並不能在 全部狀況 下都適用(尤爲是移動 Safari 簡直是要命了),但和以前比起來那是好得多了。
客戶端的代碼不復雜,基本上長這樣:
$.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 style="display:none" src="' + url + '"></img>'); });
});
}, 'json');複製代碼
可是要作到這點,咱們必須上升到帳號級別的認證(以前是用戶級別)、改變讀取 cookie 的方式、改變這些 meta 站的登陸工做方式,同時還要將這一新的變更整合到其它應用中。好比說,Careers(如今拆成了 Talent 和 Jobs)用的是另外一份代碼庫。咱們須要讓這些應用讀取相應的 cookies,而後經過 API 調用 Q&A 應用來獲取帳戶。咱們部署了一個 NuGet 庫來減小重複代碼。底線是:你在一個地方登陸,就在全部域名都登陸。不彈框,不重載頁面。
技術的層面上看,咱們不用再關心 *.*.stackexchange.com
是什麼了,只要它們是 stackexchange.com
下就行。這看起來和 HTTPS 沒有關係,但這讓咱們能夠把 meta.gaming.stackexchange.com
變成 gaming.meta.stackexchange.com
而不影響用戶。
要想作得更好的話,本地環境應該儘可能與開發和生產環境保持一致。幸虧咱們用的是 IIS,這件事情還簡單的。咱們使用一個工具來設置開發者環境,這個工具的名字叫「本地開發設置」——單純吧?它能夠安裝工具(Visual Studio、git、SSMS 等)、服務(SQL Server、Redis、Elasticsearch)、倉庫、數據庫、網站以及一些其它東西。作好了基本的工具設置以後,咱們要作的只是添加 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 頭來設置網站(經過別名添加),若是直連的話就給它一個證書(嗯,如今應該把這個行爲默認改成 $true
了),而後容許 AppPool 帳號來訪問數據庫,因而咱們本地也在使用 https://
開發了。嗯,我知道咱們應該把這個設置過程開源出來,不過咱們仍需去掉一些專有的業務。會有這麼一天的。
爲何這件事情很重要? 在此以前,咱們從 /content
加載靜態內容,而不是從另外一個域名。這很方便,但也隱藏了相似於跨域請求(CORS)的問題。在同一個域名下用同一個協議能正常加載的資源,換到開發或者生產環境下就有可能出錯。「在我這裏是好的。」
當咱們使用和生產環境中一樣協議以及一樣架構的 CDN 還有域名設置時,咱們就能夠在開發機器上找出並修復更多的問題。好比,你是否知道,從 https://
跳轉到 http://
時,瀏覽器是不會發送 referer 的?這是一個安全上的問題,referer 頭中可能帶有以明文傳輸的敏感信息。
「Nick 你就扯吧,咱們能拿到從 Google 拿到 referer 啊!」確實。可是這是由於他們主動選擇這一行爲。若是你看一下 Google 的搜索頁面,你能夠看到這樣的 <meta>
指令:
<meta content="origin" id="mref" name="referrer">複製代碼
這也就是爲何你能夠取到 referer。
好的,咱們已經設置好了,如今該作些什麼呢?
混合內容是個筐,什麼都能往裏裝。咱們這些年下來積累了哪些混合內容呢?不幸的是,有不少。這個列表裏咱們必須處理的用戶提交內容:
http://
圖片,出如今問題、答案、標籤、wiki 等內容中使用http://
頭像http://
頭像,出如今聊天中(站點側邊欄)http://
圖片,出現於我的資料頁的「關於我」部分http://
圖片,出現於幫助中心的文章中http://
YouTube 視頻(有些站點啓用了,好比 gaming.stackexchange.com)http://
圖片,出現於特權描述中http://
圖片,出現於開發者故事中http://
圖片,出現於工做描述中http://
圖片,出現於公司頁面中http://
源地址,出如今 JavaScript 代碼中.上面的每個都帶有本身獨有的問題,我僅僅會覆蓋一下值得一提的部分。注意:我談論的每個解決方案都必須擴展到咱們這個架構下的幾百個站點和數據庫上。
在上面的全部狀況中(除了代碼片斷),要消除混合內容的第一步工做就是:你必須先消除新的混合內容。不然,這個清理過程將會無窮無盡。要作到這一點,咱們開始全網強制僅容許內嵌 https://
圖片。一旦這個完成以後,咱們就能夠開始清理了。
對於問題、答案以及其餘帖子形式中,咱們須要具體問題具體分析。咱們先來搞定 90% 以上的狀況:stack.imgur.com
。在我來以前 Stack Overflow 就已經有本身託管的 Imgur 實例了。你在編輯器中上傳的圖片就會傳到那裏去。絕大部分的帖子都是用的這種方法,而他們幾年前就爲咱們添加了 HTTPS 支持。因此這個就是一個很直接的查找替換(咱們稱爲帖子 markdown 重處理)。
而後咱們經過經過 Elasticsearch 對全部內容的索引來找出全部剩下的文件。我說的咱們其實指的是 Samo。他在這裏處理了大量的混合內容工做。當咱們看到大部分的域名其實已經支持 HTTPS 了以後,咱們決定:
<img>
的源地址都嘗試替換成 https://
。若是能正常工做則替換帖子中的連接https://
,將其轉一個連接固然,並無那麼順利。咱們發現用於匹配 URL 的正則表達式其實已經壞了好幾年了,而且沒有人發現……因此咱們修復了正則,從新作了索引。
有人問咱們:「爲何不作個代理呢?」呃,從法律和道德上來講,代理對咱們的內容來講是個灰色地帶。好比,咱們 photo.stackexchange.com 上的攝像師會明確聲明不用 Imgur 以保留他們的權利。咱們充分理解。若是咱們開始代理並緩存全圖,這在法律上有點問題。咱們後來發如今幾百萬張內嵌圖片中,只有幾千張即不支持 https://
也沒有 404 失效的。這個比例(低於 1%)不足於讓咱們去搭一個代理。
咱們確實研究過搭一個代理相關的問題。費用有多少?須要多少存儲?咱們的帶寬足夠嗎?咱們有了一個大致上的估算,固然有點答案也不是很肯定。好比咱們是否要用 Fastly,仍是直接走運營商?哪種比較快?哪種比較便宜?哪種能夠擴展?這個足夠寫另外一篇博客了,若是你有具體問題的話能夠在評論裏提出,我會盡力回答。
所幸,在這個過程當中,爲了解決幾個問題,balpha 更改了用 HTML5 嵌入 YouTube 的方式。咱們也就順便強制了一下 YouTube 的 https://
嵌入。
剩下的幾個內容領域的事情差很少:先阻止新的混合內容進來,再替換掉老的。這須要咱們在下面幾個領域進行更改:
聲明:JavaScript 片斷的問題仍然沒有解決。這個有點難度的緣由是:
https://
的方式存在(好比一個庫)並非處理完用戶提交的內容就解決問題了。咱們本身仍是有很多 http://
的地方須要處理。這些更改自己沒什麼特別的,可是這至少能解答「爲何花了那麼長時間?」這個問題:
/jobs
下的全部東西(這實際上是個代理)http://
的地方JavaScript 和連接比較使人痛苦,因此我在這裏稍微提一下。
JavaScript 是一個很多人遺忘的角落,但這顯然不能被無視。咱們很多地方將主機域名傳遞給 JavaScript 時假定它是 http://
,同時也有很多地方寫死了 meta 站裏的 meta.
前綴。不少,真的不少,救命。還好如今已經不這樣了,咱們如今用服務器渲染出一個站點,而後在頁面頂部放入相應的選擇:
StackExchange.init({
"locale":"en",
"stackAuthUrl":"https://stackauth.com",
"site":{
"name":"Stack Overflow"
"childUrl":"https://meta.stackoverflow.com",
"protocol":"http"
},
"user":{
"gravatar":"<div class=\"gravatar-wrapper-32\"><img src=\"https://i.stack.imgur.com/nGCYr.jpg\"></div>",
"profileUrl":"https://stackoverflow.com/users/13249/nick-craver"
}
});複製代碼
這幾年來咱們在代碼裏也用到了不少靜態連接。好比,在頁尾,在頁腳,在幫助區域……處處都是。對每個來講,解決方式都不復雜:把它們改爲 <site>.Url("/path")
的形式就行了。不過要找出這些連接有點意思,由於你不能直接搜 "http://"
。感謝 W3C 的豐功偉績:
<svg xmlns="http://www.w3.org/2000/svg"...複製代碼
是的,這些是標識符,是不能改的。因此我但願 Visual Studio 在查找文件框中增長一個「排除文件類型」的選項。Visual Studio 你聽見了嗎?VS Code 前段時間就加了這個功能。我這要求不過度。
這件事情很枯燥,就是在代碼中找出一千個連接而後替換而已(包括註釋、許可連接等)。但這就是人生,咱們必需要作。把這些連接改爲 .Url()
的形式以後,一旦站點支持 HTTPS 的時候,咱們就可讓連接動態切換過去。好比咱們得等到 meta.*.stackexchange.com
搬遷完成以後再進行切換。插播一下咱們數據中心的密碼是「煎餅餜子」拼音全稱,應該沒有人會讀到這裏吧,因此在這裏存密碼很安全。當站點遷完以後,.Url()
仍會正常工做,而後用 .Url()
來渲染默認爲 HTTPS 的站點也會繼續工做。這將靜態連接變成了動態。
另外一件重要的事情:這讓咱們的開發和本地環境都能正常工做,而不只僅是鏈到生產環境上。這件事情雖然枯燥,但仍是值得去作的。對了,由於咱們的規範網址(canonical)也經過 .Url()
來作了,因此一旦用戶開始用上 HTTPS,Google 也能夠感知到。
一旦一個站點遷到 HTTPS 以後,咱們會讓爬蟲來更新站點連接。咱們把這個叫修正「Google 果汁」,同時這也可讓用戶再也不碰到 301。
當你把站點移動到 HTTPS 以後,爲了和 Google 配合,你有兩件重要的事情要作:
<link rel="canonical" href="https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454" />
http://
連接經過 301 跳轉至 https://
這個不復雜,也不是浩大的工程,但這很是很是重要。Stack Overflow 大部分的流量都是從 Google 搜索結果中過來的,因此咱們得保證這個不產生負面影響。這個是咱們的生計,若是咱們所以丟了流量那我真是要失業了。還記得那些 .internal
的 API 調用嗎?對,咱們一樣不能把全部東西都進行跳轉。因此咱們在處理跳轉的時候須要必定的邏輯(好比咱們也不能跳轉 POST
請求,由於瀏覽器處理得很差),固然這個處理仍是比較直接的。這裏是實際上用到的代碼:
public static void PerformHttpsRedirects()
{
var https = Settings.HTTPS;
// If we're on HTTPS, never redirect back
if (Request.IsSecureConnection) return;
// Not HTTPS-by-default? Abort.
if (!https.IsDefault) return;
// Not supposed to redirect anyone yet? Abort.
if (https.RedirectFor == SiteSettings.RedirectAudience.NoOne) return;
// Don't redirect .internal or any other direct connection
// ...as this would break direct HOSTS to webserver as well
if (RequestIPIsInternal()) return;
// Only redirect GET/HEAD during the transition - we'll 301 and HSTS everything in Fastly later
if (string.Equals(Request.HttpMethod, "GET", StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(Request.HttpMethod, "HEAD", StringComparison.InvariantCultureIgnoreCase))
{
// Only redirect if we're redirecting everyone, or a crawler (if we're a crawler)
if (https.RedirectFor == SiteSettings.RedirectAudience.Everyone
|| (https.RedirectFor == SiteSettings.RedirectAudience.Crawlers && Current.IsSearchEngine))
{
var resp = Context.InnerHttpContext.Response;
// 301 when we're really sure (302 is the default)
if (https.RedirectVia301)
{
resp.RedirectPermanent(Site.Url(Request.Url.PathAndQuery), false);
}
else
{
resp.Redirect(Site.Url(Request.Url.PathAndQuery), false);
}
Context.InnerHttpContext.ApplicationInstance.CompleteRequest();
}
}
}複製代碼
注意咱們並非默認就跳 301(有一個 .RedirectVia301
設置),由於咱們作一些會產生永久影響的事情以前必須仔細測試。咱們會晚一點來討論 HSTS 以及後續影響。
這一塊會過得快一點。Websocket 不難,從某種角度來講,這是咱們作過的最簡單的事情。咱們用 websockets 來處理實時的用戶影響力變化、收件箱通知、新問的問題、新增長的答案等等。這也就說基本上每開一個 Stack Overflow 的頁面,咱們都會有一個對應的 websocket 鏈接連到咱們的負載均衡器上。
因此怎麼改呢?其實很簡單:安裝一個證書,監聽 :443
端口,而後用 wss://qa.sockets.stackexchange.com
來代替 ws://
。後者其實早就作完了(咱們用了一個專有的證書,可是這不重要)。從 ws://
到 wss://
只是配置一下的問題。一開始咱們還用 ws://
做爲 wss://
的備份方案,不事後來就變成僅用 wss://
了。這麼作有兩個緣由:
https://
下面會有混合內容警告最大的問題就是:「咱們能處理了這個負載嗎?」咱們全網處理了很多併發 websocket,在我寫這估的時候咱們有超過 600000 個併發的鏈接。這個是咱們 HAProxy 的儀表盤在 Opserver 中的界面:
無論是在終端、抽象命名空間套接字仍是前端來講都有不少鏈接。因爲啓用了 TLS 會話恢復,HAProxy 自己的負載也很重。要讓用戶下一次從新鏈接更快,第一次協商以後用戶會拿到一個令牌,下一次會把這個令牌發送過來。若是咱們的內存足夠而且沒有超時,咱們會恢復上次的會話而不是再開一個。這個操做能夠節省 CPU,對用戶來講有性能提高,但會用到到更多內存。這個多因 key 大小而異(2048,4096 或是更多?)咱們如今用的是 4096 位的 key。在開了 600000 個 websocket 的狀況下,咱們只用掉了負載均衡器 64GB 內存裏的 19GB。這裏面 12GB 是 HAProxy 在用,大多數爲 TLS 會話緩存。因此結果來講還不錯,若是咱們不得不買內存的話,這也會是整個 HTTPS 遷移中最便宜的東西。
我猜如今多是咱們來談論一些未知問題的時候。有些問題是在咱們嘗試以前沒法真正知道的:
https://
嗎?)有不少人都談過他們轉化成 https://
的心得,但對咱們卻有點不同。咱們不是一個站點。咱們是多個域名下的多個站點。咱們不知道 Google 會怎麼對待咱們的網絡。它會知道 stackoverflow.com
和 superuser.com
有關聯嗎?不知道。咱們也不能期望 Google 來告訴咱們這些。
因此咱們就作測試。在咱們全網發佈 中,咱們測試了幾個域名:
對,這些是 Samo 和我會了仔細討論出來的結果,花了有三分鐘那麼久吧。Meta 是由於這是咱們最重要的反饋網站。Security 站上有不少專家可能會注意到相關的問題,特別是 HTTPS 方面。最後一個,Super User,咱們須要知道搜索對咱們內容的影響。比起 meta 和 security 來講法,Super User 的流量要大得多。最重要的是,它有來自 Google 的原生流量。
咱們一直在觀察並評估搜索的影響,因此 Super User 上了以後其餘網站過了好久纔跟上。到目前爲止咱們能說的是:基本上沒影響。搜索、結果、點擊還有排名的周變化都在正常範圍內。咱們公司依賴於這個流量,這對咱們真的很重要。所幸,沒有什麼值得咱們擔憂的點,咱們能夠繼續發佈。
若是不提到咱們搞砸的部分,這篇文章就還不夠好。錯誤永遠是個選擇。讓咱們來總結一下這一路讓咱們後悔的事情:
若是你的一個資源有一個 URL 的話,通常來講你會看到一些 http://example.com
或者 https://example.com
之類的東西,包括咱們圖片的路徑等等。另外一個選項就是你能夠使用 //example.com
。這被稱爲相對協議 URL。咱們很早以前就在圖片、JavaScript、CSS 等中這麼用了(咱們自有的資源,不是指用戶提交)。幾年後,咱們發現這不是一個好主意,至少對咱們來講不是。相對協議連接中的「相對」是對於頁面而言。當你在 http://stackoverflow.com
時,//example.com
指的是 http://example.com
;若是你在 https://stackoverflow.com
時,就和 https://example.com
等同。那麼這個有什麼問題呢?
問題在於,圖片 URL 不只是用在頁面中,它們還用在郵件、API 還有移動應用中。當咱們理了一下路徑結構而後在處處都使用圖片路徑時咱們發現不對了。雖然這個變化極大下降了代碼冗餘,而且簡化了不少東西,結果倒是咱們在郵件中使用了相對 URL。絕大多數郵件客戶端都不能處理相對協議 URL 的圖片。由於它們不知道是什麼協議。Email 不是 http://
也不是 https://
。只有你在瀏覽器裏查看郵件,有多是預期的效果。
那該怎麼辦?咱們把全部的地方都換成了 https://
。我把咱們全部的路徑代碼統一到兩個變量上:CDN 根路徑,和對應特定站點的文件夾。例如 Stack Overflow 的樣式表在 https://cdn.sstatic.net/Sites/stackoverflow/all.css
上(固然咱們有緩存中斷器),換成本地就是 https://local.sstatic.net/Sites/stackoverflow/all.css
。你能看出其中的共同點。經過拼接路徑,邏輯簡單了很多。則 經過強制 https://
,用戶還能夠在整站切換以前就享受 HTTP/2 的好處,由於全部靜態資源都已經就位。都用 https://
也表示咱們能夠在頁面、郵件、移動還有 API 上使用同一個屬性。這種統一也意味着咱們有一個固定的地方來處理全部路徑——咱們處處都有緩存中斷器。
注意:若是你像咱們同樣中斷緩存,好比 https://cdn.sstatic.net/Sites/stackoverflow/all.css?v=070eac3e8cf4
,請不要用構建號。咱們的緩存中斷使用的是文件的校驗值,也就是說只有當文件真正變化的時候你纔會下載一個新的文件。用構建號的話可能會稍微簡單點,但同時也會對你的費用還有性能有所損傷。
能作這個固然很好,可咱們爲何不從一開始就作呢?由於 HTTPS 在那個時候性能還不行。用戶經過 https://
訪問會比 http://
慢不少。舉一個大一點的例子:咱們上個月在 sstatic.net
上收到了四百萬個請求,總共有 94TB。若是 HTTPS 性能很差的話,這裏累積下來的延遲就很可觀了。不過由於咱們上了 HTTP/2,以及設置好 CDN/代理層,性能的問題已經好不少了。對於用戶來講更快了,對咱們來講則更簡單,何樂不爲呢!
當咱們把代理架起來開始測試的時候發現了什麼?咱們忘了一件很重要的事,準確地說,我忘了一件很重要的事。咱們在內部 API 裏大量地使用了 HTTP。固然這個是正常工做的,只是它們變得更慢、更復雜、也更容易出問題了。
比方說一個內部 API 須要訪問 stackoverflow.com/some-internal-route
,以前,節點是這些:
這是由於咱們是能夠解析 stackoverflow.com
的,解析出來的 IP 就是咱們的負載均衡器。當有代理的狀況下,爲了讓用戶能訪問到最近的節點,他們訪問到的是不一樣的 IP 和目標點。他們的 DNS 解析出來的 IP 是 CDN/代理層 (Fastly)。糟了,這意識着咱們如今的路徑是這樣的:
嗯,這個看起來更糟了。爲了實現一個從 A 調用一下 B,咱們多了不少沒必要要的依賴,同時性能也降低了。我不是說咱們的代理很慢,只是本來只須要 1ms 就能夠連到咱們數據中心……好吧,咱們的代理很慢。
咱們內部討論了屢次如何用最簡單的方法解決這個問題。咱們能夠把請求改爲 internal.stackoverflow.com
,可是這會產生可觀的修改(也許也會產生衝突)。咱們也建立一個 DNS 來專門解析內部地址(但這樣會產生通配符繼承的問題)。咱們也能夠在內部把 stackoverflow.com
解析成不一樣的地址(這被稱爲水平分割 DNS),可是這一來很差調試,二來在多數據中心的場景下不知道該到哪個。
最終,咱們在全部暴露給外部 DNS 的域名後面都加了一個 .internal
後續。好比,在咱們的網絡中,stackoverflow.com.internal
會解析到咱們的負載均衡器後面(DMZ)的一個內部子網內。咱們這麼作有幾個緣由:
.internal
從 Host
頭中移除(應用層無感知).internal
)咱們客戶端的 API 代碼是大部分是由 Marc Gravell 寫的一個 StackExchange.Network
的 NuGet 庫。對於每個要訪問的 URL,咱們都用靜態的方法調用(因此也就只有通用的獲取方法那幾個地方)。若是存在的話就會返回一個「內部化」URL,不然保持不變。這意味着一次簡單的 NuGet 更新就能夠把這個邏輯變化部署到全部應用上。這個調用挺簡單的:
uri = SubstituteInternalUrl(uri);複製代碼
這裏是 stackoverflow.com
DNS 行爲的一個例子:
記得咱們以前提到的 dnscontrol 嗎?咱們能夠用這個快速同步。歸功於 JavaScript 的配置/定義,咱們能夠簡單地共享、簡化代碼。咱們匹配全部全部子網和全部數據中心中的全部 IP 的最後一個字節,因此用幾個變量,全部 AD 和外部的 DNS 條目都對齊了。這也意味着咱們的 HAProxy 配置更簡單了,基本上就是這樣:
stacklb::external::frontend_normal { 't1_http-in':
section_name => 'http-in',
maxconn => $t1_http_in_maxconn,
inputs => {
"${external_ip_base}.16:80" => [ 'name stackexchange' ],
"${external_ip_base}.17:80" => [ 'name careers' ],
"${external_ip_base}.18:80" => [ 'name openid' ],
"${external_ip_base}.24:80" => [ 'name misc' ],複製代碼
綜上,API 路徑更快了,也更可靠了:
咱們解決了幾個問題,還剩下幾百個等着咱們。
在從 http://
301 跳到 https://
時有一點咱們沒有意識的是,Fastly 緩存了咱們的返回值。在 Fastly 中,默認的緩存鍵並不考慮協議。我我的不一樣意這個行爲,由於在源站默認啓用 301 跳轉會致使無限循環。這個問題是這樣形成的:
http://
上的一個網絡https://
https://
訪問同一個頁面https://
的 301,儘可能你已經在這個頁面上了這就是爲何咱們會有無限循環。要解決這個問題,咱們得關掉 301,清掉 Fastly 緩存,而後開始調查。Fastly 建議咱們在 vary 中加入 Fastly-SSL
,像這樣:
sub vcl_fetch {
set beresp.http.Vary = if(beresp.http.Vary, beresp.http.Vary ",", "") "Fastly-SSL";複製代碼
在我看來,這應該是默認行爲。
記得咱們必須修復的幫助文檔嗎?幫助文檔都是按語言區分,只有極少數是按站點來分,因此原本它們是能夠共享的。爲了避免產生大量重複代碼及存儲結構,咱們作了一點小小的處理。咱們把實際上的帖子對象(和問題、答案同樣)存在了 meta.stackexchange.com
或者是這篇帖子關聯的站點中。咱們把生成的 HelpPost
存在中心的 Sites
數據庫裏,其實也就是生成的 HTML。在處理混合內容的時候,咱們也處理了單個站裏的帖子,簡單吧!
當原始的帖子修復後,咱們只須要爲每一個站點去再生成 HTML 而後填充回去就好了。可是這個時候我犯了個錯誤。回填的時候拿的是當前站點(調用回填的那個站點),而不是原始站。這致使 meta.stackexchange.com
裏的 12345 帖子被 stackoverflow.com
裏的 12345 帖子所替代。有的時候是答案、有的時候是問題,有的時候有一個 tag wiki。這也致使了一些頗有意思的幫助文檔。這裏有一些相應的後果。
我只能說,還好修復的過程挺簡單的:
再一次將數據填充回去就能修復了。不過怎麼說,這個當時算是在公共場合鬧了個笑話。抱歉。
這裏有咱們在這個過程當中產出的項目,幫助咱們改進了 HTTPS 部署的工做,但願有一天這些能拯救世界吧:
咱們的工做並無作完。接下去還有一此要作的:
https://
HSTS 指的是「HTTP 嚴格傳輸安全」。OWASP 在這裏有一篇很好的總結。這個概念其實很簡單:
https://
頁面的時候,咱們給你發一個這樣的頭部:Strict-Transport-Security: max-age=31536000
https://
訪問這個域名哪怕你是點擊一個 http://
的連接,你的瀏覽器也會直接跳到 https://
。哪怕你有可能已經設置了一個 http://
的跳轉,但你的瀏覽器不會訪問,它會直接訪問 SSL/TLS。這也避免了用戶訪問不安全的 http://
而遭到劫持。好比它能夠把你劫持到一個 https://stack<長得很像o但實際是個圈的unicode>verflow.com
上,那個站點甚至有可能部好了 SSL/TLS 證書。只有不訪問這個站點纔是安全的。
但這須要咱們至少訪問一次站點,而後纔能有這個頭部,對吧?對。因此咱們有 HSTS 預加載,這是一個域名列表,隨着全部主流瀏覽器分發且由它們預加載。也就是說它們在第一次訪問的時候就會跳到 https://
去,因此永遠不會有任何 http://
通訊。
很贊吧!因此要怎麼才能上這個列表呢?這裏是要求:
這聽起來還行吧?咱們全部的活躍域名都支持 HTTPS 而且有有效的證書了。不對,咱們還有一個問題。記得咱們有一個 meta.gaming.stackexchange.com
吧,雖然它跳到 gaming.meta.stackexchange.com
,但這個跳轉自己並無有效證書。
以 meta 爲例,若是咱們在 HSTS 頭裏加入 includeSubDomains
指令,那麼網上全部指向舊域名的連接都會踩坑。它們本該跳到一個 http:///
站點上(如今是這樣的),一旦改了就會變成一個非法證書錯誤。昨天咱們看了一下流量日誌,天天仍有 8 萬次訪問的是經過 301 跳到 meta 子域上的。這裏有不少是爬蟲,但仍是有很多人爲的流量是從博客或者收藏夾過來的……而有些爬蟲真的很蠢,歷來不根據 301 來更新他們的信息。嗯,你還在看這篇文章?我本身寫着寫着都已經睡着 3 次了。
咱們該怎麼辦呢?咱們是否要啓用 SAN 證書,加入幾百個域名,而後調整咱們的基礎架構使得 301 跳轉也嚴格遵照 HTTPS 呢?若是要經過 Fastly 來作的話就會提高咱們的成本(須要更多 IP、證書等等)。Let’s Encrypt 卻是真的能幫上點忙。獲取證書的成本比較低,若是你不考慮設置及維護的人力成本的話(由於咱們因爲上文所述內容並無在使用它).
還有一塊是上古遺留問題:咱們內部的域名是 ds.stackexchange.com
。爲何是 ds.
?我不肯定。我猜多是咱們不知道怎麼拼 data center 這個詞。這意味着 includeSubDomains
會自動包含全部內部終端。雖然咱們大部分都已經上了 https://
,可是若是什麼都走 HTTPS 會致使一些問題,也會帶來必定延時。不是說咱們不想在內部也用 https://
,只不過這是一個總體的項目(大部分是證書分發和維護,還有多級證書),咱們不想增長耦合。那爲何不改一下內部域名呢?主要仍是時間問題,這一動遷須要大量的時間和協調。
目前,咱們將 HSTS 的 max-age
設爲兩年,而且不包括 includeSubDomains
。除非迫不得以,我不會從代碼裏移除這個設定,由於它太危險了。一旦咱們把全部 Q&A 站點的 HSTS 時間都設置好以後,咱們會和 Google 聊一下是否是能在不加 includeSubDomains
的狀況下把咱們加進 HSTS 列表中,至少咱們會試試看。你能夠看到,雖然很罕見,但目前的這份列表中仍是出現了這種狀況的。但願從增強 Stack Overflow 安全性的角度,他們能贊成這一點。
爲了儘快啓用 安全
cookie(僅在 HTTPS 下發送),咱們會將聊天(chat.stackoverflow.com、[chat.stackexchange.com 及 chat.meta.stackexchange.com)跳轉至 https://
。 正如咱們的通用登陸所作的那樣,聊天會依賴於二級域名下的 cookie。若是 cookie 僅在 https://
下發送,你就只能在 https://
下登陸。
這一塊有待斟酌,但其實在有混合內容的狀況下將聊天遷至 https://
是一件好事。咱們的網絡更加安全了,而咱們也能夠處理實時聊天中的混合內容。但願這個能在接下去的一兩週以內實施,這在個人計劃之中。
無論怎麼說,這就是咱們今天到達的地步,也是咱們過去四年中一直在作的事情。確實有不少更高優先級的事情阻擋了 HTTPS 的腳步——這也遠遠不是咱們惟一在作的事情。但這就是生活。作這件事情的人們還在不少大家看不見的地方努力着,而涉及到的人也遠不止我所提到的這些。在這篇文章中我只提到了一些花了咱們不少時間的、比較複雜的話題(不然就會太長了),可是這一路上無論是 Stack Overflow 內部仍是外部都有不少人幫助過咱們。
我知道大家會有不少的疑問、顧慮、報怨、建議等等。咱們很是歡迎這些內容。本週咱們會關注底下的評論、咱們的 meta 站、Reddit、Hacker News 以及 Twitter,並儘量地回答/幫助大家。感謝閱讀,能全文讀下的來真是太棒了。(比心)