Posterous theme by Cory Watilo

Filed under: apache

Monitoring apache with GOD

“Посчастливилось” мне заполучить VPS с сайтом на битриксе. Из-за Zend Optimizer (сорсы закриптованы) нет возможности установить APC. Вроде бы xcache умеет работать вместе с ZO, еще не проверял. Периодически апач съедает всю память, свап, и машинка умирает окончательно. Хуже то, что апач работает с mpm-prefork, то есть он порождает не потоки, а процессы. И при мониторинге виден родительский процесс, который занимает всего 13М памяти.

Вначале для рестарта апача в период дикого роста я пытался использовать monit, но результат был неадекватный – при указании memory > 90% он убивал его когда памяти была занята всего треть.

После monit я попробовал использовать god. Но он мониторит только один процесс, без его чайлдов.

Вроде бы есть проект bluepill, который идейно растет из god. Но примеры найти оказалось проблематично, да и в процессе решил таки остаться с god.

Среди условий у god была обнаружена фича – :lambda. После недолгого размышления на bash была написана строка, показывающая занятый апачем процент памяти:

ps -e -o pmem,cmd | grep apache2 | grep -v grep | awk '{sum += $1;}END{print sum;}'

что сразу переродилось в

w.restart_if do |restart|
  restart.condition(:lambda) do |c|
    c.lambda = lambda{`ps -e -o pmem,cmd | grep apache2 | grep -v grep | awk '{sum += \$1;}END{print sum;}'`.to_i > 90}
  end
end

Итого конфиг для апача выглядит так:

God::Contacts::Email.delivery_method = :sendmail

God.contact(:email) do |c|
  c.name = 'maintainer'
  c.email = 'pager@sms.gate.isp'
end


God::Contacts::Email.format = lambda do |name, email, message, time, priority, category, host|
  <<-EOF
From: god
To: #{name} <#{email}>
Subject: Alert!
Date: #{Time.now.httpdate}
Message-Id: < #{rand(1000000000).to_s(36)}.#{$$}.#{self.message_settings[:from]}>

#{host} (#{priority}): #{message}
  EOF
end


%w{80}.each do |port|
  God.watch do |w|
    w.name = "apache2"
    w.pid_file = "/var/run/apache2.pid"
    w.interval = 10.seconds # default
    w.start = "/etc/init.d/apache2 start"
    w.stop = "/etc/init.d/apache2 stop"
    w.restart = "/etc/init.d/apache2 restart"
    w.start_grace = 10.seconds
    w.restart_grace = 20.seconds

    w.start_if do |start|
      start.condition(:process_running) do |c|
          c.interval = 5.seconds
          c.running = false
      end
    end
    w.restart_if do |restart|
      restart.condition(:lambda) do |c|
        c.lambda = lambda{`ps -e -o pmem,cmd | grep apache2 | grep -v grep | awk '{sum += \$1;}END{print sum;}'`.to_i > 90}
        c.notify = 'maintainer'
      end
    end
  end
end

LAMP through fcgid with suexec

Взглянем на Apache+mod_php. Плюсы:

  • настраивается максимально просто
  • интерпретатор стартует вместе с каждым форком апача

Минусы:

  • mpm_prefork далеко не самый быстрый
  • все работает под одним пользователем (да-да, можно накрутить mod_itk)

В попытках сделать секьюрно и по возможности быстро я решил скрутить apache (mpm_worker) + mod_fcgid + suexec. Сам по себе CGI очень небыстр за счет того, что при каждом запросе подымается интерпретатор. FastCGI быстрее, так как интерпретатор держится отдельным процессом. А mod_fcgid – модуль, бинарно совместимый с mod_fastcgi, с новой стратегией управления процессами.

Suexec в свою очередь позволяет выполнять CGI/FastCGI/SSI с указанными uid/gid. Да-да, в системе будут заводиться реальные пользователи.

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

Приступим.

apt-get install apache2-mpm-worker apache2-suexec ache2-threaded-dev libapache2-mod-fcgid php5-cgi

Теперь когда у нас все есть, создадим скелет для будущих площадок.

