2016-03-18 38 views
19

Tôi đang cố gắng thực hiện this Matasano crypto challenge liên quan đến việc thực hiện tấn công theo thời gian đối với máy chủ có chức năng so sánh chuỗi bị chậm giả tạo. Nó nói để sử dụng "khung web của bạn lựa chọn", nhưng tôi không cảm thấy như cài đặt một khuôn khổ web, vì vậy tôi quyết định sử dụng các HTTPServer class được xây dựng vào mô-đun http.server.Ngoại lệ bí ẩn khi thực hiện nhiều yêu cầu đồng thời từ urllib.request đến HTTPServer

Tôi đã đưa ra một cái gì đó đã làm việc, nhưng nó đã rất chậm, vì vậy tôi đã cố gắng tăng tốc nó bằng cách sử dụng hồ bơi chủ đề (kém tài liệu) được xây dựng thành multiprocessing.dummy. Nó nhanh hơn nhiều, nhưng tôi nhận thấy điều gì đó kỳ lạ: nếu tôi thực hiện 8 yêu cầu ít hơn đồng thời, nó hoạt động tốt. Nếu tôi có nhiều hơn thế, nó hoạt động trong một thời gian và cho tôi lỗi ở những thời điểm dường như ngẫu nhiên. Các lỗi dường như không nhất quán và không phải lúc nào cũng giống nhau, nhưng chúng thường có Connection refused, invalid argument, OSError: [Errno 22] Invalid argument, urllib.error.URLError: <urlopen error [Errno 22] Invalid argument>, BrokenPipeError: [Errno 32] Broken pipe hoặc urllib.error.URLError: <urlopen error [Errno 61] Connection refused> trong đó.

Có giới hạn nào về số lượng kết nối mà máy chủ có thể xử lý không? Tôi không nghĩ rằng số lượng luồng cho mỗi bài viết là vấn đề, bởi vì tôi đã viết một hàm đơn giản làm so sánh chuỗi bị chậm lại mà không cần chạy máy chủ web và gọi nó với 500 chuỗi đồng thời và nó hoạt động tốt. Tôi không nghĩ rằng chỉ đơn giản là yêu cầu từ nhiều chủ đề đó là vấn đề, bởi vì tôi đã tạo ra các trình thu thập thông tin sử dụng hơn 100 luồng (tất cả yêu cầu đồng thời cho cùng một trang web) và chúng hoạt động tốt. Có vẻ như HTTPServer không có nghĩa là lưu trữ tin cậy các trang web sản xuất có lượng lưu lượng truy cập lớn, nhưng tôi ngạc nhiên rằng điều này rất dễ gây ra sự cố.

Tôi đã cố gắng xóa nội dung khỏi mã của mình trông không liên quan đến vấn đề, như tôi thường làm khi chẩn đoán các lỗi bí ẩn như thế này, nhưng điều đó không hữu ích trong trường hợp này. Có vẻ như tôi đang xóa mã dường như không liên quan, số lượng kết nối mà máy chủ có thể xử lý dần dần tăng lên, nhưng không có nguyên nhân rõ ràng nào về các sự cố.

Có ai biết cách tăng số lượng yêu cầu tôi có thể thực hiện cùng một lúc hay ít nhất là tại sao điều này xảy ra?

Mã của tôi là phức tạp, nhưng tôi đã đưa ra chương trình này đơn giản mà chứng tỏ vấn đề:

#!/usr/bin/env python3 

import os 
import random 

from http.server import BaseHTTPRequestHandler, HTTPServer 
from multiprocessing.dummy import Pool as ThreadPool 
from socketserver import ForkingMixIn, ThreadingMixIn 
from threading import Thread 
from time import sleep 
from urllib.error import HTTPError 
from urllib.request import urlopen 


class FancyHTTPServer(ThreadingMixIn, HTTPServer): 
    pass 


class MyRequestHandler(BaseHTTPRequestHandler): 
    def do_GET(self): 
     sleep(random.uniform(0, 2)) 
     self.send_response(200) 
     self.end_headers() 
     self.wfile.write(b"foo") 

    def log_request(self, code=None, size=None): 
     pass 

