Python Microservices Development

3.8 (10 reviews total)
By Tarek Ziadé
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Understanding Microservices

About this book

We often deploy our web applications into the cloud, and our code needs to interact with many third-party services. An efficient way to build applications to do this is through microservices architecture. But, in practice, it's hard to get this right due to the complexity of all the pieces interacting with each other.

This book will teach you how to overcome these issues and craft applications that are built as small standard units, using all the proven best practices and avoiding the usual traps. It's a practical book: you’ll build everything using Python 3 and its amazing tooling ecosystem. You will understand the principles of TDD and apply them.

You will use Flask, Tox, and other tools to build your services using best practices. You will learn how to secure connections between services, and how to script Nginx using Lua to build web application firewall features such as rate limiting. You will also familiarize yourself with Docker’s role in microservices, and use Docker containers, CoreOS, and Amazon Web Services to deploy your services.

This book will take you on a journey, ending with the creation of a complete Python application based on microservices. By the end of the book, you will be well versed with the fundamentals of building, designing, testing, and deploying your Python microservices.

Publication date:
July 2017
Publisher
Packt
Pages
340
ISBN
9781785881114

 

Chapter 1. Understanding Microservices

We're always trying to improve how we build software, and since the punched-card era, we have improved a lot, to say the least.

The microservices trend is one improvement that has emerged in the last few years, partially based on companies' willingness to speed up their release cycles. They want to ship new products and new features to their customers as fast as possible. They want to be agile by iterating often, and they want to ship, ship, and ship again.

If thousands, or even millions, of customers use your service, pushing in production an experimental feature, and removing it if it does not work, is considered good practice rather than baking it for months before you publish it.

Companies such as Netflix are promoting their continuous delivery techniques where small changes are made very often into production, and tested on a subset of the user base. They've developed tools such as Spinnaker (http://www.spinnaker.io/) to automate as many steps as possible to update production, and ship their features in the cloud as independent microservices.

But if you read Hacker News or Reddit, it can be quite hard to detangle what's useful for you and what's just buzzwords-compliant journalistic-style info.

"Write a paper promising salvation, make it a structured something or a virtual something, or abstract, distributed or higher-order or applicative and you can almost be certain of having started a new cult.

- Edsger W. Dijkstra

This chapter is going to help you understand what are microservices, and will then focus on the various ways in which you can implement them using Python. It's composed of the following few sections:

  • A word on Service-Oriented Architecture
  • Monolithic approach of building an application
  • Microservices approach of building applications
  • Benefits of microservices
  • Pitfalls in microservices
  • Implementing microservices with Python

Hopefully, once you've reached the end of the chapter, you will be able to dive into building microservices with a good understanding of what they are and what they aren't--and how you can use Python.

 

Origins of Service-Oriented Architecture


There are many definitions out there, since there is no official standard for microservices. People often mention Service-Oriented Architecture (SOA) when they are trying to explain what microservices are.

SOA predates microservices, and its core principle is the idea that you organize applications into a discrete unit of functionality that can be accessed remotely and acted upon and updated independently.

- Wikipedia

Each unit in this preceding definition is a self-contained service, which implements one facet of a business, and provides its feature through some interface.

While SOA clearly states that services should be standalone processes, it does not enforce what protocols should be used for those processes to interact with each other, and stays quite vague about how you deploy and organize your application.

