Alexander Dymo - Barcamp 2009 - Faster Higher Sql

861 views

Published on

Published in: Technology
  • Be the first to comment

Alexander Dymo - Barcamp 2009 - Faster Higher Sql

  1. 1. Оптимизация Rails: быстрее, выше, SQL'нее Александр Дымо Ruby And Rails Barcamp 2009 www.acunote.com
  2. 2. О чем речь? Оптимизация Rails приложения опыт 3.5 лет разработки нестандартный путь ^^^^^^^^ Александр Дымо Director of Engineering [email_address]
  3. 3. Что за приложение? Acunote www.acunote.com Онлайновое средство управления проектами для компаний и команд использующих Agile методологии разработки (в частности, Scrum) ~4900 организаций Хостинг на Engine Yard (3 VPS) Хостинг на серверах клиента nginx + mongrel PostgreSQL
  4. 4. Стандартные пути оптимизации 1. Более эффективный Ruby код RubyProf в помощь
  5. 5. Стандартные пути оптимизации 1. Более эффективный Ruby код RubyProf в помощь 2. Давайте перепишем все на C Date-Performance: http://github.com/rtomayko/date-performance/ Monkeysupport: http://github.com/burke/monkeysupport http://burkelibbey.posterous.com/
  6. 6. А мы пойдем своим путем 1. Более эффективный Ruby код RubyProf в помощь 2. Давайте перепишем все на C Date-Performance Monkeysupport 3. Наш путь - перепишем все на SQL !
  7. 7. Так все же, о чем речь? Активное использование SQL Перенос функциональности и логики в SQL Почему Postgres? Оптимизация БД Тестирование производительности
  8. 8. Активное использование SQL Выборка дополнительных атрибутов к моделям: foos = Foo.find(:all, :select => "*, 2+2 as hard_stuff") Почему? Производительность: в SQL: explain analyze select 2+2 as hard_stuff; QUERY PLAN ------------------------------------------------------------------- Result (cost=0.00..0.01 rows=1 width=0) (actual time= 0.009..0.012 rows=1 loops=1) Total runtime: 0.068 ms а в Ruby: sprintf("%0.3 ms", Benchmark.realtime{ 2+2 }*1000) > 0.017 ms 2x!
  9. 9. Активное использование SQL Выборка дополнительных атрибутов к моделям: Tasks Tags Tasks_Tags id serial id serial tag_id integer name varchar name varchar task_id integer tasks = Task.find(:all, :include => :tags) > 0.058 сек 2 SQL запроса select * from tasks select * from tags inner join tasks_tags on tags.id = tasks_tags.tag_id where tasks_tags.task_id in (1,2,3,..) Rals должен создать модели для каждого тэга а это не быстро и занимает память
  10. 10. Активное использование SQL Выборка дополнительных атрибутов к моделям: Tasks Tags Tasks_Tags id serial id serial tag_id integer name varchar name varchar task_id integer tasks = Task.find(:all, :select => "*, array( select tags.name from tags inner join tasks_tags on (tags.id = tasks_tags.tag_id) where tasks_tasks.task_id=tasks.id) as tag_names ") > 0.018 сек 1 SQL запрос Rails не создает модели >3x быстрее (было 0.058 сек, стало 0.018 сек) 3x!
  11. 11. Активное использование SQL Выборка дополнительных атрибутов к моделям: Tasks Tags Tasks_Tags id serial id serial tag_id integer name varchar name varchar task_id integer tasks = Task.find(:all, :select => "*, array( select tags.name from tags inner join tasks_tags on (tags.id = tasks_tags.tag_id) where tasks_tasks.task_id=tasks.id) as tag_names ") puts tasks.first.tag_names > "{Foo,Bar,Zee}"
  12. 12. Активное использование SQL Выборка дополнительных атрибутов соединениями требует указания параметра select в методе find : foos = Foo.find(:all, :joins => "left outer join bars using bar_id" не добавит атрибуты из bars потому что Rails сделает > select foos.* from foos left outer join bars... Нужно добавлять select: foos = Foo.find(:all, :select => "*" , :joins => "left outer join bars using bar_id" > select foos.* from foos left outer join bars...
  13. 13. Активное использование SQL Еще о предварительной загрузке ассоциаций... она не работает с find_by_sql class Foo belongs_to :bar end foos = Foo. find_by_sql ( 'select * from foos inner join bars' ) foos. first . bar #еще 1 SQL запрос!
  14. 14. Активное использование SQL Virtual Attributes plugin: http://github.com/acunote/virtual_attributes/ class Bar end class Foo belongs_to :bar preloadable_association :bar end foos = Foo. find_by_sql ( ' select * from foos left outer join (select id as preloaded_bar_id, name as preloaded_bar_name from bars) as bars on foos.bar_id = bars.preloaded_bar_id' ) foos. first . bar #no extra SQL query!
  15. 15. Так все же, о чем речь? Активное использование SQL Перенос функциональности и логики в SQL Почему Postgres? Оптимизация БД Тестирование производительности
  16. 16. Acunote > Сложные запросы Дерево задач (3 уровня) (+2 соединения и 1 вл. запрос) Тэги к задачам (+2 вложенных запроса) Счетчики свойств задач (+4 вложенных запроса) Последние значения изменяемых во времени атрибутов (+4 соединения с "бухгалтерскими запросами") и т.д... - 12 соединений и вложенных запросов 1 4 2 3
  17. 17. Acunote > Один за всех! Этот "монстро" выбирает дерево из нескольких сотен элементов и загружает всю необходимую информацию к ним не более чем за 50 мс! Даже на EeePC это занимает 58мс Эквивалентный Ruby/Rails код занимал до 8сек
  18. 18. Acunote > "Бухгалтерский" запрос Issues Timecells Цель: выбрать самое последнее значение из ячеек
  19. 19. Acunote > &quot;Бухгалтерский&quot; запрос Issues Timecells select * from Issues left outer join ( select distinct on (issue_id) issue_id as cell_issue_id, value as cell_value, date as cell_date from Timecells where date <= '2009-06-09' order by issue_id, date desc ) as cells on (issues.id = cells.cell_issue_id)
  20. 20. Acunote > Эффективные деревья Обычный способ моделирования деревьев в Rails : create table Task ( id serial not null , parent_id integer ) class Task < ActiveRecord::Base acts_as_tree end Использует N+1 запросов для загрузки N узлов из дерева: (root) select * from tasks where parent_id = nil - 1 select * from tasks where parent_id = 1 - 11 select * from tasks where parent_id = 11 - 111 select * from tasks where parent_id = 111 - 112 select * from tasks where parent_id = 112 - 12 select * from tasks where parent_id = 12 - 2 select * from tasks where parent_id = 2 - 21 select * from tasks where parent_id = 21
  21. 21. Acunote > Эффективные деревья Обычный способ моделирования деревьев в Rails : create table Task ( id serial not null , parent_id integer ) class Task < ActiveRecord::Base acts_as_tree end А должно быть так: select * from tasks left outer join (select id as parents_id, parent_id as parents_parent_id from tasks) as parents on (tasks.parent_id = parents_id) left outer join (select id as parents_parents_id from tasks) as parents_parents on (parents_parent_id = parents_parents_id)
  22. 22. Acunote > Разбиение по страницам Разбиение по страницам это: select * from Issues where <conditions> limit N offset M
  23. 23. Acunote > Efficient Pagination Но будьте осторожны со вложенными запросами: select *, (select count(*) from attachments where issue_id = issues.id) as num_attachments from issues limit 100 offset 0 ; Limit (cost=0.00..831.22 rows=100 width=143) (actual time=0.050..1.242 rows=100 loops=1) -> Seq Scan on issues (cost=0.00..2509172.92 rows=301866 width=143) (actual time=0.049..1.119 rows=100 loops=1) SubPlan -> Aggregate (cost=8.27..8.28 rows=1 width=0) (actual time=0.006..0.006 rows=1 loops= 100 ) -> Index Scan using attachments_issue_id_idx on attachments (cost=0.00..8.27 rows=1 width=0) (actual time=0.004..0.004 rows=0 loops= 100 ) Index Cond: (issue_id = $0) Total runtime: 1.383 ms
  24. 24. Acunote > Efficient Pagination Но будьте осторожны со вложенными запросами: select *, (select count(*) from attachments where issue_id = issues.id) as num_attachments from issues limit 100 offset 100 ; Limit (cost=831.22..1662.44 rows=100 width=143) (actual time=1.070..7.927 rows=100 loops=1) -> Seq Scan on issues (cost=0.00..2509172.92 rows=301866 width=143) (actual time=0.039..7.763 rows=200 loops=1) SubPlan -> Aggregate (cost=8.27..8.28 rows=1 width=0) (actual time=0.034..0.034 rows=1 loops= 200 ) -> Index Scan using attachments_issue_id_idx on attachments (cost=0.00..8.27 rows=1 width=0) (actual time=0.032..0.032 rows=0 loops= 200 ) Index Cond: (issue_id = $0) Total runtime: 8.065 ms
  25. 25. Acunote > Efficient Pagination Но будьте осторожны со вложенными запросами: они выполняются limit + offset раз ! Используйте соединения в таком случае
  26. 26. Acunote > Пользователи и роли Упрощенная модель ролей для пользователей: Users Role Roles_Users id serial id serial user_id integer name varchar name varchar role_id integer privilege1 boolean privilege2 boolean ... user = User.find(:first, :include => :roles) can_do_1 = user.roles.any { |role| role.privilege1? }
  27. 27. Acunote > Пользователи и роли Упрощенная модель ролей для пользователей: Users Role Roles_Users id serial id serial user_id integer name varchar name varchar role_id integer privilege1 boolean privilege2 boolean ... user = User.find(:first, :include => :roles) can_do_1 = user.roles.any { |role| role.privilege1? } В чем проблема? - 2 SQL запроса - заставляем Rails создавать объекты ролей - заставляем Ruby делать цикл
  28. 28. Acunote > Пользователи и роли Перепишем на SQL: Users Role Roles_Users id serial id serial user_id integer name varchar name varchar role_id integer privilege1 boolean user = User.find(:first, :select => &quot;*&quot;, :joins => &quot; inner join (select user_id, bool_or(privilege1) as privilege1 from roles_users inner join roles on (roles.id = roles_users.role_id) group by user_id ) as roles_users on (users.id = roles_users.user_id) &quot; ) can_do_1 = ActiveRecord::ConnectionAdapters::Column. value_to_boolean(user.privilege1)
  29. 29. Acunote > Пользователи и роли Есть ли эффект от такого SQL? цифры из жизни: can_do_1 = user.roles.any { |role| role.privilege1? } > код с использованием такого подхода исполнялся 2.1 сек can_do_1 = ActiveRecord::ConnectionAdapters::Column. value_to_boolean(user.privilege1) > код с использованием такого подхода исполнялся 64 мс !!!
  30. 30. Acunote > OLAP и его подобия Делаете операции аггрегирования и выборки на больших объемах данных? Делайте их в SQL! цифры из жизни: 600 000 строк с данными, куб в 3х измерениях, срез и аггрегирование в Ruby: ~1 Gb памяти, ~90 сек в SQL: до 5 сек
  31. 31. Общий вывод Хотите оптимизировать? Правило такое: Пишите Ruby код который генерирует SQL который экономит гигабайт памяти который выполняется в десятки/сотни раз быстрее и лучше масштабируется
  32. 32. Так все же, о чем речь? Активное использование SQL Перенос функциональности и логики в SQL Почему Postgres? Оптимизация БД Тестирование производительности
  33. 33. Почему Postgres? Соответствие стандарту Хорошая документация Понятный процесс разработки Плюс при принятии на работу
  34. 34. Postgres спасет отца русской демократии Ограничения, ссылочная целостность Автоопределение deadlock'ов Хороший оптимизатор (иногда делает чудеса) Понятный Explain Analyze Полезные надстройки над стандартом (напр. массивы)
  35. 35. Postgres спасет отца русской демократии Вывод: если СУБД для вас - это не только средство хранения данных, используйте Postgres
  36. 36. Так все же, о чем речь? Активное использование SQL Перенос функциональности и логики в SQL Почему Postgres? Оптимизация БД Тестирование производительности
  37. 37. Оптимизация БД > Основы Единственный путь оптимизации PostgreSQL: explain analyze explain analyze explain analyze ...
  38. 38. Оптимизация БД > „Холодные“ запросы EXPLAIN ANALYZE все объясняет, но... ... его нужно делать и для „холодного“ состояния базы! Пример: сложный запрос к таблице из 230 000 строк 9 вложенных запросов и соединений: холодное состояние: 28 сек, горячее состояние: 2.42 сек Перезапуск СУБД не помогает! Нужно очищать дисковый кэш: sudo echo 3 | sudo tee /proc/sys/vm/drop_caches (Linux)
  39. 39. Оптимизация БД > Общий сервер Если ваш хостинг предоставляет общий сервер БД для всех клиентов, то вы соревнуетесь с ними за кэш в памяти: 1. две БД с одинаковой загрузкой честно поделят кэш
  40. 40. Optimize Database > Shared Database Если ваш хостинг предоставляет общий сервер БД для всех клиентов, то вы соревнуетесь с ними за кэш в памяти: 2. менее нагруженная БД проигрывает битву за кэш
  41. 41. Optimize Database > Shared Database В результате, ваша БД будет всегда холодной и вы будете читать данные с диска а не с памяти! помните пример со сложным запросом: с диска: 28 сек, из памяти: 2.42 сек Решения: оптимизация для холодного состояния указание большего кол-ва условий выборки sudo echo 3 | sudo tee /proc/sys/vm/drop_caches
  42. 42. Оптимизация БД > array() Используйте any(array ()) всесто in() для того чтобы получить subselect а не join explain analyze select * from issues where id in (select issue_id from tags_issues); QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------- Merge IN Join (actual time=0.096..576.704 rows=55363 loops=1) Merge Cond: (issues.id = tags_issues.issue_id) -> Index Scan using issues_pkey on issues (actual time=0.027..270.557 rows=229991 loops=1) -> Index Scan using tags_issues_issue_id_key on tags_issues (actual time=0.051..73.903 rows=70052loops=1) Total runtime: 605.274 ms explain analyze select * from issues where id = any( array( (select issue_id from tags_issues) ) ); QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------ Bitmap Heap Scan on issues (actual time=247.358..297.932 rows=55363 loops=1) Recheck Cond: (id = ANY ($0)) InitPlan -> Seq Scan on tags_issues (actual time=0.017..51.291 rows=70052 loops=1) -> Bitmap Index Scan on issues_pkey (actual time=246.589..246.589 rows=70052 loops=1) Index Cond: (id = ANY ($0)) Total runtime: 325.205 ms 2x!
  43. 43. Оптимизация БД > Условия выборки Самостоятельно указывайте условия выборки во вложенных запросах и соединениях PostgreSQL зачастую сам не может этого сделать select *, ( select notes.author from notes where notes.bug_id = bugs.id ) as note_authors from bugs where org_id = 1 select *, ( select notes.author from notes where notes.bug_id = bugs.id and org_id = 1 ) as note_authors from bugs where org_id = 1 Bugs id serial name varchar org_id integer Notes id serial name varchar bug_id integer org_id integer
  44. 44. Так все же, о чем речь? Активное использование SQL Перенос функциональности и логики в SQL Почему Postgres? Оптимизация БД Тестирование производительности
  45. 45. Тестирование производительности Во первых, создайте набор тестов, измеряющих производительность самых популярных страниц. Например: Benchmark Burndown 120 0.70 ± 0.00 Benchmark Inc. Burndown 120 0.92 ± 0.01 Benchmark Sprint 20 x (1+5) (C) 0.45 ± 0.00 Benchmark Issues 100 (C) 0.34 ± 0.00 Benchmark Prediction 120 0.56 ± 0.00 Benchmark Progress 120 0.23 ± 0.00 Benchmark Sprint 20 x (1+5) 0.93 ± 0.00 Benchmark Timeline 5x100 0.11 ± 0.00 Benchmark Signup 0.77 ± 0.00 Benchmark Export 0.20 ± 0.00 Benchmark Move Here 20/120 0.89 ± 0.00 Benchmark Order By User 0.98 ± 0.00 Benchmark Set Field (EP) 0.21 ± 0.00 Benchmark Task Create + Tag 0.23 ± 0.00 ...
  46. 46. Тестирование производительности Еще один тип интеграционного тестирования: class RenderingTest < ActionController::IntegrationTest def test_sprint_rendering login_with users (:user), &quot;user&quot; benchmark :title => &quot;Sprint 20 x (1+5) (C)&quot;, :route => &quot;projects/1/sprints/3/show&quot;, :assert_template => &quot;tasks/index&quot; end end Benchmark Sprint 20 x (1+5) (C) 0.45 ± 0.00
  47. 47. Тестирование производительности Еще один тип интеграционного тестирования: def benchmark (options = {}) (0..100). each do |i| GC. start pid = fork do begin out = File. open (&quot;values&quot;, &quot;a&quot;) ActiveRecord::Base. transaction do elapsed_time = Benchmark:: realtime do request_method = options[:post] ? :post : :get send (request_method, options[:route]) end out. puts elapsed_time if i > 0 out. close raise CustomTransactionError end rescue CustomTransactionError exit end end Process:: waitpid pid ActiveRecord::Base. connection . reconnect ! end values = File. read (&quot;values&quot;) print &quot;#{ mean (values).to_02f} ± #{ sigma (values).to_02f} &quot; end
  48. 48. Тестирование производительности Осторожно! Потеря 10мс в тесте зачастую только кажется безобидной Потому что может случиться так, что эти 10мс уходят на случайно добавленный SQL запрос
  49. 49. Тестирование производительности Тесты запросов def test_queries queries = track_queries do get :index end assert_equal queries, [ &quot;Foo Load&quot;, &quot;Bar Load&quot;, &quot;Moo Create&quot; ] end
  50. 50. Тестирование производительности module ActiveSupport class BufferedLogger attr_reader :tracked_queries def tracking=(val) @tracked_queries = [] @tracking = val end def add_with_tracking(severity, message = nil, progname = nil, &block) @tracked_queries << $1 if @tracking && message =~ /3[56];1m(.* (Load|Create|Update|Destroy)) (/ @tracked_queries << $1 if @tracking && message =~ /3[56];1m(SQL) (/ add_without_tracking (severity, message, progname, &block) end alias_method_chain :add, :tracking end end class ActiveSupport::TestCase def track_queries(&block) RAILS_DEFAULT_LOGGER. tracking = true yield result = RAILS_DEFAULT_LOGGER. tracked_queries RAILS_DEFAULT_LOGGER. tracking = false result end end
  51. 51. Интересно оптимизировать Rails? Приходите в нашу команду! http://www.acunote.com/pluron/jobs
  52. 52. Cпасибо за внимание! Вопросы? Наш блог про Rails Performance: http://blog.pluron.com Александр Дымо Director of Engineering [email_address]

×