QEMU/KVM USB设备直通/透传虚拟机

英文应该是叫qemu kvm usb device passthrough,有些人翻译成直通,有些人叫透传,总之就是passthrough。

下面的内容是综合整理了网上的教程和咨询前华为同事的结果,以及自己试验的一些结论。

准备工作:

  1. 确认宿主机BIOS里面打开了VT-d/VT-x/VT-i等所有硬件虚拟化支持开关
  2. 打开Linux操作系统的iommu开关,在grub启动命令行里面配置,Intel CPU和AMD CPU配置参数有区别:Intel CPU: intel_iommu=on;AMD CPU: amd_iommu=on
  3. 重启服务器,检查iommu配置是否生效(dmesg | grep -i iommu,输出“Intel-IOMMU: enabled”表示生效)

grub配置iommu参考资料:https://www.jianshu.com/p/035287ba9acb 《CentOS7 minimal kvm iommu 辅助虚拟化 vt-x (用于pci透传)》

在宿主机上通过lsusb命令获取USB设备信息(yum install usbutils -y安装该工具):

字段信息解释:Bus:001,Device:032,vendor id:0781,product id:5581

libvirt配置修改:

在虚拟机的libvirt xml配置文件的<devices></devices>段内添加如下配置:

vendor id和product id就是lsusb获取的需要直通的usb设备信息,之后启动虚拟机,正常情况下就可以在虚拟机里看到usb设备了。

注意事项:

有些usb设备不在windows设备管理器的“通用串行总线控制器”里面(U盘一般属于这个),比如我试过的USB无线网卡,是属于“网络适配器”,U盾则属于“DVD/CD-ROM驱动器”,而加密狗设备,则属于“人体学输入设备”(我用的加密狗是这个,不同的加密狗可能有区别)。要区分设备类型,可以用lsusb -t命令查看(比如我用的加密狗设备信息如下):

我就是通过Class=Human Interface Device,才想到它属于“人体学输入设备”的。

工行U盾直通效果:

另外nova里面(Mitaka版本)支持PCI设备的直通,但是usb设备好像还不支持,还没仔细研究过代码。

因此先临时用libvirt管理这台虚拟机了。注意如果手工修改了libvirt的xml配置,通过nova对虚拟机做操作,如reboot、stop/start、rebuild、resize等等会重置虚拟机的xml文件,相关usb配置都会丢失。(我是通过复制一份xml,修改掉uuid,让nova管理不了我这台虚拟机来解决的,用的系统盘、虚拟网卡还是nova创建的,nova看到的虚拟机永远是关机状态的就好了,它是用来为我手工管理的虚拟机占坑用的)。

 

OpenStack Trove&manila的网络依赖

这俩项目架构都差不多,

Trove是trove-api(接收用户请求)、 trove-taskmanager(用户管理操作逻辑适配层)、 trove-conductor(通过RPC接收数据库操作) trove-guestagent(运行在虚拟机里面,实际管理数据库实例,具有多种类型数据库driver以驱动不同数据库类型)。

https://docs.openstack.org/trove/latest/install/get_started.html

Manila是manila-api(接收用户请求)、manila-data(处理备份、迁移等数据相关逻辑)、manila-scheduler(调度文件共享服务节点,也即share service节点)、manila-share(文件共享服务节点,提供实际的共享文件服务,可以运行在物理机上或者虚拟机里)。

https://docs.openstack.org/manila/pike/install/get-started-with-shared-file-systems.html

这两个服务有一个共同点,多个子服务同时运行在物理机和虚拟机里面,这种场景下,就得考虑物理网络到虚拟网络的连通性问题,否则服务之间不能互通,肯定没法正常运行。2种服务的解决方案也比较类似,都是通过L2或者L3来打通物理和虚拟网络:

https://wiki.openstack.org/wiki/Manila/Networking

L2方式下,需要使用FLAT网络,所有物理机和虚拟机都在一个2层下,业务和管理数据都在一个平面,性能好,但是不安全,大规模环境下也存在网络广播风暴问题。

L3方式下,也有两种方法,虚拟路由和物理路由。虚拟路由模式下,服务提供节点(数据库的guestagent节点和共享文件服务节点)需要跟物理机上的管理服务互通,以便接收用户管理操作请求,但实际的业务面数据(客户端虚拟机到服务节点虚拟机,如读写数据库、读写共享文件)仍然是走的同一个私有网络(同一个network的subnet)。如果服务节点(提供数据库或共享文件服务的虚拟机)上有2个port,可以配置为1个租户私有网port,用来提供业务面的网络数据服务,另一个配置为service port(也即FLAT模式网络),用来提供跟物理机网络互通的控制面网络数据服务。如果服务节点上只有一个port,那就需要两个虚拟路由器,一个是租户私有网的,一个是服务网络,二者之间要通过一个interface来打通。

