Рубрики
Без рубрики

Найти и исправлять утечки памяти в Python

Одним из основных преимуществ, предусмотренных в динамических интерпретированных языках, таких как Python, заключается в том, что они делают … Помечено Python, Memoryleaks, Debug.

Одним из основных преимуществ, предоставляемых в динамических интерпретированных языках, таких как Python, является то, что они облегчают управление памятью. Объекты, такие как массивы и строки, растут динамически по мере необходимости, и их память очищена, когда больше не нужна. Поскольку управление памятью обрабатывается языком, утечки памяти менее распространены с проблемой, чем на языках, таких как C и C ++, где оставляется программистом для запроса и бесплатной памяти.

Технологический стек BuzzFeed включает в себя архитектуру микросервера, которая поддерживает более ста услуг, многие из которых построены с Python. Мы контролируем службы для общих свойств системы, такие как память и нагрузка. В случае памяти Уверенная служба будет использовать память и бесплатную память. Он выполняет, как эта диаграмма отчетности в памяти, используемой в течение трехмесячного периода.

MicrovICE, которое утечки памяти со временем выставит поведение пилы, поскольку память увеличивается до некоторой точки (например, максимальная доступна максимальная память), где служба выключается, освобождая всю память, а затем перезапускается.

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

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

Инструменты

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

Другой инструмент, Tracemalloc , является частью библиотеки системы Python. По существу Tracemalloc используется для снимков памяти Python. Начать использовать Tracemalloc первый звонок Tracemalloc.Start () Инициализировать Tracemalloc Затем сделайте снимок, используя:

snapshot=tracemalloc.take_snapshot()

TRACEMALLOC может показать отсортированный список высших ассигнований в снимке, используя метод статистики () на снимке. В этом фрагменте зарегистрированы выделения Top 5 Tox, зарегистрированные исходным именем.

for i, stat in enumerate(snapshot.statistics('filename')[:5], 1):
    logging.info("top_current",i=i, stat=str(stat))

Выход будет выглядеть похоже на это:

1 /usr/local/lib/python3.6/ssl.py:0: size=2569 KiB, count=41, average=62.7 KiB
2 /usr/local/lib/python3.6/tracemalloc.py:0: size=944 KiB, count=15152, average=64 B
3 /usr/local/lib/python3.6/socket.py:0: size=575 KiB, count=4461, average=132 B
4 /usr/local/lib/python3.6/site-packages/tornado/gen.py:0: size=142 KiB, count=500, average=290 B
5 /usr/local/lib/python3.6/mimetypes.py:0: size=130 KiB, count=1686, average=79 B

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

Мы делаем снимок в начале нашей программы и реализовать обратный вызов, который запускается каждые несколько минут, чтобы сделать снимок памяти. Сравнение двух снимков показывает изменения с распределением памяти. Мы сравниваем каждый снимок, сделанный в начале. Наблюдая за любым выделением, которое увеличивается с течением времени, мы можем захватить объект, который протекает память. Способ сравнения_to () вызывается на снимках, чтобы сравнить его с другим снимком. Параметр «FileName» используется для группировки всех распределений модулем. Это помогает сузить поиск модуля, который протекает память.

current = tracemalloc.take_snapshot()
stats = current.compare_to(start, 'filename')
for i, stat in enumerate(stats[:5], 1):
    logging.info("since_start", i=i, stat=str(stat))

Выход будет выглядеть похоже на это:

1 /usr/local/lib/python3.6/ssl.py:0: size=2569 KiB (+2569 KiB), count=43 (+43), average=59.8 KiB
2 /usr/local/lib/python3.6/socket.py:0: size=1845 KiB (+1845 KiB), count=13761 (+13761), average=137 B
3 /usr/local/lib/python3.6/tracemalloc.py:0: size=671 KiB (+671 KiB), count=10862 (+10862), average=63 B
4 /usr/local/lib/python3.6/linecache.py:0: size=371 KiB (+371 KiB), count=3639 (+3639), average=104 B
5 /usr/local/lib/python3.6/mimetypes.py:0: size=126 KiB (+126 KiB), count=1679 (+1679), average=77 B

После того, как подозреваемый модуль идентифицирован, возможно, можно найти точную линию кода, ответственного за распределение памяти. Tracemalloc Предоставляет способ просмотра трассировки стека для любого распределения памяти. Как и в случае с помощью Traceback Exception Python, он показывает линию и модуль, где произошло распределение, и все звонки, которые пришли ранее.

traces = current.statistics('traceback')
for stat in traces[1]:
    logging.info("traceback", memory_blocks=stat.count, size_kB=stat.size / 1024))
    for line in stat.traceback.format():
        logging.info(line)

memory_blocks=2725 size_kB=346.0341796875
  File "/usr/local/lib/python3.6/socket.py", line 657
    self._sock = None
  File "/usr/local/lib/python3.6/http/client.py", line 403
    fp.close()
  File "/usr/local/lib/python3.6/http/client.py", line 410
    self._close_conn()
  File "/usr/local/lib/python3.6/site-packages/ddtrace/writer.py", line 166
    result_traces = None
  File "/usr/local/lib/python3.6/threading.py", line 864
  File "/usr/local/lib/python3.6/threading.py", line 916
    self.run()
  File "/usr/local/lib/python3.6/threading.py", line 884
    self._bootstrap_inner()

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

