-
Notifications
You must be signed in to change notification settings - Fork 113
[Task 3] Database usage optimization #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,3 +2,4 @@ | |
| /tmp | ||
| /log | ||
| /public | ||
| .idea | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # Model for convenient access to join table | ||
| class BusesService < ApplicationRecord | ||
| end | ||
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| ### 1. Оптимизация загрузки данных в базу приложения | ||
|
|
||
| #### Проблема | ||
| Рейк-таск для загрузки данных в базу приложения работает слишком долго: ~8 секунд для файла с 1_000 записей, ~67 секунд для файла с 10_000 записей. | ||
|
|
||
| #### Метрика | ||
| Чтобы понять, несут ли изменения положительный эффект, в качестве метрики выбрано время загрузки файлов поставляемых с программой. Также в процессе оптимизации планирую следить за потреблением памяти. | ||
|
|
||
| #### Гарантия корректности работы программы | ||
| Для гарантии корректной работы написан тест проверяющий что в базу корректно загружается файл `fixtures/example.json` | ||
|
|
||
| #### Feedback-loop | ||
| Для получения быстрой обратной связи добавлены скрипты генерации отчетов профилировщиков. | ||
|
|
||
| #### Процесс оптимизации | ||
| В исходном состоянии скрипт загрузки имеет следующие показатели для файла small.json (1к записей, размер ~304K) | ||
| Потребление памяти (вместе со всем rails приложением) | ||
| ``` | ||
| Total allocated: 467.01 MB (4716422 objects) | ||
| Total retained: 10.68 MB (9833 objects) | ||
| ``` | ||
| Время работы - ~9 сек | ||
|
|
||
| Ruby-prof по времени не показал точек роста - отчет в основном состоит из внутренностей Rails. | ||
|
|
||
| Собрал лог из базы данных и скормил его pgbadger - оказалось что для вставки 1_000 записей выполняется 13_528 запросов к базе. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| Из них 68% (9_253) это SELECT, т.е. даже не вставка данных. Несложно было догадаться что основная масса из них это проверка "справочников" и валидации AR выполняемые при сохранении данных. | ||
| Решил сделать как было предложено в подсказках к заданию - использовать гем `activerecord-import` а также собрать справочники и загрузить их одной пачкой. | ||
|
|
||
| Благодаря этим действиям удалось значительно снизить время загрузки данных : | ||
| Без AR валидаций small.json - ~1.1 секунд, large.json - ~7.7 секунд | ||
| С AR валидациями small.json - ~1.3 секунд, large.json - ~18.4 секунд | ||
| (данные, включая время старта окружения) | ||
|
|
||
| Потребление памяти также сократилось: | ||
| small.json: | ||
| ``` | ||
| Total allocated: 24.79 MB (159304 objects) | ||
| Total retained: 10.61 MB (9125 objects) | ||
| ``` | ||
| large.json | ||
| ``` | ||
| Total allocated: 710.76 MB (5513967 objects) | ||
| Total retained: 10.61 MB (9120 objects) | ||
| ``` | ||
| Естественно что без стриминга мы грузим в память файл который обрабатываем целиком, но bonus часть я не выполнял, так что оставляю как есть. | ||
|
|
||
| #### Результат | ||
| В результате проведенной оптимизации удалось уложиться в рамки задачи. | ||
|
|
||
| #### Защита от регрессии | ||
| Для защиты от регрессии добавлен тест, который проверяет время загрузки файла с 1к записей. | ||
|
|
||
|
|
||
| ### 2. Оптимизация времени загрузки страницы | ||
|
|
||
| #### Проблема | ||
| Страница с расписанием автобусов загружается слишком долго. | ||
|
|
||
| #### Метрика | ||
| Чтобы понять, несут ли изменения положительный эффект, в качестве метрики выбрано время загрузки страницы для файла large.json. | ||
|
|
||
| #### Гарантия корректности работы программы | ||
| Для гарантии корректной работы написан тест проверяющий что контроллер выдает корректный html для файла `example.json` | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
|
||
| #### Feedback-loop | ||
| Профилировщики выдают отчеты прямо на оптимизируемой странице. | ||
|
|
||
| #### Процесс оптимизации | ||
| Перед оптимизацией страница загружалась за ~15-19 секунд для файла large.json. | ||
|
|
||
| Rails-panel и rack mini profiler показали что основная масса времени тратится на рендеринг. | ||
| Решил собрать все темплейты в один файл, поскольку они очень простые. | ||
| В результате время загрузки страницы почти не изменилось. | ||
|
|
||
| И оба профайлера выше и логи показали что страница делает ~2000 запросов в базу, прямо из вьюх (видимо поэтому rails panel отнес все затраты времени в рендеринг) | ||
| Уменьшил количество запросов к базе поправив N+2 (заюзал `includes`), и сделал принудительную загрузку данных в контроллере (`.to_a` на запросе) | ||
| Стало 6 запросов к базе, время загрузки сократилось значительно - до ~450-500 ms. Плюс стало четко видно сколько времени занимает непосредственно рендеринг - ~80-120ms | ||
|
|
||
| Добавил индексов на id поля, но pg_hero поругал меня за дубликаты, пришлось удалить :) | ||
|
|
||
| #### Результат | ||
| В результате проведенной оптимизации удалось уложиться в рамки задачи (~500ms)? | ||
|
|
||
| #### Защита от регрессии | ||
| Для защиты от регрессии добавил тест, который проверяет время ответа контроллера. К сожалению время ответа очень сильно различается от теста к тесту, поэтому с запасом указал 0.5 сек для файла example.json | ||
|
|
||
| #### P.S. | ||
| Понимаю, что нет предела совершенству - можно и "справочники" превратить в константы, и кеширование добавить во вьюхи и пагинацию на страницы, но устал :) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 😄 |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| Rails.application.routes.draw do | ||
| # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html | ||
| mount PgHero::Engine, at: 'pghero' if Rails.env.development? | ||
|
|
||
| get "/" => "statistics#index" | ||
| get "автобусы/:from/:to" => "trips#index" | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # Populates database with records from json file | ||
| class PopulateDatabase | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍👍 |
||
| include Singleton | ||
|
|
||
| class << self | ||
| delegate :call, to: :instance | ||
| end | ||
|
|
||
| def call(file_path:, validate: false) | ||
| json = JSON.parse(File.read(file_path)) | ||
|
|
||
| clear_database | ||
| process_common_entities(json, validate) | ||
| process_trips(json, validate) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def clear_database | ||
| ActiveRecord::Base.transaction do | ||
| City.delete_all | ||
| Bus.delete_all | ||
| Service.delete_all | ||
| Trip.delete_all | ||
| BusesService.delete_all | ||
| end | ||
| end | ||
|
|
||
| def process_common_entities(json, validate) | ||
| cities = Set.new | ||
| buses = Set.new | ||
| services = Set.new | ||
|
|
||
| json.each do |trip| | ||
| cities << { name: trip['from'] } | ||
| cities << { name: trip['to'] } | ||
| buses << { number: trip['bus']['number'], model: trip['bus']['model'] } | ||
|
|
||
| trip['bus']['services'].each { |service_name| services << { name: service_name } } | ||
| end | ||
|
|
||
| City.import cities.to_a, validate: validate | ||
| Bus.import buses.to_a, validate: validate | ||
| Service.import services.to_a, validate: validate | ||
| end | ||
|
|
||
| def process_trips(json, validate) | ||
| cities = City.pluck(:name, :id).to_h | ||
| buses = Bus.all.each_with_object({}) do |bus, memo| | ||
| key = [bus.number, bus.model] | ||
| memo[key] = bus.id | ||
| end | ||
| services = Service.pluck(:name, :id).to_h | ||
| buses_services = Set.new | ||
| trips = [] | ||
|
|
||
| json.each do |trip| | ||
| bus_key = [trip['bus']['number'], trip['bus']['model']] | ||
| bus_id = buses[bus_key] | ||
| trip['bus']['services'].each do |service_name| | ||
| service_id = services[service_name] | ||
| buses_services << { bus_id: bus_id, service_id: service_id } | ||
| end | ||
|
|
||
| trips << { | ||
| from_id: cities[trip['from']], | ||
| to_id: cities[trip['to']], | ||
| start_time: trip['start_time'], | ||
| duration_minutes: trip['duration_minutes'], | ||
| price_cents: trip['price_cents'], | ||
| bus_id: bus_id | ||
| } | ||
| end | ||
|
|
||
| BusesService.import buses_services.to_a, validate: false | ||
| Trip.import trips, validate: validate, batch_size: 1_000 | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,8 @@ | ||
| # Наивная загрузка данных из json-файла в БД | ||
| # rake reload_json[fixtures/small.json] | ||
| task :reload_json, [:file_name] => :environment do |_task, args| | ||
| json = JSON.parse(File.read(args.file_name)) | ||
| # frozen_string_literal: true | ||
|
|
||
| ActiveRecord::Base.transaction do | ||
| City.delete_all | ||
| Bus.delete_all | ||
| Service.delete_all | ||
| Trip.delete_all | ||
| ActiveRecord::Base.connection.execute('delete from buses_services;') | ||
| require 'populate_database' | ||
|
|
||
| json.each do |trip| | ||
| from = City.find_or_create_by(name: trip['from']) | ||
| to = City.find_or_create_by(name: trip['to']) | ||
| services = [] | ||
| trip['bus']['services'].each do |service| | ||
| s = Service.find_or_create_by(name: service) | ||
| services << s | ||
| end | ||
| bus = Bus.find_or_create_by(number: trip['bus']['number']) | ||
| bus.update(model: trip['bus']['model'], services: services) | ||
|
|
||
| Trip.create!( | ||
| from: from, | ||
| to: to, | ||
| bus: bus, | ||
| start_time: trip['start_time'], | ||
| duration_minutes: trip['duration_minutes'], | ||
| price_cents: trip['price_cents'], | ||
| ) | ||
| end | ||
| end | ||
| desc 'Loads datum from *.json dump file to DB' | ||
| task :reload_json, [:file_path] => :environment do |_task, args| | ||
| PopulateDatabase.call(file_path: args.file_path) | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| FROM perl:5.28-stretch | ||
| RUN apt-get update && apt-get install wget tar | ||
| RUN wget https://github.com/darold/pgbadger/archive/v11.0.tar.gz && tar xzf v11.0.tar.gz | ||
| RUN cd pgbadger-11.0/ && perl Makefile.PL && make && make install | ||
| CMD bash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