当 Redis 集群发生主从切换(Failover)时,Flink 任务会崩溃吗?如何利用 Sentinel 实现高可用?

引言:从"能用"到"高可用"

在上一篇文章中,我们基于 FlinkJedisPoolConfig 构建了一个可运行的 Flink Redis Sink。但生产环境从来不是"能跑就行"------当 Redis 单节点宕机时,整个 Flink 任务会直接崩溃,因为连接池配置的 localhost:6379 已经不可用了。

那么问题来了:如果部署了 Redis 主从+Sentinel 高可用架构,Flink 任务能在主从切换时自动恢复吗?

答案是:能,但有前提------你必须使用 FlinkJedisSentinelConfig 替代 FlinkJedisPoolConfig,并且正确配置重试机制。 否则,任务依然会崩溃。

本文将深入剖析:

  1. Redis Sentinel 的故障转移机制及其对 Flink 任务的影响
  2. Flink Redis Connector 在 Sentinel 模式下的连接管理原理
  3. 一份可直接运行的 Sentinel 高可用配置代码
  4. 主从切换期间的"数据黑洞"风险与应对策略
  5. 生产环境必做的 5 项高可用加固措施

一、前置知识:Redis Sentinel 如何实现高可用?

1.1 主从复制 + 哨兵 = 自动故障转移

Redis Sentinel(哨兵)是 Redis 官方提供的高可用解决方案。它的核心工作流程如下:

