[转帖]磁盘的排队论的理论和实践

磁盘,排队,理论,实践 · 浏览次数 : 0

小编点评

**M/M/1队列的统计** **平均响应时间** * 0.0001sc: 平均响应时间0.0001s * 0.00014s: 平均响应时间0.00014s * 0.000067sc: 平均响应时间0.000067s * 0.000109s: 平均响应时间0.000109s **队列长度的统计** * 16:队列长度的最小值 * 256:队列长度的最大值 **其他统计** * svctm: queue长度的均值 * svctm_histogram: queue长度的分布 * svctm_percentile: queue长度的百分位 **关于队列长度的统计** 队列长度的统计可以帮助我们理解队列的性能。队列长度的最小值代表队列可以处理的最少请求。队列长度的最大值代表队列可以处理的最多请求。队列长度的均值和分布可以帮助我们了解队列的平均性能。

正文

https://zhuanlan.zhihu.com/p/138887556

 

队列广泛应用在性能分析领域, 通过观察队列可以知道当时系统的繁忙程度和请求的延时, 甚至可以用排队论去做容量规划等. 对存储有一定了解的同学都或多或少听说过, 当iostat的util大于70%以后, 响应时间会如下图所示大幅升高, 但是用fio去压测的时候, 观测的结果却并不相同. 这是为什么呢?

 

 

IOSTAT

相信大家都有这样的经历, 系统卡顿时马上运行iostat来看一下是否慢I/O导致.

 

 

除了和I/O直接相关的几个统计数据之外, iostat提供了这些通用的队列统计:

  • util. 队列的利用率, 这是一个非常重要的指标, util太高往往会影响到延时
  • svctm. 请求处理时间, 也就是请求使用资源的时间, 不包括排队时间
  • await. 请求的处理时间, 包括请求在队列的时间以及处理时间
  • avgqu-sz. 平均队列长度, 越大意味着排队时间越长
  • iops. 也就是r/s+w/s, 表示每秒的请求个数, 在排队论中用λ表示

统计值应该是多少

先抛开具体实现, 我们先看一下iostat的输出应该是怎么样的. 下图中请求使用黑色的横线表示, 两端分别是进队列时间和请求完成时间, 这里没有画出请求的处理时间.

 

  • util的计算. 有请求的时间除以总时间
    • 第0秒. 100%的util, 任何时间点都有请求
    • 第1秒. 应该是100%的util, 任何时间都有请求.
    • 第2秒, 应该是100%的util, 任何时间都有请求
    • 第3秒, <100%的util, 队列有空闲
  • avgqu-sz. 时间按队列长度的加权平均
    • 第0秒. 1 x t1 + 2 x t2 + 3 x t3 + 2 x t4 + 1 x t5. 可以看出等于3个请求的处理时间之和
    • 第1秒. 应该是1, 每个时间点都刚好有且仅有一个请求
    • 第2秒. 应该是1, 每个时间点都刚好有且仅有一个请求
    • 第3秒. <1

内核实现

iostat的实现分2部分:

  • 内核会在特定点上收集队列情况, 特别是request进入队列和完成时会更新统计信息
  • 用户态程序iostat读取2次统计并进行计算

内核使用disk_stats结构来记录队列信息, 暂且不区分读写I/O, 可以简化如下:

struct queue_stat {
  uint64_t ios;  // 完成的IO个数
  uint64_t ticks;  // 所有req花费的时间总和, 用于计算await
  uint64_t io_ticks;  // 队列非idle状态的时间, 用于计算util
  uint64_t time_in_queue;  // 按队列长度加权的时间, 用于计算avgqu-sz
  uint64_t stamp;  // stat最后更新的时间
};

void enqueue(req) {
  // part_round_stats()
  if (in_flight) {
    time_in_queue = in_flight * (now - stamp);
    io_ticks = now - stamp;
  }
  stamp = now;

  in_flight++;
}

void dequeue(req) {
  ios++;
  ticks += now - req->start_time;

  // part_round_stats()
  time_in_queue += in_flight * (now - stamp);
  io_ticks = now - stamp;
  stamp = now;

  in_flight--;
}

如果只在request enqueue/dequeue的时候更新统计信息, 那么对于上图第1秒的统计来说, 因为后面的那个I/O并没有结束所以统计不到, 导致的明显问题就是util在第1秒的时候小于100%, 而第2秒的util因为该I/O又大于100%, 所以当用户态程序iostat去读取比如/proc/diskstats的时候, 内核会通过part_round_stats()补上对应统计. 注意: 当用户态iostat进程去读取diskstats文件时, 内核是能够感知这个操作并进行订正. 如果队列统计信息是在用户态收集, 并通过共享内存方式供类似iostat的进程访问, 由于没有这个订正操作, 就会出现大于100%的情况.

