自建CDN系统需要持续记录每一次边缘节点的请求与响应,典型字段包括访问时间、客户端IP、请求域名、完整URL、响应状态码、传输字节数、是否命中缓存、回源耗时等。当节点数量增多、业务流量上升后,每天积累的日志很容易达到数十亿行,单日数据量可达TB级别。分析团队经常需要回答这样的问题:哪些域名消耗了最多带宽?某个文件的缓存命中率是多少?回源延迟集中在哪些地区?这些查询如果跑在按行存储的数据库上,往往会因为读取大量无关字段而变得非常慢。ClickHouse凭借列式存储、向量化计算和稀疏索引,能够在秒级完成这类聚合分析。

一、列式存储为什么适合CDN日志分析
列式存储与行式存储的物理布局差异,直接决定了日志分析场景下的IO效率。行式存储把一行记录的所有字段连续存放在一起,读取一行就需要把所有列加载进内存;列式存储则把每一列独立组织成数据文件,同一列的数据类型一致、取值范围相近,压缩比通常高出数倍。CDN日志查询绝大多数是聚合统计,例如统计域名流量只需要domain和body_bytes_sent,判定缓存命中率只需domain和cache_status。列式存储允许ClickHouse只扫描这些目标列,其他无关列完全不参与磁盘读取,IO开销大幅下降。
除了按列读取,压缩带来的好处也不能忽视。CDN日志中的状态码、缓存状态、域名等字段重复度很高,列式压缩后体积可能只有原始文本的十分之一。更紧凑的数据块可以在相同内存中容纳更多记录,CPU缓存命中率随之提高。ClickHouse执行聚合时使用向量化引擎,一次处理一批列数据,而不是一行一行解释执行,进一步放大了列式存储的性能优势。再配合MergeTree表引擎的稀疏索引,查询可以在分区和数据块级别跳过大量无关数据,最终将分钟级的查询压缩到秒级。
二、设计ClickHouse表结构的关键点
在ClickHouse中处理CDN日志,通常使用MergeTree系列表引擎。建表时需要重点考虑分区键、排序键和字段类型。下面是一个适合CDN访问日志的基础表结构:
CREATE TABLE cdn_logs
(
log_time DateTime,
client_ip String,
domain String,
url String,
status_code UInt16,
body_bytes_sent UInt64,
cache_status LowCardinality(String),
upstream_response_time Float32,
user_agent String
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(log_time)
ORDER BY (domain, log_time)
TTL log_time + INTERVAL 30 DAY
SETTINGS index_granularity = 8192;
分区键选择toYYYYMMDD(log_time),数据按天划分分区。这样做的直接好处是,清理过期数据时只需删除整个分区,而不需要逐行执行删除操作,维护成本低且不会产生大量碎片。排序键设置为(domain, log_time),原因是CDN日志查询经常先按域名过滤,再限定时间范围。稀疏索引会依据排序键快速定位到满足条件的连续数据块,避免扫描整个分区。
字段类型方面,cache_status使用LowCardinality(String)可以显著降低内存占用,因为缓存状态通常只有HIT、MISS、EXPIRED等少数几种取值。client_ip和user_agent这类高基数字段保留普通字符串即可,不必建立二级索引。TTL设置log_time + INTERVAL 30 DAY可以让ClickHouse后台自动合并时删除过期分区,控制总存储量。如果查询模式还频繁涉及按URL前缀过滤,可以考虑将URL拆分为路径和查询参数两列,或者在建表后使用物化列提取路径前缀,进一步提升过滤效率。
三、典型SQL查询与性能调优实践
CDN日志分析中最常见的需求之一是计算缓存命中率。用传统SQL可能需要两条查询或子查询分别统计命中数和总数,而ClickHouse的条件聚合函数可以在一次扫描中完成:
SELECT
domain,
countIf(cache_status = 'HIT') AS hit_count,
count() AS total_count,
round(hit_count / total_count * 100, 2) AS hit_ratio
FROM cdn_logs
WHERE log_time >= now() - INTERVAL 7 DAY
GROUP BY domain
ORDER BY total_count DESC
LIMIT 20;
这里countIf是ClickHouse内置的聚合函数,它在单次顺序扫描中同时完成条件计数和全量计数,避免了多次扫描明细数据。查询执行时,分区裁剪会让ClickHouse只读取最近7天的数据分区,排序键(domain, log_time)则帮助快速定位每个域名对应的数据块。由于只涉及domain、cache_status和log_time三列,整体扫描量远低于行式数据库读取整行的开销。
另一个高频需求是统计某个域名下流量最大的URL。下面这条SQL可以快速输出Top URL的字节数和请求次数:
SELECT
url,
sum(body_bytes_sent) AS total_bytes,
count() AS request_count
FROM cdn_logs
WHERE log_time >= now() - INTERVAL 1 DAY
AND domain = 'img.ippipp.com'
GROUP BY url
ORDER BY total_bytes DESC
LIMIT 50;
此查询只会读取url、body_bytes_sent、log_time和domain四个列,其他列完全不访问。如果某个域名下的URL数量非常多,GROUP BY url可能会消耗较多内存,可以在分组前使用substring截取路径前若干字符,降低分组基数。ClickHouse还支持SAMPLE子句进行抽样统计,例如在表结构中设置采样键后,通过SAMPLE 0.1可以快速估算流量分布,适合做趋势分析而不必扫描全量明细。
进一步调优时,可以根据实际查询模式组合多种手段:使用物化视图或投影将分钟级聚合结果预先计算,避免重复扫描明细数据;对于状态码这类低基数字段使用LowCardinality;避免在WHERE条件中对列使用函数,尽量让过滤条件命中分区键和排序键;对超长URL字符串使用截断或哈希分桶降低内存压力。这些措施叠加后,自建CDN日志分析的查询延迟可以从分钟级下降到秒级,同时保持较低的硬件成本。
ClickHouse的列式存储从磁盘读取层面降低了CDN日志分析的开销,配合MergeTree的稀疏索引和分区裁剪,能够让自建CDN场景下的日志分析从小时级压缩到秒级。实际落地时,重点在于表结构设计是否匹配真实的日志查询模式,以及SQL是否充分利用了列式扫描和条件聚合的优势。合理规划分区键、排序键和TTL,再结合物化视图等进阶功能,可以构建一套高效稳定的日志分析平台。
ClickHouseCDN日志分析列式存储修改时间:2026-08-30 03:51:51