後端知識體系--一次完整的HTTP請求
這裡講的請求是後端DevOps可以控制的範圍內,不包括DNS解析,層層的路由等等,一切都從請求到達我們自己架設的伺服器開始。
1.與伺服器建立連線
1.1 TCP連線的建立
客戶端的請求到達伺服器,首先就是建立TCP連線
Client首先發送一個連線試探,ACK=0 表示確認號無效,SYN = 1 表示這是一個連線請求或連線接受報文,同時表示這個資料報不能攜帶資料,seq = x 表示Client自己的初始序號(seq = 0 就代表這是第0號包),這時候Client進入syn_sent狀態,表示客戶端等待伺服器的回覆
Server監聽到連線請求報文後,如同意建立連線,則向Client傳送確認。TCP報文首部中的SYN 和 ACK都置1 ,ack = x + 1表示期望收到對方下一個報文段的第一個資料位元組序號是x+1,同時表明x為止的所有資料都已正確收到(ack=1其實是ack=0+1,也就是期望客戶端的第1個包),seq = y 表示Server 自己的初始序號(seq=0就代表這是伺服器這邊發出的第0號包)。這時伺服器進入syn_rcvd,表示伺服器已經收到Client的連線請求,等待client的確認。
Client收到確認後還需再次傳送確認,同時攜帶要傳送給Server的資料。ACK 置1 表示確認號ack= y + 1 有效(代表期望收到伺服器的第1個包),Client自己的序號seq= x + 1(表示這就是我的第1個包,相對於第0個包來說的),一旦收到Client的確認之後,這個TCP連線就進入Established狀態,就可以發起http請求了。
1.2 常見TCP連線限制
1.2.1 修改使用者程序可開啟檔案數限制
在Linux平臺上,無論編寫客戶端程式還是服務端程式,在進行高併發TCP連線處理時,最高的併發數量都要受到系統對使用者單一程序同時可開啟檔案數量的限制(這是因為系統為每個TCP連線都要建立一個socket控制代碼,每個socket控制代碼同時也是一個檔案控制代碼)。可使用ulimit命令檢視系統允許當前使用者程序開啟的檔案數限制,windows上是256,linux是1024,這個部落格的伺服器是65535
1.2.2 修改網路核心對TCP連線的有關限制
在Linux上編寫支援高併發TCP連線的客戶端通訊處理程式時,有時會發現儘管已經解除了系統對使用者同時開啟檔案數的限制,但仍會出現併發TCP連線數增加到一定數量時,再也無法成功建立新的TCP連線的現象。出現這種現在的原因有多種。
第一種原因可能是因為Linux網路核心對本地埠號範圍有限制。此時,進一步分析為什麼無法建立TCP連線,會發現問題出在connect()呼叫返回失敗,檢視系統錯誤提示訊息是“Can’t assign requestedaddress”。同時,如果在此時用tcpdump工具監視網路,會發現根本沒有TCP連線時客戶端發SYN包的網路流量。這些情況說明問題在於本地Linux系統核心中有限制。
其實,問題的根本原因在於Linux核心的TCP/IP協議實現模組對系統中所有的客戶端TCP連線對應的本地埠號的範圍進行了限制(例如,核心限制本地埠號的範圍為1024~32768之間)。當系統中某一時刻同時存在太多的TCP客戶端連線時,由於每個TCP客戶端連線都要佔用一個唯一的本地埠號(此埠號在系統的本地埠號範圍限制中),如果現有的TCP客戶端連線已將所有的本地埠號佔滿,則此時就無法為新的TCP客戶端連線分配一個本地埠號了,因此係統會在這種情況下在connect()呼叫中返回失敗,並將錯誤提示訊息設為“Can’t assignrequested address”。
2.發起HTTP請求
2.1 請求格式
例如這樣的一個請求
Accept 就是告訴伺服器端,我接受那些MIME型別
Accept-Encoding 這個看起來是接受那些壓縮方式的檔案
Accept-Lanague 告訴伺服器能夠傳送哪些語言
Connection 告訴伺服器支援keep-alive特性
Cookie 每次請求時都會攜帶上Cookie以方便伺服器端識別是否是同一個客戶端
Host 用來標識請求伺服器上的那個虛擬主機,比如Nginx裡面可以定義很多個虛擬主機
那這裡就是用來標識要訪問那個虛擬主機。
User-Agent 使用者代理,一般情況是瀏覽器,也有其他型別,如:wget curl 搜尋引擎的蜘蛛等
條件請求首部:
If-Modified-Since 是瀏覽器向伺服器端詢問某個資原始檔如果自從什麼時間修改過,那麼重新發給我,這樣就保證伺服器端資源
檔案更新時,瀏覽器再次去請求,而不是使用快取中的檔案
安全請求首部:
Authorization: 客戶端提供給伺服器的認證資訊
2.2 keep-alive/persitent
每次HTTP請求都重新建立TCP連線的開銷是很大的,於是就出現了keep-alive這個首部,它允許在一次TCP連線中傳送/接收多個HTTP報文
然而,keep-alive是有弊端的。在HTTP1.0中,客戶端發起請求是加上keep-alive首部,服務端響應時也加上keep-alive首部,那麼這個請求就被認為是keep-alive的,直到其中一方主動斷開為止。如果沒有正確斷開,這個資源就會一直被佔用了。
啞代理問題:啞代理只是單純的轉發請求,並不能進行解析處理、維持持久連線等其他工作,而聰明的代理可以解析接收到的報文同時可以維持持久連線。
如上圖,當客戶端與伺服器之間存在不解析直接轉發的代理時,connection:keep-alive這個首部是直接轉發給伺服器的,伺服器接收了這個請求之後,就會向客戶端傳送帶有connection:keep-alive的響應,同樣盲代理不會解析響應,直接將全部響應轉發回客戶端。因為客戶端收到了這個首部,就認為建立持久連線已經成功了,但是中間的”笨代理“,並不知道這些事情,笨代理只有一種行為模式:在轉發請求和回送伺服器響應請求之後就認為這次事務結束了,等待連線斷開,而這時由於connection:keep-alive首部已經發送到伺服器和客戶端,雙方都認為持久連線已經建立完成,這樣就變成了兩邊認為持久連線OK而中間的啞代理等待連線斷開的情況,這種情況下如果客戶端再一次在這條連線上傳送請求,請求就會在亞代理處停止,因為啞代理已經在等待連線關閉。這種狀態會導致瀏覽器一直處於掛起狀態,直到客戶端或伺服器之中一個連線超時,關閉連線為止,一段美好的牽手就這麼沒了(啞代理就是把內容原封不動的轉發到代理)。
為了避免這種情況,現代的代理是不會轉發connection:keep-alive這個首部的。
persistent
HTTP/1.1的持久連線預設是開啟的,只有首部中包含connection:close,才會事務結束之後關閉連線。當然伺服器和客戶端仍可以隨時關閉持久連線。
當傳送了connection:close首部之後客戶端就沒有辦法在那條連線上傳送更多的請求了。當然根據持久連線的特性,一定要傳輸正確的content-length。
還有根據HTTP/1.1的特性,是不應該和HTTP/1.0客戶端建立持久連線的。最後,一定要做好重發的準備。
管道化連線
HTTP/1.1允許在持久連線上使用管道,這樣就不用等待前一個請求的響應,直接在管道上傳送第二個請求,在高延遲下,提高效能。
管道化連線的限制:
- 不是持久連線就不能使用管道。
- 必須按照同樣的傳送順序回送響應,因為報文沒有標籤,很可能就順序就亂咯。
- 因為可以隨時關閉持久連線,所以要隨時做好重發準備
- 不應該使用管道化傳送重複傳送會有副作用的請求(如post,重複提交)。
3.負載均衡
接收到HTTP請求之後,就輪到負載均衡登場了,它位於網站的最前端,把短時間內較高的訪問量分攤到不同機器上處理。負載均衡方案有軟體、硬體兩種
F5 BIG-IP是著名的硬體方案,但這裡不作討論
軟體方案有LVS HAProxy Nginx等,留作以後補充
4.Nginx(WEB伺服器)
在典型的Rails應用部署方案中,Nginx的作用有兩個
- 處理靜態檔案請求
- 轉發請求給後端的Rails應用
這是一個簡單的Nginx配置檔案
後端的Rails伺服器通過unix socket與Nginx通訊,Nginx伺服public資料夾裡的靜態檔案給使用者
5.Rails(應用伺服器)
這篇文章無敵了,我沒有更多可以寫的,只能說一句我用的是Puma。因為伺服器是單核的,只能用多執行緒Puma或者事件驅動的Thin,考慮到以後可能用上Rails 5 ActionCabel,還是直接上Puma吧。
6.資料庫(資料庫伺服器)
應用伺服器想訪問資料庫,就需要與資料庫建立連線。Rails讀取database.yml中的配置,訪問對應的資料庫。
一個重要的配置指標pool: Rails中的資料庫連線是完全執行緒安全的,所有pool的值要配置成與Puma的最大執行緒數相等,這樣就不會出現執行緒等待資料庫連線的情況。