Cách tốt nhất để quản lý tập lệnh php chạy dài?


81

Tôi có một tập lệnh PHP mất nhiều thời gian (5-30 phút) để hoàn thành. Chỉ trong trường hợp có vấn đề, tập lệnh đang sử dụng curl để quét dữ liệu từ một máy chủ khác. Đây là lý do khiến nó mất nhiều thời gian; nó phải đợi tải từng trang trước khi xử lý và chuyển sang trang tiếp theo.

Tôi muốn có thể bắt đầu tập lệnh và để nó cho đến khi hoàn thành, điều này sẽ đặt cờ trong bảng cơ sở dữ liệu.

Điều tôi cần biết là làm thế nào để có thể kết thúc yêu cầu http trước khi tập lệnh chạy xong. Ngoài ra, một script php có phải là cách tốt nhất để làm điều này không?


1
Mặc dù bạn không đề cập đến nó bằng các ngôn ngữ được máy chủ của bạn hỗ trợ, tôi sẽ đoán nếu bạn có khả năng chạy Ruby và Perl, bạn có thể thêm Node.js một cách thành thạo và điều này đối với tôi nghe giống như một trường hợp sử dụng hoàn hảo cho Javascript : tập lệnh của bạn sẽ dành phần lớn thời gian để chờ các yêu cầu hoàn thành, đây là khu vực mà mô hình không đồng bộ vượt trội. Không có luồng nào có nghĩa là đồng bộ hóa dễ dàng, đồng thời có nghĩa là spead.
djfm

Bạn có thể làm điều này với PHP. Tôi sẽ sử dụng GoutteGuzzletriển khai các chuỗi đồng thời. Bạn cũng có thể xem xét Gearmanđể khởi chạy các yêu cầu song song dưới dạng công nhân.
Andre Garcia

Câu trả lời:


114

Chắc chắn nó có thể được thực hiện với PHP, tuy nhiên bạn KHÔNG nên làm điều này như một tác vụ nền - quy trình mới phải được tách biệt khỏi nhóm quy trình nơi nó được bắt đầu.

Vì mọi người liên tục đưa ra cùng một câu trả lời sai cho Câu hỏi thường gặp này, tôi đã viết một câu trả lời đầy đủ hơn ở đây:

http://symcbean.blogspot.com/2010/02/php-and-long-running-processes.html

Từ các ý kiến:

Phiên bản ngắn là shell_exec('echo /usr/bin/php -q longThing.php | at now');vậy nhưng lý do tại sao hơi dài để đưa vào đây.


Bài đăng trên blog này là câu trả lời thực sự. Hệ thống & thực thi của PHP có quá nhiều cạm bẫy tiềm ẩn.
incredimike

2
bất kỳ cơ hội sao chép các chi tiết liên quan vào câu trả lời? có quá nhiều câu trả lời cũ liên kết đến các blog đã chết. Blog đó chưa chết (chưa) nhưng sẽ có một ngày nào đó.
Murphy

5
Phiên bản ngắn là shell_exec('echo /usr/bin/php -q longThing.php | at now');vậy nhưng lý do tại sao hơi dài để đưa vào đây.
symcbean

1
Câu trả lời được bình chọn cao cho một câu hỏi được bình chọn cao, nhưng câu trả lời không liên quan đến một bài đăng blog. Vui lòng thêm câu trả lời thực tế, theo meta.stackexchange.com/questions/8231/… và / hoặc trung tâm trợ giúp
Nanne

1
Tôi có thể biết tùy chọn -q này đang làm gì không?
Kiren Siva

11

Cách nhanh chóng và dễ dàng là sử dụng ignore_user_aborthàm trong php. Về cơ bản, điều này nói lên rằng: Đừng quan tâm người dùng làm gì, hãy chạy tập lệnh này cho đến khi nó hoàn thành. Điều này hơi nguy hiểm nếu đó là một trang web công khai (vì có thể bạn sẽ có 20 ++ phiên bản script chạy cùng lúc nếu nó được khởi chạy 20 lần).

Cách "sạch" (ít nhất là IMHO) là đặt cờ (trong db chẳng hạn) khi bạn muốn bắt đầu quá trình và chạy cronjob mỗi giờ (hoặc lâu hơn) để kiểm tra xem cờ đó đã được đặt chưa. Nếu nó được đặt, tập lệnh chạy dài sẽ bắt đầu, nếu nó KHÔNG được đặt, sẽ không xảy ra.


Vì vậy, phương thức "ignore_user_abort" sẽ cho phép người dùng đóng cửa sổ trình duyệt, nhưng tôi có thể làm gì để nó trả lại phản hồi HTTP cho máy khách trước khi chạy xong không?
kbanman

1
@kbanman Yep. Bạn cần để đóng kết nối: header("Connection: close", true);. Và đừng quên để flush ()
Benubird

8

Bạn có thể sử dụng exec hoặc hệ thống để bắt đầu một công việc nền, và sau đó làm việc ở đó.

Ngoài ra, có những cách tiếp cận tốt hơn để tìm kiếm trang web mà bạn đang sử dụng. Bạn có thể sử dụng cách tiếp cận theo luồng (nhiều luồng thực hiện một trang tại một thời điểm) hoặc sử dụng một chuỗi sự kiện (một luồng thực hiện nhiều trang cùng một lúc). Phương pháp tiếp cận cá nhân của tôi sử dụng Perl sẽ sử dụng AnyEvent :: HTTP .

ETA: symcbean giải thích cách tách quá trình nền đúng cách tại đây .


5
Gần đúng. Chỉ cần sử dụng thực thi hoặc hệ thống sẽ quay lại cắn bạn. Xem câu trả lời của tôi để biết chi tiết.
symcbean

5

Không, PHP không phải là giải pháp tốt nhất.

Tôi không chắc về Ruby hay Perl, nhưng với Python, bạn có thể viết lại trình quét trang của mình thành đa luồng và nó có thể sẽ chạy nhanh hơn ít nhất 20 lần. Viết các ứng dụng đa luồng có thể hơi khó khăn, nhưng ứng dụng Python đầu tiên mà tôi viết là trình quét trang có nhiều luồng. Và bạn có thể chỉ cần gọi tập lệnh Python từ bên trong trang PHP của mình bằng cách sử dụng một trong các hàm thực thi shell.


Phần xử lý thực tế của việc cạo của tôi rất hiệu quả. Như tôi đã đề cập ở trên, việc tải từng trang sẽ giết chết tôi. Điều tôi băn khoăn là liệu PHP có được chạy trong thời gian dài như vậy không.
kbanman

Tôi hơi thiên vị vì từ khi học Python, tôi hoàn toàn không thích PHP. Tuy nhiên, nếu bạn đang cạo nhiều hơn một trang (theo chuỗi), bạn gần như chắc chắn sẽ có được hiệu suất tốt hơn bằng cách thực hiện song song với một ứng dụng đa luồng.
jamieb

1
Bất kỳ cơ hội nào bạn có thể gửi cho tôi một ví dụ về trình quét trang như vậy? Nó sẽ giúp tôi nhìn thấy rất nhiều khi tôi chưa chạm vào Python.
kbanman

Nếu tôi phải viết lại nó, tôi chỉ sử dụng eventlet. Nó làm cho mã của tôi đơn giản hơn khoảng 10 lần: eventlet.net/doc
jamieb

5

Có, bạn có thể làm điều đó bằng PHP. Nhưng ngoài PHP, sẽ là khôn ngoan khi sử dụng Trình quản lý hàng đợi. Đây là chiến lược:

  1. Chia nhỏ nhiệm vụ lớn của bạn thành các nhiệm vụ nhỏ hơn. Trong trường hợp của bạn, mỗi tác vụ có thể tải một trang.

  2. Gửi từng nhiệm vụ nhỏ vào hàng đợi.

  3. Chạy nhân viên xếp hàng của bạn ở đâu đó.

Sử dụng chiến lược này có những lợi thế sau:

  1. Đối với các tác vụ chạy dài, nó có khả năng phục hồi trong trường hợp có sự cố nghiêm trọng xảy ra ở giữa quá trình chạy - không cần bắt đầu lại từ đầu.

  2. Nếu các nhiệm vụ của bạn không phải chạy tuần tự, bạn có thể chạy nhiều công nhân để chạy các tác vụ đồng thời.

