Redis Sentinel(哨兵模式)

 

本文永久地址:https://www.cnblogs.com/erbiao/p/9156215.html

 

Redis官方文档 https://redis.io/topics/sentinel#redis-sentinel-documentation

 

 

Redis Sentinel(Sentinel)用于为Redis提供高可用性,这就意味着使用sentinel能创建一个故障时不需要人工立即参与修复的环境。此外,sentinel还能实现其他的功能,如监控,提醒,为客户端提供配置( monitoring, notifications and acts as a configuration provider for clients.)

 

Redis sentinel功能

  ·监控(Monitoring)sentinel会不间断地检查Redis master和Redis slave是否正常运行

  ·提醒(Notification):当其中一个被监控的Redis实例出现问题,sentinel能通过API或其他程序通知管理员

  ·自动故障转移(Automatic failover):当Redis master故障不能正常工作时,sentinel会故障切换进程,将一个slave提升为master,另外的Redis slave将更新配置使用新的master,此后有新连接时,会连接到新的Redis master

  ·配置提供者(Configuration provider)Redis充当客户端服务发现的权威来源:客户端连接到sentinel,以请求当前可靠的Redis master地址,若发生故障转移,sentinels将报告新地址

 

Sentinel的分布式特性

  Redis sentinel是一个分布式系统,可以在一个架构中运行多个sentinel进程,优势如下:

    ·  当多个sentinels确定master不再可用,就进行故障检测,这降低了误报的可能性

    ·  当在不同服务器上运行多个sentinel进程,然后将sentinel做集群,即使其中一个故障,也可以进行热切换,降低对客户端的影响,从而提升了系统健壮性

    ·  Redis客户端可连接任意sentinel来使用Redis集群

 

关于sentinel版本

  Sentinel当前版本被称为sentinel 2,它是使用更强大和更简单的预测算法(在本文档中进行了解释)重写了最初的Sentinel实现。自Redis 2.8版本,Redis sentinel发布了稳定版本。Redis Sentinel版本1与Redis 2.6一起发布,但已弃用,不建议使用

  Sentinel 程序是redis编译安装完成后src目录下的“redis-sentinel”文件

 

启动sentinel

  使用redis-sentinel启动:

    redis-sentinel sentinel.conf

  使用redis-server以sentinel模式启动:

    redis-server sentinel.conf --sentinel

 

部署Redis sentinel前要了解

  ·  Sentinel运行默认侦听端口26379

  ·  运行sentinel必须指定配置文件,因为系统使用此文件来保存当前状态,一遍重启sentinel时重新加载。指定的配置文件有问题或不指定配置文件,sentinel会拒绝启动;

  ·  至少三个sentinel实例才能提升系统健壮性,因为自动故障转移时,必须有剩余大多数sentinels存活,且sentinels间能互相通信

  ·  三个sentinel实例应放在相对独立的虚拟机,甚至物理机,甚至不同区域

  ·  由于Redis使用异步复制,sentinel+Redis不能保证故障期间保留已确认的写入,但可配置sentinel允许丢失有限的写入。另外还有一些安全性较低的部署方式

  ·  使用的客户端要支持sentinel,大多数热门的都支持sentinel,但不是全部

  ·  没有完全健壮的HA设置,所以要经常在测试环境中测试

  ·  sentinel在docker、端口映射或网络地址转换的环境中配置要格外小心: 在重新映射端口的情况下,真实端口可能与转发的端口不同,会破坏Sentinel自动发现其他的sentinel进程和master的slave列表。

 

配置sentinel

  Redis安装完成会生成Redis根目录下的sentinel.conf配置文件,最小配置如下:

    bind 0.0.0.0 #侦听地址

    port 26379 #默认侦听端口

    dir /tmp #sentinel工作目录

    sentinel monitor mymaster 127.0.0.1 6379 2

    sentinel down-after-milliseconds mymaster 60000

    sentinel failover-timeout mymaster 180000

    sentinel parallel-syncs mymaster 1

 

    sentinel monitor resque 192.168.1.3 6380 4

    sentinel down-after-milliseconds resque 10000

    sentinel failover-timeout resque 180000

    sentinel parallel-syncs resque 5

 

  仅须指定masters去监控,每个不同的master都应该有不同的名称 ,由于slave是自动发现的,所以没必要指定,且sentinel会自动更新所有slave的状态,信息等到配置文件,以便在重新启动时保留信息。每次进行故障转移,slave被提升为master时,每当有新sentinel被发现时,配置都会被重写。

  上面两组示例配置主要监控两组redis实例,每个实例由一个master和未知数量的从节点组成,一组实例集合调用“mymaster”,另一组调用“resque”

 

  Sentinel monitor语法:

    sentinel monitor <master-group-name> <ip> <port> <quorum>

 

    ·  master-group-name:Redis master实例名称

    ·  ip,port:master IP、master PORT

    ·  quorum:将master判断为失效至少需要N个Sentinel同意,仅用于检测master连接失败。如redis集群中有5个sentinel实例,若这里的票数是2,master挂掉,表示有2个sentinel认为master挂掉才能被认为是真正的挂掉,此时会触发自动故障转移。其中sentinel集群中各个sentinel也能通过gossip协议互相通信。这意味着在故障期间,若大多数sentinel进程间无法互相通信,自动故障转移将不会被触发(aka no failover in the minority partition)

  

  那上述例子第一组Redis实例监控的master叫“mymaster”,连接地址为127.0.0.1:6379,当sentinel集群中有2个实例认为mymaster挂掉时,自动进行故障转移。

 

  上述最小配置中其它选项几乎总是下面形式:

    sentinel <option_name> <master_name> <option_value>

 

  用于配置以下参数:

  ·  down-after-milliseconds

  若服务器在给定的毫秒数之内, 没有返回Sentinel发送的PING命令的回复,或者返回一个错误, 那么Sentinel将这个服务器标记为主观下线(subjectively down,简称SDOWN)。

  不过只有一个Sentinel将服务器标记为主观下线并不一定会引起服务器的自动故障迁移:只有在足够数量的Sentinel都将一个服务器标记为主观下线之后, 服务器才会被标记为客观下线(objectively down,简称ODOWN),这时自动故障迁移才会执行。

  将服务器标记为客观下线所需的Sentinel数量由对master的配置决定

 

  ·  parallel-sync

  选项指定了在执行故障转移时,最多可以有多少个slave同时对新的master进行异步复制(并发数量),这个数字越小,完成故障转移所需的时间就越长(数字越小,同时能进行复制的slave越少)。

  slave被设置为允许使用过期数据集或未更新的数据集(参见redis.conf中对slave-serve-stale-data选项的说明),那么架构上可能不希望所有的slave与新master同一时间进行异步复制,因为尽管slave与master间的复制过程绝大部分步骤不会阻塞slave,但slave在载入master发来的RDB文件时,仍然会造成slave在一小段时间内不能处理命令,如果全部slave一起对新master进行异步复制,那么会造成所有slave在短时间内全部不可用的情况。

  当然可设置该值为1来保证每次只有一个slave与新 master进行异步复制,且不能处理命令。

 

  ·  failover-timeout

  故障转移的超时时间 failover-timeout(默认三分钟)可以用在以下这些方面:   

    ·  同一个sentinel对同一个master两次failover之间的间隔时间。  

    ·  当一个slave从一个错误的master那里异步复制数据开始计算时间,直到slave被纠正为向正确的master那里复制数据时。  

    ·  当想要取消一个正在进行的failover所需要的时间。    

    ·  当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了

 

  当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码  

  设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码  

    sentinel auth-pass <master-name> <password>

    sentinel auth-pass mymaster MySUPER--secret-0123passw0rd

 

