目录

问题定位与优化

fork操作

概念

fork耗时问题定位

如何改善fork操作的耗时

子进程开销监控和优化

CPU

内存

硬盘

AOF追加阻塞

多实例部署

持久化总结

问题定位与优化

  • Redis持久化功能一直是影响Redis性能的高发

fork操作

概念

  • 当Redis做RDB或AOF重写时,一个必不可少的操作就是执行fork操作创建子进程,对于大多数操作系统来说fork是个重量级错误。
  • 虽然fork创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页表
  • 例如对于10GB的Redis进程,需要复制大约20MB的内存页表,因此fork操作耗时跟进程总内存量息息相关,如果使用虚拟化技术,特别是Xen虚拟机,fork操作会更耗时。

fork耗时问题定位

  • 对于高流量的Redis实例OPS可达5万以上,如果fork操作耗时在秒级别将拖慢Redis几万条命令执行,对线上应用延迟影响非常明显。
  • 正常情况下fork耗时应该是每GB消耗20毫秒左右
  • 可以在info stats统计中查latest_fork_usec指标获取最近一次fork操作耗时,单位微秒。

如何改善fork操作的耗时

  • 1)优先使用物理机或者高效支持fork操作的虚拟化技术,避免使用Xen。
  • 2)控制Redis实例最大可用内存,fork耗时跟内存量成正比,线上建议每个Redis实例内存控制在10GB以内。
  • 3)合理配置Linux内存分配策略,避免物理内存不足导致fork失败。
  • 4)降低fork操作的频率,如适度放宽AOF自动触发时机,避免不必要的全量复制等。

子进程开销监控和优化

  • 子进程负责AOF或者RDB文件的重写,它的运行过程主要涉及CPU、内存、硬盘三部分的消耗。

CPU

  • CPU开销分析
  • 子进程负责把进程内的数据分批写入文件,这个过程属于CPU密集操作,通常子进程对单核CPU利用率接近90%.
  • CPU消耗优化
  • Redis是CPU密集型服务,不要做绑定单核CPU操作。
  • 由于子进程非常消耗CPU,会和父进程产生单核资源竞争。
  • 不要和其他CPU密集型服务部署在一起,造成CPU过度竞争。
  • 如果部署多个Redis实例,尽量保证同一时刻只有一个子进程执行重写工作

内存

  • 内存消耗分析
  • 子进程通过fork操作产生,占用内存大小等同于父进程,理论上需要两倍的内存来完成持久化操作,但Linux有写时复制机制(copy-on-write)。
  • 父子进程会共享相同的物理内存页,当父进程处理写请求时会把要修改的页创建副本,而子进程在fork操作过程中共享整个父进程内存快照。
  • 内存消耗监控
  • RDB重写时,Redis日志输出容如下:


* Background saving started by pid 7692 * DB saved on disk * RDB: 5 MB of memory used by copy-on-write * Background saving terminated with success


  • 如果重写过程中存在内存修改操作,父进程负责创建所修改内存页的副本,从日志中可以看出这部分内存消耗了5MB,可以等价认为RDB重写消耗了5MB的内存。
  • AOF重写时,Redis日志输出容如下:



* Background append only file rewriting started by pid 8937 * AOF rewrite child asks to stop sending diffs. * Parent agreed to stop sending diffs. Finalizing AOF... * Concatenating 0.00 MB of AOF diff received from parent. * SYNC append only file rewrite performed * AOF rewrite: 53 MB of memory used by copy-on-write * Background AOF rewrite terminated with success * Residual parent diff successfully flushed to the rewritten AOF (1.49 MB) * Background AOF rewrite finished successfully


  • 父进程维护页副本消耗同RDB重写过程类似,不同之处在于AOF重写需要AOF重写缓冲区
  • 根据以上日志可以预估内存消耗为:
  • 53MB+1.49MB,也就是AOF重写时子进程消耗的内存量。


  • 注:
  • 编写shell脚本根据Redis日志可快速定位子进程重写期间内存过度消耗情况
  • 内存消耗优化
  • 同CPU优化一样,如果部署多个Redis实例,尽量保证同一时刻只有一个子进程在工作。
  • 避免在大量写入时做子进程重写操作,这样将导致父进程维护大量页副本,造成内存消耗。
  • Linux kernel在2.6.38内核增加了Transparent Huge Pages(THP),支持huge page(2MB)的页分配,默认开启。
  • 当开启时可以降低fork创建子进程的速度,但执行fork之后,如果开启THP,复制页单位从原来4KB变为2MB,会大幅增加重写期间父进程内存消耗。建议设置“sudo echonever>/sys/kernel/mm/transparent_hugepage/enabled”关闭THP。

硬盘

  • 硬盘开销分析
  • 子进程主要职责是把AOF或者RDB文件写入硬盘持久化,势必造成硬盘写入压力。
  • 根据Redis重写AOF/RDB的数据量,结合系统工具如sar、iostat、iotop等,可分析出重写期间硬盘负载情况。
  • 硬盘开销优化
  • 优化方法如下:
  • 不要和其他高硬盘负载的服务部署在一起。如:存储服务、消息队列服务等。
  • AOF重写时会消耗大量硬盘IO,可以开启配置no-appendfsync-on-rewrite,默认关闭。表示在AOF重写期间不做fsync操作。
  • 当开启AOF功能的Redis用于高流量写入场景时,如果使用普通机械磁盘,写入吞吐一般在100MB/s左右,这时Redis实例的瓶颈主要在AOF同步硬盘上。
  • 对于单机配置多个Redis实例的情况,可以配置不同实例分盘存储AOF文件,分摊硬盘写入压力。
  • 配置no-appendfsync-on-rewrite=yes时,在极端情况下可能丢失整个AOF重写期间的数据,需要根据数据安全性决定是否配置。

AOF追加阻塞

  • 当开启AOF持久化时,常用的同步硬盘的策略是everysec,用于平衡性能和数据安全性。对于这种方式,Redis使用另一条线程每秒执行fsync同步硬盘。当系统硬盘资源繁忙时,会造成Redis主线程阻塞,如下图所示。

redis的fork子进程还是线程 redis fork子进程会卡住吗_Redis

  • 阻塞流程分析:
  • 1)主线程负责写入AOF缓冲区
  • 2)AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间。
  • 3)主线程负责对比上次AOF同步时间
  • 如果 距上次同步成功时间在2秒内,主线程直接返回。
  • 如果距 上 次同步成功时间超过2秒,主线程将会阻塞,直到同步操作完成。
  • 通过对AOF阻塞流程可以发现两个问题:
  • 1)everysec配置最多 可能丢失2秒数据,不是1秒。
  • 2)如果系统fsync缓慢, 将会导致Redis主线程阻塞影响效率
  • AOF阻塞问题定位:
  •  1)发生AOF阻塞时,Redis输出如下日志,用于记录AOF fsync阻塞导致拖慢Redis服务的行为:

Asynchronous AOF fsync is taking too long (disk is busy). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis

  • 2)每当发生AOF追加阻塞事件发生时,在info Persistence统计中,aof_delayed_fsync指标会累加,查看这个指标方便定位AOF阻塞问题。
  • 3)AOF同步最多允许2秒的延迟,当延迟发生时说明硬盘存在高负载问题,可以通过监控工具如iotop,定位消耗硬盘IO资源的进程。
  • 优化AOF追加阻塞问题主要是优化系统硬盘负载

多实例部署

  • Redis单线程架构导致无法充分利用CPU多核特性,通常的做法是在一台机器上部署多个Redis实例。
  • 当多个实例开启AOF重写后,彼此之间会产生对CPU和IO的竞争。
  • 对于单机多Redis部署,如果同一时刻运行多个子进程,对当前系统影响将非常明显,因此需要采用一种措施,把子进程工作进行隔离
  • Redis在info Persistence提供了监控子进程运行状况的度量指标,如下表所示。

redis的fork子进程还是线程 redis fork子进程会卡住吗_持久化_02

  • 基于以上指标,可以通过外部程序轮询控制AOF重写操作的执行,整个过程如图所示

redis的fork子进程还是线程 redis fork子进程会卡住吗_Redis_03

  • 流程说明:
  • 1)外部程序定时轮询监控机器(machine)上所有Redis实例。
  • 2)对于开启AOF的实例,查看(aof_current_size-aof_base_size)/aof_base_size确认增长率
  • 3)当增长率超过特定阈值(如100%),执行bgrewriteaof命令手动触发当前实例的AOF重写。
  • 4)运行期间循环检查aof_rewrite_in_progress和aof_current_rewrite_time_sec指标,直到AOF重写结束。
  • 5)确认实例AOF重写完成后,再检查其他实例并重复2)~4)步操作。从而保证机器内每个Redis实例AOF重写串行化执行。

持久化总结

  • Redis提供了两种持久化方式:RDBAOF
  • RDB使用一次性生成内存快照的方式,产生的文件紧凑压缩比更高,因此读取RDB恢复速度更快
  • 由于每次生成RDB开销较大无法做到实时持久化,一般用于数据冷备和复制传输
  • save命令会阻塞主线程不建议使用,bgsave命令通过fork操作创建子进程生成RDB避免阻塞。
  • AOF通过追加写命令到文件实现持久化,通过appendfsync参数可以控制实时/秒级持久化。
  • 因为需要不断追加写命令,所以AOF文件体积逐渐变大,需要定期执行重写操作来降低文件体积
  • AOF重写可以通过auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数控制自动触发,也可以使用bgrewriteaof命令手动触发。
  • 子进程执行期间使用copy-on-write机制与父进程共享内存,避免内存消耗翻倍。
  • AOF重写期间还需要维护重写缓冲区,保存新的写入命令避免数据丢失。
  • 持久化阻塞主线程场景有:fork阻塞AOF追加阻塞
  • fork阻塞时间跟内存量和系统有关
  • AOF追加阻塞说明硬盘资源紧张
  • 单机下部署多个实例时,为了防止出现多个子进程执行重写操作,建议做隔离控制,避免CPU和IO资源竞争。
  • 【注】:参考《Redis开发与运维》