ZooKeeper 避坑实践:如何调优 jute.maxbuffer

3,453 阅读5分钟

作者:子葵

背景

在日常运维 ZooKeeper 中,经常会遇到长时间无法选主,恢复时进程启动又退出,进而导致内存暴涨,CPU飙升,GC频繁,影响业务可用性,这些问题有可能和 jute.maxbuffer 的设置有关。本篇文章就深入 ZooKeeper 源码,一起探究一下ZooKeeper 的 jute.maxbuffer 参数的最佳实践。

1.png

2.png

分析

首先我们通过 ZooKeeper 的官网上看到 jute.maxbuffer 的描述:

jute.maxbuffer : 

(Java system property:jute.maxbuffer).

......, It specifies the maximum size of the data that can be stored in a znode. The unit is: byte. The default is 0xfffff(1048575) bytes, or just under 1M.

When jute.maxbuffer in the client side is greater than the server side, the client wants to write the data exceeds jute.maxbuffer in the server side, the server side will get java.io.IOException: Len error

When jute.maxbuffer in the client side is less than the server side, the client wants to read the data exceeds jute.maxbuffer in the client side, the client side will get java.io.IOException: Unreasonable length or Packet len is out of range!

从官网的描述中我们可以知道,jute.maxbuffer 能够限制 Znode 大小,需要在Server端和Client端合理设置,否则有可能引起异常。

但事实并非如此,我们在ZooKeeper的代码中寻找 jute.maxbuffer 的定义和引用:

public static final int maxBuffer = Integer.getInteger("jute.maxbuffer", 0xfffff);

在 org.apache.jute.BinaryInputArchive 类型中通过 System Properties 读取到 jute.maxbuffer的值,可以看到默认值是1M,checkLength 方法引用了此静态值:

// Since this is a rough sanity check, add some padding to maxBuffer to
// make up for extra fields, etc. (otherwise e.g. clients may be able to
// write buffers larger than we can read from disk!)
private void checkLength(int len) throws IOException {
    if (len < 0 || len > maxBufferSize + extraMaxBufferSize) {
        throw new IOException(UNREASONBLE_LENGTH + len);
    }
}

只要参数 len 超过maxBufferSize 和 extraMaxBufferSize的和,就会抛出 Unreasonable length 的异常,在生产环境中这个异常往往会导致非预期的选主或者Server无法启动。

再看一下 extraMaxBufferSize 的赋值:

static {
    final Integer configuredExtraMaxBuffer =
        Integer.getInteger("zookeeper.jute.maxbuffer.extrasize", maxBuffer);
    if (configuredExtraMaxBuffer < 1024) {
        extraMaxBuffer = 1024;
    } else {
        extraMaxBuffer = configuredExtraMaxBuffer;
    }
}

可以看到 extraMaxBufferSize 默认会使用maxBuffer的值,并且最小值为 1024 (这里是为了和以前的版本兼容),因此在默认的情况下,checkLength方法抛出异常的阈值是 1M + 1K。

接着我们看一下 checkLength 方法的引用链:

有两个地方引用到了 checkLength 方法:即 org.apache.jute.BinaryInputArchive 类型的 readString 和 readBuffer方法。

    public String readString(String tag) throws IOException {
        ......
        checkLength(len);
        ......
    }

    public byte[] readBuffer(String tag) throws IOException {
        ......
        checkLength(len);
        ......
    }

而这两个方法在几乎所有的 org.apache.jute.Recod 类型中都有引用,也就是说 ZooKeeper 中几乎所有的序列化对象在反序列化的时候都会进行 checkLength 检查,因此可以得出结论 jute.maxbuffer 不仅仅限制 Znode 的大小,而是所有调用 readString 和 readBuffer 的 Record 的大小。

这其中就包含 org.apache.zookeeper.server.quorum.QuorumPacket 类型。

此类型是在 Server 进行 Proposal 时传输数据使用的序列化类型,包含写请求产生的 Txn 在 Server 之间进行同步时传递数据都是通过此类型进行序列化的,如果事务太大就会导致 checkLength 失败抛出异常,如果是普通的写请求,因为在请求收到的时候就会 checkLength,因此在预处理请求的时候就可以避免产生过大的 QuorumPacket,但是如果是 CloseSession 请求,在这种情况下就可能出现异常。

