磁盘 分区 lvm之间await util的统计关系

最近的项目需要监控机器的IO负载, 提到IO负载,首当其冲的当然是await util这两个指标。
util: 过去的一段时间内,设备处理IO请求的时间占总时间的百分比。
await: 一个请求在IOscheduler里排队时间加上物理设备处理时间 (一个IO请求从通用块设备层提交到IOscheduler时开始计算,到底层处理完这个请求再次返回到通用块层的时间差)

  • iostat和/proc/diskstats

常见的iostat sar等工具都提供了这两个指标,当然它们提供的都是一段时间的平均值。但iostat只是负责换算,并不负责这些统计数据的采集。
IO栈在处理IO请求时,采集这些统计数据。/proc/diskstats文件中以行为单位展示了每个逻辑设备的统计信息。

总共14个字段,解释如下:
第01列 : 主设备号major
第02列 : 次设备号minor
第03列 : 设备名name
第04列 : 读请求完成总数rio
第05列 : 合并读请求总数rmerge
第06列 : 读扇区总数rsect
第07列 : 读数据花费的时间rticks,单位是ms
第08列 : 写请求完成总数wio
第09列 : 合并写请求总数wmerge
第10列 : 写扇区总数wsect
第11列 : 写数据花费时间wticks,单位是ms
第12列 : 正在进行I/O数inFlight
第13列 : IO花费时间ioticks,单位ms
第14列 : IO花费时间time_in_queue,单位ms(加了权重的)
类似iostat的工具其实都是读取这个文件再经过计算后得出磁盘的util await iops 吞吐这些信息。这个文件所有的统计结果都是累加的,
因此iostat至少需要采集两次才能计算。以rio为例,(rio1 - rio0) / interval(采集间隔 s)  就是过去interval时间 平均每秒读iops。

先给出iostat计算await util的算法。iostat代码就省略了,不是重点。
await = ((wticks1 - wticks0) + (rticks1 - rticks0)) / ((rio1 + wio1) - (rio0 + wio0))
就是采集两次diskstats,读写总共花费的时间 / 读写请求完成个数 = 每个请求平均花费的时间即await.

util = (ioticks1 - ioticks0) / interval
采集两次diskstats,interval是两次采集的时间间隔。很好理解,过去的interval毫秒里,有多少毫秒在处理IO。占比就是磁盘的繁忙程度即util。

本文的重点是磁盘 分区 lvm间await util的统计关系。以我的虚拟机为例:有块物理盘vdb,分成了两个分区vdb1 vdb2。vdb1 vdb2又组成vg后全部分给了逻辑卷dm-0。
上面我们看到vdb vdb1 vdb2 dm-0在diskstats中都有独立的统计。那么当我读写vdb2时,会更新vdb1的统计吗?会更新vdb吗?读写dm-0呢?

从上面iostat的分析我们发现,await util这两个指标的计算跟(w/r)ticks  (w/r)io  ioticks这几个统计数据相关。下面分析下内核代码,答案自然就浮现出来了。

  • 代码分析(内核版本:2.6.28)

这里直切主题,IO栈的相关介绍请查阅其他文章。

先看两个结构体:

struct disk_stats:存储每个块设备的IO统计数据。diskstats里统计相关的除了inFlight都在这里。
后面就用这个结构来描述,请自行对应到diskstats里的列。

struct disk_stats {
    unsigned long sectors[2];   /* READs and WRITEs */
    unsigned long ios[2];
    unsigned long merges[2];
    unsigned long ticks[2];
    unsigned long io_ticks;
    unsigned long time_in_queue;
};

struct hd_struct:分区结构体。一个物理盘会有多个分区,每个分区由一个hd_struct。包括物理盘自身也对应一个hd_struct。
struct hd_struct {
    sector_t start_sect;
    sector_t nr_sects;
    struct device __dev;
    struct kobject *holder_dir;
    int policy, partno;     //partno为分区编号,磁盘自身该值为0
    ...
    unsigned long stamp;    //一个时间戳,统计ioticks时用到
    int in_flight;          //该分区当前有多少个请求正在处理。对应diskstats里的inFlight
#ifdef  CONFIG_SMP
    struct disk_stats *dkstats; //包含一个disk_stats存储统计信息
#else
    struct disk_stats dkstats;
#endif
    struct rcu_head rcu_head;
};

通用块设备层向IOscheduler提交IO请求时,需要把struct bio转换成struct request。然后调用IOscheduler队列的入队函数将request push进等待队列。入队函数注册为__make_request。

static int __make_request(struct request_queue *q, struct bio *bio)
{
    struct request *req;
get_rq:
    ...
    req = get_request_wait(q, rw_flags, bio);  //根据bio创建request
    ...
    init_request_from_bio(req, bio);        //初始化request
    ...
    add_request(q, req);                //将request add进queue
    ...
end_io:
    bio_endio(bio, err);
    return 0;
}

__make_request简单来说就是根据传入的bio,首先判断能否merge,能则merge。否则创建新的request。然后将request 加进queue里。
当然merge部分的代码被省略了。
跟await相关的代码封装在init_request_from_bio里,看看:

void init_request_from_bio(struct request *req, struct bio *bio)
{
    ....
    req->errors = 0;
    req->hard_sector = req->sector = bio->bi_sector;
    req->ioprio = bio_prio(bio);
    req->start_time = jiffies;      //这个请求push进queue时的jiffies
    blk_rq_bio_prep(req->q, req, bio);
}

init_request_from_bio在请求进入queue之前用req->start_time记录当前时间戳,等底层执行完该请求的时刻跟req->start_time求差,不就是该请求的await么。
但内核统计是以分区为单位的,所以只是将时间差累加到分区对应的disk_stats.ticks里。由iostat再去算出平均的await。
跟util相关的代码在add_request里。

static inline void add_request(struct request_queue *q, struct request *req)
{
    drive_stat_acct(req, 1);
    __elv_add_request(q, req, ELEVATOR_INSERT_SORT, 0);
}

static void drive_stat_acct(struct request *rq, int new_io)
{
    struct hd_struct *part;
    int rw = rq_data_dir(rq);
    int cpu;

    cpu = part_stat_lock();
    part = disk_map_sector_rcu(rq->rq_disk, rq->sector);    //用req找到分区part

    part_round_stats(cpu, part);        //更新分区统计
    part_inc_in_flight(part);

}

drive_stat_acct函数里,通过req找到分区part,然后更新分区的统计数据。
其中part_round_stats函数更新disk_stats.io_ticks。part_inc_in_flight函数更新hd_struct.in_flight。

这两个指标还是息息相关的。通用块设备层每向下层下发一个request就给hd_struct.in_flight++,
底层每完成一个request,相应hd_struct.in_flight--。这样in_flight就代表当前有多少个请求正在处理。

而disk_stats.io_ticks的算法是:每次下发request或者request完成时,检查hd_struct.in_flight。如果hd_struct.in_flight=0,则认为设备这段时间空闲,否则(只要不是0,不管有多少request正在处理)就认为设备繁忙。这段时间怎么表示?上面已提到。用hd_struct.stamp记录。看下代码:

void part_round_stats(int cpu, struct hd_struct *part)
{
    unsigned long now = jiffies;    //获取当前时间戳

    if (part->partno)       //如果是分区,则同步更新主分区即物理盘的统计。
        part_round_stats_single(cpu, &part_to_disk(part)->part0, now);
    part_round_stats_single(cpu, part, now);   //更新io_ticks
}

static void part_round_stats_single(int cpu, struct hd_struct *part,
                    unsigned long now)
{
    if (now == part->stamp)
        return;

    if (part->in_flight) {          //in_flight非0,需要更新。
        __part_stat_add(cpu, part, time_in_queue,
                part->in_flight * (now - part->stamp)); //将(now - part->stamp)*in_flight 累加到hd_struct.disk_stats.io_ticks上
        __part_stat_add(cpu, part, io_ticks, (now - part->stamp)); //将(now - part->stamp)累加到hd_struct.disk_stats.io_ticks上
    }
    part->stamp = now;      //stamp更新为当前时间
}
如果该设备是分区,同步更新其物理盘分区。in_flight非0时更新io_ticks和time_in_queue。time_in_queue累加时乘了in_flight。所以是加了权重的IO花费时间。

part_inc_in_flight函数就负责in_flight的自增了。
static inline void part_inc_in_flight(struct hd_struct *part)
{
    part->in_flight++;
    if (part->partno)       //物理盘同步自增
        part_to_disk(part)->part0.in_flight++;
}

前面把request进入队列时的代码分析了,作为呼应,贴出request完成时的代码。
请求完成的函数栈也是很长,大概是scsi_softirq_done->...->blk_end_request->blk_end_io->end_that_request_last。

static void end_that_request_last(struct request *req, int error)
{
    struct gendisk *disk = req->rq_disk;
    ...
    if (disk && blk_fs_request(req) && req != &req->q->bar_rq) {
        unsigned long duration = jiffies - req->start_time;   //完成时间-请求时间=该请求的await
        const int rw = rq_data_dir(req);
        struct hd_struct *part;
        int cpu;

        cpu = part_stat_lock();
        part = disk_map_sector_rcu(disk, req->sector);

        part_stat_inc(cpu, part, ios[rw]);              //该分区完成读/写请求数+1
        part_stat_add(cpu, part, ticks[rw], duration);  //单个请求的await累加到分区的统计里。
        part_round_stats(cpu, part);                   //更新disk_stats.io_ticks
        part_dec_in_flight(part);                   //hd_struct.in_flight--

        part_stat_unlock();
    }

    ...
}

