1. 程式人生 > >OpenStack Nova啟動一個虛擬機器內部流程

OpenStack Nova啟動一個虛擬機器內部流程

這篇文章描述了Nova啟動一個例項的內部流程,原文地址是:

我作了一個簡單的翻譯,希望對英文不是很發了的同學有所幫助,如果你英文還可以,建立你看原文

概況

啟動一個例項涉及到nova內部的多個元件:

  • API服務: 處理使用者請求並轉發到雲控制器
  • 雲控制器: 處理計算節點,網路控制服務API服務和排程之間的通訊
  • 排程: 選擇一個節點啟動例項
  • 計算服務: 管理例項: 啟動/終止例項, 連上/卸下邏輯卷
  • 網路控制器: 管理網路資源: 分配fixed IP地址,配置VLAN

注意: nova中還有更多的元件如認證管理,物件儲存和邏輯卷控制,但我們不研究他們因為這篇檔案的重點是例項的重啟

啟動例項的流程類似於這樣: API服務從使用者收到一個run_instances的命令,API服務轉發這個命令給雲控制器(1),在這裡執行認證以確保該使用者有相應的許可權.去控制器把這條資訊發給排程(2). 排程將這條資訊扔給一個隨機的主機(計算節點)讓他啟動一個新的例項(3).這臺主機上的計算服務抓到這條資訊(4).計算服務需要一個fixed IP來啟動一個新例項,所以她發了一條資訊給網路控制器(5,6,7,8).計算服務繼續建立這個例項.下面我們將深入這些步驟的細節中去

API

你可以使用OpenStack的API或者EC2的API來啟動一個例項,這裡我們使用EC2 API, 我們添加了一個新的key pair並用他來啟動一個flavor為m1.tiny的例項

cd /tmp/
euca-add-keypair test > test.pem
euca-run-instances -k test -t m1.tiny ami-tiny

api/ec2/cloud.py裡的run_instances()被呼叫,它再呼叫compute/API.py裡的create()

