我所经历的一次Dubbo服务雪崩,这是一个漫长的故事

在一个处理用户点击广告的高并发服务上找到了问题。看到服务打印的日记后我完全蒙了,全是jedis读超时,Read time out!一直用的是亚马逊的Redis服务,很难想象Jedis会读超时。

看了服务的负载均衡统计,发现并发增长了一倍,从每分钟3到4万的请求数,增长到8.6万,很显然,是并发翻倍导致的服务雪崩。

服务的部署:

处理广告点击的服务:2台2核8g的实例,每台部署一个节点(服务)。下文统称服务A

规则匹配服务(Rpc远程调用服务提供者):2个节点,2台2核4g实例。下文统称服务B

还有其它的服务提供者,但不是影响本次服务雪崩的凶手,这里就不列举了。

从日记可以看出的问题:

一是远程rpc调用大量超时,我配置的dubbo参数是,每个接口的超时时间都是3秒。服务提供者接口的实现都是缓存级别的操作,3秒的超时理论上除了网络问题,调用不应该会超过这个值。在服务消费端,我配置每个接口与服务端保持10个长连接,避免共享一个长连接导致应用层数据包排队发送和处理接收。

二是刚说的Jedis读操作超时,Jedis我配置每个服务节点200个最小连接数的连接池,这是根据netty工作线程数配置的,即读写操作就算200个线程并发执行,也能为每个线程分配一个连接。这是我设置Jedis连接池连接数的依据。

三是文件句柄数达到上线。SocketChannel套接字会占用一个文件句柄,有多少个客户端连接就占用多少个文件句柄。我在服务的启动脚本上为每个进程配置102400的最大文件打开数,理论上目前不会达到这个值。服务A底层用的是基于Netty实现的http服务引擎,没有限制最大连接数。

所以,解决服务雪崩问题就是要围绕这三个问题出发。

第一次是怀疑redis服务扛不住这么大的并发请求。估算广告的一次点击需要执行20次get操作从redis获取数据,那么每分钟8w并发,就需要执行160w次get请求,而redis除了本文提到的服务A和服务B用到外,还有其它两个并发量高的服务在用,保守估计,redis每分钟需要承受300w的读写请求。转为每秒就是5w的请求,与理论值redis每秒可以处理超过 10万次读写操作已经过半。

由于历史原因,redis使用的还是2.x版本的,用的一主一从,jedis配置连接池是读写分离的连接池,也就是写请求打到主节点,读请求打到从节点,每秒接近5w读请求只有一个redis从节点处理,非常的吃力。所以我们将redis升级到4.x版本,并由主从集群改为分布式集群,两主无从。别问两主无从是怎么做到的,我也不懂,只有亚马逊清楚。

Redis升级后,理论上,两个主节点,分槽位后请求会平摊到两个节点上,性能会好很多。但好景不长,服务重新上线一个小时不到,并发又突增到了六七万每分钟,这次是大量的RPC远程调用超时,已经没有jedis的读超时Read time out了,相比之前好了点,至少不用再给Redis加节点。

这次的事故是并发量超过临界值,超过redis的实际最大qps(跟存储的数据结构和数量有关),虽然升级后没有Read time out! 但Jedis的Get读操作还是很耗时,这才是罪魁祸首。Redis的命令耗时与Jedis的读操作Read time out不同。

redis执行一条命令的过程是:

  1. 接收客户端请求
  2. 进入队列等待执行
  3. 执行命令
  4. 响应结果给客户端

由于redis执行命令是单线程的,所以命令到达服务端后不是立即执行,而是进入队列等待。redis慢查询日记记录slowlog get的是执行命令的耗时,对应步骤3,执行命令耗时是根据key去找到数据所在的内存地址这段时间的耗时,所以这对于key-value字符串类型的命令而言,并不会因为value的大小而导致命令耗时长。

为验证这个观点,我进行了简单的测试。

分别写入四个key,每个key对应的value长度都不等,一个比一个长。再来看下两组查询日记。先通过CONFIG SET slowlog-log-slower-than 0命令,让每条命令都记录耗时。

key_4的value长度比key_3的长两倍,但get耗时比key_3少,而key_1的value长度比key_2短,但耗时比key_2长。

第二组数据也是一样的,跟value的值大小无关。所以可以排除项目中因value长度过长导致的slowlog记录到慢查询问题。慢操作应该是set、hset、hmset、hget、hgetall等命令耗时比较长导致。

而Jedis的Read time out则是包括1、2、3、4步骤,从命令的发出到接收完成Redis服务端的响应结果,超时原因有两大原因:

  • redis的并发量增加,导致命令等待队列过长,等待时间长
  • get请求读取的数据量大,数据传输时间长

所以将Redis从一主一从改为两主之后,导致Jedis的Read time out的原因一有所缓解,分摊了部分压力。但是原因2还是存在,耗时依然是问题。

Jedis的get耗时长导致服务B接口执行耗时超过设置的3s。由于dubbo消费端超时放弃请求,但是请求已经发出,就算消费端取消,提供者无法感知服务端超时放弃了,还是要执行完一次调用的业务逻辑,就像说出去的话收不回来一样。

由于dubbo有重试机制,默认会重试两次,所以并发8w对于服务b而言,就变成了并发24w。最后导致业务线程池一直被占用状态,RPC远程调用又多出了一个异常,就是远程服务线程池已满,直接响应失败。

问题最终还是要回到Redis上,就是key对应的value太大,传输耗时,最终业务代码拿到value后将value分割成数组,判断请求参数是否在数组中,非常耗时,就会导致服务B接口耗时超过3s,从而拖垮整个服务。

模拟服务B接口做的事情,业务代码(1)。

/**
 * @author wujiuye
 * @version 1.0 on 2019/10/20 {描述:}
 */
public class Match {

    static class Task implements Runnable {
        private String value;

        public Task(String value) {
            this.value = value;
        }

        @Override
        public void run() {
            for (; ; ) {
                // 模拟jedis get耗时
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // =====> 实际业务代码
                long start = System.currentTimeMillis();
                List<String> ids = Arrays.stream(value.split(",")).collect(Collectors.toList());
                boolean exist = ids.contains("4029000");
                // ====> 输出结果,耗时171ms .
                System.out.println("exist:" + exist + ",time:" + (System.currentTimeMillis() - start));
            }
        }
    }

    ;

    public static void main(String[] args) {
        // ====> 模拟业务场景,从缓存中获取到的字符串
        StringBuilder value = new StringBuilder();
        for (int i = 4000000; i <= 4029000; i++) {
            value.append(String.valueOf(i)).append(",");
        }
        String strValue = value.toString();
        System.out.println(strValue.length());
        for (int i = 0; i < 200; i++) {
            new Thread(new Task(strValue)).start();
        }
    }
}

这段代码很简单,就是模拟高并发,把200个业务线程全部耗尽的场景下,一个简单的判断元素是否存在的业务逻辑执行需要多长时间。把这段代码跑一遍,你会发现很多执行耗时超过1500ms,再加上Jedis读取到数据的耗时,直接导致接口执行耗时超过3000ms。

这段代码不仅耗时,还很耗内存,没错,就是这个Bug了。改进就是将id拼接成字符串的存储方式改为hash存储,直接hget方式判断一个元素是否存在,不需要将这么大的数据读取到本地,即避免了网络传输消耗,也优化了接口的执行速度。

由于并发量的增长,导致redis读并发上升,Jedis的get耗时长,加上业务代码的缺陷,导致服务B接口耗时长,从而导致服务A远程RPC调用超时,导致dubbo超时重试,导致服务B并发乘3,再导致服务B业务线程池全是工作状态以及Redis并发又增加,导致服务A调用异常。正是这种连环效应导致服务雪崩。

最后优化分三步

一是优化数据的redis缓存的结构,刚也提到,由大量id拼接成字符串的key-value改成hash结构缓存,请求判断某个id是否在缓存中用hget,除了能降低redis的大value传输耗时,也能将判断一个元素是否存在的时间复杂度从O(n)变为O(1),接口耗时降低,消除RPC远程调用超时。

二是业务逻辑优化,降低Redis并发。将服务B由一个服务拆分成两个服务。这里就不多说了。

三是Dubbo调优,将Dubbo的重试次数改为0,失败直接放弃当前的广告点击请求。为避免突发性的并发量上升,导致服务雪崩,为服务提供者加入熔断器,估算服务所能承受的最大QPS,当服务达到临界值时,放弃处理远程RPC调用。

