flink实时任务因jedis连接池导致的任务阻塞问题

问题现象

有个flink实时任务,读kafka和redis,中间有复杂的逻辑处理过程,最终结果写redis。flink实时任务运行一段时间后阻塞了,有时是几个小时后,有时是一两天后。

任务看起来正常是正常的,但kafka消费已经停止,checkpoint也失败。看日志,当问题出现后,kafka一直WARN,提示如下

Marking the coordinator xxxxxx dead.
Marking the coordinator xxxxxx dead.
Marking the coordinator xxxxxx dead.

……

任务看起来正常运行,但不再消费kafka,checkpoint失败,除此之外,cpu、mem、io、日志,均看不出异常;集群节点登陆需要申请权限,比较麻烦。

原因分析

步骤1

刚开始以为是kafka消费的问题,导致了任务阻塞。于是寻找解决办法,发现也有部分人遇到类似的问题,但办法不管用。

后面了解到kafka集群版本之前是0.9,最近升级到了2.2.0,建议更新了kafka客户端版本。升级到2.2.0客户端版本后,没有"Marking the coordinator xxx dead."的日志了,但任务还是在运行一段时间后回挂起,问题还没有解决。

步骤2

由于没有权限登陆集群节点,只能通过flink ui,观察cpu、mem、io正常,查看job manager和task manager日志,也没有发现异常。

每次任务阻塞后查看checkpoint,都是失败。刚开始以为checkpoint间隔5秒太短,于是一直往上调整到10分钟,还是照样挂。

有次checkpoint刚挂不久,发现checkpoint挂在sink redis了,其他操作都是几到几十毫秒。那大概原因找到了,写redis有问题。

同时通过最近几次任务挂起后的观察,sink redis数量好像都是在1608,资源申请的是8核cpu、单核内存4G,配置文件prod.sink.redis.maxPools=200,(200+1)*8=1608,看起来有联系?

将prod.sink.redis.maxPools设置为1或者10,问题很快就复现了,可以基本确定,问题发生在sink redis操作了。

步骤3

分析代码,因为写redis需要设置key有效时间,所以没有用原生的flink-redis-connector,而是用了flink-redis-connector+jedis,基于二者做了稍显复杂的封装,用于读写redis三种集群模式:主从复制、哨兵模式、集群模式。

最终发现,setex实现方法有问题,使用完连接后,并没有释放资源。

基本代码逻辑如下:

    @Override
    public String setex(String key, int seconds, String value) {
        String res = null;
        Jedis jedis = null;
        try {
            jedis = getInstance();
            res = jedis.setex(key, seconds, value);
        } catch (Exception e) {
            logger.error("Cannot get Redis message with command GET to key {} error message {}", key, e.getMessage());
            throw e;
        }

        return res;
    }

使用后应该释放资源,正确姿势如下:

    @Override
    public String setex(String key, int seconds, String value) {
        String res = null;
        Jedis jedis = null;
        try {
            jedis = getInstance();
            res = jedis.setex(key, seconds, value);
        } catch (Exception e) {
            logger.error("Cannot get Redis message with command GET to key {} error message {}", key, e.getMessage());
            throw e;
        } finally {
            releaseInstance(jedis);
        }

        return res;
    }

上面优化对于并发的场景,还是有问题。提供另外一个版本,可以解决上面并发问题:

    @Override
    public String setex(String key, int seconds, String value) {
        String res = null;
        try (Jedis jedis = getInstance()) {
            res = jedis.setex(key, seconds, value);
        } catch (Exception e) {
            logger.error("Cannot get Redis message with command GET to key {} error message {}", key, e.getMessage());
        }

        return res;
    }

但是,为啥没有抛异常,没有error或者warn日志? 

深层分析

明明是jedis连接池不够了,为什么不报错,为什么日志没有error、warn等任何有效信息?是flink集群设置问题,还是其他原因?带着这些问题,继续寻找深层原因。

先看看获取jedis连接的主体代码逻辑

public static RedisContainerInterface build(FlinkJedisPoolConfig flinkJedisPoolConfig) {
    logger.info("flinkJedisPoolConfig:" + flinkJedisPoolConfig.toString());
    Preconditions.checkNotNull(flinkJedisPoolConfig);

    GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
    genericObjectPoolConfig.setMaxIdle(flinkJedisPoolConfig.getMaxIdle());
    genericObjectPoolConfig.setMinIdle(flinkJedisPoolConfig.getMinIdle());
    genericObjectPoolConfig.setMaxTotal(flinkJedisPoolConfig.getMaxTotal());

    JedisPool jedisPool = new JedisPool(genericObjectPoolConfig,
            flinkJedisPoolConfig.getHost(),
            flinkJedisPoolConfig.getPort(),
            flinkJedisPoolConfig.getConnectionTimeout(),
            flinkJedisPoolConfig.getPassword(),
            flinkJedisPoolConfig.getDatabase());

    return new RedisContainerImpl(jedisPool);
}
public RedisContainerImpl(JedisPool jedisPool) {
    Preconditions.checkNotNull(jedisPool, "JedisPool can not be null");
    this.jedisPool = jedisPool;
}

