我是靠谱客的博主 鲤鱼歌曲,最近开发中收集的这篇文章主要介绍iar最高优化模式选择size对代码的影响_优化innochecksum实战,四倍加速榨干NVMe SSD磁盘带宽...,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

2e2c140a8641699bcc1321ea306cab83.png

innodhecksum[1]是MySQL自带的offline工具bin,可检查innodb数据文件是否损坏。使用方式如下。

shell> bin/innochecksum [options] file_name

MySQL的系统表空间(ibdata)、用户表空间(*.ibd)、redo log(ib_logfile)等文件,结构类似,都由page构成,UNIV_PAGE_SIZE默认16KB,定义如下。了解更多可参考文章[2]或者innodb-java-reader工具[3]。

#define UNIV_PAGE_SIZE     (2 * 8192)

如下图所示,page前后是38字节的header和8字节的trailer。checksum校验算法会利用index4-26以及index38-16376这两部分计算。详细实现参考ut0crc32.cc[4]。

682254850471a896e08460d70045cfcc.png
图片一部分参考链接[5]

page的checksum算法有三种,参考官方文档[6]。

innodb
crc32
none

从MySQL 5.7.7版本之后crc32是默认的checksum算法。
crc32的校验算法有两种实现,如果CPU支持SIMD SSE4_2指令集,则硬件加速,直接inline汇编代码;如果不支持,fallback到软件算法实现。
innochecksum执行的逻辑很简单。

while (!feof(fil_in)) {
   bytes = read_file //read 16KB
   is_page_corrupted //using crc32/innodb/none algorithm
}

优化innochecksum前,先看看基准性能如何。环境如下。

  • CPU: Intel Xeon CPU E5-2682 v4 @ 2.50GHz(开启超线程,逻辑核数64)
  • 内存: 256G
  • NVMe SSD硬盘:PCIe 3.0 x4, NVMe, Intel® SSD DC P3600 Series, 1.6TB

Intel官方上查企业级DC P3600 1.6TB这块盘[7]。正面如下。

bc57f4bf43e10a9d9a2df9c446d1dd18.png

背面图如下,可以看到排列整齐的MLC NAND闪存颗粒[8]。

5fb979abfc1e885ade27d1b4792581f6.png

规格如下。官方资料,顺序写1600 MB/s,顺序读2600 MB/s。

d94b28fd55b976a14d8fa03b08c93a69.png

测试数据文件是TPC-H的LINEITEM,大小有72G左右。(dbgen -s 70生成后导入MySQL,文件较大,为了更好的观测)。
直接上图,MySQL自带的bin执行innochecksum -C crc32 file_name,带宽只有642 MB/s,远低于这块盘的顺序读吞吐限。

00f96f06d824bfaa4b8e67f19079648e.png

优化后的最优版本可以达到2503 MB/s,非常接近官方最高吞吐。
下面第一部分 测试NVMe SSD读写极限吞吐,第二部分 阐述优化实现方案。

1. 测试NVMe SSD读写极限吞吐

fio[9]测试读写吞吐,ioengine选择libaio。

fio写:

./fio --filename=test -iodepth=16 -ioengine=libaio -direct=1 -rw=write -bs=1m -size=512g -numjobs=8 -runtime=50 -group_reporting -name=test-write

write: bw=1543.2MB/s, iops=1543

fio读:

./fio --filename=test -iodepth=16 -ioengine=libaio -direct=1 -rw=read -bs=2m -size=512g -numjobs=8 -runtime=50 -group_reporting -name=test-read

read : bw=2382.8MB/s, iops=2382

测试结果基本符合官方的数据,但都差一些,所以手写代码再压测下,部分代码参考文章[10]。源码链接如下。

https://github.com/neoremind/io_benchmark​github.com

测试buffer io write-back写,代码如下。

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/stat.h> 
#include <vector>
#include <thread>
#include <fcntl.h>

#define WRITE_ONCE_BYTE_SIZE 4096

static char data[WRITE_ONCE_BYTE_SIZE] __attribute__((aligned(WRITE_ONCE_BYTE_SIZE))) = {'a'};
static const uint64_t kWriteCountPerThread = 1000 * 1000;
static const uint64_t kConcurrency = 64;
static const uint64_t kWriteBytesPerThread = sizeof(data) * kWriteCountPerThread;
static const uint64_t kTotalWriteBytes = kWriteBytesPerThread * kConcurrency;