def request_is_ok(number): 
    try: 
     urlopen("http://localhost:31415/test" + str(number)) 
    except HTTPError: 
     return False 
    else: 
     return True 


server = FancyHTTPServer(("localhost", 31415), MyRequestHandler) 
try: 
    Thread(target=server.serve_forever).start() 
    with ThreadPool(200) as pool: 
     for i in range(10): 
      numbers = [random.randint(0, 99999) for j in range(20000)] 
      for j, result in enumerate(pool.imap(request_is_ok, numbers)): 
       if j % 20 == 0: 
        print(i, j) 
finally: 
    server.shutdown() 
    server.server_close() 
    print("done testing server") 

Đối với một số lý do, chương trình trên hoạt động tốt trừ khi nó có hơn 100 chủ đề hoặc lâu hơn, nhưng tôi mã thực sự cho thử thách chỉ có thể xử lý 8 luồng. Nếu tôi chạy nó với 9, tôi thường nhận được lỗi kết nối, và với 10, tôi luôn luôn nhận được lỗi kết nối. Tôi đã thử sử dụng concurrent.futures.ThreadPoolExecutor, concurrent.futures.ProcessPoolExecutormultiprocessing.pool thay vì multiprocessing.dummy.pool và không ai trong số đó có vẻ hữu ích. Tôi đã thử sử dụng đối tượng đơn giản là HTTPServer (không có ThreadingMixIn) và điều đó khiến mọi thứ chạy rất chậm và không khắc phục được sự cố. Tôi đã thử sử dụng ForkingMixIn và điều đó cũng không khắc phục được.

Tôi phải làm gì với điều này? Tôi đang chạy Python 3.5.1 vào cuối năm 2013 MacBook Pro chạy OS X 10.11.3.

EDIT: tôi đã cố gắng thêm vài thứ, bao gồm chạy các máy chủ trong một quá trình thay vì một chủ đề, như một đơn giản HTTPServer, với ForkingMixIn, và với sự ThreadingMixIn. Không ai giúp.

EDIT: Vấn đề này lạ hơn tôi nghĩ.Tôi đã thử tạo một kịch bản với máy chủ và một tập lệnh khác với nhiều luồng tạo yêu cầu và chạy chúng trong các tab khác nhau trong thiết bị đầu cuối của tôi. Quá trình với máy chủ chạy tốt, nhưng yêu cầu thực hiện đã bị lỗi. Các trường hợp ngoại lệ là kết hợp của ConnectionResetError: [Errno 54] Connection reset by peer, urllib.error.URLError: <urlopen error [Errno 54] Connection reset by peer>, OSError: [Errno 41] Protocol wrong type for socket, urllib.error.URLError: <urlopen error [Errno 41] Protocol wrong type for socket>, urllib.error.URLError: <urlopen error [Errno 22] Invalid argument>.

Tôi đã thử với một máy chủ giả như trên và nếu tôi giới hạn số lượng yêu cầu đồng thời xuống 5 hoặc ít hơn, nó hoạt động tốt, nhưng với 6 yêu cầu, quy trình khách hàng bị lỗi. Đã có một số lỗi từ máy chủ, nhưng nó vẫn tiếp diễn. Khách hàng đã gặp bất kể tôi có đang sử dụng các luồng hoặc các quy trình để thực hiện các yêu cầu hay không. Sau đó tôi đã thử đặt chức năng bị chậm lại trong máy chủ và nó có thể xử lý 60 yêu cầu đồng thời, nhưng nó bị lỗi với 70. Điều này có vẻ như nó có thể mâu thuẫn với bằng chứng rằng vấn đề là với máy chủ.

EDIT: Tôi đã thử hầu hết những điều tôi mô tả bằng cách sử dụng requests thay vì urllib.request và gặp sự cố tương tự.

EDIT: Tôi hiện đang chạy OS X 10.11.4 và chạy vào cùng một vấn đề.

+0

Bạn có đảm bảo bạn đang đóng các kết nối khách hàng không được sử dụng của mình không? –

+0

@Cory Shay, tôi đã thử làm 'x = urlopen (bất kỳ)' sau đó 'x.close()', và điều đó dường như không giúp ích gì. –

+0

Tôi phải thừa nhận rằng lý do mà tôi đã nêu không nhất thiết là lý do tại sao vấn đề này xảy ra. Có khả năng có thể là những người khác. Nhưng một số câu hỏi để hỏi có thể giúp điều tra điều này là "điều gì xảy ra nếu bạn phát hành' ulimit -r $ ((32 * 1024)) '?" và "đầu ra từ' netstat -anp | grep SERVERPROCESSNAME' là gì? " –

Trả lời

8

Bạn đang sử dụng giá trị mặc định listen() tồn đọng, có thể là nguyên nhân gây ra rất nhiều lỗi đó. Đây không phải là số lượng khách hàng đồng thời với kết nối đã được thiết lập, nhưng số lượng khách hàng chờ đợi trên hàng đợi nghe trước khi kết nối được thiết lập. Thay đổi lớp máy chủ của bạn thành:

class FancyHTTPServer(ThreadingMixIn, HTTPServer): 
    def server_activate(self): 
     self.socket.listen(128) 

128 là giới hạn hợp lý. Bạn có thể muốn kiểm tra socket.SOMAXCONN hoặc hệ điều hành somaxconn của bạn nếu bạn muốn tăng thêm nữa. Nếu bạn vẫn có lỗi ngẫu nhiên dưới tải nặng, bạn nên kiểm tra cài đặt ulimit của mình và tăng nếu cần.

Tôi đã làm điều đó với ví dụ của bạn và tôi đã nhận được hơn 1000 chủ đề chạy tốt, vì vậy tôi nghĩ rằng nên giải quyết vấn đề của bạn.


Cập nhật

Nếu nó được cải thiện nhưng nó vẫn đâm với 200 khách hàng đồng thời, sau đó tôi khá chắc chắn rằng vấn đề chính của bạn là kích thước tồn đọng. Lưu ý rằng vấn đề của bạn không phải là số lượng khách hàng đồng thời, nhưng số lượng yêu cầu kết nối đồng thời. Một lời giải thích ngắn gọn về điều đó có nghĩa là gì, mà không đi quá sâu vào TCP internals.

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
s.bind((HOST, PORT)) 
s.listen(BACKLOG) 
while running: 
    conn, addr = s.accept() 
    do_something(conn, addr) 

Trong ví dụ này, các ổ cắm bây giờ chấp nhận các kết nối trên cổng nhất định, và cuộc gọi s.accept() sẽ chặn cho đến khi một client kết nối. Bạn có thể có nhiều khách hàng cố gắng kết nối đồng thời, và tùy thuộc vào ứng dụng của bạn, bạn có thể không gọi được s.accept() và gửi kết nối máy khách nhanh như khách hàng đang cố kết nối. Các máy khách đang chờ xử lý được xếp hàng đợi và kích thước tối đa của hàng đợi đó được xác định bởi giá trị BACKLOG. Nếu hàng đợi đầy, khách hàng sẽ thất bại với lỗi kết nối bị từ chối.

Luồng không hiệu quả, vì những gì mà lớp ThreadingMixIn thực hiện là thực hiện cuộc gọi do_something(conn, addr) trong một luồng riêng biệt, do đó máy chủ có thể quay lại vòng lặp chính và cuộc gọi s.accept().

Bạn có thể thử tăng tiếp tục tồn đọng, nhưng sẽ có một điểm mà điều đó sẽ không hữu ích vì nếu hàng đợi phát triển quá lớn, một số khách hàng sẽ hết giờ trước khi máy chủ thực hiện cuộc gọi s.accept().

Vì vậy, như tôi đã nói ở trên, vấn đề của bạn là số lần thử kết nối đồng thời, không phải số lượng khách hàng đồng thời. Có thể 128 là đủ cho ứng dụng thực sự của bạn, nhưng bạn đang gặp lỗi trên thử nghiệm của bạn bởi vì bạn đang cố gắng kết nối với tất cả 200 luồng cùng một lúc và làm ngập hàng đợi.

Đừng lo lắng về ulimit trừ khi bạn nhận được lỗi Too many open files, nhưng nếu bạn muốn tăng backlog vượt quá 128, hãy thực hiện một số nghiên cứu trên socket.SOMAXCONN. Đây là một khởi đầu tốt: https://utcc.utoronto.ca/~cks/space/blog/python/AvoidSOMAXCONN

