zk session expire会引起HA模式的rm一直处于standby吗

【概述】


最近连续在多个环境中遇到了同一个问题:在HA模式下,两个resourcemanager均为standby,并且持续没有选举出新的leader。经过一番分析,并对照源码梳理问题出现前后的逻辑流程,最后发现是因为zk会话过期(session expire)引起的问题,本文就复盘总结下。

【RM的正常选举流程】

在很早之前的文章中,介绍过hadoop里namenode的HA机制(戳这里),RM的选举流程其实是复用了同样的框架,只是以一个独立线程的方式运行,而不是像namenode一样,有个独立的进程(zkfc)负责与zk连接并选举。

因此,整体的选举流程会和namenode的选举方式基本雷同,即首先向zk建立连接,当连接建立成功后,在zk上竞争创建临时锁节点,成功创建的rm成为active,失败的则成为standby。

【与zk之间网络异常后的情况】

正常逻辑是相对简单的,那我们再来看看与zk之间网络出现异常,以及网络异常恢复之后的处理逻辑,具体如下图所示:

10cd742afc2ea01202548ee9c8a94df3.jpeg

1. 当ZK服务出现故障,或者网络出现故障,导致网络完全不可达时,客户端与ZK的连接会出现在指定时间内没有读到任何数据,从而引发会话超时。(也可能是读异常,此时产生的是EndOfStreamException,后续处理逻辑与会话超时的逻辑一样)。

这个时候,zk客户端的发送线程会抛会话超时的异常,同时内部捕获该异常, 向事件回调线程的队列中插入连接断开的事件。此后,循环执行与zk的重连动作。

while (state.isAlive()) {
    try {
        ...
        if (to <= 0) {
            String warnInfo;
            warnInfo = 
                "Client session timed out, have not heard from server in " +
                clientCnxnSocket.getIdleRecv() + "ms" + 
                " for sessionid 0x" + Long.toHexString(sessionId);
            LOG.warn(warnInfo);
            throw new SessionTimeoutException(warnInfo);
        }
    } catch (Throwable e) {
        ...
        if (state.isAlive()) {
            eventThread.queueEvent(
                new WatchedEvent(Event.EventType.None, Event.KeeperState.Disconnected, null));
        }
        ...
    }
}

2. zk客户端中的事件回调线程接收到事件后,向上进行回调通知。在RM的回调处理中,启动定时器线程,触发成为standby。

synchronized void processWatchEvent(ZooKeeper zk, WatchedEvent event) {
    ...
    if (eventType == Event.EventType.None) {
        switch (event.getState()) {
        case Disconnected:
            LOG.info("Session disconnected. Entering neutral mode...");

            zkConnectionState = ConnectionState.DISCONNECTED;
            enterNeutralMode();
            break;
        ...
        }
    }
}

private void enterNeutralMode() {
    if (state != State.NEUTRAL) {
   &nb
非常好的问题! > **“`session` 是指 session 工厂吗?”** ### ✅ 简短回答: **不是。你注入到路由中的 `session` 通常是一个 *已经创建好的数据库会话实例*(即 `Session` 实例),而 `session 工厂` 是用来 *生成这种 session 实例的函数或类*。** --- ## 🔍 概念解析 | 名称 | 含义 | 类型示例 | 作用 | |------|------|----------|------| | 🔹 Session Factory(会话工厂) | 一个可调用对象,每次调用都返回一个新的 `Session` 实例 | `sessionmaker`, `async_sessionmaker` | 创建 session | | 🔹 Session(会话) | 实际用于执行数据库操作的对象(如 `add`, `exec`, `commit`) | `AsyncSession` | 数据库 CRUD | | 🔹 Dependency(依赖) | FastAPI 中通过 `Depends()` 提供的 `session` 实例 | `yield session` | 在请求生命周期中共享会话 | --- ## ✅ 示例代码说明一切 ```python # database.py from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker # 1. 创建异步引擎 engine = create_async_engine("sqlite+aiosqlite:///./app.db", echo=True) # 2. 创建【Session 工厂】 ✅ AsyncSessionLocal = async_sessionmaker( bind=engine, class_=AsyncSession, expire_on_commit=False ) # 3. 定义依赖:提供实际的 session 实例 async def get_session(): async with AsyncSessionLocal() as session: # ← 这里调用工厂创建 session yield session # ← 返回的是一个 session 实例! ``` ### 📌 关键点: - `AsyncSessionLocal` 是 **工厂**(Factory) - `AsyncSessionLocal()` 调用一次 → 得到一个 `session` 实例 - `get_session()` 函数使用这个工厂创建并管理一个 session 生命周期 --- ## ✅ 在路由中使用时发生了什么? ```python @router.post("/user") async def create_user( data: UserCreate, session: AsyncSession = Depends(get_session) # ← 注入的是 session 实例 ): user = User(**data.dict()) session.add(user) await session.commit() await session.refresh(user) return user ``` 这里的 `session` 是: > ✅ 由 `AsyncSessionLocal()` 工厂创建出来的 **具体会话实例**,不是工厂本身。 你可以把它理解为: ```python session = AsyncSessionLocal() # 工厂造出一个实例 ``` --- ## 🧠 类比理解(工厂模式) | 现实比喻 | 对应技术概念 | |--------|-------------| | 🔨 汽车生产线(工厂) | `async_sessionmaker` / `sessionmaker` | | 🚗 生产出的一辆汽车 | `AsyncSession()` 实例 | | 🏭 使用汽车的人 | 你的 FastAPI 路由函数 | | ⏳ 用完后回收销毁 | `yield` 后自动关闭 session | 所以: > ❌ `session` 不是工厂 > ✅ `session` 是工厂生产出来的“产品” --- ## ✅ 常见误区澄清 | 误解 | 正解 | |------|------| | `get_session` 返回的是工厂 | 错!它返回的是 `session` 实例(通过工厂创建) | | 每次 `Depends(get_session)` 都复用同一个 session | 错!每个请求都会新建一个 session(由工厂创建) | | 可以直接 `Depends(AsyncSessionLocal)` | 不推荐!虽然能工作,但无法控制生命周期 | --- ## ✅ 最佳实践总结 ```python # ✅ 正确做法:定义依赖来管理 session 生命周期 async def get_session() -> AsyncSession: async with AsyncSessionLocal() as session: yield session ``` 然后在所有需要数据库操作的地方: ```python def some_function(session: AsyncSession = Depends(get_session)): result = await session.exec(select(User)) return result.all() ``` 这样你就做到了: - ✅ 每个请求独享一个 session - ✅ 请求结束自动关闭连接 - ✅ 支持事务控制 - ✅ 可测试、可替换(比如测试时用内存数据库) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值