一、复制

一.一  配置

一、建立复制

参与复制的redis实例被分为主节点和从节点,默认情况下是主节点。每个从节点只能有一个主节点,每个主节点可以有多个从节点。复制的流程是单向的,只能从主机点复制到从节点。配置复制的方式有以下三种方式

1)、在配置文件中加入slaveof  {主节点ip} {主节点端口},然后启动redis就会生效。

2)、使用redis-server启动redis时,加上--slaveof  主节点ip  主节点端口   的命令

3)、redis已经开启了,则可以使用命令动态设置,使用  slaveof  主节点ip  主节点端口 的命令即可生效。

例如:本地启动两个端口为6379和6380的Redis节点。此时以6379为主机点。

在6380节点执行以下命令:(这样就可以在6379和6380直接建立复制关系了)

127.0.0.1:6380>slaveof 127.0.0.1 6379

此时则可以在主服务器上set值,再在从服务器上get值查看是否成功,如果在从服务器上set值则会失败。报如下错误。(主节点可以读写,从节点只能读)

为什么redis字符串最多512m redis为什么6379_数据

slaveof本身是异步命令,执行slaveof命令时,节点只保存主节点信息后返回。后续复制流程在节点内部异步执行。主从复制成功建立后,可以使用info replication命令查看主从复制的状态。

在主节点6379查看复制状态信息:

为什么redis字符串最多512m redis为什么6379_偏移量_02

从节点查看:

为什么redis字符串最多512m redis为什么6379_偏移量_03


二、断开复制

        通过slaveof <masterip> <masterport>命令建立主从复制关系以后,可以通过slaveof no one断开。需要注意的是,从节点断开复制后,不会删除已有的数据,只是不再接受主节点新的数据变化。断开了的从节点会断掉和主节点的复制关系,并且晋级为主节点。

slaveof还可以切换从节点的对应的主节点,执行slaveof  {newMasterIp} {newMasterPort}命令即可。(例如将6380原先对应的主节点6379变成新的主节点6381)

切换的流程:断开和旧主节点的复制关系——和新主节点建立复制关系——删除从节点当前所有数据——对新主节点进行复制数据操作。(切主后从节点会清空之前所有的数据,线上人工操作时小心slaveof在错误的节点上执行或者指向错误的主节点。)

三、安全性

对于比较重要的节点,则需要对redis设置用户名密码等,主节点会通过设置requirepass参数进行密码验证,这时所有的客户端访问必须使用auth命令实行校验。从节点与主节点的复制连接是通过一个特殊标识的客户端来完成,因此需要配置从节点的masterauth参数与主节点密码保持一致,这样从节点才可以正确地连接到主节点并发起复制流程。

主节点配置的密码:

为什么redis字符串最多512m redis为什么6379_数据_04

从节点配置主节点的访问密码:

为什么redis字符串最多512m redis为什么6379_为什么redis字符串最多512m_05


 四、只读

默认情况下从节点会默认使用只读模式,即默认slave-read-only=yes,一般不要区修改它。

为什么redis字符串最多512m redis为什么6379_redis_06

 五、传输延迟

主从节点一般部署在不同的机器上,复制时的网络延迟就成为需要考虑的问题,Redis为我们提供了repl-disable-tcp-nodelay参数用于控制是否关闭TCP_NODELAY,默认关闭,说明如下:

1、当设置为关闭时:主节点产生的命令数据无论大小都会及时的发送给从节点,主从之间延迟小,但增加了网络带宽的消耗。(适合网络带宽良好的同机房部署)

2、当设置为开启时:主节点会将命令合并为较小的TCP数据包,默认发送时间间隔取决于linux的内核,一般默认40毫秒。这样节省了带宽但增大了主从间的延迟。(适用于网络带宽紧张的跨机房部署)

部署主从节点时需要考虑网络延迟、带宽使用率、防灾级别等因素,如要求低延迟时,建议同机架或同机房部署并关闭repl-disable-tcp-nodelay;如果考虑高容灾性,可以同城跨机房部署并开启repl-disable-tcp-nodelay。

一.二  拓扑

redis的主从复制结构可以支持单层或多层复制关系,大概可以分为下面三种:一主一从、一主多从、树状主从结构

1、一主一从

最简单的主从结构,当应用写命令并发较高且需要持久化时,可以在从节点上开启AOF,避免增加主节点的压力。但是如果主节点没做持久化功能,要避免自动重启的操作,防止为同步的数据丢失。如果主节点没做持久化就重启,重启后的主节点数据都被情况,这时如果从节点继续复制主节点会导致从节点数据也被清空。(所以要重启未作持久化的主节点前需要先断开和从节点的复制关系,避免数据都丢失了)。

2、一主多从(又称星形拓扑结构)

一个主节点多个从节点。常用于读多的场景,减少主节点的压力。(A主其他都是从)

为什么redis字符串最多512m redis为什么6379_偏移量_07

3、树状主从

为什么redis字符串最多512m redis为什么6379_偏移量_08

即A为主节点,BCDE都是从节点。此时从节点不但可以复制主节点的数据,还可以复制其他从节点的数据,即D也可以复制B的数据。就是数据的复制可传递性。这样就可以再进一步的减少主节点的压力。

一.三  主从复制原理

一、复制过程

在从节点执行slaveof命令后,复制的过程就开始了。复制的整个过程大致分为6个过程:

为什么redis字符串最多512m redis为什么6379_数据_09

 1、保存主节点信息

执行slaveof后保存完主节点地址信息就直接返回了,这个阶段在从节点查看复制状态info replication会打印如下信息

master_host:127.0.0.1 master_port:6379 master_link_status:down

此时可以发现主节点的连接状态还只是下线down状态。

2、主从建立socket连接

 从节点内部会又一个定时任务,每秒定时去查看是否有新的主节点信息,如果发现则会与该主节点建立网络连接(socket),从节点会建立一个socket套接字,例如下面会建立一个端口为24555的套接字,专门用于接收主节点发送的复制命令。

为什么redis字符串最多512m redis为什么6379_为什么redis字符串最多512m_10

从节点连接成功会打印如下日志:

* Connecting to MASTER 127.0.0.1:6379 * MASTER <-> SLAVE sync started

如果从节点无法建立连接,定时任务会无限重试直到连接成功或者执行slaveof no one取消复制。

关于连接失败,可以在从节点执行info replication查看master_link_down_since_seconds指标,它会记录与主节点连接失败的系统时间。从节点连接主节点失败时也会每秒打印如下日志,方便运维人员发现问题:

# Error condition on socket for SYNC: {socket_error_reason}

3、发送ping命令

主从连接成功后,从节点会发送ping请求进行首次通信,首次通信的作用是检测主从之间网络嵌套字是否可用和检测主节点当前是否可用接受处理指令。

如果从节点没有接收到主节点的pong回复或者超时,则从节点会断开连接,等待下次定时任务再次重连。接收到pong会打印如下日志并继续下面的流程:

Master replied to PING, replication can continue...

4、权限验证

如果主节点在配置文件中通过requirepass参数设置了密码,此时从节点就需要配置masterauth参数保证与主节点相同的密码才能通过验证。

5、同步数据集

主从节点通信成功后,主节点会将持有的数据都发给从节点(只有全量同步),这一步是最耗时的。redis后面的版本采用新的命令psync进行数据同步(存在全量同步和部分同步)(对于这两个后面会讲到)

6、命令持续复制

 当主节点八当前持有的数据同步给从节点之后,便完成了复制的建立流程。接下来主节点会持续地把写命令发送给从节点,依次保证主从地数据一致性。(这里是将命令发送给从节点???)

二、数据同步

2.8版本之后,redis内部使用psync命令完成主从数据同步,该同步过程有全量复制和部分复制。

全量复制:一次性将持有地数据发给从节点

部分复制:主要用于处理主从复制中因网络闪退等原因造成地数据丢失场景,当从节点再次连上主节点后,主节点会补发丢失数据给从节点。这样就不用再做一次全量复制了。

而psync命令运行需要有以下组件支持:
1)、主从节点各自复制偏移量。2)、主节点复制积压缓冲区。3)、主节点运行id。

1、主从节点各自复制偏移量。

参与复制的主从节点都会维护自身复制偏移量。主节点每处理完一个写入命令,就会把命令的字节长度做累加记录。我们可以使用info relication命令查看master_repl_offset指标看看其偏移量。

而从节点也会做同样的累加操作(存在slave_repl_offset指标),然后每秒钟上报自身的复制偏移量给主节点,然后主节点将所有从节点的偏移量也会记录起来。同样用info relication命令可用查看。

127.0.0.1:6379> info replication connected_slaves:1 slave0:ip=127.0.0.1,port=6380,state=online,offset=1055214,lag=1 ...

通过判断主从节点的复制偏移量就可以判断数据是否一致。

2、主节点复制积压缓冲区

复制积压缓冲区是在主节点上的一个固定长度的队列,默认大小为1MB。当主节点有对应的从节点时被创建,后面如果主节点执行完写命令后,不但会把命令发送给从节点,还会将其写入复制积压缓冲区中。

为什么redis字符串最多512m redis为什么6379_为什么redis字符串最多512m_11

这个复制积压缓冲区就是保存最近的部分已复制的数据,用于部分复制和复制命令丢失的数据补救。

在主节点执行info replication就可以看到复制积压缓冲区的情况:

127.0.0.1:6379> info replication # Replication role:master ... repl_backlog_active:1 // 开启复制缓冲区 repl_backlog_size:1048576 // 缓冲区最大长度 repl_backlog_first_byte_offset:7479 // 起始偏移量,计算当前缓冲区可用范围 repl_backlog_histlen:1048576 // 已保存数据的有效长度。

 根据统计指标,可算出复制积压缓冲区内的可用偏移量范围:
[repl_backlog_first_byte_offset,repl_backlog_first_byte_offset+repl_backlog_histlen]。

3、主节点运行ID

每个redis节点启动后都会动态分配一个40位的十六进制字符串作为运行ID,运行ID可用用来唯一标识redis节点。(例如从节点保存主节点的运行ID就知道自己在复制的是哪个节点了),如果从节点只按照ip+port来标识主节点,那么如果主节点重启变更了且变更了整体数据(重写了RDB或AOF文件),此时只有ip+port则会根据主从节点的偏移量进行部分复制(但这样复制数据是不安全的(因为主节点已经修改RDB或AOF文件,之前复制过的数据已经被修改过了),这个时候是需要进行全量复制的),所以基于ID进行标识,当主节点重启时,运行ID就会改变(每启动一次运行ID改变一次),运行ID改变了则会对从节点做全量复制。

