Redis集群迁移工具redis-migrate-tool使用介绍

Redis集群迁移工具redis-migrate-tool,基于redis复制实现,有快速,稳定的特点。

特点

  • 快速
  • 多线程
  • 基于redis复制
  • 实时迁移
  • 迁移过程中,源集群不影响对外提供服务
  • 异构迁移
  • 支持Twemproxy集群,redis cluster集群,rdb文件 和 aof文件
  • 过滤功能
  • 当目标集群是Twemproxy,数据会跳过Twemproxy直接导入到后端的redis
  • 迁移状态显示
  • 完善的数据抽样校验

redis-migrate-tool迁移工具的数据来源可以是:单独的redis实例,twemproxy集群,redis cluster,rdb文件,aof文件。

redis-migrate-tool迁移工具的目标可以是:单独的redis实例,twemproxy集群,redis cluster,rdb文件。

版本说明

https://github.com/vipshop/redis-migrate-tool #仅支持redis3及以下版本

https://github.com/tanruixing88/redis-migrate-tool #基于上述版本修改,支持redis4及以上版本

依赖


安装


配置文件rmt.conf

配置文件示例:从redis cluster集群迁移数据到twemproxy集群


配置文件示例:从redis cluster集群迁移数据到另外一个cluster集群


配置文件示例:从rdb文件恢复数据到redis cluster集群


运行


状态

通过redis-cli连接redis-migrate-tool监控的端口,运行info命令


数据校验


 

0

Redis面试题:为什么Redis很快?

面试官经常会问到单线程的Redis为什么这么快?
为了阐明这个问题, 可以分三部分讲解:
(1) 第一部分: Redis到底有多快
(2) 第二部分: 详细讲解Redis高性能原因
(3) 第三部分: 影响Redis性能的因素

Redis到底有多快

  1. 可以使用redis-benchmark对Redis的性能进行评估,命令行提供了普通/流水线方式、不同压力评估特定命令的性能的功能。
  2. redis性能卓越,作为key-value系统最大负载数量级为10W/s, set和get耗时数量级为10ms和5ms。使用流水线的方式可以提升redis操作的性能。

redis-benchmark实用程序可模拟N个客户端同时发送M个总查询的运行命令(类似于Apache的ab实用程序)。

支持以下选项:

测试数据示例

上例截取了SET/GET/INCR的测试结果。

测试结果包括测试的环境参数(请求量、client数量、有效载荷)以及请求耗时的TP值。

redis-benchmark默认使用10万请求量, 50个clinet,有效载荷为3字节进行测试。

返回结果可以看出SET/GET/INCR命令在10万的请求量下,总的请求耗时均低于0.1s以内。 以QPS=10W为例, 计算出来的平均耗时为2ms左右(1/(10W/50))。

Redis测试经验数据

硬件环境和软件配置

Redis系统负载

  1. 不使用流水线测试结果

  1. 使用流水线测试结果

从以上可以看出Redis作为key-value系统读写负载大致在10W+QPS, 使用流水线技术能够显著提升读写性能。

耗时情况

  1. 不使用流水线测试结果

所有set操作均在10ms内完成, get操作均在5ms以下。

Redis为什么那么快

Redis是一个单线程应用,所说的单线程指的是Redis使用单个线程处理客户端的请求。
虽然Redis是单线程的应用,但是即便不通过部署多个Redis实例和集群的方式提升系统吞吐, 从官网给出的数据可以看出,Redis处理速度非常快。

Redis性能非常高的原因主要有以下几点:

  • 内存存储:Redis是使用内存(in-memeroy)存储,没有磁盘IO上的开销
  • 单线程实现:Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销
  • 非阻塞IO:Redis使用多路复用IO技术,在poll,epool,kqueue选择最优IO实现
  • 优化的数据结构:Redis有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能

下面详细介绍非阻塞IO和优化的数据结构

多路复用IO

在《unix网络编程 卷I》中详细讲解了unix服务器中的5种IO模型。

一个IO操作一般分为两个步骤:

  1. 等待数据从网络到达, 数据到达后加载到内核空间缓冲区
  2. 数据从内核空间缓冲区复制到用户空间缓冲区

按照两个步骤是否阻塞线程,分为阻塞/非阻塞, 同步/异步。

五种IO模型分类:

阻塞 非阻塞
同步 阻塞IO 非阻塞IO,IO多路复用,信号驱动IO
异步IO 异步IO

阻塞IO

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:


非阻塞IO

Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

IO多路复用

IO multiplexing这个词可能有点陌生,但是如果我说select/epoll,大概就都能明白了。有些地方也称这种IO方式为事件驱动IO(event driven IO)。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:

信号驱动IO

异步IO

Linux下的asynchronous IO其实用得不多,从内核2.6版本才开始引入。先看一下它的流程:

介绍完unix或者类unix系统IO模型之后, 我们看下redis怎么处理客户端连接的?

Reids的IO处理

总的来说Redis使用一种封装多种(select,epoll, kqueue等)实现的Reactor设计模式多路复用IO处理客户端的请求。

Reactor设计模式

 

