OpenStack Cinder源代码流程简析
版权声明:本博客欢迎转载,转载时请以超链接形式标明文章原始出处!谢谢!
博客地址:http://blog.csdn.net/i_chips
一、概况
OpenStack的各个模块都有对应的client模块实现,其作用是为用户訪问详细模块提供了接口,同一时候也作为模块之间相互訪问的途径。
对应的,OpenStack的Cinder模块分为两个组件:cinderclient和cinder(本文是在H版的基础上分析的),其源代码分别參见例如以下:
https://github.com/openstack/python-cinderclient
https://github.com/openstack/cinder
在OpenStack环境中,使用cinder create命令创建volume等,cinderclient会对命令行參数进行解析和封装,处理完毕后发送给cinder,cinder收到请求后,会解析、运行请求并响应创建消息。
以下来依据源代码详细分析。
二、cinder服务启动流程
1. cinder-api服务启动流程
① 脚本bin.cinder-api(读取配置文件,获取cinder路径,导入必须的模块,调用service.WSGIService('osapi_volume'))
② cinder.service:WSGIService.__init__(使用osapi_volume初始化了一个WSGIService服务;调用wsgi.Server)
③ cinder.wsgi:Server.__init__(wsgi.Server类的对象初始化操作,创建了一个GreenPool协程池,但并不启动WSGI服务)
④ 脚本bin.cinder-api(调用service.serve(server))
⑤ cinder.service:serve(初始化Launcher类,然后调用_launcher.launch_server)
⑥ cinder.service:Launcher.launch_server(调用eventlet.spawn,建立一个新的绿色线程用来执行变量func所指定的方法;调用self.run_server)
⑦ cinder.service:Launcher.run_server(先调用server.start,再调用server.wait)
⑧ 启动cinder-api服务
a) cinder.service:WSGIService.start(调用self.server.start())
b) cinder.wsgi:Server.start(spawn了一个协程,在self._start中又调用eventlet.wsgi.server,启动了'osapi_volume'所指定的堵塞式eventlet WSGI服务,也就是说,cinder-api服务启动成功了)
⑨ 等待cinder-api服务结束
a) cinder.service:WSGIService.wait(等待WSGIService服务结束;调用self.server.wait)
b) cinder.wsgi:Server.wait(调用self._server.wait())
c) cinder.service:wait(调用_launcher.wait())
d) cinder.service:Launcher.wait(等待cinder-api协程结束,然后关闭RPC协议框架)
2. cinder-scheduler服务启动流程
① 脚本bin.cinder-scheduler(读取配置文件,获取cinder路径,导入必须的模块,调用service.Service.create(binary='cinder-scheduler'))
② cinder.service:Service.create(进行了一系列变量的初始化操作,然后对类进行初始化)
③ 脚本bin.cinder-api(调用service.serve(server))
④ cinder.service:serve(初始化Launcher类,然后调用_launcher.launch_server)
⑤ cinder.service:Launcher.launch_server(调用eventlet.spawn,建立一个新的绿色线程用来执行变量func所指定的方法;调用self.run_server)
⑥ cinder.service:Launcher.run_server(先调用server.start,再调用server.wait)
⑦ 启动cinder-scheduler服务
a) cinder.service:Service.start(创建RPC连接,启动消费者线程,然后等待队列消息。当轮询查询到消息到达后,创建协程处理相关消息)
⑧ 等待cinder-scheduler服务结束
a) cinder.service:Service.wait(等待Service服务结束;调用x.wait())
b) cinder.service:wait(调用_launcher.wait())
d) cinder.service:Launcher.wait(等待cinder-scheduler协程结束,然后关闭RPC协议框架)
3. cinder-volume服务启动流程(cinder-backup服务启动和cinder-volume类似,就不单列了)
① 脚本bin.cinder-volume(导入相关模块,读取配置文件,获取cinder路径,调用service.ProcessLauncher())
② cinder.service:ProcessLauncher.__init__(对ProcessLauncher类进行实例化)
③ 脚本bin.cinder-volume(调用service.Service.create(binary='cinder-volume'))
④ cinder.service:Service.create(进行了一系列变量的初始化操作,然后对类进行初始化)
⑤ 脚本bin.cinder-volume(调用launcher.launch_server(server))
⑥ cinder.service:ProcessLauncher.launch_server(调用self._start_child(wrap))
⑦ cinder.service:ProcessLauncher._start_child(os.fork()会fork一个子进程,子进程创建成功后,就会调用self._child_process(wrap.server))
⑧ cinder.service:ProcessLauncher._child_process(等待信号并关闭pipe,然后初始化Launcher类,再调用launcher.run_server(server))
⑨ cinder.service:Launcher.run_server(先调用server.start,再调用server.wait)
⑩ 启动cinder-volume服务
a) cinder.service:Service.start(创建RPC连接,启动消费者线程,然后等待队列消息。当轮询查询到消息到达后,创建协程处理相关消息)
⑾ 等待cinder-volume服务结束
a) cinder.service:Service.wait(等待Service服务结束;调用x.wait())
b) cinder.service:wait(调用_launcher.wait())
d) cinder.service:Launcher.wait(等待cinder-volume协程结束,然后关闭RPC协议框架)
三、cinderclient部分create流程
1. cinderclient部分create volume流程
① cinderclient.shell:main(调用OpenStackCinderShell().main)
② cinderclient.shell:OpenStackCinderShell.main(对命令行參数进行解析等一系列操作;方法main的最后一句args.func(self.cs, args)解析之后为do_create)
③ cinderclient.v1.shell:do_create或cinderclient.v2.shell:do_create(调用cs.volumes.create)
④ cinderclient.v1.volumes:VolumeManager.create或cinderclient.v2.volumes:VolumeManager.create(对body进行赋值,最后调用self._create)
⑤ cinderclient.base:Manager._create(通过self.api.client.post把url和body内容传递下去)
⑥ cinderclient.client:HTTPClient.post(调用self._cs_request,在_cs_request中又调用self.request)
⑦ cinderclient.client:HTTPClient.request(调用requests.request,requests库遵循HTTP协议,实现了訪问远程server并等待响应的功能)
2. cinderclient部分create snapshot流程
① cinderclient.shell:main(调用OpenStackCinderShell().main)
② cinderclient.shell:OpenStackCinderShell.main(对命令行參数进行解析等一系列操作;方法main的最后一句args.func(self.cs, args)解析之后为do_snapshot_create)
③ cinderclient.v1.shell:do_snapshot_create或cinderclient.v2.shell:do_snapshot_create(调用cs.volume_snapshots.create)
④ cinderclient.v1.volume_snapshots:SnapshotManager.create或cinderclient.v2.volume_snapshots:SnapshotManager.create(对body进行赋值,最后调用self._create)
⑤ cinderclient.base:Manager._create(通过self.api.client.post把url和body内容传递下去)
⑥ cinderclient.client:HTTPClient.post(调用self._cs_request,在_cs_request中又调用self.request)
⑦ cinderclient.client:HTTPClient.request(调用requests.request,requests库遵循HTTP协议,实现了訪问远程server并等待响应的功能)
3. cinderclient部分create backup流程
① cinderclient.shell:main(调用OpenStackCinderShell().main)
② cinderclient.shell:OpenStackCinderShell.main(对命令行參数进行解析等一系列操作;方法main的最后一句args.func(self.cs, args)解析之后为do_backup_create)
③ cinderclient.v1.shell:do_backup_create或cinderclient.v2.shell:do_backup_create(调用cs.backups.create)
④ cinderclient.v1.volume_backups:VolumeBackupManager.create或cinderclient.v2.volume_backups:VolumeBackupManager.create(对body进行赋值,最后调用self._create)
⑤ cinderclient.base:Manager._create(通过self.api.client.post把url和body内容传递下去)
⑥ cinderclient.client:HTTPClient.post(调用self._cs_request,在_cs_request中又调用self.request)
⑦ cinderclient.client:HTTPClient.request(调用requests.request,requests库遵循HTTP协议,实现了訪问远程server并等待响应的功能)
四、cinder部分通用create流程
1. 使用若干中间件对client发送过来的请求消息进行过滤(解封和封装)处理
① 配置文件api-paste.ini的解析
a) 配置文件/etc/cinder/api-paste.ini中的[composite:osapi_volume](XXXX/XXXX形式的API交给apiversions来处理,XXXX/V1/XXXX形式的API交给openstack_volume_api_v1来处理,XXXX/V2/XXXX形式的API交给openstack_volume_api_v2来处理)
b) 配置文件/etc/cinder/api-paste.ini中的[pipeline:apiversions](使用pipeline section实现faultwrap对osvolumeversionapp的过滤操作)
c) 配置文件/etc/cinder/api-paste.ini中的[composite:openstack_volume_api_v1]或[composite:openstack_volume_api_v2](在參数noauth,keystone和keystone_nolimit中,分别集成了多个过滤器faultwrap、sizelimit等,以及应用apiv1或apiv2)
② FaultWrapper(关于错误异常的处理)
a) 配置文件/etc/cinder/api-paste.ini中的[filter:faultwrap](通过此过滤器,调用类FaultWrapper)
b) cinder.api.middleware.fault:FaultWrapper.__call__(调用self._error进行错误检測)
c) cinder.api.middleware.fault:FaultWrapper._error(关于错误异常的处理)
③ RequestBodySizeLimiter(对请求消息中body部分长度的检測)
a) 配置文件/etc/cinder/api-paste.ini中的[filter:sizelimit](通过此过滤器,调用类RequestBodySizeLimiter)
b) cinder.api.middleware.sizelimit:RequestBodySizeLimiter.__call__(对请求消息中body部分长度的检測)
④ 身份认证处理
a) 配置文件/etc/cinder/api-paste.ini中的[filter:noauth](通过此过滤器,调用类NoAuthMiddleware)或[filter:keystonecontext](通过此过滤器,调用类CinderKeystoneContext)
b) cinder.api.middleware.auth:NoAuthMiddleware.__call__或cinder.api.middleware.auth:CinderKeystoneContext.__call__
⑤ APIRouter(对请求消息的路由处理)
a) 配置文件/etc/cinder/api-paste.ini中的[app:apiv1]或[app:apiv2](通过此过滤器,调用类APIRouter)
b) cinder.api.v1.Router:APIRouter或cinder.api.v2.Router:APIRouter(对请求消息的路由处理)
c) cinder.api.openstack.__init__:APIRouter.__init__
d) cinder.api.extensions:ExtensionManager
e) cinder.api.v1.Router:APIRouter._setup_routes
f) cinder.api.openstack.__init__:APIRouter._setup_ext_routes(在一个控制器中单纯地实现新的Restful资源功能,调用wsgi.Resource)
g) cinder.api.openstack.__init__:APIRouter._setup_extensions
h) cinder.wsgi:Router.__init__
⑥ Resource(action运行前的一些预处理)
a) cinder.api.openstack.wsgi:Resource.__call__(对获取到的响应信息进行若干格式化上的转换和处理)
b) cinder.api.openstack.wsgi:Resource._process_stack(主要完毕了请求信息完整的运行过程)
2. 获取要运行的方法及其相关的扩展方法
cinder.api.openstack.wsgi:Resource.get_method(获取要运行的action方法及相关的扩展方法)
3. 请求中body部分的反序列化
cinder.api.openstack.wsgi:Resource.deserialize(确定反序列化的方法,实现对body的反序列化操作)
4. 所获取的action相关的扩展方法的运行
cinder.api.openstack.wsgi:Resource.pre_process_extensions(运行之前获取的扩展方法)
5. 运行详细的方法(如卷的创建方法create)
cinder.api.openstack.wsgi:Resource.dispatch(运行之前获取的action方法,获取并返回方法运行的结果)
6. 响应信息的生成
① cinder.api.openstack.wsgi:Resource.preserialize(依据accept确定对应对象所要使用的序列化方法或类,进行类的初始化操作)
② cinder.api.openstack.wsgi:Resource.serialize(进行序列化操作,从而形成响应信息中的body部分)
五、cinder部分create volume流程
1. cinder.api.v1.volumes:VolumeController.create或cinder.api.v2.volumes:VolumeController.create(通过req和body获取创建卷所需的相关參数;调用self.volume_api.create)
2. 调用方法create实现卷的创建
① cinder.volume.api:API.create(构建字典create_what,整合创建卷的详细參数;调用create_volume.get_api_flow)
② cinder.volume.flows.create_volume.__init__:get_api_flow(构建并返回用于创建卷的flow;调用linear_flow.Flow等)
a) cinder.taskflow.patterns.linear_flow:Flow.__init__(初始化一个Flow类)
b) cinder.taskflow.patterns.linear_flow:Flow.add(按顺序加入相关的task到flow中)
c) cinder.volume.flows.utils:attach_debug_listeners(返回加入task后的flow给调用者)
③ cinder.taskflow.patterns.linear_flow:Flow.run(运行用于创建卷的flow,运行获取到的task列表,运行完毕后对task状态进行推断,假设不成功则进行回滚操作)
a) cinder.volume.flows.create_volume.__init__:VolumeCastTask.__call__(VolumeCastTask是创建卷最关键的一个task,我们重点分析这个;调用self._cast_create_volume)
b) cinder.volume.flows.create_volume.__init__:VolumeCastTask._cast_create_volume(远程调用创建卷的方法,实现卷的创建操作,对參数内容进行推断,确定创建卷的方式、创建卷到哪个host等;调用self.scheduler_rpcapi.create_volume)
c) cinder.scheduler.rpcapi:SchedulerAPI.create_volume(通过cast广播方式实现远程调用方法create_volume)
d) cinder.scheduler.manager:SchedulerManager.create_volume(调用create_volume.get_scheduler_flow)
e) cinder.volume.flows.create_volume.__init__:get_scheduler_flow(创建flow,并将三个task加入到该flow中,当中最重要的task是schedule_create_volume)
f) Scheduler在未指明host的情况下,有三种算法选择host(FilterScheduler、ChanceScheduler、SimpleScheduler),通过配置文件/etc/cinder/cinder.conf可知,scheduler_driver默认指定cinder.scheduler.filter_scheduler.FilterScheduler
g) cinder.scheduler.filter_scheduler:FilterScheduler.schedule_create_volume(调用self._schedule,在_schedule中又调用self._get_weighted_candidates)
h) cinder.scheduler.filter_scheduler:FilterScheduler._get_weighted_candidates(self._get_weighted_candidates对可用的host进行weight推断,过滤选择合适的主机;接着schedule_create_volume调用self.volume_rpcapi.create_volume)
i) cinder.volume.rpcapi:VolumeAPI.create_volume(通过cast广播方式实现远程调用方法create_volume)
j) cinder.volume.manager:VolumeManager.create_volume(调用create_volume.get_manager_flow)
k) cinder.volume.flows.create_volume.__init__:get_manager_flow(创建flow,并将若干个task加入到该flow中,当中最重要的task是CreateVolumeFromSpecTask,实现卷的创建;创建卷有四种方式:raw卷、从快照创建、克隆、从镜像创建)
l) 创建raw卷
cinder.volume.flows.create_volume.__init__:CreateVolumeFromSpecTask._create_raw_volume(调用self.driver.create_volume;Driver是底层採用的驱动,能够是LVM、GlusterFS、Ceph(RBD)等)
cinder.volume.drivers.glusterfs:GlusterfsDriver.create_volume(这里以GlusterFS为例,推断对应的文件夹是否挂载,然后调用self._do_create_volume)
cinder.volume.drivers.glusterfs:GlusterfsDriver._do_create_volume(有两种创建方式:qcow2和regular_file;依据配置文件/etc/cinder/cinder.conf中的glusterfs_qcow2_volumes值决定採用那种方式,默认使用_create_regular_file方式)
cinder.volume.drivers.nfs:RemoteFsDriver._create_regular_file(_create_regular_file通过dd命令创建块存储;而_create_qcow2_file通过qemu-img命令创建块存储)
m) 从快照创建卷
cinder.volume.flows.create_volume.__init__:CreateVolumeFromSpecTask._create_from_snapshot
n) 克隆
cinder.volume.flows.create_volume.__init__:CreateVolumeFromSpecTask._create_from_source_volume
o) 从镜像创建卷
cinder.volume.flows.create_volume.__init__:CreateVolumeFromSpecTask._create_from_image
④ volume = flow.results[uuid]['volume'](从flow中获取创建卷的反馈信息)
3. 获取创建卷后的反馈信息,实现格式转换,并获取当中重要的属性信息,以便上层调用生成卷创建的响应信息。
六、cinder部分create snapshot流程
1. cinder.api.v1.snapshots:SnapshotsController.create或cinder.api.v2.volumes:SnapshotsController.create(通过req和body获取创建卷所需的相关參数,然后调用self.volume_api.create_snapshot或self.volume_api.create_snapshot_force)
2. 调用方法create_snapshot实现快照的创建
① cinder.volume.api:API.create_snapshot或cinder.volume.api:API.create_snapshot_force(调用self._create_snapshot)
② cinder.volume.api:API._create_snapshot(整合创建快照的详细參数,更新数据库信息,然后调用self.volume_rpcapi.create_snapshot)
③ cinder.volume.rpcapi:VolumeAPI.create_snapshot(通过cast广播方式实现远程调用方法create_snapshot)
④ cinder.volume.manager:VolumeManager.create_snapshot(调用self.driver.create_snapshot;Driver是底层採用的驱动,能够是LVM、GlusterFS、Ceph(RBD)等)
⑤ cinder.volume.drivers.glusterfs:GlusterfsDriver.create_snapshot(这里以GlusterFS为例)
a) 卷被挂载的情况下:
cinder.volume.drivers.glusterfs:GlusterfsDriver._create_qcow2_snap_file(通过qemu-img命令创建快照)
cinder.compute.nova:API.create_volume_snapshot(调用nova.assisted_volume_snapshots.create,由Nova来完毕快照数据写入)
b) 卷未被挂载的情况下:
cinder.volume.drivers.glusterfs:GlusterfsDriver._create_qcow2_snap_file(通过qemu-img命令创建快照)
cinder.volume.drivers.glusterfs:GlusterfsDriver._create_snapshot(先调用self._read_info_file,再调用self._write_info_file)
cinder.volume.drivers.glusterfs:GlusterfsDriver._write_info_file(将快照数据写入指定路径中)
3. 获取创建快照后的反馈信息,实现格式转换,并获取当中重要的属性信息,以便上层调用生成快照创建的响应信息。
七、cinder部分create backup流程
1. cinder.api.contrib.backups:BackupsController.create(通过req和body获取创建卷所需的相关參数,然后调用self.backup_api.create)
2. 调用方法create实现备份的创建
① cinder.backup.api:API.create(对源卷状态进行推断,对backup服务进行检查,更新数据库信息,然后调用self.backup_rpcapi.create_backup)
② cinder.backup.rpcapi:BackupAPI.create_backup(通过cast广播方式实现远程调用方法create_backup)
③ cinder.backup.manager:BackupManager.create_backup(获取备份的相关信息,初始化驱动并调用底层驱动创建备份;调用self._get_driver(backend).backup_volume)
④ cinder.volume.drivers.rbd:RBDDriver.backup_volume(这里以Ceph为例,获取卷相关的RBD的相关数据,然后调用backup_service.backup)
⑤ cinder.backup.drivers.rbd:RBDDriver.backup(解析參数,并推断源卷是否为RBD卷,假设是源卷为RBD卷,则使用增量备份,否则使用全量备份)
a) 增量备份
cinder.backup.drivers.rbd:RBDDriver._backup_rbd(获取前一个snapshot,创建新的snapshot,调用self._rbd_diff_transfer方法将两个snap之间的changes进行拷贝)
b) 全量备份
cinder.backup.drivers.rbd:RBDDriver._full_backup(首先调用self.rbd.RBD().create创建一个base backup image,然后调用self._transfer_data将数据从源卷复制到这个backup中)
3. 获取创建快照后的反馈信息,实现格式转换,并获取当中重要的属性信息,以便上层调用生成快照创建的响应信息。
八、參考资料
1. 《OpenStack源代码分析-Cinder》:http://blog.csdn.net/gaoxingnengjisuan/article/category/1853287
OpenStack Cinder源代码流程简析的更多相关文章
- zxing二维码扫描的流程简析(Android版)
目前市面上二维码的扫描似乎用开源google的zxing比较多,接下去以2.2版本做一个简析吧,勿喷... 下载下来后定位两个文件夹,core和android,core是一些核心的库,android是 ...
- Tomcat启动流程简析
Tomcat是一款我们平时开发过程中最常用到的Servlet容器.本系列博客会记录Tomcat的整体架构.主要组件.IO线程模型.请求在Tomcat内部的流转过程以及一些Tomcat调优的相关知识. ...
- LinkedHashMap结构get和put源码流程简析及LRU应用
原理这篇讲得比较透彻Java集合之LinkedHashMap. 本文属于源码阅读笔记,因put,get调用逻辑及链表维护逻辑复杂(至少网上其它文章的逻辑描述及配图,我都没看明白LinkedHashMa ...
- jquery选择器的实现流程简析及提高性能建议!
当我们洋洋得意的使用jquery强大的选择器功能时有没有在意过jquery的选择性能问题呢,其实要想高效的使用jquery选择器,了解其实现流程是很有必要的,那么这篇文章我就简单的讲讲其实现流程,相信 ...
- Pig源代码分析: 简析运行计划的生成
摘要 本文通过跟代码的方式,分析从输入一批Pig-latin到输出物理运行计划(与launcher引擎有关,通常是MR运行计划.也能够是Spark RDD的运行算子)的总体流程. 不会详细涉及AST怎 ...
- React Native 启动流程简析
导读:本文以 react-native-cli 创建的示例工程(安卓部分)为例,分析 React Native 的启动流程. 工程创建步骤可以参考官网.本文所分析 React Native 版本为 v ...
- 【Java虚拟机10】ClassLoader.getSystemClassLoader()流程简析
前言 学习类加载必然离开不了sun.misc.Launcher这个类和Class.forName()这个方法. 分析ClassLoader.getSystemClassLoader()这个流程可以明白 ...
- HTTPS及流程简析
[序] 在我们在浏览某些网站的时候,有时候浏览器提示需要安装根证书,可是为什么浏览器会提示呢?估计一部分人想也没想就直接安装了,不求甚解不好吗? 那么什么是根证书呢?在大概的囫囵吞枣式的百度之后知道了 ...
- Postfix 发送邮件流程简析
PostFix接受和转发邮件的说明 来源ip符合inet_interfaces,收件人域符合mydestination, Postfix将接收到本地. 来源ip符合inet_interfaces, ...
随机推荐
- Debian 8.0(Jessie) 无线网卡,ATI显卡驱动和输入法等安装记录。
转载请注明作者与出处!谢谢! 最近准备彻底转换到Linux平台,之前一直用的是Red Hat,对Debian不是很熟悉,花了不少时间摸索.下面记录一下安装的过程以便备忘,顺便给他人能做个参考. 我的是 ...
- CSAPP LAB: Buffer Overflow
这是CSAPP官网上的著名实验,通过注入汇编代码实现堆栈溢出攻击.实验材料可到我的github仓库 https://github.com/Cheukyin/CSAPP-LAB/ 选择buffer-ov ...
- php函数——『解析 xml数据』
<?php //该文件是 //$raw_post_data = file_get_contents('php://input'); //file_put_contents('a.txt', $r ...
- openshif ssh proxy
最近google又被墙了.没办法 1:注册一个openshift账号.申请注册一个app,获取一个免费主机. https://www.openshift.com/ 2:去PuTTY官方网站下载pL ...
- 搞Solr这一年(本人QQ 282335345 群412268049 欢迎大家一起学习Solr 非诚勿扰)
搞Solr这一年 去年6月份毕业到现在已经快一年半了,很庆幸从事了搜索引擎这份工作,虽然谈不上有多深入,但至少已经入门了.在这一年半里,搞了3个月的hbase和mapreduce,搞了一个月的nutc ...
- 发一下关于公司的HOUSE OF HELLO 关于假货网站的声明吧
HOUSE OF HELLO,致力于为新时代潮流女性提供优质时尚的手袋,公司核心价值观是:坚韧开拓市场,真诚铸就成长,行动改变命运,激情成就梦想.公司从上到下的员工,都富于激情的努力工作,以积极,主动 ...
- [Fw]人和人之间在八小时之外的差别
原文地址:http://hankjin.blog.163.com/blog/static/3373193720083249387801/ 业余八小时人的活动千姿百态.八小时以外你在干什么,恰恰决定着你 ...
- Unity3D 3D横版跑酷
Unity3d 3D横版跑酷系列(Character Controller组件) @广州小龙 目前在做一个3D跑酷的横版游戏,目前说一下 Character Controller组件! 1.Slop ...
- SCVMM更换数据库,如何搞?
因为SCVMM和SQL不是集成在同一台机器上的. 所以,当SQL换机器或是换名字后,SCVMM就不能启动了. 并且MS没提供直观的更改数据库连接的工具,只是在安装的时候有选项. 网上找了方法,修改注册 ...
- Mysql中类似于nvl()函数的ifnull()函数
IFNULL(expr1,expr2) 如果expr1不是NULL,IFNULL()返回expr1,否则它返回expr2.IFNULL()返回一个数字或字符串值,取决于它被使用的上下文环境. mysq ...