From 6a924d6066ebd39ad76496939cb847828542d2e7 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Mon, 1 Jan 2024 18:40:41 +0300 Subject: [PATCH] Extract PG combinator into a separate module --- mon/lp-optimizer.js | 283 +++++--------------------------- mon/mon.js | 73 ++++---- mon/simple_pgs.js | 247 ++++++++++++++++++++++++++++ mon/test-nonuniform.js | 12 +- mon/test-optimize-simple.js | 11 +- mon/test-optimize-undersized.js | 17 +- mon/test-optimize-unfeasible.js | 5 +- mon/test-optimize.js | 15 +- 8 files changed, 357 insertions(+), 306 deletions(-) create mode 100644 mon/simple_pgs.js diff --git a/mon/lp-optimizer.js b/mon/lp-optimizer.js index 8505c252..e71a552c 100644 --- a/mon/lp-optimizer.js +++ b/mon/lp-optimizer.js @@ -50,15 +50,15 @@ async function lp_solve(text) return { score, vars }; } -async function optimize_initial({ osd_tree, pg_count, pg_size = 3, pg_minsize = 2, max_combinations = 10000, parity_space = 1, ordered = false }) +// osd_weights = { [id]: weight } +async function optimize_initial({ osd_weights, combinator, pg_count, pg_size = 3, pg_minsize = 2, parity_space = 1, ordered = false }) { - if (!pg_count || !osd_tree) + if (!pg_count || !osd_weights) { return null; } - const all_weights = Object.assign({}, ...Object.values(osd_tree)); - const total_weight = Object.values(all_weights).reduce((a, c) => Number(a) + Number(c), 0); - const all_pgs = Object.values(random_combinations(osd_tree, pg_size, max_combinations, parity_space > 1)); + const total_weight = Object.values(osd_weights).reduce((a, c) => Number(a) + Number(c), 0); + const all_pgs = Object.values(make_cyclic(combinator.random_combinations(), parity_space)); const pg_per_osd = {}; for (const pg of all_pgs) { @@ -69,15 +69,15 @@ async function optimize_initial({ osd_tree, pg_count, pg_size = 3, pg_minsize = pg_per_osd[osd].push((i >= pg_minsize ? parity_space+'*' : '')+"pg_"+pg.join("_")); } } - const pg_effsize = Math.min(pg_minsize, Object.keys(osd_tree).length) - + Math.max(0, Math.min(pg_size, Object.keys(osd_tree).length) - pg_minsize) * parity_space; + let pg_effsize = all_pgs.reduce((a, c) => Math.max(a, c.filter(e => e != NO_OSD).length), 0); + pg_effsize = Math.min(pg_minsize, pg_effsize) + Math.max(0, Math.min(pg_size, pg_effsize) - pg_minsize) * parity_space; let lp = ''; lp += "max: "+all_pgs.map(pg => 'pg_'+pg.join('_')).join(' + ')+";\n"; for (const osd in pg_per_osd) { if (osd !== NO_OSD) { - let osd_pg_count = all_weights[osd]/total_weight*pg_effsize*pg_count; + let osd_pg_count = osd_weights[osd]/total_weight*pg_effsize*pg_count; lp += pg_per_osd[osd].join(' + ')+' <= '+osd_pg_count+';\n'; } } @@ -93,7 +93,7 @@ async function optimize_initial({ osd_tree, pg_count, pg_size = 3, pg_minsize = throw new Error('Problem is infeasible or unbounded - is it a bug?'); } const int_pgs = make_int_pgs(lp_result.vars, pg_count, ordered); - const eff = pg_list_space_efficiency(int_pgs, all_weights, pg_minsize, parity_space); + const eff = pg_list_space_efficiency(int_pgs, osd_weights, pg_minsize, parity_space); const res = { score: lp_result.score, weights: lp_result.vars, @@ -104,6 +104,22 @@ async function optimize_initial({ osd_tree, pg_count, pg_size = 3, pg_minsize = return res; } +function make_cyclic(pgs, parity_space) +{ + if (parity_space > 1) + { + for (const pg in pgs) + { + for (let i = 1; i < pg.size; i++) + { + const cyclic = [ ...pg.slice(i), ...pg.slice(0, i) ]; + pgs['pg_'+cyclic.join('_')] = cyclic; + } + } + } + return pgs; +} + function shuffle(array) { for (let i = array.length - 1, j, x; i > 0; i--) @@ -216,47 +232,17 @@ function calc_intersect_weights(old_pg_size, pg_size, pg_count, prev_weights, al return move_weights; } -function add_valid_previous(osd_tree, prev_weights, all_pgs) -{ - // Add previous combinations that are still valid - const hosts = Object.keys(osd_tree).sort(); - const host_per_osd = {}; - for (const host in osd_tree) - { - for (const osd in osd_tree[host]) - { - host_per_osd[osd] = host; - } - } - skip_pg: for (const pg_name in prev_weights) - { - const seen_hosts = {}; - const pg = pg_name.substr(3).split(/_/); - for (const osd of pg) - { - if (!host_per_osd[osd] || seen_hosts[host_per_osd[osd]]) - { - continue skip_pg; - } - seen_hosts[host_per_osd[osd]] = true; - } - if (!all_pgs[pg_name]) - { - all_pgs[pg_name] = pg; - } - } -} - // Try to minimize data movement -async function optimize_change({ prev_pgs: prev_int_pgs, osd_tree, pg_size = 3, pg_minsize = 2, max_combinations = 10000, parity_space = 1, ordered = false }) +async function optimize_change({ prev_pgs: prev_int_pgs, osd_weights, combinator, pg_size = 3, pg_minsize = 2, parity_space = 1, ordered = false }) { - if (!osd_tree) + if (!osd_weights) { return null; } // FIXME: use parity_chunks with parity_space instead of pg_minsize - const pg_effsize = Math.min(pg_minsize, Object.keys(osd_tree).length) - + Math.max(0, Math.min(pg_size, Object.keys(osd_tree).length) - pg_minsize) * parity_space; + let all_pgs = make_cyclic(combinator.random_combinations(), parity_space); + let pg_effsize = Object.values(all_pgs).reduce((a, c) => Math.max(a, c.filter(e => e != NO_OSD).length), 0); + pg_effsize = Math.min(pg_minsize, pg_effsize) + Math.max(0, Math.min(pg_size, pg_effsize) - pg_minsize) * parity_space; const pg_count = prev_int_pgs.length; const prev_weights = {}; const prev_pg_per_osd = {}; @@ -273,10 +259,13 @@ async function optimize_change({ prev_pgs: prev_int_pgs, osd_tree, pg_size = 3, } const old_pg_size = prev_int_pgs[0].length; // Get all combinations - let all_pgs = random_combinations(osd_tree, pg_size, max_combinations, parity_space > 1); if (old_pg_size == pg_size) { - add_valid_previous(osd_tree, prev_weights, all_pgs); + const still_valid = combinator.check_combinations(Object.keys(prev_weights).map(pg_name => pg_name.substr(3).split('_'))); + for (const pg of still_valid) + { + all_pgs['pg_'+pg.join('_')] = pg; + } } all_pgs = Object.values(all_pgs); const pg_per_osd = {}; @@ -295,8 +284,7 @@ async function optimize_change({ prev_pgs: prev_int_pgs, osd_tree, pg_size = 3, // Calculate total weight - old PG weights const all_pg_names = all_pgs.map(pg => 'pg_'+pg.join('_')); const all_pgs_hash = all_pg_names.reduce((a, c) => { a[c] = true; return a; }, {}); - const all_weights = Object.assign({}, ...Object.values(osd_tree)); - const total_weight = Object.values(all_weights).reduce((a, c) => Number(a) + Number(c), 0); + const total_weight = Object.values(osd_weights).reduce((a, c) => Number(a) + Number(c), 0); // Generate the LP problem let lp = ''; lp += 'max: '+all_pg_names.map(pg_name => ( @@ -311,7 +299,7 @@ async function optimize_change({ prev_pgs: prev_int_pgs, osd_tree, pg_size = 3, )).join(' + '); const rm_osd_pg_count = (prev_pg_per_osd[osd]||[]) .reduce((a, [ old_pg_name, space ]) => (a + (all_pgs_hash[old_pg_name] ? space : 0)), 0); - const osd_pg_count = all_weights[osd]*pg_effsize/total_weight*pg_count - rm_osd_pg_count; + const osd_pg_count = osd_weights[osd]*pg_effsize/total_weight*pg_count - rm_osd_pg_count; lp += osd_sum + ' <= ' + osd_pg_count + ';\n'; } } @@ -421,7 +409,7 @@ async function optimize_change({ prev_pgs: prev_int_pgs, osd_tree, pg_size = 3, int_pgs: new_pgs, differs, osd_differs, - space: pg_effsize * pg_list_space_efficiency(new_pgs, all_weights, pg_minsize, parity_space), + space: pg_effsize * pg_list_space_efficiency(new_pgs, osd_weights, pg_minsize, parity_space), total_space: total_weight, }; } @@ -502,198 +490,6 @@ function put_aligned_pgs(aligned_pgs, int_pgs, prev_int_pgs, keygen) } } -// Convert multi-level osd_tree = { level: number|string, id?: string, size?: number, children?: osd_tree }[] -// levels = { string: number } -// to a two-level osd_tree suitable for all_combinations() -function flatten_tree(osd_tree, levels, failure_domain_level, osd_level, domains = {}, i = { i: 1 }) -{ - osd_level = levels[osd_level] || osd_level; - failure_domain_level = levels[failure_domain_level] || failure_domain_level; - for (const node of osd_tree) - { - if ((levels[node.level] || node.level) < failure_domain_level) - { - flatten_tree(node.children||[], levels, failure_domain_level, osd_level, domains, i); - } - else - { - domains['dom'+(i.i++)] = extract_osds([ node ], levels, osd_level); - } - } - return domains; -} - -function extract_osds(osd_tree, levels, osd_level, osds = {}) -{ - for (const node of osd_tree) - { - if ((levels[node.level] || node.level) >= osd_level) - { - osds[node.id] = node.size; - } - else - { - extract_osds(node.children||[], levels, osd_level, osds); - } - } - return osds; -} - -// ordered = don't treat (x,y) and (y,x) as equal -function random_combinations(osd_tree, pg_size, count, ordered) -{ - let seed = 0x5f020e43; - let rng = () => - { - seed ^= seed << 13; - seed ^= seed >> 17; - seed ^= seed << 5; - return seed + 2147483648; - }; - const osds = Object.keys(osd_tree).reduce((a, c) => { a[c] = Object.keys(osd_tree[c]).sort(); return a; }, {}); - const hosts = Object.keys(osd_tree).sort().filter(h => osds[h].length > 0); - const r = {}; - // Generate random combinations including each OSD at least once - for (let h = 0; h < hosts.length; h++) - { - for (let o = 0; o < osds[hosts[h]].length; o++) - { - const pg = [ osds[hosts[h]][o] ]; - const cur_hosts = [ ...hosts ]; - cur_hosts.splice(h, 1); - for (let i = 1; i < pg_size && i < hosts.length; i++) - { - const next_host = rng() % cur_hosts.length; - const next_osd = rng() % osds[cur_hosts[next_host]].length; - pg.push(osds[cur_hosts[next_host]][next_osd]); - cur_hosts.splice(next_host, 1); - } - const cyclic_pgs = [ pg ]; - if (ordered) - { - for (let i = 1; i < pg.size; i++) - { - cyclic_pgs.push([ ...pg.slice(i), ...pg.slice(0, i) ]); - } - } - for (const pg of cyclic_pgs) - { - while (pg.length < pg_size) - { - pg.push(NO_OSD); - } - r['pg_'+pg.join('_')] = pg; - } - } - } - // Generate purely random combinations - while (count > 0) - { - let host_idx = []; - const cur_hosts = [ ...hosts.map((h, i) => i) ]; - const max_hosts = pg_size < hosts.length ? pg_size : hosts.length; - if (ordered) - { - for (let i = 0; i < max_hosts; i++) - { - const r = rng() % cur_hosts.length; - host_idx[i] = cur_hosts[r]; - cur_hosts.splice(r, 1); - } - } - else - { - for (let i = 0; i < max_hosts; i++) - { - const r = rng() % (cur_hosts.length - (max_hosts - i - 1)); - host_idx[i] = cur_hosts[r]; - cur_hosts.splice(0, r+1); - } - } - let pg = host_idx.map(h => osds[hosts[h]][rng() % osds[hosts[h]].length]); - while (pg.length < pg_size) - { - pg.push(NO_OSD); - } - r['pg_'+pg.join('_')] = pg; - count--; - } - return r; -} - -// Super-stupid algorithm. Given the current OSD tree, generate all possible OSD combinations -// osd_tree = { failure_domain1: { osd1: size1, ... }, ... } -// ordered = return combinations without duplicates having different order -function all_combinations(osd_tree, pg_size, ordered, count) -{ - const hosts = Object.keys(osd_tree).sort(); - const osds = Object.keys(osd_tree).reduce((a, c) => { a[c] = Object.keys(osd_tree[c]).sort(); return a; }, {}); - while (hosts.length < pg_size) - { - osds[NO_OSD] = [ NO_OSD ]; - hosts.push(NO_OSD); - } - let host_idx = []; - let osd_idx = []; - for (let i = 0; i < pg_size; i++) - { - host_idx.push(i); - osd_idx.push(0); - } - const r = []; - while (!count || count < 0 || r.length < count) - { - r.push(host_idx.map((hi, i) => osds[hosts[hi]][osd_idx[i]])); - let inc = pg_size-1; - while (inc >= 0) - { - osd_idx[inc]++; - if (osd_idx[inc] >= osds[hosts[host_idx[inc]]].length) - { - osd_idx[inc] = 0; - inc--; - } - else - { - break; - } - } - if (inc < 0) - { - // no osds left in the current host combination, select the next one - inc = pg_size-1; - same_again: while (inc >= 0) - { - host_idx[inc]++; - for (let prev_host = 0; prev_host < inc; prev_host++) - { - if (host_idx[prev_host] == host_idx[inc]) - { - continue same_again; - } - } - if (host_idx[inc] < (ordered ? hosts.length-(pg_size-1-inc) : hosts.length)) - { - while ((++inc) < pg_size) - { - host_idx[inc] = (ordered ? host_idx[inc-1]+1 : 0); - } - break; - } - else - { - inc--; - } - } - if (inc < 0) - { - break; - } - } - } - return r; -} - function pg_weights_space_efficiency(weights, pg_count, osd_sizes) { const per_osd = {}; @@ -752,11 +548,8 @@ module.exports = { pg_weights_space_efficiency, pg_list_space_efficiency, pg_per_osd_space_efficiency, - flatten_tree, lp_solve, make_int_pgs, align_pgs, - random_combinations, - all_combinations, }; diff --git a/mon/mon.js b/mon/mon.js index 79df13e0..8b264476 100644 --- a/mon/mon.js +++ b/mon/mon.js @@ -6,6 +6,7 @@ const http = require('http'); const crypto = require('crypto'); const os = require('os'); const WebSocket = require('ws'); +const { SimpleCombinator } = require('./simple_pgs.js'); const LPOptimizer = require('./lp-optimizer.js'); const stableStringify = require('./stable-stringify.js'); const PGUtil = require('./PGUtil.js'); @@ -930,7 +931,6 @@ class Mon // Parent's level must be less than child's; OSDs must be leaves const parent = parent_level && parent_level < node_level ? node_cfg.parent : ''; tree[parent].children.push(tree[node_id]); - delete node_cfg.parent; } return { up_osds, levels, osd_tree: tree }; } @@ -1179,7 +1179,25 @@ class Mon return true; } - filter_osds_by_tags(orig_tree, flat_tree, tags) + filter_osds_by_root_node(pool_tree, root_node) + { + if (!root_node) + { + return; + } + pool_tree = pool_tree[pool_cfg.root_node]; + const cur = [ ...(pool_tree||{}).children||[] ]; + for (let i = 0; i < cur.length; i++) + { + if (cur.children) + { + cur.splice(i+1, 1, ...cur.children); + } + } + return cur; + } + + filter_osds_by_tags(orig_tree, tags) { if (!tags) { @@ -1187,30 +1205,22 @@ class Mon } for (const tag of (tags instanceof Array ? tags : [ tags ])) { - for (const host in flat_tree) + for (const osd in orig_tree) { - let found = 0; - for (const osd in flat_tree[host]) + if (orig_tree[osd].level === 'osd' && + (!orig_tree[osd].tags || !orig_tree[osd].tags[tag])) { - if (!orig_tree[osd].tags || !orig_tree[osd].tags[tag]) - delete flat_tree[host][osd]; - else - found++; - } - if (!found) - { - delete flat_tree[host]; + delete orig_tree[osd]; } } } } - filter_osds_by_block_layout(flat_tree, block_size, bitmap_granularity, immediate_commit) + filter_osds_by_block_layout(orig_tree, block_size, bitmap_granularity, immediate_commit) { - for (const host in flat_tree) + for (const osd in orig_tree) { - let found = 0; - for (const osd in flat_tree[host]) + if (orig_tree[osd].level === 'osd') { const osd_stat = this.state.osd.stats[osd]; if (osd_stat && (osd_stat.bs_block_size && osd_stat.bs_block_size != block_size || @@ -1218,16 +1228,8 @@ class Mon osd_stat.immediate_commit == 'small' && immediate_commit == 'all' || osd_stat.immediate_commit == 'none' && immediate_commit != 'none')) { - delete flat_tree[host][osd]; + delete orig_tree[host][osd]; } - else - { - found++; - } - } - if (!found) - { - delete flat_tree[host]; } } } @@ -1237,8 +1239,12 @@ class Mon let aff_osds = up_osds; if (pool_cfg.primary_affinity_tags) { - aff_osds = { ...up_osds }; - this.filter_osds_by_tags(osd_tree, { x: aff_osds }, pool_cfg.primary_affinity_tags); + aff_osds = Object.keys(up_osds).reduce((a, c) => { a[c] = osd_tree[c]; return a; }, {}); + this.filter_osds_by_tags(aff_osds, pool_cfg.primary_affinity_tags); + for (const osd in aff_osds) + { + aff_osds[osd] = true; + } } return aff_osds; } @@ -1250,10 +1256,9 @@ class Mon { return null; } - let pool_tree = osd_tree[pool_cfg.root_node || '']; - pool_tree = pool_tree ? pool_tree.children : []; - pool_tree = LPOptimizer.flatten_tree(pool_tree, levels, pool_cfg.failure_domain, 'osd'); - this.filter_osds_by_tags(osd_tree, pool_tree, pool_cfg.osd_tags); + let pool_tree = osd_tree; + this.filter_osds_by_root_node(pool_tree, pool_cfg.root_node); + this.filter_osds_by_tags(pool_tree, pool_cfg.osd_tags); this.filter_osds_by_block_layout( pool_tree, pool_cfg.block_size || this.config.block_size || 131072, @@ -1276,11 +1281,11 @@ class Mon } const old_pg_count = prev_pgs.length; const optimize_cfg = { - osd_tree: pool_tree, + osd_weights: Object.values(pool_tree).filter(item => item.level === 'osd').reduce((a, c) => { a[c.id] = c.size; return a; }, {}), + combinator: new SimpleCombinator(flatten_tree(osd_tree, levels, pool_cfg.failure_domain, 'osd'), pool_cfg.pg_size, pool_cfg.max_osd_combinations), pg_count: pool_cfg.pg_count, pg_size: pool_cfg.pg_size, pg_minsize: pool_cfg.pg_minsize, - max_combinations: pool_cfg.max_osd_combinations, ordered: pool_cfg.scheme != 'replicated', }; let optimize_result; diff --git a/mon/simple_pgs.js b/mon/simple_pgs.js new file mode 100644 index 00000000..099b0236 --- /dev/null +++ b/mon/simple_pgs.js @@ -0,0 +1,247 @@ +const NO_OSD = 'Z'; + +class SimpleCombinator +{ + constructor(flat_tree, pg_size, max_combinations, ordered) + { + this.osd_tree = flat_tree; + this.pg_size = pg_size; + this.max_combinations = max_combinations; + this.ordered = ordered; + } + + random_combinations() + { + return random_combinations(this.osd_tree, this.pg_size, this.max_combinations, this.ordered); + } + + check_combinations(pgs) + { + return check_combinations(this.osd_tree, pgs); + } +} + +// Convert multi-level osd_tree = { level: number|string, id?: string, size?: number, children?: osd_tree }[] +// levels = { string: number } +// to a two-level osd_tree suitable for all_combinations() +function flatten_tree(osd_tree, levels, failure_domain_level, osd_level, domains = {}, i = { i: 1 }) +{ + osd_level = levels[osd_level] || osd_level; + failure_domain_level = levels[failure_domain_level] || failure_domain_level; + for (const node of osd_tree) + { + if ((levels[node.level] || node.level) < failure_domain_level) + { + flatten_tree(node.children||[], levels, failure_domain_level, osd_level, domains, i); + } + else + { + domains['dom'+(i.i++)] = extract_osds([ node ], levels, osd_level); + } + } + return domains; +} + +function extract_osds(osd_tree, levels, osd_level, osds = {}) +{ + for (const node of osd_tree) + { + if ((levels[node.level] || node.level) >= osd_level) + { + osds[node.id] = node.size; + } + else + { + extract_osds(node.children||[], levels, osd_level, osds); + } + } + return osds; +} + +// ordered = don't treat (x,y) and (y,x) as equal +function random_combinations(osd_tree, pg_size, count, ordered) +{ + let seed = 0x5f020e43; + let rng = () => + { + seed ^= seed << 13; + seed ^= seed >> 17; + seed ^= seed << 5; + return seed + 2147483648; + }; + const osds = Object.keys(osd_tree).reduce((a, c) => { a[c] = Object.keys(osd_tree[c]).sort(); return a; }, {}); + const hosts = Object.keys(osd_tree).sort().filter(h => osds[h].length > 0); + const r = {}; + // Generate random combinations including each OSD at least once + for (let h = 0; h < hosts.length; h++) + { + for (let o = 0; o < osds[hosts[h]].length; o++) + { + const pg = [ osds[hosts[h]][o] ]; + const cur_hosts = [ ...hosts ]; + cur_hosts.splice(h, 1); + for (let i = 1; i < pg_size && i < hosts.length; i++) + { + const next_host = rng() % cur_hosts.length; + const next_osd = rng() % osds[cur_hosts[next_host]].length; + pg.push(osds[cur_hosts[next_host]][next_osd]); + cur_hosts.splice(next_host, 1); + } + while (pg.length < pg_size) + { + pg.push(NO_OSD); + } + r['pg_'+pg.join('_')] = pg; + } + } + // Generate purely random combinations + while (count > 0) + { + let host_idx = []; + const cur_hosts = [ ...hosts.map((h, i) => i) ]; + const max_hosts = pg_size < hosts.length ? pg_size : hosts.length; + if (ordered) + { + for (let i = 0; i < max_hosts; i++) + { + const r = rng() % cur_hosts.length; + host_idx[i] = cur_hosts[r]; + cur_hosts.splice(r, 1); + } + } + else + { + for (let i = 0; i < max_hosts; i++) + { + const r = rng() % (cur_hosts.length - (max_hosts - i - 1)); + host_idx[i] = cur_hosts[r]; + cur_hosts.splice(0, r+1); + } + } + let pg = host_idx.map(h => osds[hosts[h]][rng() % osds[hosts[h]].length]); + while (pg.length < pg_size) + { + pg.push(NO_OSD); + } + r['pg_'+pg.join('_')] = pg; + count--; + } + return r; +} + +// Super-stupid algorithm. Given the current OSD tree, generate all possible OSD combinations +// osd_tree = { failure_domain1: { osd1: size1, ... }, ... } +// ordered = return combinations without duplicates having different order +function all_combinations(osd_tree, pg_size, ordered, count) +{ + const hosts = Object.keys(osd_tree).sort(); + const osds = Object.keys(osd_tree).reduce((a, c) => { a[c] = Object.keys(osd_tree[c]).sort(); return a; }, {}); + while (hosts.length < pg_size) + { + osds[NO_OSD] = [ NO_OSD ]; + hosts.push(NO_OSD); + } + let host_idx = []; + let osd_idx = []; + for (let i = 0; i < pg_size; i++) + { + host_idx.push(i); + osd_idx.push(0); + } + const r = []; + while (!count || count < 0 || r.length < count) + { + r.push(host_idx.map((hi, i) => osds[hosts[hi]][osd_idx[i]])); + let inc = pg_size-1; + while (inc >= 0) + { + osd_idx[inc]++; + if (osd_idx[inc] >= osds[hosts[host_idx[inc]]].length) + { + osd_idx[inc] = 0; + inc--; + } + else + { + break; + } + } + if (inc < 0) + { + // no osds left in the current host combination, select the next one + inc = pg_size-1; + same_again: while (inc >= 0) + { + host_idx[inc]++; + for (let prev_host = 0; prev_host < inc; prev_host++) + { + if (host_idx[prev_host] == host_idx[inc]) + { + continue same_again; + } + } + if (host_idx[inc] < (ordered ? hosts.length-(pg_size-1-inc) : hosts.length)) + { + while ((++inc) < pg_size) + { + host_idx[inc] = (ordered ? host_idx[inc-1]+1 : 0); + } + break; + } + else + { + inc--; + } + } + if (inc < 0) + { + break; + } + } + } + return r; +} + +function check_combinations(osd_tree, pgs) +{ + const hosts = Object.keys(osd_tree).sort(); + const host_per_osd = {}; + for (const host in osd_tree) + { + for (const osd in osd_tree[host]) + { + host_per_osd[osd] = host; + } + } + const res = []; + skip_pg: for (const pg of pgs) + { + const seen_hosts = {}; + for (const osd of pg) + { + if (!host_per_osd[osd] || seen_hosts[host_per_osd[osd]]) + { + continue skip_pg; + } + seen_hosts[host_per_osd[osd]] = true; + } + res.push(pg); + } + return res; +} + +function compat(params) +{ + return { + ...params, + osd_weights: Object.assign({}, ...Object.values(params.osd_tree)), + combinator: new SimpleCombinator(params.osd_tree, params.pg_size, params.max_combinations||10000), + }; +} + +module.exports = { + flatten_tree, + SimpleCombinator, + compat, + NO_OSD, +}; diff --git a/mon/test-nonuniform.js b/mon/test-nonuniform.js index b2029ead..746c9a18 100644 --- a/mon/test-nonuniform.js +++ b/mon/test-nonuniform.js @@ -7,6 +7,7 @@ // This leads to really uneven OSD fill ratio in Ceph even when PGs are perfectly balanced. // But we support this case with the "parity_space" parameter in optimize_initial()/optimize_change(). +const { SimpleCombinator } = require('./simple_pgs.js'); const LPOptimizer = require('./lp-optimizer.js'); const osd_tree = { @@ -114,16 +115,17 @@ Fine, let's try to optimize for it. async function run() { - const all_weights = Object.assign({}, ...Object.values(osd_tree)); - const total_weight = Object.values(all_weights).reduce((a, c) => Number(a) + Number(c), 0); - const eff = LPOptimizer.pg_list_space_efficiency(prev_pgs, all_weights, 2, 2.26); + const osd_weights = Object.assign({}, ...Object.values(osd_tree)); + const total_weight = Object.values(osd_weights).reduce((a, c) => Number(a) + Number(c), 0); + const eff = LPOptimizer.pg_list_space_efficiency(prev_pgs, osd_weights, 2, 2.26); const orig = eff*4.26 / total_weight; console.log('Original efficiency was: '+Math.round(orig*10000)/100+' %'); - let prev = await LPOptimizer.optimize_initial({ osd_tree, pg_size: 3, pg_count: 256, parity_space: 2.26 }); + const combinator = new SimpleCombinator(osd_tree, 3, 10000); + let prev = await LPOptimizer.optimize_initial({ osd_weights, combinator, pg_size: 3, pg_count: 256, parity_space: 2.26 }); LPOptimizer.print_change_stats(prev); - let next = await LPOptimizer.optimize_change({ prev_pgs, osd_tree, pg_size: 3, max_combinations: 10000, parity_space: 2.26 }); + let next = await LPOptimizer.optimize_change({ prev_pgs, osd_weights, combinator, pg_size: 3, parity_space: 2.26 }); LPOptimizer.print_change_stats(next); } diff --git a/mon/test-optimize-simple.js b/mon/test-optimize-simple.js index 70f89047..a439c4a2 100644 --- a/mon/test-optimize-simple.js +++ b/mon/test-optimize-simple.js @@ -1,6 +1,7 @@ // Copyright (c) Vitaliy Filippov, 2019+ // License: VNPL-1.1 (see README.md for details) +const { compat } = require('./simple_pgs.js'); const LPOptimizer = require('./lp-optimizer.js'); async function run() @@ -14,26 +15,26 @@ async function run() let res; console.log('16 PGs, size=3'); - res = await LPOptimizer.optimize_initial({ osd_tree, pg_size: 3, pg_count: 16, ordered: false }); + res = await LPOptimizer.optimize_initial(compat({ osd_tree, pg_size: 3, pg_count: 16, ordered: false })); LPOptimizer.print_change_stats(res, false); assert(res.space == 3, 'Initial distribution'); console.log('\nChange size to 2'); - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree, pg_size: 2, ordered: false }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree, pg_size: 2, ordered: false })); LPOptimizer.print_change_stats(res, false); assert(res.space >= 3*14/16 && res.osd_differs == 0, 'Redistribution'); console.log('\nRemove OSD 3'); const no3_tree = { ...osd_tree }; delete no3_tree['300']; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree: no3_tree, pg_size: 2, ordered: false }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree: no3_tree, pg_size: 2, ordered: false })); LPOptimizer.print_change_stats(res, false); assert(res.space == 2, 'Redistribution after OSD removal'); console.log('\n16 PGs, size=3, ordered'); - res = await LPOptimizer.optimize_initial({ osd_tree, pg_size: 3, pg_count: 16, ordered: true }); + res = await LPOptimizer.optimize_initial(compat({ osd_tree, pg_size: 3, pg_count: 16, ordered: true })); LPOptimizer.print_change_stats(res, false); assert(res.space == 3, 'Initial distribution'); console.log('\nChange size to 2, ordered'); - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree, pg_size: 2, ordered: true }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree, pg_size: 2, ordered: true })); LPOptimizer.print_change_stats(res, false); assert(res.space >= 3*14/16 && res.osd_differs < 8, 'Redistribution'); } diff --git a/mon/test-optimize-undersized.js b/mon/test-optimize-undersized.js index d1a766e6..824404ae 100644 --- a/mon/test-optimize-undersized.js +++ b/mon/test-optimize-undersized.js @@ -1,6 +1,7 @@ // Copyright (c) Vitaliy Filippov, 2019+ // License: VNPL-1.1 (see README.md for details) +const { compat, flatten_tree } = require('./simple_pgs.js'); const LPOptimizer = require('./lp-optimizer.js'); const crush_tree = [ @@ -36,44 +37,44 @@ const crush_tree = [ ] }, ]; -const osd_tree = LPOptimizer.flatten_tree(crush_tree, {}, 1, 3); +const osd_tree = flatten_tree(crush_tree, {}, 1, 3); console.log(osd_tree); async function run() { const cur_tree = {}; console.log('Empty tree:'); - let res = await LPOptimizer.optimize_initial({ osd_tree: cur_tree, pg_size: 3, pg_count: 256 }); + let res = await LPOptimizer.optimize_initial(compat({ osd_tree: cur_tree, pg_size: 3, pg_count: 256 })); LPOptimizer.print_change_stats(res, false); assert(res.space == 0); console.log('\nAdding 1st failure domain:'); cur_tree['dom1'] = osd_tree['dom1']; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 })); LPOptimizer.print_change_stats(res, false); assert(res.space == 12 && res.total_space == 12); console.log('\nAdding 2nd failure domain:'); cur_tree['dom2'] = osd_tree['dom2']; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 })); LPOptimizer.print_change_stats(res, false); assert(res.space == 24 && res.total_space == 24); console.log('\nAdding 3rd failure domain:'); cur_tree['dom3'] = osd_tree['dom3']; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 })); LPOptimizer.print_change_stats(res, false); assert(res.space == 36 && res.total_space == 36); console.log('\nRemoving 3rd failure domain:'); delete cur_tree['dom3']; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 })); LPOptimizer.print_change_stats(res, false); assert(res.space == 24 && res.total_space == 24); console.log('\nRemoving 2nd failure domain:'); delete cur_tree['dom2']; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 })); LPOptimizer.print_change_stats(res, false); assert(res.space == 12 && res.total_space == 12); console.log('\nRemoving 1st failure domain:'); delete cur_tree['dom1']; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree: cur_tree, pg_size: 3 })); LPOptimizer.print_change_stats(res, false); assert(res.space == 0); } diff --git a/mon/test-optimize-unfeasible.js b/mon/test-optimize-unfeasible.js index c632f9cc..8c685eca 100644 --- a/mon/test-optimize-unfeasible.js +++ b/mon/test-optimize-unfeasible.js @@ -1,6 +1,7 @@ // Copyright (c) Vitaliy Filippov, 2019+ // License: VNPL-1.1 (see README.md for details) +const { compat } = require('./simple_pgs.js'); const LPOptimizer = require('./lp-optimizer.js'); const osd_tree = { @@ -20,13 +21,13 @@ async function run() { let res; console.log('256 PGs, 3+3 OSDs, size=2'); - res = await LPOptimizer.optimize_initial({ osd_tree, pg_size: 2, pg_count: 256 }); + res = await LPOptimizer.optimize_initial(compat({ osd_tree, pg_size: 2, pg_count: 256 })); LPOptimizer.print_change_stats(res, false); // Should NOT fail with the "unfeasible or unbounded" exception console.log('\nRemoving osd.2'); delete osd_tree[100][2]; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree, pg_size: 2 }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree, pg_size: 2 })); LPOptimizer.print_change_stats(res, false); } diff --git a/mon/test-optimize.js b/mon/test-optimize.js index 309650b3..34b357aa 100644 --- a/mon/test-optimize.js +++ b/mon/test-optimize.js @@ -1,6 +1,7 @@ // Copyright (c) Vitaliy Filippov, 2019+ // License: VNPL-1.1 (see README.md for details) +const { compat, flatten_tree } = require('./simple_pgs.js'); const LPOptimizer = require('./lp-optimizer.js'); const osd_tree = { @@ -84,31 +85,31 @@ async function run() // Space efficiency is ~99% in all cases. console.log('256 PGs, size=2'); - res = await LPOptimizer.optimize_initial({ osd_tree, pg_size: 2, pg_count: 256 }); + res = await LPOptimizer.optimize_initial(compat({ osd_tree, pg_size: 2, pg_count: 256 })); LPOptimizer.print_change_stats(res, false); console.log('\nAdding osd.8'); osd_tree[500][8] = 3.58589; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree, pg_size: 2 }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree, pg_size: 2 })); LPOptimizer.print_change_stats(res, false); console.log('\nRemoving osd.8'); delete osd_tree[500][8]; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree, pg_size: 2 }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree, pg_size: 2 })); LPOptimizer.print_change_stats(res, false); console.log('\n256 PGs, size=3'); - res = await LPOptimizer.optimize_initial({ osd_tree, pg_size: 3, pg_count: 256 }); + res = await LPOptimizer.optimize_initial(compat({ osd_tree, pg_size: 3, pg_count: 256 })); LPOptimizer.print_change_stats(res, false); console.log('\nAdding osd.8'); osd_tree[500][8] = 3.58589; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree, pg_size: 3 }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree, pg_size: 3 })); LPOptimizer.print_change_stats(res, false); console.log('\nRemoving osd.8'); delete osd_tree[500][8]; - res = await LPOptimizer.optimize_change({ prev_pgs: res.int_pgs, osd_tree, pg_size: 3 }); + res = await LPOptimizer.optimize_change(compat({ prev_pgs: res.int_pgs, osd_tree, pg_size: 3 })); LPOptimizer.print_change_stats(res, false); console.log('\n256 PGs, size=3, failure domain=rack'); - res = await LPOptimizer.optimize_initial({ osd_tree: LPOptimizer.flatten_tree(crush_tree, {}, 1, 3), pg_size: 3, pg_count: 256 }); + res = await LPOptimizer.optimize_initial(compat({ osd_tree: flatten_tree(crush_tree, {}, 1, 3), pg_size: 3, pg_count: 256 })); LPOptimizer.print_change_stats(res, false); }