2009-05-24

Введение в nginx, часть 2: Другие возможности

Данная статья была опубликована в электронном приложением к журналу "Системный администратор"- "Open Source #042"

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

Failover и балансировка

Крупные проекты редко состоят из одного сервера приложений. Часто их два или больше, и возникает задача балансировки клиентов по этим серверам, а также выполнения failover — необходимо чтобы выход из строя одного из серверов не был заметен для клиентов.
Простейший способ рещить эту задачу — dns round-robin, т.е. назначение доменному имени нескольких ip-адресов. Но это решение имеет ряд недостатков, и гораздо лучше выглядит решение балансировки запросов по бакендам на фронтенде nginx. В конфигурационном файле выглядит это примерно так:

# объявляем upstream — список бакендов
upstream  backend  {
  # перечисляем dns-имена или ip-адреса серверов и их «вес»
  server   web1                 weight=5;
  server   1.2.3.4:8080         weight=5;
  # а так можно подключаться к бакенду через unix-сокет
  server   unix:/tmp/backend3   weight=1;
}
# конфигурация виртуального сервера
server {
  listen <...>;
  server_name myserver.com;
  # отправляем все запросы из локейшена / в апстрим
  location / {
    proxy_pass  http://backend;
  }
}

Запросы, приходящие к nginx, распределяются по бакендам соответственно указаному весу. Кроме того, можно сделать так, чтобы запросы с одних и тех же IP-адресов отправлялись на одни и те же серверы (для этого в upstream нужно указать директиву ip_hash). Так можно решить проблему с сессиями, но все же лучше найти какой-нибудь способ их репликации или (что еще лучше) использовать RESTful-подход.
В случае, если один из серверов откажется принимать соединения или соединение к нему отвалится по таймауту, он на некоторое время будет исключен из upstream.



Оптимизация nginx


1. Увеличение количества и объема буферов


Для хранения принятых запросов и еще не отданных ответов nginx использует буферы в памяти, а если запрос или ответ не помещается в них, nginx записывает его во временный файл (и пишет при этом предупреждение в log-файл). Поэтому необходимо установить такие размеры, чтобы в большинстве случаев не требовалось обращаться к временному файлу, а с другой стороны — чтобы буферы не использовали слишком много памяти.

Для этого используются следующие параметры:
client_body_buffer_size (по умолчанию: 8k/16k — в зависимости от архитектуры) — задает размер буфера для чтения тела запроса клиента. Обычно стандартного значения хватает, его требуется повышать, только если ваше приложение устанавливает огромные cookies.
proxy_buffer_size (по умолчанию: 4k/8k) — задает размер буфера, в который будет читаться первая часть ответа, получаемого от проксируемого сервера. В этой части ответа находится, как правило, небольшой заголовок ответа. Стандартного значения обычно хватает.
proxy_buffers (по умолчанию: 8 4k/8k) — задает число и размер буферов для одного соединения, в которые будет читаться ответ, получаемый от проксируемого сервера. Установите этот параметр так, чтобы большинство ответов от бэкенда помещалось в буферы.

2. Механизмы обработки соединений


Есть одна тонкость, касающаяся механизма обработки соединений, а именно — способ получения информации о событиях на сокетах. Существуют следующие методы:

  • select — стандартный метод. На большой нагрузке сильно нагружает процессор.
  • poll — стандартный метод. Также сильно нагружает процессор.
  • kqueue — эффективный метод, используемый в операционных системах FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0 и Mac OS X. На 2-процессорных машинах под управлением Mac OS X использование kqueue может привести к kernel panic.
  • epoll — эффективный метод, используемый в Linux 2.6+. В некоторых старых дистрибутивах есть патчи для поддержки epoll ядром 2.4.
  • rtsig — real time signals, эффективный метод, используемый в Linux 2.2.19+. При больших количествах одновременных соединений (более 1024) с ним могут быть проблемы (их можно обойти, но на мой взгляд, лучше с этим не связываться).
  • /dev/poll — эффективный метод, используемый в Solaris 7 11/99+, HP/UX 11.22+ (eventport), IRIX 6.5.15+ и Tru64 UNIX 5.1A+.

При компиляции nginx автоматически выбирается максимально эффективный найденый метод, однако скрипту configure можно насильно указать какой метод использовать. Если вы решили сделать это, то лучше использовать такие методы:
Linux 2.6: epoll;
FreeBSD: kqueue;
Solaris, HP/UX и другие: /dev/poll;
Linux 2.4 и 2.2: rtsig, не рекомендуется при больших нагрузках.



Включение gzip позволяет сжимать ответ, отправляемый клиенту, что положительно сказывается на удовлетворенности пользователя, но требует больше времени CPU. Gzip включается директивой gzip (on|off). Кроме того, стоит обратить на следующие важные директивы модуля gzip:

