Простий приклад використання PHP та AJAX. Зберігання php-сесій у Redis з блокуваннями

). Хмара призначена для того, щоб запускати різні PHP скрипти за розкладом або через API. Як правило, ці скрипти обробляють черги, і навантаження розмазується приблизно по 100 серверам. Раніше ми акцентували увагу на тому, як реалізована логіка управління, яка відповідає за рівномірний розподіл навантаження за такою кількістю серверів і генерацію завдань за розкладом. Але, крім цього, нам потрібно було написати демон, який міг би запускати наші PHP-скрипти в CLI і стежити за статусом їх виконання.

Спочатку він був написаний на Сі, як і всі інші демони нашої компанії. Однак ми зіткнулися з тим, що суттєва частина процесорного часу (близько 10%) витрачалася по суті марно: це запуск інтерпретатора та завантаження «ядра» нашого фреймворку. Тому, щоб мати можливість ініціалізувати інтерпретатор та наш фреймворк лише один раз, було ухвалено рішення переписати демон на PHP. Ми назвали його Php rock syd (за аналогією з Phproxyd – PHP Proxy Daemon, демоном на Сі, який у нас був до цього). Він приймає запити на запуск окремих класів і робить fork() на кожен запит, а також вміє повідомляти про статус виконання кожного із запусків. Така архітектура багато в чому схожа на модель веб-сервера Apache, коли вся ініціалізація робиться один раз у «майстрі» та «діти» займаються саме обробкою запиту. Як додаткова «плюшка» ми отримуємо можливість включити opcode cache в CLI, який правильно працюватиме, оскільки всі діти успадковують ту ж область загальної пам'яті, що і майстер-процес. Щоб зменшити затримки при обробці запиту на запуск, можна робити заздалегідь fork() (prefork-модель), але в нашому випадку затримки на fork() становлять близько 1 мс, що нас цілком влаштовує.

Однак, оскільки ми оновлюємо код часто, цей демон також доводиться часто перезапускати, інакше код, який завантажений в нього, може застаріти. Оскільки кожен рестарт супроводжувався б масою помилок виду connection reset by peer, включаючи відмови в обслуговуванні кінцевих користувачів (демон корисний не тільки для хмари, але й для частини нашого сайту), ми вирішили пошукати способи зробити рестарт демона без втрати вже встановлених з'єднань. Існує одна популярна техніка, за допомогою якої робиться graceful reloadдля демонів: робиться fork-exec і у своїй нащадку передається дескриптор від listen-сокета. Таким чином, нові з'єднання приймаються вже новою версією демона, а старі «допрацьовують» із використанням старої версії.

У цій статті ми розглянемо ускладнений варіант graceful reload: старі підключення будуть продовжувати оброблятися новою версією демона, що важливо в нашому випадку, оскільки він буде запускати старий код.

Теорія Давайте спочатку подумаємо: чи можливе те, що ми хочемо отримати? І якщо так, то як цього досягти?

Оскільки демон працює під Linux, який є POSIX-сумісним, нам доступні такі можливості:

  • Всі відкриті файли та сокети – це числа, що відповідають номеру відкритого дескриптора. Стандартне введення, виведення та потік помилок мають дескриптори 0, 1 і 2 відповідно.
  • Жодних суттєвих відмінностей між відкритим файлом, сокетом і каналом (pipe) немає (наприклад, із сокетами можна працювати як за допомогою системних викликів read/write, так і sendto/recvfrom).
  • При виконанні системного виклику fork() усі відкриті дескриптори успадковуються зі збереженням їх номерів та позицій читання/запису (у файлах).
  • При виконанні системного виклику execve() всі відкриті дескриптори також успадковуються, причому на додаток зберігається процес PID і, отже, прив'язка до своїх дітей.
  • Список відкритих дескрипторів процесу доступний з директорії /dev/fd, який у Linux є симлінком на /proc/self/fd.
  • Таким чином, у нас є всі підстави вважати, що наше завдання можна здійснити, причому без особливих зусиль. Отже, приступимо. демона не підходить, оскільки ми дуже ретельно стежимо за відкритими дескрипторами, щоб не створювати витоків при рестарті та запуску дочірніх процесів).

    Для початку ми внесемо пару невеликих патчів у код PHP, щоб додати можливість отримати fd у потоку (stream) і зробити так, щоб fopen(php://fd/) не приводив до відкриття копії дескриптора (друга зміна несумісна з поточною поведінкою PHP, тому замість нього можна додати нову «адресу», наприклад, php://fdraw/):

    Код патчу

    diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c index f8d7bda..fee964c 100644 --- a/ext/standard/php_fopen_wrapper.c +++ b/ext/standard/ph c @@ -24,6 +24,7 @@ #if HAVE_UNISTD_H #include #endif +#include #include "php.h" #include "php_globals.h" @@ -296,11 +297,11 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, char *path, ch "Телеграфні файли повинні бути невід'ємними номерами смаллера than %d", dtablesize); return NULL; ) - - fd = dup (fildes_ori); - if (fd == -1) ( + + fd = fildes_ori; + if (fcntl(fildes_ori, F_GETFD) == -1) ( php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, - "Error duping file descriptor %ld; possibly it doesn "t exist: " + "File descriptor %ld invalid: " "[%d]: %s", fildes_ori, errno, strerror(errno)); return NULL; ) diff --git a/ext/standard/streamsfuncs. c b/ext/standard/streamsfuncs.c index 0610ecf..14fd3b0 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -24,6 +24,7 @ @ #include "ext/standard/flock_compat.h" #include "ext/standard/file.h" #include "ext/standard/php_filestat.h" +#include "ext/standard/php_fopen_wrappers.h" #include "php_open_temporary_file .h" #include "ext/standard/basic_functions.h" #include "php_ini.h" @@ -484,6 +485,7 @@ PHP_FUNCTION(stream_get_meta_data) zval *arg1; php_stream *stream; zval *newval; + int tmp_fd; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &arg1) == FAILURE) ( return; @@ -502,6 +504,9 @@ PHP_FUNCTION(stream_get_meta_data) add_as char *)stream->wrapper->wops->label, 1); ) add_assoc_string(return_value, "stream_type", (char *) stream->ops->label, 1); + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&tmp_fd, 1) && tmp_fd != -1) ( + add_assoc_long(return_val string(return_value, "mode ", stream->mode, 1);


    Ми додали поле fd у результат, що повертається функцією stream_get_meta_data(), якщо воно має сенс (наприклад, для zlib-потоків поле fd не буде). Також ми замінили виклик dup() від переданого файлового дескриптора на просту перевірку. На жаль, цей код не буде працювати без модифікацій під Windows, оскільки виклик fcntl() - це POSIX-specific, так що повний патч повинен містити в собі додаткові гілки коду під інші ОС.Демон без можливості перезапуску зможе приймати запити у форматі JSON і віддавати будь-яку відповідь. Наприклад, він віддаватиме кількість елементів у масиві, який надійшов у запиті.

    Демон прослуховує порт 31337. Результат роботи повинен бути приблизно наступним:

    $ telnet localhost 31337 Trying 127.0.0.1... Connected to localhost. Escape character is "^]". ("hash":1) # введення користувача "Request had 1 keys" ("hash":1,"cnt":2) # введення користувача "Request had 2 keys"

    Ми будемо використовувати stream_socket_server() для того, щоб почати слухати порт і stream_select() для того, щоб визначити, які дескриптори готові до читання/запису.

    Код найпростішої реалізації (Simple.php)