Пользователь
0,0
рейтинг
28 сентября 2012 в 17:47

Разработка → Молниеносный JSON в Ruby on Rails перевод tutorial

Вывод результата в JSON достаточно прост в Rails:

render json: @statuses

Это работает отлично, если нужно вывести небольшое количество записей. Но что случится, если нам потребуется вывести сразу 10'000 записей? Производительность серьезно просядет, а самыми затратными по времени окажутся сериализация JSON и операции с базой данных.



Включайте только необходимые атрибуты


Первое очевидное решение — генерировать JSON только с необходимыми нам атрибутами, т.е.:

render json: @statuses, methods: [:latitude, :longitude, :timestamp, :virtual_odometer]

Отфильтрованный JSON даст нам более 20% прироста производительности:

default    5.940000   0.080000   6.020000 (  6.094221)
attrs      4.820000   0.010000   4.830000 (  4.932337)


Делайте выборку только необходимых полей


Второе решение — забирать из базы не все, а только необходимые нам поля.

render json: @statuses.select([:latitude, :longitude, :timestamp, :virtual_odometer])

Это поможет нам избежать передачи огромного количества лишних данных из базы в приложение и даст нам 2х прирост скорости:

default    5.940000   0.080000   6.020000 (  6.094221)
attrs      4.820000   0.010000   4.830000 (  4.932337)
select     2.170000   0.020000   2.190000 (  2.222277)


Не инициализируйте объекты ActiveRecord, если это возможно


Давайте реализуем метод, который будет возвращать «молниеносный» массив хэшей вместо объектов ActiveRecord:

def self.lightning
  connection.select_all(select([:latitude, :longitude, :timestamp, :virtual_odometer]).arel).each do |attrs|
    attrs.each_key do |attr|
      attrs[attr] = type_cast_attribute(attr, attrs)
    end
  end
end

Это работает также как метод pluck, но возвращает массив хэшей, а не массив значений одного поля. Вызовем наш новый метод в контроллере:

render json: @statuses.lightning

Использование легковесных хэшей ускоряет создание JSON еще в 2 раза:

default    5.940000   0.080000   6.020000 (  6.094221)
attrs      4.820000   0.010000   4.830000 (  4.932337)
select     2.170000   0.020000   2.190000 (  2.222277)
lightning  1.120000   0.010000   1.130000 (  1.148763)


Используйте самый быстрый дампер JSON


На текущий момент доступно несколько библиотек JSON:
  • JSON — gem JSON по-умолчанию (+ C-расширения, поставляется вместе с Ruby 1.9)
  • Yajl — Yet Another JSON Library (автор Brian Lopez)
  • Oj — Optimized JSON (автор Peter Ohler)

Хорошая идея использовать самую быструю из них:

json       0.810000   0.020000   0.830000 (  0.841307)
yajl       0.760000   0.020000   0.780000 (  0.809903)
oj         0.640000   0.010000   0.650000 (  0.666230)

Так что мы выбираем Oj дампер:

render json: Oj.dump(@statuses.lightning, mode: :compat)

Обобщенные результаты теста:

   user     system      total        real
