Обслуживание тысяч запросов в секунду на примере XBT Tracker

    Недавно проводили тест, результаты которого показали, что одно приложение обрабатывает 2000 запросов в секунду на скромном сервере, где это было не единственной нагрузкой. При этом результат каждого запроса записывается в 3-5 таблиц в MySQL. Честно говоря, меня такой результат удивил, поэтому решил поделиться с хабрасообществом описанием архитектуры этого приложения. Подобный подход применим от баннерных показов до чатов и микроблогов, надеюсь кому-нибудь покажется интересным.

    Во-первых, это приложение однопоточное. Всё делается одним процессом, работа с сокетами — неблокирующими epoll/select, никаких ожидающих ввода/вывода потоков (threads). С развитием HTTP, сначала появлением Keep-Alive, затем AJAX и набирающим популярность COMET, количество постоянных соединений с веб-сервером растёт, на нагруженных проектах измеряется тысячами и даже десятками тысяч, и если для каждого создавать свой поток (thread) со своим стеком и постоянно переключаться между ними — ресурсов сервера очень быстро не хватит.

    Второй ключевой момент — что один SELECT… WHERE pk in (k1, k2, ..., kN) выполняется быстрее, чем несколько SELECT… WHERE pk=… Выполняя работу с базой данных большими пачками можно уменьшить не только число запросов в секунду, но и общую нагрузку.

    Предметная область


    XBT Tracker (XBTT) — битторрент трекер. Просьба воздержаться от темы авторских прав, ибо торрент официально используется, например, для распространения дистрибутивов линукса и патчей к World of Warcraft. В отличии от ed2k и DC++ есть возможность поместить в один торрент несколько файлов, не пакуя их в архив, и в любой момент проверить целостность файла, а при необходимости восстановить его скачав битые куски.

    При скачивании клиент периодически обращается к трекеру, сообщая статистику трафика и получая адреса других раздающих и качающих. Чем чаще такие обращения, тем точнее учёт трафика (если это закрытый трекер) и тем быстрее новые участники раздачи узнают друг о друге.

    XBT Tracker, о котором этот пост, написан на си++ и используется на многих зарубежных трекерах, как открытых, так и закрытых, и даже на паре-тройке российских. Другой высокопроизводительный трекер, OpenTracker, закрытых трекеров с учётом трафика не поддерживает, поэтому ему не нужно писать результаты запросов в базу данных, поэтому в данном контексте он менее интересен.

    Неблокирующий ввод-вывод


    В 90-х годах при работе с сокетами использовался блокирующий ввод-вывод, когда при вызове методов recv и send текущий поток «зависал» до ожидания результатов. Для каждого принятого соединения создавался (fork) отдельный процесс, в котором шла обработка его запроса. Но каждый процесс требует памяти под стэк и процессорного времени на переключение контекста между процессами. На небольших нагрузках это не страшно, да и веб тогда не был интерактивным, полностью в режиме запрос-ответ, динамического контекста (CGI) было мало, в основном — счётчики посещений страницы и примитивные недо-форумы. Apache до сих пор работает таким образом. В apache2 есть возможность использование более лёгких потоков (threads) вместо процессов, но суть осталась прежней.

    В качестве альтернативы этому появился неблокирующий ввод-вывод, когда один процесс мог открыть множество сокетов, периодически опрашивать их состояние, и если появились какие-то события, например поступило новое соединение или пришли данные для чтения — обслуживать их. Именно так работает, например, nginx. В Java версии 1.4 и выше для этого есть NIO.

    В дальнейшем появились улучшения, например, TCP_DEFER_ACCEPT, позволяющее «откладывать» приём соединения до тех пор, пока по нему не пришли данные, SO_ACCEPTFILTER, откладывающий соединение пока не пришёл полноценный HTTP-запрос. Появилась возможность увеличить длину очереди непринятых соединений (по умолчанию их всего 128) при помощи sysctl kern.ipc.somaxconn в BSD и sysctl net.core.somaxconn в Linux, что особенно актуально если возникают паузы в обработке сокетов.

    Обслуживание запросов


    Запросы в XBTT очень простые, их обработка не требует особых вычислительных ресурсов, все необходимые данных он держит в памяти, поэтому нет проблем выполнять их в том же процессе, что и работу с сокетами. В случае более серьёзных задач по-прежнему требуется создавать отдельные потоки для их обслуживания.

    Один из выходов — создание пула потоков (thread pool), которым передаётся запрос для обработки, после чего поток возвращается обратно в пул. Если свободных потоков нет — запрос ждёт в очереди. Такой подход позволяет уменьшить общее число используемых потоков, и каждый раз не приходиться создавать новый и убивать после завершения обработки запроса.

    Ещё лучший механизм под названием «актёры» (actors) есть в языках эрланг (erlang) и скала (scala), возможно — в виде библиотек — и для других языков. Обработка выполняется посредством асинхронной передачи сообщений между актёрами, что можно себе представить как пересылку е-мейлов с входящим почтовым ящиком у каждого, но эта тема выходит за рамки данного поста (например, вот свежий пост об этом).

    Пакетная работа с базой данных


    Результат каждого обращения к трекеру XBTT записывает в несколько таблиц. У пользователя увеличивается его скачанный и залитый трафик. Увеличивается статистика у торрента. Заполняется таблица текущих участников раздачи. Плюс пара служебных таблиц с историей закачек.

    При традиционном способе обработки на каждый запрос к трекеру выполнялось бы как минимум 3 отдельных INSERT или UPDATE, клиент ждал бы их исполнения, таким образом серверу базы данных пришлось бы выполнять по 3 запроса на каждое обращение к трекеру.

    XBTT выполняет их не сразу, а накапливает большую пачку INSERT… VALUES (...), (...). ..., (...) ON DUPLICATE KEY UPDATE f1=VALUES(f1), ..., fN=VALUES(fN), и исполняет раз в несколько секунд. За счёт чего число запросов в базу уменьшается с нескольких на запрос к трекеру до нескольких в минуту. Также периодически он перечитывает необходимые данные, которые могли измениться извне (веб-интерфейс независим от трекера).

    Критичность откладывания записи


    В данном приложении потеря части данных совершенно не критична. Если в базу не будет записана статистика торрент-трафика за несколько секунд — ничего страшного не произойдёт. Хотя при аварийном завершении он записывает накопившиеся буфера в базу, на сервере может быть UPS на случай отключения электричества и т.п. — гарантии что все переданные клиентом данные записаны на диск нет. Для баннерной сети это тоже не страшно, но бывают задачи где сохранение всех данных критично.

    Аналогично, не во всех приложениях есть возможность хранения всех данных в памяти. Для обработки запроса клиента может потребоваться выборка данных из базы.

    Но и в таком случае блочная обработка данных возможна. Организуется конвейер (pipeline; для его реализации отлично подойдут actors) из нескольких стадий, на каждой происходит сборка группы данных для запроса, как только набралось достаточное количество (естественно, настраиваемое) или прошло некоторое время (например, 10-100 миллисекунд), за которое нужного количества не набралось, — делается групповой запрос к базе, где вместо «ключ=значение» ставится условие «ключ IN (накопленный список)».

    Если необходимо заблокировать (lock) эти записи, то к запросу можно добавить FOR UPDATE SKIP LOCKED (естественно, запись результатов нужно будет выполнять в том же соединении к базе, той же транзакции). Можно использовать Prepared Statement в тех базах данных, которые это поддерживают, чтобы один раз выполнить анализ запроса (parse), выбрать оптимальный план его выполнения, а потом каждый раз только подставлять в него данные. Чтобы уменьшить количество таких подготовленных запросов, число параметров к нему можно брать только степенями двойки: 1, 2, 4, 8, 16, 32… Также можно группировать (batch) запросы, сначала не исполняя каждый, а только добавляя в пачку, а потом выполнить все сразу.
    Поделиться публикацией

    Похожие публикации

    Комментарии 41

      +3
      XBTT вобще жостко оптимизирован на скорость и является на данный момент самым быстрым трекером из тех что мне попадались.
      Как побочный результат — достаточно запутанный код, который тяжело затачивать под свои нужды.
        +3
        +1 к оптимизированности, про запутанность кода — мне так не показалось, объектные «плюсы», большая часть логики, которую имеет смысл «затачивать» в одном методе — insert_peer. впрочем, это скорее спор о вкусе и цвете фломастеров :)
          0
          Про фломастеры это вы верно подметили :)
          Просто я что бы разобраться потратил достаточно много времени.
            +2
            если не секрет — что именно затачивали? в плане обменяться опытом и «заточками»
              +1
              Я к сожалению сыграл пассивную роль в данном проекте :), все сделал мой товарищ.
              Стояла задача заменить PHPBTTracker на XBTT, последний отказывался кушать паскеи.
              Проблема оказалась в SHA1 ф-ции, которую пришлось отключить.
              Это так, не заточка, а просто нужно было обеспечить совместимость со старой базой.
              Позже была идея добавить функционал, так как трекер у нас локальным был, нужно было добавить возможность отдавать список пиров в зависимости от айпи клиента (во внутрисеть — внутресетевые, во внешку — внешние). Вот тут то я и слил, долго думал куда бы приткнуть и так и не осилил по молодости. Сейчас может и получилось бы что, но проблема отпала, так как у многих во внутрисети появился анлим и вопрос мирового трафика уже не стоит так остро.
              Трекер жив благодаря его создателю Кравченко Ивану, за что ему респект и уважение :)
              Это, кстати, трекер сети Киевского Политехнического Института — bete.tv

            +1
            Мне вот как-то тоже код не показался сложным, хотя я под плюсы и не писал толком никогда :) Тем не менее, модифицированный мною (некоторая кастом-функциональность) xbtt работает вот уже более полугода без рестарта… :) Я и проект-то бросил давно :)
              0
              Тут скорее у меня просто опыта не хватило. Но все же и сейчас не считаю код красивым.
              Генериловал для него диаграммы вызовов, так они очень уже запутанные получались :)
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                … и ни одного комментария в коде!
              +1
              По второй части — накопления запросов — сами доходили до этого.
              А по первой части — однопоточном приложении, обрабатывающем запросы без блокировки — очень интересно.
                +2
                накопление угу, довольно банальная вещь. мы и в php в этом же проекте аналогично накапливаем часть счётчиков в APC и изредка сбрасываем в базу.

                однопоточность в данном случае не идеал, просто так сложилось исторически, на мой взгляд всё-таки работу с базой стоило бы вынести в отдельный thread, но автор не хочет, мне тоже лень.
                  +2
                  > однопоточность в данном случае не идеал
                  Если Вы о том, что обработка одного запроса может быть слишком долгой, то да, тяжёлые куски лучше выносить в отдельные потоки.
                  Но, во-первых, в таком варианте количество потоков может быть меньше количества соединений.
                  А во-вторых, обработка запросов может быть достаточно быстрой, чтобы обойтись одним потоком, и избежать жуткого геморроя с отладкой и дедлоками.

                  И вообще: блокироваться — это как-то неправильно в принципе +)
                    +1
                    согласен, читаемости и надежности это только в ущерб, а выигрыш сомнительный
                +1
                Это разве все в плане оптимизации? Имхо, это тока вершина айсберга
                  +1
                  Этим людям хватило. А у вас синдром преждевременной эяк оптимизации.
                    0
                    Я имею ввиду оптимизацию системы. Всяко ведь не на стандартной конфигурации все работает.
                      0
                      система FreeBSD, настроена по мотивам www.opennet.ru/base/net/tune_freebsd.txt.html

                      но мне хотелось описать именно архитектуру приложения, с акцентом на обработку большими пачками как сетевого I/O так и работы с базой. в теме C10K акцент в основном на отдачу статики, а здесь приложение умудряется ещё и в MySQL всё тысячи запросов в секунду писать
                  0
                  опа, а еще отдельное спасибо за «ON DUPLICATE KEY UPDATE» =) не знал
                    0
                    По-первому — см. c10k в поисковиках

                    > и если для каждого создавать свой поток (thread) со своим стеком и постоянно переключаться между ними — ресурсов сервера очень быстро не хватит.
                    Не более чем точка зрения. Современные ОС могут держать без проблем даже миллионы тредов и выбор thread/poll не всегда столь очевиден, как вы описали.

                      +4
                      У вас точно такая же точка зрения :) Дело в том, что переключения контекста и стека — это всегда затратно. Тут надо искать золотую середину между количеством тредов и эффективностью работы всего алгоритма. Можно хоть 10 тыс тредов создать, по одному на соединение, но работать они будут едва ли не медленее, чем 100 тредов и ассинхронное IO.
                        +1
                        я, разрабатывая сервер для большого количества постоянных соединений остановился на смешанной модели пул процессов (не тредов) + асинхронная работа с сокетами.
                          +1
                          А почему процессы, а не треды? Процессы — это еще затратнее. Тут еще сброс TLB кеша…
                            +1
                            уперся в лимит одновременно открытых файловых дескрипторов.
                            расширять его по какой-то причине было нельзя.
                          0
                          > У вас точно такая же точка зрения
                          Откуда вы узнали мою точку зрения? Я её не публиковал :)
                          Я не говорил, что poll или треды это панацея. Есть задачи, где «рулит» poll, а есть — где «рулят» треды. Есть и такие, где «рулит» золотая середина.

                          Просто могу посоветовать посмотреть на затраты переключения и размеры стека для треда, к примеру, в том же линуксе. В последнее время всё не столь печально, как раньше, треды стали достаточно легковестными в плане переключения задач.
                            0
                            > У вас точно такая же точка зрения
                            Откуда вы узнали мою точку зрения? Я её не публиковал :)
                            Я не говорил, что poll или треды это панацея. Есть задачи, где «рулит» poll, а есть — где «рулят» треды. Есть и такие, где «рулит» золотая середина.

                            Просто могу посоветовать посмотреть на затраты переключения и размеры стека для треда, к примеру, в том же линуксе. В последнее время всё не столь печально, как раньше, треды стали достаточно легковестными в плане переключения задач.
                              0
                              > У вас точно такая же точка зрения
                              Откуда вы узнали мою точку зрения? Я её не публиковал :)
                              Я не говорил, что poll или треды это панацея. Есть задачи, где «рулит» poll, а есть — где «рулят» треды. Есть и такие, где «рулит» золотая середина.

                              Просто могу посоветовать посмотреть на затраты переключения и размеры стека для треда, к примеру, в том же линуксе. В последнее время всё не столь печально, как раньше, треды стали достаточно легковестными в плане переключения задач.
                                0
                                > У вас точно такая же точка зрения
                                Откуда вы узнали мою точку зрения? Я её не публиковал :)
                                Я не говорил, что poll или треды это панацея. Есть задачи, где «рулит» poll, а есть — где «рулят» треды. Есть и такие, где «рулит» золотая середина.

                                Просто могу посоветовать посмотреть на затраты переключения и размеры стека для треда, к примеру, в том же линуксе. В последнее время всё не столь печально, как раньше, треды стали достаточно легковестными в плане переключения задач.
                                  0
                                  > У вас точно такая же точка зрения
                                  Откуда вы узнали мою точку зрения? Я её не публиковал :)
                                  Я не говорил, что poll или треды это панацея. Есть задачи, где «рулит» poll, а есть — где «рулят» треды. Есть и такие, где «рулит» золотая середина.

                                  Просто могу посоветовать посмотреть на затраты переключения и размеры стека для треда, к примеру, в том же линуксе. В последнее время всё не столь печально, как раньше, треды стали достаточно легковестными в плане переключения задач.
                                    +2
                                    Сори… имхо хабр должен бороться с дубляжом
                                  0
                                  Современные ОС могут держать без проблем даже миллионы тредов

                                  Приведите источник, пожалуйста.
                                    0
                                    www.kegel.com/c10k.html#threaded конкретно NPTL есть ссылка про «10^6 threads»
                                      0
                                      Из той-же статьи: If each thread gets a 2MB stack (not an uncommon default value), you run out of *virtual memory* at (2^30 / 2^21) = 512 threads on a 32 bit machine with 1GB user-accessible VM (like, say, Linux as normally shipped on x86). You can work around this by giving each thread a smaller stack, but since most thread libraries don't allow growing thread stacks once created, doing this means designing your program to minimize stack use.
                                    0
                                    Спасибо за информацию. Сейчас как раз занимаемся переносом трекера с php на xbtt.

                                      0
                                      А можно пояснить про актеров в Erlang-е? Что-то я там ни чего подобного не припомню…
                                        0
                                          0
                                          Я понял, что это такая модель. Но я ни когда не видел, что бы где-то в Erlang-е использовали это понятие. Хотя из описания данной модели вижу, что VM именно так и работает.
                                        0
                                        Странно, что никто не упомянул такой недостаток описанной модели, как использование только одного ядра процессора. С некоторой натяжкой можно признать что второе ядро загрузит MySQL. Но если ядер больше двух, то модель с неблокирующим I/O в одном процессе начинает проигрывать модели с несколькими такими процессами.

                                        Кроме того, помимо производительности есть фактор простоты кода (т.е. поддерживаемости приложения). Я тоже много лет занимаюсь неблокирующим I/O, epoll, etc. и могу искренне заявить, что этот код никогда не будет таким же простым как код блокирующего I/O в нитях.

                                        Если для разработки таких серверов использовать языки/системы с очень лёгкими нитями (вроде Inferno), то производительность нитей с блокирующим I/O будет сравнима с производительностью epoll (сам проверял :)). При этом код будет значительно проще, плюс сервер будет автоматически масштабироваться при использовании многоядерных CPU (а в ближайшее время кол-во ядер скорее всего очень сильно увеличится).
                                          0
                                          всегда найдется то, чем загрузить второе и третье и четвертое ядро.
                                            0
                                            я не так крут и пишу серверы только год
                                            и до этого использовал блокирующий в/в

                                            ищу примеры неблокирующего I/O
                                            тоже хочу использовать
                                            0
                                            похожую схему я использовал на tradebox
                                            ну и др проектах

                                            правда это к трекеру не относится,
                                              0
                                              Для больших вставок в базу данных я использую механизм load data in file. Непревзойденная скорость вставки. Майскуэль парсит только один запрос, полезные данные в файле почти все, и дополнительных только разделители. Легко справляется с тысячами, десятками и сотнями тысяч строк в секунду. Если структуры таблиц не повзоляют такого делать, то можно делать отдельную таблицу для вставки и из нее делать необходимые инсерты-апдейты по другим таблицам. Эту временную таблицу так же можно создавать в памяти.

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

                                              Самое читаемое