Kylin Memcached IO 线程死亡诊断

Kylin Memcached IO 线程死亡诊断

故障现象

集群中某 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:1102_pv_uv_users_di[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:1102_pv_uv_users_di[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=memcached2/172.16.17.3:11211, #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=memcached2/172.16.17.3:11211, #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=memcached1/172.16.16.3:11211, #Rops=1, #Wops=1, #iq=0, topRop=null, topWop=Cmd: -1 Opaque: -1 Keys: , toWrite=0, interested=4}
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)

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

初步确定原因

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

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

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

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

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

image_name

且终端打印出异常信息:

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

顺便吐槽 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("t1", 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。

image_name

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

结论

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348

推荐阅读更多精彩内容

  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,256评论 0 9
  • 一. 操作系统概念 操作系统位于底层硬件与应用软件之间的一层.工作方式: 向下管理硬件,向上提供接口.操作系统进行...
    月亮是我踢弯得阅读 5,959评论 3 28
  • 线程池ThreadPoolExecutor corepoolsize:核心池的大小,默认情况下,在创建了线程池之后...
    irckwk1阅读 717评论 0 0
  • Memcached 教程 Memcached是一个自由开源的,高性能,分布式内存对象缓存系统。 Memcached...
    大熊_7d48阅读 2,478评论 0 0
  • 水库啊水库,自由的水库。凉风啊凉风,慵懒的凉风。夜色啊夜色,朦胧的夜色。灯光啊灯光,耀眼的灯光。波浪啊波浪,粼粼的...
    更向远行阅读 306评论 0 0