Kyligence AI 服务 - 让大模型完成准确、可靠的数值计算和回答! 立即了解更多

跑着跑着,突然就卡了? 卡着卡着,突然还挂了?

周天鹏
2019年 12月 19日

使用 Kylin 2.6.4 之前的版本并且以 Memcached 作为查询缓存时,即有概率触发 Memcached IO 线程死亡问题,本文作者为大家分享了如何通过实验一步步排查,找出问题根源,并通过非常简单的方式解决这一 bug 的过程。

前提条件

使用 Kylin 2.6.4 之前的版本并且使用 Memcached 作为查询缓存,即有概率触发该问题。

故障现象

Kylin 实例响应很慢,日志中出现如下信息:

2019-07-01 15:21:18,024 INFO  [Query 3f75c477-9c38-35bf-24a0-656459d9ebb1-212641] v2.CubeHBaseEndpointRPC:188 : Query-3f75c477-9c38-35bf-24a0-656459d9ebb1: try to get segment result from cache for segment:******[20150901000000_20160101000000]

2019-07-01 15:21:20,028 INFO  [Query 3f75c477-9c38-35bf-24a0-656459d9ebb1-212641] v2.CubeHBaseEndpointRPC:197 : Query-3f75c477-9c38-35bf-24a0-656459d9ebb1: no segment result is cached for segment:******[20150901000000_20160101000000], take time:2000ms

初步认定为在 try to get segment result 时,发生超时(2秒正好是默认超时时间)。但检查 Memcached 各个实例,发现均为可用状态。

该问题在访问分区 Cube 时会被严重放大,因为一个 Segment 访问超时时间是 2 秒,当 Cube 存在 20 个的 Segment 时,一条 SQL 至少要 40 秒才能返回结果。

因此最近几次故障发生时,未分区的全量 Cube 几乎无感知,但分区的 Cube 基本均全处于不可用状态。

此外,异常发生前日志中持续打印下列信息:

2019-07-04 13:59:48.989 INFO net.spy.memcached.MemcachedConnection:  Reconnecting due to exception on {QA sa=******, #Rops=0, #Wops=2, #iq=0, topRop=null, topWop=Cmd: 1 Opaque: 2297 Key: PROD:query_segment_cache:36b344a063b0f914448b311229db4c4d3a2b30c2 Cas: 0 Exp: 604800 Flags: 2048 Data Length: 350795, toWrite=0, interested=4}java.lang.NullPointerException
2019-07-04 13:59:48.992 WARN net.spy.memcached.MemcachedConnection: Closing, and reopening {QA sa=******, #Rops=0, #Wops=2, #iq=0, topRop=null, topWop=Cmd: 1 Opaque: 2297 Key: PROD:query_segment_cache:36b344a063b0f914448b311229db4c4d3a2b30c2 Cas: 0 Exp: 604800 Flags: 2048 Data Length: 350795, toWrite=0, interested=4}, attempt 0.
2019-07-04 13:59:48.993 INFO net.spy.memcached.protocol.binary.BinaryMemcachedNodeImpl: No buffer for current write op, removing
2019-07-04 13:59:50.123 INFO net.spy.memcached.MemcachedConnection: Reconnecting due to exception on {QA sa=******, #Rops=1, #Wops=1, #iq=0, topRop=null, topWop=Cmd: -1 Opaque: -1 Keys: , toWrite=0, interested=4}java.lang.NullPointerException atnet.spy.memcached.protocol.TCPMemcachedNodeImpl.fillWriteBuffer(TCPMemcachedNodeImpl.java:206)
atnet.spy.memcached.MemcachedConnection.handleWrites(MemcachedConnection.java:811)
atnet.spy.memcached.MemcachedConnection.handleInputQueue(MemcachedConnection.java:596)
atnet.spy.memcached.MemcachedConnection.handleIO(MemcachedConnection.java:423)
atnet.spy.memcached.MemcachedConnection.run(MemcachedConnection.java:1457) at net.spy.memcached.MemcachedConnection.run(MemcachedConnection.java:1457)

重启后,该故障可 100% 解决。如果不重启,该状态持续一段时间后也会自动触发 OOM 然后被进程管理工具自动重启。

初步确定原因

经过对 Memcached 的多次压测,响应时间最高不超过 50 ms,因此初步排除 Memcached 真的响应过慢导致 Kylin Memcached 客户端 2000 ms 超时的可能。

后开始检查 Kylin 操作 Memcached 的相关逻辑,也并未发现明显异常。

最终开启测试实例加载真实数据,进行 Debug,重复执行生产环境 Kylin 实例发生异常前几十秒正在执行的 SQL,最终成功复现线上 2000 ms 超时的问题。

通过检查测试 Kylin 实例中正在运行的线程,终于发现异常。处理 Memcached IO 的线程竟然不存在了。

然后从断点中观察到 Memcached IO 队列中发现大量积压,无法读出也无法写入。

且终端打印出异常信息:

Exception in thread "Memcached IO over {MemcachedConnection to ******}" java.lang.NullPointerException 
at net.spy.memcached.protocol.TCPMemcachedNodeImpl.fillWriteBuffer(TCPMemcachedNodeImpl.java:206)
at net.spy.memcached.MemcachedConnection.handleWrites(MemcachedConnection.java:811)
at net.spy.memcached.MemcachedConnection.handleInputQueue(MemcachedConnection.java:596)
at net.spy.memcached.MemcachedConnection.handleIO(MemcachedConnection.java:423) 
at net.spy.memcached.MemcachedConnection.run(MemcachedConnection.java:1457)

所以基本确定,造成故障的原因为负责对 Memcached 进行 IO 操作的线程异常退出。

进一步找出问题根源

通过查看 Memcached 客户端 Spymemcached 的源码,并无法成功找出可能造成 Memcached IO 线程 NPE 退出的原因。正当百思不得其解之际,在 GitHub 中发现了下面这个 Pull Request:

NPE in fillWriteBuffer() with binary protocol

(https://github.com/couchbase/spymemcached/pull/17)

顺便吐槽 spymemcached 的 issue tracker 真心难找,这个 PR 的作者是拿 PR 来报 Issue 的。

读完之后恍然大悟,只有在写入的数据大于等于 1024 * 1024 byte 即 1 MB 时,才可能触发该异常。

但是大于 1 MB 的数据是写不进去的,所以基本可以推测出 Kylin 的客户端一定是大量并发的写入极接近 1 MB 或等于 1 MB 的数据时,触发了这个异常,导致 Memcached IO 线程死亡。

但是查看了 Kylin 的源码发现下面的信息,又使我陷入了沉思。。。

private SegmentQueryCache() {       KylinConfig kylinConfig = KylinConfig.getInstanceFromEnv();       MemcachedCacheConfig memcachedCacheConfig = new MemcachedCacheConfig();       String configHosts = kylinConfig.getMemCachedHosts();       memcachedCacheConfig.setTimeout(kylinConfig.getQuerySegmentCacheTimeout());       // set max object size a little less than 1024 * 1024, because the key of the segment result cache is long       // if set to 1024 * 1024 will cause memcached client exceed max size error       memcachedCacheConfig.setMaxObjectSize(1040000);       memcachedCacheConfig.setHosts(configHosts);       //Reverse the compression setting between Hbase coprocessor and memcached, if Hbase result is compressed, memcached will not compress.       memcachedCacheConfig.setEnableCompression(!kylinConfig.getCompressionResult());       String cacheName = SEG_QUERY_CACHE_NAME;       memcachedCache = new MemcachedChunkingCache(MemcachedCache.create(memcachedCacheConfig, cacheName));  }

看来 Kylin 在操作 Memcached 时已经意识到大并发写入 1 MB 的数据可能会出现问题,所以将 maxObjectSize 置为了 1040000,并不接近 1048576。

最终只能靠实验来测试到底什么样的数据量可能触发这个问题

import net.spy.memcached.BinaryConnectionFactory;import net.spy.memcached.MemcachedClient;import net.spy.memcached.transcoders.SerializingTranscoder;
import java.io.IOException;import java.net.InetSocketAddress;import java.util.ArrayList;import java.util.List;
public class Main2 { public static void main(String[] args) throws IOException { List<InetSocketAddress> addrs = new ArrayList<>(); addrs.add(new InetSocketAddress("******", 12000)); MemcachedClient client = new MemcachedClient(new BinaryConnectionFactory(), addrs);
SerializingTranscoder transcoder = new SerializingTranscoder(); transcoder.setCompressionThreshold(Integer.MAX_VALUE);
byte[] data = new byte[1048000];
while (true) { String keyS = "test";
client.set(keyS, 60, data, transcoder); } }}

通过上述代码疯狂写入 Memcached,并未出现空指针异常。

但是该例子并不能代表 Kylin 实际向 Memcached 中写入的最大的数据量,因为 Kylin 是通过自己包装的 MemcachedChunkingCache 进行写入操作。

下面只需要算一算什么样的 byte[] 能触发 Kylin 写入极限大小的数据量了。

观察下面这部分代码:

public static int getValueSplit(MemcachedCacheConfig config, String keyS, int valueBLen) {       // the number 6 means the chunk number size never exceeds 6 bytes       final int valueSize = config.getMaxObjectSize() - Shorts.BYTES - Ints.BYTES               - keyS.getBytes(Charsets.UTF_8).length - 6;       final int maxValueSize = config.getMaxChunkSize() * valueSize;       Preconditions.checkArgument(valueBLen <= maxValueSize,               "the value bytes length [%d] exceeds maximum value size [%d]", valueBLen, maxValueSize);       return (valueBLen - 1) / valueSize + 1;  }

只要让 valueBLen 是 valueSize 的整数倍即可。我们来个 50 倍试试看。

valueSize 算出是 1047230,再算上序列化操作后的一些字节变化,最后我们只需要保证数据量为 52361473 即可。

测试代码改成如下样子:

import cn.dxy.ops.chaos.cache.memcached.MemcachedCache;import cn.dxy.ops.chaos.cache.memcached.MemcachedCacheConfig;import cn.dxy.ops.chaos.cache.memcached.MemcachedChunkingCache;
public class Main3 { public static void main(String[] args) { MemcachedCacheConfig memcachedCacheConfig = new MemcachedCacheConfig(); memcachedCacheConfig.setHosts("t1:12000"); memcachedCacheConfig.setEnableCompression(false); memcachedCacheConfig.setTimeout(2000); MemcachedCache memcachedCache = new MemcachedChunkingCache(MemcachedCache.create(memcachedCacheConfig, "query_segment_cache"));
byte[] data = new byte[52361473]; while (true) { String keyS = "test_agg_group_294958b3-1829-cd88-6d01-f44ed1d22067_.org.apache.kylin.cube.gridtable.CubeCodeSystem\\x00\\x04\\x00\\x8F\\x82\\xAC\\xED\\x00\\x05sr\\x00-org.apache.kylin.cube.gridtable.TrimmedDimEnc\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x0C\\x00\\x00xr\\x00,org.apache.kylin.dimension.DimensionEncoding\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x0C\\x00\\x00xpw\\x02\\x00\\x03x\\x00\\x8F\\x82\\xAC\\xED\\x00\\x05sr\\x00-org.apache.kylin.cube.gridtable.TrimmedDimEnc\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x0C\\x00\\x00xr\\x00,org.apache.kylin.dimension.DimensionEncoding\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x0C\\x00\\x00xpw\\x02\\x00\\x03x\\x00\\x8F\\x82\\xAC\\xED\\x00\\x05sr\\x00-org.apache.kylin.cube.gridtable.TrimmedDimEnc\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x0C\\x00\\x00xr\\x00,org.apache.kylin.dimension.DimensionEncoding\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x0C\\x00\\x00xpw\\x02\\x00\\x03x\\x00\\x8F\\x82\\xAC\\xED\\x00\\x05sr\\x00-org.apache.kylin.cube.gridtable.TrimmedDimEnc\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x0C\\x00\\x00xr\\x00,org.apache.kylin.dimension.DimensionEncoding\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x0C\\x00\\x00xpw\\x02\\x00\\x01x\\x09Cuboid 15\\x06\\x07integer\\xFF\\xFF\\x07integer\\xFF\\xFF\\x07integer\\xFF\\xFF\\x07integer\\xFF\\xFF\\x06bigint\\xFF\\xFF\\x06bigint\\xFF\\xFF\\x00\\x01\\x0F\\x02\\x01\\x0F\\x010\\x00\\x00\\x01\\x0F\\x00\\x01\\x0F\\x00\\x00\\x00\\x08NO_LIMIT\\x8C\\x7F\\xFF\\xFF\\xFF\\x8C\\x7F\\xFF\\xFF\\xFF\\x19SCAN_FILTER_AGGR_CHECKMEM\\x01\\x01"; memcachedCache.put(keyS, data); } }}

执行上面的代码基本基本每次都可以触发空指针异常,有概率触发 Memcached IO 线程死亡。

Exception in thread "Memcached IO over {MemcachedConnection to ******}" java.lang.NullPointerException at net.spy.memcached.protocol.TCPMemcachedNodeImpl.fillWriteBuffer (TCPMemcachedNodeImpl.java:206) at net.spy.memcached.MemcachedConnection.handleWrites (MemcachedConnection.java:811) at net.spy.memcached.MemcachedConnection.handleInputQueue (MemcachedConnection.java:596) at net.spy.memcached.MemcachedConnection.handleIO(MemcachedConnection.java:423) at net.spy.memcached.MemcachedConnection.run(MemcachedConnection.java:1457)

此时,在对数据进行 UTF-8 编码之后,实际并发写入的数据量为 1048571 byte。

我们将 Main2 中的数据量从 1048000 改为 1048571 然后再次执行,会发现也有概率触发 Exception in thread "Memcached IO over {MemcachedConnection to ******" java.lang.NullPointerException 的问题了。

结论

越是诡异的问题,改动起来可能越小,这么看来,直接将 maxObjectSize 从 1040000 改为 1000000 可能更保险一些。

相关 Issue:KYLIN-4074

本文作者简介

周天鹏,丁香园大数据运维工程师,Apache Kylin Committer,“2019 最佳 Apache Kylin 社区贡献个人奖”获得者,主要负责 OLAP 平台的开发与维护。

作者简书账号:

https://www.jianshu.com/u/76a9b45adedb

添加企微

kyligence
关注我们

kyligence