Code war of Angel


  • Home

  • Archives

GMP调度模型

Posted on 2021-06-25

概念

  • G:goroutine
  • P:它代表了 M 所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接 M 和 G 的调度上下文,将等待执行的 G 与 M 对接。
  • M:工作线程

调度模型

1. 创建idle p(GOMAXPROCS)

2. 主线程运行: 创建P,绑定P0与M0

3. 创建一个goroutine

  • Wakeup 唤醒机制:有空闲的 P 而没有在 spinning 状态的 M 时候, 需要去唤醒一个空闲(睡眠)的 M 或者新建一个。当线程首次创建时,会执行一个特殊的 G,即 g0,它负责管理和调度 G。
  • goroutine创建完后,会尝试放在当前P的local queue,如果local queue已满(256),将本地队列的前半部分及新建的goroutine迁移到全局队列。

4. 抢占任务

  • 由于3 创建的goroutine可能在本地队列或全局队列,此时新创建的P本地队列为空,这次新的M对执行g0代码,企图去找任务。
  • 当一个 P 执行完本地所有的 G 之后,会尝试挑选一个受害者 P,从它的 G 队列中窃取一半的 G。当尝试若干次窃取都失败之后,会从全局队列中获取(当前个数/GOMAXPROCS)个 G。 为避免全局队列饥饿,P 的调度算法中还会每个 N 轮调度之后就去全局队列拿一个 G。

5. 发生syscall

  • 调用 syscall 后会解绑 P,然后 M 和 G 进入阻塞。
  • 系统监视器 (system monitor),称为 sysmon,会定时扫描。在执行 syscall 时, 如果某个 P 的 G 执行超过一个 sysmon tick(10ms),就会把他设为 idle,重新调度给需要的 M,强制解绑。
  • syscall结束后。如果在短时间内阻塞的 M 就唤醒了,那么 M 会优先来重新获取这个 P,能获取到就继续绑回去,这样有利于数据的局部性。找不到就找其他空闲的P,否则放回全局队列。
  • PS:使用 syscall 写程序要认真考虑 pthread exhaust 问题。

6. 自旋的M

  • 类型1:M 不带 P 的找 P 挂载(一有 P 释放就结合)
  • 类型2:M 带 P 的找 G 运行(一有 runable 的 G 就执行)

总结

  1. 单一全局互斥锁(Sched.Lock)和集中状态存储
    • G 被分成全局队列和 P 的本地队列,全局队列依旧是全局锁,但是使用场景明显很少,P 本地队列使用无锁队列,使用原子操作来面对可能的并发场景。
  2. Goroutine 传递问题
    • G 创建时就在 P 的本地队列,可以避免在 G 之间传递(窃取除外),G 对 P 的数据局部性好; 当 G 开始执行了,系统调用返回后 M 会尝试获取可用 P,获取到了的话可以避免在 M 之间传递。而且优先获取调用阻塞前的 P,所以 G 对 M 数据局部性好,G 对 P 的数据局部性也好。
  3. Per-M 持有内存缓存 (M.mcache)
    • 内存 mcache 只存在 P 结构中,P 最多只有 GOMAXPROCS 个,远小于 M 的个数,所以内存没有过多的消耗。

计算机组成原理笔记

Posted on 2021-03-19

基本组成

  1. 冯·诺伊曼体系结构:运算器、控制器、存储器、输入输出设备
  2. 计算机性能:
    • CPU主频
    • 响应时间:
    • 吞吐率
    • 程序的 CPU 执行时间 = 指令数×CPI(每条指令的平均时钟周期数)×Clock Cycle Time(时钟周期时间/主频)
  3. 计算机功耗
    • 散热
    • 能耗/电力

指令运算

计算机指令

机器码

  1. 不同的 CPU (Intel 的 CPU/ ARM 的 CPU)支持不同的计算机指令集。
  2. 编译–汇编–机器码(指令)
  3. 指令类型:
    • 算术类指令
    • 数据传输类指令
    • 逻辑类指令
    • 条件分支类指令
    • 无条件跳转
  4. 寄存器
    • PC 寄存器(Program Counter Register),我们也叫指令地址寄存器
    • 指令寄存器(Instruction Register),用来存放当前正在执行的指令
    • 条件码寄存器(Status Register),用里面的一个一个标记位(Flag),存放 CPU 进行算术或者逻辑计算的结果
    • 通用寄存器
  5. 程序运行:CPU 会根据 PC 寄存器里的地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。可以看到,一个程序的一条条指令,在内存里面是连续保存的,也会一条条顺序加载。
  6. 程序栈:
    • 栈帧:整个函数 A 所占用的所有内存空间,就是函数 A 的栈帧(Stack Frame)
    • 压栈/出栈:push rbp 这个指令,就是在进行压栈。这里的 rbp 又叫栈帧指针(Frame Pointer),是一个存放了当前栈帧位置的寄存器。push rbp 就把之前调用函数,也就是 main 函数的栈帧的栈底地址,压到栈顶。mov rbp, rsp 里,则是把 rsp 这个栈指针(Stack Pointer)的值复制到 rbp 里,而 rsp 始终会指向栈顶
    • 函数内联:内联带来的优化是,CPU 需要执行的指令数变少了,根据地址跳转的过程不需要了,压栈和出栈的过程也不用了。

程序执行

  1. 不同操作系统下可执行文件的格式不一样。Linux 下的 ELF 文件格式,而 Windows 的可执行文件格式是一种叫作 PE(Portable Executable Format)的文件格式。
  2. 装载器:把对应的指令和数据加载到内存:
    • 虚拟地址空间 映射 物理内存空间
    • 分段:找出一段连续的物理内存和虚拟内存地址进行映射的方法
    • 分段引起的内存碎片:内存交换,但每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上,是一个很慢的行为。
    • 优化内存碎片:内存分页,和分段这样分配一整段连续的空间给到程序相比,分页是把整个物理内存空间切成一段段固定尺寸的大小。在 Linux 下,我们通常只设置成 4KB。
    • 内存分页好处:
      • 内存交换是以页为单位
      • 在加载程序的时候,不再需要一次性都把程序加载到物理内存中。当要读取特定的页,却发现数据并没有加载到物理内存里的时候,就会触发一个来自于 CPU 的缺页错误(Page Fault)。操作系统捕捉到这个错误,然后将对应的页,从存放在硬盘上的虚拟内存里读取出来,加载到物理内存里。
  3. 动态链接、静态链接
  4. 字符集(Unicode / ASCII)、字符编码(UTF-8、UTF-16、UTF-32)
  5. 门电路:创建 CPU 和内存的基本逻辑单元。我们的各种对于计算机二进制的“0”和“1”的操作,其实就是来自于门电路,叫作组合逻辑电路。
  6. 浮点数:随着指数位 e 的值的不同,小数点的位置也在变动
    • 精度丢失:先对齐、再计算,其中指数位较小的数,需要在有效位进行右移,在右移的过程中,最右侧的有效位就被丢弃掉了。

处理器设计

  1. 指令周期:Fetch - Decode - Execute
  2. 一个指令周期,包含多个 CPU 周期(从内存里面读取一条指令的最短时间),而一个 CPU 周期包含多个时钟周期(CPU 的主频是由一个晶体振荡器来实现的,而这个晶体振荡器生成的电路信号,就是我们的时钟信号)。
  3. 所有 CPU 支持的指令,都会在控制器里面,被解析成不同的输出信号
  4. Cpu四种基本电路:
    • ALU 这样的组合逻辑电路
    • 用来存储数据的锁存器和 D 触发器电路
    • 用来实现 PC 寄存器的计数器电路
    • 以及用来解码和寻址的译码器电路。
  5. CPU工作流程:
    
  6. 五级流水线:取指令(IF)- 指令译码(ID)- 指令执行(EX)- 内存访问(MEM)- 数据写回(WB)。保障一个最复杂的流水线级的操作,在一个时钟周期内完成就好了
  7. 流水线技术并不能缩短单条指令的响应时间这个性能指标,但是可以增加在运行很多条指令时候的吞吐率

Cpu冒险

  1. 结构冒险:来自于同一时钟周期不同的指令,要访问相同的硬件资源。
    • 解决:现代的 CPU 虽然没有在内存层面进行对应的拆分,却在 CPU 内部的高速缓存部分进行了区分,把高速缓存分成了指令缓存(Instruction Cache)和数据缓存(Data Cache)两部,使得我们的 CPU 在进行数据访问和取指令的时候,不会再发生资源冲突的问题了。
  2. 数据冒险:数据之间各种依赖。
    • 解决:操作数前堆:通过在硬件层面制造一条旁路,让一条指令的计算结果,可以直接传输给下一条指令
  3. 解决流水线阻塞:乱序执行:在指令执行的阶段通过一个类似线程池的保留站,让系统自己去动态调度先执行哪些指令,等到在指令结果的最终提交的阶段,再通过重排序的方式。
  4. 控制冒险:动态分支预测:一级分支预测,或者叫 1 比特饱和计数的方法。其实就是认为,预测结果和上一次的条件跳转是一致的。

Cpu并行

  1. 超线程,其实是一个“线程级并行”的解决方案。它通过让一个物理 CPU 核心,“装作”两个逻辑层面的 CPU 核心,使得 CPU 可以同时运行两个不同线程的指令。
  2. SIMD 技术,则是一种“指令级并行”的加速方案,或者我们可以说,它是一种“数据并行”的加速方案,比如numpy

Cpu异常

  1. 处理流程:处理流程,也就是上面所说的,“保存现场、异常代码查询、异常处理程序调用”
  2. 上下文切换:除了普通需要压栈的数据外,计算机还需要把所有寄存器信息都存储到栈里面去
    • 除了本来程序压栈要做的事情之外,我们还需要把 CPU 内当前运行程序用到的所有寄存器,都放到栈里面
    • 像陷阱这样的异常,涉及程序指令在用户态和内核态之间的切换。对应压栈的时候,对应的数据是压到内核栈里,而不是程序栈里
    • 像故障这样的异常,在异常处理程序执行完成之后。从栈里返回出来,继续执行的不是顺序的下一条指令,而是故障发生的当前指令
  3. 硬中断即网卡利用信号“告知”CPU有包到来,CPU执行中断向量对应的处理程序,即收到的包拷贝到计算机的内存。然后“通知”软中断有任务需要处理,中断处理程序返回。

CISI & RISC

  1. RISC 的通过减少 CPI 来提升性能,而 CISC 通过减少需要的指令数来提升性能。
  2. 通过指令译码器和 L0 缓存的组合,使得这些指令可以快速翻译成 RISC 风格的微指令

存储与I/O

  1. 各个存储器只和相邻的一层存储器打交道,并且随着一层层向下,存储器的容量逐层增大,访问速度逐层变慢,而单位存储成本也逐层下降,也就构成了我们日常所说的存储器层次结构。
  2. 优化思路—局部性原理:
    • 时间局部性:如果一个数据被访问了,那么它在短时间内还会被再次访问
    • 如果一个数据被访问了,那么和它相邻的数据也很快会被访问
  3. Cache Line(缓存块/缓存行):CPU 从内存中读取数据到 CPU Cache 的过程中,是一小块一小块来读取数据的,64 位 Intel CPU 的计算机中大小通常是 64 字节。
  4. 一个内存的访问地址,最终包括高位代表的组标记、低位代表的索引,以及在对应的 Data Block 中定位对应字的位置偏移量。
  5. 直接映射法:
    • 根据内存地址的低位,计算在 Cache 中的索引;
    • 判断有效位,确认 Cache 中的数据是有效的;
    • 对比内存访问地址的高位,和 Cache 中的组标记,确认 Cache 中的数据就是我们要访问的内存数据,从 Cache Line 中读取到对应的数据块(Data Block);
    • 根据内存地址的 Offset 位,从 Data Block 中,读取希望读取到的字(Word)。

      一致性

  6. 写入策略
    • 写直达:写入前,如果数据已经在 Cache 里面了,我们先把数据写入更新到 Cache 里面,再写入到主内存里面;如果数据不在 Cache 里,我们就只更新主内存
    • 写回:果发现我们要写入的数据,就在 CPU Cache 里面,那么我们就只是更新 CPU Cache 里面的数据。同时,我们会标记 CPU Cache 里的这个 Block 是脏(Dirty)的(与主存不一致)。如果Cache Block已经是脏的,先将脏数据写回主存,再将新数据写入Cache Block
  7. 实现一致性需要满足条件:写传播、事务串行化
  8. 总线嗅探
  9. MESI协议:
    • Cache Line 的四个不同的标记
      • M:代表已修改(Modified):Cache Block 里面的内容我们已经更新过了,但是还没有写回到主内存里面
      • E:代表独占(Exclusive):干净的,在独占状态下,对应的 Cache Line 只加载到了当前 CPU 核所拥有的 Cache 里。
      • S:代表共享(Shared)::干净的,在独占状态下的数据,如果收到了一个来自于总线的读取对应缓存的请求,它就会变成共享状态。
      • I:代表已失效(Invalidated):Cache Block 里面的数据已经失效了
    • RFO(request for ownership):在共享状态下,当我们想要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他 CPU 核心里面的 Cache,都变成无效的状态,然后再更新当前 Cache 里面的数据。
  10. False Sharing:
    • A B 两个对象在内存上被连续的创建在一起,假若这两个对象都很小,小于一个 Cache Line 大小,那他们可能会共用同一个 Cache Line。如果再有两个线程 Thread 1 和 Thread 2 会去操作这两个对象,我们从代码角度保证 Thread 1 只会操作 A,而 Thread2 只会操作 B。那按道理这两个 Thread 访问 A B 不该有相互影响,都能并行操作。但现在因为它俩刚好在同一个 Cache Line 内,就会出现 A B 对象所在 Cache Line 在两个 CPU 上来回搬迁的问题。
  11. 内存屏障:写屏障是一种机制,用于强制执行对计算机系统中存储系统的一系列写入操作。
    • Write Barrier:其作用是将 Write Barrier 之前所有操作的 Cache Line 都打上标记,Barrier 之后的写入操作不能直接操作 Cache Line 而也要先写 Store Buffer 去,但不用打特殊标记。等 Store Buffer 内带着标记的写入因为收到 Invalidate Ack 而能写 Cache Line 后,这些没有打标记的写入操作才能写入 Cache Line。
    • Read Barrier:CPU 看到 Read Barrier 只是标记 Invalidate Queue 上的 Cache Line,之后继续执行别的指令,直到看到下一个 Load 操作要从 Cache Line 里读数据了,CPU 才会等待 Invalidate Queue 内所有刚才被标记的 Cache Line 都处理完才继续执行下一个 Load。
    • https://nextfe.com/memory-barrier/

虚拟内存

  1. 内存需要被分成固定大小的页(Page),然后再通过虚拟内存地址(Virtual Address)到物理内存地址(Physical Address)的地址转换(Address Translation),才能到达实际存放数据的物理内存位置
  2. 页表
    • 一个内存地址分成页号(Directory)和偏移量(Offset)两个部分。映射表需要保留虚拟内存地址的页号和物理内存地址的页号之间的映射关系就可以了
    • 多级页表:多级页表就像是一颗树。因为一个进程的内存地址相对集中和连续,所以采用这种页表树的方式,可以大大节省页表所需要的空间
    • 多级页表节约内存:https://www.polarxiong.com/archives/多级页表如何节约内存.html
  3. TLB,全称是地址变换高速缓冲(Translation-Lookaside Buffer)。这块缓存存放了之前已经进行过地址转换的查询结果。
  4. 在 CPU 芯片里面,我们封装了内存管理单元(MMU,Memory Management Unit)芯片,用来完成地址转换。
  5. 内存保护措施:可执行空间保护,地址空间布局随机化。

总线

  1. CPU 的双独立总线
    • 本地总线(Local Bus):与高速缓存通信
    • 速度相对较慢的前端总线(Front-side Bus):与主内存以及输入输出设备通信的
  2. 总线不能同时给多个设备提供通信功能

IO wait

  1. 如何查看:
    • top :%cpu wa指标:CPU 等待 IO 完成操作花费的时间占 CPU 的百分比
    • iostat:tps 指标,对应硬盘的 IOPS 性能。而 kB_read/s 和 kB_wrtn/s 指标,对应着我们的数据传输率
    • iotop: 查看io占用进程
  2. IOPS 和 DTR(Data Transfer Rate,数据传输率)才是输入输出性能的核心指标。
  3. https://louwrentius.com/understanding-iops-latency-and-storage-performance.html

硬盘

  1. 机械硬盘的硬件,主要由盘面、磁头和悬臂三部分组成。实际的一次对于硬盘的访问,需要把盘面旋转到某一个“几何扇区”,对准悬臂的位置。然后,悬臂通过寻道,把磁头放到我们实际要读取的扇区上。
  2. 随机数据的访问速度= 旋转盘面的平均延时 + 移动悬臂的寻道时间
  3. 7200 转机械硬盘的 IOPS,只能做到 100 左
  4. SSD 的物理原理,也就是“电容 + 电压计”的组合。适合读多写少的场景。SSD寿命取决于擦除的次数。
  5. FTL 这个映射层,使得操作系统无需关心物理块的擦写次数,而是由 FTL 里的软件算法,来协调到底每一次写入应该磨损哪一块。
  6. SSD 这个需要进行垃圾回收的特性,使得我们在写入数据的时候,会遇到写入放大
  7. DMA:DMA 技术就是我们在主板上放一块独立的芯片。在进行内存和 I/O 设备的数据传输的时候,我们不再通过 CPU 来控制数据传输,而直接通过 DMA 控制器(DMA Controller,简称 DMAC)
  8. 只有主设备(CPU)可以发起数据传输,如果从设备(I/O设备)需要发起数据传输,只能发送一个控制信号到主设备。
    • 而DMAC是主设备也是从设备,由CPU通知DMAC源地址的初始值以及传输时候的地址增减方式、目标地址初始值和传输时候的地址增减方式、要传输的数据长度,由DMAC搬运数据。
  9. Kafka的零拷贝:Kafka 的代码调用了 Java NIO 库,具体是 FileChannel 里面的 transferTo 方法。我们的数据并没有读到中间的应用内存里面,而是直接通过 Channel,写入到对应的网络设备里。并且,对于 Socket 的操作,也不是写入到 Socket 的 Buffer 里面,而是直接根据描述符(Descriptor)写入到网卡的缓冲区里面。
    • 通过 DMA,从硬盘直接读到操作系统内核的读缓冲区里面。
    • 根据 Socket 的描述符信息,直接从读缓冲区里面,写入到网卡的缓冲区里面。
    • http://notes.stephenholiday.com/Kafka.pdf

应用

Disruptor 消息队列

  1. 利用Cache Line:Disruptor 很取巧地在需要频繁高速访问的变量,也就是 RingBufferFields 里面的 indexMask 这些字段前后,各定义了 7 个没有任何作用和读写请求的 long 类型的变量。这样,无论在内存的什么位置上,这些变量所在的 Cache Line 都不会有任何写更新的请求。我们就可以始终在 Cache Line 里面读到它的值,而不需要从内存里面去读取数据,也就大大加速了 Disruptor 的性能。
  2. 利用数组而不是链表:RingBuffer 底层的数据结构则是一个固定长度的数组。这个数组不仅让我们更容易用好 CPU Cache,对 CPU 执行过程中的分支预测也非常有利。
  3. 无锁队列:通过 CAS 这样的操作,去进行序号的自增和对比,使得 CPU 不需要获取操作系统的锁。因为单指令是原子性操作。

从一次kswapd0占用cpu过高问题讲起

Posted on 2020-10-26
最近在线上机器碰到kswapd0占用cpu过高(1100%),几乎吃满的所有cpu,所以这台机器其他服务都异常退出了。借此机会学习一下。
  1. 问题1: free -m 查看mem free是真的内存耗完了吗?buffer/cache是什么?
    查看free -m 命令。

    • Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。
    • Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。SReclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。
    • Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中。
    • 因此,free中看到的buffer/cache,实际也是可用的,一般看available是比较准确的可用内存值。
  2. 什么是swap?为什么会swap?

    • swap space是磁盘上的一块区域,可以是一个分区,也可以是一个文件,或者是他们的组合。简单点说,当系统物理内存吃紧时,Linux会将内存中不常访问的数据保存到swap上,这样系统就有更多的物理内存为各个进程服务,而当系统需要访问swap上存储的内容时,再将swap上的数据加载到内存中,这就是我们常说的swap out和swap in。
  3. kswapd0是什么?

    • kswapd0进程的作用:它是虚拟内存管理中,负责换页的,操作系统每过一定时间就会唤醒kswapd ,看看内存是否紧张,如果不紧张,则睡眠,在 kswapd 中,有2 个阀值,pages_hige 和 pages_low,当空闲内存页的数量低于 pages_low 的时候,kswapd进程就会扫描内存并且每次释放出32 个free pages,直到 free page 的数量到达pages_high。通过阻止kswapd0进程过渡活跃地消耗CPU的方法是设置大页内存。
  4. 第一次尝试

    • 调整/proc/sys/vm/swappiness 参数,默认60,调整为10,表示内存剩下10%的时候才触发swap。
    • 但是遗留了一个问题:为什么kswapd0进程一直驻留,但从vmstat看si so 为0?为什么这个进程的cpu是user使用不是sys?
  5. 继续查找:

    • 后来找到了一篇文章《kswapd0挖矿病毒查杀过程》,通过netstat -antlp 发现了一个同样荷兰的ip 45.9.148.99通过rsync在/root/.configrc/a 植入了kswapd0文件。初步判断是这个假装swap的进程其实值挖矿病毒。
  6. 解决:

    • kill调病毒进程。
    • 删除可疑文件。
    • 删除病毒创建的计划任务。
  1. 参考连接:
    • kswapd0挖矿病毒查杀过程
    • Linux交换空间(swap space)
    • 内存篇 | 02 | 怎么理解内存中的Buffer和Cache?

Redis笔记

Posted on 2020-09-24

[TOC]

Redis模块组成

Redis数据结构(String,List,Hash,Set,SortSet,BitMap,HyperLogLog,GEO)

  1. Redis 使用了一个哈希表来保存所有键值对。哈希表的每一项是一个 dictEntry 的结构体。dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节。key、value都是用一个 RedisObject 结构体来统一记录这些元数据,同时指向实际数据指针。
  2. Redis 解决哈希冲突的方式,就是链式哈希。
  3. 渐进式 rehash:
    1. 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
    2. 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
    3. 释放哈希表 1 的空间
  4. 压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来,对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有 O(1)
  5. String类型以SDS结构体存储,包括三部分:buf–字节数组,保存实际数据。len:占 4 个字节,表示 buf 的已用长度。alloc:也占个 4 字节,表示 buf 的实际分配长度。
  6. 内存分配上:jemalloc 在分配内存时,会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,这样可以减少频繁分配的次数。
  7. hash类型使用压缩列表还是哈希表作为低层数据结构,主要取决与:
    1. hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。
    2. hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。
  8. 集合计算
  9. 时序序列:hash、sortset、 RedisTimeSeries
  10. 事务:使用MULTI和EXEC命令时,建议客户端使用pipeline,当使用pipeline时,客户端会把命令一次性批量发送给服务端。

IO模型

  1. Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果
  2. select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。
  3. 性能瓶颈主要包括2个方面:
    1. 任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
      • 操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey、过期释放内存同样会产生耗时;
      • 使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;
      • 大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;
      • 淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;
      • AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;
      • 主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;
    2. 并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。

日志