gzip_comp_level 1..9 — устанавливает уровень сжатия. Опытным путем выявлено, что оптимальные значения лежат в промежутке от 3 до 5, большие значения дают маленький выигрыш, но создают существенно большую нагрузку на процессор, меньшие — дают слишком маленький коэффициент сжатия.
gzip_min_length (по умолчанию, 0) — минимальный размер ответа, который будет сжиматься. Имеет смысл поставить этот параметр в 1024, чтобы слишком малеьнике файлы не сжимались (т.к. эффективность этого будет мала).
gzip_types mime-тип [mime-тип ...] - разрешает сжатие ответа методом gzip для указанных MIME-типов в дополнение к "text/html". "text/html" сжимается всегда. Имеет смысл добавить такие mime-типы как text/css, text/javascript и подобные. Разумеется, сжимать gif, jpg и прочие уже компрессированые форматы не имеет смысла.

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

location /files/ {
  gzip on;
  gzip_min_length 1024;
  gzip_types text/css text/javascript;
  gzip_comp_level 5;
  gzip_static on;
}

При использовании такой конфигурации в случае запроса «/files/test.html» nginx будет проверять наличие «/files/test.html.gz», и, если этот файл существует и дата его последнего изменения больше, чем дата последнего изменения файла test.html, будет отдан уже сжатый файл, что сохранит ресурсы процессора, которые потребовались бы для сжатия оригинального файла.

Оптимизация приложений


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

log_format  my_combined  '$remote_addr - $remote_user [$time_local] '
    '"$request" $status $body_bytes_sent '
    '"$http_referer" "$http_user_agent" '
    '$upstream_response_time "$host"'

access_log /var/log/nginx/access_log my_combined;

Переменная $upstream_response_time содержит время ответа бэкенда, поэтому в лог попадает время обработки каждого запроса бэкендом. Далее понадобятся два скрипта:

1. /usr/local/bin/url_stats_report.sh:

#!/bin/sh

echo "=== Requests which took most of the time ===" > /tmp/report.txt
echo "overall time - number of requests - average time - url" >> /tmp/report.txt

cat /var/log/nginx/*access.log | /usr/local/bin/url_stats.py >> /tmp/report.txt
cat /tmp/report.txt | mail -s "url performance report" root

2. /usr/local/bin/url_stats.py:

#!/usr/bin/env python

import sys

urls = {}

try:
    while 1:
        line = raw_input()
        line_arr = line.split(" ")
        try:
            host = line_arr[-1]
            host = host[1:]
            host = host[:-1]
            url = line_arr[6]
            t = float(line_arr[-2])
            #print host, url, t

            try:
                urls[host + url] = (urls[host + url][0] + t, urls[host + url][1] + 1)
            except KeyError, e:
                urls[host + url] = (t, 1)
        except ValueError, e:
            pass


except EOFError, e:
   pass

def sort_by_value(d):
    """ Returns the keys of dictionary d sorted by their values """
    items=d.items()
    backitems=[ [v[1],v[0]] for v in items]
    backitems.sort(reverse=True)
    return [backitems[i][1] for i in range(0,len(backitems))]

if (len(sys.argv) > 1):
    f = open(sys.argv[1], 'r')
    for k in f.readlines():
        k = k.strip()
        try:
             print urls[k][0], urls[k][1], urls[k][0] / urls[k][1], k
        except:
             print 0, 0, k
else:
    i = 0
    for k in sort_by_value(urls):
        print urls[k][0], urls[k][1], urls[k][0] / urls[k][1],  k
        i += 1
        if i > 100: break

Они не идеальны, но задачу выполняют: запуская /usr/local/bin/url_stats_report.sh (например, в postrotate утилиты logrotate), вы получаете наглядную картину, какие запросы занимают большую часть времени бэкенда.

Кеширование

Незадолго до выхода этой статьи, Игорь Сысоев выпустил версию nginx 0.7.44 с экспериментальной поддержкой кеширования. Из-за большого количества багов, за которкое время было выпущено несколько версий, и в настоящее время последняя версия — 0.7.50 уже достаточно хорошо (хотя и не идеально) работает с кешированием.
Однако это все еще экспериментальная функция, но я решил рассказать о ней, т.к. ее одень давно ждали многое администраторы, да и лично для меня она очень полезна.
Сейчас nginx умеет кешировать на диске ответы от http и fastcgi запросов на бакенды, указывать ключ для кеширования, учитывать заголовки "X-Accel-Expires", "Expires" и "Cache-Control" и вручную устанавливать максимальное время жизни объекта в кеше.
Обслуживанием кеша (очиста старых файлов, наблюдение за размером и т.п.) занимается специальный процесс cache manager. Положительной особенностью реализации является то, что при старте nginx cache manager начинает проверку кеша в фоне, благодаря чему nginx не делает то что называется «дает сквида», т.е. он не висит несколько минут проверяя кеш перед стартом.
Я намеренно не указываю пример конфигурации, т.к. во-первых директивы могут еще поменяться, а во-вторых нужно глубокое понимание механизма кеширования, что требует вдумчивого чтения документации (http://sysoev.ru/nginx/docs/http/ngx_http_proxy_module.html#proxy_cache) и архивов рассылки nginx-ru.

За кадром


В статье освещена лишь та часть возможностей nginx, которыми я пользуюсь чаще всего. За пределами рассказа остались такие вопросы, как поддержка SSI, работа с memcached, экспериментальный встроенный Perl и сторонние модули, реализующие дополнительную функциональность.