def run_instances(self, context, **kwargs):
  ...
  instances = self.compute_api.create(context,
            instance_type=instance_types.get_by_type(
                kwargs.get('instance_type', None)),
            image_id=kwargs['image_id'],
            ...

Compute API的create()做了這些:

  • 檢查是否已達到啟動這個型別的例項的最大數目
  • 如果不存在就建立一個安全組
  • 給這個新例項生成一個MAC地址和主機名
  • 給排程發一條資訊告訴她已經啟動例項了

Cast

讓我們暫停一會來看下資訊是怎麼發給排程的.OpenStack中這種資訊的傳遞被定義為RPC casting. 這裡用RabbitMQ來傳遞.資訊釋出者(API)發信息給一個topic exchange(scheduler topic). 資訊接受者(排程服務)從佇列中拿到這條資訊.不會等待返回資訊因為這裡是一個cast而不是呼叫,後面我們會看到呼叫

這裡是casting那條資訊的程式碼

LOG.debug(_("Casting to scheduler for %(pid)s/%(uid)s's"
        " instance %(instance_id)s") % locals())
rpc.cast(context,
         FLAGS.scheduler_topic,
         {"method": "run_instance",
          "args": {"topic": FLAGS.compute_topic,
                   "instance_id": instance_id,
                   "availability_zone": availability_zone}})

你可以看到這裡使用的是排程的topic,並且這條資訊的引數也指明瞭排程用什麼topic傳遞他的訊息.假如這樣,我們希望市長使用compute topic釋出這條訊息

排程

排程收到這條訊息併發送這個run_instance訊息給一個隨機主機.這裡使用chance排程演算法.還有更多排程演算法如zone scheduler(在一個指定的可用zone中隨機選一個主機)或者simple schedule(選擇最小負載的主機).現在已經選擇好了一個主機,下面的程式碼傳送訊息給這個主機上的計算服務

rpc.cast(context,
         db.queue_get_for(context, topic, host),
         {"method": method,
          "args": kwargs})
LOG.debug(_("Casting to %(topic)s %(host)s for %(method)s") % locals())

Compute

計算服務收到這條資訊並且呼叫compute/manager.py裡的下面方法

def run_instance(self, context, instance_id, **_kwargs):
  """Launch a new instance with specified options."""
  ...

run_instance()做了這些:

  • 檢查這個例項是否已經在執行
  • 分配一個fixed IP地址
  • 如果沒有設定就設定一個VLAN和橋
  • 用虛擬化工具建立這個例項

呼叫網路控制器

為分配fixed IP使用了一個RPC呼叫.RPC呼叫和RPC cast不同因為她使用一個topic.host exchange意味著目標是一個特定的主機,還會等待一個返回

建立例項

下一步就是虛擬化工具建立例項的過程.我們的例子中使用libvirt.我們將要看到的程式碼在virt/libvirt_conn.py

第一個需要完成的工作是建立一個libvirt的xml檔案來啟動例項.使用to_xml()方法來生成xml內容.下面是我們例項的XML

<domain type='qemu'>
    <name>instance-00000001</name>
    <memory>524288</memory>
    <os>
        <type>hvm</type>
        <kernel>/opt/novascript/trunk/nova/..//instances/instance-00000001/kernel</kernel>
        <cmdline>root=/dev/vda console=ttyS0</cmdline>
        <initrd>/opt/novascript/trunk/nova/..//instances/instance-00000001/ramdisk</initrd>
    </os>
    <features>
        <acpi/>
    </features>
    <vcpu>1</vcpu>
    <devices>
        <disk type='file'>
            <driver type='qcow2'/>
            <source file='/opt/novascript/trunk/nova/..//instances/instance-00000001/disk'/>
            <target dev='vda' bus='virtio'/>
        </disk>
        <interface type='bridge'>
            <source bridge='br100'/>
            <mac address='02:16:3e:17:35:39'/>
            <!--   <model type='virtio'/>  CANT RUN virtio network right now -->
            <filterref filter="nova-instance-instance-00000001">
                <parameter name="IP" value="10.0.0.3" />
                <parameter name="DHCPSERVER" value="10.0.0.1" />
                <parameter name="RASERVER" value="fe80::1031:39ff:fe04:58f5/64" />
                <parameter name="PROJNET" value="10.0.0.0" />
                <parameter name="PROJMASK" value="255.255.255.224" />
                <parameter name="PROJNETV6" value="fd00::" />
                <parameter name="PROJMASKV6" value="64" />
            </filterref>
        </interface>

        <!-- The order is significant here.  File must be defined first -->
        <serial type="file">
            <source path='/opt/novascript/trunk/nova/..//instances/instance-00000001/console.log'/>
            <target port='1'/>
        </serial>

        <console type='pty' tty='/dev/pts/2'>
            <source path='/dev/pts/2'/>
            <target port='0'/>
        </console>

        <serial type='pty'>
            <source path='/dev/pts/2'/>
            <target port='0'/>
        </serial>

    </devices>
</domain>

我們使用qemu做為管理程式.將會給這個例項分配524 ktytes的記憶體.這個例項將從儲存在永動機上的一個kernel和initrd檔案啟動

這個例項的虛擬CPU數是一個.ACPI開啟以管理電源

定義了多個裝置:

  • 磁碟映象是一個儲存在宿主機上的一個qcow2檔案.qcow2是qemu磁碟映象的copy-on-write格式
  • 網路裝置是一個虛擬機器可見的橋.我們定義了一些網路過濾引數如IP地址,意味著將總是使用10.0.0.3做為源IP地址
  • 裝置日誌檔案.所有發到字元裝置的資料都會寫到console.log.
  • 偽終端(Pseudo TTY):virsh終端可以用來連到本地的序列介面上.

下一步是準備網路過濾.防火牆驅動預設使用iptables.規則定義在類IptablesFirewallDriver裡的apply_ruleset()中.我們來看一下給這個例項的防火牆鏈和規則

*filter
...
:nova-ipv4-fallback - [0:0]
:nova-local - [0:0]
:nova-inst-1 - [0:0]
:nova-sg-1 - [0:0]
-A nova-ipv4-fallback -j DROP
-A FORWARD -j nova-local
-A nova-local -d 10.0.0.3 -j nova-inst-1
-A nova-inst-1 -m state --state INVALID -j DROP
-A nova-inst-1 -m state --state ESTABLISHED,RELATED -j ACCEPT
-A nova-inst-1 -j nova-sg-1
-A nova-inst-1 -s 10.1.3.254 -p udp --sport 67 --dport 68
-A nova-inst-1 -j nova-ipv4-fallback
-A nova-sg-1 -p tcp -s 10.0.0.0/27 -m multiport --dports 1:65535 -j ACCEPT
-A nova-sg-1 -p udp -s 10.0.0.0/27 -m multiport --dports 1:65535 -j ACCEPT
-A nova-sg-1 -p icmp -s 10.0.0.0/27 -m icmp --icmp-type 1/65535 -j ACCEPT
COMMIT

首先你要這些鏈:nova-local,nova-inst-1,nova-sg-1,nova-ipv4-fallback然後是規則

讓我們來看一看不同的鏈和規則

路由到虛擬網路的資料包由nova-local處理

-A FORWARD -j nova-local

如果目標地址是10.0.0.3則是要到我們的虛擬機器所以跟轉到鏈nova-inst-1

-A nova-local -d 10.0.0.3 -j nova-inst-1

如果這個包不能被鑑定(無效包),丟掉它

-A nova-inst-1 -m state --state INVALID -j DROP

如果這個包和一個已建立的連線相關或者是和一個已經存在的連線相關的新連線,接受它

-A nova-inst-1 -m state --state ESTABLISHED,RELATED -j ACCEPT

接受所有的DHCO迴應

-A nova-inst-1 -s 10.0.0.254 -p udp --sport 67 --dport 68

跳轉到安全組鏈做進一步的檢查

-A nova-inst-1 -j nova-sg-1

安全組鏈.接受所有從10.0.0.0/27來的目標埠從1到65535的TCP資料包

-A nova-sg-1 -p tcp -s 10.0.0.0/27 -m multiport --dports 1:65535 -j ACCEPT

接受所有從從10.0.0.0/27來的目標埠從1到65535的UDP資料包

-A nova-sg-1 -p udp -s 10.0.0.0/27 -m multiport --dports 1:65535 -j ACCEPT

接受所有從從10.0.0.0/27來的型別是1到65535的ICMP資料包

-A nova-sg-1 -p icmp -s 10.0.0.0/27 -m icmp --icmp-type 1/65535 -j ACCEPT

跳轉到fallback鏈

-A nova-inst-1 -j nova-ipv4-fallback

fallback鏈的規則,丟掉所有

-A nova-ipv4-fallback -j DROP

防火牆規則準備好之後就建立映象,這發生在_create_image()

def _create_image(self, inst, libvirt_xml, suffix='', disk_images=None):
...

在這個方法中,根據前面生成的XML建立libvirt.xml

複製管理程式(hypervisor)要用到的ramdisk, initrd, disk映象檔案

如果網路管理使用的是flat,一個網路配置會被注入到虛擬的映象中,這個例子中我們使用VLAN

例項的SSH key注入到映象中,讓我們看下這部分的細節,disk的inject_data()方法被呼叫

disk.inject_data(basepath('disk'), key, net,
                 partition=target_partition,
                 nbd=FLAGS.use_cow_images)

basepath(‘disk’)是映象檔案在宿主機上存放的位置,key是SSH key字串,我們的例子中沒有設定net因為我們不用注入一個網路配置,分割槽是None因為我們使用kernel映象,否則我們可以使用一個有分割槽的磁碟映象,讓我們看看inject_data()內部

首先發生的是連線映象到裝置,這發生在_link_device()

device = _allocate_device()
utils.execute('sudo qemu-nbd -c %s %s' % (device, image))
# NOTE(vish): this forks into another process, so give it a chance
#             to set up before continuuing
for i in xrange(10):
    if os.path.exists("/sys/block/%s/pid" % os.path.basename(device)):
        return device
    time.sleep(1)
raise exception.Error(_('nbd device %s did not show up') % device)

_allocate_device()返回下一個可用的ndb裝置:/dev/ndbx x從0到15.qemu-nbd是一個QEMU磁碟網路塊裝置服務,一旦這完成了,我們就有裝置了

假設是:/dev/ndb0

對這個裝置我們禁止檔案系統檢查,這裡mapped_device是:’/dev/ndb0′.

out, err = utils.execute('sudo tune2fs -c 0 -i 0 %s' % mapped_device)

掛載這個檔案系統到臨時目錄並把SSH key加到authorized_keys檔案中

sshdir = os.path.join(fs, 'root', '.ssh')
utils.execute('sudo mkdir -p %s' % sshdir)  # existing dir doesn't matter
utils.execute('sudo chown root %s' % sshdir)
utils.execute('sudo chmod 700 %s' % sshdir)
keyfile = os.path.join(sshdir, 'authorized_keys')
utils.execute('sudo tee -a %s' % keyfile, '\n' + key.strip() + '\n')

上面的程式碼中fs是臨時目錄

最後我們解除安裝這個檔案系統並且unlink這個裝置,這總結了映象的建立和設定

虛擬化驅動spawn()方法中下一步是使用驅動createXML()啟動例項,接著是防火牆規則的應用

就這樣吧,希望你喜歡這篇文章,如果你有任何反饋請寫評論,如果你在開發Python專案或建立一個新web服務方面需要幫助,我作為一個自由職業者可以提供幫助. LinkedIn:http://www.linkedin.com/in/lluce Twitter: @laurentluce