本文档剩余的内容将对Sentinel系统的其他选项进行介绍, 示例配置文件sentinel.conf也对相关的选项进行了完整的注释。

 

Redis sentinel部署实例

  到目前为止,已经了解了sentinel基本信息,接下来将通过一系列示例来讲解架构中需要多少sentinel进程,如何放置sentinel进程。首先是一些说明:

    ·  Masters表示为:M1、M2、M3...Mn

    ·  Slaves表示为:R1、R2、R3...Rn(R stands for replica)

    ·  Sentinels表示为:S1、S2、S3...Sn

    ·  Clientis表示为:C1、C2、C3...Cn

    ·  当一个实例因Sentinel动作而改变角色时,我们把它放在方括号内,所以[M1]表示由于Sentinel干预而成为master的实例

 

  要注意的是,至少三个sentinel实例才能提升系统健壮性,因为自动故障转移时,必须有剩余大多数sentinels存活,且sentinels间能互相通信

 

  

 

 

  例一:2个独立系统,2个sentinel,2节点redis集群(1主1从)的情况(不建议的方案)

  

  ·  此种架构中,若master M1故障,2个sentinel完全可以就故障打成一致,然后授权自动进行故障切换,此时仍然正常的R1将被提升为master [M1]

  ·  但,若M1所在的系统故障宕机,S1也会停止工作,另一系统中运行的sentinel将无法授权故障转移,因此整个Redis集群将无法使用

 

  要注意,多数sentinel存活是必要的,主要应对不同的故障转移场景,然后将最新的配置信息发到至其他所有的sentinels。且,在没有任何投票协商的情况下,进行自动故障转移是非常危险的:

  

  上图中,两系统网络连接出现问题,会产生两个完全一样的master(假设S2未经投票协商自动进行故障切换),外部客户端会无休止的向两边写入同时数据,自此M1与[M1]获得的数据可能会有差异,当网络恢复正常时,Redis实例将无法判断哪份数据是正确的。此为脑裂。因此,至少在三个独立的系统中配置三个sentinel,以此解决脑裂的问题

 

  例二:3个独立系统,3个sentinel,3节点redis集群(1主2从)的情况(能保证redis sentinel集群安全的最基础方案)

  基于3个独立系统,每个系统都运行一个redis进程和sentinel进程

  

  若master M1故障,S2和S3会就故障达成一致,发起投票,提升一个slave为新master,且自动切换故障,因此客户端能继续请求Redis集群

 

  Sentinel和redis进程同样也是异步复制,当M1所在系统网络出现问题,断开与R2,R3的连接。假设为主写从读架构,那么在M1上确认了的写入请求将无法复制到slave(或者被提升为master的slave),如下图:

  

  此时,旧的master M1被网络隔离,因此slave R2被提升为master M2,然而与旧masterM1连接的客户端(如C1)将会继续写入旧数据。当连接恢复正常后,且旧master M1将更新配置,成为新master的slave,且丢弃所有数据集,从新master更新数据,如此便丢弃了连接断开期间客户端写入到M1的数据

  使用Redis复制功能选项能缓解此问题,该功能允许在master检测到不再能将写入操作复制到指定数量的slave时停止写入操作。

    min-slaves-to-write 1

    min-slaves-max-lag 10

 

    最少与1个slave正常通信且延迟小于10s才能接收客户端写入操作(复制是异步的,因此不能写入实际上意味着slave已断开连接,或者未发送超过指定max-lag秒数的异步确认)。

    使用此配置,上例中Redis 旧master M1将在10s后不可用。

    

    但,此配置是一把双刃剑,当两个slave关掉或者挂掉,master将停止接收写入请求,整个架构将变得只读。

 

  例三:sentinel与客户端运行在一个系统

  有时只有两个服务器可用于运行Redis实例,一主一从。例二中的配置在这个场景中不可用,因此我们将sentinel放在客户端系统上:

  

  此架构中,sentinel与客户端一样,若大多数客户端都可以访问master,那就没毛病。C1、C2、C3在这里表示通用客户端,如rails应用程序或者一个应用程序服务器。

  若M1和S1所在服务器系统挂掉,将开始故障切换,但不同的网络区域将会导致不同的行为,如客户端和Redis服务器间的网络断开,则sentinel将无法提升slave为新的master,此时Redismaster和Redis slave都将不可用。

 

  注意:若C3与M1是运行在同一系统 (hardly possible with the network described above, but more likely possible with different layouts, or because of failures at the software layer),有一个类似于例二中所描述的问题,区别在于不会产生脑裂,由于只有一主一从,且配置了min-slaves-to-write和min-slaves-max-lag,此时,master必须要能同时接受write/read请求,否则master与slave断开连接达到一定时间后,master将变的不可写,只可读。

  So this is a valid setup but the setup in the Example 2 has advantages such as the HA system of Redis running in the same boxes as Redis itself which may be simpler to manage, and the ability to put a bound on the amount of time amasterinto the minority partition can receive writes.

 

  例四:sentinel运行在每个系统,redis集群(一主一从),两个应用层客户端

  若客户端服务器不足3个,那现在所描述的配置在例三中就不在适用了,此处我们要混合配置,如下:

  

  与例三环境类似,但此处在四个系统都运行了sentinel,若master M1系统挂掉,其他三个sentinel将执行故障切换。理论上可以移除C2、S4,且设置quorum为2,然而此时应用层就会没有高可用了

 

