高并发服务的几条优化经验

   前言: 如何优化高并发服务,这里指的是qps在20万以上的在线服务,注意不是离线服务,在线服务会存在哪些挑战呢?①无法做离线缓存,所有的数据都是实时读的 ②大量的请求会打到线上服务,对于服务的响应时间要求较高,一般都是限制要求在300ms以内,如果超过这个时间那么对用户造成的体验就会急剧下降 ③数据量较大,单次如果超过50W的qps,单条1kb,50万就是5GB了,1分钟30G,对于底层的数据存储与访问都有巨大的压力~ 如何应对这些棘手的问题,本篇博客来讨论一下

 

一:向关系型数据库sayno

一个真正的大型互联网面向c端的服务都不会直接使用数据库作为自己的存储系统,无论你是采用的是分库分表还是底层用了各种优秀的连接池等,mysql/oracle在面对大型在线服务是存在天然的劣势,再如何优化,也难以抵挡qps大于50万流量带来的冲击。所以换个思路,我们必须使用nosql类缓存系统,比如redis/mermCache等作为自己的"数据库",而mysql等关系型数据库只是一种兜底,用于异步去写作为数据查询的备份系统。

场景举例:京东双11主会场,上架了部分商品,这部分商品都是在会场开始上架的时候直接写入redis中的,当上架完成之后,通过异步消息写入到mysql中。面向c端的查询都是直接读redis,而不是数据库.而b端的查询,可以走数据库去查询。这部分流量不是很高,数据库绝对可以抵挡的住。

二:多级缓存

都知道缓存是高并发提高性能的利器之一。而如何使用好缓存进而利用好多级缓存,是需要我们去思考的问题。redis目前是缓存的第一首选.单机可达6-8万的qps,在面对高并发的情况下,我们可以手动的水平扩容,以达到应对qps可能无线增长的场景。但是这种做法也存在弊端,因为redis是单线程的,并且会存在热点问题。虽然redis内部用crc16算法做了hash打散,但是同一个key还是会落到一个单独的机器上,就会使机器的负载增加,redis典型的存在缓存击穿和缓存穿透两个问题,尤其在秒杀这个场景中,如果要解决热点问题,就变的比较棘手。这个时候多级缓存就必须要考虑了,典型的在秒杀的场景中,单sku商品在售卖开始的瞬间,qps会急剧上升.而我们这时候需要用memeryCache来挡一层,memeryCache是多线程的,比redis拥有更好的并发能力,并且它是天然可以解决热点问题的。有了memeryCache,我们还需要localCache,本地缓存,这是一种以内存换速度的方式。本地缓存会接入用户的第一层请求,如果它找不到,接下来走memeryCache,然后走redis,这套流程下来可以挡住百万的qps.

三:多线程

我记得在刚开始入行的时候,每次面试都会被问到多线程,那时候是一脸懵逼,多线程有这么厉害吗?干嘛都说多线程,为什么要使用多线程,不用行不行?要讲明这个道理,我先来说一个实例.曾经我优化过一个接口,很典型的一个场景。原始的方式是循环一个30-40万的list,list执行的操作很简单,就是读redis的数据,读一次大概需要3ms左右,这是同步的方式,在预览环境测试,直接30秒+超时。后来优化的方式就是把原有的同步调用改为线程池调用,线程池里的线程数或阻塞队列大小需要自己调优,最后实测接口rt只需要3秒。足以见多线程的威力。在多核服务的今天,如果还不用多线程就是对服务器资源的一种浪费。这里需要说一句,使用多线层一定要做好监控,你需要随时知道线程的状态,如果线程数和queueSize设置的不恰当,将会严重影响业务~ 当然多线程也要分场景,如果为了多线程而多线程反而是一种浪费,因为多线程调度的时候会造成线程在内核态和用户态之间来回切换,如果使用不当反而会有反作用

四: 降级和熔断

降级和熔断是一种自我保护措施,这和电路上的熔断器的基本原理是一样的,防止电流过大引起火灾等,面对不可控的巨大流量请求很有可能会击垮服务器的数据库或者redis,使服务器宕机或者瘫痪造成不可挽回的损失。因为我们服务的本身需要有防御机制,以抵挡外部服务对于自身的侵入导致服务受损引起连带反应。降级和熔断有所不同,两者的区别在于降级是将一些线上主链路的功能关闭,不影响到主链路.熔断的话,是指A请求B,B检测到服务流量多大启动了熔断,那么请求会直接进入熔断池,直接返回失败。如何抉择使用哪一个需要在实际中结合业务场景来考虑.

五: 优化IO

很多人都会忽视IO这个问题,频繁的建联和断联都是对系统的重负。在并发请求中,如果存在单个请求的放大效那么将会使io呈指数倍增加。举个例子,比如主会场的商品信息,如果需要商品的某个具体的详情,而这个详情需要调用下游来单个获取.随着主会场商品的热卖,商品越来越多,一次就要经过商品数X下游请求的数量,在海量的qps请求下,IO数被占据,大量的请求被阻塞,接口的响应速度就会呈指数级下降。所以需要批量的请求接口,所有的优化为一次IO

六: 慎用重试

重试作为对临时异常的一种处理的常见手法,常见应对的方式是请求某个服务失败或者写数据库了重新再试,使用重试一定要注意以下几点①控制好重试次数②重试的间隔时间得衡量好③是否重试要做到配置化。之前我们线上出了一个bug,kafka消费出现了严重的lag,单词消耗时间是10几秒,看代码之后发现是重试的次数过多导致的,并且次数还不支持配置化修改,所以当时的做法只能是临时改代码后上线.重试作为一种业务的二次尝试,极大提升了程序的请求success,但是也要注意以上几点。

七:边界case的判断和兜底

作为互联网老手,很多人写出的代码都不错,但是在经历过几轮的故障review之后发现很多酿成重大事故的代码背后都是缺少对一些边界问题的处理,所犯的错误非常简单,但是往往就是这些小问题就能酿成大事故.曾经review过一次重大的事故,后来发现最终的原因居然是没有对空数组进行判空,导致传入下游的rpc是空的,下游直接返回全量的业务数据,影响数百万用户。这个代码改动起来很简单,但是是令人需要反省的,小小的不足酿成了大祸

八:学会优雅的打印日志

日志作为追溯线上问题的最佳利器,可谓保留bug现场的唯一来源。虽然有arthas这样的利器方便我们排查问题,但是对于一些比较复杂的场景,还是需要日志来记录程序的数据.但是在高流量的场景中,如果全量打印日志对于线上来说就是一种灾难,有以下缺点①严重占用磁盘,估算以下,如果接口的qps在20万左右,日志一秒就几千兆,一天下来就是上千GB ②大量的日志需要输出,占用了程序IO,增加了接口的RT(响应时间) 如果需要解决这个问题,①我们可以利用限流组件来实现一个基于限流的日志组件,令牌桶算法可以限制打印日志的流量,比如一秒只允许打印一条日志 ②基于白名单的日志打印,线上配置了白名单用户才可以打印出来,节省了大量了无效日志输出

   总结: 本篇博客讨论了高并发服务在面对大流量时的一些基本注意事项和应对的点,当然实际线上的比目前的更复杂,这里只是给出几条建议,希望我们在高并发的路上保持敬畏,继续探索.更好的深耕c端服务,做更好的互联网应用,加油

  

上一篇:Minesweeper(蓝桥杯)


下一篇:性能测试——“问题分析”