Merge "object-updater: add concurrent updates"
This commit is contained in:
commit
ea33638d0c
@ -909,7 +909,9 @@ log_facility LOG_LOCAL0 Syslog log facility
|
|||||||
log_level INFO Logging level
|
log_level INFO Logging level
|
||||||
log_address /dev/log Logging directory
|
log_address /dev/log Logging directory
|
||||||
interval 300 Minimum time for a pass to take
|
interval 300 Minimum time for a pass to take
|
||||||
concurrency 1 Number of updater workers to spawn
|
updater_workers 1 Number of worker processes
|
||||||
|
concurrency 8 Number of updates to run concurrently in
|
||||||
|
each worker process
|
||||||
node_timeout DEFAULT or 10 Request timeout to external services. This
|
node_timeout DEFAULT or 10 Request timeout to external services. This
|
||||||
uses what's set here, or what's set in the
|
uses what's set here, or what's set in the
|
||||||
DEFAULT section, or 10 (though other
|
DEFAULT section, or 10 (though other
|
||||||
|
@ -371,9 +371,16 @@ use = egg:swift#recon
|
|||||||
# log_address = /dev/log
|
# log_address = /dev/log
|
||||||
#
|
#
|
||||||
# interval = 300
|
# interval = 300
|
||||||
# concurrency = 1
|
|
||||||
# node_timeout = <whatever's in the DEFAULT section or 10>
|
# node_timeout = <whatever's in the DEFAULT section or 10>
|
||||||
#
|
#
|
||||||
|
# updater_workers controls how many processes the object updater will
|
||||||
|
# spawn, while concurrency controls how many async_pending records
|
||||||
|
# each updater process will operate on at any one time. With
|
||||||
|
# concurrency=C and updater_workers=W, there will be up to W*C
|
||||||
|
# async_pending records being processed at once.
|
||||||
|
# concurrency = 8
|
||||||
|
# updater_workers = 1
|
||||||
|
#
|
||||||
# Send at most this many object updates per second
|
# Send at most this many object updates per second
|
||||||
# objects_per_second = 50
|
# objects_per_second = 50
|
||||||
#
|
#
|
||||||
|
@ -29,7 +29,7 @@ from swift.common.exceptions import ConnectionTimeout
|
|||||||
from swift.common.ring import Ring
|
from swift.common.ring import Ring
|
||||||
from swift.common.utils import get_logger, renamer, write_pickle, \
|
from swift.common.utils import get_logger, renamer, write_pickle, \
|
||||||
dump_recon_cache, config_true_value, RateLimitedIterator, split_path, \
|
dump_recon_cache, config_true_value, RateLimitedIterator, split_path, \
|
||||||
eventlet_monkey_patch, get_redirect_data
|
eventlet_monkey_patch, get_redirect_data, ContextPool
|
||||||
from swift.common.daemon import Daemon
|
from swift.common.daemon import Daemon
|
||||||
from swift.common.header_key_dict import HeaderKeyDict
|
from swift.common.header_key_dict import HeaderKeyDict
|
||||||
from swift.common.storage_policy import split_policy_string, PolicyError
|
from swift.common.storage_policy import split_policy_string, PolicyError
|
||||||
@ -94,7 +94,8 @@ class ObjectUpdater(Daemon):
|
|||||||
self.swift_dir = conf.get('swift_dir', '/etc/swift')
|
self.swift_dir = conf.get('swift_dir', '/etc/swift')
|
||||||
self.interval = int(conf.get('interval', 300))
|
self.interval = int(conf.get('interval', 300))
|
||||||
self.container_ring = None
|
self.container_ring = None
|
||||||
self.concurrency = int(conf.get('concurrency', 1))
|
self.concurrency = int(conf.get('concurrency', 8))
|
||||||
|
self.updater_workers = int(conf.get('updater_workers', 1))
|
||||||
if 'slowdown' in conf:
|
if 'slowdown' in conf:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'The slowdown option is deprecated in favor of '
|
'The slowdown option is deprecated in favor of '
|
||||||
@ -150,7 +151,7 @@ class ObjectUpdater(Daemon):
|
|||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
_('Skipping %s as it is not mounted'), device)
|
_('Skipping %s as it is not mounted'), device)
|
||||||
continue
|
continue
|
||||||
while len(pids) >= self.concurrency:
|
while len(pids) >= self.updater_workers:
|
||||||
pids.remove(os.wait()[0])
|
pids.remove(os.wait()[0])
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
if pid:
|
if pid:
|
||||||
@ -230,6 +231,7 @@ class ObjectUpdater(Daemon):
|
|||||||
prefix_path = os.path.join(async_pending, prefix)
|
prefix_path = os.path.join(async_pending, prefix)
|
||||||
if not os.path.isdir(prefix_path):
|
if not os.path.isdir(prefix_path):
|
||||||
continue
|
continue
|
||||||
|
last_obj_hash = None
|
||||||
for update in sorted(self._listdir(prefix_path), reverse=True):
|
for update in sorted(self._listdir(prefix_path), reverse=True):
|
||||||
update_path = os.path.join(prefix_path, update)
|
update_path = os.path.join(prefix_path, update)
|
||||||
if not os.path.isfile(update_path):
|
if not os.path.isfile(update_path):
|
||||||
@ -244,13 +246,34 @@ class ObjectUpdater(Daemon):
|
|||||||
'name %s')
|
'name %s')
|
||||||
% (update_path))
|
% (update_path))
|
||||||
continue
|
continue
|
||||||
|
# Async pendings are stored on disk like this:
|
||||||
|
#
|
||||||
|
# <device>/async_pending/<suffix>/<obj_hash>-<timestamp>
|
||||||
|
#
|
||||||
|
# If there are multiple updates for a given object,
|
||||||
|
# they'll look like this:
|
||||||
|
#
|
||||||
|
# <device>/async_pending/<obj_suffix>/<obj_hash>-<timestamp1>
|
||||||
|
# <device>/async_pending/<obj_suffix>/<obj_hash>-<timestamp2>
|
||||||
|
# <device>/async_pending/<obj_suffix>/<obj_hash>-<timestamp3>
|
||||||
|
#
|
||||||
|
# Async updates also have the property that newer
|
||||||
|
# updates contain all the information in older updates.
|
||||||
|
# Since we sorted the directory listing in reverse
|
||||||
|
# order, we'll see timestamp3 first, yield it, and then
|
||||||
|
# unlink timestamp2 and timestamp1 since we know they
|
||||||
|
# are obsolete.
|
||||||
|
#
|
||||||
|
# This way, our caller only gets useful async_pendings.
|
||||||
|
if obj_hash == last_obj_hash:
|
||||||
|
self.stats.unlinks += 1
|
||||||
|
self.logger.increment('unlinks')
|
||||||
|
os.unlink(update_path)
|
||||||
|
else:
|
||||||
|
last_obj_hash = obj_hash
|
||||||
yield {'device': device, 'policy': policy,
|
yield {'device': device, 'policy': policy,
|
||||||
'path': update_path,
|
'path': update_path,
|
||||||
'obj_hash': obj_hash, 'timestamp': timestamp}
|
'obj_hash': obj_hash, 'timestamp': timestamp}
|
||||||
try:
|
|
||||||
os.rmdir(prefix_path)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def object_sweep(self, device):
|
def object_sweep(self, device):
|
||||||
"""
|
"""
|
||||||
@ -265,20 +288,13 @@ class ObjectUpdater(Daemon):
|
|||||||
self.logger.info("Object update sweep starting on %s (pid: %d)",
|
self.logger.info("Object update sweep starting on %s (pid: %d)",
|
||||||
device, my_pid)
|
device, my_pid)
|
||||||
|
|
||||||
last_obj_hash = None
|
|
||||||
ap_iter = RateLimitedIterator(
|
ap_iter = RateLimitedIterator(
|
||||||
self._iter_async_pendings(device),
|
self._iter_async_pendings(device),
|
||||||
elements_per_second=self.max_objects_per_second)
|
elements_per_second=self.max_objects_per_second)
|
||||||
|
with ContextPool(self.concurrency) as pool:
|
||||||
for update in ap_iter:
|
for update in ap_iter:
|
||||||
if update['obj_hash'] == last_obj_hash:
|
pool.spawn(self.process_object_update,
|
||||||
self.stats.unlinks += 1
|
update['path'], update['device'], update['policy'])
|
||||||
self.logger.increment('unlinks')
|
|
||||||
os.unlink(update['path'])
|
|
||||||
else:
|
|
||||||
self.process_object_update(update['path'], update['device'],
|
|
||||||
update['policy'])
|
|
||||||
last_obj_hash = update['obj_hash']
|
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - last_status_update >= self.report_interval:
|
if now - last_status_update >= self.report_interval:
|
||||||
this_sweep = self.stats.since(start_stats)
|
this_sweep = self.stats.since(start_stats)
|
||||||
@ -290,6 +306,7 @@ class ObjectUpdater(Daemon):
|
|||||||
'pid': my_pid,
|
'pid': my_pid,
|
||||||
'stats': this_sweep})
|
'stats': this_sweep})
|
||||||
last_status_update = now
|
last_status_update = now
|
||||||
|
pool.waitall()
|
||||||
|
|
||||||
self.logger.timing_since('timing', start_time)
|
self.logger.timing_since('timing', start_time)
|
||||||
sweep_totals = self.stats.since(start_stats)
|
sweep_totals = self.stats.since(start_stats)
|
||||||
@ -370,6 +387,13 @@ class ObjectUpdater(Daemon):
|
|||||||
self.stats.unlinks += 1
|
self.stats.unlinks += 1
|
||||||
self.logger.increment('unlinks')
|
self.logger.increment('unlinks')
|
||||||
os.unlink(update_path)
|
os.unlink(update_path)
|
||||||
|
try:
|
||||||
|
# If this was the last async_pending in the directory,
|
||||||
|
# then this will succeed. Otherwise, it'll fail, and
|
||||||
|
# that's okay.
|
||||||
|
os.rmdir(os.path.dirname(update_path))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
elif redirects:
|
elif redirects:
|
||||||
# erase any previous successes
|
# erase any previous successes
|
||||||
update.pop('successes', None)
|
update.pop('successes', None)
|
||||||
|
@ -43,6 +43,23 @@ from swift.common.utils import (
|
|||||||
from swift.common.storage_policy import StoragePolicy, POLICIES
|
from swift.common.storage_policy import StoragePolicy, POLICIES
|
||||||
|
|
||||||
|
|
||||||
|
class MockPool(object):
|
||||||
|
def __init__(self, *a, **kw):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def spawn(self, func, *args, **kwargs):
|
||||||
|
func(*args, **kwargs)
|
||||||
|
|
||||||
|
def waitall(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *a, **kw):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
_mocked_policies = [StoragePolicy(0, 'zero', False),
|
_mocked_policies = [StoragePolicy(0, 'zero', False),
|
||||||
StoragePolicy(1, 'one', True)]
|
StoragePolicy(1, 'one', True)]
|
||||||
|
|
||||||
@ -104,7 +121,8 @@ class TestObjectUpdater(unittest.TestCase):
|
|||||||
self.assertEqual(daemon.mount_check, True)
|
self.assertEqual(daemon.mount_check, True)
|
||||||
self.assertEqual(daemon.swift_dir, '/etc/swift')
|
self.assertEqual(daemon.swift_dir, '/etc/swift')
|
||||||
self.assertEqual(daemon.interval, 300)
|
self.assertEqual(daemon.interval, 300)
|
||||||
self.assertEqual(daemon.concurrency, 1)
|
self.assertEqual(daemon.concurrency, 8)
|
||||||
|
self.assertEqual(daemon.updater_workers, 1)
|
||||||
self.assertEqual(daemon.max_objects_per_second, 50.0)
|
self.assertEqual(daemon.max_objects_per_second, 50.0)
|
||||||
|
|
||||||
# non-defaults
|
# non-defaults
|
||||||
@ -114,6 +132,7 @@ class TestObjectUpdater(unittest.TestCase):
|
|||||||
'swift_dir': '/not/here',
|
'swift_dir': '/not/here',
|
||||||
'interval': '600',
|
'interval': '600',
|
||||||
'concurrency': '2',
|
'concurrency': '2',
|
||||||
|
'updater_workers': '3',
|
||||||
'objects_per_second': '10.5',
|
'objects_per_second': '10.5',
|
||||||
}
|
}
|
||||||
daemon = object_updater.ObjectUpdater(conf, logger=self.logger)
|
daemon = object_updater.ObjectUpdater(conf, logger=self.logger)
|
||||||
@ -122,6 +141,7 @@ class TestObjectUpdater(unittest.TestCase):
|
|||||||
self.assertEqual(daemon.swift_dir, '/not/here')
|
self.assertEqual(daemon.swift_dir, '/not/here')
|
||||||
self.assertEqual(daemon.interval, 600)
|
self.assertEqual(daemon.interval, 600)
|
||||||
self.assertEqual(daemon.concurrency, 2)
|
self.assertEqual(daemon.concurrency, 2)
|
||||||
|
self.assertEqual(daemon.updater_workers, 3)
|
||||||
self.assertEqual(daemon.max_objects_per_second, 10.5)
|
self.assertEqual(daemon.max_objects_per_second, 10.5)
|
||||||
|
|
||||||
# check deprecated option
|
# check deprecated option
|
||||||
@ -234,10 +254,8 @@ class TestObjectUpdater(unittest.TestCase):
|
|||||||
if should_skip:
|
if should_skip:
|
||||||
# if we were supposed to skip over the dir, we didn't process
|
# if we were supposed to skip over the dir, we didn't process
|
||||||
# anything at all
|
# anything at all
|
||||||
self.assertTrue(os.path.exists(prefix_dir))
|
|
||||||
self.assertEqual(set(), seen)
|
self.assertEqual(set(), seen)
|
||||||
else:
|
else:
|
||||||
self.assertTrue(not os.path.exists(prefix_dir))
|
|
||||||
self.assertEqual(expected, seen)
|
self.assertEqual(expected, seen)
|
||||||
|
|
||||||
# test cleanup: the tempdir gets cleaned up between runs, but this
|
# test cleanup: the tempdir gets cleaned up between runs, but this
|
||||||
@ -291,7 +309,8 @@ class TestObjectUpdater(unittest.TestCase):
|
|||||||
# and 5 async_pendings on disk, we should get at least two progress
|
# and 5 async_pendings on disk, we should get at least two progress
|
||||||
# lines.
|
# lines.
|
||||||
with mock.patch('swift.obj.updater.time',
|
with mock.patch('swift.obj.updater.time',
|
||||||
mock.MagicMock(time=mock_time_function)):
|
mock.MagicMock(time=mock_time_function)), \
|
||||||
|
mock.patch.object(object_updater, 'ContextPool', MockPool):
|
||||||
ou.object_sweep(self.sda1)
|
ou.object_sweep(self.sda1)
|
||||||
|
|
||||||
info_lines = logger.get_lines_for_level('info')
|
info_lines = logger.get_lines_for_level('info')
|
||||||
@ -444,7 +463,6 @@ class TestObjectUpdater(unittest.TestCase):
|
|||||||
os.mkdir(odd_dir)
|
os.mkdir(odd_dir)
|
||||||
ou.run_once()
|
ou.run_once()
|
||||||
self.assertTrue(os.path.exists(async_dir))
|
self.assertTrue(os.path.exists(async_dir))
|
||||||
self.assertFalse(os.path.exists(odd_dir))
|
|
||||||
self.assertEqual([
|
self.assertEqual([
|
||||||
mock.call(self.devices_dir, 'sda1', True),
|
mock.call(self.devices_dir, 'sda1', True),
|
||||||
], mock_check_drive.mock_calls)
|
], mock_check_drive.mock_calls)
|
||||||
@ -548,7 +566,13 @@ class TestObjectUpdater(unittest.TestCase):
|
|||||||
err = event.wait()
|
err = event.wait()
|
||||||
if err:
|
if err:
|
||||||
raise err
|
raise err
|
||||||
self.assertTrue(not os.path.exists(op_path))
|
|
||||||
|
# we remove the async_pending and its containing suffix dir, but not
|
||||||
|
# anything above that
|
||||||
|
self.assertFalse(os.path.exists(op_path))
|
||||||
|
self.assertFalse(os.path.exists(os.path.dirname(op_path)))
|
||||||
|
self.assertTrue(os.path.exists(os.path.dirname(os.path.dirname(
|
||||||
|
op_path))))
|
||||||
self.assertEqual(ou.logger.get_increment_counts(),
|
self.assertEqual(ou.logger.get_increment_counts(),
|
||||||
{'unlinks': 1, 'successes': 1})
|
{'unlinks': 1, 'successes': 1})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user