Reactor设计模式常常用来实现事件驱动。除此之外, Redis还封装了不同平台多路复用IO的不同的库。处理过程如下:

IO库封装

因为 Redis 需要在多个平台上运行,同时为了最大化执行的效率与性能,所以会根据编译平台的不同选择不同的 I/O 多路复用函数作为子模块。

具体选择过程如下:
io多路复用的函数选择

Redis 会优先选择时间复杂度为 O(1) 的 I/O 多路复用函数作为底层实现,包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue,上述的这些函数都使用了内核内部的结构,并且能够服务几十万的文件描述符。

但是如果当前编译环境没有上述函数,就会选择 select 作为备选方案,由于其在使用时会扫描全部监听的描述符,所以其时间复杂度较差 O(n),并且只能同时服务 1024 个文件描述符,所以一般并不会以 select 作为第一方案使用。

丰富高效的数据结构

Redis提供了丰富的数据结构,并且不同场景下提供不同实现。

Redis作为key-value系统,不同类型的key对应不同的操作或者操作对应不同的实现,相同的key也会有不同的实现。Redis对key进行操作时,会进行类型检查,调用不同的实现。

为了解决以上问题, Redis 构建了自己的类型系统, 这个系统的主要功能包括:

redisObject 对象。
基于 redisObject 对象的类型检查。
基于 redisObject 对象的显式多态函数。
对 redisObject 进行分配、共享和销毁的机制。

redisObject定义:

type 、 encoding 和 ptr 是最重要的三个属性。

Redis支持4种type, 8种编码, 分别为:

redis的type

有了redisObject之后, 对于特定key的操作过程就可以很容易的实现:

Redis命令的调用过程

Redis除了提供丰富的高效的数据结构外, 还提供了如HyperLogLog, Geo索引这样高效的算法。

影响 Redis 性能的 5 大因素

  1. Redis 内部的阻塞式操作;
  2. CPU 核和 NUMA 架构的影响;
  3. Redis 关键系统配置;
  4. Redis 内存碎片;
  5. Redis 缓冲区。

一、Redis 内部的阻塞式操作

1.1 有哪些阻塞点?
看看要与哪些对象交互以及有什么操作:

逐个分析

1.1.1 和客户端交互时的阻塞点
Redis 使用了 IO 多路复用机制,网络IO将不是阻塞点(只考虑Redis本身而非网络环境)。

键值对的增删改查操作是 Redis 和客户端交互的主要部分,复杂度高的增删改查操作肯定会阻塞 Redis。O(N)级别的操作肯定会阻塞了。

Redis 中涉及集合的操作复杂度通常为 O(N),例如集合元素全量查询操作 HGETALL、SMEMBERS,以及集合的聚合统计操作,例如求交、并和差集。

第一个阻塞点:集合全量查询和聚合操作。

其实,删除操作的本质是要释放键值对占用的内存空间。释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序,所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞。

删除大量键值对数据的时候,最典型的就是删除包含了大量元素的集合,也称为 bigkey 删除。

不同元素数量的集合在进行删除操作时所消耗的时间:

很显然,Redis 的第二个阻塞点是 bigkey 删除操作

频繁删除键值对都是潜在的阻塞点了,那么 FLUSHDB 和 FLUSHALL 操作必然也是一个潜在的阻塞风险,这就是 Redis 的第三个阻塞点:清空数据库。

1.1.2 和磁盘的交互的阻塞点
磁盘 IO 一般都是比较费时费力的,需要重点关注。

其实Redis 进一步设计为采用子进程的方式生成 RDB 快照文件,以及执行 AOF 日志重写操作,这样慢速的磁盘 IO 就不会阻塞主线程了。

但Redis 直接记录 AOF 日志时,同步写磁盘的操作的耗时大约是 1~2ms,如果有大量的写操作需要记录在 AOF 日志中,并同步写回的话,就会阻塞主线程了。

Redis 的第四个阻塞点了:AOF 日志同步写。

1.1.3 主从节点交互时的阻塞点
主从同步时,主库复制创建+传输RDB文件都是子进程完成并不阻塞,但从库接收RDB更新时必定flushdb清库,形成上面讲的第三个阻塞点。

另外从库加载RDB到内存和RDB大小有关,越大越慢,Redis 的第五个阻塞点:加载 RDB 文件

1.1.4 切片集群交互的阻塞点
每个 Redis 实例上分配的哈希槽信息需要在不同实例间进行传递,同时,当需要进行负载均衡或者有实例增删时,数据会在不同的实例间进行迁移。不过,哈希槽的信息量不大,而数据迁移是渐进式执行的,所以,一般来说,这两类操作对 Redis 主线程的阻塞风险不大。

如果你使用了 Redis Cluster 方案,而且同时正好迁移的是 bigkey 的话,就会造成主线程的阻塞,因为 Redis Cluster 使用了同步迁移。

1.2 解决办法、异步执行一下?
总结下五个阻塞点:

  1. 集合全量查询和聚合操作;
  2. bigkey 删除;
  3. 清空数据库;
  4. AOF 日志同步写;
  5. 从库加载 RDB 文件。

Redis 提供了异步线程机制,这五大阻塞式操作都可以被异步执行吗?

