OpenStack啓動虛擬機時Nova內部工作流程

原文地址:這裏

Overview

OpenStack Nova中與啓動虛擬機相關的組件:

  • API server: 處理來自用戶的請求並轉發到cloud controller.
  • Cloud controller: 處理計算節點, the networking controllers, the API server and the scheduler之間的通信
  • Scheduler: 選擇一個節點來執行命令(啓動虛擬機)
  • Compute worker: 管理虛擬機實例:啓動/終止虛擬機,綁定,解綁磁盤卷
  • Network controller: 管理網絡資源:分配固定IP,管理VLANs…

Note: 本文僅關注虛擬機啓動,而對身份驗證,鏡像存儲等相關細節不做探究


首先API server收到一個來自用戶的run_instances命令:
  1. API server把這條消息轉發給雲控制器
  2. 先要認證用戶有需要的權限。雲控制器把消息發給調度器
  3. 調度器把這條消息扔給一個隨機的host(計算節點),並請求啓動一臺新虛擬機。
  4. 計算節點上的compute worker捕獲這條消息
  5. 6.7.8. compute worker需要一個固定IP來啓動新虛擬機,因此發送一條消息給network controller。


API


有兩種API OpenStack API和EC2 API。這裏使用EC2 API。先添加一個新的key pair,然後用它來啓一臺規模是m1.tiny的虛擬機

1 cd /tmp/
2 euca-add-keypair test > test.pem
3 euca-run-instances -k test -t m1.tiny ami-tiny
api/ec2/cloud.py中的 run_instances()使用了compute API ——位於compute/API.py下的create() 

