11.1.5 其他问题
11.1.5.1 只读性
主从复制架构下,默认Slave是只读的,如果写入则会报错:
127.0.0.1:6379> set foo bar (error) READONLY You can't write against a read only slave.
注意这个行为是可以修改的,虽然这样的修改没有意义:
127.0.0.1:6379> CONFIG SET slave-read-only no OK 127.0.0.1:6379> set foo bar OK
11.1.5.1 事件通知
在sentinel中,如果出现warning以上级别的事件发生, 是可以通过如下配置进行脚本调用的(对于该脚本redis启动用户需要有执行权限):
sentinel notification-script mymaster /redis/script/notify.py比如说,我们希望在发生这些事件的时候进行邮件通知,那么,notify.py就是一个触发邮件调用的东东,传入第一个参数为事件类型,第二个参数为事件信息:
#!/bin/python from sendmail import send_mail import sys event_type = sys.argv[1] event_desc = sys.argv[2] mail_content = event_type + ":" + event_desc send_mail("xxxx@qq.com", ["xxxxx@cmbc.com.cn","xxxx@gmail.com"], "Redis Sentinel Event Notification Mail", mail_content, cc=["xxx@gmail.com","xxx@139.com"], bcc=["xxxx@qq.com"] )有两个注意事项: 1) 这个时候如果集群发生了切换会产生很多事件,此脚本是在每一个事件发生时调用一次,那么你将短时间收到很多封邮件,加上很多的邮件网关是不允许在一个短时间内发送太多的邮件的,因此这个仅仅是一个示例,并不具备实际上的作用。 2) 一般我们会采用多个sentinel,只需在一个sentinel上配置即可,否则将同一个消息会被多个sentinel多次处理。
附sendmail模块代码:
import smtplib import os from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication from email.mime.base import MIMEBase from email.mime.text import MIMEText from email.utils import formatdate from email import Encoders from email.message import Message import datetime def send_mail(fromPerson,toPerson, subject="", text="",files=[], cc=[], bcc=[]): server = "smtp.qq.com" assert type(toPerson)==list assert type(files)==list assert type(cc)==list assert type(bcc)==list message = MIMEMultipart() message['From'] = fromPerson message['To'] = ', '.join(toPerson) message['Date'] = formatdate(localtime=True) message['Subject'] = subject message['Cc'] = ','.join(cc) message['Bcc'] = ','.join(bcc) message.attach(MIMEText(text)) for f in files: part = MIMEApplication(open(f,"rb").read()) part.add_header('Content-Disposition', 'attachment', filename=filename) message.attach(part) addresses = [] for x in toPerson: addresses.append(x) for x in cc: addresses.append(x) for x in bcc: addresses.append(x) smtp = smtplib.SMTP_SSL(server) smtp.login("xxxx@qq.com","xxxx") smtp.sendmail(message['From'],addresses,message.as_string()) smtp.close()最佳实践:采用ELK(Elastic+Logstash+Kibana)进行日志收集告警(ElastAlert用起来不错),不启用这个事件通知功能。如果你的环境中没有ELK,或者启动一个Tcp Server进程,notify脚本将事件通过tcp方式吐给这个server,该Server收集一批事件后再做诸如发邮件的处理。
11.1.5.2 虚拟IP切换
在sentinel进行切换时还会自动调用一个脚本(如果设置的话),做一些自动化操作,比如如果我们需要一个虚拟IP永远飘在Master上(这个VIP可不是被应用用来连接redis 的,用过的人都知道连接redis sentinel并不依赖于VIP的),那么可以在sentinel配置文件中配置:
sentinel client-reconfig-script mymaster /redis/script/failover.sh在发生主从切换,Master发生变化时,该脚本会被sentinel进行调用,调用的参数如其配置文件所描述的:
# The following arguments are passed to the script: # # <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port> # # <state> is currently always "failover" # <role> is either "leader" or "observer" # # The arguments from-ip, from-port, to-ip, to-port are used to communicate # the old address of the master and the new address of the elected slave # (now a master).因此,我们可以在failover.sh中进行判断,如果该脚本所运行的主机IP等于新的Master IP,那么将VIP加上,如果不等于,则该机器为Slave,就去掉VIP。通过这种方式进行VIP的切换:
#!/bin/sh _DEBUG="on" DEBUGFILE=/tmp/sentinel_failover.log VIP='192.168.2.120' MASTERIP=${6} MASK='24' IFACE='eno33554960' MYIP=$(ip -4 -o addr show dev ${IFACE}| grep -v secondary| awk '{split($4,a,"/");print a[1]}') DEBUG () { if [ "$_DEBUG" = "on" ]; then echo `$@` >> ${DEBUGFILE} fi } set -e DEBUG date DEBUG echo $@ DEBUG echo "Master: ${MASTERIP} My IP: ${MYIP}" if [ ${MASTERIP} = ${MYIP} ]; then if [ $(ip addr show ${IFACE} | grep ${VIP} | wc -l) = 0 ]; then /sbin/ip addr add ${VIP}/${MASK} dev ${IFACE} DEBUG date DEBUG echo "/sbin/ip addr add ${VIP}/${MASK} dev ${IFACE}" DEBUG date DEBUG echo "IP Arp cleaning: /usr/sbin/arping -q -f -c 1 -A ${VIP} -I ${IFACE}" /usr/sbin/arping -q -f -c 1 -A ${VIP} -I ${IFACE} DEBUG date DEBUG echo "IP Failover finished!" fi exit 0 else if [ $(ip addr show ${IFACE} | grep ${VIP} | wc -l) != 0 ]; then /sbin/ip addr del ${VIP}/${MASK} dev ${IFACE} DEBUG echo "/sbin/ip addr del ${VIP}/${MASK} dev ${IFACE}" fi exit 0 fi exit 1最早这样的用法是一个日本人写的blog,请参见:http://blog.youyo.info/blog/2014/05/24/redis-cluster/
11.1.5.3 持久化动态修改
其实相对于VIP的切换,动态修改持久化则是比较常见的一个需求,一般在一主多从多Sentinel的HA环境中,为了性能常常在Master上关闭持久化,而在Slave上开启持久化,但是如果发生切换就必须有人工干预才能实现这个功能。可以利用client-reconfig-script自动化该进程,无需人工守护,我们就以RDB的动态控制为例: Sentinel配置文件如下:
sentinel client-reconfig-script mymaster /redis/script/rdbctl.shrdbctl.sh源代码:
#!/bin/bash _DEBUG="on" DEBUGFILE="/smsred/redis-3.0.4/log/sentinel_failover.log" MASTERIP=${6} MASTERPORT=${7} SLAVEIP=${4} SLAVEPORT=${5} MASK='24' IFACE='bond0' MYIP=$(ip -4 -o addr show dev ${IFACE}| grep -v secondary| awk '{split($4,a,"/");print a[1]}') DEBUG () { if [ "$_DEBUG" = "on" ]; then echo `$@` >> ${DEBUGFILE} fi } set -e DEBUG date DEBUG echo $@ DEBUG echo "===Begin Failover===" #If Master if [ ${MASTERIP} = ${MYIP} ]; then #Disable RDB redis-cli -h ${MYIP} -p ${MASTERPORT} -a c1m2b3c4 config set save "" DEBUG echo ${MYIP} DEBUG echo "Disable Master RDB:" ${MYIP} ${MASTERPORT} DEBUG echo "===End Failover===" exit 0 #Or Slave else echo "test5" >> $DEBUGFILE redis-cli -h ${MYIP} -p ${SLAVEPORT} -a c1m2b3c4 config set save "900 1 300 10 60 100000000" DEBUG echo ${MYIP} DEBUG echo "Enable Slave RDB:" ${MYIP} ${SLAVEPORT} DEBUG echo "===End Failover===" exit 0 fi exit 1原理和VIP切换一节基本一致,不再赘述。
11.1.5.4 Sentinel最大连接数
1. 问题描述
某准生产系统,测试运行一段时间后程序和命令行工具连接sentinel均报错,报错信息为:
jedis.exceptions.JedisDataException: ERR max number of clients reached此时应用创建redis新连接由于sentinel已经无法响应而无法找到master的IP与端口,因此无法连接redis,并且此时如果发生redis宕机亦无法进行生产切换。
2. 问题初步排查过程
首先,通过netstat对sentinel所监听端口26379进行连接数统计,此时连接则报错。如下图:
通过sentinel服务器端统计发现,redis sentinel 的连接中大部分都是来自于两台非同网段(中间有防火墙)的应用服务器连接(均为Established状态),并且来自其的连接也大约半个小时后稳步增加一次,而同网段的应用服务器连接sentinel的连接数基本保持一致。排除了应用的特殊性(采用的jedis版本和封装的工具类都是一样的)后,初步判断此问题与网络有关,更详细的说是连接数增加与防火墙切断连接后的重连有关。
3. 问题查证过程
此问题分为两个子问题: 1) 防火墙将TCP连接设置为无效时sentinel服务器为何没有断开连接,保持Established状态? 2) 为何连接数还会不断增加?
对于问题1) ,TCP在三次握手建立连接时OS会启动一个Timer来进行倒计时,经过一个设定的时间(这个时间建立socket的程序可以设置,如果没有设置则采用OS的参数tcp_keepalive_time,这个参数默认为7200s,即2小时)后这个连接还是没有数据传输,它就会以一定间隔(程序可以设定,如果没有设置则采用OS的参数tcp_keepalive_intvl,默认为75s)发出N(程序可以设定,如果没有设置则采用OS的参数tcp_keepalive_probes,默认为9次)次Keep Alive包。TCP连接就是通过上述的过程,在没有流量时是通过发送TCP Keep-Alive数据包,然后对方回应TCP Keep-Alive ACK来确定信道是否还在真实连接。通过查看Sentinel源代码,其默认是不开启Keepalive的(而jedis默认是开启的),并且默认对于不活动的连接也不会主动关闭的(timeout默认为0)。
对于防火墙,通过翻阅防火墙技术资料(详见下列描述,摘自:《Junos Enterprise Switching: A Practical Guide to Junos Switches and Certification》),我司采用的Juniper防火墙对于没有流量的TCP连接默认是30分钟,30分钟内没有流量就会断掉链路,而不会发送TCP Reset,同时在防火墙策略上并没有开长连接,使用的即为此默认设置。
因此在防火墙每半个小时将连接置为无效时,sentinel同时又禁止了Keepalive(因为默认设置Keepalive为0,即disable发送Keepalive包)。应用服务器的jedis虽然开启了keepalive,但是它发送的keepalive包由于防火墙已经将此链路标记为无效,而无法发送到sentinel端,而且jedis由于采用了OS默认参数,因此需要等待tcp_keepalive_time(2小时)后才启动发送Keep Alive包进行探活的,在tcp_keepalive_time+tcp_keepalive_intvl*tcp_keepalive_probes=7895s=131.58分钟后,jedis端才会认定这个连接断掉而清理掉这个连接。简单的说就是jedis会在很长一段时间后才会发keepalive包,并且这个包也是发不到sentinel上的,而sentinel本身也不会发送keepalive包,所以从sentinel这端看连接一直存在,而从jedis那端看7895s之后就会清理一次连接。这也解释了为什么防火墙将TCP连接断开后,sentinel端的连接并没有释放。
对于问题2) ,翻阅jedis源代码,jedis通过连接sentinel并pubsub来监听集群事件,以确定是否发生了切换,并且拿到新的master 地址和端口。如果断开则会5秒后尝试重连(JedisSentinelPool.java)。
因此,这是导致连接数不断上升的原因。 综上,防火墙相对频繁的断开和服务器不断重连导致在一个相对较短的时间内连接骤增,造成到达sentinel最大连接数,sentinel 的最大连接数在redis.h中定义,为10000: 4. 问题解决过程
此系统由于访问关系与网段规划间的安全问题,必须跨越防火墙,因此试图从配置角度解决此问题。
首先,联系网络相关同事,进行网络变更,开启从应用服务器到sentinel的链路相对的长连接,即无流量超时而断开的时间设置为8小时。以此手段降低断开频率,以便缓解短时间内不断重试连接造成的sentinel连接增长。
然后,通过阅读redis源代码(net.c),发现,sentinel也采用了redis 所有参数设置(通过config.c的函数void loadServerConfigFromString(char *config))。因此,通过设置redis 的下列两个参数可以解决这个问题,第一个参数是TCP Keepalive参数,此参数默认为0,也就是不发送keepalive。也就是改变OS默认的tcp_keepalive_time参数(在Unix C的socket编程中TCP_KEEPIDLE参数对应OS 的tcp_keepalive_time参数)。
该参数的官方解释为:
# TCP keepalive. # # If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence # of communication. This is useful for two reasons: # # 1) Detect dead peers. # 2) Take the connection alive from the point of view of network # equipment in the middle. # # On Linux, the specified value (in seconds) is the period used to send ACKs. # Note that to close the connection the double of the time is needed. # On other kernels the period depends on the kernel configuration. # # A reasonable value for this option is 60 seconds.我们设置为tcp-keepalive 60,加快回收连接速度,从网络断开到连接清理时间缩短为60+75*9=12.25分钟。
同时,通过设置maxclients为65536,增大sentinel最大连接数,使得在上述12.25分钟即使有某种异常导致sentinel连接数增加也不至于到达最大限制。此参数的官方解释为:
################################### LIMITS #################################### # Set the max number of connected clients at the same time. By default # this limit is set to 10000 clients, however if the Redis server is not # able to configure the process file limit to allow for the specified limit # the max number of allowed clients is set to the current file limit # minus 32 (as Redis reserves a few file descriptors for internal uses). # # Once the limit is reached Redis will close all the new connections sending # an error 'max number of clients reached'. # maxclients 10000对于redis 的timeout参数,由于启用这个参数有程序微小开销(会调用redis.c中的int clientsCronHandleTimeout(redisClient *c, mstime_t now_ms)),决定保持默认为0,而通过上述参数使用OS进行连接断开。
5. 问题解决结果
通过开发、网络和数据库团队的协同努力,配置上述参数和修改防火墙策略后,手动增加sentinel进程,超过原默认最大连接数10000后sentinel可以正常访问操作,并且通过tcpdump进行抓包,在指定时间内(1分钟),就有KeepAlive包对每个sentinel TCP连接进行探活,经过观察sentinel连接稳定,再未出现短时间内暴涨的情况。
6. 问题后续
在redis中默认不开启keepalive就是为了尽可能减小网络负载,榨干网络性能,尽可能达到redis的。在后续的程序运行中,如果发现网络是瓶颈时(在相当长的一段时间内不会),可以加大sentinel的keepalive参数,减小keepalive数据包的传输,这个修改是不影响redis对外服务的。
参考文档: http://www.tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO/
附录:如何用TCPDUMP进行keep alive抓包
tcpdump -pni bond0 -v "src port 26379 and ( tcp[tcpflags] & tcp-ack != 0 and ( (ip[2:2] - ((ip[0]&0xf)<<2) ) - ((tcp[12]&0xf0)>>2) ) == 0 ) "7. 问题再后续
我们后来在这个应用上发现一旦网络有抖动,sentinel的连接增加就回大幅度增加,后来通过jmap查看sentinelpool的实例竟然多达200多个,也就是说这个就是程序的问题,在sentinelpool上不应该多次实例化,而是采用已有连接进行重连。
本文为《Redis开发运维实践指南》内容,该书作者为黄鹏程,已授权云栖社区转载。