Java程式設計師進階三條必經之路:資料庫、虛擬機器、非同步通訊。
前言
雖然非同步是我們急需掌握的高階技術,但是不積跬步無以至千里,同步技術的學習是不能省略的。今天這篇文章主要用Python來介紹Web併發模型,直觀地展現同步技術的缺陷以及非同步好在哪裡。
最簡單的併發
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import socket response = 'HTTP/1.1 200 OK\r\nConnection: Close\r\nContent-Length: 11\r\n\r\nHello World' server = socket.socket() server.bind(('0.0.0.0', 9527)) server.listen(1024) while True: client, clientaddr = server.accept() # blocking request = client.recv(1024) # blocking client.send(response) # maybe blocking client.close() |
上面這個例子太簡單了,訪問localhost:9527,返回“Hello World”。用ab來測試效能,資料如下:
1 2 |
ab -n 100000 -c 8 http://localhost:9527/ Time taken for tests: 1.568 seconds |
傳送10萬個請求,8(我的CPU核數為8)個請求同時併發,耗時1.568秒。
效能瓶頸在哪裡呢?就在上面的兩個半阻塞。
accept和recv是完全阻塞的,而為什麼send是半個阻塞呢?
在核心的 socket實現中,會有兩個快取 (buffer)。read buffer 和 write buffer 。當核心接收到網路卡傳來的客戶端資料後,把資料複製到 read buffer ,這個時候 recv阻塞的程式就可以被喚醒。
當呼叫 send的時候,核心只是把 send的資料複製到 write buffer 裡,然後立即返回。只有 write buffer 的空間不夠時 send才會被阻塞,需要等待網路卡傳送資料騰空 write buffer 。在 write buffer的空間足夠放下 send的資料時程式才可以被喚醒。
如果一個請求處理地很慢,其他請求只能排隊,那麼併發量肯定會受到影響。
多程式
每個請求對應一個程式倒是能解決上面的問題,但是程式太佔資源,每個請求的資源都是獨立的,無法共享,而且程式的上下文切換成本也很高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import socket import signal import multiprocessing response = 'HTTP/1.1 200 OK\r\nConnection: Close\r\nContent-Length: 11\r\n\r\nHello World' server = socket.socket() server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('0.0.0.0', 9527)) server.listen(1024) def handler(client): request = client.recv(1024) client.send(response) client.close() #多程式裡的子程式執行完後並不會死掉,而是變成殭屍程式,等待主程式掛掉後才會死掉,下面這條語句可以解決這個問題。 signal.signal(signal.SIGCHLD,signal.SIG_IGN) while True: client, addr = server.accept() process = multiprocessing.Process(target=handler, args=(client,)) process.start() |
Prefork
這是多程式的改良版,預先分配好和CPU核數一樣的程式數,可以控制資源佔用,高效處理請求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import socket import multiprocessing response = 'HTTP/1.1 200 OK\r\nConnection: Close\r\nContent-Length: 11\r\n\r\nHello World' server = socket.socket() server.bind(('0.0.0.0', 9527)) server.listen(1024) def handler(): while True: client, addr = server.accept() request = client.recv(1024) client.send(response) client.close() processors = 8 for i in range(0, processors): process = multiprocessing.Process(target=handler, args=()) process.start() |
耗時:1.640秒。
執行緒池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import Queue import socket import threading response = 'HTTP/1.1 200 OK\r\nConnection: Close\r\nContent-Length: 11\r\n\r\nHello World' server = socket.socket() server.bind(('0.0.0.0', 9527)) server.listen(1024) def handler(queue): while True: client = queue.get() request = client.recv(1024) client.send(response) client.close() queue = Queue.Queue() processors = 8 for i in range(0, processors): thread = threading.Thread(target=handler, args=(queue,)) thread.daemon = True thread.start() while True: client, clientaddr = server.accept() queue.put(client) |
耗時:3.901秒,大部分時間花在佇列上,執行緒佔用資源比程式少(資源可以共享),但是要考慮執行緒安全問題和鎖的效能,而且python有臭名昭著的GIL,導致不能有效利用多核CPU。
epoll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import select import socket response = 'HTTP/1.1 200 OK\r\nConnection: Close\r\nContent-Length: 11\r\n\r\nHello World' server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setblocking(False) server_address = ('localhost', 9527) server.bind(server_address) server.listen(1024) READ_ONLY = select.EPOLLIN | select.EPOLLPRI epoll = select.epoll() epoll.register(server, READ_ONLY) timeout = 60 fd_to_socket = { server.fileno(): server} while True: events = epoll.poll(timeout) for fd, flag in events: sock = fd_to_socket[fd] if flag & READ_ONLY: if sock is server: conn, client_address = sock.accept() conn.setblocking(False) fd_to_socket[conn.fileno()] = conn epoll.register(conn, READ_ONLY) else: request = sock.recv(1024) sock.send(response) sock.close() del fd_to_socket[fd] |
最後祭出epoll大神,三大非同步通訊框架Netty、NodeJS、Tornado共同採用的通訊技術,耗時1.582秒,但是要注意是單程式單執行緒哦。epoll真正發揮作用是在長連線應用裡,單執行緒處理上萬個長連線玩一樣,佔用資源極少。