阶段 哨兵行为 耗时
监控 每 1 秒向主从节点发送 PING,检测存活状态 持续
主观下线(SDOWN) 单个哨兵发现主节点无响应,标记为 SDOWN 约 3 秒
客观下线(ODOWN) 多数哨兵(quorum)确认主节点不可用,触发故障转移 取决于 quorum 配置
Leader 选举 哨兵集群通过 Raft 协议选举一个 Leader 执行故障转移 数秒
主从切换 Leader 将一个从节点提升为新主节点,其他从节点切换复制目标 数秒
客户端感知 哨兵将新主节点信息通知客户端(通过 SENTINEL GET-MASTER-ADDR-BY-NAME 即时

关键数据 :在 3 节点 Sentinel 集群中,需至少 2 个节点确认故障才会触发 ODOWN,避免网络分区导致的误切换。整个故障转移过程通常在 10~30 秒 内完成。

假设你的 Flink 任务正在向 Redis 主节点(master-1:6379)写入数据,此时主节点宕机:

阶段一:故障发生 → 哨兵检测(0~5 秒)

  • Flink 任务尝试写入 Redis,但连接已断开。
  • Jedis 客户端抛出异常(如 JedisConnectionExceptionSocketTimeoutException)。
  • 此时 Flink 任务会报错,但尚未崩溃------错误会被 Flink 的容错机制捕获。

阶段二:哨兵选举 + 主从切换(5~15 秒)

  • 哨兵集群正在选举新主节点,Redis 服务暂时不可用。
  • Flink 任务持续重试写入,不断报错。
  • 如果重试策略配置不当,任务会在多次失败后崩溃

阶段三:新主节点上线 + 客户端感知(15~30 秒)

  • 新主节点(原 slave-1)已提升为 master-2
  • Sentinel 客户端(JedisSentinelPool)通过哨兵获取新主节点地址。
  • 如果使用了 FlinkJedisSentinelConfig,连接池会自动发现新主节点并重建连接
  • 任务恢复写入,数据继续流向新主节点。

结论FlinkJedisPoolConfig(直连单节点)→ 任务崩溃FlinkJedisSentinelConfig(通过哨兵获取主节点)→ 任务短暂报错后自动恢复


二、核心剖析:FlinkJedisSentinelConfig 的工作原理

2.1 三种配置类的本质区别

Flink Redis Connector 提供了三种配置类,对应三种 Redis 部署模式:

配置类 适用场景 主从切换时行为
FlinkJedisPoolConfig 单机 Redis ❌ 连接失效,任务崩溃
FlinkJedisClusterConfig Redis Cluster 集群模式 ⚠️ 客户端可感知拓扑变化,但对主从切换支持有限
FlinkJedisSentinelConfig Redis Sentinel 高可用模式 ✅ 自动发现新主节点,连接自动恢复

2.2 Sentinel 模式下的连接获取流程

RedisSink 使用 FlinkJedisSentinelConfig 时,内部连接管理流程如下:

复制代码
1. RedisSink.open() 被调用
   ↓
2. RedisCommandsContainerBuilder.build(jedisSentinelConfig)
   ↓
3. 创建 JedisSentinelPool(内部持有哨兵地址列表)
   ↓
4. JedisSentinelPool 调用 SENTINEL GET-MASTER-ADDR-BY-NAME master
   ↓
5. 获取当前主节点 IP + 端口
   ↓
6. 建立到主节点的连接池
   ↓
7. 每条数据写入时:从连接池借用 Jedis 连接 → 执行 HSET → 归还

关键差异JedisSentinelPool 不会在初始化时"固定"一个主节点地址。每次获取连接时,它都会先向哨兵查询当前主节点,再建立连接。这意味着即使主节点发生切换,下一次获取连接时就能拿到新主节点地址。

2.3 一个容易被忽略的坑:连接池缓存

JedisSentinelPool 内部有一个 master 字段缓存了主节点地址。如果故障转移发生在两次连接获取之间,缓存的地址可能已经失效

Jedis 的处理方式是:当使用缓存地址连接失败时,会重新向哨兵查询 并更新缓存。但这个过程需要时间,且可能抛出异常 。因此,Flink 任务在切换瞬间仍可能出现短暂报错------这是正常现象,只要配置了重试机制,任务就能自愈。


三、手把手实操:从 PoolConfig 升级到 SentinelConfig

3.1 环境准备:搭建 Redis Sentinel 集群

最低配置:1 个主节点 + 1 个从节点 + 3 个哨兵(生产环境哨兵至少 3 个)

bash 复制代码
# 以 Docker Compose 为例(快速验证用)
version: '3'
services:
  redis-master:
    image: redis:7
    command: redis-server --port 6379 --appendonly yes
    ports:
      - "6379:6379"
  
  redis-slave:
    image: redis:7
    command: redis-server --port 6380 --slaveof redis-master 6379 --appendonly yes
    ports:
      - "6380:6380"
  
  sentinel-1:
    image: redis:7
    command: redis-sentinel /usr/local/etc/redis/sentinel.conf
    volumes:
      - ./sentinel.conf:/usr/local/etc/redis/sentinel.conf
    ports:
      - "26379:26379"
  # sentinel-2, sentinel-3 同理...

sentinel.conf 核心配置:

复制代码
port 26379
sentinel monitor mymaster redis-master 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000

3.2 升级后的 Scala 代码(完整可运行)

scala 复制代码
package sink

import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.redis.RedisSink
import org.apache.flink.streaming.connectors.redis.common.config.FlinkJedisSentinelConfig
import org.apache.flink.streaming.connectors.redis.common.mapper.{RedisCommand, RedisCommandDescription, RedisMapper}
import source.{ClickSource, Event}

import java.util.{HashSet => JHashSet}
import scala.collection.JavaConverters._

object sinkToRedisSentinel {
  def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.enableCheckpointing(10000)

    val dataStream: DataStream[Event] = env.addSource(new ClickSource)
    dataStream.print("Input from Source")

    // 1. 配置哨兵地址(至少 3 个)
    val sentinels = new JHashSet[String]()
    sentinels.add("sentinel-1:26379")
    sentinels.add("sentinel-2:26379")
    sentinels.add("sentinel-3:26379")

    // 2. 构建 Sentinel 配置(核心!)
    val conf: FlinkJedisSentinelConfig = new FlinkJedisSentinelConfig.Builder()
      .setMasterName("mymaster")              // 必须与 sentinel.conf 中的名称一致
      .setSentinels(sentinels)                // 哨兵地址列表
      .setConnectionTimeout(5000)             // 连接超时 5 秒
      .setSoTimeout(5000)                     // Socket 超时 5 秒
      .setMaxTotal(20)                        // 最大连接数
      .setMaxIdle(10)                         // 最大空闲连接
      .setMinIdle(5)                          // 最小空闲连接
      .setTestOnBorrow(true)                  // 借用时检查连接可用性
      .setTestWhileIdle(true)                 // 空闲时检查连接可用性
      .build()

    // 3. 添加 Redis Sink(Mapper 逻辑与之前完全一致)
    val redisSink = new RedisSink[Event](conf, new RedisMapper[Event] {
      override def getCommandDescription: RedisCommandDescription =
        new RedisCommandDescription(RedisCommand.HSET, "click")

      override def getKeyFromData(t: Event): String = t.user
      override def getValueFromData(t: Event): String = t.url
    })

    dataStream.addSink(redisSink)
      .name("Redis Sentinel Sink")
      .setParallelism(1)

    env.execute("Flink Redis Sentinel Job")
  }
}

