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

в 5:24, , рубрики: 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 << ""}

      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.

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

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


Автор: slivu

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js