Цель: установить и поддерживать миллион одновременных соединений используя Ruby.
Приложение должно:
— оставаться отзывчивой и обрабатывать минимум 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 << ""}
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: %snn" % 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
Автор: slivu