Java程序員進(jìn)階三條必經(jīng)之路:數(shù)據(jù)庫、虛擬機(jī)、異步通信。
前言
雖然異步是我們急需掌握的高階技術(shù),但是不積跬步無以至千里,同步技術(shù)的學(xué)習(xí)是不能省略的。今天這篇文章主要用Python來介紹Web并發(fā)模型,直觀地展現(xiàn)同步技術(shù)的缺陷以及異步好在哪里。
最簡單的并發(fā)
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()
上面這個(gè)例子太簡單了,訪問localhost:9527,返回“Hello World”。用ab來測試性能,數(shù)據(jù)如下:
ab -n 100000 -c 8 http://localhost:9527/
Time taken for tests: 1.568 seconds
發(fā)送10萬個(gè)請求,8(我的CPU核數(shù)為8)個(gè)請求同時(shí)并發(fā),耗時(shí)1.568秒。
性能瓶頸在哪里呢?就在上面的兩個(gè)半阻塞。
accept和recv是完全阻塞的,而為什么send是半個(gè)阻塞呢?
在內(nèi)核的 socket實(shí)現(xiàn)中,會有兩個(gè)緩存 (buffer)。read buffer 和 write buffer 。當(dāng)內(nèi)核接收到網(wǎng)卡傳來的客戶端數(shù)據(jù)后,把數(shù)據(jù)復(fù)制到 read buffer ,這個(gè)時(shí)候 recv阻塞的進(jìn)程就可以被喚醒。
當(dāng)調(diào)用 send的時(shí)候,內(nèi)核只是把 send的數(shù)據(jù)復(fù)制到 write buffer 里,然后立即返回。只有 write buffer 的空間不夠時(shí) send才會被阻塞,需要等待網(wǎng)卡發(fā)送數(shù)據(jù)騰空 write buffer 。在 write buffer的空間足夠放下 send的數(shù)據(jù)時(shí)進(jìn)程才可以被喚醒。
如果一個(gè)請求處理地很慢,其他請求只能排隊(duì),那么并發(fā)量肯定會受到影響。
多進(jìn)程
每個(gè)請求對應(yīng)一個(gè)進(jìn)程倒是能解決上面的問題,但是進(jìn)程太占資源,每個(gè)請求的資源都是獨(dú)立的,無法共享,而且進(jìn)程的上下文切換成本也很高。
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()
#多進(jìn)程里的子進(jìn)程執(zhí)行完后并不會死掉,而是變成僵尸進(jìn)程,等待主進(jìn)程掛掉后才會死掉,下面這條語句可以解決這個(gè)問題。
signal.signal(signal.SIGCHLD,signal.SIG_IGN)
while True:
client, addr = server.accept()
process = multiprocessing.Process(target=handler, args=(client,))
process.start()
Prefork
這是多進(jìn)程的改良版,預(yù)先分配好和CPU核數(shù)一樣的進(jìn)程數(shù),可以控制資源占用,高效處理請求。
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()
耗時(shí):1.640秒。
線程池
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)
耗時(shí):3.901秒,大部分時(shí)間花在隊(duì)列上,線程占用資源比進(jìn)程少(資源可以共享),但是要考慮線程安全問題和鎖的性能,而且python有臭名昭著的GIL,導(dǎo)致不能有效利用多核CPU。
epoll
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共同采用的通信技術(shù),耗時(shí)1.582秒,但是要注意是單進(jìn)程單線程哦。epoll真正發(fā)揮作用是在長連接應(yīng)用里,單線程處理上萬個(gè)長連接玩一樣,占用資源極少。