可以用info  server 查看当前节点的运行ID:

127.0.0.1:6379> info server # Server redis_version:3.0.7 ... run_id:545f7c76183d0798a327591395b030000ee6def9

当然也有启动时不改变运行ID的方法:

这时可以使用debug reload命令重新加载RDB并保持运行ID不变,从而有效避免不必要的全量复制。命令如下:

# redis-cli -p 6379 info server | grep run_id run_id:2b2ec5f49f752f35c2b2da4d05775b5b3aaa57ca # redis-cli debug reload OK # redis-cli -p 6379 info server | grep run_id run_id:2b2ec5f49f752f35c2b2da4d05775b5b3aaa57ca

注意:debug reload命令会阻塞当前Redis节点主线程,阻塞期间会生成本地RDB快照并清空数据之后再加载RDB文件。因此对于大数据量的主节点和无法容忍阻塞的应用场景,谨慎使用。

(如果主节点的RDB或AOF重写了,那么会对从节点做全量复制吗?????还是做其他操作???)

4、psync命令

从节点也可以手动是要psync命令完成部分复制和全量复制功能。

命令格式:psync {runId} {offset}

runId:主节点运行ID 。     offset:当前从节点已复制的数据偏移量。主要流程如下:

为什么redis字符串最多512m redis为什么6379_偏移量_12

 1)、从节点根据主节点ID(默认值为从节点保存的主节点id)+偏移量offset(默认值为-1,即第一次需要全量复制)发送psync请求到主节点,

2)、主节点根据请求的参数响应相应的结果:
如果回复+FULLRESYNC {runId} {offset}则会触发全量复制流程(此时就可以拿到主节点运行ID),如果回复+CONTINUE则会触发部分复制流程,如果回复+ERR则是版本太低无法识别psync命令。(此时可以使用旧版的sync命令进行全量复制)

三、全量复制

全量复制redis最早支持的复制方式,也是第一次建立复制时必须要经历的阶段。psync和sync都可以触发全量复制(psync还可以触发部分复制)。这里介绍psync的全量复制流程(和sync的全量流程基本一致)。

1、发送psync命令进行同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行ID,所以只发送了psync -1。

2、主节点根据-1解析出需要进行全量复制,此时回复+FULLRESYNC {runId} {offset}

3、此时从节点保存主节点的运行ID和主节点的偏移量。

4、主节点执行bgsave保存RDB文件到本地。此时主节点bgsave会打印相应的日志:

M * Full resync requested by slave 127.0.0.1:6380 M * Starting BGSAVE for SYNC with target: disk C * Background saving started by pid 32618 C * RDB: 0 MB of memory used by copy-on-write M * Background saving terminated with success

M=当前为主节点日志,S=当前为从节点日志,C=子进程日志。

5、主节点发送RDB文件到从节点,从节点将接收到的RDB文件保存到本地并直接作为从节点的数据文件。注意如果传输的RDB、文件过大,此时传输耗时会较长,如果总时间超过repl-timeout所配置的值(默认60秒),从节点将放弃接受RDB文件并清理已经下载的临时文件,导致全量复制失败。

所以对于文件较大的节点,建议调大repl-timeout参数。

无盘复制:为了降低主节点磁盘开销,redis支持无盘复制。即主节点生成的RDB不先保存到磁盘中,而是直接发送给从节点的。该配置通过repl-diskless-sync参数控制,默认关闭。无盘复制适用于主节点所在机器磁盘性能较差但网络带宽较充裕的场景。

6、对于从节点开始接收RDB快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性。如果主节点创建和传输RDB的时间过长,对于高流量写入场景非常容易造成主节点复制客户端缓冲区溢出。默认配置为client-output-buffer-limit slave256MB64MB60,如果60秒内缓冲区消耗持续大于64MB或者直接超过256MB时,主节点将直接关闭复制客户端连接,造成全量同步失败。对应日志如下:

M 27 May 12:13:33.669 # Client id=2 addr=127.0.0.1:24555 age=1 idle=1 flags=S qbuf=0 qbuf-free=0 obl=18824 oll=21382 omem=268442640 events=r cmd=psync scheduled to be closed ASAP for overcoming of output buffer limits.

因此,运维人员需要根据主节点数据量和写命令并发量调整client-output-buffer-limit slave配置,避免全量复制期间客户端缓冲区溢出。
对于主节点,当发送完所有的数据后就认为全量复制完成,打印成功日志:Synchronization with slave127.0.0.1:6380succeeded,但是对于从节点全量复制依然没有完成,还有后续步骤需要处理。

7、从节点接收到主节点传来的全部数据后会清空自身旧数据。

8、从节点清空完旧数据后开始加载RDB文件,对于线上做读写分离的场景,从节点也负责响应读命令。如果此时从节点正出于全量复制阶段或者复制中断,那么从节点在响应读命令可能拿到过期或错误的数据。对于这种场景,Redis复制提供了slave-serve-stale-data参
数,默认开启状态。如果开启则从节点依然响应所有命令。对于无法容忍不一致的应用场景可以设置no来关闭命令执行,此时从节点除了info和slaveof命令之外所有的命令只返回“SYNC with master in progress”信息。

9、从节点成功加载完RDB后,如果当前节点开启了AOF持久化功能,他会立即做AOF重写操作(bgrewriteaof),为了保证全量复制后AOF持久化文件可以使用。

全量复制中,存在大量的耗时操作:RDB文件网络传输时间、从节点清空数据时间、从节点加载RDB的时间、·可能的AOF重写时间
(例如我们线上数据量在6G左右的主节点,从节点发起全量复制的总耗时在2分钟左右)。除了第一次必须要弄的全量复制,后面的尽量不要用全量复制,为了优化redis后面又提供了部分复制功能

四、部分复制

部分复制是对全量复制的一个优化,也是使用psync命令,是为了防止在进行全量复制时因为网络闪退或者命令丢失等异常情况下,部分数据没有复制过去,此时如果在旧的版本使用sync命令则会再进行一次全量复制(注意第一次全量复制成功后,后面的复制都是主节点执行完命令就发给从节点),所以为了防止再次全量复制,此时psync则提供了部分复制的功能。

为什么redis字符串最多512m redis为什么6379_偏移量_13

 下面是部分复制的流程:

1、如果主从节点出现中断,且时间超过repl-timeout,从节点会中断复制连接。

2、当主从节点网络恢复后,从节点会再次连上主节点,由于之前从节点保存了自身已复制的偏移量和主节点的运行ID。

此时会使用psync将这些参数发送给主节点。

3、主节点接收到psync请求,先核对runId,然后根据参数判断要进行部分复制,此时则根据参数中的offset查看自身的复制积压缓冲区是否有,有则给从节点发送+CONTINUE响应表示即将要进行部分复制。

4、此时主节点真正给从节点发送复制积压缓冲区里的数据。(注意只有复制缓冲区有才进行部分复制,该缓冲区大小为1M)

五、心跳

主从节点建立复制后,他们之间会维护着长连接,并彼此发送心跳命令。

为什么redis字符串最多512m redis为什么6379_偏移量_14

主从心跳判断机制:

1、主从节点都有心跳检测机制,各自模拟成对方的客户端进行通信。可以通过client  list查看相关的客户端信息。其中主节点的连接状态为flags=M,从节点连接状态为flags=S。

2、主节点默认每10秒对从节点发送ping命令,判断从节点的存活性和连接状态。(可通过参数repl-ping-slave-period控制发送频率)

3、从节点每隔1秒就发送replconf ack{offset}命令给主节点上报自身当前的复制偏移量。

replconf命令的主要作用如下:

1)、实时监控主从节点网络状态

2)、上报自身复制偏移量,检测复制数据是否丢失,如果从节点数据丢失则再从主节点的复制积压缓冲区中拉取数据。

3)、实现从节点的数量和延迟性功能,通过min-slaves-to-write、min-slaves-max-lag参数来配置定义。

主节点判定从节点下线后,如果从节点重新恢复,心跳检测会继续进行。(为了降低主从延迟,一般把Redis主从节点部署在相同的机房/同城机房,避免网络延迟和网络分区造成的心跳中断等情况。)

六、异步复制

主节点不但负责数据读写,还负责将写命令同步给从节点。其中写命令的发送是通过异步方式的(就是主节点执行完命令后异步发送给从节点就之间返回了)。

既然是异步的,那么主从之间就会存在一定的延迟。

一.四 、主从在运维中的问题

前面讲了通过复制机制,数据集可以存在多个副本(从节点),这些从节点可以用来读写分离、故障转移、实时备份等场景。

但是在主从复制中,仍然存在一些坑需要踩。

一、读写分离

对于读请求较多的场景,从节点提供读的功能,大大的减轻了主节点的压力。同时写请求只由主节点区执行。

使用从节点响应读请求时,会遇到如下问题:

1、数据延迟(异步复制的)

由于主节点执行完写操作后异步发送给从节点,所以在网络带宽和命令阻塞的情况下,是存在较大的延迟的。

此时就可以编写外部监控程序定时监听主从节点的复制偏移量,当延迟比较大时则触发报警。

监控流程:监控程序定期检查主从节点的偏移量,相减比较两个节点延迟的字节量,当字节量较高时则进行报警并通知客户端(可以参考zookeeper的监听回调机制实现客户端通知),客户端接收到高延迟通知时,修改读命令路由到其他从节点或者主节点。当延迟恢复后,再次通知客户端,恢复从节点的读请求。(该方案成本较高)

2、读到过期的数据或者读不到数据

当主节点存储大量设置超时的数据时,如缓存数据,redis内部需要维护过期数据删除策略。删除策略主要有两种:惰性删除和定性删除。

惰性删除:主节点每次处理读请求时,都会检查数据是否超时,如果超时则执行del命令删除对象,之后del命令(属于写命令)会异步发送给从节点,注意为了保证复制的一致性,从节点自身永远不会主动删除超时任务(只有主节点传递过来删除操作才会去删除)。

定时删除:主节点内部有定时任务会循环采取一定量的键,如果发现样品中的键过期了则执行del命令,之后异步传给从节点。

        如果现在有一个键过期了,而读请求没到主节点(所以不会触发惰性删除),并且假设该键也没被定时任务扫描到,此时读请求发送到了从节点,因为从节点不会做主动的删除工作(都是被动的接收主节点的命令才去做删除任务的),所以是可以读到过期了的键的。

        不过这个问题在redis3.2版本后就进行规避了,虽然从节点不做主动删除等操作,不过在3.2版本中,从节点读取数据时会看该键是否过期,过期则不返回数据。所以可以通过升级版本来解决这个问题。         

3、从节点故障

二、主从配置不一致

主从配置中有一些配置是可以不一致的,例如主节点关闭AOF,从节点开启AOF。

但有一些内存相关的配置必须一致,如maxmemory、hash-max-ziplist-entries等参数。

如果配置不一致,例如从节点的maxmemory小于主节点的,如果复制的数据量较大时,可能大小就会超过从节点的maxmemory。此时会根据maxmemory-policy策略进行内存溢出控制,此时从节点的数据就会出现丢失的情况(但是主从复制流程会正常进行,复制偏移量也是正常的)。此时要修复这个问题只能手动全量复制。

如果压缩列表的相关参数不一致,虽然主从节点存储的数据一致但实际两者内存占用的情况的相差比较大。

三、规避全量复制

发生全量复制的情况一般有以下几种:
1、第一次建立复制:无法避免,尽量在低峰时进行

2、节点运行ID不匹配:例如主节点挂了重启,此时主节点运行ID就会改变,这样从节点以为换了个主节点就进行了全量复制,

可以在主节点挂时,选举某个从节点为主节点例如后面会讲的哨兵模式或者集群模式。(这里提个疑问,将从节点转移为主节点,那运行ID不是也换了吗???那不是也要进行全量复制???)

3、复制积压缓冲区不足:主从节点的连接断开后,节点再次连上主节点时会发送psync{offset}{runId}命令请求部分复制,如果请求的偏移量不在主节点的积压缓冲区内,则无法提供给从节点数据,因此部分复制会退化为全量复制。这个时候可以改善网络情况、增大积压缓冲区的大小。

四、规避复制风暴

复制风暴是多个从节点对同一主节点同时发起全量复制。这样会造成主节点的大量开销。

1、单主节点复制风暴

当主节点重启时,运行ID改变,此时所以从节点都会发起全量复制流程,此时主节点会为从节点创建RDB快照,在快照创建完毕浅,如果有多个从节点进行请求,则这多个从节点会共享这个RDB快照。但是如果一个主节点同时向多个从节点发送RDB快照,此时会使主节点的网络带宽消耗严重,造成主节点的延迟变大。极端情况下还可能发送主从节点断开导致复制失败。

解决方案:减少该主节点的从节点数量,或者采用树状复制结构(从节点可以复制给从节点)

(从节点采用树状树非常有用,网络开销交给位于中间层的从节点,而不必消耗顶层的主节点。但是这种树状结构也带来了运维的复杂性,增加了手动和自动处理故障转移的难度)

2、单机器复制风暴

在一台机器中部署多个redis实例,且有多个主节点。如果这台机器出现故障,需要重启恢复时,那么这台机器的从节点都需要进行全量复制,这样就会造成这台机器带宽消耗。

解决方案:尽量讲主节点分散到多个机器上,且主节点所在机器故障后提供故障转移机制,避免机器恢复后进行密集的全量复制。

二、阻塞

一、发现阻塞

如果redis发送阻塞,一般应用方会收到大量的redis超时处理。例如jedis会抛出JedisConnectionException异常。一般的做法就是在应用方加入异常统计并通过邮件或短信进行报警。至于怎么统计,可以结合日志系统,当异常发生时,异常信息最终会被日志
系统收集到Appender(输出目的地),默认的Appender一般是具体的日志文件,开发人员可以自定义一个Appender,用于专门统计异和触发报警逻辑。

log4j组件介绍  
Log4j主要有三个组件: 
     

  • Logger:负责供客户端代码调用,执行debug(Object msg)、info(Object msg)、warn(Object msg)、error(Object msg)等方法。
  • Appender:负责日志的输出,Log4j已经实现了多种不同目标的输出方式,可以向文件输出日志、向控制台输出日志、向Socket输出日志等。
  • Layout:负责日志信息的格式化。

为什么redis字符串最多512m redis为什么6379_为什么redis字符串最多512m_15

具体实现代码:(这里有个疑问,这不会收集到其他异常信息???(非redis的日志信息))

public class RedisAppender extends AppenderBase<ILoggingEvent> { // 使用 guava 的 AtomicLongMap, 用于并发计数 public static final AtomicLongMap<String> ATOMIC_LONG_MAP = AtomicLongMap.create(); static { // 自定义 Appender 加入到 logback 的 rootLogger 中 LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME); ErrorStatisticsAppender errorStatisticsAppender = new ErrorStatisticsAppender(); errorStatisticsAppender.setContext(loggerContext); errorStatisticsAppender.start(); rootLogger.addAppender(errorStatisticsAppender); } // 重写接收日志事件方法 protected void append(ILoggingEvent event) { // 只监控 error 级别日志 if (event.getLevel() == Level.ERROR) { IThrowableProxy throwableProxy = event.getThrowableProxy(); // 确认抛出异常 if (throwableProxy != null) { // 以每分钟为 key ,记录每分钟异常数量 String key = DateUtil.formatDate(new Date(), "yyyyMMddHHmm"); //抛一次一次则自增1 long errorCount = ATOMIC_LONG_MAP.incrementAndGet(key); if (errorCount > 10) { // 超过 10 次触发报警代码 } // 清理历史计数统计,防止极端情况下内存泄露 for (String oldKey : ATOMIC_LONG_MAP.asMap().keySet()) { if (!StringUtils.equals(key, oldKey)) { ATOMIC_LONG_MAP.remove(oldKey); } } } } }

 此时如果存在多个redis节点,那么此时这个时候很难去确定异常是出自哪个redis。所以可以去修改客户端的日志打印方式,例如修改jedis的Connection类下的connect、sendCommand、readProtocolWithCheckingBroken方法专门捕获连接,发送命令,协议读取事件的异常。这样就可以准确的打印出对应节点的ip、端口、执行命令等信息。还有就是可以使用监控平台对redis进行监控,这样也能很好的定位到问题。控系统所监控的关键指标有很多,如命令耗时、慢查询、持久化阻塞、连接拒绝、CPU/内存/网络/磁盘使用过载等。

二、内在原因(不合理使用、cpu饱和、持久化阻塞等)

如果定位到了具体redis的节点异常后,首先需要排除是否是redis自身原因导致。具体可以围绕下面这几个点进行排查。

1、API或数据结构使用不合理。

例如使用了O(n)的命令(hgetall等)。那么我们如何发现这些问题?

1)、如何发现慢查询

redis本身使用slowlog  get {n} 命令可以获取到最近的n条慢查询命令。默认执行超过10毫秒的命令会被记录到一个定长的队列中。(线上案例建议设置为1毫秒。这样可以排除毫秒级的命令,因为如果命令是毫秒级,其实际的ops只有1000左右,此时还是尽量进行优化),慢查询队列长度默认128,可以适当的调大。(注意慢查询不包含网络传输时间和命令排队时间)。

一旦发生慢查询后,开发人员需要做出及时的调整。

(1)使用复杂度低的命令,例如hgetall改为hmget等,禁用keys、sort等命令。

(2)调整大对象:缩减大对象或把大对象拆分为多个小对象,防止之后一次操作过多的数据。例如存储一个好友列表,可以根据性别、时间等分成多个维度进行存储,具体根据业务去拆分,这样可以尽量的减少对大对象的操作。

2)、如何发现大对象

redis本身提供发现大对象的命令。redis-cli  -h {ip} -p {port}  bigkeys。内部原理是采用分段进行scan操作。效果如下:
 

# redis-cli --bigkeys # Scanning the entire keyspace to find biggest keys as well as # average sizes per key type. You can use -i 0.1 to sleep 0.1 sec # per 100 SCAN commands (not usually needed). [00.00%] Biggest string found so far 'ptc:-571805194744395733' with 17 bytes [00.00%] Biggest string found so far 'RVF#2570599,1' with 3881 bytes [00.01%] Biggest hash found so far 'pcl:8752795333786343845' with 208 fields [00.37%] Biggest string found so far 'RVF#1224557,1' with 3882 bytes [00.75%] Biggest string found so far 'ptc:2404721392920303995' with 4791 bytes [04.64%] Biggest string found so far 'pcltm:614' with 5176729 bytes [08.08%] Biggest string found so far 'pcltm:8561' with 11669889 bytes [21.08%] Biggest string found so far 'pcltm:8598' with 12300864 bytes .. 忽略更多输出 ... -------- summary ------- Sampled 3192437 keys in the keyspace! Total key length in bytes is 78299956 (avg len 24.53) Biggest string found 'pcltm:121' has 17735928 bytes Biggest hash found 'pcl:3650040409957394505' has 209 fields 2526878 strings with 954999242 bytes (79.15% of keys, avg size 377.94) 0 lists with 0 items (00.00% of keys, avg size 0.00) 0 sets with 0 members (00.00% of keys, avg size 0.00) 665559 hashs with 19013973 fields (20.85% of keys, avg size 28.57) 0 zsets with 0 members (00.00% of keys, avg size 0.00)

2、cpu饱和

单线程的redis在处理命令时只能使用一个cpu。而cpu饱和是指redis把单核cpu使用率跑到接近100%。使用top命令可以很容易识别对应redis进程的cpu使用率。cpu饱和后redis将无法处理更多的命令。严重的影响吞吐量和应用方的稳定性。此时可以使用统计命令redis-cli -h{ip} -p{port}  --stat获取当前Redis使用情况。该命令每秒输出一行统计信息。

# redis-cli --stat ------- data ------ --------------------- load -------------------- - child - keys mem clients blocked requests connections 3789785 3.20G 507 0 8867955607 (+0) 555894 3789813 3.20G 507 0 8867959511 (+63904) 555894 3789822 3.20G 507 0 8867961602 (+62091) 555894 3789831 3.20G 507 0 8867965049 (+63447) 555894 3789842 3.20G 507 0 8867969520 (+62675) 555894 3789845 3.20G 507 0 8867971943 (+62423) 555894

这个统计信息可以看到,这个redis每秒处理请求6万多个,这个在命令层次已经很难优化了。这时就需要对reids做集群处理来分摊ops压力。(如果几百几千ops的实例接近cpu饱和那就不是正常的)有可能使用了高算法复杂度的命令。还有一种情况是过度的内存优化,这种情况有些隐蔽,需要我们根据info  commandstats统计信息分析出命令不合理开销时间,例如下面的耗时统计:

cmdstat_hset:calls=198757512,usec=27021957243,usec_per_call=135.95

查看这个统计可以发现一个问题,hset命令算法复杂度只有O(1)但平均耗时却达到135微秒,显然不合理,正常情况耗时应该在10微秒以下。这是因为上面的Redis实例为了追求低内存使用量,过度放宽ziplist使用条件(修改了hash-max-ziplist-entries和hash-max-ziplist-value配置)。进程内的
hash对象平均存储着上万个元素,而针对ziplist的操作算法复杂度在O(n)到O(n2)之间。虽然采用ziplist编码后hash结构内存占用会变小,但是操作变得更慢且更消耗CPU。ziplist压缩编码是Redis用来平衡空间和效率的优化手段,不可过度使用。(ziplist编码后面会讲)

3、持久化阻塞

持久化过程可能出现阻塞的操作有:ork阻塞、AOF刷盘阻塞、HugePage写操作阻塞。

1)、fork阻塞

fork操作发生在RDB和AOF重写、RDB持久化。如果这个时间耗时较长,必然会导致主线程的阻塞。

可以执行info status 命令获取latet_fork_usec指标,查看最近一次redis的fork操作耗时,如果耗时很大(例如超过1秒),则需要做出优化调整。如避免使用过大内存实例和规避fork缓慢的操作系统等(具体措施在上面持久化的文章有说)。

2)、AOF刷盘阻塞(AOF追加阻塞)

当我们开启AOF持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时,fsync操作需要等待,直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒,为了数据安全性它会阻塞直到后台线程执行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.

也可以查看info persistence统计中的aof_delayed_fsync指标,每次发生fdatasync阻塞主线程时会累加。

(该问题就是AOF追加阻塞问题,在之前的持久化文章有说明,对于优化AOF追加阻塞问题主要是优化系统硬盘负载,具体可以看之前文章)

(硬盘压力可能是Redis进程引起的,也可能是其他进程引起的,可以使用iotop查看具体是哪个进程消耗过多的硬盘资源。)

3)、HugePage写操作阻塞

子进程在执行重写期间利用Linux写时复制技术降低内存开销,因此只有写操作时Redis才复制要修改的内存页。对于开启了Transparent HugePages的操作系统,每次写命令引起的复制内存页单位由4K变为2MB,放大了512倍,会拖慢写操作的执行时间,导致大量写操作慢查询。例如简单的incr命令也会出现在慢查询中。关于Transparent HugePages的细节后面会讲。

Redis官方文档中针对绝大多数的阻塞问题进行了分类说明,这里不再详细介绍,细节请见:http://www.redis.io/topics/latency。

三、外在原因(cpu竞争、内存交换、网络问题)

自身的问题排除完后如果还没定位到问题,此时就可以对外部原因进行排查。

1、cpu竞争

进程竞争:redis是典型的cpu密集型应用,所以一般不推荐和其他cpu密集型服务部署在一起。可以使用top、sar等命令定位到cpu消耗的时间点和具体进程。

绑定cpu:部署redis时为充分利用多核cpu,通常一台机器会部署多个实例(每个实例对应一个cpu)。并且常见的方法时将redis绑定在一个cpu上,这样可以减少cpu频繁的上下文切换。但这也存在问题,就是当父进程创建子进程进行RDB/AOF重写时,如果做了cpu绑定,此时两个进程共享一个cpu。此时就会使得cpu的使用率在90以上。这样就产生了cpu竞争,极大的影响redis稳定。所以一般开启了持久化或存在主从复制的节点不建议绑定cpu。

2、内存交换

内存交换(swap)对redis来说是非常致命的,redis保证高性能的一个前提是所有数据都在内存中,如果使用了swap(硬盘)存储数据,这样其读写速度就会相差几个数量级。这样就导致redis的性能急剧下降。可以使用以下方法检查redis的内存交换:

1)、查询redis进程号:查看6383这个实例的信息,利用管道符过滤出进程id的信息。

# redis-cli -p 6383 info server | grep process_id process_id:4476

2)、根据进程号查询内存交换信息

# cat /proc/4476/smaps | grep Swap Swap: 0 kB Swap: 0 kB Swap: 4 kB Swap: 0 kB Swap: 0 kB .....

如果是0kb或者少量的4kb,这属于正常的现象。

预防内存交换的方法有:
保证机器有充足的可用内存(这样就不会因为内存不足去大量使用交换区的空间);设置redis的最大可用内存(maxmemory)防止内存不可控的增长(这样内存达到了就会抛异常,不会大量的去使用交换区的空间),降低系统使用swap使用级(可以减少对交换区的使用)如echo10>/proc/sys/vm/swappiness。

3、网络问题

常见的网络问题有:连接拒绝、网络延迟、网卡软中断等问题。

1)、连接拒绝

当网络闪断或者连接数溢出时,客户端则无法连接到redis,常见的以下几种情况:
(1)、网络闪退:般发生在网络割接或者带宽耗尽的情况,对于网络闪断的识别比较困难,常见的做法可以通过sar-n DEV查看本机历史
流量是否正常,或者借助外部系统监控工具(如Ganglia)进行识别。对于重要的Redis服务需要充分考虑部署架构的优化,尽量避免客户端与Redis之间异地跨机房调用。

(2)redis连接拒绝:Redis通过maxclients参数控制客户端最大连接数,默认10000。当Redis连接数大于maxclients时会拒绝新的连接进入,info stats的rejected_connections统计指标记录所有被拒绝连接的数量。Redis使用多路复用IO模型可支撑大量连接,但是不代表可以无限连接。客户端访问Redis时尽量采用NIO长连接或者连接池的方式。

当Redis用于大量分布式节点访问且生命周期比较短的场景时,如比较典型的在Map/Reduce中使用Redis。因为客户端服务存在频繁启动和销毁的情况且默认Redis不会主动关闭长时间闲置连接或检查关闭无效的TCP连接,因此会导致Redis连接数快速消耗且无法释放的问题。这种场景下建议设置tcp-keepalive和timeout参数让Redis主动检查和关闭无效连接。

(3)、连接溢出:这是指操作系统或者Redis客户端在连接时的问题。下面介绍两种原因

(3-1)进程限制:客户端想成功连接上Redis服务需要操作系统和Redis的限制都通过才可以,如图:

为什么redis字符串最多512m redis为什么6379_redis_16

操作系统一般会对进程使用的资源做限制,其中一项是对进程可打开最大文件数控制,通过ulimit-n查看,通常默认1024。由于Linux系统对TCP连接也定义为一个文件句柄,因此对于支撑大量连接的Redis来说需要增大这个值,如设置ulimit-n65535,防止Too many open files错误。

(3-2)backlog队列溢出

系统对于特定端口的TCP连接使用backlog队列保存。Redis默认的长度为511,通过tcp-backlog参数设置。如果Redis用于高并发场景为了防止缓慢连接占用,可适当增大这个设置,但必须大于操作系统允许值才能生效。当Redis启动时如果tcp-backlog设置大于系统允许值将以系统值为准,Redis打印如下警告日志:

# WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/ net/core/somaxconn is set to the lower value of 128.

系统的backlog默认值为128,使用echo 511 >/proc/sys/net/core/somaxconn命令进行修改。可以通过netstat-s命令获取因backlog队列溢出造成的连接拒绝统计,如下:

# netstat -s | grep overflowed 663 times the listen queue of a socket overflowed

如果怀疑是backlog队列溢出,线上可以使用cron定时执行netstat-s|grep overflowed统计,查看是否有持续增长的连接拒绝情况。

2)、网络延迟

网络延迟取决于客户端到Redis服务器之间的网络环境。主要包括它们之间的物理拓扑和带宽占用情况。常见的物理拓扑按网络延迟由快到慢可分为:同物理机>同机架>跨机架>同机房>同城机房>异地机房。但它们容灾性正好相反,同物理机容灾性最低而异地机房容灾性最高。Redis提供了测量机器之间网络延迟的工具,在redis-cli-h{host}-p{port}命令后面加入如下参

数进行延迟测试:
·--latency:持续进行延迟测试,分别统计:最小值、最大值、平均值、采样次数。
·--latency-history:统计结果同--latency,但默认每15秒完成一行统计,可通过-i参数控制采样时间。
·--latency-dist:使用统计图的形式展示延迟统计,每1秒采样一次。

网络延迟问题经常出现在跨机房的部署结构上,对于机房之间延迟比较严重的场景需要调整拓扑结构,如把客户端和Redis部署在同机房或同城机房等。
带宽瓶颈通常出现在以下几个方面:
·机器网卡带宽。
·机架交换机带宽。
·机房之间专线带宽。
带宽占用主要根据当时使用率是否达到瓶颈有关,如频繁操作Redis的大对象对于千兆网卡的机器很容易达到网卡瓶颈,因此需要重点监控机器流量,及时发现网卡打满产生的网络延迟或通信中断等情况,而机房专线和交换机带宽一般由上层运维监控支持,通常出现瓶颈的概率较小。

 3)、网卡软中断

网卡软中断是指由于单个网卡队列只能使用一个CPU,高并发下网卡数据交互都集中在同一个CPU,导致无法充分利用多核CPU的情况。网卡软中断瓶颈一般出现在网络高流量吞吐的场景,如下使用“top+数字1”命令(先输top,然后再按1,就会出现)可以很明显看到CPU1的软中断指标(si)过高:

# top Cpu0 : 15.3%us, 0.3%sy, 0.0%ni, 84.4%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu1 : 16.6%us, 2.0%sy, 0.0%ni, 47.1%id, 3.3%wa, 0.0%hi, 31.0%si, 0.0%st Cpu2 : 13.3%us, 0.7%sy, 0.0%ni, 86.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu3 : 14.3%us, 1.7%sy, 0.0%ni, 82.4%id, 1.0%wa, 0.0%hi, 0.7%si, 0.0%st ..... Cpu15 : 10.3%us, 8.0%sy, 0.0%ni, 78.7%id, 1.7%wa, 0.3%hi, 1.0%si, 0.0%st

Linux在内核2.6.35以后支持Receive Packet Steering(RPS),实现了在软件层面模拟硬件的多队列网卡功能。如何配置多CPU分摊软中断已超出本书的范畴。