private Jedis getInstance() {
    return jedisPool.getResource();
}

再看看jedis中关于GenericObjectPoolConfig及父类的定义,重点跟连接池有关的部分

public class JedisPoolConfig extends GenericObjectPoolConfig {
  public JedisPoolConfig() {
    // defaults to make your life with connection pool easier :)
    setTestWhileIdle(true);
    setMinEvictableIdleTimeMillis(60000);
    setTimeBetweenEvictionRunsMillis(30000);
    setNumTestsPerEvictionRun(-1);
  }
}

public class GenericObjectPoolConfig<T> extends BaseObjectPoolConfig<T> {
    ……
}

public abstract class BaseObjectPoolConfig<T> extends BaseObject implements Cloneable {

    public static final boolean DEFAULT_BLOCK_WHEN_EXHAUSTED = true;
    public static final long DEFAULT_MAX_WAIT_MILLIS = -1L;


    private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;
    private long maxWaitMillis = DEFAULT_MAX_WAIT_MILLIS;

    public boolean getBlockWhenExhausted() {
        return blockWhenExhausted;
    }

    public void setBlockWhenExhausted(final boolean blockWhenExhausted) {
        this.blockWhenExhausted = blockWhenExhausted;
    }

    public void setMaxWaitMillis(final long maxWaitMillis) {
        this.maxWaitMillis = maxWaitMillis;
    }

    public long getMaxWaitMillis() {
        return maxWaitMillis;
    }
    ……
}

看上面代码,BaseObjectPoolConfig类中有两个关键属性:

blockWhenExhausted:如果超时就抛异常,小于零:阻塞不确定的时间, 默认-1。

maxWaitMillis:获取连接时的最大等待毫秒数(如果设置为阻塞时)。

所以,如果要在获取不到连接时抛出异常,需要修改前面的代码,有两种方法:

一是blockWhenExhausted设置为false,即不阻塞,如下

public static RedisContainerInterface build(FlinkJedisPoolConfig flinkJedisPoolConfig) {
    logger.info("flinkJedisPoolConfig:" + flinkJedisPoolConfig.toString());
    Preconditions.checkNotNull(flinkJedisPoolConfig);

    GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
    genericObjectPoolConfig.setMaxIdle(flinkJedisPoolConfig.getMaxIdle());
    genericObjectPoolConfig.setMinIdle(flinkJedisPoolConfig.getMinIdle());
    genericObjectPoolConfig.setMaxTotal(flinkJedisPoolConfig.getMaxTotal());
    genericObjectPoolConfig.setBlockWhenExhausted(false);

    JedisPool jedisPool = new JedisPool(genericObjectPoolConfig,
            flinkJedisPoolConfig.getHost(),
            flinkJedisPoolConfig.getPort(),
            flinkJedisPoolConfig.getConnectionTimeout(),
            flinkJedisPoolConfig.getPassword(),
            flinkJedisPoolConfig.getDatabase());

    return new RedisContainerImpl(jedisPool);
}

二是blockWhenExhausted设置为true,且将maxWaitMillis设置一个超时时间,如下

public static RedisContainerInterface build(FlinkJedisPoolConfig flinkJedisPoolConfig) {
    logger.info("flinkJedisPoolConfig:" + flinkJedisPoolConfig.toString());
    Preconditions.checkNotNull(flinkJedisPoolConfig);

    GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
    genericObjectPoolConfig.setMaxIdle(flinkJedisPoolConfig.getMaxIdle());
    genericObjectPoolConfig.setMinIdle(flinkJedisPoolConfig.getMinIdle());
    genericObjectPoolConfig.setMaxTotal(flinkJedisPoolConfig.getMaxTotal());
    genericObjectPoolConfig.setBlockWhenExhausted(true);
    genericObjectPoolConfig.setMaxWaitMillis(2000);

    JedisPool jedisPool = new JedisPool(genericObjectPoolConfig,
            flinkJedisPoolConfig.getHost(),
            flinkJedisPoolConfig.getPort(),
            flinkJedisPoolConfig.getConnectionTimeout(),
            flinkJedisPoolConfig.getPassword(),
            flinkJedisPoolConfig.getDatabase());

    return new RedisContainerImpl(jedisPool);
}

问题总结

1. kafka之所以有上面的报错,是因为任务已经阻塞了,导致kafka客户端与集群之间通信出问题了。kafka的warn日志只是问题导致的现象,并不是导致问题的原因。尽量保持kafka客户端和集群之间的版本一致,避免一些不必要的麻烦。

2. 问题的根本原因是写redis时,扩展实现了setex方法,但没有释放资源。测试和上线前期,尽量不要将资源设置太大,比如jedis资源池,如果测试环境设置成1,正式环境先设置成10,加上压测,其实问题很快就复现了。

3. jedis连接池资源不够但不抛异常没有日志,导致任务一直阻塞,造成问题分析没那么直观。建议BaseObjectPoolConfig的BlockWhenExhausted默认设置为false,并将MaxWaitMillis设置一个默认超时时间。

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值