前言

日常Bug排查系列都是一些简单Bug排查。笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材_

Bug现场

最近碰到一个产线问题,表现为某个应用集群所有的节点全部下线了。导致上游调用全部报错。而且从时间线分析来看。这个应用的节点是逐步失去响应的。因为请求量较小,直到最后一台也失去响应后,才发现这个集群有问题。

线程逐步耗尽

笔者观察了下监控,发现每台机器的BusyThread从上次发布开始就逐步增长,一直到BusyThread线程数达到200才停止,而这个时间和每台机器从注册中心中摘除的时间相同。看了下代码,其配置的最大处理请求线程数就是200。

查看线程栈

很容易的,我们就想到去观察相关机器的线程栈。发现其所有的的请求处理线程全部Block在com.google.common.util.concurrent.SettableFuture的不同实例上。卡住的堆栈如下所示:


at sun.misc.Unsafe.park (Native Method: )
at java.util.concurrent.locks.LockSupport.park (LockSupport.java: 175)
at com.google.common.util.concurrent.AbstractFuture.get (AbstractFuture.java: 469)
at com.google.common.util.concurrent.AbstractFuture$TrustedFuture.get (AbstractFuture.java: 76)
at com.google.common.util.concurrent.Uninterruptibles.getUninterruptibly (Uninterruptibles.java: 142)
at com.google.common.cache.LocalCache$LoadingValueReference.waitForValue (LocalCache.java: 3661)
at com.google.common.cache.LocalCache$Segment.waitForLoadingValue (LocalCache.java: 2315)
at com.google.common.cache.LocalCache$Segment.get (LocalCache.java: 2202)
at com.google.common.cache.LocalCache.get (LocalCache.java: 4053)
at com.google.common.cache.LocalCache.getOrLoad (LocalCache.java: 4057)
at com.google.common.cache.LocalCache$LocalLoadingCache.get (LocalCache.java: 4986)
at com.google.common.cache.ForwardingLoadingCache.get (ForwardingLoadingCache.java: 45)
at com.google.common.cache.ForwardingLoadingCache.get (ForwardingLoadingCache.java: 45)
at com.google.common.cache.ForwardingLoadingCache.get
......
at com.XXX.business.getCache
......

从GuavaCache获取缓存为什么会被卡住

GuavaCache是一个非常成熟的组件了,为什么会卡住呢?使用的姿势不对?于是,笔者翻了翻使用GuavaCache的源代码。其简化如下:

