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)
+27
11644
124
Svyatov 24,0 G+

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Попробую объяснить псевдокодом:
Ваш подход:
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
lair #
Не только меньше памяти, но и, как уже говорилось, выиграть в TTLB.
0
micbsv #
>Чем поможет стриминг?

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

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

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

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

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

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