Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add the option to minimize ovs l2 polling
This change adds the ability to monitor the local ovsdb for
interface changes so that the l2 agent can avoid unnecessary
polling.  Minimal changes are made to the agent so the risk
of breakage should be low.  Future efforts to make the agent
entirely event-based may be able to use OvsdbMonitor as a
starting point.

By default polling minimization is not done, and can only be
enabled by setting 'minimize_polling = True' in the ovs
section of the l2 agent's config file.

Closes-Bug: #1177973

Change-Id: I26c035b48a74df2148696869c5a9affae5ab3d27
  • Loading branch information
marun committed Oct 14, 2013
1 parent 010bd1f commit cb0df59
Show file tree
Hide file tree
Showing 13 changed files with 660 additions and 28 deletions.
3 changes: 3 additions & 0 deletions etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini
Expand Up @@ -87,6 +87,9 @@
# Agent's polling interval in seconds
# polling_interval = 2

# Minimize polling by monitoring ovsdb for interface changes
# minimize_polling = False

# (ListOpt) The types of tenant network tunnels supported by the agent.
# Setting this will enable tunneling support in the agent. This can be set to
# either 'gre' or 'vxlan'. If this is unset, it will default to [] and
Expand Down
2 changes: 2 additions & 0 deletions etc/neutron/rootwrap.d/openvswitch-plugin.filters
Expand Up @@ -13,6 +13,8 @@
# from the old mechanism
ovs-vsctl: CommandFilter, ovs-vsctl, root
ovs-ofctl: CommandFilter, ovs-ofctl, root
kill_ovsdb_client: KillFilter, root, /usr/bin/ovsdb-client, -9
ovsdb-client: CommandFilter, ovsdb-client, root
xe: CommandFilter, xe, root

# ip_lib
Expand Down
5 changes: 4 additions & 1 deletion neutron/agent/linux/async_process.py
Expand Up @@ -70,9 +70,12 @@ def __init__(self, cmd, root_helper=None, respawn_interval=None):
self.respawn_interval = respawn_interval
self._process = None
self._kill_event = None
self._reset_queues()
self._watchers = []

def _reset_queues(self):
self._stdout_lines = eventlet.queue.LightQueue()
self._stderr_lines = eventlet.queue.LightQueue()
self._watchers = []

def start(self):
"""Launch a process and monitor it asynchronously."""
Expand Down
114 changes: 114 additions & 0 deletions neutron/agent/linux/ovsdb_monitor.py
@@ -0,0 +1,114 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2013 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import eventlet

from neutron.agent.linux import async_process
from neutron.openstack.common import log as logging


LOG = logging.getLogger(__name__)


class OvsdbMonitor(async_process.AsyncProcess):
"""Manages an invocation of 'ovsdb-client monitor'."""

def __init__(self, table_name, columns=None, format=None,
root_helper=None, respawn_interval=None):

cmd = ['ovsdb-client', 'monitor', table_name]
if columns:
cmd.append(','.join(columns))
if format:
cmd.append('--format=%s' % format)
super(OvsdbMonitor, self).__init__(cmd,
root_helper=root_helper,
respawn_interval=respawn_interval)

def _read_stdout(self):
data = self._process.stdout.readline()
if not data:
return
#TODO(marun) The default root helper outputs exit errors to
# stdout due to bug #1219530. This check can be moved to
# _read_stderr once the error is correctly output to stderr.
if self.root_helper and self.root_helper in data:
self._stderr_lines.put(data)
LOG.error(_('Error received from ovsdb monitor: %s') % data)
else:
self._stdout_lines.put(data)
LOG.debug(_('Output received from ovsdb monitor: %s') % data)
return data

def _read_stderr(self):
data = super(OvsdbMonitor, self)._read_stderr()
if data:
LOG.error(_('Error received from ovsdb monitor: %s') % data)
# Do not return value to ensure that stderr output will
# stop the monitor.


class SimpleInterfaceMonitor(OvsdbMonitor):
"""Monitors the Interface table of the local host's ovsdb for changes.
The has_updates() method indicates whether changes to the ovsdb
Interface table have been detected since the monitor started or
since the previous access.
"""

def __init__(self, root_helper=None, respawn_interval=None):
super(SimpleInterfaceMonitor, self).__init__(
'Interface',
columns=['name'],
format='json',
root_helper=root_helper,
respawn_interval=respawn_interval,
)
self.data_received = False

@property
def is_active(self):
return (self.data_received and
self._kill_event and
not self._kill_event.ready())

@property
def has_updates(self):
"""Indicate whether the ovsdb Interface table has been updated.
True will be returned if the monitor process is not active.
This 'failing open' minimizes the risk of falsely indicating
the absense of updates at the expense of potential false
positives.
"""
return bool(list(self.iter_stdout())) or not self.is_active

def start(self, block=False, timeout=5):
super(SimpleInterfaceMonitor, self).start()
if block:
eventlet.timeout.Timeout(timeout)
while not self.is_active:
eventlet.sleep()

def _kill(self, *args, **kwargs):
self.data_received = False
super(SimpleInterfaceMonitor, self)._kill(*args, **kwargs)

def _read_stdout(self):
data = super(SimpleInterfaceMonitor, self)._read_stdout()
if data and not self.data_received:
self.data_received = True
return data
104 changes: 104 additions & 0 deletions neutron/agent/linux/polling.py
@@ -0,0 +1,104 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2013 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import contextlib

import eventlet

from neutron.agent.linux import ovsdb_monitor


@contextlib.contextmanager
def get_polling_manager(minimize_polling=False, root_helper=None):
if minimize_polling:
pm = InterfacePollingMinimizer(root_helper=root_helper)
pm.start()
else:
pm = AlwaysPoll()
try:
yield pm
finally:
if minimize_polling:
pm.stop()


class BasePollingManager(object):

def __init__(self):
self._force_polling = False
self._polling_completed = True

def force_polling(self):
self._force_polling = True

def polling_completed(self):
self._polling_completed = True

def _is_polling_required(self):
raise NotImplemented

@property
def is_polling_required(self):
# Always consume the updates to minimize polling.
polling_required = self._is_polling_required()

# Polling is required regardless of whether updates have been
# detected.
if self._force_polling:
self._force_polling = False
polling_required = True

# Polling is required if not yet done for previously detected
# updates.
if not self._polling_completed:
polling_required = True

if polling_required:
# Track whether polling has been completed to ensure that
# polling can be required until the caller indicates via a
# call to polling_completed() that polling has been
# successfully performed.
self._polling_completed = False

return polling_required


class AlwaysPoll(BasePollingManager):

@property
def is_polling_required(self):
return True


class InterfacePollingMinimizer(BasePollingManager):
"""Monitors ovsdb to determine when polling is required."""

def __init__(self, root_helper=None):
super(InterfacePollingMinimizer, self).__init__()
self._monitor = ovsdb_monitor.SimpleInterfaceMonitor(
root_helper=root_helper)

def start(self):
self._monitor.start()

def stop(self):
self._monitor.stop()

def _is_polling_required(self):
# Maximize the chances of update detection having a chance to
# collect output.
eventlet.sleep()
return self._monitor.has_updates
51 changes: 34 additions & 17 deletions neutron/plugins/openvswitch/agent/ovs_neutron_agent.py
Expand Up @@ -32,6 +32,7 @@
from neutron.agent import l2population_rpc
from neutron.agent.linux import ip_lib
from neutron.agent.linux import ovs_lib
from neutron.agent.linux import polling
from neutron.agent.linux import utils
from neutron.agent import rpc as agent_rpc
from neutron.agent import securitygroups_rpc as sg_rpc
Expand Down Expand Up @@ -156,7 +157,8 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
def __init__(self, integ_br, tun_br, local_ip,
bridge_mappings, root_helper,
polling_interval, tunnel_types=None,
veth_mtu=None, l2_population=False):
veth_mtu=None, l2_population=False,
minimize_polling=False):
'''Constructor.
:param integ_br: name of the integration bridge.
Expand All @@ -169,6 +171,8 @@ def __init__(self, integ_br, tun_br, local_ip,
the agent. If set, will automatically set enable_tunneling to
True.
:param veth_mtu: MTU size for veth interfaces.
:param minimize_polling: Optional, whether to minimize polling by
monitoring ovsdb for interface changes.
'''
self.veth_mtu = veth_mtu
self.root_helper = root_helper
Expand Down Expand Up @@ -199,6 +203,7 @@ def __init__(self, integ_br, tun_br, local_ip,
constants.TYPE_VXLAN: {}}

self.polling_interval = polling_interval
self.minimize_polling = minimize_polling

if tunnel_types:
self.enable_tunneling = True
Expand Down Expand Up @@ -1042,7 +1047,10 @@ def tunnel_sync(self):
resync = True
return resync

def rpc_loop(self):
def rpc_loop(self, polling_manager=None):
if not polling_manager:
polling_manager = polling.AlwaysPoll()

sync = True
ports = set()
ancillary_ports = set()
Expand All @@ -1056,28 +1064,34 @@ def rpc_loop(self):
ports.clear()
ancillary_ports.clear()
sync = False
polling_manager.force_polling()

# Notify the plugin of tunnel IP
if self.enable_tunneling and tunnel_sync:
LOG.info(_("Agent tunnel out of sync with plugin!"))
tunnel_sync = self.tunnel_sync()

port_info = self.update_ports(ports)

# notify plugin about port deltas
if port_info:
LOG.debug(_("Agent loop has new devices!"))
# If treat devices fails - must resync with plugin
sync = self.process_network_ports(port_info)
ports = port_info['current']
if polling_manager.is_polling_required:
port_info = self.update_ports(ports)

# Treat ancillary devices if they exist
if self.ancillary_brs:
port_info = self.update_ancillary_ports(ancillary_ports)
# notify plugin about port deltas
if port_info:
rc = self.process_ancillary_network_ports(port_info)
ancillary_ports = port_info['current']
sync = sync | rc
LOG.debug(_("Agent loop has new devices!"))
# If treat devices fails - must resync with plugin
sync = self.process_network_ports(port_info)
ports = port_info['current']

# Treat ancillary devices if they exist
if self.ancillary_brs:
port_info = self.update_ancillary_ports(
ancillary_ports)
if port_info:
rc = self.process_ancillary_network_ports(
port_info)
ancillary_ports = port_info['current']
sync = sync | rc

polling_manager.polling_completed()

except Exception:
LOG.exception(_("Error in agent event loop"))
Expand All @@ -1095,7 +1109,9 @@ def rpc_loop(self):
'elapsed': elapsed})

def daemon_loop(self):
self.rpc_loop()
with polling.get_polling_manager(self.minimize_polling,
self.root_helper) as pm:
self.rpc_loop(polling_manager=pm)


def check_ovs_version(min_required_version, root_helper):
Expand Down Expand Up @@ -1154,6 +1170,7 @@ def create_agent_config_map(config):
bridge_mappings=bridge_mappings,
root_helper=config.AGENT.root_helper,
polling_interval=config.AGENT.polling_interval,
minimize_polling=config.AGENT.minimize_polling,
tunnel_types=config.AGENT.tunnel_types,
veth_mtu=config.AGENT.veth_mtu,
l2_population=config.AGENT.l2_population,
Expand Down
4 changes: 4 additions & 0 deletions neutron/plugins/openvswitch/common/config.py
Expand Up @@ -62,6 +62,10 @@
cfg.IntOpt('polling_interval', default=2,
help=_("The number of seconds the agent will wait between "
"polling for local device changes.")),
cfg.BoolOpt('minimize_polling',
default=False,
help=_("Minimize polling by monitoring ovsdb for interface "
"changes.")),
cfg.ListOpt('tunnel_types', default=DEFAULT_TUNNEL_TYPES,
help=_("Network types supported by the agent "
"(gre and/or vxlan)")),
Expand Down

0 comments on commit cb0df59

Please sign in to comment.