Make worker-multiplier sane in container environments

Resync charm-helpers to pickup the capped worker-multiplier
changes when deploying in containers.

Drop the default value for worker-multiplier of 2.0; this
is now handled from within the codebase rather than via a
default configuration value, reflecting the differing
behaviours between container and non-container deployments.

Change-Id: Idb10dc73b357cc3ed774365bae92962b8dfd0bfb
Closes-Bug: 1665270
This commit is contained in:
James Page 2017-04-27 09:34:00 +01:00
parent 4efc95a857
commit d6165b16d5
8 changed files with 146 additions and 75 deletions

View File

@ -257,11 +257,13 @@ options:
to run. Supported modules currently include os, ssh, apache and mysql. to run. Supported modules currently include os, ssh, apache and mysql.
worker-multiplier: worker-multiplier:
type: float type: float
default: 2.0 default:
description: | description: |
The CPU core multiplier to use when configuring worker processes for The CPU core multiplier to use when configuring worker processes for
neutron and nova-metadata-api. By default, the number of workers for this service. By default, the number of workers for each daemon is
each daemon is set to twice the number of CPU cores a service unit has. set to twice the number of CPU cores a service unit has. When deployed
in a LXD container, this default value will be capped to 4 workers
unless this configuration option is set.
aa-profile-mode: aa-profile-mode:
type: string type: string
default: 'disable' default: 'disable'

View File

@ -239,6 +239,16 @@ def format_ipv6_addr(address):
return None return None
def is_ipv6_disabled():
try:
result = subprocess.check_output(
['sysctl', 'net.ipv6.conf.all.disable_ipv6'],
stderr=subprocess.STDOUT)
return "net.ipv6.conf.all.disable_ipv6 = 1" in result
except subprocess.CalledProcessError:
return True
def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False, def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
fatal=True, exc_list=None): fatal=True, exc_list=None):
"""Return the assigned IP address for a given interface, if any. """Return the assigned IP address for a given interface, if any.
@ -544,31 +554,38 @@ def assert_charm_supports_ipv6():
"versions less than Trusty 14.04") "versions less than Trusty 14.04")
def get_relation_ip(interface, config_override=None): def get_relation_ip(interface, cidr_network=None):
"""Return this unit's IP for the given relation. """Return this unit's IP for the given interface.
Allow for an arbitrary interface to use with network-get to select an IP. Allow for an arbitrary interface to use with network-get to select an IP.
Handle all address selection options including configuration parameter Handle all address selection options including passed cidr network and
override and IPv6. IPv6.
Usage: get_relation_ip('amqp', config_override='access-network') Usage: get_relation_ip('amqp', cidr_network='10.0.0.0/8')
@param interface: string name of the relation. @param interface: string name of the relation.
@param config_override: string name of the config option for network @param cidr_network: string CIDR Network to select an address from.
override. Supports legacy network override configuration parameters.
@raises Exception if prefer-ipv6 is configured but IPv6 unsupported. @raises Exception if prefer-ipv6 is configured but IPv6 unsupported.
@returns IPv6 or IPv4 address @returns IPv6 or IPv4 address
""" """
# Select the interface address first
# For possible use as a fallback bellow with get_address_in_network
try:
# Get the interface specific IP
address = network_get_primary_address(interface)
except NotImplementedError:
# If network-get is not available
address = get_host_ip(unit_get('private-address'))
fallback = get_host_ip(unit_get('private-address'))
if config('prefer-ipv6'): if config('prefer-ipv6'):
# Currently IPv6 has priority, eventually we want IPv6 to just be
# another network space.
assert_charm_supports_ipv6() assert_charm_supports_ipv6()
return get_ipv6_addr()[0] return get_ipv6_addr()[0]
elif config_override and config(config_override): elif cidr_network:
return get_address_in_network(config(config_override), # If a specific CIDR network is passed get the address from that
fallback) # network.
else: return get_address_in_network(cidr_network, address)
try:
return network_get_primary_address(interface) # Return the interface address
except NotImplementedError: return address
return fallback

View File

@ -547,7 +547,7 @@ class OpenStackAmuletUtils(AmuletUtils):
"""Create the specified instance.""" """Create the specified instance."""
self.log.debug('Creating instance ' self.log.debug('Creating instance '
'({}|{}|{})'.format(instance_name, image_name, flavor)) '({}|{}|{})'.format(instance_name, image_name, flavor))
image = nova.images.find(name=image_name) image = nova.glance.find_image(image_name)
flavor = nova.flavors.find(name=flavor) flavor = nova.flavors.find(name=flavor)
instance = nova.servers.create(name=instance_name, image=image, instance = nova.servers.create(name=instance_name, image=image,
flavor=flavor) flavor=flavor)

View File

@ -89,6 +89,7 @@ from charmhelpers.contrib.network.ip import (
format_ipv6_addr, format_ipv6_addr,
is_address_in_network, is_address_in_network,
is_bridge_member, is_bridge_member,
is_ipv6_disabled,
) )
from charmhelpers.contrib.openstack.utils import ( from charmhelpers.contrib.openstack.utils import (
config_flags_parser, config_flags_parser,
@ -110,6 +111,7 @@ except ImportError:
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt' CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
ADDRESS_TYPES = ['admin', 'internal', 'public'] ADDRESS_TYPES = ['admin', 'internal', 'public']
HAPROXY_RUN_DIR = '/var/run/haproxy/'
def ensure_packages(packages): def ensure_packages(packages):
@ -535,6 +537,8 @@ class HAProxyContext(OSContextGenerator):
"""Provides half a context for the haproxy template, which describes """Provides half a context for the haproxy template, which describes
all peers to be included in the cluster. Each charm needs to include all peers to be included in the cluster. Each charm needs to include
its own context generator that describes the port mapping. its own context generator that describes the port mapping.
:side effect: mkdir is called on HAPROXY_RUN_DIR
""" """
interfaces = ['cluster'] interfaces = ['cluster']
@ -542,6 +546,8 @@ class HAProxyContext(OSContextGenerator):
self.singlenode_mode = singlenode_mode self.singlenode_mode = singlenode_mode
def __call__(self): def __call__(self):
if not os.path.isdir(HAPROXY_RUN_DIR):
mkdir(path=HAPROXY_RUN_DIR)
if not relation_ids('cluster') and not self.singlenode_mode: if not relation_ids('cluster') and not self.singlenode_mode:
return {} return {}
@ -1226,31 +1232,50 @@ MAX_DEFAULT_WORKERS = 4
DEFAULT_MULTIPLIER = 2 DEFAULT_MULTIPLIER = 2
def _calculate_workers():
'''
Determine the number of worker processes based on the CPU
count of the unit containing the application.
Workers will be limited to MAX_DEFAULT_WORKERS in
container environments where no worker-multipler configuration
option been set.
@returns int: number of worker processes to use
'''
multiplier = config('worker-multiplier') or DEFAULT_MULTIPLIER
count = int(_num_cpus() * multiplier)
if multiplier > 0 and count == 0:
count = 1
if config('worker-multiplier') is None and is_container():
# NOTE(jamespage): Limit unconfigured worker-multiplier
# to MAX_DEFAULT_WORKERS to avoid insane
# worker configuration in LXD containers
# on large servers
# Reference: https://pad.lv/1665270
count = min(count, MAX_DEFAULT_WORKERS)
return count
def _num_cpus():
'''
Compatibility wrapper for calculating the number of CPU's
a unit has.
@returns: int: number of CPU cores detected
'''
try:
return psutil.cpu_count()
except AttributeError:
return psutil.NUM_CPUS
class WorkerConfigContext(OSContextGenerator): class WorkerConfigContext(OSContextGenerator):
@property
def num_cpus(self):
# NOTE: use cpu_count if present (16.04 support)
if hasattr(psutil, 'cpu_count'):
return psutil.cpu_count()
else:
return psutil.NUM_CPUS
def __call__(self): def __call__(self):
multiplier = config('worker-multiplier') or DEFAULT_MULTIPLIER ctxt = {"workers": _calculate_workers()}
count = int(self.num_cpus * multiplier)
if multiplier > 0 and count == 0:
count = 1
if config('worker-multiplier') is None and is_container():
# NOTE(jamespage): Limit unconfigured worker-multiplier
# to MAX_DEFAULT_WORKERS to avoid insane
# worker configuration in LXD containers
# on large servers
# Reference: https://pad.lv/1665270
count = min(count, MAX_DEFAULT_WORKERS)
ctxt = {"workers": count}
return ctxt return ctxt
@ -1258,7 +1283,7 @@ class WSGIWorkerConfigContext(WorkerConfigContext):
def __init__(self, name=None, script=None, admin_script=None, def __init__(self, name=None, script=None, admin_script=None,
public_script=None, process_weight=1.00, public_script=None, process_weight=1.00,
admin_process_weight=0.75, public_process_weight=0.25): admin_process_weight=0.25, public_process_weight=0.75):
self.service_name = name self.service_name = name
self.user = name self.user = name
self.group = name self.group = name
@ -1270,8 +1295,7 @@ class WSGIWorkerConfigContext(WorkerConfigContext):
self.public_process_weight = public_process_weight self.public_process_weight = public_process_weight
def __call__(self): def __call__(self):
multiplier = config('worker-multiplier') or 1 total_processes = _calculate_workers()
total_processes = self.num_cpus * multiplier
ctxt = { ctxt = {
"service_name": self.service_name, "service_name": self.service_name,
"user": self.user, "user": self.user,
@ -1602,7 +1626,7 @@ class MemcacheContext(OSContextGenerator):
"""Memcache context """Memcache context
This context provides options for configuring a local memcache client and This context provides options for configuring a local memcache client and
server server for both IPv4 and IPv6
""" """
def __init__(self, package=None): def __init__(self, package=None):
@ -1620,13 +1644,24 @@ class MemcacheContext(OSContextGenerator):
# Trusty version of memcached does not support ::1 as a listen # Trusty version of memcached does not support ::1 as a listen
# address so use host file entry instead # address so use host file entry instead
release = lsb_release()['DISTRIB_CODENAME'].lower() release = lsb_release()['DISTRIB_CODENAME'].lower()
if CompareHostReleases(release) > 'trusty': if is_ipv6_disabled():
ctxt['memcache_server'] = '::1' if CompareHostReleases(release) > 'trusty':
ctxt['memcache_server'] = '127.0.0.1'
else:
ctxt['memcache_server'] = 'localhost'
ctxt['memcache_server_formatted'] = '127.0.0.1'
ctxt['memcache_port'] = '11211'
ctxt['memcache_url'] = '{}:{}'.format(
ctxt['memcache_server_formatted'],
ctxt['memcache_port'])
else: else:
ctxt['memcache_server'] = 'ip6-localhost' if CompareHostReleases(release) > 'trusty':
ctxt['memcache_server_formatted'] = '[::1]' ctxt['memcache_server'] = '::1'
ctxt['memcache_port'] = '11211' else:
ctxt['memcache_url'] = 'inet6:{}:{}'.format( ctxt['memcache_server'] = 'ip6-localhost'
ctxt['memcache_server_formatted'], ctxt['memcache_server_formatted'] = '[::1]'
ctxt['memcache_port']) ctxt['memcache_port'] = '11211'
ctxt['memcache_url'] = 'inet6:{}:{}'.format(
ctxt['memcache_server_formatted'],
ctxt['memcache_port'])
return ctxt return ctxt

View File

@ -191,7 +191,7 @@ def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
sysv_file = os.path.join(initd_dir, service_name) sysv_file = os.path.join(initd_dir, service_name)
if init_is_systemd(): if init_is_systemd():
service('disable', service_name) service('mask', service_name)
elif os.path.exists(upstart_file): elif os.path.exists(upstart_file):
override_path = os.path.join( override_path = os.path.join(
init_dir, '{}.override'.format(service_name)) init_dir, '{}.override'.format(service_name))
@ -224,7 +224,7 @@ def service_resume(service_name, init_dir="/etc/init",
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
sysv_file = os.path.join(initd_dir, service_name) sysv_file = os.path.join(initd_dir, service_name)
if init_is_systemd(): if init_is_systemd():
service('enable', service_name) service('unmask', service_name)
elif os.path.exists(upstart_file): elif os.path.exists(upstart_file):
override_path = os.path.join( override_path = os.path.join(
init_dir, '{}.override'.format(service_name)) init_dir, '{}.override'.format(service_name))

View File

@ -239,6 +239,16 @@ def format_ipv6_addr(address):
return None return None
def is_ipv6_disabled():
try:
result = subprocess.check_output(
['sysctl', 'net.ipv6.conf.all.disable_ipv6'],
stderr=subprocess.STDOUT)
return "net.ipv6.conf.all.disable_ipv6 = 1" in result
except subprocess.CalledProcessError:
return True
def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False, def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
fatal=True, exc_list=None): fatal=True, exc_list=None):
"""Return the assigned IP address for a given interface, if any. """Return the assigned IP address for a given interface, if any.
@ -544,31 +554,38 @@ def assert_charm_supports_ipv6():
"versions less than Trusty 14.04") "versions less than Trusty 14.04")
def get_relation_ip(interface, config_override=None): def get_relation_ip(interface, cidr_network=None):
"""Return this unit's IP for the given relation. """Return this unit's IP for the given interface.
Allow for an arbitrary interface to use with network-get to select an IP. Allow for an arbitrary interface to use with network-get to select an IP.
Handle all address selection options including configuration parameter Handle all address selection options including passed cidr network and
override and IPv6. IPv6.
Usage: get_relation_ip('amqp', config_override='access-network') Usage: get_relation_ip('amqp', cidr_network='10.0.0.0/8')
@param interface: string name of the relation. @param interface: string name of the relation.
@param config_override: string name of the config option for network @param cidr_network: string CIDR Network to select an address from.
override. Supports legacy network override configuration parameters.
@raises Exception if prefer-ipv6 is configured but IPv6 unsupported. @raises Exception if prefer-ipv6 is configured but IPv6 unsupported.
@returns IPv6 or IPv4 address @returns IPv6 or IPv4 address
""" """
# Select the interface address first
# For possible use as a fallback bellow with get_address_in_network
try:
# Get the interface specific IP
address = network_get_primary_address(interface)
except NotImplementedError:
# If network-get is not available
address = get_host_ip(unit_get('private-address'))
fallback = get_host_ip(unit_get('private-address'))
if config('prefer-ipv6'): if config('prefer-ipv6'):
# Currently IPv6 has priority, eventually we want IPv6 to just be
# another network space.
assert_charm_supports_ipv6() assert_charm_supports_ipv6()
return get_ipv6_addr()[0] return get_ipv6_addr()[0]
elif config_override and config(config_override): elif cidr_network:
return get_address_in_network(config(config_override), # If a specific CIDR network is passed get the address from that
fallback) # network.
else: return get_address_in_network(cidr_network, address)
try:
return network_get_primary_address(interface) # Return the interface address
except NotImplementedError: return address
return fallback

View File

@ -547,7 +547,7 @@ class OpenStackAmuletUtils(AmuletUtils):
"""Create the specified instance.""" """Create the specified instance."""
self.log.debug('Creating instance ' self.log.debug('Creating instance '
'({}|{}|{})'.format(instance_name, image_name, flavor)) '({}|{}|{})'.format(instance_name, image_name, flavor))
image = nova.images.find(name=image_name) image = nova.glance.find_image(image_name)
flavor = nova.flavors.find(name=flavor) flavor = nova.flavors.find(name=flavor)
instance = nova.servers.create(name=instance_name, image=image, instance = nova.servers.create(name=instance_name, image=image,
flavor=flavor) flavor=flavor)

View File

@ -191,7 +191,7 @@ def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
sysv_file = os.path.join(initd_dir, service_name) sysv_file = os.path.join(initd_dir, service_name)
if init_is_systemd(): if init_is_systemd():
service('disable', service_name) service('mask', service_name)
elif os.path.exists(upstart_file): elif os.path.exists(upstart_file):
override_path = os.path.join( override_path = os.path.join(
init_dir, '{}.override'.format(service_name)) init_dir, '{}.override'.format(service_name))
@ -224,7 +224,7 @@ def service_resume(service_name, init_dir="/etc/init",
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
sysv_file = os.path.join(initd_dir, service_name) sysv_file = os.path.join(initd_dir, service_name)
if init_is_systemd(): if init_is_systemd():
service('enable', service_name) service('unmask', service_name)
elif os.path.exists(upstart_file): elif os.path.exists(upstart_file):
override_path = os.path.join( override_path = os.path.join(
init_dir, '{}.override'.format(service_name)) init_dir, '{}.override'.format(service_name))