AOF

  1. Redis 是先执行命令,把数据写入内存,然后才记录日志。
  2. AOF 日志也是在主线程中执行的。策略
    1. Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘,主线程调用fsync操作完成。
    2. Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;使用后台的子线程异步完成 fsync 的操作。
    3. No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
    
  3. AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件。重写过程是由子进程 bgrewriteaof 来完成的。
  4. 重写的过程总结为“一个拷贝,两处日志”:主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
  5. AOF重写子进程写入量大->fsync线程阻塞->主线程阻塞

RDB

  1. Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照。策略:
    1. save:在主线程中执行,会导致阻塞;
    2. bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。
  2. Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

主从模式

  1. 步骤:
    1. 从库给主库发送 psync 命令,表示要进行数据同步。psync 命令包含了主库的 runID 和复制进度 offset。
    2. 主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset。
    3. 主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。
    4. 只要有从库存在,主库会记录 RDB 文件生成后收到的所有写操作到环形缓存区repl_backlog_buffer, 如果主库与从库建立了连接,写入client buffer:replication buffer中。当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库。
  2. 通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。
  3. 长连接复制是主从库正常运行后的常规同步阶段。在这个阶段中,主从库之间通过命令传播实现同步。
  4. 主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前对于环形缓存区real_backlog_buffer的 slave_repl_offset 发给主库,主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。
    1. 如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。
    2. 可以调整repl_backlog_size 这个参数。这个参数和所需的缓冲空间大小有关。缓冲空间的计算公式是:缓冲空间大小 = 主库写入命令速度 操作大小 - 主从库间网络传输命令速度 操作大小。

哨兵机制

  1. 流程
    1. 监控:监控是指哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。
    2. 选主:主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库。哨兵集群在判断了主库“客观下线”后,经过投票仲裁,选举一个 Leader 出来,由它负责实际的主从切换。
    3. 通知:在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。
  2. 主观下线:哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。
  3. 客观下线:当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”。
  4. 选主方法:
    1. 筛选:1. 检查从库的当前在线状态;2. 在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好。
    2. 打分:进行三轮打分,这三个规则分别是从库优先级(slave-priority)、从库复制进度(slave_repl_offset)以及从库 ID 号(ID 号小的从库得分高)。只要在某一轮中,有从库得分最高,那么它就是主库了。
  5. 哨兵集群发现:哨兵和主库建立起了连接,就可以在主库上PUB/SUB,当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP 地址和端口。
  6. 哨兵与从库建立连接:哨兵向主库发送 INFO 命令,主库把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接。
  7. 哨兵与客户端通讯:
  8. 要保证所有哨兵实例的配置是一致的,尤其是主观下线的判断值 down-after-milliseconds

切片集群

  1. 部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上。例如,如果集群中有 N 个实例,那么,每个实例上的槽个数为 16384/N 个。
  2. 在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。
  3. Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例。客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。
  4. 重定向
    1. 原因:实例数量变动;负载均衡
    2. 步骤:当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,那么,这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。客户端更新缓存。
    3. 迁移部分完成:客户端就会收到一条 ASK 报错信息,返回最新实例地址,客户端需要给新实例发送 ASKING 命令,然后再发送操作命令。ASK 命令并不会更新客户端缓存的哈希槽分配信息。

消息队列

  1. List用作队列时,为了保证消息可靠性,使用BRPOPLPUSH命令把消息取出的同时,还把消息插入到备份队列中,从而防止消费者故障导致消息丢失。每次执行BRPOPLPUSH命令后,当消费者成功消费取出的消息后,最好把备份队列中的消息删除,防止备份队列存储过多无用的数据。
  2. 丢消息情况及处理:
    1. 生产者在发布消息时异常:
      a) 网络故障或其他问题导致发布失败(直接返回错误,消息根本没发出去)
      b) 网络抖动导致发布超时(可能发送数据包成功,但读取响应结果超时了,不知道结果如何)
      消费者需要保证在收到重复消息时,依旧能保证业务的正确性(设计幂等逻辑)。
    2. 消费者在处理消息时异常:Streams则是采用ack的方式,消费成功后告知中间件。
    3. 消息队列中间件丢失消息: 在用Redis当作队列或存储数据时,是有可能丢失数据的:一个场景是,如果打开AOF并且是每秒写盘,因为这个写盘过程是异步的,Redis宕机时会丢失1秒的数据。而如果AOF改为同步写盘,那么写入性能会下降。另一个场景是,如果采用主从集群,如果写入量比较大,从库同步存在延迟,此时进行主从切换,也存在丢失数据的可能。总的来说,Redis不保证严格的数据完整性和主从切换时的一致性。我们在使用Redis时需要注意。

性能

  1. 影响因素:
    • Redis 内部的阻塞式操作;
    • CPU 核和 NUMA 架构的影响;
    • Redis 关键系统配置;
    • Redis 内存碎片;
    • Redis 缓冲区。
  2. 阻塞点:
    • 集合全量查询和聚合操作;
    • bigkey 删除;删除操作的本质是要释放键值对占用的内存空间,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加。
    • 清空数据库;
    • AOF 日志同步写;
    • 从库加载 RDB 文件。
  3. Redis4.0以后,Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别由它们负责 AOF 日志写操作、键值对删除(lazy free)以及文件关闭的异步执行。
  4. bigkey 删除时,建议:先使用集合类型提供的 SCAN 命令读取数据,然后再进行删除。
  5. 在多核 CPU 架构下,Redis 如果在不同的核上运行,就需要频繁地进行上下文切换,这个过程会增加 Redis 的执行时间,客户端也会观察到较高的尾延迟了。所以,建议在 Redis 运行时,把实例和某个核绑定。为了提升 Redis 的网络性能,我们有时还会把网络中断处理程序和 CPU 核绑定。
  6. 过期删除规则:默认情况下,Redis 每 100 毫秒会删除一些过期 key,具体的算法如下:
    1. 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个数的 key,并将其中过期的 key 全部删除;
    2. 如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下。
  7. Redis基线性能:redis-cli 命令提供了–intrinsic-latency 选项,可以用来监测和统计测试期间内的最大延迟。
  8. AOF重写子进程写入量大->fsync线程阻塞->主线程阻塞
  9. 操作系统的内存 swap(物理机器内存不足)
  10. 内存大页(支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行):Redis在写时复制时,如果采用了内存大页,那么,即使客户端请求只修改 100B 的数据,Redis 也需要拷贝 2MB 的大页。
  11. 内存碎片:
    1. 内因:内存分配器的分配策略就决定了操作系统无法做到“按需分配”。这是因为,内存分配器一般是按固定大小来分配内存,而不是完全按照应用程序申请的内存空间大小给程序分配。
    2. 外因:键值对大小不一样和删改操作
    3. 查看:mem_fragmentation_ratio 的指标表示的就是 Redis 当前的内存碎片率。大于 1 但小于 1.5是合理的,过高表示碎片过高,过低表示wu里内存不足,导致swap。
    4. 碎片清理:Redis 需要启用自动内存碎片清理,可以把 activedefrag 配置项设置为 yes。

缓冲区

  1. AOF 缓冲区(由定时任务写入文件)、AOF重写缓冲区(由子进程写入文件)
    1. 在子进程进行AOF重写期间,Redis主进程执行的命令会被保存在AOF重写缓冲区里面
    2. 当子进程完成AOF重写工作之后,父进程将AOF重写缓冲区中的所有内容写入到新的AOF文件中,对新的AOF文件进行改名、覆盖旧文件
    3. no-appendfsync-on-rewrite设置为yes(表示在日志重写时,不进行命令追加操作,而只是将命令放在重写缓冲区里,避免与命令的追加造成磁盘IO上的冲突)
  2. 客户端输入和输出缓冲区
    1. CLIENT LIST: qbuf,表示输入缓冲区已经使用的大小; qbuf-free,表示输入缓冲区尚未使用的大小
    2. 如果使用多个客户端,导致 Redis 内存占用过大,也会导致内存溢出(out-of-memory)问题
    3. 缓冲区溢出:
      • 服务器端返回 bigkey 的大量结果;
      • 执行了 MONITOR 命令;
      • 缓冲区大小设置得不合理 client-output-buffer-limit。
  3. 复制缓冲区、复制积压缓冲区
    1. 主节点上复制缓冲区的内存开销,会是每个从节点客户端输出缓冲区占用内存的总和。如果集群中的从节点数非常多的话,主节点的内存开销就会非常大。

