Class: AdmissionControlledResourceScheduler

Inherits:
Object
  • Object
show all
Defined in:
lib/rbvmomi/utils/admission_control.rb

Overview

An admission controlled resource scheduler for large scale vSphere deployments

While DRS (Dynamic Resource Scheduler) in vSphere handles CPU and Memory allocations within a single vSphere cluster, larger deployments require another layer of scheduling to make the use of multiple clusters transparent. So this class doesn’t replace DRS, but in fact works on top of it.

The scheduler in this class performs admission control to make sure clusters don’t get overloaded. It does so by adding additional metrics to the already existing CPU and Memory reservation system that DRS has. After admission control it also performs very basic initial placement. Note that in-cluster placement and load-balancing is left to DRS. Also note that no cross-cluster load balancing is done.

This class uses the concept of a Pod: A set of clusters that share a set of datastores. From a datastore perspective, we are free to place a VM on any host or cluster. So admission control is done at the Pod level first. Pods are automatically dicovered based on lists of clusters and datastores.

Admission control covers the following metrics:

  • Host availability: If no hosts are available within a cluster or pod, admission is denied.

  • Minimum free space: If a datastore falls below this free space percentage, admission to it will be denied. Admission to a pod is granted as long at least one datastore passes admission control.

  • Maximum number of VMs: If a Pod exceeds a configured number of powered on VMs, admission is denied. This is a crude but effective catch-all metric in case users didn’t set proper individual CPU or Memory reservations or if the scalability limit doesn’t originate from CPU or Memory.

Placement after admission control:

  • Cluster selection: A load metric based on a combination of CPU and Memory load is used to always select the “least loaded” cluster. The metric is very crude and only meant to do very rough load balancing. If DRS clusters are large enough, this is good enough in most cases though.

  • Datastore selection: Right now NO intelligence is implemented here.

Usage: Instantiate the class, call make_placement_decision and then use the exposed computer (cluster), resource pool, vm_folder and datastore. Currently once computed, a new updated placement can’t be generated.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(vim, opts = {}) ⇒ AdmissionControlledResourceScheduler

Returns a new instance of AdmissionControlledResourceScheduler.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/rbvmomi/utils/admission_control.rb', line 50

def initialize vim, opts = {}
  @vim = vim

  @datacenter = opts[:datacenter]
  @datacenter_path = opts[:datacenter_path]
  @vm_folder = opts[:vm_folder]
  @vm_folder_path = opts[:vm_folder_path]
  @rp_path = opts[:rp_path]
  @computers = opts[:computers]
  @computer_names = opts[:computer_names]
  @datastores = opts[:datastores]
  @datastore_paths = opts[:datastore_paths]

  @max_vms_per_pod = opts[:max_vms_per_pod]
  @min_ds_free = opts[:min_ds_free]
  @service_docs_url = opts[:service_docs_url]

  @pc = @vim.serviceContent.propertyCollector
  @root_folder = @vim.serviceContent.rootFolder

  @logger = opts[:logger]
end

Instance Attribute Details

#rpObject (readonly)

Returns the value of attribute rp.



48
49
50
# File 'lib/rbvmomi/utils/admission_control.rb', line 48

def rp
  @rp
end

Instance Method Details

#computersArray

Returns the candidate computers (aka clusters). If not set yet, uses the computer_names to look them up.

Returns:

  • (Array)

    List of [RbVmomi::VIM::ClusterComputeResource, Hash] tuples, where the Hash is a list of stats about the computer



133
134
135
136
137
138
139
140
141
# File 'lib/rbvmomi/utils/admission_control.rb', line 133

def computers
  if !@computers
    @computers = @computer_names.map do |name|
      computer = datacenter.find_compute_resource(name)
      [computer, computer.stats]
    end
  end
  @computers
end

#datacenterRbVmomi::VIM::Datacenter

Returns the used Datacenter. If not set yet, uses the datacenter_path to lookup the datacenter.

Returns:



