At the beginning of this chapter, we talked about the need to scale applications. So far, we have shown how to take a very simple client-server application and split it between two instances of Python.
However, scaling applications typically implies running multiple copies of an application in order to handle larger loads. Usually, some type of load balancer is needed. Let's explore how we can use Spring Python's remoting services mixed with some simple Python to create a multi-node version of our service.
The first step is to create another application context. We are going to spin up a different configuration of components, that is a different blue print, and plan to re-use our existing business code without making any changes.
from springpython.config import * from springpython.context import scope from springpython.remoting.pyro import * from simple_service import * class HappyBirthdayContext(PythonConfig): def __init__(self): PythonConfig.__init__(self) @Object(scope.PROTOTYPE) def target_service(self): return Service() @Object() def service_exporter1(self): exporter = PyroServiceExporter() exporter.service_name = "service" exporter.service = self.target_service() exporter.service_port = 7001 return exporter @Object() def service_exporter2(self): exporter = PyroServiceExporter() exporter.service_name = "service" exporter.service = self.target_service() exporter.service_port = 7002 return exporter
PyroServiceExporters
, each with a different port number. While it appears they are both using the target_service
, notice how we have changed the scope to PROTOTYPE
. This means each exporter gets its own instance. To run this new context, we need a different startup script.import logging from springpython.context import * from multi_server_ctx import * if __name__ == "__main__": logger = logging.getLogger("springpython.remoting") loggingLevel = logging.DEBUG logger.setLevel(loggingLevel) ch = logging.StreamHandler() ch.setLevel(loggingLevel) formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") ch.setFormatter(formatter) logger.addHandler(ch) ctx = ApplicationContext(HappyBirthdayContext())
In this script, we turned on some of Spring Python's built in logging, so we could see some of parts involved with starting up remoting. We can see two different Pyro daemon threads being launched, one for each of the ports. And then we see two instances of our Service
code being registered, one with each thread.
We now have two copies of our service ready and waiting to be called.
There are many different ways to code a dispatcher. For our example, let's pick a simple round robin algorithm where we cycle through a fixed list.
Let's create an application context that includes a round robin dispatcher that acts like our service, fed with a list of PyroProxyFactory
elements.
from springpython.config import * from springpython.remoting.pyro import * class RoundRobinDispatcher(object): def __init__(self, proxies): self.proxies = proxies self.counter = 0 def happy_birthday(self, parm): self.counter += 1 proxy = self.proxies[self.counter % 2] print "Calling %s" % proxy return proxy.happy_birthday(parm) class HappyBirthdayContext(PythonConfig): def __init__(self): PythonConfig.__init__(self) @Object def service_proxies(self): proxies = [] for port in ["7001", "7002"]: proxy = PyroProxyFactory() proxy.service_url ="PYROLOC://127.0.0.1:%s/service" % port proxies.append(proxy) return proxies @Object def service(self): return RoundRobinDispatcher(self.service_proxies())
We configured service_proxies
as an array of PyroProxyFactorys
. This lets us define a static list of connections.
The RoundRobinDispatcher
class is injected with the proxies, and mimics the API of our Happy Birthday service. Every call into the dispatcher increments the counter. It then picks one of the proxies to make the actual call.
Let's write the client script to use our round robin dispatcher.
from springpython.context import * from multi_client_ctx import * if __name__ == "__main__": ctx = ApplicationContext(HappyBirthdayContext()) s = ctx.get_object("service") print " ".join(s.happy_birthday("Greg")) print " ".join(s.happy_birthday("Greg")) print " ".join(s.happy_birthday("Greg"))
There is hardly an impact to the client. The only difference is the import statement, and the fact that we are calling the service multiple times. If the client was actually a GUI application with this tied to a button, there would be no difference.
Now let's run the client script.
As can be seen, each invocation shows which ProyProxyFactory
was called, and they clearly show switching back and forth between the two proxies registered. With each call smoothly integrated with a corresponding PyroServiceExporter
, we have scaled our application into a two-node version.