【51CTO.com原创稿件】程序裸奔在服务器,没有经过任何的处理(至于怎么处理,这里先卖个关子,待会儿给大家慢慢道来......),当大流量来临后,会发生什么呢?
图片来自 Pexels
关键时刻服务崩溃,损失了一个亿
有程序员说,对程序进行调优?调优能解决问题码?调优肯定可以解决一部分问题,但是调优仅仅可以缓解程序出现问题,并不能消灭问题。
各位试想一下,当系统面临大流量冲击的时候,CPU 被打满了(请参考下图所示):
可以发现 CPU 爆满,占比几乎达到 100%,对于系统来说是一个很危险的信号,随时都有可能崩溃掉。
内存被打满了(如下图),这个使用程序都可能出现崩溃:
这样的场景就不要调优能解决的了,这涉及到你的可利用的资源就这些,超越了资源使用的极限了,服务肯定扛不住。
可以发现内存平均占用已经超过了 80%,甚至有时候占用达到了接近 100%,这对于系统来说很危险,随时面临崩溃的风险。
那么这个服务该如何解决呢?至于如何解决这些问题,那就是一个非常大的课题,接下来我尽可能的通过有限的篇幅来描述清楚,这个问题该如何解决,如何在高并发,大流量环境下解保证系统平稳,高效的运行。
高并发下系统如何平稳运行
①流量集中,CPU 瘫痪
启动服务:运行 deploy-shop.sh 启动脚本,启动你的服务即可。启动过程中可以查看日志:tail -f t.log,观察是否启动成功。
压力测试:服务启动后,我们可以给定 10w 个样本进行压力测试,来观察服务在压力测试情况下会发生什么问题。
top 指令查询,观察 CPU 指标:
可以看见 load average 最终飙到 10 以上,说明 CPU 已经出现了严重的阻塞现象,如果不能及时解决,线上系统就会出现很严重的问题。
观察 TPS:
最终结果,就是导致程序程序大量的错误;这些错误还有持续变高的趋势,这对于线上系统来说可以不是一件好事情。
②搞不好,出现 OOM
启动脚本 2:使用脚本:deploy2.sh 启动项目。
这个脚本中限定了项目启动的内存大小为:500M。
Java 堆溢出 (java.lang.OutofMemoryError:Java heap space),内存溢出 out of memory:是指程序在申请内存时,没有足够的内存空间供其使用,出现 out of memory。
PermGen space(永久区内部不足)。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
StackOverflowError(虚拟机在扩展栈时无法申请到足够的内存空间)。
直接崩溃。
压力测试:
监控内存:使用 JProfiler 监控 JVM 内存情况是一种比较好的有效的手段,可以观察内存溢出,内存泄露等等异常情况。
可以发现内存一直处于飙升状态,甚至一度达到了 100%,最后甚至程序直接卡死,走不下去了,程序直接崩溃掉了。
查看日志:
接下来,我们去看一下日志,我们发现直接 OOM 了,甚至程序错误率 100%,直接崩溃。
③服务雪崩效应
在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。
服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。
上图所示,如果服务 T 因为高并发宕机,会影响到服务 U,而服务 U 又关联了 3 条线,最终将导致整个系统崩溃,也就是灾难性雪崩效应。
造成的原因:
服务提供者不可用(硬件故障,Bug,大量请求)
重试加大流量(用户重试不断发送请求,代码逻辑重试)
服务调用者不可用(同步等待造成资源耗尽)
有什么好的解决方案呢?
①服务降级
超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。
实现一个 fallback 方法,当请求后端服务出现异常的时候,可以使用 fallback 方法返回的值。
当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作。
某电商网站在搞活动时,活动期间压力太大,如果再进行下去,整个系统有可能挂掉,这个时候可以释放掉一些资源,将一些不那么重要的服务采取降级措施,比如登录、注册。
登录服务停掉之后就不会有更多的用户抢购,同时释放了一些资源,登录、注册服务就算停掉了也不影响商品抢购。
降级策略:当触发服务降级后,新的交易再次到达时,我们该如何来处理这些请求呢?
从微服务架构全局的视角来看,我们通常有以下是几种常用的降级处理方案:
页面降级:可视化界面禁用点击按钮、调整静态页面。
延迟服务:如定时任务延迟处理、消息入 MQ 后延迟处理。
写降级:直接禁止相关写操作的服务请求。
读降级:直接禁止相关读的服务请求。
缓存降级:使用缓存方式来降级部分读频繁的服务接口。
针对后端代码层面的降级处理策略,则我们通常使用以下几种处理措施进行降级处理:
抛异常
返回 NULL
调用 Mock 数据
调用 Fallback 处理逻辑
②服务超时
为每次请求设置一个比较短的超时时间,不管这次请求是否能够成功,短时间内这个线程就会被释放,那么这个服务就不会那么容易被底层服务拖死了。
此时,只要服务T达到阈值,就对服务 T 进行限流,至少服务不会被服务 T 拖死了。大大提高了我们服务的稳定性。
③服务限流
如果 B 服务器最大能承载的 TPS 为 1000,那么我们可以对服务设置一个阈值 800, 那么当 TPS 的请求超过 800 的时候,就立即对其进行限流,防止服务被大流量的冲垮。
④服务隔离
服务隔离:服务隔离的方式叫做仓壁模式。那么什么叫做仓壁模式呢???
可以看一下轮船的设计,底层设计的时候把轮船分为一个一个的格子,当一个,或者多个格子进水了,不影响这个轮船的运转。
当然也轮船最后沉没了,那是因为进水太多了,或者超过了容错的极限,因此这个轮船沉没了。
同样的道理,服务也可以参考这种设计,使用仓壁模式,实现服务隔离,从而使得服务具有更好的稳定性,健壮性。
那么服务中服务如何实现隔离呢??通常情况下,服务隔离我们都采用线程池隔离,那么什么是线程池隔离呢??搞清楚这个问题之前,我们先看看不做隔离会发生什么??
如下图所示,我们直接使用 Tomcat 本身的线程开始工作,服务分别会调用 用户服务,推荐服务,积分服务,当推荐服务不可用,或者资源耗尽,那么就会影响整个服务的正常运转。
因此这对服务来说,是一个及其不稳定的因素,那么如果使用隔离术,给他隔离呢??
此时当推荐服务资源耗尽,CPU 阻塞,线程池无可用连接,也不会影响整个服务出现崩溃,推荐服务仅仅是因为资源耗尽,而拒接连接。
限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。
类似的隔离技术还有很多:
进程隔离:说白了就是集群部署,当其中一个服务宕机,通过 Nginx 负载均衡来件故障容错。
集群隔离:所谓的集群隔离就是对相同的服务部署多个集群组,来达到集群隔离的目标。
如下图所示:
随着调用方的增多,当秒杀服务被刷会影响到其他服务的稳定性,此时应该考虑为秒杀提供单独的服务集群,即为服务分组,从而当某一个分组出现问题不会影响到其他分组,从而实现了故障隔离。
如下图所示:
机房隔离:随着对系统可用性的要求,会进行多机房部署,每个机房的服务都有自己的服务分组,本机房的服务应该只调用本机房服务,不进行跨机房调用。
其中一个机房服务发生问题时可以通过 DNS/负载均衡将请求全部切到另一个机房;或者考虑服务能自动重试其他机房的服务从而提升系统可用性。
读写隔离:通过主从模式将读和写集群分离,读服务只从从 Redis 集群获取数据,当主 Redis 集群出现问题时,从 Redis 集群还是可用的,从而不影响用户访问;而当从 Redis 集群出现问题时可以进行其他集群的重试。
动静隔离:把静态资源放入 Nginx,CDN 服务。达到动静隔离。防止有页面直接加载大量静态资源。因为访问量大,导致网络带宽打满,导致卡死,出现不可用。
热点隔离:秒杀、抢购属于非常合适的热点例子;对于这种热点是能提前知道的,所以可以将秒杀和抢购做成独立系统或服务进行隔离,从而保证秒杀/抢购流程出现问题不影响主流程。
还存在一些热点可能是因为价格或突发事件引起的;对于读热点我使用多级缓存搞定;而写热点我们一般通过缓存+队列模式削峰。
资源隔离:最常见的资源如磁盘、CPU、网络;对于宝贵的资源都会存在竞争问题。(实在不行单独部署一个服务,也可使用容器技术,或者虚拟机的方式实现资源隔离)
⑤服务熔断
服务熔断是一种断路器模式,断路器的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。
当失败率(如因网络故障/超时造成的失败率高)达到阀值自动触发降级,熔断器触发的快速失败会进行快速恢复。
服务限流:胃只有那么大,想吃很多好吃的但是装不下,所以只能打饭打少点。
服务降级:吃的太多了,别人没啥吃了,妈妈只好给你少打点饭,给兄弟们分点。
服务熔断:打翻你的饭碗,你直接别吃了。
C 对 S 熔断后,那么原本需要调用 S 实现的逻辑怎么办呢?C 可以使用 Mock 数据、缓存数据、缺省数据等替代,或者干脆就是抛异常返回错误信息。
⑥服务缓存
提供了请求缓存:
当大流量请求来临的时候,使用缓存也是非常有效减轻服务压力的重要手段。
当我们使用相同的请求不停的重复请求服务器资源的时候,此时我们就不要从缓存中命中数据,从而到达减轻服务压力,增加服务稳定性的目的,防止因为资源耗尽而出现服务崩溃。
⑦请求合并
请求合并减少 HTTP 请求:资源合并与压缩减少 HTTP 请求主要的两个优化点是减少 HTTP 请求的数量和减少请求资源的大小。
HTTP 协议是无状态的应用层协议,意味着每次 HTTP 请求都需要建立通信链路、进行数据传输,而在服务器端,每个 HTTP 都需要启动独立的线程去处理。
这些通信和服务的开销都很昂贵,减少 HTTP 请求的数量和减少请求资源的大小可有效提高访问性能。
减少 HTTP 的主要手段是合并 CSS、合并 JavaScript、合并图片。将浏览器一次访问需要的 JavaScript 和 CSS 合并成一个文件,这样浏览器就只需要一次请求。
图片也可以合并,多张图片合并成一张,如果每张图片都有不同的超链接,可通过 CSS 偏移响应鼠标点击操作,构造不同的 URL。将图片 base64,这样也可以减少请求。
在微服务架构中,我们将一个项目拆分成很多个独立的模块,这些独立的模块通过远程。
调用来互相配合工作,但是,在高并发情况下,通信次数的增加会导致总的通信时间增加,同时,线程池的资源也是有限的,高并发环境会导致有大量的线程处于等待状态,进而导致响应延迟,为了解决这些问题,我们采用请求合并。
当然请求合并也不是万能的解决的方案,只能说我们在这些方案中选择一个折中的处理方案来进行处理相关的问题。
总结
好了,以上就是保证系统如何在流量洪峰中稳定,平稳的运行的解决方案了。
当然上面所有的都是从理论的角度来阐述如何解决这些问题,如何想要进一步对其进行落地,那么就可以借助一些框架(例如:spring cloud Alibaba sentinel)组件,来帮助我们实现系统的升级。
作者:JackHu
简介:水滴健康基础架构资深技术专家
编辑:陶家龙
征稿:有投稿、寻求报道意向技术人请联络 editor@51cto.com
【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】