plantegg

java tcp mysql performance network docker Linux

存储原理

本文记录各种存储、接口等原理性东西

img

img

磁盘信息

Here are some common ones:

  • hdX — ATA hard disk, pre-libata. You’ll only see this with old distros (probably based on Linux 2.4.x or older)
  • sdX — “SCSI” hard disk. Also includes SATA and SAS. And IDE disks using libata (on any recent distro).
  • hdXY, sdXY — Partition on the hard disk hdX or sdX.
  • loopX — Loopback device, used for mounting disk images, etc.
  • loopXpY — Partitions on the loopback device loopX; used when mounting an image of a complete hard drive, etc.
  • scdX, srX — “SCSI” CD, using same weird definition of “SCSI”. Also includes DVD, Blu-ray, etc.
  • mdX — Linux MDraid
  • dm-X — Device Mapper. Use -N to see what these are, or ls -l /dev/mapper. Device Mapper underlies LVM2 and dm-crypt. If you’re using either LVM or encrypted volumes, you’ll see dm-X devices.

回顾串行磁盘技术的发展历史,从光纤通道,到SATA,再到SAS,几种技术各有所长。光纤通道最早出现的串行化存储技术,可以满足高性能、高可靠和高扩展性的存储需要,但是价格居高不下;SATA硬盘成本倒是降下来了,但主要是用于近线存储和非关键性应用,毕竟在性能等方面差强人意;SAS应该算是个全才,可以支持SAS和SATA磁盘,很方便地满足不同性价比的存储需求,是具有高性能、高可靠和高扩展性的解决方案。

SSD中,SATA、m2、PCIE和NVME各有什么意义

高速信号协议

SAS,SATA,PCIe 这三个是同一个层面上的,模拟串行高速接口。

  • SAS 对扩容比较友好,也支持双控双活。接上SAS RAID 卡,一般在阵列上用的比较多。
  • SATA 对热插拔很友好,早先台式机装机市场的 SSD基本上都是SATA的,现在的 机械硬盘也是SATA接口居多。但速率上最高只能到 6Gb/s,上限 768MB/s左右,现在已经慢慢被pcie取代。
  • PCIe 支持速率更高,也离CPU最近。很多设备 如 网卡,显卡也都走pcie接口,当然也有SSD。现在比较主流的是PCIe 3.0,8Gb/s 看起来好像也没比 SATA 高多少,但是 PCIe 支持多个LANE,每个LANE都是 8Gb/s,这样性能就倍数增加了。目前,SSD主流的是 PCIe 3.0x4 lane,性能可以做到 3500MB/s 左右。

传输层协议

SCSI,ATA,NVMe 都属于这一层。主要是定义命令集,数字逻辑层。

  • SCSI 命令集 历史悠久,应用也很广泛。U盘,SAS 盘,还有手机上 UFS 之类很多设备都走的这个命令集。
  • ATA 则只是跑在SATA 协议上
  • NVMe 协议是有特意为 NAND 进行优化。相比于上面两者,效率更高。主要是跑在 PCIe 上的。当然,也有NVMe-MI,NVMe-of之类的。是个很好的传输层协议。

NVMe(Non-Volatile Memory Express)

NVMe其实与AHCI一样都是逻辑设备接口标准(是接口标准,并不是接口),NVMe全称Non-Volatile Memory Express,非易失性存储器标准,是使用PCI-E通道的SSD一种规范,NVMe的设计之初就有充分利用到PCI-E SSD的低延时以及并行性,还有当代处理器、平台与应用的并行性。SSD的并行性可以充分被主机的硬件与软件充分利用,相比与现在的AHCI标准,NVMe标准可以带来多方面的性能提升。

NVMe标准是面向PCI-E SSD的,使用原生PCI-E通道与CPU直连可以免去SATA与SAS接口的外置控制器(PCH)与CPU通信所带来的延时。而在软件层方面,NVMe标准的延时只有AHCI的一半不到,NVMe精简了调用方式,执行命令时不需要读取寄存器;而AHCI每条命令则需要读取4次寄存器,一共会消耗8000次CPU循环,从而造成大概2.5微秒的延迟。

NVMe标准和传统的SATA/SAS相比,一个重大的差别是引入了多队列机制

主机与SSD进行数据交互采用“生产者-消费者”模型进行数据交互。在原有AHCI规范中,只定义了一个交互队列,主机与HDD之间的数据交互只能通过一个队列通信,多核处理器也只能通过一个队列与HDD进行数据交互。在传统磁盘存储时代,单队列在一个IO调度器,可以很好的保证提交请求的IO顺序最优化。

而NAND存储介质具有很高的性能,AHCI原有的规范不再适用,NVMe规范替代了原有的AHCI规范,在软件层面的处理命令也进行了重新定义,不再采用SCSI/ATA命令规范集。相比以前AHCI、SAS等协议规范,NVMe规范是一种非常简化,面向新型存储介质的协议规范。该规范将存储外设拉到了处理器局部总线上,性能大为提升。并且主机和SSD处理器之间采用多队列的设计,适应了多核的发展趋势,每个处理器核与SSD之间可以采用独立的硬件Queue Pair进行数据交互。

img

如上图从软件的角度来看,每个CPU Core都可以创建一对Queue Pair和SSD进行数据交互。Queue Pair由Submission Queue与Completion Queue构成,通过Submission queue发送数据;通过Completion queue接受完成事件。SSD硬件和主机驱动软件控制queue的Head与Tail指针完成双方的数据交互。

nvme多队列

img

img

物理接口

M.2 , U.2 , AIC, NGFF 这些属于物理接口

像 M.2 可以是 SATA SSD 也可以是 NVMe(PCIe) SSD。金手指上有一个 SATA/PCIe 的选择信号,来区分两者。很多笔记本的M.2 接口也是同时支持两种类型的盘的。

  • M.2 , 主要用在 笔记本上,优点是体积小,缺点是散热不好。
  • U.2,主要用在 数据中心或者一些企业级用户,对热插拔需求高的地方。优点热插拔,散热也不错。一般主要是pcie ssd(也有sas ssd),受限于接口,最多只能是 pcie 4lane
  • AIC,企业,行业用户用的比较多。通常会支持pcie 4lane/8lane,带宽上限更高

SSD 的性能特性和机制

SSD 的内部工作方式和 HDD 大相径庭,我们先了解几个概念。

单元(Cell)、页面(Page)、块(Block)

当今的主流 SSD 是基于 NAND 的,它将数字位存储在单元中。每个 SSD 单元可以存储一位或多位。对单元的每次擦除都会降低单元的寿命,所以单元只能承受一定数量的擦除。单元存储的位数越多,制造成本就越低,SSD 的容量也就越大,但是耐久性(擦除次数)也会降低。

一个页面包括很多单元,典型的页面大小是 4KB,页面也是要读写的最小存储单元。SSD 上没有“重写”操作,不像 HDD 可以直接对任何字节重写覆盖。一个页面一旦写入内容后就不能进行部分重写,必须和其它相邻页面一起被整体擦除重置。

多个页面组合成块。一个块(Block)的典型大小为 512KB 或 1MB,也就是大约 128 或 256 (Page–16KB)页。块是擦除的基本单位,每次擦除都是整个块内的所有页面都被重置。

image-20210915090731401

擦除速度相对很慢,通常为几毫秒。所以对同步的 IO,发出 IO 的应用程序可能会因为块的擦除,而经历很大的写入延迟。为了尽量地减少这样的场景,保持空闲块的阈值对于快速的写响应是很有必要的。SSD 的垃圾回收(GC)的目的就在于此。GC 可以回收用过的块,这样可以确保以后的页写入可以快速分配到一个全新的页。

SSD的基本结构:

image-20210915090459823

比如Intel P4510 SSD控制器内部集成了两个Cotex A15 ARM core,这两个CPU core各自处理50%物理地址空间的读写命令(不同CPU负责不同的Die,以提高并发度)。在处理IO命令的过程中,为了充分发挥两个cpu的并行处理效率,每个cpu core单次处理的最大数据块是128kB。所以P4510对于128k对齐(4k,8k,16k,32k,64k,128k)或者128k整数倍(256k,512k,1024k)的数据块的处理效率最高。因为这些数据块都能够在SSD内部被组装或者拆分为完整的128k数据块。但是,对于非128k对齐的数据块(68k,132k,260k,516k,1028k),由于每个提交给SSD的写命令都有一个非128k对齐的“尾巴”需要跨CPU来处理,这样便会导致SSD处理单个命令的效率下降,写带宽随之也下降。

imgimg

SSD内部使用写缓存。写缓存主要用来降低写延迟。当写请求发送给SSD时,写数据会被先保存在写缓存,此时SSD会直接发送确认消息通知主机端写请求已完成,实现最低的写延迟。SSD固件在后台会异步的定期把写缓存中的数据通过写操作命令刷回给NAND颗粒。为了满足写操作的持久化语义,SSD内有大容量电容保证写缓存中数据的安全。当紧急断电情况发生时,固件会及时把写缓存中的数据写回NAND颗粒. 也就是紧急断电后还能通过大电容供电来维持最后的落盘。

SSD内嵌内存容量的问题也限制了大容量NVMe SSD的发展,为了解决内存问题,目前一种可行的方法是增大sector size。标准NVMe SSD的sector size为4KB,为了进一步增大NVMe SSD的容量,有些厂商已经开始采用16KB的sector size。16KB Sector size的普及应用,会加速大容量NVMe SSD的推广。

以海康威视E200P为例,PCB上的硬件PLP掉电保护电路从D200Pro的10个钽电容+6个电感,简化为6个钽电容+6个电感。钽电容来自Panasonic,单颗47uF,6个钽电容并联可以为SSD提供几十毫秒的放电时间,让SSD把处理中的数据写入NAND中并更新映射表。这样的硬件PLP电路对比普通的家用产品要强悍很多。

img

或者如下结构:

image-20220923172817591

SSD存储持久化原理

记录一个比特很容易理解。给电容里面充上电有电压的时候就是 1,给电容放电里面没有电就是 0。采用这样方式存储数据的 SSD 硬盘,我们一般称之为使用了 SLC 的颗粒,全称是 Single-Level Cell,也就是一个存储单元中只有一位数据。

但是,这样的方式会遇到和 CPU Cache 类似的问题,那就是,同样的面积下,能够存放下的元器件是有限的。如果只用 SLC,我们就会遇到,存储容量上不去,并且价格下不来的问题。于是呢,硬件工程师们就陆续发明了 MLC(Multi-Level Cell)、TLC(Triple-Level Cell)以及 QLC(Quad-Level Cell),也就是能在一个电容里面存下 2 个、3 个乃至 4 个比特。

只有一个电容,我们怎么能够表示更多的比特呢?别忘了,这里我们还有一个电压计。4 个比特一共可以从 0000-1111 表示 16 个不同的数。那么,如果我们能往电容里面充电的时候,充上 15 个不同的电压,并且我们电压计能够区分出这 15 个不同的电压。加上电容被放空代表的 0,就能够代表从 0000-1111 这样 4 个比特了。

不过,要想表示 15 个不同的电压,充电和读取的时候,对于精度的要求就会更高。这会导致充电和读取的时候都更慢,所以 QLC 的 SSD 的读写速度,要比 SLC 的慢上好几倍。

SSD对碎片很敏感,类似JVM的内存碎片需要整理,碎片整理就带来了写入放大。也就是写入空间不够的时候需要先进行碎片整理、搬运,这样写入的数据更大了。

SSD寿命:以Intel 335为例再来算一下,BT用户可以用600TB × 1024 / 843 = 728天,普通用户可以用600TB/2 = 300年!情况十分乐观

两种逻辑门

NAND(NOT-AND) gate

NOR(NOT-OR) gate

如上两种门实现的介质都是非易失存储介质在写入前都需要擦除。实际上NOR Flash的一个bit可以从1变成0,而要从0变1就要擦除整块。NAND flash都需要擦除。

NAND Flash NOR Flash
芯片容量 <32GBit <1GBit
访问方式 块读写(顺序读写) 随机读写
接口方式 任意I/O口 特定完整存储器接口
读写性能 读取快(顺序读) 写入快 擦除快(可按块擦除) 读取快(RAM方式) 写入慢 檫除很慢
使用寿命 百万次 十万次
价格 低廉 高昂

NAND Flash更适合在各类需要大数据的设备中使用,如U盘、SSD、各种存储卡、MP3播放器等,而NOR Flash更适合用在高性能的工业产品中。

高端SSD会选取MLC(Multi-Level Cell)甚至SLC(Single-Level Cell),低端SSD则选取 TLC(Triple-Level Cell)。SD卡一般选取 TLC(Triple-Level Cell)

image-20210603161822079

image-20220105201724752

slc-mlc-tlc-buckets

umlc

image-20220105201749003

NOR FLash主要用于:Bios、机顶盒,大小一般是1-32MB

对于TLC NAND (每个NAND cell存储3 bits的信息),下面列出了每种操作的典型耗时的范围:

​ 读操作(Tread) : 50-100us,

​ 写操作(Tprog) : 500us-5ms,

​ 擦除操作(Terase) : 2-10ms。

为什么断电后SSD不丢数据

SSD的存储硬件都是NAND Flash。实现原理和通过改变电压,让电子进入绝缘层的浮栅(Floating Gate)内。断电之后,电子仍然在FG里面。但是如果长时间不通电,比如几年,仍然可能会丢数据。所以换句话说,SSD的确也不适合作为冷数据备份。比如标准要求SSD:温度在30度的情况下,数据要能保持52周。

写入放大(Write Amplification, or WA)

这是 SSD 相对于 HDD 的一个缺点,即实际写入 SSD 的物理数据量,有可能是应用层写入数据量的多倍。一方面,页级别的写入需要移动已有的数据来腾空页面。另一方面,GC 的操作也会移动用户数据来进行块级别的擦除。所以对 SSD 真正的写操作的数据可能比实际写的数据量大,这就是写入放大。一块 SSD 只能进行有限的擦除次数,也称为编程 / 擦除(P/E)周期,所以写入放大效用会缩短 SSD 的寿命。

SSD 的读取和写入的基本单位,不是一个比特(bit)或者一个字节(byte),而是一个页(Page)。SSD 的擦除单位就更夸张了,我们不仅不能按照比特或者字节来擦除,连按照页来擦除都不行,我们必须按照块来擦除。

SLC 的芯片,可以擦除的次数大概在 10 万次,MLC 就在 1 万次左右,而 TLC 和 QLC 就只在几千次了。这也是为什么,你去购买 SSD 硬盘,会看到同样的容量的价格差别很大,因为它们的芯片颗粒和寿命完全不一样。

从本质上讲,NAND Flash是一种不可靠介质,非常容易出现Bit翻转问题。SSD通过控制器和固件程序将这种不可靠的NAND Flash变成了可靠的数据存储介质。

为了在这种不可靠介质上构建可靠存储,SSD内部做了大量工作。在硬件层面,需要通过ECC单元解决经常出现的比特翻转问题。每次数据存储的时候,硬件单元需要为存储的数据计算ECC校验码;在数据读取的时候,硬件单元会根据校验码恢复被破坏的bit数据。ECC硬件单元集成在SSD控制器内部,代表了SSD控制器的能力。在MLC存储时代,BCH编解码技术可以解决问题,4KB数据中存在100bit翻转时可以纠正错误;在TLC存储时代,bit错误率大为提升,需要采用更高纠错能力的LDPC编解码技术,在4KB出现550bit翻转时,LDPC硬解码仍然可以恢复数据。对比LDPC硬解码、BCH以及LDPC软解码之间的能力,可以看出LDPC软解码具有更强的纠错能力,通常使用在硬解码失效的情况下。LDPC软解码的不足之处在于增加了IO的延迟。

在软件层面,SSD内部设计了FTL(Flash Translation Layer),该软件层的设计思想和Log-Structured File System设计思想类似。采用log追加写的方式记录数据,采用LBA至PBA的地址映射表记录数据组织方式。Log-structured系统最大的一个问题就是垃圾回收(GC)。因此,虽然NAND Flash本身具有很高的IO性能,但受限于GC的影响,SSD层面的性能会大受影响,并且存在十分严重的IO QoS问题,这也是目前标准NVMe SSD一个很重要的问题。

耗损平衡 (Wear Leveling)

对每一个块而言,一旦达到最大数量,该块就会死亡。对于 SLC 块,P/E 周期的典型数目是十万次;对于 MLC 块,P/E 周期的数目是一万;而对于 TLC 块,则可能是几千。为了确保 SSD 的容量和性能,我们需要在擦除次数上保持平衡,SSD 控制器具有这种“耗损平衡”机制可以实现这一目标。在损耗平衡期间,数据在各个块之间移动,以实现均衡的损耗,这种机制也会对前面讲的写入放大推波助澜。

non-volatile memory (NVM)

NVM是一种新型的硬件存储介质,同时具备磁盘和DRAM的一些特性。突出的NVM技术产品有:PC-RAM、STT-RAM和R-RAM。因为NVM具有设备层次上的持久性,所以不需要向DRAM一样的刷新周期以维持数据状态。因此NVM和DRAM相比,每bit耗费的能量更少。另外,NVM比硬盘有更小的延迟,读延迟甚至和DRAM相当;字节寻址;比DRAM密度更大。

1、NVM特性

数据访问延迟:NVM的读延迟比磁盘小很多。由于NVM仍处于开发阶段,来源不同延迟不同。STT-RAM的延迟1-20ns。尽管如此,他的延迟也已经非常接近DRAM了。

PC_RAM 和R-RAM的写延迟比DRAM高。但是写延迟不是很重要,因为可以通过buffer来缓解。

密度:NVM的密度比DRAM高,可以作为主存的替代品,尤其是在嵌入式系统中。例如,相对于DRAM,PC-RAM提供2到4倍的容量,便于扩展。

耐久性:即每个内存单元写的最大次数。最具竞争性的是PC-RAM和STT-RAM,提供接近DRAM的耐久性。更精确的说,NVM的耐久性是1015而DRAM是1016。另外,NVM比闪存技术的耐久性更大。

能量消耗:NVM不需要像DRAM一样周期性刷写以维护内存中数据,所以消耗的能量更少。PC-RAM比DRAM消耗能量显著的少,其他比较接近。

此外,还有字节寻址、持久性。Interl和Micron已经发起了3D XPoint技术,同时Interl开发了新的指令以支持持久内存的使用。

傲腾 PMem

Optane 的工作原理与 NAND Flash 有很大区别,NAND Flash 是基于 Floating-gate MOSFET 而 Optane 虽然因特尔没有官方资料说明但是根据国外机构用质谱法 测试的结果表明 Optane 是一种基于 PCM (相变化存储器) 原理的存储器,PCM 简单来说就是某种物质目前主要是一种或多种硫族化物的玻璃,经由加热可以改变它的状态,成为晶体或者非晶体,这些不同的状态具有相应的电阻值。

img

从以上数据来看 PCM 读接近 DRAM,比 NAND Flash 快了500倍,PCM 写比 DRAM 慢了20倍,但是仍然比 NAND Flash 快了500倍。DRAM 读写速度一致,PCM 和 NAND Flash 写都比读要慢20倍。

磁盘类型查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$cat /sys/block/vda/queue/rotational
1 //1表示旋转,非ssd,0表示ssd
or
lsblk -d -o name,rota,size,label,uuid

SATA SSD测试数据
# cat /sys/block/sda/queue/rotational
0
# lsblk -d -o name,rota
NAME ROTA
sda 0
sfdv0n1 0

ESSD磁盘测试用一块虚拟的阿里云网络盘,不能算完整意义的SSD(承诺IOPS 4200),数据仅供参考,磁盘概况:
$df -lh
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 99G 30G 65G 32% /

$cat /sys/block/vda/queue/rotational
1

fio 结果解读

slat,异步场景下才有

其中slat指的是发起IO的时间,在异步IO模式下,发起IO以后,IO会异步完成。例如调用一个异步的write,虽然write返回成功了,但是IO还未完成,slat约等于发起write的耗时;

slat (usec): min=4, max=6154, avg=48.82, stdev=56.38: The first latency metric you’ll see is the ‘slat’ or submission latency. It is pretty much what it sounds like, meaning “how long did it take to submit this IO to the kernel for processing?”

clat

clat指的是完成时间,从发起IO后到完成IO的时间,在同步IO模式下,clat是指整个写动作完成时间

lat

lat是总延迟时间,指的是IO单元创建到完成的总时间,通常这项数据关注较多。同步场景几乎等于clat,异步场景等于clat+slat
这项数据需要关注的是max,看看有没有极端的高延迟IO;另外还需要关注stdev,这项数据越大说明,IO响应时间波动越大,反之越小,波动越小

clat percentiles (usec):处于某个百分位的io操作时延

cpu : usr=9.11%, sys=57.07%, ctx=762410, majf=0, minf=1769 //用户和系统的CPU占用时间百分比,线程切换次数,major以及minor页面错误的数量。

SSD的direct和buffered似乎很奇怪,应该是direct=0性能更好,实际不是这样,这里还需要找资料求证下

  • direct``=bool

    If value is true, use non-buffered I/O. This is usually O_DIRECT. Note that OpenBSD and ZFS on Solaris don’t support direct I/O. On Windows the synchronous ioengines don’t support direct I/O. Default: false.

  • buffered``=bool

    If value is true, use buffered I/O. This is the opposite of the direct option. Defaults to true.

iostat 结果解读

Dm-0就是lvm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
0.32 0.00 3.34 0.13 0.00 96.21

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 11.40 66.00 7.20 1227.20 74.40 35.56 0.03 0.43 0.47 0.08 0.12 0.88
nvme0n1 0.00 8612.00 0.00 51749.60 0.00 241463.20 9.33 4.51 0.09 0.00 0.09 0.02 78.56
dm-0 0.00 0.00 0.00 60361.80 0.00 241463.20 8.00 152.52 2.53 0.00 2.53 0.01 78.26

avg-cpu: %user %nice %system %iowait %steal %idle
0.36 0.00 3.46 0.17 0.00 96.00

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda 0.00 8.80 9.20 5.20 1047.20 67.20 154.78 0.01 0.36 0.46 0.19 0.33 0.48
nvme0n1 0.00 11354.20 0.00 50876.80 0.00 248944.00 9.79 5.25 0.10 0.00 0.10 0.02 80.06
dm-0 0.00 0.00 0.00 62231.00 0.00 248944.80 8.00 199.49 3.21 0.00 3.21 0.01 78.86

avgqu_sz,是iostat的一项比较重要的数据。如果队列过长,则表示有大量IO在处理或等待,但是这还不足以说明后端的存储系统达到了处理极限。例如后端存储的并发能力是4096,客户端并发发送了256个IO下去,那么队列长度就是256。即使长时间队列长度是256,也不能说明什么,仅仅表明队列长度是256,有256个IO在处理或者排队。

那么怎么判断IO是在调度队列排队等待,还是在设备上处理呢?iostat有两项数据可以给出一个大致的判断。svctime,这项数据的指的是IO在设备处理中耗费的时间。另外一项数据await,指的是IO从排队到完成的时间,包括了svctime和排队等待的时间。那么通过对比这两项数据,如果两项数据差不多,则说明IO基本没有排队等待,耗费的时间都是设备处理。如果await远大于svctime,则说明有大量的IO在排队,并没有发送给设备处理。

rq_affinity

参考aliyun测试文档 , rq_affinity增加2的commit: git show 5757a6d76c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function RunFio
{
numjobs=$1 # 实例中的测试线程数,例如示例中的10
iodepth=$2 # 同时发出I/O数的上限,例如示例中的64
bs=$3 # 单次I/O的块文件大小,例如示例中的4k
rw=$4 # 测试时的读写策略,例如示例中的randwrite
filename=$5 # 指定测试文件的名称,例如示例中的/dev/your_device
nr_cpus=`cat /proc/cpuinfo |grep "processor" |wc -l`
if [ $nr_cpus -lt $numjobs ];then
echo “Numjobs is more than cpu cores, exit!”
exit -1
fi
let nu=$numjobs+1
cpulist=""
for ((i=1;i<10;i++))
do
list=`cat /sys/block/your_device/mq/*/cpu_list | awk '{if(i<=NF) print $i;}' i="$i" | tr -d ',' | tr '\n' ','`
if [ -z $list ];then
break
fi
cpulist=${cpulist}${list}
done
spincpu=`echo $cpulist | cut -d ',' -f 2-${nu}`
echo $spincpu
fio --ioengine=libaio --runtime=30s --numjobs=${numjobs} --iodepth=${iodepth} --bs=${bs} --rw=${rw} --filename=${filename} --time_based=1 --direct=1 --name=test --group_reporting --cpus_allowed=$spincpu --cpus_allowed_policy=split
}
echo 2 > /sys/block/your_device/queue/rq_affinity
sleep 5
RunFio 10 64 4k randwrite filename

对NVME SSD进行测试,左边rq_affinity是2,右边rq_affinity为1,在这个测试参数下rq_affinity为1的性能要好(后许多次测试两者性能差不多)

image-20210607113709945

参考资料

http://cizixs.com/2017/01/03/how-slow-is-disk-and-network

https://tobert.github.io/post/2014-04-17-fio-output-explained.html

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

块存储NVMe云盘原型实践

机械硬盘随机IO慢的超乎你的想象

搭载固态硬盘的服务器究竟比搭机械硬盘快多少?

SSD基本工作原理

SSD原理解读

Backblaze 的 2021 年硬盘死亡報告

ssd/san/sas 磁盘 光纤性能比较

正好有机会用到一个san存储设备,跑了一把性能数据,记录一下

image.png

所使用的测试命令:

1
fio -ioengine=libaio -bs=4k -direct=1 -thread -rw=randwrite -size=1000G -filename=/data/fio.test -name="EBS 4K randwrite test" -iodepth=64 -runtime=60

ssd(Solid State Drive)和san的比较是在同一台物理机上,所以排除了其他因素的干扰。

简要的结论:

  • 本地ssd性能最好、sas机械盘(RAID10)性能最差

  • san存储走特定的光纤网络,不是走tcp的san(至少从网卡看不到san的流量),性能居中

  • 从rt来看 ssd:san:sas 大概是 1:3:15

  • san比本地sas机械盘性能要好,这也许取决于san的网络传输性能和san存储中的设备(比如用的ssd而不是机械盘)

NVMe SSD 和 HDD的性能比较

image.png

表中性能差异比上面测试还要大,SSD 的随机 IO 延迟比传统硬盘快百倍以上,一般在微妙级别;IO 带宽也高很多倍,可以达到每秒几个 GB;随机 IOPS 更是快了上千倍,可以达到几十万。

HDD只有一个磁头,并发没有意义,但是SSD支持高并发写入读取。SSD没有磁头、不需要旋转,所以随机读取和顺序读取基本没有差别。

SSD 的性能特性和机制

SSD 的内部工作方式和 HDD 大相径庭,我们先了解几个概念。

单元(Cell)、页面(Page)、块(Block)。当今的主流 SSD 是基于 NAND 的,它将数字位存储在单元中。每个 SSD 单元可以存储一位或多位。对单元的每次擦除都会降低单元的寿命,所以单元只能承受一定数量的擦除。单元存储的位数越多,制造成本就越少,SSD 的容量也就越大,但是耐久性(擦除次数)也会降低。

一个页面包括很多单元,典型的页面大小是 4KB,页面也是要读写的最小存储单元。SSD 上没有“重写”操作,不像 HDD 可以直接对任何字节重写覆盖。一个页面一旦写入内容后就不能进行部分重写,必须和其它相邻页面一起被整体擦除重置。

多个页面组合成块。一个块的典型大小为 512KB 或 1MB,也就是大约 128 或 256 页。块是擦除的基本单位,每次擦除都是整个块内的所有页面都被重置。

擦除速度相对很慢,通常为几毫秒。所以对同步的 IO,发出 IO 的应用程序可能会因为块的擦除,而经历很大的写入延迟。为了尽量地减少这样的场景,保持空闲块的阈值对于快速的写响应是很有必要的。SSD 的垃圾回收(GC)的目的就在于此。GC 可以回收用过的块,这样可以确保以后的页写入可以快速分配到一个全新的页。

SSD原理

对于 SSD 硬盘,类似SRAM(CPU cache)它是由一个电容加上一个电压计组合在一起,记录了一个或者多个比特。能够记录一个比特很容易理解。给电容里面充上电有电压的时候就是 1,给电容放电里面没有电就是 0。采用这样方式存储数据的 SSD 硬盘,我们一般称之为使用了 SLC 的颗粒,全称是 Single-Level Cell,也就是一个存储单元中只有一位数据。

但是,这样的方式会遇到和 CPU Cache 类似的问题,那就是,同样的面积下,能够存放下的元器件是有限的。如果只用 SLC,我们就会遇到,存储容量上不去,并且价格下不来的问题。于是呢,硬件工程师们就陆续发明了 MLC(Multi-Level Cell)、TLC(Triple-Level Cell)以及 QLC(Quad-Level Cell),也就是能在一个电容里面存下 2 个、3 个乃至 4 个比特。

只有一个电容,我们怎么能够表示更多的比特呢?别忘了,这里我们还有一个电压计。4 个比特一共可以从 0000-1111 表示 16 个不同的数。那么,如果我们能往电容里面充电的时候,充上 15 个不同的电压,并且我们电压计能够区分出这 15 个不同的电压。加上电容被放空代表的 0,就能够代表从 0000-1111 这样 4 个比特了。

不过,要想表示 15 个不同的电压,充电和读取的时候,对于精度的要求就会更高。这会导致充电和读取的时候都更慢,所以 QLC 的 SSD 的读写速度,要比 SLC 的慢上好几倍。

SSD对碎片很敏感,类似JVM的内存碎片需要整理,碎片整理就带来了写入放大。也就是写入空间不够的时候需要先进行碎片整理、搬运,这样写入的数据更大了。

SSD寿命:以Intel 335为例再来算一下,BT用户可以用600TB × 1024 / 843 = 728天,普通用户可以用600TB/2 = 300年!情况十分乐观

两种逻辑门

NAND(NOT-AND) gate

NOR(NOT-OR) gate

如上两种门实现的介质都是非易失存储介质在写入前都需要擦除。实际上NOR Flash的一个bit可以从1变成0,而要从0变1就要擦除整块。NAND flash都需要擦除。

NAND Flash NOR Flash
芯片容量 <32GBit <1GBit
访问方式 块读写(顺序读写) 随机读写
接口方式 任意I/O口 特定完整存储器接口
读写性能 读取快(顺序读) 写入快 擦除快(可按块擦除) 读取快(RAM方式) 写入慢 檫除很慢
使用寿命 百万次 十万次
价格 低廉 高昂

NAND Flash更适合在各类需要大数据的设备中使用,如U盘、SSD、各种存储卡、MP3播放器等,而NOR Flash更适合用在高性能的工业产品中。

高端SSD会选取MLC(Multi-Level Cell)甚至SLC(Single-Level Cell),低端SSD则选取 TLC(Triple-Level Cell)。SD卡一般选取 TLC(Triple-Level Cell)

image-20210603161822079

slc-mlc-tlc-buckets

NOR FLash主要用于:Bios、机顶盒,大小一般是1-32MB

为什么断电后SSD不丢数据

SSD的存储硬件都是NAND Flash。实现原理和通过改变电压,让电子进入绝缘层的浮栅(Floating Gate)内。断电之后,电子仍然在FG里面。但是如果长时间不通电,比如几年,仍然可能会丢数据。所以换句话说,SSD的确也不适合作为冷数据备份。

比如标准要求SSD:温度在30度的情况下,数据要能保持52周。

写入放大(Write Amplification, or WA)

这是 SSD 相对于 HDD 的一个缺点,即实际写入 SSD 的物理数据量,有可能是应用层写入数据量的多倍。一方面,页级别的写入需要移动已有的数据来腾空页面。另一方面,GC 的操作也会移动用户数据来进行块级别的擦除。所以对 SSD 真正的写操作的数据可能比实际写的数据量大,这就是写入放大。一块 SSD 只能进行有限的擦除次数,也称为编程 / 擦除(P/E)周期,所以写入放大效用会缩短 SSD 的寿命。

SSD 的读取和写入的基本单位,不是一个比特(bit)或者一个字节(byte),而是一个页(Page)。SSD 的擦除单位就更夸张了,我们不仅不能按照比特或者字节来擦除,连按照页来擦除都不行,我们必须按照块来擦除。

SLC 的芯片,可以擦除的次数大概在 10 万次,MLC 就在 1 万次左右,而 TLC 和 QLC 就只在几千次了。这也是为什么,你去购买 SSD 硬盘,会看到同样的容量的价格差别很大,因为它们的芯片颗粒和寿命完全不一样。

耗损平衡 (Wear Leveling)

对每一个块而言,一旦达到最大数量,该块就会死亡。对于 SLC 块,P/E 周期的典型数目是十万次;对于 MLC 块,P/E 周期的数目是一万;而对于 TLC 块,则可能是几千。为了确保 SSD 的容量和性能,我们需要在擦除次数上保持平衡,SSD 控制器具有这种“耗损平衡”机制可以实现这一目标。在损耗平衡期间,数据在各个块之间移动,以实现均衡的损耗,这种机制也会对前面讲的写入放大推波助澜。

磁盘类型查看

1
2
3
4
5
$cat /sys/block/vda/queue/rotational
1 //1表示旋转,非ssd,0表示ssd

或者
lsblk -d -o name,rota

fio测试

以下是两块测试的SSD磁盘测试前的基本情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/dev/sda	240.06G  SSD_SATA  //sata
/dev/sfd0n1 3200G SSD_PCIE //PCIE

Filesystem Size Used Avail Use% Mounted on
/dev/sda3 49G 29G 18G 63% /
/dev/sfdv0n1p1 2.0T 803G 1.3T 40% /data

# cat /sys/block/sda/queue/rotational
0
# cat /sys/block/sfdv0n1/queue/rotational
0

#测试前的iostat状态
# iostat -d sfdv0n1 sda3 1 -x
Linux 3.10.0-957.el7.x86_64 (nu4d01142.sqa.nu8) 2021年02月23日 _x86_64_ (104 CPU)

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda3 0.00 10.67 1.24 18.78 7.82 220.69 22.83 0.03 1.64 1.39 1.66 0.08 0.17
sfdv0n1 0.00 0.21 9.91 841.42 128.15 8237.10 19.65 0.93 0.04 0.25 0.04 1.05 89.52

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda3 0.00 15.00 0.00 17.00 0.00 136.00 16.00 0.03 2.00 0.00 2.00 1.29 2.20
sfdv0n1 0.00 0.00 0.00 11158.00 0.00 54448.00 9.76 1.03 0.02 0.00 0.02 0.09 100.00

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda3 0.00 5.00 0.00 18.00 0.00 104.00 11.56 0.01 0.61 0.00 0.61 0.61 1.10
sfdv0n1 0.00 0.00 0.00 10970.00 0.00 53216.00 9.70 1.02 0.03 0.00 0.03 0.09 100.10

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda3 0.00 0.00 0.00 24.00 0.00 100.00 8.33 0.01 0.58 0.00 0.58 0.08 0.20
sfdv0n1 0.00 0.00 0.00 11206.00 0.00 54476.00 9.72 1.03 0.03 0.00 0.03 0.09 99.90

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sda3 0.00 14.00 0.00 21.00 0.00 148.00 14.10 0.01 0.48 0.00 0.48 0.33 0.70
sfdv0n1 0.00 0.00 0.00 10071.00 0.00 49028.00 9.74 1.02 0.03 0.00 0.03 0.10 99.80

NVMe SSD测试数据

