- PVSM.RU - https://www.pvsm.ru -

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

Цель: установить и поддерживать миллион одновременных соединений используя Ruby.

Приложение должно:

— оставаться отзывчивой и обрабатывать минимум 100 запросов в секунду
— использовать максимум 15GB RAM; нагрузка 8-и ядерного компьютера должно остаться ниже 10-и
— «общаться» с клиентами каждые 15 секунд без особых затрат

Приложение будем строить используя:

Espresso [1] — исключительно быстрый и понятный фреймворк
Rainbows! [2] — веб сервер с поддержкой streaming и forking
EventMachine [3] — наиболее стабильная и эффективная 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 [4] за
«Миллион одновременных соединений на Node.js» [5] и создание потрясающего инструмента для управления флота EC2 инстансов — ec2-fleet [6]!

Благодоря 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.

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

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

Как видно из снимка, все цели достигнуты на ура:

— приложение обрабатывает 100 и более запросов в секунду
— использует меньше 15GB RAM и нагрузка остаётся ниже 10-и
— «общается» с клиентами каждые 15 секунд — смотри «Network History» — ~3MB/s in/out

После установления последнего соединения держал клиентов подключёнными в течении часа.

Соединения держались довольно стабильно, ни один клиент не исчез.

Нагрузка и память оставались на том же уровне, что примечательно так как исключены утечки памяти.

Потребление памяти и нагрузка в зависимости от количества соединений:

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

Среднее время обработки запроса в зависимости от количества соединений:

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

Каждую секунду отправляется запрос и регистрируется время ответа.
Среднее значение последних 60-и ответов и есть среднее время обработки запроса.

Тут можно увидеть как всё происходило — снимки делались каждые 15 секунд(история начинается с 12-го слайда): https://speakerdeck.com/slivu/ruby-handling-1-million-concurrent-connections [7]


Автор: slivu

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/ruby/27553

Ссылки в тексте:

[1] Espresso: https://github.com/espresso/espresso

[2] Rainbows!: http://rainbows.rubyforge.org/

[3] EventMachine: https://github.com/eventmachine/eventmachine

[4] ashtuchkin: http://habrahabr.ru/users/ashtuchkin/

[5] «Миллион одновременных соединений на Node.js»: http://habrahabr.ru/post/123154/

[6] ec2-fleet: https://github.com/ashtuchkin/ec2-fleet/

[7] https://speakerdeck.com/slivu/ruby-handling-1-million-concurrent-connections: https://speakerdeck.com/slivu/ruby-handling-1-million-concurrent-connections

[8] Источник: http://habrahabr.ru/post/170033/