mkdir -p /root/web/skel
cd /root/web/skel
mkdir {cgi-bin,etc,logs,tmp,www}
chmod 770 tmp
chmod 751 {etc,logs,www}
cp /etc/php5/cgi/php.ini ./etc

Правим php.ini на предмет вывода ошибок и прочих мелких твиков.

Теперь сделаем враппер для самого обработчика (cgi-bin/php-cgi):

#!/bin/bash

cd $CGI_BIN_DIR
PHP_INI=../etc/php.ini

if [ ! -f $PHP_INI ]; then
    PHP_INI=/etc/php5/cgi/php.ini
fi

exec /usr/bin/php5-cgi -c $PHP_INI

И делаем

chown -R 755 cgi-bin
cd /root/web
touch {adduser,awstats,vhost}.skel
touch add_site.sh && chmod +x add_site.sh

adduser.skel:

DSHELL=/bin/false
DHOME=/var/www
GROUPHOMES=no
LETTERHOMES=no
SKEL=/root/web/skel
FIRST_SYSTEM_UID=2000
LAST_SYSTEM_UID=2999
FIRST_SYSTEM_GID=2000
LAST_SYSTEM_GID=2999
FIRST_UID=2000
LAST_UID=29999
FIRST_GID=2000
LAST_GID=2999
USERGROUPS=yes
USERS_GID=100
DIR_MODE=0751
SETGID_HOME=no
QUOTAUSER=""
SKEL_IGNORE_REGEX="dpkg-(old|new|dist)"

awstats.skel:

LogFile="/var/www/#USER/logs/#SITE-access.log"
SiteDomain="#SITE"
HostAliases="localhost 127.0.0.1 REGEX[#SITE$]"
Include "/etc/awstats/awstats.conf.local"

vhost.skel:

<virtualhost *>
    ServerName #SITE
    ServerAlias www.#SITE
    DocumentRoot /var/www/#USER/www/#SITE/public_html

    SuexecUserGroup #USER #USER

    ScriptAlias /cgi-bin/ /var/www/#USER/cgi-bin/
    <directory /var/www/#USER/www/#SITE/public_html>
    Options -Indexes +ExecCGI
    AllowOverride All
    AddHandler fcgid-script .php
    FCGIWrapper /var/www/#USER/cgi-bin/php-cgi .php
    Order allow,deny
    Allow from all
    </directory>

    ErrorLog /var/www/#USER/logs/#SITE-error.log
    CustomLog /var/www/#USER/logs/#SITE-access.log combined

    SetEnv AWSTATS_FORCE_CONFIG #SITE
    <location /cgi-bin/awstats.pl >
    AuthUserFile /var/www/#USER/etc/awstats.passwd
    AuthName "Website stats for #SITE"
    AuthType Basic
    require valid-user
    </location>
</virtualhost>

add_site.sh:

#!/bin/bash

if [ -z $1 ] || [ -z $2 ]; then
    echo "Oops. Some param not given."
    exit 1
fi

# If no such user exists - add one right now
if [ ! -d /var/www/$2 ]; then
    adduser --conf ./adduser.skel --disabled-login --gecos '' $2 || exit 1
fi

# Buld generic folders
mkdir -p /var/www/$2/www/$1/public_html
chown -R $2:$2 /var/www/$2/www/$1

# Build generic vhost for apache
cat vhost.skel | sed "s/#USER/${2}/g;s/#SITE/${1}/g" > ${2}_${1}

# Activate vhost
a2ensite ${2}_${1}

# Build generic awstats config
cat awstats.skel | sed "s/#USER/${2}/g;s/#SITE/${1}/g" >> /etc/awstats/awstats.${1}.conf

echo "Restart apache..."
apache2ctl configtest && apache2ctl restart

Вот таким нехитрым образом можно добавить сайт:

cd /root/web
./add_site example.org web_example

В результате будет создан пользователь web_example и у него сайт – example.org.

Tip

