25 Май 2017

Высокопроизводительный веб-сервер H2O

High performance web server H2O

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

Не является и исключением и один из самых широко распространённых сетевых протоколов - HTTP, являющейся базой для функционирования Всемирной паутины. Появившись на свет в 1992 году, за прошедшее HTTP претерпел множество усовершенствований и измений включая и его вторую версию — HTTP/2, переход на которую в настоящее время набирает обороты. Так, по состоянию на май 2017 года HTTP/2 используется около 14% всех видимых в сети Интернет сайтов.

Если не останавливаться здесь подробно на преимуществах использования второй версии HTTP над предшествующими (с подробным описанием каковых читатель может ознакомиться самостоятельно в многочисленных статьях доступных в сети), то тезисно можно сказать, что, HTTP/2 существенно быстрее HTTP/1 и HTTP/1.1 благодаря бинарной форме передачи данных, наличию встроенного сжатия заголовков, мультиплексированию соединений, а также возможности приоретизировать передачу данных добиваясь, вследствие всего этого, существенно более быстрой отрисовки сайтов у пользователей.

Будучи клиент-серверным протоколом, все версии HTTP предполагают наличие своей поддержки как в браузерах, так и в веб-серверах. И если говоря о HTTP/2 можно смело заявить о его поддержке в той или иной степени во всех популярных браузерах, то ситуация с серверной частью пока не столь оптимистичная.

HTTP/2 browser support

Несмотря на наличие поддержки HTTP/2 в трёх популярных веб-серверах Apache, Nginx и IIS, доля которых на март 2017 года в совокупности составляет свыше 80%, реализация данного протокола в них является неполной. Кроме того, к данным веб-серверам есть существенные вопросы по части производительности при отдаче данных по второй версии HTTP протокола.

1. Почему H2O?

В связи с вышеизложенным автор обратил внимание на менее распространённые и, следовательно, существенно менее известные веб-серверы с поддержкой HTTP/2, одним из которых и явился высокопроизводительный HTTP сервер H2O.

Согласно исследованиям, H2O обладает лучшей на сегодня поддержкой заложенных стандартом (см. RFC7540 черновик 16 редакции) особенностей HTTP/2 при лучшей производительности. Последнее особенно заметно при использовании при обмене данных криптографических протоколов TLS, которые, по факту, являются неотъемлемой частью в обмене данными по HTTP/2. Также H2O отличают:

  • полная настраиваемая поддержка приоритизации трафика, включая кэширование выдаваемых методом push ресурсов;
  • продвинутая поддержка TLS и организация работы с секретными ключами на стороне сервера;
  • поддержка FastCGI и проксирования;
  • наличие встроенной поддержки скриптинга на Ruby;
  • и, наконец, возможность обновления конфигурации и перезапуска сервера без остановки обработки существующих соединений.

Кроме того, несмотря на то, что первый выпуск веб-сервера H2O появился только в 2014 году, проект весьма активно развивается, вбирая в себя новый и улучшая существующий функционал.

2. Базовая настройка

Рассмотрим базовую настройку текущей актуальной версии веб-сервера H2O для типовой конфигурации в среде FreeBSD.

root@beta:~ # uname -v
FreeBSD 11.0-RELEASE-p9 #0: Tue Apr 11 08:48:40 UTC 2017     root@amd64-builder.daemonology.net:/usr/obj/usr/src/sys/GENERIC
root@beta:~ # h2o -v
h2o version 2.2.2
OpenSSL: OpenSSL 1.0.2k-freebsd  26 Jan 2017
mruby: YES

Сам сервер можно установить как из пакетов, так и из портов. В последнем случае автор не рекомендует отключать опцию встроенной поддержки скриптинга на Ruby (mruby). Из дальнейшего изложения станет ясной основания для данной рекомендации.

Основной файл конфигурации H2O соответствует стандарту YAML 1.1 и довольно легко читаем.

root@beta:/usr/local/etc/h2o # cat h2o.conf
# see https://h2o.examp1e.net/ for detailed documentation
# see h2o --help for command-line options and settings

# v.20170524 (c)2017 by Max Kostikov http://kostikov.co e-mail: max@kostikov.co

user: www
pid-file: /var/run/h2o.pid
access-log: /var/log/h2o/h2o-access.log
error-log: /var/log/h2o/h2o-error.log

expires: off
file.dirlisting: off
file.send-compressed: on

file.index: [ 'index.html', 'index.php' ]

file.custom-handler:
    extension: ".php"
    fastcgi.connect:
        host: 127.0.0.1
        port: 9000
        type: tcp

listen:
    port: 80
listen:
    port: 443
    ssl: &default_ssl
        cipher-suite: ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
        cipher-preference: server
        dh-file: /etc/ssl/dhparams.pem
        certificate-file: /usr/local/etc/letsencrypt/live/my.server/fullchain.pem
        key-file: /usr/local/etc/letsencrypt/live/my.server/privkey.pem

hosts:
# --- my.sever
    "foo.my.server:80":
        listen:
            port: 80
        paths:
            "/":
                redirect:
                    status: 301
                    url: https://foo.my.server/
    "foo.my.server:443":
        <<: !file /usr/local/etc/h2o/hdr/foo.my.server.h2o
        listen:
            port: 443
            ssl: *default_ssl
        paths:
            "/":
                mruby.handler: |
                    require "htpasswd.rb"
                    acl {
                        deny { ! path.start_with?("/amavis", "/dbadmin", "/stats") }
                        use Htpasswd.new("/usr/local/etc/h2o/.htpasswd", "admin")
                    }
                file.dir: /usr/local/www

    "my.server":
        paths:
            "/":
                mruby.handler: |
                    Proc.new do |env|
                        headers = {}
                        File.open("/usr/local/etc/h2o/hdr/my.server.h2o") do |file|
                            file.each_line do |line|
                                data, value = line.chomp.downcase[/'(.*)'/, 1].split(': ', 2)
                                next if env["SERVER_PORT"] == "80" && ["strict-transport-security", "public-key-pins"].include?(data)
                                headers[data] = value
                            end
                        end
                        [399, headers, []]
                    end
                file.dir: /usr/local/www/my.server

Для корректной настройки важно понимать, что файл конфигурации имеет четыре уровня — глобальный, хоста, пути и расширений, в зависимости от которых действуют (или не действуют) конфигурационные опции H2O.

Глобальные параметры в приведённом выше файле настроек h2o.conf по большей части очевидны и поняты без комментариев. Обращу лишь внимание на параметр file.custom-handler который определяет внешний обработчик для файлов PHP через обращение к сервису php-fpm посредством протокола FastCGI. Директивы listen сообщают на каких портах сервера будет работать H2O по умолчанию. При желании можно задать конкретные IP-адреса для привязки через параметр host. Далее в параметре ssl перечислены поддерживаемые методы шифрования, порядок их предпочтения и пути к файлам с сертификатами (подробнее о списке шифров см. статью "Усиливаем HTTPS на web-сервере").

Отдельное внимание обращаю на строку &default_ssl там же. Ею задаётся псевдоним всей следующей на более низком уровне настройки шифров и путей. В дальшейшем при конфигурировании виртуальных хостов можно будет обратиться ко всем эти значением простой отсылкой по имени псевдонима без необходимости повторного перечисления тех же значений — см. как это используется в случае с описанием хоста с именем foo.my.server:443. Если же, к примеру, для того же хоста имеется необходимость в изменении параметров скрывающихся за данным псевдонимом, то можно вспользоваться следующей формой обращения к нему.

...
    "foo.my.server:443":
        <<: !file /usr/local/etc/h2o/hdr/foo.my.server.h2o
        listen:
            port: 443
            ssl:
                 <<: *default_ssl
                 certificate-file: /usr/local/etc/letsencrypt/live/foo.my.server/fullchain.pem
                 key-file: /usr/local/etc/letsencrypt/live/foo.my.server/privkey.pem
...

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

Попутно заметьте, что в том же хосте foo.my.server:443 при помощи директивы <<: !file используется включение части конфигурационных настроек из внешнего файла /usr/local/etc/h2o/hdr/foo.my.server.h2o. Это также позволяет вынести типовые и / или повторяющиеся конструкции во внешние файлы включая по мере необходимости просто ссылки на них. В частности, в вышеупомянутом файле содержатся дополнительные заголовки безопасности для протокола HTTPS подробнее о которых уже писалось в этом блоге (см. статью "Заголовки HTTP и безопасность web-сервера").

root@beta:/usr/local/etc/h2o # cat hdr/foo.my.server.h2o
header.add: 'Public-Key-Pins: pin-sha256="ZAFB1O+V8DLlZid0V2ceHxA68dxuGUg+apA0k5VCsQw="; pin-sha256="FOFoPdvQnMgX485K10cTf7i0bsSWCvCW3hWgIoWuUW4="; max-age=3456000; includeSubDomains'
header.add: 'Strict-Transport-Security: max-age=15768000; includeSubDomains; preload'

Обратите внимание, что в описании того же хоста foo.my.server:80 чуть выше при обращении к нему по протоколу HTTP данные заголовки не добавляются ввиду того, что в отрыве от TLS они не имеют смысла. Там же производится переадресация на защищённое соединение с HTTP кодом 301 Moved Permanently.

Используемый для этого параметр redirect работает исключительно на уровне путей, поэтому в H2O отсутствует возможность, к примеру, задать глобальную переадресацию с HTTP на HTTPS для всех виртуальных доменов. Именно для этого и применяется раздельная запись для одного и того же хоста при обращении по разным портам (и соответствующих им протоколам). Это, как и в случае с необходимостью использовать различные заголовки, может показаться неудобным в сравнении, к примеру, с конфигурацией веб-сервера Lighttpd, который регулярно упоминается на страницах данного блога, однако тут на помощь приходит встроенная реализация mruby.