对一块ssd进行如下测试(挂载在/data 目录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
fio -ioengine=libaio -bs=4k -direct=1 -thread -rw=randwrite -rwmixread=70 -size=16G -filename=/data/fio.test -name="EBS 4K randwrite test" -iodepth=64 -runtime=60
EBS 4K randwrite test: (g=0): rw=randwrite, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.7
Starting 1 thread
EBS 4K randwrite test: Laying out IO file (1 file / 16384MiB)
Jobs: 1 (f=1): [w(1)][100.0%][r=0KiB/s,w=63.8MiB/s][r=0,w=16.3k IOPS][eta 00m:00s]
EBS 4K randwrite test: (groupid=0, jobs=1): err= 0: pid=258871: Tue Feb 23 14:12:23 2021
write: IOPS=18.9k, BW=74.0MiB/s (77.6MB/s)(4441MiB/60001msec)
slat (usec): min=4, max=6154, avg=48.82, stdev=56.38
clat (nsec): min=1049, max=12360k, avg=3326362.62, stdev=920683.43
lat (usec): min=68, max=12414, avg=3375.52, stdev=928.97
clat percentiles (usec):
| 1.00th=[ 1483], 5.00th=[ 1811], 10.00th=[ 2114], 20.00th=[ 2376],
| 30.00th=[ 2704], 40.00th=[ 3130], 50.00th=[ 3523], 60.00th=[ 3785],
| 70.00th=[ 3949], 80.00th=[ 4080], 90.00th=[ 4293], 95.00th=[ 4490],
| 99.00th=[ 5604], 99.50th=[ 5997], 99.90th=[ 7111], 99.95th=[ 7832],
| 99.99th=[ 9634]
bw ( KiB/s): min=61024, max=118256, per=99.98%, avg=75779.58, stdev=12747.95, samples=120
iops : min=15256, max=29564, avg=18944.88, stdev=3186.97, samples=120
lat (usec) : 2=0.01%, 100=0.01%, 250=0.01%, 500=0.01%, 750=0.02%
lat (usec) : 1000=0.06%
lat (msec) : 2=7.40%, 4=66.19%, 10=26.32%, 20=0.01%
cpu : usr=5.23%, sys=46.71%, ctx=846953, majf=0, minf=6
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
issued rwts: total=0,1136905,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
WRITE: bw=74.0MiB/s (77.6MB/s), 74.0MiB/s-74.0MiB/s (77.6MB/s-77.6MB/s), io=4441MiB (4657MB), run=60001-60001msec

Disk stats (read/write):
sfdv0n1: ios=0/1821771, merge=0/7335, ticks=0/39708, in_queue=78295, util=100.00%

slat (usec): min=4, max=6154, avg=48.82, stdev=56.38: The first latency metric you’ll see is the ‘slat’ or submission latency. It is pretty much what it sounds like, meaning “how long did it take to submit this IO to the kernel for processing?”

如上测试iops为:18944,测试期间的iostat,测试中一直有mysql在导入数据,所以测试开始前util就已经100%了,并且w/s到了13K左右

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# iostat -d sfdv0n1 3 -x
Linux 3.10.0-957.el7.x86_64 (nu4d01142.sqa.nu8) 2021年02月23日 _x86_64_ (104 CPU)

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sfdv0n1 0.00 0.18 3.45 769.17 102.83 7885.16 20.68 0.93 0.04 0.26 0.04 1.16 89.46

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sfdv0n1 0.00 0.00 0.00 13168.67 0.00 66244.00 10.06 1.05 0.03 0.00 0.03 0.08 100.10

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sfdv0n1 0.00 0.00 0.00 12822.67 0.00 65542.67 10.22 1.04 0.02 0.00 0.02 0.08 100.07

//增加压力
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sfdv0n1 0.00 0.00 0.00 27348.33 0.00 214928.00 15.72 1.27 0.02 0.00 0.02 0.04 100.17

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sfdv0n1 0.00 1.00 0.00 32661.67 0.00 271660.00 16.63 1.32 0.02 0.00 0.02 0.03 100.37

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sfdv0n1 0.00 0.00 0.00 31645.00 0.00 265988.00 16.81 1.33 0.02 0.00 0.02 0.03 100.37

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sfdv0n1 0.00 574.00 0.00 31961.67 0.00 271094.67 16.96 1.36 0.02 0.00 0.02 0.03 100.13

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
sfdv0n1 0.00 0.00 0.00 27656.33 0.00 224586.67 16.24 1.28 0.02 0.00 0.02 0.04 100.37

从iostat看出,测试开始前util已经100%(因为ssd,util失去参考意义),w/s 13K左右,压力跑起来后w/s能到30K,svctm、await均保持稳定

SSD的direct和buffered似乎很奇怪,应该是direct=0性能更好,实际不是这样,这里还需要找资料求证下

  • direct``=bool

    If value is true, use non-buffered I/O. This is usually O_DIRECT. Note that OpenBSD and ZFS on Solaris don’t support direct I/O. On Windows the synchronous ioengines don’t support direct I/O. Default: false.

  • buffered``=bool

    If value is true, use buffered I/O. This is the opposite of the direct option. Defaults to true.

如下测试中direct=1和direct=0的write avg iops分别为42K、16K

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# fio -ioengine=libaio -bs=4k -direct=1 -buffered=0 -thread -rw=randrw -rwmixread=70 -size=16G -filename=/data/fio.test -name="EBS 4K randwrite test" -iodepth=64 -runtime=60 
EBS 4K randwrite test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.7
Starting 1 thread
Jobs: 1 (f=1): [m(1)][100.0%][r=507MiB/s,w=216MiB/s][r=130k,w=55.2k IOPS][eta 00m:00s]
EBS 4K randwrite test: (groupid=0, jobs=1): err= 0: pid=415921: Tue Feb 23 14:34:33 2021
read: IOPS=99.8k, BW=390MiB/s (409MB/s)(11.2GiB/29432msec)
slat (nsec): min=1043, max=917837, avg=4273.86, stdev=3792.17
clat (usec): min=2, max=4313, avg=459.80, stdev=239.61
lat (usec): min=4, max=4328, avg=464.16, stdev=241.81
clat percentiles (usec):
| 1.00th=[ 251], 5.00th=[ 277], 10.00th=[ 289], 20.00th=[ 310],
| 30.00th=[ 326], 40.00th=[ 343], 50.00th=[ 363], 60.00th=[ 400],
| 70.00th=[ 502], 80.00th=[ 603], 90.00th=[ 750], 95.00th=[ 881],
| 99.00th=[ 1172], 99.50th=[ 1401], 99.90th=[ 3032], 99.95th=[ 3359],
| 99.99th=[ 3785]
bw ( KiB/s): min=182520, max=574856, per=99.24%, avg=395975.64, stdev=119541.78, samples=58
iops : min=45630, max=143714, avg=98993.90, stdev=29885.42, samples=58
write: IOPS=42.8k, BW=167MiB/s (175MB/s)(4915MiB/29432msec)
slat (usec): min=3, max=263, avg= 9.34, stdev= 4.35
clat (usec): min=14, max=2057, avg=402.26, stdev=140.67
lat (usec): min=19, max=2070, avg=411.72, stdev=142.67
clat percentiles (usec):
| 1.00th=[ 237], 5.00th=[ 281], 10.00th=[ 293], 20.00th=[ 314],
| 30.00th=[ 330], 40.00th=[ 343], 50.00th=[ 359], 60.00th=[ 379],
| 70.00th=[ 404], 80.00th=[ 457], 90.00th=[ 586], 95.00th=[ 717],
| 99.00th=[ 930], 99.50th=[ 1004], 99.90th=[ 1254], 99.95th=[ 1385],
| 99.99th=[ 1532]
bw ( KiB/s): min=78104, max=244408, per=99.22%, avg=169671.52, stdev=51142.10, samples=58
iops : min=19526, max=61102, avg=42417.86, stdev=12785.51, samples=58
lat (usec) : 4=0.01%, 10=0.01%, 20=0.01%, 50=0.02%, 100=0.04%
lat (usec) : 250=1.02%, 500=73.32%, 750=17.28%, 1000=6.30%
lat (msec) : 2=1.83%, 4=0.19%, 10=0.01%
cpu : usr=15.84%, sys=83.31%, ctx=13765, majf=0, minf=7
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
issued rwts: total=2936000,1258304,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
READ: bw=390MiB/s (409MB/s), 390MiB/s-390MiB/s (409MB/s-409MB/s), io=11.2GiB (12.0GB), run=29432-29432msec
WRITE: bw=167MiB/s (175MB/s), 167MiB/s-167MiB/s (175MB/s-175MB/s), io=4915MiB (5154MB), run=29432-29432msec

Disk stats (read/write):
sfdv0n1: ios=795793/1618341, merge=0/11, ticks=218710/27721, in_queue=264935, util=100.00%
[root@nu4d01142 data]#
[root@nu4d01142 data]# fio -ioengine=libaio -bs=4k -direct=0 -buffered=0 -thread -rw=randrw -rwmixread=70 -size=6G -filename=/data/fio.test -name="EBS 4K randwrite test" -iodepth=64 -runtime=60
EBS 4K randwrite test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.7
Starting 1 thread
Jobs: 1 (f=1): [m(1)][100.0%][r=124MiB/s,w=53.5MiB/s][r=31.7k,w=13.7k IOPS][eta 00m:00s]
EBS 4K randwrite test: (groupid=0, jobs=1): err= 0: pid=437523: Tue Feb 23 14:37:54 2021
read: IOPS=38.6k, BW=151MiB/s (158MB/s)(4300MiB/28550msec)
slat (nsec): min=1205, max=1826.7k, avg=13253.36, stdev=17173.87
clat (nsec): min=236, max=5816.8k, avg=1135969.25, stdev=337142.34
lat (nsec): min=1977, max=5831.2k, avg=1149404.84, stdev=341232.87
clat percentiles (usec):
| 1.00th=[ 461], 5.00th=[ 627], 10.00th=[ 717], 20.00th=[ 840],
| 30.00th=[ 938], 40.00th=[ 1029], 50.00th=[ 1123], 60.00th=[ 1221],
| 70.00th=[ 1319], 80.00th=[ 1434], 90.00th=[ 1565], 95.00th=[ 1680],
| 99.00th=[ 1893], 99.50th=[ 1975], 99.90th=[ 2671], 99.95th=[ 3261],
| 99.99th=[ 3851]
bw ( KiB/s): min=119304, max=216648, per=100.00%, avg=154273.07, stdev=29925.10, samples=57
iops : min=29826, max=54162, avg=38568.25, stdev=7481.30, samples=57
write: IOPS=16.5k, BW=64.6MiB/s (67.7MB/s)(1844MiB/28550msec)
slat (usec): min=3, max=3565, avg=21.07, stdev=22.23
clat (usec): min=14, max=9983, avg=1164.21, stdev=459.66
lat (usec): min=21, max=10011, avg=1185.57, stdev=463.28
clat percentiles (usec):
| 1.00th=[ 498], 5.00th=[ 619], 10.00th=[ 709], 20.00th=[ 832],
| 30.00th=[ 930], 40.00th=[ 1020], 50.00th=[ 1123], 60.00th=[ 1237],
| 70.00th=[ 1336], 80.00th=[ 1450], 90.00th=[ 1598], 95.00th=[ 1713],
| 99.00th=[ 2311], 99.50th=[ 3851], 99.90th=[ 5932], 99.95th=[ 6456],
| 99.99th=[ 7701]
bw ( KiB/s): min=50800, max=92328, per=100.00%, avg=66128.47, stdev=12890.64, samples=57
iops : min=12700, max=23082, avg=16532.07, stdev=3222.66, samples=57
lat (nsec) : 250=0.01%, 500=0.01%, 750=0.01%, 1000=0.01%
lat (usec) : 2=0.01%, 4=0.01%, 10=0.01%, 20=0.02%, 50=0.03%
lat (usec) : 100=0.04%, 250=0.18%, 500=1.01%, 750=11.05%, 1000=25.02%
lat (msec) : 2=61.87%, 4=0.62%, 10=0.14%
cpu : usr=10.87%, sys=61.98%, ctx=218415, majf=0, minf=7
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
issued rwts: total=1100924,471940,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
READ: bw=151MiB/s (158MB/s), 151MiB/s-151MiB/s (158MB/s-158MB/s), io=4300MiB (4509MB), run=28550-28550msec
WRITE: bw=64.6MiB/s (67.7MB/s), 64.6MiB/s-64.6MiB/s (67.7MB/s-67.7MB/s), io=1844MiB (1933MB), run=28550-28550msec

Disk stats (read/write):
sfdv0n1: ios=536103/822037, merge=0/1442, ticks=66507/17141, in_queue=99429, util=100.00%

SATA SSD测试数据

1
2
3
4
5
6
# cat /sys/block/sda/queue/rotational 
0
# lsblk -d -o name,rota
NAME ROTA
sda 0
sfdv0n1 0

-direct=0 -buffered=0读写iops分别为15.8K、6.8K 比ssd差了不少(都是direct=0),如果direct、buffered都是1的话,ESSD性能很差,读写iops分别为4312、1852

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# fio -ioengine=libaio -bs=4k -direct=0 -buffered=0 -thread -rw=randrw -rwmixread=70 -size=2G -filename=/var/lib/docker/fio.test -name="EBS 4K randwrite test" -iodepth=64 -runtime=60 
EBS 4K randwrite test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.7
Starting 1 thread
EBS 4K randwrite test: Laying out IO file (1 file / 2048MiB)
Jobs: 1 (f=1): [m(1)][100.0%][r=68.7MiB/s,w=29.7MiB/s][r=17.6k,w=7594 IOPS][eta 00m:00s]
EBS 4K randwrite test: (groupid=0, jobs=1): err= 0: pid=13261: Tue Feb 23 14:42:41 2021
read: IOPS=15.8k, BW=61.8MiB/s (64.8MB/s)(1432MiB/23172msec)
slat (nsec): min=1266, max=7261.0k, avg=7101.88, stdev=20655.54
clat (usec): min=167, max=27670, avg=2832.68, stdev=1786.18
lat (usec): min=175, max=27674, avg=2839.93, stdev=1784.42
clat percentiles (usec):
| 1.00th=[ 437], 5.00th=[ 668], 10.00th=[ 873], 20.00th=[ 988],
| 30.00th=[ 1401], 40.00th=[ 2442], 50.00th=[ 2835], 60.00th=[ 3195],
| 70.00th=[ 3523], 80.00th=[ 4047], 90.00th=[ 5014], 95.00th=[ 5866],
| 99.00th=[ 8160], 99.50th=[ 9372], 99.90th=[13829], 99.95th=[15008],
| 99.99th=[23725]
bw ( KiB/s): min=44183, max=149440, per=99.28%, avg=62836.17, stdev=26590.84, samples=46
iops : min=11045, max=37360, avg=15709.02, stdev=6647.72, samples=46
write: IOPS=6803, BW=26.6MiB/s (27.9MB/s)(616MiB/23172msec)
slat (nsec): min=1566, max=11474k, avg=8460.17, stdev=38221.51
clat (usec): min=77, max=24047, avg=2789.68, stdev=2042.55
lat (usec): min=80, max=24054, avg=2798.29, stdev=2040.85
clat percentiles (usec):
| 1.00th=[ 265], 5.00th=[ 433], 10.00th=[ 635], 20.00th=[ 840],
| 30.00th=[ 979], 40.00th=[ 2212], 50.00th=[ 2671], 60.00th=[ 3130],
| 70.00th=[ 3523], 80.00th=[ 4228], 90.00th=[ 5342], 95.00th=[ 6456],
| 99.00th=[ 9241], 99.50th=[10421], 99.90th=[13960], 99.95th=[15533],
| 99.99th=[23725]
bw ( KiB/s): min=18435, max=63112, per=99.26%, avg=27012.57, stdev=11299.42, samples=46
iops : min= 4608, max=15778, avg=6753.11, stdev=2824.87, samples=46
lat (usec) : 100=0.01%, 250=0.23%, 500=3.14%, 750=5.46%, 1000=15.27%
lat (msec) : 2=11.47%, 4=43.09%, 10=20.88%, 20=0.44%, 50=0.01%
cpu : usr=3.53%, sys=18.08%, ctx=47448, majf=0, minf=6
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
issued rwts: total=366638,157650,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
READ: bw=61.8MiB/s (64.8MB/s), 61.8MiB/s-61.8MiB/s (64.8MB/s-64.8MB/s), io=1432MiB (1502MB), run=23172-23172msec
WRITE: bw=26.6MiB/s (27.9MB/s), 26.6MiB/s-26.6MiB/s (27.9MB/s-27.9MB/s), io=616MiB (646MB), run=23172-23172msec

Disk stats (read/write):
sda: ios=359202/155123, merge=299/377, ticks=946305/407820, in_queue=1354596, util=99.61%

# fio -ioengine=libaio -bs=4k -direct=1 -buffered=0 -thread -rw=randrw -rwmixread=70 -size=2G -filename=/var/lib/docker/fio.test -name="EBS 4K randwrite test" -iodepth=64 -runtime=60
EBS 4K randwrite test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.7
Starting 1 thread
Jobs: 1 (f=1): [m(1)][95.5%][r=57.8MiB/s,w=25.7MiB/s][r=14.8k,w=6568 IOPS][eta 00m:01s]
EBS 4K randwrite test: (groupid=0, jobs=1): err= 0: pid=26167: Tue Feb 23 14:44:40 2021
read: IOPS=16.9k, BW=65.9MiB/s (69.1MB/s)(1432MiB/21730msec)
slat (nsec): min=1312, max=4454.2k, avg=8489.99, stdev=15763.97
clat (usec): min=201, max=18856, avg=2679.38, stdev=1720.02
lat (usec): min=206, max=18860, avg=2688.03, stdev=1717.19
clat percentiles (usec):
| 1.00th=[ 635], 5.00th=[ 832], 10.00th=[ 914], 20.00th=[ 971],
| 30.00th=[ 1090], 40.00th=[ 2114], 50.00th=[ 2704], 60.00th=[ 3064],
| 70.00th=[ 3392], 80.00th=[ 3851], 90.00th=[ 4817], 95.00th=[ 5735],
| 99.00th=[ 7767], 99.50th=[ 8979], 99.90th=[13698], 99.95th=[15139],
| 99.99th=[16581]
bw ( KiB/s): min=45168, max=127528, per=100.00%, avg=67625.19, stdev=26620.82, samples=43
iops : min=11292, max=31882, avg=16906.28, stdev=6655.20, samples=43
write: IOPS=7254, BW=28.3MiB/s (29.7MB/s)(616MiB/21730msec)
slat (nsec): min=1749, max=3412.2k, avg=9816.22, stdev=14501.05
clat (usec): min=97, max=23473, avg=2556.02, stdev=1980.53
lat (usec): min=107, max=23477, avg=2566.01, stdev=1977.65
clat percentiles (usec):
| 1.00th=[ 277], 5.00th=[ 486], 10.00th=[ 693], 20.00th=[ 824],
| 30.00th=[ 881], 40.00th=[ 1205], 50.00th=[ 2442], 60.00th=[ 2868],
| 70.00th=[ 3326], 80.00th=[ 3949], 90.00th=[ 5080], 95.00th=[ 6128],
| 99.00th=[ 8717], 99.50th=[10159], 99.90th=[14484], 99.95th=[15926],
| 99.99th=[18744]
bw ( KiB/s): min=19360, max=55040, per=100.00%, avg=29064.05, stdev=11373.59, samples=43
iops : min= 4840, max=13760, avg=7266.00, stdev=2843.41, samples=43
lat (usec) : 100=0.01%, 250=0.17%, 500=1.66%, 750=3.74%, 1000=22.57%
lat (msec) : 2=12.66%, 4=40.62%, 10=18.20%, 20=0.38%, 50=0.01%
cpu : usr=4.17%, sys=22.27%, ctx=14314, majf=0, minf=7
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
issued rwts: total=366638,157650,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
READ: bw=65.9MiB/s (69.1MB/s), 65.9MiB/s-65.9MiB/s (69.1MB/s-69.1MB/s), io=1432MiB (1502MB), run=21730-21730msec
WRITE: bw=28.3MiB/s (29.7MB/s), 28.3MiB/s-28.3MiB/s (29.7MB/s-29.7MB/s), io=616MiB (646MB), run=21730-21730msec

Disk stats (read/write):
sda: ios=364744/157621, merge=779/473, ticks=851759/352008, in_queue=1204024, util=99.61%

# fio -ioengine=libaio -bs=4k -direct=1 -buffered=1 -thread -rw=randrw -rwmixread=70 -size=2G -filename=/var/lib/docker/fio.test -name="EBS 4K randwrite test" -iodepth=64 -runtime=60
EBS 4K randwrite test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.7
Starting 1 thread
Jobs: 1 (f=1): [m(1)][100.0%][r=15.9MiB/s,w=7308KiB/s][r=4081,w=1827 IOPS][eta 00m:00s]
EBS 4K randwrite test: (groupid=0, jobs=1): err= 0: pid=31560: Tue Feb 23 14:46:10 2021
read: IOPS=4312, BW=16.8MiB/s (17.7MB/s)(1011MiB/60001msec)
slat (usec): min=63, max=14320, avg=216.76, stdev=430.61
clat (usec): min=5, max=778861, avg=10254.92, stdev=22345.40
lat (usec): min=1900, max=782277, avg=10472.16, stdev=22657.06
clat percentiles (msec):
| 1.00th=[ 6], 5.00th=[ 6], 10.00th=[ 6], 20.00th=[ 7],
| 30.00th=[ 7], 40.00th=[ 7], 50.00th=[ 7], 60.00th=[ 7],
| 70.00th=[ 8], 80.00th=[ 8], 90.00th=[ 8], 95.00th=[ 11],
| 99.00th=[ 107], 99.50th=[ 113], 99.90th=[ 132], 99.95th=[ 197],
| 99.99th=[ 760]
bw ( KiB/s): min= 168, max=29784, per=100.00%, avg=17390.92, stdev=10932.90, samples=119
iops : min= 42, max= 7446, avg=4347.71, stdev=2733.21, samples=119
write: IOPS=1852, BW=7410KiB/s (7588kB/s)(434MiB/60001msec)
slat (usec): min=3, max=666432, avg=23.59, stdev=2745.39
clat (msec): min=3, max=781, avg=10.14, stdev=20.50
lat (msec): min=3, max=781, avg=10.16, stdev=20.72
clat percentiles (msec):
| 1.00th=[ 6], 5.00th=[ 6], 10.00th=[ 6], 20.00th=[ 7],
| 30.00th=[ 7], 40.00th=[ 7], 50.00th=[ 7], 60.00th=[ 7],
| 70.00th=[ 7], 80.00th=[ 8], 90.00th=[ 8], 95.00th=[ 11],
| 99.00th=[ 107], 99.50th=[ 113], 99.90th=[ 131], 99.95th=[ 157],
| 99.99th=[ 760]
bw ( KiB/s): min= 80, max=12328, per=100.00%, avg=7469.53, stdev=4696.69, samples=119
iops : min= 20, max= 3082, avg=1867.34, stdev=1174.19, samples=119
lat (usec) : 10=0.01%
lat (msec) : 2=0.01%, 4=0.01%, 10=94.64%, 20=1.78%, 50=0.11%
lat (msec) : 100=1.80%, 250=1.63%, 500=0.01%, 750=0.02%, 1000=0.01%
cpu : usr=2.51%, sys=10.98%, ctx=260210, majf=0, minf=7
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
issued rwts: total=258768,111147,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
READ: bw=16.8MiB/s (17.7MB/s), 16.8MiB/s-16.8MiB/s (17.7MB/s-17.7MB/s), io=1011MiB (1060MB), run=60001-60001msec
WRITE: bw=7410KiB/s (7588kB/s), 7410KiB/s-7410KiB/s (7588kB/s-7588kB/s), io=434MiB (455MB), run=60001-60001msec

Disk stats (read/write):
sda: ios=258717/89376, merge=0/735, ticks=52540/564186, in_queue=616999, util=90.07%

ESSD磁盘测试数据

这是一块虚拟的阿里云网络盘,不能算完整意义的SSD(承诺IOPS 4200),数据仅供参考,磁盘概况:

1
2
3
4
5
6
$df -lh
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 99G 30G 65G 32% /

$cat /sys/block/vda/queue/rotational
1

测试数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
$fio -ioengine=libaio -bs=4k -direct=1 -buffered=1  -thread -rw=randrw  -size=4G -filename=/home/admin/fio.test -name="EBS 4K randwrite test" -iodepth=64 -runtime=60
EBS 4K randwrite test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.1
Starting 1 thread
Jobs: 1 (f=1): [m(1)][100.0%][r=10.8MiB/s,w=11.2MiB/s][r=2757,w=2876 IOPS][eta 00m:00s]
EBS 4K randwrite test: (groupid=0, jobs=1): err= 0: pid=25641: Tue Feb 23 16:35:19 2021
read: IOPS=2136, BW=8545KiB/s (8750kB/s)(501MiB/60001msec)
slat (usec): min=190, max=830992, avg=457.20, stdev=3088.80
clat (nsec): min=1792, max=1721.3M, avg=14657528.60, stdev=63188988.75
lat (usec): min=344, max=1751.1k, avg=15115.20, stdev=65165.80
clat percentiles (msec):
| 1.00th=[ 8], 5.00th=[ 9], 10.00th=[ 9], 20.00th=[ 10],
| 30.00th=[ 10], 40.00th=[ 11], 50.00th=[ 11], 60.00th=[ 11],
| 70.00th=[ 12], 80.00th=[ 12], 90.00th=[ 13], 95.00th=[ 14],
| 99.00th=[ 17], 99.50th=[ 53], 99.90th=[ 1028], 99.95th=[ 1167],
| 99.99th=[ 1653]
bw ( KiB/s): min= 56, max=12648, per=100.00%, avg=8598.92, stdev=5289.40, samples=118
iops : min= 14, max= 3162, avg=2149.73, stdev=1322.35, samples=118
write: IOPS=2137, BW=8548KiB/s (8753kB/s)(501MiB/60001msec)
slat (usec): min=2, max=181, avg= 6.67, stdev= 7.22
clat (usec): min=628, max=1721.1k, avg=14825.32, stdev=65017.66
lat (usec): min=636, max=1721.1k, avg=14832.10, stdev=65018.10
clat percentiles (msec):
| 1.00th=[ 8], 5.00th=[ 9], 10.00th=[ 9], 20.00th=[ 10],
| 30.00th=[ 10], 40.00th=[ 11], 50.00th=[ 11], 60.00th=[ 11],
| 70.00th=[ 12], 80.00th=[ 12], 90.00th=[ 13], 95.00th=[ 14],
| 99.00th=[ 17], 99.50th=[ 53], 99.90th=[ 1045], 99.95th=[ 1200],
| 99.99th=[ 1687]
bw ( KiB/s): min= 72, max=13304, per=100.00%, avg=8602.99, stdev=5296.31, samples=118
iops : min= 18, max= 3326, avg=2150.75, stdev=1324.08, samples=118
lat (usec) : 2=0.01%, 500=0.01%, 750=0.01%
lat (msec) : 2=0.01%, 4=0.01%, 10=37.85%, 20=61.53%, 50=0.10%
lat (msec) : 100=0.06%, 250=0.03%, 500=0.01%, 750=0.03%, 1000=0.25%
lat (msec) : 2000=0.14%
cpu : usr=0.70%, sys=4.01%, ctx=135029, majf=0, minf=4
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
issued rwt: total=128180,128223,0, short=0,0,0, dropped=0,0,0
latency : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
READ: bw=8545KiB/s (8750kB/s), 8545KiB/s-8545KiB/s (8750kB/s-8750kB/s), io=501MiB (525MB), run=60001-60001msec
WRITE: bw=8548KiB/s (8753kB/s), 8548KiB/s-8548KiB/s (8753kB/s-8753kB/s), io=501MiB (525MB), run=60001-60001msec

Disk stats (read/write):
vda: ios=127922/87337, merge=0/237, ticks=55122/4269885, in_queue=2209125, util=94.29%

$fio -ioengine=libaio -bs=4k -direct=1 -buffered=0 -thread -rw=randrw -size=4G -filename=/home/admin/fio.test -name="EBS 4K randwrite test" -iodepth=64 -runtime=60
EBS 4K randwrite test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.1
Starting 1 thread
Jobs: 1 (f=1): [m(1)][100.0%][r=9680KiB/s,w=9712KiB/s][r=2420,w=2428 IOPS][eta 00m:00s]
EBS 4K randwrite test: (groupid=0, jobs=1): err= 0: pid=25375: Tue Feb 23 16:33:03 2021
read: IOPS=2462, BW=9849KiB/s (10.1MB/s)(577MiB/60011msec)
slat (nsec): min=1558, max=10663k, avg=5900.28, stdev=46286.64
clat (usec): min=290, max=93493, avg=13054.57, stdev=4301.89
lat (usec): min=332, max=93497, avg=13060.60, stdev=4301.68
clat percentiles (usec):
| 1.00th=[ 1844], 5.00th=[10159], 10.00th=[10290], 20.00th=[10421],
| 30.00th=[10552], 40.00th=[10552], 50.00th=[10683], 60.00th=[10814],
| 70.00th=[18482], 80.00th=[19006], 90.00th=[19006], 95.00th=[19268],
| 99.00th=[19530], 99.50th=[19792], 99.90th=[29492], 99.95th=[30278],
| 99.99th=[43779]
bw ( KiB/s): min= 9128, max=30392, per=100.00%, avg=9850.12, stdev=1902.00, samples=120
iops : min= 2282, max= 7598, avg=2462.52, stdev=475.50, samples=120
write: IOPS=2465, BW=9864KiB/s (10.1MB/s)(578MiB/60011msec)
slat (usec): min=2, max=10586, avg= 6.92, stdev=67.34
clat (usec): min=240, max=69922, avg=12902.33, stdev=4307.92
lat (usec): min=244, max=69927, avg=12909.37, stdev=4307.03
clat percentiles (usec):
| 1.00th=[ 1729], 5.00th=[10159], 10.00th=[10290], 20.00th=[10290],
| 30.00th=[10421], 40.00th=[10421], 50.00th=[10552], 60.00th=[10683],
| 70.00th=[18220], 80.00th=[18744], 90.00th=[19006], 95.00th=[19006],
| 99.00th=[19268], 99.50th=[19530], 99.90th=[21103], 99.95th=[35390],
| 99.99th=[50594]
bw ( KiB/s): min= 8496, max=31352, per=100.00%, avg=9862.92, stdev=1991.48, samples=120
iops : min= 2124, max= 7838, avg=2465.72, stdev=497.87, samples=120
lat (usec) : 250=0.01%, 500=0.03%, 750=0.02%, 1000=0.02%
lat (msec) : 2=1.70%, 4=0.41%, 10=1.25%, 20=96.22%, 50=0.34%
lat (msec) : 100=0.01%
cpu : usr=0.89%, sys=4.09%, ctx=206337, majf=0, minf=4
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
issued rwt: total=147768,147981,0, short=0,0,0, dropped=0,0,0
latency : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
READ: bw=9849KiB/s (10.1MB/s), 9849KiB/s-9849KiB/s (10.1MB/s-10.1MB/s), io=577MiB (605MB), run=60011-60011msec
WRITE: bw=9864KiB/s (10.1MB/s), 9864KiB/s-9864KiB/s (10.1MB/s-10.1MB/s), io=578MiB (606MB), run=60011-60011msec

Disk stats (read/write):
vda: ios=147515/148154, merge=0/231, ticks=1922378/1915751, in_queue=3780605, util=98.46%

$fio -ioengine=libaio -bs=4k -direct=0 -buffered=1 -thread -rw=randrw -size=4G -filename=/home/admin/fio.test -name="EBS 4K randwrite test" -iodepth=64 -runtime=60
EBS 4K randwrite test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.1
Starting 1 thread
Jobs: 1 (f=1): [m(1)][100.0%][r=132KiB/s,w=148KiB/s][r=33,w=37 IOPS][eta 00m:00s]
EBS 4K randwrite test: (groupid=0, jobs=1): err= 0: pid=25892: Tue Feb 23 16:37:41 2021
read: IOPS=1987, BW=7949KiB/s (8140kB/s)(467MiB/60150msec)
slat (usec): min=192, max=599873, avg=479.26, stdev=2917.52
clat (usec): min=15, max=1975.6k, avg=16004.22, stdev=76024.60
lat (msec): min=5, max=2005, avg=16.48, stdev=78.00
clat percentiles (msec):
| 1.00th=[ 8], 5.00th=[ 9], 10.00th=[ 9], 20.00th=[ 10],
| 30.00th=[ 10], 40.00th=[ 11], 50.00th=[ 11], 60.00th=[ 11],
| 70.00th=[ 12], 80.00th=[ 12], 90.00th=[ 13], 95.00th=[ 14],
| 99.00th=[ 19], 99.50th=[ 317], 99.90th=[ 1133], 99.95th=[ 1435],
| 99.99th=[ 1871]
bw ( KiB/s): min= 32, max=12672, per=100.00%, avg=8034.08, stdev=5399.63, samples=119
iops : min= 8, max= 3168, avg=2008.52, stdev=1349.91, samples=119
write: IOPS=1984, BW=7937KiB/s (8127kB/s)(466MiB/60150msec)
slat (usec): min=2, max=839634, avg=18.39, stdev=2747.10
clat (msec): min=5, max=1975, avg=15.64, stdev=73.06
lat (msec): min=5, max=1975, avg=15.66, stdev=73.28
clat percentiles (msec):
| 1.00th=[ 8], 5.00th=[ 9], 10.00th=[ 9], 20.00th=[ 10],
| 30.00th=[ 10], 40.00th=[ 11], 50.00th=[ 11], 60.00th=[ 11],
| 70.00th=[ 12], 80.00th=[ 12], 90.00th=[ 13], 95.00th=[ 14],
| 99.00th=[ 18], 99.50th=[ 153], 99.90th=[ 1116], 99.95th=[ 1435],
| 99.99th=[ 1921]
bw ( KiB/s): min= 24, max=13160, per=100.00%, avg=8021.18, stdev=5405.12, samples=119
iops : min= 6, max= 3290, avg=2005.29, stdev=1351.28, samples=119
lat (usec) : 20=0.01%
lat (msec) : 10=36.51%, 20=62.63%, 50=0.21%, 100=0.12%, 250=0.05%
lat (msec) : 500=0.02%, 750=0.02%, 1000=0.19%, 2000=0.26%
cpu : usr=0.62%, sys=4.04%, ctx=125974, majf=0, minf=3
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
issued rwt: total=119533,119347,0, short=0,0,0, dropped=0,0,0
latency : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
READ: bw=7949KiB/s (8140kB/s), 7949KiB/s-7949KiB/s (8140kB/s-8140kB/s), io=467MiB (490MB), run=60150-60150msec
WRITE: bw=7937KiB/s (8127kB/s), 7937KiB/s-7937KiB/s (8127kB/s-8127kB/s), io=466MiB (489MB), run=60150-60150msec

Disk stats (read/write):
vda: ios=119533/108186, merge=0/214, ticks=54093/4937255, in_queue=2525052, util=93.99%

$fio -ioengine=libaio -bs=4k -direct=0 -buffered=0 -thread -rw=randrw -size=4G -filename=/home/admin/fio.test -name="EBS 4K randwrite test" -iodepth=64 -runtime=60
EBS 4K randwrite test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.1
Starting 1 thread
Jobs: 1 (f=1): [m(1)][100.0%][r=9644KiB/s,w=9792KiB/s][r=2411,w=2448 IOPS][eta 00m:00s]
EBS 4K randwrite test: (groupid=0, jobs=1): err= 0: pid=26139: Tue Feb 23 16:39:43 2021
read: IOPS=2455, BW=9823KiB/s (10.1MB/s)(576MiB/60015msec)
slat (nsec): min=1619, max=18282k, avg=5882.81, stdev=71214.52
clat (usec): min=281, max=64630, avg=13055.68, stdev=4233.17
lat (usec): min=323, max=64636, avg=13061.69, stdev=4232.79
clat percentiles (usec):
| 1.00th=[ 2040], 5.00th=[10290], 10.00th=[10421], 20.00th=[10421],
| 30.00th=[10552], 40.00th=[10552], 50.00th=[10683], 60.00th=[10814],
| 70.00th=[18220], 80.00th=[19006], 90.00th=[19006], 95.00th=[19268],
| 99.00th=[19530], 99.50th=[20055], 99.90th=[28967], 99.95th=[29754],
| 99.99th=[30540]
bw ( KiB/s): min= 8776, max=27648, per=100.00%, avg=9824.29, stdev=1655.78, samples=120
iops : min= 2194, max= 6912, avg=2456.05, stdev=413.95, samples=120
write: IOPS=2458, BW=9835KiB/s (10.1MB/s)(576MiB/60015msec)
slat (usec): min=2, max=10681, avg= 6.79, stdev=71.30
clat (usec): min=221, max=70411, avg=12909.50, stdev=4312.40
lat (usec): min=225, max=70414, avg=12916.40, stdev=4312.05
clat percentiles (usec):
| 1.00th=[ 1909], 5.00th=[10159], 10.00th=[10290], 20.00th=[10290],
| 30.00th=[10421], 40.00th=[10421], 50.00th=[10552], 60.00th=[10683],
| 70.00th=[18220], 80.00th=[18744], 90.00th=[19006], 95.00th=[19006],
| 99.00th=[19268], 99.50th=[19530], 99.90th=[28705], 99.95th=[40109],
| 99.99th=[60031]
bw ( KiB/s): min= 8568, max=28544, per=100.00%, avg=9836.03, stdev=1737.29, samples=120
iops : min= 2142, max= 7136, avg=2458.98, stdev=434.32, samples=120
lat (usec) : 250=0.01%, 500=0.03%, 750=0.02%, 1000=0.02%
lat (msec) : 2=1.03%, 4=1.10%, 10=0.98%, 20=96.43%, 50=0.38%
lat (msec) : 100=0.01%
cpu : usr=0.82%, sys=4.32%, ctx=212008, majf=0, minf=4
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
issued rwt: total=147386,147564,0, short=0,0,0, dropped=0,0,0
latency : target=0, window=0, percentile=100.00%, depth=64

Run status group 0 (all jobs):
READ: bw=9823KiB/s (10.1MB/s), 9823KiB/s-9823KiB/s (10.1MB/s-10.1MB/s), io=576MiB (604MB), run=60015-60015msec
WRITE: bw=9835KiB/s (10.1MB/s), 9835KiB/s-9835KiB/s (10.1MB/s-10.1MB/s), io=576MiB (604MB), run=60015-60015msec

Disk stats (read/write):
vda: ios=147097/147865, merge=0/241, ticks=1916703/1915836, in_queue=3791443, util=98.68%

测试数据总结

-direct=1 -buffered=1 -direct=1 -buffered=0 -direct=0 -buffered=0 -direct=0 -buffered=1
NVMe SSD R=10.6k W=4544 R=99.8K W=42.8K R=38.6k W=16.5k R=10.8K W=4642
SATA SSD R=4312 W=1852 R=16.9k W=7254 R=15.8k W=6803 R=5389 W=2314
ESSD R=2149 W=2150 R=2462 W=2465 R=2455 W=2458 R=1987 W=1984

看起来,对于SSD如果buffered为1的话direct没啥用,如果buffered为0那么direct为1性能要好很多

SATA SSD的IOPS比NVMe性能差很多

SATA SSD当-buffered=1参数下SATA SSD的latency在7-10us之间。

NVMe SSD以及SATA SSD当buffered=0的条件下latency均为2-3us, NVMe SSD latency参考文章第一个表格, 和本次NVMe测试结果一致.

ESSD的latency基本是13-16us。

以上NVMe SSD测试数据是在测试过程中还有mysql在全力导入数据的情况下,用fio测试所得。所以空闲情况下测试结果会更好。

HDD性能测试数据

img

从上图可以看到这个磁盘的IOPS 读 935 写 400,读rt 10731nsec 大约10us, 写 17us。如果IOPS是1000的话,rt应该是1ms,实际比1ms小两个数量级,应该是cache、磁盘阵列在起作用。

SATA硬盘,10K转

万转机械硬盘组成RAID5阵列,在顺序条件最好的情况下,带宽可以达到1GB/s以上,平均延时也非常低,最低只有20多us。但是在随机IO的情况下,机械硬盘的短板就充分暴露了,零点几兆的带宽,将近5ms的延迟,IOPS只有200左右。其原因是因为

  • 随机访问直接让RAID卡缓存成了个摆设
  • 磁盘不能并行工作,因为我的机器RAID宽度Strip Size为128 KB
  • 机械轴也得在各个磁道之间跳来跳去。

理解了磁盘顺序IO时候的几十M甚至一个GB的带宽,随机IO这个真的是太可怜了。

从上面的测试数据中我们看到了机械硬盘在顺序IO和随机IO下的巨大性能差异。在顺序IO情况下,磁盘是最擅长的顺序IO,再加上Raid卡缓存命中率也高。这时带宽表现有几十、几百M,最好条件下甚至能达到1GB。IOPS这时候能有2-3W左右。到了随机IO的情形下,机械轴也被逼的跳来跳去寻道,RAID卡缓存也失效了。带宽跌到了1MB以下,最低只有100K,IOPS也只有可怜巴巴的200左右。

网上测试数据参考

我们来一起看一下具体的数据。首先来看NVMe如何减小了协议栈本身的时间消耗,我们用blktrace工具来分析一组传输在应用程序层、操作系统层、驱动层和硬件层消耗的时间和占比,来了解AHCI和NVMe协议的性能区别:

img

硬盘HDD作为一个参考基准,它的时延是非常大的,达到14ms,而AHCI SATA为125us,NVMe为111us。我们从图中可以看出,NVMe相对AHCI,协议栈及之下所占用的时间比重明显减小,应用程序层面等待的时间占比很高,这是因为SSD物理硬盘速度不够快,导致应用空转。NVMe也为将来Optane硬盘这种低延迟介质的速度提高留下了广阔的空间。

rq_affinity

参考aliyun测试文档 , rq_affinity增加2的commit: git show 5757a6d76c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function RunFio
{
numjobs=$1 # 实例中的测试线程数,例如示例中的10
iodepth=$2 # 同时发出I/O数的上限,例如示例中的64
bs=$3 # 单次I/O的块文件大小,例如示例中的4k
rw=$4 # 测试时的读写策略,例如示例中的randwrite
filename=$5 # 指定测试文件的名称,例如示例中的/dev/your_device
nr_cpus=`cat /proc/cpuinfo |grep "processor" |wc -l`
if [ $nr_cpus -lt $numjobs ];then
echo “Numjobs is more than cpu cores, exit!”
exit -1
fi
let nu=$numjobs+1
cpulist=""
for ((i=1;i<10;i++))
do
list=`cat /sys/block/your_device/mq/*/cpu_list | awk '{if(i<=NF) print $i;}' i="$i" | tr -d ',' | tr '\n' ','`
if [ -z $list ];then
break
fi
cpulist=${cpulist}${list}
done
spincpu=`echo $cpulist | cut -d ',' -f 2-${nu}`
echo $spincpu
fio --ioengine=libaio --runtime=30s --numjobs=${numjobs} --iodepth=${iodepth} --bs=${bs} --rw=${rw} --filename=${filename} --time_based=1 --direct=1 --name=test --group_reporting --cpus_allowed=$spincpu --cpus_allowed_policy=split
}
echo 2 > /sys/block/your_device/queue/rq_affinity
sleep 5
RunFio 10 64 4k randwrite filename

对NVME SSD进行测试,左边rq_affinity是2,右边rq_affinity为1,在这个测试参数下rq_affinity为1的性能要好(后许多次测试两者性能差不多)

image-20210607113709945

LVM性能对比

磁盘信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 223.6G 0 disk
├─sda1 8:1 0 3M 0 part
├─sda2 8:2 0 1G 0 part /boot
├─sda3 8:3 0 96G 0 part /
├─sda4 8:4 0 10G 0 part /tmp
└─sda5 8:5 0 116.6G 0 part /home
nvme0n1 259:4 0 2.7T 0 disk
└─nvme0n1p1 259:5 0 2.7T 0 part
└─vg1-drds 252:0 0 5.4T 0 lvm /drds
nvme1n1 259:0 0 2.7T 0 disk
└─nvme1n1p1 259:2 0 2.7T 0 part /u02
nvme2n1 259:1 0 2.7T 0 disk
└─nvme2n1p1 259:3 0 2.7T 0 part
└─vg1-drds 252:0 0 5.4T 0 lvm /drds

单块nvme SSD盘跑mysql server,运行sysbench导入测试数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#iostat -x nvme1n1 1
Linux 3.10.0-327.ali2017.alios7.x86_64 (k28a11352.eu95sqa) 05/13/2021 _x86_64_ (64 CPU)

avg-cpu: %user %nice %system %iowait %steal %idle
0.32 0.00 0.17 0.07 0.00 99.44

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
nvme1n1 0.00 47.19 0.19 445.15 2.03 43110.89 193.62 0.31 0.70 0.03 0.70 0.06 2.85

avg-cpu: %user %nice %system %iowait %steal %idle
1.16 0.00 0.36 0.17 0.00 98.31

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
nvme1n1 0.00 122.00 0.00 3290.00 0.00 271052.00 164.77 1.65 0.50 0.00 0.50 0.05 17.00

#iostat 1
Linux 3.10.0-327.ali2017.alios7.x86_64 (k28a11352.eu95sqa) 05/13/2021 _x86_64_ (64 CPU)

avg-cpu: %user %nice %system %iowait %steal %idle
0.14 0.00 0.13 0.05 0.00 99.67

Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 49.21 554.51 2315.83 1416900 5917488
nvme1n1 5.65 2.34 844.73 5989 2158468
nvme2n1 0.06 1.13 0.00 2896 0
nvme0n1 0.06 1.13 0.00 2900 0
dm-0 0.02 0.41 0.00 1036 0

avg-cpu: %user %nice %system %iowait %steal %idle
1.39 0.00 0.23 0.08 0.00 98.30

Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 8.00 0.00 60.00 0 60
nvme1n1 868.00 0.00 132100.00 0 132100
nvme2n1 0.00 0.00 0.00 0 0
nvme0n1 0.00 0.00 0.00 0 0
dm-0 0.00 0.00 0.00 0 0

avg-cpu: %user %nice %system %iowait %steal %idle
1.44 0.00 0.14 0.09 0.00 98.33

Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 0.00 0.00 0.00 0 0
nvme1n1 766.00 0.00 132780.00 0 132780
nvme2n1 0.00 0.00 0.00 0 0
nvme0n1 0.00 0.00 0.00 0 0
dm-0 0.00 0.00 0.00 0 0

avg-cpu: %user %nice %system %iowait %steal %idle
1.41 0.00 0.16 0.09 0.00 98.34

Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 105.00 0.00 532.00 0 532
nvme1n1 760.00 0.00 122236.00 0 122236
nvme2n1 0.00 0.00 0.00 0 0
nvme0n1 0.00 0.00 0.00 0 0
dm-0 0.00 0.00 0.00 0 0

如果同样写lvm,由两块nvme组成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
nvme2n1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
nvme0n1 0.00 137.00 0.00 5730.00 0.00 421112.00 146.98 2.95 0.52 0.00 0.52 0.05 27.30

avg-cpu: %user %nice %system %iowait %steal %idle
1.17 0.00 0.34 0.19 0.00 98.30

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
nvme2n1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
nvme0n1 0.00 109.00 0.00 2533.00 0.00 271236.00 214.16 1.08 0.43 0.00 0.43 0.06 15.90

avg-cpu: %user %nice %system %iowait %steal %idle
1.38 0.00 0.42 0.20 0.00 98.00

Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
nvme2n1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
nvme0n1 0.00 118.00 0.00 3336.00 0.00 320708.00 192.27 1.50 0.45 0.00 0.45 0.06 20.00

[root@k28a11352.eu95sqa /var/lib]
#iostat 1
Linux 3.10.0-327.ali2017.alios7.x86_64 (k28a11352.eu95sqa) 05/13/2021 _x86_64_ (64 CPU)

avg-cpu: %user %nice %system %iowait %steal %idle
0.40 0.00 0.20 0.07 0.00 99.33

Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 38.96 334.64 1449.68 1419236 6148304
nvme1n1 324.95 1.43 31201.30 6069 132329072
nvme2n1 0.07 0.90 0.00 3808 0
nvme0n1 256.24 1.60 22918.46 6801 97200388
dm-0 266.98 1.38 22918.46 5849 97200388

avg-cpu: %user %nice %system %iowait %steal %idle
1.20 0.00 0.42 0.25 0.00 98.12

Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 0.00 0.00 0.00 0 0
nvme1n1 0.00 0.00 0.00 0 0
nvme2n1 0.00 0.00 0.00 0 0
nvme0n1 4460.00 0.00 332288.00 0 332288
dm-0 4608.00 0.00 332288.00 0 332288

avg-cpu: %user %nice %system %iowait %steal %idle
1.35 0.00 0.38 0.22 0.00 98.06

Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn
sda 48.00 0.00 200.00 0 200
nvme1n1 0.00 0.00 0.00 0 0
nvme2n1 0.00 0.00 0.00 0 0
nvme0n1 4187.00 0.00 332368.00 0 332368
dm-0 4348.00 0.00 332368.00 0 332368

不知道为什么只有一个块ssd有流量,可能跟只写一个文件有关系

SSD中,SATA、m2、PCIE和NVME各有什么意义

高速信号协议

SAS,SATA,PCIe 这三个是同一个层面上的,模拟串行高速接口。

  • SAS 对扩容比较友好,也支持双控双活。接上SAS RAID 卡,一般在阵列上用的比较多。
  • SATA 对热插拔很友好,早先台式机装机市场的 SSD基本上都是SATA的,现在的 机械硬盘也是SATA接口居多。但速率上最高只能到 6Gb/s,上限 768MB/s左右,现在已经慢慢被pcie取代。
  • PCIe 支持速率更高,也离CPU最近。很多设备 如 网卡,显卡也都走pcie接口,当然也有SSD。现在比较主流的是PCIe 3.0,8Gb/s 看起来好像也没比 SATA 高多少,但是 PCIe 支持多个LANE,每个LANE都是 8Gb/s,这样性能就倍数增加了。目前,SSD主流的是 PCIe 3.0x4 lane,性能可以做到 3500MB/s 左右。

传输层协议

SCSI,ATA,NVMe 都属于这一层。主要是定义命令集,数字逻辑层。

  • SCSI 命令集 历史悠久,应用也很广泛。U盘,SAS 盘,还有手机上 UFS 之类很多设备都走的这个命令集。
  • ATA 则只是跑在SATA 协议上
  • NVMe 协议是有特意为 NAND 进行优化。相比于上面两者,效率更高。主要是跑在 PCIe 上的。当然,也有NVMe-MI,NVMe-of之类的。是个很好的传输层协议。

物理接口

M.2 , U.2 , AIC, NGFF 这些属于物理接口

像 M.2 可以是 SATA SSD 也可以是 NVMe(PCIe) SSD。金手指上有一个 SATA/PCIe 的选择信号,来区分两者。很多笔记本的M.2 接口也是同时支持两种类型的盘的。

  • M.2 , 主要用在 笔记本上,优点是体积小,缺点是散热不好。

  • U.2,主要用在 数据中心或者一些企业级用户,对热插拔需求高的地方。优点热插拔,散热也不错。一般主要是pcie ssd(也有sas ssd),受限于接口,最多只能是 pcie 4lane

  • AIC,企业,行业用户用的比较多。通常会支持pcie 4lane/8lane,带宽上限更高

数据总结

  • 性能排序 NVMe SSD > SATA SSD > SAN > ESSD > HDD
  • 本地ssd性能最好、sas机械盘(RAID10)性能最差
  • san存储走特定的光纤网络,不是走tcp的san(至少从网卡看不到san的流量),性能居中
  • 从rt来看 ssd:san:sas 大概是 1:3:15
  • san比本地sas机械盘性能要好,这也许取决于san的网络传输性能和san存储中的设备(比如用的ssd而不是机械盘)
  • NVMe SSD比SATA SSD快很多,latency更稳定
  • 阿里云的云盘ESSD比本地SAS RAID10阵列性能还好

参考资料

http://cizixs.com/2017/01/03/how-slow-is-disk-and-network

https://tobert.github.io/post/2014-04-17-fio-output-explained.html

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

块存储NVMe云盘原型实践

机械硬盘随机IO慢的超乎你的想象

搭载固态硬盘的服务器究竟比搭机械硬盘快多少?

如何制作本地yum repository

某些情况下在没有外网的环境需要安装一些软件,但是软件依赖比较多,那么可以提前将所有依赖下载到本地,然后将他们制作成一个yum repo,安装的时候就会自动将依赖包都安装好。

收集所有rpm包

创建一个文件夹,比如 Yum,将收集到的所有rpm包放在里面,比如安装ansible和docker需要的依赖文件:

1
2
3
4
5
6
7
8
9
10
11
12
-rwxr-xr-x 1 root root  73K 7月  12 14:22 audit-libs-python-2.8.4-4.el7.x86_64.rpm
-rwxr-xr-x 1 root root 295K 7月 12 14:22 checkpolicy-2.5-8.el7.x86_64.rpm
-rwxr-xr-x 1 root root 23M 7月 12 14:22 containerd.io-1.2.2-3.el7.x86_64.rpm
-rwxr-xr-x 1 root root 26K 7月 12 14:22 container-selinux-2.9-4.el7.noarch.rpm
-rwxr-xr-x 1 root root 37K 7月 12 14:22 container-selinux-2.74-1.el7.noarch.rpm
-rwxr-xr-x 1 root root 14M 7月 12 14:22 docker-ce-cli-18.09.0-3.el7.x86_64.rpm
-rwxr-xr-x 1 root root 29K 7月 12 14:22 docker-ce-selinux-17.03.2.ce-1.el7.centos.noarch.rpm
-r-xr-xr-x 1 root root 22K 7月 12 14:23 sshpass-1.06-2.el7.x86_64.rpm
-r-xr-xr-x 1 root root 22K 7月 12 14:23 sshpass-1.06-1.el7.x86_64.rpm
-r-xr-xr-x 1 root root 154K 7月 12 14:23 PyYAML-3.10-11.el7.x86_64.rpm
-r-xr-xr-x 1 root root 29K 7月 12 14:23 python-six-1.9.0-2.el7.noarch.rpm
-r-xr-xr-x 1 root root 397K 7月 12 14:23 python-setuptools-0.9.8-7.el7.noarch.rpm

收集方法:

1
2
3
4
5
6
7
8
//先安装yum工具
yum install yum-utils -y
//将 ansible 依赖包都下载下来
repoquery --requires --resolve --recursive ansible | xargs -r yumdownloader --destdir=/tmp/ansible
//将ansible rpm自己下载回来
yumdownloader --destdir=/tmp/ansible --resolve ansible
//验证一下依赖关系是完整的
//repotrack ansible

创建仓库索引

需要安装工具 yum install createrepo -y:

1
2
3
4
5
6
7
8
9
10
# createrepo ./public_yum/
Spawning worker 0 with 6 pkgs
Spawning worker 1 with 6 pkgs
Spawning worker 23 with 5 pkgs
Workers Finished
Saving Primary metadata
Saving file lists metadata
Saving other metadata
Generating sqlite DBs
Sqlite DBs complete

会在yum文件夹下生成一个索引文件夹 repodata

1
2
3
4
5
6
7
8
9
drwxr-xr-x 2 root root 4.0K 7月  12 14:25 repodata
[root@az1-drds-79 yum]# ls repodata/
5e15c62fec1fe43c6025ecf4d370d632f4b3f607500016e045ad94b70f87bac3-filelists.xml.gz
7a314396d6e90532c5c534567f9bd34eee94c3f8945fc2191b225b2861ace2b6-other.xml.gz
ce9dce19f6b426b8856747b01d51ceaa2e744b6bbd5fbc68733aa3195f724590-primary.xml.gz
ee33b7d79e32fe6ad813af92a778a0ec8e5cc2dfdc9b16d0be8cff6a13e80d99-filelists.sqlite.bz2
f7e8177e7207a4ff94bade329a0f6b572a72e21da106dd9144f8b1cdf0489cab-primary.sqlite.bz2
ff52e1f1859790a7b573d2708b02404eb8b29aa4b0c337bda83af75b305bfb36-other.sqlite.bz2
repomd.xml

生成iso镜像文件

非必要步骤,如果需要带到客户环境可以先生成iso,不过不够灵活。

也可以不用生成iso,直接在drds.repo中指定 createrepo 的目录也可以,记得要先执行 yum clean all和yum update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#mkisofs -r -o docker_ansible.iso ./yum/
I: -input-charset not specified, using utf-8 (detected in locale settings)
Using PYTHO000.RPM;1 for /python-httplib2-0.7.7-3.el7.noarch.rpm (python-httplib2-0.9.1-3.el7.noarch.rpm)
Using MARIA006.RPM;1 for /mariadb-5.5.56-2.el7.x86_64.rpm (mariadb-libs-5.5.56-2.el7.x86_64.rpm)
Using LIBTO001.RPM;1 for /libtomcrypt-1.17-25.el7.x86_64.rpm (libtomcrypt-1.17-26.el7.x86_64.rpm)
6.11% done, estimate finish Sun Jul 12 14:26:47 2020
97.60% done, estimate finish Sun Jul 12 14:26:48 2020
Total translation table size: 0
Total rockridge attributes bytes: 14838
Total directory bytes: 2048
Path table size(bytes): 26
Max brk space used 21000
81981 extents written (160 MB)

将 生成的 iso挂载到目标机器上

1
2
3
# mkdir /mnt/iso
# mount ./docker_ansible.iso /mnt/iso
mount: /dev/loop0 is write-protected, mounting read-only

配置本地 yum 源

yum repository不是必须要求iso挂载,直接指向rpm文件夹(必须要有 createrepo 建立索引了)也可以

1
2
3
4
5
6
7
8
9
# cat /etc/yum.repos.d/drds.repo 
[drds]
name=drds Extra Packages for Enterprise Linux 7 - $basearch
enabled=1
failovermethod=priority
baseurl=file:///mnt/repo #baseurl=http://192.168.1.91:8000/ 本地内网
priority=1 #添加priority=1,数字越小优先级越高,也可以修改网络源的priority的值
gpgcheck=0
#gpgkey=file:///mnt/cdrom/RPM-GPG-KEY-CentOS-5 #注:这个你cd /mnt/cdrom/可以看到这个key,这里仅仅是个例子, 因为gpgcheck是0 ,所以gpgkey不需要了

到此就可以在没有网络环境的机器上直接:yum install ansible docker -y 了

测试

测试的话可以指定repo 源: yum install ansible –enablerepo=drds (drds 优先级最高)

本地会cache一些rpm的版本信息,可以执行 yum clean all 得到一个干净的测试环境

1
2
3
yum clean all
yum list
yum deplist ansible

yum 源问题处理

Yum commands error “pycurl.so: undefined symbol”

xargs 作用

xargs命令的作用,是将标准输入转为命令行参数。因为有些命令是不接受标准输入的,比如echo

xargs的作用在于,大多数命令(比如rmmkdirls)与管道一起使用时,都需要xargs将标准输入转为命令行参数。

dnf 使用

DNF 是新一代的rpm软件包管理器。他首先出现在 Fedora 18 这个发行版中。而最近,它取代了yum,正式成为 Fedora 22 的包管理器。

DNF包管理器克服了YUM包管理器的一些瓶颈,提升了包括用户体验,内存占用,依赖分析,运行速度等多方面的内容。DNF使用 RPM, libsolv 和 hawkey 库进行包管理操作。尽管它没有预装在 CentOS 和 RHEL 7 中,但你可以在使用 YUM 的同时使用 DNF 。你可以在这里获得关于 DNF 的更多知识:《 DNF 代替 YUM ,你所不知道的缘由》

DNF 包管理器作为 YUM 包管理器的升级替代品,它能自动完成更多的操作。但在我看来,正因如此,所以 DNF 包管理器不会太受那些经验老道的 Linux 系统管理者的欢迎。举例如下:

  1. 在 DNF 中没有 –skip-broken 命令,并且没有替代命令供选择。
  2. 在 DNF 中没有判断哪个包提供了指定依赖的 resolvedep 命令。
  3. 在 DNF 中没有用来列出某个软件依赖包的 deplist 命令。
  4. 当你在 DNF 中排除了某个软件库,那么该操作将会影响到你之后所有的操作,不像在 YUM 下那样,你的排除操作只会咋升级和安装软件时才起作用。

安装yum源

安装7.70版本curl yum源

1
rpm -Uvh http://www.city-fan.org/ftp/contrib/yum-repo/city-fan.org-release-2-1.rhel7.noarch.rpm

其它技巧

rpm依赖查询

1
2
3
4
rpm -q --whatprovides file-name //查询一个文件来自哪个rpm包
rpm -qf /usr/lib/systemd/libsystemd-shared-239.so // 查询一个so lib来自哪个rpm包
或者 yum whatprovides /usr/lib/systemd/libsystemd-shared-239.so
yum provides */libmysqlclient.so.18

制作debian 仓库

适合ubuntu、deepin、uos等, 参考:https://lework.github.io/2021/04/03/debian-kubeadm-install/

1
2
#添加新仓库
sudo apt-add-repository 'deb http://ftp.us.debian.org/debian stretch main contrib non-free'

img

rpm转换成 dpkg

apt-mirror

先要安装apt-mirror 工具,安装后会生成配置文件 /etc/apt/mirror.list 然后需要手工修改配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#cat /etc/apt/mirror.list
############# config ##################

#下载下来的仓库文件放在哪里
set base_path /polarx/debian

set mirror_path $base_path/mirror
set skel_path $base_path/skel
set var_path $base_path/var
set cleanscript $var_path/clean.sh
set defaultarch amd64
#set postmirror_script $var_path/postmirror.sh
set run_postmirror 0
set nthreads 20
set _tilde 0
#
############# end config ##############

#从哪里镜像仓库
deb http://yum.tbsite.net/mirrors/debian/ buster main non-free contrib
#deb https://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial main

#deb http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-src http://ftp.us.debian.org/debian unstable main contrib non-free

# mirror additional architectures
#deb-alpha http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-amd64 http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-armel http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-hppa http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-i386 http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-ia64 http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-m68k http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-mips http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-mipsel http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-powerpc http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-s390 http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-sparc http://ftp.us.debian.org/debian unstable main contrib non-free

#clean http://ftp.us.debian.org/debian
clean http://yum.tbsite.net/mirrors/debian/

debian仓库介绍

一个Debian仓库包含多个发行版。Debian 的发行版是以 “玩具总动员 “电影中的角色命名的 (wheezy, jessie, stretch, …)。 代号有别名,叫做套件(stable, oldstable, testing, unstable)。一个发行版会被分成几个组件。在 Debian 中,这些组件被命名为 main, contrib, 和 non-free,并表并表示它们所包含的软件的授权条款。一个版本也有各种架构(amd64, i386, mips, powerpc, s390x, …)的软件包,以及源码和架构独立的软件包。

仓库的根目录下有一个dists 目录,而这个目录又有每个发行版和套件的目录,后者通常是前者的符号链接,但浏览器不会向您显示出这个区别。每个发行版子目录都包含一个加密签名的Release文件和每个组件的目录,里面是不同架构的目录,名为binary-*<架构>*和sources。而在这些文件中,Packages是文本文件,包含了软件包。嗯,那么实际的软件包在哪里?

image-20220829163817671

软件包本身在仓库根目录下的pool。在pool下面又有所有组件的目录,其中有0,…,9ab,.., z, liba, … , libz。 而在这些目录中,是以它们所包含的软件包命名的目录,这些目录最后包含实际的软件包,即.deb文件。这个名字不一定是软件包本身的名字,例如,软件包bsdutils在目录pool/main/u/util-linux 下,它是生成软件包的源码的名称。一个上游源可能会生成多个二进制软件包,而所有这些软件包最终都会在pool下面的同一个子目录中。额外的单字母目录只是一个技巧,以避免在一个目录中有太多的条目,因为这是很多系统传统上存在性能问题的原因。

pool下面的子目录中,通常会有多个版本的软件包,而每个版本的软件包属于什么发行版的信息只存在于索引中。这样一来,同一个版本的包可软件以属于多个发行版,但只使用一次磁盘空间,而且不需要求助于硬链接或符号链接,所以镜像相当简单,甚至可以在没有这些概念的系统中进行。

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apt install kubeadm=1.20.12-00 //指定版本安装

#查询可用版本
apt-cache policy kubeadm
apt list --all-versions kubeadm

#清理
apt clean --dry-run
apt update
apt list
apt show kubeadm

#查询安装包的所有文件
dpkg-query -L kubeadm

#列出所有依赖包
apt-cache depends ansible

#被依赖查询
apt-cache rdepends kubelet

dpkg -I kubernetes/pool/kubeadm_1.21.0-00_amd64.deb

#下载依赖包
apt-get download $(apt-rdepends kubeadm|grep -v "^ ")
aptitude --download-only -y install $(apt-rdepends kubeadm|grep -v "^ ") //不能下载已经安装了的依赖包

apt download $(apt-cache depends --recurse --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances kubeadm | grep "^\w" | sort -u)

简单仓库

下载所有 deb 包以及他们的依赖

1
apt download $(apt-cache depends --recurse --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances kubeadm | grep "^\w" | sort -u)

生成 index

1
dpkg-scanpackages -m . > Packages

apt source 指向这个目录

1
deb [trusted=yes] file:/polarx/test /

Kubernetes 仓库

debian 上通过kubeadm 安装 kubernetes 集群

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//官方
echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | tee -a /etc/apt/sources.list.d/kubernetes.list

//阿里云仓库
echo 'deb https://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial main' > /etc/apt/sources.list.d/kubernetes.list
curl -s https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add -
apt-get update

export KUBE_VERSION="1.20.5"
apt-get install -y kubeadm=$KUBE_VERSION-00 kubelet=$KUBE_VERSION-00 kubectl=$KUBE_VERSION-00
sudo apt-mark hold kubelet kubeadm kubectl

[ -d /etc/bash_completion.d ] && \
{ kubectl completion bash > /etc/bash_completion.d/kubectl; \
kubeadm completion bash > /etc/bash_completion.d/kubadm; }

[ ! -d /usr/lib/systemd/system/kubelet.service.d ] && mkdir -p /usr/lib/systemd/system/kubelet.service.d
cat << EOF > /usr/lib/systemd/system/kubelet.service.d/11-cgroup.conf
[Service]
CPUAccounting=true
MemoryAccounting=true
BlockIOAccounting=true
ExecStartPre=/usr/bin/bash -c '/usr/bin/mkdir -p /sys/fs/cgroup/{cpuset,memory,systemd,pids,"cpu,cpuacct"}/{system,kube,kubepods}.slice'
Slice=kube.slice
EOF
systemctl daemon-reload

systemctl enable kubelet.service

docker 仓库

1
2
3
4
5
6
7
apt-get install -y apt-transport-https ca-certificates curl gnupg2 lsb-release bash-completion

curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/debian/gpg | sudo apt-key add -
echo "deb [arch=amd64] https://mirrors.aliyun.com/docker-ce/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker-ce.list
sudo apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io
apt-mark hold docker-ce docker-ce-cli containerd.io

参考资料

xargs 命令教程

如何制作软件仓库

某些情况下在没有外网的环境需要安装一些软件,但是软件依赖比较多,那么可以提前将所有依赖下载到本地,然后将他们制作成一个yum repo,安装的时候就会自动将依赖包都安装好。

centos下是 yum 仓库,Debian、ubuntu下是apt仓库,我们先讲 yum 仓库的制作,Debian apt 仓库类似

收集所有rpm包

创建一个文件夹,比如 Yum,将收集到的所有rpm包放在里面,比如安装ansible和docker需要的依赖文件:

1
2
3
4
5
6
7
8
9
10
11
12
-rwxr-xr-x 1 root root  73K 7月  12 14:22 audit-libs-python-2.8.4-4.el7.x86_64.rpm
-rwxr-xr-x 1 root root 295K 7月 12 14:22 checkpolicy-2.5-8.el7.x86_64.rpm
-rwxr-xr-x 1 root root 23M 7月 12 14:22 containerd.io-1.2.2-3.el7.x86_64.rpm
-rwxr-xr-x 1 root root 26K 7月 12 14:22 container-selinux-2.9-4.el7.noarch.rpm
-rwxr-xr-x 1 root root 37K 7月 12 14:22 container-selinux-2.74-1.el7.noarch.rpm
-rwxr-xr-x 1 root root 14M 7月 12 14:22 docker-ce-cli-18.09.0-3.el7.x86_64.rpm
-rwxr-xr-x 1 root root 29K 7月 12 14:22 docker-ce-selinux-17.03.2.ce-1.el7.centos.noarch.rpm
-r-xr-xr-x 1 root root 22K 7月 12 14:23 sshpass-1.06-2.el7.x86_64.rpm
-r-xr-xr-x 1 root root 22K 7月 12 14:23 sshpass-1.06-1.el7.x86_64.rpm
-r-xr-xr-x 1 root root 154K 7月 12 14:23 PyYAML-3.10-11.el7.x86_64.rpm
-r-xr-xr-x 1 root root 29K 7月 12 14:23 python-six-1.9.0-2.el7.noarch.rpm
-r-xr-xr-x 1 root root 397K 7月 12 14:23 python-setuptools-0.9.8-7.el7.noarch.rpm

收集方法:

1
2
3
4
5
6
7
8
//先安装yum工具
yum install yum-utils -y
//将 ansible 依赖包都下载下来
repoquery --requires --resolve --recursive ansible | xargs -r yumdownloader --destdir=/tmp/ansible
//将ansible rpm自己下载回来
yumdownloader --destdir=/tmp/ansible --resolve ansible
//验证一下依赖关系是完整的
//repotrack ansible

创建仓库索引

需要安装工具 yum install createrepo -y:

1
2
3
4
5
6
7
8
9
10
# createrepo ./public_yum/
Spawning worker 0 with 6 pkgs
Spawning worker 1 with 6 pkgs
Spawning worker 23 with 5 pkgs
Workers Finished
Saving Primary metadata
Saving file lists metadata
Saving other metadata
Generating sqlite DBs
Sqlite DBs complete

会在yum文件夹下生成一个索引文件夹 repodata

1
2
3
4
5
6
7
8
9
drwxr-xr-x 2 root root 4.0K 7月  12 14:25 repodata
[root@az1-drds-79 yum]# ls repodata/
5e15c62fec1fe43c6025ecf4d370d632f4b3f607500016e045ad94b70f87bac3-filelists.xml.gz
7a314396d6e90532c5c534567f9bd34eee94c3f8945fc2191b225b2861ace2b6-other.xml.gz
ce9dce19f6b426b8856747b01d51ceaa2e744b6bbd5fbc68733aa3195f724590-primary.xml.gz
ee33b7d79e32fe6ad813af92a778a0ec8e5cc2dfdc9b16d0be8cff6a13e80d99-filelists.sqlite.bz2
f7e8177e7207a4ff94bade329a0f6b572a72e21da106dd9144f8b1cdf0489cab-primary.sqlite.bz2
ff52e1f1859790a7b573d2708b02404eb8b29aa4b0c337bda83af75b305bfb36-other.sqlite.bz2
repomd.xml

生成iso镜像文件

非必要步骤,如果需要带到客户环境可以先生成iso,不过不够灵活。

也可以不用生成iso,直接在drds.repo中指定 createrepo 的目录也可以,记得要先执行 yum clean all和yum update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#mkisofs -r -o docker_ansible.iso ./yum/
I: -input-charset not specified, using utf-8 (detected in locale settings)
Using PYTHO000.RPM;1 for /python-httplib2-0.7.7-3.el7.noarch.rpm (python-httplib2-0.9.1-3.el7.noarch.rpm)
Using MARIA006.RPM;1 for /mariadb-5.5.56-2.el7.x86_64.rpm (mariadb-libs-5.5.56-2.el7.x86_64.rpm)
Using LIBTO001.RPM;1 for /libtomcrypt-1.17-25.el7.x86_64.rpm (libtomcrypt-1.17-26.el7.x86_64.rpm)
6.11% done, estimate finish Sun Jul 12 14:26:47 2020
97.60% done, estimate finish Sun Jul 12 14:26:48 2020
Total translation table size: 0
Total rockridge attributes bytes: 14838
Total directory bytes: 2048
Path table size(bytes): 26
Max brk space used 21000
81981 extents written (160 MB)

将 生成的 iso挂载到目标机器上

1
2
3
# mkdir /mnt/iso
# mount ./docker_ansible.iso /mnt/iso
mount: /dev/loop0 is write-protected, mounting read-only

配置本地 yum 源

yum repository不是必须要求iso挂载,直接指向rpm文件夹(必须要有 createrepo 建立索引了)也可以

1
2
3
4
5
6
7
8
9
# cat /etc/yum.repos.d/drds.repo 
[drds]
name=drds Extra Packages for Enterprise Linux 7 - $basearch
enabled=1
failovermethod=priority
baseurl=file:///mnt/repo #baseurl=http://192.168.1.91:8000/ 本地内网
priority=1 #添加priority=1,数字越小优先级越高,也可以修改网络源的priority的值
gpgcheck=0
#gpgkey=file:///mnt/cdrom/RPM-GPG-KEY-CentOS-5 #注:这个你cd /mnt/cdrom/可以看到这个key,这里仅仅是个例子, 因为gpgcheck是0 ,所以gpgkey不需要了

到此就可以在没有网络环境的机器上直接:yum install ansible docker -y 了

yum 仓库代理

如果需要代理可以在docker.repo 最后添加:

1
proxy=socks5://127.0.0.1:12345 //socks代理 或者 proxy=http://ip:port

如果要给全局配置代理,则在 /etc/yum.conf 最后添加如上行

测试

测试的话可以指定repo 源: yum install ansible –enablerepo=drds (drds 优先级最高)

本地会cache一些rpm的版本信息,可以执行 yum clean all 得到一个干净的测试环境

1
2
3
yum clean all
yum list
yum deplist ansible

yum 源问题处理

Yum commands error “pycurl.so: undefined symbol”

xargs 作用

xargs命令的作用,是将标准输入转为命令行参数。因为有些命令是不接受标准输入的,比如echo

xargs的作用在于,大多数命令(比如rmmkdirls)与管道一起使用时,都需要xargs将标准输入转为命令行参数。

dnf 使用

DNF 是新一代的rpm软件包管理器。他首先出现在 Fedora 18 这个发行版中。而最近,它取代了yum,正式成为 Fedora 22 的包管理器。

DNF包管理器克服了YUM包管理器的一些瓶颈,提升了包括用户体验,内存占用,依赖分析,运行速度等多方面的内容。DNF使用 RPM, libsolv 和 hawkey 库进行包管理操作。尽管它没有预装在 CentOS 和 RHEL 7 中,但你可以在使用 YUM 的同时使用 DNF 。你可以在这里获得关于 DNF 的更多知识:《 DNF 代替 YUM ,你所不知道的缘由》

DNF 包管理器作为 YUM 包管理器的升级替代品,它能自动完成更多的操作。但在我看来,正因如此,所以 DNF 包管理器不会太受那些经验老道的 Linux 系统管理者的欢迎。举例如下:

  1. 在 DNF 中没有 –skip-broken 命令,并且没有替代命令供选择。
  2. 在 DNF 中没有判断哪个包提供了指定依赖的 resolvedep 命令。
  3. 在 DNF 中没有用来列出某个软件依赖包的 deplist 命令。
  4. 当你在 DNF 中排除了某个软件库,那么该操作将会影响到你之后所有的操作,不像在 YUM 下那样,你的排除操作只会咋升级和安装软件时才起作用。

安装yum源

安装7.70版本curl yum源

1
rpm -Uvh http://www.city-fan.org/ftp/contrib/yum-repo/city-fan.org-release-2-1.rhel7.noarch.rpm

其它技巧

rpm依赖查询

1
2
3
4
5
6
rpm -q --whatprovides file-name //查询一个文件来自哪个rpm包
rpm -qf /usr/lib/systemd/libsystemd-shared-239.so // 查询一个so lib来自哪个rpm包
或者 yum whatprovides /usr/lib/systemd/libsystemd-shared-239.so
yum provides */libmysqlclient.so.18

rpm -qi //查看rpm包安装的安装信息

多版本问题

1
2
3
4
5
6
7
//查看protobuf 的多个版本
yum --showduplicates list protobuf
//or
repoquery --show-duplicates protobuf

//指定版本安装,一般是安装教老的版本
yum install protobuf-3.6.1-4.el7

制作debian 仓库

适合ubuntu、deepin、uos等, 参考:https://lework.github.io/2021/04/03/debian-kubeadm-install/

1
2
#添加新仓库
sudo apt-add-repository 'deb http://ftp.us.debian.org/debian stretch main contrib non-free'

img

rpm转换成 dpkg

apt-mirror

先要安装apt-mirror 工具,安装后会生成配置文件 /etc/apt/mirror.list 然后需要手工修改配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#cat /etc/apt/mirror.list
############# config ##################

#下载下来的仓库文件放在哪里
set base_path /polarx/debian

set mirror_path $base_path/mirror
set skel_path $base_path/skel
set var_path $base_path/var
set cleanscript $var_path/clean.sh
set defaultarch amd64
#set postmirror_script $var_path/postmirror.sh
set run_postmirror 0
set nthreads 20
set _tilde 0
#
############# end config ##############

#从哪里镜像仓库
deb http://yum.tbsite.net/mirrors/debian/ buster main non-free contrib
#deb https://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial main

#deb http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-src http://ftp.us.debian.org/debian unstable main contrib non-free

# mirror additional architectures
#deb-alpha http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-amd64 http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-armel http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-hppa http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-i386 http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-ia64 http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-m68k http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-mips http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-mipsel http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-powerpc http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-s390 http://ftp.us.debian.org/debian unstable main contrib non-free
#deb-sparc http://ftp.us.debian.org/debian unstable main contrib non-free

#clean http://ftp.us.debian.org/debian
clean http://yum.tbsite.net/mirrors/debian/

debian仓库介绍

一个Debian仓库包含多个发行版。Debian 的发行版是以 “玩具总动员 “电影中的角色命名的 (wheezy, jessie, stretch, …)。 代号有别名,叫做套件(stable, oldstable, testing, unstable)。一个发行版会被分成几个组件。在 Debian 中,这些组件被命名为 main, contrib, 和 non-free,并表并表示它们所包含的软件的授权条款。一个版本也有各种架构(amd64, i386, mips, powerpc, s390x, …)的软件包,以及源码和架构独立的软件包。

仓库的根目录下有一个dists 目录,而这个目录又有每个发行版和套件的目录,后者通常是前者的符号链接,但浏览器不会向您显示出这个区别。每个发行版子目录都包含一个加密签名的Release文件和每个组件的目录,里面是不同架构的目录,名为binary-*<架构>*和sources。而在这些文件中,Packages是文本文件,包含了软件包。嗯,那么实际的软件包在哪里?

image-20220829163817671

软件包本身在仓库根目录下的pool。在pool下面又有所有组件的目录,其中有0,…,9ab,.., z, liba, … , libz。 而在这些目录中,是以它们所包含的软件包命名的目录,这些目录最后包含实际的软件包,即.deb文件。这个名字不一定是软件包本身的名字,例如,软件包bsdutils在目录pool/main/u/util-linux 下,它是生成软件包的源码的名称。一个上游源可能会生成多个二进制软件包,而所有这些软件包最终都会在pool下面的同一个子目录中。额外的单字母目录只是一个技巧,以避免在一个目录中有太多的条目,因为这是很多系统传统上存在性能问题的原因。

pool下面的子目录中,通常会有多个版本的软件包,而每个版本的软件包属于什么发行版的信息只存在于索引中。这样一来,同一个版本的包可软件以属于多个发行版,但只使用一次磁盘空间,而且不需要求助于硬链接或符号链接,所以镜像相当简单,甚至可以在没有这些概念的系统中进行。

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apt install kubeadm=1.20.12-00 //指定版本安装

#查询可用版本
apt-cache policy kubeadm
apt list --all-versions kubeadm

#清理
apt clean --dry-run
apt update
apt list/dpkg -l //列出本机所有已经安装的软件
apt show kubeadm //显示版本、作者等信息

#查询某个安装包的所有文件
dpkg-query -L kubeadm

#列出所有依赖包
apt-cache depends ansible

#被依赖查询
apt-cache rdepends kubelet

dpkg -I kubernetes/pool/kubeadm_1.21.0-00_amd64.deb

#下载依赖包
apt-get download $(apt-rdepends kubeadm|grep -v "^ ")
aptitude --download-only -y install $(apt-rdepends kubeadm|grep -v "^ ") //不能下载已经安装了的依赖包

apt download $(apt-cache depends --recurse --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances kubeadm | grep "^\w" | sort -u)

简单仓库

下载所有 deb 包以及他们的依赖

1
apt download $(apt-cache depends --recurse --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances kubeadm | grep "^\w" | sort -u)

到deb 包所在的目录下生成 index

1
dpkg-scanpackages -m . > Packages

/etc/apt/source.list 指向这个目录

1
2
3
deb [trusted=yes] file:/polarx/test /
or
deb [trusted=yes] http://kunpeng/pool/ ./

验证:

1
apt update //看是否能拉取你刚配置的仓库

Kubernetes 仓库

debian 上通过kubeadm 安装 kubernetes 集群

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//官方
echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | tee -a /etc/apt/sources.list.d/kubernetes.list

//阿里云仓库
echo 'deb https://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial main' > /etc/apt/sources.list.d/kubernetes.list
curl -s https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add -
apt-get update

export KUBE_VERSION="1.20.5"
apt-get install -y kubeadm=$KUBE_VERSION-00 kubelet=$KUBE_VERSION-00 kubectl=$KUBE_VERSION-00
sudo apt-mark hold kubelet kubeadm kubectl

[ -d /etc/bash_completion.d ] && \
{ kubectl completion bash > /etc/bash_completion.d/kubectl; \
kubeadm completion bash > /etc/bash_completion.d/kubadm; }

[ ! -d /usr/lib/systemd/system/kubelet.service.d ] && mkdir -p /usr/lib/systemd/system/kubelet.service.d
cat << EOF > /usr/lib/systemd/system/kubelet.service.d/11-cgroup.conf
[Service]
CPUAccounting=true
MemoryAccounting=true
BlockIOAccounting=true
ExecStartPre=/usr/bin/bash -c '/usr/bin/mkdir -p /sys/fs/cgroup/{cpuset,memory,systemd,pids,"cpu,cpuacct"}/{system,kube,kubepods}.slice'
Slice=kube.slice
EOF
systemctl daemon-reload

systemctl enable kubelet.service

docker 仓库

1
2
3
4
5
6
7
apt-get install -y apt-transport-https ca-certificates curl gnupg2 lsb-release bash-completion

curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/debian/gpg | sudo apt-key add -
echo "deb [arch=amd64] https://mirrors.aliyun.com/docker-ce/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker-ce.list
sudo apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io
apt-mark hold docker-ce docker-ce-cli containerd.io

锁定已安装版本

1
2
//锁定这三个软件的版本,避免意外升级导致版本错误:
sudo apt-mark hold kubeadm kubelet kubectl

参考资料

xargs 命令教程

kubernetes service 和 kube-proxy详解

service 模式

根据创建Service的type类型不同,可分成4种模式:

  • ClusterIP: 默认方式。根据是否生成ClusterIP又可分为普通Service和Headless Service两类:
    • 普通Service:通过为Kubernetes的Service分配一个集群内部可访问的固定虚拟IP(Cluster IP),实现集群内的访问。为最常见的方式。
    • Headless Service:该服务不会分配Cluster IP,也不通过kube-proxy做反向代理和负载均衡。而是通过DNS提供稳定的网络ID来访问,DNS会将headless service的后端直接解析为podIP列表。主要供StatefulSet中对应POD的序列用。
  • NodePort:除了使用Cluster IP之外,还通过将service的port映射到集群内每个节点的相同一个端口,实现通过nodeIP:nodePort从集群外访问服务。NodePort会RR转发给后端的任意一个POD,跟ClusterIP类似
  • LoadBalancer:和nodePort类似,不过除了使用一个Cluster IP和nodePort之外,还会向所使用的公有云申请一个负载均衡器,实现从集群外通过LB访问服务。在公有云提供的 Kubernetes 服务里,都使用了一个叫作 CloudProvider 的转接层,来跟公有云本身的 API 进行对接。所以,在上述 LoadBalancer 类型的 Service 被提交后,Kubernetes 就会调用 CloudProvider 在公有云上为你创建一个负载均衡服务,并且把被代理的 Pod 的 IP 地址配置给负载均衡服务做后端。
  • ExternalName:是 Service 的特例。此模式主要面向运行在集群外部的服务,通过它可以将外部服务映射进k8s集群,且具备k8s内服务的一些特征(如具备namespace等属性),来为集群内部提供服务。此模式要求kube-dns的版本为1.7或以上。这种模式和前三种模式(除headless service)最大的不同是重定向依赖的是dns层次,而不是通过kube-proxy。

service yaml案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
name: nginx-ren
labels:
app: web
spec:
type: NodePort
# clusterIP: None
ports:
- port: 8080
targetPort: 80
nodePort: 30080
selector:
app: ren

ports 字段指定服务的端口信息:

  • port:虚拟 ip 要绑定的 port,每个 service 会创建出来一个虚拟 ip,通过访问 vip:port 就能获取服务的内容。这个 port 可以用户随机选取,因为每个服务都有自己的 vip,也不用担心冲突的情况
  • targetPort:pod 中暴露出来的 port,这是运行的容器中具体暴露出来的端口,一定不能写错–一般用name来代替具体的port
  • protocol:提供服务的协议类型,可以是 TCP 或者 UDP
  • nodePort: 仅在type为nodePort模式下有用,宿主机暴露端口

但是nodePort和loadbalancer可以被外部访问,loadbalancer需要一个外部ip,流量走外部ip进出

NodePort向外部暴露了多个宿主机的端口,外部可以部署负载均衡将这些地址配置进去。

默认情况下,服务会rr转发到可用的后端。如果希望保持会话(同一个 client 永远都转发到相同的 pod),可以把 service.spec.sessionAffinity 设置为 ClientIP

1
2
3
4
5
6
7
8
9
10
iptables-save | grep 3306

iptables-save | grep KUBE-SERVICES

#iptables-save |grep KUBE-SVC-RVEVH2XMONK6VC5O
:KUBE-SVC-RVEVH2XMONK6VC5O - [0:0]
-A KUBE-SERVICES -d 10.10.70.95/32 -p tcp -m comment --comment "drds/mysql-read:mysql cluster IP" -m tcp --dport 3306 -j KUBE-SVC-RVEVH2XMONK6VC5O
-A KUBE-SVC-RVEVH2XMONK6VC5O -m comment --comment "drds/mysql-read:mysql" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-XC4TZYIZFYB653VI
-A KUBE-SVC-RVEVH2XMONK6VC5O -m comment --comment "drds/mysql-read:mysql" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-MK4XPBZUIJGFXKED
-A KUBE-SVC-RVEVH2XMONK6VC5O -m comment --comment "drds/mysql-read:mysql" -j KUBE-SEP-AAYXWGQJBDHUJUQ3

看起来 service 是个完美的方案,可以解决服务访问的所有问题,但是 service 这个方案(iptables 模式)也有自己的缺点。

首先,如果转发的 pod 不能正常提供服务,它不会自动尝试另一个 pod,当然这个可以通过 readiness probes 来解决。每个 pod 都有一个健康检查的机制,当有 pod 健康状况有问题时,kube-proxy 会删除对应的转发规则。

另外,nodePort 类型的服务也无法添加 TLS 或者更复杂的报文路由机制。因为只做了NAT

NodePort 的一些问题

  • 首先endpoint回复不能走node 1给client,因为会被client reset(如果在node1上将src ip替换成node2的ip可能会路由不通)。回复包在 node1上要snat给node2
  • 经过snat后endpoint没法拿到client ip(slb之类是通过option带过来)
1
2
3
4
5
6
7
8
9
          client
\ ^
\ \
v \
node 1 <--- node 2
| ^ SNAT
| | --->
v |
endpoint

可以将 Service 的 spec.externalTrafficPolicy 字段设置为 local,这就保证了所有 Pod 通过 Service 收到请求之后,一定可以看到真正的、外部 client 的源地址。

而这个机制的实现原理也非常简单:这时候,一台宿主机上的 iptables 规则,会设置为只将 IP 包转发给运行在这台宿主机上的 Pod。所以这时候,Pod 就可以直接使用源地址将回复包发出,不需要事先进行 SNAT 了。这个流程,如下所示:

1
2
3
4
5
6
7
8
9
      client
^ / \
/ / \
/ v X
node 1 node 2
^ |
| |
| v
endpoint

当然,这也就意味着如果在一台宿主机上,没有任何一个被代理的 Pod 存在,比如上图中的 node 2,那么你使用 node 2 的 IP 地址访问这个 Service,就是无效的。此时,你的请求会直接被 DROP 掉。

Service和kube-proxy的工作原理

kube-proxy有两种主要的实现(userspace基本没有使用了):

  • [[iptables使用]]来做NAT以及负载均衡
  • ipvs来做NAT以及负载均衡

Service 是由 kube-proxy 组件通过监听 Pod 的变化事件,在宿主机上维护iptables规则或者ipvs规则。

Kube-proxy 主要监听两个对象,一个是 Service,一个是 Endpoint,监听他们启停。以及通过selector将他们绑定。

IPVS 是专门为LB设计的。它用hash table管理service,对service的增删查找都是*O(1)*的时间复杂度。不过IPVS内核模块没有SNAT功能,因此借用了iptables的SNAT功能。IPVS 针对报文做DNAT后,将连接信息保存在nf_conntrack中,iptables据此接力做SNAT。该模式是目前Kubernetes网络性能最好的选择。但是由于nf_conntrack的复杂性,带来了很大的性能损耗。

iptables 实现负载均衡的工作流程

如果kube-proxy不是用的ipvs模式,那么主要靠iptables来做DNAT和SNAT以及负载均衡

iptables+clusterIP工作流程:

  1. 集群内访问svc 10.10.35.224:3306 命中 kube-services iptables
  2. iptables 规则:KUBE-SEP-F4QDAAVSZYZMFXZQ 对应到 KUBE-SEP-F4QDAAVSZYZMFXZQ
  3. KUBE-SEP-F4QDAAVSZYZMFXZQ 指示 DNAT到 宿主机:192.168.0.83:10379(在内核中将包改写了ip port)
  4. 从svc description中可以看到这个endpoint的地址 192.168.0.83:10379(pod使用Host network)

image.png

在对应的宿主机上可以清楚地看到容器中的mysqld进程正好监听着 10379端口

1
2
3
4
5
6
7
8
[root@az1-drds-83 ~]# ss -lntp |grep 10379
LISTEN 0 128 :::10379 :::* users:(("mysqld",pid=17707,fd=18))
[root@az1-drds-83 ~]# ps auxff | grep 17707 -B2
root 13606 0.0 0.0 10720 3764 ? Sl 17:09 0:00 \_ containerd-shim -namespace moby -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/moby/ead57b52b11902b9b5004db0b72abb060b56a1af7ee7ad7066bd09c946abcb97 -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd -runtime-root /var/run/docker/runtime-runc

root 13624 0.0 0.0 103044 10424 ? Ss 17:09 0:00 | \_ python /entrypoint.py
root 14835 0.0 0.0 11768 1636 ? S 17:10 0:00 | \_ /bin/sh /u01/xcluster/bin/mysqld_safe --defaults-file=/home/mysql/my10379.cnf
alidb 17707 0.6 0.0 1269128 67452 ? Sl 17:10 0:25 | \_ /u01/xcluster_20200303/bin/mysqld --defaults-file=/home/mysql/my10379.cnf --basedir=/u01/xcluster_20200303 --datadir=/home/mysql/data10379/dbs10379 --plugin-dir=/u01/xcluster_20200303/lib/plugin --user=mysql --log-error=/home/mysql/data10379/mysql/master-error.log --open-files-limit=8192 --pid-file=/home/mysql/data10379/dbs10379/az1-drds-83.pid --socket=/home/mysql/data10379/tmp/mysql.sock --port=10379

对应的这个pod的description:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#kubectl describe pod apsaradbcluster010-cv6w
Name: apsaradbcluster010-cv6w
Namespace: default
Priority: 0
Node: az1-drds-83/192.168.0.83
Start Time: Thu, 10 Sep 2020 17:09:33 +0800
Labels: alisql.clusterName=apsaradbcluster010
alisql.pod_name=apsaradbcluster010-cv6w
alisql.pod_role=leader
Annotations: apsara.metric.pod_name: apsaradbcluster010-cv6w
Status: Running
IP: 192.168.0.83
IPs:
IP: 192.168.0.83
Controlled By: ApsaradbCluster/apsaradbcluster010
Containers:
engine:
Container ID: docker://ead57b52b11902b9b5004db0b72abb060b56a1af7ee7ad7066bd09c946abcb97
Image: reg.docker.alibaba-inc.com/apsaradb/alisqlcluster-engine:develop-20200910140415
Image ID: docker://sha256:7ad5cc53c87b34806eefec829d70f5f0192f4127c7ee4e867cb3da3bb6c2d709
Ports: 10379/TCP, 20383/TCP, 46846/TCP
Host Ports: 10379/TCP, 20383/TCP, 46846/TCP
State: Running
Started: Thu, 10 Sep 2020 17:09:35 +0800
Ready: True
Restart Count: 0
Environment:
ALISQL_POD_NAME: apsaradbcluster010-cv6w (v1:metadata.name)
ALISQL_POD_PORT: 10379
Mounts:
/dev/shm from devshm (rw)
/etc/localtime from etclocaltime (rw)
/home/mysql/data from data-dir (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-n2bmn (ro)
exporter:
Container ID: docker://b49865b7798f9036b431203d54994ac8fdfcadacb01a2ab4494b13b2681c482d
Image: reg.docker.alibaba-inc.com/apsaradb/alisqlcluster-exporter:latest
Image ID: docker://sha256:432cdd0a0e7c74c6eb66551b6f6af9e4013f60fb07a871445755f6577b44da19
Port: 47272/TCP
Host Port: 47272/TCP
Args:
--web.listen-address=:47272
--collect.binlog_size
--collect.engine_innodb_status
--collect.info_schema.innodb_metrics
--collect.info_schema.processlist
--collect.info_schema.tables
--collect.info_schema.tablestats
--collect.slave_hosts
State: Running
Started: Thu, 10 Sep 2020 17:09:35 +0800
Ready: True
Restart Count: 0
Environment:
ALISQL_POD_NAME: apsaradbcluster010-cv6w (v1:metadata.name)
DATA_SOURCE_NAME: root:@(127.0.0.1:10379)/
Mounts:
/dev/shm from devshm (rw)
/etc/localtime from etclocaltime (rw)
/home/mysql/data from data-dir (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-n2bmn (ro)

DNAT 规则的作用,就是在 PREROUTING 检查点之前,也就是在路由之前,将流入 IP 包的目的地址和端口,改成–to-destination 所指定的新的目的地址和端口。可以看到,这个目的地址和端口,正是被代理 Pod 的 IP 地址和端口。

哪些组件会修改iptables

image.png

ipvs 实现负载均衡的原理

ipvs模式下,kube-proxy会先创建虚拟网卡,kube-ipvs0下面的每个ip都对应着svc的一个clusterIP:

1
2
3
4
5
6
# ip addr
...
5: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether de:29:17:2a:8d:79 brd ff:ff:ff:ff:ff:ff
inet 10.68.70.130/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever

kube-ipvs0下面绑的这些ip就是在发包的时候让内核知道如果目标ip是这些地址的话,这些地址是自身的所以包不会发出去,而是给INPUT链,这样ipvs内核模块有机会改写包做完NAT后再发走。

ipvs会放置DNAT钩子在INPUT链上,因此必须要让内核识别 VIP 是本机的 IP。这样才会过INPUT 链,要不然就通过OUTPUT链出去了。k8s 通过kube-proxy将service cluster ip 绑定到虚拟网卡kube-ipvs0。

同时在路由表中增加一些ipvs 的路由条目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ip route show table local
local 10.68.0.1 dev kube-ipvs0 proto kernel scope host src 10.68.0.1
local 10.68.0.2 dev kube-ipvs0 proto kernel scope host src 10.68.0.2
local 10.68.70.130 dev kube-ipvs0 proto kernel scope host src 10.68.70.130 -- ipvs
broadcast 127.0.0.0 dev lo proto kernel scope link src 127.0.0.1
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1
broadcast 172.17.0.0 dev docker0 proto kernel scope link src 172.17.0.1
local 172.17.0.1 dev docker0 proto kernel scope host src 172.17.0.1
broadcast 172.17.255.255 dev docker0 proto kernel scope link src 172.17.0.1
local 172.20.185.192 dev tunl0 proto kernel scope host src 172.20.185.192
broadcast 172.20.185.192 dev tunl0 proto kernel scope link src 172.20.185.192
broadcast 172.26.128.0 dev eth0 proto kernel scope link src 172.26.137.117
local 172.26.137.117 dev eth0 proto kernel scope host src 172.26.137.117
broadcast 172.26.143.255 dev eth0 proto kernel scope link src 172.26.137.117

而接下来,kube-proxy 就会通过 Linux 的 IPVS 模块,为这个 IP 地址设置三个 IPVS 虚拟主机,并设置这三个虚拟主机之间使用轮询模式 (rr) 来作为负载均衡策略。我们可以通过 ipvsadm 查看到这个设置,如下所示:

1
2
3
4
5
ipvsadm -ln |grep 10.68.114.131 -A5
TCP 10.68.114.131:3306 rr
-> 172.20.120.143:3306 Masq 1 0 0
-> 172.20.185.209:3306 Masq 1 0 0
-> 172.20.248.143:3306 Masq 1 0 0

172.20.. 是后端真正pod的ip, 10.68.114.131 是cluster-ip.

完整的工作流程如下:

  1. 因为service cluster ip 绑定到虚拟网卡kube-ipvs0上,内核可以识别访问的 VIP 是本机的 IP.
  2. 数据包到达INPUT链.
  3. ipvs监听到达input链的数据包,比对数据包请求的服务是为集群服务,修改数据包的目标IP地址为对应pod的IP,然后将数据包发至POSTROUTING链.
  4. 数据包经过POSTROUTING链选路由后,将数据包通过tunl0网卡(calico网络模型)发送出去。从tunl0虚拟网卡获得源IP.
  5. 经过tunl0后进行ipip封包,丢到物理网络,路由到目标node(目标pod所在的node)
  6. 目标node进行ipip解包后给pod对应的网卡
  7. pod接收到请求之后,构建响应报文,改变源地址和目的地址,返回给客户端。

image.png

ipvs实际案例

ipvs负载均衡下一次完整的syn握手抓包。

宿主机上访问 curl clusterip+port 后因为这个ip绑定在kube-ipvs0上,本来是应该发出去的包(prerouting)但是内核认为这个包是访问自己,于是给INPUT链,接着被ipvs放置在INPUT中的DNAT钩子勾住,将dest ip根据负载均衡逻辑改成pod-ip,然后将数据包再发至POSTROUTING链。这时因为目标ip是POD-IP了,根据ip route 选择到出口网卡是tunl0。

可以看下内核中的路由规则:

1
2
3
4
5
# ip route get 10.68.70.130
local 10.68.70.130 dev lo src 10.68.70.130 //这条规则指示了clusterIP是发给自己的
cache <local>
# ip route get 172.20.185.217
172.20.185.217 via 172.26.137.117 dev tunl0 src 172.20.22.192 //这条规则指示clusterIP替换成POD IP后发给本地tunl0做ipip封包

于是cip变成了tunl0的IP,这个tunl0是ipip模式,于是将这个包打包成ipip,也就是外层sip、dip都是宿主机ip,再将这个包丢入到物理网络

网络收包到达内核后的处理流程如下,核心都是查路由表,出包也会查路由表(判断是否本机内部通信,或者外部通信的话需要选用哪个网卡)

补两张内核netfilter框架的图:

packet filtering in IPTables

image.png

完整版

image.png

ipvs的一些分析

ipvs是一个内核态的四层负载均衡,支持NAT以及IPIP隧道模式,但LB和RS不能跨子网,IPIP性能次之,通过ipip隧道解决跨网段传输问题,因此能够支持跨子网。而NAT模式没有限制,这也是唯一一种支持端口映射的模式。

但是ipvs只有NAT(也就是DNAT),NAT也俗称三角模式,要求RS和LVS 在一个二层网络,并且LVS是RS的网关,这样回包一定会到网关,网关再次做SNAT,这样client看到SNAT后的src ip是LVS ip而不是RS-ip。默认实现不支持ful-NAT,所以像公有云厂商为了适应公有云场景基本都会定制实现ful-NAT模式的lvs。

我们不难猜想,由于Kubernetes Service需要使用端口映射功能,因此kube-proxy必然只能使用ipvs的NAT模式。

如下Masq表示MASQUERADE(也就是SNAT),跟iptables里面的 MASQUERADE 是一个意思

1
2
3
# ipvsadm -L -n  |grep 70.130 -A12
TCP 10.68.70.130:12380 rr
-> 172.20.185.217:9376 Masq 1 0 0

kuberletes对iptables的修改(图中黄色部分):

image.png

kube-proxy

在 Kubernetes v1.0 版本,代理完全在 userspace 实现。Kubernetes v1.1 版本新增了 iptables 代理模式,但并不是默认的运行模式。从 Kubernetes v1.2 起,默认使用 iptables 代理。在 Kubernetes v1.8.0-beta.0 中,添加了 ipvs 代理模式

kube-proxy相当于service的管控方,业务流量不会走到kube-proxy,业务流量的负载均衡都是由内核层面的iptables或者ipvs来分发。

kube-proxy的三种模式:

image.png

一直以来,基于 iptables 的 Service 实现,都是制约 Kubernetes 项目承载更多量级的 Pod 的主要障碍。

ipvs 就是用于解决在大量 Service 时,iptables 规则同步变得不可用的性能问题。与 iptables 比较像的是,ipvs 的实现虽然也基于 netfilter 的钩子函数,但是它却使用哈希表作为底层的数据结构并且工作在内核态,这也就是说 ipvs 在重定向流量和同步代理规则有着更好的性能。

除了能够提升性能之外,ipvs 也提供了多种类型的负载均衡算法,除了最常见的 Round-Robin 之外,还支持最小连接、目标哈希、最小延迟等算法,能够很好地提升负载均衡的效率。

而相比于 iptables,IPVS 在内核中的实现其实也是基于 Netfilter 的 NAT 模式,所以在转发这一层上,理论上 IPVS 并没有显著的性能提升。但是,IPVS 并不需要在宿主机上为每个 Pod 设置 iptables 规则,而是把对这些“规则”的处理放到了内核态,从而极大地降低了维护这些规则的代价,“将重要操作放入内核态”是提高性能的重要手段。

IPVS 模块只负责上述的负载均衡和代理功能。而一个完整的 Service 流程正常工作所需要的包过滤、SNAT 等操作,还是要靠 iptables 来实现。只不过,这些辅助性的 iptables 规则数量有限,也不会随着 Pod 数量的增加而增加。

ipvs 和 iptables 都是基于 Netfilter 实现的。

Kubernetes 中已经使用 ipvs 作为 kube-proxy 的默认代理模式。

1
/opt/kube/bin/kube-proxy --bind-address=172.26.137.117 --cluster-cidr=172.20.0.0/16 --hostname-override=172.26.137.117 --kubeconfig=/etc/kubernetes/kube-proxy.kubeconfig --logtostderr=true --proxy-mode=ipvs

image.png

为什么clusterIP不能ping通

集群内访问cluster ip(不能ping,只能cluster ip+port)就是在到达网卡之前被内核iptalbes做了dnat/snat, cluster IP是一个虚拟ip,可以针对具体的服务固定下来,这样服务后面的pod可以随便变化。

iptables模式的svc会ping不通clusterIP,可以看如下iptables和route(留意:–reject-with icmp-port-unreachable):

1
2
3
4
5
6
7
8
9
10
11
12
13
#ping 10.96.229.40
PING 10.96.229.40 (10.96.229.40) 56(84) bytes of data.
^C
--- 10.96.229.40 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 999ms


#iptables-save |grep 10.96.229.40
-A KUBE-SERVICES -d 10.96.229.40/32 -p tcp -m comment --comment "***-service:https has no endpoints" -m tcp --dport 8443 -j REJECT --reject-with icmp-port-unreachable

#ip route get 10.96.229.40
10.96.229.40 via 11.164.219.253 dev eth0 src 11.164.219.119
cache

准确来说如果用ipvs实现的clusterIP是可以ping通的:

  • 如果用iptables 来做转发是ping不通的,因为iptables里面这条规则只处理tcp包,reject了icmp
  • ipvs实现的clusterIP都能ping通
  • ipvs下的clusterIP ping通了也不是转发到pod,ipvs负载均衡只转发tcp协议的包
  • ipvs 的clusterIP在本地配置了route路由到回环网卡,这个包是lo网卡回复的

ipvs实现的clusterIP,在本地有添加路由到lo网卡

image.png

然后在本机抓包(到ipvs后端的pod上抓不到icmp包):

image.png

从上面可以看出显然ipvs只会转发tcp包到后端pod,所以icmp包不会通过ipvs转发到pod上,同时在本地回环网卡lo上抓到了进去的icmp包。因为本地添加了一条路由规则,目标clusterIP被指示发到lo网卡上,lo网卡回复了这个ping包,所以通了。

port-forward

port-forward后外部也能够像nodePort一样访问到,但是port-forward不适合大流量,一般用于管理端口,启动的时候port-forward会固定转发到一个具体的Pod上,也没有负载均衡的能力。

1
2
#在本机监听1080端口,并转发给后端的svc/nginx-ren(总是给发给svc中的一个pod)
kubectl port-forward --address 0.0.0.0 svc/nginx-ren 1080:80

kubectl looks up a Pod from the service information provided on the command line and forwards directly to a Pod rather than forwarding to the ClusterIP/Service port and allowing the cluster to load balance the service like regular service traffic.

The portforward.go Complete function is where kubectl portforward does the first look up for a pod from options via AttachablePodForObjectFn:

The AttachablePodForObjectFn is defined as attachablePodForObject in this interface, then here is the attachablePodForObject function.

To my (inexperienced) Go eyes, it appears the attachablePodForObject is the thing kubectl uses to look up a Pod to from a Service defined on the command line.

Then from there on everything deals with filling in the Pod specific PortForwardOptions (which doesn’t include a service) and is passed to the kubernetes API.

Service 和 DNS 的关系

Service 和 Pod 都会被分配对应的 DNS A 记录(从域名解析 IP 的记录)。

对于 ClusterIP 模式的 Service 来说(比如我们上面的例子),它的 A 记录的格式是:..svc.cluster.local。当你访问这条 A 记录的时候,它解析到的就是该 Service 的 VIP 地址。

而对于指定了 clusterIP=None 的 Headless Service 来说,它的 A 记录的格式也是:..svc.cluster.local。但是,当你访问这条 A 记录的时候,它返回的是所有被代理的 Pod 的 IP 地址的集合。当然,如果你的客户端没办法解析这个集合的话,它可能会只会拿到第一个 Pod 的 IP 地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#kubectl get pod -l app=mysql-r -o wide
NAME READY STATUS RESTARTS IP NODE
mysql-r-0 2/2 Running 0 172.20.120.143 172.26.137.118
mysql-r-1 2/2 Running 4 172.20.248.143 172.26.137.116
mysql-r-2 2/2 Running 0 172.20.185.209 172.26.137.117

/ # nslookup mysql-r-1.mysql-r
Server: 10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name: mysql-r-1.mysql-r
Address 1: 172.20.248.143 mysql-r-1.mysql-r.default.svc.cluster.local
/ #
/ # nslookup mysql-r-2.mysql-r
Server: 10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name: mysql-r-2.mysql-r
Address 1: 172.20.185.209 mysql-r-2.mysql-r.default.svc.cluster.local

#如果service是headless(也就是明确指定了 clusterIP: None)
/ # nslookup mysql-r
Server: 10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name: mysql-r
Address 1: 172.20.185.209 mysql-r-2.mysql-r.default.svc.cluster.local
Address 2: 172.20.248.143 mysql-r-1.mysql-r.default.svc.cluster.local
Address 3: 172.20.120.143 mysql-r-0.mysql-r.default.svc.cluster.local

#如果service 没有指定 clusterIP: None,也就是会分配一个clusterIP给集群
/ # nslookup mysql-r
Server: 10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name: mysql-r
Address 1: 10.68.90.172 mysql-r.default.svc.cluster.local

不是每个pod都会向DNS注册,只有:

  • StatefulSet中的POD会向dns注册,因为他们要保证顺序行
  • POD显式指定了hostname和subdomain,说明要靠hostname/subdomain来解析
  • Headless Service代理的POD也会注册

Ingress

kube-proxy 只能路由 Kubernetes 集群内部的流量,而我们知道 Kubernetes 集群的 Pod 位于 CNI 创建的外网络中,集群外部是无法直接与其通信的,因此 Kubernetes 中创建了 ingress 这个资源对象,它由位于 Kubernetes 边缘节点(这样的节点可以是很多个也可以是一组)的 Ingress controller 驱动,负责管理南北向流量,Ingress 必须对接各种 Ingress Controller 才能使用,比如 nginx ingress controllertraefik。Ingress 只适用于 HTTP 流量,使用方式也很简单,只能对 service、port、HTTP 路径等有限字段匹配来路由流量,这导致它无法路由如 MySQL、Redis 和各种私有 RPC 等 TCP 流量。要想直接路由南北向的流量,只能使用 Service 的 LoadBalancer 或 NodePort,前者需要云厂商支持,后者需要进行额外的端口管理。有些 Ingress controller 支持暴露 TCP 和 UDP 服务,但是只能使用 Service 来暴露,Ingress 本身是不支持的,例如 nginx ingress controller,服务暴露的端口是通过创建 ConfigMap 的方式来配置的。

Ingress是授权入站连接到达集群服务的规则集合。 你可以给Ingress配置提供外部可访问的URL、负载均衡、SSL、基于名称的虚拟主机等。 用户通过POST Ingress资源到API server的方式来请求ingress。

1
2
3
4
5
 internet
|
[ Ingress ]
--|-----|--
[ Services ]

可以将 Ingress 配置为服务提供外部可访问的 URL、负载均衡流量、终止 SSL/TLS,以及提供基于名称的虚拟主机等能力。 Ingress 控制器 通常负责通过负载均衡器来实现 Ingress,尽管它也可以配置边缘路由器或其他前端来帮助处理流量。

Ingress 不会公开任意端口或协议。 将 HTTP 和 HTTPS 以外的服务公开到 Internet 时,通常使用 Service.Type=NodePortService.Type=LoadBalancer 类型的服务。

Ingress 其实不是Service的一个类型,但是它可以作用于多个Service,作为集群内部服务的入口。Ingress 能做许多不同的事,比如根据不同的路由,将请求转发到不同的Service上等等。

image.png

Ingress 对象,其实就是 Kubernetes 项目对“反向代理”的一种抽象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: cafe-ingress
spec:
tls:
- hosts:
- cafe.example.com
secretName: cafe-secret
rules:
- host: cafe.example.com
http:
paths:
- path: /tea --入口url路径
backend:
serviceName: tea-svc --对应的service
servicePort: 80
- path: /coffee
backend:
serviceName: coffee-svc
servicePort: 80

在实际的使用中,你只需要从社区里选择一个具体的 Ingress Controller,把它部署在 Kubernetes 集群里即可。然后,这个 Ingress Controller 会根据你定义的 Ingress 对象,提供对应的代理能力。

目前,业界常用的各种反向代理项目,比如 Nginx、HAProxy、Envoy、Traefik 等,都已经为 Kubernetes 专门维护了对应的 Ingress Controller。

一个 Ingress Controller 可以根据 Ingress 对象和被代理后端 Service 的变化,来自动进行更新的 Nginx 负载均衡器。

对service未来的一些探索

eBPF(extended Berkeley Packet Filter)和网络

eBPF 最早出现在 3.18 内核中,此后原来的 BPF 就被称为 “经典” BPF(classic BPF, cBPF),cBPF 现在基本已经废弃了。很多人知道 cBPF 是因为它是 tcpdump 的包过滤语言。现在,Linux 内核只运行 eBPF,内核会将加载的 cBPF 字节码 透明地转换成 eBPF 再执行。如无特殊说明,本文中所说的 BPF 都是泛指 BPF 技术。

2015年eBPF 添加了一个新 fast path:XDP,XDP 是 eXpress DataPath 的缩写,支持在网卡驱动中运行 eBPF 代码,而无需将包送 到复杂的协议栈进行处理,因此处理代价很小,速度极快。

BPF 当时用于 tcpdump,在内核中尽量前面的位置抓包,它不会 crash 内核;

bcc 是 tracing frontend for eBPF。

内核添加了一个新 socket 类型 AF_XDP。它提供的能力是:在零拷贝( zero-copy)的前提下将包从网卡驱动送到用户空间。

AF_XDP 提供的能力与 DPDK 有点类似,不过:

  • DPDK 需要重写网卡驱动,需要额外维护用户空间的驱动代码。
  • AF_XDP 在复用内核网卡驱动的情况下,能达到与 DPDK 一样的性能。

而且由于复用了内核基础设施,所有的网络管理工具还都是可以用的,因此非常方便, 而 DPDK 这种 bypass 内核的方案导致绝大大部分现有工具都用不了了。

由于所有这些操作都是发生在 XDP 层的,因此它称为 AF_XDP。插入到这里的 BPF 代码 能直接将包送到 socket。

Facebook 公布了生产环境 XDP+eBPF 使用案例(DDoS & LB)

  • 用 XDP/eBPF 重写了原来基于 IPVS 的 L4LB,性能 10x。
  • eBPF 经受住了严苛的考验:从 2017 开始,每个进入 facebook.com 的包,都是经过了 XDP & eBPF 处理的。

Cilium 1.6 发布 第一次支持完全干掉基于 iptables 的 kube-proxy,全部功能基于 eBPF。Cilium 1.8 支持基于 XDP 的 Service 负载均衡和 host network policies。

传统的 kube-proxy 处理 Kubernetes Service 时,包在内核中的 转发路径是怎样的?如下图所示:

image.png

步骤:

  1. 网卡收到一个包(通过 DMA 放到 ring-buffer)。
  2. 包经过 XDP hook 点。
  3. 内核给包分配内存,此时才有了大家熟悉的 skb(包的内核结构体表示),然后 送到内核协议栈。
  4. 包经过 GRO 处理,对分片包进行重组。
  5. 包进入 tc(traffic control)的 ingress hook。接下来,所有橙色的框都是 Netfilter 处理点。
  6. Netfilter:在 PREROUTING hook 点处理 raw table 里的 iptables 规则。
  7. 包经过内核的连接跟踪(conntrack)模块。
  8. Netfilter:在 PREROUTING hook 点处理 mangle table 的 iptables 规则。
  9. Netfilter:在 PREROUTING hook 点处理 nat table 的 iptables 规则。
  10. 进行路由判断(FIB:Forwarding Information Base,路由条目的内核表示,译者注) 。接下来又是四个 Netfilter 处理点。
  11. Netfilter:在 FORWARD hook 点处理 mangle table 里的iptables 规则。
  12. Netfilter:在 FORWARD hook 点处理 filter table 里的iptables 规则。
  13. Netfilter:在 POSTROUTING hook 点处理 mangle table 里的iptables 规则。
  14. Netfilter:在 POSTROUTING hook 点处理 nat table 里的iptables 规则。
  15. 包到达 TC egress hook 点,会进行出方向(egress)的判断,例如判断这个包是到本 地设备,还是到主机外。
  16. 对大包进行分片。根据 step 15 判断的结果,这个包接下来可能会:发送到一个本机 veth 设备,或者一个本机 service endpoint, 或者,如果目的 IP 是主机外,就通过网卡发出去。

Cilium 如何处理POD之间的流量(东西向流量)

image.png

如上图所示,Socket 层的 BPF 程序主要处理 Cilium 节点的东西向流量(E-W)。

  • 将 Service 的 IP:Port 映射到具体的 backend pods,并做负载均衡。
  • 当应用发起 connect、sendmsg、recvmsg 等请求(系统调用)时,拦截这些请求, 并根据请求的IP:Port 映射到后端 pod,直接发送过去。反向进行相反的变换。

这里实现的好处:性能更高。

  • 不需要包级别(packet leve)的地址转换(NAT)。在系统调用时,还没有创建包,因此性能更高。
  • 省去了 kube-proxy 路径中的很多中间节点(intermediate node hops) 可以看出,应用对这种拦截和重定向是无感知的(符合 Kubernetes Service 的设计)。

Cilium处理外部流量(南北向流量)

image.png

集群外来的流量到达 node 时,由 XDP 和 tc 层的 BPF 程序进行处理, 它们做的事情与 socket 层的差不多,将 Service 的 IP:Port 映射到后端的 PodIP:Port,如果 backend pod 不在本 node,就通过网络再发出去。发出去的流程我们 在前面 Cilium eBPF 包转发路径 讲过了。

这里 BPF 做的事情:执行 DNAT。这个功能可以在 XDP 层做,也可以在 TC 层做,但 在XDP 层代价更小,性能也更高。

总结起来,Cilium的核心理念就是:

  • 将东西向流量放在离 socket 层尽量近的地方做。
  • 将南北向流量放在离驱动(XDP 和 tc)层尽量近的地方做。

性能比较

测试环境:两台物理节点,一个发包,一个收包,收到的包做 Service loadbalancing 转发给后端 Pods。

image.png

可以看出:

  • Cilium XDP eBPF 模式能处理接收到的全部 10Mpps(packets per second)。
  • Cilium tc eBPF 模式能处理 3.5Mpps。
  • kube-proxy iptables 只能处理 2.3Mpps,因为它的 hook 点在收发包路径上更后面的位置。
  • kube-proxy ipvs 模式这里表现更差,它相比 iptables 的优势要在 backend 数量很多的时候才能体现出来。

cpu:

  • XDP 性能最好,是因为 XDP BPF 在驱动层执行,不需要将包 push 到内核协议栈。
  • kube-proxy 不管是 iptables 还是 ipvs 模式,都在处理软中断(softirq)上消耗了大量 CPU。

标签和选择算符

标签(Labels) 是附加到 Kubernetes 对象(比如 Pods)上的键值对。 标签旨在用于指定对用户有意义且相关的对象的标识属性,但不直接对核心系统有语义含义。 标签可以用于组织和选择对象的子集。标签可以在创建时附加到对象,随后可以随时添加和修改。 每个对象都可以定义一组键/值标签。每个键对于给定对象必须是唯一的。

标签选择符

selector要和template中的labels一致

1
2
3
4
5
6
7
8
9
10
11
spec:
serviceName: "nginx-test"
replicas: 2
selector:
matchLabels:
app: ren
template:
metadata:
labels:
app: web

selector就是要找别人的label和自己匹配的,label是给别人来寻找的。如下case,svc中的 Selector: app=ren 是表示这个svc要绑定到app=ren的deployment/statefulset上.

被 selector 选中的 Pod,就称为 Service 的 Endpoints

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@poc117 mysql-cluster]# kubectl describe svc nginx-ren 
Name: nginx-ren
Namespace: default
Labels: app=web
Annotations: <none>
Selector: app=ren
Type: NodePort
IP: 10.68.34.173
Port: <unset> 8080/TCP
TargetPort: 80/TCP
NodePort: <unset> 30080/TCP
Endpoints: 172.20.22.226:80,172.20.56.169:80
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
[root@poc117 mysql-cluster]# kubectl get svc -l app=web
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-ren NodePort 10.68.34.173 <none> 8080:30080/TCP 13m
[root@poc117 mysql-cluster]# kubectl get svc -l app=ren
No resources found in default namespace.
[root@poc117 mysql-cluster]# kubectl get svc -l app=web
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-ren NodePort 10.68.34.173 <none> 8080:30080/TCP 14m

service mesh

  • Kubernetes 的本质是应用的生命周期管理,具体来说就是部署和管理(扩缩容、自动恢复、发布)。
  • Kubernetes 为微服务提供了可扩展、高弹性的部署和管理平台。
  • Service Mesh 的基础是透明代理,通过 sidecar proxy 拦截到微服务间流量后再通过控制平面配置管理微服务的行为。
  • Service Mesh 将流量管理从 Kubernetes 中解耦,Service Mesh 内部的流量无需 kube-proxy 组件的支持,通过为更接近微服务应用层的抽象,管理服务间的流量、安全性和可观察性。
  • xDS 定义了 Service Mesh 配置的协议标准。
  • Service Mesh 是对 Kubernetes 中的 service 更上层的抽象,它的下一步是 serverless。

Sidecar 注入及流量劫持步骤概述

下面是从 Sidecar 注入、Pod 启动到 Sidecar proxy 拦截流量及 Envoy 处理路由的步骤概览。

1. Kubernetes 通过 Admission Controller 自动注入,或者用户使用 istioctl 命令手动注入 sidecar 容器。

2. 应用 YAML 配置部署应用,此时 Kubernetes API server 接收到的服务创建配置文件中已经包含了 Init 容器及 sidecar proxy。

3. 在 sidecar proxy 容器和应用容器启动之前,首先运行 Init 容器,Init 容器用于设置 iptables(Istio 中默认的流量拦截方式,还可以使用 BPF、IPVS 等方式) 将进入 pod 的流量劫持到 Envoy sidecar proxy。所有 TCP 流量(Envoy 目前只支持 TCP 流量)将被 sidecar 劫持,其他协议的流量将按原来的目的地请求。

4. 启动 Pod 中的 Envoy sidecar proxy 和应用程序容器。这一步的过程请参考通过管理接口获取完整配置

5. 不论是进入还是从 Pod 发出的 TCP 请求都会被 iptables 劫持,inbound 流量被劫持后经 Inbound Handler 处理后转交给应用程序容器处理,outbound 流量被 iptables 劫持后转交给 Outbound Handler 处理,并确定转发的 upstream 和 Endpoint。

6. Sidecar proxy 请求 Pilot 使用 xDS 协议同步 Envoy 配置,其中包括 LDS、EDS、CDS 等,不过为了保证更新的顺序,Envoy 会直接使用 ADS 向 Pilot 请求配置更新

参考资料

https://imroc.io/posts/kubernetes/troubleshooting-with-kubernetes-network/ Kubernetes 网络疑难杂症排查方法

https://blog.csdn.net/qq_36183935/article/details/90734936 kube-proxy ipvs模式详解

http://arthurchiao.art/blog/ebpf-and-k8s-zh/ 大规模微服务利器:eBPF 与 Kubernetes

http://arthurchiao.art/blog/cilium-life-of-a-packet-pod-to-service-zh/ Life of a Packet in Cilium:实地探索 Pod-to-Service 转发路径及 BPF 处理逻辑

http://arthurchiao.art/blog/understanding-ebpf-datapath-in-cilium-zh/ 深入理解 Cilium 的 eBPF 收发包路径(datapath)(KubeCon, 2019)

https://jiayu0x.com/2014/12/02/iptables-essential-summary/

kubernetes 多集群管理

kubectl 管理多集群

指定config配置文件的方式访问不同的集群

1
kubectl --kubeconfig=/etc/kubernetes/admin.conf get nodes

一个kubectl可以管理多个集群,主要是 ~/.kube/config 里面的配置,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
clusters:
- cluster:
certificate-authority: /root/k8s-cluster.ca
server: https://192.168.0.80:6443
name: context-az1
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCQl0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
server: https://192.168.0.97:6443
name: context-az3

- context:
cluster: context-az1
namespace: default
user: az1-admin
name: az1
- context:
cluster: context-az3
namespace: default
user: az3-read
name: az3
current-context: az3 //当前使用的集群

kind: Config
preferences: {}
users:
- name: az1-admin
user:
client-certificate: /root/k8s.crt //key放在配置文件中
client-key: /root/k8s.key
- name: az3-read
user:
client-certificate-data: LS0tLS1CRUQ0FURS0tLS0tCg==
client-key-data: LS0tLS1CRUdJThuL2VPM0YxSWpEcXBQdmRNbUdiU2c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=

多个集群中切换的话 : kubectl config use-context az3

快速合并两个cluster

简单来讲就是把两个集群的 .kube/config 文件合并,注意context、cluster name别重复了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 必须提前保证两个config文件中的cluster、context名字不能重复
export KUBECONFIG=~/.kube/config:~/someotherconfig
kubectl config view --flatten

#激活这个上下文
kubectl config use-context az1

#查看所有context
kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
az1 context-az1 az1-admin default
* az2 kubernetes kubernetes-admin
az3 context-az3 az3-read default

背后的原理类似于这个流程:

1
2
3
4
5
6
7
8
9
# 添加集群 集群地址上一步有获取 ,需要指定ca文件,上一步有获取 
kubectl config set-cluster cluster-az1 --server https://192.168.146.150:6444 --certificate-authority=/usr/program/k8s-certs/k8s-cluster.ca

# 添加用户 需要指定crt,key文件,上一步有获取
kubectl config set-credentials az1-admin --client-certificate=/usr/program/k8s-certs/k8s.crt --client-key=/usr/program/k8s-certs/k8s.key

# 指定一个上下文的名字,我这里叫做 az1,随便你叫啥 关联刚才的用户
kubectl config set-context az1 --cluster=context-az1 --namespace=default --user=az1-admin

参考资料

http://coreos.com/blog/kubectl-tips-and-tricks

https://stackoverflow.com/questions/46184125/how-to-merge-kubectl-config-file-with-kube-config

Linux 内存问题汇总

本系列有如下几篇

[Linux 内存问题汇总](/2020/01/15/Linux 内存问题汇总/)

Linux内存–PageCache

Linux内存–管理和碎片

Linux内存–HugePage

Linux内存–零拷贝

内存使用观察

# free -m
         total       used       free     shared    buffers     cached
Mem:          7515       1115       6400          0        189        492
-/+ buffers/cache:        432       7082
Swap:            0          0          0

其中,cached 列表示当前的页缓存(Page Cache)占用量,buffers 列表示当前的块缓存(buffer cache)占用量。用一句话来解释:**Page Cache 用于缓存文件的页数据,buffer cache 用于缓存块设备(如磁盘)的块数据。**页是逻辑上的概念,因此 Page Cache 是与文件系统同级的;块是物理上的概念,因此 buffer cache 是与块设备驱动程序同级的。

image.png

上图中-/+ buffers/cache: -是指userd去掉buffers/cached后真正使用掉的内存; +是指free加上buffers和cached后真正free的内存大小。

free

free是从 /proc/meminfo 读取数据然后展示:

buff/cache = Buffers + Cached + SReclaimable

Buffers + Cached + SwapCached = Active(file) + Inactive(file) + Shmem + SwapCached

1
2
3
4
5
6
7
8
9
10
11
[root@az1-drds-79 ~]# cat /proc/meminfo |egrep -i "buff|cach|SReclai"
Buffers: 817764 kB
Cached: 76629252 kB
SwapCached: 0 kB
SReclaimable: 7202264 kB
[root@az1-drds-79 ~]# free -k
total used free shared buffers cached
Mem: 97267672 95522336 1745336 0 817764 76629352
-/+ buffers/cache: 18075220 79192452
Swap: 0 0 0

在内核启动时,物理页面将加入到伙伴系统 (Buddy System)中,用户申请内存时分配,释放时回收。为了照顾慢速设备及兼顾多种 workload,Linux 将页面类型分为匿名页(Anon Page)和文件页 (Page Cache),及 swapness,使用 Page Cache 缓存文件 (慢速设备),通过 swap cache 和 swapness 交由用户根据负载特征决定内存不足时回收二者的比例。

cached过高回收

系统内存大体可分为三块,应用程序使用内存、系统Cache 使用内存(包括page cache、buffer,内核slab 等)和Free 内存。

  • 应用程序使用内存:应用使用都是虚拟内存,应用申请内存时只是分配了地址空间,并未真正分配出物理内存,等到应用真正访问内存时会触发内核的缺页中断,这时候才真正的分配出物理内存,映射到用户的地址空间,因此应用使用内存是不需要连续的,内核有机制将非连续的物理映射到连续的进程地址空间中(mmu),缺页中断申请的物理内存,内核优先给低阶碎内存。

  • 系统Cache 使用内存:使用的也是虚拟内存,申请机制与应用程序相同。

  • Free 内存,未被使用的物理内存,这部分内存以4k 页的形式被管理在内核伙伴算法结构中,相邻的2^n 个物理页会被伙伴算法组织到一起,形成一块连续物理内存,所谓的阶内存就是这里的n (0<= n <=10),高阶内存指的就是一块连续的物理内存,在OSS 的场景中,如果3阶内存个数比较小的情况下,如果系统有吞吐burst 就会触发Drop cache 情况。

cache回收:
echo 1/2/3 >/proc/sys/vm/drop_caches

查看回收后:

cat /proc/meminfo

手动回收系统Cache、Buffer,这个文件可以设置的值分别为1、2、3。它们所表示的含义为:

echo 1 > /proc/sys/vm/drop_caches:表示清除pagecache。

echo 2 > /proc/sys/vm/drop_caches:表示清除回收slab分配器中的对象(包括目录项缓存和inode缓存)。slab分配器是内核中管理内存的一种机制,其中很多缓存数据实现都是用的pagecache。

echo 3 > /proc/sys/vm/drop_caches:表示清除pagecache和slab分配器中的缓存对象。

cached无法回收

可能是正打开的文件占用了cached,比如 vim 打开了一个巨大的文件;比如 mount的 tmpfs; 比如 journald 日志等等

通过vmtouch 查看

# vmtouch -v test.x86_64.rpm 
test.x86_64.rpm
[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 10988/10988

           Files: 1
     Directories: 0
  Resident Pages: 10988/10988  42M/42M  100%
         Elapsed: 0.000594 seconds

# ls -lh test.x86_64.rpm
-rw-r--r-- 1 root root 43M 10月  8 14:11 test.x86_64.rpm

如上,表示整个文件 test.x86_64.rpm 都被cached了,回收的话执行:

vmtouch -e test.x86_64.rpm // 或者: echo 3 >/proc/sys/vm/drop_cached

遍历某个目录下的所有文件被cached了多少

# vmtouch -vt /var/log/journal/
/var/log/journal/20190829214900434421844640356160/user-1000@ad408d9cb9d94f9f93f2c2396c26b542-000000000011ba49-00059979e0926f43.journal
[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 4096/4096
/var/log/journal/20190829214900434421844640356160/system@782ec314565e436b900454c59655247c-0000000000152f41-00059b2c88eb4344.journal
[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 14336/14336
/var/log/journal/20190829214900434421844640356160/user-1000@ad408d9cb9d94f9f93f2c2396c26b542-00000000000f2181-000598335fcd492f.journal
[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 4096/4096
/var/log/journal/20190829214900434421844640356160/system@782ec314565e436b900454c59655247c-0000000000129aea-000599e83996db80.journal
[OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO] 14336/14336
/var/log/journal/20190829214900434421844640356160/user-1000@ad408d9cb9d94f9f93f2c2396c26b542-000000000009f171-000595a722ead670.journal
…………
           Files: 48
 Directories: 2
 Touched Pages: 468992 (1G)
 Elapsed: 13.274 seconds

vmtouch 清理目录

如下脚本传入一个指定目录(业务方来确认哪些目录占用 pagecache 较大, 且可以清理),然后用vmtouch 遍历排序最大的几个清理掉,可能会造成业务的卡度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash
#
#echo "*/2 * * * * root bash /root/cron/os_pagecache_clean.sh -n 5 -e > /root/cron/os_pagecache_clean.out 2>&1" > /etc/cron.d/os_pagecache_clean

function usage(){
cat << EOF
usage:
$0 -n topN [-l|-e]
option:
-l list top n redis_dir
-e list and evict top n redis_dir
-n top n
EOF
exit 1
}

while getopts "n:leh" opt; do
case $opt in
l) list=1 ;;
e) list=1 && evict=1 ;;
n) n=${OPTARG} ;;
h) usage ;;
esac
done

[[ -z $n ]] && usage
[[ -z $list && -z $evict ]] && usage

# list must = 1
cd /root && ls | while read dirname ; do
page=$(vmtouch $dirname | grep "Resident Pages")
echo -e "$dirname\t$page"
done | tr "/" " " | sort -nr -k4 | head -n $n | awk '{print $1,$6}' | while read dirname cache_size; do
echo -e "$dirname\t$cache_size"
[[ $evict == 1 ]] && vmtouch -e $dirname
done

消失的内存

OS刚启动后就报内存不够了,什么都没跑就500G没了,cached和buffer基本没用,纯粹就是used占用高,top按内存排序没有超过0.5%的进程

参考: https://cloud.tencent.com/developer/article/1087455

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
[aliyun@uos15 18:40 /u02/backup_15/leo/benchmark/run]
$free -g
total used free shared buff/cache available
Mem: 503 501 1 0 0 1
Swap: 15 12 3

$cat /proc/meminfo
MemTotal: 528031512 kB
MemFree: 1469632 kB
MemAvailable: 0 kB
VmallocTotal: 135290290112 kB
VmallocUsed: 0 kB
VmallocChunk: 0 kB
Percpu: 81920 kB
AnonHugePages: 950272 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
HugePages_Total: 252557 ----- 预分配太多,一个2M,加起来刚好500G了
HugePages_Free: 252557
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 517236736 kB

以下是一台正常的机器对比:
Percpu: 41856 kB
AnonHugePages: 11442176 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
HugePages_Total: 0 ----没有做预分配
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB

[aliyun@uos16 18:43 /home/aliyun]
$free -g
total used free shared buff/cache available
Mem: 503 20 481 0 1 480
Swap: 15 0 15

对有问题的机器执行:
# echo 1024 > /proc/sys/vm/nr_hugepages
可以看到内存恢复正常了
root@uos15:/u02/backup_15/leo/benchmark/run# free -g
total used free shared buff/cache available
Mem: 503 10 492 0 0 490
Swap: 15 12 3
root@uos15:/u02/backup_15/leo/benchmark/run# cat /proc/meminfo
MemTotal: 528031512 kB
MemFree: 516106832 kB
MemAvailable: 514454408 kB
VmallocTotal: 135290290112 kB
VmallocUsed: 0 kB
VmallocChunk: 0 kB
Percpu: 81920 kB
AnonHugePages: 313344 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
HugePages_Total: 1024
HugePages_Free: 1024
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 2097152 kB

定制内存

物理内存700多G,要求OS只能用512G:

1
2
3
4
5
6
7
8
9
10
11
12
24条32G的内存条,总内存768G
# dmidecode -t memory |grep "Size: 32 GB"
Size: 32 GB
…………
Size: 32 GB
Size: 32 GB
root@uos15:/etc# dmidecode -t memory |grep "Size: 32 GB" | wc -l
24

# cat /boot/grub/grub.cfg |grep 512
linux /vmlinuz-4.19.0-arm64-server root=UUID=dbc68010-8c36-40bf-b794-271e59ff5727 ro splash quiet console=tty video=VGA-1:1280x1024@60 mem=512G DEEPIN_GFXMODE=$DEEPIN_GFXMODE
linux /vmlinuz-4.19.0-arm64-server root=UUID=dbc68010-8c36-40bf-b794-271e59ff5727 ro splash quiet console=tty video=VGA-1:1280x1024@60 mem=512G DEEPIN_GFXMODE=$DEEPIN_GFXMODE

参考资料

https://www.atatech.org/articles/66885

https://cloud.tencent.com/developer/article/1087455

https://www.cnblogs.com/xiaolincoding/p/13719610.html

kubernetes volume and storage

通常部署应用需要一些永久存储,kubernetes提供了PersistentVolume (PV,实际存储)、PersistentVolumeClaim (PVC,Pod访问PV的接口)、StorageClass来支持。

它为 PersistentVolume 定义了 StorageClass 名称manual,StorageClass 名称用来将 PersistentVolumeClaim 请求绑定到该 PersistentVolume。

PVC是用来描述希望使用什么样的或者说是满足什么条件的存储,它的全称是Persistent Volume Claim,也就是持久化存储声明。开发人员使用这个来描述该容器需要一个什么存储。

PVC就相当于是容器和PV之间的一个接口,使用人员只需要和PVC打交道即可。另外你可能也会想到如果当前环境中没有合适的PV和我的PVC绑定,那么我创建的POD不就失败了么?的确是这样的,不过如果发现这个问题,那么就赶快创建一个合适的PV,那么这时候持久化存储循环控制器会不断的检查PVC和PV,当发现有合适的可以绑定之后它会自动给你绑定上然后被挂起的POD就会自动启动,而不需要你重建POD。

创建 PersistentVolumeClaim 之后,Kubernetes 控制平面将查找满足申领要求的 PersistentVolume。 如果控制平面找到具有相同 StorageClass 的适当的 PersistentVolume,则将 PersistentVolumeClaim 绑定到该 PersistentVolume 上。PVC的大小可以小于PV的大小

一旦 PV 和 PVC 绑定后,PersistentVolumeClaim 绑定是排他性的,不管它们是如何绑定的。 PVC 跟 PV 绑定是一对一的映射。

注意:PV必须先于POD创建,而且只能是网络存储不能属于任何Node,虽然它支持HostPath类型但由于你不知道POD会被调度到哪个Node上,所以你要定义HostPath类型的PV就要保证所有节点都要有HostPath中指定的路径。

PV 和PVC的关系

PVC就会和PV进行绑定,绑定的一些原则:

  1. PV和PVC中的spec关键字段要匹配,比如存储(storage)大小。
  2. PV和PVC中的storageClassName字段必须一致,这个后面再说。
  3. 上面的labels中的标签只是增加一些描述,对于PVC和PV的绑定没有关系

PV的accessModes:支持三种类型

  • ReadWriteMany 多路读写,卷能被集群多个节点挂载并读写
  • ReadWriteOnce 单路读写,卷只能被单一集群节点挂载读写
  • ReadOnlyMany 多路只读,卷能被多个集群节点挂载且只能读

PV状态:

  • Available – 资源尚未被claim使用
  • Bound – 卷已经被绑定到claim了
  • Released – claim被删除,卷处于释放状态,但未被集群回收。
  • Failed – 卷自动回收失败

PV回收Recycling—pv可以设置三种回收策略:保留(Retain),回收(Recycle)和删除(Delete)。

  • 保留(Retain): 当删除与之绑定的PVC时候,这个PV被标记为released(PVC与PV解绑但还没有执行回收策略)且之前的数据依然保存在该PV上,但是该PV不可用,需要手动来处理这些数据并删除该PV。
  • 删除(Delete):当删除与之绑定的PVC时候
  • 回收(Recycle):这个在1.14版本中以及被废弃,取而代之的是推荐使用动态存储供给策略,它的功能是当删除与该PV关联的PVC时,自动删除该PV中的所有数据

更改 PersistentVolume 的回收策略

1
2
#kubectl patch pv wordpress-data -p '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}'
persistentvolume/wordpress-data patched

本地卷(hostPath)也就是LPV不支持动态供给的方式,延迟绑定,就是为了综合考虑所有因素再进行POD调度。其根本原因是动态供给是先调度POD到节点,然后动态创建PV以及绑定PVC最后运行POD;而LPV是先创建与某一节点关联的PV,然后在调度的时候综合考虑各种因素而且要包括PV在哪个节点,然后再进行调度,到达该节点后在进行PVC的绑定。也就说动态供给不考虑节点,LPV必须考虑节点。所以这两种机制有冲突导致无法在动态供给策略下使用LPV。换句话说动态供给是PV跟着POD走,而LPV是POD跟着PV走。

PV 和 PVC

创建 pv controller 和pvc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#cat mysql-pv.yaml 
apiVersion: v1
kind: PersistentVolume
metadata:
name: simple-pv-volume
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/mnt/simple"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pv-claim
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi

StorageClass

PV是运维人员来创建的,开发操作PVC,可是大规模集群中可能会有很多PV,如果这些PV都需要运维手动来处理这也是一件很繁琐的事情,所以就有了动态供给概念,也就是Dynamic Provisioning。而我们上面的创建的PV都是静态供给方式,也就是Static Provisioning。而动态供给的关键就是StorageClass,它的作用就是创建PV模板。

创建StorageClass里面需要定义PV属性比如存储类型、大小等;另外创建这种PV需要用到存储插件。最终效果是,用户提交PVC,里面指定存储类型,如果符合我们定义的StorageClass,则会为其自动创建PV并进行绑定。

简单可以把storageClass理解为名字,只是这个名字可以重复,然后pvc和pv之间通过storageClass来绑定。

如下case中两个pv和两个pvc的绑定就是通过storageClass(一致)来实现的(当然pvc要求的大小也必须和pv一致):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
mariadb-pv 8Gi RWO Retain Bound default/data-wordpress-mariadb-0 db 3m54s
wordpress-data 10Gi RWO Retain Bound default/wordpress wordpress 3m54s

[root@az3-k8s-11 15:35 /root/charts/bitnami/wordpress]
#kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-wordpress-mariadb-0 Bound mariadb-pv 8Gi RWO db 4m21s
wordpress Bound wordpress-data 10Gi RWO wordpress 4m21s

#cat create-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: mariadb-pv
spec:
capacity:
storage: 8Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: db
hostPath:
path: /mnt/mariadb-pv

---

apiVersion: v1
kind: PersistentVolume
metadata:
name: wordpress-data
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: wordpress
hostPath:
path: /mnt/wordpress-pv

----对应 pvc的定义参数:
persistence:
enabled: true
storageClass: "wordpress"
accessMode: ReadWriteOnce
size: 10Gi

persistence:
enabled: true
mountPath: /bitnami/mariadb
storageClass: "db"
annotations: {}
accessModes:
- ReadWriteOnce
size: 8Gi

定义StorageClass

1
2
3
4
5
6
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

定义PVC

1
2
3
4
5
6
7
8
9
10
11
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: local-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: local-storage

delete pv 卡住

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#kubectl describe pv wordpress-pv
Name: wordpress-pv
Labels: <none>
Annotations: pv.kubernetes.io/bound-by-controller: yes
Finalizers: [kubernetes.io/pv-protection] --- 问题在finalizers
StorageClass:
Status: Terminating (lasts 18h)
Claim: default/wordpress
Reclaim Policy: Retain
Access Modes: RWO
VolumeMode: Filesystem
Capacity: 10Gi
Node Affinity: <none>
Message:
Source:
Type: NFS (an NFS mount that lasts the lifetime of a pod)
Server: 192.168.0.111
Path: /mnt/wordpress-pv
ReadOnly: false
Events: <none>

先执行后就能自动删除了:
kubectl patch pv wordpress-pv -p '{"metadata":{"finalizers": []}}' --type=merge

kubernetes 集群部署

部署

系统参数修改

docker部署

kubeadm install

https://www.kubernetes.org.cn/4256.html

https://github.com/opsnull/follow-me-install-kubernetes-cluster

镜像源被墙,可以用阿里云镜像源

1
2
3
4
5
6
7
8
9
10
11
12
13
# 配置源
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF

# 安装
yum install -y kubelet kubeadm kubectl ipvsadm

初始化集群

多网卡情况下有必要指定网卡:–apiserver-advertise-address=192.168.0.80

1
2
3
4
5
6
7
8
9
10
11
12
13
# 使用本地 image repository
kubeadm init --kubernetes-version=1.18.0 --apiserver-advertise-address=192.168.0.110 --image-repository registry:5000/registry.aliyuncs.com/google_containers --service-cidr=10.10.0.0/16 --pod-network-cidr=10.122.0.0/16

# 给api-server 指定外网地址,在服务器有内网、外网多个ip的时候适用
kubeadm init --control-plane-endpoint 外网-ip:6443 --image-repository=registry:5000/registry.aliyuncs.com/google_containers --kubernetes-version=v1.21.0 --pod-network-cidr=172.16.0.0/16
#--apiserver-advertise-address=30.1.1.1,设置 apiserver 的 IP 地址,对于多网卡服务器来说很重要(比如 VirtualBox 虚拟机就用了两块网卡),可以指定 apiserver 在哪个网卡上对外提供服务。

# node join command
#kubeadm token create --print-join-command
kubeadm join 192.168.0.110:6443 --token 1042rl.b4qn9iuz6xv1ri7b --discovery-token-ca-cert-hash sha256:341a4bcfde9668077ef29211c2a151fe6e9334eea8955f645698706b3bf47a49

## 查看集群配置
kubectl get configmap -n kube-system kubeadm-config -o yaml

将一个node设置为不可调度,隔离出来,比如master 默认是不可调度的

1
2
kubectl cordon <node-name>
kubectl uncordon <node-name>

kubectl 管理多集群

一个kubectl可以管理多个集群,主要是 ~/.kube/config 里面的配置,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
clusters:
- cluster:
certificate-authority: /root/k8s-cluster.ca
server: https://192.168.0.80:6443
name: context-az1
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCQl0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
server: https://192.168.0.97:6443
name: context-az3

- context:
cluster: context-az1
namespace: default
user: az1-admin
name: az1
- context:
cluster: context-az3
namespace: default
user: az3-read
name: az3
current-context: az3 //当前使用的集群

kind: Config
preferences: {}
users:
- name: az1-admin
user:
client-certificate: /root/k8s.crt //key放在配置文件中
client-key: /root/k8s.key
- name: az3-read
user:
client-certificate-data: LS0tLS1CRUQ0FURS0tLS0tCg==
client-key-data: LS0tLS1CRUdJThuL2VPM0YxSWpEcXBQdmRNbUdiU2c9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=

多个集群中切换的话 : kubectl config use-context az3

快速合并两个cluster

简单来讲就是把两个集群的 .kube/config 文件合并,注意context、cluster name别重复了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 必须提前保证两个config文件中的cluster、context名字不能重复
export KUBECONFIG=~/.kube/config:~/someotherconfig
kubectl config view --flatten

#激活这个上下文
kubectl config use-context az1

#查看所有context
kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
az1 context-az1 az1-admin default
* az2 kubernetes kubernetes-admin
az3 context-az3 az3-read default

背后的原理类似于这个流程:

1
2
3
4
5
6
7
8
9
# 添加集群 集群地址上一步有获取 ,需要指定ca文件,上一步有获取 
kubectl config set-cluster cluster-az1 --server https://192.168.146.150:6444 --certificate-authority=/usr/program/k8s-certs/k8s-cluster.ca

# 添加用户 需要指定crt,key文件,上一步有获取
kubectl config set-credentials az1-admin --client-certificate=/usr/program/k8s-certs/k8s.crt --client-key=/usr/program/k8s-certs/k8s.key

# 指定一个上下文的名字,我这里叫做 az1,随便你叫啥 关联刚才的用户
kubectl config set-context az1 --cluster=context-az1 --namespace=default --user=az1-admin

apiserver高可用

默认只有一个apiserver,可以考虑用haproxy和keepalive来做一组apiserver的负载均衡:

1
2
3
4
5
6
docker run -d --name kube-haproxy \
-v /etc/haproxy:/usr/local/etc/haproxy:ro \
-p 8443:8443 \
-p 1080:1080 \
--restart always \
haproxy:1.7.8-alpine

haproxy配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#cat /etc/haproxy/haproxy.cfg 
global
log 127.0.0.1 local0 err
maxconn 50000
uid 99
gid 99
#daemon
nbproc 1
pidfile haproxy.pid

defaults
mode http
log 127.0.0.1 local0 err
maxconn 50000
retries 3
timeout connect 5s
timeout client 30s
timeout server 30s
timeout check 2s

listen admin_stats
mode http
bind 0.0.0.0:1080
log 127.0.0.1 local0 err
stats refresh 30s
stats uri /haproxy-status
stats realm Haproxy\ Statistics
stats auth will:will
stats hide-version
stats admin if TRUE

frontend k8s-https
bind 0.0.0.0:8443
mode tcp
#maxconn 50000
default_backend k8s-https

backend k8s-https
mode tcp
balance roundrobin
server lab1 192.168.1.81:6443 weight 1 maxconn 1000 check inter 2000 rise 2 fall 3
server lab2 192.168.1.82:6443 weight 1 maxconn 1000 check inter 2000 rise 2 fall 3
server lab3 192.168.1.83:6443 weight 1 maxconn 1000 check inter 2000 rise 2 fall 3

网络

1
2
3
4
kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml

#或者老版本的calico
curl https://docs.projectcalico.org/v3.15/manifests/calico.yaml -o calico.yaml

默认calico用的是ipip封包(这个性能跟原生网络差多少有待验证,本质也是overlay网络,比flannel那种要好很多吗?)

在所有node节点都在一个二层网络时候,flannel提供hostgw实现,避免vxlan实现的udp封装开销,估计是目前最高效的;calico也针对L3 Fabric,推出了IPinIP的选项,利用了GRE隧道封装;因此这些插件都能适合很多实际应用场景。

Service cluster IP尽可在集群内部访问,外部请求需要通过NodePort、LoadBalance或者Ingress来访问

网络插件由 containernetworking-plugins rpm包来提供,一般里面会有flannel、vlan等,安装在 /usr/libexec/cni/ 下(老版本没有带calico)

kubelet启动参数会配置 KUBELET_NETWORK_ARGS=–network-plugin=cni –cni-conf-dir=/etc/cni/net.d –cni-bin-dir=/usr/libexec/cni

kubectl 启动容器

1
2
kubectl run -i --tty busybox --image=registry:5000/busybox -- sh
kubectl attach busybox -c busybox -i -t

dashboard

1
2
3
4
kubectl apply -f  https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc7/aio/deploy/recommented.yaml

#暴露 dashboard 服务端口 (recommended中如果已经定义了 30000这个nodeport,所以这个命令不需要了)
kubectl port-forward -n kubernetes-dashboard svc/kubernetes-dashboard 30000:443 --address 0.0.0.0

dashboard login token:

1
2
#kubectl describe secrets -n kubernetes-dashboard   | grep token | awk 'NR==3{print $2}'
eyJhbGciOiJSUzI1NiIsImtpZCI6IndRc0hiMkdpWHRwN1FObTcyeUdhOHI0eUxYLTlvODd2U0NBcU1GY0t1Sk0ifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkZWZhdWx0LXRva2VuLXRia3o5Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImRlZmF1bHQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIwYzM2MzBhOS0xMjBjLTRhNmYtYjM0ZS0zM2JhMTE1OWU1OWMiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6ZGVmYXVsdCJ9.SP4JEw0kGDmyxrtcUC3HALq99Xr99E-tie5fk4R8odLJBAYN6HxEx80RbTSnkeSMJNApbtwXBLrp4I_w48kTkr93HJFM-oxie3RVLK_mEpZBF2JcfMk6qhfz4RjPiqmG6mGyW47mmY4kQ4fgpYSmZYR4LPJmVMw5W2zo5CGhZT8rKtgmi5_ROmYpWcd2ZUORaexePgesjjKwY19bLEXFOwdsqekwEvj1_zaJhKAehF_dBdgW9foFXkbXOX0xAC0QNnKUwKPanuFOVZDg1fhyV-eyi6c9-KoTYqZMJTqZyIzscIwruIRw0oauJypcdgi7ykxAubMQ4sWEyyFafSEYWg

dashboard 显示为空的话(留意报错信息,一般是用户权限,重新授权即可)

1
2
kubectl delete clusterrolebinding kubernetes-dashboard
kubectl create clusterrolebinding kubernetes-dashboard --clusterrole=cluster-admin --serviceaccount=kube-system:kubernetes-dashboard --user="system:serviceaccount:kubernetes-dashboard:default"

其中:system:serviceaccount:kubernetes-dashboard:default 来自于报错信息中的用户名

默认dashboard login很快expired,可以设置不过期:

1
2
3
4
5
6
7
8
9
$ kubectl -n kubernetes-dashboard edit deployments kubernetes-dashboard
...
spec:
containers:
- args:
- --auto-generate-certificates
- --token-ttl=0 //增加这行表示不expire

--enable-skip-login //增加这行表示不需要token 就能login,不推荐

kubectl proxy –address 0.0.0.0 –accept-hosts ‘.*’

node管理调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//如何优雅删除node
kubectl drain my-node # 对 my-node 节点进行清空操作,为节点维护做准备
kubectl drain ky4 --ignore-daemonsets --delete-local-data # 驱逐pod
kubectl delete node ky4 # 删除node

kubectl cordon my-node # 标记 my-node 节点为不可调度
kubectl uncordon my-node # 标记 my-node 节点为可以调度
kubectl top node my-node # 显示给定节点的度量值
kubectl cluster-info # 显示主控节点和服务的地址
kubectl cluster-info dump # 将当前集群状态转储到标准输出
kubectl cluster-info dump --output-directory=/path/to/cluster-state # 将当前集群状态输出到 /path/to/cluster-state

# 如果已存在具有指定键和效果的污点,则替换其值为指定值
kubectl taint nodes foo dedicated=special-user:NoSchedule
kubectl taint nodes poc65 node-role.kubernetes.io/master:NoSchedule-

地址

这些字段的用法取决于你的云服务商或者物理机配置。

  • HostName:由节点的内核设置。可以通过 kubelet 的 --hostname-override 参数覆盖。
  • ExternalIP:通常是节点的可外部路由(从集群外可访问)的 IP 地址。
  • InternalIP:通常是节点的仅可在集群内部路由的 IP 地址。

状况

1
2
3
4
5
# kubectl get node -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
172.26.137.114 Ready master 6d1h v1.19.0 172.26.137.114 <none> CentOS Linux 7 (Core) 3.10.0-957.21.3.el7.x86_64 docker://19.3.8
172.26.137.115 Ready node 6d1h v1.19.0 172.26.137.115 <none> CentOS Linux 7 (Core) 3.10.0-957.21.3.el7.x86_64 docker://19.3.8
172.26.137.116 Ready,SchedulingDisabled node 6d1h v1.19.0 172.26.137.116 <none> CentOS Linux 7 (Core) 3.10.0-957.21.3.el7.x86_64 docker://19.3.8

如果 Ready 条件处于 Unknown 或者 False 状态的时间超过了 pod-eviction-timeout 值, (一个传递给 kube-controller-manager 的参数), 节点上的所有 Pod 都会被节点控制器计划删除。默认的逐出超时时长为 5 分钟。 某些情况下,当节点不可达时,API 服务器不能和其上的 kubelet 通信。 删除 Pod 的决定不能传达给 kubelet,直到它重新建立和 API 服务器的连接为止。 与此同时,被计划删除的 Pod 可能会继续在游离的节点上运行。

node cidr 缺失

flannel pod 运行正常,pod无法创建,检查flannel日志发现该node cidr缺失

1
2
3
4
I0818 08:06:38.951132       1 main.go:733] Defaulting external v6 address to interface address (<nil>)
I0818 08:06:38.951231 1 vxlan.go:137] VXLAN config: VNI=1 Port=0 GBP=false Learning=false DirectRouting=false
E0818 08:06:38.951550 1 main.go:325] Error registering network: failed to acquire lease: node "ky3" pod cidr not assigned
I0818 08:06:38.951604 1 main.go:439] Stopping shutdownHandler...