Sentinel,docker,NAT以及可能的问题

  Docker使用端口映射技术:docker容器中运行的程序使用的端口可能会与暴露的端口不同,这对于在同一服务器中同时使用相同端口运行多个容器很有用。

  NAT技术中也可能存在端口映射,甚至是IP。

 

  重新映射IP或端口会产生两个问题:

  ·   sentinel的自动发现不再工作,因为sentinel通过hello包通告给其他sentinel自己的侦听信息(端口+IP)。Sentinel是无法识别IP或端口是否被重新映射的,因此错误的通告信息用于sentinel间连接会失败。

  ·  Redis master命令“INFO”输出slave的连接信息,该连接信息用于master用于检测远程连接,端口或地址是slave自身广播过来的,然而这个端口可能是错,无法用于master与slave间通信,就像上面一点那样。

 

  在docker环境下,sentinel使用master“INFO”输出信息自动检测slave,检测到的slave将不可达,且sentinel无法为master进行故障切换,因为从sentinel和master的角度看,没有一个slave是正常的。Sentinel也无法监控这组运行在docker中的master实例,除非配置docker为1:1的端口映射

  对于第一个问题,若场景需求要在网络映射环境下运行Redis实例或者sentinel实例,你可以使用以下两个sentinel配置来强制sentinel公布一个IP和端口:

    sentinel announce-ip <ip>

    sentinel announce-port <port>

 

  请注意,Docker能够在主机联网模式下运行(请查看--net=host选项以获取更多信息)。这应该不会造成任何问题,因为在此设置中不会重新映射端口。

 

主观下线和客观下线

  Redis中的sentinel有两个关于下线(down)的概念:

  ·  主观下线(Subjectively Down, 简称 SDOWN)指的是单个Sentinel实例对服务器做出的下线判断。

  ·  客观下线(Objectively Down, 简称 ODOWN)指的是多个Sentinel实例在对同一个服务器做出SDOWN判断, 并且通过SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。(一个Sentinel可以通过向另一个Sentinel发送“SENTINEL is-master-down-by-addr”命令来询问对方是否认为给定的服务器已下线)

 

  若一个服务器没有在“master-down-after-milliseconds”选项指定时间内,对向他发送“PING”的sentinel返回一个有效回复(valid reply),那么sentinel会将此服务器标记为主观下线(SDOWN)

  服务器对PING命令的有效回复可以是三种中的一种:

    ·  返回+PONG

    ·  返回-LOADING错误

    ·  返回-MASTERDOWN错误

  除此三种外的回复或者指定时间内没有回复,sentinel都会标记该服务器回复无效(non-vaild)

 

  注意,一个服务器必须在“master-down-after-milliseconds”周期时间(毫秒)返回无效回复才会被sentinel标记为主观下线,如若“master-down-after-milliseconds”为30000ms(30s),只要服务器在每29秒之内返回至少一次有效回复,此服务器就会被认为处于正常状态。

  从主观下线状态切换到客观下线状态并没有使用严格的法定人数算法(strong quorum algorithm),而是使用了流言协议:如果Sentinel在给定的时间范围内, 从其他Sentinel那里接收到了足够数量的master下线报告, 那么Sentinel就会将master的状态从主观下线改变为客观下线。 如果之后其他Sentinel不再报告master已下线, 那么客观下线状态就会被移除

  客观下线条件只适用于master:对于任何其他类型的Redis实例,Sentinel在将它们判断为下线前不需要进行协商,所以slave或者其他Sentinel永远不会达到客观下线条件

 

故障转移流程

  分为两部分,第一部分选出sentinel leader,第二部分进行故障转移:

  1、sentinel集群中的一个sentinel发现master疑似下线,标记该master的状态为主观下线(SDOWN)sentinel节点会每1s向master发送PING消息,若一个服务器没有在“master-down-after-milliseconds”选项指定时间内,对向他发送“PING”的sentinel返回一个有效回复(valid reply),那么sentinel会将此服务器标记为主观下线(SDOWN)】

  2、选出sentinel leader。该sentinel查看自己有无投给其他sentinel节点票,若已经投过,那么在两倍故障转移超时时间内不会成为leader,则转换身份为follower;没有投过票则申请成为leader(Candidate)。Sentinel集群正常运行时,每个节点epoch(版本号,具体是什么翻看本文章上下文)相同,当需要故障转移时会在sentinel集群中选出leader,由leader发起并执行故障转移操作。Sentinel采用Raft协议实现了Sentinel间选举Leader的算法(不过也不完全一样,具体参看http://weizijun.cn/2015/04/30/Raft协议实战之Redis%20Sentinel的选举Leader源码解析/),Sentinel集群故障转移完成后,所有Sentinel又会恢复平等。Leader仅仅是故障转移操作才会出现的角色

  3、Candidate(申请成为leader的sentinel节点)此时更新故障转移状态为“start”,使当前epoch自增1(进入新的选举周期),给自己投一票以及向其他sentinel节点请求投票等

  4、Candidate不断统计自己的票数,当达到一半且超过它配置的quorum的票数,那么它就成为Leader(若在一个选举时间内,Candidate没有获得超过一半且超过它配置的quorum的票数,此次选举失败,那么在设定的故障迁移超时时间的两倍之后, 重新尝试当选)

  5、sentinel leader迭代所有的sentinel follower节点,检测是否需要将该master标记为客观下线(ODOWN)状态,是则继续,否则取消选举投票

  6、选出合适的slave,sentinel leader使用以下流程来选择合适的slave:

    a、 ·在故障master属下的slave当中, 那些被标记为主观下线、已断线、或者最后一次回复PING命令的时间大于五秒钟的slave都会被淘汰

      ·在故障master属下的slave当中, 那些与失效master连接断开的时长超过 down-after 选项指定的时长十倍的slave都会被淘汰

    b、选择“slave-priority”(slave优先级)最高的slave,存在则选取该slave成为新master,存在相同优先级则继续向下

    c、复制偏移量(replication offset,即复制数据最完整)最大的那个slave作为新master,存在相同复制偏移量则继续向下

    d、选择“run_id”(“INFO SERVER”查看)最小的slave

  7、sentinel leader向被选中的slave发送命令“SLAVEOF NO ONE”,提升选中的slave为master

  8、通过发布与订阅功能,将更新后的配置传播给所有其他Sentinel, 其他Sentinel对它们自己的配置进行更新

  9、向已下线master的slave发送“SLAVEOF”命令,让它们从新master复制数据

  10、当所有slave都已经开始复制新master时,sentinel leader 终止这次故障迁移操作,所有Sentinel又会恢复平等身份

 

  每当一个 Redis 实例被重新配置(reconfigured) —— 无论是被设置成master、slave、又或者被设置成其他master的slave ——Sentinel都会向被重新配置的实例发送一个 CONFIG REWRITE 命令, 从而确保这些配置会持久化在硬盘里。

 

  Slave优先级

  Redis实例有配置参数“slave-priority”,此参数在“INFO”中也能查询,sentinel使用此配置从slave来挑选故障切换后的新master:        

    ·  “slave-priority”设置为0的slave,不能升级为master

    ·  优先级小的salve会优先考虑提升为master。如master有2个slave(S1,S2),优先级S1为10,S2为100,若master故障且S1,S2都可用,则S1将是新master首选

 

  Slave选举

  当一个sentinel leader实例准备进行故障转移,由于master处于ODOWN状态,且sentinel leader从已知的大多数sentinel实例收到故障转移授权,此时一个合适的slave就要被选举出来。slave选举考量以下几项:

    ·  与master断开时间

    ·  slave优先级

    ·  复制偏移量

    ·  运行ID

 

  一个Slave若被发现与master断开连接的时间超过master配置的超时时间(down-after-milliseconds选项)的十倍,加上从正在执行故障转移的sentinel leader角度看master不可用的时间,将被认为是不合适的且会被跳过。

  即slave若不可用时间超过以下时间,会被认为是不适合成为master的节点:

    (down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state

 

  通过了以上测试的slave会继续进行以下筛选:

    1、然后按照slave配置文件中“slave-priority”的大小进行排序,数字小且不为0的slave将被选择为新master

    2、“slave-priority”相同,则检查slave的复制偏移量,偏移量大(从旧master接收到更多的数据)的会被选择为新master

    3、若多个slave有相同的优先级和复制偏移量,则则选择“run_id”(INFO SERVER可以查看到)小的slave成为新master

 

  Redis集群中,若有合适的机器作为slave,强烈建议将其优先级数配置小一点,使其优先级高一些另外,复制偏移量相同的情况并不少见,且所有的redis实例都可以运行在默认“run_id”上,想想多么可怕...

 

 

每个sentinel都需要定期执行的任务

  ·  每个Sentinel以每秒钟一次的频率向它所知的master、slave以及其他Sentinel实例发送一个PING命令。

  ·  如果一个实例(instance)距离最后一次有效回复PING命令的时间超过 down-after-milliseconds 选项所指定的值, 那么这个实例会被Sentinel标记为主观下线。 一个有效回复可以是:+PONG、-LOADING或-MASTERDOWN

  ·  如果一个master被标记为主观下线, 那么正在监控这个master的所有Sentinel要以每秒一次的频率确认master的确进入了主观下线状态

  ·  如果一个master被标记为主观下线, 并且有足够数量的Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断, 那么这个master被标记为客观下线

  ·  在一般情况下, 每个Sentinel会以每 10 秒一次的频率向它已知的所有master和slave发送 INFO 命令。 当一个master被Sentinel标记为客观下线时,Sentinel向下线master的所有slave发送INFO命令的频率会从10秒一次改为每秒一次

  ·  当没有足够数量的Sentinel同意master已经下线master的客观下线状态就会被移除。 当master重新向Sentinel的PING命令返回有效回复时, master的主管下线状态就会被移除

 

“-BUSY”状态处理

  当Lua脚本运行时间超过配置的Lua脚本时限时,Redis实例将返回“-BUSY”错误,在触发故障切换前,Redis尝试发送一个“SCRIPT KILL”命令尝试杀死脚本执行进程,若脚本只读(the script was read-only),命令将执行成功。若该实例在尝试后仍处于错误状态,实例将进行故障切换

 

快速教程

  接下来,将逐步介绍所有关于sentinel API,配置和语义。同时,本节也将会介绍如何配置3个sentinel实例并与之交互。

  此处假设3个实例运行在端口5000,5001,5002,且有redis集群一主一从,分别运行在端口6379,6380,然后这些端口全部侦听在一个机器的127.0.0.1回环地址

 

  3个sentinel配置类似:

    port 5000

    sentinel monitor mymaster 127.0.0.1 6379 2

  sentinel down-after-milliseconds mymaster 5000

  sentinel failover-timeout mymaster 60000

  sentinel parallel-syncs mymaster 1

 

  其他两个sentinel配置修改端口为5001,5002即可

 

  以上:

  ·  Master实例组定义为mymaster。依据不同的master实例组名称,sentinel能同时监控不同的Redis主从集群

  ·  quorum设置为2,sentinel monitor配置指令的最后一个参数值

  ·  down-after-milliseconds值为5000ms,即5s,因此只要在这段时间没有收到master的ping回复,master就会被认定为连接失败

 

  启动sentinel会收到Redis集群登录消息:

    +monitormastermymaster 127.0.0.1 6379 quorum 2

  

    此为sentinel事件信息,也可使用“发布/订阅”接收这类事件。在故障检测和故障转移期间,sentinel会生成并记录不同的事件

 

  查询sentinel中master的状态

    redis-cli -p 5000Sentinelmaster mymaster

     1) "name"

     2) "mymaster"

     3) "ip"

     4) "127.0.0.1"

     5) "port"

     6) "6379"

     7) "runid"

     8) "8f315c998236a332cb327e6c33e5efead00f14c9"

     9) "flags"

    10) "master"

    11) "link-pending-commands"

    12) "0"

    13) "link-refcount"

    14) "1"

    15) "last-ping-sent"

    16) "0"

    17) "last-ok-ping-reply"

    18) "368"

    19) "last-ping-reply"

    20) "368"

    21) "down-after-milliseconds"

    22) "30000"

    23) "info-refresh"

    24) "2704"

    25) "role-reported"

    26) "master"

    27) "role-reported-time"

    28) "1318596"

    29) "config-epoch"

    30) "2"

    31) "num-slaves"

    32) "1"

    33) "num-other-sentinels"

    34) "2"

    35) "quorum"

    36) "2"

    37) "failover-timeout"

    38) "180000"

    39) "parallel-syncs"

    40) "1"

 

    一些关键信息:

    ·  num-other-sentinels为2,表示sentinel检测到其余两个sentinel。在日志中也会看到+sentinel生成的事件

    ·  flags只有master,若master挂掉,那可以在这看到s_down或o_down标记

    ·  num-slaves为1,表示sentinel检测到有一个slave连接到master

 

    sentinel查看slave信息

      sentinel slaves mymaster

 

    查看sentinel信息

      sentinel sentinels mymaster

 

    获取当前master的地址

      SENTINEL get-master-addr-by-name mymaster


  故障转移测试

  此时,sentinel测试环境已部署好,可以暂时关闭master并检查配置是否发生改变,可以让master暂停300s:

    redis-cli -p 6379 DEBUG sleep 300

 

  此命令将master休眠300s,模拟了master故障无法连接的情况。此时查看sentinel日志,能看到相关操作:

    ·  每个sentinel检测到master +sdown事件

    ·  事件后来升级到+odown,意味着多个sentinel确认master不可达

    ·  sentinel投票支持将启动第一次故障转移尝试的sentinel

    ·  开始故障转移

    ·  ...

 

  此时再次查询master地址信息,会得到不同的回复:

    127.0.0.1:5000>Sentinelget-master-addr-by-name mymaster

    1) "127.0.0.1"

    2) "6380

 

Sentinel和Redis认证

  当master增加安全性被配置为需要客户端密码进行连接时,slave也要知道该密码,以便master与slave用于异步复制协议的主从连接。

  使用以下指令配置密码:

    ·  requirepass在master中配置,配置后可确保不处理未验证客户端的请求

    ·  masterauth在slave配置,以便slave与master进行身份验证,正常的复制数据

 

  当使用sentinel模式时,架构中并不只有一个master,那么依据sentinel机制,故障转移后,slave会改变角色成为新master,旧master恢复正常后会成为新master的slave,因此上述配置要在所有Redis集群中实例配置,不论是master还是slave。

 

  然而,在少数情况下,我们可能需要谋个slave节点不需要验证就可被客户端读取数据,可以通过设置此slave优先级为0,来避免该slave升级为master,且仅配置“masterauth”,此时就可实现未经身份验证的客户端读取数据

 

  为了使sentinel能正常连接到配置了“requirepass”的Redis实例,sentinel必须配置“sentinel auth-pass”指令,格式如下:

    sentinel auth-pass <master-group-name> <pass>

 

sentinel客户端实施

  Sentinel需要明确的客户端支持,除非系统配置为执行将所有请求透明重定向到新的主实例(虚拟IP或其他类似系统)的脚本。Sentinel客户端指南中介绍了客户端库实现的主题

 

Sentinel API

  Sentinel提供一个API来检查其状态,检查受监控master和slave的运行状态,订阅以获得特定通知,并在运行时更改sentinel配置

  默认sentinel侦听在TCP端口26379(6379是Redis master和Redis slave端口),sentinel使用redis协议通信,因此可使用redis-cli与sentinel进行交互

  可以直接从sentinel的监控信息中查看Redis实例状态,也能查询到其他sentinel的信息等,同时,使用PUB/SUB可 从sentinel接收推送通知,如故障转移或进入错误状态的实例,都可以进行推送。

 

Sentinel命令

  以下是命令列表,但不包括修改sentinel配置的命令(稍后介绍)

    PING #返回PONG

    SENTINEL masters #显示所有被监控的master集齐状态

    SENTINELmaster<master name> #显示指定的master的状态信息

    SENTINEL slaves <master name> #显示指定的master的slave状态信息

    SENTINEL sentinels <master name> #显示指定master的sentinel状态信息

    SENTINEL get-master-addr-by-name <master name> #显示指定名称master的IP和PORT,若master正在进行故障切换或被停止,则返回被提升为master的slave的IP和PORT

    SENTINEL reset <pattern> #重置所有包含关键字的master。 “pattern”参数是一个全局风格的模式。重置进程会清除master所有先前保存的状态(包括正在进行故障切换),且删除已发现并与master关联的每个slave及其状态

    SENTINEL failover <master name> #手动强制故障切换,当master失效时,在不询问其他Sentinel意见的情况下, 强制开始一次自动故障迁移(不过发起故障转移的Sentinel会向其他Sentinel发送一个新的配置,其他Sentinel会根据这个配置进行相应的更新)

    SENTINEL ckquorum <master name> #检查当前sentinel的配置能否达到故障切换master所需的数量,执行此命令也会检测其他大多数sentinel是否正常(检测是否在线,以及是否能授权故障切换)。此命令应在sentinel中用于检测sentinel部署是否正常

    SENTINEL flushconfig #强制sentinel将运行时配置写入磁盘,包括当前sentinel状态。一般,sentinel每次在其状态改变时都会重写配置(在sentinel状态信息被持久化到磁盘后重启)。然而,有时会出现配置文件丢失的情况,如操作失误,磁盘故障,软件包升级或配置管理器而导致配置文件丢失。此时,强制sentinel重写配置文件就很必要了,也很方便(老的配置文件丢失不会影响该命令的执行)

 

运行时修改配置文件

  从Redis 2.8.4版开始,sentinel提供一个API来添加、删除或更改master的配置。注意:若有多个sentinel,你应该手动把所有的改变同步到其他Redis sentinel实例中,意思是更改单个sentinel配置,不会自动将更改同步到网络中的sentinels。

  以下是sentinel用于更新sentinel实例配置的命令列表:

    SENTINEL MONITOR <name> <ip> <port> <quorum> #此命令通知sentinel开始监控一个新的master,且指定其名称,ip,port,quorum,与sentinel.conf配置文件里“sentinel monitor”指令语法类似,不同的是你不能使用主机名代替IP,只能使用IPv4或IPv6地址

    SENTINEL REMOVE <name> #移除指定master:这个master将从sentinel监控列表移除,将从sentinel的内部状态信息中完全清除,sentinel,masters也无法列出该master

    SENTINEL SET <name> <option> <value> #与“CONFIG SET”命令相似,用于改变特定master的配置参数。指定多“option/value”对是可以的(单对也是可以的);sentinel.conf里所有可配置参数都可以使用SET命令进行配置。

 

  以下是SENTINEL SET命令的一个示例,用于修改所down-after-milliseconds调用的主设备的配置objects-cache:

    SENTINEL SET objects-cache-master down-after-milliseconds 1000

 

  如前所述,sentinel SET可用来所设置所有配置文件中可配置的配置参数,此外,他能在不重新添加master(先SENTINEL REMOVE再SENTINEL MONITOR)的情况下,修改quorum:

    SENTINEL SET objects-cache-master quorum 5

 

  请注意,由于SENTINEL MASTER以简单解析格式(作为字段/值对数组)提供所有配置参数,因此没有等效的GET命令

 

添加/删除sentinel

  基于sentinel自动发现机制,添加一个新sentinel是非常简单的。你需要仅仅是启动新的sentinel,配置危机监控当前所有活动的master,10s内sentinel将获得其他sentinel列表及被监控master的所有slave信息

 

  若要添加多个sentinel

  建议一个一个添加,等待其他sentinels已经能和新sentinel通信,再添加下一个。新增sentinel有可能失败,一个一个添加sentinel能有效保证大多数sentinel都是正常的(若一次性添加的sentinel数量大于现有sentinel数量,并且添加出现问题,可能会导致现有sentinel出现问题)。一般的每30s添加一个sentinel是比较常见的。

  添加结束后,可用命令“SENTINELmastermastername”检查所有sentinel是否已经完全获取到所有master的信息

 

  删除sentinel比较复杂

  即使被删除的sentinel很长时间无法访问,sentinels不会完全清除已经添加过的sentinels信息,因为要尽量减少其他sentinel的配置版本的更新,删除sentinel也有可能引起故障转移

  因此为了移除一个sentinel,在没有网络隔离的情况下应遵循以下步骤:

    1、停止要删除的sentinel进程

    2、执行命令“SENTINEL RESET *”,向所有其他sentinel实例发送命令重置状态信息(*表示重置所有master,也可以指定master名称)。信息会一个一个的更新,大概需要30s

    3、执行命令“SENTINELmastermastername”检查每个sentinel显示的sentinel数量是否一致

 

  删除旧的master或无法无法访问的slave

  Sentinel不会完全清除指定master的slave,即使slave长时间无法访问。在故障转移后,一旦旧master再次可用,会自动成为新master(旧slave)的slave,此时,新master和新slave会组成新的复制架构。

  若想从sentinel监控的master/slave列表里永久删除一个slave(可能是旧master),在停止slave进程后,你需要向所有sentinel发送命令“SENTINEL RESET mastername”,重置mastername所有状态信息

 

Pub/Sub消息

  客户端可以将sentinel看做一个只提供了订阅功能的Redis服务器,不能使用PUBLISH命令向这个服务器发送信息,但可使用“SUBSCRIBE”命令或“PSUBSCRIBE”命令,通过订阅给定的频道来获取相应的事件提醒。

一个频道能接收和此频道名称相同的事件,如名为+sdown的频道就可接收所有实例进入主观下线(SDOWN)状态的事件

 

  通过执行PSUBSCRIBE *”命令能接收所有事件信息。

 

  以下是能利用此API接收的频道和消息格式的列表。第一个英文单词是频道/事件的名称,其余是数据格式。

  注意,当格式中包含“instance details”时,表示频道所返回信息中包含了以下用于识别目标实例的内容:、

    <instance-type> <name> <ip> <port> @ <master-name> <master-ip> <master-port>

 

  “@”字符串后的内容指定master(即@到最后部分,此部分是可选的),仅在“@”字符之前的内容指定的实例不是master时使用。

    · +reset-master <instance details> #master已被重置

    · +slave <instance details> #一个新slave已被sentinel识别并关联

    · +failover-state-reconf-slaves <instance details> #故障转移状态切换到了reconf-slaves状态

    · +failover-detected <instance details> #另一个sentinel开始了一次故障转移操作,或一个slave转换成了master

    · +slave-reconf-sent <instance details> #领头(leader)的sentinel向实例发送了命令“SLAVEOF”,为实例重新设置新的Redis master

    · +slave-reconf-inprog <instance details> #实例正在将自己设置为master的slave,但相应的同步过程仍未完成

    · +slave-reconf-done <instance details> #slave已成功完成对新master的同步

    · -dup-sentinel <instance details> #对给定master进行监控的一个或多个sentinel已经因为重复出现而被移除(当sentinel实例重启时会出现此种情况)

    · +sentinel <instance details> #master有新sentinel被识别并添加

    · +sdown <instance details> #指定实例现处于主观下线Subjectively Down状态

    · -sdown <instance details> #指定实例现不再处于主观下线状态

    · +odown <instance details> #指定实例现处于客观下线(Objectively Down)状态

    · -odown <instance details> #指定实例现不再处于客观下线状态

    · +new-epoch <instance details> #Slot的版本号或者消息的版本号已被更新。节点如何判断收到的集群消息是较新的。Redis中使用版本号机制来判断消息的新旧,版本号越高表示消息越新。在Redis中这种版本号被称作configEpoch,每个节点都有一个configEpoch,它是一个64位的整数。其实configEpoch更应该被称作是Slot的版本号,或者某个Slot与节点映射关系的版本号。怎么理解呢?我们从消息冲突的角度看这个问题,因为configEpoch就是为了解决消息冲突的嘛。如上文所说集群中每个节点都会维护一份集群状态快照。快照中每个Slot都有一个与之对应的节点,每个节点都有一个configEpoch,所以直观上configEpoch就像是Slot的版本号。当某一个节点负责的Slot有变化的时候,节点会更新自己的configEpoch,并随着集群心跳传播该消息,集群消息是一系列Slot、节点与configEpoch组成的三元组。当新的集群消息传播到其它节点的时候,节点会对比节点本身与集群消息中对应Slot的configEpoch来决定是否更新本地集群状态快照,从这层意义上看configEpoch也更适合被称作Slot的版本号或者消息的版本号。

    · +try-failover <instance details> #正在进行新的故障转移,正处于等待多数选举状态

    · +elected-leader <instance details> #赢得指定版本的选举,可以进行故障迁移

    · +failover-state-select-slave <instance details> #故障转移操作现处于“select-slave”状态,sentinel正在寻找一个能被提升为master的slave

    · no-good-slave <instance details> #sentinel未找到合适的slave去升级。Sentinel会在一段时间后重试,但是在这种情况下,可能会改变状态机并终止故障切换

    · selected-slave <instance details> #sentinel找到合适的slave去提升

    · failover-state-send-slaveof-noone <instance details> #sentinel正在讲指定的slave提升为master,等待功能升级完成

    · failover-end-for-timeout <instance details> #故障转移因超时而中止,不过最终所有slave都会开始复制新master(slaves will eventually be configured to replicate with the newmasteranyway)

    · failover-end <instance details> #故障转移操作顺利完成,所有slave开始从新master复制数据

    · switch-master <master name> <oldip> <oldport> <newip> <newport> #master连接配置变更,外部客户端都关心的信息

    · +tilt #进入tilt模式

    · -tilt #退出tilt模式

 

自动发现Sentinel和slave

  哨兵与其他哨兵保持联系,以便互相检查对方的可用性,并进行信息交换。无须为运行的每个Sentinel分别设置其他Sentinel的地址, 因为Sentinel可以通过发布与订阅功能来自动发现正在监控相同master的其他Sentinel, 这一功能是通过向频道 sentinel:hello 发送信息来实现的。

  与此类似, 你也不必手动列出master属下的所有slave, 因为Sentinel可以通过询问master来获得所有slave的信息。

    ·  每个Sentinel会以每两秒一次的频率, 通过发布与订阅功能,向被它监控的所有master和slave的 sentinel:hello 频道发送一条信息,信息中包含了Sentinel的 IP 地址、端口号和运行 ID (runid)。

    ·  每个Sentinel都订阅了被它监控的所有master和slave的 sentinel:hello 频道, 查找之前未出现过的Sentinel(looking for unknown sentinels)。当一个Sentinel发现一个新的Sentinel时, 它会将新的Sentinel添加到一个列表中, 这个列表保存了Sentinel已知的, 监控同一个master的所有其他Sentinel。

    ·  Sentinel 发送的信息中还包括完整的master当前配置(configuration)。 如果一个Sentinel包含的master配置比另一个Sentinel发送的配置要旧, 那么这个Sentinel会立即升级到新配置上。

    ·  在将一个新Sentinel添加到监控master的列表上面之前,Sentinel会先检查列表中是否已经包含了和要添加的Sentinel拥有相同运行 ID 或者相同地址(包括 IP 地址和端口号)的Sentinel, 如果是的话,Sentinel会先移除列表中已有的那些拥有相同运行 ID 或者相同地址的Sentinel,然后再添加新Sentinel。

 

Sentinel在非故障转移的情况下对实例进行重新配置

  即使没有自动故障迁移操作在进行,sentinel总会尝试将当前配置应用到被监控的实例上,特别是:

    ·  依据当前配置,若一个slave被提升为master,那他会成为旧master原有slave的复制对象,连接了错误master的slave会被重新配置,以便能从正确的master获取数据

    ·  旧master挂掉重新上线后,会被配置为新master的slave

  不过, 在以上这些条件满足之后,sentinel在对实例进行重新配置之前仍然会等待一段足够长的时间, 确保可以接收到其他sentinel发来的配置更新, 从而避免自身因为保存了过期的配置而对实例进行了不必要的重新配置

 

算法与内部结构

  Quorum

  如前所述,每个被sentinel监控的master都配置了quorum,指定需要对master的不可达性或故障达成一致的sentinel数量,以便触发故障转移。另外,网络问题导致架构间产生网络分区,此时故障转移绝不会在只有小部分的Sentinels存在的网络分区中执行,只会在大部分sentinels存在的网络分区中执行

 

  简单的说就是:

    ·  quorum:为了将master标记为ODOWN,需要发现并确认错误的sentinel数量

    ·  故障转移在ODOWN状态被触发

    ·  一旦故障转移被触发,尝试进行故障转移的sentinel会请求大多数sentinel的授权(在quorum为大多数的情况下)

 

  如,架构中有5个sentinel 实例,quorum为2,那么至少2个sentinel确认master不可达后,故障转移才会被触发,然后能执行故障转移的只有其中一个sentinel。

  若quorum为5,则所有sentinel都必须就master故障达成一致,且执行故障转移的sentinel leader要得到所有sentinel的授权才能进行故障转移。

 

  这意味着quorum可有两种方式调整sentinel:

    ·  若quorum设置为小于部署的大多数sentinel的数量,此时sentinel对master故障更加敏感,且一旦少数的sentinel无法和master通信就会触发故障转移

    ·  若quorum设置为大于部署的大多数sentinel的数量,此时只有大多数sentinel都确认master故障时,sentinel leader才能开始故障转移

 

  配置版本号(epoch)

  为了开始故障转移,Sentinels leader需要从大多数sentinel得到授权,原因:

  当一个Sentinel leader被授权,它会为故障转移后的新master获得一个唯一性的epoch,用来标记故障转移完成后sentinels的新配置版本号,另外,只有确认了master故障的大多数sentinel才能获得这个版本号。这意味着,每次故障转移的配置都会更新一个独一无二的版本号,这很重要,后面会解释为什么这么重要。

  此外Sentinels有机制:若一个sentinel为故障转移一个新master而投票给其他sentinel,它将等待一段时间再次尝试故障转移这个master,在sentinel.conf中可配置这个延迟时间failover-timeout。这意味着Sentinels在相同的时间内不会尝试故障转移相同的master,第一次请求授权成功后会尝试,失败则另一个sentinel将会在一段时间后尝试,类推。

 

  Redis Sentinel保证了活性(liveness)特点:如果大多数Sentinels 能够互相通信,master故障,最后一定会有一个sentinel会被授权开始故障转移。

  Redis Sentinel同样也保证了安全(safety)特点:一次故障转移中,每个Sentinel故障转移同一个master将使用不同的epoch版本号,失败后由另外的sentinel会重新生成唯一的epoch版本号,然后发起故障转移;而第一次生成的epoch版本号因为没有故障转移成功而丢弃,生成的配置也不是最终配置也会被丢弃

 