代码变化对比

原版(PoolConfig) 新版(SentinelConfig)
.setHost("localhost") .setSentinels(sentinels) + .setMasterName("mymaster")
直连单节点 通过哨兵动态发现主节点
主从切换 → 任务崩溃 主从切换 → 自动重连

3.3 验证 Sentinel 高可用效果

Step 1:启动 Flink 任务,确认数据正常写入 Redis。

bash 复制代码
redis-cli -h localhost -p 6379 HGETALL click
# 正常返回数据

Step 2:模拟主节点宕机。

bash 复制代码
docker stop redis-master

Step 3:观察 Flink 任务日志。

复制代码
# 预期会看到类似以下日志(非精确,取决于 Jedis 版本):
WARN  JedisSentinelPool - master mymaster is down, trying to discover new master
WARN  JedisSentinelPool - new master found: redis-slave:6380
INFO  RedisSink - reconnected to Redis successfully

Step 4:验证数据仍在写入(新主节点)。

bash 复制代码
redis-cli -h localhost -p 6380 HGETALL click
# 数据持续增长,说明故障转移成功

四、进阶思考:主从切换期间的"数据黑洞"

4.1 异步复制导致的数据丢失风险

Redis 主从复制是异步的。这意味着:

  1. 主节点收到写入请求 → 返回 OK 给客户端 → 然后才异步同步到从节点。
  2. 如果在同步完成之前主节点宕机,这部分数据永远丢失了

这就是所谓的 "数据黑洞" ------在故障转移期间,部分已确认写入的数据可能永久消失。

对 Flink 任务的影响

  • Flink 的 Checkpoint 机制认为数据已成功写入(因为 RedisSink 返回了成功)。
  • 但实际上数据并未复制到从节点,主节点宕机后数据丢失。
  • Flink 的 Exactly-Once 语义无法覆盖这种场景,因为数据丢失发生在 Redis 内部,Flink 无法感知。

4.2 应对策略

策略 实现方式 优缺点
开启 AOF 持久化 appendonly yes + appendfsync always 数据更安全,但性能下降明显
使用 WAIT 命令 主节点写入后等待从节点确认 牺牲可用性换取一致性
业务层容忍丢失 接受故障转移期间少量数据丢失 大多数实时场景可接受
双写 + 去重 同时写入两个 Redis 集群,消费端去重 成本翻倍,复杂度高

生产建议 :对于实时点击流这类非关键数据 ,可以容忍少量丢失;对于交易数据,则不应依赖 Redis 作为唯一存储,而应使用支持事务的数据库。


五、生产环境必做的 5 项高可用加固

scala 复制代码
// 在 env 创建后立即配置
env.setRestartStrategy(
  RestartStrategies.fixedDelayRestart(
    10,                 // 最多重试 10 次
    Time.seconds(30)    // 每次重试间隔 30 秒
  )
)

为什么重要:故障转移期间 Redis 可能不可用 10~30 秒,如果没有重启策略,任务会在第一次报错时就失败。

5.2 设置合理的超时时间

scala 复制代码
.setConnectionTimeout(10000)   // 连接超时 10 秒(故障转移期间可能较长)
.setSoTimeout(10000)           // 读取超时 10 秒

