
It's harder than it sounds. There was really three challenges. Challenge #1 Initial Assignment =============================== Before starting to assign parts on this new shiny ring you've constructed, maybe we'll pause for a moment up front and consider the lay of the land. This process is called the replica_plan. The replica_plan approach is separating part assignment failures into two modes: 1) we considered the cluster topology and it's weights and came up with the wrong plan 2) we failed to execute on the plan I failed at both parts plenty of times before I got it this close. I'm sure a counter example still exists, but when we find it the new helper methods will let us reason about where things went wrong. Challenge #2 Fixing Placement ============================= With a sound plan in hand, it's much easier to fail to execute on it the less material you have to execute with - so we gather up as many parts as we can - as long as we think we can find them a better home. Picking the right parts for gather is a black art - when you notice a balance is slow it's because it's spending so much time iterating over replica2part2dev trying to decide just the right parts to gather. The replica plan can help at least in the gross dispersion collection to gather up the worst offenders first before considering balance. I think trying to avoid picking up parts that are stuck to the tier before falling into a forced grab on anything over parts_wanted helps with stability generally - but depending on where the parts_wanted are in relation to the full devices it's pretty easy pick up something that'll end up really close to where it started. I tried to break the gather methods into smaller pieces so it looked like I knew what I was doing. Going with a MAXIMUM gather iteration instead of balance (which doesn't reflect the replica_plan) doesn't seem to be costing me anything - most of the time the exit condition is either solved or all the parts overly aggressively locked up on min_part_hours. So far, it mostly seemds if the thing is going to balance this round it'll get it in the first couple of shakes. Challenge #3 Crazy replica2part2dev tables ========================================== I think there's lots of ways "scars" can build up a ring which can result in very particular replica2part2dev tables that are physically difficult to dig out of. It's repairing these scars that will take multiple rebalances to resolve. ... but at this point ... ... lacking a counter example ... I've been able to close up all the edge cases I was able to find. It may not be quick, but progress will be made. Basically my strategy just required a better understanding of how previous algorithms were able to *mostly* keep things moving by brute forcing the whole mess with a bunch of randomness. Then when we detect our "elegant" careful part selection isn't making progress - we can fall back to same old tricks. Validation ========== We validate against duplicate part replica assignment after rebalance and raise an ERROR if we detect more than one replica of a part assigned to the same device. In order to meet that requirement we have to have as many devices as replicas, so attempting to rebalance with too few devices w/o changing your replica_count is also an ERROR not a warning. Random Thoughts =============== As usual with rings, the test diff can be hard to reason about - hopefully I've added enough comments to assure future me that these assertions make sense. Despite being a large rewrite of a lot of important code, the existing code is known to have failed us. This change fixes a critical bug that's trivial to reproduce in a critical component of the system. There's probably a bunch of error messages and exit status stuff that's not as helpful as it could be considering the new behaviors. Change-Id: I1bbe7be38806fc1c8b9181a722933c18a6c76e05 Closes-Bug: #1452431
3693 lines
160 KiB
Python
3693 lines
160 KiB
Python
# Copyright (c) 2010-2012 OpenStack Foundation
|
|
#
|
|
# 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 copy
|
|
import errno
|
|
import mock
|
|
import operator
|
|
import os
|
|
import unittest
|
|
import six.moves.cPickle as pickle
|
|
from array import array
|
|
from collections import defaultdict
|
|
from math import ceil
|
|
from tempfile import mkdtemp
|
|
from shutil import rmtree
|
|
import random
|
|
|
|
from six.moves import range
|
|
|
|
from swift.common import exceptions
|
|
from swift.common import ring
|
|
from swift.common.ring import utils
|
|
from swift.common.ring.builder import MAX_BALANCE
|
|
|
|
|
|
class TestRingBuilder(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.testdir = mkdtemp()
|
|
|
|
def tearDown(self):
|
|
rmtree(self.testdir, ignore_errors=1)
|
|
|
|
def _partition_counts(self, builder, key='id'):
|
|
"""
|
|
Returns a dictionary mapping the given device key to (number of
|
|
partitions assigned to to that key).
|
|
"""
|
|
counts = defaultdict(int)
|
|
for part2dev_id in builder._replica2part2dev:
|
|
for dev_id in part2dev_id:
|
|
counts[builder.devs[dev_id][key]] += 1
|
|
return counts
|
|
|
|
def _get_population_by_region(self, builder):
|
|
"""
|
|
Returns a dictionary mapping region to number of partitions in that
|
|
region.
|
|
"""
|
|
return self._partition_counts(builder, key='region')
|
|
|
|
def test_init(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
self.assertEqual(rb.part_power, 8)
|
|
self.assertEqual(rb.replicas, 3)
|
|
self.assertEqual(rb.min_part_hours, 1)
|
|
self.assertEqual(rb.parts, 2 ** 8)
|
|
self.assertEqual(rb.devs, [])
|
|
self.assertEqual(rb.devs_changed, False)
|
|
self.assertEqual(rb.version, 0)
|
|
|
|
def test_overlarge_part_powers(self):
|
|
ring.RingBuilder(32, 3, 1) # passes by not crashing
|
|
self.assertRaises(ValueError, ring.RingBuilder, 33, 3, 1)
|
|
|
|
def test_insufficient_replicas(self):
|
|
ring.RingBuilder(8, 1.0, 1) # passes by not crashing
|
|
self.assertRaises(ValueError, ring.RingBuilder, 8, 0.999, 1)
|
|
|
|
def test_negative_min_part_hours(self):
|
|
ring.RingBuilder(8, 3, 0) # passes by not crashing
|
|
self.assertRaises(ValueError, ring.RingBuilder, 8, 3, -1)
|
|
|
|
def test_deepcopy(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb1'})
|
|
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdb1'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sdb1'})
|
|
|
|
# more devices in zone #1
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10004, 'device': 'sdc1'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10004, 'device': 'sdd1'})
|
|
rb.rebalance()
|
|
rb_copy = copy.deepcopy(rb)
|
|
|
|
self.assertEqual(rb.to_dict(), rb_copy.to_dict())
|
|
self.assertTrue(rb.devs is not rb_copy.devs)
|
|
self.assertTrue(rb._replica2part2dev is not rb_copy._replica2part2dev)
|
|
self.assertTrue(rb._last_part_moves is not rb_copy._last_part_moves)
|
|
self.assertTrue(rb._remove_devs is not rb_copy._remove_devs)
|
|
self.assertTrue(rb._dispersion_graph is not rb_copy._dispersion_graph)
|
|
|
|
def test_get_ring(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10004, 'device': 'sda1'})
|
|
rb.remove_dev(1)
|
|
rb.rebalance()
|
|
r = rb.get_ring()
|
|
self.assertTrue(isinstance(r, ring.RingData))
|
|
r2 = rb.get_ring()
|
|
self.assertTrue(r is r2)
|
|
rb.rebalance()
|
|
r3 = rb.get_ring()
|
|
self.assertTrue(r3 is not r2)
|
|
r4 = rb.get_ring()
|
|
self.assertTrue(r3 is r4)
|
|
|
|
def test_rebalance_with_seed(self):
|
|
devs = [(0, 10000), (1, 10001), (2, 10002), (1, 10003)]
|
|
ring_builders = []
|
|
for n in range(3):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
idx = 0
|
|
for zone, port in devs:
|
|
for d in ('sda1', 'sdb1'):
|
|
rb.add_dev({'id': idx, 'region': 0, 'zone': zone,
|
|
'ip': '127.0.0.1', 'port': port,
|
|
'device': d, 'weight': 1})
|
|
idx += 1
|
|
ring_builders.append(rb)
|
|
|
|
rb0 = ring_builders[0]
|
|
rb1 = ring_builders[1]
|
|
rb2 = ring_builders[2]
|
|
|
|
r0 = rb0.get_ring()
|
|
self.assertTrue(rb0.get_ring() is r0)
|
|
|
|
rb0.rebalance() # NO SEED
|
|
rb1.rebalance(seed=10)
|
|
rb2.rebalance(seed=10)
|
|
|
|
r1 = rb1.get_ring()
|
|
r2 = rb2.get_ring()
|
|
|
|
self.assertFalse(rb0.get_ring() is r0)
|
|
self.assertNotEqual(r0.to_dict(), r1.to_dict())
|
|
self.assertEqual(r1.to_dict(), r2.to_dict())
|
|
|
|
def test_rebalance_part_on_deleted_other_part_on_drained(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 1, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 1, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 1, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
rb.add_dev({'id': 3, 'region': 1, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
rb.add_dev({'id': 4, 'region': 1, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10004, 'device': 'sda1'})
|
|
rb.add_dev({'id': 5, 'region': 1, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10005, 'device': 'sda1'})
|
|
|
|
rb.rebalance(seed=1)
|
|
# We want a partition where 1 replica is on a removed device, 1
|
|
# replica is on a 0-weight device, and 1 on a normal device. To
|
|
# guarantee we have one, we see where partition 123 is, then
|
|
# manipulate its devices accordingly.
|
|
zero_weight_dev_id = rb._replica2part2dev[1][123]
|
|
delete_dev_id = rb._replica2part2dev[2][123]
|
|
|
|
rb.set_dev_weight(zero_weight_dev_id, 0.0)
|
|
rb.remove_dev(delete_dev_id)
|
|
rb.rebalance()
|
|
|
|
def test_set_replicas(self):
|
|
rb = ring.RingBuilder(8, 3.2, 1)
|
|
rb.devs_changed = False
|
|
rb.set_replicas(3.25)
|
|
self.assertTrue(rb.devs_changed)
|
|
|
|
rb.devs_changed = False
|
|
rb.set_replicas(3.2500001)
|
|
self.assertFalse(rb.devs_changed)
|
|
|
|
def test_add_dev(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
dev = {'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000}
|
|
dev_id = rb.add_dev(dev)
|
|
self.assertRaises(exceptions.DuplicateDeviceError, rb.add_dev, dev)
|
|
self.assertEqual(dev_id, 0)
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
# test add new dev with no id
|
|
dev_id = rb.add_dev({'zone': 0, 'region': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 6000})
|
|
self.assertEqual(rb.devs[0]['id'], 0)
|
|
self.assertEqual(dev_id, 0)
|
|
# test add another dev with no id
|
|
dev_id = rb.add_dev({'zone': 3, 'region': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 6000})
|
|
self.assertEqual(rb.devs[1]['id'], 1)
|
|
self.assertEqual(dev_id, 1)
|
|
|
|
def test_set_dev_weight(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
rb.rebalance()
|
|
r = rb.get_ring()
|
|
counts = {}
|
|
for part2dev_id in r._replica2part2dev_id:
|
|
for dev_id in part2dev_id:
|
|
counts[dev_id] = counts.get(dev_id, 0) + 1
|
|
self.assertEqual(counts, {0: 128, 1: 128, 2: 256, 3: 256})
|
|
rb.set_dev_weight(0, 0.75)
|
|
rb.set_dev_weight(1, 0.25)
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance()
|
|
r = rb.get_ring()
|
|
counts = {}
|
|
for part2dev_id in r._replica2part2dev_id:
|
|
for dev_id in part2dev_id:
|
|
counts[dev_id] = counts.get(dev_id, 0) + 1
|
|
self.assertEqual(counts, {0: 192, 1: 64, 2: 256, 3: 256})
|
|
|
|
def test_remove_dev(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 3, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
rb.rebalance()
|
|
r = rb.get_ring()
|
|
counts = {}
|
|
for part2dev_id in r._replica2part2dev_id:
|
|
for dev_id in part2dev_id:
|
|
counts[dev_id] = counts.get(dev_id, 0) + 1
|
|
self.assertEqual(counts, {0: 192, 1: 192, 2: 192, 3: 192})
|
|
rb.remove_dev(1)
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance()
|
|
r = rb.get_ring()
|
|
counts = {}
|
|
for part2dev_id in r._replica2part2dev_id:
|
|
for dev_id in part2dev_id:
|
|
counts[dev_id] = counts.get(dev_id, 0) + 1
|
|
self.assertEqual(counts, {0: 256, 2: 256, 3: 256})
|
|
|
|
def test_remove_a_lot(self):
|
|
rb = ring.RingBuilder(3, 3, 1)
|
|
rb.add_dev({'id': 0, 'device': 'd0', 'ip': '10.0.0.1',
|
|
'port': 6002, 'weight': 1000.0, 'region': 0, 'zone': 1})
|
|
rb.add_dev({'id': 1, 'device': 'd1', 'ip': '10.0.0.2',
|
|
'port': 6002, 'weight': 1000.0, 'region': 0, 'zone': 2})
|
|
rb.add_dev({'id': 2, 'device': 'd2', 'ip': '10.0.0.3',
|
|
'port': 6002, 'weight': 1000.0, 'region': 0, 'zone': 3})
|
|
rb.add_dev({'id': 3, 'device': 'd3', 'ip': '10.0.0.1',
|
|
'port': 6002, 'weight': 1000.0, 'region': 0, 'zone': 1})
|
|
rb.add_dev({'id': 4, 'device': 'd4', 'ip': '10.0.0.2',
|
|
'port': 6002, 'weight': 1000.0, 'region': 0, 'zone': 2})
|
|
rb.add_dev({'id': 5, 'device': 'd5', 'ip': '10.0.0.3',
|
|
'port': 6002, 'weight': 1000.0, 'region': 0, 'zone': 3})
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
# this has to put more than 1/3 of the partitions in the
|
|
# cluster on removed devices in order to ensure that at least
|
|
# one partition has multiple replicas that need to move.
|
|
#
|
|
# (for an N-replica ring, it's more than 1/N of the
|
|
# partitions, of course)
|
|
rb.remove_dev(3)
|
|
rb.remove_dev(4)
|
|
rb.remove_dev(5)
|
|
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
def test_remove_zero_weighted(self):
|
|
rb = ring.RingBuilder(8, 3, 0)
|
|
rb.add_dev({'id': 0, 'device': 'd0', 'ip': '10.0.0.1',
|
|
'port': 6002, 'weight': 1000.0, 'region': 0, 'zone': 1})
|
|
rb.add_dev({'id': 1, 'device': 'd1', 'ip': '10.0.0.2',
|
|
'port': 6002, 'weight': 0.0, 'region': 0, 'zone': 2})
|
|
rb.add_dev({'id': 2, 'device': 'd2', 'ip': '10.0.0.3',
|
|
'port': 6002, 'weight': 1000.0, 'region': 0, 'zone': 3})
|
|
rb.add_dev({'id': 3, 'device': 'd3', 'ip': '10.0.0.1',
|
|
'port': 6002, 'weight': 1000.0, 'region': 0, 'zone': 1})
|
|
rb.rebalance()
|
|
|
|
rb.remove_dev(1)
|
|
parts, balance, removed = rb.rebalance()
|
|
self.assertEqual(removed, 1)
|
|
|
|
def test_shuffled_gather(self):
|
|
if self._shuffled_gather_helper() and \
|
|
self._shuffled_gather_helper():
|
|
raise AssertionError('It is highly likely the ring is no '
|
|
'longer shuffling the set of partitions '
|
|
'to reassign on a rebalance.')
|
|
|
|
def _shuffled_gather_helper(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
rb.rebalance()
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 3, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
replica_plan = rb._build_replica_plan()
|
|
rb._set_parts_wanted(replica_plan)
|
|
for dev in rb._iter_devs():
|
|
dev['tiers'] = utils.tiers_for_dev(dev)
|
|
assign_parts = defaultdict(list)
|
|
rb._gather_parts_for_balance(assign_parts, replica_plan)
|
|
max_run = 0
|
|
run = 0
|
|
last_part = 0
|
|
for part, _ in assign_parts.items():
|
|
if part > last_part:
|
|
run += 1
|
|
else:
|
|
if run > max_run:
|
|
max_run = run
|
|
run = 0
|
|
last_part = part
|
|
if run > max_run:
|
|
max_run = run
|
|
return max_run > len(assign_parts) / 2
|
|
|
|
def test_initial_balance(self):
|
|
# 2 boxes, 2 drives each in zone 1
|
|
# 1 box, 2 drives in zone 2
|
|
#
|
|
# This is balanceable, but there used to be some nondeterminism in
|
|
# rebalance() that would sometimes give you an imbalanced ring.
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'region': 1, 'zone': 1, 'weight': 4000.0,
|
|
'ip': '10.1.1.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'region': 1, 'zone': 1, 'weight': 4000.0,
|
|
'ip': '10.1.1.1', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'region': 1, 'zone': 1, 'weight': 4000.0,
|
|
'ip': '10.1.1.2', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'region': 1, 'zone': 1, 'weight': 4000.0,
|
|
'ip': '10.1.1.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'region': 1, 'zone': 2, 'weight': 4000.0,
|
|
'ip': '10.1.1.3', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'region': 1, 'zone': 2, 'weight': 4000.0,
|
|
'ip': '10.1.1.3', 'port': 10000, 'device': 'sdb'})
|
|
|
|
_, balance, _ = rb.rebalance(seed=2)
|
|
|
|
# maybe not *perfect*, but should be close
|
|
self.assertTrue(balance <= 1)
|
|
|
|
def test_multitier_partial(self):
|
|
# Multitier test, nothing full
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 1, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 2, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 3, 'region': 3, 'zone': 3, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
for part in range(rb.parts):
|
|
counts = defaultdict(lambda: defaultdict(int))
|
|
for replica in range(rb.replicas):
|
|
dev = rb.devs[rb._replica2part2dev[replica][part]]
|
|
counts['region'][dev['region']] += 1
|
|
counts['zone'][dev['zone']] += 1
|
|
|
|
if any(c > 1 for c in counts['region'].values()):
|
|
raise AssertionError(
|
|
"Partition %d not evenly region-distributed (got %r)" %
|
|
(part, counts['region']))
|
|
if any(c > 1 for c in counts['zone'].values()):
|
|
raise AssertionError(
|
|
"Partition %d not evenly zone-distributed (got %r)" %
|
|
(part, counts['zone']))
|
|
|
|
# Multitier test, zones full, nodes not full
|
|
rb = ring.RingBuilder(8, 6, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdd'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sde'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdf'})
|
|
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sdg'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sdh'})
|
|
rb.add_dev({'id': 8, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sdi'})
|
|
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
for part in range(rb.parts):
|
|
counts = defaultdict(lambda: defaultdict(int))
|
|
for replica in range(rb.replicas):
|
|
dev = rb.devs[rb._replica2part2dev[replica][part]]
|
|
counts['zone'][dev['zone']] += 1
|
|
counts['dev_id'][dev['id']] += 1
|
|
if counts['zone'] != {0: 2, 1: 2, 2: 2}:
|
|
raise AssertionError(
|
|
"Partition %d not evenly distributed (got %r)" %
|
|
(part, counts['zone']))
|
|
for dev_id, replica_count in counts['dev_id'].items():
|
|
if replica_count > 1:
|
|
raise AssertionError(
|
|
"Partition %d is on device %d more than once (%r)" %
|
|
(part, dev_id, counts['dev_id']))
|
|
|
|
def test_multitier_full(self):
|
|
# Multitier test, #replicas == #devs
|
|
rb = ring.RingBuilder(8, 6, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdd'})
|
|
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sde'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdf'})
|
|
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
for part in range(rb.parts):
|
|
counts = defaultdict(lambda: defaultdict(int))
|
|
for replica in range(rb.replicas):
|
|
dev = rb.devs[rb._replica2part2dev[replica][part]]
|
|
counts['zone'][dev['zone']] += 1
|
|
counts['dev_id'][dev['id']] += 1
|
|
if counts['zone'] != {0: 2, 1: 2, 2: 2}:
|
|
raise AssertionError(
|
|
"Partition %d not evenly distributed (got %r)" %
|
|
(part, counts['zone']))
|
|
for dev_id, replica_count in counts['dev_id'].items():
|
|
if replica_count != 1:
|
|
raise AssertionError(
|
|
"Partition %d is on device %d %d times, not 1 (%r)" %
|
|
(part, dev_id, replica_count, counts['dev_id']))
|
|
|
|
def test_multitier_overfull(self):
|
|
# Multitier test, #replicas > #zones (to prove even distribution)
|
|
rb = ring.RingBuilder(8, 8, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdg'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdd'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdh'})
|
|
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sde'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdf'})
|
|
rb.add_dev({'id': 8, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdi'})
|
|
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
for part in range(rb.parts):
|
|
counts = defaultdict(lambda: defaultdict(int))
|
|
for replica in range(rb.replicas):
|
|
dev = rb.devs[rb._replica2part2dev[replica][part]]
|
|
counts['zone'][dev['zone']] += 1
|
|
counts['dev_id'][dev['id']] += 1
|
|
|
|
self.assertEqual(8, sum(counts['zone'].values()))
|
|
for zone, replica_count in counts['zone'].items():
|
|
if replica_count not in (2, 3):
|
|
raise AssertionError(
|
|
"Partition %d not evenly distributed (got %r)" %
|
|
(part, counts['zone']))
|
|
for dev_id, replica_count in counts['dev_id'].items():
|
|
if replica_count not in (1, 2):
|
|
raise AssertionError(
|
|
"Partition %d is on device %d %d times, "
|
|
"not 1 or 2 (%r)" %
|
|
(part, dev_id, replica_count, counts['dev_id']))
|
|
|
|
def test_multitier_expansion_more_devices(self):
|
|
rb = ring.RingBuilder(8, 6, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 8, 'region': 0, 'zone': 2, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sde'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdf'})
|
|
rb.add_dev({'id': 9, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
rb.add_dev({'id': 10, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sde'})
|
|
rb.add_dev({'id': 11, 'region': 0, 'zone': 2, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdf'})
|
|
|
|
for _ in range(5):
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
for part in range(rb.parts):
|
|
counts = dict(zone=defaultdict(int),
|
|
dev_id=defaultdict(int))
|
|
for replica in range(rb.replicas):
|
|
dev = rb.devs[rb._replica2part2dev[replica][part]]
|
|
counts['zone'][dev['zone']] += 1
|
|
counts['dev_id'][dev['id']] += 1
|
|
|
|
self.assertEqual({0: 2, 1: 2, 2: 2}, dict(counts['zone']))
|
|
# each part is assigned once to six unique devices
|
|
self.assertEqual((counts['dev_id'].values()), [1] * 6)
|
|
self.assertEqual(len(set(counts['dev_id'].keys())), 6)
|
|
|
|
def test_multitier_part_moves_with_0_min_part_hours(self):
|
|
rb = ring.RingBuilder(8, 3, 0)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd1'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sde1'})
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
# min_part_hours is 0, so we're clear to move 2 replicas to
|
|
# new devs
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc1'})
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
for part in range(rb.parts):
|
|
devs = set()
|
|
for replica in range(rb.replicas):
|
|
devs.add(rb._replica2part2dev[replica][part])
|
|
|
|
if len(devs) != 3:
|
|
raise AssertionError(
|
|
"Partition %d not on 3 devs (got %r)" % (part, devs))
|
|
|
|
def test_multitier_part_moves_with_positive_min_part_hours(self):
|
|
rb = ring.RingBuilder(8, 3, 99)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd1'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sde1'})
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
# min_part_hours is >0, so we'll only be able to move 1
|
|
# replica to a new home
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc1'})
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
for part in range(rb.parts):
|
|
devs = set()
|
|
for replica in range(rb.replicas):
|
|
devs.add(rb._replica2part2dev[replica][part])
|
|
if not any(rb.devs[dev_id]['zone'] == 1 for dev_id in devs):
|
|
raise AssertionError(
|
|
"Partition %d did not move (got %r)" % (part, devs))
|
|
|
|
def test_multitier_dont_move_too_many_replicas(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
# there'll be at least one replica in z0 and z1
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb1'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb1'})
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
# only 1 replica should move
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd1'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 3, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sde1'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 4, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdf1'})
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
for part in range(rb.parts):
|
|
zones = set()
|
|
for replica in range(rb.replicas):
|
|
zones.add(rb.devs[rb._replica2part2dev[replica][part]]['zone'])
|
|
|
|
if len(zones) != 3:
|
|
raise AssertionError(
|
|
"Partition %d not in 3 zones (got %r)" % (part, zones))
|
|
if 0 not in zones or 1 not in zones:
|
|
raise AssertionError(
|
|
"Partition %d not in zones 0 and 1 (got %r)" %
|
|
(part, zones))
|
|
|
|
def test_min_part_hours_zero_will_move_whatever_it_takes(self):
|
|
rb = ring.RingBuilder(8, 3, 0)
|
|
# there'll be at least one replica in z0 and z1
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb1'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb1'})
|
|
rb.rebalance(seed=1)
|
|
rb.validate()
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd1'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 3, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sde1'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 4, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdf1'})
|
|
rb.rebalance(seed=3)
|
|
rb.validate()
|
|
|
|
self.assertEqual(0, rb.dispersion)
|
|
# a balance of w/i a 1% isn't too bad for 3 replicas on 7
|
|
# devices when part power is only 8
|
|
self.assertAlmostEqual(rb.get_balance(), 0, delta=0.5)
|
|
|
|
# every zone has either 153 or 154 parts
|
|
for zone, count in self._partition_counts(
|
|
rb, key='zone').items():
|
|
self.assertAlmostEqual(153.5, count, delta=1)
|
|
|
|
parts_with_moved_count = defaultdict(int)
|
|
for part in range(rb.parts):
|
|
zones = set()
|
|
for replica in range(rb.replicas):
|
|
zones.add(rb.devs[rb._replica2part2dev[replica][part]]['zone'])
|
|
moved_replicas = len(zones - {0, 1})
|
|
parts_with_moved_count[moved_replicas] += 1
|
|
|
|
# as usual, the real numbers depend on the seed, but we want to
|
|
# validate a few things here:
|
|
#
|
|
# 1) every part had to move one replica to hit dispersion (so no
|
|
# one can have a moved count 0)
|
|
#
|
|
# 2) it's quite reasonable that some small percent of parts will
|
|
# have a replica in {0, 1, X} (meaning only one replica of the
|
|
# part moved)
|
|
#
|
|
# 3) when min_part_hours is 0, more than one replica of a part
|
|
# can move in a rebalance, and since that movement would get to
|
|
# better dispersion faster we expect to observe most parts in
|
|
# {[0,1], X, X} (meaning *two* replicas of the part moved)
|
|
#
|
|
# 4) there's plenty of weight in z0 & z1 to hold a whole
|
|
# replicanth, so there is no reason for any part to have to move
|
|
# all three replicas out of those zones (meaning no one can have
|
|
# a moved count 3)
|
|
#
|
|
expected = {
|
|
1: 52,
|
|
2: 204,
|
|
}
|
|
self.assertEqual(parts_with_moved_count, expected)
|
|
|
|
def test_rerebalance(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
rb.rebalance()
|
|
counts = self._partition_counts(rb)
|
|
self.assertEqual(counts, {0: 256, 1: 256, 2: 256})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 3, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance()
|
|
counts = self._partition_counts(rb)
|
|
self.assertEqual(counts, {0: 192, 1: 192, 2: 192, 3: 192})
|
|
rb.set_dev_weight(3, 100)
|
|
rb.rebalance()
|
|
counts = self._partition_counts(rb)
|
|
self.assertEqual(counts[3], 256)
|
|
|
|
def test_add_rebalance_add_rebalance_delete_rebalance(self):
|
|
# Test for https://bugs.launchpad.net/swift/+bug/845952
|
|
# min_part of 0 to allow for rapid rebalancing
|
|
rb = ring.RingBuilder(8, 3, 0)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10004, 'device': 'sda1'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10005, 'device': 'sda1'})
|
|
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
rb.remove_dev(1)
|
|
|
|
# well now we have only one device in z0
|
|
rb.set_overload(0.5)
|
|
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
def test_remove_last_partition_from_zero_weight(self):
|
|
rb = ring.RingBuilder(4, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 1, 'weight': 1.0,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 2, 'weight': 1.0,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 2, 'weight': 1.0,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 3, 'weight': 1.0,
|
|
'ip': '127.0.0.3', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 3, 'weight': 1.0,
|
|
'ip': '127.0.0.3', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 3, 'weight': 1.0,
|
|
'ip': '127.0.0.3', 'port': 10000, 'device': 'sdc'})
|
|
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 3, 'weight': 0.4,
|
|
'ip': '127.0.0.3', 'port': 10001, 'device': 'zero'})
|
|
|
|
zero_weight_dev = 3
|
|
|
|
rb.rebalance(seed=1)
|
|
|
|
# We want at least one partition with replicas only in zone 2 and 3
|
|
# due to device weights. It would *like* to spread out into zone 1,
|
|
# but can't, due to device weight.
|
|
#
|
|
# Also, we want such a partition to have a replica on device 3,
|
|
# which we will then reduce to zero weight. This should cause the
|
|
# removal of the replica from device 3.
|
|
#
|
|
# Getting this to happen by chance is hard, so let's just set up a
|
|
# builder so that it's in the state we want. This is a synthetic
|
|
# example; while the bug has happened on a real cluster, that
|
|
# builder file had a part_power of 16, so its contents are much too
|
|
# big to include here.
|
|
rb._replica2part2dev = [
|
|
# these are the relevant ones
|
|
# | | |
|
|
# v v v
|
|
array('H', [2, 5, 6, 2, 5, 6, 2, 5, 6, 2, 5, 6, 2, 5, 6, 2]),
|
|
array('H', [1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4]),
|
|
array('H', [0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 5, 6, 2, 5, 6])]
|
|
|
|
# fix up bookkeeping
|
|
new_dev_parts = defaultdict(int)
|
|
for part2dev_id in rb._replica2part2dev:
|
|
for dev_id in part2dev_id:
|
|
new_dev_parts[dev_id] += 1
|
|
for dev in rb._iter_devs():
|
|
dev['parts'] = new_dev_parts[dev['id']]
|
|
|
|
rb.set_dev_weight(zero_weight_dev, 0.0)
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=1)
|
|
|
|
node_counts = defaultdict(int)
|
|
for part2dev_id in rb._replica2part2dev:
|
|
for dev_id in part2dev_id:
|
|
node_counts[dev_id] += 1
|
|
|
|
self.assertEqual(node_counts[zero_weight_dev], 0)
|
|
|
|
# it's as balanced as it gets, so nothing moves anymore
|
|
rb.pretend_min_part_hours_passed()
|
|
parts_moved, _balance, _removed = rb.rebalance(seed=1)
|
|
|
|
new_node_counts = defaultdict(int)
|
|
for part2dev_id in rb._replica2part2dev:
|
|
for dev_id in part2dev_id:
|
|
new_node_counts[dev_id] += 1
|
|
|
|
del node_counts[zero_weight_dev]
|
|
self.assertEqual(node_counts, new_node_counts)
|
|
|
|
self.assertEqual(parts_moved, 0)
|
|
|
|
def test_part_swapping_problem(self):
|
|
rb = ring.RingBuilder(4, 3, 1)
|
|
# 127.0.0.1 (2 devs)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
# 127.0.0.2 (3 devs)
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdc'})
|
|
|
|
expected = {
|
|
'127.0.0.1': 1.2,
|
|
'127.0.0.2': 1.7999999999999998,
|
|
}
|
|
for wr in (rb._build_weighted_replicas_by_tier(),
|
|
rb._build_wanted_replicas_by_tier(),
|
|
rb._build_target_replicas_by_tier()):
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
wr.items() if len(t) == 3})
|
|
self.assertEqual(rb.get_required_overload(), 0)
|
|
rb.rebalance(seed=3)
|
|
# so 127.0.0.1 ended up with...
|
|
tier = (0, 0, '127.0.0.1')
|
|
# ... 6 parts with 1 replicas
|
|
self.assertEqual(rb._dispersion_graph[tier][1], 12)
|
|
# ... 4 parts with 2 replicas
|
|
self.assertEqual(rb._dispersion_graph[tier][2], 4)
|
|
# but since we only have two tiers, this is *totally* dispersed
|
|
self.assertEqual(0, rb.dispersion)
|
|
|
|
# small rings are hard to balance...
|
|
expected = {0: 10, 1: 10, 2: 10, 3: 9, 4: 9}
|
|
self.assertEqual(expected, {d['id']: d['parts']
|
|
for d in rb._iter_devs()})
|
|
# everyone wants 9.6 parts
|
|
expected = {
|
|
0: 4.166666666666671,
|
|
1: 4.166666666666671,
|
|
2: 4.166666666666671,
|
|
3: -6.25,
|
|
4: -6.25,
|
|
}
|
|
self.assertEqual(expected, rb._build_balance_per_dev())
|
|
|
|
# original sorted _replica2part2dev
|
|
"""
|
|
rb._replica2part2dev = [
|
|
array('H', [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1]),
|
|
array('H', [1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 2, 2, 2, 3, 3, 3]),
|
|
array('H', [2, 2, 2, 2, 3, 3, 4, 4, 4, 4, 3, 4, 4, 4, 4, 4])]
|
|
"""
|
|
|
|
# now imagine if we came along this _replica2part2dev through no
|
|
# fault of our own; if instead of the 12 parts with only one
|
|
# replica on 127.0.0.1 being split evenly (6 and 6) on device's
|
|
# 0 and 1 - device 1 inexplicitly had 3 extra parts
|
|
rb._replica2part2dev = [
|
|
# these are the relevant one's here
|
|
# | | |
|
|
# v v v
|
|
array('H', [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]),
|
|
array('H', [1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 2, 2, 2, 3, 3, 3]),
|
|
array('H', [2, 2, 2, 2, 3, 3, 4, 4, 4, 4, 3, 4, 4, 4, 4, 4])]
|
|
|
|
# fix up bookkeeping
|
|
new_dev_parts = defaultdict(int)
|
|
for part2dev_id in rb._replica2part2dev:
|
|
for dev_id in part2dev_id:
|
|
new_dev_parts[dev_id] += 1
|
|
for dev in rb._iter_devs():
|
|
dev['parts'] = new_dev_parts[dev['id']]
|
|
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance()
|
|
expected = {
|
|
0: 4.166666666666671,
|
|
1: 4.166666666666671,
|
|
2: 4.166666666666671,
|
|
3: -6.25,
|
|
4: -6.25,
|
|
}
|
|
self.assertEqual(expected, rb._build_balance_per_dev())
|
|
|
|
self.assertEqual(rb.get_balance(), 6.25)
|
|
|
|
def test_wrong_tier_with_no_where_to_go(self):
|
|
rb = ring.RingBuilder(4, 3, 1)
|
|
|
|
# 127.0.0.1 (even devices)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 900,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 0, 'weight': 900,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 0, 'weight': 900,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
# 127.0.0.2 (odd devices)
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdd'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdd'})
|
|
|
|
expected = {
|
|
'127.0.0.1': 1.75,
|
|
'127.0.0.2': 1.25,
|
|
}
|
|
for wr in (rb._build_weighted_replicas_by_tier(),
|
|
rb._build_wanted_replicas_by_tier(),
|
|
rb._build_target_replicas_by_tier()):
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
wr.items() if len(t) == 3})
|
|
self.assertEqual(rb.get_required_overload(), 0)
|
|
rb.rebalance(seed=3)
|
|
# so 127.0.0.1 ended up with...
|
|
tier = (0, 0, '127.0.0.1')
|
|
# ... 4 parts with 1 replicas
|
|
self.assertEqual(rb._dispersion_graph[tier][1], 4)
|
|
# ... 12 parts with 2 replicas
|
|
self.assertEqual(rb._dispersion_graph[tier][2], 12)
|
|
# ... and of course 0 parts with 3 replicas
|
|
self.assertEqual(rb._dispersion_graph[tier][3], 0)
|
|
# but since we only have two tiers, this is *totally* dispersed
|
|
self.assertEqual(0, rb.dispersion)
|
|
|
|
# small rings are hard to balance, but it's possible when
|
|
# part-replicas (3 * 2 ** 4) can go evenly into device weights
|
|
# (4800) like we've done here
|
|
expected = {
|
|
0: 1,
|
|
2: 9,
|
|
4: 9,
|
|
6: 9,
|
|
1: 5,
|
|
3: 5,
|
|
5: 5,
|
|
7: 5,
|
|
}
|
|
self.assertEqual(expected, {d['id']: d['parts']
|
|
for d in rb._iter_devs()})
|
|
expected = {
|
|
0: 0.0,
|
|
1: 0.0,
|
|
2: 0.0,
|
|
3: 0.0,
|
|
4: 0.0,
|
|
5: 0.0,
|
|
6: 0.0,
|
|
7: 0.0,
|
|
}
|
|
self.assertEqual(expected, rb._build_balance_per_dev())
|
|
|
|
# all devices have exactly the # of parts they want
|
|
expected = {
|
|
0: 0,
|
|
2: 0,
|
|
4: 0,
|
|
6: 0,
|
|
1: 0,
|
|
3: 0,
|
|
5: 0,
|
|
7: 0,
|
|
}
|
|
self.assertEqual(expected, {d['id']: d['parts_wanted']
|
|
for d in rb._iter_devs()})
|
|
|
|
# original sorted _replica2part2dev
|
|
"""
|
|
rb._replica2part2dev = [
|
|
array('H', [0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, 4, 4, 4, ]),
|
|
array('H', [4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 1, 1, 1, 1, ]),
|
|
array('H', [1, 3, 3, 3, 3, 3, 5, 5, 5, 5, 5, 7, 7, 7, 7, 7, ])]
|
|
"""
|
|
# now imagine if we came along this _replica2part2dev through no
|
|
# fault of our own; and device 0 had extra parts, but both
|
|
# copies of the other replicas were already in the other tier!
|
|
rb._replica2part2dev = [
|
|
# these are the relevant one's here
|
|
# | |
|
|
# v v
|
|
array('H', [2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, 4, 4, 0, 0]),
|
|
array('H', [4, 4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6, 1, 1, 1]),
|
|
array('H', [1, 1, 3, 3, 3, 3, 5, 5, 5, 5, 5, 7, 7, 7, 7, 7])]
|
|
|
|
# fix up bookkeeping
|
|
new_dev_parts = defaultdict(int)
|
|
for part2dev_id in rb._replica2part2dev:
|
|
for dev_id in part2dev_id:
|
|
new_dev_parts[dev_id] += 1
|
|
for dev in rb._iter_devs():
|
|
dev['parts'] = new_dev_parts[dev['id']]
|
|
replica_plan = rb._build_replica_plan()
|
|
rb._set_parts_wanted(replica_plan)
|
|
|
|
expected = {
|
|
0: -1, # this device wants to shed
|
|
2: 0,
|
|
4: 0,
|
|
6: 0,
|
|
1: 0,
|
|
3: 1, # there's devices with room on the other server
|
|
5: 0,
|
|
7: 0,
|
|
}
|
|
self.assertEqual(expected, {d['id']: d['parts_wanted']
|
|
for d in rb._iter_devs()})
|
|
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance()
|
|
self.assertEqual(rb.get_balance(), 0)
|
|
|
|
def test_region_fullness_with_balanceable_ring(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 1, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
rb.add_dev({'id': 3, 'region': 1, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10004, 'device': 'sda1'})
|
|
|
|
rb.add_dev({'id': 4, 'region': 2, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10005, 'device': 'sda1'})
|
|
rb.add_dev({'id': 5, 'region': 2, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10006, 'device': 'sda1'})
|
|
|
|
rb.add_dev({'id': 6, 'region': 3, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10007, 'device': 'sda1'})
|
|
rb.add_dev({'id': 7, 'region': 3, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10008, 'device': 'sda1'})
|
|
rb.rebalance(seed=2)
|
|
|
|
population_by_region = self._get_population_by_region(rb)
|
|
self.assertEqual(population_by_region,
|
|
{0: 192, 1: 192, 2: 192, 3: 192})
|
|
|
|
def test_region_fullness_with_unbalanceable_ring(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 2,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 2,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 1, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
rb.add_dev({'id': 3, 'region': 1, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10004, 'device': 'sda1'})
|
|
rb.rebalance(seed=2)
|
|
|
|
population_by_region = self._get_population_by_region(rb)
|
|
self.assertEqual(population_by_region, {0: 512, 1: 256})
|
|
|
|
def test_adding_region_slowly_with_unbalanceable_ring(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb1'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc1'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 0, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd1'})
|
|
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdb1'})
|
|
rb.add_dev({'id': 8, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdc1'})
|
|
rb.add_dev({'id': 9, 'region': 0, 'zone': 1, 'weight': 0.5,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdd1'})
|
|
rb.rebalance(seed=2)
|
|
|
|
rb.add_dev({'id': 2, 'region': 1, 'zone': 0, 'weight': 0.25,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
rb.add_dev({'id': 3, 'region': 1, 'zone': 1, 'weight': 0.25,
|
|
'ip': '127.0.0.1', 'port': 10004, 'device': 'sda1'})
|
|
rb.pretend_min_part_hours_passed()
|
|
changed_parts, _balance, _removed = rb.rebalance(seed=2)
|
|
|
|
# there's not enough room in r1 for every partition to have a replica
|
|
# in it, so only 86 assignments occur in r1 (that's ~1/5 of the total,
|
|
# since r1 has 1/5 of the weight).
|
|
population_by_region = self._get_population_by_region(rb)
|
|
self.assertEqual(population_by_region, {0: 682, 1: 86})
|
|
|
|
# really 86 parts *should* move (to the new region) but to avoid
|
|
# accidentally picking up too many and causing some parts to randomly
|
|
# flop around devices in the original region - our gather algorithm
|
|
# is conservative when picking up only from devices that are for sure
|
|
# holding more parts than they want (math.ceil() of the replica_plan)
|
|
# which guarantees any parts picked up will have new homes in a better
|
|
# tier or failure_domain.
|
|
self.assertEqual(86, changed_parts)
|
|
|
|
# and since there's not enough room, subsequent rebalances will not
|
|
# cause additional assignments to r1
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=2)
|
|
rb.validate()
|
|
population_by_region = self._get_population_by_region(rb)
|
|
self.assertEqual(population_by_region, {0: 682, 1: 86})
|
|
|
|
# after you add more weight, more partition assignments move
|
|
rb.set_dev_weight(2, 0.5)
|
|
rb.set_dev_weight(3, 0.5)
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=2)
|
|
rb.validate()
|
|
population_by_region = self._get_population_by_region(rb)
|
|
self.assertEqual(population_by_region, {0: 614, 1: 154})
|
|
|
|
rb.set_dev_weight(2, 1.0)
|
|
rb.set_dev_weight(3, 1.0)
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=2)
|
|
rb.validate()
|
|
population_by_region = self._get_population_by_region(rb)
|
|
self.assertEqual(population_by_region, {0: 512, 1: 256})
|
|
|
|
def test_avoid_tier_change_new_region(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
for i in range(5):
|
|
rb.add_dev({'id': i, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': i, 'device': 'sda1'})
|
|
rb.rebalance(seed=2)
|
|
|
|
# Add a new device in new region to a balanced ring
|
|
rb.add_dev({'id': 5, 'region': 1, 'zone': 0, 'weight': 0,
|
|
'ip': '127.0.0.5', 'port': 10000, 'device': 'sda1'})
|
|
|
|
# Increase the weight of region 1 slowly
|
|
moved_partitions = []
|
|
errors = []
|
|
for weight in range(0, 101, 10):
|
|
rb.set_dev_weight(5, weight)
|
|
rb.pretend_min_part_hours_passed()
|
|
changed_parts, _balance, _removed = rb.rebalance(seed=2)
|
|
rb.validate()
|
|
moved_partitions.append(changed_parts)
|
|
# Ensure that the second region has enough partitions
|
|
# Otherwise there will be replicas at risk
|
|
min_parts_for_r1 = ceil(weight / (500.0 + weight) * 768)
|
|
parts_for_r1 = self._get_population_by_region(rb).get(1, 0)
|
|
try:
|
|
self.assertEqual(min_parts_for_r1, parts_for_r1)
|
|
except AssertionError:
|
|
errors.append('weight %s got %s parts but expected %s' % (
|
|
weight, parts_for_r1, min_parts_for_r1))
|
|
|
|
self.assertFalse(errors)
|
|
|
|
# Number of partitions moved on each rebalance
|
|
# 10/510 * 768 ~ 15.06 -> move at least 15 partitions in first step
|
|
ref = [0, 16, 14, 14, 13, 13, 13, 12, 11, 12, 10]
|
|
self.assertEqual(ref, moved_partitions)
|
|
|
|
def test_set_replicas_increase(self):
|
|
rb = ring.RingBuilder(8, 2, 0)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
rb.replicas = 2.1
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
self.assertEqual([len(p2d) for p2d in rb._replica2part2dev],
|
|
[256, 256, 25])
|
|
|
|
rb.replicas = 2.2
|
|
rb.rebalance()
|
|
rb.validate()
|
|
self.assertEqual([len(p2d) for p2d in rb._replica2part2dev],
|
|
[256, 256, 51])
|
|
|
|
def test_set_replicas_decrease(self):
|
|
rb = ring.RingBuilder(4, 5, 0)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
rb.replicas = 4.9
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
self.assertEqual([len(p2d) for p2d in rb._replica2part2dev],
|
|
[16, 16, 16, 16, 14])
|
|
|
|
# cross a couple of integer thresholds (4 and 3)
|
|
rb.replicas = 2.5
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
self.assertEqual([len(p2d) for p2d in rb._replica2part2dev],
|
|
[16, 16, 8])
|
|
|
|
def test_fractional_replicas_rebalance(self):
|
|
rb = ring.RingBuilder(8, 2.5, 0)
|
|
rb.add_dev({'id': 0, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.rebalance() # passes by not crashing
|
|
rb.validate() # also passes by not crashing
|
|
self.assertEqual([len(p2d) for p2d in rb._replica2part2dev],
|
|
[256, 256, 128])
|
|
|
|
def test_create_add_dev_add_replica_rebalance(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'region': 0, 'zone': 0, 'weight': 3,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'region': 0, 'zone': 0, 'weight': 3,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'region': 0, 'zone': 0, 'weight': 3,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'region': 0, 'zone': 0, 'weight': 3,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.set_replicas(4)
|
|
rb.rebalance() # this would crash since parts_wanted was not set
|
|
rb.validate()
|
|
|
|
def test_rebalance_post_upgrade(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
# 5 devices: 5 is the smallest number that does not divide 3 * 2^8,
|
|
# which forces some rounding to happen.
|
|
rb.add_dev({'id': 0, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sde'})
|
|
rb.rebalance()
|
|
rb.validate()
|
|
|
|
# Older versions of the ring builder code would round down when
|
|
# computing parts_wanted, while the new code rounds up. Make sure we
|
|
# can handle a ring built by the old method.
|
|
#
|
|
# This code mimics the old _set_parts_wanted.
|
|
weight_of_one_part = rb.weight_of_one_part()
|
|
for dev in rb._iter_devs():
|
|
if not dev['weight']:
|
|
dev['parts_wanted'] = -rb.parts * rb.replicas
|
|
else:
|
|
dev['parts_wanted'] = (
|
|
int(weight_of_one_part * dev['weight']) -
|
|
dev['parts'])
|
|
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance() # this crashes unless rebalance resets parts_wanted
|
|
rb.validate()
|
|
|
|
def test_add_replicas_then_rebalance_respects_weight(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'region': 0, 'zone': 0, 'weight': 3,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'region': 0, 'zone': 0, 'weight': 3,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
|
|
rb.add_dev({'id': 4, 'region': 0, 'region': 0, 'zone': 1, 'weight': 3,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sde'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'region': 0, 'zone': 1, 'weight': 3,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdf'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdg'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdh'})
|
|
|
|
rb.add_dev({'id': 8, 'region': 0, 'region': 0, 'zone': 2, 'weight': 3,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdi'})
|
|
rb.add_dev({'id': 9, 'region': 0, 'region': 0, 'zone': 2, 'weight': 3,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdj'})
|
|
rb.add_dev({'id': 10, 'region': 0, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdk'})
|
|
rb.add_dev({'id': 11, 'region': 0, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdl'})
|
|
|
|
rb.rebalance(seed=1)
|
|
|
|
r = rb.get_ring()
|
|
counts = {}
|
|
for part2dev_id in r._replica2part2dev_id:
|
|
for dev_id in part2dev_id:
|
|
counts[dev_id] = counts.get(dev_id, 0) + 1
|
|
self.assertEqual(counts, {0: 96, 1: 96,
|
|
2: 32, 3: 32,
|
|
4: 96, 5: 96,
|
|
6: 32, 7: 32,
|
|
8: 96, 9: 96,
|
|
10: 32, 11: 32})
|
|
|
|
rb.replicas *= 2
|
|
rb.rebalance(seed=1)
|
|
|
|
r = rb.get_ring()
|
|
counts = {}
|
|
for part2dev_id in r._replica2part2dev_id:
|
|
for dev_id in part2dev_id:
|
|
counts[dev_id] = counts.get(dev_id, 0) + 1
|
|
self.assertEqual(counts, {0: 192, 1: 192,
|
|
2: 64, 3: 64,
|
|
4: 192, 5: 192,
|
|
6: 64, 7: 64,
|
|
8: 192, 9: 192,
|
|
10: 64, 11: 64})
|
|
|
|
def test_overload(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sde'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdf'})
|
|
|
|
rb.add_dev({'id': 1, 'region': 0, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdb'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdg'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdh'})
|
|
rb.add_dev({'id': 8, 'region': 0, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdi'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'region': 0, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.2', 'port': 10002, 'device': 'sdc'})
|
|
rb.add_dev({'id': 9, 'region': 0, 'region': 0, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.2', 'port': 10002, 'device': 'sdj'})
|
|
rb.add_dev({'id': 10, 'region': 0, 'region': 0, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.2', 'port': 10002, 'device': 'sdk'})
|
|
rb.add_dev({'id': 11, 'region': 0, 'region': 0, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.2', 'port': 10002, 'device': 'sdl'})
|
|
|
|
rb.rebalance(seed=12345)
|
|
rb.validate()
|
|
|
|
# sanity check: balance respects weights, so default
|
|
part_counts = self._partition_counts(rb, key='zone')
|
|
self.assertEqual(part_counts[0], 192)
|
|
self.assertEqual(part_counts[1], 192)
|
|
self.assertEqual(part_counts[2], 384)
|
|
|
|
# Devices 0 and 1 take 10% more than their fair shares by weight since
|
|
# overload is 10% (0.1).
|
|
rb.set_overload(0.1)
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance()
|
|
|
|
part_counts = self._partition_counts(rb, key='zone')
|
|
self.assertEqual(part_counts[0], 212)
|
|
self.assertEqual(part_counts[1], 211)
|
|
self.assertEqual(part_counts[2], 345)
|
|
|
|
# Now, devices 0 and 1 take 50% more than their fair shares by
|
|
# weight.
|
|
rb.set_overload(0.5)
|
|
for _ in range(3):
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=12345)
|
|
|
|
part_counts = self._partition_counts(rb, key='zone')
|
|
self.assertEqual(part_counts[0], 256)
|
|
self.assertEqual(part_counts[1], 256)
|
|
self.assertEqual(part_counts[2], 256)
|
|
|
|
# Devices 0 and 1 may take up to 75% over their fair share, but the
|
|
# placement algorithm only wants to spread things out evenly between
|
|
# all drives, so the devices stay at 50% more.
|
|
rb.set_overload(0.75)
|
|
for _ in range(3):
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=12345)
|
|
|
|
part_counts = self._partition_counts(rb, key='zone')
|
|
self.assertEqual(part_counts[0], 256)
|
|
self.assertEqual(part_counts[1], 256)
|
|
self.assertEqual(part_counts[2], 256)
|
|
|
|
def test_unoverload(self):
|
|
# Start off needing overload to balance, then add capacity until we
|
|
# don't need overload any more and see that things still balance.
|
|
# Overload doesn't prevent optimal balancing.
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.set_overload(0.125)
|
|
rb.add_dev({'id': 0, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
|
|
rb.add_dev({'id': 1, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 8, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'region': 0, 'zone': 0, 'weight': 2,
|
|
'ip': '127.0.0.3', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 9, 'region': 0, 'region': 0, 'zone': 0, 'weight': 2,
|
|
'ip': '127.0.0.3', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 10, 'region': 0, 'region': 0, 'zone': 0, 'weight': 2,
|
|
'ip': '127.0.0.3', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 11, 'region': 0, 'region': 0, 'zone': 0, 'weight': 2,
|
|
'ip': '127.0.0.3', 'port': 10000, 'device': 'sdc'})
|
|
rb.rebalance(seed=12345)
|
|
|
|
# sanity check: our overload is big enough to balance things
|
|
part_counts = self._partition_counts(rb, key='ip')
|
|
self.assertEqual(part_counts['127.0.0.1'], 216)
|
|
self.assertEqual(part_counts['127.0.0.2'], 216)
|
|
self.assertEqual(part_counts['127.0.0.3'], 336)
|
|
|
|
# Add some weight: balance improves
|
|
for dev in rb.devs:
|
|
if dev['ip'] in ('127.0.0.1', '127.0.0.2'):
|
|
rb.set_dev_weight(dev['id'], 1.22)
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=12345)
|
|
|
|
part_counts = self._partition_counts(rb, key='ip')
|
|
self.assertEqual(part_counts['127.0.0.1'], 238)
|
|
self.assertEqual(part_counts['127.0.0.2'], 237)
|
|
self.assertEqual(part_counts['127.0.0.3'], 293)
|
|
|
|
# Even out the weights: balance becomes perfect
|
|
for dev in rb.devs:
|
|
if dev['ip'] in ('127.0.0.1', '127.0.0.2'):
|
|
rb.set_dev_weight(dev['id'], 2)
|
|
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=12345)
|
|
|
|
part_counts = self._partition_counts(rb, key='ip')
|
|
self.assertEqual(part_counts['127.0.0.1'], 256)
|
|
self.assertEqual(part_counts['127.0.0.2'], 256)
|
|
self.assertEqual(part_counts['127.0.0.3'], 256)
|
|
|
|
# Add a new server: balance stays optimal
|
|
rb.add_dev({'id': 12, 'region': 0, 'region': 0, 'zone': 0,
|
|
'weight': 2,
|
|
'ip': '127.0.0.4', 'port': 10000, 'device': 'sdd'})
|
|
rb.add_dev({'id': 13, 'region': 0, 'region': 0, 'zone': 0,
|
|
'weight': 2,
|
|
'ip': '127.0.0.4', 'port': 10000, 'device': 'sde'})
|
|
rb.add_dev({'id': 14, 'region': 0, 'region': 0, 'zone': 0,
|
|
'weight': 2,
|
|
'ip': '127.0.0.4', 'port': 10000, 'device': 'sdf'})
|
|
rb.add_dev({'id': 15, 'region': 0, 'region': 0, 'zone': 0,
|
|
'weight': 2,
|
|
'ip': '127.0.0.4', 'port': 10000, 'device': 'sdf'})
|
|
|
|
# we're moving more than 1/3 of the replicas but fewer than 2/3, so
|
|
# we have to do this twice
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=12345)
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=12345)
|
|
|
|
expected = {
|
|
'127.0.0.1': 192,
|
|
'127.0.0.2': 192,
|
|
'127.0.0.3': 192,
|
|
'127.0.0.4': 192,
|
|
}
|
|
|
|
part_counts = self._partition_counts(rb, key='ip')
|
|
self.assertEqual(part_counts, expected)
|
|
|
|
def test_overload_keeps_balanceable_things_balanced_initially(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'region': 0, 'zone': 0, 'weight': 8,
|
|
'ip': '10.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'region': 0, 'zone': 0, 'weight': 8,
|
|
'ip': '10.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 4, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.3', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.3', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 6, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.4', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.4', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 8, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.5', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 9, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.5', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.set_overload(99999)
|
|
rb.rebalance(seed=12345)
|
|
|
|
part_counts = self._partition_counts(rb)
|
|
self.assertEqual(part_counts, {
|
|
0: 128,
|
|
1: 128,
|
|
2: 64,
|
|
3: 64,
|
|
4: 64,
|
|
5: 64,
|
|
6: 64,
|
|
7: 64,
|
|
8: 64,
|
|
9: 64,
|
|
})
|
|
|
|
def test_overload_keeps_balanceable_things_balanced_on_rebalance(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'region': 0, 'zone': 0, 'weight': 8,
|
|
'ip': '10.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'region': 0, 'zone': 0, 'weight': 8,
|
|
'ip': '10.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 4, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.3', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.3', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 6, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.4', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.4', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 8, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.5', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 9, 'region': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '10.0.0.5', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.set_overload(99999)
|
|
|
|
rb.rebalance(seed=123)
|
|
part_counts = self._partition_counts(rb)
|
|
self.assertEqual(part_counts, {
|
|
0: 128,
|
|
1: 128,
|
|
2: 64,
|
|
3: 64,
|
|
4: 64,
|
|
5: 64,
|
|
6: 64,
|
|
7: 64,
|
|
8: 64,
|
|
9: 64,
|
|
})
|
|
|
|
# swap weights between 10.0.0.1 and 10.0.0.2
|
|
rb.set_dev_weight(0, 4)
|
|
rb.set_dev_weight(1, 4)
|
|
rb.set_dev_weight(2, 8)
|
|
rb.set_dev_weight(1, 8)
|
|
|
|
rb.rebalance(seed=456)
|
|
part_counts = self._partition_counts(rb)
|
|
self.assertEqual(part_counts, {
|
|
0: 128,
|
|
1: 128,
|
|
2: 64,
|
|
3: 64,
|
|
4: 64,
|
|
5: 64,
|
|
6: 64,
|
|
7: 64,
|
|
8: 64,
|
|
9: 64,
|
|
})
|
|
|
|
def test_server_per_port(self):
|
|
# 3 servers, 3 disks each, with each disk on its own port
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '10.0.0.1', 'port': 10000, 'device': 'sdx'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '10.0.0.1', 'port': 10001, 'device': 'sdy'})
|
|
|
|
rb.add_dev({'id': 3, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '10.0.0.2', 'port': 10000, 'device': 'sdx'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '10.0.0.2', 'port': 10001, 'device': 'sdy'})
|
|
|
|
rb.add_dev({'id': 6, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '10.0.0.3', 'port': 10000, 'device': 'sdx'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '10.0.0.3', 'port': 10001, 'device': 'sdy'})
|
|
|
|
rb.rebalance(seed=1)
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '10.0.0.1', 'port': 10002, 'device': 'sdz'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '10.0.0.2', 'port': 10002, 'device': 'sdz'})
|
|
rb.add_dev({'id': 8, 'region': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '10.0.0.3', 'port': 10002, 'device': 'sdz'})
|
|
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=1)
|
|
|
|
poorly_dispersed = []
|
|
for part in range(rb.parts):
|
|
on_nodes = set()
|
|
for replica in range(rb.replicas):
|
|
dev_id = rb._replica2part2dev[replica][part]
|
|
on_nodes.add(rb.devs[dev_id]['ip'])
|
|
if len(on_nodes) < rb.replicas:
|
|
poorly_dispersed.append(part)
|
|
self.assertEqual(poorly_dispersed, [])
|
|
|
|
def test_load(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
devs = [{'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sda1',
|
|
'meta': 'meta0'},
|
|
{'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdb1',
|
|
'meta': 'meta1'},
|
|
{'id': 2, 'region': 0, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.2', 'port': 10002, 'device': 'sdc1',
|
|
'meta': 'meta2'},
|
|
{'id': 3, 'region': 0, 'zone': 3, 'weight': 2,
|
|
'ip': '127.0.0.3', 'port': 10003, 'device': 'sdd1'}]
|
|
for d in devs:
|
|
rb.add_dev(d)
|
|
rb.rebalance()
|
|
|
|
real_pickle = pickle.load
|
|
fake_open = mock.mock_open()
|
|
|
|
io_error_not_found = IOError()
|
|
io_error_not_found.errno = errno.ENOENT
|
|
|
|
io_error_no_perm = IOError()
|
|
io_error_no_perm.errno = errno.EPERM
|
|
|
|
io_error_generic = IOError()
|
|
io_error_generic.errno = errno.EOPNOTSUPP
|
|
try:
|
|
# test a legit builder
|
|
fake_pickle = mock.Mock(return_value=rb)
|
|
pickle.load = fake_pickle
|
|
builder = ring.RingBuilder.load('fake.builder', open=fake_open)
|
|
self.assertEqual(fake_pickle.call_count, 1)
|
|
fake_open.assert_has_calls([mock.call('fake.builder', 'rb')])
|
|
self.assertEqual(builder, rb)
|
|
fake_pickle.reset_mock()
|
|
|
|
# test old style builder
|
|
fake_pickle.return_value = rb.to_dict()
|
|
pickle.load = fake_pickle
|
|
builder = ring.RingBuilder.load('fake.builder', open=fake_open)
|
|
fake_open.assert_has_calls([mock.call('fake.builder', 'rb')])
|
|
self.assertEqual(builder.devs, rb.devs)
|
|
fake_pickle.reset_mock()
|
|
|
|
# test old devs but no meta
|
|
no_meta_builder = rb
|
|
for dev in no_meta_builder.devs:
|
|
del(dev['meta'])
|
|
fake_pickle.return_value = no_meta_builder
|
|
pickle.load = fake_pickle
|
|
builder = ring.RingBuilder.load('fake.builder', open=fake_open)
|
|
fake_open.assert_has_calls([mock.call('fake.builder', 'rb')])
|
|
self.assertEqual(builder.devs, rb.devs)
|
|
|
|
# test an empty builder
|
|
fake_pickle.side_effect = EOFError
|
|
pickle.load = fake_pickle
|
|
self.assertRaises(exceptions.UnPicklingError,
|
|
ring.RingBuilder.load, 'fake.builder',
|
|
open=fake_open)
|
|
|
|
# test a corrupted builder
|
|
fake_pickle.side_effect = pickle.UnpicklingError
|
|
pickle.load = fake_pickle
|
|
self.assertRaises(exceptions.UnPicklingError,
|
|
ring.RingBuilder.load, 'fake.builder',
|
|
open=fake_open)
|
|
|
|
# test some error
|
|
fake_pickle.side_effect = AttributeError
|
|
pickle.load = fake_pickle
|
|
self.assertRaises(exceptions.UnPicklingError,
|
|
ring.RingBuilder.load, 'fake.builder',
|
|
open=fake_open)
|
|
finally:
|
|
pickle.load = real_pickle
|
|
|
|
# test non existent builder file
|
|
fake_open.side_effect = io_error_not_found
|
|
self.assertRaises(exceptions.FileNotFoundError,
|
|
ring.RingBuilder.load, 'fake.builder',
|
|
open=fake_open)
|
|
|
|
# test non accessible builder file
|
|
fake_open.side_effect = io_error_no_perm
|
|
self.assertRaises(exceptions.PermissionError,
|
|
ring.RingBuilder.load, 'fake.builder',
|
|
open=fake_open)
|
|
|
|
# test an error other then ENOENT and ENOPERM
|
|
fake_open.side_effect = io_error_generic
|
|
self.assertRaises(IOError,
|
|
ring.RingBuilder.load, 'fake.builder',
|
|
open=fake_open)
|
|
|
|
def test_save_load(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
devs = [{'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.0', 'port': 10000,
|
|
'replication_ip': '127.0.0.0', 'replication_port': 10000,
|
|
'device': 'sda1', 'meta': 'meta0'},
|
|
{'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001,
|
|
'replication_ip': '127.0.0.1', 'replication_port': 10001,
|
|
'device': 'sdb1', 'meta': 'meta1'},
|
|
{'id': 2, 'region': 0, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.2', 'port': 10002,
|
|
'replication_ip': '127.0.0.2', 'replication_port': 10002,
|
|
'device': 'sdc1', 'meta': 'meta2'},
|
|
{'id': 3, 'region': 0, 'zone': 3, 'weight': 2,
|
|
'ip': '127.0.0.3', 'port': 10003,
|
|
'replication_ip': '127.0.0.3', 'replication_port': 10003,
|
|
'device': 'sdd1', 'meta': ''}]
|
|
rb.set_overload(3.14159)
|
|
for d in devs:
|
|
rb.add_dev(d)
|
|
rb.rebalance()
|
|
builder_file = os.path.join(self.testdir, 'test_save.builder')
|
|
rb.save(builder_file)
|
|
loaded_rb = ring.RingBuilder.load(builder_file)
|
|
self.maxDiff = None
|
|
self.assertEqual(loaded_rb.to_dict(), rb.to_dict())
|
|
self.assertEqual(loaded_rb.overload, 3.14159)
|
|
|
|
@mock.patch('six.moves.builtins.open', autospec=True)
|
|
@mock.patch('swift.common.ring.builder.pickle.dump', autospec=True)
|
|
def test_save(self, mock_pickle_dump, mock_open):
|
|
mock_open.return_value = mock_fh = mock.MagicMock()
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
devs = [{'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sda1',
|
|
'meta': 'meta0'},
|
|
{'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdb1',
|
|
'meta': 'meta1'},
|
|
{'id': 2, 'region': 0, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.2', 'port': 10002, 'device': 'sdc1',
|
|
'meta': 'meta2'},
|
|
{'id': 3, 'region': 0, 'zone': 3, 'weight': 2,
|
|
'ip': '127.0.0.3', 'port': 10003, 'device': 'sdd1'}]
|
|
for d in devs:
|
|
rb.add_dev(d)
|
|
rb.rebalance()
|
|
rb.save('some.builder')
|
|
mock_open.assert_called_once_with('some.builder', 'wb')
|
|
mock_pickle_dump.assert_called_once_with(rb.to_dict(),
|
|
mock_fh.__enter__(),
|
|
protocol=2)
|
|
|
|
def test_search_devs(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
devs = [{'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sda1',
|
|
'meta': 'meta0'},
|
|
{'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdb1',
|
|
'meta': 'meta1'},
|
|
{'id': 2, 'region': 1, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.2', 'port': 10002, 'device': 'sdc1',
|
|
'meta': 'meta2'},
|
|
{'id': 3, 'region': 1, 'zone': 3, 'weight': 2,
|
|
'ip': '127.0.0.3', 'port': 10003, 'device': 'sdd1',
|
|
'meta': 'meta3'},
|
|
{'id': 4, 'region': 2, 'zone': 4, 'weight': 1,
|
|
'ip': '127.0.0.4', 'port': 10004, 'device': 'sde1',
|
|
'meta': 'meta4', 'replication_ip': '127.0.0.10',
|
|
'replication_port': 20000},
|
|
{'id': 5, 'region': 2, 'zone': 5, 'weight': 2,
|
|
'ip': '127.0.0.5', 'port': 10005, 'device': 'sdf1',
|
|
'meta': 'meta5', 'replication_ip': '127.0.0.11',
|
|
'replication_port': 20001},
|
|
{'id': 6, 'region': 2, 'zone': 6, 'weight': 2,
|
|
'ip': '127.0.0.6', 'port': 10006, 'device': 'sdg1',
|
|
'meta': 'meta6', 'replication_ip': '127.0.0.12',
|
|
'replication_port': 20002}]
|
|
for d in devs:
|
|
rb.add_dev(d)
|
|
rb.rebalance()
|
|
res = rb.search_devs({'region': 0})
|
|
self.assertEqual(res, [devs[0], devs[1]])
|
|
res = rb.search_devs({'region': 1})
|
|
self.assertEqual(res, [devs[2], devs[3]])
|
|
res = rb.search_devs({'region': 1, 'zone': 2})
|
|
self.assertEqual(res, [devs[2]])
|
|
res = rb.search_devs({'id': 1})
|
|
self.assertEqual(res, [devs[1]])
|
|
res = rb.search_devs({'zone': 1})
|
|
self.assertEqual(res, [devs[1]])
|
|
res = rb.search_devs({'ip': '127.0.0.1'})
|
|
self.assertEqual(res, [devs[1]])
|
|
res = rb.search_devs({'ip': '127.0.0.1', 'port': 10001})
|
|
self.assertEqual(res, [devs[1]])
|
|
res = rb.search_devs({'port': 10001})
|
|
self.assertEqual(res, [devs[1]])
|
|
res = rb.search_devs({'replication_ip': '127.0.0.10'})
|
|
self.assertEqual(res, [devs[4]])
|
|
res = rb.search_devs({'replication_ip': '127.0.0.10',
|
|
'replication_port': 20000})
|
|
self.assertEqual(res, [devs[4]])
|
|
res = rb.search_devs({'replication_port': 20000})
|
|
self.assertEqual(res, [devs[4]])
|
|
res = rb.search_devs({'device': 'sdb1'})
|
|
self.assertEqual(res, [devs[1]])
|
|
res = rb.search_devs({'meta': 'meta1'})
|
|
self.assertEqual(res, [devs[1]])
|
|
|
|
def test_validate(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 8, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 9, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
rb.add_dev({'id': 10, 'region': 0, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
rb.add_dev({'id': 11, 'region': 0, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
rb.add_dev({'id': 12, 'region': 0, 'zone': 2, 'weight': 2,
|
|
'ip': '127.0.0.1', 'port': 10002, 'device': 'sda1'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 3, 'weight': 2,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
rb.add_dev({'id': 13, 'region': 0, 'zone': 3, 'weight': 2,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
rb.add_dev({'id': 14, 'region': 0, 'zone': 3, 'weight': 2,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
rb.add_dev({'id': 15, 'region': 0, 'zone': 3, 'weight': 2,
|
|
'ip': '127.0.0.1', 'port': 10003, 'device': 'sda1'})
|
|
|
|
# Degenerate case: devices added but not rebalanced yet
|
|
self.assertRaises(exceptions.RingValidationError, rb.validate)
|
|
|
|
rb.rebalance()
|
|
counts = self._partition_counts(rb, key='zone')
|
|
self.assertEqual(counts, {0: 128, 1: 128, 2: 256, 3: 256})
|
|
|
|
dev_usage, worst = rb.validate()
|
|
self.assertTrue(dev_usage is None)
|
|
self.assertTrue(worst is None)
|
|
|
|
dev_usage, worst = rb.validate(stats=True)
|
|
self.assertEqual(list(dev_usage), [32, 32, 64, 64,
|
|
32, 32, 32, # added zone0
|
|
32, 32, 32, # added zone1
|
|
64, 64, 64, # added zone2
|
|
64, 64, 64, # added zone3
|
|
])
|
|
self.assertEqual(int(worst), 0)
|
|
|
|
# min part hours should pin all the parts assigned to this zero
|
|
# weight device onto it such that the balance will look horrible
|
|
rb.set_dev_weight(2, 0)
|
|
rb.rebalance()
|
|
self.assertEqual(rb.validate(stats=True)[1], MAX_BALANCE)
|
|
|
|
# Test not all partitions doubly accounted for
|
|
rb.devs[1]['parts'] -= 1
|
|
self.assertRaises(exceptions.RingValidationError, rb.validate)
|
|
rb.devs[1]['parts'] += 1
|
|
|
|
# Test non-numeric port
|
|
rb.devs[1]['port'] = '10001'
|
|
self.assertRaises(exceptions.RingValidationError, rb.validate)
|
|
rb.devs[1]['port'] = 10001
|
|
|
|
# Test partition on nonexistent device
|
|
rb.pretend_min_part_hours_passed()
|
|
orig_dev_id = rb._replica2part2dev[0][0]
|
|
rb._replica2part2dev[0][0] = len(rb.devs)
|
|
self.assertRaises(exceptions.RingValidationError, rb.validate)
|
|
rb._replica2part2dev[0][0] = orig_dev_id
|
|
|
|
# Tests that validate can handle 'holes' in .devs
|
|
rb.remove_dev(2)
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance()
|
|
rb.validate(stats=True)
|
|
|
|
# Test partition assigned to a hole
|
|
if rb.devs[2]:
|
|
rb.remove_dev(2)
|
|
rb.pretend_min_part_hours_passed()
|
|
orig_dev_id = rb._replica2part2dev[0][0]
|
|
rb._replica2part2dev[0][0] = 2
|
|
self.assertRaises(exceptions.RingValidationError, rb.validate)
|
|
rb._replica2part2dev[0][0] = orig_dev_id
|
|
|
|
# Validate that zero weight devices with no partitions don't count on
|
|
# the 'worst' value.
|
|
self.assertNotEqual(rb.validate(stats=True)[1], MAX_BALANCE)
|
|
rb.add_dev({'id': 16, 'region': 0, 'zone': 0, 'weight': 0,
|
|
'ip': '127.0.0.1', 'port': 10004, 'device': 'sda1'})
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance()
|
|
self.assertNotEqual(rb.validate(stats=True)[1], MAX_BALANCE)
|
|
|
|
def test_validate_partial_replica(self):
|
|
rb = ring.RingBuilder(8, 2.5, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdc'})
|
|
rb.rebalance()
|
|
rb.validate() # sanity
|
|
self.assertEqual(len(rb._replica2part2dev[0]), 256)
|
|
self.assertEqual(len(rb._replica2part2dev[1]), 256)
|
|
self.assertEqual(len(rb._replica2part2dev[2]), 128)
|
|
|
|
# now swap partial replica part maps
|
|
rb._replica2part2dev[1], rb._replica2part2dev[2] = \
|
|
rb._replica2part2dev[2], rb._replica2part2dev[1]
|
|
self.assertRaises(exceptions.RingValidationError, rb.validate)
|
|
|
|
def test_validate_duplicate_part_assignment(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sdc'})
|
|
rb.rebalance()
|
|
rb.validate() # sanity
|
|
# now double up a device assignment
|
|
rb._replica2part2dev[1][200] = rb._replica2part2dev[2][200]
|
|
|
|
class SubStringMatcher(object):
|
|
def __init__(self, substr):
|
|
self.substr = substr
|
|
|
|
def __eq__(self, other):
|
|
return self.substr in other
|
|
|
|
with self.assertRaises(exceptions.RingValidationError) as e:
|
|
rb.validate()
|
|
|
|
expected = 'The partition 200 has been assigned to duplicate devices'
|
|
self.assertIn(expected, str(e.exception))
|
|
|
|
def test_get_part_devices(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
self.assertEqual(rb.get_part_devices(0), [])
|
|
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.rebalance()
|
|
|
|
part_devs = sorted(rb.get_part_devices(0),
|
|
key=operator.itemgetter('id'))
|
|
self.assertEqual(part_devs, [rb.devs[0], rb.devs[1], rb.devs[2]])
|
|
|
|
def test_get_part_devices_partial_replicas(self):
|
|
rb = ring.RingBuilder(8, 2.5, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda1'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10001, 'device': 'sda1'})
|
|
rb.rebalance(seed=4)
|
|
|
|
# note: partition 255 will only have 2 replicas
|
|
part_devs = sorted(rb.get_part_devices(255),
|
|
key=operator.itemgetter('id'))
|
|
self.assertEqual(part_devs, [rb.devs[1], rb.devs[2]])
|
|
|
|
def test_dispersion_with_zero_weight_devices(self):
|
|
rb = ring.RingBuilder(8, 3.0, 0)
|
|
# add two devices to a single server in a single zone
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
# and a zero weight device
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'weight': 0,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
rb.rebalance()
|
|
self.assertEqual(rb.dispersion, 0.0)
|
|
self.assertEqual(rb._dispersion_graph, {
|
|
(0,): [0, 0, 0, 256],
|
|
(0, 0): [0, 0, 0, 256],
|
|
(0, 0, '127.0.0.1'): [0, 0, 0, 256],
|
|
(0, 0, '127.0.0.1', 0): [0, 256, 0, 0],
|
|
(0, 0, '127.0.0.1', 1): [0, 256, 0, 0],
|
|
(0, 0, '127.0.0.1', 2): [0, 256, 0, 0],
|
|
})
|
|
|
|
def test_dispersion_with_zero_weight_devices_with_parts(self):
|
|
rb = ring.RingBuilder(8, 3.0, 1)
|
|
# add four devices to a single server in a single zone
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
rb.rebalance(seed=1)
|
|
self.assertEqual(rb.dispersion, 0.0)
|
|
self.assertEqual(rb._dispersion_graph, {
|
|
(0,): [0, 0, 0, 256],
|
|
(0, 0): [0, 0, 0, 256],
|
|
(0, 0, '127.0.0.1'): [0, 0, 0, 256],
|
|
(0, 0, '127.0.0.1', 0): [64, 192, 0, 0],
|
|
(0, 0, '127.0.0.1', 1): [64, 192, 0, 0],
|
|
(0, 0, '127.0.0.1', 2): [64, 192, 0, 0],
|
|
(0, 0, '127.0.0.1', 3): [64, 192, 0, 0],
|
|
})
|
|
# now mark a device 2 for decom
|
|
rb.set_dev_weight(2, 0.0)
|
|
# we'll rebalance but can't move any parts
|
|
rb.rebalance(seed=1)
|
|
# zero weight tier has one copy of 1/4 part-replica
|
|
self.assertEqual(rb.dispersion, 75.0)
|
|
self.assertEqual(rb._dispersion_graph, {
|
|
(0,): [0, 0, 0, 256],
|
|
(0, 0): [0, 0, 0, 256],
|
|
(0, 0, '127.0.0.1'): [0, 0, 0, 256],
|
|
(0, 0, '127.0.0.1', 0): [64, 192, 0, 0],
|
|
(0, 0, '127.0.0.1', 1): [64, 192, 0, 0],
|
|
(0, 0, '127.0.0.1', 2): [64, 192, 0, 0],
|
|
(0, 0, '127.0.0.1', 3): [64, 192, 0, 0],
|
|
})
|
|
# unlock the stuck parts
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=3)
|
|
self.assertEqual(rb.dispersion, 0.0)
|
|
self.assertEqual(rb._dispersion_graph, {
|
|
(0,): [0, 0, 0, 256],
|
|
(0, 0): [0, 0, 0, 256],
|
|
(0, 0, '127.0.0.1'): [0, 0, 0, 256],
|
|
(0, 0, '127.0.0.1', 0): [0, 256, 0, 0],
|
|
(0, 0, '127.0.0.1', 1): [0, 256, 0, 0],
|
|
(0, 0, '127.0.0.1', 3): [0, 256, 0, 0],
|
|
})
|
|
|
|
def test_effective_overload(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
# z0
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sdb'})
|
|
# z1
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 1, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 1, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
# z2
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 2, 'weight': 100,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 2, 'weight': 100,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
# this ring requires overload
|
|
required = rb.get_required_overload()
|
|
self.assertGreater(required, 0.1)
|
|
|
|
# and we'll use a little bit
|
|
rb.set_overload(0.1)
|
|
|
|
rb.rebalance(seed=7)
|
|
rb.validate()
|
|
|
|
# but with-out enough overload we're not dispersed
|
|
self.assertGreater(rb.dispersion, 0)
|
|
|
|
# add the other dev to z2
|
|
rb.add_dev({'id': 8, 'region': 0, 'zone': 2, 'weight': 100,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdc'})
|
|
# but also fail another device in the same!
|
|
rb.remove_dev(6)
|
|
|
|
# we still require overload
|
|
required = rb.get_required_overload()
|
|
self.assertGreater(required, 0.1)
|
|
|
|
rb.pretend_min_part_hours_passed()
|
|
rb.rebalance(seed=7)
|
|
rb.validate()
|
|
|
|
# ... and without enough we're full dispersed
|
|
self.assertGreater(rb.dispersion, 0)
|
|
|
|
# ok, let's fix z2's weight for real
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 2, 'weight': 100,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
|
|
# ... technically, we no longer require overload
|
|
self.assertEqual(rb.get_required_overload(), 0.0)
|
|
|
|
# so let's rebalance w/o resetting min_part_hours
|
|
rb.rebalance(seed=7)
|
|
rb.validate()
|
|
|
|
# ... and that got it in one pass boo-yah!
|
|
self.assertEqual(rb.dispersion, 0)
|
|
|
|
def zone_weights_over_device_count(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
# z0
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sda'})
|
|
# z1
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
# z2
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 2, 'weight': 200,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
|
|
rb.rebalance(seed=7)
|
|
rb.validate()
|
|
self.assertEqual(rb.dispersion, 0)
|
|
self.assertAlmostEqual(rb.get_balance(), (1.0 / 3.0) * 100)
|
|
|
|
def test_more_devices_than_replicas_validation_when_removed_dev(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'weight': 1.0, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'weight': 1.0, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'weight': 1.0, 'device': 'sdc'})
|
|
rb.rebalance()
|
|
rb.remove_dev(2)
|
|
with self.assertRaises(ValueError) as e:
|
|
rb.set_dev_weight(2, 1)
|
|
msg = "Can not set weight of dev_id 2 because it is marked " \
|
|
"for removal"
|
|
self.assertIn(msg, str(e.exception))
|
|
with self.assertRaises(exceptions.RingValidationError) as e:
|
|
rb.rebalance()
|
|
msg = 'Replica count of 3 requires more than 2 devices'
|
|
self.assertIn(msg, str(e.exception))
|
|
|
|
|
|
class TestGetRequiredOverload(unittest.TestCase):
|
|
|
|
maxDiff = None
|
|
|
|
def test_none_needed(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'weight': 1,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
|
|
# 4 equal-weight devs and 3 replicas: this can be balanced without
|
|
# resorting to overload at all
|
|
self.assertAlmostEqual(rb.get_required_overload(), 0)
|
|
|
|
expected = {
|
|
(0, 0, '127.0.0.1', 0): 0.75,
|
|
(0, 0, '127.0.0.1', 1): 0.75,
|
|
(0, 0, '127.0.0.1', 2): 0.75,
|
|
(0, 0, '127.0.0.1', 3): 0.75,
|
|
}
|
|
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected, {
|
|
tier: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 4})
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
# since no overload is needed, target_replicas is the same
|
|
rb.set_overload(0.10)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
# ... no matter how high you go!
|
|
rb.set_overload(100.0)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
# 3 equal-weight devs and 3 replicas: this can also be balanced
|
|
rb.remove_dev(3)
|
|
self.assertAlmostEqual(rb.get_required_overload(), 0)
|
|
|
|
expected = {
|
|
(0, 0, '127.0.0.1', 0): 1.0,
|
|
(0, 0, '127.0.0.1', 1): 1.0,
|
|
(0, 0, '127.0.0.1', 2): 1.0,
|
|
}
|
|
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 4})
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
# ... still no overload
|
|
rb.set_overload(100.0)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
def test_equal_replica_and_devices_count_ignore_weights(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 7.47,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 5.91,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 6.44,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
expected = {
|
|
0: 1.0,
|
|
1: 1.0,
|
|
2: 1.0,
|
|
}
|
|
# simplicity itself
|
|
self.assertEqual(expected, {
|
|
t[-1]: r for (t, r) in
|
|
rb._build_weighted_replicas_by_tier().items()
|
|
if len(t) == 4})
|
|
self.assertEqual(expected, {
|
|
t[-1]: r for (t, r) in
|
|
rb._build_wanted_replicas_by_tier().items()
|
|
if len(t) == 4})
|
|
self.assertEqual(expected, {
|
|
t[-1]: r for (t, r) in
|
|
rb._build_target_replicas_by_tier().items()
|
|
if len(t) == 4})
|
|
# ... no overload required!
|
|
self.assertEqual(0, rb.get_required_overload())
|
|
|
|
rb.rebalance()
|
|
expected = {
|
|
0: 256,
|
|
1: 256,
|
|
2: 256,
|
|
}
|
|
self.assertEqual(expected, {d['id']: d['parts'] for d in
|
|
rb._iter_devs()})
|
|
|
|
def test_small_zone(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 4,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'weight': 4,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': 4,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 2, 'weight': 4,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'weight': 3,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
|
|
expected = {
|
|
(0, 0): 1.0434782608695652,
|
|
(0, 1): 1.0434782608695652,
|
|
(0, 2): 0.9130434782608695,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 2})
|
|
expected = {
|
|
(0, 0): 1.0,
|
|
(0, 1): 1.0,
|
|
(0, 2): 1.0,
|
|
}
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
# the device tier is interesting because one of the devices in zone
|
|
# two has a different weight
|
|
expected = {
|
|
0: 0.5217391304347826,
|
|
1: 0.5217391304347826,
|
|
2: 0.5217391304347826,
|
|
3: 0.5217391304347826,
|
|
4: 0.5217391304347826,
|
|
5: 0.3913043478260869,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier[3]: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
# ... but, each pair of devices still needs to hold a whole
|
|
# replicanth; which we'll try distribute fairly among devices in
|
|
# zone 2, so that they can share the burden and ultimately the
|
|
# required overload will be as small as possible.
|
|
expected = {
|
|
0: 0.5,
|
|
1: 0.5,
|
|
2: 0.5,
|
|
3: 0.5,
|
|
4: 0.5714285714285715,
|
|
5: 0.42857142857142855,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier[3]: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
# full dispersion requires zone two's devices to eat more than
|
|
# they're weighted for
|
|
self.assertAlmostEqual(rb.get_required_overload(), 0.095238,
|
|
delta=1e-5)
|
|
|
|
# so... if we give it enough overload it we should get full dispersion
|
|
rb.set_overload(0.1)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier[3]: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
def test_multiple_small_zones(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 8, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 9, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'weight': 150,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': 150,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
rb.add_dev({'id': 10, 'region': 0, 'zone': 1, 'weight': 150,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 2, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 3, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 3, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
|
|
expected = {
|
|
(0, 0): 2.1052631578947367,
|
|
(0, 1): 0.47368421052631576,
|
|
(0, 2): 0.21052631578947367,
|
|
(0, 3): 0.21052631578947367,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
# without any overload, we get weight
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: r
|
|
for (tier, r) in target_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
expected = {
|
|
(0, 0): 1.0,
|
|
(0, 1): 1.0,
|
|
(0, 2): 0.49999999999999994,
|
|
(0, 3): 0.49999999999999994,
|
|
}
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{t: r
|
|
for (t, r) in wanted_replicas.items()
|
|
if len(t) == 2})
|
|
|
|
self.assertEqual(1.3750000000000002, rb.get_required_overload())
|
|
|
|
# with enough overload we get the full dispersion
|
|
rb.set_overload(1.5)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: r
|
|
for (tier, r) in target_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
# with not enough overload, we get somewhere in the middle
|
|
rb.set_overload(1.0)
|
|
expected = {
|
|
(0, 0): 1.3014354066985647,
|
|
(0, 1): 0.8564593301435406,
|
|
(0, 2): 0.4210526315789473,
|
|
(0, 3): 0.4210526315789473,
|
|
}
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: r
|
|
for (tier, r) in target_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
def test_big_zone(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'weight': 60,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': 60,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 2, 'weight': 60,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'weight': 60,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 3, 'weight': 60,
|
|
'ip': '127.0.0.3', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 3, 'weight': 60,
|
|
'ip': '127.0.0.3', 'port': 10000, 'device': 'sdb'})
|
|
|
|
expected = {
|
|
(0, 0): 1.0714285714285714,
|
|
(0, 1): 0.6428571428571429,
|
|
(0, 2): 0.6428571428571429,
|
|
(0, 3): 0.6428571428571429,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 2})
|
|
expected = {
|
|
(0, 0): 1.0,
|
|
(0, 1): 0.6666666666666667,
|
|
(0, 2): 0.6666666666666667,
|
|
(0, 3): 0.6666666666666667,
|
|
}
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
# when all the devices and servers in a zone are evenly weighted
|
|
# it will accurately proxy their required overload, all the
|
|
# zones besides 0 require the same overload
|
|
t = random.choice([t for t in weighted_replicas
|
|
if len(t) == 2
|
|
and t[1] != 0])
|
|
expected_overload = ((wanted_replicas[t] - weighted_replicas[t])
|
|
/ weighted_replicas[t])
|
|
self.assertAlmostEqual(rb.get_required_overload(),
|
|
expected_overload)
|
|
|
|
# but if you only give it out half of that
|
|
rb.set_overload(expected_overload / 2.0)
|
|
# ... you can expect it's not going to full disperse
|
|
expected = {
|
|
(0, 0): 1.0357142857142856,
|
|
(0, 1): 0.6547619047619049,
|
|
(0, 2): 0.6547619047619049,
|
|
(0, 3): 0.6547619047619049,
|
|
}
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
def test_enormous_zone(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'weight': 500,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 1, 'weight': 60,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 1, 'weight': 60,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 2, 'weight': 60,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 2, 'weight': 60,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 8, 'region': 0, 'zone': 3, 'weight': 60,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 9, 'region': 0, 'zone': 3, 'weight': 60,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
expected = {
|
|
(0, 0): 2.542372881355932,
|
|
(0, 1): 0.15254237288135591,
|
|
(0, 2): 0.15254237288135591,
|
|
(0, 3): 0.15254237288135591,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
expected = {
|
|
(0, 0): 1.0,
|
|
(0, 1): 0.6666666666666667,
|
|
(0, 2): 0.6666666666666667,
|
|
(0, 3): 0.6666666666666667,
|
|
}
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
# ouch, those "tiny" devices need to hold 3x more than their
|
|
# weighted for!
|
|
self.assertAlmostEqual(rb.get_required_overload(), 3.370370,
|
|
delta=1e-5)
|
|
|
|
# let's get a little crazy, and let devices eat up to 1x more than
|
|
# their capacity is weighted for - see how far that gets us...
|
|
rb.set_overload(1)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
expected = {
|
|
(0, 0): 2.084745762711864,
|
|
(0, 1): 0.30508474576271183,
|
|
(0, 2): 0.30508474576271183,
|
|
(0, 3): 0.30508474576271183,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
def test_two_big_two_small(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 100,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': 100,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 2, 'weight': 45,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'weight': 45,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 3, 'weight': 35,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 3, 'weight': 35,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
expected = {
|
|
(0, 0): 1.0714285714285714,
|
|
(0, 1): 1.0714285714285714,
|
|
(0, 2): 0.48214285714285715,
|
|
(0, 3): 0.375,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
expected = {
|
|
(0, 0): 1.0,
|
|
(0, 1): 1.0,
|
|
(0, 2): 0.5625,
|
|
(0, 3): 0.43749999999999994,
|
|
}
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
# I'm not sure it's significant or coincidental that the devices
|
|
# in zone 2 & 3 who end up splitting the 3rd replica turn out to
|
|
# need to eat ~1/6th extra replicanths
|
|
self.assertAlmostEqual(rb.get_required_overload(), 1.0 / 6.0)
|
|
|
|
# ... *so* 10% isn't *quite* enough
|
|
rb.set_overload(0.1)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
expected = {
|
|
(0, 0): 1.0285714285714285,
|
|
(0, 1): 1.0285714285714285,
|
|
(0, 2): 0.5303571428571429,
|
|
(0, 3): 0.4125,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
# ... but 20% will do the trick!
|
|
rb.set_overload(0.2)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
expected = {
|
|
(0, 0): 1.0,
|
|
(0, 1): 1.0,
|
|
(0, 2): 0.5625,
|
|
(0, 3): 0.43749999999999994,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
def test_multiple_replicas_each(self):
|
|
rb = ring.RingBuilder(8, 7, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': 80,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': 80,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'weight': 80,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'weight': 80,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sdd'})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 0, 'weight': 80,
|
|
'ip': '127.0.0.0', 'port': 10000, 'device': 'sde'})
|
|
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 1, 'weight': 70,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 1, 'weight': 70,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdb'})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 1, 'weight': 70,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdc'})
|
|
rb.add_dev({'id': 8, 'region': 0, 'zone': 1, 'weight': 70,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sdd'})
|
|
|
|
expected = {
|
|
(0, 0): 4.117647058823529,
|
|
(0, 1): 2.8823529411764706,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
expected = {
|
|
(0, 0): 4.0,
|
|
(0, 1): 3.0,
|
|
}
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
# I guess 2.88 => 3.0 is about a 4% increase
|
|
self.assertAlmostEqual(rb.get_required_overload(),
|
|
0.040816326530612256)
|
|
|
|
# ... 10% is plenty enough here
|
|
rb.set_overload(0.1)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
def test_small_extra_server_in_zone_with_multiple_replicas(self):
|
|
rb = ring.RingBuilder(8, 5, 1)
|
|
|
|
# z0
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 1000})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sdb', 'weight': 1000})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sdc', 'weight': 1000})
|
|
|
|
# z1
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 1000})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 1, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sdb', 'weight': 1000})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 1, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sdc', 'weight': 1000})
|
|
|
|
# z1 - extra small server
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 1, 'ip': '127.0.0.3',
|
|
'port': 6000, 'device': 'sda', 'weight': 50})
|
|
|
|
expected = {
|
|
(0, 0): 2.479338842975207,
|
|
(0, 1): 2.5206611570247937,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected, {t: r for (t, r) in
|
|
weighted_replicas.items()
|
|
if len(t) == 2})
|
|
|
|
# dispersion is fine with this at the zone tier
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected, {t: r for (t, r) in
|
|
wanted_replicas.items()
|
|
if len(t) == 2})
|
|
|
|
# ... but not ok with that tiny server
|
|
expected = {
|
|
'127.0.0.1': 2.479338842975207,
|
|
'127.0.0.2': 1.5206611570247937,
|
|
'127.0.0.3': 1.0,
|
|
}
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
wanted_replicas.items()
|
|
if len(t) == 3})
|
|
|
|
self.assertAlmostEqual(23.2, rb.get_required_overload())
|
|
|
|
def test_multiple_replicas_in_zone_with_single_device(self):
|
|
rb = ring.RingBuilder(8, 5, 0)
|
|
# z0
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 100})
|
|
# z1
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 1, 'ip': '127.0.1.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 100})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'ip': '127.0.1.1',
|
|
'port': 6000, 'device': 'sdb', 'weight': 100})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'ip': '127.0.1.2',
|
|
'port': 6000, 'device': 'sdc', 'weight': 100})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 1, 'ip': '127.0.1.2',
|
|
'port': 6000, 'device': 'sdd', 'weight': 100})
|
|
|
|
# first things first, make sure we do this right
|
|
rb.rebalance()
|
|
|
|
# each device get's a sing replica of every part
|
|
expected = {
|
|
0: 256,
|
|
1: 256,
|
|
2: 256,
|
|
3: 256,
|
|
4: 256,
|
|
}
|
|
self.assertEqual(expected, {d['id']: d['parts']
|
|
for d in rb._iter_devs()})
|
|
|
|
# but let's make sure we're thinking about it right too
|
|
expected = {
|
|
0: 1.0,
|
|
1: 1.0,
|
|
2: 1.0,
|
|
3: 1.0,
|
|
4: 1.0,
|
|
}
|
|
|
|
# by weight everyone is equal
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
weighted_replicas.items()
|
|
if len(t) == 4})
|
|
|
|
# wanted might have liked to have fewer replicas in z1, but the
|
|
# single device in z0 limits us one replica per device
|
|
with rb.debug():
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
wanted_replicas.items()
|
|
if len(t) == 4})
|
|
|
|
# even with some overload - still one replica per device
|
|
rb.set_overload(1.0)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
target_replicas.items()
|
|
if len(t) == 4})
|
|
|
|
# when overload can not change the outcome none is required
|
|
self.assertEqual(0.0, rb.get_required_overload())
|
|
# even though dispersion is terrible (in z1 particularly)
|
|
self.assertEqual(100.0, rb.dispersion)
|
|
|
|
def test_one_big_guy_does_not_spoil_his_buddy(self):
|
|
rb = ring.RingBuilder(8, 3, 0)
|
|
|
|
# z0
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 100})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 100})
|
|
# z1
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'ip': '127.0.1.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 100})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'ip': '127.0.1.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 100})
|
|
# z2
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 2, 'ip': '127.0.2.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 100})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'ip': '127.0.2.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 10000})
|
|
|
|
# obviously d5 gets one whole replica; the other two replicas
|
|
# are split evenly among the five other devices
|
|
# (i.e. ~0.4 replicanths for each 100 units of weight)
|
|
expected = {
|
|
0: 0.39999999999999997,
|
|
1: 0.39999999999999997,
|
|
2: 0.39999999999999997,
|
|
3: 0.39999999999999997,
|
|
4: 0.39999999999999997,
|
|
5: 1.0,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
weighted_replicas.items()
|
|
if len(t) == 4})
|
|
|
|
# with no overload we get the "balanced" placement
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
target_replicas.items()
|
|
if len(t) == 4})
|
|
|
|
# but in reality, these devices having such disparate weights
|
|
# leads to a *terrible* balance even w/o overload!
|
|
rb.rebalance(seed=9)
|
|
self.assertEqual(rb.get_balance(), 1308.2031249999998)
|
|
|
|
# even though part assignment is pretty reasonable
|
|
expected = {
|
|
0: 103,
|
|
1: 102,
|
|
2: 103,
|
|
3: 102,
|
|
4: 102,
|
|
5: 256,
|
|
}
|
|
self.assertEqual(expected, {
|
|
d['id']: d['parts'] for d in rb._iter_devs()})
|
|
|
|
# so whats happening is the small devices are holding *way* more
|
|
# *real* parts than their *relative* portion of the weight would
|
|
# like them too!
|
|
expected = {
|
|
0: 1308.2031249999998,
|
|
1: 1294.5312499999998,
|
|
2: 1308.2031249999998,
|
|
3: 1294.5312499999998,
|
|
4: 1294.5312499999998,
|
|
5: -65.0,
|
|
|
|
}
|
|
self.assertEqual(expected, rb._build_balance_per_dev())
|
|
|
|
# increasing overload moves towards one replica in each tier
|
|
rb.set_overload(0.20)
|
|
expected = {
|
|
0: 0.48,
|
|
1: 0.48,
|
|
2: 0.48,
|
|
3: 0.48,
|
|
4: 0.30857142857142855,
|
|
5: 0.7714285714285714,
|
|
}
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
target_replicas.items()
|
|
if len(t) == 4})
|
|
|
|
# ... and as always increasing overload makes balance *worse*
|
|
rb.rebalance(seed=17)
|
|
self.assertEqual(rb.get_balance(), 1581.6406249999998)
|
|
|
|
# but despite the overall trend toward imbalance, in the tier
|
|
# with the huge device, the small device is trying to shed parts
|
|
# as effectively as it can (which would be useful if it was the
|
|
# only small device isolated in a tier with other huge devices
|
|
# trying to gobble up all the replicanths in the tier - see
|
|
# `test_one_small_guy_does_not_spoil_his_buddy`!)
|
|
expected = {
|
|
0: 123,
|
|
1: 123,
|
|
2: 123,
|
|
3: 123,
|
|
4: 79,
|
|
5: 197,
|
|
}
|
|
self.assertEqual(expected, {
|
|
d['id']: d['parts'] for d in rb._iter_devs()})
|
|
|
|
# *see*, at least *someones* balance is getting better!
|
|
expected = {
|
|
0: 1581.6406249999998,
|
|
1: 1581.6406249999998,
|
|
2: 1581.6406249999998,
|
|
3: 1581.6406249999998,
|
|
4: 980.078125,
|
|
5: -73.06640625,
|
|
}
|
|
self.assertEqual(expected, rb._build_balance_per_dev())
|
|
|
|
def test_one_small_guy_does_not_spoil_his_buddy(self):
|
|
rb = ring.RingBuilder(8, 3, 0)
|
|
|
|
# z0
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 10000})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 10000})
|
|
# z1
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'ip': '127.0.1.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 10000})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'ip': '127.0.1.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 10000})
|
|
# z2
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 2, 'ip': '127.0.2.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 10000})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'ip': '127.0.2.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 100})
|
|
|
|
# it's almost like 3.0 / 5 ~= 0.6, but that one little guy get's
|
|
# his fair share
|
|
expected = {
|
|
0: 0.5988023952095808,
|
|
1: 0.5988023952095808,
|
|
2: 0.5988023952095808,
|
|
3: 0.5988023952095808,
|
|
4: 0.5988023952095808,
|
|
5: 0.005988023952095809,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
weighted_replicas.items()
|
|
if len(t) == 4})
|
|
|
|
# with no overload we get a nice balanced placement
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
target_replicas.items()
|
|
if len(t) == 4})
|
|
rb.rebalance(seed=9)
|
|
|
|
# part placement looks goods
|
|
expected = {
|
|
0: 154,
|
|
1: 153,
|
|
2: 153,
|
|
3: 153,
|
|
4: 153,
|
|
5: 2,
|
|
}
|
|
self.assertEqual(expected, {
|
|
d['id']: d['parts'] for d in rb._iter_devs()})
|
|
|
|
# ... balance is a little lumpy on the small guy since he wants
|
|
# one and a half parts :\
|
|
expected = {
|
|
0: 0.4609375000000142,
|
|
1: -0.1914062499999858,
|
|
2: -0.1914062499999858,
|
|
3: -0.1914062499999858,
|
|
4: -0.1914062499999858,
|
|
5: 30.46875,
|
|
}
|
|
self.assertEqual(expected, rb._build_balance_per_dev())
|
|
|
|
self.assertEqual(rb.get_balance(), 30.46875)
|
|
|
|
# increasing overload moves towards one replica in each tier
|
|
rb.set_overload(0.5)
|
|
expected = {
|
|
0: 0.5232035928143712,
|
|
1: 0.5232035928143712,
|
|
2: 0.5232035928143712,
|
|
3: 0.5232035928143712,
|
|
4: 0.8982035928143712,
|
|
5: 0.008982035928143714,
|
|
}
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
self.assertEqual(expected, {t[-1]: r for (t, r) in
|
|
target_replicas.items()
|
|
if len(t) == 4})
|
|
|
|
# ... and as always increasing overload makes balance *worse*
|
|
rb.rebalance(seed=17)
|
|
self.assertEqual(rb.get_balance(), 95.703125)
|
|
|
|
# but despite the overall trend toward imbalance, the little guy
|
|
# isn't really taking on many new parts!
|
|
expected = {
|
|
0: 134,
|
|
1: 134,
|
|
2: 134,
|
|
3: 133,
|
|
4: 230,
|
|
5: 3,
|
|
}
|
|
self.assertEqual(expected, {
|
|
d['id']: d['parts'] for d in rb._iter_devs()})
|
|
|
|
# *see*, at everyone's balance is getting worse *together*!
|
|
expected = {
|
|
0: -12.585937499999986,
|
|
1: -12.585937499999986,
|
|
2: -12.585937499999986,
|
|
3: -13.238281249999986,
|
|
4: 50.0390625,
|
|
5: 95.703125,
|
|
}
|
|
self.assertEqual(expected, rb._build_balance_per_dev())
|
|
|
|
def test_two_servers_with_more_than_one_replica(self):
|
|
rb = ring.RingBuilder(8, 3, 0)
|
|
# z0
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 60})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 60})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'ip': '127.0.0.3',
|
|
'port': 6000, 'device': 'sda', 'weight': 60})
|
|
# z1
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'ip': '127.0.1.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 80})
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 1, 'ip': '127.0.1.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 128})
|
|
# z2
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'ip': '127.0.2.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 80})
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 2, 'ip': '127.0.2.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 240})
|
|
|
|
rb.set_overload(0.1)
|
|
rb.rebalance()
|
|
self.assertEqual(12.161458333333343, rb.get_balance())
|
|
|
|
replica_plan = rb._build_target_replicas_by_tier()
|
|
for dev in rb._iter_devs():
|
|
tier = (dev['region'], dev['zone'], dev['ip'], dev['id'])
|
|
expected_parts = replica_plan[tier] * rb.parts
|
|
self.assertAlmostEqual(dev['parts'], expected_parts,
|
|
delta=1)
|
|
|
|
def test_multi_zone_with_failed_device(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 2000})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sdb', 'weight': 2000})
|
|
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 2000})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sdb', 'weight': 2000})
|
|
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 2, 'ip': '127.0.0.3',
|
|
'port': 6000, 'device': 'sda', 'weight': 2000})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 2, 'ip': '127.0.0.3',
|
|
'port': 6000, 'device': 'sdb', 'weight': 2000})
|
|
|
|
# sanity, balanced and dispersed
|
|
expected = {
|
|
(0, 0): 1.0,
|
|
(0, 1): 1.0,
|
|
(0, 2): 1.0,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 2})
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
self.assertEqual(rb.get_required_overload(), 0.0)
|
|
|
|
# fail a device in zone 2
|
|
rb.remove_dev(4)
|
|
|
|
expected = {
|
|
0: 0.6,
|
|
1: 0.6,
|
|
2: 0.6,
|
|
3: 0.6,
|
|
5: 0.6,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier[3]: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
expected = {
|
|
0: 0.5,
|
|
1: 0.5,
|
|
2: 0.5,
|
|
3: 0.5,
|
|
5: 1.0,
|
|
}
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier[3]: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
# does this make sense? every zone was holding 1/3rd of the
|
|
# replicas, so each device was 1/6th, remove a device and
|
|
# suddenly it's holding *both* sixths which is 2/3rds?
|
|
self.assertAlmostEqual(rb.get_required_overload(), 2.0 / 3.0)
|
|
|
|
# 10% isn't nearly enough
|
|
rb.set_overload(0.1)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
expected = {
|
|
0: 0.585,
|
|
1: 0.585,
|
|
2: 0.585,
|
|
3: 0.585,
|
|
5: 0.6599999999999999,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier[3]: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
# 50% isn't even enough
|
|
rb.set_overload(0.5)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
expected = {
|
|
0: 0.525,
|
|
1: 0.525,
|
|
2: 0.525,
|
|
3: 0.525,
|
|
5: 0.8999999999999999,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier[3]: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
# even 65% isn't enough (but it's getting closer)
|
|
rb.set_overload(0.65)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
expected = {
|
|
0: 0.5025000000000001,
|
|
1: 0.5025000000000001,
|
|
2: 0.5025000000000001,
|
|
3: 0.5025000000000001,
|
|
5: 0.99,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier[3]: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
def test_balanced_zones_unbalanced_servers(self):
|
|
rb = ring.RingBuilder(8, 3, 1)
|
|
# zone 0 server 127.0.0.1
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 3000})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sdb', 'weight': 3000})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 3000})
|
|
# zone 1 server 127.0.0.2
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 1, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 4000})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 1, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sdb', 'weight': 4000})
|
|
# zone 1 (again) server 127.0.0.3
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 1, 'ip': '127.0.0.3',
|
|
'port': 6000, 'device': 'sda', 'weight': 1000})
|
|
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
|
|
# zones are evenly weighted
|
|
expected = {
|
|
(0, 0): 1.5,
|
|
(0, 1): 1.5,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 2})
|
|
|
|
# ... but servers are not
|
|
expected = {
|
|
'127.0.0.1': 1.5,
|
|
'127.0.0.2': 1.3333333333333333,
|
|
'127.0.0.3': 0.16666666666666666,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier[2]: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 3})
|
|
|
|
# make sure wanted will even it out
|
|
expected = {
|
|
'127.0.0.1': 1.5,
|
|
'127.0.0.2': 1.0,
|
|
'127.0.0.3': 0.4999999999999999,
|
|
}
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier[2]: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 3})
|
|
|
|
# so it wants 1/6th and eats 1/2 - that's 2/6ths more than it
|
|
# wants which is a 200% increase
|
|
self.assertAlmostEqual(rb.get_required_overload(), 2.0)
|
|
|
|
# the overload doesn't effect the tiers that are already dispersed
|
|
rb.set_overload(1)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
expected = {
|
|
'127.0.0.1': 1.5,
|
|
# notice with half the overload 1/6th replicanth swapped servers
|
|
'127.0.0.2': 1.1666666666666665,
|
|
'127.0.0.3': 0.3333333333333333,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier[2]: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 3})
|
|
|
|
def test_adding_second_zone(self):
|
|
rb = ring.RingBuilder(3, 3, 1)
|
|
# zone 0 server 127.0.0.1
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 2000})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sdb', 'weight': 2000})
|
|
# zone 0 server 127.0.0.2
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 2000})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sdb', 'weight': 2000})
|
|
# zone 0 server 127.0.0.3
|
|
rb.add_dev({'id': 4, 'region': 0, 'zone': 0, 'ip': '127.0.0.3',
|
|
'port': 6000, 'device': 'sda', 'weight': 2000})
|
|
rb.add_dev({'id': 5, 'region': 0, 'zone': 0, 'ip': '127.0.0.3',
|
|
'port': 6000, 'device': 'sdb', 'weight': 2000})
|
|
|
|
# sanity, balanced and dispersed
|
|
expected = {
|
|
'127.0.0.1': 1.0,
|
|
'127.0.0.2': 1.0,
|
|
'127.0.0.3': 1.0,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier[2]: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 3})
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier[2]: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 3})
|
|
|
|
self.assertEqual(rb.get_required_overload(), 0)
|
|
|
|
# start adding a second zone
|
|
|
|
# zone 1 server 127.0.1.1
|
|
rb.add_dev({'id': 6, 'region': 0, 'zone': 1, 'ip': '127.0.1.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 100})
|
|
rb.add_dev({'id': 7, 'region': 0, 'zone': 1, 'ip': '127.0.1.1',
|
|
'port': 6000, 'device': 'sdb', 'weight': 100})
|
|
# zone 1 server 127.0.1.2
|
|
rb.add_dev({'id': 8, 'region': 0, 'zone': 1, 'ip': '127.0.1.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 100})
|
|
rb.add_dev({'id': 9, 'region': 0, 'zone': 1, 'ip': '127.0.1.2',
|
|
'port': 6000, 'device': 'sdb', 'weight': 100})
|
|
# zone 1 server 127.0.1.3
|
|
rb.add_dev({'id': 10, 'region': 0, 'zone': 1, 'ip': '127.0.1.3',
|
|
'port': 6000, 'device': 'sda', 'weight': 100})
|
|
rb.add_dev({'id': 11, 'region': 0, 'zone': 1, 'ip': '127.0.1.3',
|
|
'port': 6000, 'device': 'sdb', 'weight': 100})
|
|
|
|
# this messes things up pretty royally
|
|
expected = {
|
|
'127.0.0.1': 0.9523809523809523,
|
|
'127.0.0.2': 0.9523809523809523,
|
|
'127.0.0.3': 0.9523809523809523,
|
|
'127.0.1.1': 0.047619047619047616,
|
|
'127.0.1.2': 0.047619047619047616,
|
|
'127.0.1.3': 0.047619047619047616,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier[2]: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 3})
|
|
expected = {
|
|
'127.0.0.1': 0.6666666666666667,
|
|
'127.0.0.2': 0.6666666666666667,
|
|
'127.0.0.3': 0.6666666666666667,
|
|
'127.0.1.1': 0.3333333333333333,
|
|
'127.0.1.2': 0.3333333333333333,
|
|
'127.0.1.3': 0.3333333333333333,
|
|
}
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected,
|
|
{tier[2]: weighted
|
|
for (tier, weighted) in wanted_replicas.items()
|
|
if len(tier) == 3})
|
|
|
|
# so dispersion would require these devices hold 6x more than
|
|
# prescribed by weight, defeating any attempt at gradually
|
|
# anything
|
|
self.assertAlmostEqual(rb.get_required_overload(), 6.0)
|
|
|
|
# so let's suppose we only allow for 10% overload
|
|
rb.set_overload(0.10)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
|
|
expected = {
|
|
# we expect servers in zone 0 to be between 0.952 and 0.666
|
|
'127.0.0.1': 0.9476190476190476,
|
|
'127.0.0.2': 0.9476190476190476,
|
|
'127.0.0.3': 0.9476190476190476,
|
|
# we expect servers in zone 1 to be between 0.0476 and 0.333
|
|
# and in fact its ~10% increase (very little compared to 6x!)
|
|
'127.0.1.1': 0.052380952380952375,
|
|
'127.0.1.2': 0.052380952380952375,
|
|
'127.0.1.3': 0.052380952380952375,
|
|
}
|
|
self.assertEqual(expected,
|
|
{tier[2]: weighted
|
|
for (tier, weighted) in target_replicas.items()
|
|
if len(tier) == 3})
|
|
|
|
def test_gradual_replica_count(self):
|
|
rb = ring.RingBuilder(3, 2.5, 1)
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sda', 'weight': 2000})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'ip': '127.0.0.1',
|
|
'port': 6000, 'device': 'sdb', 'weight': 2000})
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 0, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sda', 'weight': 2000})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 0, 'ip': '127.0.0.2',
|
|
'port': 6000, 'device': 'sdb', 'weight': 2000})
|
|
|
|
expected = {
|
|
0: 0.625,
|
|
1: 0.625,
|
|
2: 0.625,
|
|
3: 0.625,
|
|
}
|
|
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected, {
|
|
tier[3]: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 4})
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected, {
|
|
tier[3]: wanted
|
|
for (tier, wanted) in wanted_replicas.items()
|
|
if len(tier) == 4})
|
|
|
|
self.assertEqual(rb.get_required_overload(), 0)
|
|
|
|
# server 127.0.0.2 will have only one device
|
|
rb.remove_dev(2)
|
|
|
|
# server 127.0.0.1 has twice the capacity of 127.0.0.2
|
|
expected = {
|
|
'127.0.0.1': 1.6666666666666667,
|
|
'127.0.0.2': 0.8333333333333334,
|
|
}
|
|
weighted_replicas = rb._build_weighted_replicas_by_tier()
|
|
self.assertEqual(expected, {
|
|
tier[2]: weighted
|
|
for (tier, weighted) in weighted_replicas.items()
|
|
if len(tier) == 3})
|
|
|
|
# dispersion requirements extend only to whole replicas
|
|
expected = {
|
|
'127.0.0.1': 1.4999999999999998,
|
|
'127.0.0.2': 1.0,
|
|
}
|
|
wanted_replicas = rb._build_wanted_replicas_by_tier()
|
|
self.assertEqual(expected, {
|
|
tier[2]: wanted
|
|
for (tier, wanted) in wanted_replicas.items()
|
|
if len(tier) == 3})
|
|
|
|
# 5/6ths to a whole replicanth is a 20% increase
|
|
self.assertAlmostEqual(rb.get_required_overload(), 0.2)
|
|
|
|
# so let's suppose we only allow for 10% overload
|
|
rb.set_overload(0.1)
|
|
target_replicas = rb._build_target_replicas_by_tier()
|
|
expected = {
|
|
'127.0.0.1': 1.5833333333333333,
|
|
'127.0.0.2': 0.9166666666666667,
|
|
}
|
|
self.assertEqual(expected, {
|
|
tier[2]: wanted
|
|
for (tier, wanted) in target_replicas.items()
|
|
if len(tier) == 3})
|
|
|
|
def test_perfect_four_zone_four_replica_bad_placement(self):
|
|
rb = ring.RingBuilder(4, 4, 1)
|
|
|
|
# this weight is sorta nuts, but it's really just to help the
|
|
# weight_of_one_part hit a magic number where floats mess up
|
|
# like they would on ring with a part power of 19 and 100's of
|
|
# 1000's of units of weight.
|
|
weight = 21739130434795e-11
|
|
|
|
# r0z0
|
|
rb.add_dev({'id': 0, 'region': 0, 'zone': 0, 'weight': weight,
|
|
'ip': '127.0.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 1, 'region': 0, 'zone': 0, 'weight': weight,
|
|
'ip': '127.0.0.2', 'port': 10000, 'device': 'sdb'})
|
|
# r0z1
|
|
rb.add_dev({'id': 2, 'region': 0, 'zone': 1, 'weight': weight,
|
|
'ip': '127.0.1.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 3, 'region': 0, 'zone': 1, 'weight': weight,
|
|
'ip': '127.0.1.2', 'port': 10000, 'device': 'sdb'})
|
|
# r1z0
|
|
rb.add_dev({'id': 4, 'region': 1, 'zone': 0, 'weight': weight,
|
|
'ip': '127.1.0.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 5, 'region': 1, 'zone': 0, 'weight': weight,
|
|
'ip': '127.1.0.2', 'port': 10000, 'device': 'sdb'})
|
|
# r1z1
|
|
rb.add_dev({'id': 6, 'region': 1, 'zone': 1, 'weight': weight,
|
|
'ip': '127.1.1.1', 'port': 10000, 'device': 'sda'})
|
|
rb.add_dev({'id': 7, 'region': 1, 'zone': 1, 'weight': weight,
|
|
'ip': '127.1.1.2', 'port': 10000, 'device': 'sdb'})
|
|
|
|
# the replica plan is sound
|
|
expectations = {
|
|
# tier_len => expected replicas
|
|
1: {
|
|
(0,): 2.0,
|
|
(1,): 2.0,
|
|
},
|
|
2: {
|
|
(0, 0): 1.0,
|
|
(0, 1): 1.0,
|
|
(1, 0): 1.0,
|
|
(1, 1): 1.0,
|
|
}
|
|
}
|
|
wr = rb._build_replica_plan()
|
|
for tier_len, expected in expectations.items():
|
|
self.assertEqual(expected, {t: r['max'] for (t, r) in
|
|
wr.items() if len(t) == tier_len})
|
|
|
|
# even thought a naive ceil of weights is surprisingly wrong
|
|
expectations = {
|
|
# tier_len => expected replicas
|
|
1: {
|
|
(0,): 3.0,
|
|
(1,): 3.0,
|
|
},
|
|
2: {
|
|
(0, 0): 2.0,
|
|
(0, 1): 2.0,
|
|
(1, 0): 2.0,
|
|
(1, 1): 2.0,
|
|
}
|
|
}
|
|
wr = rb._build_weighted_replicas_by_tier()
|
|
for tier_len, expected in expectations.items():
|
|
self.assertEqual(expected, {t: ceil(r) for (t, r) in
|
|
wr.items() if len(t) == tier_len})
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|