Иногда надо позволить скрипту выполяться боее 30 секунд. Думаете для этого достаточно подправить php.ini? Нет, в таком случае fcgid отстрелит скрипт по достижении 40 секунд. Для этих случаев в конфиге vhost’а требуется задать значение IPCCommTimeout в секундах (например, 300). Есть баг – глобальное значение отчего-то не хочет применяться к vhost’у, потому надо указать его непосредственно в vhost’е.

В общем и целом – конфиги показал, идею донес (надеюсь). Enjoy.

Apache, suexec and awstats

Столкнулся сегодня с проблемой: переменные, установленные через SetEnv в апаче не видны в cgi-скриптах. Поиски произростания ног привели к suEXEC, который фильтрует набор переменных окружения. И, естественно, в списке разрешенных нет ничего про AWSTATS_FORCE_CONFIG. Проблему решил следующим образом:

mkdir ~/build_suexec && cd ~/build_suexec
wget http://svn.apache.org/repos/asf/httpd/httpd/trunk/support/suexec.{c,h}

Далее в suexec.c находим внутри большой список переменных, и добавляем туда то, что нужно:

# diff ./suexec.c.orig ./suexec.c 
77a78
>     "AWSTATS_FORCE_CONFIG=",

И собираем получившееся такой командой (при условии что установлены пакеты apache2-threaded-dev и build-essential):

gcc -DLOG_EXEC='"/var/log/apache2/suexec.log"' \
    -DAP_DOC_ROOT='"/var/www"' \
    -DAP_GID_MIN=100 \
    -DAP_HTTPD_USER='"www-data"' \
    -DAP_LOG_EXEC='"/var/log/apache2/suexec.log"' \
    -DAP_SAFE_PATH='"/usr/local/bin:/usr/bin:/bin"' \
    -DAP_UID_MIN=100 \
    -DAP_USERDIR_SUFFIX='"public_html"' \
    -I/usr/include/apr-1.0 \
    -I/usr/include/apache2 \
    -o suexec suexec.c && chmod 4755 suexec

В убунту надо будет заменить получившимся бинарником враппер /usr/lib/apache2/suexec.

PS: да, для того чтоб у пользователя работал awstats при suexec, который ставит ограничение на скрипты в /var/www, да еще и разыменовывает симлинки, достаточно в cgi-bin создать простейший враппер (awstats.pl):

#!/usr/bin/env perl

exec('/usr/lib/cgi-bin/awstats.pl');

Хотя можно (по идее!) в AP_DOC_ROOT прописать /var/www,/usr/lib/cgi-bin.

PS2: в списке разрешенных переменных окружения можно задавать множества – например, “AWSTATS_”.

PS3: посоветовал бы кто-то настраиваемую замену для злого suEXEC – у него-то все опции при компиляции задаются.

LVM Rocks

Давно заметил что в Ubuntu (server edition) во время инсталляции начали предлагать использовать LVM. Но я все не решался поставить production на него. Затем пообщался со теми кто его использовал, почитал доку - и последний год стал его использовать, так как постиг скрытую в нем мощь :) Допустим, у нас есть простенький бюджетный сервер. Мы развернули новое приложение, и его база стала расти весьма стремительно. Итого - база, веб-файлы и система живут на одном физическом диске. Был куплен диск WD Razor, и на него перенесли базу. Нагрузка диска (iostat -x -m 1) составила 1-2%. Решено перенести туда же и веб-файлы, однако решение это пришло лишь через пару дней. Так что получилось наглядно продемонстрировать возможности LVM.

Part1. Creating...

