Use werkzeug to run the developement API server

wsgi.simple_server in a mono threaded process that can handle only
5 requests at a time.

Even the doc recommands to setup Ceilometer through an other WSGI services
like Apache 'mod_wsgi', we can provide a better testing API server.

So this patch changes the default HTTP server to the werkzeug one with
a autodiscovery of number of workers that we can use.

The client queue of werkzeug is 128, so on a 4 cpus machine, ceilometer-api
can now handle 512 connections instead of 5.

Also the change adds references of how to deploy pecan application in
the documentation.

The config option enable_reverse_dns_lookup can be safely removed,
because werkzeug doesn't do any reverse dns lookup.

DocImpact: configuration options changed:
enable_reverse_dns_lookup removed, api_workers added

Change-Id: If7450b393ea88bc185e5c82b706ace9c38ce350e
This commit is contained in:
Mehdi Abaakouk 2015-02-10 09:41:05 +01:00 committed by Mehdi Abaakouk
parent fa27b13390
commit 09a2f0994f
6 changed files with 28 additions and 63 deletions

View File

@ -27,12 +27,6 @@ OPTS = [
default='0.0.0.0', default='0.0.0.0',
help='The listen IP for the ceilometer API server.', help='The listen IP for the ceilometer API server.',
), ),
cfg.BoolOpt('enable_reverse_dns_lookup',
default=False,
help=('Set it to False if your environment does not need '
'or have a DNS server, otherwise it will delay the '
'response from the API.')
),
] ]
CONF = cfg.CONF CONF = cfg.CONF

View File

@ -15,19 +15,20 @@
import logging import logging
import os import os
import socket
from wsgiref import simple_server
import netaddr
from oslo_config import cfg from oslo_config import cfg
from paste import deploy from paste import deploy
import pecan import pecan
from werkzeug import serving
from ceilometer.api import config as api_config from ceilometer.api import config as api_config
from ceilometer.api import hooks from ceilometer.api import hooks
from ceilometer.api import middleware from ceilometer.api import middleware
from ceilometer.i18n import _ from ceilometer.i18n import _
from ceilometer.i18n import _LW
from ceilometer.openstack.common import log from ceilometer.openstack.common import log
from ceilometer import service
from ceilometer import storage from ceilometer import storage
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -39,6 +40,8 @@ OPTS = [
default="api_paste.ini", default="api_paste.ini",
help="Configuration file for WSGI definition of API." help="Configuration file for WSGI definition of API."
), ),
cfg.IntOpt('api_workers', default=1,
help='Number of workers for Ceilometer API server.'),
] ]
API_OPTS = [ API_OPTS = [
@ -77,9 +80,16 @@ def setup_app(pecan_config=None, extra_hooks=None):
cfg.set_defaults(API_OPTS, pecan_debug=CONF.debug) cfg.set_defaults(API_OPTS, pecan_debug=CONF.debug)
# NOTE(sileht): pecan debug won't work in multi-process environment
pecan_debug = CONF.api.pecan_debug
if service.get_workers('api') != 1 and pecan_debug:
pecan_debug = False
LOG.warning(_LW('pecan_debug cannot be enabled, if workers is > 1, '
'the value is overrided with False'))
app = pecan.make_app( app = pecan.make_app(
pecan_config.app.root, pecan_config.app.root,
debug=CONF.api.pecan_debug, debug=pecan_debug,
force_canonical=getattr(pecan_config.app, 'force_canonical', True), force_canonical=getattr(pecan_config.app, 'force_canonical', True),
hooks=app_hooks, hooks=app_hooks,
wrap_app=middleware.ParsableErrorMiddleware, wrap_app=middleware.ParsableErrorMiddleware,
@ -106,36 +116,6 @@ class VersionSelectorApplication(object):
return self.v2(environ, start_response) return self.v2(environ, start_response)
def get_server_cls(host):
"""Return an appropriate WSGI server class base on provided host
:param host: The listen host for the ceilometer API server.
"""
server_cls = simple_server.WSGIServer
if netaddr.valid_ipv6(host):
# NOTE(dzyu) make sure use IPv6 sockets if host is in IPv6 pattern
if getattr(server_cls, 'address_family') == socket.AF_INET:
class ipv6_server_cls(server_cls):
address_family = socket.AF_INET6
return ipv6_server_cls
return server_cls
def get_handler_cls():
cls = simple_server.WSGIRequestHandler
# old-style class doesn't support super
class CeilometerHandler(cls, object):
def address_string(self):
if cfg.CONF.api.enable_reverse_dns_lookup:
return super(CeilometerHandler, self).address_string()
else:
# disable reverse dns lookup, directly return ip address
return self.client_address[0]
return CeilometerHandler
def load_app(): def load_app():
# Build the WSGI app # Build the WSGI app
cfg_file = None cfg_file = None
@ -155,10 +135,6 @@ def build_server():
app = load_app() app = load_app()
# Create the WSGI server and start it # Create the WSGI server and start it
host, port = cfg.CONF.api.host, cfg.CONF.api.port host, port = cfg.CONF.api.host, cfg.CONF.api.port
server_cls = get_server_cls(host)
srv = simple_server.make_server(host, port, app,
server_cls, get_handler_cls())
LOG.info(_('Starting server in PID %s') % os.getpid()) LOG.info(_('Starting server in PID %s') % os.getpid())
LOG.info(_("Configuration:")) LOG.info(_("Configuration:"))
@ -172,7 +148,9 @@ def build_server():
LOG.info(_("serving on http://%(host)s:%(port)s") % ( LOG.info(_("serving on http://%(host)s:%(port)s") % (
{'host': host, 'port': port})) {'host': host, 'port': port}))
return srv workers = service.get_workers('api')
serving.run_simple(cfg.CONF.api.host, cfg.CONF.api.port,
app, processes=workers)
def app_factory(global_config, **local_conf): def app_factory(global_config, **local_conf):

View File

@ -20,5 +20,4 @@ from ceilometer import service
def main(): def main():
service.prepare_service() service.prepare_service()
srv = app.build_server() app.build_server()
srv.serve_forever()

View File

@ -15,8 +15,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import socket
import mock import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_config import fixture as fixture_config from oslo_config import fixture as fixture_config
@ -31,19 +29,6 @@ class TestApp(base.BaseTestCase):
super(TestApp, self).setUp() super(TestApp, self).setUp()
self.CONF = self.useFixture(fixture_config.Config()).conf self.CONF = self.useFixture(fixture_config.Config()).conf
def test_WSGI_address_family(self):
self.CONF.set_override('host', '::', group='api')
server_cls = app.get_server_cls(cfg.CONF.api.host)
self.assertEqual(server_cls.address_family, socket.AF_INET6)
self.CONF.set_override('host', '127.0.0.1', group='api')
server_cls = app.get_server_cls(cfg.CONF.api.host)
self.assertEqual(server_cls.address_family, socket.AF_INET)
self.CONF.set_override('host', 'ddddd', group='api')
server_cls = app.get_server_cls(cfg.CONF.api.host)
self.assertEqual(server_cls.address_family, socket.AF_INET)
def test_api_paste_file_not_exist(self): def test_api_paste_file_not_exist(self):
self.CONF.set_override('api_paste_config', 'non-existent-file') self.CONF.set_override('api_paste_config', 'non-existent-file')
with mock.patch.object(self.CONF, 'find_file') as ff: with mock.patch.object(self.CONF, 'find_file') as ff:
@ -55,10 +40,11 @@ class TestApp(base.BaseTestCase):
@mock.patch('ceilometer.api.hooks.PipelineHook', mock.MagicMock()) @mock.patch('ceilometer.api.hooks.PipelineHook', mock.MagicMock())
@mock.patch('pecan.make_app') @mock.patch('pecan.make_app')
def test_pecan_debug(self, mocked): def test_pecan_debug(self, mocked):
def _check_pecan_debug(g_debug, p_debug, expected): def _check_pecan_debug(g_debug, p_debug, expected, workers=1):
self.CONF.set_override('debug', g_debug) self.CONF.set_override('debug', g_debug)
if p_debug is not None: if p_debug is not None:
self.CONF.set_override('pecan_debug', p_debug, group='api') self.CONF.set_override('pecan_debug', p_debug, group='api')
self.CONF.set_override('api_workers', workers)
app.setup_app() app.setup_app()
args, kwargs = mocked.call_args args, kwargs = mocked.call_args
self.assertEqual(expected, kwargs.get('debug')) self.assertEqual(expected, kwargs.get('debug'))
@ -67,3 +53,7 @@ class TestApp(base.BaseTestCase):
_check_pecan_debug(g_debug=True, p_debug=None, expected=True) _check_pecan_debug(g_debug=True, p_debug=None, expected=True)
_check_pecan_debug(g_debug=True, p_debug=False, expected=False) _check_pecan_debug(g_debug=True, p_debug=False, expected=False)
_check_pecan_debug(g_debug=False, p_debug=True, expected=True) _check_pecan_debug(g_debug=False, p_debug=True, expected=True)
_check_pecan_debug(g_debug=True, p_debug=None, expected=False,
workers=5)
_check_pecan_debug(g_debug=False, p_debug=True, expected=False,
workers=5)

View File

@ -63,3 +63,6 @@ multiple processes, there is no way to set debug mode in the multiprocessing
case. To allow multiple processes the DebugMiddleware may be turned off by case. To allow multiple processes the DebugMiddleware may be turned off by
setting ``pecan_debug`` to ``False`` in the ``api`` section of setting ``pecan_debug`` to ``False`` in the ``api`` section of
``ceilometer.conf``. ``ceilometer.conf``.
For other WSGI setup you can refer to the `pecan deployement`_ documentation.
.. _`pecan deployement`: http://pecan.readthedocs.org/en/latest/deployment.html#deployment

View File

@ -44,5 +44,6 @@ SQLAlchemy>=0.9.7,<=0.9.99
sqlalchemy-migrate>=0.9.1,!=0.9.2 sqlalchemy-migrate>=0.9.1,!=0.9.2
stevedore>=1.1.0 # Apache-2.0 stevedore>=1.1.0 # Apache-2.0
tooz>=0.3 # Apache-2.0 tooz>=0.3 # Apache-2.0
werkzeug>=0.7 # BSD License
WebOb>=1.2.3 WebOb>=1.2.3
WSME>=0.6 WSME>=0.6