南航移动Redis-Cluster趟坑记 - 推酷

标签: 南航 移动 redis | 发表时间:2017-08-28 11:40 | 作者:
出处:http://www.tuicool.com

背景

当今IT界正处于移动互联浪潮中,涌现出一批批优秀的门户网站和电商平台。在巨大的利润驱动下,这些公司都全力打造各自的系统以适应互联网市场发展的需要,而且在此过程中各个系统还不停地接受着亿万网民的检验。经历过千锤百炼后,那些“名门大厂”都纷纷总结出“高可用,高可靠,高并发下低延迟”的优秀实践。

而南航电商营销平台这种带有国企背景和传统行业特色的电商系统在这股浪潮的引领之下,也逐步向这些“大厂”学习,结合自身的实际情况,在“高可用,高可靠,高并发下低延迟”的系统优化之路上展开一番游历探索,期间也游览了不少大坑暗沟。

我们商务移动团队作为部门里先锋部队,在打造一套以Redis为基础的缓存系统(取名为“黑曜石”)用于支撑南航电商营销平台,帮助公司实现“南航e行”战略目标的过程中对Redis环境中的坑涧丘壑展开了一番游历。

初步思考

2015年是移动电商的“井喷式”发展年份,南航的官方APP承势而起,不过事实印证了那句话——“理想很丰满,现实很骨感”。那个时候APP的后台架构是按传统的企业内部应用思路搭建的,系统的研发思路是只管实现功能,不注重接口性能。

让人纠结的地方比较多,一时三刻难以对其进行全面优化改造,但上级下达的KPI要在短期内完成,必须要在短时间内提升一些关键接口的性能,我们第一时间想到的而且最好开刀的莫过于类似航班动态和机票信息等查询类功能接口,因为只要加上缓存,性能马上能大幅度提升。

基于这方面考虑,我们当时认为第一要务就是要搭建一套能面向互联网的缓存系统,经过几番选型后,目光开始集中到Redis-Cluster上。

深入探索

我们在探索期间一边翻资料,一边在测试环境搭建了一套Redis-Cluster,经过一段时间折腾后,在2016年3月份正式投产使用。

在投产初期信心不大,所以找了小接口来尝试,缓存的数据量小(小于1KB),结构简单,基本都是一些系统配置信息,如功能开关,APP版本信息等,Redis-Cluster对此表示毫无压力。

紧接着,在16年3月底,我们开始把Redis-Cluster投放到正规战场上,比如缓存机票信息,航班动态更新。这个接口数据量比之前的大(50-100KB),结构比较复杂,而且是我们关键功能的核心接口。使用Redis-Cluster缓存机票信息以及优化了接口的通讯协议后,对查票接口加速效果十分显著,接口的响应时间从7-8秒降到一百多毫秒,再配合IOS和安卓端的原生页面渲染方式优化后,实现机票信息"秒出",这标志着南航系统开始踏进“秒极俱乐部”的门槛。在一次跟全美航空的交流中,机票查询的速度把他们吓了一跳。(当时全美app查一下机票信息需要8-9秒)

应对大流量

我们解决在常规服务状态下的核心功能查询类接口的返回缓慢的问题,但这仅仅是开始,因为移动电商最大的特色就是搞促销活动,像秒杀、抽奖、派券等等,这些活动都会引发瞬时的访问高峰,访问量往往是常规服务状态下的十倍或以上,南航自2015年10月28日搞了第一次会员日活动后,往后的每月28日都会搞一次,每次的零点峰值都会对我们系统造成毁灭性打击,其实这就好像一个没穿衣服的人在冰天雪地中行走一样,所以几乎在完成接口提速的同一时间,我们用Redis-Cluster做了件“棉袄”让系统“穿”上——把所有流量转嫁到Redis-Cluster上。

利用Redis的单线程原子性管理访问许可证池,许可证池的大小根据活动接口的性能灵活调整。只有得到许可证的请求才能访问相关接口,当接口返回后把许可证释放回许可证池中,而得不到许可证的请求则进入Redis blpop的等待队列中。 这项措施结合Nginx-Lua的服务升降级和限流熔断机制(主要保护那些写操作功能,比如下单),确保了南航营销平台在往后的会员日或其他大促活动期间承受千万级的访问流量时仍能平稳地提供服务。