Bạn có nhiều tùy chọn (đây chỉ là một vài):

  1. RabbitMQ ( https://www.rabbitmq.com/tutorials/tutorial-one-php.html )
  2. ZeroMQ ( http://zeromq.org/bindings:php )
  3. Nếu bạn đang sử dụng khung Laravel, hàng đợi được tích hợp sẵn ( https://laravel.com/docs/5.4/queues ), với trình điều khiển cho AWS SES, Redis, Beanstalkd

3

PHP có thể là công cụ tốt nhất hoặc có thể không, nhưng bạn biết cách sử dụng nó và phần còn lại của ứng dụng của bạn được viết bằng cách sử dụng nó. Hai phẩm chất này, kết hợp với thực tế là PHP "đủ tốt" tạo nên một trường hợp khá tốt để sử dụng nó, thay vì Perl, Ruby hoặc Python.

Nếu mục tiêu của bạn là học một ngôn ngữ khác, hãy chọn một ngôn ngữ và sử dụng nó. Bất kỳ ngôn ngữ nào bạn đề cập sẽ thực hiện công việc, không có vấn đề. Tôi tình cờ thích Perl, nhưng những gì bạn thích có thể khác.

Symcbean có một số lời khuyên hữu ích về cách quản lý các quy trình nền tại liên kết của anh ấy.

Tóm lại, hãy viết một tập lệnh PHP CLI để xử lý các bit dài. Đảm bảo rằng nó báo cáo trạng thái theo một cách nào đó. Tạo một trang php để xử lý các cập nhật trạng thái, sử dụng AJAX hoặc các phương pháp truyền thống. Tập lệnh khởi động của bạn sẽ bắt đầu quá trình chạy trong phiên của chính nó và trả về xác nhận rằng quá trình đang diễn ra.

Chúc may mắn.


1

Tôi đồng ý với các câu trả lời nói rằng điều này nên được chạy trong một quy trình nền. Nhưng điều quan trọng là bạn phải báo cáo về trạng thái để người dùng biết rằng công việc đang được thực hiện.

Khi nhận được yêu cầu PHP để bắt đầu quá trình, bạn có thể lưu trữ trong cơ sở dữ liệu một biểu diễn của nhiệm vụ với một mã định danh duy nhất. Sau đó, bắt đầu quá trình quét màn hình, chuyển cho nó số nhận dạng duy nhất. Báo cáo lại cho ứng dụng iPhone rằng nhiệm vụ đã được bắt đầu và nó nên kiểm tra một URL được chỉ định, chứa ID nhiệm vụ mới, để có trạng thái mới nhất. Ứng dụng iPhone hiện có thể thăm dò ý kiến ​​(hoặc thậm chí là "thăm dò dài") URL này. Trong thời gian chờ đợi, quy trình nền sẽ cập nhật biểu diễn cơ sở dữ liệu của nhiệm vụ khi nó hoạt động với tỷ lệ phần trăm hoàn thành, bước hiện tại hoặc bất kỳ chỉ báo trạng thái nào khác mà bạn muốn. Và khi nó kết thúc, nó sẽ đặt một lá cờ đã hoàn thành.


1

Bạn có thể gửi nó dưới dạng một yêu cầu XHR (Ajax). Khách hàng thường không có bất kỳ thời gian chờ nào cho XHR, không giống như các yêu cầu HTTP thông thường.


1

Tôi nhận ra đây là một câu hỏi khá cũ nhưng tôi muốn thử lại. Tập lệnh này cố gắng giải quyết cả cuộc gọi khởi động ban đầu để kết thúc nhanh chóng và giảm tải nặng thành các phần nhỏ hơn. Tôi chưa thử nghiệm giải pháp này.

<?php
/**
 * crawler.php located at http://mysite.com/crawler.php
 */

// Make sure this script will keep on runing after we close the connection with
// it.
ignore_user_abort(TRUE);


function get_remote_sources_to_crawl() {
  // Do a database or a log file query here.

  $query_result = array (
    1 => 'http://exemple.com',
    2 => 'http://exemple1.com',
    3 => 'http://exemple2.com',
    4 => 'http://exemple3.com',
    // ... and so on.
  );

  // Returns the first one on the list.
  foreach ($query_result as $id => $url) {
    return $url;
  }
  return FALSE;
}

function update_remote_sources_to_crawl($id) {
  // Update my database or log file list so the $id record wont show up
  // on my next call to get_remote_sources_to_crawl()
}

$crawling_source = get_remote_sources_to_crawl();

if ($crawling_source) {


  // Run your scraping code on $crawling_source here.


  if ($your_scraping_has_finished) {
    // Update you database or log file.
    update_remote_sources_to_crawl($id);

    $ctx = stream_context_create(array(
      'http' => array(
        // I am not quite sure but I reckon the timeout set here actually
        // starts rolling after the connection to the remote server is made
        // limiting only how long the downloading of the remote content should take.
        // So as we are only interested to trigger this script again, 5 seconds 
        // should be plenty of time.
        'timeout' => 5,
      )
    ));

    // Open a new connection to this script and close it after 5 seconds in.
    file_get_contents('http://' . $_SERVER['HTTP_HOST'] . '/crawler.php', FALSE, $ctx);

    print 'The cronjob kick off has been initiated.';
  }
}
else {
  print 'Yay! The whole thing is done.';
}

@symcbean Tôi đã đọc bài đăng mà bạn đề xuất và muốn nghe suy nghĩ của bạn về giải pháp thay thế này.
Francisco Luz

Đầu tiên, bạn đã cho tôi một ý tưởng khởi đầu cho con bot đầu tiên của tôi (teehee). Thứ hai, bạn đã tìm thấy hiệu suất của giải pháp của mình như thế nào? Bạn đã làm việc với nó thêm và học thêm được gì không? Tôi quan tâm đến việc thực hiện một cái gì đó tương tự như nạo vét thông qua 26.000 hình ảnh (1,3GB), thực hiện các hoạt động khác nhau, v.v. Sẽ mất một lúc. Yours là giải pháp duy nhất không có vẻ gì là hacky, sử dụng lệnh exe () rùng mình hoặc yêu cầu Linux (một số người thua cuộc vẫn phải sử dụng Windows). Tôi thích học hỏi từ headbashing của bạn, chứ không phải của riêng tôi: P
Just Plain cao

@HighPriestessofTheTech Chào bạn, tôi chưa đi xa hơn. Vào thời điểm tôi viết bài này, tôi chỉ đang đưa ra một thử nghiệm suy nghĩ.
Francisco Luz

1
Ôi trời ... Vì vậy, tôi sẽ học hỏi từ cách chải đầu của chính mình ... Tôi sẽ cho bạn biết mọi thứ diễn ra như thế nào;)
Just Plain High

1
Tôi đã thử điều này và tôi thấy nó khá hữu ích.
Alex

1

Tôi muốn đề xuất một giải pháp hơi khác với của symcbean, chủ yếu là vì tôi có yêu cầu bổ sung rằng quá trình chạy dài cần được chạy với tư cách người dùng khác chứ không phải người dùng apache / www-data.

Giải pháp đầu tiên sử dụng cron để thăm dò bảng nhiệm vụ nền:

  • Trang web PHP chèn vào một bảng nhiệm vụ nền, trạng thái 'ĐÃ GỬI'
  • cron chạy 3 phút một lần, sử dụng người dùng khác, chạy tập lệnh PHP CLI để kiểm tra bảng tác vụ nền cho các hàng 'SUBMITTED'
  • PHP CLI sẽ cập nhật cột trạng thái trong hàng thành 'PROCESSING' và bắt đầu xử lý, sau khi hoàn thành nó sẽ được cập nhật thành 'COMPLETED'

Giải pháp thứ hai sử dụng tiện ích inotify của Linux:

  • Trang web PHP cập nhật một tệp điều khiển với các tham số do người dùng đặt và cũng cung cấp một id tác vụ
  • shell script (với tư cách là người dùng không phải www) chạy inotifywait sẽ đợi tệp điều khiển được ghi
  • sau khi tệp điều khiển được ghi, sự kiện close_write sẽ được đưa ra, kịch bản shell sẽ tiếp tục
  • shell script thực thi PHP CLI để thực hiện quá trình chạy dài
  • PHP CLI ghi đầu ra vào tệp nhật ký được xác định bằng id tác vụ hoặc cập nhật tiến độ trong bảng trạng thái
  • Trang web PHP có thể thăm dò tệp nhật ký (dựa trên id nhiệm vụ) để hiển thị tiến trình của quá trình chạy lâu dài hoặc nó cũng có thể truy vấn bảng trạng thái

Một số thông tin bổ sung có thể được tìm thấy trong bài đăng của tôi: http://inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html


0

Tôi đã làm những việc tương tự với Perl, double fork () và tách khỏi quy trình mẹ. Tất cả công việc tìm nạp http phải được thực hiện trong quá trình phân nhánh.



0

những gì TÔI LUÔN LUÔN sử dụng là một trong những biến thể này (vì các phiên bản Linux khác nhau có các quy tắc khác nhau về xử lý đầu ra / một số chương trình xuất ra khác nhau):

Biến thể I @exec ('./ myscript.php \ 1> / dev / null \ 2> / dev / null &');

Biến thể II @exec ('php -f myscript.php \ 1> / dev / null \ 2> / dev / null &');

Biến thể III @exec ('nohup myscript.php \ 1> / dev / null \ 2> / dev / null &');

Bạn có thể phải cài đặt "nohup". Nhưng ví dụ: khi tôi đang tự động chuyển đổi video FFMPEG, bằng cách nào đó, giao diện đầu ra không được xử lý 100% bằng cách chuyển hướng các luồng đầu ra 1 & 2, vì vậy tôi đã sử dụng nohup VÀ chuyển hướng đầu ra.


0

nếu bạn có tập lệnh dài thì hãy phân chia công việc của trang với sự trợ giúp của tham số đầu vào cho mỗi tác vụ. (khi đó mỗi trang hoạt động như một chuỗi) tức là nếu trang có 1 vòng lặp quy trình dài lac product_keywords thì thay vì vòng lặp hãy tạo logic cho một từ khóa và chuyển từ khóa này từ magic hoặc cornjobpage.php (trong ví dụ sau)

và đối với background worker, tôi nghĩ bạn nên thử kỹ thuật này, nó sẽ giúp gọi bao nhiêu trang bạn thích, tất cả các trang sẽ chạy độc lập cùng một lúc mà không cần đợi phản hồi của từng trang là không đồng bộ.

Cornjobpage.php // trang chính

    <?php

post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue");
//post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2");
//post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue");
//call as many as pages you like all pages will run at once independently without waiting for each page response as asynchronous.
            ?>
            <?php

            /*
             * Executes a PHP page asynchronously so the current page does not have to wait for it to     finish running.
             *  
             */
            function post_async($url,$params)
            {

                $post_string = $params;

                $parts=parse_url($url);

                $fp = fsockopen($parts['host'],
                    isset($parts['port'])?$parts['port']:80,
                    $errno, $errstr, 30);

                $out = "GET ".$parts['path']."?$post_string"." HTTP/1.1\r\n";//you can use POST instead of GET if you like
                $out.= "Host: ".$parts['host']."\r\n";
                $out.= "Content-Type: application/x-www-form-urlencoded\r\n";
                $out.= "Content-Length: ".strlen($post_string)."\r\n";
                $out.= "Connection: Close\r\n\r\n";
                fwrite($fp, $out);
                fclose($fp);
            }
            ?>

testpage.php

    <?
    echo $_REQUEST["Keywordname"];//case1 Output > testValue
    ?>

Tái bút: nếu bạn muốn gửi tham số url dưới dạng vòng lặp thì hãy làm theo câu trả lời sau: https://stackoverflow.com/a/41225209/6295712


0

Không phải là cách tiếp cận tốt nhất, như nhiều người đã nêu ở đây, nhưng điều này có thể giúp:

ignore_user_abort(1); // run script in background even if user closes browser
set_time_limit(1800); // run it for 30 minutes

// Long running script here

0

Nếu đầu ra mong muốn của tập lệnh của bạn là một số quá trình xử lý, không phải là một trang web, thì tôi tin rằng giải pháp mong muốn là chạy tập lệnh của bạn từ trình bao, đơn giản là

php my_script.php

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.