diff --git a/packages/xo-server-load-balancer/src/index.js b/packages/xo-server-load-balancer/src/index.js index ce7311e05..75a4ad7ca 100644 --- a/packages/xo-server-load-balancer/src/index.js +++ b/packages/xo-server-load-balancer/src/index.js @@ -25,10 +25,6 @@ const debug = LOAD_BALANCER_DEBUG const PERFORMANCE_MODE = 0 const DENSITY_MODE = 1 -const LOW_BEHAVIOR = 0 -const NORMAL_BEHAVIOR = 1 -const AGGRESSIVE_BEHAVIOR = 2 - // Delay between each ressources evaluation in minutes. // Must be less than MINUTES_OF_HISTORICAL_DATA. const EXECUTION_DELAY = 1 @@ -40,6 +36,7 @@ const DEFAULT_CRITICAL_THRESHOLD_CPU = 90.0 // Memory threshold in MB. const DEFAULT_CRITICAL_THRESHOLD_MEMORY_FREE = 64.0 +// Thresholds factors. const HIGH_THRESHOLD_FACTOR = 0.85 const LOW_THRESHOLD_FACTOR = 0.25 @@ -79,22 +76,6 @@ export const configurationSchema = { ] }, - behavior: { - type: 'object', - - properties: { - low: { type: 'boolean' }, - normal: { type: 'boolean' }, - aggressive: { type: 'boolean' } - }, - - oneOf: [ - { required: ['low'] }, - { required: ['normal'] }, - { required: ['aggressive'] } - ] - }, - pools: { type: 'array', description: 'list of pools id where to apply the policy', @@ -118,6 +99,8 @@ export const configurationSchema = { // =================================================================== +// Create a job not enabled by default. +// A job is a cron task, a running and enabled state. const makeJob = (cronPattern, fn) => { const job = { running: false, @@ -146,6 +129,23 @@ const makeJob = (cronPattern, fn) => { return job } +// Compare a list of objects and give the best. +function searchObject (objects, fun) { + let object = objects[0] + + for (let i = 1; i < objects.length; i++) { + if (fun(object, objects[i]) > 0) { + object = objects[i] + } + } + + return object +} + +// =================================================================== +// Averages. +// =================================================================== + function computeAverage (values, nPoints = values.length) { let sum = 0 let tot = 0 @@ -204,28 +204,15 @@ function setRealCpuAverageOfVms (vms, vmsAverages) { } } -function searchObject (objects, fun) { - let object = objects[0] - - for (let i = 1; i < objects.length; i++) { - if (fun(object, objects[i]) > 0) { - object = objects[i] - } - } - - return object -} - // =================================================================== class Plan { - constructor (xo, { name, mode, behavior, poolIds, thresholds = {} }) { + constructor (xo, name, poolIds, { + thresholds = {} + } = {}) { this.xo = xo - this._name = name // Useful ? - this._mode = mode - this._behavior = behavior + this._name = name this._poolIds = poolIds - this._thresholds = { cpu: { critical: thresholds.cpu || DEFAULT_CRITICAL_THRESHOLD_CPU @@ -251,29 +238,24 @@ class Plan { } } - async execute () { - if (this._mode === PERFORMANCE_MODE) { - await this._executeInPerformanceMode() - } else { - await this._executeInDensityMode() - } + execute () { + throw new Error('Not implemented') } - // ================================================================= - // Performance mode. - // ================================================================= + // =================================================================== + // Get hosts to optimize. + // =================================================================== - async _executeInPerformanceMode () { + async _findHostsToOptimize () { const hosts = this._getHosts() const hostsStats = await this._getHostsStats(hosts, 'minutes') // 1. Check if a ressource's utilization exceeds threshold. const avgNow = computeRessourcesAverage(hosts, hostsStats, EXECUTION_DELAY) - let exceededHosts = this._checkRessourcesThresholds(hosts, avgNow) + const toOptimize = this._checkRessourcesThresholds(hosts, avgNow) // No ressource's utilization problem. - if (exceededHosts.length === 0) { - debug('No ressource\'s utilization problem.') + if (toOptimize.length === 0) { return } @@ -281,118 +263,15 @@ class Plan { const avgBefore = computeRessourcesAverage(hosts, hostsStats, MINUTES_OF_HISTORICAL_DATA) const avgWithRatio = computeRessourcesAverageWithWeight(avgNow, avgBefore, 0.75) - exceededHosts = this._checkRessourcesThresholds(exceededHosts, avgWithRatio) - - // No ressource's utilization problem. - if (exceededHosts.length === 0) { - debug('No ressource\'s utilization problem.') - return + return { + toOptimize: this._checkRessourcesThresholds(toOptimize, avgWithRatio), + averages: avgWithRatio, + hosts } - - // 3. Search the worst exceeded host by priority. - const toOptimize = searchObject(exceededHosts, (a, b) => { - a = avgWithRatio[a.id] - b = avgWithRatio[b.id] - - return (b.cpu - a.cpu) || (a.memoryFree - b.memoryFree) - }) - - // 4. Search bests combinations for the worst host. - await this._optimize( - toOptimize, - filter(hosts, host => host.id !== toOptimize.id), - avgWithRatio - ) } - async _optimize (exceededHost, hosts, hostsAverages) { - const vms = await this._getVms(exceededHost.id) - const vmsStats = await this._getVmsStats(vms, 'minutes') - const vmsAverages = computeRessourcesAverageWithWeight( - computeRessourcesAverage(vms, vmsStats, EXECUTION_DELAY), - computeRessourcesAverage(vms, vmsStats, MINUTES_OF_HISTORICAL_DATA), - 0.75 - ) - - // Compute real CPU usage. Virtuals cpus to reals cpus. - setRealCpuAverageOfVms(vms, vmsAverages) - - // Sort vms by cpu usage. (higher to lower) - vms.sort((a, b) => - vmsAverages[b.id].cpu - vmsAverages[a.id].cpu - ) - - const exceededAverages = hostsAverages[exceededHost.id] - const promises = [] - - const xapiSrc = this.xo.getXapi(exceededHost) - - for (const vm of vms) { - // Search host with lower cpu usage. - const destination = searchObject(hosts, (a, b) => - hostsAverages[b.id].cpu - hostsAverages[a.id].cpu - ) - const destinationAverages = hostsAverages[destination.id] - const vmAverages = vmsAverages[vm.id] - - // Unable to move the vm. - if ( - exceededAverages.cpu - vmAverages.cpu < destinationAverages.cpu + vmAverages.cpu || - destinationAverages.memoryFree < vmAverages.memory - ) { - continue - } - - exceededAverages.cpu -= vmAverages.cpu - destinationAverages.cpu += vmAverages.cpu - - exceededAverages.memoryFree += vmAverages.memory - destinationAverages.memoryFree -= vmAverages.memory - - debug(`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${exceededHost.id})`) - - // promises.push( - // xapiSrc.migrateVm(vm._xapiId, this.xo.getXapi(destination), destination._xapiId) - // ) - } - - return - } - - // ================================================================= - // Density mode. - // ================================================================= - - async _executeInDensityMode () { - throw new Error('not yet implemented') - } - - // ================================================================= - // Check ressources. - // ================================================================= - - _checkRessourcesThresholds (objects, averages, mode = PERFORMANCE_MODE) { - if (mode === PERFORMANCE_MODE) { - return filter(objects, object => { - const objectAverages = averages[object.id] - - return ( - objectAverages.cpu >= this._thresholds.cpu.high || - objectAverages.memoryFree <= this._thresholds.memoryFree.high - ) - }) - } else if (mode === DENSITY_MODE) { - return filter(objects, object => { - const objectAverages = averages[object.id] - - return ( - objectAverages.cpu < this._thresholds.cpu.low || - objectAverages.memoryFree > this._thresholds.memoryFree.low - ) - }) - } else { - throw new Error('Unsupported load balancing mode.') - } + _checkRessourcesThresholds () { + throw new Error('Not implemented') } // =================================================================== @@ -451,6 +330,145 @@ class Plan { } } +// =================================================================== + +class PerformancePlan extends Plan { + constructor (xo, name, poolIds, options) { + super(xo, name, poolIds, options) + } + + _checkRessourcesThresholds (objects, averages) { + return filter(objects, object => { + const objectAverages = averages[object.id] + + return ( + objectAverages.cpu >= this._thresholds.cpu.high || + objectAverages.memoryFree <= this._thresholds.memoryFree.high + ) + }) + } + + async execute () { + const { + averages, + hosts, + toOptimize + } = await this._findHostsToOptimize() + + if (toOptimize.length === 0) { + return + } + + const exceededHost = searchObject(toOptimize, (a, b) => { + a = averages[a.id] + b = averages[b.id] + + return (b.cpu - a.cpu) || (a.memoryFree - b.memoryFree) + }) + + // 3. Search bests combinations for the worst host. + await this._optimize({ + exceededHost, + hosts: filter(hosts, host => host.id !== exceededHost.id), + hostsAverages: averages + }) + } + + async _optimize ({ exceededHost, hosts, hostsAverages }) { + const vms = await this._getVms(exceededHost.id) + const vmsStats = await this._getVmsStats(vms, 'minutes') + const vmsAverages = computeRessourcesAverageWithWeight( + computeRessourcesAverage(vms, vmsStats, EXECUTION_DELAY), + computeRessourcesAverage(vms, vmsStats, MINUTES_OF_HISTORICAL_DATA), + 0.75 + ) + + // Compute real CPU usage. Virtuals cpus to reals cpus. + setRealCpuAverageOfVms(vms, vmsAverages) + + // Sort vms by cpu usage. (higher to lower) + vms.sort((a, b) => + vmsAverages[b.id].cpu - vmsAverages[a.id].cpu + ) + + const exceededAverages = hostsAverages[exceededHost.id] + const promises = [] + + const xapiSrc = this.xo.getXapi(exceededHost) + + for (const vm of vms) { + // Search host with lower cpu usage. + const destination = searchObject(hosts, (a, b) => + hostsAverages[b.id].cpu - hostsAverages[a.id].cpu + ) + const destinationAverages = hostsAverages[destination.id] + const vmAverages = vmsAverages[vm.id] + + // Unable to move the vm. + if ( + exceededAverages.cpu - vmAverages.cpu < destinationAverages.cpu + vmAverages.cpu || + destinationAverages.memoryFree > vmAverages.memory + ) { + continue + } + + exceededAverages.cpu -= vmAverages.cpu + destinationAverages.cpu += vmAverages.cpu + + exceededAverages.memoryFree += vmAverages.memory + destinationAverages.memoryFree -= vmAverages.memory + + debug(`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${exceededHost.id})`) + + // promises.push( + // xapiSrc.migrateVm(vm._xapiId, this.xo.getXapi(destination), destination._xapiId) + // ) + } + + await Promise.all(promises) + + return + } +} + +// =================================================================== + +class DensityPlan extends Plan { + constructor (xo, name, poolIds, options) { + super(xo, name, poolIds, options) + } + + _checkRessourcesThresholds (objects, averages) { + return filter(objects, object => { + const objectAverages = averages[object.id] + + return ( + objectAverages.cpu < this._thresholds.cpu.low || + objectAverages.memoryFree > this._thresholds.memoryFree.low + ) + }) + } + + async execute () { + throw new Error('Not implemented') + + const hosts = this._getHosts() + const hostsStats = await this._getHostsStats(hosts, 'minutes') + + // 1. Check if a ressource's utilization is under lower threshold. + const avgNow = computeRessourcesAverage(hosts, hostsStats, EXECUTION_DELAY) + let exceededHosts = this._checkRessourcesThresholds(hosts, avgNow, DENSITY_MODE) + + // No ressource's utilization problem. + if (exceededHosts.length === 0) { + debug('No optimization found.') + return + } + + // TODO + } +} + // =================================================================== // =================================================================== @@ -479,22 +497,13 @@ class LoadBalancerPlugin { if (plans) { for (const plan of plans) { - const mode = plan.mode.performance - ? PERFORMANCE_MODE - : DENSITY_MODE - - const { behavior: planBehavior } = plan - let behavior - - if (planBehavior.low) { - behavior = LOW_BEHAVIOR - } else if (planBehavior.normal) { - behavior = NORMAL_BEHAVIOR - } else { - behavior = AGGRESSIVE_BEHAVIOR - } - - this._addPlan({ name: plan.name, mode, behavior, poolIds: plan.pools }) + this._addPlan({ + name: plan.name, + mode: plan.mode.performance + ? PERFORMANCE_MODE + : DENSITY_MODE, + poolIds: plan.pools + }) } } @@ -511,15 +520,19 @@ class LoadBalancerPlugin { this._job.cron.stop() } - _addPlan (plan) { - const poolIds = plan.poolIds = uniq(plan.poolIds) + _addPlan ({ name, mode, poolIds }) { + poolIds = uniq(poolIds) // Check already used pools. - if (intersection(poolIds, this._poolIds) > 0) { + if (intersection(poolIds, this._poolIds).length > 0) { throw new Error(`Pool(s) already included in an other plan: ${poolIds}`) } - this._plans.push(new Plan(this.xo, plan)) + this._poolIds = this._poolIds.concat(poolIds) + this._plans.push(mode === PERFORMANCE_MODE + ? new PerformancePlan(this.xo, name, poolIds) + : new DensityPlan(this.xo, name, poolIds) + ) } _executePlans () {