1.ovirt介绍

Ovirt(openVirtualization)是一个基于KVM的开源IaaS项目,是redhat虚拟化管理平台RHEV的开源版本,其前身是Redhat的虚拟化商业产品。在架构设计上它使用了Node/Engine分离结构,以方便功能的划分与管理。

图1.oVirt逻辑结构图

图1从总体框架上说明了oVirt的工作流程。对外,oVirt中的Engine通过HTTP协议向外提供HTTP API,同时提供内建的网页服务供用户和系统管理员使用。系统管理员通过网页可以创建、修改虚拟机及相关设备或用户权限,用户在拥有权限的情况下可以操作自己的虚拟机,并通过VNC或SSH登陆自己的虚拟机。Engine在整个系统中充当管理者的角色并对外提供管理服务,它挂载了自己的数据库记录整个系统中所有的虚拟机配置,各个节点的自身状态,系统的网络状态,存储器状态。管理的逻辑,状态及策略全部在Engine中设置与实现。Node只负责功能上的实现,不进行任何状态的记录和任何策略的实现。Engine与Node之间的关系十分像Linux中驱动程序与应用程序的功能分割关系:驱动仅仅负责功能的实现,如设备的读、写、开启与关闭,如何使用这些功能留给应用层。同样Node仅仅负责实现虚拟机器与设备的创建与修改,资源的共享与保护,如何使用这些功能交给Engine处理。Node暴露两种基于网络的API与Engine交互,XMLRPC与REST。Engine通过这些接口控制各个Node上功能的启动。当然用户也可以调用这些API进行第三方程序的开发。oVirt里的Node可以由一个普通的Linux上安装VDSM(VirtualDesktop Server Manager) 构成,也可以由一个专为oVirt定制的Linux系统构成。在定制的情况下,Node上的许多文件系统都是ramdisk(基于内存的Linux磁盘设备),系统重启后其中的内容消失,从而保证了Node的无状态性。Engine/Node的设计不仅方便将来的开发,更简化了用户的安装使用,在定制的情况下Node可以快速大量部署。

图2.Node上运行的主要组件

简单概括起来,VDSM的功能主要有:负责Node的自动启动与注册;虚拟机的操作与生命周期管理;网络管理;存储管理;Host与VM状态监视与报告;提供对虚拟机的外部干涉功能;提供内存与存储的合并与超支功能(OverCommitment)。
2.Ovirt体系架构

一个标准的oVirt部署包括三部分,主要有:

1.ovirt-engine,这个被用于部署,监管,移除,停止和创建VM镜像,配置存储,网络,等等。

2.一个或多个ovirt-node,我们在node上运行虚拟机(VMs)。

3.一个或多个存储节点,其上保存着对应虚拟机的磁盘镜像和ISO镜像。

Ovirt-node上安装了VDSM和libvirt以及安装了一些额外的包,这些包可以实现网络和其他系统服务的虚拟化。

存储节点可以使用块和文件存储,并且可以通过NFS在本地或者远程进行访问。通过POSIXFS存储类型是可以支持Gluster这样的存储技术的。存储节点被分在一个存储池中,这样可以保证高可用和冗余。
2.1 Ovirt Engine

