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.



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

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.



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

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



139
140
141
142
143
144
145
146
147
# File 'lib/rbvmomi/utils/admission_control.rb', line 139

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:



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

def datacenter
  if !@datacenter
    @datacenter = @root_folder.traverse(@datacenter_path, RbVmomi::VIM::Datacenter) 
    if !@datacenter 
      fail "datacenter #{@datacenter_path} not found"
    end
  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:



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/rbvmomi/utils/admission_control.rb', line 337

def datastore placementHint = nil
  if @datastore
    return @datastore
  end
  
  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

  if eligible.length == 0
    fail "Couldn't find any eligible datastore. Admission control should have prevented this"
  end
  
  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



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/rbvmomi/utils/admission_control.rb', line 119

def datastores
  if !@datastores
    @datastores = @datastore_paths.map do |path|
      ds = datacenter.datastoreFolder.traverse(path, RbVmomi::VIM::Datastore)
      if !ds 
        fail "datastore #{path} not found"
      end
      ds
    end
  end
  if !@datastore_props
    @datastore_props = @pc.collectMultiple(@datastores, 'summary', 'name')
  end
  @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



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
294
295
296
297
298
299
300
301
# File 'lib/rbvmomi/utils/admission_control.rb', line 221

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."
    if @service_docs_url
      log "Check #{@service_docs_url} to see which other Pods you may be able to use"
    end
    fail "Admission denied"
  end
  @filtered_pods
end

#log(x) ⇒ Object



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

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.



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/rbvmomi/utils/admission_control.rb', line 370

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)
  if !@rp 
    fail "Resource pool #{@rp_path} not found"
  end
  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)



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/rbvmomi/utils/admission_control.rb', line 307

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
      if eligible.length > 0
        computer = eligible.map{|x| x[0]}[placementhint % eligible.length]
      end
    else
      computer, = eligible.min_by do |computer,stats|
        2**(stats[:usedCPU].to_f/stats[:totalCPU]) + (stats[:usedMem].to_f/stats[:totalMem])
      end
    end
  
    if !computer 
      fail "No clusters available, should have been prevented by admission control"
    end
    @computer = computer
  end
  @computer
end

#pod_datastores(pod) ⇒ Array

Returns all candidate datastores for a given pod.

Returns:

  • (Array)

    List of RbVmomi::VIM::Datastore



213
214
215
# File 'lib/rbvmomi/utils/admission_control.rb', line 213

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.



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
204
205
206
207
208
209
# File 'lib/rbvmomi/utils/admission_control.rb', line 172

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



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/rbvmomi/utils/admission_control.rb', line 152

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:



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

def vm_folder 
  retries = 1
  begin
    @vm_folder ||= datacenter.vmFolder.traverse!(@vm_folder_path, RbVmomi::VIM::Folder)
    if !@vm_folder 
      fail "VM folder #{@vm_folder_path} not found"
    end
  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