https://www.jianshu.com/p/d04f829e3330

http://ju.outofmemory.cn/entry/113174

我们用的是VLAN网络模式,这种模式下需要用到物理路由器,来打通各个VLAN的子网,这样就可以做到物理网络和虚拟网络(分属不同VLAN,可避免广播风暴问题),这种方案在中小规模私有云下,非常稳定可靠,性能也更好,也更接近传统IDC的网络模型,对传统企业的IT运维人员比较友好。

 

Gerrit删除一个review提交记录

举例:我想删除这个review提交,http://10.0.30.120/#/c/3018/,也即gerrit数据库change_id=3018的提交记录

目标效果:gerrit web页面上看不到这个提交,历史记录也没有它,上面的链接也打不开,但是如果是已经merge的提交,则git库中仍然还是有这个commit的。

网上找了好久,自己把数据库里面相关的几个表都清理了(删除了所有change_id=3018的记录),结果gerrit web页面上还是能看到它,只不过打开就报错了,唯一能想到的就是缓存问题了。

最后找到一篇这个:https://stackoverflow.com/questions/29575600/fully-delete-abandoned-commit-from-gerrit-db-and-query

关键是重建gerrit索引这一步: java -jar path/to/gerrit.war reindex -d path/to/gerrit-site-dir

kube-apiserver RestFul API route创建流程分析

看完《kubernetes权威指南》和《Go程序设计语言》两本书之后,终于可以进入实际的代码分析阶段,根据之前熟悉其他开源项目源码(如libvirt、OpenStack等)的经验,首先从接口/API开始分析。

k8s开发环境搭建:使用kubeasz快速搭建k8s集群all-in-one开发测试环境

Golang调试环境搭建:kubernetes源码调试体验

k8s源码版本:v1.10

分析API:GET 127.0.0.1:8080/api/v1/nodes/10.0.90.22(10.0.90.22是我环境中的一个node,8080为kubectl proxy API代理端口)

分析目标:搞清楚用户发送请求到这个API之后kube-apiserver的处理流程

参考资料:http://www.wklken.me/posts/2017/09/23/source-apiserver-04.html(还有前面几篇,这里面讲的是老版本的,有很多代码已经改了,但是主要流程值得参考)

源码流程很绕(至少目前我是这么认为,可能是因为我刚开始看源码),需要多看,多动手调试,看很多遍,调很多遍,整天琢磨它,应该都能看明白。

route注册({path} to {handler})

要搞清楚route注册流程,就必须先把go-restful框架的用法搞明白,官方Readme文档有说明,也有示例代码。这里给出上面提到的参考资料:http://www.wklken.me/posts/2017/09/23/source-apiserver-01.html

我们只需要记住,go-restful框架中route注册需要经过如下几个步骤:

  1. 创建一个container(默认使用default)
  2. 创建一个web service
  3. 创建handler(也就是API的请求处理函数)
  4. 把API path和handler绑定到web service
  5. 把web service(可多个,但root Path不能相同)绑定到container
  6. 启动包含container(可多个)的http server

我这边试验的一个简单的示例代码:

 

我们下面所有的流程都以这个为基础进行分析。

kube-apiserver的main入口:

这里用到了github.com/spf13/cobra这个包来解析启动参数并启动可执行程序。

具体注册流程就不一步一步的分析了,直接根据断点的bt输出跟着代码查看吧:

NewLegacyRESTStorage这个方法(注意是LegacyRESTStorageProvider类型的方法),返回了3个参数,restStorage, apiGroupInfo, nil,最后一个是错误信息可忽略,第一个对我们流程分析没啥影响(应该是),中间这个apiGroupInfo是重点,InstallLegacyAPIGroup就是注册/api这个path的,apiPrefix这个参数是 DefaultLegacyAPIPrefix = "/api" ,apiGroupInfo.PrioritizedVersions目前就”v1″一个版本。

注意这里的getAPIGroupVersion方法,它把apiGroupInfo封装到了apiGroupVersion结构体里面,具体是apiGroupVersion.Storage,下面会用到:

 

