RPC伺服器之【多程式描述符傳遞】高階模型
今天老師要給大家介紹一個比較特別的 RPC 伺服器模型,這個模型不同於 Nginx、不同於 Redis、不同於 Apache、不同於 Tornado、不同於 Netty,它的原型是 Node Cluster 的多程式併發模型。
Nginx 併發模型
我們知道 Nginx 的併發模型是一個多程式併發模型,它的 Master 程式在繫結監聽地址埠後 fork 出了多個 Slave 程式共同競爭處理這個服務端套接字接收到的很多客戶端連線。
這多個 Slave 程式會共享同一個處於作業系統核心態的套接字佇列,作業系統的網路模組在處理完三次握手後就會將套接字塞進這個佇列。這是一個生產者消費者模型,生產者是作業系統的網路模組,消費者是多個 Slave 程式,佇列中的物件是客戶端套接字。
這種模型在負載均衡上有一個缺點,那就是套接字分配不均勻,形成了類似於貧富分化的局面,也就是「閒者愈閒,忙者愈忙」的狀態。這是因為當多個程式競爭同一個套接字佇列時,作業系統採用了 LIFO 的策略,最後一個來 accept 的程式最優先拿到 套接字。越是繁忙的程式越是有更多的機會呼叫 accept,它能拿到的套接字也就越多。
Node Cluster 併發模型
Node Cluster 為了解決負載均衡問題,它採用了不同的策略。它也是多程式併發模型,Master 程式會 fork 出多個子程式來處理客戶端套接字。但是不存在競爭問題,因為負責 accept 套接字的只能是 Master 程式,Slave 程式只負責處理客戶端套接字請求。那就存在一個問題,Master 程式拿到的客戶端套接字如何傳遞給 Slave 程式。
這時,神奇的 sendmsg 登場了。它是作業系統提供的系統呼叫,可以在不同的程式之間傳遞檔案描述符。sendmsg 會搭乘一個特殊的「管道」將 Master 程式的套接字描述符傳遞到 Slave 程式,Slave 程式通過 recvmsg 系統呼叫從這個「管道」中將描述符取出來。這個「管道」比較特殊,它是 Unix 域套接字。普通的套接字可以跨機器傳輸訊息,Unix 域套接字只能在同一個機器的不同程式之間傳遞訊息。同管道一樣,Unix 域套接字也分為有名套接字和無名套接字,有名套接字會在檔案系統指定一個路徑名,無關程式之間都可以通過這個路徑來訪問 Unix 域套接字。而無名套接字一般用於父子程式之間,父程式會通過 socketpair 呼叫來建立套接字,然後 fork 出來子程式,這樣子程式也會同時持有這個套接字的引用。後續父子程式就可以通過這個套接字互相通訊。
注意這裡的傳遞描述符,本質上不是傳遞,而是複製。父程式的描述符並不會在 sendmsg 自動關閉自動消失,子程式收到的描述符和父程式的描述符也不是同一個整數值。但是父子程式的描述符都會指向同一個核心套接字物件。
有了描述符的傳遞能力,父程式就可以將 accept 到的客戶端套接字輪流傳遞給多個 Slave 程式,負載均衡的目標就可以順利實現了。
接下來我們就是用 Python 程式碼來擼一遍 Node Cluster 的併發模型。因為 sendmsg 和 recvmsg 方法到了 Python3.5 才內建進來,所以下面的程式碼需要使用 Python3.5+才可以執行。
我們看 sendmsg 方法的定義
socket.sendmsg(buffers[, ancdata[, flags[, address]]])
我們只需要關心第二個引數 ancdata,描述符是通過ancdata 引數傳遞的,它的意思是 「輔助資料」,而 buffers 表示需要傳遞的訊息內容,因為訊息內容這裡沒有意義,所以這個欄位可以任意填寫,但是必須要有內容,如果沒有內容,sendmsg 方法就是一個空呼叫。
import socket, struct
def send_fds(sock, fd):
return sock.sendmsg([b`x`], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, struct.pack("i", fd))])
# ancdata 引數是一個三元組的列表,三元組的第一個參數列示網路協議棧級別 level,第二個參數列示輔助資料的型別 type,第三個引數才是攜帶的資料,level=SOL_SOCKET 表示傳遞的資料處於 TCP 協議層級,type=SCM_RIGHTS 就表示攜帶的資料是檔案描述符。我們傳遞的描述符 fd 是一個整數,需要使用 struct 包將它序列化成二進位制。
再看 recvmsg 方法的定義
msg, ancdata, flags, addr = socket.recvmsg(bufsize[, ancbufsize[, flags]])
同樣,我們只需要關心返回的 ancdata 資料,它裡面包含了我們需要的檔案描述符。但是需要提供訊息體的長度和輔助資料的長度引數。輔助資料的長度比較特殊,需要使用 CMSG_LEN 方法來計算,因為輔助資料裡面還有我們看不到的額外的頭部資訊。
bufsize = 1 # 訊息內容的長度
ancbufsize = socket.CMSG_LEN(struct.calcsize(`i`)) # 輔助資料的長度
msg, ancdata, flags, addr = socket.recvmsg(bufsize, ancbufsize) # 收取訊息
level, type, fd_bytes = ancdata[0] # 取第一個元祖,注意傳送訊息時我們傳遞的是一個三元組的列表
fd = struct.unpack(`i`, fd_bytes) # 反序列化
程式碼實現
下面我來獻上完整的伺服器程式碼,為了簡單起見,我們在 Slave 程式中處理 RPC 請求使用同步模型。
# coding: utf
# sendmsg recvmsg python3.5+才可以支援
import os
import json
import struct
import socket
def handle_conn(conn, addr, handlers):
print(addr, "comes")
while True:
# 簡單起見,這裡就沒有使用迴圈讀取了
length_prefix = conn.recv(4)
if not length_prefix:
print(addr, "bye")
conn.close()
break # 關閉連線,繼續處理下一個連線
length, = struct.unpack("I", length_prefix)
body = conn.recv(length)
request = json.loads(body)
in_ = request[`in`]
params = request[`params`]
print(in_, params)
handler = handlers[in_]
handler(conn, params)
def loop_slave(pr, handlers):
while True:
bufsize = 1
ancsize = socket.CMSG_LEN(struct.calcsize(`i`))
msg, ancdata, flags, addr = pr.recvmsg(bufsize, ancsize)
cmsg_level, cmsg_type, cmsg_data = ancdata[0]
fd = struct.unpack(`i`, cmsg_data)[0]
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, fileno=fd)
handle_conn(sock, sock.getpeername(), handlers)
def ping(conn, params):
send_result(conn, "pong", params)
def send_result(conn, out, result):
response = json.dumps({"out": out, "result": result}).encode(`utf-8`)
length_prefix = struct.pack("I", len(response))
conn.sendall(length_prefix)
conn.sendall(response)
def loop_master(serv_sock, pws):
idx = 0
while True:
sock, addr = serv_sock.accept()
pw = pws[idx % len(pws)]
# 訊息資料,whatever
msg = [b`x`]
# 輔助資料,攜帶描述符
ancdata = [(
socket.SOL_SOCKET,
socket.SCM_RIGHTS,
struct.pack(`i`, sock.fileno()))]
pw.sendmsg(msg, ancdata)
sock.close() # 關閉引用
idx += 1
def prefork(serv_sock, n):
pws = []
for i in range(n):
# 開闢父子程式通訊「管道」
pr, pw = socket.socketpair()
pid = os.fork()
if pid < 0: # fork error
return pws
if pid > 0:
# 父程式
pr.close() # 父程式不用讀
pws.append(pw)
continue
if pid == 0:
# 子程式
serv_sock.close() # 關閉引用
pw.close() # 子程式不用寫
return pr
return pws
if __name__ == `__main__`:
serv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serv_sock.bind(("localhost", 8080))
serv_sock.listen(1)
pws_or_pr = prefork(serv_sock, 10)
if hasattr(pws_or_pr, `__len__`):
if pws_or_pr:
loop_master(serv_sock, pws_or_pr)
else:
# fork 全部失敗,沒有子程式,Game Over
serv_sock.close()
else:
handlers = {
"ping": ping
}
loop_slave(pws_or_pr, handlers)
父程式使用 fork 呼叫建立了多個子程式,然後又使用 socketpair 呼叫為每一個子程式都建立一個無名套接字用來傳遞描述符。父程式使用 roundrobin 策略平均分配接收到的客戶端套接字。子程式接收到的是一個描述符整數,需要將描述符包裝成套接字物件後方可讀寫。列印對比傳送和接收到的描述符,你會發現它們倆的值並不相同,這是因為 sendmsg 將描述符傳送到核心後,核心給描述符指向的核心套接字又重新分配了一個新的描述符物件。
歡迎工作一到五年的Java工程師朋友們加入Java架構開發:744677563
本群提供免費的學習指導 架構資料 以及免費的解答
不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導
相關文章
- RPC 伺服器之【多程式描述符傳遞】高階模型RPC伺服器模型
- 高階IO模型之kqueue和epoll模型
- Go高階特性 13 | 引數傳遞:值、引用及指標之間的區別?Go指標
- Laravel集合探學系列——高階訊息傳遞實現(二)Laravel
- Django高階程式設計之自定義Field實現多語言Django程式設計
- 前沿高階技術之遞迴神經網路(RNN)遞迴神經網路RNN
- JavaScript之按值傳遞JavaScript
- vue + axios 實現分頁引數傳遞,高階搜尋功能實現VueiOS
- 伺服器端程式設計之 IO 模型伺服器程式設計模型
- AbilitySlice之間的傳遞值
- SOLIDWORKS外掛SolidKits高階BOM之批次寫入模型屬性Solid模型
- 微信小程式父子元件之間的資料傳遞微信小程式元件
- Jmeter(五十二) - 從入門到精通高階篇 - jmeter之跨執行緒組傳遞引數(詳解教程)JMeter執行緒
- Rust 程式設計影片教程(進階)——017_1 訊息傳遞 1Rust程式設計
- Rust 程式設計影片教程(進階)——017_2 訊息傳遞 2Rust程式設計
- Rust 程式設計影片教程(進階)——017_3 訊息傳遞 3Rust程式設計
- Ability之間或者程式間資料傳遞之物件(Sequenceable序列化)物件
- 從事件驅動程式設計模型分析Handler訊息傳遞機制事件程式設計模型
- Cisco靜態路由高階用法(干涉距離向量協議的路由條目的傳遞)路由協議
- IO 模型 select 編寫多程式 Web 伺服器 PHP 版模型Web伺服器PHP
- Python 高階程式設計:深入探索高階程式碼實踐Python程式設計
- 《前端之路》之 JavaScript 高階技巧、高階函式(一)前端JavaScript函式
- Maven 高階篇之構建多模組專案的方法Maven
- mysql多條件過濾查詢之mysq高階查詢MySql
- 頁面之間傳遞資料
- 兄弟元件之間資訊傳遞元件
- Vue父子之間的值傳遞Vue
- 引數傳遞機制之JWTJWT
- JAVA基礎之-引數傳遞Java
- Rust 程式設計視訊教程(進階)——017_1 訊息傳遞 1Rust程式設計
- Rust 程式設計視訊教程(進階)——017_2 訊息傳遞 2Rust程式設計
- Rust 程式設計視訊教程(進階)——017_3 訊息傳遞 3Rust程式設計
- JavaScript高階程式設計學習(一)之介紹JavaScript程式設計
- Python學習之物件導向高階程式設計Python物件程式設計
- 值傳遞和引用傳遞
- 高階前端的進階——CSS之flex前端CSSFlex
- Java高階特性之集合Java
- js高階之-new map()JS