#define part_stat_inc(cpu, gendiskp, field)             \
    part_stat_add(cpu, gendiskp, field, 1)

#define part_stat_add(cpu, part, field, addnd)  do {            \   //addnd为1
    __part_stat_add((cpu), (part), field, addnd);               if ((part)->partno)                             __part_stat_add((cpu), &part_to_disk((part))->part0,    \       //物理盘同步+1
                field, addnd);              } while (0)

这里除了part_stat_inc宏,其他代码跟前面的呼应,不重复展开了。每完成一个request,在part_stat_inc里将hd_struct.disk_stats.ios+1。对应到diskstats里就是wio rio了。当然物理盘也同步+1。

至此,与await util相关的统计指标都分析了。
util不管有多少请求在处理,只要in_flight非0,就认为磁盘忙碌。也就解释了很多博客都强调util 100%磁盘并不一定真的忙碌。

同时,分区和物理盘的统计关系也清晰了。更新分区统计时也会同步更新物理盘。所以物理盘的统计是其所有分区之和。

再分析下lvm。
lvm是靠Device Mapper实现的,有自己的hd_struct。其mapped_device只是一个逻辑设备,上层对lvm发起的IO请求,最终被转发到物理设备处理。
因此mapped_device的入队函数被注册为dm_request。dm_request做两件事,更新自己的IO统计,转发请求。

static int dm_request(struct request_queue *q, struct bio *bio)
{
    int r = -EIO;
    int rw = bio_data_dir(bio);
    struct mapped_device *md = q->queuedata;
    int cpu;

    cpu = part_stat_lock();
    part_stat_inc(cpu, &dm_disk(md)->part0, ios[rw]);       //更新统计
    part_stat_add(cpu, &dm_disk(md)->part0, sectors[rw], bio_sectors(bio));
    part_stat_unlock();
    ...
    r = __split_bio(md, bio);
    up_read(&md->io_lock);
    return 0;
}

static int __split_bio(struct mapped_device *md, struct bio *bio)
{
    struct clone_info ci;
    int error = 0;
    ...
    start_io_acct(ci.io);       //更新统计
    while (ci.sector_count && !error)
        error = __clone_and_map(&ci);

    /* drop the extra reference count */
    dec_pending(ci.io, error);
    dm_table_put(ci.map);

    return 0;
}

static void start_io_acct(struct dm_io *io)
{
    struct mapped_device *md = io->md;
    int cpu;
    io->start_time = jiffies;
    cpu = part_stat_lock();
    part_round_stats(cpu, &dm_disk(md)->part0);
    part_stat_unlock();
    dm_disk(md)->part0.in_flight = atomic_inc_return(&md->pending);
}

可以看出,lvm虽然是逻辑设备。但是IO统计是独立的。因此读写lvm时,首先更新lvm的IO统计,请求被转发到分区时更新该分区的IO统计,当然该分区所属的物理盘也会更新。

  • 总结:

读写lvm,更新lvmIO统计 更新该请求所属分区的IO统计(lvm可能由多个分区组成)  更新物理盘IO统计。
读写分区,更新分区IO统计  更新物理盘IO统计。
读写盘,只更新物理盘IO统计。

如有不对,请指正。

时间: 2024-11-04 14:56:44

磁盘 分区 lvm之间await util的统计关系的相关文章

Linux磁盘分区,目录树,文件系统的关系(转)

研究了很久,自始至终不能够从三者的区别和联系中找到一个大脑与这些概念之间合适的相处方式.对于基本概念和理论理解不到位,在工作之中会走很多弯路和犯很多错误.今天花一天的时间,终于对三者的区别和联系有了更进一步的理解,特此记录并分享之,供大家探讨交流. (一)磁盘分区 首先要明白的是磁盘为什么要分区. 一是从数据安全方面考虑,二是从系统访问磁盘的性能考虑.一个磁盘的某个分区损坏,不在该分区的数据将不会受到影响,这样就能够有效地保护不同业务的数据.过大的磁盘分区在系统进行读写的时候,会降低系统的读写性

Linux磁盘分区与LVM详解

内容大纲: 一.磁盘分区是怎样表示的? 二.Linux磁盘分区与文件系统类命令 三.LinuxLVM逻辑卷管理 四.磁盘分区相关命令操作演示 五.磁盘分区及LVM操作演示 一.磁盘分区是怎样表示的? IDE磁盘的设备文件采用/dev/hdx 来命名,分区则采用/dev/hdxy来命名,其中想表示磁盘(a是第一块磁盘,b是第二块磁盘,以此类推),与代表分区的号码(由1开始,1,2,3,以此类推) SCSI设备和分区采用/dev/sdx和/dev/sdxy来命名(x和y的命名规则与IED磁盘命名规则

磁盘分区及LVM

###1.磁盘分区####fdisk /dev/磁盘fdisk /dev/vdbWelcome to fdisk (util-linux 2.23.2). Changes will remain in memory only, until you decide to write them.Be careful before using the write command. Device does not contain a recognized partition tableBuilding a

Debian 7.x 安装教程、网络配置、软件源配置、磁盘分区、LVM、U盘安装、网络安装

目录 一.准备安装Debian系统      1.1Debian简介          1.1.1介绍Debian版本          1.1.2Debian的正式发音          1.1.3Debian软件包管理          1.1.4Debian分支          1.1.5官方网站与文档          1.1.6介绍  Debian 7.1  1.2获得Debian发行版          1.2.1从镜像站点上下载ISO的镜像文件          1.2.2将IS

磁盘分区管理机制之----LVM

不知道朋友们有没有经历过这样的事情,你在一开始规划主机的时候只给了/home 一定大小的空间,然而随着用户数以及不断地往里面存放数据,该目录的空间已经不够了,那么你会怎么办呢? 可能你会选择重新搞块更大的磁盘进行格式化,然后将/home下的数据完全拷过去之后将这块新的磁盘挂载到/home目录下,但是你不觉得这样子会很麻烦么,没错,LVM就是这样一个能够帮你解决此问题的一个磁盘管理机制,下面就让我们来好好了解它吧. LVM的介绍 LVM是逻辑盘卷管理(Logical Volume Manager)

Red Hat Enterprise 6.5磁盘分区,LVM管理及磁盘配额设置

Linux磁盘分区.LVM管理及磁盘配额设置第一部分:磁盘分区 为虚拟机添加一块新的磁盘,重启生效. 管理新添加的sdb磁盘.为其创建不同类型的分区,ext4.fat.及swap.2.1创建第一个主分区2.2创建第二个主分区,方法相同. 2.3将剩下的都划分为扩展分区(逻辑分区需要在扩展分区基础上建立) 2.4在扩展分区内划出两个逻辑分区2.5创建好分区之后,我们需要根据需求改变分区的类型,fat类型分区更改方法(方法同样适用于主分区更改,此处用逻辑分区作例.)2.6变更sdb6为swap分区,

Linux 磁盘分区、永久挂载、创建LVM逻辑卷

实验项目: 1掌握管理Linux磁盘和分区的方法 2掌握挂载并卸载文件系统的方法 3掌握创建并管理LVM分区的方法 理论部分:一:磁盘和分区简介 1磁盘分区的表示:常见的硬盘可以划分为主分区.扩展分区.和逻辑分区.通常主分区只有4个,而扩展分区看成一个特殊的主分区类型,在扩展分区可以建立逻辑分区2现在用的硬盘都是SCSI硬盘,所以在这里主要介绍SCSI硬盘分区的结构.对于SCSI接口的硬盘表示方式是:第一块SCSI硬盘我们可以表示为"sda",第二块SCSI硬盘表示为"sdb

主机规划与磁盘分区

!根目录是一定要挂载到某个分区下的 !linux主机也是可以充当路由器实现NAT功能的,直是耗电能力比路由器大得多 分区的时候,只分出/和swap分区,但是会有个问题,如果根目录所在的分区出现坏道,可能会导致整个根目录损毁. 在linux的环境中,以下几个目录是容量比较大且读写频繁的: / /usr /var /home Swap 可以将这些独立挂载到单独分区,这样,一个分区出现故障,不会影响其他的分区. 硬盘分区设备号 sata为例: /dev/sda1 /dev/sda2 /dev/sda3

linux磁盘分区详解【转】

本文装载自:http://blog.csdn.net/aaronychen/article/details/2270048#comments 在学习 Linux 的过程中,安装 Linux 是每一个初学者的第一个门槛.在这个过程中间,最大的困惑莫过于给硬盘进行分区.虽然,现在各种发行版本的 Linux 已经提供了友好的图形交互界面,但是很多的人还是感觉无从下手.这其中的原因主要是不清楚 Linux 的分区规定,以及它下面最有效的分区工具― Fdisk 的使用方法. 首先我们要对硬盘分区的基本概念