FastNetMon

четверг, 24 июня 2010 г.

Как ограничить число php5-cgi процессов при использовании mod_fcgid?

Есть такая директива:

DefaultMaxClassProcessCount 2

Она ограничит число процессов для одного сайта двумя штуками? Или для пользователя? А хрен вот знает на самом деле.

Тут пишут, что поведение директивы было изменено в версии 2.3.5.
Somehow there was a decision made that changed max PHP processes from
per-user to per-vhost between the old mod-fcgid and the newer 2.3.5 version.

Так что попробуем разобраться с тем, что же значит эта директива для версии mod_fcgid 2.2-1 (Debian 5 Lenny) на основе исходных кодов.

cd /usr/src/
apt-get source libapache2-mod-fcgid
apt-get build-dep -y libapache2-mod-fcgid
cd libapache2-mod-fcgid-2.2

Первое упоминание этой опции идет в файле mod_fcgid.c:
571 AP_INIT_TAKE1("DefaultMaxClassProcessCount",
572 set_max_class_process,
573 NULL, RSRC_CONF,
574 "Max process count of one class of fastcgi application"),

Далее эта функция упоминается в файле fcgid_conf.c:
465 const char *set_max_class_process(cmd_parms * cmd, void *dummy,
466 const char *arg)
467 {
468 server_rec *s = cmd->server;
469 fcgid_server_conf *config =
470 ap_get_module_config(s->module_config, &fcgid_module);
471 config->max_class_process_count = atol(arg);
472 return NULL;
473 }

Чуть нижее нее есть функция получения значения этого лимита:
475 int get_max_class_process(server_rec * s)
476 {
477 fcgid_server_conf *config =
478 ap_get_module_config(s->module_config, &fcgid_module);
479 return config ? config->
480 max_class_process_count : DEFAULT_MAX_CLASS_PROCESS_COUNT;
481 }

Далее вторая функция вызывается в файле fcgid_spawn_ctl.c (предпоследняя строка) для инициализации переменной g_max_class_process :
99 void spawn_control_init(server_rec * main_server, apr_pool_t * configpool)
100 {
101 apr_status_t rv;
102
103 if ((rv = apr_pool_create(&g_stat_pool, configpool)) != APR_SUCCESS) {
104 /* Fatal error */
105 ap_log_error(APLOG_MARK, APLOG_EMERG, rv, main_server,
106 "mod_fcgid: can't create stat pool");
107 exit(1);
108 }
109
110 /* Initialize the variables from configuration */
111 g_time_score = get_time_score(main_server);
112 g_termination_score = get_termination_score(main_server);
113 g_spawn_score = get_spawn_score(main_server);
114 g_score_uplimit = get_spawnscore_uplimit(main_server);
115 g_max_process = get_max_process(main_server); // этот лимит установлен стандартно в DEFAULT_MAX_PROCESS_COUNT, а он равен 1000
116 g_max_class_process = get_max_class_process(main_server); // этот лимит стандартно установлен в DEFAULT_MAX_CLASS_PROCESS_COUNT, а он равен 100
117 g_min_class_process = get_min_class_process(main_server); // этот лимит стандартно установлен в DEFAULT_MIN_CLASS_PROCESS_COUNT, а он равен 3
118 }


Далее идет использование переменных g_max_class_process и g_max_process (глобальный лимит FastCGI процессов) в коде функции is_spawn_allowed:
182
183 /* Total process count higher than up limit? */
184 if (g_total_process >= g_max_process) {
185 ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, main_server,
186 "mod_fcgid: %s total process count %d >= %d, skip the spawn requ
est",
187 command->cgipath, g_total_process, g_max_process);
188 return 0;
189 }
190
191 /*
192 Process count of this class higher than up limit?
193 */
194 /* I need max class proccess count */
195 if (current_node->process_counter >= g_max_class_process) {
196 ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, main_server,
197 "mod_fcgid: too much %s process(current:%d, max:%d), skip the sp awn request",
198 command->cgipath, current_node->process_counter,
199 g_max_class_process);
200 return 0;
201 }


Где g_max_process (глобальный лимит процессов) сравниваем с g_total_process, тут все очевидно, это глобальное число процессов, которые были форкнуты. А вот g_max_class_process сравнивается с неким current_node->process_counter. Что есть этот current_node ?

Идем далее, функция is_spawn_allowed имеет прототип int is_spawn_allowed(server_rec * main_server, fcgid_command * command), по логике, второй параметр, command - это запускаемый в данный момент бинарик (блок информации о нем), как раз там мы и можем понять, каким образом они разделяются.

Вот описание структуры fcgid_command (см. fcgid_pm.h):
8 typedef struct {
9 char cgipath[_POSIX_PATH_MAX];
10 char wrapperpath[_POSIX_PATH_MAX];
11 apr_ino_t inode; // он-то нам и нужен!
12 dev_t deviceid;
13 apr_size_t share_grp_id;
14 uid_t uid; /* For suEXEC */
15 gid_t gid; /* For suEXEC */
16 int userdir; /* For suEXEC */
17 char initenv_key[INITENV_CNT][INITENV_KEY_LEN];
18 char initenv_val[INITENV_CNT][INITENV_VAL_LEN];
19 } fcgid_command;

Вызов функции is_spawn_allowed идет из функции pm_main (файл fcgid_pm_main.c) в виде:
466 apr_status_t pm_main(server_rec * main_server, apr_pool_t * configpool)
467 {
468 fcgid_command command;
469
470 /* Initialize the variables from configuration */
471 g_idle_timeout = get_idle_timeout(main_server);
472 g_idle_scan_interval = get_idle_scan_interval(main_server);
473 g_busy_scan_interval = get_busy_scan_interval(main_server);
474 g_proc_lifetime = get_proc_lifetime(main_server);
475 g_error_scan_interval = get_error_scan_interval(main_server);
476 g_zombie_scan_interval = get_zombie_scan_interval(main_server);
477 g_busy_timeout = get_busy_timeout(main_server);
478 g_busy_timeout += 10;
479 g_shutdown_timeout = get_shutdown_timeout(main_server);
480
481 while (1) {
482 if (procmgr_must_exit())
483 break;
484
485 /* Wait for command */
486 if (procmgr_peek_cmd(&command, main_server) == APR_SUCCESS) {
487 if (is_spawn_allowed(main_server, &command))
488 fastcgi_spawn(&command, main_server, configpool);
489
490 procmgr_finish_notify(main_server);
491 }
492


То есть, содержимое command мы получаем откуда-то извне посредством вызова команды procmgr_peek_cmd, которая читает этот самый command из некоего pipe (способ межпроцессного обмена данными) посредством функции (вот ее описание) apr_file_read_full(g_pm_read_pipe, command, sizeof(*command), NULL), файл arch/unix/fcgid_pm_unix.c:

464 apr_status_t procmgr_peek_cmd(fcgid_command * command,
465 server_rec * main_server)
466 {
467 apr_status_t rv;
468
469 /* Sanity check */
470 if (!g_pm_read_pipe)
471 return APR_EPIPE;
472
473 /* Wait for next command */
474 rv = apr_wait_for_io_or_timeout(g_pm_read_pipe, NULL, FOR_READ);
475
476 /* Log any unexpect result */
477 if (rv != APR_SUCCESS && !APR_STATUS_IS_TIMEUP(rv)) {
478 ap_log_error(APLOG_MARK, LOG_WARNING, rv, main_server,
479 "mod_fcgid: wait io error while getting message from pipe");
480 return rv;
481 }
482
483 /* Timeout */
484 if (rv != APR_SUCCESS)
485 return rv;
486
487 return apr_file_read_full(g_pm_read_pipe, command, sizeof(*command),
488 NULL);
489 }

Этот pipe создается следующей функцией (см. arch/unix/fcgid_pm_unix.c):
308 if ((rv = apr_file_pipe_create(&g_pm_read_pipe, &g_ap_write_pipe,

pipe этот предназначен для обмена данными между Апачем и процесс-менеджером (так понимаю, он является отдельным процессом), который отфоркивает нам процессы.

Процесс-менеджер создается функцией apr_proc_fork, это обычный fork процесса (см. arch/unix/fcgid_pm_unix.c):
198 static apr_status_t
199 create_process_manager(server_rec * main_server, apr_pool_t * configpool)
200 {
201 apr_status_t rv;
202
203 g_process_manager =
204 (apr_proc_t *) apr_pcalloc(configpool, sizeof(*g_process_manager));
205 rv = apr_proc_fork(g_process_manager, configpool);


Осталось понять, откуда эти блоки данных типа fcgid_command отправляются.

А делает это функция procmgr_post_spawn_cmd (arch/unix/fcgid_pm_unix.c):
401 apr_status_t procmgr_post_spawn_cmd(fcgid_command * command,
402 request_rec * r)
403 {
404 apr_status_t rv;
405 char notifybyte;
406 apr_size_t nbytes = sizeof(*command);
407 server_rec *main_server = r->server;
408
409 /* Sanity check first */
410 if (g_caughtSigTerm || !g_ap_write_pipe)
411 return APR_SUCCESS;
412
413 /* Get the global mutex before posting the request */
414 if ((rv = apr_global_mutex_lock(g_pipelock)) != APR_SUCCESS) {
415 ap_log_error(APLOG_MARK, LOG_WARNING, rv, main_server,
416 "mod_fcgid: can't get pipe mutex");
417 exit(0);
418 }
419
420 if ((rv =
421 apr_file_write_full(g_ap_write_pipe, command, nbytes,
422 NULL)) != APR_SUCCESS) {


А вызывается она из функции handle_request (fcgid_bridge.c):
270 static int
271 handle_request(request_rec * r, int role, const char *argv0,
272 fcgid_wrapper_conf * wrapper_conf,
273 apr_bucket_brigade * output_brigade)
274 {
...
342
343 /* Send a spawn request if I can't get a process slot */
344 procmgr_post_spawn_cmd(&fcgi_request, r);


В частности посылается структура fcgi_request, которая чуть выше инициализируется вот так:
320 /* Init spawn request */
321 procmgr_init_spawn_cmd(&fcgi_request, r, argv0, deviceid,
322 inode, shareid);
323

Сама функция инициализации command имеет вид:
337 void procmgr_init_spawn_cmd(fcgid_command * command, request_rec * r,
338 const char *argv0, dev_t deviceid,
339 apr_ino_t inode, apr_size_t share_grp_id)
340 {
...
384 strncpy(command->cgipath, argv0, _POSIX_PATH_MAX);
385 command->cgipath[_POSIX_PATH_MAX - 1] = '\0';
386 command->deviceid = deviceid;
387 command->inode = inode; // внимание!
388 command->share_grp_id = share_grp_id;
389
390 /* Update fcgid_command with wrapper info */
391 command->wrapperpath[0] = '\0';
392 if ((wrapperconf = get_wrapper_info(argv0, r))) {
393 strncpy(command->wrapperpath, wrapperconf->args, _POSIX_PATH_MAX);
394 command->wrapperpath[_POSIX_PATH_MAX - 1] = '\0';
395 command->deviceid = wrapperconf->deviceid;
396 command->inode = wrapperconf->inode; // внимание!
397 command->share_grp_id = wrapperconf->share_group_id;
398 }


Итого, у нас два варианта заполнения поля inode - обычный и из wrapper. И использование / не использование inode от wrapper управляется функцией get_wrapper_info (fcgid_conf.c), которая имеет вид:
846 fcgid_wrapper_conf *get_wrapper_info(const char *cgipath, request_rec * r)
847 {
848 char *extension;
849 fcgid_wrapper_conf *wrapper;
850 fcgid_dir_conf *config =
851 ap_get_module_config(r->per_dir_config, &fcgid_module);
852
853 /* Get file name extension */
854 extension = ap_strrchr_c(cgipath, '.');
855 if (extension == NULL)
856 return NULL;
857
858 /* Search file name extension in per_dir_config */
859 if (config
860 && (wrapper =
861 apr_hash_get(config->wrapper_info_hash, extension,
862 strlen(extension))))
863 return wrapper;
864
865 return NULL;
866 }

В моем случае wrapper используется и задается директивой FCGIWrapper в конфиге Апача, далее (fcgid_conf.c) для инициализации таблицы этих самых wrapper`ов используется функция set_wrapper_config (fcgid_conf.c):
773 const char *set_wrapper_config(cmd_parms * cmd, void *dirconfig,
774 const char *wrapperpath,
775 const char *extension)
776 {
777 const char *path, *tmp;
778 apr_status_t rv;
779 apr_finfo_t finfo;
780 const char *userdata_key = "fcgid_wrapper_id";
781 wrapper_id_info *id_info;
782 apr_size_t *wrapper_id;
783 fcgid_wrapper_conf *wrapper = NULL;
784 fcgid_dir_conf *config = (fcgid_dir_conf *) dirconfig;

Там же получаем stat файла враппера:
824 /* Is the wrapper exist? */
825 if ((rv = apr_stat(&finfo, path, APR_FINFO_NORM,
826 cmd->temp_pool)) != APR_SUCCESS) {
827 return apr_psprintf(cmd->pool,
828 "can't get fastcgi file info: %s(%s), errno: %d",
829 wrapperpath, path, apr_get_os_error());
830 }
831

И чуть ниже inode от него помещаем в требуемую нам структуру:
832 strncpy(wrapper->args, wrapperpath, _POSIX_PATH_MAX - 1);
833 wrapper->args[_POSIX_PATH_MAX - 1] = '\0';
834 wrapper->inode = finfo.inode;
835 wrapper->deviceid = finfo.device;
836 wrapper->share_group_id = *wrapper_id;
837 *wrapper_id++;
838
839 /* Add the node now */
840 apr_hash_set(config->wrapper_info_hash, extension, strlen(extension),
841 wrapper);
842
843 return NULL;
844 }

Итого, выходит, что идентификация идет по inode враппера, то есть, "число соединений до 1 сайта" это и ничто иное.


Возможно, где-то в рассуждениях я ошибся, для проверки результатов проведем тесты.

Открываем файл:
vi fcgid_spawn_ctl.c

Ищем блок:
194 /* I need max class proccess count */
195 if (current_node->process_counter >= g_max_class_process) {
196 ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, main_server,
197 "mod_fcgid: too much %s process(current:%d, max:%d), skip the sp awn request",
198 command->cgipath, current_node->process_counter,
199 g_max_class_process);
200 return 0;
201 }

И корректируем ее до вида (добавляем дамп inode при ошибке spawn нового процесса):
194 /* I need max class proccess count */
195 if (current_node->process_counter >= g_max_class_process) {
196 ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, main_server,
197 "mod_fcgid: too much %s process(current:%d, max:%d, inode:%d), skip the sp awn request",
198 command->cgipath, current_node->process_counter,
199 g_max_class_process, command->inode);
200 return 0;
201 }


И пересобираем mod_fcgid ( debuild -us -uc ) и пробуем вызвать ошибку.

Получаем ошибку в логе:
[Sat Oct 30 04:07:40 2010] [notice] mod_fcgid: too much /var/www/yyyy/data/www/forum.xxxx.ru/xxxxx.php process(current:2, max:2, inode:85541102), skip the spawn request

Теперь смотрим inode враппера:
ls -i /var/www/yyyy/data/php-bin/php
85541102 /var/www/yyyy/data/php-bin/php

Таким образом, исследование кода было проведено верно (но, к слову, не в полной мере, вариант отсутствия wrapper, если он, конечно, возможен, не рассмотрен) :)

Заключение

Параметр конфига, указанный в самом начале поста, регулирует число рабочих процессов для всех сайтов, которые используют один wrapper.


Вот пример такой конфигурации:
[Sat Oct 30 04:07:51 2010] [notice] mod_fcgid: too much /var/www/v001001/data/www/xxxxx.ru/index.php process(current:2, max:2, inode:85541102), skip the spawn request
[Sat Oct 30 04:07:51 2010] [notice] mod_fcgid: too much /var/www/v001001/data/www/yyyyyy.com/index.php process(current:2, max:2, inode:85541102), skip the spawn request

6 комментариев :

  1. Так это как раз для fastcgi - mod_fcgid.

    ОтветитьУдалить
  2. а как можно етим управлять с ISPManager? Или только вручную?

    При создании реселлера есть возможность виставлять количество процессов, интересно, ето тоже самое?

    ОтветитьУдалить
  3. Не, из ИСП невозможо. Там немного другой лимит и я, честно говоря, не знаю, работат ли он. Тот, что я писал выше - глобальный для всей машины.

    ОтветитьУдалить
  4. это ограничение на один аккаунт? т.е. будет только 2-ва процессы одного пользователя?

    ОтветитьУдалить

Примечание. Отправлять комментарии могут только участники этого блога.