paths变量就是上面NewLegacyRESTStorage里restStorageMap map的key,也即”pods”、”nodes”等path。a.registerResourceHandlers()就是注册各个path的handler,也即restStorageMap的value。

下面分析handler具体是什么方法?以restfulGetResource为例:

也即r.Get是最终的实际请求处理函数,它是怎么来的?

r就是getter也就是storage.(rest.Getter),上面提到过参数storage是restStorageMap的value,path是key,针对我们分析的API对应”nodes”: nodeStorage.Node(storage == nodeStorage.Node)。rest.Getter是一个go-restful里面定义的接口,接口也是一种类型,根据Go语言的接口类型定义,接口即约定,可以在任何地方(包)为任何数据类型(int、string、struct等等)实现接口(也即实现接口约定的具有指定参数类型和返回类型的函数),我们看下这个接口的定义:

再看下storage也即nodeStorage.Node有没有实现它,很郁闷,没找到,但是不要灰心,想想看,struct是可以嵌套的(类似父子类的继承关系),并且支持匿名成员,使用匿名成员可以省略中间struct的名称(尤其是匿名成员根本没有嵌套的中间struct变量名可用)(如果没印象了,可以翻看一下《Go程序设计语言》),看看Node的嵌套struct里面有没有实现接口,

对,最终我们在genericregistry.Store这个struct定义的包文件里面找到了它的Get接口实现方法:

下面继续找e.Storage.Get,其实根据接口的定义,我已经找到了真正的后端实现了,k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/storage/etcd3.(*store).Get,为啥是etcd3?需要分析etcd存储后端(k8s元数据存储后端)类型注册(默认是etcd3后端)过程:

fff但是怎么反推回来handler?也就是e.Storage.Get,最简单的办法当然是加断点调试,调试结果在下面的请求处理部分有贴出来,这里就不贴了。但是看了之后还是有疑问,怎么从e.Storage.Get走到etcd3的Get方法的?

NewCacherFromConfig返回的Cache结构体指针,结构体实现了storage.Interface定义的各个接口,

所以e.Storage.Get就是k8s.io/apiserver/pkg/storage/cacher.go:Get方法,它里面又调用了etcd3后端的Get方法:

c.storage==config.Storage由NewRawStorage生成,根据上面的调试结果可以确定它最终调用到k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/storage/etcd3.newStore,所以c.storage.Get调用的就是:

这个Get就是最终处理用户发送的Get API请求的位置。

 

请求处理(HTTP RestFul request to {handler})

这部分就不多说了,直接看调用栈吧:

 

未解决的问题

  • 上面代码分析过程中没搞清楚的两个Go语言知识点,都是关于接口的
  • container到http server,这个应该不复杂,不在本次代码分析的目标之内,就没关注
  • 各种filter注册和使用,同上,没关注
  • etcd部署及使用,etcd在k8s中的使用,这部分属于扩展知识,有时间再看下

跟OpenStack API处理流程的比较

最大的差异当然还是语言上的,python和Go还是有点不太一样的,所以用到的RestFul HTTP Server的框架也不一样,route定义和注册流程也差别比较大,当然还是习惯问题,如果整天看这些代码,用这些框架,也就不会有刚开始看代码时很强烈的差异感了。犹记得当年刚开始看OpenStack nova-api route注册转发流程也是一脸懵逼好久好久。。。

其他方面就是交互流程不一样,当然Get方法差不多,都是接收用户API请求,然后分发到具体的处理方法(route到controller或handler),之后controller或handler从后端数据库(MySQL或etcd)查询用户请求的数据,最后封装返回给用户,再由controller或handler获取请求数据之前还会对用户请求进行多次filter过程,把不合法或不合适的请求过滤掉,只允许合法合适(如未被限流)的请求被controller或handler处理并正常返回用户。

而Create方法,则差异比较大,OpenStack一般是用消息队列进行消息传递,从API服务把具体执行的动作RPC到其他服务(如调度服务、计算节点管理服务等),动作执行过程中或者执行完毕也会通过RPC更新操作状态到数据库(当然最开始是直接访问数据库,后面为了安全改为经过RPC)。而k8s则是完全通过etcd来完成各个组件之间的异步交互,通过watch各自关系的key来实现消息传递和异步调用,操作状态更新也是通过更新etcd来实现。

 

nova、cinder的snapshot与ceph相关交互

