Приложение должно:
— оставаться отзывчивой и обрабатывать минимум 100 запросов в секунду
— использовать максимум 15GB RAM; нагрузка 8-и ядерного компьютера должно остаться ниже 10-и
— «общаться» с клиентами каждые 15 секунд без особых затрат
Приложение будем строить используя:
— Espresso — исключительно быстрый и понятный фреймворк
— Rainbows! — веб сервер с поддержкой streaming и forking
— EventMachine — наиболее стабильная и эффективная I/O библиотека для Ruby
Конфиг для Rainbows!:
Rainbows! do use :EventMachine keepalive_timeout 3600*12 worker_connections 128_000 client_max_body_size nil client_header_buffer_size 512 end worker_processes 8
Rainbows! форкнет 8 дочерних процессов — по одному на каждое ядро — которые будут параллельно обрабатывать запросы.
Если вы знаете как заставить один Ruby процесс обрабатывать миллион одновременных соединений,
прошу повторить данный тест с одним Rainbows! воркером или с Thin веб сервером.
Код приложения:
class App < E map '/' # index and status_watcher actions should return event-stream content type before :index, :status_watcher do content_type 'text/event-stream' end def index stream :keep_open do |stream| # communicate to client every 15 seconds timer = EM.add_periodic_timer(15) {stream << "\0"} stream.errback do # when connection closed/errored: DB.decr :connections # 1. decrement connections amount by 1 timer.cancel # 2. cancel timer that communicate to client end # increment connections amount by 1 DB.incr :connections end end # frontend for status watchers - http://localhost:5252/status def status render end # backend for status watchers def status_watcher stream :keep_open do |stream| # adding a timer that will update status watchers every second timer = EM.add_periodic_timer(1) do connections = FormatHelper.humanize_number(DB.get :connections) stream << "data: %s\n\n" % connections end stream.errback { timer.cancel } # cancel timer if connection closed/errored end end def get_ping end end
Ruby версия и тунинг
Использовался MRI 1.9.3p385, установленный и управляемый через rbenv.
Для ускорения Ruby использовалось:
# The initial number of heap slots as well as the minimum number of slots allocated. RUBY_HEAP_MIN_SLOTS=1000000 # The minimum number of heap slots that should be available after the GC runs. # If they are not available then, ruby will allocate more slots. RUBY_HEAP_FREE_MIN=100000 # The number of C data structures that can be allocated before the GC kicks in. # If set too low, the GC kicks in even if there are still heap slots available. RUBY_GC_MALLOC_LIMIT=80000000
JRuby не пробовал по двум причинам:
— не знаю ни одного легко настраиваемого/общедоступного JRuby сервера с поддержкой streaming
— есть подозрение что JRuby нужно намного больше 15GB RAM для миллиона соединений
Пробовал Rubinius 2.0.0rc1 1.9mode но без успеха — segfault после ~10,000 соединений.
Что-то связано с libpthread — полностью разобраться не успел.
Операционная система
Выбор пал на Ubuntu 12.04 — легко установить и не требует особой настройки для получения большого количества соединений.
Надо редактировать всего два файла:
/etc/security/limits.conf:
* - nofile 1048576
/etc/sysctl.conf:
net.ipv4.netfilter.ip_conntrack_max = 1048576
Как повторить тест
Настройка сервера:
Клонируем 1mc2 репозиторий:
$ git clone https://github.com/slivu/1mc2
Устанавливаем нужные гемы:
$ cd 1mc2/ $ bundle $ rbenv rehash # in case you are using rbenv
Запускаем Redis используя конфиг из 1mc2 репозитория:
$ redis-server ./redis.conf
Запускаем приложение:
$ ./run
Настройка «клиентов»:
Для создания нагрузки было использовано 50 EC2 микро инстансов.
Огромное Спасибо ashtuchkin за
«Миллион одновременных соединений на Node.js» и создание потрясающего инструмента для управления флота EC2 инстансов — ec2-fleet!
Благодоря ec2-fleet мне хватило всего 5 строк чтобы решить большую-пребольшую проблему с нагрузкой:
Стартуем 50 инстансов:
$ ./aws.js start 50
Ждём около двух минут и проверяем всё-ли готово:
$ ./aws.js status
Натравливаем клиентов на наш сервер:
$ ./aws.js set host <ip>
Указываем порт на котором приложение принимает соединения:
$ ./aws.js set port 5252
Задаём нагрузку — 50 инстансов по 20,000 клиентов на каждом — получаем 1,000,000 соединений:
$ ./aws set n 20000
Приложение должно начать принимать соединения.
Чтобы увидеть как это происходит заходим на http://localhost:5252/status
Браузер должен быть из последних версий Chrome/Firefox/Safari/Opera с поддержкой ServerSentEvents.
Если всё сделано правильно, браузер должен показывать сколько всего соединений установлено на данный момент, сколько запросов обрабатывается в секунду и среднее время для обработки одного запроса.
Как видно из снимка, все цели достигнуты на ура:
— приложение обрабатывает 100 и более запросов в секунду
— использует меньше 15GB RAM и нагрузка остаётся ниже 10-и
— «общается» с клиентами каждые 15 секунд — смотри «Network History» — ~3MB/s in/out
После установления последнего соединения держал клиентов подключёнными в течении часа.
Соединения держались довольно стабильно, ни один клиент не исчез.
Нагрузка и память оставались на том же уровне, что примечательно так как исключены утечки памяти.
Потребление памяти и нагрузка в зависимости от количества соединений:
Среднее время обработки запроса в зависимости от количества соединений:
Каждую секунду отправляется запрос и регистрируется время ответа.
Среднее значение последних 60-и ответов и есть среднее время обработки запроса.
Тут можно увидеть как всё происходило — снимки делались каждые 15 секунд(история начинается с 12-го слайда): https://speakerdeck.com/slivu/ruby-handling-1-million-concurrent-connections
ссылка на оригинал статьи http://habrahabr.ru/post/170033/
Добавить комментарий