Use uWSGI for python runtime

Make python runtime more production-ready. Actually, the docker image
has been built and used in Qinling gate job.

Change-Id: Ie7a0ae19042acecc1cfcbb2ac13fa019ca5735a9
This commit is contained in:
Lingxian Kong 2018-03-01 14:53:03 +13:00
parent c52b9dbbf7
commit 5615dd55da
8 changed files with 52 additions and 95 deletions

View File

@ -4,15 +4,15 @@
pbr!=2.1.0,>=2.0.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0
Babel!=2.4.0,>=2.3.4 # BSD Babel!=2.4.0,>=2.3.4 # BSD
keystoneauth1>=3.3.0 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0
keystonemiddleware>=4.17.0 # Apache-2.0 keystonemiddleware>=4.17.0 # Apache-2.0
oslo.concurrency>=3.20.0 # Apache-2.0 oslo.concurrency>=3.25.0 # Apache-2.0
oslo.config>=5.1.0 # Apache-2.0 oslo.config>=5.1.0 # Apache-2.0
oslo.db>=4.27.0 # Apache-2.0 oslo.db>=4.27.0 # Apache-2.0
oslo.messaging>=5.29.0 # Apache-2.0 oslo.messaging>=5.29.0 # Apache-2.0
oslo.policy>=1.30.0 # Apache-2.0 oslo.policy>=1.30.0 # Apache-2.0
oslo.utils>=3.33.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0
oslo.log>=3.30.0 # Apache-2.0 oslo.log>=3.36.0 # Apache-2.0
oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
oslo.service!=1.28.1,>=1.24.0 # Apache-2.0 oslo.service!=1.28.1,>=1.24.0 # Apache-2.0
pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD
@ -22,7 +22,7 @@ SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT
sqlalchemy-migrate>=0.11.0 # Apache-2.0 sqlalchemy-migrate>=0.11.0 # Apache-2.0
stevedore>=1.20.0 # Apache-2.0 stevedore>=1.20.0 # Apache-2.0
WSME>=0.8.0 # MIT WSME>=0.8.0 # MIT
kubernetes>=4.0.0 # Apache-2.0 kubernetes>=4.0.0 # Apache-2.0
PyYAML>=3.10 # MIT PyYAML>=3.10 # MIT
python-swiftclient>=3.2.0 # Apache-2.0 python-swiftclient>=3.2.0 # Apache-2.0
croniter>=0.3.4 # MIT License croniter>=0.3.4 # MIT License
@ -30,4 +30,4 @@ python-dateutil>=2.4.2 # BSD
tenacity>=3.2.1 # Apache-2.0 tenacity>=3.2.1 # Apache-2.0
PyMySQL>=0.7.6 # MIT License PyMySQL>=0.7.6 # MIT License
etcd3gw>=0.2.0 # Apache-2.0 etcd3gw>=0.2.0 # Apache-2.0
cotyledon>=1.3.0 # Apache-2.0 cotyledon>=1.3.0 # Apache-2.0

View File

@ -6,12 +6,14 @@ RUN useradd -Ms /bin/bash qinling
RUN apt-get update && \ RUN apt-get update && \
apt-get -y install python-dev python-setuptools libffi-dev libxslt1-dev libxml2-dev libyaml-dev libssl-dev python-pip && \ apt-get -y install python-dev python-setuptools libffi-dev libxslt1-dev libxml2-dev libyaml-dev libssl-dev python-pip && \
pip install -U pip setuptools pip install -U pip setuptools uwsgi
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN pip install -r requirements.txt && \ RUN pip install -r requirements.txt && \
chmod 0750 custom-entrypoint.sh && \ chmod 0750 custom-entrypoint.sh && \
chown -R qinling:qinling /app mkdir -p /var/lock/qinling && \
chown -R qinling:qinling /app /var/lock/qinling
CMD ["/bin/bash", "custom-entrypoint.sh"] # uwsgi --http :9090 --uid qinling --wsgi-file server.py --callable app --master --processes 5 --threads 1
CMD ["/usr/local/bin/uwsgi", "--http", ":9090", "--uid", "qinling", "--wsgi-file", "server.py", "--callable", "app", "--master", "--processes", "5", "--threads", "1"]

