背景
一個月前在線上發生了內存泄露。但很奇怪的是,那次發佈只涉及幾行代碼,且都不涉及內存的主動分配。
我們都知道,如果發生了內存泄露,那麼一定會有內存分配這個動作。
但當前的業務代碼裏並沒有“內存分配”這個操作,所以問題八成是出在框架內部。
所修改的代碼如下:
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【好像等於沒說】