(我用的是Sentinel,官方文档传送门:

https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0

所以,缓存并不是简单的Get,Set就行了,Redis提供这么多的数据结构的支持要用好,结合业务逻辑优化缓存结构。避免高并发接口读取的缓存value过长,导致数据传输耗时。同时,Redis的特性也要清楚,分布式集群相比单一主从集群的优点。反省img。

经过两次的项目重构,项目已经是分布式微服务架构,同时业务的合理划分让各个服务之间完美解耦,每个服务内部的实现合理利用设计模式,完成业务的高内聚低耦合,这是一次非常大的改进,但还是有还多历史遗留的问题不能很好的解决。同时,分布式也带来了很多问题,总之,有利必有弊。

有时候就需要这样,被项目推着往前走。在未发生该事故之前,我花一个月时间也没想出困扰我的两大难题,是这次的事故,让我从一个短暂的夜晚找出答案,一个通宵让我想通很多问题。

原文地址:https://blog.51cto.com/14570694/2447551

时间: 2024-09-29 21:54:12

我所经历的一次Dubbo服务雪崩,这是一个漫长的故事的相关文章

记一次redis挂机导致的服务雪崩事故,不对,是故事~

事故时常有,最近特别多!但每次事故总会有人出来背锅!如果不是自己的锅,解决了对自己是一种成长,如果是自己的锅,恐怕锅大了,就得走人了,哈哈哈... 这不,最近又出了一个锅:从周五开始,每天到11点就不停的接到服务器报警,对于一般的报警,我们早已见怪不怪了,然后作了稍微排查(监控工具: CAT),发现是redis问题,没找到原因,然后过了一会自己就好了,所以刚开始也没怎么管他.然后,第二天报警,第三天报警,领导火了,然后只好说,要不等到周一上班咱们再解决吧! 周一,开发同学还没去找运维同学查问题,

当当网开源Dubbox,扩展Dubbo服务框架支持REST风格远程调用

当当网近日开源了Dubbox项目,可为Dubbo服务框架提供多项扩展功能,包括REST风格远程调用.Kryo/FST序列化等等. 当当网架构部和技术委员会架构师沈理向InfoQ中文站介绍了Dubbox项目,开发背景和主要特点描述如下: Dubbo是一个被国内很多互联网公司广泛使用的开源分布式服务框架,即使从国际视野来看应该也是一个非常全面的SOA基础框架.作为一个重要的技术研究课题,在当当网我们根据自身的需求,为Dubbo实现了一些新的功能,并将其命名为Dubbox(即Dubbo eXtensi

dubbo服务与消费

一.前言 项目中用到了Dubbo,临时抱大腿,学习了dubbo的简单实用方法.现在就来总结一下dubbo如何提供服务,如何消费服务,并做了一个简单的demo作为参考. 二.Dubbo是什么 Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案.简单的说,dubbo就是个服务框架,如果没有分布式的需求,其实是不需要用的,只有在分布式的时候,才有dubbo这样的分布式服务框架的需求,并且本质上是个服务调用的东东,说白了就是个远程服务调用的分布式框架

基于Spring开发的DUBBO服务接口测试

基于Spring开发的DUBBO服务接口测试 知识共享主要内容: 1. Dubbo相关概念和架构,以及dubbo服务程序开发步骤. 2. 基于Spring开发框架的dubbo服务接口测试相关配置. 3. spring test+junit和spring test+TestNG两种测试框架脚本编写方法. 一.        DUBBO与DUBBO架构 1.          什么是dubbo?DUBBO是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,是阿里巴巴SOA服务化治

学习dubbo(六):部署dubbo服务

1.上传jar至服务器 将打包好的jar包上传,我这上传到/edu/service/user 2.使用java命令启动     java -jar edu-service-user.jar & 3.查看管控台 如上,OK启动成功了 自定义dubbo服务维护的shell脚本 脚本命名规范:/edu/service/xxx/service-xxx.sh 脚本命名,如:/edu/service/user/service-user.sh 效果: cd /edu/service/user ./servic

Dubbo服务调用的动态代理和负载均衡

Dubbo服务调用的动态代理及负载均衡源码解析请参见:http://manzhizhen.iteye.com/blog/2314514

跟我学习dubbo-使用Maven构建Dubbo服务的可执行jar包(4)

Dubbo服务的运行方式: 1.使用Servlet容器运行(Tomcat.Jetty等)----不可取 缺点:增加复杂性(端口.管理) 浪费资源(内存) 官方:服务容器是一个standalone的启动程序,因为后台服务不需要Tomcat或JBoss等Web容器的功能,如果硬要用Web容器去加载服务提供方,增加复杂性,也浪费资源. 2.自建Main方法类来运行(Spring容器) ----不建议(本地调试可用) 缺点: Dobbo本身提供的高级特性没用上 自已编写启动类可能会有缺陷 官方:服务容器

钻牛角尖还是走进死胡同--shell脚本根据名称获得 dubbo 服务的 pid

到了下午,突然觉得坐立不安,可能是因为中午没有休息好.老大不小了还在做页面整合的事情,这是参加工作时就干的工作了.然后突然想去挑战高级一点的缺陷排查,结果一不小心就钻了一个牛角尖.启动 dubbo 服务的shell 脚本总是让我觉得不爽,于是一研究,就不想干别的了,非要把它整顺不可.虽然买了鸟哥Linux私房菜的书,但没有认真看,很多东西都不记得了,只好度娘了一下午,但是度娘出来的结果质量不高,后来换了谷歌一下就搜索到高质量的文章.于是整明白了 Bash Shell 和 shell 脚本还是两码

Tomcat中部署web应用 ---- Dubbo服务消费者Web应用war包的部署

IP:192.168.2.61 部署容器:apache-tomcat-7.0.57 端口:8080 应用:edu-web-boss.war 1.下载(或上传)最新版的Tomcat7: $wget http://mirrors.hust.edu.cn/apache/tomcat/tomcat-7/v7.0.57/bin/apache-tomcat-7.0.57.tar.gz 2.规范安装目录: /home/wusc/edu/web/xxx-tomcat 如: /home/wusc/edu/web/