背景
在日常运维ZooKeeper中,经常会遇到长时间无法选主,恢复时进程启动又退出,进而导致内存暴涨,CPU飙升,GC频繁,影响业务可用性,这些问题有可能和 jute.maxbuffer 的设置有关。本篇文章就深入 ZooKeeper 源码,一起探究一下ZooKeeper 的 jute.maxbuffer 参数的最佳实践。
分析
首先我们通过 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.
- Whenjute.maxbufferin the client side is greater than the server side, the client wants to write the data exceedsjute.maxbufferin the server side, the server side will getjava.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 设置不合理导致的集群无法正常选主或者无法正常启动 ?
- 在checkLength方法检查失败的时候会抛出异常,关键字是 Unreasonable length,同时follower会断开和leader的连接,关键字是 Exception when following the leader,可以通过检索关键字快速确认。
- ZooKeeper 在新版本中提供了 last_proposal_size metrics指标,可以通过此指标监控集群的proposal大小数据,当有proposal大于jute.maxbuffer 的值的时候就需要排查问题
jute.maxbuffer 如何正确设置?
- 官方文档首先建议我们在客户端和服务端正确的设置jute.maxbuffer ,最好保持一致,避免非预期的checkLength检查失败。
- 官方文档建议 jute.maxbuffer 的值不宜过大,大的Znode可能会导致server之间同步数据超时,在大数据请求到达Server的时候就被拦截掉。
- 在实际的生产环境中,为了保证生产环境的稳定,如果jute.maxbuffer 的值设置过小,服务端有可能持续不可用,需要需要更改jute.maxbuffer 的值才能正常启动,因此这个值也不能太小
- Dubbo 低版本存在重复注册问题,当重复注册达到一定的量级,就有可能触发这个阈值(1M),Dubbo 单节点注册的Path长度按照670字节计算,默认阈值最多容纳1565次重复注册,因此在业务侧需要规避重复注册的问题
综上,在使用的ZooKeeper的过程中,jute.maxbuffer的设置还需要考虑到单个session创建过多的 ephemeral 节点这一种情况,合理配置 jute.maxbuffer 的值。
在MSE ZooKeeper中,可以通过控制台快捷修改jute.maxbuffer 参数
设置 MSE ZooKeeper 选主时间告警以及节点不可用告警
首先进入告警管理页面,创建新的告警
分组选择ZooKeeper 专业版,告警项选择选主时间,然后设置阈值
POD 状态告警,分组选择ZooKeeper专业版,告警项选择ZooKeeper 单 POD状态
配置节点不可用和选主时间告警,及时发现问题进行排查
运营活动
重磅推出MSE 专业版,更具性价比,可直接从基础版一键平滑升级到专业版!