计算公式

iops = ios / duration;
avgqu-sz = time_in_queue / duration;
await = ticks / ios;
util = io_ticks / duration;

以图中第0秒为例, ticks和time_in_queue表示的都是该区间内所有I/O花费的时间之和, 也就是说如果所有I/O都没有跨越时间边线的话:

avgqu-sz = time_in_queue / duration
         = ticks / duration
         = (ticks / ios ) * (ios / duration)
         = await * iops

这就是对Little's Law的直观印象. 那么在await已经记录的情况下, 真的需要再去统计time_in_queue用于avgqu-sz的计算吗? 注意上面说的part_round_stats()订正手段, 这是和计算time_in_queue一致的.

另外我们可以看一下平均服务时间service time怎么计算而得:

svctm = io_ticks / ios
      = (io_ticks / duration) / (ios / duration)
      = util / iops

队列实践

为了更精确地了解队列的运行情况, 下面我们会实现一个生产者消费者队列

  • 生产者生成的request里面指定预期的执行时间
  • 消费者根据request里指定的时间, 经过换算后得到最终的执行时间. 因为我们要实现svctm的不同分布, 所以真正的执行时间和request的值可以不同, 但符合一定的规律
  • 队列会严格控制最大长度, 在测试开始的时候会进行设置
  • 每个生产者/消费者都是一个独立的线程
  • 队列通过mutex来实现并发访问
  • 由于生产者逻辑简单, 即使只有一个也不会成为瓶颈, 所以我们只使用一个生产者线程
  • 当需要唤醒消费者的时候, 使用notify_all唤醒所有消费者, 确保消费者的并发度

单消费者

请求匀速到达

首先我们测试一下在请求匀速到达的情况:

  • 生产者根据iops匀速生成相同svctm的req
  • 消费者运行req指定的svctm时间
void produce_uniform(int iops, int svctm) {
  req.svctm = svctm;
  for (int i = 0; i < iops; ++i) {
    queue_.enqueue(req);
    busywait(US_P_S / iops);  // waiting
  }
}

void consume() {
  while (!done) {
    queue_.dequeue(req);
    busywait(req.svctm);  // working
  }
}

我们以如下进行如下测试:

for (int qdepth: {16})
for (int iops : {10000})
for (int svctm : {50, 70, 80, 90, 95, 98, 100}) { do_test(); }

 

 

  • 在10000 iops, 50us svctm的时候, 我们期待的util是50%, 但是这里是57.69%, 这是因为我们模拟实现上的误差
  • 在95us svctm的时候, 统计到的util已经达到99.14%, 但是此时avgqu-sz也才1.00, req的响应时间(await)和svctm非常接近, 并没有形成积压. 在HDD的年代, 一般认为util达到70%及以上的时候, 磁盘的响应时间会迅速提高, 但是上面的测试中并没有出现
  • 因为req是匀速进入的, 理论上10000 iops, 100us svctm都不会产生积压, 所以这个实验结果是基本可信的
  • 那为什么util和wait之间到底有什么关系?

请求泊松分布

真实的场景下I/O的到达时间是很难遵从均匀分布的, 一般可以认为遵从某种随机分布, 特别是泊松分布. 为了实现泊松分布, 我们直接计算下次发生的时间点. 如果事件到达符合泊松分布, 那么事件到达时间的间隔符合指数分布, Knuth早有算法生成, fio代码里面也使用该算法. 我们使用这种方法

void produce_fio_poisson(int iops) {
  req.svctm = svctm;
  std::random_device rd;
  std::mt19937_64 gen(rd());
  uint64_t FRAND64_MAX = -1UL;
  for (int i = 0; i < iops; ++i) {
    queue_.enqueue(req);
    uint64_t next = (int64_t)(US_P_S / iops) * -logf((gen() + 1.0) / (FRAND64_MAX + 1.0));
    busywait(next);
  }
}

 

 

  • await和avgqu-sz明显比uniform更高, 并且随着util增加, avgqu-sz增幅越来越大
  • 70% util看起来仍然不是转折点
  • 注意这里是M/G/1的队列, 根据 I/O: A Little Queuing Theory, RAID

 

 

  • 因为consume的时候, 请求处理时间完全一样, C=0, 所以队列长度是 u / (2 * (1 - u))
  • 这里可以提一下, 70%的来源是公式 u / (1 - u), 这里的u指的是utilization.

