Почему dry -структура инициализация медленная? - PullRequest
0 голосов
/ 02 апреля 2020

У меня есть приложение Rails, которое хранит свою конфигурацию в 34 MySQL таблицах, состоящих из различных объектов и ассоциаций, всего около 900 записей. До недавнего времени бизнес-логика c была построена на ActiveRecord, но производительность была нестабильной, потому что у меня не было достаточного контроля над количеством выполненных запросов. Недавно я «портировал» бизнес-логи c на Dry::Struct, продублировав все задействованные классы ActiveRecord на Dry::Struct модели значений и предварительно загрузив все объекты конфигурации внутри экземпляра Configuration: это резко сократило количество запросов до небольшое и фиксированное количество, а также улучшенная производительность с заметным запасом, потому что вся ассоциация «ходьба» выполняется в памяти, и ее много.

Пока все хорошо, но загрузка 34 таблиц, большинство из которых мне нужно при каждом запросе, все еще занимает 34 запроса и около 160 мс. В ActiveRecord стратегия работала на низком уровне, загружая все записи во всех таблицах в виде простых хэшей, а затем инициализируя структуры с этими и сохраняя все в объекте Configuration.

Я хотел еще больше повысить производительность Таким образом, у меня была идея собрать все данные одним запросом, сделав UNION из всех полей во всех таблицах. Это составляет громоздкий 30-килобайтный запрос SQL, который неожиданно выполняется всего за 20 мс. Отлично! Теперь мне просто нужно развернуть эту большую структуру за ~ 10 мс, и я победил!

Ну, нет. Оказывается, что простое сканирование массива результатов занимает 48 мс (20 из которых - запрос SQL), это время увеличивается до 78 мс при разборе некоторых полей JSON тут и там и подготовке хэшей для инициализации структур. .. и затем для инициализации этих операций требуется дополнительные 89 мс. Я бы не поверил, если бы я не измерял каждый шаг, повторяя алгоритм 100 раз (после предварительного подогрева запомненных значений, конечно), но это так. В целом, по сравнению с предыдущим, гораздо более простым алгоритмом загрузки каждой таблицы отдельно, выигрыш в производительности вообще отсутствует, хотя один запрос эффективен.

Вот как выглядит SQL:

SELECT a1, a2, NULL, NULL, NULL, NULL FROM table_a
UNION ALL
SELECT NULL, NULL, b1, b2, NULL, NULL FROM table_b
UNION ALL
SELECT NULL, NULL, NULL, NULL, c1, c2 FROM table_c

, которая дает «диагональную» структуру, подобную этой

"string", 3, NULL, NULL, NULL, NULL -- from table_a
"other string", 5, NULL, NULL, NULL, NULL -- from table_a
NULL, NULL, 1, "{\"json\":true}", NULL, NULL -- from table_b
NULL, NULL, 2, NULL, NULL, NULL -- from table_b
NULL, NULL, NULL, NULL, 7, 10 -- from table_c
NULL, NULL, NULL, NULL, 9, 51 -- from table_c

, затем следующий алгоритм разворачивает ее в исходные записи:

  def preload_all!
    ranges = self.class.preload_field_ranges.invert
    logger.measure_debug("Preloaded configuration") do
      ApplicationRecord.connection.execute(self.class.preload_query).each do |data|
        # finding where the first significant column is
        pos = data.index { |i| !i.nil? }
        # resolving the table name based on where the significant value was found, exiting early
        range, table_name = ranges.select { |k, v| break [ k, v ] if pos.in?(k) }
        v_class = self.class.tables_to_value_classes[table_name]
        values = data[range].map do |i|
          case i
          when String
            # horrible kludge to parse JSON fields, because I wasn't able to inspect AR
            # classes to ask them which fields are serialized, any help is appreciated
            case
            when i[0].in?([ "{", "[" ]) then JSON.parse(i)
            else i
            end
          else i
          end
        end
        # assignments are for clarity, doing these operations inline shaves about 10 ms
        ivar = "@#{table_name}"
        hash = self.class.ar_attribute_names[table_name].zip(values).to_h
        v_model = v_class.new(hash.merge(configuration: self))
        vhash = instance_variable_get(ivar) || {}
        instance_variable_set(ivar, vhash.tap { |h| h[v_model.id] = v_model })
      end
    end
    self
  end

Я пытался сжать код как насколько я мог, но просто удаление части v_class.new сокращает время вдвое. Есть ли место для улучшения?

В качестве примечания, загрузка объекта Marshal ed Закончено Configuration из Redis занимает всего 10 мс, но я хотел избежать использования Redis для предотвращения смещения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...