В этом первом разделе мы видели, что Tracemalloc Делает снимки памяти и обеспечивает статистику о распределении памяти. Следующий раздел описывает поиск фактической утечки памяти в одном Microsvice BuzzFeed.

Поиск нашей утечки памяти

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

Мы выделили микросервис с вызовом на trace_leak () Для регистрации статистики найдены в снимках Tracemalloc. Кодовые циклы навсегда и спит для некоторой задержки в каждом петле.

async def trace_leak(delay=60, top=20, trace=1):
    """
    Use spawn_callback to invoke:
        tornado.ioloop.IOLoop.current().spawn_callback(trace_leak, delay=300, top=10, trace=3)
    :param delay: in seconds (int)
    :param top: number of top allocations to list (int)
    :param trace: number of top allocations to trace (int)
    :return: None
    """
    logger.info('start_trace', delay=delay, top=top, trace=trace)
    tracemalloc.start(25)
    start = tracemalloc.take_snapshot()
    prev = start
    while True:
            await tornado.gen.sleep(delay)
            current = tracemalloc.take_snapshot()
                # compare current snapshot to starting snapshot
            stats = current.compare_to(start, 'filename')
                # compare current snapshot to previous snapshot
            prev_stats = current.compare_to(prev, 'lineno')

            logger.info('Top Diffs since Start')
        # Print top diffs: current snapshot - start snapshot       
        for i, stat in enumerate(stats[:top], 1):
                logger.info('top_diffs', i=i, stat=str(stat))

            logger.info('Top Incremental')
                # Print top incremental stats: current snapshot - previous snapshot 
            for i, stat in enumerate(prev_stats[:top], 1):
                logger.info('top_incremental', i=i, stat=str(stat))

            logger.info('Top Current')
                # Print top current stats
            for i, stat in enumerate(current.statistics('filename')[:top], 1):
                logger.info('top_current', i=i, stat=str(stat))

                # get tracebacks (stack trace) for the current snapshot
            traces = current.statistics('traceback')
            for stat in traces[:trace]:
                logger.info('traceback', memory_blocks=stat.count, size_kB=stat.size / 1024)
                for line in stat.traceback.format():
                        logger.info(line)
        # set previous snapshot to current snapshot
        prev = current

Micrevice построен с использованием торнадо Итак, мы называем это, используя Spawn_Callback () и пройти параметры задержка , верх и трассировка :

tornado.ioloop.IOLoop.current().spawn_callback(trace_leak, delay=300, top=5, trace=1)

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

1 /usr/local/lib/python3.6/ssl.py:0: size=2569 KiB (+2569 KiB), count=43 (+43), average=59.8 KiB
2 /usr/local/lib/python3.6/socket.py:0: size=1845 KiB (+1845 KiB), count=13761 (+13761), average=137 B
3 /usr/local/lib/python3.6/tracemalloc.py:0: size=671 KiB (+671 KiB), count=10862 (+10862), average=63 B
4 /usr/local/lib/python3.6/linecache.py:0: size=371 KiB (+371 KiB), count=3639 (+3639), average=104 B
5 /usr/local/lib/python3.6/mimetypes.py:0: size=126 KiB (+126 KiB), count=1679 (+1679), average=77 B

Tracemalloc не является источником утечки памяти! Тем не менее, это требует некоторой памяти, поэтому она отображается здесь. После запуска службы на несколько часов мы используем DataDog для фильтрации журналов по модулю, и мы начинаем видеть шаблон с Socket.py :

/usr/local/lib/python3.6/socket.py:0: size=1840 KiB (+1840 KiB)
/usr/local/lib/python3.6/socket.py:0: size=1840 KiB (+1840 KiB)
/usr/local/lib/python3.6/socket.py:0: size=1841 KiB (+1841 KiB)
#                               Increase here ^
/usr/local/lib/python3.6/socket.py:0: size=1841 KiB (+1841 KiB)
/usr/local/lib/python3.6/socket.py:0: size=1841 KiB (+1841 KiB)
/usr/local/lib/python3.6/socket.py:0: size=1841 KiB (+1841 KiB)
/usr/local/lib/python3.6/socket.py:0: size=1842 KiB (+1842 KiB)
#                               Increase here ^
/usr/local/lib/python3.6/socket.py:0: size=1843 KiB (+1843 KiB)
/usr/local/lib/python3.6/socket.py:0: size=1843 KiB (+1843 KiB)
#                               Increase here ^
/usr/local/lib/python3.6/socket.py:0: size=1844 KiB (+1844 KiB)
#                               Increase here ^
/usr/local/lib/python3.6/socket.py:0: size=1844 KiB (+1844 KiB)
/usr/local/lib/python3.6/socket.py:0: size=1845 KiB (+1845 KiB)
#                               Increase here ^
/usr/local/lib/python3.6/socket.py:0: size=1845 KiB (+1845 KiB)

Размер распределения для Socket.py увеличивается с 1840 киб до 1845 года. Ни один из других модулей не показал эту четкую тенденцию. Теперь мы посмотрим на трассировку для Socket.py Отказ

Мы определяем возможную причину

Мы получаем трассировку стека от Tracemalloc для розетка модуль.

  File "/usr/local/lib/python3.6/socket.py", line 657
    self._sock = None
  File "/usr/local/lib/python3.6/http/client.py", line 403
    fp.close()
  File "/usr/local/lib/python3.6/http/client.py", line 410
    self._close_conn()
  File "/usr/local/lib/python3.6/site-packages/ddtrace/writer.py", line 166
    result_traces = None
  File "/usr/local/lib/python3.6/threading.py", line 864
  File "/usr/local/lib/python3.6/threading.py", line 916
    self.run()
  File "/usr/local/lib/python3.6/threading.py", line 884
    self._bootstrap_inner()

Первоначально я хочу предположить, что Python и стандартная библиотека солидная и не протекающая память. Все в этом следе является частью стандартной библиотеки Python 3.6, за исключением пакета DDTRACE DATADOG DDTRACE/WRITER.PY. Учитывая мое предположение о целостности Python, пакет, предоставленный третьей стороной, похоже на хорошее место, чтобы начать расследовать дальше.

Это все еще утечка

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

Другой взгляд на журналы

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

1: /usr/local/lib/python3.6/ssl.py:0: size=2568 KiB (+2568 KiB), count=28 (+28), average=91.7 KiB
2: /usr/local/lib/python3.6/tracemalloc.py:0: size=816 KiB (+816 KiB), count=13126 (+13126), average=64 B
3: /usr/local/lib/python3.6/linecache.py:0: size=521 KiB (+521 KiB), count=5150 (+5150), average=104 B
4: /usr/local/lib/python3.6/mimetypes.py:0: size=130 KiB (+130 KiB), count=1699 (+1699), average=78 B
5: /usr/local/lib/python3.6/site-packages/tornado/gen.py:0: size=120 KiB (+120 KiB), count=368 (+368),

В этих журналах нет ничего, что выглядит подозрительно самостоятельно. Однако SSL.py выделяет самый большой кусок, безусловно, 2,5 МБ памяти. Со временем журналы показывают, что это остается постоянным, не увеличивается и уменьшается. Без большего, чтобы продолжать, мы начинаем проверять трассировку для SSL.PY Отказ

  File "/usr/local/lib/python3.6/ssl.py", line 645
    return self._sslobj.peer_certificate(binary_form)
  File "/usr/local/lib/python3.6/ssl.py", line 688
  File "/usr/local/lib/python3.6/ssl.py", line 1061
    self._sslobj.do_handshake()
  File "/usr/local/lib/python3.6/site-packages/tornado/iostream.py", line 1310
    self.socket.do_handshake()
  File "/usr/local/lib/python3.6/site-packages/tornado/iostream.py", line 1390
    self._do_ssl_handshake()
  File "/usr/local/lib/python3.6/site-packages/tornado/iostream.py", line 519
    self._handle_read()
  File "/usr/local/lib/python3.6/site-packages/tornado/stack_context.py", line 277
    return fn(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/tornado/ioloop.py", line 888
    handler_func(fd_obj, events)
  File "/app/socialmc.py", line 76
    tornado.ioloop.IOLoop.instance().start()

Солидный провод

Верхняя часть стека показывает вызов на линии 645 SSL.PY для Peer_Certificate (). Без большего, чтобы продолжать, мы делаем Google Google Long-Shot для «Python Memory Heak SSL Peer_Certificate» и получите ссылку на Отчет об ошибке Python . К счастью, эта ошибка была решена. Теперь это было просто вопрос обновления нашего контейнеровоза из Python 3.6.1 в Python 3.6.4, чтобы получить фиксированную версию и посмотреть, разрешит ли она нашу утечку памяти.

Выглядит неплохо

После обновления изображения мы снова отслеживаем память с DataDog. После свежего развертывания 9 сентября память сейчас проходит квартиру.

Резюме

Правильные инструменты для работы могут иметь разницу между решением проблемы, а не. Поиск нашей утечки памяти состоялся в течение двух месяцев. TRACEMALLOC обеспечивает хорошее понимание ассигнования памяти, происходящее в программе Python; Однако он не знает о распределениях памяти, которые проходят в пакетах, которые выделяют память в C/C ++. В конце концов, отслеживание утечек памяти требует терпения, постоянства и немного детективной работы.

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

https://docs.ython.org/3/Library/traCemalloc.html https://www.fugue.co/blog/2017-03-06-diagnozy-and-fixing-memory-leaks-in-python.html.html.

Этот пост был первоначально размещен 17 января 2019 года, на BF Tech’s Середина .

Оригинал: “https://dev.to/buzzfeedtech/finding-and-fixing-memory-leaks-in-python-cd1”