Учитывая, что набор данных объемом 1 ТБ на диске содержит около 1 КБ на запись данных, как найти дубликаты, используя 512 МБ ОЗУ и бесконечное дисковое пространство? - PullRequest
45 голосов
/ 04 апреля 2010

На диске имеется 1 ТБ данных с объемом около 1 КБ на одну запись данных. Как найти дубликаты, используя 512 МБ ОЗУ и бесконечное дисковое пространство?

Ответы [ 8 ]

19 голосов
/ 05 апреля 2010

Предлагаемые решения кажутся слишком сложными. Фильтр Блума , являющийся структурой данных du jour за последние несколько лет, не рекомендуется применять в такой ситуации: потому что никакие данные не могут быть связаны с хешированным содержимым вы должны не только поддерживать фильтр Блума, но вы все равно должны записывать каждое (только 6-битное!) хеш-значение и записывать на диск, уничтожая преимущество фильтра Блума и имея нелепо высокую частоту столкновений.

С другой стороны, сортировка слиянием всего терабайта потребует не только O(n log n) сравнений, но и O(n log n) дискового трафика, поскольку большинство промежуточных файлов придется объединять с диска, а не из памяти , Любое реальное решение должно пытаться максимально сократить дисковый трафик, так как это наше основное узкое место.

Мое решение простое, исходя из одного предположения: терабайт данных записывается в один файл.

Перебирайте записи файла терабайта и хешируйте их. Криптографический хеш здесь не нужен, дорог и слишком велик; вместо этого используйте что-то вроде 64-битной версии murmurhash . Он может хэшировать более 2 ГиБ / с (намного быстрее, чем нам, вероятно, потребуется, учитывая скорость хранения в наши дни) и имеет отличное (хотя и не криптографически безопасное) сопротивление столкновениям. С 64-битным хешем мы ожидаем, что наше первое столкновение будет 2 ^ 32 , поэтому вполне вероятно, что примерно у нашего миллиарда записей вообще не будет никаких коллизий.

Записать хэши и связанные с ними записи смещения в другой файл. Поскольку записи содержат произвольные двоичные данные, мы не можем полагаться на сортировку Unix (1) для ее сортировки, потому что некоторые из хэшей и смещений могут содержать то, что sort (1) будет интерпретировать как переводы строки. Мы просто запишем записи как записи фиксированной ширины (вероятно, 16 байтов: 8 байтов для 64-битного хэша murmur2 и 8 байтов для смещения в терабайтном файле). Полученный файл должен быть около 16 ГБ, учитывая наше количество записей.

Мы можем отсортировать этот файл, прочитав количество записей, которые безопасно поместятся в память, и отсортировав их, сбросив отсортированные фрагменты обратно на диск. Мы можем разместить больше записей в памяти с помощью heapsort (он использует пространство O(1)), чем с быстрой сортировкой (которая использует O(log n) память для стека вызовов), но в большинстве реализаций quicksort выигрывает благодаря своей локальности памяти и ниже. количество команд Эти промежуточные файлы (их должно быть 35-40) будут записаны на диск.

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

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

Для диска I / O он будет читать терабайтный файл данных, записывать 16 ГБ на диск, считывать эти 16 ГБ с диска и записывать обратно отсортированными, затем читать и возвращать дубликаты. В качестве оптимизации процесс хеширования записей может накапливать их в памяти, прежде чем записывать их на диск, а затем сортировать их: это обрезает промежуточный файл размером 16 ГБ и позволяет процессу перейти непосредственно от хеширования к объединению и составлению отчетов о дубликатах. .

18 голосов
/ 04 апреля 2010

Используйте Фильтр Блума : таблица одновременных хэшей. Согласно Википедии, оптимальное количество хэшей составляет ln(2) * 2^32 / 2^30 ≈ 2.77 ≈ 3. (Хм, подключение 4 дает меньше ложных срабатываний, но 3 все же лучше для этого приложения.) Это означает, что у вас есть таблица 512 мегабайт, или 4 гигабит, и обработка каждой записи устанавливает три новых бита в этом огромном море. Если все три бита уже установлены, это потенциальное совпадение. Запишите три хеш-значения в файл. В противном случае запишите их в другой файл. Запишите индекс записи вместе с каждым совпадением.