通过这种办法,在IT团队规模远远不如那些有名气的电商公司同类系统研发团队的情况下,经过一个月的改造,我们把一个每次都是躺着过零点高峰的系统,变成基本上能安安稳稳地站着过零点高峰的系统。

全天候服务

由于当时应用的问题较多,单链路基本难以保证7*24小时不间断服务,三头两日会跪一下,最直接的处理方式就是当监控报警时让运维帮忙重启。尽管我们有多条链路,但一旦某条链路down机重启,过程中肯定会影响到部分用户。我们团队的研发资源实在有限,而且那段时间全部精力放在确保会员日这种促销活动上(支撑业务部门冲KPI),但这个问题又不能放任不管,所以采用比较省事的方式——在16年的4月初,我们把各条链路的session状态信息统一缓存到Redis-Cluster中,这样可以把个别链路的down机对用户的影响降到最低,另外写了简单监控接口让监控系统调用,当监控系统通过这个接口发现某条链路down了就调一下该链路上的重启脚本。

这样做一方面为团队争取了休息时间,不用为故障疲于奔命,减轻研发人员压力,另一方面其实也算把系统修成7*24不间断服务了,最重要的是能让团队有更充分的时间制定优化改造计划和方案,使得后来我们能在比较从容的情况下通过代码层面的优化和JVM调优等措施把应用出现的各种问题一一解决。

进一步优化

当链路能保证7*24小时不间断服务后,我们又回过头来优化那些会员日和其他促销活动中用到的写操作功能接口,如下订单、派优惠券和领优惠券之类,尤其优惠券相关的接口不但涉及双表信息写入,而且还带事务,并发一高数据库连接就占满。

最初只能通过限流的方式先处理,但这样做极不合理,活动期间大部分用户的感受是既派不了券又领不了券。

后来大概在16年7月改成把入库数据丢进队列里,排队入库,不过这样用户体验也不好,比如有人点了领券按钮,然后马上去券包查看,甚至马上使用时发现没券,要等上一段时间才看到刚才所领的券,原因是数据还在队列里,还没入库。

再后来我们就想能否先写缓存,读的时候也是先读缓存,这样就能满足用户需要。可是看看我们的Redis:

  1. 不能支持结构化存储
  2. 不支持事务。

当时mongoDB可以支持结构化存储,而且支持Sql查询,并且承诺即将支持事务,然而我们的存储中间件已经有Mysql和Redis,出于团队规模和技术栈的管理,我们不太希望把技术栈搞得太臃肿,因为不想降低本来就不算高的研发效率,避免出现技术实现时出现选择困难,而且就那么一两个写架构代码、封装搭建底层组件的人,维护多套技术岂不吐血。

当时有个同事提议把结构化存储转化成k-v,再利用key的命名规则来模仿事务,这样完全可以做出基于Redis作为底层存储的内存数据库。于是我们进行了一些pojo结构转换和key标签封装,并把所有相关的API通过JDBC来封装,最终的效果不但支持POJO的结构化存储以及SQL语句操作,而且还支持事务。通过这种方式把优惠券信息先缓存到Redis-Cluster后再根据我们封装的”事务“持久化到Mysql中,这样就基本满足了各方需求。

在代码上看,在Service层把POJO持久化到数据库与缓存到Redis是无差别的,为此我的同事把这套实现称为内存数据库模块。

/**
* @author DeanPhipray
* OBSI DB存储示例
*
* */
@Transactional
public int saveToRedis(String fieldName,Student student,Teacher Teacher) throws RdbException{
    Row row=new Row();
    try {
        row.setValue(fieldName, SeqFactory.getOID());
        rStudentDao.insert("dual", row);
        row.setValue(fieldName, SeqFactory.getOID());
        rStudentDao.insert("dual", row);

        row.setValue(fieldName, SeqFactory.getOID());
        rStudentDao.insert(student);
        row.setValue(fieldName, SeqFactory.getOID());
        rTeacherDao.insert(teacher);
    }catch (Exception e) {
        log.error("缓存实例失败");
        throw new RdbException("Rdb save fail",e);
    }
    return 2;
}


/**
* @author DeanPhipray
* OBSI DB query示例
*
* */