为什么重要:故障转移期间,连接建立可能比平时慢,过短的超时会导致误判。

5.3 开启连接池健康检查

scala 复制代码
.setTestOnBorrow(true)
.setTestWhileIdle(true)
.setTimeBetweenEvictionRunsMillis(30000)  // 每 30 秒检查一次空闲连接

为什么重要 :主从切换后,旧主节点的连接已失效。开启 TestOnBorrow 可以在借用连接时执行 PING 检测,确保不会拿到死连接。

5.4 监控告警

监控项 告警阈值 意义
Flink 任务重启次数 > 3 次/小时 可能存在持续性故障
Redis 连接异常日志 任何 JedisConnectionException 主从切换或网络问题
Redis 主从延迟 > 1 秒 异步复制积压,可能丢数据
哨兵集群健康 任意哨兵不可达 哨兵集群本身出问题

5.5 避免雪崩:连接池预热与限流

故障恢复后,所有并行子任务可能同时尝试重新连接 Redis,瞬间产生大量连接请求。

应对

scala 复制代码
// 方式一:限制 Sink 并行度
.setParallelism(1)

// 方式二:在连接池配置中限制最大连接数
.setMaxTotal(10)  // 不要设置过大,避免恢复时打爆 Redis

六、总结

问题 答案
Redis 主从切换时 Flink 任务会崩溃吗? 使用 FlinkJedisPoolConfig会崩溃 ;使用 FlinkJedisSentinelConfig不会崩溃,会自动恢复
故障转移期间会发生什么? Flink 任务会短暂报错(连接超时),但配合重启策略和 Sentinel 自动重连,任务可在 10~30 秒内自愈。
数据会丢失吗? 可能。Redis 异步复制导致主从切换时存在"数据黑洞",需根据业务重要性决定是否容忍。
生产环境还需要做什么? 配置重启策略、合理超时、连接池健康检查、监控告警、防止恢复时的连接风暴。

核心要点回顾

  1. 配置类升级 :从 FlinkJedisPoolConfig 切换到 FlinkJedisSentinelConfig,是让 Flink 任务在 Redis 主从切换时"活下来"的第一步。
  2. 重启策略是保底 :即使 Sentinel 自动重连,故障转移期间的短暂不可用仍可能触发 Flink 报错。fixedDelayRestart 是必选项。
  3. 数据一致性是上限:Redis 的 AP 特性决定了它在故障转移时无法保证数据零丢失。关键数据请使用支持事务的存储系统。
  4. 监控是最后的防线:没有监控的高可用是"伪高可用"------你永远不会知道故障发生过,直到数据出问题。

下期预告 :当 Redis 写入成为性能瓶颈时,如何利用 异步批量 Sink 将吞吐量从 1w QPS 提升到 10w+?敬请期待。

相关推荐
渣渣盟2 小时前
当 Checkpoint 稳定运行后,如何进一步优化 Flink 作业的启动和恢复速度,让大状态作业的扩缩容从“小时级”降到“分钟级”?
大数据·flink
ZCBUS实时计算15 小时前
金融证券实时数仓建设实践:轻量化实时计算平台落地,实现交易数据端到端秒级处理
大数据·数据库·数据仓库·金融·flink·dba·etl
海上小飞龙16 小时前
Redis 分布式锁原理:从 SET NX EX 到 Redisson 看门狗
数据库·redis·分布式
万物皆字节19 小时前
【避坑】使用CacheEvict需要注意的坑
redis
范什么特西1 天前
回答知识总结04(redis)
数据库·redis·缓存
higherzjm2 天前
如何通过Linux命令自动切割nginx和redis日志
linux·redis·nginx
PC2005-cloud2 天前
Redis学习笔记:Cluster 集群读写分离实战,Lettuce + Spring Boot 从配置到验证
redis·笔记·学习
Crazy________2 天前
Redis02:库切换、监控与安全配置
linux·redis·云原生·mybatis
海上小飞龙2 天前
Redis 持久化:RDB 与 AOF
数据库·redis·缓存