Bạn có nên sử dụng Hỗ trợ không đồng bộ Django không?

Giống như tôi, bạn có thể đã cân nhắc sử dụng Django nếu bạn muốn có một khung đầy đủ tính năng để phát triển web nhanh chóng.
Thật không may, Django không được đánh giá cao về hiệu suất — có thể do tính năng phục vụ Đồng bộ (Sync) mặc định của nó. Đây là những gì phần giới thiệu đi qua (https://docs.djangoproject.com/en/4.1/intro/tutorial01/).
Đồng bộ hóa hoạt động kém vì luồng máy chủ chỉ có thể phục vụ một yêu cầu tại một thời điểm. Điều quan trọng là các yêu cầu I/O chặn luồng. Thông lượng có thể được tăng lên bằng cách thêm các luồng, nhưng các luồng máy chủ Python chiếm gần như cùng một lượng RAM như một quy trình máy chủ Python. Điều này làm cho việc tăng thông lượng bằng cách thêm luồng là không khả thi.
Thiết kế không đồng bộ (Async) đã được thêm vào trong Django 3.0 (https://docs.djangoproject.com/en/3.0/topics/async/), và không được đề cập trong phần giới thiệu. Nó được đề cập là chủ đề cuối cùng trong việc sử dụng Django (https://docs.djangoproject.com/en/4.1/topics/).
Async có thể hoạt động tốt hơn Sync vì một chuỗi máy chủ Async duy nhất có thể phục vụ nhiều yêu cầu cùng một lúc. Trong các yêu cầu I/O có thể chờ đợi, điều khiển chờ đợi sẽ mang lại quyền kiểm soát cho luồng và cho phép các yêu cầu khác tiếp tục. Nó tương tự như vòng lặp sự kiện của Javascript, nhưng không hoàn toàn (https://stackoverflow.com/questions/68139555/difference-between-async-await-in-python-vs-javascript).
Vì vậy, điều đó đã đặt ra câu hỏi:
- Django có thể đạt được bao nhiêu hiệu suất bằng cách sử dụng thiết lập Không đồng bộ?
- Tuyến đường/điểm cuối đang làm gì (CPU nặng so với IO nặng) có quan trọng không?
- Nếu có độ trễ thấp (máy chủ ở gần) hoặc độ trễ cao (máy chủ ở xa) thì sao?
Vì vậy, tôi đã chạy một thử nghiệm kiểm tra tải. Bạn có thể tìm thấy mã và chi tiết tại đâyhttps://github.com/nalkhish/Asynchronize-Django. Tóm lại, đây là những gì tôi đã điều tra:
For each setup (sync, async limited, async unlimited):
For each request route (high cpu, high io create, high io read):
For each added latency (200, 100, 0):
Emulate load and generate report
- Đối với Đồng bộ hóa, Gunicorn đã được sử dụng để tạo ra 1 nhân viên Đồng bộ hóa (https://docs.gunicorn.org/en/stable/design.html).
- Đối với Async Limited, Gunicorn được sử dụng để tạo ra 1 công nhân Uvicorn (https://www.uvicorn.org/deployment/)
- Đối với Async Unlimited, Gunicorn được sử dụng để tạo ra 1 nhân viên Uvicorn (https://www.uvicorn.org/deployment/), nhưng postgres đã được thiết lập để cho phép 1000 kết nối đồng thời (nhiều hơn 100 mặc định).
3 tuyến đường đã được sử dụng:
- cpu - chỉ ra rằng tuyến đường nặng về cpu
def cpu(request: HttpRequest):
"""High cpu route
CPU: High
I/O: low
"""
ct = 0
for i in range(10**7):
ct += i * i
return JsonResponse({"ct": ct})
# In sync
def io_create(request: HttpRequest):
"""Create 1 post
CPU: low
I/O: High
"""
new_post = Post.objects.create(
title=request.POST["title"],
content=request.POST["content"],
)
return JsonResponse(new_post.to_dict())
# In async
async def io_create(request: HttpRequest):
"""Create 1 post
CPU: low
I/O: High
"""
new_post = await Post.objects.acreate(
title=request.POST["title"],
content=request.POST["content"],
)
return JsonResponse(new_post.to_dict())
# In sync
def io_read(request: HttpRequest):
"""Get first 25 posts
CPU: low
I/O: High
"""
posts = Post.objects.all()[:25]
ct = Post.objects.count()
return JsonResponse(
{
"ct": ct,
"posts": [post.to_dict() for post in posts],
}
)
# In Async
async def io_read(request: HttpRequest):
"""Get first 25 posts
CPU: low
I/O: High
"""
posts = []
async for p in Post.objects.all()[:25]:
posts.append(p)
ct = await Post.objects.acount()
return JsonResponse(
{
"ct": ct,
"posts": [post.to_dict() for post in posts],
}
)
docker exec <container_name> tc qdisc add dev eth0 root netem delay 100ms
Trong đó <container_name> là tên của vùng chứa
Tải thi đua
Tải được mô phỏng bằng công cụ mã nguồn mở Locust (https://locust.io/). Locust tấn công các cổng với các yêu cầu. Ngay sau khi 'người dùng' hoàn thành nhiệm vụ của mình, nó sẽ bắt đầu lại.
Tổng tải kéo dài 3 phút:
- Bắt đầu với 0 người dùng.
- Trong 100 giây đầu tiên, tăng với tốc độ 5 người dùng mỗi giây để đạt tối đa 500 người dùng đồng thời.
- Trong 80 giây qua, người dùng được duy trì ở mức 500.
Kết quả:
Các định nghĩa:
- sRPS tối đa: Số yêu cầu thành công tối đa mỗi giây thu được bằng cách lấy chênh lệch tối đa giữa yêu cầu tổng thể và yêu cầu không thành công cho toàn bộ biểu đồ báo cáo (xem số liệu bổ sung trong repo ).
- Tổng số thành công: Tổng số yêu cầu thành công có được bằng cách trừ # yêu cầu thất bại khỏi # tổng số yêu cầu (xem số liệu bổ sung trong repo ).
Bảng 1: Giới hạn CPU. sRPS tối đa & Thành công tổng thể đối với các kết hợp thiết lập (Đồng bộ hóa/Không đồng bộ có giới hạn/Không đồng bộ không giới hạn) và Độ trễ (0 mili giây, 100 mili giây, 200 mili giây). Async thực sự hoạt động kém.

Async giới hạn so với Async không giới hạn:
- Tại sao Async không giới hạn làm tốt hơn Async giới hạn? Async không giới hạn cho phép nhiều kết nối cơ sở dữ liệu hơn, nhưng thiết lập tuyến CPU và phần mềm trung gian không sử dụng cơ sở dữ liệu. Điều này cần phải được điều tra thêm.
- Trong mọi trường hợp, cả hai thiết lập Async đều có động lực không thể đoán trước (xem số liệu bổ sung trên repohttps://github.com/nalkhish/Asynchronize-Django).
- Đồng bộ hóa có sRPS tối đa thấp hơn Async không giới hạn. Điều này có thể là do các máy chủ không đồng bộ có thể xử lý nhiều yêu cầu cùng một lúc và do đó, nhiều yêu cầu đã xảy ra để kết thúc cùng một lúc. Điều này thật đáng ngạc nhiên vì asyncio được cho là không chuyển đổi ngữ cảnh trừ khi nó chạm vào câu lệnh chờ đợi, câu lệnh này không tồn tại trong lộ trình cpu. Điều này cần phải được điều tra thêm.
- Đồng bộ hóa có động lực có thể dự đoán được và có thành công tổng thể cao hơn Async. Điều này đủ để đảm bảo sử dụng Đồng bộ hóa cho các dịch vụ liên kết với cpu.
Bảng 2: Đọc cơ sở dữ liệu giới hạn I/O. sRPS tối đa & Thành công chung cho các kết hợp Máy chủ (Không đồng bộ/Đồng bộ hóa) và Độ trễ (0ms, 100ms, 200ms). Có thông lượng cao hơn ở độ trễ cao khi sử dụng Async thay vì Sync.

Bảng 3: Tạo cơ sở dữ liệu ràng buộc I/O. sRPS tối đa & Thành công chung cho các kết hợp Máy chủ (Không đồng bộ/Đồng bộ hóa) và Độ trễ (0ms, 100ms, 200ms). Có thông lượng cao hơn ở độ trễ cao khi sử dụng Async.

Async giới hạn so với Async không giới hạn: Async không giới hạn có sRPS tối đa cao hơn và thành công tổng thể cao hơn so với Async giới hạn.
- Đối với tuyến đọc giới hạn IO, điều này có thể là do cơ sở dữ liệu bị tắc nghẽn vì nó bị lỗi.
- Đối với tuyến tạo giới hạn IO, điều này cần được nghiên cứu thêm vì cơ sở dữ liệu không bị lỗi đối với giới hạn Async (xem hình bổ sung)
- Đối với cả hai tuyến io_read và io_create, Đồng bộ hóa có hiệu suất thấp hơn nhiều so với Async (đối với độ trễ thêm 200 mili giây, sự khác biệt về thông lượng tổng thể là 40 lần đối với io_read và 230 lần đối với io_create).
- Điều này có thể là do luồng nhân viên máy chủ đang đợi các yêu cầu cơ sở dữ liệu kết thúc trước khi có thể xử lý yêu cầu tiếp theo. Lý thuyết này được hỗ trợ bởi mối quan hệ nghịch đảo giữa độ trễ và sRPS tối đa và thành công chung cho Đồng bộ hóa.
Những hạn chế được khám phá thêm tại repo readmehttps://github.com/nalkhish/Asynchronize-Django(ít thông tin được thảo luận ở đây, không có logic thử lại và không có độ trễ gửi đến), nhưng nghiên cứu này khám phá đầy đủ khả năng phục vụ Django không đồng bộ và đồng bộ.
Nếu bạn sử dụng Django và có tải giới hạn CPU, hãy sử dụng Đồng bộ hóa. Mặt khác, nếu tải bị ràng buộc I/O, hãy sử dụng Async. Nó có thể sẽ nhiều hơn gấp 10 lần thông lượng công nhân của bạn.