วิธีที่เร็วที่สุดในการส่งคำขอ HTTP 100,000 รายการใน Python คืออะไร


287

ฉันกำลังเปิดไฟล์ซึ่งมี 100,000 URL ฉันต้องส่งคำขอ HTTP ไปยังแต่ละ URL แล้วพิมพ์รหัสสถานะ ฉันใช้ Python 2.6 และดูวิธีที่สับสนหลายครั้งที่ Python ใช้เธรด / การทำงานพร้อมกัน ฉันได้ดูแม้ที่หลามสามัคคีห้องสมุด แต่ไม่สามารถคิดออกว่าจะเขียนโปรแกรมนี้ได้อย่างถูกต้อง มีใครเจอปัญหาที่คล้ายกัน? ฉันเดาว่าโดยทั่วไปฉันจำเป็นต้องรู้วิธีปฏิบัติงานหลายพันอย่างใน Python ให้เร็วที่สุดเท่าที่จะทำได้ฉันคิดว่านั่นหมายถึง 'พร้อมกัน'


47
ตรวจสอบให้แน่ใจว่าคุณทำตามคำขอ HEAD เท่านั้น (เพื่อไม่ให้คุณดาวน์โหลดทั้งเอกสาร) ดู: stackoverflow.com/questions/107405/…
Tarnay Kálmán

5
จุดที่ยอดเยี่ยม Kalmi หากทุกอิกอร์ต้องการคือสถานะของคำขอคำขอ 100K เหล่านี้จะไปมากเร็วกว่ามาก เร็วกว่ามาก
Adam Crossland

1
คุณไม่ต้องการเธรดสำหรับสิ่งนี้ วิธีที่มีประสิทธิภาพที่สุดน่าจะใช้ไลบรารีแบบอะซิงโครนัสเช่น Twisted
jemfinch

3
นี่คือตัวอย่างโค้ด gevent, twisted และ asyncio-based (ทดสอบกับคำขอ 1000000 รายการ)
jfs

4
@ TarnayKálmánเป็นไปได้สำหรับrequests.getและrequests.head(เช่นคำขอหน้าเทียบกับคำขอหัว) เพื่อส่งคืนรหัสสถานะที่แตกต่างกันดังนั้นนี่ไม่ใช่คำแนะนำที่ดีที่สุด
AlexG

คำตอบ:


200

วิธีการแก้ปัญหา Twistedless:

from urlparse import urlparse
from threading import Thread
import httplib, sys
from Queue import Queue

concurrent = 200

def doWork():
    while True:
        url = q.get()
        status, url = getStatus(url)
        doSomethingWithResult(status, url)
        q.task_done()

def getStatus(ourl):
    try:
        url = urlparse(ourl)
        conn = httplib.HTTPConnection(url.netloc)   
        conn.request("HEAD", url.path)
        res = conn.getresponse()
        return res.status, ourl
    except:
        return "error", ourl

def doSomethingWithResult(status, url):
    print status, url

q = Queue(concurrent * 2)
for i in range(concurrent):
    t = Thread(target=doWork)
    t.daemon = True
    t.start()
try:
    for url in open('urllist.txt'):
        q.put(url.strip())
    q.join()
except KeyboardInterrupt:
    sys.exit(1)

อันนี้เร็วกว่าโซลูชันที่บิดเบี้ยวเล็กน้อยและใช้ CPU น้อยลง


10
@Kalmi ทำไมคุณถึงตั้งคิวเพื่อconcurrent*2?
Marcel Wilson

8
อย่าลืมที่จะปิดการเชื่อมต่อ conn.close()การเปิดการเชื่อมต่อ http มากเกินไปอาจทำให้สคริปต์ของคุณหยุดทำงานในบางจุดและกินหน่วยความจำ
Aamir Adnan

4
@hyh Queueโมดูลได้รับการเปลี่ยนชื่อเป็นqueuePython 3 นี่คือรหัส Python 2
Tarnay Kálmán

3
คุณจะไปได้เร็วแค่ไหนถ้าคุณต้องการคุยกับเซิร์ฟเวอร์ SAME ในแต่ละครั้งโดยคงการเชื่อมต่ออยู่ สามารถทำได้แม้กระทั่งข้ามเธรดหรือด้วยการเชื่อมต่อแบบต่อเนื่องหนึ่งเธรด?
mdurant

2
@mptevsion หากคุณใช้ CPython คุณสามารถทำได้ (เช่น) เพียงแค่แทนที่ "สถานะการพิมพ์ url" ด้วย "my_global_list.append ((สถานะ url))" (การดำเนินการส่วนใหญ่บน) รายการนั้นมีความปลอดภัยของเธรดโดยปริยายใน CPython (และการใช้งานไพ ธ อนอื่น ๆ ) เนื่องจาก GIL ดังนั้นสิ่งนี้จึงปลอดภัยที่จะทำ
Tarnay Kálmán

54

วิธีการแก้ปัญหาโดยใช้ห้องสมุดเครือข่ายtornado asynchronous

from tornado import ioloop, httpclient

i = 0

def handle_request(response):
    print(response.code)
    global i
    i -= 1
    if i == 0:
        ioloop.IOLoop.instance().stop()