public Student query(@RequestParam("id") Long id,String tableName) throws RdbException{
    Student student= null;
    try {
        String sql = "select * from "+ tableName + "where id = ?";
        List params = new LinkedList<>();
        params.add(id);
        student= (Student) rStudentDao.query(sql,params);
    } catch (Exception e) {
        log.error("查询实例失败");
        throw new RdbException("Rdb query fail",e);
    }
    return student;
}

近期团队开始搞敏捷转型,我们跟一些敏捷顾问的交流中提到我们一直为技术栈做keepfit的理念,基本得到对方的认同。

踩坑经历

一号坑:僵尸连接

在一个月黑风高的上线夜,当大家都以为上线任务快完成时,突然有同事告诉我发布新包重启系统后,系统无法获取Redis-Cluster链接,重启过好几次还是这样,我马上检查集群状态,发现一切正常,但检查连接数时就惊奇地发现所有节点的连接数到达了上限。我们算了一下觉得很奇怪,因为接入的系统十根手指头数得完,而且每个系统的配置都是按我们制定的模板配参数,我们最大连接数才配了200,空闲最大连接数50,空闲最小连接数是10,一般各个应用实例只会以10个连接连到Redis-Cluster各个节点中,怎么算都到不了连接数的上限啊。为了尽快恢复,我们先通过脚本命令在Redis服务器上清除连接,解去燃眉之急,不过治标不治本,幸亏每次清除完连接,客户端会自动重连,不影响服务,而连接数再次到达上限,大概要两天时间。

echo "client list" | redis-cli -c -p {port}|awk -F '=|
 ' '$12>3600{print $4}' | sed "s/^/client kill /g" | redis-cli -c -p {port}

填坑攻略:消除僵尸链接

在随后几天里,我们发现客户端设了最大超时,如果连接一直处于空闲状态,大概5分钟就会断开与服务器之间的长连接,但奇怪的是服务端不承认客户端的断连状态,一直保持该连接,结果从客户端的服务器看不到这种连接,但在Redis服务器上却看到大量这种连接,最终导致服务端连接数被占满,无法再创建新连接对外提供服务。为了让链接有一定的弹性,我们在客户端设置连接超时时间、连接池大小、最大空闲连接数、最小空闲连接数等。

<!-- jedis configuration starts -->
<bean id="config" class="org.apache.commons.pool2.impl.GenericObjectPoolConfig">
    <property name="maxTotal" value="200"></property>
    <property name="maxIdle" value="50"></property>
    <property name="minIdle" value="10"></property>
    <property name="maxWaitMillis" value="15000"></property>
    <property name="lifo" value="true"></property>
    <property name="blockWhenExhausted" value="true"></property>
    <property name="testOnBorrow" value="false"></property>
    <property name="testOnReturn" value="false"></property>
    <property name="testWhileIdle" value="true"></property>
    <property name="timeBetweenEvictionRunsMillis" value="30000"></property>
</bean>
<bean id="jedisCluster" class="com.csair.csmbp.util.JedisClusterFactory">
    <property name="addressKeyPrefix" value="address" />
    <property name="timeout" value="300000" />
    <property name="maxRedirections" value="6" />
    <property name="config" ref="config" />
</bean>

在服务端根据实际情况设置tcp-keepalived和Timeout这两个参数,其中建议Timeout的值跟客户端的超时时间一致。

二号坑:客户端过多

随着应用场景的逐渐增多,这套缓存系统引起了部门内很多项目组的兴趣和关注,接着就是纷纷踊跃接入,一下子诞生了很多客户端,带来的问题就是连接数配置难以统一规管,连接数暴增,结果某些系统/个别链路分不到连接,这一来就引出一个比较经典的场景,某大领导用我们的系统总是报错,而我们模仿操作想重现错误时,基本是正常(让我们极度崩溃)。

填坑攻略:搭建代理层

这个问题发生时监控系统是不会报警的,因为监控系统是固定频率发送检测请求,一直固定占用着一条链路,而且此时的监控系统还没去监控集群的连接数。后来我们通过跨链路的日志分析系统检查日志时发现个别应用连不上Redis-Cluster,再看看Redis服务器上的连接数是处于爆满状态,不过绝对大部分连接是空闲状态,没数据流动的,由此就诞生了用代理把连接统一管理的想法。