104
105
106
107
108
109
110
# File 'lib/rbvmomi/utils/admission_control.rb', line 104

def datacenter
  if !@datacenter
    @datacenter = @root_folder.traverse(@datacenter_path, RbVmomi::VIM::Datacenter)
    raise "datacenter #{@datacenter_path} not found" if !@datacenter
  end
  @datacenter
end

#datastore(placementHint = nil) ⇒ RbVmomi::VIM::Datastore

Returns the datastore to be used for placement. If not set yet, picks a datastore without much intelligence, as long as it passes admission control.

Returns:



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/rbvmomi/utils/admission_control.rb', line 326

def datastore placementHint = nil
  return @datastore if @datastore

  pod_datastores = pick_computer.datastore & datastores

  eligible = pod_datastores.select do |ds|
    min_ds_free = @min_ds_free
    if min_ds_free && min_ds_free > 0
      ds_sum = @datastore_props[ds]['summary']
      free_percent = ds_sum.freeSpace.to_f * 100 / ds_sum.capacity
      free_percent > min_ds_free
    else
      true
    end
  end

  raise "Couldn't find any eligible datastore. Admission control should have prevented this" if eligible.length == 0

  if placementHint && placementHint > 0
    @datastore = eligible[placementHint % eligible.length]
  else
    @datastore = eligible.first
  end
  @datastore
end

#datastoresArray

Returns the candidate datastores. If not set yet, uses the datastore_paths to lookup the datastores under the datacenter. As a side effect, also looks up properties about all the datastores

Returns:

  • (Array)

    List of RbVmomi::VIM::Datastore



116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/rbvmomi/utils/admission_control.rb', line 116

def datastores
  if !@datastores
    @datastores = @datastore_paths.map do |path|
      ds = datacenter.datastoreFolder.traverse(path, RbVmomi::VIM::Datastore)
      raise "datastore #{path} not found" if !ds

      ds
    end
  end
  @datastore_props = @pc.collectMultiple(@datastores, 'summary', 'name') if !@datastore_props
  @datastores
end

#filtered_podsArray

Returns the list of pods that pass admission control. If not set yet, performs admission control to compute the list. If no pods passed the admission control, an exception is thrown.

Returns:

  • (Array)

    List of pods, where a pod is a list of RbVmomi::VIM::ClusterComputeResource



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/rbvmomi/utils/admission_control.rb', line 215

def filtered_pods
  # This function applies admission control and returns those pods that have
  # passed admission control. An exception is thrown if access was denied to
  # all pods.
  if !@filtered_pods
    log 'Performing admission control:'
    @filtered_pods = self.pods.select do |pod|
      # Gather some statistics about the pod ...
      on_vms = pod_vms(pod).select{ |k, v| v['runtime.powerState'] == 'poweredOn' }
      num_pod_vms = on_vms.length
      pod_datastores = self.pod_datastores(pod)
      log "Pod: #{pod.map{ |x| x.name }.join(', ')}"
      log "   #{num_pod_vms} VMs"
      pod_datastores.each do |ds|
        ds_sum = @datastore_props[ds]['summary']
        @datastore_props[ds]['free_percent'] = ds_sum.freeSpace.to_f * 100 / ds_sum.capacity
      end
      pod_datastores.each do |ds|
        ds_props = @datastore_props[ds]
        ds_name = ds_props['name']
        free = ds_props['free_percent']
        free_gb = ds_props['summary'].freeSpace.to_f / 1024**3
        free_str = '%.2f GB (%.2f%%)' % [free_gb, free]
        log "   Datastore #{ds_name}: #{free_str} free"
      end

      # Admission check: VM limit
      denied = false
      max_vms = @max_vms_per_pod
      if max_vms && max_vms > 0
        if num_pod_vms > max_vms
          err = "VM limit (#{max_vms}) exceeded on this Pod"
          denied = true
        end
      end

      # Admission check: Free space on datastores
      min_ds_free = @min_ds_free
      if min_ds_free && min_ds_free > 0
        # We need at least one datastore with enough free space
        low_list = pod_datastores.select do |ds|
          @datastore_props[ds]['free_percent'] <= min_ds_free
        end

        if low_list.length == pod_datastores.length
          dsNames = low_list.map{ |ds| @datastore_props[ds]['name'] }.join(', ')
          err = "Datastores #{dsNames} below minimum free disk space (#{min_ds_free}%)"
          denied = true
        end
      end

      # Admission check: Hosts are available
      if !denied
        hosts_available = pod.any? do |computer|
          stats = Hash[self.computers][computer]
          stats[:totalCPU] > 0 && stats[:totalMem] > 0
        end
        if !hosts_available
          err = 'No hosts are current available in this pod'
          denied = true
        end
      end

      if denied
        log "   Admission DENIED: #{err}"
      else
        log '   Admission granted'
      end

      !denied
    end
  end
  if @filtered_pods.length == 0
    log "Couldn't find any Pod with enough resources."
    log "Check #{@service_docs_url} to see which other Pods you may be able to use" if @service_docs_url
    raise 'Admission denied'
  end
  @filtered_pods
end

#log(x) ⇒ Object



73
74
75
76
77
78
79
# File 'lib/rbvmomi/utils/admission_control.rb', line 73

def log x
  if @logger
    @logger.info x
  else
    puts "#{Time.now}: #{x}"
  end
end

#make_placement_decision(opts = {}) ⇒ Object

Runs the placement algorithm and populates all the various properties as a side effect. Run this first, before using the other functions of this class.



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/rbvmomi/utils/admission_control.rb', line 355

def make_placement_decision opts = {}
  self.filtered_pods
  self.pick_computer opts[:placementHint]
  log "Selected compute resource: #{@computer.name}"

  @rp = @computer.resourcePool.traverse(@rp_path)
  raise "Resource pool #{@rp_path} not found" if !@rp

  log "Resource pool: #{@rp.pretty_path}"

  stats = @computer.stats
  if stats[:totalMem] > 0 && stats[:totalCPU] > 0
    cpu_load = "#{(100*stats[:usedCPU])/stats[:totalCPU]}% cpu"
    mem_load = "#{(100*stats[:usedMem])/stats[:totalMem]}% mem"
    log "Cluster utilization: #{cpu_load}, #{mem_load}"
  end

  user_vms = vm_folder.inventory_flat('VirtualMachine' => %w(name storage)).select do |k, v|
    k.is_a?(RbVmomi::VIM::VirtualMachine)
  end
  numVms = user_vms.length
  unshared = user_vms.map do |vm, info|
    info['storage'].perDatastoreUsage.map{ |x| x.unshared }.inject(0, &:+)
  end.inject(0, &:+)
  log "User stats: #{numVms} VMs using %.2fGB of storage" % [unshared.to_f / 1024**3]

  @placement_hint = opts[:placement_hint] || (rand(100) + 1)
  datastore = self.datastore @placement_hint
  log "Datastore: #{datastore.name}"
end

#pick_computer(placementhint = nil) ⇒ RbVmomi::VIM::ClusterComputeResource

Returns the computer (aka cluster) to be used for placement. If not set yet, computs the least loaded cluster (using a metric that combines CPU and Memory load) that passes admission control.

Returns:

  • (RbVmomi::VIM::ClusterComputeResource)

    Chosen computer (aka cluster)



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/rbvmomi/utils/admission_control.rb', line 299

def pick_computer placementhint = nil
  if !@computer
    # Out of the pods to which we have been granted access, pick the cluster
    # (aka computer) with the lowest CPU/Mem utilization for load balancing
    available = self.filtered_pods.flatten
    eligible = self.computers.select do |computer, stats|
      available.member?(computer) && stats[:totalCPU] > 0 and stats[:totalMem] > 0
    end
    computer = nil
    if placementhint
      computer = eligible.map{ |x| x[0] }[placementhint % eligible.length] if eligible.length > 0
    else
      computer, = eligible.min_by do |computer, stats|
        2**(stats[:usedCPU].to_f/stats[:totalCPU]) + (stats[:usedMem].to_f/stats[:totalMem])
      end
    end

    raise 'No clusters available, should have been prevented by admission control' if !computer

    @computer = computer
  end
  @computer
end

#pod_datastores(pod) ⇒ Array

Returns all candidate datastores for a given pod.

Returns:

  • (Array)

    List of RbVmomi::VIM::Datastore



207
208
209
# File 'lib/rbvmomi/utils/admission_control.rb', line 207

def pod_datastores pod
  pod.first.datastore & self.datastores
end

#pod_vms(pod) ⇒ Hash

Returns all VMs residing with a pod. Doesn’t account for templates. Does so very efficiently using a single API query.

Returns:

  • (Hash)

    Hash of VMs as keys and their properties as values.



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/rbvmomi/utils/admission_control.rb', line 166

def pod_vms pod
  # This function retrieves all VMs residing inside a pod
  filterSpec = RbVmomi::VIM.PropertyFilterSpec(
    objectSet: pod.map do |computer, stats|
      {
        obj: computer.resourcePool,
        selectSet: [
          RbVmomi::VIM.TraversalSpec(
            name: 'tsFolder',
            type: 'ResourcePool',
            path: 'resourcePool',
            skip: false,
            selectSet: [
              RbVmomi::VIM.SelectionSpec(name: 'tsFolder'),
              RbVmomi::VIM.SelectionSpec(name: 'tsVM'),
            ]
          ),
          RbVmomi::VIM.TraversalSpec(
            name: 'tsVM',
            type: 'ResourcePool',
            path: 'vm',
            skip: false,
            selectSet: [],
          )
        ]
      }
    end,
    propSet: [
      { type: 'ResourcePool', pathSet: ['name'] },
      { type: 'VirtualMachine', pathSet: %w(runtime.powerState) }
    ]
  )

  result = @vim.propertyCollector.RetrieveProperties(specSet: [filterSpec])

  out = result.map { |x| [x.obj, Hash[x.propSet.map { |y| [y.name, y.val] }]] }
  out.select{ |obj, props| obj.is_a?(RbVmomi::VIM::VirtualMachine) }
end

#podsArray

Returns the candidate pods. If not set, automatically computes the pods based on the list of computers (aka clusters) and datastores.

Returns:

  • (Array)

    List of pods, where a pod is a list of RbVmomi::VIM::ClusterComputeResource



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/rbvmomi/utils/admission_control.rb', line 146

def pods
  if !@pods
    # A pod is defined as a set of clusters (aka computers) that share the same
    # datastore accessibility. Computing pods is done automatically using simple
    # set theory math.
    computersProps = @pc.collectMultiple(computers.map{ |x| x[0] }, 'datastore')
    @pods = computers.map do |computer, stats|
      computersProps[computer]['datastore'] & self.datastores
    end.uniq.map do |ds_list|
      computers.map{ |x| x[0] }.select do |computer|
        (computer.datastore & self.datastores) == ds_list
      end
    end
  end
  @pods
end

#vm_folderRbVmomi::VIM::Folder

Returns the used VM folder. If not set yet, uses the vm_folder_path to lookup the folder. If it doesn’t exist, it is created. Collisions between multiple clients concurrently creating the same folder are handled.

Returns:



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/rbvmomi/utils/admission_control.rb', line 85

def vm_folder
  retries = 1
  begin
    @vm_folder ||= datacenter.vmFolder.traverse!(@vm_folder_path, RbVmomi::VIM::Folder)
    raise "VM folder #{@vm_folder_path} not found" if !@vm_folder
  rescue RbVmomi::Fault => fault
    if !fault.fault.is_a?(RbVmomi::VIM::DuplicateName)
      raise
    else
      retries -= 1
      retry if retries >= 0
    end
  end
  @vm_folder
end