正常来说describe node会看到如下的cidr信息

1
2
3
 Kube-Proxy Version:         v1.15.8-beta.0
PodCIDR: 172.19.1.0/24
Non-terminated Pods: (3 in total)

可以手工给node添加cidr

1
kubectl patch node ky3 -p '{"spec":{"podCIDR":"172.19.3.0/24"}}'

prometheus

1
2
3
git clone https://github.com/coreos/kube-prometheus.git
kubectl apply -f manifests/setup
kubectl apply -f manifests/

暴露grafana端口:

1
kubectl port-forward --address 0.0.0.0 svc/grafana -n monitoring 3000:3000 

部署应用

DRDS deployment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
apiVersion: v1
kind: Namespace
metadata:
name: drds

---

apiVersion: apps/v1
kind: Deployment
metadata:
name: drds-deployment
namespace: drds
labels:
app: drds-server
spec:
# 创建2个nginx容器
replicas: 3
selector:
matchLabels:
app: drds-server
template:
metadata:
labels:
app: drds-server
spec:
containers:
- name: drds-server
image: registry:5000/drds-image:v5_wisp_5.4.5-15940932
ports:
- containerPort: 8507
- containerPort: 8607
env:
- name: diamond_server_port
value: "8100"
- name: diamond_server_list
value: "192.168.0.79,192.168.0.82"
- name: drds_server_id
value: "1"

