据我多年做全链路分析系统的经验来看,大部分公司数据体量,用单机版基本足够了。像Clickhouse这种OLAP 实时分析型数据库,在大量数据插入和查询方面都特别猛,一次插入几万条数据跟玩似的。但是更新和删除的时候都比较拉胯了,不过正常情况下,也没人专门更新或者删除一条日志啊,所以中小公司用单机版加个备份脚本就可以了。但是集群有集群的好处,它容错率高啊,在高可用场景下很有必要,今天我们就来介绍一个低成本的clickhouse集群。
我们也做了一套全链路监控产品Webfunny,我们就是采用的这种集群方式,既能满足并发场景,成本也比较低,有兴趣的小伙伴可以了解一下。
一、架构设计
推荐架构(1 分片 2 副本 + 独立 ZooKeeper)
┌─────────────────────────────────────────────┐
│ 应用服务层 │
│ Node.js / 业务服务 │
└──────────────┬──────────────────────────────┘
│ HTTP 8123 / TCP 9000
┌──────────────┴──────────────────────────────┐
│ ClickHouse 集群 │
│ │
│ ch-node1 (8c16g+SSD) ch-node2 (8c16g+SSD)│
│ ClickHouse 主库 ←→ ClickHouse 从库 │
│ 写入优先 读取优先 │
└──────────────┬──────────────────────────────┘
│ 2181/2182/2183
┌──────────────┴──────────────────────────────┐
│ ZooKeeper 集群(独立服务器) │
│ │
│ zk-server (2c4g+SSD) │
│ 实例1: 2181 实例2: 2182 实例3: 2183 │
└─────────────────────────────────────────────┘
为什么 ZooKeeper 要独立部署?
- ZooKeeper 与 ClickHouse 共存会导致资源竞争,CPU 相互影响
- ZooKeeper 对磁盘 IO 延迟极敏感,ClickHouse 的高 IO 会影响 ZooKeeper 响应
- ZooKeeper JVM 进程本身占用约 500MB~2GB 内存,独立部署更可控
硬件要求
| 角色 | 规格 | 磁盘 |
|---|---|---|
| ClickHouse 主库 | 8c16g 起步 | ESSD PL1 500GB+ |
| ClickHouse 从库 | 8c16g 起步 | ESSD PL1 500GB+ |
| ZooKeeper 专用 | 4c8g | ESSD Entry 40GB(系统盘即可) |
磁盘选型说明:ClickHouse 的写入性能对磁盘 IOPS 要求较高,推荐 ESSD PL1(5万 IOPS)。ZooKeeper 只需低延迟顺序写,ESSD Entry(1万 IOPS,0.3ms 延迟)完全够用。
二、ZooKeeper 集群部署(单机 3 实例)
这里采用的一台机器启动3个zookeeper实例,虽然成本比较低,但是如果这个机器挂了,集群就歇菜了,所以这个集群也叫伪集群模式,哈哈。这个模式主要是为了降低成本,又可以增加clickhouse的性能,如果预算充足,就启动三个机器部署zookeeper最稳妥
1. 安装 Java 11
# CentOS/RHEL
yum install -y java-11-openjdk java-11-openjdk-devel
# Ubuntu/Debian
apt-get install -y openjdk-11-jdk
# 验证
java -version
2. 下载并安装 ZooKeeper 3.8.4
cd /opt
# 使用国内镜像下载(速度更快)
wget https://mirrors.aliyun.com/apache/zookeeper/zookeeper-3.8.4/apache-zookeeper-3.8.4-bin.tar.gz
# 解压
tar -zxvf apache-zookeeper-3.8.4-bin.tar.gz
# 创建软链接
ln -s /opt/apache-zookeeper-3.8.4-bin /opt/zookeeper
# 验证
ls /opt/zookeeper/bin/
⚠️ 注意:软链接要在创建配置目录之前完成,否则后续创建的 conf 目录会被软链接覆盖。
3. 创建目录结构
# 数据目录(独立于安装目录,防止被升级覆盖)
mkdir -p /opt/zkdata/node1/{data,log}
mkdir -p /opt/zkdata/node2/{data,log}
mkdir -p /opt/zkdata/node3/{data,log}
# 配置目录(独立于安装目录)
mkdir -p /opt/zkconf/conf1
mkdir -p /opt/zkconf/conf2
mkdir -p /opt/zkconf/conf3
# 统一日志目录
mkdir -p /opt/zkdata/logs
4. 创建配置文件
以 ZooKeeper 服务器 IP 为 172.19.117.126 为例:
实例1(客户端端口 2181):
cat > /opt/zkconf/conf1/zoo.cfg << 'EOF'
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/zkdata/node1/data
dataLogDir=/opt/zkdata/node1/log
clientPort=2181
adminPort=8081
maxClientCnxns=1000
maxSessionTimeout=60000
autopurge.purgeInterval=1
autopurge.snapRetainCount=5
4lw.commands.whitelist=*
server.1=172.19.117.126:2888:3888
server.2=172.19.117.126:2889:3889
server.3=172.19.117.126:2890:3890
EOF
实例2(客户端端口 2182):
cat > /opt/zkconf/conf2/zoo.cfg << 'EOF'
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/zkdata/node2/data
dataLogDir=/opt/zkdata/node2/log
clientPort=2182
adminPort=8082
maxClientCnxns=1000
maxSessionTimeout=60000
autopurge.purgeInterval=1
autopurge.snapRetainCount=5
4lw.commands.whitelist=*
server.1=172.19.117.126:2888:3888
server.2=172.19.117.126:2889:3889
server.3=172.19.117.126:2890:3890
EOF
实例3(客户端端口 2183):
cat > /opt/zkconf/conf3/zoo.cfg << 'EOF'
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/zkdata/node3/data
dataLogDir=/opt/zkdata/node3/log
clientPort=2183
adminPort=8083
maxClientCnxns=1000
maxSessionTimeout=60000
autopurge.purgeInterval=1
autopurge.snapRetainCount=5
4lw.commands.whitelist=*
server.1=172.19.117.126:2888:3888
server.2=172.19.117.126:2889:3889
server.3=172.19.117.126:2890:3890
EOF
5. 创建 myid 文件
echo "1" > /opt/zkdata/node1/data/myid
echo "2" > /opt/zkdata/node2/data/myid
echo "3" > /opt/zkdata/node3/data/myid
# 验证
cat /opt/zkdata/node1/data/myid # 应输出 1
cat /opt/zkdata/node2/data/myid # 应输出 2
cat /opt/zkdata/node3/data/myid # 应输出 3
6. 配置日志轮转(防止日志无限增长)
ZooKeeper 3.8.x 使用 logback 管理日志,默认不限制日志文件大小,在高负载场景下可能导致磁盘被撑满。
cat > /opt/zookeeper/conf/logback.xml << 'EOF'
<?xml version="1.0"?>
<configuration>
<property name="zookeeper.log.dir" value="/opt/zkdata/logs"/>
<property name="zookeeper.log.file" value="zookeeper.log"/>
<appender name="ROLLINGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${zookeeper.log.dir}/${zookeeper.log.file}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${zookeeper.log.dir}/zookeeper.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>5</maxHistory>
<totalSizeCap>500MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="ROLLINGFILE"/>
</root>
</configuration>
EOF
7. 创建 systemd 服务(3 个实例)
for i in 1 2 3; do
cat > /etc/systemd/system/zookeeper${i}.service << EOF
[Unit]
Description=ZooKeeper Instance ${i}
After=network.target
[Service]
Type=forking
Environment="ZOOCFGDIR=/opt/zkconf/conf${i}"
Environment="ZOO_LOG_DIR=/opt/zkdata/logs"
Environment="ZOO_LOG4J_PROP=INFO,ROLLINGFILE"
Environment="JVMFLAGS=-Xms512m -Xmx1g"
ExecStart=/opt/zookeeper/bin/zkServer.sh start /opt/zkconf/conf${i}/zoo.cfg
ExecStop=/opt/zookeeper/bin/zkServer.sh stop /opt/zkconf/conf${i}/zoo.cfg
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
done
systemctl daemon-reload
8. 启动并验证集群
# 启动
systemctl start zookeeper1 zookeeper2 zookeeper3
# 设置开机自启
systemctl enable zookeeper1 zookeeper2 zookeeper3
# 验证集群选主状态
/opt/zookeeper/bin/zkServer.sh status /opt/zkconf/conf1/zoo.cfg
/opt/zookeeper/bin/zkServer.sh status /opt/zkconf/conf2/zoo.cfg
/opt/zookeeper/bin/zkServer.sh status /opt/zkconf/conf3/zoo.cfg
正常输出示例:
Mode: follower
Mode: leader
Mode: follower
三、ClickHouse 集群配置
1. 配置 /etc/hosts(所有 ClickHouse 节点都要执行)
cat >> /etc/hosts << EOF
172.19.117.121 ck1
172.19.117.120 ck2
172.19.117.126 zk-server
EOF
# 锁定文件,防止云服务器重启后被 cloud-init 覆盖
chattr +i /etc/hosts
# 验证锁定
lsattr /etc/hosts
# 输出应包含 ----i--------
2. 配置 ZooKeeper 连接地址
编辑 /etc/clickhouse-server/config.xml,找到 <zookeeper> 段修改为:
<zookeeper>
<node>
<host>172.19.117.126</host>
<port>2181</port>
</node>
<node>
<host>172.19.117.126</host>
<port>2182</port>
</node>
<node>
<host>172.19.117.126</host>
<port>2183</port>
</node>
</zookeeper>
⚠️ 必须使用 IP 地址,不要使用主机名,否则 DNS 解析失败会导致 ClickHouse 无法连接 ZooKeeper。
3. 配置集群拓扑
<remote_servers>
<webfunny_cluster>
<shard>
<replica>
<host>172.19.117.121</host>
<port>9000</port>
<user>your_user</user>
<password>your_password</password>
</replica>
<replica>
<host>172.19.117.120</host>
<port>9000</port>
<user>your_user</user>
<password>your_password</password>
</replica>
</shard>
</webfunny_cluster>
</remote_servers>
<!-- 宏变量,用于 ReplicatedMergeTree 表路径 -->
<macros>
<shard>01</shard>
<!-- 每台机器不同:replica1 / replica2 -->
<replica>replica1</replica>
</macros>
4. 建表语句(ReplicatedMergeTree)
-- 在集群所有节点上执行
CREATE TABLE IF NOT EXISTS MonitorData ON CLUSTER webfunny_cluster
(
happenTime DateTime,
projectId String,
userId String,
-- 其他字段...
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{database}/{shard}/{table}',
'{replica}'
)
PARTITION BY toYYYYMM(happenTime)
ORDER BY (projectId, happenTime)
TTL toDate(happenTime) + INTERVAL 90 DAY;
5. 重启 ClickHouse 并验证
sudo systemctl restart clickhouse-server
sudo systemctl status clickhouse-server
# 验证 ZooKeeper 连接正常(日志里不再出现 Cannot resolve host)
sudo tail -f /var/log/clickhouse-server/clickhouse-server.log | grep -i zookeeper
四、容量规划参考
按照这个配置,每天存5000万 ~ 2亿的日志量,应该是没有问题的,快来试试吧。如有不妥的地方,也欢迎指正。
| 日志量 | 写入速度(峰值) | 推荐架构 | 单节点配置 |
|---|---|---|---|
| < 5000万/天 | < 600行/秒 | 1分片2副本 | 8c16g |
| 5000万~2亿/天 | < 2500行/秒 | 1分片2副本(优化后足够) | 16c32g |
| 2亿~10亿/天 | < 12000行/秒 | 2分片2副本 | 16c32g × 4 |
| > 10亿/天 | > 12000行/秒 | 4+ 分片 | 按需扩展 |