> 本文鏈接:[Web開發新人培訓系列(三)——上線](http://rapheal.sinaapp.com/2015/01/22/webdev-release/ "本文固定鏈接 http://rapheal.sinaapp.com/2015/01/22/webdev-release/")
> 來源:[拉風的博客](http://rapheal.sinaapp.com/)
## 前言
上一篇我們介紹清楚了一個普通Web應用的網絡結構,瀏覽器發起一個請求的目的無非就是想要Web服務器給他一個資源。資源對于這一篇的主題又有什么聯系?看來沒有解釋清楚我這里想說的“資源”,上線這個話題沒法繼續了。
發個招聘廣告:微信開放平臺基礎部招后臺開發,有意者把簡歷發到我郵箱:raphealguo@qq.com
## 資源
我們看看瀏覽器其實最后收到服務器返回的都是什么“資源”:描述頁面結構的HTML文件;網頁的樣式CSS文件;網頁的腳本JS文件;網頁的音頻/視頻/Flash……
因此我們得到一個簡單的結論:
> 資源 == 文件
對于那些動態數據(通過PHP/Java等執行后得到的結果),例如:HTML內容;JSON數據…… 其實我們也可以把這個請求的資源等同于對應的CGI執行文件(PHP腳本文件/CGI二進制等程序)
瀏覽器需要知道服務器回的是什么內容,所以每次的響應包還有一個類型說明這個資源是什么東西,也就是我們經常說的MIME type`(Multipurpose Internet Mail Extensions)代表互聯網媒體類型(Internet media type)`
> MIME type == 文件類型
## 上線?
好,既然瀏覽器每次需要服務器返回一個資源,那我們的Web服務器其實就是放著對應服務所需要的資源。所以我們把這個操作定義為上線:
> 上線 == 把資源放上Web服務器
而資源等同于文件,因此:
> 上線 == 把文件放上Web服務器
像云龍([@前端農民工](http://weibo.com/fouber))說過的,Web架構最重要的能力就是:如何管理資源跟如何部署資源,這里的部署資源我們可以簡單認為就是上線。 留意一下,下文為了通順,有些地方用了`發布`?跟?`部署`這個字眼來描述*上線*的一個動作。
## 靜態資源 & 動態資源
其實我們可以對Web服務器上的資源做個簡單的分類,像JS文件、CSS文件這些資源對于所有用戶來說都是一樣的,我們把它們歸類到靜態資源。對于不同用戶,它們看到的網頁其實是不一致的(例如頁面里邊有他們各自的昵稱,id等信息),這是通過CGI程序實時計算,為不同用戶生成不同的HTML產生,我們把這樣的HTML資源歸類為動態資源。
由于動態資源是需要實時獲取的,因此請求最后都需要落到我們的CGI程序上。而對于靜態資源來說,所有請求都是一樣的,不需要通過CGI去實時獲取文件內容做響應。
## 1臺Web服務器
我們假設世界很美好,我們僅需要一臺服務器就能抗住所有的訪問。OK,現在我們在自己機器(下文把開發階段使用的機器稱為`開發機`)上開發完畢的代碼通過文件傳輸到Web服務器。 傳輸完畢的那一時刻,我們就完成了上線!!

對于像二進制等程序,在傳輸完成后,我們還需要重啟該程序,讓新的程序生效。
## 多臺Web服務器
但是現實往往不是這樣的,在前一篇文章也介紹過,一般我們需要多臺Web服務器,在它們上層的proxy機器把用戶的請求平攤在這些機器上。我們現實的網絡結構大多數是這樣的:

看到這里,你有沒有意識到其實上線的過程隱含一個什么樣的問題!? 剛剛我們在只有一臺服務器的情況下,我們只需要把文件傳輸到這臺服務器就算此次上線完畢。同樣道理在多臺服務器的網絡結構下,我們把文件依次上傳到這些服務器上,對于所有服務器都傳輸完畢后才算此次上線完畢。

你可以注意到,在上線的過程中,有一些服務器是新資源,有一些服務器是舊資源。問題就來了:
*服務器1已經部署上新的資源,而服務器2上還是舊資源。假設這樣一個場景,用戶第一個HTML請求到了服務器1,然后返回一個新的HTML,接著用戶第二個JS請求又跑到服務器2。*
在用戶側的表現就是一個舊的JS文件作用在一個新的HTML上,這樣可能就引發了異常。

存在問題我們就要解決它!分析一下:既然問題是出在服務器資源不一致,那有沒有辦法使得服務器資源一致呢?仔細想想沒法做到資源的實時一致,多臺服務器的網絡結構下必然會存在某小段時間間隙存在資源不一致的現象。既然我們沒法解決這個問題的根本,那就繞開,我們讓同一個用戶的多次請求永遠都在同一臺機器上,如何?
*用戶第一個HTML請求到了服務器1,用戶第二個JS請求還是到服務器1,其他CSS請求、Flash請求……所有資源都落在服務器1*?這樣在沒完成上線的這個間隙時間,用戶要么訪問到的全部都是舊資源,要么全部都是新資源。這樣就沒有新舊資源依賴關系混亂造成的異常了。非常完美! 那剩下的問題就是我們怎么讓同一個用戶的請求一直都落在同一臺機器上了!我們按照下邊三步走,通過proxy把同一個用戶的請求轉發到同一個Web服務器:
1. proxy反向代理機器收到用戶請求
2. proxy通過這個請求某些flagid標記得知當前這個請求是A用戶的
3. 接著proxy定義一個hash算法getServer,通過計算`svr = getServer(flag)`得到最后這個請求要去到的Web服務器svr
所以我們只要保證getServer這個hash算法每次輸出是唯一即可,這個很容易做到。嗯,最后遺留了一個小問題,用戶的請求帶上的flagid是怎么帶的?我們先不糾結這個怎么做,我們只要在用戶登陸我們系統后,在cookies種上一個能標記出這個用戶的id即可。
## 灰度
我們回顧一下剛剛講到了那個讓我們頭疼的問題在經過我們解決之后得到一個什么樣的現象:
* 用戶a所有的資源請求都落在服務器1
* 用戶b所有的資源請求都落在服務器2
到這里,我們看到如果我們上線一個新的功能,在上線的過程中,有一些用戶可以體驗到新功能,一部分用戶則體驗不到。這是個不錯的AB test方法!倘若我們有一個非常重要的更新,或者做了一個可能對后臺服務器有壓力的功能,我們可以通過控制這個上傳的過程先讓小部分用戶體驗,確認新功能在各方面都正常(包括:用戶體驗、服務器壓力等等)之后,我們才把這次更新慢慢部署到所有服務器上。 我們竟然在解決一個問題的情況下還得到了一個非常棒的灰度發布方案! 當然我們也可以通過代碼來控制新功能的灰度,有時候我們也可以兩種灰度方案相互結合,有時候可以避免更大的災難。
最后我們簡單總結一下:
> 在灰度上線過程中,新/舊兩套資源是同時存在現網服務的。
## 測試環境
我們現在整個過程是這樣:

按照這個流程,測試人員在開發機測試完畢后,把開發機的資源發布到現網服務器。這里存在一些非常容易出錯的環節:
1. 測試過程中,開發人員在開發機修改代碼,可能會打斷測試流程
2. 在發布的過程中,如果開發機器的代碼不小心被某個開發刪掉或者修改,就會出現現網bug
3. ……
歸根到底,出現這些問題的原因就是我們沒有把上線和不穩定的開發環境相互隔離!
既然這樣,我們在開發機跟現網服務器中間在加入一個中間層:測試機。為了保證資源發布到測試機的過程不會發生以上問題,所以我們再加入一層:編譯機。我們來看現在上線的流程:

開發人員在不穩定的環境開發,通過輔助系統(我們這邊稱為變更系統)提個變更單記錄需要上線的資源。開發每次在修改完確定代碼穩定后,去編譯機上把需要變更的資源更新。接著測試人員通過變更系統可以把編譯機上的文件部署到測試機,測試就可以專心在一個穩定的測試環境上開始工作了。直到整個發布特性都測試完畢,再通過變更系統把測試機變更的資源發布到現網Web服務器。
到這里好像已經把上線涉及的所有問題都說清楚了。但是還沒考慮全!再回顧一下我上一篇文章[《Web開發新人培訓系列(二)——經典的Web應用網絡模型》](http://rapheal.sinaapp.com/2014/10/24/webdev-network-model/ "《Web開發新人培訓系列(二)——經典的Web應用網絡模型》")。到現在為止我們一直在講述如何把資源上線到我們自己的Web服務器,而忽略了Web網絡結構重要的角色CDN。
## CDN
上次說到我們把資源部署到CDN有兩種方式:
1. 主動推送資源到CDN
2. CDN被動回源到Web服務器請求資源
我們設定一個場景:要上線的資源index.php和index.js,由于index.js是靜態資源,所以我們把它部署到CDN上。 我們留意到一個正常的訪問是html頁面請求之后才發起對靜態資源請求,也就是有新的js引用(index.php),才有新的js請求。所以我們在index.php部署前,先把靜態資源部署好,這樣就不會有混亂的現象出現了。
接下來下邊簡單敘述一下兩種方式的上線時序。
### 主動推送資源到CDN
在我們把index.php部署到我們的Web服務器上之前,我們需要先把index.js的內容推送到CDN的全部節點上,等待推送完畢后,我們才能上線動態資源index.php。
### CDN被動回源
對于回源,有個非常重要的節點,就是回源機器!也就是CDN收到JS文件請求index.js,然后CDN節點上沒有這個文件,它需要向回源機器要這個資源。我們上線第一步是把靜態資源index.js先部署到回源機器。
### 遺留問題
也許你會注意到,如果我把新的index.js部署到CDN后,舊的index.js就沒了,這樣就違背了我們剛剛前邊說的結論:灰度上線過程,新舊資源在現網服務共存。 當然這個問題是可以解決的,為了不把問題再擴散,這篇文章就到此結束吧,下次再整理文章詳細說一下我們在實際項目中整套資源的部署圖。
## 后話
我計劃下一篇文章大概寫一下SVN的相關概念,緊接著再有一篇專門寫我們如何把SVN代碼管理跟上線流程更規范化,提高在上線/測試環節的效率以及減少我們人工操作帶來的失誤。