DRDS Service

每个 drds 容器会通过8507提供服务,service通过3306来为一组8507做负载均衡,这个service的3306是在cluster-ip上,外部无法访问

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: drds-service
namespace: drds
spec:
selector:
app: drds-server
ports:
- protocol: TCP
port: 3306
targetPort: 8507

通过node port来访问 drds service(同时会有负载均衡):

1
kubectl port-forward --address 0.0.0.0 svc/drds-service -n drds 3306:3306

部署mysql statefulset应用

drds-pv-mysql-0 后面的mysql 会用来做存储,下面用到了三个mysql(需要三个pvc)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#cat mysql-deployment.yaml 
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.7
name: mysql
env:
# Use secret in real usage
- name: MYSQL_ROOT_PASSWORD
value: "123456"
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: pv-claim

清理:

1
2
3
kubectl delete deployment,svc mysql
kubectl delete pvc mysql-pv-claim
kubectl delete pv mysql-pv-volume

查看所有pod ip以及node ip:

1
kubectl get pods -o wide

配置 Pod 使用 ConfigMap

ConfigMap 允许你将配置文件与镜像文件分离,以使容器化的应用程序具有可移植性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# cat mysql-configmap.yaml  //mysql配置文件放入: configmap
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql
labels:
app: mysql
data:
master.cnf: |
# Apply this config only on the master.
[mysqld]
log-bin

