درخواست های حلقه ای دسته ای در PHP با استفاده از چند دستگیره

اخیراً من یک کار در محل کار داشتم که در آن مجبور شدم مبلغ مناسبی از درخواست های HTTP را در یک اسکریپت ارسال کنم. به طور طبیعی ، یکی از اولین ایده ها استفاده از مکانیسم دسته بندی برای ارسال چندین درخواست به صورت موازی بود. با این حال ، هیچ منبعی با برخی از دنیای واقعی (هر آنچه که معنی دارد) با استفاده از PHP وجود ندارد ، بنابراین من آنچه را که معمولاً در این موارد انجام می دهم انجام دادم: این را خودم فهمیدم و درباره آن وبلاگ می کنم.
برخی از زمینه ها: من از PHP 8.4.5 در Macbook Pro 16 خود با یک Apple M2 Pro استفاده می کنم تا اسکریپت ها را در این پست وبلاگ اجرا کنم. من چندین بار اسکریپت ها را اجرا کردم و همیشه سریعترین اجرا را منتشر کردم.
پردازش متوالی
ساده ترین راه برای ارسال چندین درخواست با استفاده از اسکریپت PHP ، حلقه در تمام URL ها و استفاده از مراحل زیر است:
بنابراین اولین اجرای ساده لوحانه احتمالاً منجر به چنین چیزی خواهد شد:
$urls = [
'https://example.com/',
'https://example.com/1',
'https://example.com/2',
'https://example.com/3',
'https://example.com/4',
'https://example.com/5',
'https://example.com/6',
'https://example.com/7',
'https://example.com/8',
'https://example.com/9',
];
$total_start = microtime(true);
foreach ($urls as $url) {
$request_start = microtime(true);
$handle = curl_init();
curl_setopt($handle, CURLOPT_URL, $url);
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($handle);
$response_code = curl_getinfo($handle, CURLINFO_HTTP_CODE);
$request_duration = number_format(microtime(true) - $request_start, 3);
echo "Response code for request to {$url} is {$response_code} in {$request_duration}s\n";
}
$total_duration = number_format(microtime(true) - $total_start, 3);
echo "All requests took {$total_duration}s\n";
خروجی این اسکریپت چیزی شبیه به این در دستگاه من است (من از آن استفاده کرده ام example.com
دامنه به عنوان تظاهرات مانند این هدف آنهاست ، اما متأسفانه همه چیز به جز مسیر ریشه یک کد وضعیت را برمی گرداند 404
):
$ php 01_sequential_processing.php
Response code for request to https://example.com/ is 200 in 0.414s
Response code for request to https://example.com/1 is 404 in 0.414s
Response code for request to https://example.com/2 is 404 in 0.447s
Response code for request to https://example.com/3 is 404 in 0.453s
Response code for request to https://example.com/4 is 404 in 0.496s
Response code for request to https://example.com/5 is 404 in 0.418s
Response code for request to https://example.com/6 is 404 in 0.433s
Response code for request to https://example.com/7 is 404 in 0.465s
Response code for request to https://example.com/8 is 404 in 0.470s
Response code for request to https://example.com/9 is 404 in 0.415s
All requests took 4.427s
بنابراین این اسکریپت برای اجرای 10 درخواست به حدود 4.4 ثانیه نیاز دارد. این پایه بدون هیچ گونه بهینه سازی است. بخش های بعدی سعی در بهبود این اجرای ساده لوحانه دارند.
استفاده مجدد از دسته حلقه
یک بهینه سازی بسیار ساده اما در عین حال کاملاً مؤثر ، حرکت اولیه کردن دسته حلقه قبل از حلقه است. به این ترتیب ما فقط یک بار دسته حلقه را شروع می کنیم و از همان دسته حلقه استفاده می کنیم تا تمام درخواست های خود را ارسال کنیم. علاوه بر curl_init
تماس با تمام گزینه های حلقه کدگذاری شده سخت را نیز می توانید قبل از حلقه منتقل کنید ، که در این حالت است CURLOPT_RETURNTRANSFER
گزینه ایجاد curl_exec
برای بازگشت پاسخ به جای مستقیم خروجی آن.
$urls = [
'https://example.com/',
'https://example.com/1',
'https://example.com/2',
'https://example.com/3',
'https://example.com/4',
'https://example.com/5',
'https://example.com/6',
'https://example.com/7',
'https://example.com/8',
'https://example.com/9',
];
$total_start = microtime(true);
$handle = curl_init();
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
foreach ($urls as $url) {
$request_start = microtime(true);
curl_setopt($handle, CURLOPT_URL, $url);
$response = curl_exec($handle);
$response_code = curl_getinfo($handle, CURLINFO_HTTP_CODE);
$request_duration = number_format(microtime(true) - $request_start, 3);
echo "Response code for request to {$url} is {$response_code} in {$request_duration}s\n";
}
$total_duration = number_format(microtime(true) - $total_start, 3);
echo "All requests took {$total_duration}s\n";
هنگام اجرای این نسخه ، ممکن است قبلاً متوجه شوید که خیلی سریعتر از حالت قبلی اجرا می شود:
$ php 02_reuse_handles.php
Response code for request to https://example.com/ is 200 in 0.400s
Response code for request to https://example.com/1 is 404 in 0.130s
Response code for request to https://example.com/2 is 404 in 0.135s
Response code for request to https://example.com/3 is 404 in 0.130s
Response code for request to https://example.com/4 is 404 in 0.144s
Response code for request to https://example.com/5 is 404 in 0.212s
Response code for request to https://example.com/6 is 404 in 0.139s
Response code for request to https://example.com/7 is 404 in 0.130s
Response code for request to https://example.com/8 is 404 in 0.134s
Response code for request to https://example.com/9 is 404 in 0.133s
All requests took 1.688s
همانطور که به نظر می رسد ، به نظر می رسد اولیه سازی چنین دسته ای کاملاً کار سنگین است. من می دانم که اجرای چیزی فقط یک بار (یا حتی چندین بار ، همانطور که در این مثالها انجام دادم) معیار خوبی نیست ، اما بهبود پرفروش بیش از 60 ٪ هنوز هم قابل توجه است.
ارسال درخواست های مجاری به صورت موازی
حتی با پیشرفت دوم ، تمام درخواست ها به صورت توالی اجرا می شوند ، یعنی یک درخواست باید قبل از شروع کار بعدی به پایان برسد. اگر همه این درخواست ها به صورت موازی اجرا شوند ، بسیار سریعتر خواهد بود. خوشبختانه ، PHP با برخی از روشهای پیشوند با curl_multi_
، که توسعه دهندگان را قادر می سازد دقیقاً همین کار را انجام دهند. با این حال ، من تمام این روش ها را کمی گیج کننده دیدم ، که احتمالاً انگیزه اصلی برای نوشتن این پست وبلاگ بود.
بنابراین اگر می خواهیم همه درخواست ها را از مثالهای قبلی موازی کنیم ، کد مانند موارد زیر به نظر می رسد:
$urls = [
'https://example.com/',
'https://example.com/1',
'https://example.com/2',
'https://example.com/3',
'https://example.com/4',
'https://example.com/5',
'https://example.com/6',
'https://example.com/7',
'https://example.com/8',
'https://example.com/9',
];
$total_start = microtime(true);
$multi_handle = curl_multi_init();
$handles = [];
for ($i = 0; $i < count($urls); ++$i) {
$handles[$i] = curl_init();
curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
curl_setopt($handles[$i], CURLOPT_URL, $urls[$i]);
curl_multi_add_handle($multi_handle, $handles[$i]);
}
$running = null;
do {
curl_multi_exec($multi_handle, $running);
curl_multi_select($multi_handle);
} while ($running > 0);
for ($i = 0; $i < count($urls); $i++) {
$response_code = curl_getinfo($handles[$i], CURLINFO_HTTP_CODE);
echo "Response code for request to {$urls[$i]} is {$response_code}\n";
}
$total_duration = number_format(microtime(true) - $total_start, 3);
echo "All requests took {$total_duration}s\n";
تفاوت اصلی در مثالهای قبلی همه این تماس ها به curl_multi_
توابع قسمتهای زیر جالب ترین موارد این کد است:
- در
curl_multi_init
روش شروع یک دسته چند قطبی ، که می توانیم چندین دسته حلقه معمول را اضافه کنیم و بعداً آنها را به طور موازی اجرا کنیم. - در
for
دستگیره های حلقه حلقه ایجاد می شوند ، تنظیم می شوند و سپس با استفاده از دسته Multi Curl اضافه می شوندcurl_multi_add_handle
بشر - در
do-while
سپس حلقه مسئول اجرای آن درخواست ها است و آن قسمت برای من گیج کننده ترین بود.curl_multi_exec
تمام این درخواست ها را اجرا می کند ، اما مسدود نمی شود ، در عوض پارامتر دوم مرجعی است که می تواند به عنوان پرچمی تعبیر شود که نشان می دهد برخی از درخواست ها هنوز در حال اجرا هستند. با این حال ، این پرچم به عنوان شرایط حلقه استفاده می شود. بدون تماس دیگری بهcurl_multi_select
این یک انتظار شلوغ خواهد بودبشرcurl_multi_select
در واقع عملیات مسدود کردن است که تا زمانی که درخواست های چند دسته پیشرفت داشته باشد ، مسدود می شود. از نظر من عجیب است که این اتفاق برای هر درخواست رخ می دهد ، به خصوص که این روش نمی گوید کدام یک پیشرفت کرده است. این دلیل این است که هنوز هم این را در یک حلقه قرار دهید ، به این معنی که پس از پایان حلقه تمام درخواست ها به پایان می رسد. - سرانجام دیگری وجود دارد
for
حلقه ای که برخی از پیام ها را خروجی می کند. در اینجا از دستگیره های معمولی Curl (نه چند دسته!) باید دوباره استفاده شود. در این مثال این استcurl_getinfo
دوباره برای بازیابی کد وضعیت برای هر دسته. جالب اینجاست که اگر می خواهید محتوای یک درخواست را دریافت کنید ، باید از آن استفاده کنیدcurl_multi_getcontent
و همچنین برای یک درخواست واحد ، دسته حلقه را منتقل کنید. احتمالاً به این دلیل است کهcurl_exec
معمولاً بدن پاسخ را برمی گرداند ، چیزی که با چند دستگیره کار نمی کند ، از آنجا کهcurl_exec
در کد نامیده نمی شود.
حتی اگر این کار کند ، به نظر من نوعی API عجیب است. با این حال ، خروجی زیر نشان می دهد که استفاده از آن از نظر عملکرد پرداخت می شود:
$ php 03_multi_curl.php
Response code for request to https://example.com/ is 200
Response code for request to https://example.com/1 is 404
Response code for request to https://example.com/2 is 404
Response code for request to https://example.com/3 is 404
Response code for request to https://example.com/4 is 404
Response code for request to https://example.com/5 is 404
Response code for request to https://example.com/6 is 404
Response code for request to https://example.com/7 is 404
Response code for request to https://example.com/8 is 404
Response code for request to https://example.com/9 is 404
All requests took 0.499s
این حتی خیلی سریعتر از مثال قبلی است ، با گرفتن یک سوم از زمان روی دستگاه من. تقریباً مقدار زمان کمترین درخواست ها را می گیرد ، زیرا تا آن زمان تمام درخواست های دیگر در حال حاضر به پایان رسیده اند.
دسته بندی درخواست های فرفری موازی
اما متأسفانه محدودیتی در این مورد وجود دارد: شما نمی توانید همزمان یک مقدار نامحدود از درخواست ها را شروع کنید. ظاهراً دستگاه من می تواند 10 درخواست را به طور موازی انجام دهد ، اما با شروع تعدادی از درخواست ها احتمالاً منطقی است که درخواست های دسته ای را انجام دهد. مثال زیر فرض می شود که این شماره 3 است ، یعنی ما فقط 3 درخواست را به طور همزمان ارسال خواهیم کرد. علاوه بر این ، دستگیره های حلقوی مانند اولین مرحله بهبود ما باید دوباره مورد استفاده قرار گیرند.
کد زیر نحوه اجرای چنین مکانیسم دسته بندی را با CURL در PHP نشان می دهد:
$urls = [
'https://example.com/',
'https://example.com/1',
'https://example.com/2',
'https://example.com/3',
'https://example.com/4',
'https://example.com/5',
'https://example.com/6',
'https://example.com/7',
'https://example.com/8',
'https://example.com/9',
];
$total_start = microtime(true);
$multi_handle = curl_multi_init();
$batch_size = 3;
$handles = [];
for ($i = 0; $i < $batch_size; ++$i) {
$handles[$i] = curl_init();
curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
}
$batch = [];
foreach ($urls as $index => $url) {
$batch[] = $url;
if (count($batch) % $batch_size !== 0 && $index < count($urls) - 1) {
continue;
}
for ($i = 0; $i < count($batch); ++$i) {
curl_multi_add_handle($multi_handle, $handles[$i]);
curl_setopt($handles[$i], CURLOPT_URL, $batch[$i]);
}
$running = null;
do {
curl_multi_exec($multi_handle, $running);
curl_multi_select($multi_handle);
} while ($running > 0);
for ($i = 0; $i < count($batch); ++$i) {
$response_code = curl_getinfo($handles[$i], CURLINFO_HTTP_CODE);
echo "Response code for request to {$batch[$i]} is {$response_code}\n";
curl_multi_remove_handle($multi_handle, $handles[$i]);
}
$batch = [];
}
$total_duration = number_format(microtime(true) - $total_start, 3);
echo "All requests took {$total_duration}s\n";
ساختار کمی متفاوت از مثالهای قبلی است ، حتی اگر فقط یک روش جدید وجود داشته باشد (curl_multi_remove_handle
) استفاده:
- اولی
for
حلقه سه دسته حلقه را تنظیم می کند که برای همه درخواست های ارسال شده استفاده می شود. - سپس قسمت اول
foreach
مسئول دسته بندی است (تا زمانی کهcontinue
بیانیه ، که در صورت پر کردن صحیح دسته ، از اجرای کد زیر جلوگیری می کند). - اگر یک دسته پر شود ، دیگری
for
حلقه تعداد صحیح دستگیره ها را به چند دسته اضافه می کند و URL را برای آن دسته تنظیم می کند. - پس از آن همان حلقه با
curl_multi_exec
وتcurl_multi_select
همانطور که در مثال قبلی برای اجرای درخواست های دسته فعلی استفاده می شود. - موارد زیر
for
حلقه اطلاعات را خروجی می کند و دسته را از چند دسته با استفاده از آن خارج می کندcurl_multi_remove_handle
بشر
ممکن است تماس وسوسه انگیز باشد curl_multi_add_handle
فقط یک بار برای هر دسته در اول for
حلقه ای که در آن آغاز می شود و تماس نمی گیرید curl_multi_remove_handle
بعد از هر دسته به منظور بهبود بیشتر عملکرد. با این حال ، معلوم می شود که این یک اثر جانبی ناخواسته خواهد داشت: به دلایلی curl_multi_exec
سپس از اطلاعات موجود از پاسخ های قبلاً دریافت شده استفاده مجدد می شود ، یعنی فقط 3 درخواست به جای 10 لازم ارسال می شود. بنابراین این تماس های مکرر به curl_multi_add_handle
وت curl_multi_remove_handle
کاملاً ضروری هستند
نتیجه به همان اندازه مثال قبلی نیست ، زیرا این رویکرد فقط در صورت ارسال درخواست های بیشتر معنی دارد و همه آنها نمی توانند همزمان ارسال شوند. هنوز هم در اینجا اعداد وجود دارد:
$ php 04_chunking_parallel_requests.php
Response code for request to https://example.com/ is 200
Response code for request to https://example.com/1 is 404
Response code for request to https://example.com/2 is 404
Response code for request to https://example.com/3 is 404
Response code for request to https://example.com/4 is 404
Response code for request to https://example.com/5 is 404
Response code for request to https://example.com/6 is 404
Response code for request to https://example.com/7 is 404
Response code for request to https://example.com/8 is 404
Response code for request to https://example.com/9 is 404
All requests took 0.801s
اما نکته اصلی این است که موازی سازی درخواست های HTTP می تواند منجر به بهبود عملکرد قابل توجهی شود. امیدوارم که یافته های من بتواند به دیگران کمک کند تا با همان مسائل دست و پنجه نرم کنند.