Ruby — обрабатываем миллион одновременных соединений

от автора

Цель: установить и поддерживать миллион одновременных соединений используя 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 << "\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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *