Skip to content

Commit

Permalink
Use neutron port_list when filtering instance by ip
Browse files Browse the repository at this point in the history
The performance of filtering instance by IP is poor, due to the fact that
the IP address is part of a JSON-encoded filed and we need to unpack the
field and do a search on the field for each record in instances table, we
have to iterate one by one to find the instance that matches the request.

As discussed in Queens PTG[1], one possible solution is to get filtered
ports from Neutron and retrieve the instance uuid from the port.device_id
and then merge to the other filters.

As Nova provides regex matching manner filtering for IP filter, so this
depend on Neutron changes that add substring matching functionality to
the GET /ports API[2]

The performance test results of the POC are presented in[3].

[1]http://lists.openstack.org/pipermail/openstack-dev/2017-September/122258.html
[2]https://bugs.launchpad.net/neutron/+bug/1718605
[3]http://lists.openstack.org/pipermail/openstack-dev/2018-January/125990.html

Depends-On: I9549b2ba676e1bad0812682c3f3f3c97de15f5f6

Co-Authored-By: Matt Riedemann <mriedem.os@gmail.com>

Implements: blueprint improve-filter-instances-by-ip-performance

Change-Id: I06aabfdce4aaefde080c7e122552ce4f36da5804
  • Loading branch information
Kevin_Zheng committed Jan 23, 2018
1 parent 66eb27f commit 3f35fe6
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 4 deletions.
53 changes: 50 additions & 3 deletions nova/compute/api.py
Expand Up @@ -2347,9 +2347,36 @@ def _remap_fixed_ip_filter(fixed_ip):
# limit so that it can be applied after the IP filter.
filter_ip = 'ip6' in filters or 'ip' in filters
orig_limit = limit
if filter_ip and limit:
LOG.debug('Removing limit for DB query due to IP filter')
limit = None
if filter_ip:
if self.network_api.has_substr_port_filtering_extension(context):
# We're going to filter by IP using Neutron so set filter_ip
# to False so we don't attempt post-DB query filtering in
# memory below.
filter_ip = False
instance_uuids = self._ip_filter_using_neutron(context,
filters)
if instance_uuids:
# Note that 'uuid' is not in the 2.1 GET /servers query
# parameter schema, however, we allow additionalProperties
# so someone could filter instances by uuid, which doesn't
# make a lot of sense but we have to account for it.
if 'uuid' in filters and filters['uuid']:
filter_uuids = filters['uuid']
if isinstance(filter_uuids, list):
instance_uuids.extend(filter_uuids)
else:
# Assume a string. If it's a dict or tuple or
# something, well...that's too bad. This is why
# we have query parameter schema definitions.
if filter_uuids not in instance_uuids:
instance_uuids.append(filter_uuids)
filters['uuid'] = instance_uuids
else:
# No matches on the ip filter(s), return an empty list.
return objects.InstanceList()
elif limit:
LOG.debug('Removing limit for DB query due to IP filter')
limit = None

# The ordering of instances will be
# [sorted instances with no host] + [sorted instances with host].
Expand Down Expand Up @@ -2484,6 +2511,26 @@ def _match_instance(instance):
break
return objects.InstanceList(objects=result_objs)

def _ip_filter_using_neutron(self, context, filters):
ip4_address = filters.get('ip')
ip6_address = filters.get('ip6')
addresses = [ip4_address, ip6_address]
uuids = []
for address in addresses:
if address:
try:
ports = self.network_api.list_ports(
context, fixed_ips='ip_address_substr=' + address,
fields=['device_id'])['ports']
for port in ports:
uuids.append(port['device_id'])
except Exception as e:
LOG.error('An error occurred while listing ports '
'with an ip_address filter value of "%s". '
'Error: %s',
address, six.text_type(e))
return uuids

def _get_instances_by_filters(self, context, filters,
limit=None, marker=None, fields=None,
sort_keys=None, sort_dirs=None):
Expand Down
3 changes: 3 additions & 0 deletions nova/network/base_api.py
Expand Up @@ -367,3 +367,6 @@ def update_instance_vnic_index(self, context, instance, vif, index):
:param index: The index on the instance for the VIF.
"""
pass

def has_substr_port_filtering_extension(self, context):
return False
4 changes: 4 additions & 0 deletions nova/network/neutronv2/api.py
Expand Up @@ -1112,6 +1112,10 @@ def _has_qos_queue_extension(self, context, neutron=None):
self._refresh_neutron_extensions_cache(context, neutron=neutron)
return constants.QOS_QUEUE in self.extensions

def has_substr_port_filtering_extension(self, context):
self._refresh_neutron_extensions_cache(context)
return constants.SUBSTR_PORT_FILTERING in self.extensions

def _get_pci_device_profile(self, pci_dev):
dev_spec = self.pci_whitelist.get_devspec(pci_dev)
if dev_spec:
Expand Down
1 change: 1 addition & 0 deletions nova/network/neutronv2/constants.py
Expand Up @@ -18,3 +18,4 @@
VNIC_INDEX_EXT = 'VNIC Index'
DNS_INTEGRATION = 'DNS Integration'
MULTI_NET_EXT = 'Multi Provider Network'
SUBSTR_PORT_FILTERING = 'IP address substring filtering'
5 changes: 4 additions & 1 deletion nova/tests/unit/compute/test_compute.py
Expand Up @@ -11374,7 +11374,10 @@ def test_ip_filtering_two_matches_limit(self):
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
@mock.patch.object(objects.CellMapping, 'get_by_uuid',
side_effect=exception.CellMappingNotFound(uuid='fake'))
def test_ip_filtering_no_limit_to_db(self, _mock_cell_map_get,
@mock.patch('nova.network.neutronv2.api.API.'
'has_substr_port_filtering_extension', return_value=False)
def test_ip_filtering_no_limit_to_db(self, mock_has_port_filter_ext,
_mock_cell_map_get,
mock_buildreq_get):
c = context.get_admin_context()
# Limit is not supplied to the DB when using an IP filter
Expand Down
118 changes: 118 additions & 0 deletions nova/tests/unit/compute/test_compute_api.py
Expand Up @@ -38,6 +38,7 @@
from nova import context
from nova import db
from nova import exception
from nova.network.neutronv2 import api as neutron_api
from nova import objects
from nova.objects import base as obj_base
from nova.objects import block_device as block_device_obj
Expand Down Expand Up @@ -5630,6 +5631,123 @@ def test_attach_volume_shelved_offloaded_fails(
self.compute_api.attach_volume,
self.context, instance, uuids.volumeid)

@mock.patch.object(neutron_api.API, 'has_substr_port_filtering_extension')
@mock.patch.object(neutron_api.API, 'list_ports')
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
def test_get_all_ip_filter_use_neutron(self, mock_buildreq_get,
mock_list_port, mock_check_ext):
mock_check_ext.return_value = True
cell_instances = self._list_of_instances(2)
mock_list_port.return_value = {
'ports': [{'device_id': 'fake_device_id'}]}
with mock.patch('nova.compute.instance_list.'
'get_instance_objects_sorted') as mock_inst_get:
mock_inst_get.return_value = objects.InstanceList(
self.context, objects=cell_instances)

self.compute_api.get_all(
self.context, search_opts={'ip': 'fake'},
limit=None, marker='fake-marker', sort_keys=['baz'],
sort_dirs=['desc'])

mock_list_port.assert_called_once_with(
self.context, fixed_ips='ip_address_substr=fake',
fields=['device_id'])
mock_buildreq_get.assert_called_once_with(
self.context, {'ip': 'fake', 'uuid': ['fake_device_id']},
limit=None, marker='fake-marker',
sort_keys=['baz'], sort_dirs=['desc'])
fields = ['metadata', 'info_cache', 'security_groups']
mock_inst_get.assert_called_once_with(
self.context, {'ip': 'fake', 'uuid': ['fake_device_id']},
None, None, fields, ['baz'], ['desc'])

@mock.patch.object(neutron_api.API, 'has_substr_port_filtering_extension')
@mock.patch.object(neutron_api.API, 'list_ports')
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
def test_get_all_ip6_filter_use_neutron(self, mock_buildreq_get,
mock_list_port, mock_check_ext):
mock_check_ext.return_value = True
cell_instances = self._list_of_instances(2)
mock_list_port.return_value = {
'ports': [{'device_id': 'fake_device_id'}]}
with mock.patch('nova.compute.instance_list.'
'get_instance_objects_sorted') as mock_inst_get:
mock_inst_get.return_value = objects.InstanceList(
self.context, objects=cell_instances)

self.compute_api.get_all(
self.context, search_opts={'ip6': 'fake'},
limit=None, marker='fake-marker', sort_keys=['baz'],
sort_dirs=['desc'])

mock_list_port.assert_called_once_with(
self.context, fixed_ips='ip_address_substr=fake',
fields=['device_id'])
mock_buildreq_get.assert_called_once_with(
self.context, {'ip6': 'fake', 'uuid': ['fake_device_id']},
limit=None, marker='fake-marker',
sort_keys=['baz'], sort_dirs=['desc'])
fields = ['metadata', 'info_cache', 'security_groups']
mock_inst_get.assert_called_once_with(
self.context, {'ip6': 'fake', 'uuid': ['fake_device_id']},
None, None, fields, ['baz'], ['desc'])

@mock.patch.object(neutron_api.API, 'has_substr_port_filtering_extension')
@mock.patch.object(neutron_api.API, 'list_ports')
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
def test_get_all_ip_and_ip6_filter_use_neutron(self, mock_buildreq_get,
mock_list_port,
mock_check_ext):
mock_check_ext.return_value = True
cell_instances = self._list_of_instances(2)
mock_list_port.return_value = {
'ports': [{'device_id': 'fake_device_id'}]}
with mock.patch('nova.compute.instance_list.'
'get_instance_objects_sorted') as mock_inst_get:
mock_inst_get.return_value = objects.InstanceList(
self.context, objects=cell_instances)

self.compute_api.get_all(
self.context, search_opts={'ip': 'fake1', 'ip6': 'fake2'},
limit=None, marker='fake-marker', sort_keys=['baz'],
sort_dirs=['desc'])

mock_list_port.assert_has_calls([
mock.call(
self.context, fixed_ips='ip_address_substr=fake1',
fields=['device_id']),
mock.call(
self.context, fixed_ips='ip_address_substr=fake2',
fields=['device_id'])
])
mock_buildreq_get.assert_called_once_with(
self.context, {'ip': 'fake1', 'ip6': 'fake2',
'uuid': ['fake_device_id', 'fake_device_id']},
limit=None, marker='fake-marker',
sort_keys=['baz'], sort_dirs=['desc'])
fields = ['metadata', 'info_cache', 'security_groups']
mock_inst_get.assert_called_once_with(
self.context, {'ip': 'fake1', 'ip6': 'fake2',
'uuid': ['fake_device_id', 'fake_device_id']},
None, None, fields, ['baz'], ['desc'])

@mock.patch.object(neutron_api.API, 'has_substr_port_filtering_extension')
@mock.patch.object(neutron_api.API, 'list_ports')
def test_get_all_ip6_filter_use_neutron_exc(self, mock_list_port,
mock_check_ext):
mock_check_ext.return_value = True
mock_list_port.side_effect = exception.InternalError('fake')

instances = self.compute_api.get_all(
self.context, search_opts={'ip6': 'fake'},
limit=None, marker='fake-marker', sort_keys=['baz'],
sort_dirs=['desc'])
mock_list_port.assert_called_once_with(
self.context, fixed_ips='ip_address_substr=fake',
fields=['device_id'])
self.assertEqual([], instances.objects)


class Cellsv1DeprecatedTestMixIn(object):
@mock.patch.object(objects.BuildRequestList, 'get_by_filters')
Expand Down
@@ -0,0 +1,6 @@
---
features:
- When ``IP address substring filtering`` extension
is available in Neutron, Nova will proxy the instance
list with ``ip`` or ``ip6`` filter to Neutron for
better performance.

0 comments on commit 3f35fe6

Please sign in to comment.