Why MongoDB's python motor client is much slower than pymongo when run with starlette?

Viewed 7039

I have a web application written with python and the async framework Starlette (https://www.starlette.io/), I need to connect to MongoDB, so I installed the async pymongo driver: motor (https://motor.readthedocs.io/en/stable/) version 2.1.0

And I have some pseudo code like this:


motor_client = motor.motor_asyncio.AsyncIOMotorClient(...)
pymongo_client = pymongo.MongoClient(...)

class BlogList(HTTPEndpoint):
    async def get(self, request):
        blog_list = [item async for item in motor_client.blog.find(condition)]
        # blog_list = list(pymongo_client.blog.find(condition))
        data = {
            "request": request,
            "blogs": blog_list,
        }
        return RenderPageResponse("index.html", data)

For the blog_list line here, I benchmarked it with wrk -t10 -d10 -c100 http://localhost:8080/blog:

  • If I use the motor_client with await, the result is around "1500 requests/sec"
  • If I change it to pymongo_client (the sync mode), the result is around "1900 requests/sec"

Shouldn't the async mode with motor be faster than the sync mode with pymongo? I am wondering why the performance is much worse when using motor (async, 1500 req/s) compare to pymongo (sync, 1900 req/s)?

Other info: I run my starlette app with gunicorn -w 4 -k uvicorn.workers.UvicornWorker myblog:app, and just toggle these 2 lines to do the wrk benchmark.

I did the search but cannot find posts talking about this, so I am wondering did I miss something? Any help would be appreciated, thank you.

2 Answers

Perhaps the problem is due to the fact that the motor is not an asynchronous driver in fact. It just starts the synchronous pymongo in ThreadPoolExecutor, hence the performance drop. Unfortunately, currently there is no asynchronous driver for mongo on python :(

I did some experimentation on this using fastapi
First added 1515 documents to a collection in mongodb
Then defined 3 test cases
Test Case 1
Used pymongo to fetch documents from database from sync function

@app.get("/get_data")
def read_data():
    cursor  = coll.find({})
    li = []
    for rec in cursor:
        li.append(rec)
    length = len(li)
    return {"length": length}

Test Case 2
Used pymongo to fetch documents from database from async function

@app.get("/get_data")
async def read_data():
    cursor  = coll.find({})
    li = []
    for rec in cursor:
        li.append(rec)
    length = len(li)
    return {"length": length}

Test Case 3
Used motor to fetch documents from database from async function

@app.get("/get_data")
async def read_data():
    cursor  = coll.find({})
    li = []
    async for document in cursor:
        li.append(document)
    return {"length": len(li)}

Used uvicorn to run the app with 5 workers
uvicorn test:app --host 0.0.0.0 --port 8000 --workers 5

Used locust for testing
code:

class HelloWorldUser(HttpUser):
    @task
    def hello_world(self):
        self.client.get("/get_data")

config:
Number of total user simulated = 50
Spawn rate(per second) = 10

Results

Test Case 1

 {
   "Type":["GET"],
   "Name":["/get_data"],
   "Request Count":[1546],
   "Failure Count":[0],
   "Median Response Time":[1300],
   "Average Response Time":["1409.07469337902"],
   "Min Response Time":["213.365600000543"],
   "Max Response Time":["6493.73753599866"],
   "Average Content Size":[15],
   "Requests/s":["32.9464475484361"],
   "Failures/s":[0],
   "50%":[1300],
   "66%":[1500],
   "75%":[1700],
   "80%":[1900],
   "90%":[2300],
   "95%":[2700],
   "98%":[3200],
   "99%":[3600],
   "99.9%":[5200],
   "99.99%":[6500],
   "100%":[6500]
}

Test Case 2

{
   "Type":["GET"],
   "Name":["/get_data"],
   "Request Count":[1586],
   "Failure Count":[0],
   "Median Response Time":[2000],
   "Average Response Time":["2108.97265052965"],
   "Min Response Time":["261.412365001888"],
   "Max Response Time":["5301.57826199866"],
   "Average Content Size":[15],
   "Requests/s":["22.4672730359705"],
   "Failures/s":[0],
   "50%":[2000],
   "66%":[2400],
   "75%":[2500],
   "80%":[2700],
   "90%":[2900],
   "95%":[3300],
   "98%":[3600],
   "99%":[4100],
   "99.9%":[5100],
   "99.99%":[5300],
   "100%":[5300]
}

Test Case 3

{
   "Type":["GET"],
   "Name":["/get_data"],
   "Request Count":[1662],
   "Failure Count":[0],
   "Median Response Time":[1200],
   "Average Response Time":["1388.38606281409"],
   "Min Response Time":["116.913538000517"],
   "Max Response Time":["7983.01199100024"],
   "Average Content Size":[15],
   "Requests/s":["33.5720588256683"],
   "Failures/s":[0],
   "50%":[1200],
   "66%":[1500],
   "75%":[1700],
   "80%":[1900],
   "90%":[2300],
   "95%":[2700],
   "98%":[3400],
   "99%":[4100],
   "99.9%":[6800],
   "99.99%":[8000],
   "100%":[8000]
}
Related