mysqld.cnf: |
[mysqld]
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
datadir = /var/lib/mysql
#log-error = /var/log/mysql/error.log
# By default we only accept connections from localhost
#bind-address = 127.0.0.1
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'
# 慢查询阈值,查询时间超过阈值时写入到慢日志中
long_query_time = 2
innodb_buffer_pool_size = 257M


slave.cnf: |
# Apply this config only on slaves.
[mysqld]
super-read-only

786 26/08/20 15:27:00 kubectl create configmap game-config-env-file --from-env-file=configure-pod-container/configmap/game-env-file.properties
787 26/08/20 15:28:10 kubectl get configmap -n kube-system kubeadm-config -o yaml
788 26/08/20 15:28:11 kubectl get configmap game-config-env-file -o yaml

将mysql root密码放入secret并查看 secret密码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# cat mysql-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysql-root-password
type: Opaque
data:
password: MTIz

# echo -n '123' | base64 //生成密码编码
# kubectl get secret mysql-root-password -o jsonpath='{.data.password}' | base64 --decode -

或者创建一个新的 secret:
kubectl create secret generic my-secret --from-literal=password="Password"

在mysql容器中使用以上configmap中的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
spec:
volumes:
- name: conf
emptyDir: {}
- name: myconf
emptyDir: {}
- name: config-map
configMap:
name: mysql
initContainers:
- name: init-mysql
image: mysql:5.7
command:
- bash
- "-c"
- |
set -ex
# Generate mysql server-id from pod ordinal index.
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
echo [mysqld] > /mnt/conf.d/server-id.cnf
# Add an offset to avoid reserved server-id=0 value.
echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
#echo "innodb_buffer_pool_size=512m" > /mnt/rds.cnf
# Copy appropriate conf.d files from config-map to emptyDir.
#if [[ $ordinal -eq 0 ]]; then
cp /mnt/config-map/master.cnf /mnt/conf.d/
cp /mnt/config-map/mysqld.cnf /mnt/mysql.conf.d/
#else
# cp /mnt/config-map/slave.cnf /mnt/conf.d/
#fi
volumeMounts:
- name: conf
mountPath: /mnt/conf.d
- name: myconf
mountPath: /mnt/mysql.conf.d
- name: config-map
mountPath: /mnt/config-map
containers:
- name: mysql
image: mysql:5.7
env:
#- name: MYSQL_ALLOW_EMPTY_PASSWORD
# value: "1"
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-root-password
key: password