故障转移成功后,最终配置的传播

  一旦sentinel leader 故障转移成功,他将开始广播新的配置,以便其他sentinels更新新master的信息。

  为了确认故障转移是否成功,sentinel需要能发送命令“SLAVEOF NO ONE”给所有参与选举的slave,稍后,切换为新master的信息能在“INFO”输出中显示。

  此时,即使slave正在更新配置,故障转移也被判定为成功,且所有Sentinels需要开始报告新的配置。

  Sentinel leader传播新配置的原因是每个sentinel每次进行已授权的故障转移时,都会产生不同的唯一的配置版本号,最终所有sentinel会共享一个配置版本号。

   每个sentinel使用redis 订阅/发布功能向所有的master/slave连续地广播其监控的master的版本,同时所有的sentinel也会等待来自其他sentinel广播的消息。

   配置在__sentinel__:hello发布/订阅频道中被广播。

  每份配置都有不同的版本号,而大的版本号总是胜过小的版本号,版本号越大,越接近最终的配置版本,越有广播的意义。

   如,初始配置时,所有sentinels都持有版本号为1的配置,即mymaster的配置为192.168.1.50:6379,一段时间后发生故障转移,生成配置版本号2,若转移成功,他将广播新的配置,即192.168.1.51:9000,其他sentinel实例收到这个配置的广播会更新的自己的配置,因为此配置有更高的版本号。

  也就是说,sentinel有第二个活性特点:一个能互相通信的sentinel集群,在故障转移时,都会尝试更新版本更高的配置。

  一般若网络故障产生网络分区,每个分区的sentinel都会尝试收敛到更高版本的配置,没有网络分区时,所有sentinel都会更新配置

 

 

  网络分区下的一致性

  Redis sentinel配置保证最终一致性,产生网络分区时,每个分区都会尝试收敛到更高版本的可用配置。

  使用sentinel时,有三种角色:

    ·  Redis实例

    ·  Sentinel实例

    ·  Client

 

  为定义整个架构系统的行为,我们要考虑到上述三种角色,以下网络中有3个节点,每节点运行一个Redis实例和sentinel实例:

  

  在此系统的初始状态中,Redis3是master,Redis2和Redis3是slave。此时网络故障造成旧master与其他节点隔离,sentinel 1和 2开始一轮故障转移,提升redis1为新master。

 

  此时,Sentinel的机制保证了sentinel 1和 2持有master的最新配置,然而sentinel 3在隔离分区中仍然持有旧的配置,我们知道sentinel 3会在网络恢复时获取新的配置,但若有客户端在网络故障时访问旧master,会发生什么?

  此时客户端仍然能在Redis3(旧master)中写入,当网络恢复,Redis3将变成Redis1的slave,并清除在网络故障期间写入的所有数据。

 

  依据数据需求,或者配置需求,你可能想或不想数据丢失的情况发生:

    ·  若你使用Redis作为缓存服务器,Client B仍然可向旧master写入数据是很方便的,即使数据最终会丢失

    ·  若你使用Redis作为存储服务器,这就很不友好了,此时为了阻止这个问题需要对系统进行配置

 

  由于Redis是异步复制的,这种情况下无法完全避免数据丢失,但可使用以下配置限制Redis3和Redis1之间数据的一致性问题:

    min-slaves-to-write 1

    min-slaves-max-lag 10(秒)

 

  当master配置以上选项,若它不能向至少一个slave写入数据,将会停止接受写入请求。由于是异步复制,不能写意味着slave已断开连接,或未发送异步确认超过max-lag秒数

 

  因此上例中,redis3将在10s后不可用,网络恢复后,sentinel 3将会整合到新架构中,客户端B能重新获取有效配置并继续请求。

 

  总之,Redis+Sentinel形成的架构系统具有最终一致性(a whole are a an eventually consistent system where the merge function is last failover wins),且旧master的数据会被丢弃,然后从新mastar重新获取数据,因此总有一个节点会丢失部分已经写入的数据。 This is due to Redis asynchronous replication and the discarding nature of the "virtual" merge function of the system。但这sentinel本身没有限制,若你使用强一致性的方法协调故障转移,问题同样会出现。

 

  只有两种方法避免丢失已确认的写入:

    ·  使用同步复制(以及使用一个合适的一致性算法来运行复制行为)

    ·  使用具有最终一致性的系统,能合并相同对象的不同版本

 

Redis现在不能使用上面的任何系统,是目前的发展目标。可是有一个代理实现解决方案2在Redis存储之上,如SoundCloud Roshi或者Netflix Dynomite

 

Sentinel持久化状态

  Sentinel状态信息持久化到sentinel.conf中,如每次接收一个新配置文件或者创建一个新配置文件(sentinel leader完成),配置都会和配置版本号(epoch)一起持久化到磁盘。很明显,即使即使sentinel进程重启也能保证配置不丢失

 

TILT模式

  Redis sentinel严重依赖系统时间:为了确认实例是否可用,他会记录最后一次回复PING的时间,并和当前时间比较推断距离最后一次回复过去了多久。

 

  然而,若系统时间发生非正常改变,或者系统非常繁忙,或进程由于某些原因阻塞,sentinel可能会出现一些问题

 

  TITL模式是一种特殊的“保护”模式,sentinel能在发生一些意料之外问题时,进入这个模式,降低对系统的依赖。Sentinel定时器中断正常情况下每秒10次调用,所以能猜测在定时器中断的两次调用之间或多或少会有100毫秒的时间。

 

  Sentinel所做的就是记录之前的中断调用时间,并和当前调用时间对比:

    ·  如果两次调用时间之间的差距为负值, 或者非常大(超过 2 秒钟), 那么 Sentinel 进入 TILT 模式。

    ·  如果 Sentinel 已经进入 TILT 模式, 那么 Sentinel 延迟退出 TILT 模式的时间。

 

  当处于TITL模式,sentinel或持续监控所有状态,但:

    ·  停止处理请求

    ·  当有实例向这个Sentinel发送“SENTINEL is-master-down-by-addr”命令时,Sentinel返回负值: 因为这个Sentinel所进行的下线判断已经不再准确。

  如果 TILT 可以正常维持30秒钟, 那么Sentinel退出TILT模式。

posted @ 2018-06-08 16:13  二表  阅读(2550)  评论(0编辑  收藏  举报