1 def run_instances(self, context, **kwargs):
2   ...
3   instances = self.compute_api.create(context,
4             instance_type=instance_types.get_by_type(
5                 kwargs.get('instance_type', None)),
6             image_id=kwargs['image_id'],
7             ...

compute的API create() 會做如下這下事情 :

  • 檢查此類型虛擬機數量是否達到最大值
  • 如果沒有安全組的話創建一個
  • 爲新虛擬機生成MAC地址和hostnames
  • 向調度器發消息來啓動虛擬機

Cast


現在看看消息是怎麼發送到調度器的。在OpenStack中這種消息交付類型叫做RPC轉換,在轉換中使用了RabbitMQ。發佈者(API)發送消息到一個交易處(topic exchange),消費者(scheduler worker)從隊列中獲取消息。正是因爲這是一個cast而不是call,因此並沒有響應發生。


下面是casting消息的代碼:

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

可以看到使用了調度器主題,並且消息參數中也表名我們想要使用調度器來delivery。這個case中,我們想讓調度器使用compute主題來發送消息。
Scheduler

調度器收到消息並把run_instance消息發送到任意計算節點上。這裏用了chance scheduler。有很多調度算法,比如說zone scheduler(在一個特定可用域內pick一個隨機host),simple scheduler(pick最小負載的host)。現在已經選定了一個host,下面的代碼是在host上發送消息給一個compute worker。

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

Compute


compute worker收到消息,然後執行compute/manager.py中的run_instance方法,如下:

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

run_instance() 幹什麼呢:

  • 檢查虛擬機是否已經啓動。
  • 分配一個固定IP。
  • 如果沒有安裝VLAN和bridge的話,執行安裝操作。
  • virtualization driver“造(spawn)”虛擬機

Call to network controller


使用RPC請求來分配固定IP。一個RPC請求和RPC cast是區別在於它使用了一個topic.host exchange,這意味着它要指定目標host,因此,還要有一個請求。




Spawn instance

接下來是由虛擬化驅動器執行的虛擬機生成進程。這個case中使用libvirt,下面要看的代碼在virt/libvirt_conn.py


啓動一個虛擬機首先要做的是創建libvirt xml文件。使用to_xml()方法來獲取xml內容。

01 <domain type='qemu'>
02     <name>instance-00000001</name>
03     <memory>524288</memory>
04     <os>
05         <type>hvm</type>
06         <kernel>/opt/novascript/trunk/nova/..//instances/instance-00000001/kernel</kernel>
07         <cmdline>root=/dev/vda console=ttyS0</cmdline>
08         <initrd>/opt/novascript/trunk/nova/..//instances/instance-00000001/ramdisk</initrd>
09     </os>
10     <features>
11         <acpi/>
12     </features>
13     <vcpu>1</vcpu>
14     <devices>
15         <disk type='file'>
16             <driver type='qcow2'/>
17             <source file='/opt/novascript/trunk/nova/..//instances/instance-00000001/disk'/>
18             <target dev='vda' bus='virtio'/>
19         </disk>
20         <interface type='bridge'>
21             <source bridge='br100'/>
22             <mac address='02:16:3e:17:35:39'/>
23             <!--   <model type='virtio'/>  CANT RUN virtio network right now -->
24             <filterref filter="nova-instance-instance-00000001">
25                 <parameter name="IP" value="10.0.0.3" />
26                 <parameter name="DHCPSERVER" value="10.0.0.1" />
27                 <parameter name="RASERVER" value="fe80::1031:39ff:fe04:58f5/64" />
28                 <parameter name="PROJNET" value="10.0.0.0" />
29                 <parameter name="PROJMASK" value="255.255.255.224" />
30                 <parameter name="PROJNETV6" value="fd00::" />
31                 <parameter name="PROJMASKV6" value="64" />
32             </filterref>
33         </interface>
34  
35         <!-- The order is significant here.  File must be defined first -->
36         <serial type="file">
37             <source path='/opt/novascript/trunk/nova/..//instances/instance-00000001/console.log'/>
38             <target port='1'/>
39         </serial>
40  
41         <console type='pty' tty='/dev/pts/2'>
42             <source path='/dev/pts/2'/>
43             <target port='0'/>
44         </console>
45  
46         <serial type='pty'>
47             <source path='/dev/pts/2'/>
48             <target port='0'/>
49         </serial>
50  
51     </devices>
52 </domain>

使用的hypervisor是qemu。爲guest分配的內存是524kbytes。guest OS將會從內核啓動,initrd存儲在host OS上。

分配給guest OS 1個虛擬CPU。電源管理中允許ACPI。

同時定義了多種設備:

  • 磁盤鏡像是放在host OS上的qcow2格式的文件。qcow2是qemu磁盤鏡像copy-on-write的格式。
  • The network interface is a bridge visible to the guest. We define network filtering parameters like IP which means this interface will always use 10.0.0.3 as the source IP address.
  • 設備的日誌文件。所有發到字符設備的數據都被寫到console.log中。
  • Pseudo TTY: virsh console can be used to connect to the serial port locally.

接下來準備網絡過濾器。默認的防火牆驅動是iptables。規則定義在apply_ruleset()中的IptablesFirewallDriver類中。

01 *filter
02 ...
03 :nova-ipv4-fallback - [0:0]
04 :nova-local - [0:0]
05 :nova-inst-1 - [0:0]
06 :nova-sg-1 - [0:0]
07 -A nova-ipv4-fallback -j DROP
08 -A FORWARD -j nova-local
09 -A nova-local -d 10.0.0.3 -j nova-inst-1
10 -A nova-inst-1 -m state --state INVALID -j DROP
11 -A nova-inst-1 -m state --state ESTABLISHED,RELATED -j ACCEPT
12 -A nova-inst-1 -j nova-sg-1
13 -A nova-inst-1 -s 10.1.3.254 -p udp --sport 67 --dport 68
14 -A nova-inst-1 -j nova-ipv4-fallback
15 -A nova-sg-1 -p tcp -s 10.0.0.0/27 -m multiport --dports 1:65535 -j ACCEPT
16 -A nova-sg-1 -p udp -s 10.0.0.0/27 -m multiport --dports 1:65535 -j ACCEPT
17 -A nova-sg-1 -p icmp -s 10.0.0.0/27 -m icmp --icmp-type 1/65535 -j ACCEPT
18 COMMIT


首先有這些鏈: nova-local, nova-inst-1, nova-sg-1, nova-ipv4-fallback 和這些規則。

下面看看這些不同的鏈和規則:

數據包在虛擬網絡中的路由由nova-local來控制

1 -A FORWARD -j nova-local

如果目的地址是10.0.0.3說明使我們啓動的虛擬機,因此跳轉到nova-inst-1

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

如果包不能被識別,丟掉

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

如果包被關聯到一條已經建立的連接或者啓動一個新的連接但是關聯到一個已經存在的連接,接受。

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

允許DHCP響應。

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

跳到安全組chain並檢查包是否符合規則。

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

Security group chain. 允許所有來自10.0.0.0/27(端口從1到65535)的TCP包。

1 -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包。

1 -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包。

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

跳到fallback chain.

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

這就是fallback chain中我們丟到那個包的規則。

1 -A nova-ipv4-fallback -j DROP

Here is an example of a packet for a new TCP connection to 10.0.0.3:



接下來是創建鏡像。在_create_image()中。

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

libvirt.xml基於我們之前生成的XML來創建。

ramdisk,initrd和磁盤鏡像都要複製一份供hypervisor使用

如果使用flat網絡管理則一份網絡配置已經備註入到guest OS image中。這裏我們就用這種方式。

虛擬機的SSH key被注入到鏡像中。這裏使用了inject_data()方法

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

basepath('disk')是實例磁盤鏡像存放在host OS的位置。key是SSH key 字符串。net沒有設置因爲在此例中我們沒有注入網絡配置。partition是None因爲我們使用了一個內核鏡像,否則我們可以使用一個partitioned的磁盤鏡像。下面看一下inject_data()的內部。

首先要把鏡像鏈接到一塊設備上。在_link_device()中:

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

_allocate_device()返回下一個可用的ndb設備:/dev/ndbx,x在0到15之間。qemu-ndb是一個QEMU磁盤網絡塊設備服務器。這些做好後,我們得到了設備:/dev/ndb0

我們禁止了這個設備的文件系統檢查。

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

我們把一個文件系統掛載到一個臨時目錄,然後添加SSH key到ssh authorized_keys文件。

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

在上述代碼中,fs是所說的臨時文件。

最後,解除文件系統的掛載和設備的連接。這包括了鏡像創建和安裝。

虛擬驅動器spawn()方法的下一步是使用createXML()綁定方法來啓動實例。


OVEr

原作者的linkedin: LinkedIn profile. Twitter @laurentluce.


發佈了28 篇原創文章 · 獲贊 8 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章