Асинхронная работа с MySQL в Ruby

Ваш движок работает с MySQL? Скорее всего да. Тогда вы должны знать, что ваш код (будь это PHP или Ruby) исполняется последовательно. Формируется SQL-запрос, отправляется к базе, извлекаются данные, затем следующий запрос и т.д. А вы не думали, что можно ускорить ваше приложение, используя асинхронные запросы?

Я посмотрел несколько материалов на эту тему в сети, и нашел одну интересную статью в блоге, эксперимент из которой повторил на локальной машине. Всё началось с того, что я ребята из espace анонсировали Neverblock: библиотеку для Ruby 1.9, которая позволяет использовать (нити) и неблокирующий ввод-вывод (см. FibersRubyInside, InfoQ и MySQLPlus). Вскоре после этого я увидел анонс MySQLPlus драйвера с поддержкой асинхронных операций и поточным доступом к базе. Прекрасно!

Асинхронная панацея?

Значит ли это, что мы можем запустить ActiveRecord в режиме высокоскоростного демона? Не так быстро, пока нет 🙂 В случае, если мы сделаем нашу БД асинхронной, нам придется обслуживать множество паралелльных запросов (это уже само по себе огромная победа), без блокировки сервера, но это не уменьшит время ответа MySQL-сервера. Базы данных всё ещё являются бутылочным горлышком. Кроме того, для того, чтоыб получить преимущество неблокируемой модели, вам придётся переписать весь (ну или почти весь) код своего приложения.

Нравится нам это, или не нравится, но ActiveRecord и его аналоги (Datamapper, Sequel и др) обеспечивают великолепную абстракцию, которая скрывает большую часть взаимодействия с БД и его сложность. По этим причинам DBSlayer – действительно интересная альтернатива, особенно, вслучае когда используется в сочетании с ActiveRecord и Datamapper адаптерами а также языковыми конструкциями, такими, как JSON, поллинг соединений с БД, отказоустойчивость, и многими другими плюшками.

EM/MySQL – Асинхронный клиент к бд MySQL на Ruby

Однако, если вы интересуетесь альтернативой на Руби (а я интересуюсь 🙂 ) то обратите внимение на проект Амана Гупта – em-mysql. Это обертка EvantMachine над MySQLPlus, она УЖЕ показывает многообещающие результаты:

В этом тесте состоящем из 200 запросов моделировалась разная конкурентность (число одновременных запросов), начиная с однособытийного веб-сервера. В этом случае, каждый запрос моделирует один блокирующий вызов БД (sleep на одну секунду), и потом возвращает результат пользователю. И не удивительно, что 200 запросов по 1 секундекаждый требуют 200 секунд (т.к. конкурентность равна 1) и в случае с DBSlayer, нативным MySQL драйвером и EM/MySQL. Однако, как только мы повышаем конкурентность, нативный драйвер начинает отставать, а вот неблокирующие EM/MySQL и DBSlayer вырываются вперед. Они ~2 секунды умудряются обслужить все 200 запросов.

Асинхронные запросы к MySQL в действии

Для начала нам нужно будет установить необходимые гемы:

sudo gem install eventmachine eventmachine_httpserver em-mysql

Затем скопируем содержимое гема em-mysql в папку с проектом.

cp /var/lib/gems/1.8/gems/em-mysql-0.4.2 /home/andrey/Ruby/events/

Создадим руби скрипт:

И запустим его, перед этим прописав в нём пароль для доступа  БД (запрос тоже можете подправить):

ruby ./asyncMysql.rb

Тесты

Запускаем Apache Benchmark и офигеваем от результатов.

ab -c 100 -n 1000 http://localhost:3000/

Benchmarking localhost (be patient)
Completed 100 requests

Completed 1000 requests
Finished 1000 requests

Server Software:
Server Hostname:        localhost
Server Port:            3000

Document Path:          /
Document Length:        317 bytes

Concurrency Level:      100
Time taken for tests:   0.656 seconds
Complete requests:      1000
Failed requests:        907
(Connect: 0, Receive: 0, Length: 907, Exceptions: 0)
Write errors:           0
Total transferred:      352250 bytes
HTML transferred:       311306 bytes
Requests per second:    1523.24 [#/sec] (mean)
Time per request:       65.650 [ms] (mean)
Time per request:       0.656 [ms] (mean, across all concurrent requests)
Transfer rate:          523.99 [Kbytes/sec] received

Connection Times (ms)
min  mean[+/-sd] median   max
Connect:        0    1   2.0      0       9
Processing:     4   62  10.1     63      80
Waiting:        3   62  10.1     63      80
Total:         13   62   8.9     63      83

Percentage of the requests served within a certain time (ms)
50%     63
66%     66
75%     67
80%     68
90%     71
95%     75
98%     79
99%     79
100%     83 (longest request)

Выводы

В среднем 63 мс тратится на один HTTP-запрос (в данном случае и SQL-запрос)  при 100 одновременно подключённых клиентах, и 1000 запросов. Чтобы избежать кэширования на уровне БД, id записи выбирается каждый раз случайно.

Кстати, от этього же автора есть асинхронные драйвера к Memcached и MongoDb 🙂

Скачать исходники и тесты

1. Пример можно скачать по этой ссылке:  asynchronicHttpAndMysql.

2. Другие тесты (на другом примере) доступын по этой ссылке: async-tests

Оригинал статьи на английском

2 Comments

  1. Клевая статья, прочел с интересом. Поставь кстати, хайлайтер для кода, а то неудобно его читать без отступов и всего такого.

    1. Спасибо, Андрюха! Хайлайтер то есть, вот только кисть для Ruby надо поставить.
      UPD: ну вот и поставил

Leave a Reply to Andy Cancel reply