1.2.1 异步执行对操作的要求
如果一个操作能被异步执行,就意味着它并不是 Redis 主线程的关键路径上的操作。

关键路径上的操作:客户端把请求发送给 Redis 后,就干等着 Redis 返回数据结果的操作。

1.2.2 集合查询聚合操作
读操作是典型的关键路径操作,客户端发送了读操作之后,就会等待读取的数据返回,以便进行后续的数据处理。

而 Redis 的第一个阻塞点“集合全量查询和聚合操作”都涉及到了读操作,所以,它们是不能进行异步操作了。

1.2.3 删除操作
删除操作并不需要给客户端返回具体的数据结果,所以不算是关键路径操作。第二个阻塞点“bigkey 删除”,和第三个阻塞点“清空数据库”,都是对数据做删除,并不在关键路径上。

1.2.4 AOF同步写操作
为了保证数据可靠性,Redis 实例需要保证 AOF 日志中的操作记录已经落盘,这个操作虽然需要实例等待,但它并不会返回具体的数据结果给实例。所以,我们也可以启动一个子线程来执行 AOF 日志的同步写,而不用让主线程等待 AOF 日志的写完成。

1.2.5 加载RDB文件
从库要想对客户端提供数据存取服务,就必须把 RDB 文件加载完成。所以,这个操作也属于关键路径上的操作,我们必须让从库的主线程来执行

综上,所以,我们可以使用 Redis 的异步子线程机制来实现 bigkey 删除,清空数据库,以及 AOF 日志同步写。

1.3 异步的子线程机制
Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别由它们负责 AOF 日志写操作、键值对删除以及文件关闭的异步执行。

主线程通过一个链表形式的任务队列和子线程进行交互。当收到键值对删除和清空数据库的操作时,主线程会把这个操作封装成一个任务,放入到任务队列中,然后给客户端返回一个完成信息,表明删除已经完成。

但实际上,这个时候删除还没有执行,等到后台子线程从任务队列中读取任务后,才开始实际删除键值对(Redis 4.0+),并释放相应的内存空间。因此,我们把这种异步删除也称为惰性删除(lazy free)。此时,删除或清空操作不会阻塞主线程,这就避免了对主线程的性能影响。

和惰性删除类似,当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到任务队列中。后台子线程读取任务后,开始自行写入 AOF 日志,这样主线程就不用一直等待 AOF 日志写完了。

二、CPU 核和 NUMA 架构的影响

要了解 CPU 对 Redis 具体有什么影响,我们得先了解一下 CPU 架构。

2.1 主流的 CPU 架构
2.1.1 简介
一个 CPU 处理器中一般有多个运行核心,我们把一个运行核心称为一个物理核。每个物理核都可以运行应用程序。

2.1.2 缓存
每个物理核都拥有私有的一级缓存(Level 1 cache,简称 L1 cache),包括一级指令缓存和一级数据缓存,以及私有的二级缓存(Level 2 cache,简称 L2 cache)。

当数据或指令保存在 L1、L2 缓存时,物理核访问它们的延迟不超过 10 纳秒,速度非常快。

但其他的物理核无法对这个核的缓存空间进行数据存取,且只有KB级大小。

所以,不同的物理核还会共享一个共同的三级缓存(Level 3 cache,简称为 L3 cache)。L3 缓存能够使用的存储资源比较多,所以一般比较大,能达到几 MB 到几十 MB,这就能让应用程序缓存更多的数据。当 L1、L2 缓存中没有数据缓存时,可以访问 L3,尽可能避免访问内存。

2.1.3 线程
现在主流的 CPU 处理器中,每个物理核通常都会运行两个超线程,也叫作逻辑核。同一个物理核的逻辑核会共享使用 L1、L2 缓存。总结如图:

2.1.4 多CPU架构
主流的服务器上,一个 CPU 处理器(也称 CPU Socket)会有 10 到 20 多个等几十个物理核。不同处理器间通过总线连接。CPU Socket 的架构:

在多 CPU 架构上,应用程序可以在不同的处理器上运行。

如果应用程序先在一个 Socket 上运行,并且把数据保存到了内存,然后被调度到另一个 Socket 上运行,此时,应用程序再进行内存访问时,就需要访问之前 Socket 上连接的内存,这种访问属于远端内存访问。和访问 Socket 直接连接的内存相比,远端内存访问会增加应用程序的延迟。

多 CPU 架构下,一个应用程序访问所在 Socket 的本地内存和访问远端内存的延迟并不一致,所以,我们也把这个架构称为非统一内存访问架构(Non-Uniform Memory Access,NUMA 架构)

2.2 CPU多核到底是怎么影响Redis的
在多核 CPU 的场景下,一旦应用程序需要在一个新的 CPU 核上运行,那么,运行时信息就需要重新加载到新的 CPU 核上。而且,新的 CPU 核的 L1、L2 缓存也需要重新加载数据和指令,这会导致程序的运行时间增加。

而且,Redis 实例需要等待这个重新加载的过程完成后,才能开始处理请求,所以,这也会导致一些请求的处理时间增加。

