Concurrency with Frameworks and Servers for Python
I would like to return to the topic of concurrent programming in Python today, but this time we're going to move a few levels higher. I'm talking about web technologies, so we're going to talk about web frameworks, web servers and how different types of concurrent programming are implemented there, as well as what ASGI and WSGI are, what their key differences are and why we need them at all.
When we talk about web frameworks and web servers concurrent programming starts to interest us in terms of how requests are handled. There are essentially only two models, synchronous and asynchronous, on which all interaction is built. They have fundamental differences that make them more or less suitable for certain tasks. It is important to understand this in order to be able to choose the right tool for different scenarios and application requirements. Let's start with frameworks.
Frameworks
Synchronous Web Frameworks
Synchronous web frameworks handle each request one at a time. When a request is received, it is processed completely before the framework can receive the next one from the server. This means that each request is blocking, occupying the server's resources until the request is fully handled.
Key features:
Most popular choices:
Asynchronous Web Frameworks
Asynchronous web frameworks handle requests in a non-blocking manner. They use an event loop (which is usually initialized by server) to manage independent operations, allowing the server to handle multiple requests concurrently without waiting for one to complete before starting another.
Key features:
Most popular choices:
I should make a note that in practice the categorization of frameworks is not so unambiguous. For example, in Flask you can use async/await, but it will still work according to the blocking model. And Django can work as both an asynchronous and synchronous framework. But, if we talk about what model of interaction is traditionally implemented by a particular framework, then the above list is correct.
Okay, we've learned a little bit about frameworks. Now let's go? a level higher and see how things work with web servers.
Servers
I'm sure you're aware that web servers are the backbone of any web application, which manages incoming requests, transfers them to applications (frameworks), then receives responses from applications (frameworks) and delivers them back to users. In the Python ecosystem, web servers can be also categorized as synchronous and asynchronous.?
In general, synchronous and asynchronous servers are built on the same principles as synchronous and asynchronous frameworks respectively, since they are closely related to frameworks and directly interact with them. Let's take a look.
Synchronous Web Servers
Synchronous web servers handle each request one at a time. In other words, once a request is received the server passes it to the framework for processing and this particular server worker will be blocked until the framework returns a response. Pretty the same principles as for the synchronous framework. Let's move on.
Most popular choices:?
Asynchronous Web Servers
Asynchronous web servers handle requests in a non-blocking manner. They use an event loop to manage I/O operations, allowing the server to handle multiple requests simultaneously without waiting for one to complete by framework before starting another. Each server worker can handle hundreds or thousands of client connections concurrently.
The server is the base on which any framework functions and in fact dictates the rules of how the framework should interact with it. And they interact with each other using the ASGI and WSGI interfaces, which we have already seen before. Let's talk a little bit about them.
ASGI and WSGI
ASGI (Asynchronous Server Gateway Interface) and WSGI (Web Server Gateway Interface) are specifications that define a standard interface between web servers and Python web applications or frameworks. They serve similar purposes but are designed with different capabilities in mind, reflecting the evolution of web technologies and application requirements.?
WSGI
The Synchronous Pioneer WSGI, introduced in 2003, standardized the way Python web applications communicate with web servers. It's a synchronous, single-threaded interface that processes one request at a time.
ASGI
The Asynchronous Evolution ASGI, introduced in 2016, extends WSGI's concepts to support asynchronous programming. It extends the concepts of WSGI to support asynchronous programming, WebSockets, and other protocols beyond HTTP. ASGI allows for more efficient handling of concurrent connections and long-lived requests.
Summary
We've dealt with this question a bit and it looks like we put everything into the right places, but let me break it down again and we'll look at this variety of tools from a different angle. In fact, it is not quite correct to divide frameworks and servers only into synchronous and asynchronous ones. From a technical point of view, they should also be categorized by those that implement one or another gateway interface.
For example WSGI is really traditional and probably still the preferred option for synchronous communication, but the newer and more advanced ASGI can offer both asynchronous and synchronous communication. Or, if you take a Uvicorn server, which is an ASGI server, it can work with a framework like Flask, which is a WSGI framework, via a special ASGI-to-WSGI adapter (I don't know why you would do that, but it can work nevertheless). Gunicorn can work with a special Uvicorn worker acting as a process manager for Uvicorn, eventually serving some ASGI framework. And such frameworks as for example Django or Quart can be run on both ASGI and WSGI servers.
As you can see all this is a bit confusing, but let's just try to categorize this whole “zoo” into two categories at once and I think this will give the most clear and appropriate result from a technical point of view.
Synchronous (WSGI) Frameworks and Servers
Asynchronous (ASGI) Frameworks and Servers
Frameworks Supporting Both Sync (WSGI) and Async (ASGI)
We have already dealt with a lot of things, but we still haven't started on the main topic of the article. I apologize so much, but I hasten to rejoice that this moment has come. We have just gained the critical mass of necessary knowledge and can now proceed to discuss what you've been reading all the way to these lines for.
So, how can concurrency be achieved?
WSGI Frameworks and Servers
For this setup concurrency usually can be achieved with 3 main approaches:
1. Multiprocessing
This is the only approach with true parallelism at the moment. It can lead to significant performance improvements and is widely used for CPU-bound tasks.
Benefits:
领英推荐
Drawbacks:
Example of running several workers (processes) with Gunicorn server:
?gunicorn main:app --workers=4
2. Multithreading
This approach is usually used as a traditional solution for improving performance of I/O operations for WSGI servers.
Benefits:
Drawbacks:
Example of running several threads per worker with Gunicorn server:
?gunicorn main:app --worker-class=gthread --workers=4 --threads=2
It's generally recommended to use gthread worker classes when you want to leverage multiple threads, although the sync worker class can also use multiple threads. This is because gthread is designed to be more efficient for threaded operations.
3. Gevent worker class
This approach leverages greenlets (lightweight coroutines) and cooperative multitasking to handle concurrency efficiently, especially for I/O-bound tasks.
Benefits:
Drawbacks:
Example of running several gevent workers (processes) with Gunicorn server:
?gunicorn main:app --worker-class=gevent --workers=5 --worker-connections=1000
You can also set the --worker-connections option to a high value to allow each worker to handle as many concurrent connections as you need. The default value is 1000, which is a good starting point.
ASGI Frameworks and Servers
There are 2 main approaches:
1. Multiprocessing
Pretty the same as for WSGI, which we already discussed above. We can increase the number of async workers, which will work in parallel.
Example of running several workers (processes) with Unicorn server:
?uvicorn main:app --workers=4 --limit-concurrency=1000
You can adjust --limit-concurrency parameter to manage a limit on the number of concurrent tasks (connections) that the server will handle simultaneously. It allows you to optimize server performance based on your hardware capabilities, application requirements and prevents the server from becoming overwhelmed by too many concurrent requests
Example of running several workers (processes) with Gunicorn with Uvicorn worker:
?gunicorn main:app -k uvicorn.workers.UvicornWorker --workers=4 --worker-connections=1000
2. Asynchronous Programming
The core of asynchronous programming is Event Loop, where tasks are scheduled and executed. In Python, AsyncIO provides the event loop implementation or the alternative and more performant solution as uvloop can be used. In your application you will define functions with async def which are called coroutines that can pause their execution (await) to allow other coroutines to run. The switching of coroutines will be handled by the Event Loop.
Benefits:
Drawbacks:
Workers and Threads Configuration (Gunicorn and Uvicorn example)
Synchronous WSGI server:?
For CPU-bound application:
For I/O-bound application:
Asynchronous WSGI server:
Such servers are specifically designed to be highly effective for I/O-bound applications. So, we will discuss only I/O-bound application:
Asynchronous ASGI server:
For such servers recommended configuration for I/O-bound and CPU-bound applications is pretty similar:
Remember, these are starting points. The optimal configuration can vary based on your specific application, infrastructure, and workload. It's important to test and monitor your application under realistic loads to fine-tune these settings.
General Best Practices:
Conclusion
We didn’t discuss here all possibilities for achieving concurrency with all plenty of Python’s frameworks and servers. There are a lot of more approaches to touch like uWSGI with async mode, Tornado and AIOHTTP, ASGI-WSGI adapters. But as you can see the article is already long enough and in general, I don't think it makes sense to explore all the options at once. Let’s say we tried to cover only widely used approaches and mention other possibilities you can explore in more detail if you need it.