Django2.1 2.2共用Response對象導致內存泄露問題

背景

一個月前在線上發生了內存泄露。但很奇怪的是,那次發佈只涉及幾行代碼,且都不涉及內存的主動分配。

我們都知道,如果發生了內存泄露,那麼一定會有內存分配這個動作。

但當前的業務代碼裏並沒有“內存分配”這個操作,所以問題八成是出在框架內部。

所修改的代碼如下:

from django.urls import path
from django.http.response import HttpResponse

ALIVE_ECHO = HttpResponse('Alive')

urlpatterns = [
    path('/', lambda request: ALIVE_ECHO),
]

因爲這個URL只需要返回服務可用這個信息,所以每次返回的Response都是一樣的。爲了避免每次請求都生成一個Response這種無用開銷,所以想預先分配一個Response。但正是這個操作導致了服務的內存泄露。

原因

排查的過程很曲折,從gunicorn、wsgi協議一直排查到了django http請求流程,用了Pympler這個工具幫忙排查。

直接說原因吧,Django、wsgi、gunicorn什麼的有空再寫一篇。

class BaseHandler:
	........
    def get_response(self, request):
        """Return an HttpResponse object for the given HttpRequest."""
        # Setup default url resolver for this thread
        set_urlconf(settings.ROOT_URLCONF)

        response = self._middleware_chain(request)

        response._closable_objects.append(request)

        # If the exception handler returns a TemplateResponse that has not
        # been rendered, force it to be rendered.
        if not getattr(response, 'is_rendered', True) and callable(getattr(response, 'render', None)):
            response = response.render()

        if response.status_code >= 400:
            log_response(
                '%s: %s', response.reason_phrase, request.path,
                response=response,
                request=request,
            )

        return response


class HttpResponseBase:
    .........
    def close(self):
        for closable in self._closable_objects:
            try:
                closable.close()
            except Exception:
                pass
        self.closed = True
        signals.request_finished.send(sender=self._handler_class)
        

簡單的說,請求到達Django框架後,Django會把這個request塞到 response._closable_objects 中(見get_response函數)。

wsgi服務器在請求結束時會調用 response.close() 這個函數,但這個函數只是遍歷了_closable_objects,調用每個對象的close函數,而沒有清空_closable_objects。

因爲我們共用了同一個Response,所以Response對象也不會像動態產生的Response那樣被gc,導致_closable_objects會被一直塞入request。

解決方法

這個屬於Django的Bug,影響django2.1 2.2

解決方法很簡單,在close末尾添加 self._closable_objects.clear() 即可解決問題。

Django3 已經修復了這個問題,本來想給2.1/2.2提個PR的,但他們很快都要退出支持了,還是算了。

終極解決方法,就是不要共用同一個Response【好像等於沒說】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章