用Python理解Web并發(fā)模型

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è)長連接玩一樣,占用資源極少。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容