Когда поставили разор на нем создали один раздел - LVM: [cc lang="bash"] # fdisk -l Disk /dev/sdb: 74.3 GB, 74355769344 bytes 255 heads, 63 sectors/track, 9039 cylinders Units = cylinders of 16065 * 512 = 8225280 bytes Disk identifier: 0x00000000 Device Boot Start End Blocks Id System /dev/sdb1 1 9039 72605736 8e Linux LVM [/cc] Пару слов об организации LVM. Уровень 1: volume group (vg). Это наивысший уровень абстракции, объединяющий в себе logical volumes и physical volumes. Уровень 2: physical volume (pv). Это некое блочное устройство, способное хранить данные (HDD, RAID, ...) Уровень 3: logical volume (lv). Это эквивалент раздела на жестком диске. Таким образом в группу добавляются физические тома (pv), и потом во всем этом пространстве свободного места создаются разделы (lv), на которых уже создается файловая система. Итак, сначала создавался pv: [cc lang="bash"] pvcreate /dev/sdb1 [/cc] Затем vg: [cc lang="bash"] vgcreate sys_vg /dev/sdb1 [/cc] И затем на все свободное место указанного pv (/dev/sdb1) создали lv с именем var_lib_mysql: [cc lang="bash"] lvcreate -L 100%PVS -nvar_lib_mysql sys_vg /dev/sdb1 [/cc] Дело за малым: [cc lang="bash"] mkreiserfs /dev/mapper/sys_vg-var_lib_mysql mount /dev/mapper/sys_vg-var_lib_mysql /mnt /etc/init.d/mysql stop mv /var/lib/mysql/* /mnt/ mv /var/lib/mysql/.* /mnt/ umount /mnt mount /dev/mapper/sys_vg-var_lib_mysql /var/lib/mysql chown -R mysql:mysql /var/lib/mysql /etc/init.d/mysql start [/cc] Вот собственно и почти все. Последний штрих - прописать монтирование раздела в fstab, дабы это происходило при загрузке автоматом. Можно скучно сделать это через blkid, увидеть там нужный UUID (например, f6946e54-c7d6-4688-8fac-05dcb1bf9973), скопировать его, открыть /etc/fstab и вставить туда строку вида: [cc lang="bash"] UUID=f6946e54-c7d6-4688-8fac-05dcb1bf9973 /var/lib/mysql reiserfs defaults 0 2 [/cc] а можно сделать так: [cc lang="bash"] printf "\nUUID=`blkid | grep sys_vg-var_lib_mysql | sed -r 's/.*UUID="([^"]*).*/\1/i'`\t/var/lib/mysql reiserfs defaults 0 2\n" >> /etc/fstab [/cc] Да, если сделать umount /var/lib/mysql && mount /var/lib/mysql до ребута - то /dev/disk/by-uuid/f6946e54-c7d6-4688-8fac-05dcb1bf9973 (или какой там получится) там еще не будет. Для того чтоб появился до ребута надо перезапустить udev: [cc lang="bash"] /etc/init.d/udev restart [/cc]

Part2. Resizing...

Как я говорил раньше, с опозданием пришла мысль о том, что неплохо бы вынести и статические файлы на этот же винт. И сделать это совсем просто! Для этого от того lv что был создан раньше (и именуется var_lib_mysql) откусим немного места. Сначала остановим все службы (говорят, reiserfs увеличивается/уменьшается без проблем налету, но я этого пока не пробовал на себе): [cc lang="bash"] /etc/init.d/mysql stop umount /var/lib/mysql [/cc] Затем уменьшим файловую систему, а затем и lv на 20ГБ: [cc lang="bash"] resize_reiserfs -s-20G /dev/mapper/sys_vg-var_lib_mysql lvreduce -L-20G /dev/mapper/sys_vg-var_lib_mysql [/cc] На всякий случай я предпочел проверить фс на ошибки (а вдруг!): [cc lang="bash"] reiserfsck /dev/mapper/sys_vg-var_lib_mysql [/cc] Ну и возвращаем обратно MySQL: [cc lang="bash"] mount /var/lib/mysql /etc/init.d/mysql start [/cc] Теперь создадим lv для веб-файлов, и так как их намного меньше 20ГБ, я решил оставить 5ГБ про запас, никому их не присвоив. Потом можно будет налету добавить туда где закончится место. [cc lang="bash"] lvcreate -L 15G -nvar_www sys_vg mkreiserfs /dev/mapper/sys_vg-var_www [/cc] Далее - перенос файлов: [cc lang="bash"] /etc/init.d/nginx stop /etc/init.d/apache2 stop mount /dev/mapper/sys_vg-var_www /mnt mv /var/www/* /mnt/ mv /var/www/.* /mnt/ umount /mnt mount /dev/mapper/sys_vg-var_www /var/www /etc/init.d/apache2 start /etc/init.d/nginx start [/cc] И опять не забываем про fstab: [cc lang="bash"] printf "\nUUID=`blkid | grep sys_vg-var_www | sed -r 's/.*UUID="([^"]*).*/\1/i'`\t/var/lib/mysql reiserfs defaults 0 2\n" >> /etc/fstab [/cc] По материалам:
  1. http://wiki.linuxquestions.org/wiki/LVM#example
  2. http://www.tldp.org/HOWTO/LVM-HOWTO/reducelv.html