uint64_t NowMicros() {
  struct timeval tv;
  gettimeofday(&tv, NULL);
  return static_cast<uint64_t>(tv.tv_sec) * 1000000 + tv.tv_usec;
}

void writer(int index) {
  std::string fname = "data" + std::to_string(index);

  int fd = ::open(fname.c_str(), O_NOATIME | O_RDWR | O_CREAT, 0644);

  int ret = posix_fallocate(fd, 0, kWriteBytesPerThread);
  if (ret != 0) { 
    printf("fallocate err %dn", ret);
  }
  lseek(fd, 0, SEEK_SET);

  for (int32_t i = 0; i < kWriteCountPerThread; i++) {
    ::write(fd, data, WRITE_ONCE_BYTE_SIZE);
  }

  close(fd);
}

int main() {
  uint64_t st, ed;
  st = NowMicros();
  std::vector<std::thread> threads;
  for(int i = 0; i < kConcurrency; i++) {
    std::thread worker(writer, i);
    threads.push_back(std::move(worker));
  }
  for (int i = 0; i < kConcurrency; i++) {
    threads[i].join();
  }
  ed = NowMicros();
  printf("time elapsed microsecond(us) %lld, %lld MB/sn", ed - st, kTotalWriteBytes / (ed - st));
  return 0;
}

64并发顺序写4k数据,1510 MB/s,CPU利用率非常高,vmstat显示bo存在抖动。个人分析原因,虽然使用fallocate预分配空间,减少了刷文件总metadata的overhead,但测试OS的内核不支持FALLOC_FL_ZERO_RANGE,还是会存在EXT4 extents filling zero的overhead(参考文章[11]);另外高并发的小IO写page cache,频繁刷脏页也加重了系统的负载。

procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
65  0      0 3618176 330256 247338112    0    0     0 1457344 152720 6513  0 96  4  0  0
65  0      0 3492712 330256 247428272    0    0     0 1315880 169502 4963  0 96  4  0  0
66  0      0 3545536 330256 247339136    0    0     0 1183212 180769 4215  0 96  4  0  0
65  0      0 3525324 330256 247325776    0    0     0 1309008 183833 4679  0 97  3  0  0
66  0      0 3590052 330256 247226944    0    0     0 1496924 176723 5951  0 97  3  0  0
57  0      0 3543704 330256 247167072    0    0     0 2441148 197181 7717  0 95  5  0  0
64  0      0 3565740 330256 247107824    0    0     0 1472412 170361 8038  0 94  6  0  0
55  0      0 3520116 330256 247132928    0    0     0 1457048 154563 9472  0 95  5  0  0
65  1      0 3549728 330256 247052544    0    0     0 1555564 133119 8827  0 92  8  0  0
63  0      0 3567856 330256 247002144    0    0     0 1343976 144924 7134  0 97  3  0  0

改成48并发反而带宽接近官方资料。

time elapsed microsecond(us) 123703672, 1589 MB/s

测试direct io write-through写,核心代码如下。64并发顺序写4k打不满,为保证数据不丢,通常会通过mmap拼接4k的数据到16k,尽量大块IO写,接近打满带宽,和fio结果相近。

char* base = nullptr;
char* cursor = nullptr;
uint64_t staging_offset = 0, file_offset = 0;

int staging_fd = ::open(staging_fname.c_str(), O_NOATIME | O_RDWR | O_CREAT, 0644);
int ret = posix_fallocate(staging_fd, 0, kStagingFileSize);
void* staging_ptr = mmap(NULL, kStagingFileSize, PROT_READ | PROT_WRITE,
        MAP_SHARED, staging_fd, 0);

int fd = ::open(fname.c_str(), O_DIRECT | O_NOATIME | O_RDWR | O_CREAT, 0644);
ret = posix_fallocate(fd, 0, kWriteBytesPerThread);
lseek(fd, 0, SEEK_SET);

base = static_cast<char*>(staging_ptr);
cursor = static_cast<char*>(staging_ptr);
for (int32_t i = 0; i < kWriteCountPerThread; i++) {
    if (unlikely(staging_offset >= kStagingFileSize)) {
        pwrite64(fd, staging_ptr, kStagingFileSize, file_offset);
        cursor = base;
        file_offset += kStagingFileSize;
        staging_offset = 0;
    }
    memcpy(cursor, data, 4096);
    cursor += 4096;
    staging_offset += 4096;
}

close(staging_fd);
close(fd);
munmap(staging_ptr, kStagingFileSize);
staging_ptr = nullptr;

结果如下。

time elapsed microsecond(us) 170181550, 1540 MB/s