http_client = httpclient.AsyncHTTPClient()
for url in open('urls.txt'):
    i += 1
    http_client.fetch(url.strip(), handle_request, method='HEAD')
ioloop.IOLoop.instance().start()

7
รหัสนี้ใช้ I / O เครือข่ายที่ไม่มีการปิดกั้นและไม่มีข้อ จำกัด ใด ๆ สามารถขยายการเชื่อมต่อแบบเปิดได้นับหมื่น มันจะทำงานในหัวข้อเดียว แต่จะเป็นวิธีที่เร็วกว่านั้นโซลูชั่นการทำเกลียวใด ๆ การชำระเงินแบบไม่ปิดกั้น I / O en.wikipedia.org/wiki/Asynchronous_I/O
2557

1
คุณช่วยอธิบายสิ่งที่เกิดขึ้นที่นี่กับตัวแปร global i ได้หรือไม่? การตรวจสอบข้อผิดพลาดบางอย่าง?
LittleBobbyTables

4
มันเป็นตัวนับสำหรับการพิจารณาว่าเมื่อใดที่จะออกจาก `` ioloop '- ดังนั้นเมื่อเสร็จแล้ว
Michael Dorner

1
@AndrewScottEvans สันนิษฐานว่าคุณกำลังใช้ python 2.7 และผู้รับมอบฉันทะ
Dejell

5
@Guy Avraham ขอให้โชคดีในการช่วยวางแผน ddos ​​ของคุณ
Walter

51

สิ่งต่าง ๆ เปลี่ยนไปเล็กน้อยตั้งแต่ปี 2010 เมื่อโพสต์สิ่งนี้และฉันไม่ได้ลองคำตอบอื่น ๆ ทั้งหมด แต่ฉันได้ลองไปสองสามครั้ง

ฉันสามารถดึงข้อมูลเกี่ยวกับ ~ 150 โดเมนที่ไม่ซ้ำกันต่อวินาทีที่ทำงานบน AWS

import pandas as pd
import concurrent.futures
import requests
import time

out = []
CONNECTIONS = 100
TIMEOUT = 5

tlds = open('../data/sample_1k.txt').read().splitlines()
urls = ['http://{}'.format(x) for x in tlds[1:]]

def load_url(url, timeout):
    ans = requests.head(url, timeout=timeout)
    return ans.status_code

with concurrent.futures.ThreadPoolExecutor(max_workers=CONNECTIONS) as executor:
    future_to_url = (executor.submit(load_url, url, TIMEOUT) for url in urls)
    time1 = time.time()
    for future in concurrent.futures.as_completed(future_to_url):
        try:
            data = future.result()
        except Exception as exc:
            data = str(type(exc))
        finally:
            out.append(data)

            print(str(len(out)),end="\r")

    time2 = time.time()

print(f'Took {time2-time1:.2f} s')
print(pd.Series(out).value_counts())

1
ฉันแค่ถามเพราะฉันไม่รู้ แต่สิ่งต่าง ๆ ในอนาคตนี้จะถูกแทนที่ด้วย async / รอ?
TankorSmash

1
เป็นไปได้ แต่ฉันได้พบว่าข้างต้นทำงานได้ดีขึ้น คุณสามารถใช้ aiohttp แต่ไม่ได้เป็นส่วนหนึ่งของ lib มาตรฐานและมีการเปลี่ยนแปลงค่อนข้างมาก มันใช้งานได้ แต่ฉันไม่คิดว่ามันจะทำงานได้ดี ฉันได้รับอัตราความผิดพลาดที่สูงขึ้นเมื่อฉันใช้มันและสำหรับชีวิตของฉันฉันไม่สามารถใช้งานได้รวมถึงฟิวเจอร์สที่เกิดขึ้นพร้อมกันแม้ว่าในทางทฤษฎีแล้วดูเหมือนว่ามันจะทำงานได้ดีขึ้นดู: stackoverflow.com/questions/45800857/ถ้าคุณทำให้มันทำงานได้ดีโปรดโพสต์คำตอบของคุณเพื่อที่ฉันจะได้ทำการทดสอบ
เกลน ธ อมป์สัน

1
นี่เป็น nitpick แต่ฉันคิดว่ามันสะอาดกว่าที่จะใส่time1 = time.time()ที่ด้านบนของลูปและtime2 = time.time()หลังจากลูป
Matt M.

ฉันทดสอบตัวอย่างของคุณแล้วมันจะประมวลผลสองครั้ง ฉันกำลังทำอะไรผิดหรือเปล่า? หรือมันหมายถึงการทำงานสองครั้ง? หากเป็นกรณีหลังคุณสามารถช่วยฉันให้เข้าใจว่ามันกระตุ้นสองครั้งได้อย่างไร
Ronnie

1
ไม่ควรรันสองครั้ง ไม่แน่ใจว่าทำไมคุณจะเห็นว่า
เกล็น ธ อมป์สัน

40

กระทู้ไม่ใช่คำตอบที่นี่อย่างแน่นอน พวกเขาจะให้ทั้งคอขวดกระบวนการและเคอร์เนลรวมถึงข้อ จำกัด ปริมาณงานที่ไม่สามารถยอมรับได้หากเป้าหมายโดยรวมคือ "วิธีที่เร็วที่สุด"

นิด ๆ หน่อย ๆ ของtwistedและไม่ตรงกันของHTTPลูกค้าจะให้ผลดีมาก


ironfroggy: ฉันกำลังเข้าหาความรู้สึกของคุณ ฉันพยายามใช้โซลูชันของฉันกับเธรดและคิว (สำหรับ mutexes อัตโนมัติ) แต่คุณสามารถจินตนาการได้ว่าต้องใช้เวลานานเท่าใดในการเติมคิวด้วย 100,000 รายการ? ฉันยังคงเล่นกับตัวเลือกและคำแนะนำที่แตกต่างกันโดยทุกคนในหัวข้อนี้และบางที Twisted อาจเป็นทางออกที่ดี
IgorGanapolsky

2
คุณสามารถหลีกเลี่ยงการเติมคิวด้วยสิ่งต่าง ๆ ได้ 100k เพียงดำเนินการไอเท็มครั้งละหนึ่งรายการจากอินพุตของคุณจากนั้นเรียกใช้เธรดเพื่อประมวลผลคำร้องขอที่สอดคล้องกับแต่ละไอเท็ม (ตามที่ฉันอธิบายด้านล่างใช้เธรดตัวเรียกใช้งานเพื่อเริ่มเธรดคำร้องขอ HTTP เมื่อจำนวนเธรดของคุณต่ำกว่าขีด จำกัด บางส่วนทำให้เธรดเขียนผลลัพธ์ออกเป็น URL การแมป Dict เพื่อตอบสนองหรือผนวก tuples ลงในรายการ)
Erik กองบัญชาการ

ironfroggy: นอกจากนี้ฉันยังสงสัยว่าคุณพบคอขวดประเภทใดในการใช้ Python threads และไพ ธ อนเธรดจะตอบโต้อย่างไรกับเคอร์เนลระบบปฏิบัติการ?
Erik Garrison

ตรวจสอบให้แน่ใจว่าคุณติดตั้งเครื่องปฏิกรณ์ epoll; มิฉะนั้นคุณจะใช้เลือก / สำรวจและมันจะช้ามาก นอกจากนี้หากคุณกำลังพยายามที่จะเปิดการเชื่อมต่อ 100,000 พร้อมกัน (สมมติว่าโปรแกรมของคุณเขียนในลักษณะนั้นและ URL อยู่บนเซิร์ฟเวอร์ที่ต่างกัน) คุณจะต้องปรับแต่งระบบปฏิบัติการของคุณเพื่อที่คุณจะไม่หมด ของตัวให้คำอธิบายไฟล์พอร์ตชั่วคราว ฯลฯ (อาจเป็นเรื่องง่ายกว่าเพื่อให้แน่ใจว่าคุณไม่มีมากกว่า 10,000 การเชื่อมต่อที่ยอดเยี่ยมพร้อมกัน)
มาร์คอท

erikg: คุณแนะนำความคิดที่ดี อย่างไรก็ตามผลลัพธ์ที่ดีที่สุดที่ฉันสามารถทำได้ด้วย 200 กระทู้คือประมาณ 6 นาที ฉันแน่ใจว่ามีวิธีที่จะทำให้สำเร็จในเวลาที่น้อยลง ... Mark N: ถ้า Twisted เป็นวิธีที่ฉันตัดสินใจจะไปแล้วเครื่องปฏิกรณ์ epoll ก็มีประโยชน์อย่างแน่นอน แต่ถ้าสคริปต์ของฉันจะถูกเรียกใช้จากหลายเครื่องจะไม่ที่เลี่ยงการติดตั้งบนเครื่องบิดแต่ละ? ฉันไม่รู้ว่าฉันสามารถโน้มน้าวให้เจ้านายของฉันไปตามเส้นทางนั้นได้หรือไม่ ...
IgorGanapolsky

21

ฉันรู้ว่านี้เป็นคำถามที่เก่า แต่ในหลาม 3.7 คุณสามารถทำได้โดยใช้นี้และasyncioaiohttp

import asyncio
import aiohttp
from aiohttp import ClientSession, ClientConnectorError

async def fetch_html(url: str, session: ClientSession, **kwargs) -> tuple:
    try:
        resp = await session.request(method="GET", url=url, **kwargs)
    except ClientConnectorError:
        return (url, 404)
    return (url, resp.status)

async def make_requests(urls: set, **kwargs) -> None:
    async with ClientSession() as session:
        tasks = []
        for url in urls:
            tasks.append(
                fetch_html(url=url, session=session, **kwargs)
            )
        results = await asyncio.gather(*tasks)

    for result in results:
        print(f'{result[1]} - {str(result[0])}')

if __name__ == "__main__":
    import pathlib
    import sys

    assert sys.version_info >= (3, 7), "Script requires Python 3.7+."
    here = pathlib.Path(__file__).parent

    with open(here.joinpath("urls.txt")) as infile:
        urls = set(map(str.strip, infile))

    asyncio.run(make_requests(urls=urls))

คุณสามารถอ่านเพิ่มเติมเกี่ยวกับมันและดูตัวอย่างที่นี่


