文章首發公衆號「碼農吳先生」, 歡迎訂閱關注。html
Django3.0 發佈的時候,我嘗試着用了下它的異步功能。當時它僅僅添加了對ASGI的支持(可見以前的文章 Django 3.0 異步試用分享,直到Django3.1的發佈,才支持了視圖和中間件的異步,可是關鍵的Django ORM層仍是沒有異步。Django生態對第三方異步的ORM支持又不是很友好,這就致使不少用戶面對Django的異步功能無從下手。python
很過文章在描述Django view 和中間件的異步使用方法時,由於沒有ORM的異步,在view中大多數用asyncio.sleep
來代替,並無真實的案例。這便進一步致使讀者無從下手,認爲Django 異步徹底沒生產使用價值。這觀點徹底是錯誤的,現階段Django 的異步功能徹底可用於生成。django
下邊是來自Arun Ravindran(<Django設計模式和最佳實踐>做者) 的三個生產級別的Django 異步使用案例,供你們參考。json
現階段,大多數系統架構已經從單一架構進化爲微服務架構,在業務邏輯中調用其餘服務的接口成爲常有的事情。Django 的異步view 在這種狀況下,能夠很大程度上提升性能。設計模式
讓咱們看下做者的例子:經過兩個微服務的接口來獲取最後展現在home頁的數據。api
# 同步版本
def sync_home(request):
"""Display homepage by calling two services synchronously"""
context = {}
try:
# httpx 支持異步http client ,可理解爲requests的升級異步版,徹底兼容requests 的api。
response = httpx.get(PROMO_SERVICE_URL)
if response.status_code == httpx.codes.OK:
context["promo"] = response.json()
response = httpx.get(RECCO_SERVICE_URL)
if response.status_code == httpx.codes.OK:
context["recco"] = response.json()
except httpx.RequestError as exc:
print(f"An error occurred while requesting {exc.request.url!r}.")
return render(request, "index.html", context)
# 異步版本
async def async_home_inefficient(request):
"""Display homepage by calling two awaitables synchronously (does NOT run concurrently)"""
context = {}
try:
async with httpx.AsyncClient() as client:
response = await client.get(PROMO_SERVICE_URL)
if response.status_code == httpx.codes.OK:
context["promo"] = response.json()
response = await client.get(RECCO_SERVICE_URL)
if response.status_code == httpx.codes.OK:
context["recco"] = response.json()
except httpx.RequestError as exc:
print(f"An error occurred while requesting {exc.request.url!r}.")
return render(request, "index.html", context)
# 異步升級版
async def async_home(request):
"""Display homepage by calling two services asynchronously (proper concurrency)"""
context = {}
try:
async with httpx.AsyncClient() as client:
# 使用asyncio.gather 併發執行協程
response_p, response_r = await asyncio.gather(
client.get(PROMO_SERVICE_URL), client.get(RECCO_SERVICE_URL)
)
if response_p.status_code == httpx.codes.OK:
context["promo"] = response_p.json()
if response_r.status_code == httpx.codes.OK:
context["recco"] = response_r.json()
except httpx.RequestError as exc:
print(f"An error occurred while requesting {exc.request.url!r}.")
return render(request, "index.html", context)
複製代碼
同步版本很顯然,當有一個服務慢時,總體的邏輯就會阻塞等待。服務的耗時依賴最後返回的那個接口的耗時。安全
再看異步版本,改用了異步http client 調用,這裏的寫法並不能增長該view 的速度,兩個協程並不能同時執行。當一個協查await時,只是將控制器交還回了事件循環,而不是當即執行本view的其餘邏輯或協程。對於本view來講,仍然是阻塞的。markdown
最後看下異步升級版,使用了asyncio.gather ,它會同時執行兩個協程,並在他們都完成的時候返回。升級版至關於併發,普通版至關於串行,Arun Ravindran說效率提高了一半(有待驗證)。網絡
當django 視圖須要從文件提取數據,來渲染到模板中時。無論是從本地磁盤仍是網絡環境,都會是一個潛在的阻塞I/O操做。在阻塞的這段時間內,徹底能夠幹別的事情。咱們可使用aiofile
庫來進行異步的文件I/O操做。架構
async def serve_certificate(request):
timestamp = datetime.datetime.now().isoformat()
response = HttpResponse(content_type="application/pdf")
response["Content-Disposition"] = "attachment; filename=certificate.pdf"
async with aiofiles.open("homepage/pdfs/certificate-template.pdf", mode="rb") as f:
contents = await f.read()
response.write(contents.replace(b"%timestamp%", bytes(timestamp, "utf-8")))
return response
複製代碼
此實例,使用了本地的磁盤文件,若是使用網絡文件時,記着修改對應代碼。
文件上傳是一個很長的I/O阻塞操做,結合 aiofile
的異步寫入功能,咱們能夠實現高併發的上傳功能。
async def handle_uploaded_file(f):
async with aiofiles.open(f"uploads/{f.name}", "wb+") as destination:
for chunk in f.chunks():
await destination.write(chunk)
async def async_uploader(request):
if request.method == "POST":
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
await handle_uploaded_file(request.FILES["file"])
return HttpResponseRedirect("/")
else:
form = UploadFileForm()
return render(request, "upload.html", {"form": form})
複製代碼
須要注意的是,這繞過了Django的默認文件上傳機制,所以須要注意安全隱患。
本文根據Arun Ravindran的三個準生產級別的實例,闡述了Django 現階段異步的使用。從這些例子當中能夠看出,Django 的異步加上一些異步的第三方庫,已經徹底能夠應用到生產。咱們生產系統的部分性能瓶頸,特別是I/O類型的,能夠考慮使用Django 的異步特性來優化一把了。
我是DeanWu,一個努力成爲真正SRE的人。
關注公衆號「碼農吳先生」, 可第一時間獲取最新文章。回覆關鍵字「go」「python」獲取我收集的學習資料,也可回覆關鍵字「小二」,加我wx,可拉入技術交流羣,聊技術聊人生~