Асинхронная работа с 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/
Создадим руби скрипт:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
require 'rubygems' require 'eventmachine' require 'evma_httpserver' require 'em-mysql-0.4.2/lib/em/mysql' class Handler < EventMachine::Connection include EventMachine::HttpServer attr_accessor :db def process_http_request resp = EventMachine::DelegatedHttpResponse.new( self ) id = 1+rand(1000) query = "select * from table where id = #{id}" EventedMysql.select(query) { |res| resp.status = 200 resp.content = res resp.send_response } end end EventMachine.epoll EventMachine::run { SQL = EventedMysql @mysql = EventedMysql.settings.update({ :host => 'localhost', :port => 3306, :database => 'dbname', :login => 'root', :password => 'mypassword', :connections => 1 }) EventMachine::start_server("0.0.0.0", 3000, Handler) {|conn| conn.db = @mysql} puts "Listening..." } |
И запустим его, перед этим прописав в нём пароль для доступа БД (запрос тоже можете подправить):
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 requestsServer Software:
Server Hostname: localhost
Server Port: 3000Document Path: /
Document Length: 317 bytesConcurrency 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] receivedConnection 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 83Percentage 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
Клевая статья, прочел с интересом. Поставь кстати, хайлайтер для кода, а то неудобно его читать без отступов и всего такого.
Спасибо, Андрюха! Хайлайтер то есть, вот только кисть для Ruby надо поставить.
UPD: ну вот и поставил