接下来就搭建了一套轻量级的代理层集群统一管理Redis-Cluster链接,采用Netty框架处理各个系统/各条链路的客户端请求。

(点击放大图像)

在客户端和代理之间,代理模拟Jedis跟Redis之间的通讯协议,以nio的方式处理并发请求,在代理与Redis-Cluster之间采用socket长连接复用方式做请求转发,原本的客户端完全无需做任何代码改动就能接入代理集群。

代理集群会在Redis-Cluster中缓存代理集群的节点信息和刷新各个节点的健康状态,因为Jedis客户端会定时询问集群节点信息,而代理集群只需把Master节点替换为代理集群节点,并且对代理集群节点做一次平均的Hash Slot分片就能确保:

  1. 客户端请求集中连接到代理集群上
  2. 代理集群在动态扩展新节点时能被客户端自动发现

三号坑:内存最大值限制

起初缓存的数据比较少,一直没配最大内存限制,随着接入系统越来越多,缓存数据量不断增大,结果在某个风和日丽的白天,内存被挤爆,系统除了报Cluster down外,并没更清晰的报错,当时我们一脸迷惘,莫名其妙地查了1个多小时后才发现服务器内存被耗光了。

填坑攻略:设置最大内存限制

在服务端根据服务器资源的实际情况设置maxmemory的大小,这样有个好处就是当超过这个值时,Redis会让set操作失败,而且有明确的异常信息返回。这个坑解决办法虽然非常简单,但极易被忽略,属于暗沟。

四号坑:aof文件占满磁盘空间

有一天我们刚好完成了一个季度的任务,正准备享受那份难得的按时下班带来的小愉悦,说时迟,那时快,监控报警!集群中某台服务器上的所有实例停止服务,我们马上尝试重启上面的实例,但于事无补。于是我们只好按部就班,老老实实从cpu、内存、磁盘空间、Redis日志等等逐个检查,结果发现磁盘空间满了,AOF一直阻塞,一个aof文件体积竟然有十几G(其他正常的实例上aof文件才2-3G),为了尽快恢复我们果断把其中两个从节点实例的aof文件删掉,然后再重启实例,然后就恢复正常了,不过当我们顺手重启这台服务器一个没有删除aof文件的实例后,这个实例的aof文件在重启后接近1分钟后从十几G变成了2G,在此期间该实例进入僵死状体(单线程的弊端),这明显进行了aof重写啊。

填坑攻略:控制aof文件大小

此后每天执行BGREWRITEAOF指令脚本,监控磁盘空间,减少服务器上Redis的实例数并腾空一半内存,因为一台机上部署多个Redis实例会有个隐患,万一多个实例扎堆做AOF重写会导致swap或者oom,导致重写失败,这种失败会不断重复,直至aof文件像滚雪球似的变大,最终塞满磁盘,另外重写体积较大的aof文件时,Redis会进入IO阻塞状态,停止对外服务。

(点击放大图像)

结语和寄望

在近一年半的探索和实践过程中,我们团队一路坑坑洼洼,几经颠沛地走到现在,大体上摸索出一套“高可用,高可靠,高并发下低延迟”的缓存解决方案。

希望这套脱胎于Redis(红宝石)的“黑曜石”系统,乘着“南航e行”这股东风,能得到更好的持续的优化,在未来的日子里走得更稳、更远。

作者介绍

邓卓楠,2015年6月接手南航商务移动后台重构工作,2016年7月至今担任南航商务移动团队总体架构规划,主要负责后台接口优化与系统重构。

感谢木环对本文的策划,张凯峰对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至[email protected]。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号:InfoQChina)关注我们。

相关 [南航 移动 redis] 推荐:

南航移动Redis-Cluster趟坑记 - 推酷

- -
当今IT界正处于移动互联浪潮中,涌现出一批批优秀的门户网站和电商平台. 在巨大的利润驱动下,这些公司都全力打造各自的系统以适应互联网市场发展的需要,而且在此过程中各个系统还不停地接受着亿万网民的检验. 经历过千锤百炼后,那些“名门大厂”都纷纷总结出“高可用,高可靠,高并发下低延迟”的优秀实践. 而南航电商营销平台这种带有国企背景和传统行业特色的电商系统在这股浪潮的引领之下,也逐步向这些“大厂”学习,结合自身的实际情况,在“高可用,高可靠,高并发下低延迟”的系统优化之路上展开一番游历探索,期间也游览了不少大坑暗沟.

