3 November 2009

Ошибки конфигурирования nginx (или как правильно писать рерайты)

Nginx
Привет, хабралюди!

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

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


Ошибка номер 1, самая фатальная.


Огромное количество раз упоминалась в рассылке. А именно — использование if на уровне location.
Проблема в том, что if в location устроены не так, как мы представляем. Мы думаем, что приходит запрос, проверяется условие, если оно истинно — делаются поправки к конфигурации. А на деле все совсем иначе. При запуске nginx генерирует отдельные конфигурации location'ов для истинных и ложных условий в if. Несколько жутких примеров:
location /
{
    set $true1 1; set $true2 1;
    if($true1) { proxy_pass http://127.0.0.1:8080; }
    if($true2) { set $expr 123; }
}

Segmentation fault в рабочем процессе при попытке найти upstream для proxy_pass'a из первого if'a. А все дело в том, что он не унаследовался в location, где оба условия правильные.

location /
{
    set $true 1;
    try_files /maintenance.html $uri @fallback;
    if($true) { set $expr 123; }
}
location @fallback
{
    proxy_pass 127.0.0.1:8080;
}

Полное игнорирование try_files. Просто его нет в location, который получился при истинности выражения.

Под «set $expr 123;» можно понимать почти любое выражение в if, не задающее обработчика для запроса — все set, limit_rate и прочее.

Однако, в одном случае использовать if в location все-таки можно — если сразу же после if'a мы уйдем из этого location. Сделать это можно двумя способами:
1) Через rewrite… last;
location /
{
    try_files /maintenance.html $uri @fallback;
    if($cookie_uid = '1') { rewrite ^ /user/panel last; }
}
location /user
{
    proxy_pass 127.0.0.1:8080;
}

2) Через return ...;
location /
<c
    try_files /maintenance.html $uri @fallback;
    if($cookie_bot = '1') { return 403; }
}

При этом мы можем через return как окончить обработку запроса, так и перейти в другой location, через error_page.
Кстати, if на уровне server действует именно так, как мы ожидаем. При его использовании глобальных проблем возникать не должно.

Ошибки номер 2, менее фатальные.


После apache и его htaccess с RewriteRule и RewriteCond очень хочется запихнуть все в один location с if'ами, которые будут уводить обработку в другое место. Но это некрасиво, неправильно и неэффективно.
Ошибка номер 2.1, об if (-e ...)

Специально для того, чтобы красиво записывать такие рерайты придумана специальная директива — try_files. В самом простом варианте ее обычно записывают так:
try_files $uri $uri/ @fallback
что обозначает:
1. Проверить, существует ли запрошенный файл. Если да — отдать его, если нет — идти далее.
2. Проверить, существует ли директория с запрошенным именем. Если да — отдать ее, если нет — идти далее.
3. Передать запрос на обработку в именованный location @fallback.

Внимание! В @fallback при использовании fastcgi не стоит делать rewrite. Достаточно лишь написать fastcgi_parm с требуемым скриптом. Таким образом, конструкция вроде
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php

Превращается в
root /path/to/root;
location / { try_files $uri $uri/ @fallback; }
location @fallback
{
    fastcgi_pass backend;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root/index.php; # тут, кстати, злобный хабрапарсер съел слово S_C_R_I_P_T, будьте аккуратнее - регистр важен!
}

В случае использования как бекенда не FastCGI, а HTTP, можно написать это так:
root /path/to/root;
location / { try_files $uri $uri/ /index.php; }

Нужно понимать, что все аргументы try_files, кроме последнего, будут восприниматься как простые файлы для отдачи, а последний аргумент — уже как новая цель (то есть, туда можно написать обрабатывающий uri или именованый location). Поэтому, попытка написать try_files $uri $uri/ /index_new.php /index_old.php не приведет ни к чему хорошему — вместо выполнения index_new.php будет отдано его содержимое.

С помощью try_files можно делать еще одну удивительную вещь — не редактируя конфигов, закрыть весь сайт на техобслуживание. Это делается прописыванием try_files /maintenance.html $uri $uri/ @fallback; и затем простым созданием/перемещением файла /maintenance.html с сообщением о технических работах. Это не даст ожидаемого спада производительности в связи с использованием open_file_cache, который так же кеширует и неудачные попытки открытия файла.

Ошибка номер 2.2, об if ($uri ~ )

Итак, есть достаточно простое правило — если в Вашей конфигурации есть строка, начинающаяся на «if ($uri ~», то nginx сконфигурирован неправильно. Вся логика проверки uri на что-то должна быть реализована через location, которые теперь поддерживают и выделения.
RewriteCond %{REQUEST_URI} ^/rss[0-9]{1-6}
RewriteRule ^(.*)$ rss.php

переходит в
location ~ /rss[0-9]{1-6}
{
    fastcgi_pass backend;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root/rss.php; # тут, кстати, злобный хабрапарсер съел слово S_C_R_I_P_T, будьте аккуратнее - регистр важен!
}


Ошибка номер 2.3, об if ($host ~ )

Все, сказанное для if ($uri ~ ) так же верно для if ($host ~ ). Для использования этих правил просто создайте server с server_name в виде регэкспа!

Не ошибка, но грабли с выделениями

Если Вы отважились переписать конфиг с использованием выделений в server/location то непременно наткнетесь на небольшую проблему. Она заключается в том, что при входе в location с регекспом затираются серверные выделения, при входе в if или при использовании rewrite затираются выделения location.Увы, я не слишком сильно знаком с рерайтами apache чтобы предоставить код, поэтому опишу требуемое словами.
Требуется все запросы вида abc.mysite.com/xyz.php перенаправлять на mysite.com/abc/index.php?p=xyz
Для того, чтобы это работало, приходится писать вот так:
server
{
    server_name ~ ^(.*)\.mysite\.com$;
    set $subdom $1;
    location ~ \/(.*)\.php 
    {
        set $script $1;
        rewrite ^ http://mysite.com/$subdom/index.php?p=$script;
    }
}

В следующих версиях nginx планируется для выделений на каждом уровне сделать отдельные префиксы (например, вышеописанный рерайт можно будет описать как
rewrite ^ http://mysite.com/$sc_1/index.php?p=$lc_1;
), но пока этого нет, так что приходится писать так.

Еще неплохой практикой является давание переменным осмысленных имен :-).

Заключение


Спасибо всем, кто дочитал пост до конца. Он получился достаточно длинным, но я хотел описать все максимально чисто и понятно.
Разумеется, существуют и другие ошибки при конфигурировании — буферы, пути, но это — забота системного администратора, а не вебмастера.

Использованные материалы:
Русский список рассылки nginx
Документация nginx
Tags:nginxrewriteifsegfaulttry_fiiles
Hubs: Nginx
+84
87.8k 387
Comments 56