2.2.1 案例
当时,项目需求是要对 Redis 的 99% 尾延迟进行优化,要求 GET 尾延迟小于 300 微秒,PUT 尾延迟小于 500 微秒。

99% 的请求延迟小于的值就是 99% 尾延迟。比如说,我们有 1000 个请求,假设按请求延迟从小到大排序后,第 991 个请求的延迟实测值是 1ms,而前 990 个请求的延迟都小于 1ms,所以,这里的 99% 尾延迟就是 1ms。

避免了许多延迟增加的情况,后来,仔细检测了 Redis 实例运行时的服务器 CPU 的状态指标值,这才发现,CPU 的 context switch (上下文切换)次数比较多。

可以使用 taskset 命令把一个程序绑定在一个核上运行。比如说,我们执行下面的命令,就把 Redis 实例绑在了 0 号核上,其中,“-c”选项用于设置要绑定的核编号。

taskset -c 0 ./redis-server

Redis 实例的 GET 和 PUT 的 99% 尾延迟一下子就分别降到了 260 微秒和 482 微秒

2.3 CPU 的 NUMA 架构对 Redis 性能的影响
网络中断程序是要和 Redis 实例进行网络数据交互的,网络中断处理程序从网卡硬件中读取数据,并把数据写入到操作系统内核维护的一块内存缓冲区。

内核会通过 epoll 机制触发事件,通知 Redis 实例,Redis 实例再把数据从内核的内存缓冲区拷贝到自己的内存空间,如下图所示:

如果网络中断处理程序和 Redis 实例各自所绑的 CPU 核不在同一个 CPU Socket 上,那么,Redis 实例读取网络数据时,就需要跨 CPU Socket 访问内存,这个过程会花费较多时间

网上测试显示,和访问 CPU Socket 本地内存相比,跨 CPU Socket 的内存访问延迟增加了 18%

为了避免 Redis 跨 CPU Socket 访问网络数据,我们最好把网络中断程序和 Redis 实例绑在同一个 CPU Socket 上

三、Redis 关键系统配置

3.1 文件系统:AOF 模式
为了保证数据可靠性,Redis 会采用 AOF 日志或 RDB 快照。其中,AOF 日志提供了三种日志写回策略:no、everysec、always。这三种写回策略依赖文件系统的两个系统调用完成,也就是 write 和 fsync,

write 只要把日志记录写到内核缓冲区,就可以返回了,并不需要等待日志实际写回到磁盘;而 fsync 需要把日志记录写回到磁盘后才能返回,时间较长。

AOF 重写会对磁盘进行大量 IO 操作,同时,fsync 又需要等到数据写到磁盘后才能返回,所以,当 AOF 重写的压力比较大时,就会导致 fsync 被阻塞。虽然 fsync 是由后台子线程负责执行的,但是,主线程会监控 fsync 的执行进度。

当主线程使用后台子线程执行了一次 fsync,需要再次把新接收的操作记录写回磁盘时,如果主线程发现上一次的 fsync 还没有执行完,那么它就会阻塞。

可以检查下 Redis 配置文件中的 appendfsync 配置项,该配置项的取值表明了 Redis 实例使用的是哪种 AOF 日志写回策略,如下:

如果 AOF 写回策略使用了 everysec 或 always 配置,请先确认下业务方对数据可靠性的要求,明确是否需要每一秒或每一个操作都记日志。

在有些场景中(例如 Redis 用于缓存),数据丢了还可以从后端数据库中获取,并不需要很高的数据可靠性。

3.2 操作系统:swap
Redis 是内存数据库,内存使用量大,如果没有控制好内存的使用量,或者和其他内存需求大的应用一起运行了,就可能受到 swap 的影响,而导致性能变慢。

触发 swap 的原因主要是物理机器内存不足,对于 Redis 而言,有两种常见的情况:

  • Redis 实例自身使用了大量的内存,导致物理机器的可用内存不足;
  • 和 Redis 实例在同一台机器上运行的其他进程,在进行大量的文件读写操作。文件读写本身会占用系统内存,这会导致分配给 Redis 实例的内存量变少,进而触发 Redis 发生 swap。

操作系统本身会在后台记录每个进程的 swap 使用情况,即有多少数据量发生了 swap。你可以先通过下面的命令查看 Redis 的进程号,这里是 5332。

进入 Redis 所在机器的 /proc 目录下的该进程目录中:

3.3 操作系统:内存大页
Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行的。

为了提供数据可靠性保证,需要将数据做持久化保存。

客户端的写请求可能会修改正在进行持久化的数据。在这一过程中,Redis 就会采用写时复制机制,也就是说,一旦有数据要被修改,Redis 并不会直接修改内存中的数据,而是将这些数据拷贝一份,然后再进行修改。

如果采用了内存大页,那么,即使客户端请求只修改 100B 的数据,Redis 也需要拷贝 2MB 的大页。相反,如果是常规内存页机制,只用拷贝 4KB。

关闭内存大页

四、Redis 内存碎片

经常会遇到这样一个问题:明明做了数据删除,数据量已经不大了,为什么使用 top 命令查看时,还会发现 Redis 占用了很多内存呢?