PS: после изменения размера мог измениться UUID для lv var_lib_mysql, хотя я и не уверен в этом. Но проверить не помешает. PS2: если работаете удаленно - не забывайте про screen. PS3: писалось по памяти, так что могут быть некоторые неточности. Тупой копипаст без вовлечения мыслительного процесса чреват боком. Я предупредил ;)

Server moving adventures

Второй день занимаюсь переездом содержимого одного сервера на другой. Другой - VPS под FreeBSD (привет, ДЦ Воля). В общем, это последний раз когда я до оплаты сказал что он неплох. Теперь только реальные сервера. Ну и может VDS под Linux... В общем, именно эта реализация ужасна. Меня мало интересует как и что - факт налицо.
Первая ласточка - mysql. Сообщение о нехватке памяти в логах: [cc lang="text"] 080806 13:39:10 [ERROR] /usr/local/libexec/mysqld: Out of memory (Needed 1043824 bytes) [/cc] Судя по найденому в гугле и попыткам что-либо изменить, это вылазит из-за дефолтного в i386 FreeBSD значения максимального количества памяти на процесс. И изменить его у меня не удалось. В результате куцые буфера, и веселые запросы толпятся в очереди, а MySQL уверенно пухнет. И опухает: [cc lang="text"] [root@vps ~]# ps axu | grep mysql bash: fork: Cannot allocate memory [/cc] Причем, такое поведение я уже встречал ранее. Дважды. Тогда еще и файловые дескрипторы заканчивались (привет phpbb с кучей плагинов). Но все был бы ничего, однако базы в большинстве своем живут в MyISAM, но самая тяжелая - как и полагается, в InnoDB. И вот любой запрос с джоинами на ней ложил тачку. Во время разборок с мускулем был применен киллер, найденый в темном переулке на форумах мускуля: [cc lang="php"] #!/usr/local/bin/php 10 && $row['User']!='root' ) { $sql="KILL $process_id"; mysql_query($sql); } } ?> [/cc] Теперь появилось время на мысли. mtop помог отследить, что во всем виновата одна эта БД. После запора в ней более-менее тяжелые запросы в других БД тоже застряют. Результатом был переезд этой большой базы на другой хост, и использование ее оттуда. Сервер с двухядреным оптероном и 4ГБ памяти не заметил появления балласта: запросы пролетали мгновенно. И это без попыток тюнинговать мускуль.
Далее начались проблемы с милой связкой nginx+apache. Как описано у Алексея, все заработало. Но некоторые странички отказывались показываться - браузер ругался на невозможность понять что же ему пришло. Такой ошибки я не встречал, и как оказалось никто из моего контакт-листа тоже. А получилось следующее: обожаемый ExpressionEngine (и тебе привет) пытался все отдать за-gzip-леное. Апач справедливо отдавал это как HTTP/1.1 Transfer-Encoding: chunked. Но это в ответ на запрос HTTP/1.0 от nginx! Последний нифига не понимал и результирующий фарш доставлялся браузеру. Еще бы, он не хотел это нечто отображать... Выключением опции gzip-сжатия в ExpressionEngine 1.5.3 это полечилось, однако...
... приключения с этим белым и пушистым зверьком не закончились. В форуме при публикации сообщения символы кириллицы отсутствовали. Долго я искал помощи через гугль, пока не полез в код. А в коде методом тыка нашел, что это все виноват xss_clean в версии 1.5.3. Заменив строки с [cc lang="php"] ... $str = preg_replace('#(&\#*\w+)[\x00-\x20]+;#u',"\\1;",$str); ... $str = preg_replace('#(&\#x*)([0-9A-F]+);*#iu',"\\1\\2;",$str); ... [/cc] на [cc lang="php"] $str = preg_replace('#(&\#?[0-9a-z]+)[\x00-\x20]*;?#i', "\\1;", $str); ... $str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str); [/cc] проблему вылечил. С нетерпением жду что обнаружится дальше...