缓存异常

缓存不一致

  1. 问题1: 删除缓存值或更新数据库失败而导致数据不一致。使用重试机制确保删除或更新操作成功。
  2. 问题2: 在删除缓存值、更新数据库的这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值
    1. 例子:先删除缓存,再更新数据库。A 删除缓存值后,还没有来得及更新数据库(比如说有网络延迟),线程 B 就开始读取数据了,那么这个时候,线程 B 会发现缓存缺失,就只能去数据库读取,如果读取到旧值,就把旧值写入回缓存了
    2. 解决:延迟双删:在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作(为了删线程B写的缓存)。
    3. 先更新数据库值,再删除缓存值。其他线程可能读取到旧值,但影响较少。

缓存雪崩

  1. 现象:缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。
  2. 原因1 :缓存中有大量数据同时过期。
    • 给数据的过期时间增加一个较小的随机
    • 服务降级:如果业务应用访问的是非核心数据时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息;
  3. 原因2 :Redis 缓存实例发生故障宕机
    • 业务系统中实现服务熔断或请求限流机制;熔断时缓存客户端并不把请求发给 Redis 缓存实例,而是直接返回,减少数据库的压力。
    • 事前预防:通过主从节点的方式构建 Redis 缓存高可靠集群

缓存击穿

  1. 现象:热点数据过期失效时,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。
  2. 解决:
    • 不设置过期时间
    • 使用锁

缓存穿透

  1. 现象:缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。
  2. 解决:
    • 缓存空值或缺省值
    • 使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。
      • 步骤:
        • 首先,使用 N 个哈希函数,分别计算这个数据的哈希值,得到 N 个哈希值。
        • 然后,我们把这 N 个哈希值对 bit 数组的长度取模,得到每个哈希值在数组中的对应位置。
        • 最后,我们把对应位置的 bit 位设置为 1,这就完成了在布隆过滤器中标记数据的操作。
      • 问题:Redis布隆过滤器是使用String类型实现的,存储的方式是一个bigkey,建议使用时单独部署一个实例,专门存放布隆过滤器的数据,不要和业务数据混用,否则在集群环境下,数据迁移时会导致Redis阻塞问题
    • 前端进行请求检测,过滤恶意请求

缓存淘汰策略

  1. noeviction:不淘汰
  2. random:随机选择数据进行淘汰
  3. ttl:剩余存活时间最短的数据就会被淘汰出缓存
  4. LRU:只考虑数据的访问时效
    1. Redis 是用 RedisObject 结构来保存数据的,RedisObject 结构中设置了一个 lru 字段,用来记录数据的访问时间戳;
    2. Redis 并没有为所有的数据维护一个全局的链表,而是通过随机采样方式,选取一定数量(例如 10 个)的数据放入候选集合,后续在候选集合中根据 lru 字段值的大小进行筛选。
  5. LFU:首先会筛选并淘汰访问次数少的数据,然后针对访问次数相同的数据,再筛选并淘汰访问时间最久远的数据
    1. 把原来24bit大小的lru字段,又进一步拆分成了两部分。前 16bit,表示数据的访问时间戳,后 8bit,表示数据的访问次数。
    2. 首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。
    3. 计数规则:非线性计数法;counter值衰减机制,解决有些数据在短时间内被大量访问后就不会再被访问的问题

golang中实现np.frombuffer

Posted on 2020-09-09

最近项目全部从python转golang了,之前读图片从缓存读算法存储的buffer再用opencv转,在golang下也要实现相同的功能。
还遗留一个问题:binary.Read传的数组长度不能从变量获得

python代码:

1
2
3
4
img = np.frombuffer(imgBuffer, dtype=np.uint8).reshape(data["shape"])
img = img[:,:,::-1]
newImg = Image.fromarray(img)
newImg.save(filepath)

golang代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   width := 1080
height := 1920
var imgArr [1080][1920][3]uint8 // how to gener array from width&height?
buf := bytes.NewReader([]byte(imgByte))
err := binary.Read(buf, binary.LittleEndian, &imgArr)
if err != nil {
return "", err
}

img := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{height, width}})
for x := 0; x < width; x++ {
for y := 0; y < height; y++ {
cyan := color.RGBA{imgArr[x][y][0], imgArr[x][y][1], imgArr[x][y][2], 0xff}
img.Set(y, x, cyan)
}
}
enc := &png.Encoder{
CompressionLevel: png.NoCompression,
}
f, err := os.Create(filepath)
defer f.Close()
err = enc.Encode(f, img)

《趣谈网路协议》笔记

Posted on 2020-04-21

基础

  1. 概览
  2. 只要是在网络上跑的包,都是完整的。可以有下层没上层,绝对不可能有上层没下层。
  3. IP 是地址,有定位功能;MAC 是身份证,无定位功能,从硬件角度,保证不同的网卡有不同的标识,通信范围局限在一个子网。
  4. 在 IP 地址的后面有个 scope,对于 eth0 这张网卡来讲,是 global,说明这张网卡是可以对外的,可以接收来自各个地方的包。对于 lo 来讲,是 host,说明这张网卡仅仅可以供本机相互通信。
  5. DHCP 协议主要是用来给客户租用 IP 地址,和房产中介很像,要商谈、签约、续租,广播还不能“抢单”;DHCP 协议能给客户推荐“装修队”PXE,能够安装操作系统,这个在云计算领域大有用处。

底层网络协议

  1. MAC 层是用来解决多路访问的堵车问题的;
  2. ARP 是通过广播的方式来寻找目标 MAC 地址的,广播完之后记住一段时间,这个叫作缓存;
  3. 交换机是有 MAC 地址学习能力的,结果记录在转发表。
  4. 如果 MAC 层定义了本地局域网的传输行为,IP 层定义了整个网络端到端的传输行为,这两层基本定义了这样的基因:网络传输是以包为单位的,二层叫帧,网络层叫包,传输层叫段。我们笼统地称为包。包单独传输,自行选路,在不同的设备封装解封装,不保证到达。基于这个基因,生下来的孩子 UDP 完全继承了这些特性,几乎没有自己的思想。

    TCP

  5. 三次握手
  6. 四次挥手 等待的时间设为 2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间
  7. TCP 协议使用的也是同样的模式。为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)。
  8. 发送端缓存:
  9. 接收端缓存

如何实现一个异步io框架

Posted on 2020-04-20

网卡接收到数据以后

网卡的基础知识

网卡本身是有内存的,每个网卡一般都有4k以上的内存,用来发送、接受数据。数据从主内存搬到网卡之后,不是立即就能被发送出去的,而是要先在网卡自身的内存中排队,再按先后顺序发送,同样的,数据从以太网传递到网卡时,网卡也是先把数据存储到自身的内存中,等到收到一帧数据了,再经过中断的方式,告诉CPU把网卡内存的数据读走(现在网卡大都支持DMA方式直接从网卡内存拷贝被内核内存),而读走后的内存,又被清空,再次被用来接收新的数据。

流程

  1. 数据包从外面的网络进入物理网卡。如果目的地址不是该网卡,且该网卡没有开启混杂模式,该包会被网卡丢弃。
  2. 网卡将数据包通过DMA的方式写入到指定的内存地址,该地址由网卡驱动分配并初始化。
  3. 网卡通过硬件中断(IRQ)通知CPU,告诉它有数据来了
  4. CPU根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC Driver)中相应的函数
  5. 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知CPU了,这样可以提高效率,避免CPU不停的被中断。
  6. 启动软中断。这步结束后,硬件中断处理函数就结束返回了。由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致CPU没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。
  7. 经过TCP/IP协议逐层处理。
  8. 应用程序通过read()从socket buffer读取数据。

