文章目录
迁移命令
- OpenStack Nova组件负责提供计算资源,包括虚机的创建销毁,启停,迁移等,对于在线迁移,迁移的内容包括虚机的内存,QEMU维护的设备状态,如果是磁盘迁移还有磁盘内容。普通的迁移就是将这些内容通过网络从源端传输到目的端,加密迁移顾名思义,将内容加密后再传输。本章对于迁移的介绍,会分加密迁移和非加密迁移来讲。
普通迁移
OpenStack
- 创建网络
openstack network create --provider-network-type vxlan --internal $NET
- 创建网络的子网,指定其网段
openstack subnet create $SUBNET --network $NET --subnet-range 192.168.2.0/24
- 查看并选取虚机可用的规格
openstack flavor list
- 查看并选取虚机可以用的镜像
openstack image list
- 创建虚机
openstack server create --flavor $FLAVOR --image $IMAGE --network $NET $VMNAME
- 启动虚机
openstack server start $VMNAME
- 查看虚机所在的计算节点
openstack server show $VMNAME
- 查询所有计算节点,选定要迁移的目的节点
openstack compute service list
- 确认Nova配置文件的内容/etc/nova/nova.conf
live_migration_with_native_tls=false —— 非加密迁移
live_migration_tunnelled=false —— 非隧道迁移
live_migration_permit_auto_converge=true —— 迁移收敛
live_migration_permit_post_copy=false —— 非post-copy
- 重启计算节点的Nova服务使配置生效
systemctl restart openstack-nova-compute
- 迁移虚机
openstack server migrate $VMNAME --live $HOSTNAME
- 检查虚机是否迁移成功
openstack server show $VMNAME —— 查看虚机是否再目的主机上
/var/log/nova/nova-compute.log —— 如果出错,查看计算节点的Nova日志
Libvirt
- 编辑并定义虚机xml,虚机使用共享磁盘
virsh define vm.xml
- 启动虚机
virsh start $VMNAM
- 迁移虚拟机
virsh migrate $VMNAM --live --p2p --undefinesource --persistent --auto-converge --persistent-xml /path/to/dst_persistent.xml --migrateuri tcp://$MIGRATE_NET_IP qemu+tcp://$DST_IP/system
--live: 在线迁移
--p2p: 点对点方式迁移,发起迁移的libvirt客户端只连接源端libvirtd服务,源端会控制整个迁移的过程,它直连目的端的libvirtd服务,源端和目的端点对点迁移,不需要发起迁移的libvirt客户端参与迁移流程,因此称为点对点迁移
--undefinesouce: 如果迁移成功了,将源端虚机的xml undefine掉
--persistent: 如果迁移成功了,用指定的xml定义虚机
--migrateuri: 指定虚机迁移使用的网络,当libvirt对迁移目的端的IP解析不正确,或者主机侧有多张网卡,或者目的端使用unix方式迁移的时候,都可以通过migrateuri显示指定迁移网络,迁移网络$MIGRATE_NET_IP可以和目的端网络$DST_IP相同,也可以不同
注意:libvirt在迁移时,源端虚机的vnc server监听的IP不能和OpenStack定义的虚机一样 —— 设置为本机的IP,需要设置成0.0.0.0,这样迁移前后vnc server绑定端口才能通用,否则在迁移过程中,目的端启动qemu进程时会绑定源端IP以及对应的端口号,用作vnc server的端口监听。这种操作会导致迁移报错: Cannot assign requested address。
加密迁移
证书准备
- 加密迁移分许多类型,这里(Openstack/Libvirt/Qemu)使用非对称加密。主流的非对称加密框架就是
PKI
—— Public Key Infrastructure ,TLS
是按照PKI框架基于SSL
实现的加密传输协议。X.509 certificate
是PKI框架中按照RFC 5280
标准定义的用于实现加密传输的证书。 - 拽了这么多名词,其实加密迁移我们会涉及以下文件
- 根密钥:cakey.pem,用于制作根证书,独一份,私钥,保存在证书服务器上
certtool --generate-privkey > cakey.pem
- 根证书模板文件:ca.info,用于制作根证书,通过根密钥,以该文件为模制作根证书
cn = Name of your organization
ca
cert_signing_key
- 根证书:cacert.pem,用于制作加密迁移证书,独一份,它包含公钥信息,保存在证书服务器上
certtool --generate-self-signed --load-privkey cakey.pem --template ca.info --outfile cacert.pem
- 客户端密钥:clientkey.pem,用于制作客户端证书,私钥
certtool --generate-privkey > clientkey.pem
- 客户端模板文件:client.info,包含的关键信息是客户端的节点名cn = hostname,用于制作客户端证书
country = CN
state = SiChuang
locality = ChengDu
organization = Libvirt Project
cn = client1
tls_www_client
encryption_key
signing_key
- 客户端证书:clientcert.pem,包含客户端的公钥信息以及客户端本身信息,当服务端要确认客户端身份时,会检查此证书,每个计算节点都有一份
certtool --generate-certificate --load-privkey clientkey.pem --load-ca-certificate cacert.pem --load-ca-privkey cakey.pem --template client.info --outfile clientcert.pem
- 服务端密钥:serverkey.pem,用于制作服务端证书,私钥
certtool --generate-privkey > serverkey.pem
- 服务端模板文件:server.info,包含的关键信息是服务端的节点名cn = hostname,以及IP地址,用于制作服务端证书
organization = Name of your organization
cn = compute1.libvirt.org
signing_key
- 服务端证书:servercert.pem,包含服务端的公钥信息以及服务端本身信息,当客户端要服务端身份时,会检查此证书,每个计算节点都有一份
certtool --generate-certificate --load-privkey serverkey.pem --load-ca-certificate cacert.pem --load-ca-privkey cakey.pem --template server.info --outfile servercert.pem
- TODO:非对称加密证书历史及作用
命令
- OpenStack
修改nova配置文件的加密迁移选项live_migration_with_native_tls=true
,重启openstack-nova-compute服务,迁移命令不变
openstack server migrate $VMNAME --live $HOSTNAME
- Libvirt
客户端增加tls参数,其他选项不变
virsh migrate $VMNAM --live --p2p --undefinesource --persistent --auto-converge --tls --persistent-xml /path/to/dst_persistent.xml --migrateuri tcp://$MIGRATE_NET_IP qemu+tcp://$DST_IP/system
流程浅析
Http Request -> Nova API
- OpenStack在线迁移流程由Nova负责,客户端通过向控制节点的nova-api服务发送rest api请求,url格式如下:
http://$host_ip:$nova-api-port/servers/{server_id}/action
host_ip: nova-api服务所在节点的IP,通常任意控制节点都提供nova-api service
nova-api-port: nova-api服务监听的端口号,通过配置文件配置文件/etc/nova/nova.conf的osapi_compute_listen_port选项可以指定
server_id: 通过openstack server list查到的要迁移虚机的ID
注意:$host_ip:$nova-api-port
如果不确定,可以根据openstack或nova等http客户端工具的debug选项查看到。比如nova list --debug可以跟踪nova客户端向nova-api发起的所有请求的详细信息。
- 以curl工具为例,介绍客户端发起迁移后的流程
- 客户端根据自己的认证信息(域、用户名、密码),向keystone请求token获得认证,之后的Request请求中,服务端检查到http头部添加了TOKEN,不再拒绝请求。否则会报无权限的错误。
格式:
curl -v -s -X POST $OS_AUTH_URL/auth/tokens -H "Content-Type: application/json" -d '{ "auth": { "identity": { "methods": ["password"],"password": {"user": {"domain": {"name": "'"$OS_USER_DOMAIN_NAME"'"},"name": "'"$OS_USERNAME"'", "password": "'"$OS_PASSWORD"'"} } }, "scope": { "project": { "domain": { "name": "'"$OS_PROJECT_DOMAIN_NAME"'" }, "name": "'"$OS_PROJECT_NAME"'" } } }}' \
| python -m json.tool
示例:
curl -v -i \
-H "Content-Type: application/json" \
-d '
{ "auth": {
"identity": {
"methods": ["password"],
"password": {
"user": {
"name": "admin",
"domain": { "id": "default" },
"password": "ADMIN_PASS"
}
}
},
"scope": {
"project": {
"name": "admin",
"domain": { "id": "default" }
}
}
}
}' \
"http://keystone-admin.cty.os:10006/v3/auth/tokens"
服务端响应后如果成功,返回的状态为201 Created,将其头部的X-Subject-Token字段取出作为之后rest请求的token值:
export OS_TOKEN=$X-Subject-Token
- 迁移命令需要指定目的host,设置迁移的各种参数,因此是POST方法,POST格式为JSON,字段定义参考OpenStack Compute API中对在线迁移的介绍。格式如下:
URL: /servers/{server_id}/action
Request Body:
{
"os-migrateLive": {
"host": "$hostname",
"block_migration": false,
"disk_over_commit": false,
"force": false
}
}
- 客户端发起迁移,假设我们现在将一个uuid为811d9313-02d7-4c72-ba88-422e18c3004f的虚拟机迁移到主机hb02-compute-10e114e194e14上,示例如下:
curl -g -i -X POST http://nova-api.cty.os:10010/v2.1/servers/811d9313-02d7-4c72-ba88-422e18c3004f/action -H "User-Agent: python-novaclient" -H "Content-Type: application/json" -H "Accept: application/json" -H "X-Auth-Token: $OS_TOKEN" -d '{"os-migrateLive": {"disk_over_commit": false, "block_migration": false, "host": "hb02-compute-10e114e194e14"}}'
Nova API -> Nova Compute
Http Request -> WSGI Server
WSGI Server -> WSGI Application
WSGI Application -> Nova Compute
- 对于RESTful风格的互联网软件框架,核心概念就是资源,网络中任何东西都是资源,每个资源对应一个特定的URI,并用它来标识,访问URI的各种方法就是操作资源的各种方法。OpenStack定义了很多资源,并实现了针对这些资源的各种操作函数。路由就是将请求的URL映射到对应资源,然后可以对资源进行操作。
- OpenStack引入WSGI(Web Server Gateway Interface)用来将HTTP到后端对应操作的映射以一种标准的接口实现,在WSGI中,每个资源被抽象成一个Controller对象,它包含很多操作,每个操作对应一个HTTP请求和响应。当HTTP请求到达时,WSGI首先将其路由到对应的Controller,调用Controller对应的操作函数,这是一个通用的操作。对于想要添加一个API的开发者来说,它会将其对应的实现封装成Controller的子类,在WSGI框架的调用流程中,会根据HTTP请求的不同最终调用到开发者自己提供的接口。
- 以迁移举例,服务端接收HTTP情求到最后的Hypervisor发起,可以分成以下阶段:
- HTTP请求 -> WSGI Controller
WSGI框架根据HTTP请求的内容,将其路由到对应的Controller,对于迁移来说,就是nova.api.openstack.compute.migrate_server:MigrateServerController
,MigrateServerController初始化migrate相关的所有操作,包括迁移操作。
服务启动:
nova.cmd.api:main /* nova-api服务入口 */
server = service.WSGIService(api, use_ssl=should_use_ssl) /* 初始化WSGI Server,加载配置文件中指定的应用osapi_compute */
/* 启动WSGI Server,但Http Request到达时,由osapi_compute进行处理 */
launcher.launch_service(server, workers=server.workers or 1)
/* 根据/etc/nova/api-past.ini的配置,可以逐步找到最终建立路由信息的操作实现 */
osapi_compute -> openstack_compute_api_v21 ->
osapi_compute_app_v21 -> nova.api.openstack.compute:APIRouterV21.factory ->
nova.api.openstack.compute.routes:APIRouterV21
APIRouterV21.__init__ /* APIRouterV21类在初始化的时候,会建立URL和Resource的映射关系 */
create_route
- [TODO]:如何建立路由?
路由请求:
当Request到达时,nova.api.wsgi.Router:__call__会被触发,最终将Request路由到对应的Controller上
nova.api.wsgi.Router:__call__ -> Router:_router
......
ServerMigrationsController.__init__
self.compute_api = compute.API() /* 注册所有的computerAPI */
- WSGI Controller -> MigrateServerController
- TODO
- MigrateServerController -> Nova Computer服务本地迁移
- 这个阶段
MigrateServerController._migrate_live
被触发,函数调用链如下:
/* 该函数解析Request中的body信息,设置对应的迁移参数 */
nova.api.openstack.compute.migrate_server.MigrateServerController:_migrate_live
nova.compute.api.API:live_migrate
compute_task_api.live_migrate_instance <=>
nova.conductor.api.ComputeTaskAPI:live_migrate_instance /* 调用conductor.ComputeTaskAPI的live_migrate_instance方法 */
cctxt = self.client.prepare(version=version) /* self.client是一个过程调用的句柄,用于实现远程过程调用 */
cctxt.cast(context, 'live_migrate_instance', **kw) /* 调用RPC Server注册的live_migrate_instance接口 */
- Nova中一个四个组件,nova-api,nova-compute,nova-conductor,nova-scheduler,他们之间互相的通信使用了基于AMQP实现的RPC机制,其中compute,conductor,scheduler在启动时都会注册一个RPC Server,api因为内部没有服务会调用它提供的接口,因此无需注册。
- 上面的liva_migrate_instance就是conductor的RPC Server注册的供其它组件调用的接口,Conductor RPC Server中有一个nova.conductor.manager.ComputeTaskManager类,它包含了nova-conductor对外提供的live_migrate_instance接口,因此conductor组件使用RPC远程调用live_migrate_instance方法,最终调用它本身提供的live_migrate_instance接口。这里其实是conductor组件自己调用自己的RPC接口。所以可以看到在获取调用句柄的时候,并没有指明目标host,它不是跨节点的RPC调用。
- 这段代码比较难奇怪,nova-api绕来绕去,执行迁移最终交给了conductor来做,似乎并没有交给compute组件。这里之所以这样写,和conductor组件定位有关系,API依据请求时间长短,将请求发送给conductor或者compute,对于长时任务(比如这里的迁移),请求会发送给condutor,conductor负责全程跟踪和调度,并且它除了负责长时任务还负责代理其它节点的DB访问。最终对于虚拟机的操作,比如迁移,还是都会发送到compute组件完成。接下来继续分析conductor组件中的live_migrate_instance接口
nova.conductor.manager.ComputeTaskManager:live_migrate_instance
nova.conductor.manager.ComputeTaskManager:_live_migrate
nova.conductor.task.live_migrate.LiveMigrationTask:_execute /* 创建迁移任务并开始执行 */
self.compute_rpcapi.live_migration <=> nova.compute.rpcapi.ComputeAPI:live_migration
client = self.router.client(ctxt)
cctxt = client.prepare(server=host, version=version) /* 创建RPC调用的句柄 */
cctxt.cast(ctxt, 'live_migration', instance=instance, dest=dest, block_migration=block_migration,
migrate_data=migrate_data, migration=migration) /* RPC调用,迁移请求发送到迁移源主机的nova-compute服务 */
nova.computer.manager.ComputeManager:live_migration /* 执行nova-computer服务本地迁移 */
- 至此,流程走到了本地的nova-compute服务的迁移接口。关于nova-api服务响应流程的细节,可以通过控制节点的日志/var/log/nova/nova-api.log分析。
Nova Compute -> Libvirt Daemon
- nova-compute执行迁移的核心就是调用Libvirt接口发起迁移,流程如下:
nova.computer.manager.ComputeManager:live_migration
self._live_migration_executor.submit(self._do_live_migration, context, dest, instance,
block_migration, migration, migrate_data)
nova.computer.manager.ComputeManager:_do_live_migration
nova.computer.manager.ComputeManager:_do_pre_live_migration_from_source
/* 加载对应虚拟化驱动的迁移函数,driver是一个可以配置的驱动参数
nova.conf的compute_driver可以设置,这里通常都是libvirt的driver,即nova-compute调用的是Libvirt接口实现迁移
*/
self.driver.live_migration <=> nova.virt.libvirt.driver.LibvirtDriver:live_migration
nova.virt.libvirt.driver.LibvirtDriver:_live_migration
nova.virt.libvirt.driver.LibvirtDriver:_live_migration_operation
/* 这里会调用Libvirt Python的库获取libvirt.virDomain对象,该对象就是一个虚拟机抽象
包含了libvirt提供的迁移接口migrate
*/
guest.migrate <=> libvirt.virDomain.migrate
- 至此,调用路径到了libvirt,关于nova-compute服务响应流程的细节,可以通过计算节点的日志/var/log/nova/nova-compute.log分析
- 这里再稍微分析下libvirt的迁移接口。libvirt项目C/S架构,对于服务端,就是我们熟悉的libvirtd守护进程,它完成虚机管理的所有接口实现,对于客户端,除提供virsh工具外,也提供java,python的库接口。openstack使用libvirt-python接口进行虚机管理。对于客户端到服务端流程,python接口和virsh命令类似,因此直接从virsh工具的migrate命令分析内存迁移。
virsh migrate $VMNAM --live --p2p --undefinesource --persistent --auto-converge --persistent-xml /path/to/dst_persistent.xml --migrateuri tcp://$MIGRATE_NET_IP qemu+tcp://$DST_IP/system
客户端:
/* virsh 工具命令入口 */
cmdMigrate
doMigrate
if (flags & VIR_MIGRATE_PEER2PEER) {
/* 如果指定了p2p参数,使用该接口,从服务端被调用的接口分析
Openstack指定了p2p选项,因此走的是virDomainMigrateToURI3函数
*/
virDomainMigrateToURI3(dom, desturi, params, nparams, flags)
}
virDomainMigrateUnmanagedParams
if (flags & VIR_MIGRATE_PEER2PEER) {
domain->conn->driver->domainMigratePerform3Params
(domain, dconnuri, params, nparams,
NULL, 0, NULL, NULL, flags);
}
remote服务端接口:
/* libvirt实现中,client和具体的虚拟化driver接口之间
有一层remote driver,用作适配不同的虚拟化driver,
因此这里首先连接的是remote driver的domainMigratePerform3Params
*/
remoteDispatchDomainMigratePerform3ParamsHelper
remoteDispatchDomainMigratePerform3Params
virDomainMigratePerform3Params
conn->driver->domainMigratePerform3Params(
domain, dconnuri, params, nparams, cookiein, cookieinlen,
cookieout, cookieoutlen, flags);
qemu服务端接口:
/* 这里真正的调用qemu实现的driver接口
对于lxc或者xen,相应的调用他们对应的接口
*/
qemuDomainMigratePerform3Params
qemuMigrationSrcPerform
if (flags & VIR_MIGRATE_PEER2PEER) {
qemuMigrationSrcPerformJob
}
qemuMigrationSrcPerformPeer2Peer
/* 这里,libvirtd服务连接目的端libvirtd
这是Peer2Peer与普通迁移在控制方式上的本质区别
普通迁移,会在virsh、libvirt-java、libvirt-python
等客户端流程中去连接目的端
*/
dconn = virConnectOpenAuth(dconnuri, &virConnectAuthConfig, 0);
qemuMigrationSrcPerformPeer2Peer3
/* 源端开始发起迁移 */
qemuMigrationSrcBeginPhase
/* 调用目的端libvirtd的qemu Driver Prepare接口,让目的端准备迁移 */
dconn->driver->domainMigratePrepare3Params
(dconn, params, nparams, cookiein, cookieinlen,
&cookieout, &cookieoutlen, &uri_out, destflags);
<=> qemuDomainMigratePrepare3Params
/* 源端开始执行迁移 */
qemuMigrationSrcPerformNative
/* 调用目的端libvirtd的qemu Driver Finish接口 */
dconn->driver->domainMigrateFinish3Params
(dconn, params, nparams, cookiein, cookieinlen,
&cookieout, &cookieoutlen, destflags, cancelled);
<=> qemuDomainMigrateFinish3Params
/* 源端确认迁移完成 */
qemuMigrationSrcConfirmPhase
整个迁移过程,源码中解释如下:
/*
* Sequence v3:
*
* Src: Begin
* - Generate XML to pass to dst
* - Generate optional cookie to pass to dst
*
* Dst: Prepare
* - Get ready to accept incoming VM
* - Generate optional cookie to pass to src
*
* Src: Perform
* - Start migration and wait for send completion
* - Generate optional cookie to pass to dst
*
* Dst: Finish
* - Wait for recv completion and check status
* - Kill off VM if failed, resume if success
* - Generate optional cookie to pass to src
*
* Src: Confirm
* - Kill off VM if success, resume if failed
*
* If useParams is true, params and nparams contain migration parameters and
* we know it's safe to call the API which supports extensible parameters.
* Otherwise, we have to use xmlin, dname, uri, and bandwidth and pass them
* to the old-style APIs.
*/