View File

@ -1,22 +0,0 @@
# Qinling: Python Environment
This is the Python environment for Qinling.
It's a Docker image containing a Python 2.7 runtime, along with a
dynamic loader. A few common dependencies are included in the
requirements.txt file. End users need to provide their own dependencies
in their function packages through Qinling API or CLI.
## Rebuilding and pushing the image
You'll need access to a Docker registry to push the image, by default it's
docker hub. After modification, build a new image and upload to docker hub:
docker build -t USER/python-runtime . && docker push USER/python-runtime
## Using the image in Qinling
After the image is ready in docker hub, create a runtime in Qinling:
http POST http://127.0.0.1:7070/v1/runtimes name=python2.7 image=USER/python-runtime

View File

@ -1,18 +0,0 @@
#!/usr/bin/env bash
# This is expected to run as root for setting the ulimits
set -e
# ensure increased ulimits - for nofile - for the runtime containers
# the limit on the number of files that a single process can have open at a time
ulimit -n 1024
# ensure increased ulimits - for nproc - for the runtime containers
# the limit on the number of processes
ulimit -u 128
# ensure increased ulimits - for file size - for the runtime containers
# the limit on the total file size that a single process can create, 30M
ulimit -f 61440
/sbin/setuser qinling python -u server.py

View File

@ -1,4 +1,5 @@
Flask>=0.10,!=0.11,<1.0 # BSD Flask>=0.10,!=0.11,<1.0 # BSD
oslo.concurrency>=3.25.0 # Apache-2.0
python-openstackclient>=3.3.0,!=3.10.0 # Apache-2.0 python-openstackclient>=3.3.0,!=3.10.0 # Apache-2.0
python-neutronclient>=6.3.0 # Apache-2.0 python-neutronclient>=6.3.0 # Apache-2.0
python-swiftclient>=3.2.0 # Apache-2.0 python-swiftclient>=3.2.0 # Apache-2.0

View File