socket详细流程

tcp三次握手

setblocking

backlog

syn,accept队列

accept()创建新套接字

协议栈是什么

粘包是个伪命题

buffer缓冲区大小

IO阻塞本质

读缓存,写缓冲区

send,recv原理

满加锁,空解锁

tcp ack滑动窗口

如何确定recv结构是完整的

长链接、短连接区别

socket维护长短连接手段

keepalive心跳包谁维护

高并发模型

fork模型

进程池/线程池模型

io复用模型

惊群、饥饿

上下文

什么是上下文

什么是上下文切换

为什么要上下文切换

什么时候会上下文切换

执行单元

进程、线程、协程

堆、栈

抢占、协作

io

同步、异步、阻塞、非阻塞

fd

一个线程如何管理多个fd

io多路复用

select

poll

epoll

水平触发、边缘触发

跨平台

libevnet

libev

libuv

调度器组成

  1. eventLoop
  2. 类生成器
  3. Fd生成器的关系
  4. 信号处理
  5. 文件属性变化
  6. Io可读可写
  7. 定时器
  8. periodic
  9. timeout

prefork + epoll

asyncore实现异步rpc

Posted on 2020-04-16

SERVER 端

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
import json
import struct
import socket
import asyncore
from io import BytesIO


class RPCHandler(asyncore.dispatcher_with_send): # 客户套接字处理器必须继承 dispatcher_with_send

def __init__(self, sock, addr):
asyncore.dispatcher_with_send.__init__(self, sock=sock)
self.addr = addr
self.handlers = {
"ping": self.ping
}
self.rbuf = BytesIO() # 读缓冲区由用户代码维护,写缓冲区由 asyncore 内部提供


def handle_connect(self): # 新的连接被 accept 后回调方法
print(self.addr, 'comes')

def handle_close(self): # 连接关闭之前回调方法
print(self.addr, 'bye')
self.close()

def handle_read(self): # 有读事件到来时回调方法
while True:
content = self.recv(2)
# print('read 2', content)
if content:
self.rbuf.write(content)
if len(content) < 1024:
break
self.handle_rpc()

def handle_rpc(self): # 将读到的消息解包并处理
while True: # 可能一次性收到了多个请求消息,所以需要循环处理
self.rbuf.seek(0)
length_prefix = self.rbuf.read(4)
if len(length_prefix) < 4: # 不足一个消息
break
length = int.from_bytes(length_prefix, byteorder='big')
body = self.rbuf.read(length)
if len(body) < length: # 不足一个消息
break
try:
request = json.loads(body.decode("utf-8"))
except:
break
in_ = request['in']
params = request['params']
handler = self.handlers[in_]
handler(params) # 处理消息
left = self.rbuf.getvalue()[length + 4:] # 消息处理完了,缓冲区要截断
self.rbuf = BytesIO()
self.rbuf.write(left)
self.rbuf.seek(0, 2) # 将游标挪到文件结尾,以便后续读到的内容直接追加

def ping(self, params):
self.send_result("pong", params)

def send_result(self, out, result):
response = {"out": out, "result": result}
print("out",response)
body = json.dumps(response).encode("utf-8")
length_prefix = len(body).to_bytes(4, byteorder='big')
self.send(length_prefix) # 写入缓冲区
self.send(body) # 写入缓冲区


class RPCServer(asyncore.dispatcher): # 服务器套接字处理器必须继承 dispatcher

def __init__(self, host, port):
asyncore.dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind((host, port))
self.listen(1)

def handle_accept(self):
pair = self.accept()
if pair is not None:
sock, addr = pair
RPCHandler(sock, addr)

if __name__ == '__main__':
RPCServer("localhost", 8080)
asyncore.loop()

CLIENT 端

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
import socket
import sys
import os
import time
import json


def clientSocket():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# if sock < 0:
# print('socket error')

try:
sock.connect(("127.0.0.1",8080))
except:
print("exception")
sys.exit(1)

message = {"in":"ping","params":"sha params"}
message = json.dumps(message).encode('utf-8')
encode_bytes = len(message).to_bytes(4, byteorder='big') + message
sock.sendall(encode_bytes)

while True:
data = sock.recv(100)
print('received "%s"' %data)

sock.close()

if __name__ == "__main__":
clientSocket()

使用epoll实现
SERVER端

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
import socket
import select
import queue
import time

#创建socket对象
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#设置IP地址复用
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
#ip地址和端口号
server_address = ("127.0.0.1", 8080)
#绑定IP地址
serversocket.bind(server_address)
#监听,并设置最大连接数
serversocket.listen(10)
print("服务器启动成功,监听IP:" , server_address)
#服务端设置非阻塞
serversocket.setblocking(False)
#超时时间
timeout = 10
#创建epoll事件对象,后续要监控的事件添加到其中
epoll = select.epoll()
#注册服务器监听fd到等待读事件集合
epoll.register(serversocket.fileno(), select.EPOLLIN)
#保存连接客户端消息的字典,格式为{}
message_queues = {}
#文件句柄到所对应对象的字典,格式为{句柄:对象}
fd_to_socket = {serversocket.fileno():serversocket,}


def run():
while True:
print("等待活动连接......")
#轮询注册的事件集合,返回值为[(文件句柄,对应的事件),(...),....]
events = epoll.poll(timeout)
if not events:
print("epoll超时无活动连接,重新轮询......")
continue
print("有" , len(events), "个新事件,开始处理......")

for fd, event in events:
socket = fd_to_socket[fd]
#如果活动socket为当前服务器socket,表示有新连接
if socket == serversocket:
connection, address = serversocket.accept()
print("新连接:" , address)
#新连接socket设置为非阻塞
connection.setblocking(False)
#注册新连接fd到待读事件集合
epoll.register(connection.fileno(), select.EPOLLIN)
#把新连接的文件句柄以及对象保存到字典
fd_to_socket[connection.fileno()] = connection
#以新连接的对象为键值,值存储在队列中,保存每个连接的信息
message_queues[connection] = queue.Queue()
#关闭事件
elif event & select.EPOLLHUP:
print('client close')
#在epoll中注销客户端的文件句柄
epoll.unregister(fd)
#关闭客户端的文件句柄
fd_to_socket[fd].close()
#在字典中删除与已关闭客户端相关的信息
del fd_to_socket[fd]
#可读事件
elif event & select.EPOLLIN:
print('client in')
#接收数据
data = socket.recv(1024)
if data:
print("收到数据:" , data , "客户端:" , socket.getpeername())
#将数据放入对应客户端的字典
message_queues[socket].put(data)
#修改读取到消息的连接到等待写事件集合(即对应客户端收到消息后,再将其fd修改并加入写事件集合)
data_handler(fd)
# g1.join()
else:
#在epoll中注销客户端的文件句柄
epoll.unregister(fd)
#关闭客户端的文件句柄
fd_to_socket[fd].close()
#在字典中删除与已关闭客户端相关的信息
del fd_to_socket[fd]
#可写事件
elif event & select.EPOLLOUT:
print('client out')
try:
#从字典中获取对应客户端的信息
msg = message_queues[socket].get_nowait()
except queue.Empty:
print(socket.getpeername() , " queue empty")
#修改文件句柄为读事件
epoll.modify(fd, select.EPOLLIN)
else :
print("发送数据:" , data , "客户端:" , socket.getpeername())
#发送数据
socket.send(msg)
#在epoll中注销服务端文件句柄
epoll.unregister(serversocket.fileno())
#关闭epoll
epoll.close()
#关闭服务器socket
serversocket.close()


def data_handler(fd):
socket = fd_to_socket[fd]
msg = message_queues[socket].get_nowait()
if msg == b"ping":
message_queues[socket].put("ok.......pong!".encode())
epoll.modify(fd, select.EPOLLOUT)

if __name__ == "__main__":
run()

参考:

  • 【RPC-Python】单进程异步模型
  • 字节到大整数到解包与打包

《MYSQL实战45讲》笔记

Posted on 2020-03-31

基本

  1. 基本架构 
  2. Mysql长链接使内存使用涨得快:
    • 定期断开长连接。
    • 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源。
  1. WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘。redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。

    • 得益于:
      • redo log 和 binlog 都是顺序写,磁盘的顺序写比随机写速度要快;
      • 组提交机制,可以大幅度降低磁盘的 IOPS 消耗。
  2. change buffer:当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存。

    • 何时触发merge:访问这个数据页会;系统有后台线程会定期 merge;在数据库正常关闭(shutdown)的过程中。
  3. 脏页:当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。脏页什么时候引起flush写入磁盘:

    • InnoDB 的 redo log 写满了。这时候系统会停止所有更新操作,把 checkpoint 往前推进,redo log 留出空间可以继续写。
    • 系统内存不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的是“脏页”,就要先将脏页写到磁盘。
    • 系统“空闲”的时候。
    • MySQL 正常关闭的时候。
  4. InnoDB 刷脏页的控制策略:
    • innodb_io_capacity 参数会告诉 InnoDB 你的磁盘能力。这个值我建议你设置成磁盘的 IOPS。
    • innodb_max_dirty_pages_pct 是脏页比例上限,默认值是 75%。
    • 合理地设置 innodb_io_capacity 的值,并且平时要多关注脏页比例,不要让它经常接近 75%。
    • innodb_flush_neighbors=0,只刷自己脏页,不刷邻居。
  5. 如果你创建的表没有主键,或者把一个表的主键删掉了,那么 InnoDB 会自己生成一个长度为 6 字节的 rowid 来作为主键。
  6. join算法:
    1. Index Nested-Loop Join(NLJ):先遍历表 驱动表t1,然后根据从表 t1 中取出的每行数据中的 a 值,去被驱动表 t2 根据索引中查找满足条件的记录。
    2. Block Nested-Loop Join(BNL):把驱动表 t1 的数据读入线程内存 join_buffer 中,再扫描被驱动表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比(内存操作),满足 join 条件的,作为结果集的一部分返回。
      • join_buffer 的大小是由参数 join_buffer_size 设定的,默认值是 256k。
      • 如果放不下驱动表 t1 的所有数据话,策略就是分段放。
    3. Batched Key Access(BKA):NLJ的优化,把表 t1 的数据取出来一部分,先放到一个临时内存join_buffer。在用BNL算法处理。
    4. 如果可以使用 Index Nested-Loop Join 算法,也就是说可以用上被驱动表上的索引,其实是没问题的;如果使用 Block Nested-Loop Join 算法,扫描行数就会过多。尤其是在大表上的 join 操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种 join 尽量不要用。
    5. 在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。
    6. 大表 join 操作虽然对 IO 有影响,但是在语句执行结束后,对 IO 的影响也就结束了。但是,对 Buffer Pool 的影响就是持续性的,需要依靠后续的查询请求慢慢恢复内存命中率。
  7. MySQL 是“边读边发的”,取数据发数据流程:
    1. 获取一行,写到 net_buffer 中。这块内存的大小是由参数 net_buffer_length 定义的,默认是 16k。
    2. 重复获取行,直到 net_buffer 写满,调用网络接口发出去。
    3. 如果发送成功,就清空 net_buffer,然后继续取下一行,并写入 net_buffer。
    4. 如果发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送。
    5. 对于正常的线上业务来说,如果一个查询的返回结果不会很多的话,我都建议你使用 mysql_store_result 这个接口,直接把查询结果保存到本地内存。如果返回数据很多,使用mysql_use_result。
  8. 一个稳定服务的线上系统,要保证响应时间符合要求的话,内存命中率要在 99% 以上。(执行 show engine innodb status,查询Buffer pool hit rate)。
  9. InnoDB 内存管理用的是最近最少使用 (Least Recently Used, LRU) 算法。优化:按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域。处于 old 区域的数据页,每次被访问的时候,若这个数据页在 LRU 链表中存在的时间超过了 1 秒,才把它移动到链表头部。
  10. 自增id不连续原因:
    • 唯一键冲突
    • 事务回滚
    • 批量插入数据
  11. 在生产上,尤其是有批量插入数据(包含的语句类型是 insert … select、replace … select 和 load data 语句)的场景时,从并发插入数据性能的角度考虑,建议设置:innodb_autoinc_lock_mode=2 ,并且 binlog_format=row
  12. insert … select 是很常见的在两个表之间拷贝数据的方法。你需要注意,在可重复读隔离级别下,这个语句会给 select 的表里扫描到的记录和间隙加读锁。
  13. 如果 insert 和 select 的对象是同一个表,则有可能会造成循环写入。
  14. insert 语句如果出现唯一键冲突,会在冲突的唯一值上加共享的 next-key lock(S 锁)。因此,碰到由于唯一键约束导致报错后,要尽快提交或回滚事务,避免加锁时间过长。
  15. 主备间事务同步过程:备库 B 跟主库 A 之间维持了一个长连接。主库 A 内部有一个线程,专门用于服务备库 B 的这个长连接。
    • 在备库 B 上通过 change master 命令,设置主库 A 的 IP、端口、用户名、密码,以及要从哪个位置开始请求 binlog,这个位置包含文件名和日志偏移量。
    • 在备库 B 上执行 start slave 命令,这时候备库会启动两个线程。其中 io_thread 负责与主库建立连接。主库 A 校验完用户名、密码后,开始按照备库 B 传过来的位置,从本地读取 binlog,发给 B。
    • 备库 B 拿到 binlog 后,写到本地文件,称为中转日志(relay log)。
    • sql_thread 读取中转日志,解析出日志里的命令,并执行。
  16. 主备延迟原因:
    • 备库所在机器的性能要比主库所在的机器性能差
    • 备库的压力大
    • 大事务
    • 备库的并行复制能力
  17. 备库并行复制策略(v_5.7.22):
    • COMMIT_ORDER,根据同时进入 prepare 和 commit 来判断是否可以并行的策略。
    • WRITESET,表示的是对于事务涉及更新的每一行,计算出这一行的 hash 值,组成集合 writeset。如果两个事务没有操作相同的行,也就是说它们的 writeset 没有交集,就可以并行。
    • WRITESET_SESSION,是在 WRITESET 的基础上多了一个约束,即在主库上同一个线程先后执行的两个事务,在备库执行的时候,要保证相同的先后顺序。
  18. 检测数据库是否正常:
    • health_check表
    • 检测performance_schema信息
  19. grant 语句会同时修改数据表和内存,判断权限的时候使用的是内存数据。因此,规范地使用 grant 和 revoke 语句,是不需要随后加上 flush privileges 语句的。flush privileges 语句本身会用数据表的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下再使用。

日志

  1. MySQL 的“双 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。
  2. binlog cache 是每个线程自己维护的,而 redo log buffer 是全局共用的。因为binlog 是不能“被打断的”。一个事务的 binlog 必须连续写,因此要整个事务完成后,再一起写到文件里。而 redo log 并没有这个要求。
  3. 日志逻辑序号LSN,LSN 是单调递增的,用来对应 redo log 的一个个写入点。每次写入长度为 length 的 redo log, LSN 的值就会加上 length。组提交:当其中一个事务redo log写盘的时候,其他小于该lsn的其他事务在redo log buffer 也会顺带写盘。
  4. 两阶段提交:数据库备份恢复/扩容一般用全量备份加上应用 binlog 来实现的,数据库奔溃后状态恢复使用redo log,因为两个是独立的逻辑,如果不是两阶段提交,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
    1. 流程
      • 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
      • 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
      • 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
      • 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
      • 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
    2. crash:
      • 当prepare log 写入成功且binglog写入成功后发生crash,在mysql启动时候,会自动commit这个事物;
      • 当prepare log写入成功,binlog写入失败,此时发生crash,mysql启动会自动回滚掉这个事务。

redo log

  1. redo log(重做日志):保证crash-safe,InnoDB的物理日志,记录的是“在某个数据页上做了什么修改”。固定大小,从头开始写,写到末尾就又回到开头循环写。
    
  2. write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
  3. InnoDB 用redo log保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
  4. innodb_flush_log_at_trx_commit 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。
  5. InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。
  6. redo log buffer何时写盘:
    1. 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动写盘。
    2. 并行的事务提交的时候,顺带将这个事务的 redo log buffer 持久化到磁盘。
  7. 如果把 innodb_flush_log_at_trx_commit 设置成 1,那么 redo log 在 prepare 阶段就要持久化一次
  8. 每秒一次后台轮询刷盘,再加上崩溃恢复这个逻辑,InnoDB 就认为 redo log 在 commit 的时候就不需要 fsync 了,只会 write 到文件系统的 page cache 中就够了。

binlog

  1. binlog(归档日志):Server层日志。逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。binlog 是可以追加写入的,写完一个文件新增一个文件。sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。
  2. sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
  3. binlog格式:
    • statement:SQL 语句的原文
    • row:记录操作的完整内容,容易恢复数据。
    • mixed:MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式。
  4. 用 binlog 来恢复数据的标准做法是:用 mysqlbinlog 工具解析出来,然后把解析结果整个发给 MySQL 执行。
  5. 把参数 log_slave_updates 设置为 on,表示备库执行 relay log 后生成 binlog。
  6. MySQL 在 binlog 中记录了这个命令第一次执行时所在实例的 server id。每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志。

redo log 与 binlog区别

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
  2. redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
  3. redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
  4. redo log buffer 是所有线程共用的, binlog buffer是每个线程独有的。因为binlog 是不能“被打断的”。一个事务的 binlog 必须连续写,因此要整个事务完成后,再一起写到文件里。

事务

  1. 事务支持是在引擎层实现的。数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。
  2. 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。set autocommit=1,自动提交。
  3. begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。start transaction with consistent snapshot会马上启动一个事务。
  4. 视图:InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
  5. 一致性视图:InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”(启动但未提交)的所有事务 ID。数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。
    • 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
    • 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
    • 如果落在黄色部分,那就包括两种情况
      • 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
      • 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。
  6. 更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。select 语句如果加锁(lock in share mode / for update),也是当前读。
  7. 可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
  8. 唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。因为唯一索引更新时需要将数据页读入内存,判断到没有冲突。
  9. 收缩表空间:只是 delete 掉表里面不用的数据的话,只是把记录的位置,或者数据页标记为了“可复用”,但磁盘文件的大小是不会变的。重建表方法:
    • alter table A engine=InnoDB
    • Online DDL
  10. count在Inndb需要把数据一行一行地从引擎里面读出来,然后累积计数。按照效率排序的话,count(字段)< count(主键 id)< count(1)~ count(*)。
  11. 幻读:指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。幻读仅专指“新插入的行”。在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。

索引与排序

  1. 索引:
    • 主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
    • 非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
    • 主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
  2. 覆盖索引:索引 k 已经“覆盖了”我们的查询需求,不再需要回表查整行记录,我们称为覆盖索引。
  3. 最左前缀原则:最左前缀可以是联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符。
  4. 索引下推:可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
  5. 索引基数采样统计:InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。
  6. 索引统计信息不准确导致的问题,以用 analyze table 来解决。
  7. 字符串创建索引也可用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。前缀的区分度不够好的情况时:
    • 倒序存储。
    • 使用 hash 字段。
  8. 全字段排序:
    • 初始化 sort_buffer,确定放入 name、city、age 这三个字段;
    • 从索引 city 找到第一个满足 city=’杭州’条件的主键 id,也就是图中的 ID_X;
    • 到主键 id 索引取出整行,取 name、city、age 三个字段的值,存入 sort_buffer 中;
    • 从索引 city 取下一个记录的主键 id;
    • 重复步骤 3、4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的 ID_Y;
    • 对 sort_buffer 中的数据按照字段 name 做快速排序(可能在内存中完成,也可能需要使用外部排序(归并排序算法),这取决于排序所需的内存和参数 sort_buffer_size);按照排序结果取前 1000 行返回给客户端。
  9. rowid排序:如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法。
    • 初始化 sort_buffer,确定放入两个字段,即 name 和 id;
    • 从索引 city 找到第一个满足 city=’杭州’条件的主键 id,也就是图中的 ID_X;
    • 到主键 id 索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中;
    • 从索引 city 取下一个记录的主键 id;重复步骤 3、4 直到不满足 city=’杭州’条件为止,也就是图中的 ID_Y;
    • 对 sort_buffer 中的数据按照字段 name 进行排序;遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。
  10. order by rand() 使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。
  11. 如果临时表大小超过了 tmp_table_size,那么内存临时表就会转成磁盘临时表。
  12. 对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。可能导致全索引扫描。比如隐式类型转换、字符编码转换。
    • 隐式类型转换:当字符串与数字比较时,是字符串转成数字。
    • 字符集 utf8mb4 是 utf8 的超集,所以当这两个类型的字符串在做比较的时候,MySQL 内部的操作是,先把 utf8 字符串转成 utf8mb4 字符集,再做比较。

锁

  1. 全局锁:
    • 命令是 Flush tables with read lock (FTWRL)。
    • 典型使用场景是,做全库逻辑备份。
  2. 表级锁:有两种,表锁、元数据锁。
    • 元数据锁MDL不需要显式使用,在访问一个表的时候会被自动加上。
    • 当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
  3. 行锁:
    • 两阶段锁协议: 在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。
  4. 死锁:
    • 设置超时:innodb_lock_wait_timeout
    • 发起死锁检测:innodb_deadlock_detect=on
    • 死锁检测要耗费大量的 CPU 资源
    • 热点行更新导致的性能问题:控制并发度;通过将一行改成逻辑上的多行来减少锁冲突。
  5. 间隙锁:产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入间隙锁 (Gap Lock)。
    • 跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操作。间隙锁之间都不存在冲突关系。
    • 间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。
    • 间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。
    • 间隙锁是在可重复读隔离级别下才会生效,需要把 binlog 格式设置为 binlog_format=row。
  6. 加锁规则:
    • 原则 1:加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间。
    • 原则 2:查找过程中访问到的对象才会加锁。
    • 优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
    • 优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
    • 一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
  7. 锁是加在索引上的。
  8. 以覆盖索引查找时,lock in share mode 只锁覆盖索引。for update 系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。

常见问题:

  1. 崩溃后如何恢复:
  2. 为什么会突然慢
  3. 如何调优
  4. 主从怎么实现的

json格式化性能优化

Posted on 2020-03-06
  1. 语言:python

  2. 场景:由于history接口返回图片是base64编码,当limit较大时返回大json字符串。

  3. 测试方式:在入口文件加入Profiler

    1
    2
    3
    4
    5
    from werkzeug.middleware.profiler import ProfilerMiddleware

    app, socketio = create_app()
    app.wsgi_app = ProfilerMiddleware(app.wsgi_app)
    socketio.run(app)
  4. 测试结果:

可见在 return jsonify({…})时候,simplejson encode耗时比较久,30ms左右。

  1. 改进:使用ujson,并覆盖jsonify方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import ujson as json
    def jsonify(*args, **kwargs):
    if args and kwargs:
    raise TypeError('jsonify() behavior undefined when passed both args and kwargs')
    elif len(args) == 1: # single args are passed directly to dumps()
    data = args[0]
    else:
    data = args or kwargs


    return current_app.response_class(
    json.dumps(data) + '\n',
    mimetype=current_app.config['JSONIFY_MIMETYPE']
    )

性能提升大概2倍,耗时减少至15ms左右.

12…10
Angel Teng

Angel Teng

97 posts
14 categories
37 tags
© 2021 Angel Teng
Powered by Hexo
|
Theme — NexT.Muse v5.1.4