Я читаю данные из больших CSV файлов, обрабатываю их и загружаю в базу данных SQLite. Профилирование предполагает, что 80% моего времени тратится на ввод-вывод, а 20% обрабатывает ввод, чтобы подготовить его для вставки БД. Я ускорил шаг обработки с помощью multiprocessing.Pool
, чтобы код ввода/вывода никогда не ожидал следующей записи. Но это вызвало серьезные проблемы с памятью, потому что шаг ввода-вывода не мог идти в ногу с рабочими.
Следующий пример игрушки иллюстрирует мою проблему:
#!/usr/bin/env python # 3.4.3
import time
from multiprocessing import Pool
def records(num=100):
"""Simulate generator getting data from large CSV files."""
for i in range(num):
print('Reading record {0}'.format(i))
time.sleep(0.05) # getting raw data is fast
yield i
def process(rec):
"""Simulate processing of raw text into dicts."""
print('Processing {0}'.format(rec))
time.sleep(0.1) # processing takes a little time
return rec
def writer(records):
"""Simulate saving data to SQLite database."""
for r in records:
time.sleep(0.3) # writing takes the longest
print('Wrote {0}'.format(r))
if __name__ == "__main__":
data = records(100)
with Pool(2) as pool:
writer(pool.imap_unordered(process, data, chunksize=5))
Этот код приводит к отставанию записей, которые в конечном итоге потребляют всю память, потому что я не могу быстро сохранить данные на диске. Запустите код, и вы заметите, что Pool.imap_unordered
будет потреблять все данные, когда writer
находится на 15-й записи или около того. Теперь представьте, что на этапе обработки производятся словари из сотен миллионов строк, и вы можете понять, почему у меня заканчивается память. Amdahl Law в действии возможно.
Какое исправление для этого? Я думаю, мне нужен какой-то буфер для Pool.imap_unordered
, который гласит: "Когда есть x-записи, которые требуют вставки, останавливаются и ждут, пока их не будет меньше x, прежде чем создавать больше". Я должен быть в состоянии получить некоторое улучшение скорости от подготовки следующей записи, пока последняя сохраняется.
Я попытался использовать NuMap
из модуля papy
(который я модифицировал для работы с Python 3), чтобы сделать именно это, но это было не быстрее. Фактически, это было хуже, чем запуск программы последовательно; NuMap
использует два потока плюс несколько процессов.
Функции массового импорта SQLite, вероятно, не подходят для моей задачи, потому что данные требуют существенной обработки и нормализации.
У меня есть около 85G сжатого текста для обработки. Я открыт для других технологий баз данных, но выбрал SQLite для удобства использования и потому, что это однократное чтение-многозадачное задание, в котором только 3 или 4 человека будут использовать полученную базу данных после того, как все будет загружено.
Поскольку обработка выполняется быстро, но запись выполняется медленно, это звучит так, как будто ваша проблема I/O-связанным. Следовательно, от использования многопроцессорная.
Однако можно отделить куски data
, обработать кусок и
до тех пор, пока эти данные не будут записаны перед отрывом другого фрагмента:
import itertools as IT
if __name__ == "__main__":
data = records(100)
with Pool(2) as pool:
chunksize = ...
for chunk in iter(lambda: list(IT.islice(data, chunksize)), []):
writer(pool.imap_unordered(process, chunk, chunksize=5))
multiprocessing
функцию, которая выполняет imap
с каким-то параметром буфера.
Поскольку я работал над одной и той же проблемой, я решил, что эффективным способом предотвращения переполнения пула является использование семафора с генератором:
from multiprocessing import Pool, Semaphore
def produce(semaphore, from_file):
with open(from_file) as reader:
for line in reader:
# Reduce Semaphore by 1 or wait if 0
semaphore.acquire()
# Now deliver an item to the caller (pool)
yield line
def process(item):
result = (first_function(item),
second_function(item),
third_function(item))
return result
def consume(semaphore, result):
database_con.cur.execute("INSERT INTO ResultTable VALUES (?,?,?)", result)
# Result is consumed, semaphore may now be increased by 1
semaphore.release()
def main()
global database_con
semaphore_1 = Semaphore(1024)
with Pool(2) as pool:
for result in pool.imap_unordered(process, produce(semaphore_1, "workfile.txt"), chunksize=128):
consume(semaphore1, result)
См. также:
Используя подход Семафор от этого ответа, я создал оболочку для итератора, чтобы ограничить количество полученных значений.
Похоже, все, что вам действительно нужно, - это заменить неограниченные очереди под Pool
ограниченными (и блокирующими) очередями. Таким образом, если какая-либо сторона опередит остальных, она просто блокируется, пока не будет готова.
Это было бы легко сделать, просмотрев источник, в подкласс или monkeypatch Pool
, что-то вроде:
class Pool(multiprocessing.pool.Pool):
def _setup_queues(self):
self._inqueue = self._ctx.Queue(5)
self._outqueue = self._ctx.Queue(5)
self._quick_put = self._inqueue._writer.send
self._quick_get = self._outqueue._reader.recv
self._taskqueue = queue.Queue(10)
Но это явно не переносимо (даже для CPython 3.3, а тем более для другой реализации Python 3).
Я думаю, что вы можете сделать это переносимо в 3.4+, предоставив настраиваемый context
, но я не смог получить это право, поэтому...
_taskqueue
фактически назначается после _setup_queues
. Помещение максимального размера в очередь также не работает из-за структуры imap_unordered
, которая никогда не вернет генератор, потому что очередь блокируется, если установлен максимальный размер.
concurrent.futures.ProcessPoolExecutor; on top of
многопроцессорности. Пул , you have to basically rewrite the
функции карты с нуля поверх отдельных представлений…