Python Environment: add gevent based WSGI server framework (#750)

* Python Environment: add gevent based WSGI server framework

This supply gevent based WSGI server framework to Python Environment, to
allow customer to run their functions under multi-threads.

* classify the flask based application

This set up the flask based application as individual class, to make
sure the consistent and integrity.

* Add instruction of WSGI server selection applied in Python Environment

* Python Environment starting logger: provide framework name
This commit is contained in:
xiekeyang
2018-06-26 15:50:06 +08:00
committed by Ta-Ching Chen
parent c863616b38
commit 71d8b769d5
3 changed files with 109 additions and 80 deletions
+20
View File
@@ -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.
+1
View File
@@ -5,3 +5,4 @@ python-dateutil
requests==2.7.0
redis
hiredis
gevent
+88 -80
View File
@@ -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 `<module-name>.<function-name>`.
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 `<module-name>.<function-name>`.
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)