(Если допустимая ошибка 5%, пропустите большой файл и используйте маленький файл в качестве результатов.)

Когда вы закончите, у вас должен быть файл с примерно 49M возможных положительных совпадений и файл с 975M отрицательных совпадений, которые еще могут совпадать с положительными. Прочитайте первое в vector<pair<vector<uint32_t>,vector<uint32_t> > > (индексы во втором vector, первое может быть array) и отсортируйте его. Поместите индексы в другой vector<uint32_t>; они уже отсортированы. Прочитайте большой файл, но вместо установки битов в таблицу найдите значения хеш-функции в vector. (Например, используйте equal_range.) Используйте список индексов положительного файла, чтобы отслеживать индекс текущей записи в отрицательном файле. Если совпадений не найдено, игнорируйте. В противном случае добавьте индекс записи match->second.push_back(current_negative_record_index).

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

Всего синхронных дисковых операций ввода-вывода: (один проход = 1 ТиБ) + (96 хэш-битов на запись = 12 ГиБ) + (32 индексных бита на каждый положительный = ~ 200 МБ).

Окончательное редактирование (серьезно): Если подумать, аспект фильтра Блума может и не помочь. Количество хеш-данных является более ограничивающим фактором, чем количество ложных срабатываний. С помощью только одной хэш-функции общий объем хеш-данных составит 4 ГиБ, а индексы 124 миллионов ожидаемых ложных срабатываний составят ~ 500 МБ. Это должно глобально оптимизировать эту стратегию.

Разъяснение (получил отрицательное голосование): есть различие между ложным срабатыванием фильтра Блума и коллизией хэшей. Коллизия хешей не может быть решена, кроме как путем возврата к исходным записям и сравнения, что является дорогостоящим. Ложноположительный результат Блума можно устранить, вернувшись к исходным значениям хеша и сравнив их, что и делает второй проход этого алгоритма. Итак, подумав, фильтр с одним хешем, описанный в «окончательном» редактировании, может привести к поиску на диске. Фильтр Блума с двумя хэшами увеличил бы количество ложных срабатываний, попадающих в одну корзину карты match, и уменьшил бы количество ложных срабатываний до десятков миллионов.

13 голосов
/ 04 апреля 2010

Это много записей ;-) порядка 1 000 000 000. лучше быть умным об этом ...

Природа записей не определена: мы просто обнаруживаем их по одному, последовательно читая их, или есть какой-то индекс, или они хранятся в виде файлов в различных каталогах? В вопросе также не указывается наличие базы данных, которую мы можем использовать для данных, подобных индексам (вместо того, чтобы сортировать их по собственному коду). Кроме того, [даже приблизительное] представление о количестве дубликатов поможет направить некоторые варианты выбора на эффективный процесс.

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

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

Информация, полезная в индексе, будет:

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

Выбор хеша имеет решающее значение: следует отдавать предпочтение быстрому алгоритму за счет одного, который идеально распределен; количество байтов, хэшированных для каждой записи, также является компромиссом, возможно, от 100 до 200 байтов (т. е. примерно от 10 до 20% среднего размера записи) является хорошим значением, в зависимости от ожидаемого соотношения дубликатов и в зависимости от экономии времени. это обеспечивает (по сравнению с хешированием всю запись). (см. правку ниже)

Как только такой индекс будет доступен, мы можем [относительно быстро / без усилий] получить количество возможных дубликатов; на основе этого результата может быть выполнен второй проход, направленный на улучшение качества индекса, если он не считается достаточно избирательным (исключая записи, которые легко считаются уникальными). Этот второй проход может вычислить другой хеш на всю запись (исключая первые x байтов первого хеша) или на еще одном подмножестве записи. Обратите внимание, что благодаря индексу, этот второй проход может быть многопоточным, если это возможно.

Второй или последний проход требует сортировки записей в группе возможных совпадений (одинаковой длины, одинакового хеш-кода (-ов), одинаковых первых х байтов). Это может быть достигнуто, как описано Pax Diablo, преимущество индекса в том, что такая операция, опять же, может быть многопоточной и включает в себя гораздо меньшие наборы (многие из них). Добавлено : Здесь снова Ник Джонсон замечает, что второй проход может быть ненужным, если мы используем длинный хэш-код (он предлагает SHA1 длиной 128 байт). Предполагая, что нет никакой выгоды в частичном хешировании записей, это очень правдоподобное решение, так как индекс может находиться на диске и все же быстрее сортироваться и храниться, чем если бы мы сортировали / сохраняли целые записи.