private void initCache() {
ExecutorService executor = new ThreadPoolExecutor(1, 1,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(50), // 注意这个QueueSize
new NamedThreadFactory(String.format("cache-reload-%s")),
(r, e) -> {
log.warn("cache reload rejected by threadpool!"); // 注意这个reject策略 });
this.executorService = MoreExecutors.listeningDecorator(executor);
cache = CacheBuilder.newBuilder().maximumSize(100) // 注意这个最大值
.refreshAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader<K, V>() {
@Override
public V load(K key) throws Exception {
return innerLoad(key);
} @Override
public ListenableFuture<V> reload(K key, V oldValue) throws Exception {
ListenableFuture<V> task = executorService.submit(() -> {
try {
return innerLoad(key);
} catch (Exception e) {
LogUtils.printErrorLog(e, String.format("重新加载缓存失败,key:%s", key));
return oldValue;
}
});
return task;
}
});
}

这段代码事实上写的还是不错的,其通过重载reload方法并在加载后段缓存出问题的时候使用old Value。保证了即使获取缓存的后段存储出问题了,依旧不会影响到我们缓存的获取。逻辑如下所示:



那么为什么会卡住呢?一时间看不出什么问题。那么我们就可以从系统的日志中去寻找蛛丝马迹。

日志

对应时间点日志空空如也

对于这种逐渐失去响应的,我们寻找日志的时候一般去寻找源头。也就是第一次出现卡在SettableFuture的时候发生了什么。由于我们做了定时的线程栈采集,那么很容易的,笔者挑了一台机器找到了3天之前第一次发生线程卡住的时候,grep下对应的线程名,只发现了一个请求过来到了这个线程然后卡住了,后面就什么日志都不输出了。

异步缓存的日志

继续回顾上面的代码,代码中缓存的刷新是异步执行的,很有可能是异步执行的时候出错了。再grep异步执行的相关关键词“重新加载缓存失败”,依旧什么都没有。线索又断了。

继续往前追溯

当所有线索都断了的情况下,我们可以翻看时间点前后的整体日志,看下有没有异常的点以获取灵感。往前多翻了一天的日志,然后一条线程池请求被拒绝的日志进入了笔者的视野。

cache reload rejected by threadpool!

看到这条日志的一瞬间,笔者立马就想明白了。GuavaCache的reload请求不是出错了,而是被线程池给丢了。在reload请求完成之后,GuavaCache会对相应的SettableFuture做done的动作以唤醒等待的线程。而由于我们的Reject策略只打印了日志,并没有做done的动作,导致我们请求Cache的线程一直在卡waitForValue上面。如下图所示,左边的是正常情况,右边的是异常情况。

为什么会触发线程池拒绝策略

注意我们初始化线程池的源代码

    ExecutorService executor  =  new ThreadPoolExecutor(1, 1,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(50), // 注意这个QueueSize
new NamedThreadFactory(String.format("cache-reload-%s")),
(r, e) -> {
log.warn("cache reload rejected by threadpool!"); // 注意这个reject策略 });

这个线程池是个单线程线程池,而且Queue只有50,一旦遇到同时过来的请求大于50个,就很容易触发拒绝策略。

源码分析

好了,这时候我们就可以上一下GuavaCache的源代码了。

   V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
checkNotNull(key);
checkNotNull(loader);
try {
if (count != 0) { // read-volatile
// don't call getLiveEntry, which would ignore loading values
ReferenceEntry<K, V> e = getEntry(key, hash);
if (e != null) {
long now = map.ticker.read();
V value = getLiveValue(e, now);
if (value != null) {
recordRead(e, now);
statsCounter.recordHits(1);
// scheduleRefresh中一旦Value不为null,就直接返回旧值
return scheduleRefresh(e, key, hash, value, now, loader);
}
ValueReference<K, V> valueReference = e.getValueReference();
// 如果当前Value还在loading,则等loading完毕
if (valueReference.isLoading()) {
// 这次的Bug就一直卡在loadingValue上
return waitForLoadingValue(e, key, valueReference);
}
}
} // at this point e is either null or expired;
return lockedGetOrLoad(key, hash, loader);
} catch (ExecutionException ee) {
......
}

为什么没有直接返回oldValue而是卡住

等等,在上面的GuavaCache源代码里面。一旦缓存有值之后,肯定是立马返回了。对应这段代码。

			if (value != null) {
recordRead(e, now);
statsCounter.recordHits(1);
// scheduleRefresh中一旦Value不为null,就直接返回旧值
return scheduleRefresh(e, key, hash, value, now, loader);
}

所以就算异步刷新请求被线程池Reject掉了。它也不会讲原来缓存中的值给删掉。业务线程也不应该卡住。那么这个卡住是什么原因呢?为什么缓存中没值没有触发load而是等待valueReference有没有加载完毕呢。

稍加思索之后,笔者就找到了原因,因为上述那段代码中设置了缓存的maxSize。一旦超过maxSize,一部分数据就被驱逐掉了,而如果这部分数据恰好就是线程池Reject掉请求的数据,那么就会进值为空同时需要等待valueReference loading的代码分支,从而造成卡住的现象。让我们看一下源代码:

localCache.put //
|->evictEntries
|->removeEntry
|->removeValueFromChain ReferenceEntry<K,V> removeValueFromChain(...) {
......
if(valueReference.isLoading()){
// 设置原值为null
valueReference.notifyNewValue(null);
return first;
}else {
removeEntryFromChain(first,entry)
}
}

我们看到,代码中valueReference.isLoading()的判断,一旦当前value还处于加载状态中,那么驱逐的时候只会将对应的entry(key)项设置为null而不会删掉。这样,在下次这个key的缓存被访问的时候自然就走到了value为null的分支,然后就看到当前的valueReference还处于loading状态,最后就会等待那个由于被线程reject而永远不会done的future,最后就会导致这个线程卡住。逻辑如下图所示:

什么是逐渐失去响应

因为业务的实际缓存key的项目是大于maxSize的。而一开始系统启动后加载的时候缓存的cache并没有达到最大值,所以这时候被线程池reject掉相应的刷新请求依旧能够返回旧值。但一旦出现了大于缓存cache最大Size的数据导致一些项被驱逐后,只要是一个请求去访问这些缓存项都会被卡住。但很明显的,能够被驱逐出去的旧缓存肯定是不常用的缓存(默认LRU缓存策略),那么就看这个不常用的数据的流量到底是在哪台机器上最多,那么哪台机器就是最先失去响应的了。

总结

虽然这是个较简单的问题,排查的时候也是需要一定的思路的,尤其是发生问题的时间点并往前追溯到第一个不同寻常的日志这个点,这个往往是我们破局的手段。GuavaCache虽然是个使用非常广泛的缓存工具,但不合理的配置依旧会导致灾难性的后果。

日常Bug排查-集群逐步失去响应的更多相关文章

  1. 日常Bug排查-系统失去响应-Redis使用不当

    日常Bug排查-系统失去响应-Redis使用不当 前言 日常Bug排查系列都是一些简单Bug排查,笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材_. Bug现场 开发反应线上系统出现失去响 ...

  2. 日常Bug排查-消息不消费

    日常Bug排查-消息不消费 前言 日常Bug排查系列都是一些简单Bug排查,笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材_. Bug现场 某天下午,在笔者研究某个问题正high的时候.开 ...

  3. 探针配置失误,线上容器应用异常死锁后,kubernetes集群未及时响应自愈重启容器?

    探针配置失误,线上容器应用异常死锁后,kubernetes集群未及时响应自愈重启容器? 探针配置失误,线上容器应用异常死锁后,kubernetes集群未及时响应自愈重启容器? 线上多个服务应用陷入了死 ...

  4. 日常Bug排查-抛异常不回滚

    日常Bug排查-抛异常不回滚 前言 日常Bug排查系列都是一些简单Bug排查,笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材_. Bug现场 最近有人反映java应用操作数据库的时候,抛异 ...

  5. 日常Bug排查-Nginx重复请求?

    日常Bug排查-Nginx重复请求? 前言 日常Bug排查系列都是一些简单Bug排查,笔者将在这里介绍一些排查Bug的简单技巧,其中不乏一些看起来很低级但很容易犯的问题. 问题现场 有一天运维突然找到 ...

  6. 腾讯云Elasticsearch集群规划及性能优化实践

    ​一.引言 随着腾讯云 Elasticsearch 云产品功能越来越丰富,ES 用户越来越多,云上的集群规模也越来越大.我们在日常运维工作中也经常会遇到一些由于前期集群规划不到位,导致后期业务增长集群 ...

  7. Redis分布式集群几点说道

    原文地址:http://www.cnblogs.com/verrion/p/redis_structure_type_selection.html  Redis分布式集群几点说道 Redis数据量日益 ...

  8. 【Redis】Redis分布式集群几点说道

    Redis数据量日益增大,使用的公司越来越多,不仅用于做缓存,同时趋向于存储这一块,这样必促使集群的发展,各个公司也在收集适合自己的集群方案,目前行业用的比较多的是下面几种集群架构,大部分都是采用分片 ...

  9. 分布式集群HA模式部署

    一:HDFS系统架构 (一)利用secondary node备份实现数据可靠性 (二)问题:NameNode的可用性不高,当NameNode节点宕机,则服务终止 二:HA架构---提高NameNode ...

  10. 优化openfire服务器,达到单机20万,集群50万

    openfire压测概述 个月左右的测试,总算得到预定目标(3台服务器,并发50w用户在线) 测试环境搭建 压测客户端无他-tsung,尝试了windows安装perl失败后,使用centOS6.5作 ...

随机推荐

  1. 1111error

    Allowed memo ry size of 268435456 bytes exhausted编辑的没有缓存都丢了

  2. 基于AI模型的验证码安全识别(B站,知乎等)

      bilibili 汉字识别顺序验证码 实现基本思路:    先利用Selenium模拟登录,当然在这之前做好请求伪装,get方法使边框最大化,并且将系统的windows窗口缩放比例设置为100%, ...

  3. jmeter生成HTML性能测试报告(非GUI的命令)

    非GUI的命令(在cmd执行即可 不需要打开jmeter) 使用命令:jmeter  -n  -t  [jmx file]  -l  [jtl file]  -e  -o   [report path ...

  4. 老派Sql之必要,逆天,我在ef core中使用ado.net!

    Wlkr.Core.EFCore 逆天,我在ef core中使用ado.net! 老派Sql之必要 当你开发生涯中基本只用一两种数据库 当你觉得用EF的类写报表时很别扭 当你觉自己的Sql( Serv ...

  5. client-go实战之九:手写一个kubernetes的controller

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本文是<client-go实战> ...

  6. 记一次 openSUSE Tumbleweed 下安装 k8s

    出现的问题 因为没有K8s基础的而踩了不少坑. kubeadm kubelet 最好指定版本安装,因为kubelet的版本需要小于等于kubeadm的版本,否则就会报错. 运行 kubeadm ini ...

  7. 21.8 Python 使用BeautifulSoup库

    BeautifulSoup库用于从HTML或XML文件中提取数据.它可以自动将复杂的HTML文档转换为树形结构,并提供简单的方法来搜索文档中的节点,使得我们可以轻松地遍历和修改HTML文档的内容.广泛 ...

  8. Chromium VIZ架构详解

    1. VIZ的三个端 在设计层面上 viz 的架构如下图所示: 在设计上 viz 分了三个端,分别是 client 端, host 端和 service 端. client 端用于生成要显示的画面(C ...

  9. NLP机器翻译全景:从基本原理到技术实战全解析

    机器翻译是使计算机能够将一种语言转化为另一种语言的技术领域.本文从简介.基于规则.统计和神经网络的方法入手,深入解析了各种机器翻译策略.同时,详细探讨了评估机器翻译性能的多种标准和工具,包括BLEU. ...

  10. Java中的对象到底是什么

    对象是现实世界中的一切物体(实体,或能够定义的东西) Smalltalk是第一个成功的面向对象的语言 在编程世界中,对象通过类来实例化:同一个类型的对象可以接受相同的消息 状态+行为+标识=对象 每个 ...