通过挂载方式进入到容器里的 Secret,一旦其对应的 Etcd 里的数据被更新,这些 Volume 里的文件内容,同样也会被更新。其实,这是 kubelet 组件在定时维护这些 Volume。

集群会自动创建一个 default-token-**** 的secret,然后所有pod都会自动将这个 secret通过 Porjected Volume挂载到容器,也叫 ServiceAccountToken,是一种特殊的Secret

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    Environment:    <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-ncgdl (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
default-token-ncgdl:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-ncgdl
Optional: false
QoS Class: BestEffort

apply create操作

先 kubectl create,再 replace 的操作,我们称为命令式配置文件操作

kubectl apply 命令才是“声明式 API”

kubectl replace 的执行过程,是使用新的 YAML 文件中的 API 对象,替换原有的 API 对象;

而 kubectl apply,则是执行了一个对原有 API 对象的 PATCH 操作。

kubectl set image 和 kubectl edit 也是对已有 API 对象的修改

kube-apiserver 在响应命令式请求(比如,kubectl replace)的时候,一次只能处理一个写请求,否则会有产生冲突的可能。而对于声明式请求(比如,kubectl apply),一次能处理多个写操作,并且具备 Merge 能力

声明式 API,相当于对外界所有操作(并发接收)串行merge,才是 Kubernetes 项目编排能力“赖以生存”的核心所在

如何使用控制器模式,同 Kubernetes 里 API 对象的“增、删、改、查”进行协作,进而完成用户业务逻辑的编写过程。

label

给多个节点加标签

1
2
3
4
kubectl label  --overwrite=true nodes 10.0.0.172 10.0.1.192 10.0.2.48 topology.kubernetes.io/region=cn-hangzhou

//查看
kubectl get nodes --show-labels

helm

Helm 是 Kubernetes 的包管理器。包管理器类似于我们在 Ubuntu 中使用的apt、Centos中使用的yum 或者Python中的 pip 一样,能快速查找、下载和安装软件包。Helm 由客户端组件 helm 和服务端组件 Tiller 组成, 能够将一组K8S资源打包统一管理, 是查找、共享和使用为Kubernetes构建的软件的最佳方式。

建立local repo index:

1
helm repo index [DIR] [flags]

仓库只能index 到 helm package 发布后的tgz包,意义不大。每次index后需要 helm repo update

然后可以启动一个http服务:

1
nohup python -m SimpleHTTPServer 8089 &

将local repo加入到仓库:

1
2
3
4
5
 helm repo add local http://127.0.0.1:8089

# helm repo list
NAME URL
local http://127.0.0.1:8089

install chart:

1
2
3
4
5
6
7
8
//helm3 默认不自动创建namespace,不带参数就报没有 ame 的namespace错误
helm install -name wordpress -n test --create-namespace .

helm list -n test

{{ .Release.Name }} 这种是helm内部自带的值,都是一些内建的变量,所有人都可以访问

image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 这种是我们从values.yaml文件中获取或者从命令行中获取的值。

quote是一个模板方法,可以将输入的参数添加双引号

模板片段

之前我们看到有个文件叫做_helpers.tpl,我们介绍是说存储模板片段的地方。

模板片段其实也可以在文件中定义,但是为了更好管理,可以在_helpers.tpl中定义,使用时直接调用即可。

自动补全

kubernetes自动补全:

1
2
3
source <(kubectl completion bash) 

echo "source <(kubectl completion bash)" >> ~/.bashrc

helm自动补全:

1
2
cd ~
helm completion bash > .helmrc && echo "source .helmrc" >> .bashrc && source .bashrc

两者都需要依赖 auto-completion,所以得先:

1
2
# yum install -y bash-completion
# source /usr/share/bash-completion/bash_completion

kubectl -s polarx-test-ackk8s-atp-3826.adbgw.alibabacloud.test exec -it bushu016polarx282bc7216f-5161 bash

启动时间排序

1
2
532  [2021-08-24 18:37:19] kubectl get po --sort-by=.status.startTime -ndrds
533 [2021-08-24 18:37:41] kubectl get pods --sort-by=.metadata.creationTimestamp -ndrds

kubeadm

初始化集群的时候第一看kubelet能否起来(cgroup配置),第二就是看kubelet静态起pod,kubelet参数指定yaml目录,然后kubelet拉起这个目录下的所有yaml。

kubeadm启动集群就是如此。kubeadm生成证书、etcd.yaml等yaml、然后拉起kubelet,kubelet拉起etcd、apiserver等pod,kubeadm init 的时候主要是在轮询等待apiserver的起来。

可以通过kubelet –v 256来看详细日志,kubeadm本身所做的事情并不多,所以日志没有太多的信息,主要是等待轮询apiserver的拉起。

Kubeadm config

Init 可以指定仓库以及版本

1
kubeadm init --image-repository=registry:5000/registry.aliyuncs.com/google_containers --kubernetes-version=v1.14.6  --pod-network-cidr=10.244.0.0/16

查看并修改配置

1
2
3
4
5
6
7
sudo kubeadm config view > kubeadm-config.yaml
edit kubeadm-config.yaml and replace k8s.gcr.io with your repo
sudo kubeadm upgrade apply --config kubeadm-config.yaml

kubeadm config images pull --config="/root/kubeadm-config.yaml"

kubectl get cm -n kube-system kubeadm-config -o yaml

pod镜像拉取不到的话可以在kebelet启动参数中写死pod镜像(pod_infra_container_image)

1
2
#cat /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS --pod_infra_container_image=registry:5000/registry.aliyuncs.com/google_containers/pause:3.1

构建离线镜像库

1
2
3
4
kubeadm config images list >1.24.list

cat 1.24.list | awk -F / '{ print $0 " " $3}' > 1.24.aarch.list

cni 报x509: certificate signed by unknown authority

一个集群下反复部署calico/flannel插件后,在 /etc/cni/net.d/ 下会有cni 网络配置文件残留,导致 flannel 创建容器网络的时候报证书错误。其实这不只是证书错误,还可能报其它cni配置错误,总之这是因为 10-calico.conflist 不符合 flannel要求所导致的。

1
2
3
4
5
# find /etc/cni/net.d/
/etc/cni/net.d/
/etc/cni/net.d/calico-kubeconfig
/etc/cni/net.d/10-calico.conflist //默认读取了这个配置文件,不符合flannel
/etc/cni/net.d/10-flannel.conflist

因为calico 排在 flannel前面,所以即使用flannel配置文件也是用的 10-calico.conflist。每次 kubeadm reset 的时候是不会去做 cni 的reset 的:

1
2
3
4
[reset] Deleting files: [/etc/kubernetes/admin.conf /etc/kubernetes/kubelet.conf /etc/kubernetes/bootstrap-kubelet.conf /etc/kubernetes/controller-manager.conf /etc/kubernetes/scheduler.conf]
[reset] Deleting contents of stateful directories: [/var/lib/kubelet /var/lib/dockershim /var/run/kubernetes /var/lib/cni]

The reset process does not clean CNI configuration. To do so, you must remove /etc/cni/net.d

kubernetes API 案例

用kubeadm部署kubernetes集群,会生成如下证书:

1
2
3
#ls /etc/kubernetes/pki/
apiserver-etcd-client.crt apiserver-kubelet-client.crt apiserver.crt ca.crt etcd front-proxy-ca.key front-proxy-client.key sa.pub
apiserver-etcd-client.key apiserver-kubelet-client.key apiserver.key ca.key front-proxy-ca.crt front-proxy-client.crt sa.key

curl访问api必须提供证书

1
curl --cacert /etc/kubernetes/pki/ca.crt --cert /etc/kubernetes/pki/apiserver-kubelet-client.crt --key /etc/kubernetes/pki/apiserver-kubelet-client.key https://ip:6443/apis/apps/v1/deployments

/etc/kubernetes/pki/ca.crt —- CA机构

由CA机构签发:/etc/kubernetes/pki/apiserver-kubelet-client.crt

Image

获取default namespace下的deployment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# JWT_TOKEN_DEFAULT_DEFAULT=$(kubectl get secrets \
$(kubectl get serviceaccounts/default -o jsonpath='{.secrets[0].name}') \
-o jsonpath='{.data.token}' | base64 --decode)