Redis 负载监控——redis-monitor

- - ITeye资讯频道
redis-monitor是一个Web可视化的 redis 监控程序. 使用 Flask 来开发的,代码结构非常简单,适合移植到公司内网使用. redis 服务器信息,包括 redis 版本、上线时间、 os 系统信息等等. 实时的消息处理信息,例如处理 command 数量、连接总数量等. 内存占用、 cpu 消耗实时动态图表.

Redis 起步

- - 博客园_首页
Rdis和JQuery一样是纯粹为应用而产生的,这里记录的是在CentOS 5.7上学习入门文章:. Redis是一个key-value存储系统. 和Memcached类似,但是解决了断电后数据完全丢失的情况,而且她支持更多无化的value类型,除了和string外,还支持lists(链表)、sets(集合)和zsets(有序集合)几种数据类型.

redis 配置

- - 谁主沉浮
# 当配置中需要配置内存大小时,可以使用 1k, 5GB, 4M 等类似的格式,其转换方式如下(不区分大小写). # 内存配置大小写是一样的.比如 1gb 1Gb 1GB 1gB. # daemonize no 默认情况下,redis不是在后台运行的,如果需要在后台运行,把该项的值更改为yes. # 当redis在后台运行的时候,Redis默认会把pid文件放在/var/run/redis.pid,你可以配置到其他地址.

Cassandra代替Redis?

- - Tim[后端技术]
最近用Cassandra的又逐渐多了,除了之前的360案例,在月初的QCon Shanghai 2013 篱笆网也介绍了其使用案例. 而这篇 百万用户时尚分享网站feed系统扩展实践文章则提到了Fashiolista和Instagram从Redis迁移到Cassandra的案例. 考虑到到目前仍然有不少网友在讨论Redis的用法问题,Redis是一个数据库、内存、还是Key value store?以及Redis和memcache在实际场景的抉择问题,因此简单谈下相关区别.

redis 部署

- - CSDN博客云计算推荐文章
一、单机部署 tar xvf redis-2.6.16.tar.gz cd redis-2.6.16 make make PREFIX=/usr/local/redis install  #指定安装目录为/usr/local/redis,默认安装安装到/usr/local/bin. # chkconfig: 2345 80 10       #添加redhat系列操作系统平台,开机启动需求项(运行级别,开机时服务启动顺序、关机时服务关闭顺序) # description:  Starts, stops redis server.

nagios 监控redis

- - C1G军火库
下载check_redis.pl. OK: REDIS 2.6.12 on 192.168.0.130:6379 has 1 databases (db0) with 49801 keys, up 3 days 14 hours - connected_clients is 1, blocked_clients is 0 | connected_clients=1 blocked_clients=0.

转 redis vs memcached

- - 数据库 - ITeye博客
传统MySQL+ Memcached架构遇到的问题.   实际MySQL是适合进行海量数据存储的,通过Memcached将热点数据加载到cache,加速访问,很多公司都曾经使用过这样的架构,但随着业务数据量的不断增加,和访问量的持续增长,我们遇到了很多问题:.   1.MySQL需要不断进行拆库拆表,Memcached也需不断跟着扩容,扩容和维护工作占据大量开发时间.

Redis优化

- - 数据库 - ITeye博客
键名:尽量精简,但是也不能单纯为了节约空间而使用不易理解的键名. 键值:对于键值的数量固定的话可以使用0和1这样的数字来表示,(例如:male/female、right/wrong). 当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能,不过一般都要持久化比较安全,而且是快照和aof同时使用比较安全.

笔记--redis

- - 移动开发 - ITeye博客
接着准备面试内容,今天学习了下redis,继续我的笔记加深印象. 1.为什么要使用redis.  答:主要是 性能和 并发两个方面,另外redis也可以做分布式锁和消息队列等其他功能. 但是如果只是为了分布式锁这些其他功能,完全还有其他中间件(如zookpeer等)代替,并不是非要使用redis.