Restic: Сравнение подходов к решению проблемы использования индексной памяти

Созданный на 20 дек. 2019  ·  55Комментарии  ·  Источник: restic/restic

ПРИМЕЧАНИЕ ДЛЯ РЕВЕРСЕРОВ КОДА / ОСНОВНЫХ УЧАСТНИКОВ

Пожалуйста, прочитайте этот комментарий для получения сводки изменений и структурированной информации о каждом из этих изменений. Это может быть лучшей отправной точкой , чем чтение всех комментариев в этом выпуске.

Опишите проблему

Как указано в #1988, потребление памяти индексом является одной из самых больших проблем с потреблением памяти restic. Суть в фактической реализации хранения индексов в памяти в internal/repository/index.go

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

В качестве отправной точки я открыл ветку index-alternatives , которая реализует следующие стратегии:

  • Стандарт: индекс, фактически используемый в restic
  • Перезагрузить: ничего не сохранять. Для любой операции с индексом перезагрузите файл и используйте реальную реализацию для временно созданного индекса.
  • Индекс с малым объемом памяти: храните только полные данные индекса для больших двоичных объектов дерева. Для больших двоичных объектов данных используйте набор IDSet только для сохранения имеющихся больших двоичных объектов данных. Это позволяет выполнять большинство операций с индексами. Для недостающих делаем как в Reload (хотя бы для блобов данных)
  • Bolt: использовать БД на диске через bbolt

Стратегия индекса может быть выбрана
restic -i <default|reload|low-mem|bolt>

Филиал считается незавершенным. Я добавил общий интерфейс FileIndex , который должен выполняться новыми стратегиями индексов. Кроме того, фактическая реализация просто загружает индексные файлы (файл за файлом) в стандартную Index struct , а затем перезагружает их в новую структуру данных, позволяющую очистить сборщик мусора. Вновь созданные индексные файлы не затрагиваются, поэтому эффект должен быть только для репозиториев с данными.

Обратите внимание, что большинство реализаций являются быстрыми и грязными и пропускают серьезную обработку ошибок, очистку и т. д.

Дополнительные стратегии индексирования можно легко добавить, добавив новую реализацию для FileIndex и добавив их в repository.go (где происходит загрузка) и cmd/restic/global.go (где флаг -i интерпретируется)

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

optimization

Самый полезный комментарий

@rawtaz Спасибо за ответ!

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

Я думаю, что для обсуждения полезно дать обзор изменений, которые я сделал, и причины, по которым я сделал каждое изменение. Реализация включает следующие изменения:

Серьезные изменения:

  • структура кодовой базы, а именно, какая функциональность находится в internal/repository и internal/index
  • структура определений интерфейсов в internal/restic
  • внутренняя структура MasterIndex, включая обработку «полных» индексных файлов
  • структура данных, связанная с индексным файлом
  • поддержка индексных файлов старого формата ( index_old.go )
  • новая структура данных индекса в памяти ( index_new.go )
  • изменение доступа к индексу, где это необходимо

Мелкие дополнительные изменения:

  • изменил Lookup и добавил LookupAll
  • заменено «Магазин» на StorePack РЕДАКТИРОВАТЬ: реализовано в # 2773
  • добавьте CheckSetKnown для обработки известных больших двоичных объектов во время резервного копирования. EDIT: реализовано в # 2773.
  • удалите blob IDSet из checker.go и замените его функциональностью индекса
  • использовать новую функциональность индекса в «списке больших двоичных объектов»

Основными изменениями являются IMO, ключ к достижению низкого потребления памяти, хорошей производительности и более чистого дизайна кода (по сравнению с фактической реализацией). Незначительные изменения — это быстрые победы, которые также могут быть независимыми PR. Однако, если предполагается, что они являются хорошими улучшениями, я бы предпочел оставить их в основном PR, иначе их, вероятно, придется реализовать либо дважды, либо придется отложить.

Структура кодовой базы

