列式查询优化试验失败后该看什么

封面信息图

ClickHouse 的列式执行适合日志、行为分析和指标聚合。将向量检索或计划改写接入其中,首先要确认它与内存配额、后台 Merge 和查询并发能否共存。

本文以一次向量索引集成演练为例,说明如何收集证据并判断方案是否适合继续推进。


1. 矢量化执行引擎中的内存泄漏与 Thread Pool 阻塞现象

在 ClickHouse 架构中,向量化执行的核心是将数据按 Block(默认 8192 行)组织在内存中,并使用 CPU SIMD 指令(AVX-512、AVX2)进行批量计算。

在这组演练中,团队接入了基于 HNSW 的向量检索 UDF vectorDistance(v1, v2)。单查询结果不能代表并发场景,因此需要同时观察内存、Merge 和查询日志。可能看到的日志特征如下:

2026.08.30 14:22:05.112901 [ 45102 ] {} <Error> Application: Child process was terminated by signal 9 (SIGKILL).
2026.08.30 14:22:01.884102 [ 12044 ] {a2c3491f} <Error> DynamicQueryHandler: Memory limit (for query) exceeded: would use 32.10 GiB (limit: 30.00 GiB).

底层根因定位表明:

  • JNI / C++ Native 堆外内存失控:AI 向量索引在执行 vectorDistance 时,需要在 C-Heap 分配临时的距离矩阵与 Graph 节点索引结构。ClickHouse 的 MemoryTracker 机制默认仅监控 Arena 和分配在 std::allocator 上的字节数,无法感知第三方 C++ 向量库内部使用 raw malloc 分配的堆外内存。
  • Thread Pool 锁等待链阻塞:当向量索引计算触发大量 CPU 计算时,ClickHouse 内部的 GlobalThreadPool 线程被全部打满,导致后台 Merge 线程(MergeTree Background Block Processor)无法分配到 CPU 资源。由于 Parts 无法及时 Merge,磁盘 Data Parts 数量急剧膨胀,进一步加剧了主内存中的 Primary Index 加载压力。

2. 收集线上故障证据链:从 system.query_logdmesg

要判断演练中的根因,可按时间顺序提取四层物理与逻辑日志:

证据链核心判定点:

  1. system.query_log 证据:提取崩溃前 1 分钟 read_rowsmemory_usage 曲线。发现部分 SQL 的 memory_usage 显示仅 4GB,但对应 OS 进程的 RSS 却暴增至 64GB。这直接证明了堆外内存脱离 Tracking
  2. system.merges 证据:在故障发生前,后台 active_merges 数值骤降为 0,而 system.parts 表中处于 active 状态的 Part 数升至 800+,触发 Too many parts in all data parts in table 保护警报。
  3. dmesg / OS System Error 证据:Linux oom-killer 记录的 anon-rsstotal_vm 达到物理上限。

3. Vector Search 索引与 Storage Engine 冲突的根因分析

ClickHouse 的 MergeTree 存储引擎设计基于不可变的 Data PartsMark Boundaries(标记索引)。标准索引仅保存每 8192 行数据开头的 Primary Key。

AI 向量索引与 MergeTree 架构的内在冲突体现在:

  • Part Merge 时的索引重建开销:若索引不能随 Part 合并增量维护,就需要在合并后重建。重建复杂度、CPU 占用和内存峰值应通过目标表规模测量,而不能直接套用固定倍数。
  • 标记过滤失效:MergeTree 依赖 Primary Index 快速跳过无用 Mark Range。而高维向量索引具有“小世界”聚类特性,标准 Mark 粒度的顺序存储打破了向量的空间局部性(Spatial Locality),导致每次向量查询必须强行加载整个 Part 的所有列数据。

4. 生产级故障现场证据链自动采集与分析脚本

为了在类似的 AI 实验或生产故障发生时快速锁定现场,避免运维人员盲目重启,以下是一个使用 Python 编写的生产级 ClickHouse 故障证据链自动提取工具:

#!/usr/bin/env python3
"""
ClickHouse 故障证据链自动化采集与诊断脚本
功能:
1. 解析 ClickHouse 错误日志与 system.query_log;
2. 提取 OOM 发生时的内存配额、慢查询与挂起 Merge;
3. 检查 Linux 内核 dmesg 中的 oom-killer 记录;
4. 交叉验证物理 RSS 与 ClickHouse 逻辑 Tracker 的偏离度,输出诊断证据链。
"""

import re
import subprocess
import json
import logging
from typing import Dict, List, Any

# 日志输出配置
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s'
)

class ClickHouseFaultAnalyzer:
    def __init__(self, clickhouse_client_path: str = "clickhouse-client", host: str = "127.0.0.1", port: int = 9000):
        self.client_cmd = [clickhouse_client_path, "--host", host, "--port", str(port), "--format", "JSON"]

    def run_ch_query(self, query: str) -> List[Dict[str, Any]]:
        """
        向 ClickHouse 执行诊断查询并解析 JSON 结果
        """
        try:
            cmd = self.client_cmd + ["--query", query]
            result = subprocess.run(cmd, capture_output=True, text=True, check=True)
            payload = json.loads(result.stdout)
            return payload.get("data", [])
        except Exception as e:
            logging.error(f"执行 ClickHouse 查询失败: {str(e)}")
            return []

    def fetch_oom_queries(self) -> List[Dict[str, Any]]:
        """
        搜寻 system.query_log 中被异常终止或内存超限的查询
        """
        query = """
        SELECT
            query_id,
            user,
            query,
            exception_code,
            memory_usage,
            read_rows,
            read_bytes,
            query_duration_ms
        FROM system.query_log
        WHERE type = 'ExceptionBeforeStart' OR type = 'ExceptionWhileProcessing'
          OR exception_code IN (241, 252) -- Memory limit exceeded codes
        ORDER BY event_time DESC
        LIMIT 10
        """
        return self.run_ch_query(query)

    def fetch_stuck_merges(self) -> List[Dict[str, Any]]:
        """
        搜寻被阻塞或异常堆积的 MergeTree 后台 Merge 状态
        """
        query = """
        SELECT
            database,
            table,
            elapsed,
            progress,
            num_parts,
            total_size_bytes_compressed
        FROM system.merges
        WHERE elapsed > 60
        """
        return self.run_ch_query(query)

    def inspect_dmesg_oom(self) -> List[str]:
        """
        从 OS dmesg 中搜索 ClickHouse 进程被 OOM Killer 杀死的记录
        """
        oom_logs = []
        try:
            res = subprocess.run(["dmesg", "-T"], capture_output=True, text=True)
            if res.returncode == 0:
                for line in res.stdout.split("\n"):
                    if "clickhouse-serv" in line or "oom-killer" in line:
                        oom_logs.append(line)
        except Exception as e:
            logging.warning(f"无法读取 dmesg 信息: {str(e)}")
        return oom_logs[-5:] # 返回最后 5 条记录

    def generate_evidence_chain(self):
        logging.info("开始生成 ClickHouse 故障定位证据链...")
        
        oom_queries = self.fetch_oom_queries()
        stuck_merges = self.fetch_stuck_merges()
        dmesg_logs = self.inspect_dmesg_oom()

        evidence_report = {
            "summary": "OK",
            "evidence_chain": []
        }

        # 验证证据 1:逻辑内存溢出
        if oom_queries:
            evidence_report["evidence_chain"].append({
                "level": "CRITICAL",
                "source": "system.query_log",
                "detail": f"捕获到 {len(oom_queries)} 条因 Memory Limit Exceeded 崩溃的 Query",
                "sample_query": oom_queries[0]["query"]
            })

        # 验证证据 2:后台 Merge 堵塞
        if stuck_merges:
            evidence_report["evidence_chain"].append({
                "level": "WARNING",
                "source": "system.merges",
                "detail": f"检测到 {len(stuck_merges)} 个耗时超过 60 秒的挂起 Merge 任务,后台 Part 堆积严重"
            })

        # 验证证据 3:系统级 OOM
        if dmesg_logs:
            evidence_report["evidence_chain"].append({
                "level": "FATAL",
                "source": "Linux OS Kernel (dmesg)",
                "detail": "操作系统捕获到 clickhouse-server 的 oom-killer 事件",
                "raw_logs": dmesg_logs
            })
            evidence_report["summary"] = "FAILED: 存在 C-Heap 堆外内存泄漏或物理 RAM 枯竭!"

        print("\n" + "="*60)
        print(" ClickHouse 线上故障定位证据链报告")
        print("="*60)
        print(json.dumps(evidence_report, indent=2, ensure_ascii=False))

if __name__ == "__main__":
    analyzer = ClickHouseFaultAnalyzer()
    analyzer.generate_evidence_chain()

5. AI 向量索引引擎集成与原生 Mark Range 索引 Trade-offs

在 OLAP 场景中引入 AI 增强算法时,应比较各技术路线的成本与边界:

评估维度 原生 ClickHouse (Mark Range + Primary Key) AI 增强 Vector 索引 (HNSW In-Kernel) 外挂向量数据库 (ClickHouse + Annoy/Milvus)
单查询 Latency (向量计算) 慢 (需全表/全 Part 暴力扫描解压) 极快 ($< 10\text{ms}$) 极快 ($< 5\text{ms}$)
写入与 Merge 吞吐 极高 (100,000+ rows/sec/core) 极低 (Merge 时重算 Graph 消耗巨量 CPU) 极高 (ClickHouse 专注写入,外挂库解耦)
内存使用安全性 极其安全 (受 MemoryTracker 完全掌控) 危险 (存在 C-Heap 堆外泄露与 OOM 风险) 安全 (进程间物理隔离)
架构复杂度 单一二进制,运维极简 单一二进制,但编译与 C++ 依赖复杂 双组件集群,需维护数据同步链路
适用场景 精确标量过滤与海量指标聚合 小规模数据量下的近实时向量检索 海量向量+海量标量的混合检索生产架构

总结

system.query_logsystem.merges 和操作系统日志放到同一时间线中,才能区分查询负载、Merge 和内存回收的影响。是否采用向量索引,应由这些测量结果以及清晰的回退方案共同决定。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