对每个程序员而言,故障都是悬在头上的达摩克利斯之剑,都惟恐避之不及,如何避免故障是每个程序员都在苦苦追寻但愿解决的问题。对于这一问题,你们均可以从需求分析、架构设计 、代码编写、测试、code review、上线、线上服务运维等各个视角给出本身的答案。本人结合本身两年有限的互联网后端工做经验,从某几个视角谈谈本身对这一问题的理解,不足之处,望你们多多指出。
咱们大部分服务都是以下的结构,既要给使用方使用,又依赖于他人提供的第三方服务,中间又穿插了各类业务、算法、数据等逻辑,这里面每一块均可能是故障的来源。如何避免故障?我用一句话归纳,“怀疑第三方,防备使用方,作好本身”。
1 怀疑第三方
坚持一条信念:“全部第三方服务都不可靠”,无论第三方什么天花乱坠的承诺。基于这样的信念,咱们须要有如下行动。
1.1 有兜底,制定好业务降级方案
若是第三方服务挂掉怎么办?咱们业务也跟着挂掉?显然这不是咱们但愿看到的结果,若是能制定好降级方案,那将大大提升服务的可靠性。举几个例子以便你们更好的理解。
好比咱们作个性化推荐服务时,须要从用户中心获取用户的个性化数据,以便代入到模型里进行打分排序,但若是用户中心服务挂掉,咱们获取不到数据了,那么就不推荐了?显然不行,咱们能够在cache里放置一份热门商品以便兜底;
又好比作一个数据同步的服务,这个服务须要从第三方获取最新的数据并更新到mysql中,刚好第三方提供了两种方式:1)一种是消息通知服务,只发送变动后的数据;2)一种是http服务,须要咱们本身主动调用获取数据。咱们一开始选择消息同步的方式,由于实时性更高,可是以后就遭遇到消息迟迟发送不过来的问题,并且也没什么异常,等咱们发现一天时间已过去,问题已然升级为故障。合理的方式应该两个同步方案都使用,消息方式用于实时更新,http主动同步方式定时触发(好比1小时)用于兜底,即便消息出了问题,经过主动同步也能保证一小时一更新。
有些时候第三方服务表面看起来正常,可是返回的数据是被污染的,这时还有什么方法兜底吗?有人说这个时候除了通知第三方快速恢复数据,基本只能干等了。举个例子,咱们作移动端的检索服务,其中须要调用第三方接口获取数据来构建倒排索引,若是第三方数据出错,咱们的索引也将出错,继而致使咱们的检索服务筛选出错误的内容。第三方服务恢复数据最快要半小时,咱们构建索引也须要半小时,便可能有超过1个多小时的时间检索服务将不能正常使用,这是不可接受的。如何兜底呢?咱们采起的方法是每隔一段时间保存全量索引文件快照,一旦第三方数据源出现数据污染问题,咱们先按下中止索引构建的开关,并快速回滚到早期正常的索引文件快照,这样尽管数据不是很新(可能1小时以前),可是至少能保证检索有结果,不至于对交易产生特别大的影响。
1.2 遵循快速失败原则,必定要设置超时时间
某服务调用的一个第三方接口正常响应时间是50ms,某天该第三方接口出现问题,大约有15%的请求响应时间超过2s,没过多久服务load飙高到10以上,响应时间也很是缓慢,即第三方服务将咱们服务拖垮了。
为何会被拖垮?没设置超时!咱们采用的是同步调用方式,使用了一个线程池,该线程池里最大线程数设置了50,若是全部线程都在忙,多余的请求就放置在队列里中。若是第三方接口响应时间都是50ms左右,那么线程都能很快处理完本身手中的活,并接着处理下一个请求,可是不幸的是若是有必定比例的第三方接口响应时间为2s,那么最后这50个线程都将被拖住,队列将会堆积大量的请求,从而致使总体服务能力极大降低。
正确的作法是和第三方商量肯定个较短的超时时间好比200ms,这样即便他们服务出现问题也不会对咱们服务产生很大影响。
1.3 适当保护第三方,慎重选择重试机制
须要结合本身的业务以及异常来仔细斟酌是否使用重试机制。好比调用某第三方服务,报了个异常,有些同窗就无论三七二十一就直接重试,这样是不对的,好比有些业务返回的异常表示业务逻辑出错,那么你怎么重试结果都是异常;又若有些异常是接口处理超时异常,这个时候就须要结合业务来判断了,有些时候重试每每会给后方服务形成更大压力,启到雪上加霜的效果。
2 防备使用方
这里又要坚持一条信念:“全部的使用方都不靠谱”,无论使用方什么天花乱坠的保证。基于这样的信念,咱们须要有如下行动。
2.1 设计一个好的api(RPC、Restful),避免误用
过去两年间看过很多故障,直接或间接缘由来自于糟糕的接口。若是你的接口让不少人误用,那要好好反思本身的接口设计了,接口设计虽然看着简单,可是学问很深,建议你们好好看看Joshua Bloch的演讲《How to Design a Good API & Why it Matters(如何设计一个好的API及为何这很重要)》以及《
Java API 设计清单》。
下面简单谈谈个人经验。
a) 遵循接口最少暴露原则
使用方用多少接口咱们就提供多少,由于提供的接口越多越容易出现乱用现象,言多必失嘛。此外接口暴露越多本身维护成本就越高。
b) 不要让使用方作接口能够作的事情
若是使用方须要调用咱们接口屡次才能进行一个完整的操做,那么这个接口设计就可能有问题。好比获取数据的接口,若是仅仅提供getData(int id);接口,那么使用方若是要一次性获取20个数据,它就须要循环遍历调用咱们接口20次,不只使用方性能不好,也无故增长了咱们服务的压力,这时提供getDataList(List<Integer> idList);接口显然是必要的。
c)避免长时间执行的接口
仍是以获取数据方法为例:getDataList(List<Integer> idList); 假设一个用户一次传1w个id进来,咱们的服务估计没个几秒出不来结果,并且每每是超时的结果,用户怎么调用结果都是超时异常,那怎么办?限制长度,好比限制长度为100,即每次最多只能传100个id,这样就能避免长时间执行,若是用户传的id列表长度超过100就报异常。
加了这样限制后,必需要让使用方清晰地知道这个方法有此限制。以前就遇到误用的状况,某用户一个订单买了超过100个商品,该订单服务须要调用商品中心接口获取该订单下全部商品的信息,可是怎么调用都失败,并且异常也没打出什么有价值的信息,后来排查很久才得知是商品中心接口作了长度限制。
怎么才能作到加了限制,又不让用户误用呢?
两种思路:1)接口帮用户作了分割调用操做,好比用户传了1w个id,接口内部分割成100个id列表(每一个长度100),而后循环调用,这样对使用方屏蔽了内部机制,对使用方透明;2)让用户本身作分割,本身写循环显示调用,这样须要让用户知道咱们方法作了限制,具体方法有:1)改变方法名,好比getDataListWithLimitLength(List<Integer> idList); ;2)增长注释;3)若是长度超过 100,很明确地抛出异常,很直白地进行告知。
d)参数易用原则
避免参数长度太长,通常超过3个后就较难使用,那有人说了我参数就是这么多,那怎么办?写个参数类嘛!
此外避免连续的同类型的参数,否则很容易误用。
能用其它类型如int等的尽可能不要用String类型,这也是避免误用的方法。
e)异常
接口应当最真实的反应出执行中的问题,更不能用聪明的代码作某些特别处理。常常看到一些同窗接口代码里一个try catch,无论内部抛了什么异常,捕获后返回空集合。
1
2
3
4
5
6
7
|
public List<Integer> test() {
try {
...
} catch (Exception e) {
return Collections.emptyList();
}
}
|
这让使用方很无奈,不少时候不知道是本身参数传的问题,仍是服务方内部的问题,而一旦未知就可能误用了。
2.2 流量控制,按服务分配流量,避免滥用
相信不少作太高并发服务的同窗都碰到相似事件:某天A君忽然发现本身的接口请求量忽然涨到以前的10倍,没多久该接口几乎不可以使用,并引起连锁反应致使整个系统崩溃。
为何会涨10倍,难道是接口被外人攻击了,以个人经验看通常内部人“做案”可能性更大。以前还见过有同窗mapreduce job调用线上服务,分分钟把服务搞死。
如何应对这种状况?生活给了咱们答案:好比老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理咱们的接口也须要安装上“保险丝”,以防止非预期的请求对系统压力过大而引发的系统瘫痪,当流量过大时,能够采起拒绝或者引流等机制。具体限流算法参见《
接口限流实践》一文。
3 作好本身
作好本身是个很是大的话题,从需求分析、架构设计 、代码编写、测试、code review、上线、线上服务运维等阶段均可以重点展开介绍,此次简单分享下架构设计、代码编写上的几条经验原则。
3.1 单一职责原则
对于工做了两年以上的同窗来讲,设计模式应该好好看看,我以为各类具体的设计模式其实并不重要,重要的是背后体现的原则。好比单一职责原则,在咱们的需求分析、架构设计、编码等各个阶段都很是有指导意义。
在需求分析阶段,单一职责原则能够界定咱们服务的边界,若是服务边界若是没界定清楚,各类合理的不合理的需求都接,最后致使服务出现不可维护、不可扩展、故障不断的悲哀结局。
对于架构来说,单一职责也很是重要。好比读写模块放置在一块儿,致使读服务抖动很是厉害,若是读写分离那将大大提升读服务的稳定性(读写分离);好比一个服务上同时包含了订单、搜索、推荐的接口,那么若是推荐出了问题可能影响订单的功能,那这个时候就能够将不一样接口拆分为独立服务,并独立部署,这样一个出问题也不会影响其余服务(资源隔离);又好比咱们的图片服务使用独立域名、并放置到cdn上,与其它服务独立(动静分离)。
从代码角度上讲,一个类只干一件事情,若是你的类干了多个事情,就要考虑将他分开。这样作的好处是很是清晰,之后修改起来很是方便,对其它代码的影响就很小。再细粒度看类里的方法,一个方法也只干一个事情,即只有一个功能,若是干两件事情,那就把它分开,由于修改一个功能可能会影响到另外一个功能。
3.2 控制资源的使用
写代码脑子必定要绷紧一根弦,认知到咱们所在的机器资源是有限的。机器资源有哪些?cpu、内存、网络、磁盘等,若是不作好保护控制工做,一旦某一资源满负荷,很容易致使出现线上问题。
3.2.1 CPU资源怎么限制?
a)计算算法优化
若是服务须要进行大量的计算,好比推荐排序服务,那么务必对你的计算算法进行优化,好比笔者曾经对地理空间距离计算这一重度使用的算法进行了优化,取得了较好的效果,详见《
地理空间距离计算优化》一文。
b)锁
对于不少服务而言,没有那么多耗费计算资源的算法,但cpu使用率也很高,这个时候须要看看锁的使用状况,个人建议是如无必要,尽可能不用显式使用锁。
c) 习惯问题
好比写循环的时候,千万要检查看看是否能正确退出,有些时候一不当心,在某些条件下就成为死循环,很著名的案例就是《
多线程下HashMap的死循环问题》。好比集合遍历时候使用性能较差的遍历方式、String +检查,若是有超过多个String相加,是否使用StringBuffer.append?
d)尽可能使用线程池
经过线程池来限制线程的数目,避免线程过多形成的线程上下文切换的开销。
e)jvm参数调优
3.2.2 内存资源怎么限制?
a)Jvm参数设置
b)初始化java集合类大小
使用java集合类的时候尽可能初始化大小,在长链接服务等耗费内存资源的服务中这种优化很是重要;
c)使用内存池/对象池
d)使用线程池的时候必定要设置队列的最大长度
以前看过好多起故障都是因为队列最大长度没有限制最后致使内存溢出。
e)若是数据较大避免使用本地缓存
若是数据量较大,能够考虑放置到分布式缓存如redis、tair等,否则gc均可能把本身服务卡死;
f)对缓存数据进行压缩
好比以前作推荐相关服务时,须要保存用户偏好数据,若是直接保存可能有12G,后来采用短文本压缩算法直接压缩到6G,不过这时必定要考虑好压缩解压缩算法的cpu使用率、效率与压缩率的平衡,一些压缩率很高可是性能不好的算法,也不适合线上实时调用。
有些时候直接使用probuf来序列化以后保存,这样也能节省内存空间。
g)清楚第三方软件实现细节,精确调优
在使用第三方软件时,只有清楚细节后才知道怎么节约内存,这点我在实际工做中深有体会,好比以前在阅读过lucene的源码后发现咱们的索引文件原来是能够压缩的,而这在说明文档中都找不到,具体参考《
lucene索引文件大小优化小结》一文。
3.2.3 网络资源怎么限制?
a)减小调用的次数
减小调用的次数?常常看到有同窗在循环里用redis/tair的get,若是意识到这里面的网络开销的话就应该使用批量处理;又如在推荐服务中常常遇到要去多个地方去取数据,通常采用多线程并行去取数据,这个时候不只耗费cpu资源,也耗费网络资源,一种在实际中经常采用的方法就是先将不少数据离线存储到一块 ,这时候线上服务只要一个请求就能将全部数据获取。
b)减小传输的数据量
一种方法是压缩后传输,还有一种就是按需传输,好比常常遇到的getData(int id),若是咱们返回该id对应的Data全部信息,一来人家不须要,二来数据量传输太大,这个时候能够改成getData(int id, List<String> fields),使用方传输相应的字段过来,服务端只返回使用方须要的字段便可。
3.2.4 磁盘资源怎么限制?
打日志要控制量,并按期清理。1)只打印关键的异常日志;2)对日志大小进行监控报警。我有一次就遇到了第三方服务挂了,而后我这边就不断打印调用该第三方服务异常的日志,原本个人服务有降级方案,若是第三方服务挂了会自动使用其它服务,可是忽然收到报警说我服务挂了,登上机器一看才知道是磁盘不够致使的崩溃;3)按期对日志进行清理,好比用crontab,每隔几天对日志进行清理;4)打印日志到远端,对于一些比较重要的日志能够直接将日志打印到远端HDFS文件系统里;
3.3 避免单点
不要把鸡蛋放在一个篮子上!从大层次上讲服务能够多机房部署、异地多活;从本身设计角度上讲,服务应该能作到水平扩展。
对于不少无状态的服务,经过nginx、zookeeper能轻松实现水平扩展;
对数据服务来讲,怎么避免单点呢?简而言之、能够经过分片、分层等方式来实现,后面会有个博文总结。
4 小结
如何避免故障?个人经验浓缩为一句:“怀疑第三方,防备使用方,作好本身”,你们也能够思考、总结并分享下本身的经验。