+1

Tôi đã làm điều đó và nó hoạt động, ngay cả với 150 chủ đề! Nó bị treo với 200, nhưng 150 có thể là đủ cho mục đích của tôi, và nếu nó không phải là, ít nhất tôi có thể có một số ý tưởng phải làm gì về nó. Tôi không biết thứ 'listen()' này là gì, hoặc somaxconn hay ulimit là gì, vì vậy tôi sẽ nghiên cứu tất cả những điều đó, thử các số khác nhau, và có thể chờ xem liệu tôi có được câu trả lời hay hơn trước khi trao giải tiền thưởng, nhưng câu trả lời của bạn rất hữu ích. Cảm ơn bạn. –

+1

@EliasZamaria Kiểm tra câu trả lời cập nhật của tôi. Tôi đã cung cấp một lời giải thích chi tiết hơn kể từ khi bạn bị mất một chút. –

+0

Cảm ơn lời giải thích. Công cụ TCP này thấp hơn mức tôi thường xử lý, và tôi không biết nhiều về nó. Tôi sẽ chơi xung quanh với nó một số chi tiết khi tôi có thời gian và đăng bài ở đây nếu tôi gặp phải bất kỳ vấn đề nào khác mà tôi không thể dễ dàng đối phó với bản thân mình. –

-1

Chỉ tiêu này chỉ sử dụng nhiều chuỗi làm lõi, do đó yêu cầu 8 luồng (bao gồm cả lõi ảo). Các mô hình luồng là dễ nhất để có được làm việc, nhưng nó thực sự là một cách rác để làm điều đó. Một cách tốt hơn để xử lý nhiều kết nối là sử dụng một cách tiếp cận không đồng bộ. Đó là khó khăn hơn mặc dù.

Với phương pháp luồng của bạn, bạn có thể bắt đầu bằng cách điều tra xem quy trình có mở sau khi bạn thoát khỏi chương trình hay không. Điều này có nghĩa là các chủ đề của bạn không đóng, và rõ ràng sẽ gây ra các vấn đề.

Hãy thử này ...

class FancyHTTPServer(ThreadingMixIn, HTTPServer): 
    daemon_threads = True 

Điều đó sẽ đảm bảo rằng chủ đề của bạn đóng đúng cách. Nó cũng có thể xảy ra tự động trong các hồ bơi thread nhưng nó có thể có giá trị cố gắng anyway.

+1

Đầu tiên, bạn sẽ sử dụng nhiều luồng như lõi nếu nhiệm vụ là CPU bị ràng buộc, không phải I/O bị ràng buộc. Thứ hai, các chuỗi Python chỉ chạy trong một luồng tại một thời điểm vì GIL. –

+1

Hiệu chỉnh: Các chuỗi Python chỉ chạy trong một lõi tại một thời điểm. –

1

Tôi muốn nói rằng sự cố của bạn liên quan đến một số chặn IO vì tôi đã thực thi thành công mã của bạn trên NodeJ. Tôi cũng nhận thấy rằng cả máy chủ và máy khách đều gặp sự cố khi hoạt động riêng lẻ.

Nhưng chúng ta có thể tăng số lượng yêu cầu với một vài thay đổi:

  • Xác định số lượng kết nối đồng thời:

    http.server.HTTPServer.request_queue_size = 500

  • Chạy máy chủ theo một quy trình khác:

    server = multiprocessing.Process (target = RunHTTPServer) Server.start()

  • Sử dụng một hồ bơi kết nối trên các mặt hàng để thực hiện các yêu cầu

  • Sử dụng một hồ bơi thread trên phía máy chủ để xử lý các yêu cầu

  • Cho phép tái sử dụng kết nối ở phía máy khách bằng cách đặt lược đồ và bằng cách sử dụng tiêu đề "tiếp tục sống"

Với tất cả những sửa đổi này, tôi đã quản lý để chạy mã với 500 chủ đề mà không có bất kỳ vấn đề nào. Vì vậy, nếu bạn muốn để cho nó một thử, đây là mã hoàn chỉnh:

import random 
from time import sleep, clock 
from http.server import BaseHTTPRequestHandler, HTTPServer 
from multiprocessing import Process 
from multiprocessing.pool import ThreadPool 
from socketserver import ThreadingMixIn 
from concurrent.futures import ThreadPoolExecutor 
from urllib3 import HTTPConnectionPool 
from urllib.error import HTTPError 


class HTTPServerThreaded(HTTPServer): 
    request_queue_size = 500 
    allow_reuse_address = True 

    def serve_forever(self): 
     executor = ThreadPoolExecutor(max_workers=self.request_queue_size) 

     while True: 
      try: 
       request, client_address = self.get_request() 
       executor.submit(ThreadingMixIn.process_request_thread, self, request, client_address) 
      except OSError: 
       break 

     self.server_close() 


class MyRequestHandler(BaseHTTPRequestHandler): 
    default_request_version = 'HTTP/1.1' 

    def do_GET(self): 
     sleep(random.uniform(0, 1)/100.0) 

     data = b"abcdef" 
     self.send_response(200) 
     self.send_header("Content-type", 'text/html') 
     self.send_header("Content-length", len(data)) 
     self.end_headers() 
     self.wfile.write(data) 

    def log_request(self, code=None, size=None): 
     pass 


def RunHTTPServer(): 
    server = HTTPServerThreaded(('127.0.0.1', 5674), MyRequestHandler) 
    server.serve_forever() 


client_headers = { 
    'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)', 
    'Content-Type': 'text/plain', 
    'Connection': 'keep-alive' 
} 

client_pool = None 

def request_is_ok(number): 
    response = client_pool.request('GET', "/test" + str(number), headers=client_headers) 
    return response.status == 200 and response.data == b"abcdef" 


if __name__ == '__main__': 

    # start the server in another process 
    server = Process(target=RunHTTPServer) 
    server.start() 

    # start a connection pool for the clients 
    client_pool = HTTPConnectionPool('127.0.0.1', 5674) 

    # execute the requests 
    with ThreadPool(500) as thread_pool: 
     start = clock() 

     for i in range(5): 
      numbers = [random.randint(0, 99999) for j in range(20000)] 
      for j, result in enumerate(thread_pool.imap(request_is_ok, numbers)): 
       if j % 1000 == 0: 
        print(i, j, result) 

     end = clock() 
     print("execution time: %s" % (end-start,)) 

Cập nhật 1:

Tăng request_queue_size chỉ cung cấp cho bạn nhiều không gian để lưu trữ các yêu cầu mà không thể được thực hiện tại thời gian để chúng có thể được thực thi sau này. Vì vậy, hàng đợi càng dài, độ phân tán càng cao cho thời gian phản hồi, tôi tin rằng điều ngược lại với mục tiêu của bạn ở đây. Đối với ThreadingMixIn, nó không lý tưởng vì nó tạo ra và phá hủy một chuỗi cho mọi yêu cầu và nó đắt tiền. Một lựa chọn tốt hơn để giảm hàng chờ đợi là sử dụng một nhóm các luồng có thể sử dụng lại để xử lý các yêu cầu.

Lý do để chạy máy chủ trong quá trình khác là tận dụng lợi thế của một CPU khác để giảm thời gian thực hiện.

Đối với phía máy khách sử dụng một HTTPConnectionPool là cách duy nhất tôi tìm thấy để giữ một dòng chảy liên tục của các yêu cầu kể từ khi tôi đã có một số hành vi kỳ lạ với urlopen trong khi phân tích các kết nối.

+0

Tôi đã thử 'request_queue_size', tương đương với điều' self.socket.listen' mà Pedro đề xuất và dường như đã khắc phục được sự cố của tôi. –

+0

Tôi không biết những gì 'http.server.HTTPServer.allow_reuse_address = True' là nghĩa vụ phải làm. Có vẻ như giá trị mặc định cho điều này là 1. Xem https://hg.python.org/cpython/file/3.5/Lib/http/server.py#l134 –

+0

Như đã đề cập trong phần chỉnh sửa cho câu hỏi của tôi, tôi đã thử chạy máy chủ trong một quá trình thay vì một chủ đề và điều đó không giúp ích gì. –

Các vấn đề liên quan