nginx代理socket.io服務踩坑
nginx代理了兩臺socket.io伺服器。socket.io的工作模式是polling升級到websocket
現象
通過nginx請求服務時,出現了大量的400錯誤,有時候能升級到websocket,有時候會一直報錯。但是直接通過 ip+埠
訪問時,100%能成功。
分析
sid
sid是我們這個問題的關鍵。在初始建立連線時(polling模式就是在模擬一個長連線),客戶端會發起這樣的請求:
https://***/?EIO=3&transport=polling&t=1540820717277-0
服務端收到後會建立一個物件,繫結在這個連線上,同時返回一個sid(session id),來標記這個會話。會話指什麼呢,會話是一連串的互動,這些互動之間是有聯絡的,在我們這個場景下就是,下一次的http請求到來,我需要找到之前繫結在理論上的長連線(這裡還沒有websocket,所以是理論上的)上的那個物件。我們知道http請求是無狀態的,每個請求之間獨立,所以socket.io引入了sid來做這件事。服務端收到請求後會生成一個sid,看下response:
{"sid":"EoGaL3fRQlpTOaLp5eST","upgrades":["websocket"],"pingInterval":8000,"pingTimeout":10000}
之後每次請求都需要帶上這個sid,建立websocket請求的連線也不例外。所以說,sid是polling,以及polling升級到websocket的關鍵。這之後的請求類似於:
https://***/?EIO=3&transport=polling&t=1540820717314-1&sid=EoGaL3fRQlpTOaLp5eST or wss://***/?EIO=3&transport=websocket&t=1540820717314-1&sid=EoGaL3fRQlpTOaLp5eST
那麼問題來了,如果請求是帶上的sid不是服務端生成的會怎樣呢?服務端會不認識,給你返回一個400,並告訴你
invalid sid
我們遇到的便是這個問題,nginx預設的負載均衡策略是輪詢,所以請求有可能會打到不是生成這個sid的機器上去,這時候我們就會收到一個400,如果運氣好,可能也會打到原來的機器上,運氣更好一點,甚至能堅持到websocket連線建立。
解決
這裡提出兩種方案
- nginx的負載均衡採用ip_hash,這樣能保證一個客戶端的請求都走到一臺伺服器上
- 不使用polling模式,只使用websocket
這兩種方案各有利弊。第二種顯而易見,不支援websocket的古老瀏覽器和客戶端將沒法工作。第一種的問題隱藏得比較深,試想,如果你增減了機器會怎樣,這時候ip_hash策略的模將變化,之前的連線將全部失效,而對於微服務,擴縮容是很頻繁的操作(特別是產品處於發展期),這種有損的擴縮容很大概率是不能接受的。
綜上,建議直接使用websocket,畢竟不支援websocket的老版本佔比很少,而且相對於先polling,耗時也會減少。