Редактировать : Ник Джонсон замечательно показывает, что задержка поиска в дисковом хранилище может быть такой, что простое последовательное чтение будет быстрее, а узким местом является дисковый ввод-вывод ограниченная, функция быстрого хеширования, выполняемая одновременно, может эффективно быть быстрее, чем последовательное чтение, и, следовательно, не добавлять к общему процессу. Это вероятная возможность (особенно если последовательное чтение, если эффективно требуется для определения начала / конца каждой записи и т. Д.), И именно поэтому я «откорректировал свою ставку», написав «1040 * в зависимости от экономии времени, это обеспечивает ... ". Это говорит о том, что фактическая структура записей на диске является одним из открытых параметров вопроса (например, если мы просто читаем из отдельных файлов в каталогах, следовательно, вводим непоследовательное чтение), а также хранилище размером с TeraByte, вероятно, поддерживается причудливым RAID, где задержка поиска, оставаясь проблемой, как правило, значительно улучшена.
Я поддерживаю мое предложение о том, что двухпроходный подход может быть более эффективным, чем тот, в котором каждая запись полностью хэшируется, но я бы хотел подчеркнуть возможность и преимущества однопроходной подход. Как и во многих вопросах интервью, некоторые характеристики ситуации были не определены; идея заключается не столько в том, чтобы заявитель предоставил абсолютно правильный ответ (хотя некоторые ответы могут быть совершенно неправильными!), а в том, чтобы получить представление о его / ее мыслительном процессе и способности определять варианты и точки принятия решений.

6 голосов
/ 04 апреля 2010

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

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

3 голосов
/ 04 апреля 2010

Загружайте данные в память 512 М за один раз, затем сортируйте этот блок и записывайте его на диск (как свой собственный файл). После того, как весь 1T был сделан таким образом, объедините отдельные файлы в один большой файл honkin ', затем последовательно прочитайте этот большой (отсортированный) файл, записав его в окончательный файл, удаляя дубликаты записей.

1T, 512M за раз, будет около 2,1 миллиона файлов (при условии, что двоичные определения единиц СИ, а не десятичные). 512M записей размером 1 Кбайт позволяют одновременно хранить в памяти 524 288 записей, поэтому вам, вероятно, придется выполнять сортировку слиянием в два этапа. Другими словами, объедините-сортируйте 2,1 миллиона файлов в четыре группы, чтобы создать четыре больших файла, а затем объедините-сортируйте эти четыре в большой отсортированный файл. Затем вы последовательно обрабатываете это, чтобы удалить дубликаты.

Сортировка слиянием - это просто объединение нескольких уже отсортированных файлов, просто выбрав первую оставшуюся запись из каждого файла и выбрав «самую низкую». Например, два файла a и b:

a   b
7   6
3   5
1   4
    2
 \_/
  1 (a)
  2 (b)
  3 (a)
  4 (b)
  5 (b)
  6 (b)
  7 (a)
1 голос
/ 18 сентября 2012

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

1 голос
/ 04 апреля 2010

Сначала настройте на компьютере бесконечно большой файл подкачки на бесконечно большом диске ...

1 голос
/ 04 апреля 2010

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

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

Учитывая размеры - 0,5 ГБ памяти, 1000 ГБ данных, 1 КБ на запись, то есть около 1 миллиарда записей - при условии 256-битного хэша (хотя 128-битного вполне может быть достаточно), мы бы использовали 32 байта для хэша и 4 байтов для номера записи и около 1 миллиарда записей нам потребуется около 36 ГБ для файлов сортировки, сгенерированных в 500 МБ файлах (соответствует доступной памяти), так что будет 70-80 файлов слиться в конце, что кажется довольно управляемым. В списке будут указаны номера записей - вам нужно будет получить доступ к файлу объемом 1 ТБ, чтобы прочитать записи. Вам нужно подумать, что вы собираетесь делать с дубликатами; нужна ли вам информация о первоначальной записи и дополнений, и имеет ли значение, какие из дубликатов вы храните, а какие отклоняете. И т.д.

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