一次奇妙的服务“宕机”排错之旅
起因
这天,生产上突然传来噩耗,一个服务宕机了,调用该服务都失效了,这可是大问题,第一时间想到的就是别不是这个服务宕机了吧,然后赶紧叫运维看下服务状态,可是发现服务状态是好的,可是为什么前端调用都是超时呢。没办法,现在只能先重启大法好,复制了一份服务和zuul日志下来,然后重启服务,万幸的是服务恢复了。但是为什么呢。甲方也要我们拿出措施来防止下次发生同样的事。
先说下我们的架构,简简单单的zuul+eureka+服务进行通信。
回想下我们当时的排错措施,1.前端调用失败,返回是超时。2.本地服务状态是好的,利用curl来调用,能正常提供服务。3.查看日志,发现服务日志并没有异常报错,查看zuul日志,发现只是一些超时异常。
猜想一,是不是服务挂了
第一思路就是,是不是服务的某个接口有问题,比如说用了太长时间,但是这样会导致服务整个提供服务失败吗。想想也不可能,服务有熔断机制,过长时间会自己熔断掉的。那是不是配置的原因呢,我们的服务某个配置导致失败呢。配置也没改动啊,生产上。这个只是偶发事故。配置也很久没动了。
那我就猜想,服务是怎么提供服务的,是通过线程的啊,那会不会是某个接口,调用了太多的线程,导致线程阻塞了,然后对外提供服务失败,也不对啊,本地调用成功的当时。但是当时就钻这个坑了,是不是某个地方接口开了太多线程,开发这个服务的同事说,自己在这个服务的一个接口里,调用了很多次的fegin,为了优化,又开了线程去调用fegin,但是那时候发现fegin很多直接熔断了,所以进行了个性化配置,会不会出问题在这。
先看下feign的配置。
hystrix: #熔断器配置
threadpool:
default:
coreSize: 100
command:
default:
fallback:
isolation:
semaphore:
maxConcurrentRequests: 50
execution:
timeout:
enabled: true
isolation
thread:
timeoutInMilliseconds: 600000
circuitBreaker:
requestVolumeThreshold: 1000
这个配置也很好理解,fegin的核心线程数100,最大请求50,超时时间和错误多少熔断。
这个时候我就疑问来了,fegin是怎么去调用其他服务的,我理解的是fegin也只是通过eureka来把服务名换取ip,然后自己拼接成http请求去调用服务,但是怎么管理的呢。然后我就去百度配置了。
https://blog.csdn.net/tongtong_use/article/details/78611225
https://www.jianshu.com/p/f7fb59f43485
https://blog.csdn.net/hry2015/article/details/78554846
https://www.cnblogs.com/duan2/p/9302431.html
很明显,这里比较重要的是
semaphore和thread隔离策略的区别
https://blog.csdn.net/icangfeng/article/details/81203490
https://blog.csdn.net/liaojiamin0102/article/details/94394956?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromBaidu-1.not_use_machine_learn_pai&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromBaidu-1.not_use_machine_learn_pai
第二个链接就比较清晰易懂了。
但是我这里还是一头雾水,不知道出哪了。
猜想二,zuul网关问题
我们的服务链路是nginx-zuul-服务。这个时候是zuul到服务的链路断了,应该是zuul的问题吧。
但是怎么验证呢。
这时候老大就提出了验证的方案,服务里弄个验证服务接口,接口里睡到超时,然后开个jmeter开一百个线程去调用,看看这时候能通过网关访问服务吗。
说做就做。
第一步:服务里写接口
@GetMapping("/f/test")
public String test() throws InterruptedException {
Integer next = Mytest.getNext();
System.out.println("当前第"+next+"线程执行");
Thread.sleep(40000);
System.out.println("当前第"+next+"线程执行完成");
return "success";
}
mytest是一个我写的类,里面有个类常量,用来计数的。
public class Mytest {
public static Integer num=0;
public static synchronized Integer getNext(){
Mytest.num=Mytest.num+1;
return Mytest.num;
}
}
接口写好了,然后就是jmeter了,不了解的百度下(https://www.cnblogs.com/monjeo/p/9330464.html)
看下zuul网关的设置
ribbon:
ReadTimeout: 30000
ConnectTimeout: 30000
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
eureka:
enabled: true
hystrix:
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 360000
可以看出,zuul网关应该也是用了熔断的,然后连接超时是30s,读超时是30s(应用执行超时)。然后熔断是360s。因为设置了最大重试,(30+30)*(1+2)
在*2=360
然后这又一个知识点。eureka:enabled表明zuul用的是eureka的ribbon客户端。ribbon是支持替换底层的通信框架的,本来用的原来的javaconnect。然后你可以改为http-client。
先不管其他的先测试,先测试能不能正常访问服务的其他接口,能。然后设置test接口为读超时,40s。然后jmeter一开,emm。立刻访问服务的其他的服务,发现,服务跟zuul的连接断了,问题重现了。
问题重现就好办了,我们先来分析下我们的测试,我们只是开了100线程,去访问一个读超时的接口,然后在继续访问就失败了,
那我们分析下zuul的实现,他是怎么做到请求转发的呢,其实就是调用ribbon来转发请求,分为自己管理线程来转发请求和信号量管理,那会不会是因为调用了太多线程,而线程卡在了超时的接口上,并没有超时,导致线程没有空出来,而后续的请求这个服务的都阻塞了。
然后我发现了一个细节,我调用的jemeter设置的线程数是100,而测试类的打印线程数只有50。那么思路就有了,是不是zuul设置了服务的并发量为50,超过就拒绝。思路有了,就百度。
http://www.chinacion.cn/article/4126.html
又涉及到了thread和semaphore。
然后可以发现我们zuul用的是semaphore,那是不是单个服务实例的信号量设置的过小,而一些过长时间接口,用户以为失效,然后点了过多,然后zuul的请求就阻塞这几个了,然后线程数越来越少,最后直接熔断了,拒绝服务了。
基本就是这样了,那就设置下单个服务的最大并发请求,然后设置下读超时。
ribbon:
ReadTimeout: 120000
ConnectTimeout: 30000
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
MaxConnectionsPerHost: 500 #单个后端微服务实例能接收的最大请求并发数
MaxTotalConnections: 2000
eureka:
enabled: true
hystrix:
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 900000
然后用jemeter测试,现在100个线程,控制台就打印出100个了
然后不影响通过网关调用服务的其他接口了。
https://blog.csdn.net/liuminglei1987/article/details/103676945
https://blog.csdn.net/weixin_36647532/article/details/89225800
那线程执行完毕会恢复访问吗,我在测试后,再通过网关调用服务的其他接口,恢复通信了。
那说明是不是等服务执行完毕,他们zuul不能调用的就会恢复,但是现在暂时不能验证,毕竟生产环境又不等你。
续
emm,生产上又出现这个问题了,改了配置上去没有用,emm,甲方要杀人了。
再排查问题,还是用上面的测试,改为1000的线程,发现,好像只能最多一次打印200次调用。
我明明设置了500的,为什么只有两百,然后我在想,是不是其他地方又限制导致上不去。首先先的是不是服务方的对外的线程就200?
测试的接口不从网关调用,直接本地端口调用,确实是200.
然后是找到springboot设置线程的值。
server:
tomcat:
max-threads: 1000
确实默认值是200,然后修改,然后发现,测试改为zuul,还是200,是不是zuul网关也有这个设置。
设置了以后确实测试是1000了,但是问题又来了,当我占满了zuul的线程,其实通过网关调用其他服务是失败的,但是生产上的话,只是这个模块与网关的调用失效了。
重新总结下现象。
一。zuul服务日志报超时
二。网关调用其他服务是成功的
三。本地调用是成功的,但是是在发生熔断后的。
怀疑点,1.zuul网关的并发信号量最大请求设置有问题 。2.zuul网关的线程数设置有问题。3。服务方的线程数设置有问题。
4.怀疑是不是服务心跳到eureka失败了。
后来老大怀疑是不是前端某个地方在疯狂调用这个服务,导致服务挂着了,倒真的找到了个地方。一次调用200次接口,500ms调一次。
这个肯定不行的,先改这个吧。看看后续会不会出现问题。
解决方案
现在能做的就是,zuul服务部署多个,保证一个不行,其他能行,nginx分发,(但是这样不会一次请求好,一次请求失败?)
设置单个服务的最大并发量。
优化长时间时接口,转为异步接口。
优化前端的调用设置,长时间接口调用按钮,一次后置为不可用。