这篇文章描述了Nova启动一个实例的内部流程,原文地址是:
http://www.laurentluce.com/posts/openstack-nova-internals-of-instance-launching/
我作了一个简单的翻译,希望对英文不是很发了的同学有所帮助,如果你英文还可以,建立你看原文
概况
启动一个实例涉及到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