Чтение из одного большого файла и запись во многие (десятки, сотни или тысячи) файлов в Java?

У меня есть большой файл (сжатый 4-5 ГБ) небольших сообщений, которые я хочу разобрать примерно на 6000 файлов по типу сообщения. Сообщения маленькие; от 5 до 50 байт в зависимости от типа.

Каждое сообщение начинается с поля типа фиксированного размера (6-байтовый ключ). Если я прочитаю сообщение типа «000001», я хочу write добавить его полезную нагрузку к 000001.dat и т. д. Входной файл содержит смесь сообщений; Мне нужны N однородных выходных файлов, где каждый выходной файл содержит только сообщения данного типа.

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

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

Спасибо!


person Rudiger    schedule 30.12.2009    source источник
comment
Что вы подразумеваете под эффективным? Вы хотите оптимизировать количество времени, которое занимает процесс? Объем памяти? Количество дискретных чтений из файловой системы? Сколько раз каждый выходной файл записывается на диск? Знание этого поможет найти ответы.   -  person delfuego    schedule 30.12.2009
comment
Пожалуйста, укажите, будет ли во входном файле несколько вхождений 000001, и будет ли это означать, что 2-е и последующее вхождения должны быть объединены в файл 000001.dat? Это будет иметь значение для наиболее эффективной реализации.   -  person Carl Smotricz    schedule 30.12.2009
comment
Являются ли данные последовательными, или они выиграют от отображенного ввода-вывода с произвольным доступом?   -  person Kristopher Ives    schedule 30.12.2009
comment
@carl: исходя из его цифр (4 ГБ данных, 6000 ключей, 50 байт на сообщение), я ожидаю более 50000 сообщений на ключ.   -  person President James K. Polk    schedule 30.12.2009
comment
Я исхожу из этого предположения. Ах, он отредактировал вопрос, и теперь похоже, что это действительно так. @Rudiger: Мой ответ сейчас касается такого положения дел, и я надеюсь, что это будет оптимально.   -  person Carl Smotricz    schedule 30.12.2009
comment
delfuego: я отредактировал сообщение, чтобы уточнить; Спасибо. Карл Смотрич: Для самых популярных типов могут быть миллионы сообщений. Кристофер Айвз: Не могли бы вы уточнить, как может помочь ввод-вывод с отображением памяти? Однако данные последовательны.   -  person Rudiger    schedule 30.12.2009
comment
Обратите внимание, что в большинстве файловых систем существует минимальный размер файла. Например, размер кластера по умолчанию в NTFS составляет 4 КБ. В зависимости от файловой системы некоторые операции также могут стать очень медленными, если вы поместите в папку тысячи файлов. Хранение сообщений в базе данных, вероятно, является лучшей идеей.   -  person Wim Coenen    schedule 30.12.2009
comment
Я понял, что мое решение может быть не интуитивно понятным, поэтому я разработал свой ответ с помощью псевдокода. Не проверено, естественно ;)   -  person Carl Smotricz    schedule 31.12.2009


Ответы (6)


Unix-подобная система обычно имеет ограничение на количество дескрипторов файлов, открытых в любой момент времени; в моем Linux, например, в настоящее время это 1024, хотя я мог бы изменить его в разумных пределах. Но для этих ограничений есть веские причины, так как открытые файлы являются бременем для системы.

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

Но если в вашем вводе есть несколько сообщений для одного и того же ключа, было бы эффективно держать открытыми большое количество файлов. Однако я бы посоветовал не пытаться держать все 6000 открытыми одновременно. Вместо этого я бы выбрал что-то вроде 500, открытых в порядке живой очереди; то есть вы открываете файлы для первых 500 (или около того) отдельных ключей сообщений, а затем просматриваете весь входной файл в поисках материала, который можно добавить в эти 500, а затем закрываете их все при нажатии EOF при вводе. Вам также нужно будет сохранить HashSet уже обработанных ключей, потому что затем вы снова перечитаете свой входной файл, обрабатывая следующую партию из 500 ключей, которые вы не уловили в первом раунде.

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

Псевдокод:

processedSet = [ ]
keysWaiting = true
MAXFILE = 500
handlesMap = [ ]
while (keysWaiting) {
  keysWaiting = false
  open/rewind input file
  while (not EOF(input file)) {
    read message
    if (handlesMap.containsKey(messageKey)) {
       write data to handlesMap.get(messageKey)
    } else if (processedSet.contains(messageKey) {
       continue // already processed
    } else if (handlesMap.size < MAXFILE) {
       handlesMap.put(messageKey, new FileOutputStream(messageKey + ".dat")
       processedSet.add(messageKey)
       write data to handlesMap.get(messageKey)
    else
       keysWaiting = true
    endif
  }
  for all handlesMap.values() {
     close file handle
  }
  handlesMap.clear
}
person Carl Smotricz    schedule 30.12.2009
comment
Я писал именно это. Я думаю, что это лучший вариант - разделяй и властвуй. - person Ravi Wallau; 31.12.2009
comment
Спасибо за Вашу поддержку! Я начал думать, что сошел с ума, потому что мое решение не видело голосов. +1 вам за ваши проблемы, мне (почти!) жаль, что я вас опередил;) - person Carl Smotricz; 31.12.2009
comment
Имейте в виду, что вы можете потерять проход (или больше), если ваши первые 500 различных типов записей также различаются по всему файлу. Хотя маловероятно, что это так, невозможно сказать, не взглянув на данные. Хотя ваш ответ заслуживает внимания, ни один из нас не может сказать, что у нас есть лучшее решение, не видя данных и/или системы, в которой должен выполняться процесс. - person Rory; 31.12.2009
comment
Мое решение гарантирует, что каждый выходной файл будет открыт и закрыт ровно один раз. При этом он оптимален для общего случая. Это расточительно только в крайнем случае, когда входной файл отсортирован или почти так, по ключам сообщений. В этом случае авантюра с ранним закрытием выходных файлов и освобождением места для новых действительно окупится. - person Carl Smotricz; 31.12.2009
comment
Нет, я понимаю логику. Я имею в виду, что если первые 500 записей, которые вы читаете (то есть те, которые вы выбираете для текущего прохода), различаются по всему файлу, нет смысла читать до конца файла, так как вы не найдете нужно выписать больше записей. Другими словами, вы потратили время на чтение всего файла в поисках того, чего там нет. Опять же, я хотел бы пояснить, что это всего лишь возможность, и никто не может сказать, произойдет это или не произойдет, не видя данных. - person Rory; 31.12.2009
comment
Хорошо, согласен. Это зов ОП, что он хочет сделать, я закончил порку этой дохлой лошади. - person Carl Smotricz; 31.12.2009
comment
Спасибо всем. Решение Карла сработало отлично; кстати, мой клиент хотел только 100 лучших сообщений, так что это закончилось намного быстрее. - person Rudiger; 01.01.2010

Вам может не понадобиться хэш-карта. Ты мог бы просто...

  1. Прочитать сообщение
  2. Откройте новый файл в режиме добавления
  3. Запишите сообщение в новый файл
  4. Закройте новый файл

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

person Pace    schedule 30.12.2009
comment
+1, это первое, что нужно попробовать, потому что это просто и понятно. Только если производительность неприемлема, я бы приступил к чему-то более сложному, например кешированию открытых выходных потоков. - person President James K. Polk; 30.12.2009
comment
Хорошее простое решение, но если некоторые выходные файлы в конечном итоге открываются 1000 раз, это, конечно, не самое быстрое! - person Carl Smotricz; 31.12.2009
comment
@Карл: откуда ты знаешь? Современной ОС (или, точнее, комбинации ОС и файловой системы) не потребуется никакого физического ввода-вывода для повторного открытия файла, который был открыт несколько секунд назад. - person Joachim Sauer; 31.12.2009
comment
Если одновременно читается файл размером 5 ГБ и одновременно записываются тысячи файлов, можно с уверенностью сказать, что даже современная ОС не увидит альтернативы, кроме как поспешно отказаться от своих буферов ввода-вывода. Но даже без физического ввода-вывода открытие файла обходится дорого из-за нескольких системных вызовов, необходимых для этого, особенно с append. - person Carl Smotricz; 31.12.2009
comment
+1, было бы достаточно тривиально добавить к этому пул потоков, если он окажется медленным, но вы будете удивлены, насколько хорошо вам поможет ОС. - person PSpeed; 31.12.2009
comment
@Carl, это может быть правдой и зависит от нескольких факторов, но остальная часть кода не сильно изменится, если будут добавлены пулы, и ОП будет уверен, что все это работает, прежде чем добавлять сложность. Хотя внедрить простой кэш LRU на шаге 2 не очень сложно. - person PSpeed; 31.12.2009

Я бы порекомендовал какое-то интеллектуальное объединение: держите открытыми самые большие/наиболее часто используемые файлы для повышения производительности и закрывайте остальные для экономии ресурсов.

Если основной файл состоит в основном из записей типов 1–5, держите эти файлы открытыми до тех пор, пока они необходимы. Остальные можно открывать и закрывать по мере необходимости, чтобы не истощать ресурсы системы.

person Rory    schedule 30.12.2009
comment
Определенные типы составляют подавляющее большинство (99,9%) сообщений, так что это хорошее предложение. - person Rudiger; 30.12.2009
comment
Должно быть легко сделать его адаптивным, чтобы вам не приходилось его модифицировать, если эти соотношения меняются. Просто следите за тем, сколько записей вы написали, периодически упорядочивайте их по этим значениям, закрывайте те, которые вам больше не нужны, и открывайте те, которые находятся вверху списка. - person Rory; 30.12.2009
comment
Только сейчас увидел этот комментарий. Я считаю, что мой подход автоматически справляется с этой ситуацией с очень небольшим интеллектом. - person Carl Smotricz; 30.12.2009

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

  • Каждое сообщение начинается с типа сообщения в виде поля фиксированного размера.
  • У вас есть гетерогенный входной файл, содержащий смесь сообщений; вам нужны N однородных выходных файлов, где каждый выходной файл содержит только сообщения данного типа.

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

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

Вероятно, лучший подход — установить буфер с фиксированным количеством для каждого функтора, чтобы он открывался, записывался и закрывался, скажем, после 10 сообщений. Если ваши сообщения имеют максимальный размер 50 байт, то это 500 байт (10 x 50) x 6000, которые останутся в памяти в любой момент времени.

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

public class MessageProcessor
{
    int _msgSize;                   // the number of bytes to read per message
    byte[] _buf = new byte[1024];   // bigger than I said, but it's only 6 Mb total
    int _curSize;                   // when this approaches _buf.length, write
person kdgregory    schedule 30.12.2009

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

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

person David Thornley    schedule 30.12.2009

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

Вместо этого, почему бы не сопоставить каждый ключ с буфером? в конце запишите каждый буфер на диск. Или, если вы обеспокоены тем, что у вас будет слишком много памяти, вы можете структурировать свои буферы так, чтобы записывать каждые 1 КБ, или 5 КБ, или любые другие строки. например

public class HashLogger {

          private HashMap<String,MessageBuffer> logs;

          public void write(String messageKey, String message)
          {
              if (!logs.contains(messageKey)) { logs.put(messageKey, new MessageBuffer(messageKey); }
              logs.get(messageKey).write(message);
          }

         public void flush()
         {
             for (MessageBuffer buffer: logs.values())
             {
                buffer.flush();
             }
            // ...flush all the buffers when you're done...
         }

    private class MessageBuffer {
             private MessageBuffer(String name){ ... }
             void flush(){ .. something here to write to a file specified by name ... }
             void write(String message){
             //... something here to add to internal buffer, or StringBuilder, or whatever... 
             //... you could also have something here that flushes if the internal builder gets larger than N lines ...
     }
}

Вы даже можете создать отдельные регистраторы Log4j, которые можно настроить для использования буферизованного ведения журнала, я был бы удивлен, если бы более современные среды ведения журналов, такие как slf4j, также не поддерживали это.

person Steve B.    schedule 30.12.2009
comment
Я думаю, что с публикациями SO лучше предположить, что автор дает вам самое простое определение своей проблемы; Я знаю, что знаю, потому что не хочу утомлять людей подробностями. Эй, может быть, для плаката это лучший вариант из-за встроенного форматирования сообщений. Что бы ни. - person Steve B.; 31.12.2009