Webservers benchmark

Решил потестировать PHP в разных связках, а именно - Apache + mod_php, Apache + mod_fcgid + php, Lighttpd + mod_fastcgi + php. Все это еще в двух вариантах - с APC (Advanced PHP Cache) и без него. Тестировал выполнением вот такой команды: [cc lang="bash"] ab -c 5 -n 500 http://dmitry.shaposhnik.name/ [/cc] Выполнял команду на другом сервере чтобы снизить влияние случайных факторов. И вот что получилось в результате:
Вот полный вывод в текстовом виде: testing results UPD: вот на том же сервере решил протестировать приложение-блогодвижек (Записки айтишника) на рельсах той же командой.

Read the rest of this post »

apache restart bug

Проблема: вызываем [cc lang="bash"] /etc/init.d/apache2 restart [/cc] и этот зверь ругается, что порт залочен и не может открыть логи. Причина: когда вызывается apache_stop, то он на самом деле вызывает apache2ctl graceful-stop. Это не убивает апач сразу, а дает ему некоторое время чтоб умереть самому... Но на нагруженом сервере это и есть проблема - он умирает очень долго. Решение: в /etc/init.d/apache2 найти и заменить [cc lang="bash"] $APACHE2CTL graceful-stop [/cc] на [cc lang="bash"] $APACHE2CTL stop [/cc] Тогда апач умирает мгновенно. Также есть вариант в рестарте sleep вместо 10 заменить на 15, но мне не помогло. Ссылка

PHP opcode cachers review

"Папа обещал - папа сходил"

Обещался я сделать сравнение разных средств для ускорения работы скриптов. Если кто будет говорить о неточности или неправильности метода тестирования - говорите как лучше, сделаем лучше. Итак, основным камнем преткновения для проведения тестирования послужило использование Zend Framework. Он использует чрезмерно много загрузок разных файлов, что заметно снижает скорость работы. Тестовый стенд - ноутбук Asus A6Tc, OS: Linux Ubuntu "Gutsy", Lighttpd-1.4.17, PHP 5.2.3 через FastCGI. Тестовое приложение основано на Zend Framework, использует подключение к БД (но не делает выборок), а для View использован Smarty. Тестировал Pure PHP, Zend Optimizer 3.3.0, XCache 1.2.1, APC 3.0.14, eAccelerator 0.9.5.2. Тестировал тривиально и просто - замерял время запуска и время окончания внутри скрипта, в конце вывода смарти эхал полученое время. И копипастил его (все действия для чистоты эксперимента проводились с другой машинки) в OO SpreadSheets. Вот результаты (время в мс):

Pure PHP ZO XCache APC eAccelerator
247.8 272.92 573.43 397.9 156.2
251.04 271.81 106.81 89.75 74.16
245.14 286.8 108.96 88.38 110.42
252.72 267.1 91.01 89.2 534.97
207.39 221.43 65.54 47.49 69.55
209.5 221.34 65.89 53.31 71.01
217.66 232.13 70.04 47.66 70.32
202.88 222.59 66.1 48.04 70.26
203.17 220.79 69.94 54.19 70.76
204.03 235.28 65.76 48.49 70.05
И вот по этому добру график:
С XCache был замечен баг: если дважды быстро обновить страницу то контент вылазил полтора-два-три раза. Но я думаю это из-за перегенерации шаблона в Smarty. Однако неприятный осадок остался. Жду комментариев =)

Apache & mod_ssl - certificate generation

Я безумно благодарен pag'уза наводку на эту статью. Чтобы она не затерялась в недрах интернета (при поиске я ее не видел) - я хочу опубликовать ее здесь. Я надеюсь, автор не воспримет это как проявление плагиата.

Read the rest of this post »