#curl --cacert /etc/kubernetes/pki/ca.crt --cert /etc/kubernetes/pki/apiserver-kubelet-client.crt --key /etc/kubernetes/pki/apiserver-kubelet-client.key https://11.158.239.200:6443/apis/apps/v1/namespaces/default/deployments --header "Authorization: Bearer $JWT_TOKEN_DEFAULT_DEFAULT"
{
"kind": "DeploymentList",
"apiVersion": "apps/v1",
"metadata": {
"resourceVersion": "1233307"
},
"items": [
{
"metadata": {
"name": "nginx-deployment",

//列出default namespace下所有的pod
#curl --cacert /etc/kubernetes/pki/ca.crt --cert /etc/kubernetes/pki/apiserver-kubelet-client.crt --key /etc/kubernetes/pki/apiserver-kubelet-client.key https://11.158.239.200:6443/api/v1/namespaces/default/pods --header "Authorization: Bearer $JWT_TOKEN_DEFAULT_DEFAULT"

//对应的kubectl生成的curl命令
curl --cacert /etc/kubernetes/pki/ca.crt --cert /etc/kubernetes/pki/apiserver-kubelet-client.crt --key /etc/kubernetes/pki/apiserver-kubelet-client.key -v -XGET -H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json" -H "User-Agent: kubectl/v1.23.3 (linux/arm64) kubernetes/816c97a" 'https://11.158.239.200:6443/api/v1/namespaces/default/pods?limit=500'

对应地可以通过 kubectl -v 256 get pods 来看kubectl的处理过程,以及具体访问的api、参数、返回结果等。实际kubectl最终也是通过libcurl来访问的这些api。这样也不用对api-server抓包分析了。

或者将kube api-server 代理成普通http服务

# Make Kubernetes API available on localhost:8080
# to bypass the auth step in subsequent queries:
$ kubectl proxy –port=8080

然后

curl http://localhost:8080/api/v1/namespaces

Image

抓包

用curl调用kubernetes api-server来调试,需要抓包,先在执行curl的服务器上配置环境变量

1
export SSLKEYLOGFILE=/root/ssllog/apiserver-ssl.log

然后执行tcpdump对api-server的6443端口抓包,然后将/root/ssllog/apiserver-ssl.log和抓包文件下载到本地,wireshark打开抓包文件,同时配置tls。

以下是个完整case(技巧指定curl的本地端口为12345,然后tcpdump只抓12345,所得的请求、response结果都会解密–如果抓api-server的6443则只能看到请求被解密)

1
2
3
curl --local-port 12345 --cacert /etc/kubernetes/pki/ca.crt --cert /etc/kubernetes/pki/apiserver-kubelet-client.crt --key /etc/kubernetes/pki/apiserver-kubelet-client.key https://11.158.239.200:6443/apis/apps/v1/namespaces/default/deployments --header "Authorization: Bearer $JWT_TOKEN_DEFAULT_DEFAULT"

#cat $JWT_TOKEN_DEFAULT_DEFAULT eyJhbGciOiJSUzI1NiIsImtpZCI6ImlNVVFVNmxUM2t4c3Y2Q3IyT1BzV2hDZGRVSmVxTHc5RV8wUXZ4RVM5REEifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJ: File name too long

image-20220223170008311

参考资料

https://kubernetes.io/zh/docs/reference/kubectl/cheatsheet/

Java 技巧合集

获取一直FullGC下的java进程HeapDump的小技巧

就是小技巧,操作步骤需要查询,随手记录

  • 找到java进程,gdb attach上去, 例如 gdb -p 12345
  • 找到这个HeapDumpBeforeFullGC的地址(这个flag如果为true,会在FullGC之前做HeapDump,默认是false)
1
2
(gdb) p &HeapDumpBeforeFullGC
$2 = (<data variable, no debug info> *) 0x7f7d50fc660f <HeapDumpBeforeFullGC>
  • Copy 地址:0x7f7d50fc660f
  • 然后把他设置为true,这样下次FGC之前就会生成一份dump文件
1
2
(gdb) set *0x7f7d50fc660f = 1
(gdb) quit
  • 最后,等一会,等下次FullGC触发,你就有HeapDump了!
    (如果没有指定heapdump的名字,默认是 java_pidxxx.hprof)

(PS. jstat -gcutil pid 可以查看gc的概况)

(操作完成后记得gdb上去再设置回去,不然可能一直fullgc,导致把磁盘打满).

其它

在jvm还有响应的时候可以: jinfo -flag +HeapDumpBeforeFullGC pid 设置HeapDumpBeforeFullGC 为true(- 为false,+-都不要为只打印值)

kill -3 产生coredump 存放在 kernel.core_pattern=/root/core (/etc/sysctl.conf , 先 ulimit -c unlimited;或者 gcore id 获取coredump)

得到core文件后,采用 gdb -c 执行文件 core文件 进入调试模式,对于java,有以下2个技巧:

进入gdb调试模式后,输入如下命令: info threads,观察异常的线程,定位到异常的线程后,则可以输入如下命令:thread 线程编号,则会打印出当前java代码的工作流程。

而对于这个core,亦可以用jstack jmap打印出堆信息,线程信息,具体命令:

jmap -heap 执行文件 core文件 jstack -F -l 执行文件 core文件

容器中的进程的话需要到宿主机操作,并且将容器中的 jdk文件夹复制到宿主机对应的位置。

ps auxff |grep 容器id -A10 找到JVM在宿主机上的进程id

coredump

Coredump叫做核心转储,它是进程运行时在突然崩溃的那一刻的一个内存快照。操作系统在程序发生异常而异常在进程内部又没有被捕获的情况下,会把进程此刻内存、寄存器状态、运行堆栈等信息转储保存在一个文件里。

kill -3 产生coredump 存放在 kernel.core_pattern=/root/core (/etc/sysctl.conf , 先 ulimit -c unlimited;)

或者 gcore id 获取coredump

coredump 所在位置

1
2
$cat /proc/sys/kernel/core_pattern
/home/admin/

coredump 分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//打开 coredump
$gdb /opt/taobao/java/bin/java core.24086
[New LWP 27184]
[New LWP 27186]
[New LWP 24086]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Core was generated by `/opt/tt/java_coroutine/bin/java'.
#0 0x00007f2fa4fada35 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
Missing separate debuginfos, use: debuginfo-install jdk-8.9.14-20200203164153.alios7.x86_64
(gdb) info threads //查看所有thread
Id Target Id Frame
583 Thread 0x7f2fa56177c0 (LWP 24086) 0x00007f2fa4fab017 in pthread_join () from /lib64/libpthread.so.0
582 Thread 0x7f2f695f3700 (LWP 27186) 0x00007f2fa4fada35 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
581 Thread 0x7f2f6cbfb700 (LWP 27184) 0x00007f2fa4fada35 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
580 Thread 0x7f2f691ef700 (LWP 27176) 0x00007f2fa4fada35 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
579 Thread 0x7f2f698f6700 (LWP 27174) 0x00007f2fa4fada35 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0


(gdb) thread apply all bt //查看所有线程堆栈
Thread 583 (Thread 0x7f2fa56177c0 (LWP 24086)):
#0 0x00007f2fa4fab017 in pthread_join () from /lib64/libpthread.so.0
#1 0x00007f2fa4b85085 in ContinueInNewThread0 (continuation=continuation@entry=0x7f2fa4b7fd70 <JavaMain>, stack_size=1048576, args=args@entry=0x7ffe529432d0)
at /ssd1/jenkins_home/workspace/ajdk.8.build.master/jdk/src/solaris/bin/java_md_solinux.c:1044
#2 0x00007f2fa4b81877 in ContinueInNewThread (ifn=ifn@entry=0x7ffe529433d0, threadStackSize=<optimized out>, argc=<optimized out>, argv=0x7f2fa3c163a8, mode=mode@entry=1,
what=what@entry=0x7ffe5294be17 "com.taobao.tddl.server.TddlLauncher", ret=0) at /ssd1/jenkins_home/workspace/ajdk.8.build.master/jdk/src/share/bin/java.c:2033
#3 0x00007f2fa4b8513b in JVMInit (ifn=ifn@entry=0x7ffe529433d0, threadStackSize=<optimized out>, argc=<optimized out>, argv=<optimized out>, mode=mode@entry=1,
what=what@entry=0x7ffe5294be17 "com.taobao.tddl.server.TddlLauncher", ret=ret@entry=0) at /ssd1/jenkins_home/workspace/ajdk.8.build.master/jdk/src/solaris/bin/java_md_solinux.c:1091
#4 0x00007f2fa4b8254d in JLI_Launch (argc=0, argv=0x7f2fa3c163a8, jargc=<optimized out>, jargv=<optimized out>, appclassc=1, appclassv=0x0, fullversion=0x400885 "1.8.0_232-b604",
dotversion=0x400881 "1.8", pname=0x40087c "java", lname=0x40087c "java", javaargs=0 '\000', cpwildcard=1 '\001', javaw=0 '\000', ergo=0)
at /ssd1/jenkins_home/workspace/ajdk.8.build.master/jdk/src/share/bin/java.c:304
#5 0x0000000000400635 in main ()

Thread 582 (Thread 0x7f2f695f3700 (LWP 27186)):
#0 0x00007f2fa4fada35 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
#1 0x00007f2fa342d863 in Parker::park(bool, long) () from /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/server/libjvm.so
#2 0x00007f2fa35ba3c3 in Unsafe_Park () from /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/server/libjvm.so
#3 0x00007f2f9343b44a in ?? ()
#4 0x000000008082e778 in ?? ()
#5 0x0000000000000003 in ?? ()
#6 0x00007f2f88e32758 in ?? ()
#7 0x00007f2f6f532800 in ?? ()

(gdb) thread apply 582 bt //查看582这个线程堆栈,LWP 27186(0x6a32)对应jstack 线程10进程id

Thread 582 (Thread 0x7f2f695f3700 (LWP 27186)):
#0 0x00007f2fa4fada35 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
#1 0x00007f2fa342d863 in Parker::park(bool, long) () from /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/server/libjvm.so
#2 0x00007f2fa35ba3c3 in Unsafe_Park () from /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/server/libjvm.so
#3 0x00007f2f9343b44a in ?? ()
#35 0x00000000f26fc738 in ?? ()
#36 0x00007f2fa51cec5b in arena_run_split_remove (arena=0x7f2f6ab09c34, chunk=0x80, run_ind=0, flag_dirty=0, flag_decommitted=<optimized out>, need_pages=0) at src/arena.c:398
#37 0x00007f2f695f2980 in ?? ()
#38 0x0000000000000001 in ?? ()
#39 0x00007f2f88e32758 in ?? ()
#40 0x00007f2f695f2920 in ?? ()
#41 0x00007f2fa32f46b8 in CallInfo::set_common(KlassHandle, KlassHandle, methodHandle, methodHandle, CallInfo::CallKind, int, Thread*) ()
from /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/server/libjvm.so
#42 0x00007f2f7d800000 in ?? ()

以上堆栈涉及到Java代码部分都是看不到函数,需要进一步把Java 符号替换进去

coredump 转 jmap hprof

1
jmap -dump:format=b,file=24086.hprof /opt/taobao/java/bin/java core.24086

以上命令输入是 core.24086 这个 coredump,输出是一个 jmap 的dump 24086.hprof

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$jmap -J-d64 /opt/taobao/java/bin/java core.24086

Attaching to core core.24086 from executable /opt/taobao/java/bin/java, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.232-b604
0x0000000000400000 8K /opt/taobao/java/bin/java
0x00007f2fa51be000 6679K /opt/taobao/install/ajdk-8_9_14-b604/bin/../lib/amd64/libjemalloc.so.2
0x00007f2fa4fa2000 138K /lib64/libpthread.so.0
0x00007f2fa4d8c000 88K /lib64/libz.so.1
0x00007f2fa4b7d000 280K /opt/taobao/install/ajdk-8_9_14-b604/bin/../lib/amd64/jli/libjli.so
0x00007f2fa4979000 18K /lib64/libdl.so.2
0x00007f2fa45ab000 2105K /lib64/libc.so.6
0x00007f2fa43a3000 42K /lib64/librt.so.1
0x00007f2fa40a1000 1110K /lib64/libm.so.6
0x00007f2fa5406000 159K /lib64/ld-linux-x86-64.so.2
0x00007f2fa2af1000 17898K /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/server/libjvm.so
0x00007f2fa25f1000 64K /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/libverify.so
0x00007f2fa23c2000 228K /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/libjava.so
0x00007f2fa21af000 60K /lib64/libnss_files.so.2
0x00007f2fa1fa5000 47K /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/libzip.so
0x00007f2f80ded000 96K /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/libnio.so
0x00007f2f80bd4000 119K /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/libnet.so
0x00007f2f7e1f6000 50K /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/libmanagement.so
0x00007f2f75dc8000 209K /home/admin/drds-server/lib/native/libsigar-amd64-linux.so
0x00007f2f6d8ad000 293K /opt/taobao/install/ajdk-8_9_14-b604/jre/lib/amd64/libsunec.so
0x00007f2f6d697000 86K /lib64/libgcc_s.so.1
0x00007f2f6bdf9000 30K /lib64/libnss_dns.so.2
0x00007f2f6bbdf000 107K /lib64/libresolv.so.2

coredump 生成 java stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
jstack -J-d64 /opt/taobao/java/bin/java core.24086 

Attaching to core core.24086 from executable /opt/taobao/java/bin/java, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.232-b604
Deadlock Detection:

No deadlocks found.

Thread 27186: (state = BLOCKED)
- sun.misc.Unsafe.park0(boolean, long) @bci=0 (Compiled frame; information may be imprecise)
- sun.misc.Unsafe.park(boolean, long) @bci=63, line=1038 (Compiled frame)
- java.util.concurrent.locks.LockSupport.park(java.lang.Object) @bci=14, line=176 (Compiled frame)
- java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await() @bci=42, line=2047 (Compiled frame)
- java.util.concurrent.LinkedBlockingQueue.take() @bci=29, line=446 (Compiled frame)
- java.util.concurrent.ThreadPoolExecutor.getTask() @bci=149, line=1074 (Compiled frame)
- java.util.concurrent.ThreadPoolExecutor.runWorker(java.util.concurrent.ThreadPoolExecutor$Worker) @bci=26, line=1134 (Compiled frame)
- java.util.concurrent.ThreadPoolExecutor$Worker.run() @bci=5, line=624 (Compiled frame)
- java.lang.Thread.run() @bci=11, line=858 (Compiled frame)

gdb coredump with java symbol

需要安装JVM debug info包,同时要求gdb版本在7.10以上

设置:

home 目录下创建 .gdbinit 然后放入如下内容,libjvm.so-gdb.py 就是 dbg.py 脚本,gdb启动的时候会自动加载这个脚本

1
2
$cat ~/.gdbinit
add-auto-load-safe-path /opt/install/jdk-8_9_14-b604/jre/lib/amd64/server/libjvm.so-gdb.py

使用:

1
gdb -iex "set auto-load safe-path /" /opt/install/java/bin/java ./core.24086

G1 GC为什么快

https://ata.alibaba-inc.com/articles/199497

G1比CMS GC效率更高,暂停短、更稳定,但是最终吞吐大概率是CMS要好,这是因为G1编译后代码更大

-XX:InlineSmallCode=3000告诉编译器, 汇编3000字节以内的函数需要被inline, 这个值默认是2000

另外CMS用的是Dirty Card,而G1 为了降低GC时间在Remeber Set(类似Dirty Card)的维护上花了更多的代价

Dirty Card维护代价:

  • 会影响code size
    Code size影响了inline机会
    Code size增大则instruction cache miss几率变大 (几十倍的执行时间差距)
  • 本身执行mark dirty动作耗时, 这是一个写内存+GC/mutator线程同步的操作, 可以很复杂, 也可以很简单

比如G1为了降低暂停时间,就要尽量控制Remeber Set的更新,所以还需要判断write动作是否真的有必要更新Remeber Set(类似old.ref = null这种写操作是不需要更新Remeber Set的)

简单说CMS的每次 Dirty Card维护只需要3条汇编,而G1的Remember Set维护需要十多条、几十条汇编

0%