@ -14,10 +14,10 @@
import importlib import importlib
import json import json
import logging
from multiprocessing import Manager from multiprocessing import Manager
from multiprocessing import Process from multiprocessing import Process
import os import os
import resource
import sys import sys
import time import time
import traceback import traceback
@ -27,35 +27,37 @@ from flask import request
from flask import Response from flask import Response
from keystoneauth1.identity import generic from keystoneauth1.identity import generic
from keystoneauth1 import session from keystoneauth1 import session
from oslo_concurrency import lockutils
import requests import requests
app = Flask(__name__) app = Flask(__name__)
downloaded = False
downloading = False
DOWNLOAD_ERROR = "Failed to download function package from %s, error: %s" DOWNLOAD_ERROR = "Failed to download function package from %s, error: %s"
INVOKE_ERROR = "Function execution failed because of too much resource " \ INVOKE_ERROR = "Function execution failed because of too much resource " \
"consumption" "consumption"
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)
def _print_trace(): def _print_trace():
exc_type, exc_value, exc_traceback = sys.exc_info() exc_type, exc_value, exc_traceback = sys.exc_info()
lines = traceback.format_exception(exc_type, exc_value, exc_traceback) lines = traceback.format_exception(exc_type, exc_value, exc_traceback)
print(''.join(line for line in lines)) print(''.join(line for line in lines))
def _set_ulimit():
"""Limit resources usage for the current process and/or its children.
Refer to https://docs.python.org/2.7/library/resource.html
"""
customized_limits = {
resource.RLIMIT_NOFILE: 1024,
resource.RLIMIT_NPROC: 128,
resource.RLIMIT_FSIZE: 61440
}
for t, soft in customized_limits.items():
_, hard = resource.getrlimit(t)
resource.setrlimit(t, (soft, hard))
def _get_responce(output, duration, logs, success, code): def _get_responce(output, duration, logs, success, code):
return Response( return Response(
response=json.dumps( response=json.dumps(
@ -71,8 +73,13 @@ def _get_responce(output, duration, logs, success, code):
) )
@lockutils.synchronized('download_function', external=True,
lock_path='/var/lock/qinling')
def _download_package(url, zip_file, token=None): def _download_package(url, zip_file, token=None):
app.logger.info('Downloading function, download_url:%s' % url) if os.path.isfile(zip_file):
return True, None
print('Downloading function, download_url:%s' % url)
headers = {} headers = {}
if token: if token:
@ -94,8 +101,7 @@ def _download_package(url, zip_file, token=None):
DOWNLOAD_ERROR % (url, str(e)), 0, '', False, 500 DOWNLOAD_ERROR % (url, str(e)), 0, '', False, 500
) )
app.logger.info('Downloaded function package to %s' % zip_file) print('Downloaded function package to %s' % zip_file)
return True, None return True, None
@ -135,9 +141,6 @@ def execute():
reason, e.g. unlimited memory allocation) reason, e.g. unlimited memory allocation)
- Deal with os error for process (e.g. Resource temporarily unavailable) - Deal with os error for process (e.g. Resource temporarily unavailable)
""" """
global downloading
global downloaded
params = request.get_json() or {} params = request.get_json() or {}
input = params.get('input') or {} input = params.get('input') or {}
execution_id = params['execution_id'] execution_id = params['execution_id']
@ -155,25 +158,20 @@ def execute():
if entry: if entry:
function_module, function_method = tuple(entry.rsplit('.', 1)) function_module, function_method = tuple(entry.rsplit('.', 1))
app.logger.info( print(
'Request received, request_id: %s, execution_id: %s, input: %s, ' 'Request received, request_id: %s, execution_id: %s, input: %s, '
'auth_url: %s' % 'auth_url: %s' %
(request_id, execution_id, input, auth_url) (request_id, execution_id, input, auth_url)
) )
while downloading: # Download function package if needed.
time.sleep(3) ret, resp = _download_package(
download_url,
if not downloading and not downloaded: zip_file,
downloading = True params.get('token')
)
ret, resp = _download_package(download_url, zip_file, if not ret:
params.get('token')) return resp
if not ret:
return resp
downloading = False
downloaded = True
# Provide an openstack session to user's function # Provide an openstack session to user's function
os_session = None os_session = None
@ -188,6 +186,9 @@ def execute():
os_session = session.Session(auth=auth, verify=False) os_session = session.Session(auth=auth, verify=False)
input.update({'context': {'os_session': os_session}}) input.update({'context': {'os_session': os_session}})
# Set resource limit
_set_ulimit()
manager = Manager() manager = Manager()
return_dict = manager.dict() return_dict = manager.dict()
return_dict['success'] = False return_dict['success'] = False
@ -223,10 +224,3 @@ def execute():
@app.route('/ping') @app.route('/ping')
def ping(): def ping():
return 'pong' return 'pong'
setup_logger(logging.DEBUG)
app.logger.info("Starting server")
# Just for testing purpose
app.run(host='0.0.0.0', port=9090, threaded=True)

View File

@ -25,5 +25,5 @@ except ImportError:
pass pass
setuptools.setup( setuptools.setup(
setup_requires=['pbr'], setup_requires=['pbr>=2.0.0'],
pbr=True) pbr=True)

View File

@ -4,14 +4,14 @@
hacking<0.13,>=0.12.0 # Apache-2.0 hacking<0.13,>=0.12.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0
sphinx>=1.6.2 # BSD sphinx!=1.6.6,>=1.6.2 # BSD
oslotest>=1.10.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD
testtools>=2.2.0 # MIT testtools>=2.2.0 # MIT
tempest>=17.1.0 # Apache-2.0 tempest>=17.1.0 # Apache-2.0
futurist>=1.2.0 # Apache-2.0 futurist>=1.2.0 # Apache-2.0
openstackdocstheme>=1.17.0 # Apache-2.0 openstackdocstheme>=1.18.1 # Apache-2.0
reno>=2.5.0 # Apache-2.0 reno>=2.5.0 # Apache-2.0
kubernetes>=4.0.0 # Apache-2.0 kubernetes>=4.0.0 # Apache-2.0