3. Тонкости с mruby

Наличие встроенного скриптинга на mruby помогает элегантно решить как вышеупомянутые ограничения, так и многие другие задачи. К примеру, на нём легко реализуется от сколь угодно сложной логики перезаписи путей вплоть до полноценного бэкэнда средствами исключительно самого H2O!

Небольшая демонстрация этой мощи приведена в описании хоста my.server. Функция следующая после оператора mruby.handler отвечает на запросы клиентов как по протоколу HTTP, так и по HTTPS без отдельного описания хоста для каждого из них. Однако, в зависимости от протокола может формировать различный набор заголовков, отдавая их полный набор, перечисленный в файле /usr/local/etc/h2o/hdr/my.server.h2o для данного хоста для HTTPS и исключая заголовки не имеющие смысла совместно с HTTP. Таким образом можно сформировать единый файл заголовков для обоих режимов передачи данных шифрованного и не шифрованного упростив запись конфигурации.

root@beta:/usr/local/etc/h2o # cat hdr/my.server.h2o
header.add: 'X-Content-Type-Options: nosniff'
header.add: 'X-XSS-Protection: 1; mode=block'
header.add: 'X-Frame-Options: DENY'
header.add: 'Public-Key-Pins: pin-sha256="ZAFB1O+V8DLlZid0V2ceHxA68dxuGUg+apA0k5VCsQw="; pin-sha256="FOFoPdvQnMgX485K10cTf7i0bsSWCvCW3hWgIoWuUW4="; max-age=3456000; includeSubDomains'
header.add: 'Strict-Transport-Security: max-age=15768000; includeSubDomains; preload'

Пронаблюдаем как это работает.

root@beta:/usr/local/etc/h2o # curl -I http://my.server
HTTP/1.1 200 OK
Date: Thu, 25 May 2017 17:11:35 GMT
Connection: keep-alive
Content-Length: 2552
Server: h2o/2.2.2
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
x-frame-options: deny
content-type: text/html
last-modified: Sun, 14 May 2017 16:04:18 GMT
etag: "59188002-9f8"
vary: accept-encoding
accept-ranges: bytes

root@beta:/usr/local/etc/h2o # curl -I https://my.server
HTTP/2 200
server: h2o/2.2.2
date: Thu, 25 May 2017 17:11:41 GMT
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
x-frame-options: deny
public-key-pins: pin-sha256="zafb1o+v8dllzid0v2cehxa68dxugug+apa0k5vcsqw="; pin-sha256="fofopdvqnmgx485k10ctf7i0bsswcvcw3hwgiowuuw4="; max-age=3456000; includesubdomains
strict-transport-security: max-age=15768000; includesubdomains; preload
content-type: text/html
last-modified: Sun, 14 May 2017 16:04:18 GMT
etag: "59188002-9f8"
vary: accept-encoding
accept-ranges: bytes
content-length: 2552

Небольшой комментарий к самому коду на Ruby. Используемая функция получает доступ к переменным среды HTTP окружения, получает список уже существующих заголовков, открывает файл с заголовками и разбирает его построчно. Из строк выделяются имена и значения добавляемых заголовков и в зависимости от режима подключения принимается решение добавлять их в ответ или нет. Далее заголовки отправляются клиенту.

Другим примером гибкости в работе со встроенным в H2O скриптингом на mruby может явится реализация механизма базовой HTTP-аутентификации и правил контроля доступа (ACL). Рассмотрим их вновь вернувшись к настройке хоста foo.my.server:443.

Данный хост используется для управления и мониторинга сервером через веб-интерфейс. Различные управляющие страницы доступны при обращении через URL вида https://foo.my.server/ и могут принимать значения перечисленные в методе deny, который блокирует доступ к адресам, заканчивающимся на отличные от них. Если путь правильный, то управление передаётся функции аутентификации из подгруженного чуть выше модуля командой require, которой на вход передаётя путь к файлу .htpasswd и имя области.

Совершенно идентичную функциональность можно получить и на "чистом" mruby не используя механизм ACL.

...
                mruby.handler: |
                    require "htpasswd.rb"
                    lambda do |env|
                        if env["PATH_INFO"].start_with?("/amavis", "/dbadmin", "/stats")
                            Htpasswd.new("/usr/local/etc/h2o/.htpasswd", "admin")
                            return [399, {}, []]
                        end
                        return [403, {'content-type' => 'text/plain'}, ["Forbidden"]]
                    end
...

Как видно, запись посредством ACL существенно короче.

На этом разрешите завершить небольшое вводное описание возможностей и некоторых моментов в настройке высокопроизводительного веб-сервера H2O. Надеюсь, в дальнейшем продолжить эту тему применительно к конкретным задачам по его развёртыванию для различных практических приложений.

4. PROFIT!

Статья была полезной? Тогда прошу не стесняться и поддерживать деньгами через PayPal или Яндекс.Деньги.


dev  H2O  Ruby  HTTP  SSL