我们可以通过 PreRequestProcessor的processRequest方法看到生成CloseSessionTxn 的过程:

    protected void pRequest2Txn(int type, long zxid, Request request, Record record, boolean deserialize) throws KeeperException, IOException, RequestProcessorException {
        ......
        case OpCode.closeSession:
            long startTime = Time.currentElapsedTime();
            synchronized (zks.outstandingChanges) {
                Set<String> es = zks.getZKDatabase().getEphemerals(request.sessionId);
                for (ChangeRecord c : zks.outstandingChanges) {
                    if (c.stat == null) {
                        // Doing a delete
                        es.remove(c.path);
                    } else if (c.stat.getEphemeralOwner() == request.sessionId) {
                        es.add(c.path);
                    }
                }  
                if (ZooKeeperServer.isCloseSessionTxnEnabled()) {
                    request.setTxn(new CloseSessionTxn(new ArrayList<String>(es)));
                }
                ......
    }

CloseSession 请求很小一般都可以通过 checkLength 的检查,但是 CloseSession 产生的事务却有可能很大,可以通过 org.apache.zookeeper.txn.CloseSessionTxn 类型的定义可知此 Txn 中包含所有此 Session 创建的 ephemeral 类型的 Znode,因此,如果一个Session创建了很多 ephemeral 类型的 Znode,当此 Session 有一个 CloseSession 的请求经过 Server 处理的时候,Leader 向 Follower 进行 proposal 的时候就会出现一个特别大的 QuorumPacket,导致在反序列化的时候进行 checkLength 检查的时候会抛出异常,可以通过 Follower 的 followLeader 方法看到,在出现异常的时候,Follower 会断开和 Leader 的连接:

void followLeader() throws InterruptedException {
        ......
        ......
        ......
                // create a reusable packet to reduce gc impact
                QuorumPacket qp = new QuorumPacket();
                while (this.isRunning()) {
                    readPacket(qp);
                    processPacket(qp);
                }
            } catch (Exception e) {
                LOG.warn("Exception when following the leader", e);
                closeSocket();

                // clear pending revalidations
                pendingRevalidations.clear();
            }
        } finally {
        ......
        ......
    }

当超过半数的 follower 都因为 QuorumPacket 过大而无法反序列化的时候就会导致集群重新选主,并且如果原本的 Leader 在选举中获胜,那么这个 Leader 就会在从磁盘中 load 数据的时候,从磁盘中读取事物日志的时候,读取到刚刚写入的特别大的 CloseSessionTxn 的时候 checkLength 失败,导致 Leader 状态又重新进入 LOOKING 状态,集群又开始重新选主,并且一直持续此过程,导致集群一直处于选主状态

原因

集群非预期选主,持续选主或者server 无法启动,经过以上分析有可能就是在 jute.maxbuffer 设置不合理,连接到集群的某一个 client 创建了特别多的 ephemeral 类型节点,并且当这个 session 发出 closesession 请求的时候,导致 follower 和 Leader 断连。最终导致集群选主失败或者集群无法正常启动。

最佳实践建议

首先如何发现集群是因为jute.maxbuffer 设置不合理导致的集群无法正常选主或者无法正常启动 ?

1. 在checkLength方法检查失败的时候会抛出异常,关键字是 Unreasonable length,同时 follower 会断开和 Leader的连接,关键字是 Exception when following the leader,可以通过检索关键字快速确认。

3.png

2. ZooKeeper 在新版本中提供了 last_proposal_size metrics 指标,可以通过此指标监控集群的 proposal 大小数据,当有 proposal 大于 jute.maxbuffer 的值的时候就需要排查问题。

jute.maxbuffer 如何正确设置?

1. 官方文档首先建议我们在客户端和服务端正确的设置 jute.maxbuffer ,最好保持一致,避免非预期的 checkLength 检查失败。

2. 官方文档建议 jute.maxbuffer 的值不宜过大,大的 Znode 可能会导致 Server 之间同步数据超时,在大数据请求到达 Server 的时候就被拦截掉。

3. 在实际的生产环境中,为了保证生产环境的稳定,如果 jute.maxbuffer 的值设置过小,服务端有可能持续不可用,需要需要更改 jute.maxbuffer 的值才能正常启动,因此这个值也不能太小。

4. Dubbo 低版本存在重复注册问题,当重复注册达到一定的量级,就有可能触发这个阈值(1M),Dubbo 单节点注册的 Path 长度按照 670 字节计算,默认阈值最多容纳 1565 次重复注册,因此在业务侧需要规避重复注册的问题。综上,在使用的 ZooKeeper 的过程中,jute.maxbuffer 的设置还需要考虑到单个 session 创建过多的 ephemeral 节点这一种情况,合理配置 jute.maxbuffer 的值。

在 MSE ZooKeeper 中,可以通过控制台快捷修改 jute.maxbuffer 参数:

4.png

设置 MSE ZooKeeper 选主时间告警以及节点不可用告警。首先进入告警管理页面,创建新的告警:

5.png

分组选择 ZooKeeper 专业版,告警项选择选主时间,然后设置阈值:

6.png

POD 状态告警,分组选择 ZooKeeper 专业版,告警项选择 ZooKeeper 单 POD状态:

6.png

配置节点不可用和选主时间告警,及时发现问题进行排查。

运营活动

重磅推出 MSE 专业版,更具性价比,可直接从基础版一键平滑升级到专业版!

7.png

8.png