ライフサイクル管理の全体像
仮想マシンと違い、各物理マシンは常に「インスタンス」として起動している状態として扱います。各物理マシンに「availability_zone」の属性を持たせて、この値で使用状況を管理します。また「instance_type」の属性を持たせることで、物理マシンのグループ分けを行います。(起動時にユーザが指定したinstance_typeに対して、同じinstance_typeを持つ物理マシンの中から、使用するマシンを選択する。)
また、各マシンの状態は、status属性で分類。
status | 状態 |
---|---|
inactive | 電源オフ |
active | 電源オンで未使用 |
processing | 処理中 |
used | 使用中 |
例えば、Terminateしたインスタンスは、「availability_zone="resource_pool"」として、デフォルトイメージで起動した状態になります。
nova/compute/manager.py
517 @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) 518 @checks_instance_lock 519 def terminate_instance(self, context, instance_id): 520 """Terminate an instance on this host.""" 521 def _inner_terminate_instance(): 522 try: 523 self._instance_update(context, 524 instance_id, 525 vm_state=vm_states.DELETED, 526 task_state=None, 527 terminated_at=utils.utcnow()) 528 529 bmm = self._shutdown_instance(context, instance_id, 'Terminating') 530 instance = self.db.instance_get(context.elevated(), instance_id) 531 instance_new = db.instance_create(context, 532 {"availability_zone": "resource_pool", 533 "user_id": instance["user_id"], 534 "project_id": instance["project_id"], 535 "kernel_id": instance["kernel_id"], 536 "host": instance["host"], 537 "display_name": instance["display_name"], 538 "instance_type_id": instance["instance_type_id"], 539 "vcpus": instance["vcpus"], 540 "vm_state": vm_states.BUILDING, 541 "image_ref": FLAGS.dodai_default_image}) 542 543 self.db.instance_destroy(context, instance_id) ... 550 self.driver.add_to_resource_pool(context, instance_new, bmm) 551 except Exception as ex: 552 LOG.exception(ex) 553 554 greenthread.spawn(_inner_terminate_instance)
nova/virt/dodai/connection.py
554 def add_to_resource_pool(self, context, instance, bmm): 555 # begin to install default os 556 self._install_machine(context, instance, bmm, "resource_pool", None, True)
物理マシンの使用を開始する際は、「availability_zone="[C](新規作成指定),
nova/virt/dodai/connection.py
225 def _parse_zone(self, zone): 226 create_cluster = False 227 vlan_id = None 228 cluster_name = "resource_pool" 229 instance_zone = zone 230 parts = zone.split(",") 231 if len(parts) >= 2: 232 if parts[0] == "C": 233 parts.pop(0) 234 create_cluster = True 235 236 cluster_name, vlan_id = parts 237 vlan_id = int(vlan_id) 238 instance_zone = ",".join(parts) 239 240 return instance_zone, cluster_name, vlan_id, create_cluster
ただし、connection.pyのコード内では実質的に意味を持つのは、cluster_nameだけで、対応するvlan_idなどは、この後で見るように、Nova EC2 APIでrun_instanceを実行した時点でスイッチ側に紐づけなどの登録作業が行われます。
実際にspawnする部分を追うと、次のようになります。
nova/virt/dodai/connection.py
161 def spawn(self, context, instance, # <- instanceは起動属性を含むハッシュ 162 network_info=None, block_device_info=None): ... 183 instance_zone, cluster_name, vlan_id, create_cluster = self._parse_zone(instance["availability_zone"]) # ^-- ここでクラスタ情報を取得 186 bmm, reuse = self._select_machine(context, instance) # ^-- ここで使用する物理マシンを選択 ... 198 if instance_zone == "resource_pool": # "rosource_pool"に戻す場合 199 self._install_machine(context, instance, bmm, cluster_name, vlan_id) 200 else: 201 self._update_ofc(bmm, cluster_name) 202 if bmm["instance_id"]: # 使用中マシンを選択した場合は、一旦、アンインストール 203 db.instance_destroy(context, bmm["instance_id"]) 204 205 if reuse: # インストール済みのOSを再利用する場合 206 db.bmm_update(context, bmm["id"], {"status": "used", 207 "instance_id": instance["id"]}) 208 else: # それ以外は、新規インストール 209 self._install_machine(context, instance, bmm, cluster_name, vlan_id) 210 211 if instance["key_data"]: 212 self._inject_key(bmm["pxe_ip"], str(instance["key_data"]))
使用するマシンの選択ロジックは次の通り。
nova/virt/dodai/connection.py
373 def _select_machine(self, context, instance): 374 inst_type = instance_types.get_instance_type(instance['instance_type_id']) ... 382 try: # v--- 指定のインスタンスタイプの物理マシン一覧から選択 383 bmms = db.bmm_get_all_by_instance_type(context, inst_type["name"], session) # v--- 新規導入マシンを初期化して「resource_pool」に入れる場合の処理 384 if instance["availability_zone"] == "resource_pool": #Add a machine to resource pool. 385 for bmm in bmms: 386 if bmm["availability_zone"] != "resource_pool": 387 continue 388 389 if bmm["status"] != "inactive": 390 continue 391 392 bmm_found = bmm 393 break # ^-- まだ「resource_pool」になくて、電源の入っていない「inactive」なマシンを対象とする 394 else: # <-- ここからがクラスタを指定して利用を開始する場合の選択 395 for bmm in bmms: 396 if bmm["availability_zone"] != "resource_pool": 397 continue 398 399 if bmm["status"] != "active": 400 continue 401 402 instance_ref = db.instance_get(context, bmm["instance_id"]) 403 if instance_ref["image_ref"] != instance["image_ref"]: 404 continue 405 406 bmm_found = bmm 407 reuse = True 408 break # ^--- まずは「resource_pool」で指定のOSをインストール済み(i.e. 指定のOSがデフォルトOS) # のマシンを探して再利用を試みる 409 410 if not bmm_found: 411 for bmm in bmms: 412 if bmm["status"] == "used" or bmm["status"] == "processing": 413 continue 414 415 bmm_found = bmm 416 reuse = False 417 break # ^--- 再利用マシンがない場合は、「used」「processing」以外(つまり空いているマシン)を利用 # クラスタに入っているけど、空いているマシンが選ばれる可能性もあるが、現実の起こりえるか不明 # まだ「resouce_pool」に入っていない電源起動前マシンを選ぶことを想定しているのかも?
ネットワーク関連
ネットワークの物理構成の概要はこんな感じ。(この他に、ストレージ接続用ネットワークもあるようです。)
管理ネットワークのポート/IPは固定で、事前に物理マシン属性として登録します。これにより、dodai-computeから管理操作を行います。
サービスネットワークのポート(2個)は、openFlow対応スイッチに接続して、接続ポート番号を物理マシン属性として事前登録します。物理マシンを特定のクラスタにアサインしたタイミングで、openFlowを使ってスイッチに指示を出して、接続ポートを該当クラスタのVLANに接続します。(特にopenFlowでなくても、PVID設定を動的に変更すれば対応できる気もするけど。。。。)
物理マシンのサービスIPの設定は、独自の手法を採用しています。物理マシンのゲストOSで専用のAgentを起動して、これが管理ネットワーク側にhttpの口を開けて、dodai-computeからの指示を待ちます。dodai-computeは、これに対して、特定のサービスIPの割り当てや認証キーの埋め込みなどの指示を出します。(AWSのように、ゲストOS側から「ユーザデータ」を取りに行く方式を採用しなかったのは、何か理由があるんでしょうか。。。。)
このAgentのコードは、GitHubには上がってないようですが、こっそり入手してみた所、Bondingの構成などもAgentで行なっています。便利ですが、ゲストOS依存が高くなる点が少し懸念点。
openFlow周りのコードを見ておきます。専用のユーティリィライブラリが用意されています。
クラスタ名は、「region_name」にマッピングされており、新しいregionを作ると、対応するVLANが外部ネットワーク側のトランクポートに追加されます。
nova/virt/dodai/ofc_utils.py
35 def create_region(service_url, region_name, vlan_id): 36 client = Client(service_url + "?wsdl") 37 try: 38 client.service.createRegion(region_name) 39 client.service.save() 40 except: 41 raise exception.OFCRegionCreationFailed(region_name=region_name) 42 43 try: 44 switches = db.switch_get_all(None) 45 for switch in switches: 46 client.service.setOuterPortAssociationSetting(switch["dpid"], switch["outer_port"], vlan_id, 65535, region_name) 47 48 client.service.save() 49 except: 50 client.service.destroyRegion(region_name) 51 client.service.save() 52 raise exception.OFCRegionSettingOuterPortAssocFailed(region_name=region_name, vlan_id=vlan_id)
物理マシンを特定クラスタに追加したタイミングで、該当マシンのポートを対応する「region」にアサインします。(ライブラリを使っているので詳細は不明ですが、おそらく、このポートから指定regionのVLANにパケットが通るようになるものと思われます。)
nova/virt/dodai/ofc_utils.py
9 def update_for_run_instance(service_url, region_name, server_port1, server_port2, dpid1, dpid2): 10 # check region name 11 client = Client(service_url + "?wsdl") 12 13 client.service.setServerPort(dpid1, server_port1, region_name) 14 client.service.setServerPort(dpid2, server_port2, region_name) 15 client.service.save()
一方、Terminateした物理マシンのポートをregionから削除する処理はここ。
nova/virt/dodai/ofc_utils.py
17 def update_for_terminate_instance(service_url, region_name, server_port1, server_port2, dpid1, dpid2, vlan_id): 18 client = Client(service_url + "?wsdl") 19 client.service.clearServerPort(dpid1, server_port1) 20 client.service.clearServerPort(dpid2, server_port2) 21 client.service.save() 22 23 dpid_datas = client.service.showSwitchDatapathId() 24 for dpid_data in dpid_datas: 25 ports = client.service.showPorts(dpid_data.dpid) 26 for port in ports: 27 if port.type != "ServerPort": 28 continue 29 30 if port.regionName == region_name: 31 return 32 # 該当regionの物理マシンがいなくなったら、regionそのものを削除する。 33 remove_region(service_url, region_name, vlan_id)
実際に、物理マシンの利用を開始するタイミングで、ポートを設定する流れは次の部分になります。
nova/virt/dodai/connection.py
161 def spawn(self, context, instance, 162 network_info=None, block_device_info=None): ... 201 self._update_ofc(bmm, cluster_name) # スイッチポートを設定してから ... # v--- 物理マシンのインスタンスを開始 209 self._install_machine(context, instance, bmm, cluster_name, vlan_id) ... 339 def _update_ofc(self, bmm, cluster_name): # スイッチポートを設定するところ 340 try: 341 ofc_utils.update_for_run_instance(FLAGS.ofc_service_url, 342 cluster_name, # cluster_nameをregion_nameに指定 343 bmm["server_port1"], 344 bmm["server_port2"], 345 bmm["dpid1"], 346 bmm["dpid2"]) 347 except Exception as ex: 348 LOG.exception(_("OFC exception %s"), unicode(ex))
なお、regionの作成は、dodai-compute外部で別途、行う必要があります。現在の実装では、NovaのEC2 APIの方を拡張して、run_instanceを実行するタイミングで、作成を行います。
nova/api/ec2/cloud.py
1378 def run_instances(self, context, **kwargs): 1379 max_count = int(kwargs.get('max_count', 1)) ... 1399 zone = kwargs.get('placement').get('availability_zone') 1400 def _validate_zone(zone): # ユーザが指定したavailability_zoneを解析 1401 if zone == "resource_pool": 1402 return False, zone, None ... 1411 create_cluster = False 1412 if len(parts) == 3: 1413 create_cluster = True 1414 parts.pop(0) 1415 1416 cluster_name, vlan_id = parts 1417 has_cluster = ofc_utils.has_region(FLAGS.ofc_service_url, cluster_name) 1418 if create_cluster and has_cluster: # 既存クラスタを作成指定するとエラー 1419 raise exception.OFCRegionExisted(region_name=cluster_name) 1420 1421 if not create_cluster and not has_cluster: # 再生指定なしで、存在しないクラスタを指定するとエラー 1422 raise exception.OFCRegionNotFound(region_name=cluster_name) 1423 # 「新規クラスタ作成フラグ、クラスタ名、VLAN ID」を返す # ただし、既存クラスタに異なるVLAN IDを指定した場合は、後続処理では、VLAN IDは無視されるっぽい・・・(本当?) 1424 return create_cluster, cluster_name, vlan_id ... 1441 create_cluster, cluster_name, vlan_id = _validate_zone(zone) 1442 if create_cluster and cluster_name != "resource_pool" : # ここで新規cluster/regionを作成 1443 ofc_utils.create_region(FLAGS.ofc_service_url, cluster_name, vlan_id)
この時点で、cluster_name(region_name)とvlan_idは一意に紐づくので、connection.pyの内部では、vlan_idを扱う必要ないはずですが、今の実装では、vlan_idが(実際には利用されない)パラメータとして引き回されている点がちょっとよくないですね。cluster_name(region_name)とvlan_idのマッピングを明示的にデータモデル化した方がよいかも知れません。
あと、独自Agentに指示を出しているのは、次の2箇所です。1つは、OSインストール後にKey Injectionする部分。
nova/virt/dodai/connection.py
214 def _inject_key(self, pxe_ip, key_data): 215 conn = httplib.HTTPConnection(pxe_ip, "4567") 216 params = urllib.urlencode({"key_data": key_data.strip()}) 217 headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'} 218 conn.request("PUT", "/services/dodai-instance/key.json", params, headers) 219 response = conn.getresponse() 220 data = response.read()
もうひとつは、Nova APIでFloaint IPを割り当てる部分。つまり、サービスIP(Private IP)はデフォルトではアサインされず、かならずFloating IPとして割り当てる前提となっているようです。
nova/compute/api.py
1408 def associate_floating_ip(self, context, instance_id, address): 1409 """Makes calls to network_api to associate_floating_ip. 1410 1411 :param address: is a string floating ip address 1412 """ ... 1438 def _associate_ip(): 1439 instance = self.get(context, instance_id) 1440 bmm = db.bmm_get_by_instance_id(context, instance_id) 1441 1442 conn = httplib.HTTPConnection(bmm["pxe_ip"], "4567") 1443 params = {'ip_address': ip, 1444 'subnet_mask': netmask, 1445 'default_gateway': gw, 1446 'dns': dns} 1447 index = 0 1448 if bmm["service_mac1"]: 1449 params["mac_address[%d]" % index] = bmm["service_mac1"] 1450 index += 1 1451 if bmm["service_mac2"]: 1452 params["mac_address[%d]" % index] = bmm["service_mac2"] 1453 1454 params = urllib.urlencode(params) 1455 headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'} 1456 conn.request("PUT", "/services/dodai-instance/networks.json", params, headers) 1457 response = conn.getresponse() 1458 data = response.read()