นี้จะคล้ายกับC # async / รอคอยและKotlin Coroutines?
IgorGanapolsky

@IgorGanapolsky ใช่มันคล้ายกับ C # async / await ฉันไม่คุ้นเคยกับ Kotlin Coroutines
Marius Stănescu

@sandyp ผมไม่แน่ใจว่ามันทำงาน แต่ถ้าคุณต้องการที่จะลองคุณจะต้องใช้ UnixConnector สำหรับ aiohttp อ่านเพิ่มเติมได้ที่นี่: docs.aiohttp.org/en/stable/client_reference.html#connectors
Marius Stanescu

ขอบคุณ @ MariusStănescu นั่นคือสิ่งที่ฉันใช้
sandyp

+1 สำหรับแสดง asyncio.gather (* งาน) นี่เป็นตัวอย่างหนึ่งที่ฉันใช้: urls= [fetch(construct_fetch_url(u),idx) for idx, u in enumerate(some_URI_list)] results = await asyncio.gather(*urls)
Ashwini Kumar

19

ใช้grequestsเป็นการรวมกันของคำร้องขอ + โมดูล Gevent

GRequests ช่วยให้คุณใช้คำขอกับ Gevent เพื่อให้คำขอ HTTP แบบอะซิงโครนัสได้อย่างง่ายดาย

การใช้งานง่าย:

import grequests

urls = [
   'http://www.heroku.com',
   'http://tablib.org',
   'http://httpbin.org',
   'http://python-requests.org',
   'http://kennethreitz.com'
]

สร้างชุดคำขอยังไม่ได้ส่งให้ทำดังนี้

>>> rs = (grequests.get(u) for u in urls)

ส่งพวกเขาทั้งหมดในเวลาเดียวกัน:

>>> grequests.map(rs)
[<Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>, <Response [200]>]

7
gevent ขณะนี้สนับสนุนหลาม 3
เบนจามิน Toueg

14
grequests ไม่ได้เป็นส่วนหนึ่งของคำขอปกติและดูเหมือนว่าจะไม่มีความผิดปกติอย่างมาก
Thom

8

วิธีการที่ดีในการแก้ไขปัญหานี้คือการเขียนรหัสที่ต้องการก่อนเพื่อให้ได้ผลลัพธ์หนึ่งผลลัพธ์จากนั้นจึงรวมโค้ดเธรดเพื่อทำให้แอพพลิเคชันเป็นแบบขนาน

ในโลกที่สมบูรณ์แบบนี้อาจหมายถึงการเริ่มต้น 100,000 เธรดพร้อมกันซึ่งส่งผลลัพธ์ของพวกเขาไปยังพจนานุกรมหรือรายการสำหรับการประมวลผลในภายหลัง แต่ในทางปฏิบัติคุณถูก จำกัด ในจำนวนคำขอ HTTP แบบขนานที่คุณสามารถออกได้ ในพื้นที่คุณมีข้อ จำกัด ว่าคุณสามารถเปิดซ็อกเก็ตได้พร้อมกันเท่าไหร่จำนวนเธรดการดำเนินการที่ Python ล่ามจะอนุญาต จากระยะไกลคุณอาจถูก จำกัด ในจำนวนการเชื่อมต่อพร้อมกันถ้าคำขอทั้งหมดขัดกับเซิร์ฟเวอร์เครื่องเดียวหรือหลายเครื่อง ข้อ จำกัด เหล่านี้อาจทำให้คุณต้องเขียนสคริปต์ในแบบสำรวจความคิดเห็นเพียงเล็กน้อยในแต่ละครั้ง (100 ตามที่ผู้โพสต์คนอื่นพูดถึงอาจเป็นขนาดเธรดพูลที่เหมาะสมแม้ว่าคุณอาจพบว่าคุณ ประสบความสำเร็จสามารถปรับใช้อื่น ๆ อีกมากมาย)

คุณสามารถติดตามรูปแบบการออกแบบนี้เพื่อแก้ไขปัญหาข้างต้น:

  1. เริ่มเธรดที่เรียกใช้เธรดการร้องขอใหม่จนกระทั่งจำนวนเธรดที่รันอยู่ในปัจจุบัน (คุณสามารถติดตามเธรดผ่าน threading.active_count () หรือโดยการผลักวัตถุเธรดเข้าสู่โครงสร้างข้อมูล) คือ> = จำนวนคำขอสูงสุดพร้อมกันของคุณ (พูด 100) จากนั้นหลับเป็นระยะเวลาสั้น ๆ เธรดนี้ควรยุติเมื่อไม่มี URL ที่จะดำเนินการอีกต่อไป ดังนั้นเธรดจะยังคงตื่นขึ้นเปิดตัวเธรดใหม่และนอนจนกว่าคุณจะเสร็จสิ้น
  2. ให้เธรดการร้องขอเก็บผลลัพธ์ไว้ในโครงสร้างข้อมูลบางอย่างเพื่อการดึงและเอาท์พุทในภายหลัง หากโครงสร้างที่คุณจัดเก็บผลลัพธ์เป็นlistหรือdictใน CPython คุณสามารถผนวกหรือแทรกรายการที่ไม่ซ้ำจากเธรดของคุณได้อย่างปลอดภัยโดยไม่มีการล็อกแต่ถ้าคุณเขียนไฟล์หรือต้องการการโต้ตอบข้ามเธรดที่ซับซ้อนมากขึ้นคุณควรใช้ ล็อคยกเว้นร่วมกันเพื่อปกป้องรัฐนี้จากความเสียหาย