测试buffer io读。核心代码如下。

int fd = ::open(fname.c_str(), O_NOATIME | O_RDWR, 0644);
void* buffer = NULL;
posix_memalign(&buffer, getpagesize(), kReadOnceByteSize);
uint64_t offset = 0;
for (int32_t i = 0; i < kReaderCountPerThread; i++) {
    pread64(fd, buffer, kReadOnceByteSize, offset);
    offset += kReadOnceByteSize;
}
close(fd);

64并发顺序读4k结果如下。

time elapsed microsecond(us) 109012309, 2404 MB/s

这里每次测试都需要清理page cache:echo 3 > /proc/sys/vm/drop_caches
测试direct io读。代码和上面类似,只需要打开文件的时候指定O_DIRECT。由于没有read ahead,所以带宽都在2000 MB/s以下,同样的解决思路读大块IO,56并发顺序读16k。

time elapsed microsecond(us) 90598873, 2531 MB/s

2. 优化innochecksum

首先分析`innochecksum -C crc32 file_name`,使用perf查看热点代码。

47e2e1fcdd86060a803d04baa389a885.png

42.58%在使用SIMD SSE4_2硬件加速计算checksum,34.18%是拷贝page cache数据到用户态,由于使用buffer io,同步阻塞式的从kernel space page cache拷贝小块IO 16k到user space,所以这部分开销不小。另外由于innochecksum是串行执行,CPU利用率很低。
时间消耗示意图如下。

7a37a51bbb920d148bfb8b143adb03ae.png

优化思路总结有两点。1)大块IO批量读,减少内核调用次数
打破page-at-a-time的读取模式,采用大块IO少量内核调用,缓存到用户态,由于内核IO path较长,尽量避免频繁陷入内核和数据拷贝,例如一次读16MB,与MonetDB/X100[12]提供的向量化(vectorized)执行有异曲同工之妙。同时整块的数据对于CPU的多级缓存架构也更友好。
时间消耗呈现如下图。

d130c82201412b338fcaadb62985ef36.png

2)IO计算分离
打破单线程串行执行的模式,最大化CPU利用率,避免low IPC(instructions-per-cycle)efficiency,也就是计算和IO不互相等待,模拟两级流水线,一个负责IO读,一个负责计算checksum。
在大块IO批量读的基础上,加上IO计算分离,时间消耗如下图。

a1c844098ea5afb80f264837c2c46f35.png

最终实现源码链接。

neoremind/fast_innochecksum​github.com
52e528f37b1762824ce7cdd0f5055d50.png

文件结构如下。fast_innochecksum.cpp是执行main入口,ut0crc32.*是从MySQL源码中拷贝出来计算checksum的工具类。

$ls
Makefile
fast_innochecksum.cpp
partition.cpp
partition.h
partition_scan_cache_pool.cpp
partition_scan_cache_pool.h
scanner.cpp
scanner.h
shard.cpp
shard.h
thread_pool.h
ut0crc32.cpp
ut0crc32.h
util.cpp
util.h

核心的流水线执行实现,在partition.cpp、partition_scan_cache_pool.cpp、shard.cpp三个文件中。
假设文件不再细分shard,整个文件就是一个shard,分多个shard是为了多线程执行,后面再介绍。shard再细分成partition,每个partition的大小是一次buffer io或者direct io读取的量,大块IO读取到内存中,缓存起来,由于内存有限,采用滑动窗口(sliding window)的思想,一个窗口内只含有限个partition。流水线执行(pipelining)需要两个线程,一个是IO线程,做prefetching,预读多个partition的数据到内存中缓存起来,供计算线程使用;另一个是计算线程,扫partition内存缓存的pages,计算checksum。
如下图所示,假设partition大小(代码中的kPartitionByteSize)64MB,则一次IO单元读取4000个page;滑动窗口大小(代码中的kNumOfScanCachePartition)3个,最多prefetching3个partition。窗口不断向文件后面移动,实现了IO计算分离的执行过程。

328cd9c906b95aaefef0f057ae2badf8.png

实际production环境,考虑到单个innodb文件都不会太大,可以配置更小的kPartitionByteSize,16MB也是可行的。
由于NVMe SSD出色的并发读特性,要想压榨磁盘到极限,就必须并行化。把一个文件分成若干个shard,大小配置是kShardByteSize,提交每个shard到线程池中,每个shard就可以独立的进行上面的过程。代码在scanner.cpp中。如下图所示,kShardByteSize=1G,线程池4个thread。