这是因为,当数据删除后,Redis 释放的内存空间会由内存分配器管理,并不会立即返回给操作系统。所以,操作系统仍然会记录着给 Redis 分配了大量内存。

Redis 释放的内存空间可能并不是连续的,那么,这些不连续的内存空间很有可能处于一种闲置的状态。

4.1 是什么
类似于,一个饭店有十张桌子,之前有十个人客人,每人占了一桌(空出很多单独的小座位),此时来了一对夫妇,那么他们就没得一起坐的位置了。很多单独的小座位就是内存碎片。

4.2 是如何形成的?
4.2.1 内因:内存分配器的分配策略
内存分配器的分配策略就决定了操作系统无法做到“按需分配”。这是因为,内存分配器一般是按固定大小来分配内存,而不是完全按照应用程序申请的内存空间大小给程序分配。

Redis 可以使用 libc、jemalloc、tcmalloc 多种内存分配器来分配内存,默认使用 jemalloc。

jemalloc 的分配策略之一,是按照一系列固定的大小划分内存空间,例如 8 字节、16 字节、32 字节、48 字节,…, 2KB、4KB、8KB 等。

这样的分配方式本身是为了减少分配次数。例如,Redis 申请一个 20 字节的空间保存数据,jemalloc 就会分配 32 字节,此时,如果应用还要写入 10 字节的数据,Redis 就不用再向操作系统申请空间了

如果 Redis 每次向分配器申请的内存空间大小不一样,这种分配方式就会有形成碎片的风险,而这正好来源于 Redis 的外因

4.2.2 外因是 Redis 的负载特征
应用 A 保存 6 字节数据,jemalloc 按分配策略分配 8 字节。如果应用 A 不再保存新数据,那么,这里多出来的 2 字节空间就是内存碎片了,如下图所示:

第二个外因是,这些键值对会被修改和删除,这会导致空间的扩容和释放。

如果应用 E 想要一个 3 字节的连续空间,显然是不能得到满足的。因为,虽然空间总量够,但却是碎片空间,并不是连续的。

4.3 判断是否有内存碎片?
Redis 自身提供了 INFO 命令,可以用来查询内存使用的详细信息

这里有一个 mem_fragmentation_ratio 的指标,它表示的就是 Redis 当前的内存碎片率。

知道了这个指标,我们该如何使用呢?提供一些经验阈值:

mem_fragmentation_ratio 大于 1 但小于 1.5。这种情况是合理的。这是因为,刚才我介绍的那些因素是难以避免的。毕竟,内因的内存分配器是一定要使用的,分配策略都是通用的,不会轻易修改;而外因由 Redis 负载决定,也无法限制。所以,存在内存碎片也是正常的。

mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了 50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了。

4.4 如何清理
当 Redis 发生内存碎片后,一个“简单粗暴”的方法就是重启 Redis 实例。这不是一个“优雅”的方法,毕竟,重启 Redis 会带来两个后果:

如果 Redis 中的数据没有持久化,那么,数据就会丢失;

即使 Redis 数据持久化了,我们还需要通过 AOF 或 RDB 进行恢复,恢复时长取决于 AOF 或 RDB 的大小,如果只有一个 Redis 实例,恢复阶段无法提供服务。

幸运的是,从 4.0-RC3 版本以后,Redis 自身提供了一种内存碎片自动清理的方法,我们先来看这个方法的基本机制。

需要注意的是:碎片清理是有代价的,操作系统需要把多份数据拷贝到新位置,把原有空间释放出来,这会带来时间开销。因为 Redis 是单线程,在数据拷贝时,Redis 只能等着,这就导致 Redis 无法及时处理请求,性能就会降低。

Redis 需要启用自动内存碎片清理,可以把 activedefrag 配置项设置为 yes,命令如下:

这个命令只是启用了自动清理功能,但是,具体什么时候清理,会受到下面这两个参数的控制。

  • active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100MB 时,开始清理;
  • active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理。

五、Redis 缓冲区

5.1 是什么
缓冲区的功能其实很简单,主要就是用一块内存空间来暂时存放命令数据,以免出现因为数据和命令的处理速度慢于发送速度而导致的数据丢失和性能问题。

5.2 风险
缓冲区的内存空间有限,如果往里面写入数据的速度持续地大于从里面读取数据的速度,就会导致缓冲区需要越来越多的内存来暂存数据。当缓冲区占用的内存超出了设定的上限阈值时,就会出现缓冲区溢出。如果发生了溢出,就会丢数据了。那是不是不给缓冲区的大小设置上限,就可以了呢?显然不是,随着累积的数据越来越多,缓冲区占用内存空间越来越大,一旦耗尽了 Redis 实例所在机器的可用内存,就会导致 Redis 实例崩溃。

5.3 总结
从缓冲区溢出对 Redis 的影响的角度,我再把这四个缓冲区分成两类做个总结。

  • 缓冲区溢出导致网络连接关闭:普通客户端、订阅客户端,以及从节点客户端,它们使用的缓冲区,本质上都是 Redis 客户端和服务器端之间,或是主从节点之间为了传输命令数据而维护的。这些缓冲区一旦发生溢出,处理机制都是直接把客户端和服务器端的连接,或是主从节点间的连接关闭。网络连接关闭造成的直接影响,就是业务程序无法读写 Redis,或者是主从节点全量同步失败,需要重新执行。
  • 缓冲区溢出导致命令数据丢失:主节点上的复制积压缓冲区属于环形缓冲区,一旦发生溢出,新写入的命令数据就会覆盖旧的命令数据,导致旧命令数据的丢失,进而导致主从节点重新进行全量复制。

从本质上看,缓冲区溢出,无非就是三个原因:命令数据发送过快过大;命令数据处理较慢;缓冲区空间过小。明白了这个,我们就可以有针对性地拿出应对策略了。

  • 针对命令数据发送过快过大的问题,对于普通客户端来说可以避免 bigkey,而对于复制缓冲区来说,就是避免过大的 RDB 文件。
  • 针对命令数据处理较慢的问题,解决方案就是减少 Redis 主线程上的阻塞操作,例如使用异步的删除操作。
  • 针对缓冲区空间过小的问题,解决方案就是使用 client-output-buffer-limit 配置项设置合理的输出缓冲区、复制缓冲区和复制积压缓冲区大小。当然,我们不要忘了,输入缓冲区的大小默认是固定的,我们无法通过配置来修改它,除非直接去修改 Redis 源码。

 

0

Redis Cluster集群功能测试与踩坑分享

前言

写这篇文章的契机是这样的,现就职公司的Redis是直接购买阿里的云服务,阿里的云服务大家都懂,三个字总结就是好而贵, 虽然云服务内部一定是做了高可用的,但对于使用者(开发人员)来说,这些都是隐形的,是透明的。云服务中的Redis是个黑盒,它甚至仅借用了Redis的概念, 内部是如何实现的,我们一概不知。

所以我对此产生了浓厚的兴趣,我想知道原生Redis集群可以做到何种程度,为的是一旦未来从阿里云迁移到自建服务,至少需要对集群心理有底。

Redis Cluster 与 Sentinel

关于Redis Sentinel和Redis Cluster的问题,是一个会消失还是需要结合使用,下面将进行介绍。

Sentinel

Redis supports multiple slaves replicating data from a master node. This provides a backup node which has your data on it, ready to serve data. However, in order to provide automated failover you need some tool. For Redis this tool is called Sentinel.

Redis支持多个从节点从主节点复制数据。这提供了一个备份节点,上面有您的数据,可以随时提供数据。 但是,为了提供自动故障转移,您需要一些工具。对于Redis,此工具称为Sentinel。 简单的说Sentinel就是一个failover(故障转移)的工具。

它的主要功能有以下几点:

  • 不时地监控redis是否按照预期良好地运行;
  • 如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);
  • 能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。

Sentinel本身也支持集群,只使用单个sentinel进程来监控redis集群是不可靠的,当sentinel进程宕掉后,sentinel本身也有单点问题。所以有必要将sentinel集群,这样有几个好处:

  • 如果只有一个sentinel进程,如果这个进程运行出错,或者是网络堵塞,那么将无法实现redis集群的主备切换(单点问题)。
  • 如果有多个sentinel,redis的客户端可以随意地连接任意一个sentinel来获得关于redis集群中的信息。
  • sentinel集群自身也需要多数机制,也就是2个sentinel进程时,挂掉一个另一个就不可用了。

Redis Cluster

The use cases for Cluster evolve around either spreading out load (specifically writes) and surpassing single-instance memory capabilities. If you have 2T of data, do not want to write sharding code in your access code, but have a library which supports Cluster then you probably want Redis Cluster. If you have a high write volume to a wide range of keys and your client library supports Cluster, Cluster will also be a good fit.

Cluster的用例围绕分散负载(特别是写入)和超越单实例内存功能而发展。如果您有2T的数据,并且不想在访问代码中编写分片代码,但是拥有一个支持Cluster的库, 那么您可能需要Redis Cluster。如果对大量键的写入量很高,并且客户端库支持Cluster,那么Cluster也将非常适合。

cluster的功能有已下几点:

  • 一个 Redis 集群包含 16384 个哈希槽(hash slot),数据库中的每个键都属于这 16384 个哈希槽的其中一个,集群中的每个节点负责处理一部分哈希槽。 例如一个集群有三个节点,其中:
    节点 A 负责处理 0 号至 5500 号哈希槽。
    节点 B 负责处理 5501 号至 11000 号哈希槽。
    节点 C 负责处理 11001 号至 16384 号哈希槽。
    这种将哈希槽分布到不同节点的做法使得用户可以很容易地向集群中添加或者删除节点。例如:
    如果用户将新节点 D 添加到集群中, 那么集群只需要将节点 A 、B 、 C 中的某些槽移动到节点 D 就可以了。
    如果用户要从集群中移除节点 A , 那么集群只需要将节点 A 中的所有哈希槽移动到节点 B 和节点 C , 然后再移除空白(不包含任何哈希槽)的节点 A 就可以了。
  • Redis 集群对节点使用了主从复制功能: 集群中的每个节点都有 1 个至 N 个复制品(replica), 其中一个复制品为主节点(master), 而其余的 N-1 个复制品为从节点(slave)。
  • Redis 集群的节点间通过Gossip协议通信。

对比总结

如果内存需求超过了系统内存,或者需要在多个节点之间分配写操作以保持性能水平,那么使用Redis Cluster。如果寻求高可用,则需要更多地部署Sentinel。 对于是否需要结合使用,答案是不需要,Redis Cluster集群自带failover。使用Cluster时不需要再用Sentinel。

集群部署

这里使用的是docker-compose启动的4主4从集群。
自动草稿

可用性测试

主从切换

先使用docker exec -it node-80 redis-cli -p 6380 cluster info命令查看集群状态

自动草稿

从上图可以看出集群状态显示为ok,说明集群在正常运转。

再使用docker exec -it node-80 redis-cli -p 6380 cluster nodes命令查看各节点状态

自动草稿

从上图可以看出各节点的主从关系,80和85为主从关系,85为主,80为从。

下面插入5条数据:

自动草稿

由上图可以看出数据已经成功插入了。
然后,我将node-85节点停掉。

自动草稿

由上图可以看到80已经从slave节点升级成了master节点,而且集群的状态依然为ok。

接着测试用客户端取集群内80和85分片上的数据。

自动草稿

结果也是ok的,可以查到对应的数据,所以证明了RedisCluster集群的failover能力。

集群数据完整性与可用性

你以为这就结束了?当然没有。
当停止了80节点后,有趣的事情发生了。

自动草稿

集群状态变为了fail,那岂不是整个集群都不可用了。我可是拥有4主4从的集群, 仅down了两台的情况下,就导致整个集群不可用,是不是十分不合理。 那假设我有100台,50主50从,仅down了两台的情况下,剩下98台就都用不了了? 这种重大决策,一定是要交到使用者手中的,根据不同的需求,来选择是否在失去数据完整性的情况下,让集群部分可用。 接着我查阅了大量的资料,最终在RedisCluster的官方文档上找到了答案。

cluster-require-full-coverage <yes/no>: If this is set to yes, as it is by default, the cluster stops accepting writes if some percentage of the key space is not covered by any node. If the option is set to no, the cluster will still serve queries even if only requests about a subset of keys can be processed.

该配置项可以控制当前节点在集群数据不具备完整性的情况下是否继续正常提供服务,默认值为yes。
用刚才的测试举个例子就是:80和85都停了,如果该项设置为no那么集群中剩下的机器可以正常提供服务,反之亦然。

修改了该配置项后,停掉80和85

自动草稿

可以看出,集群的状态并没有变成fail,依然是ok。

由之前的测试内容可以看出,MistRay1和MistRay5的key会路由到80/85的主从之中,所以我们在停掉80/85后, 对插入的其他key进行操作

自动草稿

由上图测试结果可知,客户端是可以对集群内健康的分片进行操作。

总结

RedisCluster高可用有两种模式,当cluster-require-full-coverage设置为yes时,只要集群不具备数据完整性, 那么整个集群直接会进入不可用的状态。当cluster-require-full-coverage设置为no时,即使集群不具备数据完整性, 活着的分片依然可以被操作(读和写)。

0

Jedis连接池会资源泄露吗?

1.前言

Jedis是我们经常使用的Redis Java客户端.在SpringBoot2.X将lettuce作为默认Redis Java客户端之前,Jedis几乎是具备统治地位的.今天我会通过复盘一个压测时遇到的问题来解析Jedis 2.9.1版本一个必现的连接池资源泄露BUG.

2.问题描述

在某次压测中,某服务中产生了这样一条异常日志,期初我们猜测这可能是Jedis连接池负载较高,导致的连接资源紧张的情况.但在持续的压力下,Jedis连接池内的资源竟然大部分都不可用了,最终测试结果以连接池中所有的资源都不可用而告终.

Jedis连接池竟然会资源泄露吗?

3.排查过程

我们马上紧急dump了堆内存,开始分析为什么连接池所有的资源都不可用了,虽然是压测,但压力还没大到把Redis连接池所有资源都繁忙的才对.所以我们一致猜测,应该是某个地方在使用JedisPool中Jedis后没有释放资源导致的.

但在排查了工具类中所有的函数后,发现并没有发现未释放资源的情况.这和我们预想的问题起因不太一致,只能继续排查堆dump,看有没有别的发现.

果不其然,我们发现了堆中Jedis对象都很奇怪,几乎所有Jedis对象里面的socket连接都是closed的状态.socket都close了,当然这个连接就用不了了.但比较诡异的一个点是明明socket已经close了,但表示连接是否损坏的broken字段却是false,意思是并没有损坏.

Jedis连接池竟然会资源泄露吗?

我又注意到了另一个很诡异的问题,明明是从连接池中取出的资源,资源与连接池绑定的映射字段dataSource却是null.难道资源在使用过程中,有什么操作导致资源和池之间解除了绑定关系么?

Jedis连接池竟然会资源泄露吗?

排查到这里基本就可以确定,这个状态不一致的问题就是导致线程池资源耗尽的元凶了,因为连接池认为资源并没有broken,但socket其实已经closed了,连接池也没办法对这些不可用的资源做回收.但想知道为什么会产生这种情况,还是需要去读Jedis的源码.

4.分析源码

从上面的代码可以看到,从JedisPool中获取资源首先要调用getResource()函数.

然后释放资源的时候调用的是Jedisclose()函数.

如果是正常情况下,获取到资源,操作Jedis,最后归还资源到池中,是不会有问题的.但这里有一个非常不明显的线程安全问题.

Jedis连接池竟然会资源泄露吗?

1.线程1在某个资源刚归还到池中并且还没执行到this.DataSource = null

2.同一资源被线程2从池里面获取出来,并将资源与JedisPool绑定

3.线程1执行到this.DataSource = null,将同一资源解绑

4.线程2使用结束后,释放资源,发现dataSource是null认为资源不是从池里取出来的,关闭了socket.

总结

Jedis团队已经在2.10.2版本将该bug修复.

理论上只要并发量够大并且服务启动时间足够长,这个问题几乎是100%复现的.

所以希望看到的小伙伴关注下自己负责的项目中Jedis的版本,如果看到Jedis的close()函数中有this.DataSource = null;这行代码,要尽快把Jedis版本升级到2.10.2及以上版本.

0

java代码实现模糊查找 redis key

以上就是java代码实现模糊查找 redis key

0

Redis清空所有数据

Redis flushall 命令用于清空整个 Redis 服务器的数据(删除所有数据库的所有 key )。

语法

redis flushall 命令基本语法如下:

可用版本

返回值

总是返回 OK 。

实例

以上就是redis清空所有数据的详细内容。

0

Redis命令行批量删除key

命令

注意

  1. -h 后面是redis主机ip, -p后面是redis端口,-a 后面是redis密码
  2. 如果密码里有特殊符号等,需要用单引号扩住密码
  3. key_* 是模糊匹配“key_”前缀的key,如果想删除包含“791202”字符的key,可以换成“791202
  4. xargs 是给命令传递参数的一个过滤器,也是组合多个命令的一个工具
0

zuul + feign + redis + oauth 实现统一认证

本文是接着《使用oltu实现自己的oauth2.0认证服务(一)》和《使用oltu实现自己的oauth2.0认证服务(二)》这两篇之后写的,主体框架是spring cloud搭建。

oauth resource 服务

zuul网关服务

添加一个feign client

OauthServiceFallback统一未认证处理类

实现ZuulFilter接口

0

Redis的Key长度对Redis性能影响分析

Redis的key如果太长,会对性能有什么影响呢?基于这个问题,做了个简单的测试。

环境

Redis和测试程序都是运行在本地,不看单次的性能,只看不同的长度堆读写性能的影响。

测试方法

使用长度分别为10, 100, 500, 1000, 2500, 5000, 7500, 10,000, and 20,000的key,value长度1000,读写1000次。

结果

从结果来看随着长度的增加,读写的耗时都随之增加。

测试代码

0

Redis的key长度对性能影响分析

Redis的key最大可以多长,太长了对性能会有影响吗?其实这些官方已经给出了解释。

官方解释

Redis keys are binary safe, this means that you can use any binary sequence as a key, from a string like “foo” to the content of a JPEG file. The empty string is also a valid key.

A few other rules about keys:

Very long keys are not a good idea. For instance a key of 1024 bytes is a bad idea not only memory-wise, but also because the lookup of the key in the dataset may require several costly key-comparisons. Even when the task at hand is to match the existence of a large value, hashing it (for example with SHA1) is a better idea, especially from the perspective of memory and bandwidth.

Very short keys are often not a good idea. There is little point in writing “u1000flw” as a key if you can instead write “user:1000:followers”. The latter is more readable and the added space is minor compared to the space used by the key object itself and the value object. While short keys will obviously consume a bit less memory, your job is to find the right balance.

Try to stick with a schema. For instance “object-type:id” is a good idea, as in “user:1000”. Dots or dashes are often used for multi-word fields, as in comment:1234:reply.to or comment:1234:reply-to.

The maximum allowed key size is 512 MB.

正常情况下我们都是用一个字符串作为Redis的key,其实Redis允许使用任何二进制序列作为key,比如像一个JPEG文件的内容。甚至空字符串也是可以的。

关于key使用的一些常识

1.不推荐使用非常长的key。太长的key会造成内存和带宽的浪费。
2.太短的key通常也不推荐使用。比如像这个d1000p,根本看不懂啥意思,可以用device:1000:private代替,可读性更好。
3.最大的key允许512MB

测试Key长度对Redis性能影响

环境

Redis和测试程序都是运行在本地,不看单次的性能,只看不同的长度堆读写性能的影响。

测试方法

使用长度分别为10, 100, 500, 1000, 2500, 5000, 7500, 10,000, and 20,000的key,value长度1000,读写1000次。

从结果来看随着长度的增加,读写的耗时都随之增加。

长度为10:写平均耗时0.053ms,读0.040ms
长度为20000:写平均耗时0.352ms,读0.084ms

测试代码

+2