背景
业务方数据在出现错误后需要重跑数据,由于业务方没有使用MergeTree的折叠表,需要删除旧的数据后,再重新跑数据写入新的正确的数据。
之前这种模式一直运转的比较好,没有出现过问题,不过近期发现,对该表发起Alter语句时,出现了ZK Connection Loss的错误,但是对其他的表发起Alter语句没有出现相同的错误。
本文主要分析一下定位问题的过程以及确定问题所在,也希望大家就该问题进行讨论提供更好的解决方案。
问题现象分析
问题描述
Clickhouse版本:20.9.3.45
表结构:
CREATE TABLE default.business_table
(
createTime DateTime,
appid int ,
totalCount bigint
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/business_table', '{replica}');
Alter语句以及响应的报错信息:
alter table default.business_table delete where toYYYYMMDDHH(createTime) =2022012020 and appid=1;
ERROR 999 (00000): Code: 999, e.displayText() = Coordination::Exception: Connection loss (version 20.9.3.45 (official build))
问题定位
首先查看了一下clickhouse的错误日志,错误日志中有相关的堆栈信息
2022.02.10 11:17:51.706169 [ 34045 ] {} executeQuery: Code: 999, e.displayText() = Coordination::Exception: Connection loss (version 20.9.3.45 (official build)) (from 12
7.0.0.1:48554) (in query: alter table default.business_table delete where toYYYYMMDDHH(createTime) =2022012020 and appid=1), Stack trace (when copying this message, always include the lines below):
0. Poco::Exception::Exception(std::__1::basic_string, std::__1::allocator > const&, int) @ 0x18e1b360 in /usr/bin/clickhouse
1. DB::Exception::Exception(std::__1::basic_string, std::__1::allocator > const&, int) @ 0xe736dad in /usr/bin/clickhouse
2. Coordination::Exception::Exception(Coordination::Error) @ 0x16887dad in /usr/bin/clickhouse
3. ? @ 0x168991b0 in /usr/bin/clickhouse
4. DB::EphemeralLocksInAllPartitions::EphemeralLocksInAllPartitions(std::__1::basic_string, std::__1::allocator > const&, std::__1::basic
_string, std::__1::allocator > const&, std::__1::basic_string, std::__1::allocator > const&, zkut
il::ZooKeeper&) @ 0x161dca16 in /usr/bin/clickhouse
5. DB::StorageReplicatedMergeTree::EphemeralLocksInAllPartitions(DB::MutationCommands const&, DB::Context const&) @ 0x1609c636 in /usr/bin/clickhouse
6. DB::InterpreterAlterQuery::execute() @ 0x15ab5126 in /usr/bin/clickhouse
再查看了一下zk的错误日志
2022-02-10 11:17:51,680 [myid:90] - WARN [NIOWorkerThread-30:NIOServerCnxn@373] - Close of session 0x5a02260902470005
java.io.IOException: Len error 1190892
at org.apache.zookeeper.server.NIOServerCnxn.readLength(NIOServerCnxn.java:541)
at org.apache.zookeeper.server.NIOServerCnxn.doIO(NIOServerCnxn.java:332)
at org.apache.zookeeper.server.NIOServerCnxnFactory$IOWorkRequest.doWork(NIOServerCnxnFactory.java:522)
at org.apache.zookeeper.server.WorkerService$ScheduledWorkRequest.run(WorkerService.java:154)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
然后大致对比了一下系统的表的大小,目前出问题的表是最大的。
mysql> select count() from system.parts where table='business_table' and active =1 ;
+---------+
| count() |
+---------+
| 8108 |
+---------+
1 row in set (0.04 sec)
从上面可以看出表的数据分片很多。
分析ZK的日志发现,ZK认为客户端发送的消息格式不正确,从而主动断开了clickhouse的连接。从clickhouse的异常日志有可以看出正在执行zk操作时出现了连接断开的错误。
现在我们从代码层面去看看问题的根因,当clickhouse执行alter操作时,如果对应的mutation如果涉及到分片数据的变更时,就需要对分片进行锁定,而分片的锁定操作是在对应的分片对应的zk子目录下面创建一个临时节点,如下面代码所示:
EphemeralLocksInAllPartitions::EphemeralLocksInAllPartitions(
const String & block_numbers_path, const String & path_prefix, const String & temp_path,
zkutil::ZooKeeper & zookeeper_)
: zookeeper(&zookeeper_)
{
std::vector holders;
while (true)
{
......
Coordination::Requests lock_ops;
// 这里没有
for (size_t i = 0; i < partitions.size(); ++i)
{
String partition_path_prefix = block_numbers_path + "/" + partitions[i] + "/" + path_prefix;
lock_ops.push_back(zkutil::makeCreateRequest(
partition_path_prefix, holders[i], zkutil::CreateMode::EphemeralSequential));
}
lock_ops.push_back(zkutil::makeCheckRequest(block_numbers_path, partitions_stat.version));
Coordination::Responses lock_responses;
// 问题出在这里
Coordination::Error rc = zookeeper->tryMulti(lock_ops, lock_responses);
if (rc == Coordination::Error::ZBADVERSION)
{
LOG_TRACE(&Poco::Logger::get("EphemeralLocksInAllPartitions"), "Someone has inserted a block in a new partition while we were creating locks. Retry.");
continue;
}
else if (rc != Coordination::Error::ZOK)
throw Coordination::Exception(rc);
clickhouse在zk的访问中,采用了大量批量操作,在上面的分片锁定操作中,它针对所有影响到的分片的锁定批量一次性提交命令到zk中,而zk的传输使用了jute,jute缺省最大的包大小为1M,具体细节可以参考一下关于zookeeper写入数据超过1M大小的踩坑记。
这里clickhouse的问题在于它没有做分包,而是对所有影响的分片合并请求后,批量向zk发起请求,从而造成了超过zk最大的传输包大小,从而造成连接断开。
为什么这里需要一次性的批量提交呢?具体的原因有朋友了解的可以分享一下,我理解可能clickhouse需要做类似事务级别的保证。
问题解决
知道了问题的根因首先考虑到增加zk的jute缺省的最大包大小,zookeeper本身,我们可以在配置上实现。但是我们查看了一下clickhouse的zk配置相关参数,能够调整的主要是ip、port和会话时长,没有看到jute大小的控制参数,所以这条路基本上行不通,经过只修改zk的参数重启后,测试也发现不能成功。
控制Alter DELETE影响的数据范围,从原来的Alter语句来看我们已经制定了时间的范围,但是看起来Clickhouse不会主动根据条件来做分区裁剪。查看源码也发现没有这块逻辑,但是从最新的clickhouse的文档中,我们可以看到Delete语句支持分区操作。
ALTER TABLE [db.]table DELETE [IN PARTITION partition_id] WHERE filter_expr
但是这个语法在20.9.3.45版本中并没有得到支持,所以最后我们对clickhouse做了升级到21.X.X.X,并让业务方采用DELETE IN PARTITION,问题暂时得到解决。如果读者有更好的解决方案,希望留言探讨