default    5.940000   0.080000   6.020000 (  6.094221)
attrs      4.820000   0.010000   4.830000 (  4.932337)
select     2.170000   0.020000   2.190000 (  2.222277)
lightning  1.120000   0.010000   1.130000 (  1.148763)
json       0.810000   0.020000   0.830000 (  0.841307)
yajl       0.760000   0.020000   0.780000 (  0.809903)
oj         0.640000   0.010000   0.650000 (  0.666230)
Перевод: Sergey Nartimov
Леонид Святов @Svyatov
карма
49,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (33)

  • +6
    Но что случится, если нам потребуется вывести сразу 10'000 записей?

    И вот тут сразу возникает вопрос: а зачем?
    • +5
      Ситуации разные бывают. Экспорт данных или передача данных на обработку в другой сервис, например.
      • 0
        Такие вещи надо делать стримингом.
        • +3
          Одно другому не мешает вроде как. Сам по себе стримминг не ускорит генерацию JSON.
          • 0
            Сам по себе стриминг сильно меняет требования к производительности, потому что у вас затраты «размазываются».
            • 0
              Здесь «производительность» измеряется во времени, поэтому как затраты не «размазывай» — времени меньше не потратится. Есть задача — отдать 10 тысяч объектов и сделать это максимально быстро. Чем поможет стриминг?
              • +1
                Тем, что у вас будет не «сначала забрали все из БД, потом упаковали в json», а «забрали запись из БД, упаковали ее в JSON», при этом первая запись пакуется в json в то время, пока вторая забирается из БД. А в реальности у вас еще есть время передачи по каналу, и тут тоже: если вы выплюнули первую запись сразу, а не когда все сконвертировалось, то в реальности в можете достигнуть результата быстрее.

                Такая вот простая параллельность.
                • +1
                  Такое решение тоже имеет право на жизнь, но превносит дополнительную сложность, как минимум. Во-первых, принимающая сторона тоже должна поддерживать стриминг. Во-вторых, это теория, а чтобы узнать что быстрее — нужны цифры из тестов. В-третьих, может стоять задача сделать моментальный снимок данных и тянуть данные по одной записи нельзя.

                  Ситуация бывают разные, поэтому и решения могут быть разными. Я не утверждаю, что решение в статье, которую я перевел, подходит всем. Но и стриминг не является в данном случае 100% альтернативой, которая будет лучшей заменой для любой подобной задачи, верно?
                  • –3
                    Во-первых, принимающая сторона тоже должна поддерживать стриминг.

                    Нет, потому что http его поддерживает.

                    В-третьих, может стоять задача сделать моментальный снимок данных и тянуть данные по одной записи нельзя.

                    Вообще-то, с точки зрения «моментальности» ничего не меняется — как вам БД отдавала данные по определенным критериям, так и отдает. Вопрос только в том, как и когда вы начинаете их обрабатывать.

                    Такое решение тоже имеет право на жизнь, но превносит дополнительную сложность, как минимум.

                    А решение из поста — не привносит?

                    Во-вторых, это теория, а чтобы узнать что быстрее — нужны цифры из тестов.

                    Бинго. Именно поэтому сначала надо спросить «зачем», потом найти ботлнек. Обычно большая часть проблем решается еще на первом шаге.
                    • +1
                      Нет, потому что http его поддерживает.

                      HTTP — это протокол, а принимающая сторона — ПО, поэтому протокол может быть и поддерживает, а вот принимающая сторона может и не поддерживать. Иначе откуда взялись костыли вроде Long polling и иже с ним для браузеров?

                      Вообще-то, с точки зрения «моментальности» ничего не меняется — как вам БД отдавала данные по определенным критериям, так и отдает. Вопрос только в том, как и когда вы начинаете их обрабатывать.

                      Вообще-то, очень даже меняется. Если данные в базе изменяются, например, 20 раз в секунду, то один запрос и 1000 запросов вернут совсем разные значения.

                      А решение из поста — не привносит?

                      Привносит минимум.
                      • +1
                        HTTP — это протокол, а принимающая сторона — ПО, поэтому протокол может быть и поддерживает, а вот принимающая сторона может и не поддерживать.

                        Вы не понимаете, как это работает. Я говорю банально о том, что, вместо того, чтобы сконвертировать весь массив в json в памяти (допустим, это 30 секунд), а потом выплюнуть его в http-поток, по которому он будет передаваться еще 30, можно сконвертировать первую запись (0.5с) и сразу ее выплюнуть (0.5с). Если клиент умеет так читать поток, он получит данные сразу (т.е., первые данные — через 1с). Если нет — у него соберется весь массив данных (через 30,5с вместо минуты). Никаких действий со стороны клиента при этом не нужно.

                        Если данные в базе изменяются, например, 20 раз в секунду, то один запрос и 1000 запросов вернут совсем разные значения.

                        Кто вам сказал, что будет 1000 запросов? Запрос-то один, просто обрабатывается по частям.

                        Привносит минимум.

                        Если что, правильно написанный стриминг приносит не больше.

                        • +1
                          Ок, т.е. все сводится к тому, что мы делаем все тоже самое, только отдаем данные по кускам? Как это кардинально меняет то, что написано в статье, не понимаю. Не надо делать выборку из базы только с нужными полями? Да нет, вроде как надо. Не надо использовать хэши вместо объектов ActiveRecord? Да нет, вроде не лишняя операция. Не стоит использовать альтернативные дамперы JSON? Возможно, но нужно тестить.

                          В сухом остатке — вся та же самая подготовка данных, но отдача стримингом. Так? Не совсем понятно зачем мы это так долго обсуждали :)
                          • 0
                            Нет, в сухом остатке тот факт, что если сделать выборку и сериализацию новых полей и стриминг, то альтернативные дампера и хэши уже могут быть и не нужными.

                            Я просто исповедую принцип наименьших изменений.
                          • 0
                            Произошла путаница с терминами.

                            Попробую объяснить псевдокодом:
                            Ваш подход:
                            var query = DB.exec_query(...);
                            var result = query.get_all();
                            var output_string = JSON.encode(result);
                            write_output(output_string);
                            


                            если «развернуть» методы, то увидим что под капотом происходит примерно следующее:

                            var query = DB.exec_query(...);
                            
                            while ( var row = query.get_next() ) {
                                  result.push(row);
                            }
                            foreach (res_row IN result ) {
                                 output_string = output_string + JSON.encode(res_row);
                            }
                            
                            write_output(output_string);
                            


                            lair предлагает такой подход:

                            var query = DB.exec_query(...);
                            
                            while ( var row = query.get_next() ) {
                                  write_output(
                                        JSON.encode( row );
                                  );
                            }
                            


                            Этот подход хорош ещё тем, что занимает меньше памяти.
                            Но! Часто его нельзя реализовать не ломая логику используемого фреймворка.

                            • 0
                              Не только меньше памяти, но и, как уже говорилось, выиграть в TTLB.
              • 0
                >Чем поможет стриминг?

                Не уверен, что стримминг поможет, но вот передача пачками — точно.
        • 0
          Стримингом? В RoR?
          • 0
            RoR действительно никак не позволяет это сделать?

            К сожалению, я не настолько хорошо с ним знаком.
            • 0
              Были какие-то потуги типа async-rails, но вообще придётся ждать 4.0 или держать по процессу на каждое соединение. По поводу, можно хотя бы по Thread'у, честно говоря, ничего сказать не могу, и в любом случае это адский костыль.
              Всё написанное ни в коем случае не относится в общем к Ruby, а только в частности к Rails. В Sinatra такое возможно, и очень просто, уже писал чуть ниже.
    • 0
      Ну можно перевернуть задачу — вывести по 10 записей но 10 000 раз. На маленьких задачах тоже есть смысл экономить если у вас много клиентов.
      • +2
        А вы уверены, что 10 000 раз по 10 записей оптимизируются так же, как обратное?

        Я вот не уверен совершенно.
  • +4
    Буквально на днях наткнулся на блог «How I Learned to Stop Using LINQ2SQL and Love nHibernate», перец пытается считать из базы «1.6 million records», жалуется на таймаут и говорит, что Linq2Sql дерьмо и всем надо срочно переходить на NHibernate.

    После прочтения этого поста на Хабре и того блога хочется биться головой о стол и кричать — «Люди!!! Человеки!!! На хера?!!! На хера вы это делаете???!!!!» Это ж как зуб пломбировать через жопу…
    • 0
      а что, если все же надо? ну ок, прям сегодня мне ставили задачу и надо было 113 000 записей выдать, чуть раньше, на прошлой неделе, было 1.13 млн. и?
      • +1
        Ну блин, кусками же! Кусками считывайте и обрабатывайте как вам надо. А считанные куски логируйте. И если вдруг где-то произойдет разрыв, то вам не придется начинать все сначала, а начнете с разорванного места.
  • –2
    Была 2 раза ситуация, когда JSON-запросы были медленные и большие. Решение было довольно простое. Перенести запрос на NodeJS… 38 строчек код ускорили отклик и получение данных в 4 раза.

    Я обожаю Rails, но бывают узкие места, которые можно очень просто перенести на NodeJS и никто не пострадает.
    • +2
      А можно вопрос — почему Node.js с его ужасной структурой, а не Sinatra, кроме того написанная на том же Ruby? (=
      • +1
        Вроде как sinatra нативно не поддерживает comet? И соответственно, вопросы производительности и архитектуры. Я бы лучше спросил почему node.js, а не erlang?)
        • 0
          JavaScript знают все Rails-разработчики. Не будет проблем при передаче проекта или введении нового человека.
        • +1
          EventMachine, faye например? (=
        • 0
      • –3
        У меня был опыт использования NodeJS, в отличии от Sinatra.
        И мне интуитивно кажется, что NodeJS быстрее отдаст запрос, чем любой Ruby-сервер…
        • 0
          Спасибо за пояснение. Наверное сказываются мои привычки, почему показалось странным. Но если нодой только JSON отдавать, без какой-то бизнес-логики — решение конечно не плохое.
        • +1
          Ваша интуиция вас подвела. EventMachine сопоставим по производительности с нодой.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.