ฉันอยากจะแนะนำให้คุณใช้เธรดโมดูลคุณสามารถใช้มันเพื่อเรียกใช้และติดตามการทำงานของเธรด การสนับสนุนเธรดของ Python นั้นไร้สาระ แต่คำอธิบายปัญหาของคุณแสดงให้เห็นว่าเพียงพอสำหรับความต้องการของคุณ

สุดท้ายหากคุณต้องการที่จะเห็นการประยุกต์ใช้ตรงไปตรงสวยของแอพลิเคชันเครือข่ายขนานเขียนในหลามตรวจสอบssh.py มันเป็นห้องสมุดขนาดเล็กที่ใช้ Python ที่เธรดเพื่อเชื่อมต่อ SSH หลายขนาน การออกแบบอยู่ใกล้เพียงพอกับความต้องการของคุณซึ่งคุณอาจพบว่ามันเป็นทรัพยากรที่ดี


1
erikg: การเข้าแถวเข้าสู่สมการของคุณจะสมเหตุสมผลหรือไม่ ฉันสงสัยว่า GIL ของ Python ไม่ได้มุ่งไปที่การเล่นด้วยเธรดนับพัน
IgorGanapolsky

ทำไมคุณต้องล็อคการแยกกันเพื่อป้องกันการสร้างเธรดมากเกินไป ฉันสงสัยว่าฉันเข้าใจผิดคำ คุณสามารถติดตามการรันเธรดในคิวเธรดลบออกเมื่อเธรดเสร็จสมบูรณ์และเพิ่มมากถึงขีด จำกัด เธรดดังกล่าว แต่ในกรณีง่าย ๆ เช่นหนึ่งในคำถามคุณยังสามารถดูจำนวนเธรดที่ใช้งานอยู่ในกระบวนการ Python ปัจจุบันรอจนกว่ามันจะต่ำกว่าขีด จำกัด และเปิดใช้เธรดเพิ่มเติมจนถึงขีด จำกัด ดังที่อธิบายไว้ ฉันเดาว่าคุณสามารถพิจารณาการล็อคนี้โดยนัย แต่ไม่จำเป็นต้องล็อคอย่างชัดแจ้ง
Erik Garrison

erikg: ไม่กระทู้หลายรัฐแบ่งปัน? บนหน้า 305 ในหนังสือของโอเรลลี "งูใหญ่สำหรับ Unix และ Linux ระบบบริหาร" มันฯ :" ... โดยใช้เกลียวโดยไม่ต้องรอคิวทำให้มันซับซ้อนกว่าที่หลาย ๆ คนแนบเนียนสามารถจัดการกับมันเป็นความคิดที่ดีมากที่จะมักจะใช้การเข้าคิว. หากคุณพบว่าคุณจำเป็นต้องใช้เธรดเพราะเหตุใดเนื่องจากโมดูลคิวยังช่วยลดความจำเป็นในการปกป้องข้อมูลด้วย mutexes อย่างชัดเจนเนื่องจากคิวนั้นได้รับการป้องกันภายในโดย mutex แล้ว " อีกครั้งฉันยินดีต้อนรับมุมมองของคุณเกี่ยวกับเรื่องนี้
IgorGanapolsky

Igor: คุณพูดถูกที่คุณควรใช้กุญแจ ฉันได้แก้ไขโพสต์เพื่อแสดงถึงสิ่งนี้ ประสบการณ์การใช้งานจริงของไพ ธ อนแนะนำว่าคุณไม่จำเป็นต้องล็อกโครงสร้างข้อมูลที่คุณแก้ไขแบบอะตอมจากเธรดของคุณเช่นโดย list.append หรือโดยการเพิ่มคีย์แฮช ฉันเชื่อว่าเหตุผลคือ GIL ซึ่งให้การดำเนินงานเช่น list.append กับระดับอะตอมมิก ฉันกำลังใช้การทดสอบเพื่อตรวจสอบสิ่งนี้ (ใช้เธรด 10k เพื่อต่อท้ายหมายเลข 0-9999 ในรายการตรวจสอบว่าผนวกทั้งหมดใช้งานได้) หลังจากการวนซ้ำเกือบ 100 ครั้งการทดสอบก็ไม่ได้ล้มเหลว
Erik Garrison

Igor: ฉันถามคำถามอื่นในหัวข้อนี้: stackoverflow.com/questions/2740435/…
Erik Garrison

7

หากคุณต้องการได้ประสิทธิภาพที่ดีที่สุดเท่าที่จะเป็นไปได้คุณอาจต้องพิจารณาใช้ Asynchronous I / O แทนการใช้เธรด ค่าใช้จ่ายที่เกี่ยวข้องกับเธรด OS เป็นพัน ๆ นั้นไม่สำคัญและการสลับบริบทภายในล่าม Python จะเพิ่มมากขึ้นไปอีก การทำเกลียวจะทำให้งานเสร็จ แต่ฉันสงสัยว่าเส้นทางแบบอะซิงโครนัสจะให้ประสิทธิภาพโดยรวมที่ดีขึ้น

โดยเฉพาะฉันขอแนะนำเว็บไคลเอ็นต์ async ในห้องสมุด Twisted ( http://www.twistedmatrix.com ) มีเส้นโค้งการเรียนรู้ที่สูงชันเป็นที่ยอมรับ แต่มันค่อนข้างใช้งานง่ายเมื่อคุณได้รับการจัดการที่ดีในการเขียนโปรแกรมแบบอะซิงโครนัสของ Twisted

วิธีใช้ API ไคลเอ็นต์เว็บอะซิงโครนัสของ Twisted มีอยู่ที่:

http://twistedmatrix.com/documents/current/web/howto/client.html


Rakis: ขณะนี้ฉันกำลังมองหา I / O แบบอะซิงโครนัสและไม่มีการปิดกั้น ฉันต้องเรียนรู้ให้ดีก่อนที่จะนำไปใช้ หนึ่งความคิดเห็นที่ฉันต้องการทำในโพสต์ของคุณคือเป็นไปไม่ได้ (อย่างน้อยก็ภายใต้การแจกจ่าย Linux ของฉัน) เพื่อวางไข่ "กระทู้ระบบปฏิบัติการนับพัน" มีจำนวนเธรดสูงสุดที่ Python จะอนุญาตให้คุณวางไข่ก่อนที่โปรแกรมจะหยุด และในกรณีของฉัน (ใน CentOS 5) จำนวนเธรดสูงสุดคือ 303
IgorGanapolsky

เป็นเรื่องดีที่รู้ ฉันไม่เคยลองวางไข่มากกว่าหนึ่งครั้งใน Python พร้อมกัน แต่ฉันคาดว่าจะสามารถสร้างมากกว่านั้นก่อนที่มันจะระเบิด
Rakis

6

ทางออก:

from twisted.internet import reactor, threads
from urlparse import urlparse
import httplib
import itertools


concurrent = 200
finished=itertools.count(1)
reactor.suggestThreadPoolSize(concurrent)

def getStatus(ourl):
    url = urlparse(ourl)
    conn = httplib.HTTPConnection(url.netloc)   
    conn.request("HEAD", url.path)
    res = conn.getresponse()
    return res.status

def processResponse(response,url):
    print response, url
    processedOne()

def processError(error,url):
    print "error", url#, error
    processedOne()

def processedOne():
    if finished.next()==added:
        reactor.stop()

def addTask(url):
    req = threads.deferToThread(getStatus, url)
    req.addCallback(processResponse, url)
    req.addErrback(processError, url)   

added=0
for url in open('urllist.txt'):
    added+=1
    addTask(url.strip())

try:
    reactor.run()
except KeyboardInterrupt:
    reactor.stop()

Testtime:

[kalmi@ubi1:~] wc -l urllist.txt
10000 urllist.txt
[kalmi@ubi1:~] time python f.py > /dev/null 

real    1m10.682s
user    0m16.020s
sys 0m10.330s
[kalmi@ubi1:~] head -n 6 urllist.txt
http://www.google.com
http://www.bix.hu
http://www.godaddy.com
http://www.google.com
http://www.bix.hu
http://www.godaddy.com
[kalmi@ubi1:~] python f.py | head -n 6
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu
200 http://www.bix.hu

Pingtime:

bix.hu is ~10 ms away from me
godaddy.com: ~170 ms
google.com: ~30 ms

6
การใช้ Twisted เป็นเธรดพูลจะไม่สนใจผลประโยชน์ส่วนใหญ่ที่คุณจะได้รับจากมัน คุณควรใช้ไคลเอ็นต์ async HTTP แทน
Jean-Paul Calderone

1

การใช้เธรดพูลเป็นตัวเลือกที่ดีและจะทำให้ง่ายขึ้น น่าเสียดายที่ไพ ธ อนไม่มีไลบรารี่มาตรฐานที่ทำให้เธรดพูลง่ายเป็นพิเศษ แต่นี่เป็นห้องสมุดที่ดีที่ควรให้คุณเริ่มต้น: http://www.chrisarndt.de/projects/threadpool/

ตัวอย่างรหัสจากเว็บไซต์ของพวกเขา:

pool = ThreadPool(poolsize)
requests = makeRequests(some_callable, list_of_args, callback)
[pool.putRequest(req) for req in requests]
pool.wait()

หวังว่านี่จะช่วยได้


ฉันขอแนะนำให้คุณระบุ q_size สำหรับ ThreadPool ดังนี้: ThreadPool (poolize, q_size = 1000) ดังนั้นคุณจะไม่มีวัตถุ WorkRequest 100000 ในหน่วยความจำ "ถ้าq_size> 0 ขนาดของคิวคำของานถูก จำกัด และเธรดพูลบล็อกเมื่อคิวเต็มและพยายามใส่คำของานเพิ่มเติมในนั้น (ดูputRequestวิธี) เว้นแต่คุณจะใช้timeoutค่าบวกสำหรับputRequest"
Tarnay Kálmán

เพื่อให้ห่างไกลฉันพยายามที่จะดำเนินการแก้ปัญหา threadpool - ตามที่แนะนำ อย่างไรก็ตามฉันไม่เข้าใจรายการพารามิเตอร์ในฟังก์ชัน makeRequests some_callable, list_of_args, callback คืออะไร บางทีถ้าฉันเห็นข้อมูลโค้ดจริงที่จะช่วย ฉันประหลาดใจที่ผู้แต่งห้องสมุดนั้นไม่ได้โพสต์ตัวอย่างใด ๆ
IgorGanapolsky

some_callable เป็นฟังก์ชั่นของคุณที่ทำงานทั้งหมดของคุณเสร็จแล้ว (เชื่อมต่อกับเซิร์ฟเวอร์ http) list_of_args เป็นอาร์กิวเมนต์ที่จะถูกส่งผ่านไปยัง some_callabe โทรกลับเป็นฟังก์ชั่นที่จะถูกเรียกเมื่อด้ายคนงานจะทำ มันต้องใช้สองข้อโต้แย้งวัตถุผู้ปฏิบัติงาน (ไม่จำเป็นต้องเกี่ยวข้องกับตัวคุณเองกับเรื่องนี้จริงๆ) และผลลัพธ์ที่คนงานดึงมา
Kevin Wiskia

1

สร้างepollวัตถุ
เปิดซ็อกเก็ตลูกค้า TCP จำนวนมาก
ปรับบัฟเฟอร์ส่งของพวกเขาจะน้อยกว่าส่วนหัวขอ
ส่งหัวข้อการร้องขอ - มันควรจะทันทีเพียงวางลงในบัฟเฟอร์ที่ลงทะเบียนซ็อกเก็ตในepollวัตถุ
ทำ.pollในepollobect,
อ่านครั้งแรก 3 ไบต์จากแต่ละซ็อกเก็ตจาก.poll,
เขียนพวกเขาที่จะsys.stdoutตามมาด้วย\n(ไม่ได้ล้าง) ใกล้ซ็อกเก็ตลูกค้า

จำกัด จำนวนซ็อกเก็ตที่เปิดพร้อมกัน - จัดการข้อผิดพลาดเมื่อสร้างซ็อกเก็ต สร้างซ็อกเก็ตใหม่เฉพาะเมื่อปิดอีกอัน
ปรับขีด จำกัด ระบบปฏิบัติการ
ลองฟอร์กลงในไม่กี่กระบวนการ (ไม่มาก): นี้อาจช่วยให้การใช้งาน CPU ได้อย่างมีประสิทธิภาพมากขึ้นอีกนิด


@IgorGanapolsky ต้องเป็น ฉันต้องการจะประหลาดใจเป็นอย่างอื่น แต่แน่นอนความต้องการทดลอง
George Sovetov

0

สำหรับกรณีของคุณการทำเกลียวอาจทำเคล็ดลับเนื่องจากคุณอาจใช้เวลาส่วนใหญ่รอการตอบกลับ มีโมดูลที่เป็นประโยชน์เช่นQueueในไลบรารีมาตรฐานที่อาจช่วยได้

ผมทำสิ่งที่คล้ายกันกับการดาวน์โหลดแบบขนานของไฟล์ก่อนและมันก็มากพอที่ดีสำหรับฉัน แต่มันก็ไม่ได้อยู่ในระดับที่คุณกำลังพูดถึง

หากงานของคุณมีข้อ จำกัด เกี่ยวกับ CPU มากขึ้นคุณอาจต้องการดูโมดูลมัลติโปรเซสเซอร์ซึ่งจะช่วยให้คุณสามารถใช้ CPU / แกน / กระทู้เพิ่มเติมได้มากขึ้น (กระบวนการเพิ่มเติมที่จะไม่ปิดกั้นแต่ละกระบวนการเนื่องจากการล็อกแต่ละกระบวนการ)


สิ่งเดียวที่ฉันต้องการพูดถึงคือการวางไข่หลายกระบวนการอาจมีราคาแพงกว่าการวางไข่หลายเธรด นอกจากนี้ยังมีไม่ได้รับประสิทธิภาพที่ชัดเจนในการส่งออก 100,000 ร้องขอ HTTP มีกระบวนการหลายเทียบกับหลายหัวข้อ
IgorGanapolsky

0

พิจารณาใช้Windmillแม้ว่า Windmill อาจทำหลายเธรดไม่ได้

คุณสามารถทำได้ด้วยสคริปต์ Python รีดด้วยมือบน 5 เครื่องแต่ละเครื่องเชื่อมต่อขาออกโดยใช้พอร์ต 40000-60000 เปิดการเชื่อมต่อ 100,000 พอร์ต

นอกจากนี้ยังอาจช่วยในการทดสอบตัวอย่างด้วยแอพ QA ที่ ได้รับการตอบรับอย่างดีเช่นOpenSTAเพื่อรับทราบว่าแต่ละเซิร์ฟเวอร์สามารถรองรับได้มากแค่ไหน

นอกจากนี้ลองค้นหาโดยใช้ Perl ธรรมดากับคลาส LWP :: ConnCache คุณอาจจะได้รับประสิทธิภาพมากขึ้น (การเชื่อมต่อมากขึ้น)


0

เว็บไคลเอ็นต์ async แบบบิดเบี้ยวนี้ค่อนข้างรวดเร็ว

#!/usr/bin/python2.7

from twisted.internet import reactor
from twisted.internet.defer import Deferred, DeferredList, DeferredLock
from twisted.internet.defer import inlineCallbacks
from twisted.web.client import Agent, HTTPConnectionPool
from twisted.web.http_headers import Headers
from pprint import pprint
from collections import defaultdict
from urlparse import urlparse
from random import randrange
import fileinput

pool = HTTPConnectionPool(reactor)
pool.maxPersistentPerHost = 16
agent = Agent(reactor, pool)
locks = defaultdict(DeferredLock)
codes = {}

def getLock(url, simultaneous = 1):
    return locks[urlparse(url).netloc, randrange(simultaneous)]

@inlineCallbacks
def getMapping(url):
    # Limit ourselves to 4 simultaneous connections per host
    # Tweak this number, but it should be no larger than pool.maxPersistentPerHost 
    lock = getLock(url,4)
    yield lock.acquire()
    try:
        resp = yield agent.request('HEAD', url)
        codes[url] = resp.code
    except Exception as e:
        codes[url] = str(e)
    finally:
        lock.release()


dl = DeferredList(getMapping(url.strip()) for url in fileinput.input())
dl.addCallback(lambda _: reactor.stop())

reactor.run()
pprint(codes)

0

ฉันพบว่าการใช้tornadoแพ็กเกจเป็นวิธีที่เร็วและง่ายที่สุดในการบรรลุเป้าหมาย:

from tornado import ioloop, httpclient, gen


def main(urls):
    """
    Asynchronously download the HTML contents of a list of URLs.
    :param urls: A list of URLs to download.
    :return: List of response objects, one for each URL.
    """

    @gen.coroutine
    def fetch_and_handle():
        httpclient.AsyncHTTPClient.configure(None, defaults=dict(user_agent='MyUserAgent'))
        http_client = httpclient.AsyncHTTPClient()
        waiter = gen.WaitIterator(*[http_client.fetch(url, raise_error=False, method='HEAD')
                                    for url in urls])
        results = []
        # Wait for the jobs to complete
        while not waiter.done():
            try:
                response = yield waiter.next()
            except httpclient.HTTPError as e:
                print(f'Non-200 HTTP response returned: {e}')
                continue
            except Exception as e:
                print(f'An unexpected error occurred querying: {e}')
                continue
            else:
                print(f'URL \'{response.request.url}\' has status code <{response.code}>')
                results.append(response)
        return results

    loop = ioloop.IOLoop.current()
    web_pages = loop.run_sync(fetch_and_handle)

    return web_pages

my_urls = ['url1.com', 'url2.com', 'url100000.com']
responses = main(my_urls)
print(responses[0])

-2

วิธีที่ง่ายที่สุดคือการใช้ไลบรารีเธรดในตัวของ Python พวกเขาไม่ใช่ "ของจริง" / เคอร์เนลเธรดพวกเขามีปัญหา (เช่นซีเรียลไลซ์เซชัน) แต่ก็ดีพอ คุณต้องการคิว & เธรดพูล ทางเลือกหนึ่งอยู่ที่นี่แต่ไม่สำคัญที่จะเขียนของคุณเอง คุณไม่สามารถโทรแบบขนานทั้งหมด 100,000 ครั้ง แต่คุณสามารถโทรออกได้ 100 สาย (หรือมากกว่านั้น) ในเวลาเดียวกัน


7
หัวข้อของงูหลามค่อนข้างเป็นจริงเมื่อเทียบกับรูบี้เช่น ภายใต้ประทุนพวกเขาจะดำเนินการเป็นกระทู้ระบบปฏิบัติการอย่างน้อยใน Unix / Linux และ Windows บางทีคุณอาจจะหมายถึง GIL แต่มันไม่ได้ทำให้หัวข้อจริงน้อย ...
Eli Bendersky

2
อีไลพูดถึงหัวข้อของ Python แต่ประเด็นของ Pestilence ที่คุณต้องการใช้กลุ่มเธรดนั้นก็ถูกต้องเช่นกัน สิ่งสุดท้ายที่คุณต้องการทำในกรณีนี้คือพยายามเริ่มเธรดแยกต่างหากสำหรับแต่ละคำขอ 100K พร้อมกัน
Adam Crossland

1
ใช่, คุณไม่สามารถโพสต์ตัวอย่างโค้ดในความคิดเห็น, แต่คุณสามารถแก้ไขคำถามของคุณและเพิ่มได้
Adam Crossland

ศัตรูพืช: คุณจะแนะนำกี่คิวและเธรดต่อคิวสำหรับวิธีการแก้ปัญหาของฉัน
IgorGanapolsky

รวมทั้งนี่คือภารกิจผูก I / O ที่ไม่ผูกกับ CPU, GIL ส่วนใหญ่มีผลกับงานที่ผูกกับ CPU
PirateApp
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.