diff --git a/environments/python/README.md b/environments/python/README.md index 69e87394..4f668c11 100644 --- a/environments/python/README.md +++ b/environments/python/README.md @@ -43,3 +43,23 @@ Or, if you already have an environment, you can update its image: After this, fission functions that have the env parameter set to the same environment name as this command will use this environment. + +## Web Server Framework + +Python environment build and start a WSGI server, to support high HTTP +traffic. As it is applied in different use cases, this provides two server +frameworks: `bjoern` and `gevent`. They all support high concurrency request. + +`bjoern` has good performance on RPS, to be ideal for most light resource +utilization cases. + +`gevent` is a good supplement because of its internal multi-threads. It +supports heavy resource load functions, with well distribution of response +time. + +Python environment pod remains `bjoern` framework by default. And it runs `gevent` +framework by setting the container environment `WSGI_FRAMEWORK` value to `GEVENT`. + +The environment value is configured normally in two ways. One way is to set in Dockerfile +and build it into image. The other way is to set in Kubernetes deployment spec during +pod running and restart it. diff --git a/environments/python/requirements.txt b/environments/python/requirements.txt index 8f437f24..04d8f591 100644 --- a/environments/python/requirements.txt +++ b/environments/python/requirements.txt @@ -5,3 +5,4 @@ python-dateutil requests==2.7.0 redis hiredis +gevent diff --git a/environments/python/server.py b/environments/python/server.py index cf4b216d..1dfd48f3 100644 --- a/environments/python/server.py +++ b/environments/python/server.py @@ -5,99 +5,107 @@ import sys import imp import os import bjoern - +from gevent.pywsgi import WSGIServer from flask import Flask, request, abort, g -app = Flask(__name__) +class FuncApp(Flask): + def __init__(self, name, loglevel = logging.DEBUG): + super(FuncApp, self).__init__(name) -userfunc = None + # init the class members + self.userfunc = None + self.root = logging.getLogger() + self.ch = logging.StreamHandler(sys.stdout) -@app.route('/specialize', methods=['POST']) -def load(): - global userfunc - # load user function from codepath - codepath = '/userfunc/user' - userfunc = (imp.load_source('user', codepath)).main - return "" + # + # Logging setup. TODO: Loglevel hard-coded for now. We could allow + # functions/routes to override this somehow; or we could create + # separate dev vs. prod environments. + # + self.root.setLevel(loglevel) + self.ch.setLevel(loglevel) + self.ch.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + self.logger.addHandler(self.ch) -@app.route('/v2/specialize', methods=['POST']) -def loadv2(): - global userfunc - body = request.get_json() - filepath = body['filepath'] - handler = body['functionName'] + # + # Register the routers + # + @self.route('/specialize', methods=['POST']) + def load(): + # load user function from codepath + codepath = '/userfunc/user' + self.userfunc = (imp.load_source('user', codepath)).main + return "" - # The value of "functionName" is consist of `.`. - moduleName, funcName = handler.split(".") - - # check whether the destination is a directory or a file - if os.path.isdir(filepath): - # add package directory path into module search path - sys.path.append(filepath) + @self.route('/v2/specialize', methods=['POST']) + def loadv2(): + body = request.get_json() + filepath = body['filepath'] + handler = body['functionName'] - # find module from package path we append previously. - # Python will try to find module from the same name file under - # the package directory. If search is successful, the return - # value is a 3-element tuple; otherwise, an exception "ImportError" - # is raised. - # Second parameter of find_module enforces python to find same - # name module from the given list of directories to prevent name - # confliction with built-in modules. - f, path, desc = imp.find_module(moduleName, [filepath]) + # The value of "functionName" is consist of `.`. + moduleName, funcName = handler.split(".") - # load module - # Return module object is the load is successful; otherwise, - # an exception is raised. - try: - mod = imp.load_module(moduleName, f, path, desc) - finally: - if f: - f.close() - else: - # load source from destination python file - mod = imp.load_source(moduleName, filepath) + # check whether the destination is a directory or a file + if os.path.isdir(filepath): + # add package directory path into module search path + sys.path.append(filepath) + + # find module from package path we append previously. + # Python will try to find module from the same name file under + # the package directory. If search is successful, the return + # value is a 3-element tuple; otherwise, an exception "ImportError" + # is raised. + # Second parameter of find_module enforces python to find same + # name module from the given list of directories to prevent name + # confliction with built-in modules. + f, path, desc = imp.find_module(moduleName, [filepath]) + + # load module + # Return module object is the load is successful; otherwise, + # an exception is raised. + try: + mod = imp.load_module(moduleName, f, path, desc) + finally: + if f: + f.close() + else: + # load source from destination python file + mod = imp.load_source(moduleName, filepath) + + # load user function from module + self.userfunc = getattr(mod, funcName) + + return "" - # load user function from module - userfunc = getattr(mod, funcName) + @self.route('/healthz', methods=['GET']) + def healthz(): + return "", 200 - return "" + @self.route('/', methods=['GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']) + def f(): + if self.userfunc == None: + print("Generic container: no requests supported") + abort(500) + # + # Customizing the request context + # + # If you want to pass something to the function, you can add it to 'g': + # g.myKey = myValue + # And the user func can then access that (after doing a "from flask import g"). + # + return self.userfunc() -@app.route('/healthz', methods=['GET']) -def healthz(): - return "", 200 - -@app.route('/', methods=['GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']) -def f(): - if userfunc == None: - print("Generic container: no requests supported") - abort(500) - # - # Customizing the request context - # - # If you want to pass something to the function, you can add it to 'g': - # g.myKey = myValue - # And the user func can then access that (after doing a "from flask import g"). - # - return userfunc() - -# -# Logging setup. TODO: Loglevel hard-coded for now. We could allow -# functions/routes to override this somehow; or we could create -# separate dev vs. prod environments. -# -def setup_logger(loglevel): - global app - root = logging.getLogger() - root.setLevel(loglevel) - ch = logging.StreamHandler(sys.stdout) - ch.setLevel(loglevel) - ch.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) - app.logger.addHandler(ch) +app = FuncApp(__name__, logging.DEBUG) # # TODO: this starts the built-in server, which isn't the most # efficient. We should use something better. # -setup_logger(logging.DEBUG) -app.logger.info("Starting server") -bjoern.run(app, '0.0.0.0', 8888, reuse_port=True) +if os.environ.get("WSGI_FRAMEWORK") == "GEVENT": + app.logger.info("Starting gevent based server") + svc = WSGIServer(('0.0.0.0', 8888), app) + svc.serve_forever() +else: + app.logger.info("Starting bjoern based server") + bjoern.run(app, '0.0.0.0', 8888, reuse_port=True)