Я переместил большую часть функций, связанных с индексами, с internal/repository на repository/index . IMO, это делает структуру базы кода более четкой, а также четко разделяет, какой код находится в каком каталоге (в отличие от фактической реализации.

Вопрос к сопровождающим: вы признаете это принципиальное изменение?

Структура интерфейса

Я также разделил определения интерфейсов на internal/restic , чтобы было понятно, что относится к Index , а что к Repository . Это также позволяет более четко изменять интерфейсы в будущем.

Вопрос к сопровождающим: вы признаете это принципиальное изменение?

Структура MasterIndex

В настоящее время MasterIndex объединяет структуры данных, связанные с индексным файлом, и обычно перебирает все эти подструктуры для операций с индексами. Каждая из структур данных, связанных с индексным файлом, может быть полной (то есть достаточно полной для сохранения в качестве полного индексного файла в репозитории) или законченной (то есть уже представленной как индексный файл).

Я полностью изменил это поведение. Теперь MasterIndex использует основной индекс в памяти, в котором должно присутствовать большинство записей индекса, особенно все те, которые также присутствуют в виде индексных файлов в репозитории, и вторую «незавершенную» структуру данных, которая используется только для сохранения нового индекса. записей и вставляется в основной индекс в памяти после того, как содержимое записывается в виде индексного файла в репозиторий.

Вопрос к сопровождающим: вы признаете это принципиальное изменение?

Структура данных, связанная с индексным файлом

В настоящее время существует три определения структуры данных, связанных с индексным файлом: одно определено JSON в internal/index , одно определено JSON в internal/repository и оптимизировано для поиска в internal/repository .
Я решил использовать структуру данных JSON из internal/index и добавил необходимые методы для ее использования в MasterIndex (и полностью удалил реализации из internal/repository ). Это полностью исключает преобразование внутренних структур данных для сохранения индексных файлов во время резервного копирования. С другой стороны, операции поиска должны проходить по всем элементам индекса. Следовательно, очень важно, чтобы эта структура данных не содержала много записей во время поиска, т. е. регулярно вставлялась в основной индекс.

Вопрос к сопровождающим: вы признаете это принципиальное изменение?

Поддержка старого формата файла индекса

Поддержка новых форматов индексных файлов теперь передана на аутсорсинг index_old_format.go .

Вопрос к сопровождающим: вы признаете это принципиальное изменение?

Новая структура данных индекса в памяти ( index_new.go )

Очевидно, это самое большое изменение. В принципе для каждого типа больших двоичных объектов поддерживается один большой список больших двоичных объектов. Список отсортирован по идентификатору большого двоичного объекта, поэтому поиск можно выполнить с помощью двоичного поиска. Поскольку оптимизация нацелена на множество записей в этом списке, размер каждой записи максимально сведен к минимуму. Это означает, что идентификатор пакета хранится в отдельной таблице, и сохраняется только ссылка, а смещение и длина составляют uint32 (фактически ограничивая максимальный размер пакета до 4 ГБ). РЕДАКТИРОВАТЬ: эти две оптимизации реализованы в #2781.
Простой отсортированный массив уже экономит накладные расходы памяти, которые находятся в пределах map , но по-прежнему генерируют некоторые накладные расходы памяти и производительности в пределах append : дополнительный «буфер» зарезервирован для будущих добавлений, и данные должны быть скопировано. Поэтому я реализовал «выгружаемый» массив с небольшими и ограниченными накладными расходами памяти и без необходимости копировать для добавления.

Проведенные тесты показывают, что этот индекс немного быстрее, чем стандартная реализация MasterIndex, сохраняя при этом более 75% памяти.

Вопрос к сопровождающим: соответствует ли эта реализация индекса вашим ожиданиям?

Изменено Lookup и добавлено LookupAll

Lookup на самом деле возвращает более одного результата (но не в том случае, если они распределены по многим индексным файлам, кстати). Во многих случаях используется только первый результат. Поэтому я изменил Lookup , чтобы он возвращал только один результат, и добавил LookupAll , который будет использоваться, когда используется более одного результата. Делает код немного чище, а также экономит ненужные циклы процессора.

Вопрос к сопровождающим: Можно ли сохранить это изменение или вы бы предпочли удалить его из PR индекса?

«Магазин» заменен на StorePack

Store хранит в индексе только один большой двоичный объект, но вызывается только для содержимого всего пакета. Поэтому я изменил его на функциональность StorePack , при которой сохраняется весь контент пакета. Это делает код более понятным, хранение в формате JSON намного проще, а также более производительным.

Вопрос к сопровождающим: Можно ли сохранить это изменение или вы бы предпочли удалить его из PR индекса?

добавьте CheckSetKnown для обработки известных больших двоичных объектов во время резервного копирования

Тесты показали, что для многих новых файлов backup использует довольно много памяти для хранения больших двоичных объектов, добавленных во время этого резервного копирования. Это делается для того, чтобы не сохранять большие двоичные объекты дважды с помощью параллельных заставок. Я переместил эту функцию в индекс, так как эта информация об «известных больших двоичных объектах» может быть удалена после добавления соответствующей записи в индекс. С этим изменением только очень немногие BLOB-объекты «известны», но (пока) не включены в индекс.

Вопрос к сопровождающим: Можно ли сохранить это изменение или вы бы предпочли удалить его из PR индекса?

Удалите IDSet blob из checker.go и замените его функциональностью индекса.

Средство проверки создает собственный индекс в памяти и дополнительно сохраняет все идентификаторы больших двоичных объектов в дополнительном наборе идентификаторов. Поскольку все большие двоичные объекты уже известны индексу, его можно заменить методом индекса Each .
Это экономит дополнительную память в пределах check .

Вопрос к сопровождающим: Можно ли сохранить это изменение или вы бы предпочли удалить его из PR индекса?

Используйте новую функциональность индекса в «списке BLOB-объектов»

Вместо того, чтобы считывать весь индекс в память, а затем перебирать все записи для печати списка больших двоичных объектов, каждый файл индекса можно загрузить в память отдельно и просмотреть это содержимое в цикле. Делает list blobs гораздо менее требовательным к памяти.

Вопрос к сопровождающим: Можно ли сохранить это изменение или вы бы предпочли удалить его из PR индекса?

Все 55 Комментарий

Мне нравится начинать с настройки, которую я использовал для тестирования во время реализации:

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

#!/bin/sh
mkdir data
for i in `seq 1 1000`; do
        echo $i
        mkdir data/$i
        for j in `seq 1 100`; do
                echo $i$j > data/$i/$j
        done
done

Это создает 100 000 небольших разных файлов в 1000 каталогах. Следовательно, это должно дать много больших двоичных объектов данных и еще довольно много двоичных объектов дерева. Можно изменить, чтобы имитировать еще больше файлов.

Я запустил первоначальную резервную копию

restic -r /path/to/repo init
restic -r /path/to/repo backup /path/to/data

и сделал повторный запуск резервного копирования с неизмененными данными для всех стратегий индексирования:

/usr/bin/time restic -r /path/to/repo -i <strategy> backup /path/to/data

помимо измерения времени я также добавил профилирование памяти.
Вот результаты:

Стандартный индекс

Files:           0 new,     0 changed, 100000 unmodified
Dirs:            0 new,     0 changed,     2 unmodified
Added to the repo: 0 B  

processed 100000 files, 567.676 KiB in 0:08
snapshot d7a3f88b saved
13.14user 1.71system 0:11.16elapsed 133%CPU (0avgtext+0avgdata 90564maxresident)k
0inputs+64outputs (0major+1756minor)pagefaults 0swaps

(pprof) top  
Showing nodes accounting for 15.21MB, 97.56% of 15.59MB total
Dropped 84 nodes (cum <= 0.08MB)
Showing top 10 nodes out of 59
      flat  flat%   sum%        cum   cum%
   12.52MB 80.26% 80.26%    12.52MB 80.26%  github.com/restic/restic/internal/repository.(*Index).store
    1.01MB  6.46% 86.72%     1.01MB  6.50%  github.com/restic/chunker.NewWithBoundaries
    0.39MB  2.52% 89.23%     0.39MB  2.52%  reflect.New
    0.36MB  2.29% 91.53%     0.37MB  2.34%  github.com/restic/restic/internal/restic.NodeFromFileInfo
    0.29MB  1.89% 93.41%     1.10MB  7.05%  github.com/restic/restic/internal/archiver.(*Archiver).SaveDir
    0.28MB  1.80% 95.22%     0.28MB  1.80%  bytes.makeSlice
    0.15MB  0.94% 96.15%     0.15MB  0.94%  github.com/restic/restic/internal/archiver.(*TreeSaver).Save
    0.09MB   0.6% 96.76%     0.09MB   0.6%  strings.(*Builder).grow
    0.08MB   0.5% 97.26%     0.08MB   0.5%  github.com/restic/restic/internal/restic.BlobSet.Insert
    0.05MB   0.3% 97.56%     0.38MB  2.46%  encoding/json.Marshal

Здесь индекс уже является огромной частью памяти. Вы также можете видеть, что время сообщает об использовании 90 МБ. Это IMO настройка сборщика мусора, о которой уже говорилось.

Перезагрузить индекс

Кажется, это работает. Однако он показал ETA около 20 минут при постоянном использовании ЦП на 100% ... Это можно легко объяснить, поскольку он постоянно перечитывает (и расшифровывает!) Индексные файлы.
Однако я считаю, что эта индексная стратегия не заслуживает более глубокого изучения....

Индекс нехватки памяти

<snip: same output as above>
processed 100000 files, 567.676 KiB in 0:08
snapshot f8822cee saved
13.47user 2.03system 0:11.90elapsed 130%CPU (0avgtext+0avgdata 109584maxresident)k
0inputs+72outputs (0major+1945minor)pagefaults 0swaps

(pprof) top
Showing nodes accounting for 7.68MB, 94.71% of 8.11MB total
Dropped 88 nodes (cum <= 0.04MB)
Showing top 10 nodes out of 63
      flat  flat%   sum%        cum   cum%
    4.84MB 59.68% 59.68%     4.84MB 59.68%  github.com/restic/restic/internal/restic.IDSet.Insert
    1.01MB 12.50% 72.18%     1.02MB 12.58%  github.com/restic/chunker.NewWithBoundaries
    0.41MB  5.05% 77.23%     0.41MB  5.05%  reflect.New
    0.35MB  4.26% 81.48%     0.35MB  4.26%  github.com/restic/restic/internal/restic.NodeFromFileInfo
    0.28MB  3.47% 84.95%     0.28MB  3.47%  bytes.makeSlice
    0.27MB  3.31% 88.26%     1.01MB 12.46%  github.com/restic/restic/internal/archiver.(*Archiver).SaveDir
    0.19MB  2.33% 90.60%     0.19MB  2.33%  github.com/restic/restic/internal/archiver.(*TreeSaver).Save
    0.18MB  2.22% 92.81%     4.94MB 60.93%  github.com/restic/restic/internal/repository.(*IndexLowMem).store
    0.08MB  0.96% 93.78%     0.08MB  0.96%  github.com/restic/restic/internal/restic.BlobSet.Insert
    0.08MB  0.93% 94.71%     0.40MB  4.92%  encoding/json.Marshal

Общий объем памяти, сообщаемый профилировщиком, уменьшен вдвое! Более того, набор данных индекса размером 12,5 МБ заменяется набором IDSet размером менее 5 МБ. По моим оценкам, при таком подходе потребность в памяти для индексов может быть снижена примерно до 40% !
Компромиссом является немного более высокое общее время резервного копирования (13,5 с по сравнению с 13,1 с со стандартным индексом).
Мне действительно интересно, как эта индексная стратегия работает в других тестовых условиях!

Индекс болта - первый запуск

<snip: same output as above>
processed 100000 files, 567.676 KiB in 0:30
snapshot 9ff3d71f saved
37.01user 3.25system 0:34.54elapsed 116%CPU (0avgtext+0avgdata 282816maxresident)k
0inputs+114096outputs (0major+5436minor)pagefaults 0swaps

(pprof) top
Showing nodes accounting for 2592.31kB, 90.31% of 2870.50kB total
Dropped 79 nodes (cum <= 14.35kB)
Showing top 10 nodes out of 81
      flat  flat%   sum%        cum   cum%
 1030.83kB 35.91% 35.91%  1037.16kB 36.13%  github.com/restic/chunker.NewWithBoundaries
  390.02kB 13.59% 49.50%   390.02kB 13.59%  reflect.New
  278.60kB  9.71% 59.20%   278.60kB  9.71%  github.com/restic/restic/internal/restic.NodeFromFileInfo
  274.66kB  9.57% 68.77%   933.59kB 32.52%  github.com/restic/restic/internal/archiver.(*Archiver).SaveDir
  244.05kB  8.50% 77.27%   244.05kB  8.50%  bytes.makeSlice
  169.81kB  5.92% 83.19%   169.81kB  5.92%  github.com/restic/restic/internal/archiver.(*TreeSaver).Save
      80kB  2.79% 85.98%       80kB  2.79%  github.com/restic/restic/internal/restic.BlobSet.Insert
   44.17kB  1.54% 87.52%   292.20kB 10.18%  github.com/restic/restic/internal/archiver.(*TreeSaver).save
   40.16kB  1.40% 88.91%    40.16kB  1.40%  strings.(*Builder).grow
      40kB  1.39% 90.31%       40kB  1.39%  github.com/restic/restic/internal/restic.NewBlobBuffer

Он также создает файл my.db размером 72 МБ, в котором сохраняется индекс.

Как и ожидалось, потребление памяти практически исчезло!
Однако это связано с большим компромиссом во времени резервного копирования (37 с по сравнению с 13,1 с со стандартным индексом).
Я думаю, было бы интересно посмотреть, как это работает с большими наборами данных. Не знаю, добавляет ли эта проблема производительности коэффициент примерно в 3 раза или даже растет больше, чем линейно.
Более того, мне кажется, что гораздо больше тестов и тонкой настройки следует рассматривать со стратегиями индексов на основе хранилища...

Индекс болта - второй проход

Поскольку в настройках болта база данных не удаляется после запуска, я перезапустил уже существующую базу данных:

processed 100000 files, 567.676 KiB in 0:08
snapshot b364fba4 saved
14.26user 1.94system 0:12.07elapsed 134%CPU (0avgtext+0avgdata 133464maxresident)k
0inputs+88outputs (0major+2601minor)pagefaults 0swaps

Профиль памяти почти идентичен первому запуску, как и ожидалось.
Мне действительно интересно, почему этот второй запуск намного быстрее, чем первый. Это кеш фс сейчас работает? Или же BoltDb каким-то образом использует информацию из уже сохраненного индекса для ускорения работы?

Как правило, это может указывать на то, что с индексом на основе хранилища нам, возможно, придется подумать о постоянном хранимом индексе, потому что он может значительно повысить производительность!

Возможно, стоит попробовать хранилище ключей/значений на основе LSM. LSM имеют совершенно другие компромиссы по производительности и дисковому пространству по сравнению с BTrees и могут работать лучше для варианта использования постоянного индекса. Я бы лично использовал RocksDB , но, вероятно, есть и достаточно хорошие магазины Go-native.

Спасибо за прототип! Выглядит очень обнадеживающе.

Индексные файлы в настоящее время зашифрованы, поэтому любое дисковое хранилище, которое мы используем, также должно быть зашифровано. Я посмотрел на https://github.com/dgraph-io/badger — он нативен для Go, основан на LSM и поддерживает шифрование.

Низкий индекс памяти хорошо подходит для резервного копирования. Я не уверен, что он будет масштабироваться для восстановления из репозиториев с огромным количеством больших двоичных объектов.

Спасибо за обсуждение.
Должен признать, что я не тратил много времени на опробование других вариантов индексов на основе хранилища. (барсук все еще в моем списке дел)
Причина в следующем: я вижу, что возникнут некоторые проблемы, если серьезно попытаться привести это в состояние производственного уровня. Я вижу проблемы с шифрованием, как очистить файлы и как справиться с прерванными операциями и так далее.

Вот почему я начал думать о другой возможности оптимизации индекса в памяти. Результат уже есть в моей ветке index-alternative и назван low-mem2. Идея состоит в том, чтобы хранить все данные индекса в отсортированных таблицах (сохраняя накладные расходы на карту), стараясь не хранить ничего дублированного. Например, идентификатор пакета сохраняется в отдельной таблице и упоминается только в основной таблице.
Я пока не проводил много тестов, но кажется, что производительность немного хуже, чем у стандартного индекса. С другой стороны, потребление памяти сравнимо с низким потреблением памяти, то есть экономия около 60% по сравнению со стандартным индексом в моих тестовых настройках, см. выше.
Учитывая, что для большинства операций (например, резервного копирования) требуется только конкретная информация об индексе, это можно дополнительно улучшить.

Итак, я бы предложил следующее: я попытаюсь сделать PR, чтобы заменить (или опционально заменить) стандартный индекс на оптимизированный для памяти (low-mem2). Мы надеемся, что это быстро улучшит проблему с памятью.
Таким образом, в будущем открываются возможности использования дискового индекса.

Что вы думаете об этом? Может быть, кто-то из основных сопровождающих может подсказать мне правильное направление для работы. Спасибо!

Сокращение использования памяти вдвое кажется мне разумным краткосрочным решением. В настоящее время я немного беспокоюсь о возможном снижении производительности при сохранении данных в отсортированном массиве. Текущая реализация MasterIndex просто перебирает список, содержащий индексы. Для больших репозиториев с несколькими тысячами индексных файлов restic тратит большую часть времени на резервное копирование/проверку с повторением различных индексов, см. #2284.

Что касается вашей ветки с низким уровнем памяти: меня немного смущают функции SortByBlob и SortByPack. Разве нельзя просто отсортировать по blobID, а затем просто выполнить итерацию по всему индексу для поиска на основе packID (как это делается в текущей реализации)? (Я бы предпочел полностью избавиться от блокировки после завершения индекса)
В настоящее время код может просто преобразовать существующий индекс в новый формат. Было бы неплохо, если бы он мог преобразовать окончательный индекс в более эффективный формат, чтобы получить улучшенное потребление памяти также во время первоначального резервного копирования.

@MichaelEischer : Спасибо, что указали на эту проблему в masterindex. Насколько я вижу, существует три возможных способа использования индекса:

  1. читать из индексных файлов, а затем только для чтения
  2. созданный во время выполнения, а затем доступный только для чтения
  3. Операции по изменению индекса (это только очистка/восстановление)

Поскольку 1. является основным вариантом использования, а 2. очень похож, я согласен, что мы должны оптимизировать для этого. Я бы предложил также изменить способ работы основного индекса и разрешить заполнение структуры индекса данными индекса многих прочитанных индексных файлов (а также для индексных файлов, созданных и затем используемых только для чтения).

О низком мем2:

  • Я добавил SortByBlob и SortByPack, так как есть вариант использования отсортированных пакетов. Но я также согласен, что мы должны попытаться разблокировать параллельные операции над индексом. Насколько я понимаю, выбор между поиском по блобу или поиском по пакету определяется основной командой. Следовательно, даже если мы делаем блокировку для переадресации, это будет эффективно неблокировать для основной операции. И, конечно же, мы должны использовать RWMutex.
    Возможно, поиск пакетов в списке, отсортированном по блобам, тоже подойдет, просто нужно немного проверить.
  • О соображениях производительности. Тот факт, что restic на данный момент просто линейно читает все индексные файлы, вселяет в меня уверенность в том, что один отсортированный список больших двоичных объектов всех индексных файлов и поиск пополам могут даже превзойти текущую реализацию для больших индексов, то есть для многих индексных файлов ;-)

Итак, как мы продолжим? Как уже упоминалось, я могу подготовить PR для замены текущей реализации index/masterindex на оптимизированную.
@MichaelEischer Я был бы признателен за ранние отзывы и предложения по улучшению - могу ли я дать вам некоторые первые части работы для обзора? Может быть, мы также найдем людей, тестирующих изменения в отношении производительности и использования памяти для реальных сценариев...

