存档

文章标签 ‘Storage’

GlusterFS 初体验

2013年12月2日 没有评论

简介

GlusterFS 是一款开源的分布式文件系统, 具有强大的 scale-out 横向扩展能力, 通过扩展能够支持PB级别的存储容量和数千客户端. 它和 CEPH 在开源界 和存储界正在发起一场基于云计算的存储风暴, 向传统的存储技术(SCSI, Fibre Channel)发起 猛烈的攻击, 同时在这场战役中竞争最激烈的也是它们俩, 所以它俩既是亲密的战友, 又是敌人. 到底谁能笑道最后, 让我们拭目以待.

环境准备

为了体验一下 GlusterFS, 我搭建了一个基于 KVM 虚拟机的环境, 如下:

我准备了三台虚拟机, 在之上安装了 ubuntu 13.10 操作系统(其他操作系统类似), 每台虚拟机有两块 virtio 的硬盘: vda, vdb, 用 XFS 作为 brick 的文件 系统, 其它具体的情况如下:

hostname ip role brick
gfs1 192.168.176.153 server /data/gv0/brick1
gfs2 192.168.176.154 server /data/gv0/brick1
gfs3 192.168.176.155 client

准备 server(在 gfs1 和 gfs2 上都执行下列操作)

设置三台主机的 hostname

# emacsclient /etc/hosts
# 最后增加
192.168.176.153 gfs1
192.168.176.154 gfs2
192.168.176.155 gfs3

设置 server 的 brick

# modprobe xfs
# apt-get install xfsprogs # xfsprogs 包含 mount.xfs, mkfs.xfs 等
# fdisk /dev/vdb # 将 vdb 分为一个区 vdb1
# mkfs.xfs -i size=512 /dev/vdb1 # 将 vdb1 格式化为 XFS 文件系统
 
# 把 vdb1 挂载到我们的 brick, 即 /data/gv0/brick1 下
# mkdir -p /data/gv0/brick1
# emacsclient /etc/fstab
# 在最后一行增加
/dev/vdb1 /data/gv0/brick1 xfs defaults 1 2
# mount -a

准备 client

设置三台主机的 hostname(同上)

安装设置

server

首先安装和启动 daemon(在 gfs1 和 gfs2 上)

# apt-get install glusterfs-server -y
# service glusterfs-server start

然后用 probe 命令把对方加到 pool 里.

# gluster peer probe gfs2 # 在 gfs1 上执行
  Probe successful
# gluster peer probe gfs1 # 在 gfs2 上执行
# gluster peer status # gfs1 上, 检查 peer 的 status
  Number of Peers: 1
 
  Hostname: gfs2
  Uuid: d8d3dad0-3cac-4e98-a28d-f59fb0a6c43f
  State: Peer in Cluster (Connected)

最后, 创建 volume(在任意一个节点上都可以执行)

# 两个节点, 数据保留两个副本
# gluster volume create gv0 replica 2 gfs1:/data/gv0/brick1 gfs2:/data/gv0/brick1
# gluster volume info
Volume Name: gv0
Type: Replicate
Status: Created
Number of Bricks: 2
Transport-type: tcp
Bricks:
Brick1: gfs1:/data/gv0/brick1
Brick2: gfs2:/data/gv0/brick1

client

client 除了 glusterfs-client, 不需要任何其他包(xfs等)

# apt-get install glusterfs-client -y
# mount -t glusterfs gfs1:/gv0 /mnt

测试

# cp /var/log/dmesg /mnt/ #  在 client 执行:
# 然后到 gfs1, gfs2 上, 可以看到在每个 server 上都有一个 dmesg 文件
# 这是因为上面我们创建 volume 的时候, 设置的副本数目为 2
# ls -lA /data/gv0/brick1(both server)

测试删除:

# 在 client 执行:
# rm /mnt/dmesg # 然后再 gfs1, gfs2 看可以看到这些副本都删除了

深入一点

之前我们用两个节点创建了有两个副本的 volume, 每次写到 GlusterFS 同时都会写到这两个节点中.

如果节点数多于副本数, 会发生什么情况.

我们把 gfs3, 也变为 server, 用上面 setup server 的方式. 把 gfs3 的 vdb 分为 2个区, vdb1, vdb2, 分别挂载到 /data/gv0/brick1 和 /data/gv0/brick2.

# fdisk /dev/vdb # 分为 vdb1, vdb2
# mkdir -p /data/gv0/brick{1,2}
# emacsclient /etc/fstab
# tail -n 2 /etc/fstab
/dev/vdb1 /data/gv0/brick1 xfs defaults 1 2
/dev/vdb2 /data/gv0/brick2 xfs defaults 1 2
# mkfs.xfs -i size=512 /dev/vdb{1,2}
# mount -a

在任何一个节点, 把 上面的两个 brick 加入到 gv0 这个 volume

# gluster volume add-brick gv0 gfs3:/data/gv0/brick1 gfs3:/data/gv0/brick2
# gluster volume info
Volume Name: gv0
Type: Distributed-Replicate
Status: Started
Number of Bricks: 2 x 2 = 4
Transport-type: tcp
Bricks:
Brick1: gfs1:/data/gv0/brick1
Brick2: gfs2:/data/gv0/brick1
Brick3: gfs3:/data/gv0/brick1
Brick4: gfs3:/data/gv0/brick2

现在我们有 4 个 brick了, 做一个简单地测试, 在 client 我们复制 10 个文件到 gfs中, 看看各个 node 会有什么变化

# for i in `seq -w 1 10`; do cp -rp /var/log/syslog /mnt/copy-test-$i; done # 在 client 执行
# ls /data/gv0/brick1/ # gfs1
copy-test-04  copy-test-05  copy-test-09
# ls /data/gv0/brick1/ # gfs2
copy-test-04  copy-test-05  copy-test-09
# ls /data/gv0/brick1/ # gfs3
copy-test-01  copy-test-03  copy-test-07  copy-test-10
copy-test-02  copy-test-06  copy-test-08
# ls /data/gv0/brick2 # gfs3
copy-test-01  copy-test-03  copy-test-07  copy-test-10
copy-test-02  copy-test-06  copy-test-08

可以看到, 文件被放到4个brick中了, 并且总体来看, 每个文件都有两个副本.

分类: Storage 标签: ,

LVM Thin Provision

2013年10月30日 没有评论

之前对 LVM 做过一番简单的探索, 但其实研究的还不够, 真正在使用的时候 LVM 不太能 满足我的需求, 比如我需要的 Thin Provision 功能就没法实现. 也就是说一般情况下, 我们使用的 LVM 是不支持 Thin Provision 的, LV 在创建之后, 大小就固定了, 虽然之后可以手动增加和减小 PE 的数量, 但是在一些场景中会带来更复杂的问题.

比如, 考虑用 LV 作为虚拟机的镜像存储方式, 我们知道很多虚拟磁盘(比如 qcow2, qde, vdi) 是支持 copy-on-write 的, 即虚拟磁盘在创建的时候只需要分配非常小的空间, 随着 用户对该磁盘的写操作, “按需” 的扩大实际占用空间. 这样做的好处是明显的: 可以创 建比真实设备允许的更多的虚拟磁盘, 即实现了 Thin Provision.

但是, 由于 LV 的限制, 在磁盘存在的那一刹那, 除非以后用户手动 resize 磁盘, 磁盘 的实际占用大小确定了, 也就是说 copy-on-write 其实已经失效了.

为了解决这个问题, 不同的虚拟化平台采用不同的办法, 比如创建虚拟镜像的 LV 的时候, 只分配很少的 PE, 然后监控该 LV 的使用情况(通过 guest agent 或者其他工具), 一旦该 LV “快满了”, 则调用 lvm 的工具 extend 该 LV. 但这样同样存在问题:

  • 监控 LV 可能消耗过多资源, 如果监控程序和 lvm 的管理程序不在一台节点上, 那么对网络带宽的消耗是不可忽视的, 还有监控间隔? 什么时候认为 LV “快满了”等等
  • 如果 LVM 的管理程序还没有来得及对磁盘进行扩充, 而 guest 的写入速度又 太快, 那么必然发生可用空间不够的错误 (errno=ENOSPC // not enough space)
  • 以上讨论的整个架构过于复杂, 太多因素需要考虑. e.g. 监控间隔, 数据包结构 …

归根结底, 上面的缺陷其实就是由于 LVM 不能 Thin Provison 导致的.

情况在 LVM 的 2.02.89 版本 开始变好了, 从这个版本开始, LVM 开始支持一种叫 “thin pool” 的特性, 该特性可以允许用户创建一个类型为 thin-pool 的 LV, 这个 LV 其实是一个 LV pool, 基于这个 LV pool 之上创建的 LV 就具有了 Thin Provison 的特性, 也就是 LV 池里面的 LV 也可以 copy-on-write 了.

下面来体验一下: 一个 10G 的磁盘用来作为 LVM 的实验环境的 PV

首先像之前 LVM 杂记 中提到的, 建立 PV, VG

# pvcreate /dev/vdb 
vgcreate test-vg /dev/vdb
# vgdisplay | grep Free
  Free  PE / Size       2559 / 10.00 GiB

创建一个类型 thin-pool, 名字为 test-pool 的 特殊的 LV, 大小为 8G, 其实就是 一个 LV pool

# lvcreate --size 8G --type thin-pool --thinpool test-pool test-vg
  Logical volume "test-pool" created

查看这个 thin-pool 的状态, 因为 thin-pool 是一个特殊的 LV, 所以可以用 lvs 查看, 可以看到和没有使用 thin pool 时候的变化, 真实地空间占用也显示出来了, 为 0%

# lvs
  LV        VG      Attr     LSize Pool Origin Data%  Move Log Copy%  Convert
  test-pool test-vg twi-a-tz 8.00g               0.00

好, 开始重头戏, 创建一个基于 thin pool 的 LV, 名字为 test-lv, 大小是 4G

# lvcreate -V4G -T test-vg/test-pool --name test-lv

看一下现在各个 LV 的情况, 可以看到我们的 test-lv 显示的是 4G, 实际占用为 0%, thin pool 仍然为 8G, 实际占用还是 0%. (因为没有数据 write, 所以也没有数据 copy)

# lvs
  LV        VG      Attr     LSize Pool      Origin Data%  Move Log Copy%  Convert
  test-lv   test-vg Vwi-a-tz 4.00g test-pool          0.00                        
  test-pool test-vg twi-a-tz 8.00g                    0.00

向 test-lv 填充一些数据, 看看 copy-on-write 怎么工作

# dd if=/dev/zero of=/dev/test-vg/test-lv bs=1M count=1024 && sync
root@cloudtimes:~# lvs
  LV        VG      Attr     LSize Pool      Origin Data%  Move Log Copy%  Convert
  test-lv   test-vg Vwi-a-tz 4.00g test-pool         25.00                        
  test-pool test-vg twi-a-tz 8.00g                   12.50

可以看到, 当我们写了 1G 的数据后, test-lv 的真实空间占用变大了 25%(1G/4G), thin pool 的空间也相应地变为 12.5%(1G/8G), 这些数值是随着写入的内容而变化的, 并不像之前那样, 我们需要用 LVM 工具干预. 真正的 Thin Provison!

下面, 为了做对比, 创建一个正常的 LV, 来看看和基于 thin pool 的 LV 的区别

#lvcreate -l 50 -n normal-lv test-vg
# lvs
  LV        VG      Attr     LSize   Pool      Origin Data%  Move Log Copy%  Convert
  normal-lv test-vg -wi-a--- 200.00m                                         <== 仔细看这行和下一行的区别
  test-lv   test-vg Vwi-a-tz   4.00g test-pool         25.00
  test-pool test-vg twi-a-tz   8.00g                   12.50
# dd if=/dev/zero of=/dev/test-vg/normal-lv bs=1M count=100 && sync
# lvs
  LV        VG      Attr     LSize   Pool      Origin Data%  Move Log Copy%  Convert
  normal-lv test-vg -wi-a--- 200.00m                                         <== 仔细看这行和下一行的区别
  test-lv   test-vg Vwi-a-tz   4.00g test-pool         25.00                        
  test-pool test-vg twi-a-tz   8.00g                   12.50

可以看到, 正常的 LV 并不能反映出到底用户真实写了多少数据, 因为不需要嘛, 它一被创建伊始就被赋予了他要求的所有空间, 自然达不到 Thin Provison(节省空间) 的效果.

分类: Storage 标签:

iSCSI 杂记

2013年10月30日 没有评论

iSCSI 是一个基于 IP 的网络存储技术, 该技术通过网络在两台计算机之间交换 传统的 SCSI 命令. iSCSI 可以看作是 Fibre Channel 存储的一个替代, 由于 iSCSI 是 基于网络的, 成本上更低廉.

iSCSI 架构是 C-S 的, 分为 initiator(client) 和 target(server), initiator 会向 target 发送 SCSI 命令.

下面简单的记录一下搭建过程:

  • target: 192.168.0.166, 有一块硬盘 /dev/vdb, 不过任何处理直接 export 给 initiator
  • initiator: 192.168.0.47

搭建 target

在一些 KISS 的发行版(gentoo, archlinux) 上, 确保内核编译并加载了相应的模块

然后安装 target 用户空间的程序, 有两种程序可以选择: iscsitarget 和 tgt(scsi-target-utils). 其实两个程序都差不多, 这里我选择 tgt 来作为 例子.

# debian 系
# apt-get install tgt
 
# Redhat 系
# yum install scsi-target-utils

配置很简单, tgt 的安装目录下也自带了一个 example 文件: targets.conf.example.gz.

下面是我的配置:

# cat /etc/tgt/targets.conf
<target iqn.2013-10.com.example:server.target0>
    direct-store /dev/vdb
</target>
# service tgt restart

查看 target 信息

# tgtadm --mode target --op show
Target 1: iqn.2013-10.com.example:server.target0
    System information:
        Driver: iscsi
        State: ready
    I_T nexus information:
    LUN information:
        LUN: 0
            Type: controller
            SCSI ID: IET     00010000
            SCSI SN: beaf10
            Size: 0 MB, Block size: 1
            Online: Yes
            Removable media: No
            Readonly: No
            Backing store type: null
            Backing store path: None
            Backing store flags: 
        LUN: 1                       <== 我们添加的 target, 可以使用的 LUN
            Type: disk
            SCSI ID: IET     00010001
            SCSI SN: beaf11
            Size: 10737 MB, Block size: 512
            Online: Yes
            Removable media: No
            Readonly: No
            Backing store type: rdwr
            Backing store path: /dev/vdb
            Backing store flags: 
    Account information:
    ACL information:
        ALL

搭建 initiator

# apt-get install open-iscsi open-iscsi-utils

“探测” target

# iscsiadm -m discovery -t st -p 192.168.0.166
192.168.0.166:3260,1 iqn.2013-10.com.example:server.target0

显示 target 信息

# iscsiadm -m node -o show
# BEGIN RECORD 2.0-871
node.name = iqn.2013-10.com.example:server.target0
node.tpgt = 1
node.startup = manual
......                    <== 省略
node.discovery_address = 192.168.0.166
node.discovery_port = 3260
node.discovery_type = send_targets
......
node.session.auth.authmethod = None
node.session.auth.username = <empty>
node.session.auth.password = <empty>
node.session.auth.username_in = <empty>
node.session.auth.password_in = <empty>
......
node.conn[0].address = 192.168.0.166
node.conn[0].port = 3260
node.conn[0].startup = manual
......
# END RECORD

登录 target

# iscsiadm -m node --login
Logging in to [iface: default, target: iqn.2013-10.com.example:server.target0, portal: 192.168.0.166,3260]
Login to [iface: default, target: iqn.2013-10.com.example:server.target0, portal: 192.168.0.166,3260]: successful

该命令执行成功后, 可以在 dmesg 信息里面看到添加的磁盘信息, 就好像插入了 一块新的 SCSI 的磁盘一样, 设备文件名 “/dev/sda”

tailf /var/log/syslog
Oct 30 17:20:40 dunrong-virtual-machine kernel: [21180.546054] scsi18 : iSCSI Initiator over TCP/IP
Oct 30 17:20:40 dunrong-virtual-machine kernel: [21180.804706] scsi 18:0:0:0: RAID              IET      Controller       0001 PQ: 0 ANSI: 5
Oct 30 17:20:40 dunrong-virtual-machine kernel: [21180.805022] scsi 18:0:0:0: Attached scsi generic sg0 type 12
Oct 30 17:20:40 dunrong-virtual-machine kernel: [21180.806690] scsi 18:0:0:1: Direct-Access     IET      VIRTUAL-DISK     0001 PQ: 0 ANSI: 5
Oct 30 17:20:40 dunrong-virtual-machine kernel: [21180.806927] sd 18:0:0:1: Attached scsi generic sg1 type 0
Oct 30 17:20:40 dunrong-virtual-machine kernel: [21180.811396] sd 18:0:0:1: [sda] 20971520 512-byte logical blocks: (10.7 GB/10.0 GiB)
Oct 30 17:20:40 dunrong-virtual-machine kernel: [21180.813404] sd 18:0:0:1: [sda] Write Protect is off
Oct 30 17:20:40 dunrong-virtual-machine kernel: [21180.813409] sd 18:0:0:1: [sda] Mode Sense: 49 00 00 08
Oct 30 17:20:40 dunrong-virtual-machine kernel: [21180.814524] sd 18:0:0:1: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
Oct 30 17:20:40 dunrong-virtual-machine kernel: [21180.819152]  sda: sda1
Oct 30 17:20:40 dunrong-virtual-machine kernel: [21180.822697] sd 18:0:0:1: [sda] Attached SCSI disk
Oct 30 17:20:41 dunrong-virtual-machine iscsid: connection17:0 is operational now

然后, 就可以像使用普通硬盘一样创建分区(或者LVM), 格式化……

此时, 如果再次在 target server(192.168.0.166) 上查看 target 的信息, 可以看到 initiator 的连接信息

# tgtadm --mode target --op show
Target 1: iqn.2013-10.com.example:server.target0    <== client 的连接信息
    System information:
        Driver: iscsi
        State: ready
    I_T nexus information:
        I_T nexus: 4
            Initiator: iqn.1993-08.org.debian:01:4393697be02
            Connection: 0
                IP Address: 192.168.0.47    <== client 的 IP 地址
    LUN information:
        LUN: 0
            Type: controller
            SCSI ID: IET     00010000
            SCSI SN: beaf10
            Size: 0 MB, Block size: 1
            Online: Yes
            Removable media: No
            Readonly: No
            Backing store type: null
            Backing store path: None
            Backing store flags: 
        LUN: 1
            Type: disk
            SCSI ID: IET     00010001
            SCSI SN: beaf11
            Size: 10737 MB, Block size: 512
            Online: Yes
            Removable media: No
            Readonly: No
            Backing store type: rdwr
            Backing store path: /dev/vdb
            Backing store flags: 
    Account information:
    ACL information:
        ALL

使用完之后, 可以使用 logout 命令退出这个 target

# iscsiadm -m node --logout
Logging out of session [sid: 16, target: iqn.2013-10.com.example:server.target0, portal: 192.168.0.166,3260]
Logout of [sid: 16, target: iqn.2013-10.com.example:server.target0, portal: 192.168.0.166,3260]: successful

分类: Storage 标签:

深入分析 QEMU 的 cache 机制

2013年10月22日 没有评论

Cache 的一些基本概念

Cache 最先指的是高速缓存, 用于平衡 CPU 和内存之间的速度差异, 后来, 进一步发展为 一种技术, 用在速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构.

本文主要指的是协调内存和硬盘速度的 Cache.

同步在 Cache 和他的后端存储一般有以下几种方式:

  • Write through: 数据一旦在 Cache 中, 就马上同步到真实设备中, 也就是保持同步
  • Write back: 见闻知意, writeback 的意思就是数据一旦在 Cache 中, 这个操作就算完成了, 并不需要马上同步到真实设备中, 除非用户手动指定(fsync), 或者此 Cache 中的内容发生了改变

QEMU 中的 Cache 模型

由于虚拟化的关系, QEMU 中的 Cache 模型有一点复杂.

在虚拟化的世界中, 一个对象通常有两端, guest 端和 host 端, 或者称为 frontend 和 backend. 比如 vcpu 对象, 在 frontend 是一个 CPU, 在 backend 端, 它只是 一个线程, 对磁盘来讲, frontend 端看到 的是一个磁盘设备, 在 backend 端, 仅仅 是一个普通的文件而已.

所以 QEMU 中的 Cache, 就有两种情况, guest(frontend) 看到的 disk 的 Cache, 和 host(backend)看到的那个普通文件的 Cache. QEMU 需要对前者进行模拟, 对后者 需要管理. 后面会用代码详细解释 QEMU 是怎么实现的.

先遍历一下 QEMU 中 Cache 模式:

Cache Mode Host Cache Guest Disk Cache
none off on
writethrough on off
writeback on on
unsafe on ignore
directsync off off

实现的代码分析

为了方便, 我选择使用 IDE 的 device 和 raw 的磁盘格式.

host end 初始化 block 的流程

根据上面所说的, 模拟磁盘的初始化分为前端(磁盘设备)和后端(镜像文件)的初始化, 在后端的初始化中, 入口函数是 drive_init(), 到最后的 qemu_open() 终止. 上图 中红色得模块就是 Cache 初始化最重要的两个地方.

简单的分析一下流程.

int bdrv_parse_cache_flags(const char *mode, int *flags)
{
    *flags &= ~BDRV_O_CACHE_MASK;
 
    /*
     * - BDRV_O_NOCACHE: host end 绕过 cache
     * - BDRV_O_CACHE_WB: guest 的磁盘设备启用 writeback cache
     * - BDRV_O_NO_FLUSH: 在 host end 永远不要把 cache 里的数据同步到文件里
     * 这几个宏的具体应用后面分析到数据读写的时候会进行分析
     */
    if (!strcmp(mode, "off") || !strcmp(mode, "none")) {
        /* 由上, 这个组合表示的是 host end 不用 Cache, 数据直接在用户空间(QEMU)
         * 和真实设备之间通过 DMA 直接传输, 但是同时, 告诉 guest 模拟的磁盘设备
         * 是有 cache 的, guest 能发起 flush 的操作(fsync/fdatasync) */
        *flags |= BDRV_O_NOCACHE | BDRV_O_CACHE_WB;
    } else if (!strcmp(mode, "directsync")) {
        /* 很好理解, 完全没有 cache, host end 和 guest end 都没有 cache, guest
         * 不会发起 flush 的操作 */
        *flags |= BDRV_O_NOCACHE;
    } else if (!strcmp(mode, "writeback")) {
        /* 和上面相反, host side 和 guest side 都有 cache, 性能最好, 但是如果
         * host 掉电, 会导致数据的损失 */
        *flags |= BDRV_O_CACHE_WB;
    } else if (!strcmp(mode, "unsafe")) {
        /* 见文可知意, 最不安全的模式, guest side 有cache, 但是 host side 不理睬
         * guest 发起的 flush 操作, 完全忽略, 这种情况性能最高, snapshot 默认使用
         * 的就是这种模式 */
        *flags |= BDRV_O_CACHE_WB;
        *flags |= BDRV_O_NO_FLUSH;
    } else if (!strcmp(mode, "writethrough")) {
        /* host end 有 cache, guest 没有 cache, 其实通过后面的代码分析可以知道,
         * 这种模式其实是 writeback + flush 的组合, 也就是每次写操作同时触发
         * 一个 host flush 的操作, 会带来一定的性能损失, 尤其是非 raw(e.g. qcow2)
         * 的网络存储(e.g. ceph), 但是很遗憾, 这是 QEMU 默认的 Cache 模式 */
        /* this is the default */
    } else {
        return -1;
    }
 
    return 0;
}
 
DriveInfo *drive_init(QemuOpts *all_opts, BlockInterfaceType block_default_type)
{
    /* code snippet */
 
    value = qemu_opt_get(all_opts, "cache");
    if (value) {
        int flags = 0;
 
        /* 解析命令行 -drive 的 cache= 选项 */
        if (bdrv_parse_cache_flags(value, &flags) != 0) {
            error_report("invalid cache option");
            return NULL;
        }
 
        /* Specific options take precedence */
        if (!qemu_opt_get(all_opts, "cache.writeback")) {
            qemu_opt_set_bool(all_opts, "cache.writeback",
                              !!(flags & BDRV_O_CACHE_WB));
        }
        if (!qemu_opt_get(all_opts, "cache.direct")) {
            qemu_opt_set_bool(all_opts, "cache.direct",
                              !!(flags & BDRV_O_NOCACHE));
        }
        if (!qemu_opt_get(all_opts, "cache.no-flush")) {
            qemu_opt_set_bool(all_opts, "cache.no-flush",
                              !!(flags & BDRV_O_NO_FLUSH));
        }
        qemu_opt_unset(all_opts, "cache");
    }
 
    return blockdev_init(all_opts, block_default_type);
}

接下来的流程稍微复杂一点:

blockdev_init
  -> bdrv_open
    -> bdrv_file_open
      -> raw_open
        -> raw_open_common
          -> raw_parse_flags
          -> qemu_open

首先是从 blockdev_init 到 raw_open 的流程, 简单分析如下:

static DriveInfo *blockdev_init(QemuOpts *all_opts,
                                BlockInterfaceType block_default_type)
{
    DriveInfo *dinfo;
 
    /* code snippet 解析和配置各种各样的参数, e.g. 磁盘格式, 启动顺序等等,
     * 最后填充到 dinfo 对象中 */
 
    snapshot = qemu_opt_get_bool(opts, "snapshot", 0);
 
    file = qemu_opt_get(opts, "file");
 
    if (qemu_opt_get_bool(opts, "cache.writeback", true)) {
        bdrv_flags |= BDRV_O_CACHE_WB;
    }
    if (qemu_opt_get_bool(opts, "cache.direct", false)) {
        bdrv_flags |= BDRV_O_NOCACHE;
    }
    if (qemu_opt_get_bool(opts, "cache.no-flush", false)) {
        bdrv_flags |= BDRV_O_NO_FLUSH;
    }
 
    if (snapshot) {
        /* 前面讲过, snapshot 打开磁盘时候, 使用 unsafe 的 cache 模式 */
        bdrv_flags &= ~BDRV_O_CACHE_MASK;
        bdrv_flags |= (BDRV_O_SNAPSHOT|BDRV_O_CACHE_WB|BDRV_O_NO_FLUSH);
    }
 
    bdrv_flags |= ro ? 0 : BDRV_O_RDWR;
 
    /* 使用 bdrv_open 打开文件 */
    ret = bdrv_open(dinfo->bdrv, file, bs_opts, bdrv_flags, drv, &error);
 
    /* 返回配置好的 DriveInfo *dinfo, 这个对象在初始化模拟磁盘设备
     * 的时候被传入, 写入该磁盘设备的 PCI config space */
    return dinfo;
}
 
int bdrv_open(BlockDriverState *bs, const char *filename, QDict *options,
              int flags, BlockDriver *drv, Error **errp)
{
    BlockDriverState *file = NULL;
    const char *drvname;
 
    /* 打开文件, QEMU 支持的镜像格式都有一个后端的 format, 比如 raw 和
     * qcow2 这个 format 就是 file, 其他的还有 sheepdog, glusterfs 的
     * gluster 等. 所以这里其实是打开一个本地文件 */
    ret = bdrv_file_open(&file, filename, file_options,
                         bdrv_open_flags(bs, flags | BDRV_O_UNMAP), &local_err);
 
    /* 其实 bdrv_file_open() 就会调用 bdrv_open_common 函数, 只不过那个时候调用
     * bdrv_open_common() 用的是 file 这个 BlockDriver, 现在使用的是磁盘文件
     * format 的 BlockDriver(qcow2, raw 等), 所以这里的函数用来初始化特定格式的
     * 磁盘文件, 如 qcow2_open 等 */
    ret = bdrv_open_common(bs, file, options, flags, drv, &local_err);
}
 
int bdrv_file_open(BlockDriverState **pbs, const char *filename,
                   QDict *options, int flags, Error **errp)
{
    BlockDriverState *bs;
 
    /* 找到相应的格式的 BlockDriver, 由于这里是 file 的 open, 因为 file
     * 还没有被打开, 所以这里第二个指针传递的是空, 注意 drv 这个参数, 表示
     * file 的 BlockDriver */
    ret = bdrv_open_common(bs, NULL, options, flags, drv, &local_err);
 
    return ret;
}
 
static int bdrv_open_common(BlockDriverState *bs, BlockDriverState *file,
    QDict *options, int flags, BlockDriver *drv, Error **errp)
{
    bs->open_flags = flags;
 
    open_flags = bdrv_open_flags(bs, flags);
 
    /* 注意这里, flags 保存着之前 bdrv_parse_cache_flags 获取到的 flags,
     * 如果用户没有指定 none, writeback, 或者是 unsafe, 那么 guest 看到
     * 的这个磁盘设备是没有 cache 的, 后面我会以 hdparm 这个工具来验证,
     * 同时, 这个变量(bs->enable_write_cache)控制着 QEMU 怎么模拟 cache
     * 行为, 后面写文件的时候会分析到 */
    bs->enable_write_cache = !!(flags & BDRV_O_CACHE_WB);
 
    /* 打开文件, 因为从上面的流程可以看到, 这里作为 file 打开, 而不是
     * 作为 image format(e.g. qcow2) 打开, 所以这里调用的是 bdrv_file_open()
     * 方法, 也就是 raw-posix.c 中 format_name="file" 的 BlockDriver 里面的
     * 的 raw_open */
    if (drv->bdrv_file_open) {
        assert(file == NULL);
        assert(drv->bdrv_parse_filename || filename != NULL);
        ret = drv->bdrv_file_open(bs, options, open_flags, &local_err);
    } else {
        if (file == NULL) {
            error_setg(errp, "Can't use '%s' as a block driver for the "
                       "protocol level", drv->format_name);
            ret = -EINVAL;
            goto free_and_fail;
        }
        bs->file = file;
        ret = drv->bdrv_open(bs, options, open_flags, &local_err);
    }
 
    return ret;
}

最后剩下核心的 raw_open 函数, 这个函数和 host side 的 Cache 相关, 通过上面 的分析, 已经比较简单了.

/* 这个函数其实是 raw_open_common */
static int raw_open(BlockDriverState *bs, QDict *options, int flags,
                    Error **errp)
{
    BDRVRawState *s = bs->opaque;
 
    s->type = FTYPE_FILE;
    return raw_open_common(bs, options, flags, 0);
}
 
static int raw_open_common(BlockDriverState *bs, QDict *options,
                           int bdrv_flags, int open_flags)
{
    s->open_flags = open_flags;
    /* 解析 open 的参数, 把 QEMU 的 BDRV_O_* 映射成 open 的 O_*, 下面详细分析 */
    raw_parse_flags(bdrv_flags, &s->open_flags);
 
    s->fd = -1;
 
    /* 用上面解析到得参数打开文件 */
    fd = qemu_open(filename, s->open_flags, 0644);
    s->fd = fd;
}
 
static void raw_parse_flags(int bdrv_flags, int *open_flags)
{
    assert(open_flags != NULL);
 
    /* 首先清空其他标志 */
    *open_flags |= O_BINARY;
    *open_flags &= ~O_ACCMODE;
    /* 设置读写权限位 */
    if (bdrv_flags & BDRV_O_RDWR) {
        *open_flags |= O_RDWR;
    } else {
        *open_flags |= O_RDONLY;
    }
 
    /* 如果设置了 cache=none, 那么直接用 O_DIRECT 打开文件, 这个标志保证数据
     * 的传输将不会通过内核空间, 而是使用 DMA 直接在用户空间到存储设备之间
     * 传送, 不保证数据是否同步. 这就是 cache=none 的由来 */
    if ((bdrv_flags & BDRV_O_NOCACHE)) {
        *open_flags |= O_DIRECT;
    }
}

guest end cache 的设置

在上面的的 bdrv_open_common() 函数中, 会设置 BlockDriverState->enable_write_cache 成员, 这个成员表示 guest 默认是否启用 writeback 的 Cache. 接下来会看到, guest 请求设备寄存器的时候, QEMU 会相应地用这个值填充寄存器的位, 下面以 IDE 硬盘来做例子.

guest 在初始访问 IDE 设备时, 会发送 IDENTIFY DRIVE (0xec) 指令, 设备收到这个 指令后, 需要返回 256 个字(512byte)的信息, 包括设备的状态, 扇区数目, 等等的信息.

static void ide_identify(IDEState *s)
{
    uint16_t *p;
    unsigned int oldsize;
    IDEDevice *dev = s->unit ? s->bus->slave : s->bus->master;
 
    memset(s->io_buffer, 0, 512);
    p = (uint16_t *)s->io_buffer;
 
    /* 看这里, bit 85 表示 writeback Cache 的状态, cache=none 之类的模式
     * 是会设置这个位的, 这样, guest 会发 fsync 指令过来, 否则, geust
     * 不会自动发送 fsync 同步数据, 当然, guest 可以在稍后设置是否启用
     * writeabck cache, QEMU 的新版本已经支持这个功能了. */
    /* 14 = NOP supported, 5=WCACHE enabled, 0=SMART feature set enabled */
    if (bdrv_enable_write_cache(s->bs))
        put_le16(p + 85, (1 << 14) | (1 << 5) | 1);
    else
        put_le16(p + 85, (1 << 14) | 1);
 
    memcpy(s->identify_data, p, sizeof(s->identify_data));
    s->identify_set = 1;
}

Cache 的工作模式

首先简单分析一下 guest 到 QEMU 的 I/O 执行路径:

下面的函数 bdrv_co_do_writev(bdrv_co_do_rw), 很好的阐释了 QEMU 怎么模拟 磁盘的 cache

static int coroutine_fn bdrv_co_do_writev(BlockDriverState *bs,
    int64_t sector_num, int nb_sectors, QEMUIOVector *qiov,
    BdrvRequestFlags flags)
{
    if (ret < 0) {
        /* Do nothing, write notifier decided to fail this request */
    } else if (flags & BDRV_REQ_ZERO_WRITE) {
        ret = bdrv_co_do_write_zeroes(bs, sector_num, nb_sectors);
    } else {
        ret = drv->bdrv_co_writev(bs, sector_num, nb_sectors, qiov);
    }
 
    /* enable_write_cache 为 false, 即 cache=writethrough或者directsync, 那么
     * 每次写完都执行一次 flush 操作, 保证数据的同步, 当然, 这样会损失很大的性能 */
    if (ret == 0 && !bs->enable_write_cache) {
        ret = bdrv_co_flush(bs);
    }
 
    return ret;
}
 
int coroutine_fn bdrv_co_flush(BlockDriverState *bs)
{
    int ret;
 
    /* 对于 unsafe 的cache, 任何时候都不需要把数据真正同步到磁盘 */
    if (bs->open_flags & BDRV_O_NO_FLUSH) {
        goto flush_parent;
    }
 
    BLKDBG_EVENT(bs->file, BLKDBG_FLUSH_TO_DISK);
    if (bs->drv->bdrv_co_flush_to_disk) {
        ret = bs->drv->bdrv_co_flush_to_disk(bs);
    } else if (bs->drv->bdrv_aio_flush) {
        BlockDriverAIOCB *acb;
        CoroutineIOCompletion co = {
            .coroutine = qemu_coroutine_self(),
        };
 
        acb = bs->drv->bdrv_aio_flush(bs, bdrv_co_io_em_complete, &co);
        if (acb == NULL) {
            ret = -EIO;
        } else {
            qemu_coroutine_yield();
            ret = co.ret;
        }
    }
 
flush_parent:
    return bdrv_co_flush(bs->file);
}

那么对于 guest side, 如果磁盘有 cache, 那么 guest 是如何保证同步到磁盘上的:

guest 会根据磁盘的 cache 情况指定相应的调度策略, 来把数据从内存同步到磁盘 中, 或者用户手动指定 fsync, 这时也会发生 flush.

关于 host 刷新的机制, 没有什么好说的, 无非就是调用系统调用 fdatasync, 如果系统不支持 fdatasync, 那么调用 fsync.

# hdparm -I /dev/sdb #cache=writeback or none
Commands/features:
    Enabled Supported:
       *    SMART feature set
       *    Write cache
       *    NOP cmd
       *    48-bit Address feature set
       *    Mandatory FLUSH_CACHE
       *    FLUSH_CACHE_EXT
# hdparm -I /dev/sdc #cache=writethrough or directsync
Commands/features:
    Enabled Supported:
       *    SMART feature set
            Write cache
       *    NOP cmd
       *    48-bit Address feature set
       *    Mandatory FLUSH_CACHE
       *    FLUSH_CACHE_EXT

测试

使用 ubuntu-13.04 server 版本, 内核版本号 3.8.0-19. 24 cores, 20GB的 memory.

PS: 这里只是比较几种 cache 模式的性能, 不能作为专业的测试结果.

FIO 配置:

[test]
rw=rw
rwmixread=50
ioengine=libaio
iodepth=1
direct=1
bs=4k
filename=/dev/sdb
runtime=180
group_reporting=1

Cache Mode Read IOPS Write IOPS
none 293/235/218/200/200 292/234/215/199/200
writethrough 5/36/72/93/113 5/35/73/93/113
writeback 1896/1973/1944/1627/2027 1894/1979/1947/1627/2023

writethough 的性能相当糟糕, 写的性能我还能理解, 读的性能完全不能理解, 仔细 想了一下, 应该是”混合读写”导致的, “写”拖慢了”读”, 我随后单独做了一个随机读 的测试, IOPS 达到 四千多, 这次应该是正常的.

结论

通过以上的分析, writethrough 的性能很差, 因为它几乎相当于 writeback + flush, writeback 的性能最好, 但是在掉电/迁移的情况下不能保证数据安全, none 的读性能 很差, 因为他完全绕过了 kernel 的 buffer.

总的来说, 选择 cache 模式的时候, 至少要考虑以下几种情况:

  • 你的存储是本地还是分布式的, 具体是那种存储, 支不支持 Direct IO(FUSE?), 一般来说网络文件系统使用 writethrough cache 性能很糟糕
  • 是否需要迁移, 有的 cache 模式迁移会导致数据丢失.

分类: QEMU, Storage 标签: ,

开始半深入研究存储体系

2013年9月2日 没有评论

我目前存储的知识仅仅来停留在大学时候阅读的 ext2 的源代码, 和刚来北京时候(没记错的话) 读的 深入 Linux 内核架构 里面的 VFS 上. 近来由于工作的原因, 研究 QEMU 的 相对多一些, 对 QEMU 内部的存储体系算是比较了解. 但是在实际业务中, 考虑的不仅仅是 底层 hypervise 的能力, 上层存储架构也很重要.

而我对上层存储的实践能力, 还仅仅在 LVM, NFS 这些比较普通的技术里, 对于时下热门的 Ceph, GlusterFS, GFS2 等, 只是 “精通这些存储技术的拼写”.

就此机会, 半深入的对这些技术进行一下研究, 相信以我已有的对于文件系统, I/O 等的基础, 实践这些技术并不难.

分类: Storage 标签:

LVM 杂记

2013年8月16日 没有评论

概念

  • PV(Physical Volume) 实际的物理卷轴, 在 LVM 的世界里, 一般是具体的硬盘或者某个硬盘分区
  • VG(Volume Group) 由多个 PV 共同组成一个 VG, 负责以 PE 作为单位来给 LV 分配磁盘资源
  • LV(Logical Volume) 最终用户看到, 能直接读写的设备, 类似于传统的硬盘分区, 只不过这个 LV 的大小可以根据使用自由的扩大和缩小
  • PE(Physical Extend) 是整个 LVM 最小的存储单位, 默认是 4M, 可以在创建 VG 的时候指定

使用流程

LVM 的使用流程很简单:

  • 首先是创建 PV, 根据前面所说的, 就是把硬盘或者硬盘分区编程 LVM 中的 PV
  • 然后是创建 VG, 在创建的时候先得至少指定一个 PV, 后续的可以通过 vgextend 添加
  • 然后再创建 LV, 创建完成后就可以像普通的硬盘分区一样的格式化, 挂载, 使用了

Use case

我有四个硬盘 /dev/vd{b,c,d,e}, 分别用他们作为 PV, 然后再其上创建 VG, 然后在创建 LV, 并使用之.

首先创建 PV:

root@kvm:~# pvcreate /dev/vd{b,c,d,e}
  Writing physical volume data to disk "/dev/vdb"
  Physical volume "/dev/vdb" successfully created
  Writing physical volume data to disk "/dev/vdc"
  Physical volume "/dev/vdc" successfully created
  ......

显示 PV

root@kvm:~# pvscan 
  PV /dev/vdb   VG lvmvg           lvm2 [20.00 GiB / 20.00 GiB free]
  PV /dev/vdc   VG lvmvg           lvm2 [20.00 GiB / 20.00 GiB free]
  PV /dev/vdd                       lvm2 [20.00 GiB]
  PV /dev/vde                       lvm2 [20.00 GiB]
  Total: 4 [79.99 GiB] / in use: 2 [39.99 GiB] / in no VG: 2 [40.00 GiB]

用 vdb 创建 VG(lvmvg)

root@kvm:~# vgcreate lvmvg /dev/vdb
  Volume group "lvmvg" successfully created

把 vdc 加入到 lvmvg

root@kvm:~# vgextend lvmvg /dev/vdc   
  Volume group "lvmvg" successfully extended

创建 LV(lvmlv)

root@kvm:~# vgdisplay | grep Free
  Free  PE / Size       10238 / 39.99 GiB //10238 个 PE, 每个 4M
root@kvm:~# lvcreate -l 2000 -n lvmlv lvmvg // 创建一个 8G(2000x4M) 的 LV
  Logical volume "lvmlv" created
root@kvm:~# lvs
  LV    VG    Attr     LSize Pool Origin Data%  Move Log Copy%  Convert
  lvmlv lvmvg -wi-a--- 7.81g

像普通磁盘分区一样使用这个 LV

root@kvm:~# lvdisplay  | grep Path
  LV Path                /dev/lvmvg/lvmlv
root@kvm:~# mkfs.ext4 /dev/lvmvg/lvmlv 
root@kvm:~# mkdir /mnt/lvm
root@kvm:~# mount /dev/lvmvg/lvmlv /mnt/
root@kvm:~# df -m | grep lvm
/dev/mapper/lvmvg-lvmlv      7747    18      7329   1% /mnt

给这个分区扩容

root@kvm:~# lvextend -l+1000 /dev/lvmvg/lvmlv   //增加 4G(1000 x4) 的容量
  Extending logical volume lvmlv to 11.72 GiB
  Logical volume lvmlv successfully resized
root@kvm:~# resize2fs /dev/lvmvg/lvmlv 
resize2fs 1.42.5 (29-Jul-2012)
Filesystem at /dev/lvmvg/lvmlv is mounted on /mnt; on-line resizing required
old_desc_blocks = 1, new_desc_blocks = 1
The filesystem on /dev/lvmvg/lvmlv is now 3072000 blocks long.
 
root@kvm:~# df -m | grep lvm
/dev/mapper/lvmvg-lvmlv     11685    20     11108   1% /mnt

分类: Storage 标签: