redis(三)运维与原理(客户端、持久化)

一、客户端

一、客户端通信协议

1、客户端和服务端之间的通信协议是在TCP协议上构建的

2、redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端和服务端的正常交互(这种协议简单高效)。

例如客户端发送一条set hello world命令给服务端,按照RESP的标准,客户端需要将其封装为如下格式(每行用\r\n分隔):

*3
$3
SET
$5
hello
$5
world

这样Redis服务端能够按照RESP将其解析为set hello world命令。

可以看到除了命令(set hello world)和返回结果(OK)本身还包含了一些特殊字符以及数字,下面将对这些格式进行说明。

1)、发送命令格式:

RESP的规定中客户端发送一条命令的格式如下:(CRLF表示\r\n)

*< 参数数量 > CRLF
$< 参数 1 的字节数量 > CRLF
< 参数 1> CRLF
...
$< 参数 N 的字节数量 > CRLF
< 参数 N> CRLF

所以*3表示这个命令有三个参数,$3表示第一个参数的字节数量为3(对应SET),依次类推。

有一点要注意的是,上面只是格式化显示的结果,实际传输格式为如下代码

*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n

2)、返回结果格式

redis的返回结果格式类型分为下面五种:
·状态回复:在RESP中第一个字节为"+"。
·错误回复:在RESP中第一个字节为"-"。
·整数回复:在RESP中第一个字节为":"。
·字符串回复:在RESP中第一个字节为"$"。
·多条字符串回复:在RESP中第一个字节为"*"。

 我们知道redis-cli只能看到最终的执行结果,那是因为redis-cli本身就是按照RESP进行结果解析的,所以看不到中间结果,redis-cli.c源码对命令结果的解析结构如下:

static sds cliFormatReplyTTY(redisReply *r, char *prefix) {
sds out = sdsempty();
switch (r->type) {
case REDIS_REPLY_ERROR:
// 处理错误回复
case REDIS_REPLY_STATUS:
// 处理状态回复
case REDIS_REPLY_INTEGER:
// 处理整数回复
case REDIS_REPLY_STRING:
// 处理字符串回复
case REDIS_REPLY_NIL:
// 处理空
case REDIS_REPLY_ARRAY:
// 处理多条字符串回复
return out;
}

例如执行set hello world,返回结果是OK,并不能看到加号:

127.0.0.1:6379> set hello world
OK

为了看到Redis服务端返回的“真正”结果,可以使用nc命令、telnet命令、甚至写一个socket程序进行模拟。下面以nc命令进行演示,首先使用nc 127.0.0.16379连接到Redis:

nc 127.0.0.1 6379

状态回复:set hello world的返回结果为+OK:

set hello world
+OK

错误回复:由于sethx这条命令不存在,那么返回结果就是"-"号加上错误消息:

sethx
-ERR unknown command 'sethx'

有了RESP提供的发送命令和返回结果的协议格式,各种编程语言就可以利用其来实现相应的Redis客户端。


二、java客户端Jedis

有了上面的发送和接收格式,所以就有人根据这些格式包装出各种语言的客户端,例如java中的jedis。

1、获取jedis(直连的方式线程不安全)

2、jedis的基本使用

3、jedis连接池的使用(线程池的获取方式线程安全)

4、jedis中Pipeline的使用

5、jedis的Lua脚本的使用


三、客户端管理(客户端API、客户端配置、客户端统计片段)

三.一、客户端API

1、client list

列出与redis相连的所有客户端连接信息。

127.0.0.1:6379> client list
id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=300210 addr=10.2.xx.215:61972 fd=3342 name= age=8054103 idle=8054103 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=5448879 addr=10.16.xx.105:51157 fd=233 name= age=411281 idle=331077 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ttl
id=2232080 addr=10.16.xx.55:32886 fd=946 name= age=603382 idle=331060 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=7125108 addr=10.10.xx.103:33403 fd=139 name= age=241 idle=1 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del
id=7125109 addr=10.10.xx.101:58658 fd=140 name= age=241 idle=1 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del
...

其中部分属性说明:
(1)标识:id、addr、fd、name
这四个属性属于客户端的标识:
·id:客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启Redis后会重置为0。
·addr:客户端连接的ip和端口。
·fd:socket的文件描述符,与lsof命令结果中的fd是同一个,如果fd=-1代表当前客户端不是外部客户端,而是Redis内部的伪装客户端。
·name:客户端的名字,后面的client setName和client getName两个命令会对其进行说明。

(2)、输入缓冲区:qbuf、qbuf-free

在redis中会为每个客户端分配一个输入缓冲区(用于将客户端发送的命令临时保存,redis也会和输入缓冲区中拉取命令进行执行),该缓冲区主要起到缓冲作用。

其中qbuf和qbuf-free分别表示该缓冲区的总容量和剩余容量。redis中没有提供相应的配置来规定这个缓冲区的大小,该缓冲区的大小会根据输入的内容的大小进行动态配置(但是如果该缓冲区的大小超过1G后客户端的连接将会被关闭)。

/* Protocol and I/O related defines */
#define REDIS_MAX_QUERYBUF_LEN (1024*1024*1024) /* 1GB max query buffer. */

注意,输入缓冲区使用不当会产生下面两个问题:

1)、输入缓冲区大小超过1G客户端会被关闭。

2)、输入缓冲区不受maxmemory控制(redis的最大内存容量,64位系统默认是0即无限制),所以当缓冲区的较大时会造成redis的数据丢失、键值淘汰、oom等情况。例如我现在redis申请的总内存是4G,此时已经存储了2G数据,此时可用内存已经剩2G,如果我此时输入缓冲区写进去3G数据,此时已经超出总容量了,此时就会对redis的键值对等进行淘汰。所以随意使用缓冲区会造成数据丢失、OOM等情况。(一般出现在大量命令输入、命令执行阻塞等情况)

而一般出现这种情况我们是需要对redis进行监控的,一般可以通过下面两种方式:

1、定期的执行client list命令查看哪些客户端的qbuf和qbuf-free有异常。然后再定位到相应的客户端进行分析。

2、使用info clients命令获取最大输入缓冲区的信息。也可以设置某个客户端的client_biggest_input_buf参数,例如设置为10M,如果输入缓冲区超过10M就会报警。

127.0.0.1:6379> info clients
# Clients
connected_clients:1414
client_longest_output_list:0
client_biggest_input_buf:2097152
blocked_clients:0

这两种方法都各有优势:

 虽然输入缓冲区的问题出现的概论比较低,但也要做好防范,在开发中要减少bitKey、减少redis阻塞、合理的监控报警。

(3)、输出缓冲区:obl、oll、omem

redis中为每个客户端分配了输入缓冲区,作用是用于保存命令执行的结果,提供缓冲作用。

和输入缓冲区不同的是,输出缓冲区的大小是可以设置的,可以通过client-output-buffer-limit来进行设置。并且输出缓冲区根据客户端的不同分为三种:普通客户端缓冲区、发布订阅客户端缓冲区、slave客户端缓冲区。

 不同缓冲区对应的配置规则如下:

client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

·<class>:客户端类型,分为三种。a)normal:普通客户端;b)slave:slave客户端,用于复制;c)pubsub:发布订阅客户端。
·<hard limit>:如果客户端使用的输出缓冲区大于<hard limit>,客户端会被立即关闭。
·<soft limit>和<soft seconds>:如果客户端使用的输出缓冲区超过了<soft limit>并且持续了<soft limit>秒,客户端会被立即关闭。

redis的默认配置是:(0表示不限制)

client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

和输入缓冲区相同的是,输出缓冲区也不会受maxmemory现在,使用不当仍然会造成数据丢失、键值淘汰、OOM等情况。

        实际上输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区用于返回比较小的执行结果,而动态缓冲区用于返回比较大的结果,例如大的字符串、hgetall、smemvers命令等结果等。(固定缓冲区使用的是字节数组,动态缓冲区用的是列表,当固定缓冲区存满后会将redis新的返回结果直接放到动态缓冲区的队列中去,队列中一个返回结果对应一个对象)

        client  list命令得到的 obl表示固定缓冲区的数据长度,oll表示动态缓冲区列表的对象个数,omem表示总共使用的字节数。

例如:这里固定缓冲区长度为0,动态缓冲区有4869个对象,两个部分加起来一共133081288字节=126M

id=7 addr=127.0.0.1:56358 fd=6 name= age=91 idle=0 flags=O db=0 sub=0 psub=0 multi=-1
qbuf=0 qbuf-free=0 obl=0 oll=4869 omem=133081288 events=rw cmd=monitor

 监控输出缓冲区的方法依然有两种:

1、定期执行client list命令,收集obl、oll、omem异常的客户端进行定位

2、使用info  clients命令找出输出缓冲区列表最大对象数的信息

127.0.0.1:6379> info clients
# Clients
connected_clients:502
client_longest_output_list:4869
client_biggest_input_buf:0
blocked_clients:0

其中,client_longest_output_list代表输出缓冲区列表最大对象数,这两种统计方法的优劣势和输入缓冲区是一样的

相对于输入缓冲区,输出缓冲区出现问题的概论比较大,此时要如何预防?

1、此时首先要进行上述的监控手段,然后设置阀值,超过阀值及时进行处理。

2、限制客户端输出缓冲区,这样就不会因为输出缓冲区过大出现maxmemory满了后删除键值、OOM等情况。例如:

client-output-buffer-limit normal 20mb 10mb 120

3、如果master节点写入较大时,slave客户端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出被kill,会造成复制重连,所以此时就可以适当的增大输出缓冲区。

4、限制容易让输出缓冲区增大的命令,例如高并发下的monitor命令就是一个危险的命令。

5、及时监控内存,如果发现内存抖动频繁,可能就是输出缓冲区过大。

(4)客户端的存活状态

client  list中的age和idle分别代表当前客户端已经连接的时间和最近一次的空闲时间:
例如:这条记录代表当期客户端连接Redis的时间为603382秒,其中空闲了331060秒

id=2232080 addr=10.16.xx.55:32886 fd=946 name= age=603382 idle=331060 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get

例如:这条记录代表当期客户端连接Redis的时间为8888581秒,其中空闲了8888581秒,实际上这种就属于不太正常的情况,当age等于idle时,说明连接一直处于空闲状态。

id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ge

(5)、客户端的限制maxclients和timeout

其中maxclients用来限制最大客户端连接数,一旦连接数超过maxclients,新的连接就会被拒绝。maxclients的默认值时10000,可以用info  clients来查询当前redis的连接数:

127.0.0.1:6379> info clients
# Clients
connected_clients:1414
...

可以用config  set  maxclients对最大客户端连接数进行动态设置:

127.0.0.1:6379> config get maxclients
1) "maxclients"
2) "10000"
127.0.0.1:6379> config set maxclients 50
OK
127.0.0.1:6379> config get maxclients
1) "maxclients"
2) "50"

在一些业务处理下存在使用不当导致大量的客户端连接问题,因此redis提供了timeout(单位秒)来限制连接的最大空闲时间,一旦连接空闲时间超过timeout,连接就会被关闭。

一旦出现空闲时间超时再次执行命令就会报连接异常JedisConnectionException,并且提示Unexpected end of stream:

stream :
world
Exception in thread "main" redis.clients.jedis.exceptions.JedisConnectionException:
Unexpected end of stream.

如果将reids的loglevel日志级别设置位debug级别,则可以看到如下日志:

12885:M 26 Aug 08:46:40.085 - Closing idle client

redis中timeout的默认值是0,也就是不限时。实际上我们应该位每个连接设置timeout,以防大量空闲连接。同时可以在使用redis时添加上空闲检测和验证等等措施,例如JedisPool使用common-pool提供的三个属性:minEvictableIdleTimeMillis、testWhileIdle、timeBetweenEvictionRunsMillis。

(6)、客户端类型

client  list命令返回的flag用于标识客户端的类型,其中flags=S代表slave客户端、flags=N代表普通客户端,flags=O

代表当前客户端正在执行monitor命令,下面列举了11种客户端类型:

 (7)其他

2、client setName  和  client  getName

client  setName用于给当前客户端设置名字

127.0.0.1:6379> client setName test_client
OK

client  getName 获取当前客户端的名字

127.0.0.1:6379> client getName
"test_client"

3、 client  kill

client kill ip:port

用于杀掉指定IP地址和端口的客户端。

4、client  pause

用于阻塞当前客户端,单位毫秒:

127.0.0.1:6379> client pause 10000
OK

此时阻塞了当前客户端10秒。

该阻塞只对普通和发布订阅客户端有效,对主从复制客户端是无效的。但一般是不建议使用这个命令,因为在生产环境下暂停客户端的成本非常高。

5、monitor

monitor命令用于监控redis其他客户端正在执行的命令,并记录详细的时间戳。

 虽然monitor可以监听其他客户端正在执行的命令,但事实并非如此美好,每个客户端都有自己的输出缓冲区,既然然monitor能监听到所有的命令,一旦redis的并发过大,monitor的客户端输出缓冲会爆涨,可能会瞬间占用大量内存,或者导致monitor客户端连接端口等问题。

三.二、客户端的相关配置

其实上面使用clients list返回值已经说明了部分的客户端配置,这里将对剩下的配置进行介绍。

1、timeout:客户端空闲空闲连接的超时时间,idle超过该时间就会关闭客户端,0则表示不进行检测。

2、maxclients:客户端最大连接数

3、tcp-keepalive:检测TCP连接活性的周期,默认值为0,也就是不进行检测。建议设置为60,这样每隔60秒就对创建的TCP连接进行活性检测,防止大量死连接占用系统资源。

4、tcp-backlog:TCP三次握手后,会将accept的连接(已完成TCP三次握手的连接)放入该队列,tcp-backlog就是该队列(accept队列)的大小,默认值是511,通常这个参数不需要进行调整,但是这个参数会受到操作系统的影响。

例如在Linux操作系统中,如果/proc/sys/net/core/somaxconn小于tcp-backlog,那么在Redis启动时会看到如下日志,并建议将/proc/sys/net/core/somaxconn设置更大。

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

(更改此参数一般还需要调整somaxconn和tcp_max_syn_backlog OS参数:https://www.cnblogs.com/z-books/p/7279218.html

修改方法也非常简单,只需要执行如下命令:

echo 511 > /proc/sys/net/core/somaxconn

三.三、客户端统计片段

1、info  clients

127.0.0.1:6379> info clients
# Clients
connected_clients:1414
client_longest_output_list:0
client_biggest_input_buf:2097152
blocked_clients:0

1)connected_clients:代表当前Redis节点的客户端连接数,需要重点监控,一旦超过maxclients,新的客户端连接将被拒绝。
2)client_longest_output_list:当前所有输出缓冲区中队列对象个数的最大值。
3)client_biggest_input_buf:当前所有输入缓冲区中占用的最大容量。
4)blocked_clients:正在执行阻塞命令(例如blpop、brpop、brpoplpush)的客户端个数。

除此之外还通过info stats提供了两个客户端相关的统计指标

2、info  stats

# Stats
total_connections_received:80
...
rejected_connections:0

1)、total_connections_received:Redis自启动以来处理的客户端连接数总数。
2)、rejected_connections:Redis自启动以来拒绝的客户端连接数,需要重点监控。

四、客户端常见异常

1、无法从连接池获取连接

JedisPool中的Jedis对象个数是有限的,默认是8个,如果8个Jedis对象被占用了并没有归还,此时如果有新的调用者要从JedisPool中获取Jedis,此时就需要进行等待(一般获取的时候我们会设置maxWaitMillis>0),此时如果获取超时就会抛出以下异常:

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource
from the pool
…
Caused by: java.util.NoSuchElementException: Timeout waiting for idle object
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.
java:449)

或者设置了blockWhenExhausted=false,那么调用者发现连接池没有资源正则不会进行等待直接抛出异常。

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource
from the pool
…
Caused by: java.util.NoSuchElementException: Pool exhausted
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.
java:464)

那么为什么会造成连接池没有资源?这就存在多个原因了。

1)、客户端:高并发下连接池设置过小,但一般情况设置比8略高就行,因为JedisPool和Jedis的处理效率足够高。

2)、客户端没有正确使用连接池,例如在连接使用后没有进行释放归还给连接池。(close()方法)

3)、客户端存在慢查询操作,导致持有jedis的时间增长最后导致资源不够

4)、redis服务端执行一些命令导致执行过程阻塞,从而导致这种异常。

2、客户端读写超时

Jedis在调用redis时,如果出现读写超时,就会报以下异常:

redis.clients.jedis.exceptions.JedisConnectionException:
java.net.SocketTimeoutException: Read timed out

造成这样异常的原因可能如下:

读写超时时间设置过短、命令本身执行时间过长、网络问题、redis自身发生阻塞。

3、客户端连接超时

当我们从连接池拿到jedis对象或者自己获取jedis对象后,我们使用jedis调用redis时,如果出现连接超时的情况,会出现下面的异常:

redis.clients.jedis.exceptions.JedisConnectionException:
java.net.SocketTimeoutException: connect timed out

造成的原因可能如下:
1)、连接超时时间设置过短,此时可以通过下面伪代码进行设置:

// 毫秒
jedis.getClient().setConnectionTimeout(time);

2)、reids发生阻塞,造成tcp-backlog已满(已三次握手成功的TCP连接),造成新的连接失败。

3)、网络问题

4、客户端缓冲区异常

jedis调用redis时,如果出现客户端数据流异常,会出现下面的异常:

redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.

造成这个原因可能如下:

1)、输出缓冲区满了,这时可以看看有没有对客户端的输出缓冲区进行限制。

2)、长时间限制连接,或者超过空闲超时时间,从而导致的客户端连接断开。

3)、不正常的并发读写:jedis对象被多个线程并发操作,可能会出现上述异常。

5、Lua脚本正在执行

如果redis正在执行Lua脚本,且其执行时间超过lua-time-limit,此时jedis调用redis时,会收到下面的异常。对于如何处理这类问题,之前在讲过了(可以进行打断之类的)。

redis.clients.jedis.exceptions.JedisDataException: BUSY Redis is busy running a
script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.

6、redis正在加载持久化文件

jedis调用redis时,如果redis正在加载持久化文件,那么就会收到下面的异常:

redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the
dataset in memory

7、redis使用的内存超过maxmemory配置

如果jedis执行操作时,redis使用内存大于maxmemory的设置,就会接收到下面的异常,此时应该调整以下maxmemory并找到造成内存增长的原因:

redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when
used memory > 'maxmemory'.

8、客户端连接数过大

如果客户端连接数超过maxclients,新申请的连接就会出现如下异常:

redis.clients.jedis.exceptions.JedisDataException: ERR max number of clients reached

解决这种异常一般要以两个方面进行解决:
1)、客户端:如果maxclients参数不是很小的话,应用方的客户端连接数基本不会超过maxclients,通常这个时候都是应用方对于redis连接使用不当造成的。如果此时应用方是分布式结构的话,此时可以通过先下线占用连接较多的应用,使得redis的连接数先降下来,从而让绝大多数的节点可以正常运行,然后再慢慢进行问题定位。

2)、服务端:如果此时客户端那边无法进行解决,而此时redis是高可用模式(例如Redis Sentinel和Redis Cluster),可以考虑下将当前redis做故障转移。(但需要注意的是最终都需要对故障进行定位,才能真正的解决这个问题)

五、客户端线上问题案例分析

一、redis内存突然陡增(案例一)

1、出现的现象

1)、服务端现象:redis主节点内存陡增,几乎用满maxmemory,而从节点内存并没有什么变化(一般情况下主从节点内存的使用量是相差不大的)。

 2)、客户端现象:客户端产生了OOM异常,也就是主节点的内存超过了maxmemory的设置。

redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when
used memory > 'maxmemory'

分析原因:
1)、确实有大量写入,但主从复制出现了问题。此时我们着手对主从节点的键值对个数进行查询

主节点的键个数:

127.0.0.1:6379> dbsize
(integer) 2126870

从节点的键个数:

127.0.0.1:6380> dbsize
(integer) 2126870

此时发现个数是一致的,所以可以证明不是主从复制的问题。那么可能就是主节点的单独的操作问题。

2)排除了主从复制的问题,此时确定是主节点自己的问题,此时就对主节点进行排查,此时使用info clients看看是否是因为缓冲区造成主节点内存陡增的。

127.0.0.1:6379> info clients
# Clients
connected_clients:1891
client_longest_output_list:225698
client_biggest_input_buf:0
blocked_clients:0

此时发现最大的输出缓冲区是20万个对象,这个明显是不太正常的,于是为了获取到详细的客户端连接信息,此时则用client list命令获取并过滤出omem不为0的信息(一般输出缓冲区的数据处理是很快的,而omem表示输出缓冲区的总字节数,所以一般正常的omem都是为0).

redis-cli client list | grep -v "omem=0"

此时找到了一条记录:
 

id=7 addr=10.10.xx.78:56358 fd=6 name= age=91 idle=0 flags=O db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=224869 omem=2129300608 events=rw cmd=monitor

此时我们看到flags=O(表示正在执行monitor命令的客户端)和cmd=monitor(最后执行的命令),此时可以明显的看出这就是客户端在执行monitor命令造成的。


处理方案:

对于这个问题的处理相对比较简单,只要使用client  kill命令处理掉这个连接,此时其他客户端就能正常进行写数据了,但是我们需要进行真正的预防的是避免以后这种问题的发生,基本上有下面三点:
1)、从运维层面禁止monitor命令,例如使用rename-command命令重置monitor命令为一个随机字符串(这样就调不到这个命令了),如果没有对monitor命令进行rename-command,那么就需要对monitor命令进行相应的监控。

2)、从开发层面,禁止在生产环境中使用monitor命令,因为有时候monitor命令在测试的时候还是比较有用的,完全禁止也不太现实。

3)、限制输出缓冲区的大小(防止造成数据丢失,键值淘汰的情况)

4)、使用专业的reids运维工具。在运维工具中对于出现异常信息时会接收到相应的报警,这样就可以快速发现和定位问题。


二、客户端周期性的超时(案例二)

1、现象

客户端现象:客户端出现大量超时,经过分析发现超时是周期性出现的。然后出现以下异常信息:

Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.
SocketTimeoutException: connect timed out

服务端现象:服务端并没有明显的异常,只是有一些慢查询的操作。

2、分析:

1)、网络问题:后面经过观察发现网络是正常的。

2)、redis本身:经过观察redis日志统计,并无发现异常。

3)、客户端:由于是周期性出现问题,就和慢查询日志的历史记录对应了一下时间,发现只要慢查询出现,客户端就会产生大量连接超时,这两个时间基本一致:

 此时可以基本认为,这就是慢查询操作造成的。慢查询中有存在hgetall(复杂度为O(n))的操作,返回的结果是有200万个元素,这种操作必然是会造成redis阻塞的。

解决方案:
1)、运维层面,需要对慢查询进行监控,一旦超过阀值就触发报警。

2)、从开发层面,加强对redis的理解(例如各种命令的复杂度),避免不正确的使用。

3)、使用专业的redis运维工具。

六、持久化之RDB(默认开启)

图解 Redis丨这就是 RDB 快照,能记录实际数据的 - 华为云开发者社区 - 博客园 (cnblogs.com)

redis支持RDB和AOF(后面的版本支持混合双开)的持久化机制,持久化机制有利于避免因进程退出而导致的数据丢失问题。

一、RDB

RDB持久化就是将数据生成快照保存到硬盘的过程,触发RDB持久化分为手动触发和自动触发。

1、手动触发(save、bgsave命令)

1)、save命令:阻塞当前reids,直到RDB过程完成,对于内存比较大的实例会造成长时间阻塞,一般不建议使用。

2)、bgsave:redis进程执行fork操作创建一个子进程进行RDB操作。完成后自动结束。只在fork阶段阻塞(时间很短)。

很显然,bgsave是基于save进行优化的,因此redis内部所有设计RDB操作都采用的是bgsave的方式,而save命令已经被废弃了。

除了手动触发外,redis内部还存在自动触发RDB的持久化机制。

2、自动触发

1)、在redis.conf配置文件中配置,如“save  m  n”,表示m秒内存在n次修改时,就触发bgsave。

2)、如果从节点执行全量复制操作,主节点会自定执行bgsave生成RDB文件并发送给从节点。

3)、执行debug  reload命令重新加载redis时,也会自动触发save操作。

4)、默认情况下执行shutdown时,如果没有开启AOF持久化功能则自动执行bgsave。

3、bgsave触发的RDB持久化流程(也使用了写时复制机制)

1)、执行bgsave命令,此时父进程会看是否有正在执行的子进程,如RDB/AOF子进程,有则直接返回。

2)、没有则父进程执行fork操作创建一个子进程(这个过程会阻塞)(通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。)

3)、fork完子进程后,父进程则可以继续执行其他命令。

4)、子进程被创建出来后则根据父进程内存生成临时快照文件,完成后对原有旧文件进行原子替换。(执行lastsave命令可以获取最后一次生成RDB的时间,对应info统计的rdb_last_save_time选项。)

5)、子进程完成后发送信号给父进程,父进程更新统计信息。子进程结束退出。

4、RDB文件的处理

 1)、保存路径:RDB文件会保存在配置文件中dir配置指定的目录下,文件名通过dbfilename配置指定。

也可以通过动态执行config set dir{newDir}和config set dbfilename{newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。

2)、压缩:redis默认使用LZF算法对生成的RDB文件做压缩处理,压缩后的文件远远小于内存大小,默认是开启的,可以通过config set rdbcompression{yes|no}动态修改。

3)、校验:如果Redis加载损坏的RDB文件时拒绝启动,并打印如下日志:

# Short read or OOM loading DB. Unrecoverable error, aborting now.

这时可以使用Redis提供的redis-check-dump工具检测RDB文件并获取对应的错误报告。

5、RDB的优缺点

1)、优点

RDB是一个紧凑压缩的二进制文件,比较适合备份、全量复制等场景。例如每6个小时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。

RDB恢复数据远远快于AOF方式

RDB文件大小相较于AOF文件比较小

2)、缺点

每次fork都是重量级操作,频繁执行成功高,不适合实时持久化。

RDB文件使用特定二进制格式进行保存,redis版本演进会改变格式,从而导致无法兼容。

因为无法实时持久化,所以相较于AOF方式,RDB丢失的数据量比较多。(RDB就是上一次刷盘完成到下一次的刷盘执行之前,这个时间段是会产生部分内存数据的,如果此时redis宕机了则会丢失掉这部分数据。而AOF的刷盘间隔是1s,相对来说丢失的数据很少)

七、持久化之AOF(默认不开启)

AOF(append only file)持久化,以独立日志的方式记录每次的写入命令。重启时再重新执行AOF文件中的命令达到恢复数据的目的。

AOF的主要作用是解决了数据持久化的实时性(可以进行短间隔的持久化操作,丢失的数据很少)

1、使用AOF

开启AOF功能需要在配置文件中配置 appendonly yes ,AOF文件名通过配置文件中的appendfilename配置进行设置,默认文件名是appendonly.aof。保存路径和RDB的一样,通过dir配置指定。

AOF的工作流程:

 1)、append命令写入:将所有的写入命令追加到aof_buf(缓冲区)中。

2)、sync文件同步:AOF缓冲区根据相应的策略向硬盘的AOF文件做同步操作。

3)、rewrite文件重写:随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。

4)、load(重启加载):当redis服务器重启时,可以加载AOF文件进行数据恢复。


下面对于AOF的几个流程进行详细的分析:

2、命令写入

AOF命令写入的内容直接是文本协议格式。例如set  hello  world这条命令,在AOF缓冲区中追加如下文本:

*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n

(这种格式和之前发送给redis的命令格式类似)(相较于RDB文件,此时存储的数据长度是比较长的)

那么AOF为什么要采用文本协议格式?

——————文本协议具有很好的兼容性,文本协议具有可读性,方便直接修改和处理。

AOF为什么把命令追加到aof_buf中?(因为使用了缓冲区,如果突然宕机,也存在少量数据丢失的情况)

——————redis使用单线程响应命令,如果每次写AOF文件都是直接追加到磁盘,那么性能会降低很多。先将数据写入到缓冲区,此时redis可以提供多种缓冲区同步到硬盘的策略,在性能和安全方面做出平衡。

3、文件同步

redis提供了多种AOF缓冲u同步硬盘文件的策略,由配置文件中的appendfsync控制,对应的策略如下:

 在说明这个之前,需要先知道两个概念,write和fsync操作

(1)write操作:会触发延迟写机制(delayed write)。linux在内核中提供页缓冲区来提高硬盘IO性能。write操作在写入系统缓冲区后直接返回。同步硬盘操作依赖于系统调度机制(就是该刷盘由系统调度机制决定),例如:缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。

(2)fsync操作:针对单个文件操作,做强制硬盘同步,fsync会阻塞直到写入硬盘完成后返回。

除了write、fsync,Linux还提供了sync、fdatasync操作。

如上面所诉,此时再来看这三个值对应的配置.

  • 配置为no时,写入缓冲区后就不理了,最多30秒操作系统会进行刷盘操作
  • 配置为ererysec(推荐使用),默认的配置。写入缓冲区后,会有专门的线程每秒调一次刷盘操作。(理论上是丢失的数据不是1秒,后面会说明)
  • 配置为always,写入缓冲区后同步进行刷盘操作。

4、重写机制

随着命令不断追加到AOF文件中,文件会越来越大,为了解决这个问题,redis引入了AOF重写机制压缩了文件的体积。

重写后的AOF文件为什么可以变小?
——进程内已经超时的数据不再写入文件

——旧的AOF文件含有无效命令,清除掉这些命令,这样AOF文件只保留最终数据的写入命令。

——多条写命令可以合并为一个(减少空间)


AOF的重写过程可以手动触发和自动触发:

(1)手动触发:直接调用bgwriteaof命令

(2)自动触发:可以在配置文件中配置auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数来确定自动触发时机。

auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB。

auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。

自动触发时机=aof_current_size>auto-aof-rewrite-min-size&&(aof_current_sizeaof_base_size)/aof_base_size>=auto-aof-rewrite-percentage

其中aof_current_size和aof_base_size可以在info Persistence命令统计信息中查看。

当触发AOF重写时,其重写流程如下:

执行流程:

(1)、AOF重写请求

如果当前进程正在执行AOF重写,则请求不执行并返回。

如果当前进程正在执行bgsave操作,则该请求延迟到bgsave完成后再执行。此时返回如下响应:

Background append only file rewriting scheduled

( 2)、父进程执行fork操作创建子进程,开销等同于bgsave过程。

(3.1)、主进程fork操作完成后,继续执行其他命令。所有修改命令依然写入AOF缓冲区并根据appendfsync策略同步到硬盘,保证原有AOF机制正确性。

(3.2)、由于fork操作运用写时复制技术,子进程只能获取到fork操作前的内存数据。而fork后fork仍然会进行响应其他命令,所以redis使用“AOF重写缓冲区”保存这部分新的数据,防止新的AOF文件生成期间丢失掉这部分数据。

(4)、子进程根据内存快照写入新的AOF文件。每次批量写入硬盘数据量由配置aof-rewrite-incremental-fsync控制,默认为32MB,防止单次刷盘数据过多造成硬盘阻塞。

(5.1)、新的AOF文件写入成功后发送信号给父进程,父进程更新统计信息,具体见info persistence下的aof_*相关统计。

(5.2)、父进程把AOF重写缓冲区的数据追加到新的AOF文件中。

(5.3)、使用新的AOF文件替换老文件,完成AOF重写所有流程。


5、重启加载

AOF和RDB文件都可以用于服务器重启时的数据恢复。如下为redis持久化文件加载流程:

(1)AOF持久化开启且存在AOF文件时,优先加载AOF文件,打印如下日志:

* DB loaded from append only file: 5.841 seconds

(2)AOF关闭或者AOF文件不存在时,加载RDB文件,打印如下日志:

* DB loaded from disk: 5.586 seconds

(3)加载AOF/RDB文件成功后,Redis启动成功。
(4)AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。


6、文件校验

加载损坏的AOF文件会拒绝启动,对于错误格式的AOF文件,先进行备份,然后采用redis-check-aof  --fix命令进行修复,修复后使用diff-u对比数据的差异,找出丢失的数据,有些可以进行人工修改补全。

AOF文件可能存在结尾不完整的情况,比如机器突然掉电导致AOF尾部文件命令写入不全。Redis为我们提供了aof-load-truncated配置来兼容这种情况,默认开启。加载AOF时,当遇到此问题时会忽略并继续启动,同时打印如下警告日志:

# !!! Warning: short read while loading the AOF file !!!
# !!! Truncating the AOF at offset 397856725 !!!
# AOF loaded anyway because aof-load-truncated is enabled

八、持久化之问题定位和优化

一、fork操作(fork过程是阻塞的)

        当redis做RDB(RDB的重写流程和AOF的类似)或AOF重写时,都需要执行一个fork操作创建子进程(这对大多数操作系统来说是一个重量级操作),虽然fork创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页表(例如10GB内存的redis进程,大概需要复制20MB左右的内存页表),因此fork操作耗时和进程内存息息相关(如果使用虚拟化技术,例如Xen虚拟机,fork会更加耗时)。

        fork耗时时间问题定位:如果fork操作耗时在秒级别的化那将会拖慢redis几万条命令执行,对线上应用延迟影响非常明显。一般每GB会消耗20毫秒左右,可以用info  stats统计命令中查看latest_fork_usec指标获取最后一次fork操作耗时(单位微妙)。

既然我们知道了fork过程是比较耗时的,那么我们如何改善优化fork的耗时:
1)、优先使用物理机或高效支持fork操作的虚拟化技术(避免使用Xen虚拟机)

2)、控制redis实例最大可以内存(一般建议10GB以内,fork耗时和内存量成正比)

3)、合理配置linux内存分配策略,避免物理内存不足导致fork失败(后面会说明怎么配置)

4)、减少fork操作频率(如放宽AOF自动触发时机),避免不必要的全量复制。

二、子进程开销监控和优化

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

1、CPU

CPU开销分析:子进程会将进程内的数据分批写入文件,这个过程是CPU密集操作,通常子进程对单核cpu利用率接近90%。

cpu消耗优化:redis本身是cpu密集型服务,不要做绑定单核cpu操作。不然子进程会和父进程产生单核资源竞争。

尽量不要和其他cpu密集型服务部署在一起,以免造成cpu过度竞争。

如部署多个redis实例,尽量保证同一时刻只有一个子进程执行重写(后面会讲)

2、内存

内存消耗分析:子进程通过fork操作产生(之前说的复制内存页表只是为fork操作做的复制,而不是这里的重写操作),子进程产生后理论上最多占用的内存约等于父进程的大小。理论上是需要两倍的内存完成持久化操作(虽然linux有写时复制操作(写操作时会复制副本copy-on-write),就是可能父进程存在写操作所以理论最多需要两倍(但一般不会需要那么多)),父子进程会共享相同的物理内存页,父进程有写请求时,对应写请求的内存页就会使用写时复制技术创建内存页快照,然后再和子进程共享这些内存快照(当所以键都有写操作时就需要全量创建快照,就是所谓的理论上的2倍内存大小)。

对RDB内存消耗进行监控,redis日志会输出内容如下:可以看到就只创建了5MB大小的快照

* 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

对于AOF监控,日志如下:

* 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重写缓冲区,因此总消耗为53+1.49MB。

可以编写shell脚本监控redis重写的消耗情况。

内存消耗优化:
1)、如部署多个redis实例,尽量保证同一个时刻只有一个子进程在工作

2)、避免做子进程重写过程进行大量的写操作,这样可以尽量减少父进程维护的页副本,减少内存消耗。

注意:Linux kernel在2.6.38内核增加了Transparent Huge Pages(THP),支持huge page(2MB)的页分配,默认开启。当开启时可以降低fork创建子进程的速度,但执行fork之后,如果开启THP,复制页单位从原来4KB变为2MB,会大幅增加重写期间父进程内存消耗。

建议设置“sudo  echo never>/sys/kernel/mm/transparent_hugepage/enabled”关闭THP。

3、硬盘

重写过程,会将内存的数据写入硬盘中,如果大量写入,势必会造成硬盘写入压力,可以使用sar、iostat、iotop等命令去查看重写期间硬盘的负载情况。

硬盘优化:
1)、尽量不要和高硬盘负载的服务部署在一起(如存储服务、消息队列服务)

2)、AOF重写会消耗大量磁盘IO,可以配置让AOF重写期间不进行刷盘操作。使用no-appendfsync-on-
rewrite开启这个功能,默认关闭。(配置no-appendfsync-on-rewrite=yes时,在极端情况下可能丢失整个AOF重写期间的数据,需要根据数据安全性决定是否配置。)

3)、当开启AOF功能的Redis用于高流量写入场景时,如果使用普通硬盘其吞吐一般在100MB/s左右,这时可以尽量使用吞吐更好的固态硬盘等。

4)、对于单机配置多个redis实例的情况,可以配置不同实例分盘存储AOF文件,这样可以分摊硬盘写入压力。

三、AOF追加阻塞

但开启AOF持久化时,常用的同步硬盘的策略是everysec(redis使用另外的线程每1秒刷盘操作),用于平衡性能和数据安全性。

而实际中,并不是一秒就固定进行刷盘操作,而是一秒去做一些流程判断,符合后再进行刷盘。而且这个流程中可能出现阻塞redis主线程的情况。

流程如下:

1)、主线程负责将追加的数据写入AOF缓冲区

2)、会有专门的AOF线程每秒执行一次同步磁盘操作,并且记录最后一次同步时间

3)、主线程会区比对上次AOF同步的时间:

如果相差在2秒内,则主线程不做任何操作。如果超过2秒,则主线程会阻塞,直到这次同步操作完成。

 此时就会发现我们之前说的问题,AOF的everysec配置最多会丢失2秒数据(因为是存在相差两秒的情况的),而不是1秒。而且如果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中为我们提供了监控子进程运行状况的度量指标。

 我们基于以上指标,可以通过外部程序轮询控制AOF重写操作的执行

流程说明:
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重写串行化执行。

AOF和RDB各自的优缺点:
 

RDB优势:
1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。

2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。

3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。

4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。

RDB劣势:
1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。

2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

AOF优势:
1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。

2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。

3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。

4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。

AOF劣势:
1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。

二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。

RDB持久化配置

Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:

save 180 1              #在180秒(3分钟)之后,如果至少有1个key发生变化,则dump内存快照。

save 120 10            #在120秒(2分钟)之后,如果至少有10个key发生变化,则dump内存快照。

save 60   100          #在60秒(1分钟)之后,如果至少有100个key发生变化,则dump内存快照。

表示在N秒之内,redis至少发生M次修改则redis抓快照到磁盘。当然我们也可以手动执行save或者bgsave(异步)命令来做快照

AOF持久化配置

在Redis的配置文件中存在三种同步方式,它们分别是:

appendfsync always     #每次有数据修改发生时都会写入AOF文件。默认的是每秒强制写入磁盘一次

appendfsync everysec  #每秒钟同步一次,该策略为AOF的缺省策略。每次执行写操作的时候就强制写入磁盘

appendfsync no          #从不同步。高效但是数据不会被持久化。性能最好但是持久化没法保证

(AOF和RDB的重写都使用了写时复制机制,只是AOF多了一个AOF重写缓冲区用来存储重写过程新产生的内存数据。AOF对于数据的完整性更好)

对于持久化策略使用RDB还是AOF,其实尽量不要单独使用某个策略,这两个策略各有优缺点,

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分就是压缩格式不再是 AOF 格式,可读性较差。


版权声明:本文为qq_35599414原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。