2016-04-21 2 views
5

У меня есть Ruby-код, который более или менее выглядеть следующим образомРубин не отпуская память

offset = 0 
index = 1 

User.establish_connection(..) # db1 
class Member < ActiveRecord::Base 
    self.table_name = 'users' 
end 

Member.establish_connection(..) #db2 

while true 
    users = User.limit(10000).offset(offset).as_json ## for a Database 1 
    offset = limit * index 
    index += 1 
    users.each do |u| 
    member = Member.find_by(name: u[:name]) 
    if member.nil? 
     Member.create(u) 
    elsif member.updated_at < u[:updated_at] 
     member.update_attributes(u) 
    end 
    end 
    break if break_condition 
end 

Что я вижу в том, что память RSS (HTOP) продолжает расти, и в какой-то момент она достигает 10GB. Я не уверен, почему это происходит, но память, похоже, никогда не будет выпущена Ruby обратно в ОС.

Я знаю, что существует длинный список вопросов, связанных с этим. Я даже попытался изменить код, чтобы выглядеть так (последние 3 строки). I.e Запуск GC.start вручную приведет к тому же.

while true 

.... 
... 
... 
users = nil 
GC.start 
break if break_condition 
end 

Испытано это на Ruby, версии 2.2.2 и 2.3.0

EDIT: Другие детали

1) OS.

DISTRIB_ID=Ubuntu 
DISTRIB_RELEASE=15.04 
DISTRIB_CODENAME=vivid 
DISTRIB_DESCRIPTION="Ubuntu 15.04" 

2) рубин установлен и выполнен через rvm.

3) ActiveRecord версия 4.2.6

+1

'когда'? Вы имеете в виду 'while'? – matt

+1

'более или менее выглядят так, может быть, лучше показать точный код? – fl00r

+0

@ fl00r Точный код ожидает, что изменится класс или модель – Viren

ответ

2

Я не могу вам сказать, источник утечки памяти, но я подсмотреть некоторые низко висящие плоды.

Но первые две вещи:

  1. уверен, что ActiveRecord является правильным способом для копирования данных из одной базы данных в другую Вы? Я очень уверен, что это не так. У каждого крупного продукта базы данных есть надежные возможности экспорта и импорта, а производительность, которую вы увидите там, будет много, во много раз лучше, чем в Ruby, и вы всегда можете вызывать эти инструменты из своего приложения. Подумайте об этом, прежде чем продолжать этот путь.

  2. Откуда взялось номер 10 000? Ваш код подсказывает, что вы знаете, что неплохо получить все записей сразу, но 10 000 все еще много записей. Вы можете увидеть некоторые выгоды, просто попробовав разные цифры: 100 или 1000, скажем.

Тем не менее, давайте копаться в то, что эта линия делает:

users = User.limit(10000).offset(offset).as_json 

Первая часть, User.limit(10000).offset(offset) создает объект ActiveRecord :: связь, представляющий запрос. Когда вы вызываете на него as_json, выполняется запрос, который создает 100 000 объектов модели пользователя и помещает их в массив, а затем хэш создается из каждого из этих атрибутов объектов пользователя. (Посмотрите на источник для ActiveRecord::Relation#as_jsonhere.)

Другими словами, вы создаете объекты из 10 000 пользователей только для того, чтобы выбросить их после того, как у вас есть их атрибуты.

Итак, быстрый выигрыш - полностью пропустить эту часть. Просто выберите исходные данные:

user_keys = User.attribute_names 

until break_condition 
    # ... 
    users_values = User.limit(10000).offset(offset).pluck(user_keys) 

    users_values.each do |vals| 
    user_attrs = user_keys.zip(vals).to_h 
    member = Member.find_by(name: user_attrs["name"]) 
    member.update_attributes(user_attrs) 
    end 
end 

ActiveRecord::Calculations#pluck возвращает массив массивов со значениями из каждой записи.Внутри цикла user_values.each мы превращаем этот массив значений в хэш. Не нужно создавать экземпляры объектов пользователя.

Теперь давайте взглянем на это:

member = Member.find_by(name: user_attrs["name"]) 
member.update_attributes(user_attrs) 

Это выбирает запись из базы данных, создает объект-члены, а затем обновляет запись в базе данных-10000 раз в в каждой итерации цикла while , Это правильный подход , если вам нужны проверки для запуска при обновлении этой записи. Если вам не нужны валидации бежать, хотя, вы можете сэкономить время и память на, опять же, не инстанцирование любых объектов:

Member.where(name: user_attrs["name"]).update_all(user_attrs) 

Разница заключается в том, что ActiveRecord::Relation#update_all не выбирает запись из базы данных или экземпляра объект Member, он просто обновляет его. Вы сказали в своем комментарии выше, что у вас есть уникальное ограничение на столбец name, поэтому мы знаем, что это обновит только одну запись.

Сделав эти изменения, вы должны по-прежнему бороться с тем фактом, что вам нужно выполнить 10 000 запросов UPDATE на каждой итерации цикла while. Опять же, рассмотрите возможность использования встроенных функций экспорта и импорта баз данных вместо попыток сделать Rails этим.

+0

Спасибо за ответ. Извините, что копия в другую базу данных не так проста и поэтому не может использовать pg_import и pg_dump. – Viren

+0

У меня есть обновленный код, чтобы показать, как копировать работу. – Viren

+0

Тем не менее, есть лучшие способы сделать это. Вы в основном делаете [upsert] (http://stackoverflow.com/questions/17267417/how-to-upsert-merge-insert-on-duplicate-update-in-postgresql) с простым условием на 'updated_at' ,Если данные были в двух отдельных таблицах в одной и той же базе данных, вы могли бы сделать JOIN с тем же условием, чтобы получить строки для повышения. Поскольку они не находятся в одной базе данных, вы можете экспортировать и импортировать в таблицу с другим именем или использовать [postgres_fdw] (http://www.postgresql.org/docs/9.3/static/postgres-fdw. html), чтобы напрямую подключаться к другой базе данных. –

Смежные вопросы