If you read the SOA Manifesto (http://www.soa-manifesto.org) that a handful of experts published on the web circa 2009, they don't even mention if the services interact via the network.

SOA services could communicate via Inter-Process Communication (IPC) using sockets on the same machine, through shared memory, through indirect message queues, or even with Remote Procedure Calls (RPC). The options are extensive, and at the end of the day, SOA can be everything and anything as long as you are not running all your application code into a single process.

However, it is common to say that microservices are one specialization of SOA, which have started to emerge over the last few years, because they fulfill some of the SOA goals which are to build apps with standalone components that interact with each other.

Now if we want to give a complete definition of what are microservices, the best way to do it is to first look at how most software are architectured.

 

The monolithic approach


Let's take a very simple example of a traditional monolithic application: a hotel booking website.

Besides the static HTML content, the website has a booking feature that will let its users book hotels in any city in the world. Users can search for hotels, then book them with their credit cards.

When a user performs a search on the hotel website, the application goes through the following steps:

  1. It runs a couple of SQL queries against its hotels' database.
  2. An HTTP request to a partner's service is made to add more hotels to the list.
  3. An HTML results page is generated using an HTML template engine.

From there, once the user has found the perfect hotel and clicked on it to book it, the application performs these steps:

  1. The customer gets created in the database if needed, and has to authenticate.
  2. Payment is carried out by interacting with the bank web service.
  3. The app saves the payment details in the database for legal reasons.
  4. A receipt is generated using a PDF generator.
  5. A recap email is sent to the user using the email service.
  6. A reservation email is forwarded to the third-party hotel using the email service.
  7. A database entry is added to keep track of the reservation.

This process is a simplified model of course, but quite realistic.

The application interacts with a database that contains the hotel's information, the reservation details, the billing, the user information, and so on. It also interacts with external services for sending emails, making payments, and getting more hotels from partners.

In the good old LAMP (Linux-Apache-MySQL-Perl/PHP/Python) architecture, every incoming request generates a cascade of SQL queries on the database, and a few network calls to external services, and then the server generates the HTML response using a template engine.

The following diagram illustrates this centralized architecture:

This application is a typical monolith, and it has a lot of obvious benefits.

The biggest one is that the whole application is in a single code base, and when the project coding starts, it makes everything simpler. Building a good test coverage is easy, and you can organize your code in a clean and structured way inside the code base. Storing all the data into a single database also simplifies the development of the application. You can tweak the data model, and how the code will query it.

The deployment is also a no brainer: we can tag the code base, build a package, and run it somewhere. To scale it, we can run several instances of the booking app, and run several databases with some replication mechanism in place.

If your application stays small, this model works well and is easy to maintain for a single team.

But projects are usually growing, and they get bigger than what was first intended. And having the whole application in a single code base brings some nasty issues along the way. For instance, if you need to make a sweeping change that is large in scope such as changing your banking service or your database layer, the whole application gets into a very unstable state. These changes are a big deal in the project's life, and they necessitate a lot of extra testing to deploy a new version. And changes like this will happen in a project life.

Small changes can also generate collateral damage because different parts of the system have different uptime and stability requirements. Putting the billing and reservation processes at risk because the function that creates the PDF crashes the server is a bit of a problem.

Uncontrolled growth is another issue. The application is bound to get new features, and with developers leaving and joining the project, the code organization might start to get messy, the tests a bit slower. This growth usually ends up with a spaghetti code base that's hard to maintain, with a hairy database that needs complicated migration plans every time some developer refactors the data model.

Big software projects usually take a couple of years to mature, and then they slowly start to turn into an incomprehensible mess that's hard to maintain. And it does not happen because developers are bad. It happens because as the complexity grows, fewer people fully understand the implications of every small change they make. So they try to work in isolation in one corner of the code base, and when you take the 10,000-foot view of the project, you can see the mess.

We've all been there.

It's not fun, and developers who work on such a project dream of building the application from scratch with the newest framework. And by doing so, they usually fall into the same issues again--the same story is repeated.

The following points summarize the pros and cons of the monolithic approach:

  • Starting a project as a monolith is easy, and probably the best approach.
  • A centralized database simplifies the design and organization of the data.
  • Deploying one application is simple.
  • Any change in the code can impact unrelated features. When something breaks, the whole application may break.
  • Solutions to scale your application are limited: you can deploy several instances, but if one particular feature inside the app takes all the resources, it impacts everything.
  • As the code base grows, it's hard to keep it clean and under control.

There are, of course, some ways to avoid some of the issues described here.

The obvious solution is to split the application into separate pieces, even if the resulting code is still going to run in a single process. Developers do this by building their apps with external libraries and frameworks. Those tools can be in-house or from the Open Source Software (OSS) community.

Building a web app in Python if you use a framework like Flask, lets you focus on the business logic, and makes it very appealing to externalize some of your code into Flask extensions and small Python packages. And splitting your code into small packages is often a good idea to control your application growth.

"Small is beautiful."

- The UNIX Philosophy

For instance, the PDF generator described in the hotel booking app could be a separate Python package that uses Reportlab and some templates to do the work.

Chances are this package can be reused in some other apps, and maybe, even published to the Python Package Index (PyPI) for the community.

But you're still building a single application and some problems remain, like the inability to scale parts differently, or any indirect issue introduced by a buggy dependency.

You'll even get new challenges, because you're now using dependencies. One problem you can get is dependency hell. If one part of your application uses a library, but the PDF generator can only use a specific version of that library, there are good chances you will eventually have to deal with it with some ugly workaround, or even fork the dependency to have a custom fix there.

Of course, all the problems described in this section do not appear on day 1 when the project starts, but rather pile up over time.

Let's now look at how the same application would look like if we were to use microservices to build it.

 

The microservice approach


If we were to build the same application using microservices, we would organize the code into several separate components that run in separate processes. Instead of having a single application in charge of everything, we would split it into many different microservices, as shown in the following diagram:

Don't be afraid of the number of components displayed in this diagram. The internal interactions of the monolithic application are just being made visible by separate pieces. We've shifted some of the complexity and ended up with these seven standalone components:

  1. Booking UI: A frontend service, which generates the web user interface, and interacts with all the other microservices.
  2. PDF reporting service: A very simple service that would create PDFs for the receipts or any other document given a template and some data.
  3. Search: A service that can be queried to get a list of hotels given a city name. This service has its own database.
  1. Payments: A service that interacts with the third-party bank service, and manages a billing database. It also sends e-mails on successful payments.
  2. Reservations: Stores reservations, and generates PDFs.
  3. Users: Stores the user information, and interacts with users via emails.
  4. Authentication: An OAuth 2-based service that returns authentication tokens, which each microservice can use to authenticate when calling others.

Those microservices, along with the few external services like the email service, would provide a feature set similar to the monolithic application. In this design, each component communicates using the HTTP protocol, and features are made available through RESTful web services.

There's no centralized database, as each microservice deals internally with its own data structures, and the data that gets in and out uses a language-agnostic format like JSON. It could use XML or YAML as long as it can be produced and consumed by any language, and travel through HTTP requests and responses.

The Booking UI service is a bit particular in that regard, since it generates the User Interface (UI). Depending on the frontend framework used to build the UI, the Booking UI output could be a mix of HTML and JSON, or even plain JSON if the interface uses a static JavaScript-based client-side tool to generate the interface directly in the browser.

But besides this particular UI case, a web application designed with microservices is a composition of several microservices, which may interact with each other through HTTP to provide the whole system.

In that context, microservices are logical units that focus on a very particular task. Here's a full definition attempt:

Note

A microservice is a lightweight application, which provides a narrowed list of features with a well-defined contract. It's a component with a single responsibility, which can be developed and deployed independently.

This definition does not mention HTTP or JSON, because you could consider a small UDP-based service that exchanges binary data as a microservice for example.

But in our case, and throughout the book, all our microservices are just simple web applications that use the HTTP protocol, and consume and produce JSON when it's not a UI.

 

Microservice benefits


While the microservices architecture looks more complicated than its monolithic counterpart, its advantages are multiple. It offers the following:

  • Separation of concerns
  • Smaller projects to deal with
  • More scaling and deployment options

We will discuss them in more detail in the following sections.

Separation of concerns

First of all, each microservice can be developed independently by a separate team. For instance, building a reservation service can be a full project on its own. The team in charge can make it in whatever programming language and database, as long as it has a well-documented HTTP API.

That also means the evolution of the app is more under control than with monoliths. For example, if the payment system changes its underlying interactions with the bank, the impact is localized inside that service, and the rest of the application stays stable and is probably unaffected.

This loose coupling improves the overall project velocity a lot, as we apply, at the service level, a philosophy similar to the single responsibility principle.

The single responsibility principle was defined by Robert Martin to explain that a class should have only one reason to change; in other words, each class should provide a single, well-defined feature. Applied to microservices, it means that we want to make sure that each microservice focuses on a single role.

Smaller projects

The second benefit is breaking the complexity of the project. When you add a feature to an application such as PDF reporting, even if you do it cleanly, you make the base code bigger, more complicated, and sometimes, slower. Building that feature in a separate application avoids this problem, and makes it easier to write it with whatever tools you want. You can refactor it often, shorten your release cycles, and stay on top of things. The growth of the application remains under your control.

Dealing with a smaller project also reduces risks when improving the application: if a team wants to try out the latest programming language or framework, they can iterate quickly on a prototype that implements the same microservice API, try it out, and decide whether or not to stick with it.

One real-life example in mind is the Firefox Sync storage microservice. There are currently some experiments to switch from the current Python + MySQL implementation to a Go-based one, which stores users' data in standalone SQLite databases. That prototype is highly experimental, but since we have isolated the storage feature in a microservice with a well-defined HTTP API, it's easy enough to give it a try with a small subset of the user base.

Scaling and deployment

Finally, having your application split into components makes it easier to scale depending on your constraints. Let's say you start getting a lot of customers who book hotels daily, and the PDF generation starts to heat up the CPUs. You can deploy that specific microservice in some servers that have bigger CPUs.

Another typical example are RAM-consuming microservices like the ones that interact with memory databases like Redis or Memcache. You could tweak your deployments, consequently, by deploying them on servers with less CPU and a lot more RAM.

We can, thus, summarize the benefits of microservices as follows:

  • A team can develop each microservice independently, and use whatever technological stack makes sense. They can define a custom release cycle. All they need to define is a language-agnostic HTTP API.
  • Developers break the application complexity into logical components. Each microservice focuses on doing one thing well.
  • Since microservices are standalone applications, there's a finer control on deployments, which makes scaling easier.

The microservices architecture is good at solving a lot of the problems that may arise once your application starts to grow. However, we need to be aware of some of the new issues they also bring in practice.

 

Microservices pitfalls


As said earlier, building an application with microservices has a lot of benefits, but it's not a silver bullet by all means.

You need to be aware of these main problems you might have to deal with when coding microservices:

  • Illogical splitting
  • More network interactions
  • Data storing and sharing
  • Compatibility issues
  • Testing

These issues will be covered in detail in the following sections.

Illogical splitting

The first issue of a microservice architecture is how it gets designed. There's no way a team can come up with the perfect microservice architecture in the first shot. Some microservices like the PDF generator are an obvious use case. But as soon as you deal with the business logic, there are good chances that your code will move around before you get a good grasp of how to split things into the right set of microservices.

The design needs to mature with some try-and-fail cycles. And adding and removing microservices can be more painful than refactoring a monolithic application.

You can mitigate this problem by avoiding splitting your app in microservices if the split is not evident.

Premature splitting is the root of all evil.

If there's any doubt that the split makes sense, keeping the code in the same app is the safe bet. It's always easier to split apart some of the code into a new microservice later than to merge back to two microservices in the same code base because the decision turned out to be wrong.

For instance, if you always have to deploy two microservices together, or if one change in a microservice impacts the data model of another one, the odds are that you did not split the application correctly, and that those two services should be reunited.

More network interactions

The second problem is the amount of network interactions added to build the same application. In the monolithic version, even if the code gets messy, everything happens in the same process, and you can send back the result without having to call too many backend services to build the actual response.

That requires extra attention on how each backend service is called, and raises a lot of questions like the following:

  • What happens when the Booking UI cannot reach the PDF reporting service because of a network split or a laggy service?
  • Does the Booking UI call the other services synchronously or asynchronously?
  • How will that impact the response time?

We will need to have a solid strategy to be able to answer all those questions, and we will address those in Chapter 5, Interacting with Other Services.

Data storing and sharing

Another problem is data storing and sharing. An effective microservice needs to be independent of other microservices, and ideally, should not share a database. What does this mean for our hotel booking app?

Again, that raises a lot of questions such as the following:

  • Do we use the same users' IDs across all databases, or do we have independent IDs in each service and keep it as a hidden implementation detail?
  • Once a user is added to the system, do we replicate some of her information in other services databases via strategies like data pumping, or is that overkill?
  • How do we deal with data removal?

These are hard questions to answer, and there are many different ways to solve those problems, as we'll learn throughout the book.

Note

Avoiding data duplication as much as possible while keeping microservices in isolation is one of the biggest challenges in designing microservices-based applications.

Compatibility issues

Another problem happens when a feature change impacts several microservices. If a change affects in a backward incompatible way the data that travels between services, you're in for some trouble.

Can you deploy your new service, and will it work with older versions of other services? Or do you need to change and deploy several services at once? Does it mean you've just stumbled on some services that should probably be merged back together?

A good versioning and API design hygiene help to mitigate those issues, as we will discover in the second part of the book when we'll build our application.

Testing

Last, when you want to do some end-to-end tests and deploy your whole app, you now have to deal with many bricks. You need to have a robust and agile deployment process to be efficient. You need to be able to play with your whole application when you develop it. You can't fully test things out with just one piece of the puzzle.

Hopefully, there are now many tools to facilitate deployments of applications that are built with several components, as we will learn about throughout this book. And all those tools probably helped in the success and adoption of microservices and vice versa.

Note

Microservices-style architecture boosts deployment tools innovation, and deployment tools lower the bar for the approval of microservices-style architecture.

The pitfalls of using microservices can be summarized as follows:

  • Premature splitting of an application into microservices can lead to architectural problems
  • Network interactions between microservices add weaknesses spots and additional overhead
  • Testing and deploying microservices can be complex
  • And the biggest challenge--data sharing between microservices is hard

You should not worry too much about all the pitfalls described in this section for now.

They may seem overwhelming, and the traditional monolithic application may look like a safer bet, but in the long term, splitting your project into microservices will make many of your tasks, as a developer or as an Operation person (Ops), easier.

 

Implementing microservices with Python


Python is an amazingly versatile language.

As you probably already know, it's used to build many different kinds of applications--from simple system scripts that perform tasks on a server to large object-oriented applications that run services for millions of users.

According to a study conducted by Philip Guo in 2014, published on the Association for Computing Machinery (ACM) website, Python has surpassed Java in top U.S. universities, and is the most popular language to learn computer science.

This trend is also true in the software industry. Python sits now in the top five languages in the TIOBE index (http://www.tiobe.com/tiobe-index/), and it's probably even bigger in the web development land, since languages like C are rarely used as main languages to build web applications.

Note

This book makes the assumption that you are already familiar with the Python programming language. If you are not an experienced Python developer, you can read the book Expert Python Programming, Second Edition, where you will learn advanced programming skills in Python.

However, some developers criticize Python for being slow and unfit for building efficient web services. Python is slow, and this is undeniable. But it still is a language of choice for building microservices, and many major companies are happily using it.

This section will give you some background on the different ways you can write microservices using Python, some insights on asynchronous versus synchronous programming, and conclude with some details on Python performances.

This section is composed of five parts:

  • The WSGI standard
  • Greenlet and Gevent
  • Twisted and Tornado
  • asyncio
  • Language performances

The WSGI standard

What strikes most web developers who start with Python is how easy it is to get a web application up and running.

The Python web community has created a standard (inspired by the Common Gateway Interface or CGI) called Web Server Gateway Interface (WSGI). It simplifies a lot how you can write a Python application in order to serve HTTP requests.

When your code uses that standard, your project can be executed by standard web servers like Apache or nginx, using WSGI extensions like uwsgi or mod_wsgi.

Your application just has to deal with incoming requests and send back JSON responses, and Python includes all that goodness in its standard library.

You can create a fully functional microservice that returns the server's local time with a vanilla Python module of fewer than 10 lines. It is given as follows:

    import json
    import time 

    def application(environ, start_response): 
        headers = [('Content-type', 'application/json')] 
        start_response('200 OK', headers) 
        return [bytes(json.dumps({'time': time.time()}), 'utf8')] 

Since its introduction, the WSGI protocol became an essential standard, and the Python web community widely adopted it. Developers wrote middlewares, which are functions you can hook before or after the WSGI application function itself, to do something within the environment.

Some web frameworks, like Bottle (http://bottlepy.org), were created specifically around that standard, and soon enough, every framework out there could be used through WSGI in one way or another.

The biggest problem with WSGI though is its synchronous nature. The application function you saw in the preceding code is called exactly once per incoming request, and when the function returns, it has to send back the response. That means that every time you call the function, it will block until the response is ready.

And writing microservices means your code will have to wait for responses from various network resources all the time. In other words, your application will be idle, and just block the client until everything is ready.

That's an entirely okay behavior for HTTP APIs. We're not talking about building bidirectional applications like web socket-based ones. But what happens when you have several incoming requests that call your application at the same time?

WSGI servers will let you run a pool of threads to serve several requests concurrently. But you can't run thousands of them, and as soon as the pool is exhausted, the next request will block the client's access even if your microservice is doing nothing but idling and waiting for backend services' responses.

That's one of the reasons why non-WSGI frameworks like Twisted andTornado, and in JavaScript land, Node.js, became very successful--it's fully async.

When you're coding a Twisted application, you can use callbacks to pause and resume the work done to build a response. That means that you can accept new requests and start to treat them. That model dramatically reduces the idling time in your process. It can serve thousands of concurrent requests. Of course, that does not mean the application will return each single response faster. It just means one process can accept more concurrent requests, and juggle between them as the data is getting ready to be sent back.

There's no simple way with the WSGI standard to introduce something similar, and the community has debated for years to come up with a consensus--and failed. The odds are that the community will eventually drop the WSGI standard for something else.

In the meantime, building microservices with synchronous frameworks is still possible and completely fine if your deployments take into account the one request == one thread limitation of the WSGI standard.

There's, however, one trick to boost synchronous web applications--Greenlet, which is explained in the following section.

Greenlet and Gevent

The general principle of asynchronous programming is that the process deals with several concurrent execution contexts to simulate parallelism.

Asynchronous applications use an event loop that pauses and resumes execution contexts when an event is triggered--only one context is active, and they take turns. Explicit instruction in the code will tell the event loop that this is where it can pause the execution.

When that occurs, the process will look for some other pending work to resume. Eventually, the process will come back to your function and continue it where it stopped. Moving from an execution context to another is called switching.

The Greenlet project (https://github.com/python-greenlet/greenlet) is a package based on the Stackless project, a particular CPython implementation, and provides greenlets.

Greenlets are pseudo-threads that are very cheap to instantiate, unlike real threads, and that can be used to call Python functions. Within those functions, you can switch, and give back the control to another function. The switching is done with an event loop, and allows you to write an asynchronous application using a thread-like interface paradigm.

Here's an example from the Greenlet documentation:

    from greenlet import greenlet
    def test1(x, y):
        z = gr2.switch(x+y)
        print(z)

    def test2(u): 
        print (u) 
        gr1.switch(42) 

    gr1 = greenlet(test1) 
    gr2 = greenlet(test2) 
    gr1.switch("hello", " world") 

The two greenlets in the preceding example explicitly switch from one to the other.

For building microservices based on the WSGI standard, if the underlying code uses greenlets, we could accept several concurrent requests, and just switch from one to another when we know a call is going to block the request--like I/O requests.

However, switching from one greenlet to another has to be done explicitly, and the resulting code can quickly become messy and hard to understand. That's where Gevent can become very useful.

The Gevent project (http://www.gevent.org/) is built on top of Greenlet, and offers an implicit and automatic way of switching between greenlets, among many other things.

It provides a cooperative version of the socket module, which uses greenlets to automatically pause and resume the execution when some data is made available in the socket. There's even a monkey patch feature, which automatically replaces the standard library socket with Gevent's version. That makes your standard synchronous code magically asynchronous every time it uses sockets--with just one extra line:

    from gevent import monkey; monkey.patch_all() 

    def application(environ, start_response): 
        headers = [('Content-type', 'application/json')] 
        start_response('200 OK', headers) 
        # ...do something with sockets here... 
        return result 

This implicit magic comes at a price though. For Gevent to work well, all the underlying code needs to be compatible with the patching that Gevent does. Some packages from the community will continue to block or even have unexpected results because of this--in particular, if they use C extensions, and bypass some of the features of the standard library Gevent patched.

But it works well for most cases. Projects that play well with Gevent are dubbed green, and when a library is not functioning well, and the community asks its authors to make it green, it usually happens.

That's what was used to scale the Firefox Sync service at Mozilla, for instance.

Twisted and Tornado

If you are building microservices where increasing the number of concurrent requests you can hold is important, it's tempting to drop the WSGI standard, and just use an asynchronous framework like Tornado (http://www.tornadoweb.org/) or Twisted (https://twistedmatrix.com/trac/).

Twisted has been around for ages. To implement the same microservices, you need to write a slightly more verbose code like this:

    import time  
    import json
    from twisted.web import server, resource 
    from twisted.internet import reactor, endpoints 

    class Simple(resource.Resource): 
        isLeaf = True 
        def render_GET(self, request): 
            request.responseHeaders.addRawHeader(b"content-type", 
                                                 b"application/json") 
            return bytes(json.dumps({'time': time.time()}), 'utf8') 

        site = server.Site(Simple()) 
        endpoint = endpoints.TCP4ServerEndpoint(reactor, 8080) 
        endpoint.listen(site) 
        reactor.run() 

While Twisted is an extremely robust and efficient framework, it suffers from a few problems when building HTTP microservices, which are as follows:

  • You need to implement each endpoint in your microservice with a class derived from a Resource class, and that implements each supported method. For a few simple APIs, it adds a lot of boilerplate code.
  • Twisted code can be hard to understand and debug due to its asynchronous nature.
  • It's easy to fall into callback hell when you chain too many functions that get triggered successively one after the other--and the code can get messy.
  • Properly testing your Twisted application is hard, and you have to use a Twisted-specific unit testing model.

Tornado is based on a similar model, but does a better job in some areas. It has a lighter routing system, and does everything possible to make the code closer to plain Python. Tornado also uses a callback model, so debugging can be hard.

But both frameworks are working hard at bridging the gap to rely on the new async features introduced in Python 3.

asyncio

When Guido van Rossum started to work on adding async features in Python 3, part of the community pushed for a Gevent-like solution, because it made a lot of sense to write applications in a synchronous, sequential fashion rather than having to add explicit callbacks like in Tornado or Twisted.

But Guido picked the explicit technique, and experimented in a project called Tulip inspired by Twisted. Eventually, the asyncio module was born out of that side project and added into Python.

In hindsight, implementing an explicit event loop mechanism in Python instead of going the Gevent way makes a lot of sense. The way the Python core developers coded asyncio, and how they elegantly extended the language with the async and await keywords to implement coroutines, made asynchronous applications built with vanilla Python 3.5+ code look very elegant and close to synchronous programming.

Note

Coroutines are functions that can suspend and resume their execution. Chapter 12, What Next?, explains in detail how they are implemented in Python and how to use them.

By doing this, Python did a great job at avoiding the callback syntax mess we sometimes see in Node.js or Twisted (Python 2) applications.

And beyond coroutines, Python 3 has introduced a full set of features and helpers in the asyncio package to build asynchronous applications, refer to https://docs.python.org/3/library/asyncio.html.

Python is now as expressive as languages like Lua to create coroutine-based applications, and there are now a few emerging frameworks that have embraced those features, and will only work with Python 3.5+ to benefit from this.

KeepSafe's aiohttp (http://aiohttp.readthedocs.io) is one of them, and building the same microservice, fully asynchronous, with it would simply need these few elegant lines:

    from aiohttp import web  
    import time 

    async def handle(request): 
        return web.json_response({'time': time.time()}) 

    if __name__ == '__main__': 
        app = web.Application() 
        app.router.add_get('/', handle) 
        web.run_app(app) 

In this small example, we're very close to how we would implement a synchronous app. The only hint we're using async is the async keyword, which marks the handle function as being a coroutine.

And that's what's going to be used at every level of an async Python app going forward. Here's another example using aiopg, a PostgreSQL library for asyncio from the project documentation:

    import asyncio 
    import aiopg 

    dsn = 'dbname=aiopg user=aiopg password=passwd host=127.0.0.1' 

    async def go(): 
        pool = await aiopg.create_pool(dsn) 
        async with pool.acquire() as conn: 
            async with conn.cursor() as cur: 
                await cur.execute("SELECT 1") 
                ret = [] 
                async for row in cur: 
                    ret.append(row) 
                assert ret == [(1,)] 

    loop = asyncio.get_event_loop() 
    loop.run_until_complete(go())  

With a few async and await prefixes, the function that performs an SQL query and sends back the result looks a lot like a synchronous function.

But asynchronous frameworks and libraries based on Python 3 are still emerging, and if you are using asyncio or a framework like aiohttp, you will need to stick with particular asynchronous implementations for each feature you need.

If you need to use a library that is not asynchronous in your code, to use it from your asynchronous code means that you will need to go through some extra and challenging work if you want to prevent blocking the event loop.

If your microservices deal with a limited number of resources, it could be manageable. But it's probably a safer bet at the time of this writing to stick with a synchronous framework that's been around for a while rather than an asynchronous one. Let's enjoy the existing ecosystem of mature packages, and wait until the asyncio ecosystem gets more sophisticated.

And there are many great synchronous frameworks to build microservices with Python, like Bottle, Pyramid with Cornice, or Flask.

Note

There are good chances that the second edition of this book will use an asynchronous framework. But for this edition, we'll use the Flask framework throughout the book. It's been around for some time, and is very robust and mature. However, keep in mind that whatever Python web framework you use, you should be able to transpose all the examples in this book. This is because most of the coding involved when building microservices is very close to plain Python, and the framework is mostly to route the requests and offer a few helpers.

Language performances

In the previous sections, we've been through the two different ways to write microservices: asynchronous versus synchronous, and whatever technique you use, the speed of Python directly impacts the performance of your microservice.

Of course, everyone knows Python is slower than Java or Go, but execution speed is not always the top priority. A microservice is often a thin layer of code that sits most of its life waiting for some network responses from other services. Its core speed is usually less important than how fast your SQL queries will take to return from your Postgres server, because the latter will represent most of the time spent to build the response.

But wanting an application that's as fast as possible is legitimate.

One controversial topic in the Python community around speeding up the language is how the Global Interpreter Lock (GIL) mutex can ruin performances, because multi-threaded applications cannot use several processes.

The GIL has good reasons to exist. It protects non-thread-safe parts of the CPython interpreter, and exists in other languages like Ruby. And all attempts to remove it so far have failed to produce a faster CPython implementation.

Note

Larry Hasting is working on a GIL-free CPython project called Gilectomy (https://github.com/larryhastings/gilectomy). Its minimal goal is to come up with a GIL-free implementation, which can run a single-threaded application as fast as CPython. As of the time of this writing, this implementation is still slower that CPython. But it's interesting to follow this work, and see if it reaches speed parity one day. That would make a GIL-free CPython very appealing.

For microservices, besides preventing the usage of multiple cores in the same process, the GIL will slightly degrade performances on high load because of the system calls overhead introduced by the mutex.

However, all the scrutiny around the GIL has been beneficial: work has been done in the past years to reduce GIL contention in the interpreter, and in some areas, Python's performance has improved a lot.

Bear in mind that even if the core team removes the GIL, Python is an interpreted and garbage collected language and suffers performance penalties for those properties.

Python provides the dis module if you are interested to see how the interpreter decomposes a function. In the following example, the interpreter will decompose a simple function that yields incremented values from a sequence in no less than 29 steps:

    >>> def myfunc(data): 
    ...     for value in data: 
    ...         yield value + 1 
    ... 
    >>> import dis 
    >>> dis.dis(myfunc) 
      2           0 SETUP_LOOP              23 (to 26) 
                  3 LOAD_FAST                0 (data) 
                  6 GET_ITER 
            >>    7 FOR_ITER                15 (to 25) 
                  10 STORE_FAST              1 (value) 

      3         13 LOAD_FAST                 1 (value) 
                16 LOAD_CONST                1 (1) 
                19 BINARY_ADD 
                20 YIELD_VALUE 
                21 POP_TOP 
                22 JUMP_ABSOLUTE        7 
          >>    25 POP_BLOCK 
          >>    26 LOAD_CONST                0 (None) 
                29 RETURN_VALUE 

A similar function written in a statically compiled language will dramatically reduce the number of operations required to produce the same result. There are ways to speed up Python execution, though.

One is to write a part of your code into compiled code by building C extensions, or using a static extension of the language like Cython (http://cython.org/), but that makes your code more complicated.

Another solution, which is the most promising one, is by simply running your application using the PyPy interpreter (http://pypy.org/).

PyPy implements a Just-In-Time (JIT) compiler. This compiler directly replaces, at runtime, pieces of Python with machine code that can be directly used by the CPU. The whole trick for the JIT is to detect in real time, ahead of the execution, when and how to do it.

Even if PyPy is always a few Python versions behind CPython, it has reached a point where you can use it in production, and its performances can be quite amazing. In one of our projects at Mozilla that needs fast execution, the PyPy version was almost as fast as the Go version, and we've decided to use Python there instead.

Note

The Pypy Speed Center website is a great place to look at how PyPy compares to CPython ( http://speed.pypy.org/).

However, if your program uses C extensions, you will need to recompile them for PyPy, and that can be a problem. In particular, if other developers maintain some of the extensions you are using.

But if you build your microservice with a standard set of libraries, chances are that it will work out of the box with the PyPy interpreter, so that's worth a try.

In any case, for most projects, the benefits of Python and its ecosystem largely surpass the performance issues described in this section, because the overhead in a microservice is rarely a problem. And if performance is a problem, the microservice approach allows you to rewrite performance-critical components without affecting the rest of the system.

 

Summary


In this chapter, we've compared the monolithic versus microservice approach to building web applications, and it became apparent that it's not a binary world where you have to pick one model on day one and stick with it.

You should see microservices as an improvement of an application that started its life as a monolith. As the project matures, parts of the service logic should migrate into microservices. It is a useful approach as we've learned in this chapter, but it should be done carefully to avoid falling into some common traps.

Another important lesson is that Python is considered to be one of the best languages to write web applications, and therefore, microservices--for the same reasons, it's a language of choice in other areas, and also because it provides tons of mature frameworks and packages to do the work.

We've rapidly looked through the chapter at several frameworks, both synchronous and asynchronous, and for the rest of the book, we'll be using Flask.

The next chapter will introduce this fantastic framework, and if you are not familiar with it, you will probably love it.

Lastly, Python is a slow language, and that can be a problem in very specific cases. But knowing what makes it slow, and the different solutions to avoid this issue will usually be enough to make that problem not relevant.

About the Author

  • Tarek Ziadé

    Tarek Ziadé is a Python developer located in the countryside near Dijon, France. He works at Mozilla in the services team. He founded a French Python user group called Afpy, and has written several books about Python in French and English. When he is not hacking on his computer or hanging out with his family, he's spending time between his two passions, running and playing the trumpet.

    You can visit his personal blog (Fetchez le Python) and follow him on Twitter (tarek_ziade).

    Browse publications by this author

Latest Reviews

(10 reviews total)
Información muy detallada y con ejemplos que hacen que la lectura sea muy sencilla y puedas poner en práctica en el momento
I buy tree ebooks about Python, but I couldn't download source files, My ******* username (email address) are not being recognized by packt portal and support insist telling me that it is my problem!!. Keep those ******* files for ya!! and fix your software!
Bad format on kindle, too small font

Recommended For You

Book Title
Unlock this full book FREE 10 day trial
Start Free Trial