簡體   English   中英

如何在 Django 異步視圖之間共享(初始化和關閉)aiohttp.ClientSession 以使用連接池

[英]How to share (initialize and close) aiohttp.ClientSession between Django async views to use connection pooling

Django 從 3.1 版開始支持異步視圖,因此它非常適合非阻塞調用外部 HTTP API(例如使用aiohttp )。

經常看到下面的代碼示例,我認為它在概念上是錯誤的(盡管它工作得很好):

import aiohttp
from django.http import HttpRequest, HttpResponse

async def view_bad_example1(request: HttpRequest):
    async with aiohttp.ClientSession() as session:
        async with session.get("https://example.com/") as example_response:
            response_text = await example_response.text()
            return HttpResponse(response_text[:42], content_type="text/plain")

此代碼為每個傳入請求創建一個ClientSession ,這是低效的。 aiohttp不能使用例如連接池。

不要為每個請求創建 session。 很可能每個應用程序都需要一個 session 來完全執行所有請求。

來源: https://docs.aiohttp.org/en/stable/client_quickstart.html#make-a-request

這同樣適用於 httpx:

另一方面,客戶端實例使用 HTTP 連接池。 這意味着當您向同一主機發出多個請求時,客戶端將重用底層 TCP 連接,而不是為每個請求重新創建一個。

資料來源: https://www.python-httpx.org/advanced/#why-use-a-client

有沒有辦法在 Django 中全局實例化aiohttp.ClientSession以便可以跨多個請求共享此實例? 不要忘記必須在正在運行的事件循環中創建ClientSession為什么在事件循環之外創建 ClientSession 很危險? ),所以我們不能實例化它,例如在 Django 設置中或作為模塊級變量。

我得到的最接近的是這段代碼。 但是,我認為這段代碼很丑陋,並且沒有解決例如關閉 session 的問題。

CLIENT_SESSSION = None

async def view_bad_example2(request: HttpRequest):
    global CLIENT_SESSSION

    if not CLIENT_SESSSION:
        CLIENT_SESSSION = aiohttp.ClientSession()

    example_response = await CLIENT_SESSSION.get("https://example.com/")
    response_text = await example_response.text()

    return HttpResponse(response_text[:42], content_type="text/plain")

基本上,我正在尋找來自 FastAPI 的等效事件,可用於在異步上下文中創建/關閉某些資源

順便說一下,這是兩個視圖之間使用 k6 的性能比較:

  • view_bad_example1 : avg=1.32s min=900.86ms med=1.14s max=2.22sp(90)=2s p(95)=2.1s
  • view_bad_example2 : avg=930.82ms min=528.28ms med=814.31ms max=1.66sp(90)=1.41sp(95)=1.52s

Django 沒有實現 ASGI Lifespan 協議。
參考: https://github.com/django/django/pull/13636

小星星 FastAPI 直接使用 Starlette 的事件處理程序實現。

以下是使用 Django 實現這一目標的方法:

  1. 在 Django 的ASGIHandler的子類中實現 ASGI Lifespan 協議。
import django
from django.core.asgi import ASGIHandler


class MyASGIHandler(ASGIHandler):
    def __init__(self):
        super().__init__()
        self.on_shutdown = []

    async def __call__(self, scope, receive, send):
        if scope['type'] == 'lifespan':
            while True:
                message = await receive()
                if message['type'] == 'lifespan.startup':
                    # Do some startup here!
                    await send({'type': 'lifespan.startup.complete'})
                elif message['type'] == 'lifespan.shutdown':
                    # Do some shutdown here!
                    await self.shutdown()
                    await send({'type': 'lifespan.shutdown.complete'})
                    return
        await super().__call__(scope, receive, send)

    async def shutdown(self):
        for handler in self.on_shutdown:
            if asyncio.iscoroutinefunction(handler):
                await handler()
            else:
                handler()


def my_get_asgi_application():
    django.setup(set_prefix=False)
    return MyASGIHandler()
  1. 替換 asgi.py 中的application
# application = get_asgi_application()
application = my_get_asgi_application()
  1. 實現一個助手get_client_session來共享實例:
import asyncio
import aiohttp
from .asgi import application

CLIENT_SESSSION = None

_lock = asyncio.Lock()


async def get_client_session():
    global CLIENT_SESSSION

    async with _lock:
        if not CLIENT_SESSSION:
            CLIENT_SESSSION = aiohttp.ClientSession()
            application.on_shutdown.append(CLIENT_SESSSION.close)

    return CLIENT_SESSSION

用法:

async def view(request: HttpRequest):
    session = await get_client_session()
    
    example_response = await session.get("https://example.com/")
    response_text = await example_response.text()

    return HttpResponse(response_text[:42], content_type="text/plain")

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM