Python fork: «Невозможно выделить память», если процесс потребляет более 50% пользы. объем памяти - PullRequest
0 голосов
/ 26 июня 2018

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

Вот пример сценария, иллюстрирующего проблему:

import os
import psutil
import subprocess
pid = os.getpid()
this_proc = psutil.Process(pid)
MAX_MEM = int(psutil.virtual_memory().free*1E-9) # in GB
def consume_memory(size):
    """ Size in GB """
    memory_consumer = []
    while get_mem_usage() < size:
        memory_consumer.append(" "*1000000) # Adding ~1MB
    return(memory_consumer)

def get_mem_usage():
    return(this_proc.memory_info()[0]/2.**30)

def get_free_mem():
    return(psutil.virtual_memory().free/2.**30)

if __name__ == "__main__":
    for i in range(1, MAX_MEM):
        consumer = consume_memory(i)
        mem_usage = get_mem_usage()
        print("\n## Memory usage %d/%d GB (%2d%%) ##" % (int(mem_usage), 
              MAX_MEM, int(mem_usage*100/MAX_MEM)))
        try:
            subprocess.call(['echo', '[OK] Fork worked.'])
        except OSError as e:
            print("[ERROR] Fork failed. Got OSError.")
            print(e)
        del consumer

Скрипт был протестирован с Python 2.7 и 3.6 на Arch Linux и использует psutils для отслеживания использования памяти. Он постепенно увеличивает использование памяти процессом Python и пытается обработать процесс с помощью subprocess.call (). Forking не удается, если более 50% прибыли. память используется родительским процессом.

## Memory usage 1/19 GB ( 5%) ##
[OK] Fork worked.

## Memory usage 2/19 GB (10%) ##
[OK] Fork worked.

## Memory usage 3/19 GB (15%) ##
[OK] Fork worked.

[...]

## Memory usage 9/19 GB (47%) ##
[OK] Fork worked.

## Memory usage 10/19 GB (52%) ##
[ERROR] Fork failed. Got OSError.
[Errno 12] Cannot allocate memory

## Memory usage 11/19 GB (57%) ##
[ERROR] Fork failed. Got OSError.
[Errno 12] Cannot allocate memory

## Memory usage 12/19 GB (63%) ##
[ERROR] Fork failed. Got OSError.
[Errno 12] Cannot allocate memory

## Memory usage 13/19 GB (68%) ##
[ERROR] Fork failed. Got OSError.
[Errno 12] Cannot allocate memory

[...]

Обратите внимание, что у меня не был активирован своп при запуске этого теста.

Похоже, есть два варианта решения этой проблемы:

  • Использование свопа, по крайней мере, вдвое превышающего объем физической памяти.
  • Изменение параметра overcommit_memory: echo 1> / proc / sys / vm / overcommit_memory

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

Также, к сожалению, заблаговременное разветвление необходимых процессов перед использованием памяти.

У кого-нибудь есть другие предложения о том, как решить эту проблему?

Спасибо!

Бест

Leonhard

1 Ответ

0 голосов
/ 01 июля 2018

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

Python или нет, вы имеете дело с тем, как Linux (или аналогичная система) создают новые процессы. Ваш родительский процесс сначала вызывает fork (2) , который создает новый дочерний процесс как копию самого себя. В то время он фактически не копирует себя в другое место (он использует копирование при записи), тем не менее, он проверяет, доступно ли достаточно места, и если нет, устанавливает errno в 12: ENOMEM -> исключение OSError, которое вы ' Смотрю.

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

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

Кажется, есть еще один вариант, но выглядит ... грязно. Существует еще один системный вызов vfork () , который создает новый процесс, который первоначально разделяет память со своим родителем, выполнение которого в этот момент приостановлено. Этот недавно созданный дочерний процесс может устанавливать только переменную, возвращаемую vfork, он может _exit или exec. Как таковой, он не предоставляется через какой-либо интерфейс Python, и если вы попытаетесь (я сделал) загрузить его непосредственно в Python, используя ctypes, это приведет к segfault (я предполагаю, что Python все равно будет делать что-то другое, а не только три действия, упомянутые после * 1022). * и прежде чем я смог exec что-то еще в ребенке).

Тем не менее, вы можете делегировать все vfork и exec общему объекту, в который вы загружаете. В качестве очень грубого доказательства концепции я сделал именно это:

#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

char run(char * const arg[]) {
    pid_t child;
    int wstatus;
    char ret_val = -1;

    child = vfork();
    if (child < 0) {
        printf("run: Failed to fork: %i\n", errno);
    } else if (child == 0) {
        printf("arg: %s\n", arg[0]);
        execv(arg[0], arg);
        _exit(-1);
    } else {
        child = waitpid(child, &wstatus, 0);
        if (WIFEXITED(wstatus))
            ret_val = WEXITSTATUS(wstatus);
    }
    return ret_val;
}

И я изменил ваш пример кода следующим образом (основная часть изменений заключается в замене subprocess.call):

import ctypes
import os
import psutil
pid = os.getpid()
this_proc = psutil.Process(pid)
MAX_MEM = int(psutil.virtual_memory().free*1E-9) # in GB
def consume_memory(size):
    """ Size in GB """
    memory_consumer = []
    while get_mem_usage() < size:
        memory_consumer.append(" "*1000000) # Adding ~1MB
    return(memory_consumer)

def get_mem_usage():
    return(this_proc.memory_info()[0]/2.**30)

def get_free_mem():
    return(psutil.virtual_memory().free/2.**30)

if __name__ == "__main__":
    forker = ctypes.CDLL("forker.so", use_errno=True)
    for i in range(1, MAX_MEM):
        consumer = consume_memory(i)
        mem_usage = get_mem_usage()
        print("\n## Memory usage %d/%d GB (%2d%%) ##" % (int(mem_usage), 
              MAX_MEM, int(mem_usage*100/MAX_MEM)))
        try:
            cmd = [b"/bin/echo", b"[OK] Fork worked."]
            c_cmd = (ctypes.c_char_p * (len(cmd) + 1))()
            c_cmd[:] = cmd + [None]
            ret = forker.run(c_cmd)
            errno = ctypes.get_errno()
            if errno:
                raise OSError(errno, os.strerror(errno))
        except OSError as e:
            print("[ERROR] Fork failed. Got OSError.")
            print(e)
        del consumer

С этим я все еще мог разветвляться на 3/4 доступной памяти, о которой сообщили, что она заполнена.

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


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

def nop():
    pass

executor = concurrent.futures.ProcessPoolExecutor(max_workers=1)
executor.submit(nop)  # start a worker process in the pool

А затем отправьте subprocess.call на него:

proc = executor.submit(subprocess.call, ['echo', '[OK] Fork worked.'])
proc.result()  # can also collect the return value
...