【翻譯】Stack Overflow 的 HTTPS 化:漫漫長路的終點(二)

原文地址:HTTPS on Stack Overflow: The End of a Long Road
作者:Nick Craver
譯者: 羅晟 & 狄敬超

前文地址:【翻譯】Stack Overflow 的 HTTPS 化:漫漫長路的終點(一)

Cloudflare

我們評估了很多 CDN/DDoS 防護層供應商。最終選擇了 Cloudflare,主要是考慮到他們的基礎設施、快速響應、還有他們承諾的 Railgun。那么我們如何測試使用了 Cloudfalre 之后用戶的真實效果?是否需要部署服務來獲取用戶數據?答案是不需要!

Stack Overflow 的數據量非常大:月 PV 過十億。記得我們上面講的客戶端耗時紀錄嗎?我們每天都有幾百萬的訪問了,所以不是直接可以問他們嗎?我們是可以這么做,只需要在頁面中嵌入 <iframe> 就行了。Cloudflare 已經是我們 cdn.sstatic.net(我們共用的無 cookie 的靜態(tài)內容域)的托管商了。但是這是通過一條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 小時。在各個時區(qū),互聯網的表現會隨著用戶作息或者 Netflix 的使用情況等發(fā)生變化。所以要測試一個國家,需要完整的一天數據。最好是在工作日(而不要半天落在周六)。我們知道會有各種意外情況?;ヂ摼W的性能并不是穩(wěn)定的,我們要通過數據來證明這一點。

我們最初的假設是,多增加了的一個節(jié)點會帶來額外的延時,我們會因此損失一部分頁面加載性能。但是 DNS 性能上的增加其實彌補了這一塊。比起我們只有一個數據中心來說,Cloudflare 的 DNS 服務器部署在離用戶更近的地方,這一塊性能要好得多得多。我希望我們能有空來放出這一塊的數據,只不過這一塊需要很多處理(以及托管),而我現在也沒有足夠多的時間。

接下來,我們開始將 teststackoverflow.com 放在 Cloudflare 的代理上做鏈路加速,同樣也是放在 <iframe> 中。我們發(fā)現美國和加拿大的服務由于多余的節(jié)點而變慢,但是世界其他地方都是持平或者更好。這滿足我們的期望。我們開始使用 Cloudflare 的網絡對接我們的服務。期間發(fā)生了一些 DDos 的攻擊,不過這是另外的事了。那么,為什么我們接受在美國和加拿大地區(qū)慢一點呢?因為每個頁面加載需要的時間僅為 200-300ms,哪怕慢一點也還是飛快。當時我們認為 Railgun 可以將這些損耗彌補回來。

這些測試完成之后,我們?yōu)榱祟A防 DDos 工作,做了一些其他工作。我們接入了額外的 ISP 服務商以供我們的 CDN/代理層對接。畢竟如果能繞過攻擊的話,我們沒必要在代理層做防護。現在每個機房都有 4 個 ISP 服務商(譯者注:相當于電信、聯通、移動、教育網),兩組路由器,他們之間使用 BGP 協議。我們還額外添置了兩組負載均衡器專門用于處理 CDN/代理層的流量。

Cloudflare: Railgun

與此配套,我們啟用了兩組 Railgun。Railgun 的原理是在 Cloudflare 那邊,使用 memcached 匹配 URL 進行緩存數據。當 Railgun 啟用的時候,每個頁面(有一個大小閾值)都會被緩存下來。那么在下一次請求時候,如果在這個 URL 在 Cloudflare 節(jié)點上和我們這里都緩存的話,我們仍然會問 web 服務器最新的數據。但是我們不需要傳輸完整的數據,只需要把傳輸和上次請求的差異數據傳給 Cloudflure。他們把這個差異運用于他們的緩存上,然后再發(fā)回給客戶端。這時候, gzip 壓縮 的操作也從 Stack Overflow 的 9 臺 Web Server 轉移到了一個 Railgun 服務上,這臺服務器得是 CPU 密集型的——我指出這點是因為,這項服務需要評估、購買,并且部署在我們這邊。

舉個例子,想象一下,兩個用戶打開同一個問題的頁面。從瀏覽效果來看,他們的頁面技術上長得幾乎一樣,僅僅有細微的差別。如果我們大部分的傳輸內容只是一個 diff 的話,這將是一個巨大的性能提升。

總而言之,Railgun 通過減少大量數據傳輸的方式提高性能。當它順利工作的時候確實是這樣。除此之外,還有一個額外的優(yōu)點:請求不會重置連接。由于 TCP 慢啟動,當連接環(huán)境較為復雜時候,可能導致連接被限流。而 Railgun 始終以固定的連接數連接到 Cloudflare 的終端,對用戶請求采用了多路復用,從而其不會受慢啟動影響。小的 diff 也減少了慢啟動的開銷。

很可惜,我們由于種種原因我們在使用 Railgun 過程中一直遇到問題。據我所知,我們擁有當時最大的 Railgun 部署規(guī)模,這把 Railgun 逼到了極限。盡管我們花了一年追蹤各種問題,最終還是不得不放棄了。這種狀況不僅沒有給我們省錢,還耗費了更多的精力?,F在幾年過去了。如果你正在評估使用 Railgun,你最好看最新的版本,他們一直在做優(yōu)化。我也建議你自己做決定是否使用 Railgun。

Fastly

我們最近才遷到 Fastly,因為我們在講 CDN/代理層,我也會順帶一提。由于很多技術工作在 Cloudflare 那邊已經完成,所以遷移本身并沒有什么值得說的。大家會更感興趣的是:為什么遷移?畢竟 Cloudflare 在各方面是不錯的:豐富的數據中心、穩(wěn)定的帶寬價格、包含 DNS 服務。答案是:它不再是我們最佳的選擇了。Flastly 提供了一些我們更為看中的特性:靈活的終端節(jié)點控制能力、配置快速分發(fā)、自動配置分發(fā)。并不是說 Cloudflare 不行,只是它不再適合 Stack Overflow 了。

事實勝于雄辯:如果我不認可 Cloudflare,我的私人博客不可能選擇它,嘿,就是這個博客,你現在正在閱讀的。

Fastly 吸引我們的主要功能是提供了 VarnishVCL。這提供了高度的終端可定制性。有些功能吧,Cloudfalre 無法快速提供(因為他們是通用化的,會影響所有用戶),在 Fastly 我們可以自己做。這是這兩家架構上的差異,這種「代碼級別高可配置」對于我們很適用。同時,我們也很喜歡他們在溝通、基礎設施的開放性。

我來展示一個 VCL 好用在哪里的例子。最近我們遇到 .NET 4.6.2 的一個超惡心 bug,它會導致 max-age 有超過 2000 年的緩存時間。快速解決方法是在終端節(jié)點上有需要的時候去覆蓋掉這個頭部,當我寫這篇文章的時候,這個 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 做了開發(fā)。

另一個 Fastly 的特點是可以使用我們自己的證書,Cloudflare 雖然也有這個服務,但是費用太高。如我上文提到的,我們現在已經具備使用 HTTP/2 推送的能力。但是,Fastly 就不支持 DNS,這個在 Cloudflare 那里是支持的。現在我們需要自己解決 DNS 的問題了。可能最有意思的就是這些來回的折騰吧?

全局 DNS

當我們從 Cloudflare 遷移到 Fastly 時候,我們必須評估并部署一個新的 DNS 供應商。這里有篇 Mark Henderson 寫的 文章 。鑒于此,我們必須管理:

  • 我們自己的 DNS 服務器(備用)
  • Name.com 的服務器(為了那些不需要 HTTPS 的跳轉服務)
  • Cloudflare DNS
  • Route 53 DNS
  • Google DNS
  • Azure DNS
  • 其他一些(測試時候使用)

這個本身就是另一個項目了。為了高效管理,我們開發(fā)了 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)

太棒了,接下來我們就可以使用客戶端響應測試工具來測試啦!上面提到的工具可以實時告訴我們真實部署情況,而不是模擬數據。但是我們還需要測試所有部分都正常。

測試

客戶端響應測試的追蹤可以方便我們做性能測試,但這個并不適合用來做配置測試??蛻舳隧憫獪y試非常適合展現結果,但是配置有時候并沒有界面,所以我們開發(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"]

每次我們更新一下防火墻、證書、綁定、跳轉時都有必要測一下。我們必須保證我們的修改不會影響用戶訪問(先在預發(fā)布環(huán)境進行部署)。 httpUnit 就是我們來做集成測試的工具。

我們還有一個開發(fā)的內部工具(由親愛的 Tom Limoncelli 開發(fā)),用來管理我們負載均衡上面的 VIP 地址 。我們先在一個備用負載均衡上面測試完成,然后將所有流量切過去,讓之前的主負載均衡保持一個穩(wěn)定狀態(tài)。如果期間發(fā)生任何問題,我們可以輕易回滾。如果一切順利,我們就把這個變更應用到那臺負載均衡上。這個工具叫做 keepctl(keepalived control 的簡稱),時間允許的話很快就會整理開源出來。

應用層準備

上面提到的只是架構方面的工作。這通常是由 Stack Overflow 的幾名網站可靠性工程師組成的團隊完成的。而應用層也有很多需要完成的工作。這個列表會很長,先讓我拿點咖啡和零食再慢慢說。

很重要的一點是,Stack Overflow 與 Stack Exchange 的架構 Q&A 采用了多租戶技術。這意味著如果你訪問 stackoverflow.com 或者 superuser.com 又或者 bicycles.stackexchange.com,你返回到的其實是同一臺服務器上的同一個 w3wp.exe 進程。我們通過瀏覽器發(fā)送的 Host 請求頭來改變請求的上下文。為了更好地理解我們下文中提到的一些概念,你需要知道我們代碼中的 Current.Site 其實指的是 請求 中的站點。Current.Site.Url()Current.Site.Paths.FaviconUrl 也是基于同樣的概念。

換一句話說:我們的 Q&A 全站都是跑在同一個服務器上的同一個進程,而用戶對此沒有感知。我們在九臺服務器上每一臺跑一個進程,只是為了發(fā)布版本和冗余的問題。

全局登錄

整個項目中有一些看起來可以獨立出來(事實上也是),不過也同屬于整個大 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 也無需登錄。

這里有很多細節(jié)的后端工作我沒有提(歸功于 Geoff DalgasAdam 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('![](' + 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 而不影響用戶。

本地 HTTPS 開發(fā)

要想做得更好的話,本地環(huán)境應該盡量與開發(fā)和生產環(huán)境保持一致。幸好我們用的是 IIS,這件事情還簡單的。我們使用一個工具來設置開發(fā)者環(huán)境,這個工具的名字叫「本地開發(fā)設置」——單純吧?它可以安裝工具(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:// 開發(fā)了。嗯,我知道我們應該把這個設置過程開源出來,不過我們仍需去掉一些專有的業(yè)務。會有這么一天的。

為什么這件事情很重要? 在此之前,我們從 /content 加載靜態(tài)內容,而不是從另一個域名。這很方便,但也隱藏了類似于跨域請求(CORS)的問題。在同一個域名下用同一個協議能正常加載的資源,換到開發(fā)或者生產環(huán)境下就有可能出錯。「在我這里是好的。」

當我們使用和生產環(huán)境中同樣協議以及同樣架構的 CDN 還有域名設置時,我們就可以在開發(fā)機器上找出并修復更多的問題。比如,你是否知道,從 https:// 跳轉到 http:// 時,瀏覽器是不會發(fā)送 referer 的?這是一個安全上的問題,referer 頭中可能帶有以明文傳輸的敏感信息。

「Nick 你就扯吧,我們能拿到從 Google 拿到 referer ??!」確實。但是這是因為他們主動選擇這一行為。如果你看一下 Google 的搜索頁面,你可以看到這樣的 <meta> 指令:

<meta content="origin" id="mref" name="referrer">

這也就是為什么你可以取到 referer。

好的,我們已經設置好了,現在該做些什么呢?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容