18d7167065bb4cfdca794f2a160efbb6.png

最终版本的时间消耗如下图。

c83d693b63fdf1dc20263c9f41047d8e.png

最终评测结果如下。MySQL innochecksum 642 MB/s,buffer io单线程IO计算分离的结果1109 MB/s,比例是57%,稍高于perf图中的实际纯计算开销42%,也比较合理。
采用不同的IO模型,direct io普遍优于buffer io,只要读取的内存是对齐的,就可以by pass pagecache,并且省了一次内存拷贝。
在采用direct io,16线程并发,shard 1G,partition 64MB的情况下可以达到2503 MB/s的读带宽,非常接近官方资料2600MB/s。

+-------------------------------------------+
|IO    |thread|Shard   |Partition|Throughput|
|mode  |count |ByteSize|ByteSize |          |
+-------------------------------------------+
|direct|   1  |  256MB |   16MB  | 2081 MB/s|
|direct|   1  |    1G  |   64MB  | 2173 MB/s|
|direct|  16  |  256MB |   16MB  | 2468 MB/s|
|direct|  16  |    1G  |   64MB  | 2503 MB/s|
|buffer|   1  |  256MB |   16MB  | 1086 MB/s|
|buffer|   1  |    1G  |   64MB  | 1109 MB/s|
|buffer|  16  |  256MB |   16MB  | 2342 MB/s|
|buffer|  16  |    1G  |   64MB  | 2386 MB/s|
+-------------------------------------------+
Note: kNumOfScanCachePartition=3

00f96f06d824bfaa4b8e67f19079648e.png

3. 思考

本次评测采用了PCIe 3.0 x4, NVMe, Intel® SSD DC P3600 Series, 1.6TB磁盘,这块盘读写测试,以及最终的benchmark都没有严格达到官方资料,差一点点,相比Intel Optane傲腾[13]系列要逊色不少。后续如果有条件,可以在傲腾上跑一跑。

本次评测主要集中在最大化吞吐,因此在IO模型上没有使用libaio,没有使用新内核版本提供的io_uring[14],这些技术可以进一步降低延迟,加大IOPS,不过本次重点还是在带宽上。

随着新硬件的不断发展,比如,RDMA、DPDK这些摒弃传统内核重IO path的kernel by pass网络传输技术,SPDK、3D XPoint NVMe SSD、NVMe on DIMM这些磁盘IO新驱动和新硬件,多核心CPU的普及,hardware-conscious design(NUMA-aware,cache-conscious等)越来越重要,很多传统的设计和数据结构并不是针对高性能硬件优化的,面向新硬件新技术重新设计,提高资源利用率,榨干硬件资源,才能进一步释放软件的性能和吞吐。本文就是在这样背景下的一次实践。

参考资料

[1] https://dev.mysql.com/doc/refman/5.7/en/innochecksum.html
[2] https://zhuanlan.zhihu.com/p/103582178
[3] https://github.com/alibaba/innodb-java-reader
[4] https://github.com/mysql/mysql-server/blob/8.0/storage/innobase/ut/crc32.cc
[5] https://blog.jcole.us/2013/01/03/the-basics-of-innodb-space-file-layout/
[6] https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_checksum_algorithm
[7] https://ark.intel.com/content/www/us/en/ark/products/80993/intel-ssd-dc-p3600-series-1-6tb-2-5in-pcie-3-0-20nm-mlc.html
[8] https://mp.weixin.qq.com/s/LeXFcQ6aLd-Cvm5o5BtEsw
[9] https://fio.readthedocs.io/en/latest/
[10] https://www.cnkirito.moe/linux-io-benchmark/
[11] https://zhuanlan.zhihu.com/p/61212603

[12] [MonetDB/X100: Hyper-Pipelining Query Execution](https://w6113.github.io/files/papers/monetdb-cidr05.pdf)
[13] https://www.intel.com/content/www/us/en/products/memory-storage/solid-state-drives/data-center-ssds/optane-dc-ssd-series.html
[14] https://zhuanlan.zhihu.com/p/62682475

最后

以上就是鲤鱼歌曲为你收集整理的iar最高优化模式选择size对代码的影响_优化innochecksum实战,四倍加速榨干NVMe SSD磁盘带宽...的全部内容,希望文章能够帮你解决iar最高优化模式选择size对代码的影响_优化innochecksum实战,四倍加速榨干NVMe SSD磁盘带宽...所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(45)

评论列表共有 0 条评论

立即
投稿
返回
顶部