@aawsome Извините за поздний ответ. Я мог бы просмотреть первые части работы, однако было бы лучше получить некоторые комментарии по общему дизайну от основных сопровождающих. Для тестирования у меня есть репозиторий объемом 8 ТБ с 40 миллионами файлов; Для моих тестов производительности индекса я до сих пор использовал команду check , которая сильно нагружает индекс.

MasterIndex используется для случаев 1 и 2. Объединение всех индексных файлов в памяти для статического индекса, которого достаточно для случая 1, должно быть довольно легко реализовать. Один отсортированный список определенно будет быстрее, чем текущий индекс, как только он достигнет определенного количества индексных файлов O(log n) против O(n) . Динамическое добавление полных/завершенных индексных файлов (случай 2) нелегко сочетается с одним отсортированным списком больших двоичных объектов, поскольку к нему нужно будет обращаться после каждого добавленного индекса. Полное использование индекса с сотнями миллионов записей несколько раз кажется мне довольно плохой идеей. [EDIT] Простое объединение двух уже отсортированных списков также работает и в основном просто составляет полную копию существующего списка. С сотнями миллионов записей это, скорее всего, все еще занимает много времени [/EDIT] . Динамическое расширение такого списка также потребует некоторого способа освободить место для новых записей без необходимости хранить в памяти полную копию старого и нового буфера списка.

Альтернативы отсортированному списку, которые приходят мне на ум прямо сейчас, это какое-то дерево или хэш-карта. Для первого, вероятно, потребуются дополнительные указатели в структуре данных, а второе будет довольно похоже на текущую реализацию. Я думал о замене MasterIndex одной большой хэш-картой, но прямо сейчас стратегия увеличения резервного буфера хэш-карты удерживала меня от попытки: Go удваивает размер резервного буфера при достижении определенного коэффициента загрузки, что в конце концов может потребоваться временное хранение довольно пустого нового резервного буфера вместе со старым в памяти. Этот сценарий, вероятно, потребует гораздо больше памяти, чем текущая реализация. Это должно быть решено с помощью двух уровней хэш-карт, где первый индексируется с использованием первых нескольких битов идентификатора большого двоичного объекта. Я просто не уверен, как эффективно обрабатывать идентификаторы больших двоичных объектов, которые распределены неравномерно и, следовательно, могут привести к большим дисбалансам в хэш-картах второго уровня. Возможно, стоит взглянуть на индексные структуры данных, используемые базами данных или borg-backup.

Случай 3 (удалить/перестроить-индекс/найти) сейчас обрабатывается index/Index.New , поэтому это не должно конфликтовать с изменениями в MasterIndex.

Методы ListPack в Index/MasterIndex выглядят для меня мертвым кодом, restic все еще компилируется, когда я удаляю этот метод (только тест Index требует некоторых настроек). Единственные варианты использования метода ListPack , которые мне удалось найти, относятся к структуре Repository . Это сделало бы SortByPack ненужным и позволило бы избежать сложностей, связанных с пересортировкой.

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

@aawsome Итак, сейчас основное внимание будет уделено оптимизации использования памяти индексом наряду с производительностью masterIndex, при этом откладывая фундаментальные изменения, такие как использование базы данных на диске, на потом?

Относительно случая 3: Планируете ли вы также заменить index/Index , сгруппированные по packID? Мое текущее понимание кода restic состоит в том, что это будут единственные потенциальные пользователи ListPack .
Прямо сейчас я могу найти только два активных использования index/Index.Packs :

  • cmd_list/runList , которому просто нужен какой-то список больших двоичных объектов и на самом деле не заботится о сортировке
  • cmd_prune/pruneRepository , который использует его для статистики, перебирает все большие двоичные объекты, чтобы найти дубликаты и принять решение, какой пакет переписать или удалить. Только последнего использования немного сложнее избежать, так как потребуются дополнительные хэш-карты для отслеживания использованных больших двоичных объектов в упаковке.

Ваш запрос на слияние «Оптимизировать индекс» добавит еще одно использование, которое на самом деле требует индекса, сгруппированного по файлам пакета.

Что касается оптимизированного MasterIndex: мой последний комментарий в основном касался использования одной большой структуры данных индекса, в которой, вероятно, потребуется использовать разбиение по ключу, чтобы избежать больших всплесков использования памяти при добавлении новых записей. У такого способа разбиения есть недостаток: добавление нового индекса, скорее всего, потребует касания каждого раздела. Кроме того, дисбаланс размера разделов может вызвать дополнительные проблемы.

Однако есть еще один вариант (который немного вдохновлен деревьями слияния, структурированными журналами): оставить количество записей в MasterIndex примерно постоянным на уровне c. Это будет означать, что новый индекс после окончательной доработки будет объединен с одной из существующих частей индекса. Таким образом, за один раз изменяется только часть индекса. Для хэш-карт это можно использовать для поддержания довольно высокого коэффициента загрузки, поскольку каждая часть будет заполнена перед использованием следующей. Для отсортированных массивов более разумной кажется другая стратегия: объединить части индекса в более крупные с примерно 1k, 2k, 4k, 8k,... элементами, объединяя при этом самый большой в настоящее время класс размера только после того, как определенная константа частей индекса с этим размером существует. Это должно обеспечить сложность O(log n) для постепенного добавления новых индексных файлов. В зависимости от константы c либо добавление новых индексных файлов является довольно дешевым (для небольших частей индекса), либо быстрый поиск (небольшое количество частей индекса).

@aawsome Итак, сейчас основное внимание будет уделено оптимизации использования памяти индексом наряду с производительностью masterIndex, при этом откладывая фундаментальные изменения, такие как использование базы данных на диске, на потом?

Да, это мой фокус. Мы можем обсудить добавление параметров команды резервного копирования только для сохранения больших двоичных объектов дерева в индексе (и, таким образом, глупое сохранение каждого нового двоичного объекта в моментальном снимке) или, возможно, только для загрузки больших двоичных объектов данных, использованных в предыдущем снимке, в индекс (и, таким образом, только с сохранением). дедупликация больших двоичных объектов в предыдущих моментальных снимках). Это позволит клиентам с малым объемом памяти выполнять резервное копирование в большие репозитории с последующим сокращением операций на машинах с большим объемом памяти для полной дедупликации.
Я отложу свою работу над дисковым индексом.

Относительно случая 3: Планируете ли вы также заменить index/Index, который сгруппирован по packID?

Я думаю, что это возможно и должно быть целью новой реализации. Я, однако, не анализировал это подробно - спасибо за ваши подсказки!

О деталях реализации: мне нужно немного времени, чтобы прочитать и подумать об этом. Большое спасибо за ваши идеи и поддержку!

Как было объявлено, я подготовил раннюю версию рефакторинга индекса, см. refactor-index
Также некоторые внутренние элементы немного изменены, чтобы использовать новый индекс. Например, для check теперь требуется гораздо меньше памяти, чем раньше...

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

  • Исправление тестов для индекса (эта часть еще не компилируется)
  • рефакторинг rebuild-index и prune для использования нового MasterIndex и нового индекса в памяти
  • тесты производительности

Реализация использует один большой (отсортированный) массив для хранения индекса и формат IndexJSON для хранения элементов, созданных в текущем запуске.
При резервном копировании IndexJSON регулярно сохраняется в репозиторий и добавляется в основной индекс в памяти.
При загрузке индекса из репозитория каждый файл индекса загружается в формате IndexJSON , а затем добавляется к основному индексу в памяти.

Следовательно, ключевая функция равна AddJSONIndex в internal/index/index_new.go , начиная со строки 278.

О ваших комментариях @MichaelEischer Я не могу реально оценить влияние добавления в индексную таблицу, см. Строку 329 в internal/index/index_new.go . Означает ли это, что вся таблица должна регулярно копироваться (вы упомянули «всплески памяти»)? Или это хорошо обрабатывается управлением памятью в go и в операционной системе?

Также после вставки таблицу необходимо снова отсортировать (по крайней мере, перед поиском в ней в первый раз). Я предполагаю, что текущая реализация должна избегать ненужных операций сортировки, но для этого нужны тесты производительности. Кто-нибудь может помочь с этими тестами?

@aawsome :

Кто-нибудь может помочь с этими тестами?

Я готов помочь с тестированием производительности. Что именно вы хотите протестировать?

@dimejo :
Спасибо большое за помощь!

Я бы предпочел сравнение 1:1 (т.е. одинаковый запуск с обеими ветками) между основной веткой (или последним выпуском) и моей веткой.
Меня интересуют различия в использовании памяти и производительности (общее время и загрузка процессора) для больших репозиториев (много файлов, много снимков и т. д.). Использование /usr/bin/time -v допустимо, возможно, включение профилирования (профилирование памяти и профилирование процессора) может помочь лучше объяснить различия.
Бэкэнд вообще не имеет значения - локальный бэкэнд в порядке, но не обязателен.

Я думаю, что следующие команды должны быть хотя бы протестированы:

  • backup 1. начальный запуск 2. другой запуск с (почти) неизмененными файлами; 3. еще один прогон с --force
  • check
  • restore
  • может быть mount
  • list blobs
  • cat blob
  • find --blob

[править: добавлены команды list, cat и find]

Пожалуйста, напишите, если вы не понимаете, что я имею в виду, или если вам нужны более подробные инструкции, как запустить эти тесты!

Я постараюсь провести все тесты в эти выходные, но мне нужна помощь в включении профилирования. Я не программист, и советы из интернета не сработали :/

Вы можете начать с
/usr/bin/time -v restic <command>

Чтобы включить профилирование, сначала скомпилируйте restic с параметром debug :
go run build.go --tags debug
а затем используйте флаг --mem-profile или --cpu-profile , например:
restic --mem-profile . <command>

Это создает файл .pprof , который можно проанализировать с помощью go tool pprof , например:
go tool pprof -pdf mem.pprof restic
возвращает хороший PDF.

Потрясающий! Я думал, что включить профилирование будет намного сложнее. Попробую и отчитаюсь.

@aawsome Спасибо за вашу работу, действительно много изменений.

Я сделал несколько пробных запусков с помощью команды check , и произошло нечто странное. При использовании restic master команда выполняется за 6 минут (327,25 реальных 613,08 пользовательских 208,48 системных), с изменением индекса это занимает более часа (4516,77 реальных 896,17 пользовательских 531,71 системных). Время где-то тратится после печати "проверить снэпшоты, деревья и блобы", а рассмотреть поближе пока не успел.

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

  • Index.Lookup больше не возвращает список, а вместо этого просто большой двоичный объект.
  • Много кода обработки индекса извлекается из репозитория.
  • masterIndex хранит только один незавершенный индекс
  • Фактический низкий индекс памяти

У меня есть некоторые (неполные) комментарии к самому коду, но я еще не слишком много думал об общей архитектуре кода:

struct IndexLowMem2.byFile : мне потребовалось довольно много времени, чтобы понять имя переменной. Пожалуйста, измените его на byPackFile или аналогичный, чтобы он был более описательным.

IndexLowMem2/FindPack : Насколько я понимаю, restic.IDs.Find предполагает наличие отсортированного списка идентификаторов. Однако idx.packTable , похоже, не отсортирован, так как это нарушит packIndex в blobWithoutType .

Состояние гонки в index_new/FindBlob : индекс может использоваться между вызовом SortByBlob и повторным получением блокировки чтения. В настоящее время я не вижу хорошего способа, как это можно исправить при использовании RWMutex. Та же проблема существует и в FindBlobByPack .
IndexLowMem2/Lookup : вызов FindBlob и доступ к blobTable не являются атомарными.

IndexLowMem2/AddJSONIndex : Вызовы добавления для среза выделяют новый буфер, когда старый слишком мал, а затем копируют все данные в новый буфер. Поскольку метод сначала собирает новые записи packTable/indexTable, а затем добавляет их за один раз, это не должно быть слишком неэффективным.
Таким образом, срезы $ packTable в массиве idx.byFile будут указывать на старые версии массива packTable , когда добавление выделяет новый буфер. Это, вероятно, приведет к потере большого количества памяти для больших packCounts . Вы можете просто добавить endPackIndex в FileInfo в дополнение к startPackIndex , а затем получить прямой доступ к packTable .
Добавление к it.blobTable выглядит немного дорого, так как позже потребуется сортировка. Должна быть возможность предварительно отсортировать новые записи table , а затем просто объединить два отсортированных списка (однако слияние на месте может быть немного сложным).

MasterIndex.Lookup/Has/Count/... : после поиска в mi.mainIndex mi.idx можно сохранить и добавить к mainIndex до получения idxMutex .

MasterIndex.Save : Кажется, это блокирует masterIndex , пока ожидается загрузка индекса. Старая реализация в Repository.SaveIndex работает без блокировки.

А, я нашел проблему: прямо сейчас вызов PrepareCache закомментирован. PrepareCache также настраивает функцию PerformReadahead кэша. Без этого Check должен загружать каждый большой двоичный объект данных дерева отдельно от серверной части.

@MichaelEischer : Большое спасибо за ваши очень ценные комментарии на этом раннем этапе реализации: +1:

Я знаю, что это довольно много изменений в коде, однако я думаю, что это также упрощение текущей реализации индекса. Отсюда и большое количество удаленных файлов/строк кода.

Извините за проблему с PrepareCache — я помню, что закомментировал ее, чтобы справиться с ней позже, но до сих пор я проводил только локальные внутренние тесты, и поэтому проблема не возникла.

По поводу ваших правильных комментариев по поводу byPack : Вы правы, это WIP и еще не используется. Я оставил его, потому что собирался использовать его позже для rebuild-index и prune . Тем не менее, я сначала сосредоточусь на том, чтобы заставить работать основной вариант использования — возможно, я удалю эту часть, прежде чем делать PR.

О сортировке: Сейчас предполагается, что код выполняет сортировку только тогда, когда это необходимо, т.е. при первом вызове Lookup или Has . Это означает, что при загрузке большого количества индексных файлов в индекс в памяти (что является обычной операцией в начале) сортировка не выполняется. Это происходит только тогда, когда все индексные файлы уже загружены. Во время резервного копирования — когда записывается больше индексных файлов — записи добавляются в индекс в памяти, а затем каждый раз, когда требуется курорт. Однако это курорт в основном отсортированного списка. Я не знаю, как sort.Sort обрабатывает почти отсортированный список, но обычно используются алгоритмы сортировки, которые очень эффективны для почти отсортированных списков.

Об остальных проблемах я позабочусь - отдельное спасибо за поиск условий гонки!

На мой взгляд, ваша фиксация с изменениями индекса слишком велика, чтобы ее можно было просмотреть, тысяча строк добавленного кода вместе с двумя тысячами удалений — это действительно много.

Этот.

Я знаю, что это довольно много изменений в коде, однако я думаю, что это также упрощение текущей реализации индекса. Отсюда и большое количество удаленных файлов/строк кода.

Это когда вы должны разделить свои изменения на отдельные отдельные PR, чтобы это было управляемо и с четкими намерениями.

@rawtaz :
Явным намерением является переработка индекса, используемого в restic, чтобы избавиться от проблем с текущей реализацией.

IMO изменение не такое большое, учитывая, что это полная повторная реализация индекса (и, пожалуйста, обратите внимание: также повторно реализуются некоторые устаревшие вещи, см. index/index_old_format.go ).

Однако это WIP и есть некоторые проблемы, которые я хочу исправить, прежде чем думать о том, как лучше всего интегрировать эту работу в основную ветку.
Я с нетерпением жду идей, как разделить его на более мелкие части. Не могли бы вы дать мне указания, какие изменения можно принять в качестве PR?

Я сделал несколько пробных запусков с помощью команды check , и произошло нечто странное. При использовании restic master команда выполняется за 6 минут (327,25 реальных 613,08 пользовательских 208,48 системных), с изменением индекса это занимает более часа (4516,77 реальных 896,17 пользовательских 531,71 системных). Время где-то тратится после печати "проверить снэпшоты, деревья и блобы", а рассмотреть поближе пока не успел.

Это должно быть исправлено немедленно.

struct IndexLowMem2.byFile : мне потребовалось довольно много времени, чтобы понять имя переменной. Пожалуйста, измените его на byPackFile или аналогичный, чтобы он был более описательным.

Изменено сейчас.

IndexLowMem2/FindPack : Насколько я понимаю, restic.IDs.Find предполагает наличие отсортированного списка идентификаторов. Однако idx.packTable , похоже, не отсортирован, так как это нарушит packIndex в blobWithoutType .

Вся логика byPack не использовалась, и вы правильно указали, что внутри также были некоторые ошибки. Я почистил этот мертвый код.

Состояние гонки в index_new/FindBlob : индекс может использоваться между вызовом SortByBlob и повторным получением блокировки чтения. В настоящее время я не вижу хорошего способа, как это можно исправить при использовании RWMutex. Та же проблема существует и в FindBlobByPack .

Надеюсь, я нашел хороший способ справиться с этим. В основном я проверяю sortedByBlob дважды, если для него установлено значение false. Следовательно, эта проблема должна быть исправлена ​​​​сейчас.

IndexLowMem2/Lookup : вызов FindBlob и доступ к blobTable не являются атомарными.

Теперь это делается с помощью нового атомарного indexTable.Get .

Таким образом, срезы packTable в idx.byFile будут указывать на старые версии массива packTable , когда добавление выделяет новый буфер. Это, вероятно, приведет к потере большого количества памяти для больших packCounts . Вы можете просто добавить endPackIndex в FileInfo в дополнение к startPackIndex , а затем получить прямой доступ к packTable .

Сейчас исправлено. packTable пока не использовалась...

MasterIndex.Lookup/Has/Count/... : после поиска в mi.mainIndex mi.idx можно сохранить и добавить к mainIndex до получения idxMutex .

Теперь это исправлено.

MasterIndex.Save : Кажется, это блокирует masterIndex , пока ожидается загрузка индекса. Старая реализация в Repository.SaveIndex работает без блокировки.

Этот вопрос все еще открыт. Я не нашел хорошего способа справиться с этим только с одним незавершенным индексом.
Я также не знаю, как это повлияет на производительность. Должно влиять только на backup . Я надеюсь увидеть некоторые тесты, если это актуально ..

Я сделал несколько пробных запусков с помощью команды check , и произошло нечто странное. При использовании restic master команда выполняется за 6 минут (327,25 реальных 613,08 пользовательских 208,48 системных), с изменением индекса это занимает более часа (4516,77 реальных 896,17 пользовательских 531,71 системных). Время где-то тратится после печати "проверить снэпшоты, деревья и блобы", а рассмотреть поближе пока не успел.

Это должно быть исправлено немедленно.

Я попробую в ближайшие дни.

Состояние гонки в index_new/FindBlob : индекс может быть использован между вызовом SortByBlob и повторным получением блокировки чтения. В настоящее время я не вижу хорошего способа, как это можно исправить при использовании RWMutex. Та же проблема существует и в FindBlobByPack .

Надеюсь, я нашел хороший способ справиться с этим. Я в основном проверяю sortedByBlob дважды, если для него установлено значение false. Следовательно, эта проблема должна быть исправлена ​​​​сейчас.

Это по-прежнему оставляет возможность столкновения между SortByBlob и AddJSONIndex . Проверка правильности сортировки индекса и фактического поиска по индексу должны происходить в том же критическом разделе. К сожалению, RWMutex не поддерживает атомарный переход от блокировки записи к блокировке чтения, что было бы самым простым способом сохранить блокировку после сортировки. В настоящее время я вижу две возможности:

  • Берем readlock, проверяем, что индекс отсортирован и запускаем поиск. Если индекс не отсортирован, снимите блокировку чтения и получите блокировку записи, отсортируйте индекс и выполните только этот поиск, удерживая блокировку записи.
  • Берем readlock, проверяем, что индекс отсортирован и запускаем поиск. Если индекс не отсортирован, снимите блокировку чтения и получите блокировку записи, отсортируйте индекс и повторите попытку с самого начала.

MasterIndex.Lookup/Has/Count/... : после поиска в mi.mainIndex mi.idx можно сохранить и добавить к mainIndex до получения idxMutex .

Кажется, вы пропустили MasterIndex.Lookup .

MasterIndex.Save : Кажется, это блокирует masterIndex , пока ожидается загрузка индекса. Старая реализация в Repository.SaveIndex работает без блокировки.

Этот вопрос все еще открыт. Я не нашел хорошего способа справиться с этим только с одним незавершенным индексом.
Я также не знаю, как это повлияет на производительность. Должно влиять только на backup . Я надеюсь увидеть некоторые тесты, если это актуально ..

Хм, основной блокировщик здесь заключается в том, что вы не можете получить идентификатор индексного пакета до завершения вызова загрузки. Было бы довольно легко решить эту проблему, разрешив несколько незавершенных индексов, в то время как только последний может быть записан, а предшественники в настоящее время загружаются.

Это по-прежнему оставляет возможность столкновения между SortByBlob и AddJSONIndex . Проверка правильности сортировки индекса и фактического поиска по индексу должны происходить в том же критическом разделе. К сожалению, RWMutex не поддерживает атомарный переход от блокировки записи к блокировке чтения, что было бы самым простым способом сохранить блокировку после сортировки. В настоящее время я вижу две возможности:

* Take the readlock, check that the index is sorted and run the search. If the index is not sorted, release the readlock and get the writelock, sort the index and complete just that search while holding the writelock.

* Take the readlock, check that the index is sorted and run the search. If the index is not sorted, release the readlock and get the writelock, sort the index and retry from the start.

Упс, на самом деле я был настолько сосредоточен на SortByBlobs, что не учел методы, которые он вызывает :-(
Спасибо за ваши предложения! Сейчас я использовал второй вариант.

Кажется, вы пропустили MasterIndex.Lookup .

Сейчас исправлено.

Хм, основной блокировщик здесь заключается в том, что вы не можете получить идентификатор индексного пакета до завершения вызова загрузки. Было бы довольно легко решить эту проблему, разрешив несколько незавершенных индексов, в то время как только последний может быть записан, а предшественники в настоящее время загружаются.

Я также исправил это. Теперь я кодирую/расшифровываю индекс и записываю его в основной индекс при блокировке, но записываю файл в серверную часть после снятия блокировки.

Кроме того, я нашел еще одну оптимизацию, которая значительно снижает потребление памяти (а также устраняет проблему с добавлением): я заменил большой массив структурой, которая реализует «выгружаемый» список больших двоичных объектов. Здесь сохраняется список «страниц», и если его нужно увеличить, просто выделяется новая страница. Это позволяет добавлять без необходимости повторного копирования всех старых элементов. Кроме того, накладные расходы фиксированы и максимальны для размера страницы, тогда как с добавлением вы получаете довольно большие накладные расходы (массив получает увеличивающийся «резерв емкости», чтобы гарантировать, что ему не нужно слишком часто повторять копирование). Эта оптимизация, кажется, экономит еще ~ 15-20% памяти.

Я рад получить обратную связь и еще больше хочу увидеть тесты производительности с большими репозиториями,

Вот результаты моего первого сравнения.
Я использовал каталог данных с 550 000 файлов (результат в 550 000 больших двоичных объектов данных) с помощью скрипта

#!/bin/sh
mkdir data
for i in `seq 1 550`; do
        echo $i
        mkdir data/$i
        for j in `seq 1 1000`; do
                echo $i$j > data/$i/$j
        done
done

Затем я сделал резервную копию этого каталога данных три раза и запустил check после:

/usr/bin/time/restic -r /path/to/repo --mem-profile . backup /path/to/data
/usr/bin/time/restic -r /path/to/repo --mem-profile . backup /path/to/data
/usr/bin/time/restic -r /path/to/repo --mem-profile . backup /path/to/data -f
/usr/bin/time/restic -r /path/to/repo --mem-profile . check

Эти шаги я выполняю один раз с основной веткой (скомпилированной в restic.old ) и моей веткой (скомпилированной в restic.new ).
Я просто использовал свой старый ноутбук с локальным SSD.

Вот результаты:
time_backup1_old.txt
mem_backup1_old.pdf
time_backup1_new.txt
mem_backup1_new.pdf

time_backup2_old.txt
mem_backup2_old.pdf
time_backup2_new.txt
mem_backup2_new.pdf

time_backup3_old.txt
mem_backup2_old.pdf
time_backup3_new.txt
mem_backup3_new.pdf

time_check_old.txt
mem_check_old.pdf
time_check_new.txt
mem_check_new.pdf

Обобщить:

  • Новая реализация индекса использует только 22% памяти , используемой реализацией индекса im master (!!!)
  • при этом использование памяти, связанное с индексом check , еще больше уменьшилось (IDSet был удален)
  • С новой реализацией использование индексной памяти больше не является единственным выдающимся потребителем памяти, что делает целесообразной оптимизацию других частей.
  • Скорость и загрузка ЦП были сопоставимы. Единственным случаем, когда новая реализация была немного медленнее, был третий запуск резервного копирования, в котором активно используются поиски по индексу. Однако мы говорим о недавно сгенерированном репозитории с несколькими индексными файлами. Следовательно, основной недостаток реализации индекса master (зацикливание на всех индексных файлах) здесь не играл большой роли. Я считаю, что со многими индексными файлами все полностью меняется, и что новая реализация будет явно превосходить текущую.

Наконец-то я закончил свои тесты. Это заняло больше времени, чем предполагалось, потому что мне пришлось повторно запустить тесты из-за каких-то загадочных результатов. Я хотел сравнить результаты между 1 и несколькими ядрами. Интересно, что виртуальная машина только с 1 виртуальным ЦП была намного быстрее почти во всех операциях. Возможно, @fd0 знает, что происходит.

результаты для ВМ1

1 виртуальный ЦП
2 ГБ ОЗУ

Обратите внимание, что я выполнил 1 профилирование ЦП и 1 профилирование памяти для каждой версии Restic. Время, показанное (в секундах) в этой таблице, является средним для обоих запусков.

| | рестик v.0.9.6 | рестик новый индекс | разница |
| :--- | ---: | ---: | ---: |
| инициировать репозиторий | 2,24 | 2,27 | 1,12% |
| 1-я резервная копия | 1574,36 | 1542,80 | -2,00% |
| 2-я резервная копия | 556,95 | 541,71 | -2,74% |
| 3-я резервная копия (--force) | 1192,90 | 1195,53 | 0,22% |
| забыть | 0,51 | 0,52 | 2,97% |
| чернослив | 43,87 | 44,02 | 0,34% |
| проверить | 30,77 | 31,59 | 2,68% |
| список больших двоичных объектов | 2,92 | 3,36 | 14,90% |
| кошачья капля | 2,58 | 2,60 | 0,97% |
| найти блоб | 22,86 | 21,17 | -7,39% |
| восстановить | 895,01 | 883,57 | -1,28% |

результаты для ВМ8

8 виртуальных ЦП
32 ГБ ОЗУ

Обратите внимание, что я выполнил 1 профилирование ЦП и 1 профилирование памяти для каждой версии Restic. Время, показанное (в секундах) в этой таблице, является средним для обоих запусков.

| | рестик v.0.9.6 | рестик новый индекс | разница |
| :--- | ---: | ---: | ---: |
| инициировать репозиторий | 2,11 | 2,09 | -0,95% |
| 1-я резервная копия | 1894,47 | 1832,65 | -3,26% |
| 2-я резервная копия | 827,95 | 776,38 | -6,23% |
| 3-я резервная копия (--force) | 1414,60 | 1411,98 | -0,19% |
| забыть | 0,60 | 0,56 | -6,72% |
| чернослив | 93,10 | 89,84 | -3,50% |
| проверить | 70,22 | 68,44 | -2,53% |
| список больших двоичных объектов | 3,86 | 7,24 | 87,68% |
| кошачья капля | 3,88 | 3,69 | -4,90% |
| найти блоб | 30,95 | 29,11 | -5,95% |
| восстановить | 1150,49 | 1089,66 | -5,29% |

Дополнительная информация

$ uname -r
5.4.15-200.fc31.x86_64
$restic-orig version
debug enabled
restic 0.9.6 (v0.9.6-40-gd70a4a93) compiled with go1.13.6 on linux/amd64
$ restic-newindex version
debug enabled
restic 0.9.6 (v0.9.6-43-g5db7c80f) compiled with go1.13.6 on linux/amd64

файлы изменены добавлены для резервной копии 1:

Files:       487211 new,     0 changed,     0 unmodified
Dirs:            2 new,     0 changed,     0 unmodified
Added to the repo: 62.069 GiB

файлы изменены добавлены для резервной копии 2:

Files:       166940 new,     0 changed, 487211 unmodified
Dirs:            0 new,     2 changed,     0 unmodified
Added to the repo: 6.805 GiB

файлы изменены добавлены для резервной копии 3:

Files:       654215 new,     0 changed,     0 unmodified
Dirs:            2 new,     0 changed,     0 unmodified
Added to the repo: 4.029 MiB

Журналы

logs_vm1_cpu.zip
logs_vm1_mem.zip
logs_vm8_cpu.zip
logs_vm8_mem.zip

@dimejo Большое спасибо за ваши тесты!

Во-первых, что касается вашего вопроса об увеличении времени для настройки 8 ЦП: вы сравниваете «время пользователя», то есть общее количество используемых ЦП секунд. При параллельной обработке это (и должно быть выше), чем при использовании одного ЦП, поскольку всегда есть некоторые накладные расходы из-за распараллеливания. На самом деле ваши 8 запусков ЦП были намного быстрее, посмотрите прошедшее время.

Вы использовали фиксацию 5db7c80f для своих тестов newindex. Возможно ли, что вы повторно протестируете последнюю фиксацию 26ec33dd ? Должно быть еще одно снижение использования памяти и (если я все сделал правильно) может быть даже лучшая производительность.

В целом я очень доволен этими результатами! Кажется, моя реализация использует гораздо меньше памяти, но при этом имеет аналогичную производительность, чем исходная реализация индекса. (Кстати: я исправлю явно открытую проблему с помощью list blobs )

Во-первых, что касается вашего вопроса об увеличении времени для настройки 8 ЦП: вы сравниваете «время пользователя», то есть общее количество используемых ЦП секунд. При параллельной обработке это (и должно быть выше), чем при использовании одного ЦП, поскольку всегда есть некоторые накладные расходы из-за распараллеливания. На самом деле ваши 8 запусков ЦП были намного быстрее, посмотрите прошедшее время.

Спасибо за объяснение. Глупый я даже не видел прошедшее время...

Вы использовали фиксацию 5db7c80f для своих тестов newindex. Возможно ли, что вы повторно протестируете последнюю фиксацию 26ec33dd? Должно быть еще одно снижение использования памяти и (если я все сделал правильно) может быть даже лучшая производительность.

Конечно.

Результаты

пройденное время

Обратите внимание, что я указал прошедшее время (в мм:сс). Снова показывает среднее время для обоих запусков.

| | v0.9.6 | новый индекс | новыйиндекс2 | разница (v0.9.6 и newindex2) |
| :--- | ---: | ---: | ---: | ---: |
| инициировать репозиторий | 00:42,4 | 01:11,6 | 00:21,5 | -49,27% |
| 1-я резервная копия | 26:42,7 | 26:58,2 | 31:00,4 | +16,08% |
| 2-я резервная копия | 10:18,9 | 09:54,1 | 09:03,7 | -12,14% |
| 3-я резервная копия (--force) | 17:32,6 | 17:05,8 | 20:37,1 | +17,52% |
| забыть | 00:00,8 | 00:00,9 | 00:00,8 | +0,61% |
| чернослив | 01:02,5 | 00:56,5 | 00:51,2 | -18,16% |
| проверить | 00:31,6 | 00:30,8 | 00:31,0 | -1,74% |
| список больших двоичных объектов | 00:05,1 | 00:06,2 | 00:05,6 | +9,44% |
| кошачья капля | 00:02,2 | 00:02,3 | 00:02,1 | -3,39% |
| найти блоб | 00:28,5 | 00:28,2 | 00:23,0 | -19,28% |
| восстановить | 13:02,0 | 12:23,3 | 14:09,1 | +8,58% |

использование памяти

В этой таблице показано общее использование памяти (в МБ) из файлов pprof. Надеюсь, это правильный.

| | v0.9.6 | новый индекс | новыйиндекс2 | разница (v0.9.6 и newindex2) |
| :--- | ---: | ---: | ---: | ---: |
| инициировать репозиторий | 32,01 | 32,01 | 32,00 | -0,03% |
| 1-я резервная копия | 227,50 | 178,48 | 174,77 | -23,18% |
| 2-я резервная копия | 227,89 | 157,92 | 149,52 | -34,39% |
| 3-я резервная копия (--force) | 204,53 | 147,15 | 136,06 | -33,48% |
| забыть | 32,25 | 32,09 | 32,26 | +0,03% |
| чернослив | 167,55 | 108,96 | 130,60 | -22,05% |
| проверить | 163,29 | 75,06 | 70,34 | -56,92% |
| список больших двоичных объектов | 33,22 | 30,21 | 25,47 | -23,33% |
| кошачья капля | 113,79 | 48,86 | 32,23 | -71,68% |
| найти блоб | 79,48 | 30,40 | 25,64 | -67,74% |
| восстановить | 226,08 | 176,54 | 198,19 | -12,33% |

Дополнительная информация

8 vCPU
32GB RAM
$ restic-newindex2 version
debug enabled
restic 0.9.6 (v0.9.6-44-g26ec33dd) compiled with go1.13.6 on linux/amd64

Журналы

logs_test3.zip

@aawsome : у меня все еще есть проблемы с командой проверки, которая не кэширует двоичные объекты дерева. Старый код под названием Repository.SetIndex также вызывал вызов PrepareCache. Теперь вызов SetIndex удален, однако замены вызову PrepareCache я не нашел.

@dimejo Большое спасибо за тесты! Это дает очень хорошую информацию, и результаты по потреблению памяти выглядят уже очень хорошо!

Я пытался понять результаты потребления памяти для v0.9.6, 3-й бэкап, но не смог воспроизвести ваши результаты. Может быть, вы использовали потребление памяти индекса (102 МБ) вместо общей памяти (204 МБ)?

Я также не понимаю, почему newindex2 показывает более высокое потребление памяти, чем newindex для prune и restore . В профиле памяти вы можете видеть, что память, используемая индексом, меньше, а более высокое потребление памяти связано с encoding/json.Marshal и restic.NewBlobBuffer в prune и restorer.(*packCache).get в восстановлении (которых в профиле для v0.9.6 в принципе нет). Также потребление памяти, показанное time , ниже для newindex2, чем для newindex.
Поэтому я считаю, что newindex2 также потребляет меньше памяти для этих двух команд, чем newindex, но я не понимаю, почему профилирование показывает, что эти дополнительные (не связанные с индексом) части потребляют память.

Об общем времени: меня немного раздражает, что newindex и newindex2 так сильно различаются и что различия иногда положительные, а иногда отрицательные. Я попытался посмотреть профили процессора некоторых прогонов и не увидел использования процессора, связанного с индексом. Например, при трех запусках резервного копирования в частях, потребляющих ресурсы ЦП, во всех запусках (v0.9.6 и newindex2) полностью доминируют sha256, chunker, системный вызов и среда выполнения, и я не понимаю, почему должны быть различия из-за другой реализации индекса.
Может ли быть так, что машина не имеет постоянной мощности процессора, и различия вызваны не разными реализациями, а разными обстоятельствами машины? Когда вы сравнивали v0.9.6 и newindex, результаты не сильно отличались, и судя по всему, эти два прогона были сделаны примерно в одно и то же время.
Другими словами: возможно ли повторно запустить измерение процессора для v0.9.6 и newindex2 одновременно? Затем мы могли бы проверить, распространены ли различия в таймингах процессора, и, возможно, получить более полное представление об изменениях производительности из-за измененной реализации.

@MichaelEischer Спасибо, что сообщили об этой все еще нерешенной проблеме, и извините, что она пока не работает.
Вызов PrepareCache находится в internal/index/masterindex.go:Load , который вызывается internal/repository/repository.go:LoadIndex .
Мне придется отлаживать эту часть кода.

Просто оставляем еще один случайный результат ветки с большим репозиторием (~ 500 ГБ/1500+ снимков) + маленькую резервную копию (1,3 ГБ). Очень многообещающе :+1:
по умолчанию-restic.txt
исправлено.txt

@MichaelEischer Я обнаружил проблему с тем, что не использовал кеш с помощью команды check : поскольку проверка перестраивает функциональность masterindex.Load() , PrepareCache нужно вызывать явно (было в repository.UseIndex() ) Теперь это исправлено.
Я также исправил проблему производительности с list blobs

@seqizz Спасибо за тестирование! Расход памяти меня вполне устраивает :smile:
У вас есть объяснение, почему истекшее время в исправленном прогоне намного выше, чем в прогоне по умолчанию? Я не могу объяснить такое поведение изменениями индекса. Вы заметили снижение параллелизма из-за другой реализации или это эффект, который можно объяснить внутри используемой вами системы?

Кстати: общее время процессора кажется сопоставимым между двумя запусками. Это именно то, чего я ожидал.

@aawsome Я бы сказал, что это был побочный эффект моего неправильного тестирования. Несмотря на то, что я forget создавал снимки между запусками, кажется, что это было медленно, потому что я сначала запускал исправленную версию. Возможно, кэширование ОС на стороне нашего хранилища (Minio) повлияло на время ответа.

Теперь, в качестве контрольного прогона, я создал основную версию restic (вместо предыдущей версии выпуска, поскольку я создал вашу ветку поверх основной ветки) и сначала запустил эту основную версию. Это показывает, что вы можете игнорировать прошедшее время, так как это зависит от порядка.

restic-upstream.txt
restic-patched.txt

Я пытался понять результаты потребления памяти для v0.9.6, 3-й бэкап, но не смог воспроизвести ваши результаты. Может быть, вы использовали потребление памяти индекса (102 МБ) вместо общей памяти (204 МБ)?

Хороший улов - вероятно, ошибка копирования и прошлого. Теперь это исправлено в моем посте выше.

Я также не понимаю, почему newindex2 показывает более высокое потребление памяти, чем newindex для обрезки и восстановления. В профиле памяти вы можете видеть, что память, используемая индексом, меньше, а более высокое потребление памяти связано с кодированием /json.Marshal и restic.NewBlobBuffer в pruneand restorer.(*packCache).get в восстановлении (которые в основном не присутствует в профиле для v0.9.6).
Поэтому я считаю, что newindex2 также потребляет меньше памяти для этих двух команд, чем newindex, но я не понимаю, почему профилирование показывает, что эти дополнительные (не связанные с индексом) части потребляют память.

ТБХ, я понятия не имею, что там произошло. Я попытался воспроизвести результаты на той же виртуальной машине, но не смог.

Также потребление памяти, показанное временем, для newindex2 ниже, чем для newindex.

Я сравнил результаты, показанные по времени для всех запусков моего 4. теста (см. ниже), и они не кажутся слишком надежными. Для некоторых тестов потребление памяти между запуском профилирования процессора и памяти различается на 15-20%.

restic-orig_test4_cpu_findblob: 253,12MB
restic-orig_test4_mem_findblob: 307,44MB
restic-newindex2_test4_cpu_catblob: 184,12MB
restic-newindex2_test4_mem_catblob: 216,56MB
restic-newindex2_test4_cpu_findblob: 174,81MB
restic-newindex2_test4_mem_findblob: 202,38MB

Может ли быть так, что машина не имеет постоянной мощности процессора, и различия вызваны не разными реализациями, а разными обстоятельствами машины? Когда вы сравнивали v0.9.6 и newindex, результаты не сильно отличались, и судя по всему, эти два прогона были сделаны примерно в одно и то же время.
Другими словами: возможно ли повторно запустить измерение процессора для v0.9.6 и newindex2 одновременно? Затем мы могли бы проверить, распространены ли различия в таймингах процессора, и, возможно, получить более полное представление об изменениях производительности из-за измененной реализации.

Используемая мной виртуальная машина не имеет выделенных ядер. Особенно с 8 ядрами Restic даже близко не использует всю доступную мощность процессора. Вот почему я думал, что это не будет иметь большого значения для моих тестов. Теперь я повторил тест с выделенными процессорами, и результаты стали намного более ровными. Похоже, именно это и вызвало несоответствие.

Результаты

прошедшее время (в мм:сс)

| | v0.9.6 | новый индекс | новыйиндекс2 | разница (v0.9.6 и newindex2) |
| :--- | ---: | ---: | ---: | ---: |
| инициировать репозиторий | 00:26,2 | 00:48,7 | 00:58,4 | +122,79% |
| 1-я резервная копия | 32:51,8 | 32:12,1 | 32:49,0 | -0,14% |
| 2-я резервная копия | 08:40,2 | 08:31,2 | 08:30,3 | -1,90% |
| 3-я резервная копия (--force) | 21:40,5 | 21:43,6 | 21:41,0 | +0,04% |
| забыть | 00:00,8 | 00:00,7 | 00:00,8 | -7,98% |
| чернослив | 00:43,7 | 00:43,8 | 00:42,2 | -3,56% |
| проверить | 00:26,3 | 00:26,9 | 00:26,4 | +0,30% |
| список больших двоичных объектов | 00:03,5 | 00:04,4 | 00:04,6 | +29,46% |
| кошачья капля | 00:01,7 | 00:01,8 | 00:01,8 | +9,04% |
| найти блоб | 00:17,7 | 00:17,3 | 00:16,7 | -5,95% |
| восстановить | 12:40,2 | 13:20,3 | 12:24,1 | -2,12% |

использование памяти (в МБ)

| | v0.9.6 | новый индекс | новыйиндекс2 | разница (v0.9.6 и newindex2) |
| :--- | ---: | ---: | ---: | ---: |
| инициировать репозиторий | 32,05 | 32,02 | 32,02 | -0,09% |
| 1-я резервная копия | 242,38 | 170,70 | 161,80 | -33,25% |
| 2-я резервная копия | 224,63 | 154,78 | 146,69 | -34,70% |
| 3-я резервная копия (--force) | 218,38 | 159,48 | 150,07 | -31,28% |
| забыть | 32,01 | 32,24 | 32,05 | +0,12% |
| чернослив | 183,05 | 110,84 | 113,29 | -38,11% |
| проверить | 195,05 | 74,28 | 70,33 | -63,94% |
| список больших двоичных объектов | 33,32 | 29,45 | 25,45 | -23,62% |
| кошачья капля | 92,94 | 49,02 | 33,13 | -64,35% |
| найти блоб | 111,51 | 29,62 | 25,41 | -77,21% |
| восстановить | 292,94 | 217,41 | 206,77 | -29,42% |

Дополнительная информация

Используемая ВМ:

8 vCPU (dedicated CPUs)
32GB RAM
$ restic-orig version
debug enabled
restic 0.9.6 (v0.9.6-40-gd70a4a93) compiled with go1.13.6 on linux/amd64



md5-6ad2d5d28ba7c01c107571884b108e74



$ restic-newindex version
debug enabled
restic 0.9.6 (v0.9.6-43-g5db7c80f) compiled with go1.13.6 on linux/amd64



md5-6ad2d5d28ba7c01c107571884b108e74



$ restic-newindex2 version
debug enabled
restic 0.9.6 (v0.9.6-44-g26ec33dd) compiled with go1.13.6 on linux/amd64

Журналы

logs_test4.zip

@dimejo Хорошая работа! Большое спасибо!
Производительность большого двоичного объекта списка (и использование памяти) значительно улучшилась после фиксации 908c8977df05ea88dcad86c37f39d201468446ad.

Остальные результаты выглядят уже неплохо. В настоящее время я ищу, может ли индекс сэкономить память для backup и restore с помощью небольшого рефакторинга этих команд (как это уже сделано с check ).

Потом поработаю над тестами и сделаю PR.
@rawtaz Мой вопрос о том, какую часть этой работы следует отделить, чтобы сделать PR приемлемым, все еще открыт. Мне понадобятся некоторые указания здесь, если изменения слишком велики для сопровождающих.
Я все еще убежден, что полное переопределение индекса здесь является ключом к этим хорошим результатам в снижении потребления памяти.

@aawsome Спасибо за исправление команды проверки. Я попробую.

Что касается ваших оптимизаций для команды check , вы можете взглянуть на мой запрос на включение # 2328, который позволяет еще немного сократить использование памяти и работает намного быстрее для репозиториев, содержащих более нескольких снимков.

После просмотра разницы между master и refactor-index у меня сложилось впечатление, что почти половина изменений вызвана перемещением кода между repository и index . Однако это не ясно, если посмотреть на коммиты. Так ли необходим этот ход?
Я также не очень уверен, что у меня есть ссылка на репозиторий, хранящийся в MasterIndex. Предыдущий способ позволить репозиторию контролировать MasterIndex кажется лучше разделенным, на мой взгляд. Хотя, вероятно, было бы неплохо перенести откат от текущего к старому формату индекса в реализацию индекса.
Я согласен с тем, что, вероятно, невозможно очень сильно уменьшить общее количество изменений без потери функциональности, но, вероятно, было бы очень полезно просто просмотреть набор коммитов, каждый из которых состоит менее чем из нескольких сотен строк.

@aawsome Все еще есть ошибка с кешированием treePacks. Строка https://github.com/aawsome/restic/blob/908c8977df05ea88dcad86c37f39d201468446ad/internal/repository/repository.go#L97 должна читаться как treePacks.Insert(pb.PackID) . Это также должно немного уменьшить использование памяти.

Текущая реализация в restic просто кэширует пакеты, содержащие только деревья. Это эквивалентно новой реализации пакетов, созданных в последних версиях restic. Однако в старых версиях Restic (я думаю <0,8) данные и блобы дерева смешиваются вместе. Эти большие двоичные объекты данных не следует кэшировать. Поэтому я бы предложил добавить вторую итерацию ко всем большим двоичным объектам и снова удалить пакеты, содержащие большие двоичные объекты данных.

@Майкл Эйшер
Спасибо, что нашли еще одну ошибку кэширования — надеюсь, теперь она действительно последняя. И жаль, что я не смог найти его; Я пока пользовался только локальным бэкендом и стандартного теста, проверяющего, правильно ли настроено кеширование, нет... Может стоит добавить такой тест...

Это (и проблема старого формата репо) исправлено в последнем коммите.

Также большое спасибо за ваш другой комментарий!
Насчет check Я согласен, что потенциал оптимизации памяти еще больше (и, конечно, скорость). Я уже думал о том, чтобы индекс мог сохранять разные пользовательские флаги для больших двоичных объектов. Таким образом, вы можете сохранить огромные IDSet s или пользовательские карты, поскольку вы сохраняете идентификатор большого двоичного объекта только один раз. Это то, о чем я думал, оптимизируя prune на втором этапе.
Затем ваш PR #2328 можно легко адаптировать для использования флагов индекса вместо определяемой пользователем карты, и мы получаем дополнительное сокращение памяти.

Я согласен с вашей проблемой дизайна и удалил repo из MasterIndex . Следовательно, команды Load и Save вернулись к repository.go , что сделало diif для мастеринга немного меньше.
Однако я не думаю, что главной целью должно быть как можно меньшее слияние. Я хотел бы реализовать лучший дизайн. О том, где найти реализацию index , у нас уже есть два места в master: internal/index и internal/repository . Поскольку вещи, связанные с индексом, представляют собой достаточно сложный код, я подумал, что его следует отделить от repository , и поэтому я выбрал internal/index в качестве места для размещения всего об индексе (а также определил новый интерфейс в internal/restic/index.go ). Весь код в index_new.go , index_old_format.go и master_index.go написан с нуля, а index.go сильно адаптирован, но при этом сохранил исходную функциональность, использованную в rebuild-index и prune .
С другой стороны, все, что связано с индексом (кроме загрузки/сохранения), удаляется из internal/repository .

Об оптимизации backup и restore :

  • С помощью backup я добавил возможность добавлять в индекс «известные» большие двоичные объекты. Поскольку они удаляются после добавления «известного» большого двоичного объекта в индекс, это должно сэкономить много памяти, используемой в backup . Я ожидаю, что использование основной памяти теперь должно быть связано с пространством, зарезервированным для параллельного блока, если у вас нет действительно ОГРОМНОГО репозитория.
  • Для restore я не нашел никакой оптимизации, связанной с индексом. Кажется, что все использование памяти связано с тем, как restore работает внутри. У меня есть ощущение, что есть возможности оптимизации, но это должно быть сделано в отдельной работе.

Так что для меня открыта только работа над тестовой реализацией (если нет других ошибок, о которых сообщается). Потом сделаю пиар.

@aawsome Привет, извините, что долго не отвечал. Я чувствую, что не могу дать вам соответствующие подробные указания, но я думал о том, чтобы разделить такие вещи, как «рефакторинг кода для подготовки к предстоящим изменениям» (например, изменение обработки индекса) и «рефакторинг кода для оптимизации или добавления функций». " (например, фактические оптимизации и прочее, к чему вы здесь стремитесь) в отдельные коммиты или даже PR (зависит от кода).

Я пытаюсь показать вам, что у вас есть что сказать получше, чем у меня, потому что я действительно думаю, что ваша работа здесь выглядит интересной!

Кроме того, я ценю, что вы начали этот вклад, открыв вопрос (вместо того, чтобы просто открыть PR), спасибо за это! Когда это возможно, было бы хорошо, если бы мы могли обсудить предложения до фактической реализации. Поступая таким образом, мы можем найти точки соприкосновения и выбрать направление, в котором все согласны (и часто возникают неожиданные проблемы или перспективы, которые меняют способ реализации, что в конечном итоге снижает ненужную работу).

@rawtaz Спасибо за ответ!

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

Я думаю, что для обсуждения полезно дать обзор изменений, которые я сделал, и причины, по которым я сделал каждое изменение. Реализация включает следующие изменения:

Серьезные изменения:

  • структура кодовой базы, а именно, какая функциональность находится в internal/repository и internal/index
  • структура определений интерфейсов в internal/restic
  • внутренняя структура MasterIndex, включая обработку «полных» индексных файлов
  • структура данных, связанная с индексным файлом
  • поддержка индексных файлов старого формата ( index_old.go )
  • новая структура данных индекса в памяти ( index_new.go )
  • изменение доступа к индексу, где это необходимо

Мелкие дополнительные изменения:

  • изменил Lookup и добавил LookupAll
  • заменено «Магазин» на StorePack РЕДАКТИРОВАТЬ: реализовано в # 2773
  • добавьте CheckSetKnown для обработки известных больших двоичных объектов во время резервного копирования. EDIT: реализовано в # 2773.
  • удалите blob IDSet из checker.go и замените его функциональностью индекса
  • использовать новую функциональность индекса в «списке больших двоичных объектов»

Основными изменениями являются IMO, ключ к достижению низкого потребления памяти, хорошей производительности и более чистого дизайна кода (по сравнению с фактической реализацией). Незначительные изменения — это быстрые победы, которые также могут быть независимыми PR. Однако, если предполагается, что они являются хорошими улучшениями, я бы предпочел оставить их в основном PR, иначе их, вероятно, придется реализовать либо дважды, либо придется отложить.

Структура кодовой базы

Я переместил большую часть функций, связанных с индексами, с internal/repository на repository/index . IMO, это делает структуру базы кода более четкой, а также четко разделяет, какой код находится в каком каталоге (в отличие от фактической реализации.

Вопрос к сопровождающим: вы признаете это принципиальное изменение?

Структура интерфейса

Я также разделил определения интерфейсов на internal/restic , чтобы было понятно, что относится к Index , а что к Repository . Это также позволяет более четко изменять интерфейсы в будущем.

Вопрос к сопровождающим: вы признаете это принципиальное изменение?

Структура MasterIndex

В настоящее время MasterIndex объединяет структуры данных, связанные с индексным файлом, и обычно перебирает все эти подструктуры для операций с индексами. Каждая из структур данных, связанных с индексным файлом, может быть полной (то есть достаточно полной для сохранения в качестве полного индексного файла в репозитории) или законченной (то есть уже представленной как индексный файл).

Я полностью изменил это поведение. Теперь MasterIndex использует основной индекс в памяти, в котором должно присутствовать большинство записей индекса, особенно все те, которые также присутствуют в виде индексных файлов в репозитории, и вторую «незавершенную» структуру данных, которая используется только для сохранения нового индекса. записей и вставляется в основной индекс в памяти после того, как содержимое записывается в виде индексного файла в репозиторий.

Вопрос к сопровождающим: вы признаете это принципиальное изменение?

Структура данных, связанная с индексным файлом

В настоящее время существует три определения структуры данных, связанных с индексным файлом: одно определено JSON в internal/index , одно определено JSON в internal/repository и оптимизировано для поиска в internal/repository .
Я решил использовать структуру данных JSON из internal/index и добавил необходимые методы для ее использования в MasterIndex (и полностью удалил реализации из internal/repository ). Это полностью исключает преобразование внутренних структур данных для сохранения индексных файлов во время резервного копирования. С другой стороны, операции поиска должны проходить по всем элементам индекса. Следовательно, очень важно, чтобы эта структура данных не содержала много записей во время поиска, т. е. регулярно вставлялась в основной индекс.

Вопрос к сопровождающим: вы признаете это принципиальное изменение?

Поддержка старого формата файла индекса

Поддержка новых форматов индексных файлов теперь передана на аутсорсинг index_old_format.go .

Вопрос к сопровождающим: вы признаете это принципиальное изменение?

Новая структура данных индекса в памяти ( index_new.go )

Очевидно, это самое большое изменение. В принципе для каждого типа больших двоичных объектов поддерживается один большой список больших двоичных объектов. Список отсортирован по идентификатору большого двоичного объекта, поэтому поиск можно выполнить с помощью двоичного поиска. Поскольку оптимизация нацелена на множество записей в этом списке, размер каждой записи максимально сведен к минимуму. Это означает, что идентификатор пакета хранится в отдельной таблице, и сохраняется только ссылка, а смещение и длина составляют uint32 (фактически ограничивая максимальный размер пакета до 4 ГБ). РЕДАКТИРОВАТЬ: эти две оптимизации реализованы в #2781.
Простой отсортированный массив уже экономит накладные расходы памяти, которые находятся в пределах map , но по-прежнему генерируют некоторые накладные расходы памяти и производительности в пределах append : дополнительный «буфер» зарезервирован для будущих добавлений, и данные должны быть скопировано. Поэтому я реализовал «выгружаемый» массив с небольшими и ограниченными накладными расходами памяти и без необходимости копировать для добавления.

Проведенные тесты показывают, что этот индекс немного быстрее, чем стандартная реализация MasterIndex, сохраняя при этом более 75% памяти.

Вопрос к сопровождающим: соответствует ли эта реализация индекса вашим ожиданиям?

Изменено Lookup и добавлено LookupAll

Lookup на самом деле возвращает более одного результата (но не в том случае, если они распределены по многим индексным файлам, кстати). Во многих случаях используется только первый результат. Поэтому я изменил Lookup , чтобы он возвращал только один результат, и добавил LookupAll , который будет использоваться, когда используется более одного результата. Делает код немного чище, а также экономит ненужные циклы процессора.

Вопрос к сопровождающим: Можно ли сохранить это изменение или вы бы предпочли удалить его из PR индекса?

«Магазин» заменен на StorePack

Store хранит в индексе только один большой двоичный объект, но вызывается только для содержимого всего пакета. Поэтому я изменил его на функциональность StorePack , при которой сохраняется весь контент пакета. Это делает код более понятным, хранение в формате JSON намного проще, а также более производительным.

Вопрос к сопровождающим: Можно ли сохранить это изменение или вы бы предпочли удалить его из PR индекса?

добавьте CheckSetKnown для обработки известных больших двоичных объектов во время резервного копирования

Тесты показали, что для многих новых файлов backup использует довольно много памяти для хранения больших двоичных объектов, добавленных во время этого резервного копирования. Это делается для того, чтобы не сохранять большие двоичные объекты дважды с помощью параллельных заставок. Я переместил эту функцию в индекс, так как эта информация об «известных больших двоичных объектах» может быть удалена после добавления соответствующей записи в индекс. С этим изменением только очень немногие BLOB-объекты «известны», но (пока) не включены в индекс.

Вопрос к сопровождающим: Можно ли сохранить это изменение или вы бы предпочли удалить его из PR индекса?

Удалите IDSet blob из checker.go и замените его функциональностью индекса.

Средство проверки создает собственный индекс в памяти и дополнительно сохраняет все идентификаторы больших двоичных объектов в дополнительном наборе идентификаторов. Поскольку все большие двоичные объекты уже известны индексу, его можно заменить методом индекса Each .
Это экономит дополнительную память в пределах check .

Вопрос к сопровождающим: Можно ли сохранить это изменение или вы бы предпочли удалить его из PR индекса?

Используйте новую функциональность индекса в «списке BLOB-объектов»

Вместо того, чтобы считывать весь индекс в память, а затем перебирать все записи для печати списка больших двоичных объектов, каждый файл индекса можно загрузить в память отдельно и просмотреть это содержимое в цикле. Делает list blobs гораздо менее требовательным к памяти.

Вопрос к сопровождающим: Можно ли сохранить это изменение или вы бы предпочли удалить его из PR индекса?

Небольшой результат теста @aawsome , я не могу сделать резервную копию в существующем репозитории после последней фиксации.
Значения от time :

|---|"Обычный" бэкап| fix masterindex.Load() фиксация|последняя фиксация|
|---|---|---|---|
|Использование памяти|~650Mb|~320Mb|~55Mb|
|Время резервного копирования|~12сек|~12сек|(отказался через 5 минут)|

Хранилище сообщает, что прекращает работу после запроса индексов. (Извините, если это было сделано из-за изменения формата)

@seqizz Спасибо за сообщение об ошибке. Я должен признать, что я тестировал только последний коммит с go run build.go -T , который не показал никаких проблем... Я исправил это в коммите 5b4009fcd275794e3ce05304fce255d5fa3e1864.

@aawsome Спасибо за этот краткий комментарий . Одна из вещей, с которыми я как неосновной разработчик (я просто сопровождаю или простые вещи) сталкиваюсь с тем, что в то время, когда основное время разработчика тратится на другие части проекта, подобные проблемы продолжают расти. Затем, всякий раз, когда основной разработчик просматривает это, он понимает, что это слишком много, а в некоторых случаях слишком много, чтобы копаться в нем в то время. Чем меньше для них будет прочитанного, тем выше шансы, что мы сможем с пользой использовать их время. Я взял на себя смелость отредактировать начальный пост в этом выпуске, чтобы добавить ссылку на ваш итоговый комментарий.

Что касается ваших вопросов, то я лично не могу на них ответить. Мне придется отложить их на некоторое время, и я прошу вашего терпения. Наше основное внимание прямо сейчас с доступным временем, которое у нас есть, находится на некоторых других вещах, но я могу сказать вам, что эта проблема и ваш PR находятся на моем радаре, и я пытаюсь в конечном итоге увидеть это.

Я думаю, что с вашим кратким комментарием, помимо исправления ошибок (я немного обеспокоен тем, что могут скрываться еще какие-то мелкие ошибки) и всего, что вы можете найти, что не отклоняется от того, что вы написали в своем комментарии, наиболее продуктивным является чтобы немного отдохнуть. Следующим шагом будет привлечение внимания основного разработчика, и пока этого не произойдет, вероятно, будет более контрпродуктивно расширять обсуждение. Дайте мне знать, если мое обоснование этого кажется бессмысленным, чтобы я мог уточнить, что я имею в виду.

Наконец-то я нашел время протестировать производительность команды резервного копирования в репозитории файлов объемом 8 ТБ и 40 М. Чтобы сократить общее время выполнения, я только что сделал резервную копию каталога размером 6 ГБ. Использование процессора restic было ограничено с помощью GOMAXPROCS=1 .

текущий мастер: cpu-idx-master.pdf
этот пр: cpu-idx-table.pdf

С этим PR restic использует около 6,5 ГБ памяти вместо 18 ГБ для текущего мастера, что является гигантским улучшением. В обоих случаях для загрузки индекса требуется около 9 минут (хост не поддерживает AES-NI, поэтому расшифровка немного медленная). Резервное копирование занимает 34 минуты для этого PR и 18 минут для текущего мастера. Взглянув на графики профилирования процессора, можно увидеть разницу: indexTable тратит 16 минут только на сортировку индекса в памяти. Индекс достаточно большой, поэтому один вызов сортировки для индекса в памяти занимает около 75 секунд (!). В течение этого времени блокируется вся обработка резервного копирования, поскольку блокируется исключительно индекс.

Индексу в памяти, вероятно, нужен способ объединить новый индекс, не перебирая все, возможно, даже реализованный постепенно. Это должно быть возможно путем предварительной сортировки нового индекса, а затем объединения его с MasterIndex с помощью одного сканирования индексной таблицы.

@MichaelEischer Большое спасибо за ваш тест! На самом деле, текущая реализация требует обращения к индексной таблице каждый раз, когда записывается индексный файл. Итак, если я правильно рассчитал, было создано 13 новых индексных файлов, верно?
Хорошим сообщением является то, что поиски в IndexJSON (которые не отсортированы и, следовательно, требуют линейного поиска) вообще не отображаются, так что это кажется хорошим способом обработки «незавершенных» индексных файлов.

На самом деле я считаю, что использование почти отсортированной таблицы с sort.Sort должно быть почти оптимальным. (Тем не менее, стоит попробовать sort.Stable или чистую сортировку слиянием...) Я действительно думаю, что основная проблема заключается в следующем: если мы хотим иметь один отсортированный индексный массив и вставлять в него вещи, нам нужно для перемещения ~ 6,5 ГБ данных в памяти.
Поэтому я предлагаю отделить записи индекса, прочитанные из индексных файлов, от вновь сгенерированных (и уже сохраненных). Это создает три структуры данных в реализации MasterIndex. Я уже изменил это в своей ветке refactor-index . Не могли бы вы перепроверить вашу установку?

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

При выполнении резервного копирования фактически было создано 16 новых индексных файлов. 75 секунд были предположением из прогона check , который запускает повторную сортировку только один раз. Как быстрая сортировка (используемая sort.Sort), так и сортировка слиянием имеют в лучшем случае временную сложность O(n log n), поэтому предварительно отсортированные данные _не улучшают производительность._ В качестве побочного примечания индекс в памяти требует меньше места, чем индекс представление на диске ^^ .

Я повторно провел тест с обновленным кодом, и производительность стала намного лучше: cpu-idx-table-split.pdf.

Однако использование отдельного индекса для новых записей индекса просто скрывает, но не решает основную проблему: просто представьте себе запуск резервного копирования, который добавляет несколько миллионов новых файлов, или большую начальную резервную копию. Текущая реализация имеет сложность O(n * n*log n) . Первые n относятся к количеству добавлений к индексу, которое несколько линейно масштабируется с объемом новых данных резервного копирования. И последняя часть n*log n — это уже упомянутая сложность сортировки индекса. Заменив быструю сортировку линейным слиянием, это число сокращается до O(n * n) , что может быть достаточно для размеров индексов менее нескольких десятков гигабайт. Кроме того, он не будет масштабироваться, но я думаю, что репозитории с петабайтами - это отдельная проблема. Линейное слияние может быть даже реализовано для постепенной работы. У меня есть идея, как уменьшить сложность до O(n * log n) , но это увеличит затраты на поиск до O(log^2 n) и потребует внесения значительных изменений в код.

Интересно, существует ли структура данных, которая идеально подходила бы для наших нужд:

  • он должен иметь низкие накладные расходы памяти
  • амортизированная сублинейная скорость запроса, предпочтительно log n или меньше
  • амортизированные сублинейные затраты на вставку, предпочтительно log n или меньше. При вставках n это приведет к затратам O(n * log n) на весь цикл резервного копирования.

При отказе от требований к действительно компактному представлению в памяти идеально подходят хэш-карты, поскольку их запросы и обновления происходят за O(1) . Однако они требуют намного больше памяти: мой текущий прототип использует примерно на 70% больше памяти, чем этот PR.

Одним из решений было бы объединить оба подхода и собирать новые записи в хэш-карте и время от времени объединять их в отсортированный массив (например, когда размер хэш-карты достигает 10% от отсортированного массива).

Тем не менее, вероятно, лучше дождаться отзыва от @fd0 , прежде чем вносить какие-либо существенные изменения. (Извините, что снова раздул дискуссию.)

Интересно, существует ли структура данных, которая идеально подходила бы для наших нужд:

Я бы сказал, что существует «много» структур данных, более или менее удовлетворяющих эти потребности, но не знаю, сколько усилий потребуется, чтобы переключиться на них в restic. В принципе любое B-дерево или вообще древовидные структуры должны удовлетворять эти потребности (хеш-карты действительно неэффективны для таких сценариев, в чем вы также убедились сами в своих измерениях).

Если я не ошибаюсь, B-дерево гарантирует только то, что его узлы заполнены как минимум наполовину. И для этого потребуются указатели, которые более или менее добавляют 8-байтовый указатель на каждые 48 байтов данных. Если B-Tree гарантирует использование пространства в среднем на 70%, то в конечном итоге это приведет к таким же накладным расходам памяти, как и моя реализация на основе хэш-карты. Однако могут быть некоторые варианты, такие как B *-деревья, которые более эффективны.

@michaeldorner да, это «старое традиционное B-дерево» - как вы написали, существует много (до сих пор я видел как минимум десять совершенно разных) структур, подобных B-дереву, каждая из которых имеет разные свойства. Другие хорошо известные древовидные структуры включают, например, красно-черные деревья (популярны для карт , которые должны гарантировать согласованное время и пространство для вставки/удаления; это контрастирует с картами на основе хэшей, которые могут страдать от «скачков» или других чрезмерных пиков как в время и пространство, что в конечном итоге приводит к серьезным проблемам с безопасностью/производительностью ). Существуют также различные «сжатые» карты — не стесняйтесь их искать. Так что вариантов действительно много.

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

Просто мои 2 цента : wink: .

Отсортированный, плотно упакованный массив элементов индекса является золотым стандартом использования памяти. Да, вы можете применить некоторое сжатие, но это поможет только с размером / смещением большого двоичного объекта и идентификатором пакета idx (примерно 16 байтов), остальные 32 байта - это идентификатор большого двоичного объекта (хэши sha256), который, как я полагаю, в основном несжимаемый. Я не уверен, что потенциальное снижение использования памяти еще на 20-30% оправдывает еще более высокую сложность.

Для деревьев всегда требуется один (и некоторое неиспользуемое пространство, как в случае с B-деревьями) или два указателя (красное-черное-дерево и другие) на каждый элемент, что уже добавило бы 8 или 16 байт служебных данных к прибл. 48 байт самой записи индекса. Я также не уверен, насколько хорошо сборщик мусора Go справится с миллионами или даже миллиардами записей в индексе.

Чтобы сохранить честь хэш-таблиц: считается, что хеширование с кукушкой (постоянное время поиска) вместе с 4 записями в корзине обеспечивает более 90% использования пространства. Если вы просто используете десять таких хэш-таблиц, то общие накладные расходы памяти также останутся ниже 33% даже при увеличении хэш-карты.

Я думаю, что главный вопрос заключается в том, нужна ли нам единая структура данных (дерево/хэш-карта) с определенными накладными расходами памяти. Или хотим ли мы снизить накладные расходы отсортированного упакованного массива, которому потребуется вспомогательная структура данных для обеспечения разумной скорости вставки. Если эта вспомогательная структура данных содержит только 10-20% всех данных индекса, то не будет иметь большого значения, имеет ли структура данных 30% или 70% накладных расходов, и в этом случае встроенная хэш-карта Go будет самым простым решением.

Использование индекса на диске имеет свою долю проблем: если индекс слишком велик для хранения в памяти, то производительность индекса, вероятно, упадет на жестких дисках (на твердотельных накопителях должно быть намного лучше), если только индекс не сможет извлечь выгоду из расположение между большими двоичными объектами, например, для одной папки/файла (в полностью отсортированном списке идентификаторов больших двоичных объектов шаблон доступа будет в основном равномерно случайным).

@MichaelEischer, спасибо за подведение итогов. Тем не менее, я все же рекомендую вам попробовать дисковый кеш (просто чтобы получить представление о возможной производительности, а не о включении в Restic), используя sqlite3 с различными настройками (например, как с индексом, так и без него, с повышенными максимальными ограничениями памяти, чем очень низкий по умолчанию, используя только одну таблицу в качестве хранилища, похожего на карту значений ключа, и т. д.), используя при этом некоторые особенности sqlite3, такие как встроенный целочисленный первичный ключ и многопоточный доступ без явной блокировки (sqlite обрабатывает это сам) и используя одну огромную транзакцию для всех доступов, а не используя WAL, поскольку он значительно медленнее для чтения (но немного быстрее для записи).

Использование дискового кэша также может значительно выиграть от потоковой обработки, но не знаю, подходит ли это для быстрого опробования. Просто идея, основанная на моем хорошем опыте хранения ключей и значений на основе sqlite3 : wink: .

@aawsome Не могли бы вы разделить оптимизацию «списка больших двоичных объектов» на отдельный PR?

Я не планирую объединять удаление blob IDSet с checker.go , поскольку #2328 также обеспечивает эту оптимизацию, но без необходимости использования основного индекса.

Это оставляет LookupAll в качестве последнего незначительного изменения. Эта функция будет полезна при добавлении поддержки поврежденных репозиториев в prune. Однако без предварительной оптимизации производительности индекса это, вероятно, просто снизит общую производительность restic. Так что с этим придется подождать.

Что касается основных изменений, то они также пока должны быть отложены.

@dumblob Проблема с использованием sqlite или любой другой универсальной базы данных для индекса на диске заключается в том, что без оптимизации, использующей локальность между большими двоичными объектами в файле (т. Е. Большие двоичные объекты в файле попадают в соседние пакеты и обычно доступны вместе), мы получим псевдослучайный доступ ко всем частям индекса, который больше, чем основная память, и поэтому должен загружать данные из случайных мест на диске. То есть общая реализация базы данных не имеет шансов обеспечить хорошую производительность.

@aawsome Не могли бы вы разделить оптимизацию «списка больших двоичных объектов» на отдельный PR?

Конечно. Скоро будет пиар.

Это оставляет LookupAll в качестве последнего незначительного изменения. Эта функция будет полезна при добавлении поддержки поврежденных репозиториев в prune. Однако без предварительной оптимизации производительности индекса это, вероятно, просто снизит общую производительность restic. Так что с этим придется подождать.

На самом деле фактическая реализация Lookup является (частично) LookupAll в том смысле, что она возвращает все результаты в индексном файле, но только результаты первого индексного файла, который имеет совпадение. .

Поскольку обычно в одном индексном файле нет дубликатов, это фактически не дает дубликатов даже в тех случаях, когда они могут понадобиться.

Я подготовлю PR, чтобы изменить Lookup и добавить LookupAll там, где это полезно. Тогда мы можем обсудить, полезно это или нет.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги