“We learn most when we have to invent” —Piaget
Trong phần 2 bạn đã được tạo một WSGI server cơ bản, có thể xử lý cơ bản HTTP GET requests. Và tôi đã hỏi bạn một câu hỏi, "Làm thế nào để server của bạn có thể xử lý nhiều hơn một requests một lúc?". Trong bài viết này bạn sẽ tìm thấy câu trả lời. Vậy, hãy tập trung và chuyển sang phần tiếp theo. Bạn sẽ có một chuyến đi thực sự nhanh. Hãy chuẩn bị sẵn Linux, Mac OS X (hoặc bất kỳ hệ thống *nix) và Python đã sẵn sàng. Bạn có thể xem code của tất cả bài viết này tại đây: https://github.com/rspivak/lsbaws/tree/master/part3
Đầu tiên hãy nhớ lại cái cơ bản của một Web server trông như thế nào và cái gì server cần để phục vụ yêu cầu của clients. Server của bạn đã tạo trong phần 1 và phần 2 là một yêu cầu của một máy khách tại một thời điểm. Nó không chấp nhận một kết nối mới cho đến khi nó hoàn thành xử lý hiện tại gửi từ clients. Một vài clients thấy không hài lòng về nó bởi vì họ sẽ phải xếp hàng để đợi và đối với máy chủ bận có thì phải đợi rất lâu.
Đây là code của server chờ đợi webserver3a.py:
#####################################################################
# Iterative server - webserver3a.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
#####################################################################
import socket
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5
def handle_request(client_connection):
request = client_connection.recv(1024)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
while True:
client_connection, client_address = listen_socket.accept()
handle_request(client_connection)
client_connection.close()
if __name__ == '__main__':
serve_forever()
Đây là mã máy chủ đang ngủ webserver3b.py:
Chạy server với lệnh:
$ python webserver3b.py
Mở cử số terminal mới và chạy lệnh curl. Bạn sẽ thấy ngay “Hello, World!” in trên màn hình.
$ curl http://localhost:8888/hello
Hello, World!
Mở ngay một cửa sổ mới và chạy lệnh curl sau:
$ curl http://localhost:8888/hello
Nếu bạn hoàn thành nó trong 60 giây, sau đó lệnh curl thứ hai không tạo ra bất kỳ đầu ra nào ngay lập tức và bị treo ở đó. Server không in ra phần thân request mới. Đây là cách nó làm việc trên Mac (cửa sổ phía dưới bên phải được đánh dấu màu vàng cho thấy lệnh curl thứ hai đang treo, đợi kết nối được chấp nhận từ máy chủ):
Socket là một giao thức trừu trượng của điểm cuối giao tiếp và nó cho phép chương trình của bạn giao tiếp với một chương trình khác sử dụng file cấu hình. Trong bài viết này, tôi nói về TCP/IP sockets trên Linux/Mac OS X. Chú ý "Hiểu được cách TCP socket kết nối".
Socket pair cho một kết nối TCP là một 4-tuple xác định 2 điểm cuối của kết nối TCP: local IP address, local port, foreign IP address, and foreign port. Một socket pair xác định duy nhất mọi kết nối trên mạng. Hai giá trị xác định mỗi điểm cuối, một IP address and một port number, thường được gọi là một socket.
Vậy, tuple {10.10.10.2:49152, 12.12.12.3:8888} là một socket pair xác định 2 điểm cuối của kết nối TCP trên client và tuple {12.12.12.3:8888, 10.10.10.2:49152} là một socket pair xác định 2 điểm cuối tương tự trên server. Hai giá trị xác định điểm cuối của kết nối TCP, IP address 12.12.12.3 và port 8888 được gọi là một socket trong trường hợp này.
Một tiêu chuẩn tuần tự một server thường tạo ra một socket và chấp nhận kết nối từ client:
1. Server tạo ra một TCP/IP socket. Dưới đây là câu lệnh trên Python:
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2. Server có thể đặt một vài giá trị tuỳ chọn (đây là tuỳ chọn, nhưng bạn có thể thấy rằng code phía trên chỉ thực hiện việc đó để sử dụng lại một địa chỉ tương tự nếu bạn quyết định kill và khởi động lại server)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
3. Sau đó server liên kết tới địa chỉ. Hàm bind gán một địa chỉ giao thức cục bộ cho socket. Với TCP, gọi bind chỉ định một port number, một IP address, cả hai hoặc cũng không.
listen_socket.bind(SERVER_ADDRESS)
4. Sau đó server lắng nghe một socket khác.
Phương thức listen chỉ được gọi bởi server. Nó nói với kernel rằng nó chấp nhận những yêu cầu kết nối tới socket này.
Đây là những gì một client cần làm để giao tiếp với server qua TCP/IP.
Đây là code mẫu cho một client để kết nối tới server, gửi một request và in response:
import socket
# create a socket and connect to a server
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 8888))
# send and receive some data
sock.sendall(b'test')
data = sock.recv(1024)
print(data.decode())
Sau khi tạo ra socket, client cần kết nối tới server. Hàm connect được gọi:
sock.connect(('localhost', 8888))
Client chỉ cung cấp remote IP address hoặc host name và remote port number của server để kết nối.
Bạn có thể thấy rằng client không gọi bind và accept. Client không cần gọi bind bởi vì client không quan tâm đến địa chỉ IP local và local port number. TCP/IP stack trong kernel tự động gán địa chỉ IP local và local port khi client gọi hàm connect. Local port được gọi là một ephemeral port, một short-lived port.
Một port trên server được biết đến như một service được client kết nối tới khi đã được gọi (ví dụ: 80 cho HTTP và 22 cho SSH). Mở Pyhton shell và tạo một kết nối tới server để server đó chạy trên localhost và thấy ephemeral port kernel chỉ định cho socket của bạn đã tạo (lưu server với tên webserver3a.py hoặc webserver3b.py và thử đoạn code bên dưới).
>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> sock.connect(('localhost', 8888))
>>> host, port = sock.getsockname()[:2]
>>> host, port
('127.0.0.1', 60589)
Trong phần này kernel đã gán ephemeral port 60589 cho socket.
Có một số khái niệm quan trọng bạn cần nắm trước khi tôi trả lời câu hỏi trong phần 2. Bạn sẽ thấy tại sao nó quan trọng. Hai khái niệm đó là một process và một file descriptor.
Một process là gì? Một process chỉ là một thể hiện của một chương trình thực thi. Khi code server được thực thi, ví dụ nó được tải bên trong bộ nhớ và một thể hiện của chương trình thực thi được gọi là một process . Kernel ghi lại một khối thông tin về process, nó là process ID. Khi bạn chạy máy chủ webserver3a.py hoặc webserver3b.py bạn chạy một process.
Chạy server webserver3b.py trong cửa sổ terminal:
$ python webserver3b.py
Và một cửa số khác sử dụng lệnh ps để lấy ra thông tin về process.
$ ps | grep webserver3b | grep -v grep
7182 ttys003 0:00.04 python webserver3b.py
Lệnh ps hiển thị ra một Python process của webserver3b. Khi một process được tạo ra kernel gán một process ID cho nó, PID. Trong UNIX mỗi process cũng có một ID cha, có ID process riêng, nó được gọi là ID tiến trình cha hoặc PPID. Giả sử rằng bạn chạy một BASH shell theo mặc định và khi bạn khởi động server, một process mới được tạo với PID, cha của PID được đặt thành PID của BASH shell.
Hãy thử nó và thấy được cách nó làm việc. Chạy Pyhton shell một lần nữa, cái sẽ tạo một process mới, và sau đó lấy PID của Python shell process và PID cha (PID của shell) sử dụng os.getpid() và os.getppid(). Sau đó, trong terminal khác chạy lệnh ps và grep PPID (parent process ID, cái mà là 3148). Trong hình phía dưới bạn thấy một ví dụ của quan hệ cha con giữa con của Python shell process và parent BASH shell process trên MAC OS X:
Khái niệm quan trọng cần phải biết nó là của file descriptor. Vậy file descriptor là cái gì? Một file descriptor là một số nguyên không âm mà kernel trả về một process khi nó mở trên tệp hiện tại, tạo một tệp mới hoặc khi nó tạo một socket mới. Có thể bạn đã từng nghe trong UNIX mọi thứ đều là tệp. Kernel đề cập đến các tệp mở của một process bởi một file descriptor. Khi bạn cần đọc hoặc viết một tệp xác định nó với file descriptor. Python cung cấp cho bạn đối tượng cấp cao để xử lý các tệp (và sockets) và bạn không phải sử dụng trực tiếp file descriptors để xác định một tệp, đó là các tệp và sockets được xác định trong UNIX bằng integer file descriptors.
Theo mặc định, UNIX shells gán file file descriptor 0 tới chuẩn input của một process, file descriptor 1 tới chuẩn output của process và file descriptor 2 tới chuẩn error.
Như tôi đã đề cập trước đó, mặc dù Python cung cấp cho bạn một high-level file hoặc file-like object để làm việc, bạn có thể luôn luôn sử dụng phương thức fileno() trên đối tượng để lấy ra mô tả file descriptor với file. Quay trở lại Python shell để thấy cách nó làm việc:
>>> import sys
>>> sys.stdin
<open file '<stdin>', mode 'r' at 0x102beb0c0>
>>> sys.stdin.fileno()
0
>>> sys.stdout.fileno()
1
>>> sys.stderr.fileno()
2
Trong khi làm việc với file và socket trong Python, bạn sẽ thường sử dụng một high-level file/socket object, nhưng có thể có thời gian nơi bạn cần sử dụng một file descriptor directly. Đây là ví dụ làm sao để bạn ghi một chuỗi với một tiêu chuẩn đầu ra sử dụng một hệ thống ghi, có một file descriptor integer như một tham số:
>>> import sys
>>> import os
>>> res = os.write(sys.stdout.fileno(), 'hello\n')
hello
Đây là một phần thú vị, bạn không ngạc nhiên vì mọi thứ đều là file trong UNIX. Socket cũng có một mô tả file descriptor của nó. Một lần nữa, khi bạn tạo một socket trong Python bạn lấy lại một đối tượng và không phải một số nguyên không âm. Nhưng bạn luôn luôn có thể truy cập trực tiếp đến integer file descriptor của socket với phương thức fileno() mà tôi đã đề cập trước đó.
>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> sock.fileno()
3
Một điều nữa tôi muốn đề cập tới: bạn có thấy rằng ví dụ thứ hai của iterative server webserver3b.py, khi server process đang ngủ 60 giây bạn vẫn có thể kết nối tới server với lệnh curl thứ hai. Chắc chắn curl không đưa ra thứ gì ngay lập tức và nó chỉ treo ở đó nhưng làm thế nào server không accepting tại thời điểm đó và client không bị từ chối ngay lâp tức. Nhưng thay vào đó có thể kết nối tới server? trả lời cho câu hỏi đó là phương thức listen của một socket object và BACKLOG argument. Cái tôi gọi là REQUEST_QUEUE_SIZE trong code. BACKLOG argument xác định kích thước của một hàng đợi trong kernel cho yêu cầu kết nối đến. Khi server webserver3b.py đã ngủ, lệnh curl thứ hai mà bạn chạy có thể kết nối tới server vì kernel có không gian sẵn sàng trong hàng đợi yêu cầu tới server socket.
Trong khi tăng đối số BACKLOG không biến máy chủ của bạn thành máy chủ có thể xử lý nhiều yêu cầu một lúc. Điều quan trọng là phải có đối số backlog cho các server bận để accept không phải chờ một kết nối mới, nhưng có thể grab kết nối mới ra khỏi hàng đợi ngay và bắt đầu xử lý yêu cầu của client mà không chời đợi.
Whoo-hoo! Hãy nhanh chóng tóm tắt những gì bạn đã học được (hoặc làm mới nếu đó là tất cả vấn đề cơ bản).
- Iterative server
- Server socket tạo tuần tự (socket, bind, listen, accept)
- Client kết nối tạo tuần tự (socket, connect)
- Socket pair
- Socket
- Port tạm thời và well-known port
- Process
- Process ID (PID), parent process ID (PPID), and the parent-child relationship
- File descriptors
- Nghĩ của BACKLOG argument of the listen socket method
Bây giờ tôi đã sẵn sàng trả lời câu hỏi từ Phần 2: "Làm thế nào bạn có thể làm cho máy chủ của bạn xử lý nhiều hơn một yêu cầu cùng một lúc?"
Cách đơn giản nhất để viết một server đồng thời trong Unix là sử dụng một fork() system call.
Dưới đây là mã của webserver3c.py server đồng thời có thể xử lý nhiều yêu cầu của client cùng một lúc (như trong ví dụ iterative server webserver3b.py, mỗi process con ngủ trong 60 giây):
###########################################################################
# Concurrent server - webserver3c.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
# #
# - Child process sleeps for 60 seconds after handling a client's request #
# - Parent and child processes close duplicate descriptors #
# #
###########################################################################
import os
import socket
import time
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5
def handle_request(client_connection):
request = client_connection.recv(1024)
print(
'Child PID: {pid}. Parent PID {ppid}'.format(
pid=os.getpid(),
ppid=os.getppid(),
)
)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
time.sleep(60)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
print('Parent PID (PPID): {pid}\n'.format(pid=os.getpid()))
while True:
client_connection, client_address = listen_socket.accept()
pid = os.fork()
if pid == 0: # child
listen_socket.close() # close child copy
handle_request(client_connection)
client_connection.close()
os._exit(0) # child exits here
else: # parent
client_connection.close() # close parent copy and loop over
if __name__ == '__main__':
serve_forever()
Trước khi đi sâu và thảo luận cách fork làm việc, hãy thử và thấy rằng server có thể xử lý nhiều yêu cầu của client cùng một lúc, không giống như webserver3a.py và webserver3b.py. Khởi đội server với lệnh sau đây:
$ python webserver3c.py
Thử hai lệnh curl giống như bạn thử trước đó với iterative server. Mặc dù server child process ngủ 60 giây sau khi phục vụ một client request, nó không ảnh hưởng đến client khác bởi chúng được phục vụ bởi các process độc lập và hoàn toàn khách nhau. Bạn sẽ thấy các lệnh curl xuất hiện “Hello, World!” ngay lập tức và sau đó treo trong 60 giây. Bạn có thể tiếp tục chạy bao nhiêu lệnh curl tùy thích và tất cả chúng sẽ xuất đáp ứng của máy chủ “Hello, World” ngay lập tức và không có bất kỳ sự chậm trễ nào.
Điều quan trọng nhất để hiểu về fork() là bạn gọi fork một lần nhưng nó trả về hai lần: một lần trong parent process và một lần trong child process. Khi bạn fork một process mới, process ID được trả lại child process là 0. Khi trả fork về parent process, nó sẽ trả về child’s PID.
Tôi vẫn còn nhớ tôi bị cuốn hút như thế nào khi tôi đọc về nó và thử nó. Nó trông giống như ma thuật đối với tôi. Ở đây tôi đã đọc code tuần tự và sau đó "boom!" mã nhân bản chính nó và bây giờ có hai trường hợp của cùng một mã chạy đồng thời. Tôi nghĩ rằng nó không có gì ma thuật cả.
Khi một parent forks con mới, child process sẽ nhận được một bản sao của parent’s file descriptors:
Bạn có thể nhận thấy rằng parent process trong code ở trên đã đóng kết nối client:
else: # parent
client_connection.close() # close parent copy and loop over
Vì vậy, làm thế nào để một child process vẫn có thể đọc dữ liệu từ client socket nếu cha mẹ của nó được kết nối với socket tương tự? Câu trả lời trong hình trên. Kernel sử dụng tham chiếu mô tả để quyết định có nên đóng một ổ cắm hay không. Nó chỉ đóng socket khi tham chiếu mô tả của nó trở thành 0. Khi máy chủ của bạn tạo ra một tiến trình con, đứa trẻ nhận được bản sao của các bộ mô tả tệp của cha và hạt nhân tăng số lượng tham chiếu cho các bộ mô tả đó. Trong trường hợp của một parent and one child, tham chiếu mô tả sẽ là 2 cho socket của client và khi quá trình cha trong cdoe trên đóng socket kết nối tới client, nó chỉ giảm số tham chiếu của nó thành 1, không đủ nhỏ để kernel đóng socket. Child process cũng đóng bản sao trùng lặp của listen_socket bởi chúng không quan tâm đến việc chấp nhận các kết nối client mới, nó chỉ quan tâm đến việc xử lý các yêu cầu từ kết nối client được thiết lập:
listen_socket.close() # close child copy
Tôi sẽ nói về những gì sẽ xảy ra nếu bạn không đóng các mô tả trùng lặp sau.
Như bạn có thể thấy từ mã nguồn của concurrent server, có vai trò duy nhất của server parent process là chấp nhận kết nối client mới, fork một child process mới để xử lý yêu cầu của client đó và lặp lại để chấp nhận một kết nối client, và không có gì hơn. Server parent process không xử lý các yêu cầu của client - các con của nó sẽ thực hiện.
Sang lề một chút. Nó có nghĩa là gì khi chúng ta nói rằng hai sự kiện là đồng thời?
Khi chúng ta nói rằng hai sự kiện là đồng thời thường có nghĩa là chúng xảy ra cùng một lúc:
Two events are concurrent if you cannot tell by looking at the program which will happen first
Tóm tắt các ý tưởng và khái niệm chính đề cập đến.
- Cách đơn giản nhất để viết một server trong Unix là sử dụng lời gọi hệ thống fork().
- Khi một tiến trình tìm kiếm một tiến trình mới, nó sẽ trở thành một parent process đối với child process.
- Parent và child chia sẻ cùng một bộ mô tả tập tin sau khi gọi đến fork.
- Kernel sử dụng số tham chiếu mô tả để quyết định có đóng file/socket hay không.
- Vai trò của một server parent process: là chấp nhận một kết nối mới từ một client, fork một con để xử lý yêu cầu của client, và lặp lại để chấp nhận một kết nối client mới.
Hãy xem điều gì sẽ xảy ra nếu bạn không đóng các bộ mô tả socket trùng lặp trong parent và child processes. Đây là phiên bản đã sửa đổi của máy chủ đồng thời, nơi máy chủ không đóng các mô tả trùng lặp, webserver3d.py:
###########################################################################
# Concurrent server - webserver3d.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
###########################################################################
import os
import socket
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5
def handle_request(client_connection):
request = client_connection.recv(1024)
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
clients = []
while True:
client_connection, client_address = listen_socket.accept()
# store the reference otherwise it's garbage collected
# on the next loop run
clients.append(client_connection)
pid = os.fork()
if pid == 0: # child
listen_socket.close() # close child copy
handle_request(client_connection)
client_connection.close()
os._exit(0) # child exits here
else: # parent
# client_connection.close()
print(len(clients))
if __name__ == '__main__':
serve_forever()
$ python webserver3d.py
Sử dụng curl để kết nối với máy chủ:
$ curl http://localhost:8888/hello
Hello, World!
Được rồi, curl đã in phản hồi từ concurrent server nhưng nó không chấm dứt và tiếp tục treo. Chuyện gì đang xảy ra ở đây? Máy chủ không còn ngủ trong 60 giây nữa: quá trình con của nó chủ động xử lý yêu cầu của client, đóng kết nối client và thoát, nhưng curl client vẫn không chấm dứt.
Vậy tại sao curl không chấm dứt? Lý do là trùng lặp file descriptors. Khi child process đóng kết nối client, kernel đã giảm số lượng tham chiếu của client socket và số đếm trở thành 1. Server child process thoát, nhưng client socket không bị đóng bởi kernel bởi vì liên quan đến count socket descriptor không phải là 0, và kết quả là gói kết thúc (called FIN in TCP/IP parlance) không được gửi đến client và client vẫn ở trên đường đó. Ngoài ra còn một số vấn đề khác. Nếu server chạy lâu mà không được đóng file descriptors trùng lặp, nó sẽ chạy ra khỏi file descriptors khả dụng:
Dừng server webserver3d.py với Control-C và kiểm tra các tài nguyên mặc định sẵn có cho server process của bạn được thiết lập bởi shell built-in command ulimit:
$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 3842
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 3842
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
Như bạn có thể thấy ở trên, số lượng tối đa các file descriptors mở (open files) có sẵn cho server process trên hộp Ubuntu của tôi là 1024.
Bây giờ, hãy xem cách server của bạn có thể chạy hết các bộ file descriptors nếu server không đóng duplicate descriptors. Trong cửa sổ hiện tại hoặc mới, hãy đặt số lượng open file descriptors tối đa cho máy chủ của bạn là 256:
$ ulimit -n 256
Khởi động server webserver3d.py trong cùng một terminal nơi bạn vừa chạy lệnh $ ulimit -n 256:
$ python webserver3d.py
và sử dụng client3.py sau đây để kiểm tra server.
#####################################################################
# Test client - client3.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
#####################################################################
import argparse
import errno
import os
import socket
SERVER_ADDRESS = 'localhost', 8888
REQUEST = b"""\
GET /hello HTTP/1.1
Host: localhost:8888
"""
def main(max_clients, max_conns):
socks = []
for client_num in range(max_clients):
pid = os.fork()
if pid == 0:
for connection_num in range(max_conns):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(SERVER_ADDRESS)
sock.sendall(REQUEST)
socks.append(sock)
print(connection_num)
os._exit(0)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Test client for LSBAWS.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
'--max-conns',
type=int,
default=1024,
help='Maximum number of connections per client.'
)
parser.add_argument(
'--max-clients',
type=int,
default=1,
help='Maximum number of clients.'
)
args = parser.parse_args()
main(args.max_clients, args.max_conns)
Trong cửa sổ terminal mới, khởi động client3.py và yêu cầu nó tạo 300 kết nối đồng thời tới server:
$ python client3.py --max-clients=300
Dưới đây là ảnh chụp màn hình của của tôi:
Bài học là rõ ràng - server nên đóng các duplicate descriptors. Nhưng ngay cả khi bạn đóng các duplicate descriptors, bạn vẫn chưa ra khỏi nó vì có một vấn đề khác với server, và vấn đề đó là zombie!
Có, code server tạo ra zombie. Hãy xem làm thế nào? Khởi động lại server:
$ python webserver3d.py
Chạy lệnh curl sau trong cửa sổ terminal khác:
$ curl http://localhost:8888/hello
Và bây giờ chạy lệnh ps để hiển thị Python processes đang chạy. Ví dụ trên Ubuntu:
$ ps auxw | grep -i python | grep -v grep
vagrant 9099 0.0 1.2 31804 6256 pts/0 S+ 16:33 0:00 python webserver3d.py
vagrant 9102 0.0 0.0 0 0 pts/0 Z+ 16:33 0:00 [python] <defunct>
Bạn có thấy dòng thứ hai hiển thị trạng thái process với PID 9102 là Z+ và tên của process là <defunct>? zombie nằm ở đó. Vấn đề là bạn không thể giết chúng.
Ngay cả khi bạn cố gắng tiêu diệt zombie với $ kill -9, chúng sẽ sống sót. Hãy thử xem.
Zombie là gì và tại sao server tạo ra chúng? Một zombie là một process đã chấm dứt, nhưng parent của nó đã không chờ đợi nó và chưa nhận được tình trạng chấm dứt của nó. Khi một child process thoát trước process cha, kernel sẽ biến process con thành một zombie và lưu trữ thông tin về process. Thông tin lưu trữ là process ID, trạng thái chấm dứt process và việc sử dụng tài nguyên bở process. Vậy zombie phục vụ một mục đích, nhưng nếu server không để ý đến zombies này thì hệ thống sẽ bị tắc nghẽn. Hãy xem điều đó xảy ra như thế nào. Hãy dừng server đang chạy, mở một terminal mới, sử dụng lệnh ulimit để đặt max user processess thành 400.
$ ulimit -u 400
$ ulimit -n 500
Chạy server webserver3d.py trong terminal tương tự nơi bạn chỉ chạy lệnh $ ulimit -u 400 .
$ python webserver3d.py
Trong cửa sổ terminal mới, chạy client3.py và yêu cầu nó tạo 500 kết nối đồng thời tới server.
$ python client3.py --max-clients=500
Server của bạn sẽ bị lỗi OSError: Resource temporarily unavailable khi cố gắng tạo một process con, nhưng nó không thể vì nó đã đạt đến giới hạn tối đa của process được tạo. Đây là ảnh chụp màn hình của lỗi này:
Bạn có thể thấy, zombies tạo ra vấn đề khi server chạy trong thời gian dài nếu nó không được để ý đến.
Tóm tắt lại những điểm chính:
- Nếu bạn không đóng duplicate descriptors, clients không thể chấp dứt do kết nối tới client không chấm dứt.
- Nếu bạn không đóng duplicate descriptors, server chạy trong thời gian dài cuối cùng sẽ thoát ra khỏi file descriptors sẵn sàng, (max open files).
- Khi bạn fork một process con và nó thoát ra và process cha không đợi nó và không thu thập trạng thái kết thúc, nó trở thành một zombies.
- Zombies cần được cái gì đó, trong trường hợp này đó là bộ nhớ. Server sẽ chạy các process có sẵn (max user processes) nếu nó không để ý đến zombies.
- Không thể kill zombies, cần đợi nó.
Vậy cần làm gì để chú ý đến zombies? Bạn cần sửa code server để chờ zombies có trạng thái chấm dứt. Bạn có thể làm điều đó bằng cách sửa đổi server bằng cách để hệ thống đợi. Không may, điều đó rất ý tưởng bởi vì nếu bạn gọi hàm wait và không có process con bị kết thúc hàm wait sẽ chặn server của bạn, ngăn server của bạn xử lý các yêu cầu kết nối mới từ client. Có tuỳ chọn khác không? Có, đó là sự kết hợp của một trình xử lý với lời gọi hệ thống chờ.
Nhân tiện, một sự kiện không đồng bộ có nghĩa là process cha không biết trước sự kiện sẽ xảy ra.
Sửa đổi code server để thiết lập trình xử lý sự kiện SIGCHLD và đợi một con chấm dứt trong xử lý sự kiện. Code trong webserver3e.py:
###########################################################################
# Concurrent server - webserver3e.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
###########################################################################
import os
import signal
import socket
import time
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5
def grim_reaper(signum, frame):
pid, status = os.wait()
print(
'Child {pid} terminated with status {status}'
'\n'.format(pid=pid, status=status)
)
def handle_request(client_connection):
request = client_connection.recv(1024)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
# sleep to allow the parent to loop over to 'accept' and block there
time.sleep(3)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
signal.signal(signal.SIGCHLD, grim_reaper)
while True:
client_connection, client_address = listen_socket.accept()
pid = os.fork()
if pid == 0: # child
listen_socket.close() # close child copy
handle_request(client_connection)
client_connection.close()
os._exit(0)
else: # parent
client_connection.close()
if __name__ == '__main__':
serve_forever()
Chạy server:
$ python webserver3e.py
Sử dụng curl để gửi yêu cầu đến server đã sửa đổi:
$ curl http://localhost:8888/hello
Nhìn đây:
Chuyện gì đã xảy ra? Lời gọi accept thất bại với lỗi EINTR.
Process cha đã chặn lời gọi hàm accept khi process con đã thoát, cái đó gây ra SIGCHLD, cái đã kích hoạt signal handler và khi signal handler hoàn thành lời gọi accept hệ thống bị gián đoạn:
Đừng lo lắng. Đó là vấn đề đơn giản để giải quyết. Tất cả những gì bạn cần làm là lời gọi accept hệ thống. Đây là phiên bản sửa lỗi webserver3f.py.
###########################################################################
# Concurrent server - webserver3f.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
###########################################################################
import errno
import os
import signal
import socket
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024
def grim_reaper(signum, frame):
pid, status = os.wait()
def handle_request(client_connection):
request = client_connection.recv(1024)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
signal.signal(signal.SIGCHLD, grim_reaper)
while True:
try:
client_connection, client_address = listen_socket.accept()
except IOError as e:
code, msg = e.args
# restart 'accept' if it was interrupted
if code == errno.EINTR:
continue
else:
raise
pid = os.fork()
if pid == 0: # child
listen_socket.close() # close child copy
handle_request(client_connection)
client_connection.close()
os._exit(0)
else: # parent
client_connection.close() # close parent copy and loop over
if __name__ == '__main__':
serve_forever()
Chạy webserver3f.py:
$ python webserver3f.py
Sử dụng curl để gửi yêu cầu đến server:
$ curl http://localhost:8888/hello
Không có ngoại lệ EINTR nữa. Hãy kiểm chứng rằng không còn zombie nào nữa và event handler SIGCHLD với wait đã được xử lý. Để thực hiện điều đó chỉ cần chạy lệnh ps và tự mình xem xét không có Python processes nào với trạng thái Z+ (no more <defunct> processes) .
- Nếu bạn fork một child và không đợi nó, nó trờ thành một zombies.
- SIGCHLD event handler để không đồng bộ wait cho một chấm dứt để có được trạng thái chấm dứt của nó.
- Khi sử dụng event handler bạn cần lưu ý rằng lời gọi hệ thống có thể bị gián đoạn và bạn cần chuẩn bị cho điều đó.
Hãy thử lại webserver3f.py, nhưng thay vì thực hiện một yêu cầu với curl, hãy sử dụng client3.py để tạo 128 kết nối đồng thời.
$ python client3.py --max-clients 128
Chạy lệnh ps:
$ ps auxw | grep -i python | grep -v grep
Và thấy rằng, oh boy, zombie đã trở lại một lần nữa!
Chuyện gì đã xảy ra lần này? Khi bạn chạy 128 client đồng thời và thiết lập 128 kết nối, những process con trên server sẽ xử lý request và thoát ra gần như đồng thời gây ra một lũ tín hiệu SIGCHLD được gửi đến process cha. Vấn đề là các tín hiệu không được xếp hàng đợi và server process bỏ qua một số tín hiệu, khiến cho một số zombies không được giám sát.
Giải pháp ở đây là thiết lập một trình xử lý sự kiện SIGCHLD nhưng thay vì wait sử dụng một lời gọi waitpid với một tuỳ chọn WNOHANG trong một vòng lặp để đảm bảng rằng tất cả các process con bị chấm dứt được xử lý. Dưới đây là mã máy của webserver3g.py:
###########################################################################
# Concurrent server - webserver3g.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
###########################################################################
import errno
import os
import signal
import socket
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024
def grim_reaper(signum, frame):
while True:
try:
pid, status = os.waitpid(
-1, # Wait for any child process
os.WNOHANG # Do not block and return EWOULDBLOCK error
)
except OSError:
return
if pid == 0: # no more zombies
return
def handle_request(client_connection):
request = client_connection.recv(1024)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
signal.signal(signal.SIGCHLD, grim_reaper)
while True:
try:
client_connection, client_address = listen_socket.accept()
except IOError as e:
code, msg = e.args
# restart 'accept' if it was interrupted
if code == errno.EINTR:
continue
else:
raise
pid = os.fork()
if pid == 0: # child
listen_socket.close() # close child copy
handle_request(client_connection)
client_connection.close()
os._exit(0)
else: # parent
client_connection.close() # close parent copy and loop over
if __name__ == '__main__':
serve_forever()
Khởi động server:
$ python webserver3g.py
Sử dụng test client client3.py
$ python client3.py --max-clients 128
Và bây giờ không có thêm zombie. Yay! Cuộc sống tốt nếu không có zombie :)
Xin chúc mừng! Đó là một hành trình khá dài nhưng tôi hy vọng bạn thích nó. Bây giờ bạn có concurrent server và code có thể phục vụ như một nền tảng cho công việc tiếp theo của bạn đối với một Web server.
Tôi sẽ để nó như một bài tập để bạn cập nhật máy chủ WSGI từ Phần 2 và làm nó đồng thời. Bạn có thể tìm phiên bản sửa đổi tại đây. Nhưng chỉ xem mã của tôi sau khi bạn đã triển khai phiên bản của riêng mình. Bạn có tất cả các thông tin cần thiết để làm điều đó. Vì vậy, đi và chỉ cần làm điều đó :)
What’s next? As Josh Billings said,
“Be like a postage stamp — stick to one thing until you get there.”
Bắt đầu làm chủ các khái niệm cơ bản. Hỏi những gì bạn đã biết. Và luôn luôn đào sâu hơn.
“If you learn only methods, you’ll be tied to your methods. But if you learn principles, you can devise your own methods.” —Ralph Waldo Emerson
Nguồn: https://ruslanspivak.com/lsbaws-part3/
Bình luận