其实这两块代码已经看过很多遍了,但是经常忘记,而且经常有同事问起来,每次都要翻一遍代码,因此决定整理下相关交互流程,后续备查。(注:仍然是基于Mitaka版本分析源码,Queens版本粗略看了下改动不大)

先说nova部分,nova的snapshot主要是针对系统盘的(root disk),相关命令行是:

从nova源码来看,backup和image(snapshot)的区别只是多了个rotation的概念,多出的这个概念主要是用来限制备份数量的(超过这个数量的backup会被滚动删除,肯定是先删除最老的),你如果把rotation设置的很大,那它就跟image没什么区别了,nova后端的代码也是一套(api入口不一样,但是到了nova compute manager那层就没什么区别了)

从上面源码可见二者调用的具体实现没有区别。

_snapshot_instance调用了libvirt driver的snapshot方法,这里面区分了live和cold的snapshot类型,并且还区分了direct snapshot和外部快照,ceph后端是用的direct snapshot,也即通过ceph的rbd image相关api来做快照。

可以看出经过了创建临时snapshot(还在nova系统盘的pool)、在glance pool中clone snapshot出新rbd卷(跨pool clone卷)、flatten(clone的新卷与snapshot解除关联)、删除临时快照(清理临时资源)、glance pool中的rbd image创建snapshot,此snapshot就是生成的云主机(虚拟机)系统盘的快照(新的镜像,或者叫自定义镜像、捕获镜像、镜像模板等,总之就是nova image-create生成的东西,可以用glance image-list看到),也就是说glance中的image(不管是管理员上传的image还是nova image-create制作的image,都是snap)对应的是rbd里面的一个snap而不是实际的卷,这样创建新的云主机(虚拟机)的时候,系统盘直接从snap clone一个rbd卷就好了,由于支持COW,因此实际clone过程中数据copy量极少、创建系统盘卷速度特别快(这也是glance镜像在有云主机使用的情况下不能删除的原因)。

rbd snapshot的原理可以参考前同事的一篇文章:http://www.sysnote.org/2016/02/28/ceph-rbd-snap/

direct+live snapshot场景下,创建临时snapshot过程中,由于云主机一直运行中,因此可能有部分数据还在内存的磁盘缓存中,没有刷新到磁盘,所以还是有一定概率导致制作的系统盘快照是损坏的。

上面是ceph后端的流程,本地存储后端的snapshot流程可参考之前的文章:Mitaka Nova在线快照数据丢失问题及解决方法

nova这边其实还有一种需要跟cinder(ceph)交互的功能,boot-from-volume,从卷启动云主机(虚拟机),这种情况下cinder list里面看到的volume是bootable的,不过这种功能在ceph后端场景下不常用,就不介绍了。

接下来是cinder部分,涉及的命令行应该有create、backup-create、snapshot-create这几个(还有没有其它的不确定,估计应该没了):

先看create,创建卷,支持多种参数,比如创建裸卷、从snapshot创建卷、从已有的volume创建卷等。

上面忽略了很多taskflow,直接到了cinder.volume.flows.manager.create_volume.CreateVolumeFromSpecTask#execute,cinder里面用到的taskflow一般都是linear类型的,顺序执行,只要一个一个看过去就行了,一般都包含一个参数解析的task,如cinder.volume.flows.manager.create_volume.ExtractVolumeSpecTask,解析出来的参数传递给下一个task使用,最后run起来,正常执行execute,有异常的话就执行revert方法。关于OpenStack的taskflow介绍:https://docs.openstack.org/taskflow/latest/

创建卷的api文档(v2版本,v3也类似):https://developer.openstack.org/api-ref/block-storage/v2/index.html#create-volume

跟snapshot相关的主要是_create_from_snapshot和_create_from_source_volume,先看第一个:

共3步,从snapshot clone新卷、flatten、resize,后面两步不是必须步骤。配置项rbd_flatten_volume_from_snapshot,

从snapshot创建卷的时候是否flatten,默认是False,不flatten。

再看_create_from_source_volume,它调用的是create_cloned_volume,

这个流程比较多,毕竟要先做一个snapshot,然后再clone新卷,相当于包含了从snapshot创建卷的流程。配置项rbd_max_clone_depth,

默认最大clone深度是5层,达到5层就flatten。

再看下backup操作(其实这个操作跟rbd snapshot没啥大关系),cinder.backup.drivers.ceph.CephBackupDriver#_backup_rbd这里是最终执行的方法,就不具体分析了,主要是有个增量备份过程: