This document describes an implementation of simple microservices in Python. We use the Tornado asynchronous networking library in both the server and client. Doing so enables us to send and handle multiple request concurrently while we wait on delays in the network.
You should be aware that Python 3 has the asyncio module in its standard library which gives equivalent capabilities.
The sample code is available here:
- The server -- microservice_server.py
- The client -- microservice_client.py
- A shell script which runs the server -- run-server-test.sh
This example has been tested with Python 3. Some modifications would be required in order to run them under Python 2. However, for those of you who need to run on Python 2, since these samples were built on top of the Tornado asynchronous networking library, they would be reasonably easy to adapt for Python 2.
I've run this sample code on Python 3.7.3 on Ubuntu GNU/Linux and with Python 3.5.2 on Raspian GNU/Linux on a Raspberry Pi. Earlier versions of Python 3, for example Python 3.4.2, have been shown not to work.
The main function in our server (1) sets up several "routes" that the server will respond to and specifies the arguments to be passed to our request handler for each route. Each route also specifies a "handler" class, which is a subclass of tornado.web.RequestHandler. The request handler (class MainHangler, in our case) contains a method initialize that is passed the dictionary created for that specific route. See the following for more on Tornado request handlers: https://www.tornadoweb.org/en/stable/web.html#request-handlers. And, then (2) the main function creates an instance of tornado.web.Application, passing it a list of routes, calls its listen method to start the HTTP server, and the Tornado IOLoop.
After that set-up, each request that matches one of the routes for MainHangler will cause (1) the initialize method to be called, and then (2) a call to one of the HTTP methods handler methods, get, put, post, etc, depending on whether the request is a GET, PUT, POST etc, request.
Each of those methods produces JSON content and "writes" it using an inherited write method.
We are using an asynchronous library. Therefore, if the server's request handlers (those get, put, post, etc methods) are reasonably I/O bound, and only if they are, we should get a reasonable amount of parallelism, in spite of Python's GIL (global interpreter lock). The call await tornado.gen.sleep(self.delay) is intended to simulate. That call effectively gives the Tornado asynchronous networking library a chance to give up control an run another task while the sleep method simulates I/O processing. However, if your request handlers are "compute bound", if, for example, your handler is using numpy, scipy, or pandas to do heavy computational work, we'd expect to see little or no speed up at all from the use of the Tornado asynchronous networking library. You can simulate this compute-bound behavior by changing the following asynchronous call:
to this synchronous call:
The client (microservice_client.py) uses the Tornado asynchronous networking library to send and then wait on multiple HTTP network requests in parallel. We create an iterable that produces the requests and then call tornado.gen.multi passing in that iterable. That is what enables us to send those requests in parallel. Here is a snippet of the code that does that:
# Create an iterable containing the requests. requests = (make_request(urls2, methods) for _ in range(options.count)) # Send the requests in parallel. result = gen.multi([ send_request( http_client, request, ) for request in requests]) # Wait until all the requests have been responded to. responses = await result
At this point, all our requests have been satisfied, and we can process the responses.
Basically, the routes, that is, the URL that is used to make a request, is your API. Look at the definition of routes in microservice_server.py.
But, also keep in mind that we've implemented an HTTP service. This means that you can also work with the HTTP verbs or methods: GET, PUT, POST, DELETE, etc. This is especially true if you use one of the command line tools cUrl or httpie as your client. You can also consider implementing your client in Python on top of the Python requests module, which would enable you to make requests using various HTTP methods. Note that our sample client microservice_client.py makes its requests and receives its responses by using the Tornado asynchronous networking library, which enables it to make asynchronous requests. The combination of HTTP methods and routes/paths in the URLs used to make requests gives us a great deal of flexibility in designing our API and the applications that implement it.
Convert the sample code so that it uses the asyncio support in the Python 3 standard library, instead of Tornado.