คุณควรใช้ Django Asynchronous Support หรือไม่
เช่นเดียวกับฉัน คุณอาจพิจารณาใช้ Django หากคุณต้องการเฟรมเวิร์กที่มีคุณสมบัติครบถ้วนสำหรับการพัฒนาเว็บอย่างรวดเร็ว
น่าเสียดายที่ Django ไม่ได้รับการยกย่องในด้านประสิทธิภาพ — อาจเป็นเพราะบริการซิงโครนัส (Sync) ที่เป็นค่าเริ่มต้น นี่คือสิ่งที่แนะนำผ่าน (https://docs.djangoproject.com/en/4.1/intro/tutorial01/).
การซิงค์ทำงานได้ไม่ดีเนื่องจากเธรดของเซิร์ฟเวอร์สามารถให้บริการได้ครั้งละหนึ่งคำขอเท่านั้น ที่สำคัญ คำขอ I/O บล็อกเธรด ปริมาณงานสามารถเพิ่มได้โดยการเพิ่มเธรด แต่เธรดเซิร์ฟเวอร์ Python ใช้ RAM เกือบเท่ากันกับกระบวนการเซิร์ฟเวอร์ Python สิ่งนี้ทำให้ไม่สามารถเพิ่มปริมาณงานโดยการเพิ่มเธรด
มีการเพิ่มการออกแบบแบบอะซิงโครนัส (Async) ใน Django 3.0 (https://docs.djangoproject.com/en/3.0/topics/async/) และไม่ได้กล่าวถึงในบทนำ กล่าวถึงเป็นหัวข้อสุดท้ายในการใช้ Django (https://docs.djangoproject.com/en/4.1/topics/).
Async มีประสิทธิภาพดีกว่าการซิงค์เนื่องจากเธรดเซิร์ฟเวอร์ Async เดียวสามารถให้บริการหลายคำขอในแต่ละครั้ง ระหว่างการร้องขอ I/O ที่รอคอย การรอคอยจะให้การควบคุมกลับไปยังเธรดและอนุญาตให้คำขออื่นๆ ดำเนินการต่อ มันคล้ายกับการวนรอบเหตุการณ์ของ Javascript แต่ไม่ทั้งหมด (https://stackoverflow.com/questions/68139555/difference-between-async-await-in-python-vs-javascript).
เลยทำให้เกิดคำถามว่า
- Django สามารถเพิ่มประสิทธิภาพได้มากเพียงใดโดยใช้การตั้งค่าแบบอะซิงโครนัส
- เส้นทาง/จุดสิ้นสุดกำลังทำอะไรอยู่ (ใช้ CPU มาก vs ใช้ IO มาก) มีความสำคัญหรือไม่
- แล้วถ้ามีเวลาแฝงต่ำ (เซิร์ฟเวอร์อยู่ใกล้ ๆ ) หรือมีเวลาแฝงสูง (เซิร์ฟเวอร์อยู่ไกล) ล่ะ?
ดังนั้นฉันจึงทำการทดสอบโหลด คุณสามารถค้นหารหัสและรายละเอียดได้ที่นี่https://github.com/nalkhish/Asynchronize-Django. สั้น ๆ นี่คือสิ่งที่ฉันตรวจสอบ:
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
- สำหรับ Sync นั้น Gunicorn ใช้เพื่อหมุน 1 Sync worker (https://docs.gunicorn.org/en/stable/design.html).
- สำหรับ Async Limited นั้น Gunicorn ถูกใช้เพื่อปั่นคนงาน Uvicorn 1 คน (https://www.uvicorn.org/deployment/)
- สำหรับ Async Unlimited นั้น Gunicorn ใช้เพื่อหมุนคนงาน Uvicorn 1 คน (https://www.uvicorn.org/deployment/) แต่ postgres ได้รับการตั้งค่าให้อนุญาตการเชื่อมต่อพร้อมกัน 1,000 รายการ (มากกว่าค่าเริ่มต้น 100 รายการ)
ใช้ 3 เส้นทาง ได้แก่
- cpu — ระบุว่าเส้นทางนั้นใช้ 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
โดยที่ <container_name> คือชื่อของคอนเทนเนอร์
โหลดอีมูเลชั่น
โหลดจำลองโดยใช้เครื่องมือโอเพ่นซอร์ส Locust (https://locust.io/). ตั๊กแตนโจมตีพอร์ตด้วยคำขอ ทันทีที่ 'ผู้ใช้' ทำงานเสร็จ มันก็จะเริ่มต้นใหม่อีกครั้ง
การโหลดทั้งหมดใช้เวลา 3 นาที:
- เริ่มต้นด้วย 0 ผู้ใช้
- ในช่วง 100 วินาทีแรก เพิ่มอัตราผู้ใช้ 5 คนต่อวินาทีเพื่อจำกัดผู้ใช้พร้อมกัน 500 คน
- ในช่วง 80 วินาทีที่ผ่านมา ผู้ใช้ยังคงอยู่ที่ 500
ผลการวิจัย:
คำจำกัดความ:
- sRPS สูงสุด: คำขอที่สำเร็จสูงสุดต่อวินาทีได้รับจากความแตกต่างสูงสุดระหว่างคำขอโดยรวมและคำขอที่ล้มเหลวสำหรับกราฟรายงานทั้งหมด (ดูตัวเลขเสริมในrepo )
- ความสำเร็จทั้งหมด: ได้รับคำขอที่สำเร็จโดยรวมโดยการลบ #คำขอที่ล้มเหลว ออกจาก #คำขอทั้งหมด (ดูตัวเลขเพิ่มเติมในrepo )
ตารางที่ 1: CPU-bound sRPS สูงสุดและความสำเร็จโดยรวมสำหรับการตั้งค่าร่วมกัน (Sync/Async Limited/Async unlimited) และ Latency (0ms, 100ms, 200ms) Async ทำงานได้ไม่ดีจริง ๆ
Async จำกัด กับ Async ไม่จำกัด:
- ทำไม Async unlimited ถึงดีกว่า Async limited? Async unlimited ช่วยให้สามารถเชื่อมต่อฐานข้อมูลได้มากขึ้น แต่เส้นทาง CPU และการตั้งค่ามิดเดิลแวร์ไม่ได้ใช้ฐานข้อมูล สิ่งนี้จะต้องมีการตรวจสอบเพิ่มเติม
- ไม่ว่าในกรณีใด การตั้งค่า Async ทั้งสองมีไดนามิกที่คาดเดาไม่ได้ (ดูตัวเลขเพิ่มเติมใน repohttps://github.com/nalkhish/Asynchronize-Django).
- การซิงค์มี sRPS สูงสุดต่ำกว่า Async unlimited อาจเป็นเพราะเซิร์ฟเวอร์ async สามารถจัดการคำขอหลายรายการพร้อมกันได้ ดังนั้นคำขอหลายรายการจึงเกิดขึ้นพร้อมกัน สิ่งนี้น่าประหลาดใจเพราะ asyncio คาดคะเนว่าจะไม่สลับบริบท เว้นแต่จะโดนคำสั่ง wait ซึ่งไม่มีอยู่ในเส้นทาง cpu สิ่งนี้จะต้องมีการตรวจสอบเพิ่มเติม
- Sync มีไดนามิกที่คาดเดาได้และมีความสำเร็จโดยรวมสูงกว่า Async นี่เพียงพอที่จะรับประกันการใช้ Sync สำหรับบริการที่ผูกกับ cpu
ตารางที่ 2: การอ่านฐานข้อมูล I/O-bound sRPS สูงสุดและความสำเร็จโดยรวมสำหรับการรวมกันของเซิร์ฟเวอร์ (Async/Sync) และเวลาแฝง (0ms, 100ms, 200ms) มีทรูพุตที่สูงขึ้นในเวลาแฝงสูงเมื่อใช้ Async แทน Sync
ตารางที่ 3: การสร้างฐานข้อมูล I/O-bound sRPS สูงสุดและความสำเร็จโดยรวมสำหรับการรวมกันของเซิร์ฟเวอร์ (Async/Sync) และเวลาแฝง (0ms, 100ms, 200ms) มีทรูพุตที่สูงขึ้นในเวลาแฝงสูงเมื่อใช้ Async
Async limited vs Async unlimited: Async unlimited มี sRPS สูงสุดและความสำเร็จโดยรวมสูงกว่า Async limited
- สำหรับเส้นทางการอ่านที่เชื่อมโยงกับ IO อาจเกิดจากฐานข้อมูลที่เป็นคอขวดเนื่องจากล้มเหลว
- สำหรับเส้นทางการสร้างขอบเขต IO จะต้องตรวจสอบเพิ่มเติมเนื่องจากฐานข้อมูลไม่ได้ล้มเหลวสำหรับ Async limited (ดูรูปเพิ่มเติม)
- สำหรับทั้งเส้นทาง io_read และ io_create การซิงค์มีประสิทธิภาพต่ำกว่า Async มาก (สำหรับเวลาแฝงที่เพิ่มขึ้น 200 มิลลิวินาที ความแตกต่างของปริมาณงานโดยรวมคือ40 เท่าสำหรับ io_read และ230 เท่าสำหรับ io_create)
- อาจเป็นเพราะเธรดผู้ปฏิบัติงานเซิร์ฟเวอร์กำลังรอให้คำขอฐานข้อมูลเสร็จสิ้นก่อนที่จะสามารถจัดการคำขอถัดไปได้ ทฤษฎีนี้สนับสนุนโดยความสัมพันธ์แบบผกผันระหว่างเวลาแฝงกับ sRPS สูงสุด และความสำเร็จโดยรวมสำหรับการซิงค์
มีการสำรวจข้อจำกัดเพิ่มเติมที่ repo readmehttps://github.com/nalkhish/Asynchronize-Django(ข้อมูลเล็กน้อยที่กล่าวถึงในที่นี้ การไม่มีตรรกะในการลองใหม่ และไม่มีความล่าช้าขาเข้า) แต่การศึกษานี้สำรวจความสามารถของการให้บริการ Django แบบอะซิงโครนัสและซิงโครนัสอย่างเพียงพอ
หากคุณใช้ Django และมี CPU-bound load ให้ใช้ Sync มิฉะนั้น หากโหลดเป็น I/O-bound ให้ใช้ Async มีแนวโน้มว่าจะมีมากกว่า 10 เท่าของปริมาณงานของผู้ปฏิบัติงานของคุณ