oVirtengine是一个基于JBoss的java应用程序(由C#演变过来),它作为web服务来运行。这个服务直接指挥node上的VDSM去部署,启动,停止,迁移和监控虚拟机,以及从模板中创建新的镜像至存储中。

engine提供的一些功能:

虚拟机生命周期管理通过LDAP(微软的AD或IPA)进行身份验证网络管理-增加本地网络并且将其附加至主机上。存储管理-管理存储域(NFS/iSCSI/Local)和虚拟VM磁盘。高可用-在其它主机中失败的主机上自动重启客户虚拟机在线迁移-在零停机的情况下移动主机之间的虚拟机。系统调度-基于资源使用/政策持续负载均衡虚拟机。节能优化-在非高峰时期将续集你集中到较少的服务器上。维护管理器-在计划的维护期间,无需为虚拟机停机。镜像管理-基于模板管理,自动精简配置和快照。监控-为系统中的所有对象-虚拟客户机,主机,网络,存储等等。导出/导入-使用OVF文件导入和导出虚拟机和模板。V2V-可以将VMware或RHEL或Xen环境下的虚拟机轻松转换到oVirt环境中

图3展示了OvirtEngine组件的不同层次:

图3.OvirtEngine组件层次
2.2 EngineCore

下面的图形显示了Engine-core的不同组件

enginecore中的主要组件有:

-负责所有与之相关的DB操作。VDS Broker -负责所有需要与VDSM交互的操作。LDAP Broker -负责验证和抓取LDAP目录下用户/组的属性(当前支持AD,IPA和RHDS)Backend Bean-一个Singletonbean ,负责运行不同实体的指令,查询和监控。

2.3VDSM

VDSM是一个以Python开发的组件,它可以为主机,虚拟机,网络和存储管理提供所需要的所有功能。

VDSMAPI是基于XML-RPC的(计划转移到RESTAPI)。这就是ovirt-engine如何与VDSM进行通信的。配置主机,网络和共享的存储。使用libvirt,进行虚拟机的生命周期操作多线程,多进程通过virtio-serial和客户代理对话。添加自定义的对扩展到数以百计的node的LVM的集群支持在所支持的存储类型(本地目录,FCP, FCoE, iSCSI, NFS, SAS)的基础上实现一个分布式镜像库。多主机系统,一个并发的元数据写入器。在数据写入器中进行扩展。

2.4Hooks机制

允许管理员定义修改虚拟机操作的脚本,例如,添加额外的操作像CPU定位,watchdog设备,直接访问LUN允许在集成完成前为新的KVM特性扩展oVirt。有一种简单的方式来测试新的kvm/libvirt/linux特性。在VDSM利用libvirt开始启动虚拟机之前调用hook机制。hook改变了虚拟机定义,并且VDSM通过定义至libvirt以开启虚拟机。

下图阐释了虚拟机生命周期中Hook机制。

2.5 MoM集成

VDSM是和MoM集成在一起的。MOM的行为配置了策略。随着这些策略的使用者可以微调主机的高内存过量使用或安全操作。为了控制它的mom实例,vdsm确实传递了一个mom配置文件并且mom策略文件可以设置mom的缺省行为。刚开始,vdsmd利用配置和策略文件导入了mom并将其初始化。从那时起,mom通过API.py中定义好的API与vdsm进行交互并且控制了运行在主机上的每个虚拟机的内存。mom实例作为vdsm守护进程内的线程运行。

VDSM对存储器的管理

存储器的管理是oVirt的重点,本节将介绍oVirt是如何组织与分配各种存储器的,用户应该如何使用它。每一个Node上都会运行一个VDSM,实现网络、存储器、虚拟机的创建与修改的功能。VDSM的大部分代码用在了存储系统上,其功能包括数据的组织,集群下的数据共享与保护,故障恢复。通常情况下每一个物理机器当作一个Node,运行一个VDSM,Node本身只携带少量存储器用以保存配置。一个集群中通常有一个Engine和数个Node,这些Node通过网络连接到SAN(StorageArea Network) 上,VDSM把Node上运行的虚拟机存储数据保存在SAN上,Node本身为无状态的节点,重新启动后状态消失,从而保证了系统整体的可用性,一般情况下不会因用户的操作而使Node失效。一旦问题发生,通常一次重启即可恢复工作状态。

在云计算环境中,SAN中往往存储着大量虚拟机器使用的virtualimage,同时每一个virtualimage在任何时候都可能被任意Node访问,同时出于性能的考虑virtualimage可能以文件或者数据块的形式出现,这些对存储系统的设计提出了挑战。

为此,VDSM基于以下原则设计了自己的存储系统:

1高可用性:一群安装有VDSM的Node在组建集群的时候,没有潜在的单点故障存在,任何一个Node崩溃不会影响整个集群的功能,它的角色会被其他Node取代。Engine不可用的情况下,Node将继续工作,用户对虚拟机的操作可以继续进行。

2高伸缩性:添加Node和SAN几乎不需要用户的设置,Node上的VDSM会自己注册自己。

3集群安全性:一个VDSM对正在操作的virtualimage进行排它性保护。

4备份与恢复:virtualimage之间有相互关连的特性记录可进行一系列引用/备份操作。

5性能优化:利用多线程与多进程减少操作堵塞状况。

StorageDomain(以下简程SD)是VDSM中的最基本存储实体,所有的virtualimage和virtualimage对应的元数据都会保存在其中。和VDSM中的StorageImage概念不同,这里的virtualimage表示的是虚拟机程序用到的虚拟磁盘数据,特指虚拟机程序最终能够操作的文件或设备文件对象。元数据是描述virtualimage相关数据大小、状态、锁等内容的一组数据集合。SD包括两种类型:FileDomain和BlockDomain。FileDomain使用文件系统存储数据并同步操作,主要针对NFS(NetworkFile System) 和LOCALFS(LocalFile System) 文件系统。在文件系统的帮助下,FileDomain拥有良好的virtualimage操作能力,每一个虚拟机的存储数据(称为Volume)和对应的元数据都以文件的方式保存。每一个Domain实际对应于Host文件系统上的一个目录,针对NFS文件系统VDSM还有额外的逻辑来处理相关意外与错误情况。而BlockDomain直接操作原始的块数据,使用Linux的LVM(LogicalVolume Manager) 功能来组织数据,主要针对iSCSI(InternetSmall Computer System Interface),FCoE(FibreChannel over Ethernet ) 等块设备。由于目标设备上通常没有一个文件系统来保证访问的安全性,VDSM使用了邮箱机制来保证任意时刻,只有一个Node可以修改Block上的内容,而其他Node则通过Socket邮箱发送自己的修改请求。因此它的操作请求速度和监视功能都会比FileDomain弱一些。通常设备将使用Linux的devicemapper机制进行一次映射,每一个Domain实际上是一个Linux中的VolumeGroup,元数据保存在其中的一个LogicVolume及其tag上,虚拟机的Volume保存在另一个LogicVolume中。

StoragePool(以下简称SP)是一组SD的组合,目标是管理跨越SD之间的操作,也就是说SD之间互相的引用、备份、恢复,合并一般发生在一个SP之中。在数据中心里,一个SP抽象了一组SD的集合供外界的Node访问或者Engine管理,并且一个SP中的所有SD必须是同一类型,如NFS或者iSCSI。

为了保证SP中的数据安全,一组SP中需要选择一个SD作为MasterDomain。这个Domain的不同之处在于它会保存SP中所有的元数据,保存一些异步请求或者任务的数据,保存所在SP的集群存储用到的锁。

为了简化管理,oVirt中抽象出了DataCenter概念,一个DataCenter将拥有一组NodeCluster用来运行虚拟机,一个StoragePool用来保存虚拟磁盘数据。NodeCluster是一组专门用来运行虚拟机的Node的集合,运行在其中的虚拟机可以动态迁移到NodeCluster中的另外一个Node上。一个DataCenter是一个完成oVirt所有功能的实体,在这个DataCenter中用户可以创建虚拟机、备份虚拟机、配置虚拟机的StorageDomain,动态迁移虚拟机。NodeEngine有一些算法在开启的时候可以自动平衡DataCenter中的Node的负载。概括起来一个DataCenter是一个管理NodeCluster与StoragePool的集合。

由于DataCenter中所有的Node都拥有对DataCenter中的StoragePool的访问权限,因此VDSM实现了一个称为SPM(StoragePool Manager) 的功能角色。在一个DataCenter中,所有的Node启动后会自动选举出一个Node充当SPM的角色,被选举者将运行VDSM上的SPM逻辑,负责完成以下功能:创建/删除/缩放所在DataCenter中的Image,快照,模板。这些操作的共同点是会影响StoragePool中的元数据,如SAN上松散块设备的分配。为了保证元数据不被多个Node同时修改,SPM拥有对StoragePool中元数据的排它性操作权限,SPM使用集中式邮箱接受其他Node的相关请求,其他Node只能通过给SPM发送操作请求的方式修改元数据,最终的操作都由SPM线性完成,从而避免了存储器操作竞态的出现。为了兼顾效率,不修改元数据的普通操作,如数据读写,Node可以不同过SPM,自己直接访问StoragePool完成。由于SPM是由一个普通Node选举出来的,因此当它因为外部原因失效后,系统将会选举出另外的Node充当SPM,从而保证系统能继续运行。

4.Ovirt guest agent

Ovirt-guest-agent是一个python编写的运行在虚拟机内部的守护程序(可执行文件为/usr/share/ovirt-guest-agent/ovirt-guest-agent.py),通过宿主机的VDSM为虚拟化管理器(ovirt-engine或rhev-m)提供虚拟机的信息,在虚拟机上通过虚拟串口virtio-serial(默认首选方式)或者isa-serial使用json协议与宿主机上的unixsocket文件进行交互。

Engine将发给guestagent的请求重定向到VDSM,VDSM通过 VirtIO通道使用JSON格式的纯文本命令与guest通信。

VDSM/GuestAgent消息结构

VDSM发送的每条信息都有一个必填字段:’name‘,它包含了指令名。

Alladditional fields in the message are considered arguments to thecommand. Please see the OVirt_Guest_Agent/CommandDefinitions pagefor detailed information about supported commands and theirarguments.

Ovirt-guest-agent通过读写串口设备与宿主机上的socket通道进行交互,宿主机上可以使用普通的unixsocket读写方式对socket文件进行读写,最终实现与Ovirt-guest-agent的交互,交互的协议与qmp(QEMUMonitor Protocol)相同(简单来说就是使用JSON格式进行数据交换),串口设备的速率通常都较低,所以比较适合小数据量的交换。

xml文件中配置方式如下:

Normal package

ifpkg.current_state == INSTALLED_STATE:

detail = (app,pkg.current_ver.ver_str)

apps.add(“%s-%s” % (detail))

virtual package

eliflen(pkg.provides_list) > 0:

for , , pkgin pkg.provides_list:

ifpkg.parent_pkg.current_state == INSTALLED_STATE:

detail= (app, pkg.parent_pkg.current_ver.ver_str)

apps.add(“%s-%s” % (detail))

return apps

由此可见,获取虚拟机中安装的应用程序信息是通过读取配置文件获得的。

5.操作系统、机器名、网络接口等信息:

在OVirtAgentLogic.py的AgentLogicBase类中:

defsendInfo(self):

self._send(‘host-name’, {‘name’: self.dr.getMachineName()})

self._send(‘os-version’, {‘version’:self.dr.getOsVersion()})

self._send(‘network-interfaces’,

{‘interfaces’:self.dr.getAllNetworkInterfaces()})

在GuestAgentLinux2.py的LinuxDataRetriver类中:

defgetMachineName(self):

returnsocket.getfqdn()

由此可见,获取虚拟机名是通过python的socket.getfqdn()函数获得的。

defgetOsVersion(self):

returnos.uname()[2]

由此可见,获取操作系统版本是通过python的os.uname()[2]函数获得的。

defgetAllNetworkInterfaces(self):

returnself.list_nics()

self.list_nics=nicmgr.list_nics

在GuestAgentLinux2.py的NicMgr类中:

self.list_nics=self.ethtool_list_nics

defethtool_list_nics(self):

interfaces = list()

try:

for dev in self.ethtool.get_devices():

flags = self.ethtool.get_flags(dev)

if flags & self.ethtool.IFF_UP and \

not(flags &self.ethtool.IFF_LOOPBACK):

devinfo =self.ethtool.get_interfaces_info(dev)[0]

interfaces.append(

{‘name’: dev,

‘inet’:self._get_ipv4_addresses(devinfo),

‘inet6’:self._get_ipv6_addresses(devinfo),

‘hw’: self.ethtool.get_hwaddr(dev)})

except:

logging.exception(“Error retrieving networkinterfaces.”)

return interfaces

在GuestAgentLinux2.py的NicMgr类中

def ethtool_list_nics(self):

interfaces = list()

try:

fordev in self.ethtool.get_devices():

flags = self.ethtool.get_flags(dev)

if flags & self.ethtool.IFF_UP and \

not(flags &self.ethtool.IFF_LOOPBACK):

devinfo =self.ethtool.get_interfaces_info(dev)[0]

interfaces.append(

{‘name’: dev,

‘inet’:self._get_ipv4_addresses(devinfo),

‘inet6’:self._get_ipv6_addresses(devinfo),

‘hw’: self.ethtool.get_hwaddr(dev)})

except:

logging.exception(“Error retrieving networkinterfaces.”)

return interfaces

def_get_ipv4_addresses(self, dev):

if hasattr(dev, ‘get_ipv4_addresses’):

ipv4_addrs = []

for ip in dev.get_ipv4_addresses():

ipv4_addrs.append(ip.address)

return ipv4_addrs

if dev.ipv4_address is not None:

return [dev.ipv4_address]

else:

return []

def _get_ipv6_addresses(self, dev):

ipv6_addrs = []

for ip in dev.get_ipv6_addresses():

ipv6_addrs.append(ip.address)

return ipv6_addrs

由此可见,获取ipv4地址、ipv6地址和网卡地址是通过python的ethtool获得的。

6.FQDN:(FullyQualified Domain Name)完全合格域名/全称域名:

在OVirtAgentLogic.py的AgentLogicBase类中:

defsendFQDN(self):

self._send(‘fqdn’, {‘fqdn’: self.dr.getFQDN()})

在OVirtAgentLogic.py的DataRetriverBase类中:

def getFQDN(self):

returnsocket.getfqdn()

由此可见,获取全称域名是通过python的socket包的getfqdn函数获得的。

7.DisksUsage

在OVirtAgentLogic.py的AgentLogicBase类中:

defsendDisksUsages(self):

self._send(‘disks-usage’, {‘disks’:self.dr.getDisksUsage(),

‘mapping’: self.dr.getDiskMapping()})

在GuestAgentLinux2.py的LinuxDataRetriver类中:

defgetDisksUsage(self):

usages = list()

try:

mounts =open(‘/proc/mounts’,’r’)

for mount in mounts:

try:

(device, path, fs) = mount.split()[:3]

if fs not in self.ignored_fs:

path might include spaces.

path = path.decode(“string-escape”)

statvfs = os.statvfs(path)

total = statvfs.f_bsize *statvfs.f_blocks

used = total - statvfs.f_bsize *statvfs.f_bfree

if self.ignore_zero_size_fs and used ==total == 0:

continue

usages.append({‘path’: path, ‘fs’: fs,’total’: total,

‘used’: used})

except:

logging.exception(“Error retrieving disksusages.”)

mounts.close()

except Exception:

logging.exception(“Error during reading mounteddevices”)

if mounts:

mounts.close()

return usages

由此可知获取虚拟机中的磁盘使用信息是通过读取虚拟机中/proc/mounts文件获得的。

def getDiskMapping(self):

CMD = ‘/usr/share/ovirt-guest-agent/diskmapper’

mapping = {}

for line in _readLinesFromProcess([CMD]):

try:

name, serial = line.split(‘|’, 1)

except ValueError:

logging.exception(“diskmapper tool used aninvalid format”)

return {}

mapping[serial] = {‘name’: name}

return mapping

由此可知获取虚拟机中的DiskMapping信息是通过执行diskmapper文件获得的。

8.NumberOfCPUs:

在OVirtAgentLogic.py的AgentLogicBase类中:

defsendNumberOfCPUs(self):

self._send(‘number-of-cpus’, {‘count’:self.dr.getNumberOfCPUs()})

defgetNumberOfCPUs(self):

try:

returnmultiproc.cpu_count()

except NotImplementedError:

return -1

又有:

multiproc= None

try:

importmultiprocessing

multiproc = multiprocessing

except ImportError:

class MultiProcessingFake:

def cpu_count(self):

return -1

multiproc = MultiProcessingFake()

由此可知获取虚拟机中的NumberOfCPUs信息是通过multiprocessing包中的cpu_count()函数获得的。

六.Vdsm代码分析

1.init启动脚本:

init启动脚本运行/usr/share/vdsm/vdsm后台进程,参数如下:

/bin/bash -e/usr/share/vdsm/respawn –minlifetime 10 –daemon –masterpid/var/run/vdsm/respawn.pid /usr/share/vdsm/vdsm –pidfile/var/run/vdsm/vdsmd.pid

在vdsm中:run()-> serve_clients(log)

在serve_clients(log)函数中首先通过libvirtconnection.start_event_loop()来registerthe handleimplementations withlibvirt,libvirtconnection在/usr/lib/python2.6/site-package/vdsm/libvirtconnection.py中:

defstart_event_loop():

__event_loop.stop()

接着在serve_clients(log)中运行主程序:

from clientIFimport clientIF # must import after config is read

cif =clientIF.getInstance(irs, log)

cif.start()

在clientIF.py的clientIF类中启动守护程序:

defstart(self):

forbinding in self.bindings.values():

binding.start()

self.thread = threading.Thread(target=self._acceptor.serve_forever,

name=’Detector thread’)

self.thread.setDaemon(True)

self.thread.start()

其中,bindings来自clientIF.py的clientIF类中:

(1)def_prepareXMLRPCBinding(self, port):

ifconfig.getboolean(‘vars’, ‘xmlrpc_enable’):

try:

from rpc.BindingXMLRPC import BindingXMLRPC

from rpc.BindingXMLRPC import XmlDetector

exceptImportError:

self.log.error(‘Unable to load the xmlrpc server module. ‘

‘Please make sure it is installed.’)

else:

xml_binding = BindingXMLRPC(self, self.log,port)

self.bindings[‘xmlrpc’] = xml_binding

xml_detector = XmlDetector(xml_binding)

self._acceptor.add_detector(xml_detector)

在BindingXMLRPC.py的BindingXMLRPC类中:

def init(self,cif, log, port):

self.cif= cif

self.log= log

self.serverPort = port

self._enabled = False

self.server = self._createXMLRPCServer()

BindingXMLRPC.py里面的代码,这个脚本基本上是其他脚本的接口,ovirt-engine通过这里提供的XMLRPC进行和ovirt-node的交互。

(2)def_prepareJSONRPCBinding(self):

ifconfig.getboolean(‘vars’, ‘jsonrpc_enable’):

try:

from rpc import Bridge

from rpc.BindingJsonRpc import BindingJsonRpc

from yajsonrpc.stompReactor import StompDetector

exceptImportError:

self.log.warn(‘Unable to load the json rpc server module. ‘

‘Please make sure it is installed.’)

else:

bridge = Bridge.DynamicBridge()

json_binding = BindingJsonRpc(bridge)

self.bindings[‘jsonrpc’] = json_binding

stomp_detector = StompDetector(json_binding)

self._acceptor.add_detector(stomp_detector)

在BindingJsonRPC.py的BindingJsonRPC类中:

def init(self,bridge):

self._server = JsonRpcServer(bridge,_simpleThreadFactory)

self._reactors = []

BindingJSONRPC.py里面的代码,这个脚本基本上是其他脚本的接口,ovirt-engine通过这里提供的JSONRPC进行和ovirt-node的交互。

Ovirt调研

Ovirt介绍

Ovirt(openVirtualization)是一个基于KVM的开源IaaS项目,是redhat虚拟化管理平台RHEV的开源版本,其前身是Redhat的虚拟化商业产品。在架构设计上它使用了Node/Engine分离结构,以方便功能的划分与管理。

图1.oVirt逻辑结构图

图1从总体框架上说明了oVirt的工作流程。对外,oVirt中的Engine通过HTTP协议向外提供HTTPAPI,同时提供内建的网页服务供用户和系统管理员使用。系统管理员通过网页可以创建、修改虚拟机及相关设备或用户权限,用户在拥有权限的情况下可以操作自己的虚拟机,并通过VNC或SSH登陆自己的虚拟机。Engine在整个系统中充当管理者的角色并对外提供管理服务,它挂载了自己的数据库记录整个系统中所有的虚拟机配置,各个节点的自身状态,系统的网络状态,存储器状态。管理的逻辑,状态及策略全部在Engine中设置与实现。Node只负责功能上的实现,不进行任何状态的记录和任何策略的实现。Engine与Node之间的关系十分像Linux中驱动程序与应用程序的功能分割关系:驱动仅仅负责功能的实现,如设备的读、写、开启与关闭,如何使用这些功能留给应用层。同样Node仅仅负责实现虚拟机器与设备的创建与修改,资源的共享与保护,如何使用这些功能交给Engine处理。Node暴露两种基于网络的API与Engine交互,XMLRPC与REST。Engine通过这些接口控制各个Node上功能的启动。当然用户也可以调用这些API进行第三方程序的开发。oVirt里的Node可以由一个普通的Linux上安装VDSM(VirtualDesktop Server Manager) 构成,也可以由一个专为oVirt定制的Linux系统构成。在定制的情况下,Node上的许多文件系统都是ramdisk(基于内存的Linux磁盘设备),系统重启后其中的内容消失,从而保证了Node的无状态性。Engine/Node的设计不仅方便将来的开发,更简化了用户的安装使用,在定制的情况下Node可以快速大量部署。

图2.Node上运行的主要组件

简单概括起来,VDSM的功能主要有:负责Node的自动启动与注册;虚拟机的操作与生命周期管理;网络管理;存储管理;Host与VM状态监视与报告;提供对虚拟机的外部干涉功能;提供内存与存储的合并与超支功能(OverCommitment)。

Ovirt体系架构

一个标准的oVirt部署包括三部分,主要有:

1.ovirt-engine,这个被用于部署,监管,移除,停止和创建VM镜像,配置存储,网络,等等。

2.一个或多个ovirt-node,我们在node上运行虚拟机(VMs)。

3.一个或多个存储节点,其上保存着对应虚拟机的磁盘镜像和ISO镜像。

Ovirt-node上安装了VDSM和libvirt以及安装了一些额外的包,这些包可以实现网络和其他系统服务的虚拟化。

存储节点可以使用块和文件存储,并且可以通过NFS在本地或者远程进行访问。通过POSIXFS存储类型是可以支持Gluster这样的存储技术的。存储节点被分在一个存储池中,这样可以保证高可用和冗余。
2.1 Ovirt Engine

oVirtengine是一个基于JBoss的java应用程序(由C#演变过来),它作为web服务来运行。这个服务直接指挥node上的VDSM去部署,启动,停止,迁移和监控虚拟机,以及从模板中创建新的镜像至存储中。

engine提供的一些功能:

虚拟机生命周期管理通过LDAP(微软的AD或IPA)进行身份验证网络管理-增加本地网络并且将其附加至主机上。存储管理-管理存储域(NFS/iSCSI/Local)和虚拟VM磁盘。高可用-在其它主机中失败的主机上自动重启客户虚拟机在线迁移-在零停机的情况下移动主机之间的虚拟机。系统调度-基于资源使用/政策持续负载均衡虚拟机。节能优化-在非高峰时期将续集你集中到较少的服务器上。维护管理器-在计划的维护期间,无需为虚拟机停机。镜像管理-基于模板管理,自动精简配置和快照。监控-为系统中的所有对象-虚拟客户机,主机,网络,存储等等。导出/导入-使用OVF文件导入和导出虚拟机和模板。V2V-可以将VMware或RHEL或Xen环境下的虚拟机轻松转换到oVirt环境中

图3展示了OvirtEngine组件的不同层次:

图3.OvirtEngine组件层次
2.2 EngineCore

下面的图形显示了Engine-core的不同组件

enginecore中的主要组件有:

-负责所有与之相关的DB操作。VDS Broker -负责所有需要与VDSM交互的操作。LDAP Broker -负责验证和抓取LDAP目录下用户/组的属性(当前支持AD,IPA和RHDS)Backend Bean-一个Singletonbean ,负责运行不同实体的指令,查询和监控。

2.3VDSM

VDSM是一个以Python开发的组件,它可以为主机,虚拟机,网络和存储管理提供所需要的所有功能。

VDSMAPI是基于XML-RPC的(计划转移到RESTAPI)。这就是ovirt-engine如何与VDSM进行通信的。配置主机,网络和共享的存储。使用libvirt,进行虚拟机的生命周期操作多线程,多进程通过virtio-serial和客户代理对话。添加自定义的对扩展到数以百计的node的LVM的集群支持在所支持的存储类型(本地目录,FCP, FCoE, iSCSI, NFS, SAS)的基础上实现一个分布式镜像库。多主机系统,一个并发的元数据写入器。在数据写入器中进行扩展。

2.4Hooks机制

允许管理员定义修改虚拟机操作的脚本,例如,添加额外的操作像CPU定位,watchdog设备,直接访问LUN允许在集成完成前为新的KVM特性扩展oVirt。有一种简单的方式来测试新的kvm/libvirt/linux特性。在VDSM利用libvirt开始启动虚拟机之前调用hook机制。hook改变了虚拟机定义,并且VDSM通过定义至libvirt以开启虚拟机。

下图阐释了虚拟机生命周期中Hook机制。

2.5 MoM集成

VDSM是和MoM集成在一起的。MOM的行为配置了策略。随着这些策略的使用者可以微调主机的高内存过量使用或安全操作。为了控制它的mom实例,vdsm确实传递了一个mom配置文件并且mom策略文件可以设置mom的缺省行为。刚开始,vdsmd利用配置和策略文件导入了mom并将其初始化。从那时起,mom通过API.py中定义好的API与vdsm进行交互并且控制了运行在主机上的每个虚拟机的内存。mom实例作为vdsm守护进程内的线程运行。

VDSM对存储器的管理

存储器的管理是oVirt的重点,本节将介绍oVirt是如何组织与分配各种存储器的,用户应该如何使用它。每一个Node上都会运行一个VDSM,实现网络、存储器、虚拟机的创建与修改的功能。VDSM的大部分代码用在了存储系统上,其功能包括数据的组织,集群下的数据共享与保护,故障恢复。通常情况下每一个物理机器当作一个Node,运行一个VDSM,Node本身只携带少量存储器用以保存配置。一个集群中通常有一个Engine和数个Node,这些Node通过网络连接到SAN(StorageArea Network) 上,VDSM把Node上运行的虚拟机存储数据保存在SAN上,Node本身为无状态的节点,重新启动后状态消失,从而保证了系统整体的可用性,一般情况下不会因用户的操作而使Node失效。一旦问题发生,通常一次重启即可恢复工作状态。

在云计算环境中,SAN中往往存储着大量虚拟机器使用的virtualimage,同时每一个virtualimage在任何时候都可能被任意Node访问,同时出于性能的考虑virtualimage可能以文件或者数据块的形式出现,这些对存储系统的设计提出了挑战。

为此,VDSM基于以下原则设计了自己的存储系统:

1高可用性:一群安装有VDSM的Node在组建集群的时候,没有潜在的单点故障存在,任何一个Node崩溃不会影响整个集群的功能,它的角色会被其他Node取代。Engine不可用的情况下,Node将继续工作,用户对虚拟机的操作可以继续进行。

2高伸缩性:添加Node和SAN几乎不需要用户的设置,Node上的VDSM会自己注册自己。

3集群安全性:一个VDSM对正在操作的virtualimage进行排它性保护。

4备份与恢复:virtualimage之间有相互关连的特性记录可进行一系列引用/备份操作。

5性能优化:利用多线程与多进程减少操作堵塞状况。

StorageDomain(以下简程SD)是VDSM中的最基本存储实体,所有的virtualimage和virtualimage对应的元数据都会保存在其中。和VDSM中的StorageImage概念不同,这里的virtualimage表示的是虚拟机程序用到的虚拟磁盘数据,特指虚拟机程序最终能够操作的文件或设备文件对象。元数据是描述virtualimage相关数据大小、状态、锁等内容的一组数据集合。SD包括两种类型:FileDomain和BlockDomain。FileDomain使用文件系统存储数据并同步操作,主要针对NFS(NetworkFile System) 和LOCALFS(LocalFile System) 文件系统。在文件系统的帮助下,FileDomain拥有良好的virtualimage操作能力,每一个虚拟机的存储数据(称为Volume)和对应的元数据都以文件的方式保存。每一个Domain实际对应于Host文件系统上的一个目录,针对NFS文件系统VDSM还有额外的逻辑来处理相关意外与错误情况。而BlockDomain直接操作原始的块数据,使用Linux的LVM(LogicalVolume Manager) 功能来组织数据,主要针对iSCSI(InternetSmall Computer System Interface),FCoE(FibreChannel over Ethernet ) 等块设备。由于目标设备上通常没有一个文件系统来保证访问的安全性,VDSM使用了邮箱机制来保证任意时刻,只有一个Node可以修改Block上的内容,而其他Node则通过Socket邮箱发送自己的修改请求。因此它的操作请求速度和监视功能都会比FileDomain弱一些。通常设备将使用Linux的devicemapper机制进行一次映射,每一个Domain实际上是一个Linux中的VolumeGroup,元数据保存在其中的一个LogicVolume及其tag上,虚拟机的Volume保存在另一个LogicVolume中。

StoragePool(以下简称SP)是一组SD的组合,目标是管理跨越SD之间的操作,也就是说SD之间互相的引用、备份、恢复,合并一般发生在一个SP之中。在数据中心里,一个SP抽象了一组SD的集合供外界的Node访问或者Engine管理,并且一个SP中的所有SD必须是同一类型,如NFS或者iSCSI。

为了保证SP中的数据安全,一组SP中需要选择一个SD作为MasterDomain。这个Domain的不同之处在于它会保存SP中所有的元数据,保存一些异步请求或者任务的数据,保存所在SP的集群存储用到的锁。

为了简化管理,oVirt中抽象出了DataCenter概念,一个DataCenter将拥有一组NodeCluster用来运行虚拟机,一个StoragePool用来保存虚拟磁盘数据。NodeCluster是一组专门用来运行虚拟机的Node的集合,运行在其中的虚拟机可以动态迁移到NodeCluster中的另外一个Node上。一个DataCenter是一个完成oVirt所有功能的实体,在这个DataCenter中用户可以创建虚拟机、备份虚拟机、配置虚拟机的StorageDomain,动态迁移虚拟机。NodeEngine有一些算法在开启的时候可以自动平衡DataCenter中的Node的负载。概括起来一个DataCenter是一个管理NodeCluster与StoragePool的集合。

由于DataCenter中所有的Node都拥有对DataCenter中的StoragePool的访问权限,因此VDSM实现了一个称为SPM(StoragePool Manager) 的功能角色。在一个DataCenter中,所有的Node启动后会自动选举出一个Node充当SPM的角色,被选举者将运行VDSM上的SPM逻辑,负责完成以下功能:创建/删除/缩放所在DataCenter中的Image,快照,模板。这些操作的共同点是会影响StoragePool中的元数据,如SAN上松散块设备的分配。为了保证元数据不被多个Node同时修改,SPM拥有对StoragePool中元数据的排它性操作权限,SPM使用集中式邮箱接受其他Node的相关请求,其他Node只能通过给SPM发送操作请求的方式修改元数据,最终的操作都由SPM线性完成,从而避免了存储器操作竞态的出现。为了兼顾效率,不修改元数据的普通操作,如数据读写,Node可以不同过SPM,自己直接访问StoragePool完成。由于SPM是由一个普通Node选举出来的,因此当它因为外部原因失效后,系统将会选举出另外的Node充当SPM,从而保证系统能继续运行。

4.Ovirt guest agent

Ovirt-guest-agent是一个python编写的运行在虚拟机内部的守护程序(可执行文件为/usr/share/ovirt-guest-agent/ovirt-guest-agent.py),通过宿主机的VDSM为虚拟化管理器(ovirt-engine或rhev-m)提供虚拟机的信息,在虚拟机上通过虚拟串口virtio-serial(默认首选方式)或者isa-serial使用json协议与宿主机上的unixsocket文件进行交互。

Engine将发给guestagent的请求重定向到VDSM,VDSM通过 VirtIO通道使用JSON格式的纯文本命令与guest通信。

VDSM/GuestAgent消息结构

VDSM发送的每条信息都有一个必填字段:’name‘,它包含了指令名。

Alladditional fields in the message are considered arguments to thecommand. Please see the OVirt_Guest_Agent/CommandDefinitions pagefor detailed information about supported commands and theirarguments.

Ovirt-guest-agent通过读写串口设备与宿主机上的socket通道进行交互,宿主机上可以使用普通的unixsocket读写方式对socket文件进行读写,最终实现与Ovirt-guest-agent的交互,交互的协议与qmp(QEMUMonitor Protocol)相同(简单来说就是使用JSON格式进行数据交换),串口设备的速率通常都较低,所以比较适合小数据量的交换。

xml文件中配置方式如下:

Normal package

ifpkg.current_state == INSTALLED_STATE:

detail = (app,pkg.current_ver.ver_str)

apps.add(“%s-%s” % (detail))

virtual package

eliflen(pkg.provides_list) > 0:

for , , pkgin pkg.provides_list:

ifpkg.parent_pkg.current_state == INSTALLED_STATE:

detail= (app, pkg.parent_pkg.current_ver.ver_str)

apps.add(“%s-%s” % (detail))

return apps

由此可见,获取虚拟机中安装的应用程序信息是通过读取配置文件获得的。

5.操作系统、机器名、网络接口等信息:

在OVirtAgentLogic.py的AgentLogicBase类中:

defsendInfo(self):

self._send(‘host-name’, {‘name’: self.dr.getMachineName()})

self._send(‘os-version’, {‘version’:self.dr.getOsVersion()})

self._send(‘network-interfaces’,

{‘interfaces’:self.dr.getAllNetworkInterfaces()})

在GuestAgentLinux2.py的LinuxDataRetriver类中:

defgetMachineName(self):

returnsocket.getfqdn()

由此可见,获取虚拟机名是通过python的socket.getfqdn()函数获得的。

defgetOsVersion(self):

returnos.uname()[2]

由此可见,获取操作系统版本是通过python的os.uname()[2]函数获得的。

defgetAllNetworkInterfaces(self):

returnself.list_nics()

self.list_nics=nicmgr.list_nics

在GuestAgentLinux2.py的NicMgr类中:

self.list_nics=self.ethtool_list_nics

defethtool_list_nics(self):

interfaces = list()

try:

for dev in self.ethtool.get_devices():

flags = self.ethtool.get_flags(dev)

if flags & self.ethtool.IFF_UP and \

not(flags &self.ethtool.IFF_LOOPBACK):

devinfo =self.ethtool.get_interfaces_info(dev)[0]

interfaces.append(

{‘name’: dev,

‘inet’:self._get_ipv4_addresses(devinfo),

‘inet6’:self._get_ipv6_addresses(devinfo),

‘hw’: self.ethtool.get_hwaddr(dev)})

except:

logging.exception(“Error retrieving networkinterfaces.”)

return interfaces

在GuestAgentLinux2.py的NicMgr类中

def ethtool_list_nics(self):

interfaces = list()

try:

fordev in self.ethtool.get_devices():

flags = self.ethtool.get_flags(dev)

if flags & self.ethtool.IFF_UP and \

not(flags &self.ethtool.IFF_LOOPBACK):

devinfo =self.ethtool.get_interfaces_info(dev)[0]

interfaces.append(

{‘name’: dev,

‘inet’:self._get_ipv4_addresses(devinfo),

‘inet6’:self._get_ipv6_addresses(devinfo),

‘hw’: self.ethtool.get_hwaddr(dev)})

except:

logging.exception(“Error retrieving networkinterfaces.”)

return interfaces

def_get_ipv4_addresses(self, dev):

if hasattr(dev, ‘get_ipv4_addresses’):

ipv4_addrs = []

for ip in dev.get_ipv4_addresses():

ipv4_addrs.append(ip.address)

return ipv4_addrs

if dev.ipv4_address is not None:

return [dev.ipv4_address]

else:

return []

def _get_ipv6_addresses(self, dev):

ipv6_addrs = []

for ip in dev.get_ipv6_addresses():

ipv6_addrs.append(ip.address)

return ipv6_addrs

由此可见,获取ipv4地址、ipv6地址和网卡地址是通过python的ethtool获得的。

6.FQDN:(FullyQualified Domain Name)完全合格域名/全称域名:

在OVirtAgentLogic.py的AgentLogicBase类中:

defsendFQDN(self):

self._send(‘fqdn’, {‘fqdn’: self.dr.getFQDN()})

在OVirtAgentLogic.py的DataRetriverBase类中:

def getFQDN(self):

returnsocket.getfqdn()

由此可见,获取全称域名是通过python的socket包的getfqdn函数获得的。

7.DisksUsage

在OVirtAgentLogic.py的AgentLogicBase类中:

defsendDisksUsages(self):

self._send(‘disks-usage’, {‘disks’:self.dr.getDisksUsage(),

‘mapping’: self.dr.getDiskMapping()})

在GuestAgentLinux2.py的LinuxDataRetriver类中:

defgetDisksUsage(self):

usages = list()

try:

mounts =open(‘/proc/mounts’,’r’)

for mount in mounts:

try:

(device, path, fs) = mount.split()[:3]

if fs not in self.ignored_fs:

path might include spaces.

path = path.decode(“string-escape”)

statvfs = os.statvfs(path)

total = statvfs.f_bsize *statvfs.f_blocks

used = total - statvfs.f_bsize *statvfs.f_bfree

if self.ignore_zero_size_fs and used ==total == 0:

continue

usages.append({‘path’: path, ‘fs’: fs,’total’: total,

‘used’: used})

except:

logging.exception(“Error retrieving disksusages.”)

mounts.close()

except Exception:

logging.exception(“Error during reading mounteddevices”)

if mounts:

mounts.close()

return usages

由此可知获取虚拟机中的磁盘使用信息是通过读取虚拟机中/proc/mounts文件获得的。

def getDiskMapping(self):

CMD = ‘/usr/share/ovirt-guest-agent/diskmapper’

mapping = {}

for line in _readLinesFromProcess([CMD]):

try:

name, serial = line.split(‘|’, 1)

except ValueError:

logging.exception(“diskmapper tool used aninvalid format”)

return {}

mapping[serial] = {‘name’: name}

return mapping

由此可知获取虚拟机中的DiskMapping信息是通过执行diskmapper文件获得的。

8.NumberOfCPUs:

在OVirtAgentLogic.py的AgentLogicBase类中:

defsendNumberOfCPUs(self):

self._send(‘number-of-cpus’, {‘count’:self.dr.getNumberOfCPUs()})

defgetNumberOfCPUs(self):

try:

returnmultiproc.cpu_count()

except NotImplementedError:

return -1

又有:

multiproc= None

try:

importmultiprocessing

multiproc = multiprocessing

except ImportError:

class MultiProcessingFake:

def cpu_count(self):

return -1

multiproc = MultiProcessingFake()

由此可知获取虚拟机中的NumberOfCPUs信息是通过multiprocessing包中的cpu_count()函数获得的。

六.Vdsm代码分析

1.init启动脚本:

init启动脚本运行/usr/share/vdsm/vdsm后台进程,参数如下:

/bin/bash -e/usr/share/vdsm/respawn –minlifetime 10 –daemon –masterpid/var/run/vdsm/respawn.pid /usr/share/vdsm/vdsm –pidfile/var/run/vdsm/vdsmd.pid

在vdsm中:run()-> serve_clients(log)

在serve_clients(log)函数中首先通过libvirtconnection.start_event_loop()来registerthe handleimplementations withlibvirt,libvirtconnection在/usr/lib/python2.6/site-package/vdsm/libvirtconnection.py中:

defstart_event_loop():

__event_loop.stop()

接着在serve_clients(log)中运行主程序:

from clientIFimport clientIF # must import after config is read

cif =clientIF.getInstance(irs, log)

cif.start()

在clientIF.py的clientIF类中启动守护程序:

defstart(self):

forbinding in self.bindings.values():

binding.start()

self.thread = threading.Thread(target=self._acceptor.serve_forever,

name=’Detector thread’)

self.thread.setDaemon(True)

self.thread.start()

其中,bindings来自clientIF.py的clientIF类中:

(1)def_prepareXMLRPCBinding(self, port):

ifconfig.getboolean(‘vars’, ‘xmlrpc_enable’):

try:

from rpc.BindingXMLRPC import BindingXMLRPC

from rpc.BindingXMLRPC import XmlDetector

exceptImportError:

self.log.error(‘Unable to load the xmlrpc server module. ‘

‘Please make sure it is installed.’)

else:

xml_binding = BindingXMLRPC(self, self.log,port)

self.bindings[‘xmlrpc’] = xml_binding

xml_detector = XmlDetector(xml_binding)

self._acceptor.add_detector(xml_detector)

在BindingXMLRPC.py的BindingXMLRPC类中:

def init(self,cif, log, port):

self.cif= cif

self.log= log

self.serverPort = port

self._enabled = False

self.server = self._createXMLRPCServer()

BindingXMLRPC.py里面的代码,这个脚本基本上是其他脚本的接口,ovirt-engine通过这里提供的XMLRPC进行和ovirt-node的交互。

(2)def_prepareJSONRPCBinding(self):

ifconfig.getboolean(‘vars’, ‘jsonrpc_enable’):

try:

from rpc import Bridge

from rpc.BindingJsonRpc import BindingJsonRpc

from yajsonrpc.stompReactor import StompDetector

exceptImportError:

self.log.warn(‘Unable to load the json rpc server module. ‘

‘Please make sure it is installed.’)

else:

bridge = Bridge.DynamicBridge()

json_binding = BindingJsonRpc(bridge)

self.bindings[‘jsonrpc’] = json_binding

stomp_detector = StompDetector(json_binding)

self._acceptor.add_detector(stomp_detector)

在BindingJsonRPC.py的BindingJsonRPC类中:

def init(self,bridge):

self._server = JsonRpcServer(bridge,_simpleThreadFactory)

self._reactors = []

BindingJSONRPC.py里面的代码,这个脚本基本上是其他脚本的接口,ovirt-engine通过这里提供的JSONRPC进行和ovirt-node的交互。

有关 ovirt 的分析相关推荐

  1. 【Ovirt 笔记】JBoss modules 配置分析与整理

    文前说明 作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅. 本文仅供学习交流使用,侵权必删. 不用于商业目的,转载请注 ...

  2. Linux虚拟化KVM-Qemu分析(十一)之virtqueue

    目录 1. 概述 2. 数据结构 3. 流程分析 3.1 发送 3.2 接收 3.3 代码分析 3.3.1 virtqueue创建 3.3.2 virtio-net驱动发送 3.3.3 Qemu vi ...

  3. Linux虚拟化KVM-Qemu分析(十)之virtio驱动

    目录 1. 概述 2. 数据结构 3. 流程分析 3.1 virtio总线创建 3.2 virtio驱动调用流程 参考 <Linux PCI驱动框架分析:(Peripheral Componen ...

  4. Linux虚拟化KVM-Qemu分析(九)之virtio设备

    目录 1. 概述 2. 流程分析 3. tap创建 - 网卡后端设备 4. virtio-net创建 4.1 数据结构 4.2 流程分析 4.2.1 class_init 4.2.2 instance ...

  5. Linux虚拟化KVM-Qemu分析(八)之virtio初探

    目录 概述 1. 网卡 1.1 网卡工作原理 1.2 Linux网卡驱动 2. 网卡全虚拟化 2.1 全虚拟化方案 2.2 弊端 3. 网卡半虚拟化 3.1 virtio 3.2 半虚拟化方案 参考 ...

  6. Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

    <Linux虚拟化KVM-Qemu分析(一)> <Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化> <Linux虚拟化KVM-Qemu分析(三)之KVM源码 ...

  7. Linux虚拟化KVM-Qemu分析(六)之中断虚拟化

    <Linux虚拟化KVM-Qemu分析(一)> <Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化> <Linux虚拟化KVM-Qemu分析(三)之KVM源码 ...

  8. Linux虚拟化KVM-Qemu分析(五)之内存虚拟化

    <Linux虚拟化KVM-Qemu分析(一)> <Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化> <Linux虚拟化KVM-Qemu分析(三)之KVM源码 ...

  9. Linux虚拟化KVM-Qemu分析(四)之CPU虚拟化(2)

    Table of Contents 1. 概述 1.1 CPU工作原理 1.2 guest模式 2. 流程分析 2.1 vcpu的创建 2.1.1 qemu中vcpu创建 2.1.2 kvm中vcpu ...

最新文章

  1. hadoop 9000端口的服务未启动_IDEA 微服务单项目多端口启动
  2. 尝试.Net Core—使用.Net Core + Entity FrameWork Core构建WebAPI(一)
  3. mysql5.5 datetime默认值不能为NOW或者CURRENT_TIMESTAMP
  4. JVM实用参数(八)GC日志
  5. 成功,要“借力”,不要“尽力”(深刻!)
  6. 解决:Unable to identify index name. XXXModel is not a Document. Make sure the document class is annota
  7. 一些看起来简单做起来难的程序员笔试面试题集锦
  8. 限制对web路径的访问
  9. KLite 移植说明 V1.0
  10. 【codecombat】 试玩全攻略 第十六关 近战
  11. Pr 2021快速入门教程,素材的导入与管理
  12. Volley(五)—— 自定义Request
  13. mmd动作:Bad End Night
  14. JAVA数据结构之Map和Set
  15. Windows10家庭版转为专业版
  16. 浅谈AWD攻防赛的生存攻略
  17. 区级医院计算机专业职称评审,医院职称晋升程序以及医生各级职称评审要求
  18. 大盘点 | 十分钟,看尽加密货币十年之现状
  19. 关于报错django.core.exceptions.ImproperlyConfigured: mysqlclient 1.3.13 or newer is required; you hav
  20. 音视频篇 - 音视频基础概念

热门文章

  1. 正方验证码智能识别及教务系统模拟登录
  2. 使用python的PIL.Image在图片上写字
  3. facebook分享 whatsapp分享 点击按钮复制链接 常用js分享内容
  4. js实现数字时钟,按钮实现暂停开始
  5. 自定义一个虚拟机+将自定义的虚拟机创建成模板机+快速创建虚拟机
  6. MATLAB imagesc中将nan、inf或者特定值设为白色(或透明色)
  7. 完美兼容IE,chrome,ff的设为首页、加入收藏及保存到桌面js代码
  8. 云祺与南非最大移动支付公司iVeri携手合作
  9. 成都拓嘉启远:拼多多上产品清单的条件
  10. 刷脸免单打折领红包带动消费者进店二次消费