服务时间指数分布

现在我们把服务时间也调成指数分布

void consume_exp() {
  std::random_device rd;
  std::mt19937_64 gen(rd());
  std::exponential_distribution<> d(1);
    while (!done) {
    queue_.dequeue(req);
    svctm = d(gen) * req.svctm;
    busywait(svctm);
  }
}

 

 

  • 我们可以看到, 现在avgqu-sz随着util升高上升幅度更大了, 但是还没有达到u / (1 - u)

QDepth的影响

在上面的测试中我们可以看到max_inflight已经到16, 那么有很大可能某一时刻待处理请求是超过16的, 只是因为队列qdepth的约束而没有进入队列, 现在考虑调高qdepth到256

  • 很明显, 现在avgqu-sz和util呈现出近似u / (1 - u)的曲线
  • 这就是M/M/1队列, 也是文档里面提到最多的一种队列

M/M/1 queue

维基百科上M/M/1 queue的定义

An M/M/1 queue is a stochastic process whose state space is the set {0,1,2,3,...} where the value corresponds to the number of customers in the system, including any currently in service. - Arrivals occur at rate λ according to a Poisson process and move the process from state i to i + 1. - Service times have an exponential distribution with rate parameter μ in the M/M/1 queue, where 1/μ is the mean service time. - A single server serves customers one at a time from the front of the queue, according to a first-come, first-served discipline. When the service is complete the customer leaves the queue and the number of customers in the system reduces by one. - The buffer is of infinite size, so there is no limit on the number of customers it can contain.

服务时间Jitter

当存储只是HDD这种机械硬盘的时候, 它的行为是比较确定的. 但是当存储的实现越来越复杂的时候, 特别是软件的比重越来越大的时候, 请求的服务时间就不一定遵循指数分布了. 假设软件有一个bug, 会导致如下10%的请求处理时间明显更高, 我们来看一下结果.

void consume_jitter() {
  std::random_device rd;
  std::mt19937_64 gen(rd());
  std::exponential_distribution<> d(1);
  static uint64_t i = 0;
    while (!done) {
    queue_.dequeue(req);
    svctm = d(gen) * req.svctm;
    if (i++ % 100 < 10) {
        svctm += req.svctm * 5;
    }
    busywait(svctm);
  }
}

 

 

  • 其中, sum输出的是所有request的处理时间
  • 我们人为构造了一个短时的100% util的情况. 看到这几种结果, 我们能反过来推导出什么? 如果知道确定的produce/consume分布后, 我们能推导出什么?

多消费者

从上面的测试可以知道, 排队论是纯粹的数学问题. 对于数学问题, 都是由公式的, 所以不再通过程序模拟. 我们可以访问这个网站: 

这里有一个很常见的问题, 提升单核处理能力和多核那个效果更好

  • 处理同样40000 rps/iops
    • c=1, mu=50000. 平均响应时间0.0001s
    • c=4, mu=50000/4. 平均响应时间0.00014s
  • 处理同样35000 rps/iops
    • c=1, mu=50000. 平均响应时间0.000067s
    • c=4, mu=50000/4. 平均响应时间0.000109s

还有一些问题这里不再一一举例, 比如:

同样, 对于排队网络, 比如M/M/1, -/M/1也是个数学问题.

 

 

 

 

队列统计的作用

首先我们知道随机并不意味着没有规律, 不同的随机过程有着不同的统计规律, 如果我们知道队列到达和服务的统计规律, 那么数学上就可以预测队列的表现, 所以排队论可以用于容量规划和性能预测等, 只要能够找出相关的统计规律及其具体参数. 那么对于问题诊断来说, 队列统计能带来什么不能带来什么?

  • 队列统计可以给人非常直观的印象, 即使是只有平均值. 当util达到100%的时候, 虽然不代表系统已经是瓶颈了(比如SSD就是典型的多消费者, 有一个消费者在工作整个系统就是busy状态, 100% util的时候SSD可能仍有余力), 但是仍然有一定的指导作用. 当iops到达一个固定值的时候, 有可能是限流开始了. 它是梳理问题的第一步, 简单直接, 不一定对所有问题都有帮助, 但是对某一类问题有很好的效果
  • 了解自己系统的统计规律是一件非常值得做的事情, 定位问题更有方向性
  • 问题诊断往往在于发现异常. 随机不是异常, 我们要发现上面Jitter例子的抖动, 这种抖动不是因为随机过程导致的队列长度的变化, 更可能的是因为软硬件问题导致的svctm的变化. svctm能够获得第一手信息, 响应时间很多时候受持续事件的影响, 而持续时间在性能诊断时是相对容易的. 所以我们需要svctm, 不只是avg svctm, 我们需要svctm histogram/percentile, 如果可以, 甚至包括其随时间的分布
  • 我们软件上的队列qdepth往往是固定并且偏小的, 比如16. 这个时候request的排队长度是相对小的. svctm带来的变化有可能比排队带来更大的变化
  • 多队列情况下, 队列统计可以直观地比较前后队列的性能

引用

    

与[转帖]磁盘的排队论的理论和实践相似的内容:

[转帖]磁盘的排队论的理论和实践

https://zhuanlan.zhihu.com/p/138887556 队列广泛应用在性能分析领域, 通过观察队列可以知道当时系统的繁忙程度和请求的延时, 甚至可以用排队论去做容量规划等. 对存储有一定了解的同学都或多或少听说过, 当iostat的util大于70%以后, 响应时间会如下图所示

[转帖]TIDB-TIDB节点磁盘已满报警

一、背景 今日突然收到tidb节点的磁盘报警,磁盘容量已经超过了80%,但是tidb是不放数据的,磁盘怎么会满,这里就需要排查了 二、问题排查 解决步骤 1.df -h查看哪里占用磁盘比较多,然后通过du -h找到具体占用多的目录 2.最终发现tidb/tidb-deploy/tidb-4000/l

[转帖]Linux中查看各文件夹大小命令du -h --max-depth=1

https://www.cnblogs.com/the-tops/p/8798678.html 最近排查服务器异常的时候,常会遇到磁盘慢的情况,这个时候,查找那个文件夹占用的内存的时候常用到这个命令:du -h --max-depth=3 一般的文件夹都超不过4层; 具体使用的时候,可以根据当前路径

【转帖】MySQL索引

数据表如何用索引快速查找 索引是 排好序的快速查找的数据结构 索引存储在文件系统中 索引的文件存储形式与存储引擎有关 索引数据结构:可以是二叉树、红黑树、Hash表、B-Tree、B+Tree 1、二叉树 使用索引的如下图:(如果是使用二叉树结构)每一个节点都存放数据行的磁盘地址【快速定位到数据】

[转帖]磁盘的基准测试

https://www.jianshu.com/p/0e25657d016d 参考 摘抄自 对永久性磁盘的性能进行基准化分析 正文 如需对永久性磁盘的性能进行基准化分析,请使用 FIO,而不是 dd 等其他磁盘基准化分析工具。默认情况下,dd 使用非常低的 I/O 队列深度,因此难以确保基准生成足够

[转帖]磁盘的基准测试

https://www.jianshu.com/p/0e25657d016d 参考 摘抄自 对永久性磁盘的性能进行基准化分析 正文 如需对永久性磁盘的性能进行基准化分析,请使用 FIO,而不是 dd 等其他磁盘基准化分析工具。默认情况下,dd 使用非常低的 I/O 队列深度,因此难以确保基准生成足够

[转帖]磁盘的基准测试

https://www.jianshu.com/p/0e25657d016d 参考 摘抄自 对永久性磁盘的性能进行基准化分析 正文 如需对永久性磁盘的性能进行基准化分析,请使用 FIO,而不是 dd 等其他磁盘基准化分析工具。默认情况下,dd 使用非常低的 I/O 队列深度,因此难以确保基准生成足够

【转帖】磁盘IOPS的计算

计算磁盘IOPS的三个因素: 1、RAID类型的读写比 不同RAID类型的IOPS计算公式: RAID类型 公式 RAID5、RAID3 Drive IOPS=Read IOPS + 4*Write IOPS RAID6 Drive IOPS=Read IOPS + 6*Write IOPS RAI

[转帖]FIO磁盘性能测试工具

https://www.cnblogs.com/lyhabc/p/16708771.html 简介 一般我们测试硬盘或者存储的性能的时候,会用Linux系统自带的dd命令,因为是自带命令,简单易使用,因此一些客户喜欢使用dd命令来测试磁盘的读写性能。 但是用dd命令来测试性能,有如下问题: 1. d

[转帖]磁盘负载指标 %iowait, await, %util 的正确理解

说明 %iowait, await, %util 是用来衡量硬盘负载的三个指标, 但是这几个指标通常容易被误解, 实际上, 这三个指标单纯的高, 并不一定能说明相应的磁盘有问题或者有瓶颈, 而是需要结合具体执行 IO 操作的程序的执行方式, 综合的来判断指标高的原因. 关于 await, %util