VM作成時のホスト決定のためのSchedulerにおいて、デフォルトで利用されるFilterの1つにRamFilterがあります。
これはホストの残りメモリ容量にたいして何倍まで許容するかというフィルタらしいですが、デフォ値が 1.5 なので計算内容や状況によっては SWAP / OOM Killer 逝きになる可能性があるのかな、と気になったので良い子の私は、どのようなメモリ値を使ってどのように計算しているのか確認してみることにしました。
リンク
Schedulerについてはこの2箇所を見ておけば大体よさそうです。デフォルト設定の確認
Schedulerは nova.conf に記述されてなかったりするけど、これがデフォ
1 2 3 4 |
scheduler_driver=nova.scheduler.multi.MultiScheduler compute_scheduler_driver=nova.scheduler.filter_scheduler.FilterScheduler scheduler_available_filters=nova.scheduler.filters.all_filters scheduler_default_filters=AvailabilityZoneFilter,RamFilter,ComputeFilter |
RamFilterのデフォ値はこう
1 |
ram_allocation_ratio=1.5 |
例では、残りメモリ 1GB だと 1.5GB までのVMが作れますよ、と。
…ということは、残り 100GB だと最後のVMは 150GB にできるってこと?挙動バランス悪くね?
(実際、そんなデカい flavor 作らんけど)
RamFilterを追う
とりあえず適当に grep して該当箇所を見つけてきました。/usr/share/pyshared/nova/scheduler/filters/ram_filter.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class RamFilter(filters.BaseHostFilter): """Ram Filter with over subscription flag.""" def host_passes(self, host_state, filter_properties): """Only return hosts with sufficient available RAM.""" instance_type = filter_properties.get('instance_type') requested_ram = instance_type['memory_mb'] free_ram_mb = host_state.free_ram_mb total_usable_ram_mb = host_state.total_usable_ram_mb memory_mb_limit = total_usable_ram_mb * CONF.ram_allocation_ratio used_ram_mb = total_usable_ram_mb - free_ram_mb usable_ram = memory_mb_limit - used_ram_mb if not usable_ram >= requested_ram: LOG.debug(_("%(host_state)s does not have %(requested_ram)s MB " "usable ram, it only has %(usable_ram)s MB usable ram."), locals()) return False # save oversubscription limit for compute node to test against: host_state.limits['memory_mb'] = memory_mb_limit return True |
requested_ram
新規VMの割り当てメモリ容量ですよ、と。free_ram_mb
どうやら、総メモリは libvirt の getInfo から取得してるっぽく、空き容量は /proc/meminfo の MemFree, Buffers, Cached を足した値としているとわかります。
この足し算値は、実際に /proc/meminfo と Schedulerのデバッグログを比較してみたけど、ジャストその通りでした。
/usr/share/pyshared/nova/virt/libvirt/driver.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class HostState(object): """Manages information about the compute node through libvirt.""" def __init__(self, driver): super(HostState, self).__init__() self._stats = {} self.driver = driver self.update_status() def get_host_stats(self, refresh=False): """Return the current state of the host. If 'refresh' is True, run update the stats first.""" if refresh: self.update_status() return self._stats def update_status(self): """Retrieve status info from libvirt.""" LOG.debug(_("Updating host stats")) data = {} data["vcpus"] = self.driver.get_vcpu_total() data["vcpus_used"] = self.driver.get_vcpu_used() data["cpu_info"] = jsonutils.loads(self.driver.get_cpu_info()) disk_info_dict = self.driver.get_local_gb_info() data["disk_total"] = disk_info_dict['total'] data["disk_used"] = disk_info_dict['used'] data["disk_available"] = disk_info_dict['free'] data["host_memory_total"] = self.driver.get_memory_mb_total() data["host_memory_free"] = (data["host_memory_total"] - self.driver.get_memory_mb_used()) data["hypervisor_type"] = self.driver.get_hypervisor_type() data["hypervisor_version"] = self.driver.get_hypervisor_version() data["hypervisor_hostname"] = self.driver.get_hypervisor_hostname() data["supported_instances"] = \ self.driver.get_instance_capabilities() self._stats = data return data |
1 2 3 4 5 6 7 8 |
def get_memory_mb_total(self): """Get the total memory size(MB) of physical computer. :returns: the total amount of memory(MB). """ return self._conn.getInfo()[1] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
def get_memory_mb_used(self): """Get the free memory size(MB) of physical computer. :returns: the total usage of memory(MB). """ if sys.platform.upper() not in ['LINUX2', 'LINUX3']: return 0 m = open('/proc/meminfo').read().split() idx1 = m.index('MemFree:') idx2 = m.index('Buffers:') idx3 = m.index('Cached:') if CONF.libvirt_type == 'xen': used = 0 for domain_id in self.list_instance_ids(): # skip dom0 dom_mem = int(self._conn.lookupByID(domain_id).info()[2]) if domain_id != 0: used += dom_mem else: # the mem reported by dom0 is be greater of what # it is being used used += (dom_mem - (int(m[idx1 + 1]) + int(m[idx2 + 1]) + int(m[idx3 + 1]))) # Convert it to MB return used / 1024 else: avail = (int(m[idx1 + 1]) + int(m[idx2 + 1]) + int(m[idx3 + 1])) # Convert it to MB return self.get_memory_mb_total() - avail / 1024 |
total_usable_ram_mb
ホストの総メモリ容量はここではDBの novadb.compute_nodes:memory_mb から持ってきているようです。/usr/share/pyshared/nova/scheduler/host_manager.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def update_from_compute_node(self, compute): """Update information about a host from its compute_node info.""" if (self.updated and compute['updated_at'] and self.updated > compute['updated_at']): return all_ram_mb = compute['memory_mb'] # Assume virtual size is all consumed by instances if use qcow2 disk. least = compute.get('disk_available_least') free_disk_mb = least if least is not None else compute['free_disk_gb'] free_disk_mb *= 1024 self.disk_mb_used = compute['local_gb_used'] * 1024 #NOTE(jogo) free_ram_mb can be negative self.free_ram_mb = compute['free_ram_mb'] self.total_usable_ram_mb = all_ram_mb |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
def get_all_host_states(self, context): """Returns a list of HostStates that represents all the hosts the HostManager knows about. Also, each of the consumable resources in HostState are pre-populated and adjusted based on data in the db. """ # Get resource usage across the available compute nodes: compute_nodes = db.compute_node_get_all(context) seen_nodes = set() for compute in compute_nodes: service = compute['service'] if not service: LOG.warn(_("No service for compute ID %s") % compute['id']) continue host = service['host'] node = compute.get('hypervisor_hostname') state_key = (host, node) capabilities = self.service_states.get(state_key, None) host_state = self.host_state_map.get(state_key) if host_state: host_state.update_capabilities(capabilities, dict(service.iteritems())) else: host_state = self.host_state_cls(host, node, capabilities=capabilities, service=dict(service.iteritems())) self.host_state_map[state_key] = host_state host_state.update_from_compute_node(compute) seen_nodes.add(state_key) |
/usr/share/pyshared/nova/db/sqlalchemy/api.py
1 2 3 4 5 6 |
@require_admin_context def compute_node_get_all(context): return model_query(context, models.ComputeNode).\ options(joinedload('service')).\ options(joinedload('stats')).\ all() |
memory_mb_limit, used_ram_mb, usable_ram
ホストの総メモリに、設定値の倍率をかけて水増し、本来の総メモリ容量から空き容量を引いて、実際の使用中容量を出しておき、
水増しパターンでの利用可能な容量を算出しています。
で、VMメモリ容量が水増し残量を超えたらアウト判定。
例題で考える
余裕シャクシャクの場合
そこそこ使ってそこそこ余ってる場合
パツパツの場合
極端に考えると
総メモリ128GBのホストで、カーネルとかその他デーモンのメモリを無視したら、1VM目は 192GB までのVMが作れて、SWAPするくらい崖っぷちでも常に 64GB のVMを作成できるということになります。(Quotaに引っかからない限り)
え”。
イヤ、でもそうとしか読み取れない… ヤバい、意外すぎて自信なくなった……w
考察
上記説明が正しいとすると、ドキュメントの説明と内容が異なっており、残りメモリ容量に対しての倍率ではなく、総メモリ容量に対する倍率となっています。
単純に現在の空き容量に倍率を掛けて比較したら良いだけな気がするのですが…。
そうしないと、このフィルタだけでは(Quotaを無視すると)無限にVMを作れてしまいます。
しかし、仮に空き容量に対するものだとしても、上記例だと 192GB ~ 1GB(下限はflavor任意)までの幅ある上限であることに変りはないです。違う所は、極端に残りが少なくなると最低 0.67GB は残っていないと 1GB のVMを作れない、という点は大きいです。
作者の意図はわからないけど、私は最初、kvmを起動して利用しても実際は全kvmプロセスがMAXまで使わないことを想定しての 1.5倍 かと思っていました。あと、空きメモリの計算は厳密には正確ではないから、とかかんとか。
が、全然予想と異なっていたので
(調査結果に納得いかなくていまだ解読が正しいか自信ないけど)、
今のところは ram_allocation_ratio=1.0 にしつつ、ちゃんと nova quota-update –ram で管理することでSWAP / OOM Killerを回避していこうかと。そして想定より早くフィルターに引っかかったら、設定を再調整する、それが健全な運用であると思います(キリッ
本件、突っ込み大歓迎でございます!