|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- title: Nameko for Microservices
- url: http://lucumr.pocoo.org/2015/4/8/microservices-with-nameko/
- hash_url: ab01c9532d507c86951542839a45c026
-
- <p>In December some of the tech guys at <a class="reference external" href="http://www.onefinestay.com/">onefinestay</a> invited me over to London to do some
- general improvements on their their <a class="reference external" href="http://nameko.readthedocs.org/en/latest/">nameko</a> library. This collaboration
- came together because nameko was pretty similar to how I generally like to
- build certain infrastructure and I had some experience with very similar
- systems.</p>
- <p>So now that some of those improvements hit the release version of nameko I
- figured it might be a good idea to give some feedback on why I like this
- sort of architecture.</p>
-
- <h2>Freeing your Mind</h2>
- <p>Right now if you want to build a web service in Python there are many
- tools you can pick from, but most of them live in a very specific part of
- your stack. The most common tool is a web framework and that will
- typically provide you with whatever glue is necessary to connect your own
- code to an incoming HTTP request that comes from your client.</p>
- <p>However that's not all you need in an application. For instance very
- often you have periodic tasks that you need to execute and in that case,
- your framework is often not just not helping, it's also in your way. For
- instance because you might have built your code with the assumption that
- it has access to the HTTP request object. If you now want to run it from
- a cronjob that request object is unavailable.</p>
- <p>In addition to crons there is often also the wish to execute something as
- the result of the request of a client, but without blocking that request.
- For instance imagine there is an admin panel in which you can trigger some
- very expensive data conversion task. What you actually want is for the
- current request to finish but the conversion task to keep on working in
- the background until your data set is converted.</p>
- <p>There are obviously many existing solutions for that. Celery comes to
- mind. However they are typically very separated from the rest of the
- stack.</p>
- <p>Having a system which treats all of this processes the same frees up your
- mind. This is what makes microservices interesting. Away with having
- HTTP request handlers that have no direct relationship with message queue
- worker tasks or cronjobs. Instead you can have a coherent system where
- any component can talk through well defined points with other parts of the
- system.</p>
- <p>This is especially useful in Python where traditionally our support for
- parallel execution has been between very bad to abysmal.</p>
-
- <h2>Enter Nameko</h2>
- <p>Nameko is an implementation of this idea. It's very similar in
- architecture to how we structure code at Fireteam. It's based on
- distributing work between processes through AMQP. It's not just AMQP
- though. Nameko abstracts away from that and allows you to write your own
- transports, while staying true to the AMQP patterns.</p>
- <p>Nameko does a handful of things and you can build very complex systems
- with it. The idea is that you build individual services which can emit
- events to which other services can subscribe to or they can directly
- invoke each other via RPC. All communication between the services happens
- through AMQP. You don't need to manually deal with any connectivity of
- those.</p>
- <p>In addition to message exchange, services also use a lifecycle management
- to find useful resources through dependency injection. That sounds like a
- mouthful but is actually very simple. Because services are classes, you
- can add special attributes to them which will be resolved at runtime. The
- lifetime of the value resolved can be customized. For instance it becomes
- possible to attach a property to the class which can provide access to a
- database connection. The lifetime of that database connection can be
- automatically managed.</p>
- <p>So how does that look in practice? Something like this:</p>
- <div class="highlight"><pre><span class="kn">from</span> <span class="nn">nameko.rpc</span> <span class="kn">import</span> <span class="n">rpc</span>
-
- <span class="k">class</span> <span class="nc">HelloWorldService</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
- <span class="n">name</span> <span class="o">=</span> <span class="s">'helloworld'</span>
-
- <span class="nd">@rpc</span>
- <span class="k">def</span> <span class="nf">hello</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
- <span class="k">return</span> <span class="s">"Hello, {}!"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
- </pre></div>
- <p>This defines a basic service that provides one method that can be invoked
- via RPC. Either another service can do that, or any other process that
- runs nameko can also invoke that, for as long as they connect to the same
- AMQP server. To experiment with this service, Nameko provides a shell
- helper that launches an interactive Python shell with an <tt class="docutils literal">n</tt> object that
- provides RPC access:</p>
- <div class="highlight"><pre><span class="gp">>>> </span><span class="n">n</span><span class="o">.</span><span class="n">rpc</span><span class="o">.</span><span class="n">helloworld</span><span class="o">.</span><span class="n">hello</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">'John'</span><span class="p">)</span>
- <span class="go">u'Hello, John!'</span>
- </pre></div>
- <p>If the AMQP server is running, <tt class="docutils literal">rpc.helloworld.hello</tt> contacts the
- <tt class="docutils literal">helloworld</tt> service and resolves the <tt class="docutils literal">hello</tt> method on it. Upon
- calling this method a message will be dispatched via the AMQP broker and
- be picked up by a nameko process. The shell will then block and wait for
- the result to come back.</p>
- <p>A more useful example is what happens when services want to collaborate on
- some activity. For instance it's quite common that one service wants to
- respond to the changes another service performs to update it's own state.
- This can be achieved through events:</p>
- <div class="highlight"><pre><span class="kn">from</span> <span class="nn">nameko.events</span> <span class="kn">import</span> <span class="n">EventDispatcher</span><span class="p">,</span> <span class="n">event_handler</span>
- <span class="kn">from</span> <span class="nn">nameko.rpc</span> <span class="kn">import</span> <span class="n">rpc</span>
-
- <span class="k">class</span> <span class="nc">ServiceA</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
- <span class="n">name</span> <span class="o">=</span> <span class="s">'servicea'</span>
- <span class="n">dispatch</span> <span class="o">=</span> <span class="n">EventDispatcher</span><span class="p">()</span>
-
- <span class="nd">@rpc</span>
- <span class="k">def</span> <span class="nf">emit_an_event</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
- <span class="bp">self</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="s">'my_event_type'</span><span class="p">,</span> <span class="s">'payload'</span><span class="p">)</span>
-
-
- <span class="k">class</span> <span class="nc">ServiceB</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
- <span class="n">name</span> <span class="o">=</span> <span class="s">'serviceb'</span>
-
- <span class="nd">@event_handler</span><span class="p">(</span><span class="s">'servicea'</span><span class="p">,</span> <span class="s">'my_event_type'</span><span class="p">)</span>
- <span class="k">def</span> <span class="nf">handle_an_event</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">payload</span><span class="p">):</span>
- <span class="k">print</span> <span class="s">'service b received'</span><span class="p">,</span> <span class="n">payload</span>
- </pre></div>
- <p>The default behavior is that one service instance of each service type
- will pick up the event. However nameko can also route an event to every
- single instance of every single service. This is useful for in-process
- cache invalidation for instance.</p>
-
- <h2>The Web</h2>
- <p>Nameko is not just good for internal communication however. It uses
- Werkzeug to provide a bridge to the outside world. This allows you to
- accept an HTTP request and to ingest a task into your service world:</p>
- <div class="highlight"><pre><span class="kn">import</span> <span class="nn">json</span>
- <span class="kn">from</span> <span class="nn">nameko.web.handlers</span> <span class="kn">import</span> <span class="n">http</span>
- <span class="kn">from</span> <span class="nn">werkzeug.wrappers</span> <span class="kn">import</span> <span class="n">Response</span>
-
- <span class="k">class</span> <span class="nc">HttpServiceService</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
- <span class="n">name</span> <span class="o">=</span> <span class="s">'helloworld'</span>
-
- <span class="nd">@http</span><span class="p">(</span><span class="s">'GET'</span><span class="p">,</span> <span class="s">'/get/<int:value>'</span><span class="p">)</span>
- <span class="k">def</span> <span class="nf">get_method</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
- <span class="k">return</span> <span class="n">Response</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span><span class="s">'value'</span><span class="p">:</span> <span class="n">value</span><span class="p">}),</span>
- <span class="n">mimetype</span><span class="o">=</span><span class="s">'application/json'</span><span class="p">)</span>
- </pre></div>
- <p>The endpoint function can itself invoke other parts of the system via RPC
- or other methods.</p>
- <p>This functionality generally also extends into the websocket world, even
- though that part is currently quite experimental. It for instance is
- possible to listen to events and forward them into websocket connections.</p>
-
- <h2>Dependency Injection</h2>
- <p>One of the really neat design concepts in Nameko is the use of dependency
- injection to find resources. A good example is the SQLAlchemy bridge
- which attaches a SQLAlchemy database session to a service through
- dependency injection. The descriptor itself will hook into the lifecycle
- management to automatically manage the database resources:</p>
- <div class="highlight"><pre><span class="kn">from</span> <span class="nn">nameko_sqlalchemy</span> <span class="kn">import</span> <span class="n">Session</span>
-
- <span class="kn">import</span> <span class="nn">sqlalchemy</span> <span class="kn">as</span> <span class="nn">sa</span>
- <span class="kn">from</span> <span class="nn">sqlalchemy.ext.declarative</span> <span class="kn">import</span> <span class="n">declarative_base</span>
-
- <span class="n">Base</span> <span class="o">=</span> <span class="n">declarative_base</span><span class="p">()</span>
-
- <span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
- <span class="n">__tablename__</span> <span class="o">=</span> <span class="s">'users'</span>
- <span class="nb">id</span> <span class="o">=</span> <span class="n">sa</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">sa</span><span class="o">.</span><span class="n">Integer</span><span class="p">,</span> <span class="n">primary_key</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
- <span class="n">username</span> <span class="o">=</span> <span class="n">sa</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">sa</span><span class="o">.</span><span class="n">String</span><span class="p">)</span>
-
-
- <span class="k">class</span> <span class="nc">MyService</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
- <span class="n">name</span> <span class="o">=</span> <span class="s">'myservice'</span>
- <span class="n">session</span> <span class="o">=</span> <span class="n">Session</span><span class="p">(</span><span class="n">Base</span><span class="p">)</span>
-
- <span class="nd">@rpc</span>
- <span class="k">def</span> <span class="nf">get_username</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">):</span>
- <span class="n">user</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">User</span><span class="p">)</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
- <span class="k">if</span> <span class="n">user</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
- <span class="k">return</span> <span class="n">user</span><span class="o">.</span><span class="n">username</span>
- </pre></div>
- <p>The implementation of the <tt class="docutils literal">Session</tt> dependency provider itself is
- ridiculously simple. The whole functionality could be implemented like
- this:</p>
- <div class="highlight"><pre><span class="kn">from</span> <span class="nn">weakref</span> <span class="kn">import</span> <span class="n">WeakKeyDictionary</span>
-
- <span class="kn">from</span> <span class="nn">nameko.extensions</span> <span class="kn">import</span> <span class="n">DependencyProvider</span>
- <span class="kn">from</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="n">create_engine</span>
- <span class="kn">from</span> <span class="nn">sqlalchemy.orm</span> <span class="kn">import</span> <span class="n">sessionmaker</span>
-
-
- <span class="k">class</span> <span class="nc">Session</span><span class="p">(</span><span class="n">DependencyProvider</span><span class="p">):</span>
-
- <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">declarative_base</span><span class="p">):</span>
- <span class="bp">self</span><span class="o">.</span><span class="n">declarative_base</span> <span class="o">=</span> <span class="n">declarative_base</span>
- <span class="bp">self</span><span class="o">.</span><span class="n">sessions</span> <span class="o">=</span> <span class="n">WeakKeyDictionary</span><span class="p">()</span>
-
- <span class="k">def</span> <span class="nf">get_dependency</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">worker_ctx</span><span class="p">):</span>
- <span class="n">db_uri</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">container</span><span class="o">.</span><span class="n">config</span><span class="p">[</span><span class="s">'DATABASE_URL'</span><span class="p">]</span>
- <span class="n">engine</span> <span class="o">=</span> <span class="n">create_engine</span><span class="p">(</span><span class="n">db_uri</span><span class="p">)</span>
- <span class="n">session_cls</span> <span class="o">=</span> <span class="n">sessionmaker</span><span class="p">(</span><span class="n">bind</span><span class="o">=</span><span class="n">engine</span><span class="p">)</span>
- <span class="bp">self</span><span class="o">.</span><span class="n">sessions</span><span class="p">[</span><span class="n">worker_ctx</span><span class="p">]</span> <span class="o">=</span> <span class="n">session</span> <span class="o">=</span> <span class="n">session_cls</span><span class="p">()</span>
- <span class="k">return</span> <span class="n">session</span>
-
- <span class="k">def</span> <span class="nf">worker_teardown</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">worker_ctx</span><span class="p">):</span>
- <span class="n">sess</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">sessions</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="n">worker_ctx</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span>
- <span class="k">if</span> <span class="n">sess</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
- <span class="n">sess</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
- </pre></div>
- <p>The actual implementation is only a tiny bit more complicated, and that is
- basically just a bit of extra code to support different database URLs for
- different services and declarative bases. Overall the concept is the same
- however. When the dependency is needed, a connection to the database is
- established and when the worker shuts down, the session is closed.</p>
-
- <h2>Concurrency and Parallelism</h2>
- <p>What makes nameko interesting is that scales out really well through the
- use of AMQP and eventlet. First of all, when nameko starts a service
- container it uses eventlet to patch up the Python interpreter to support
- green concurrency. This allows a service container to become quite
- concurrent to do multiple things at once. This is very useful when a
- service waits on another service as threads in Python are a very
- disappointing story. As this however largely eliminates the possibility
- of true parallelism it becomes necessary to start multiple instances of
- services to scale up. Thanks to the use of AMQP however, this becomes a
- very transparent process. For as long as services do not need to store
- local state, it becomes very trivial to run as many of those service
- containers as necessary.</p>
-
- <h2>My Take On It</h2>
- <p>Nameko as it stands has all the right principles for building a platform
- out of small services and it's probably the best Open Source solution for
- this problem in the Python world so far.</p>
- <p>It's a bit disappointing that Python's async story is so diverging between
- different Python versions and frameworks, but eventlet and gevent are by
- far the cleanest and most practical implementations, so for most intents
- and purposes the eventlet base in nameko is probably the best you can
- currently get for async IO. Fear not though, Nameko 2.0 now also runs on
- Python3.</p>
- <p>If you haven't tried this sort of service setup yet, you might want to
- give Nameko a try.</p>
|