单页导航网站模板,一个简单的网站怎么做,域名有没有被注册哪个网站最好,东莞百度seo在哪里ClickHouse 结合 JuiceFS 一直是一个热门的组合#xff0c;社区中有多篇实践案例。今天的文章来自美国公司 Altinity#xff0c;一家提供 ClickHouse 商业服务的企业#xff0c;作者是 Vitaliy Zakaznikov#xff0c;他尝试了这个组合并公开了过程中使用的代码。原文有两篇… ClickHouse 结合 JuiceFS 一直是一个热门的组合社区中有多篇实践案例。今天的文章来自美国公司 Altinity一家提供 ClickHouse 商业服务的企业作者是 Vitaliy Zakaznikov他尝试了这个组合并公开了过程中使用的代码。原文有两篇文章“Squeezing JuiceFS with ClickHouse (Part 1): Setting Things Up in Kubernetes”、“Squeezing JuiceFS with ClickHouse (Part 2): Bringing The Two Together” JuiceFS 是一个兼容 POSIX 的文件系统能够在 S3 对象存储上运行。作为一个分布式且云原生的文件系统它具备多种功能包括数据一致性、传输与静态加密、BSD 和 POSIX 文件锁以及数据压缩。
所有使用 S3 的应用都需适应其对象存储。对于像 ClickHouse 这样的复杂应用将表数据存储在 S3 上较为困难且原生的 S3 支持还存在一些问题。因此如果用户不希望应用程序直接处理 S3 存储的细节JuiceFS 将是一个非常理想的选择。特别是在用户的基础设施中已经集成了 JuiceFS 和 ClickHouse 的情况下将两者结合使用将是自然而然的选择。
在这篇文章中我们将详细介绍在 Kubernetes 中配置基于 S3 的 JuiceFS 的过程以及如何利用它作为存储介质来存放 ClickHouse MergeTree 表的数据。在一切设置完成后我们探讨使用 JuiceFS 和 ClickHouse 结合时用户所能期望的性能及可能面临的问题。
将 JuiceFS 与 ClickHouse 结合使用的想法已经有一段时间了。早在2021年JuiceFS 就发表了一篇名为《ClickHouse 存算分离架构探索》的博客探讨了将 JuiceFS 用作 ClickHouse MergeTree 表的存储解决方案此外还有更近期的文章《低成本读写分离Jerry 构建的主从 ClickHouse 架构》。
我们打算亲自进行尝试。不过直接管理运行 ClickHouse 和 JuiceFS 的服务器相当复杂。因此我们选择使用 Kubernetes。具体来说我们会结合使用 clickhouse-operator 和 JuiceFS CSI(Container Storage Interface) Driver这将简化我们为集群 PVC 的设置工作即设置 JuiceFS 的 StorageClass。
01 在 Kubernetes 中配置 S3 和 JuiceFS
部署 Kubernetes 集群
对于这次测试我选择了一种经济且对开发者友好的配置。我们将在 Hetzner Cloud上 部署 Kubernetes并使用 Wasabi S3 对象存储。
我将使用 hetzner-k3s 工具快速搭建一个低成本的 Kubernetes 集群。设置过程与 Low-Cost ClickHouse clusters using Hetzner Cloud with Altinity.Cloud Anywhere 文章中描述的相同。
首先下载并安装 hetzner-k3s 工具。
curl -fsSLO https://github.com/janosmiko/hetzner-k3s/releases/latest/download/hetzner-k3s_uname -s_uname -m.deb
sudo dpkg -i hetzner-k3s_uname -s_uname -m.debhetzner-k3s -vhetzner-k3s version v0.1.9Kubernetes 集群配置如下使用位于美国东部US Ashburn, VA的 CPX31 服务器它提供 4 个虚拟 CPU 和 8GB RAM。对于 S3使用位于美国东部 1 区的 Wasabi S3 存储桶。
以下是我的 k3s_cluster.yaml 文件。请使用你的 Hetzner 项目 API 令牌以及公钥和私钥 SSH 密钥。
---
hetzner_token: YOUR HETZNER PROJECT API TOKEN
cluster_name: clickhouse-cloud
kubeconfig_path: kubeconfig
k3s_version: v1.29.4k3s1
public_ssh_key_path: /home/user/.ssh/YOUR PUBLIC SSH KEY.pub
private_ssh_key_path: /home/user/.ssh/YOUR PRIVATE SSH KEY
image: ubuntu-22.04
verify_host_key: false
location: ash
schedule_workloads_on_masters: false
masters:instance_type: cpx31instance_count: 1
worker_node_pools:
- name: clickhouseinstance_type: cpx31instance_count: 3
现在我将使用以下命令创建我的 Kubernetes 集群
hetzner-k3s create-cluster -c k3s_cluster.yaml
在集群设置完成后检查所有节点是否都已启动并正常工作。请记住将 kubectl 指向我们集群的 kubeconfig 文件该文件为集群创建并位于与 k3s_cluster.yaml 相同的目录中。
kubectl --kubeconfig ./kubeconfig get nodesNAME STATUS ROLES AGE VERSION
clickhouse-cloud-cpx31-master1 Ready control-plane,etcd,master 2m32s v1.29.4k3s1
clickhouse-cloud-cpx31-pool-clickhouse-worker1 Ready none 2m20s v1.29.4k3s1
clickhouse-cloud-cpx31-pool-clickhouse-worker2 Ready none 2m21s v1.29.4k3s1
clickhouse-cloud-cpx31-pool-clickhouse-worker3 Ready none 2m22s v1.29.4k3s1安装 JuiceFS CSI Driver
在 Kubernetes 中使用 JuiceFS 的最佳方式是使用 JuiceFS CSI 驱动程序。该驱动程序允许我们通过定义 PVC 来创建 PV ClickHouse 将使用这些卷来存储数据并为在 Kubernetes 上运行的应用程序提供了一种标准的存储暴露方式。我将使用 kubectl 方式来安装这个驱动程序。
kubectl --kubeconfig kubeconfig apply -f https://raw.githubusercontent.com/juicedata/juicefs-csi-driver/master/deploy/k8s.yamlserviceaccount/juicefs-csi-controller-sa created
serviceaccount/juicefs-csi-dashboard-sa created
serviceaccount/juicefs-csi-node-sa created
clusterrole.rbac.authorization.k8s.io/juicefs-csi-dashboard-role created
clusterrole.rbac.authorization.k8s.io/juicefs-csi-external-node-service-role created
clusterrole.rbac.authorization.k8s.io/juicefs-external-provisioner-role created
clusterrolebinding.rbac.authorization.k8s.io/juicefs-csi-dashboard-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/juicefs-csi-node-service-binding created
clusterrolebinding.rbac.authorization.k8s.io/juicefs-csi-provisioner-binding created
service/juicefs-csi-dashboard created
deployment.apps/juicefs-csi-dashboard created
statefulset.apps/juicefs-csi-controller created
daemonset.apps/juicefs-csi-node created
csidriver.storage.k8s.io/csi.juicefs.com created验证安装
kubectl --kubeconfig kubeconfig -n kube-system get pods -l app.kubernetes.io/namejuicefs-csi-driverNAME READY STATUS RESTARTS AGE
juicefs-csi-controller-0 4/4 Running 0 45s
juicefs-csi-controller-1 4/4 Running 0 34s
juicefs-csi-dashboard-58d9c54877-jrphl 1/1 Running 0 45s
juicefs-csi-node-7tsr8 3/3 Running 0 44s
juicefs-csi-node-mmxbk 3/3 Running 0 44s
juicefs-csi-node-njftm 3/3 Running 0 44s
juicefs-csi-node-rnkmz 3/3 Running 0 44s创建 JuiceFS 元数据引擎 Redis 集群
在安装了 JuiceFS CSI 驱动程序后我们需要设置一个元数据存储因为 JuiceFS 使用了数据与元数据分离的架构其中数据是呈现给用户的文件内容元数据则是描述文件及文件系统本身的信息如文件属性、文件系统结构、文件内容到 S3 对象的映射等。元数据存储可以使用不同的数据库来实现。我将创建一个简单的 Redis 集群。关于选择元数据存储的更多信息请参见 JuiceFS 元数据引擎指南。
首先让我们创建一个服务。
---
apiVersion: v1
kind: Service
metadata:name: redis-servicenamespace: redislabels:app: redis
spec:ports:- port: 6379clusterIP: Noneselector:app: redis接下来我们需要创建简单的配置文件这些文件将支持 Redis 主节点和从节点。
---
apiVersion: v1
kind: ConfigMap
metadata:name: redis-confignamespace: redislabels:app: redis
data:master.conf: |maxmemory 1024mbmaxmemory-policy allkeys-lrumaxclients 20000timeout 300appendonly nodbfilename dump.rdbdir /datasecondary.conf: |slaveof redis-0.redis.redis 6379maxmemory 1024mbmaxmemory-policy allkeys-lrumaxclients 20000timeout 300dir /data我将使用 StatefulSet 来定义 Redis 集群。需要注意的是主节点和从节点将有不同的配置其中 Pod 的序号索引用来确定主节点和从节点。在这次测试中我们可以只使用 1 个副本并且只为 /data 和 /etc 文件夹使用 10Gi 的卷。
---
apiVersion: apps/v1
kind: StatefulSet
metadata:name: redisnamespace: redis
spec:serviceName: redis-servicereplicas: 1selector:matchLabels:app: redistemplate:metadata:labels:app: redisspec:initContainers:- name: init-redisimage: redis:7.2.4command:- bash- -c- |set -ex# Generate redis server-id from pod ordinal index.[[ hostname ~ -([0-9])$ ]] || exit 1ordinal${BASH_REMATCH[1]}# Copy appropriate redis config files from config-map to respective directories.if [[ $ordinal -eq 0 ]]; thencp /mnt/master.conf /etc/redis-config.confelsecp /mnt/slave.conf /etc/redis-config.conffivolumeMounts:- name: redis-claimmountPath: /etc- name: config-mapmountPath: /mnt/containers:- name: redisimage: redis:7.2.4ports:- containerPort: 6379name: rediscommand:- redis-server- /etc/redis-config.confvolumeMounts:- name: redis-datamountPath: /data- name: redis-claimmountPath: /etcvolumes:- name: config-mapconfigMap:name: redis-config volumeClaimTemplates:- metadata:name: redis-claimspec:accessModes: [ ReadWriteOnce ]resources:requests:storage: 10Gi- metadata:name: redis-dataspec:accessModes: [ ReadWriteOnce ]resources:requests:storage: 10Gi我还将为其创建一个专用的命名空间
kubectl --kubeconfig kubeconfig create namespace redisnamespace/redis created现在可以创建 Redis 集群了。
kubectl --kubeconfig kubeconfig apply -n redis -f redis_cluster.yamlservice/redis-service created
configmap/redis-config created
statefulset.apps/redis created让我们一起检查一下创建的资源。 kubectl --kubeconfig kubeconfig get configmap -n redisNAME DATA AGE
kube-root-ca.crt 1 3m22s
redis-config 2 57skubectl --kubeconfig kubeconfig get svc -n redisNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
redis-service ClusterIP None none 6379/TCP 88skubectl --kubeconfig kubeconfig get statefulset -n redis
NAME READY AGE
redis 1/1 96skubectl --kubeconfig kubeconfig get pods -n redisNAME READY STATUS RESTARTS AGE
redis-0 1/1 Running 0 115skubectl --kubeconfig kubeconfig get pvc -n redisNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
redis-claim-redis-0 Bound pvc-0d4c5dba-67f0-43df-a8e4-d44575e0bc1b 10Gi RWO hcloud-volumes unset 3m57s
redis-data-redis-0 Bound pvc-fc763f30-397f-455f-bd7e-c49db7a8d36e 10Gi RWO hcloud-volumes unset 3m57s我们可以通过直接访问主节点进行检查。
kubectl --kubeconfig kubeconfig exec -it redis-0 -c redis -n redis -- /bin/bashbash-5.2# redis-cli
127.0.0.1:6379 info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:5c09f7cf01eadafe8120d2c0862cb6c69b43c438
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0我们的元数据引擎已经就绪
创建 JuiceFS 数据存储
之后需要为 JuiceFS 创建一个数据存储。首先我们需要定义一些秘钥这些秘钥将用来指定我们的 S3 桶的凭证然后再定义数据存储。 这是我的 juicefs-sc.yaml 文件。你需要指定你的桶、访问和秘钥。
---
apiVersion: v1
kind: Secret
metadata:name: juicefs-secretnamespace: defaultlabels:juicefs.com/validate-secret: true
type: Opaque
stringData:name: ten-pb-fsmetaurl: redis://redis-service.redis:6379/1storage: s3bucket: https://BUCKET.s3.REGION.wasabisys.comaccess-key: ACCESS_KEYsecret-key: SECRET_KEY
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:name: juicefs
provisioner: csi.juicefs.com
parameters:csi.storage.k8s.io/provisioner-secret-name: juicefs-secretcsi.storage.k8s.io/provisioner-secret-namespace: defaultcsi.storage.k8s.io/node-publish-secret-name: juicefs-secretcsi.storage.k8s.io/node-publish-secret-namespace: default按照以下步骤创建数据存储。
kubectl --kubeconfig kubeconfig apply -f juicefs-sc.yamlsecret/juicefs-secret created
storageclass.storage.k8s.io/juicefs created现在检查一下数据存储是否已经准备好了。
测试 JuiceFS
在为 JuiceFS 定义了一个数据存储后我们现在可以通过定义一个 PVC 来测试它然后我们可以在我们的测试应用程序的容器中使用它。 这是我的 juicefs-pvc-sc.yaml 文件它使用了新的 juicefs StorageClass。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:name: juicefs-pvcnamespace: default
spec:accessModes:- ReadWriteManyvolumeMode: FilesystemstorageClassName: juicefsresources:requests:storage: 10P让我们应用它。
kubectl --kubeconfig kubeconfig apply -f juicefs-pvc-sc.yamlpersistentvolumeclaim/juicefs-pvc created通过使用以下的 juicefs-app.yaml 文件创建一个简单的 pod 来进行测试。
apiVersion: v1
kind: Pod
metadata:name: juicefs-appnamespace: default
spec:containers:- name: juicefs-app image: centoscommand:- sleep- infinityvolumeMounts:- mountPath: /dataname: datavolumes:- name: datapersistentVolumeClaim:claimName: juicefs-pvc
启动 pod 并检查 /data 是否成功挂载。kubectl --kubeconfig kubeconfig apply -f juicefs-app.yamlpod/juicefs-app createdkubectl --kubeconfig kubeconfig exec -it -n default juicefs-app -- /bin/bash[rootjuicefs-app /]# ls /data
[rootjuicefs-app /]#我们的 pod 中已经挂载了 /data。JuiceFS 已经可以使用了
运行 JuiceFS 提供的基准测试
将 JuiceFS 客户端安装到容器中然后运行基准测试以检查文件系统是否按预期工作。
curl -sSL https://d.juicefs.com/install | sh -运行只使用1个线程的基准测试。
juicefs bench -p 1 /dataWrite big blocks: 1024/1024 [] 96.9/s used: 10.566310616sRead big blocks: 1024/1024 [] 39.9/s used: 25.661374375s
Write small blocks: 100/100 [] 11.0/s used: 9.059618651s Read small blocks: 100/100 [] 755.4/s used: 132.409568ms Stat small files: 100/100 [] 2566.2/s used: 39.04321ms
Benchmark finished!
BlockSize: 1 MiB, BigFileSize: 1024 MiB, SmallFileSize: 128 KiB, SmallFileCount: 100, NumThreads: 1
-------------------------------------------------
| ITEM | VALUE | COST |
-------------------------------------------------
| Write big file | 96.91 MiB/s | 10.57 s/file |
| Read big file | 39.91 MiB/s | 25.66 s/file |
| Write small file | 11.0 files/s | 90.59 ms/file |
| Read small file | 758.5 files/s | 1.32 ms/file |
| Stat file | 2604.5 files/s | 0.38 ms/file |
-------------------------------------------------运行 4 线程的基准测试。
juicefs bench -p 4 /dataWrite big blocks: 4096/4096 [] 267.1/s used: 15.333303679sRead big blocks: 4096/4096 [] 112.3/s used: 36.469782933s
Write small blocks: 400/400 [] 16.5/s used: 24.257067969sRead small blocks: 400/400 [] 2392.4/s used: 167.231047ms Stat small files: 400/400 [] 10742.0/s used: 37.281491ms
Benchmark finished!
BlockSize: 1 MiB, BigFileSize: 1024 MiB, SmallFileSize: 128 KiB, SmallFileCount: 100, NumThreads: 4
---------------------------------------------------
| ITEM | VALUE | COST |
---------------------------------------------------
| Write big file | 267.14 MiB/s | 15.33 s/file |
| Read big file | 112.32 MiB/s | 36.47 s/file |
| Write small file | 16.5 files/s | 242.56 ms/file |
| Read small file | 2401.8 files/s | 1.67 ms/file |
| Stat file | 10901.5 files/s | 0.37 ms/file |
---------------------------------------------------让我们将并行度提高到 4 倍并使用 16 个线程重新运行基准测试。
juicefs bench -p 16 /dataWrite big blocks: 16384/16384 [] 307.2/s used: 53.335911256sRead big blocks: 16384/16384 [] 331.4/s used: 49.440177355s
Write small blocks: 1600/1600 [] 48.9/s used: 32.723927882sRead small blocks: 1600/1600 [] 3181.2/s used: 503.016108ms Stat small files: 1600/1600 [] 9822.4/s used: 162.940025ms
Benchmark finished!
BlockSize: 1 MiB, BigFileSize: 1024 MiB, SmallFileSize: 128 KiB, SmallFileCount: 100, NumThreads: 16
--------------------------------------------------
| ITEM | VALUE | COST |
--------------------------------------------------
| Write big file | 307.19 MiB/s | 53.34 s/file |
| Read big file | 331.39 MiB/s | 49.44 s/file |
| Write small file | 48.9 files/s | 327.23 ms/file |
| Read small file | 3184.9 files/s | 5.02 ms/file |
| Stat file | 9852.3 files/s | 1.62 ms/file |
--------------------------------------------------基准测试显示JuiceFS 文件系统正常工作并且随着工作线程数的增加写入和读取速度也有所提升。
02 JuiceFS ClickHouse MergeTree
我们现在准备将 JuiceFS 与 ClickHouse 结合使用。我们将使用 Altinity 的 clickhouse-operator 将 JuiceFS 与 ClickHouse 结合起来。我们将研究如何配置并使用 JuiceFS 磁盘用于 MergeTree 表并将其简单的写入和读取性能与 ClickHouse 标准的非缓存和缓存 S3 磁盘进行比较同时探讨可能出现的问题。
创建 ClickHouse 集群
在 Kubernetes 环境中创建 ClickHouse 集群之前我们需要安装 clickhouse-operator。安装 clickhouse-operator 很简单只需运行以下命令。我使用的版本是 0.23.5。
kubectl --kubeconfig kubeconfig apply -f https://raw.githubusercontent.com/Altinity/clickhouse-operator/release-0.23.5/deploy/operator/clickhouse-operator-install-bundle.yaml我将使用下方的 clickhouse_cluster.yaml 文件来定义 ClickHouse 集群。它配置了一个包含所有磁盘和卷的 storage.xml 配置文件。更多细节可查阅 ClickHouse 文档中关于 External Disks for Storing Data 的部分。
鉴于我们将 JuiceFS 作为一个标准文件夹挂载我们定义 JuiceFS 磁盘为本地磁盘并将数据存放在 juicefs-data 文件夹中。
clickhousestorage_configurationdisksjuicefstypelocal/typepath/juicefs-disk/juicefs-data//path/juicefs/disks/storage_configuration
/clickhouse这个磁盘对应于下面定义的卷挂载路径 /juicefs-disk/。
volumeMounts:- name: juicefs-storage-vc-templatemountPath: /juicefs-diskjuicefs-storage-vc-template 是一个卷声明模板它使用了我们为JuiceFS创建的 juicefs StorageClass。
volumeClaimTemplates:- name: juicefs-storage-vc-templatespec:accessModes:- ReadWriteOncevolumeMode: FilesystemstorageClassName: juicefsresources:requests:storage: 10P这是完整的 clickhouse_cluster.yaml 定义。
apiVersion: clickhouse.altinity.com/v1
kind: ClickHouseInstallation
metadata:name: ch-juicefs
spec:configuration:files:storage.xml: |clickhousestorage_configurationdiskss3types3/typeendpointhttps://BUCKET.s3.REGION.wasabisys.com/s3-data/{replica}//endpointaccess_key_idACCESS_KEY/access_key_idsecret_access_keySECRET_KEY/secret_access_key/s3s3_cachetypecache/typedisks3/diskpath/s3_cache//pathmax_size10Gi/max_size/s3_cachejuicefstypelocal/typepath/juicefs-disk/juicefs-data//path/juicefs/diskspoliciess3volumesmaindisks3/disk/main/volumes/s3s3_cachevolumesmaindisks3_cache/disk/main/volumes/s3_cachejuicefsvolumesmaindiskjuicefs/disk/main/volumes/juicefs/policies/storage_configuration/clickhouseclusters:- name: ch-juicefstemplates:podTemplate: pod-template-with-volumeslayout:shardsCount: 1replicasCount: 1templates:podTemplates:- name: pod-template-with-volumesspec:containers:- name: clickhouseimage: altinity/clickhouse-server:23.8.11.29.altinitystablevolumeMounts:- name: data-storage-vc-templatemountPath: /var/lib/clickhouse- name: log-storage-vc-templatemountPath: /var/log/clickhouse-server- name: juicefs-storage-vc-templatemountPath: /juicefs-diskvolumeClaimTemplates:- name: juicefs-storage-vc-templatespec:accessModes:- ReadWriteOncevolumeMode: FilesystemstorageClassName: juicefsresources:requests:storage: 10P- name: data-storage-vc-templatespec:accessModes:- ReadWriteOnceresources:requests:storage: 100Gi- name: log-storage-vc-templatespec:accessModes:- ReadWriteOnceresources:requests:storage: 10Gi为了简化操作我选择了一个只有一个分片和副本的集群。现在让我们应用 clickhouse_cluster.yaml 来创建这个集群。
kubectl --kubeconfig kubeconfig apply -f clickhouse_cluster.yamlclickhouseinstallation.clickhouse.altinity.com/ch-juicefs created创建集群后我们可以进入 ClickHouse 服务器的 pod 并启动 clickhouse-client 来检查可用的磁盘。
kubectl --kubeconfig kubeconfig exec -it -n default chi-ch-juicefs-ch-juicefs-0-0-0 -- /bin/bash然后可以启动 clickhouse-client。
Clickhouse-client在 clickhouse-client 中可以使用以下查询来检查可用的磁盘。
SELECT * FROM system.disks┌─name─────┬─path──────────────────────────┬───────────free_space─┬──────────total_space─┬─────unreserved_space─┬─keep_free_space─┬─type──┬─is_encrypted─┬─is_read_only─┬─is_write_once─┬─is_remote─┬─is_broken─┬─cache_path─┐
│ default │ /var/lib/clickhouse/ │ 64848461824 │ 105089261568 │ 64848461824 │ 0 │ local │ 0 │ 0 │ 0 │ 0 │ 0 │ │
│ juicefs │ /juicefs-disk/juicefs-data/ │ 1125830005723136 │ 1125830005731328 │ 1125830005723136 │ 0 │ local │ 0 │ 0 │ 0 │ 0 │ 0 │ │
│ s3 │ /var/lib/clickhouse/disks/s3/ │ 18446744073709551615 │ 18446744073709551615 │ 18446744073709551615 │ 0 │ s3 │ 0 │ 0 │ 0 │ 1 │ 0 │ │
│ s3_cache │ /var/lib/clickhouse/disks/s3/ │ 18446744073709551615 │ 18446744073709551615 │ 18446744073709551615 │ 0 │ s3 │ 0 │ 0 │ 0 │ 1 │ 0 │ /s3_cache/ │
└──────────┴───────────────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┴─────────────────┴───────┴──────────────┴──────────────┴───────────────┴───────────┴───────────┴────────────┘现在有了四种磁盘类型默认、juicefs、s3 和 s3_cache 我们可以创建不同的 MergeTree 表来检验每种磁盘类型的性能。
加载测试数据集
对于基本性能测试我将使用包含13亿条记录的 NYC taxi rides dataset 该数据集可以通过我们的公共 S3 桶获得。
首先将整个数据集复制到 chi-ch-juicefs-ch-juicefs-0-0-0 pod 中的 /var/lib/clickhouse/user_files 文件夹这样稍后我就可以使用 file() 表函数将其加载到使用不同磁盘的表中。
cd /var/lib/clickhouse/user_files
mkdir -p datasets/tripdata
cd datasets/tripdata
aws --no-sign-request s3 cp --recursive s3://altinity-clickhouse-data/nyc_taxi_rides/data/tripdata .数据集加载完成后我们可以开始创建 MergeTree 表了。
创建 MergeTree
我将创建三个不同的表每个磁盘一个分别命名为 tripdata_juicefs、tripdata_s3、tripdata_s3_cache。下面是 CREATE TABLE 查询的模板其中 可以是 juicefs、s3 或 s3_cache。 CREATE TABLE IF NOT EXISTS tripdata_NAME (pickup_date Date DEFAULT toDate(pickup_datetime) CODEC(Delta, LZ4),id UInt64,vendor_id String,pickup_datetime DateTime CODEC(Delta, LZ4),dropoff_datetime DateTime,passenger_count UInt8,trip_distance Float32,pickup_longitude Float32,pickup_latitude Float32,rate_code_id String,store_and_fwd_flag String,dropoff_longitude Float32,dropoff_latitude Float32,payment_type LowCardinality(String),fare_amount Float32,extra String,mta_tax Float32,tip_amount Float32,tolls_amount Float32,improvement_surcharge Float32,total_amount Float32,pickup_location_id UInt16,dropoff_location_id UInt16,junk1 String,junk2 String)
ENGINE MergeTree
PARTITION BY toYYYYMM(pickup_date)
ORDER BY (vendor_id, pickup_location_id, pickup_datetime)
SETTINGS storage_policyNAME;这里有一些关于表定义的简短示例。
CREATE TABLE IF NOT EXISTS tripdata_juicefs (...
SETTINGS storage_policyjuicefs;CREATE TABLE IF NOT EXISTS tripdata_s3 (...
SETTINGS storage_policys3;CREATE TABLE IF NOT EXISTS tripdata_s3_cache (...
SETTINGS storage_policys3_cache;检查表是否已成功创建。
SHOW TABLES┌─name──────────────┐
│ tripdata_juicefs │
| tripdata_s3 |
│ tripdata_s3_cache │
└───────────────────┘写性能
数据集加载完毕数据表创建完成后我们就可以开始比较 JuiceFS 与标准无缓存 S3 以及有缓存 S3 磁盘的性能差异了。
让我们将数据集插入到 tripdata_juicefs 表中。
INSERT INTO tripdata_juicefs
SELECT * FROM file(datasets/tripdata/data-*.csv.gz,
CSVWithNames,
pickup_date Date, id UInt64, vendor_id String, tpep_pickup_datetime DateTime, tpep_dropoff_datetime DateTime, passenger_count UInt8, trip_distance Float32, pickup_longitude Float32, pickup_latitude Float32, rate_code_id String, store_and_fwd_flag String, dropoff_longitude Float32, dropoff_latitude Float32, payment_type LowCardinality(String), fare_amount Float32, extra String, mta_tax Float32, tip_amount Float32, tolls_amount Float32, improvement_surcharge Float32, total_amount Float32, pickup_location_id UInt16, dropoff_location_id UInt16, junk1 String, junk2 String,
gzip)
settings max_threads8, max_insert_threads8, input_format_parallel_parsing0;0 rows in set. Elapsed: 1159.444 sec. Processed 1.31 billion rows, 39.98 GB (1.13 million rows/s., 34.48 MB/s.)现在让我们 对 tripdata_s3 表做相同的操作.
INSERT INTO tripdata_s3
SELECT * FROM file(datasets/tripdata/data-*.csv.gz,
CSVWithNames,
pickup_date Date, id UInt64, vendor_id String, tpep_pickup_datetime DateTime, tpep_dropoff_datetime DateTime, passenger_count UInt8, trip_distance Float32, pickup_longitude Float32, pickup_latitude Float32, rate_code_id String, store_and_fwd_flag String, dropoff_longitude Float32, dropoff_latitude Float32, payment_type LowCardinality(String), fare_amount Float32, extra String, mta_tax Float32, tip_amount Float32, tolls_amount Float32, improvement_surcharge Float32, total_amount Float32, pickup_location_id UInt16, dropoff_location_id UInt16, junk1 String, junk2 String,
gzip)
settings max_threads8, max_insert_threads8, input_format_parallel_parsing0;0 rows in set. Elapsed: 1098.654 sec. Processed 1.31 billion rows, 39.98 GB (1.19 million rows/s., 36.39 MB/s.)最后我们完成 tripdata_s3_cache 表的设置。
INSERT INTO tripdata_s3_cache
SELECT * FROM file(datasets/tripdata/data-*.csv.gz,
CSVWithNames,
pickup_date Date, id UInt64, vendor_id String, tpep_pickup_datetime DateTime, tpep_dropoff_datetime DateTime, passenger_count UInt8, trip_distance Float32, pickup_longitude Float32, pickup_latitude Float32, rate_code_id String, store_and_fwd_flag String, dropoff_longitude Float32, dropoff_latitude Float32, payment_type LowCardinality(String), fare_amount Float32, extra String, mta_tax Float32, tip_amount Float32, tolls_amount Float32, improvement_surcharge Float32, total_amount Float32, pickup_location_id UInt16, dropoff_location_id UInt16, junk1 String, junk2 String,
gzip)
settings max_threads8, max_insert_threads8, input_format_parallel_parsing0;0 rows in set. Elapsed: 1090.200 sec. Processed 1.31 billion rows, 39.98 GB (1.20 million rows/s., 36.67 MB/s.)让我们完成写入性能的数据图表。 如图所示在我们特定的测试环境中写入 JuiceFS 磁盘的速度略慢于写入 S3 以及 S3 缓存磁盘但差距并不显著。我们达到了每秒 113 万行的写入速度。接下来让我们看看读取性能。 读性能 我们可以通过选择测试表中的所有行来读取整个数据集从而检查读取性能。考虑到某些磁盘会使用缓存我们将对每个磁盘连续执行五次查询。
让我们从 JuiceFS 开始读取 tripdata_juicefs 表中的所有数据。
SELECT * FROM tripdata_juicefs FORMAT Null SETTINGS max_threads 8, max_insert_threads 8, input_format_parallel_parsing 0;我获得了以下结果:
0 rows in set. Elapsed: 172.750 sec. Processed 1.31 billion rows, 114.15 GB (7.59 million rows/s., 660.76 MB/s.)
Peak memory usage: 103.08 MiB.
0 rows in set. Elapsed: 94.315 sec. Processed 1.31 billion rows, 114.15 GB (13.90 million rows/s., 1.21 GB/s.)
Peak memory usage: 153.77 MiB.
0 rows in set. Elapsed: 76.713 sec. Processed 1.31 billion rows, 114.15 GB (17.09 million rows/s., 1.49 GB/s.)
Peak memory usage: 151.27 MiB.
0 rows in set. Elapsed: 73.887 sec. Processed 1.31 billion rows, 114.15 GB (17.74 million rows/s., 1.54 GB/s.)
Peak memory usage: 149.49 MiB.
0 rows in set. Elapsed: 74.713 sec. Processed 1.31 billion rows, 114.15 GB (17.55 million rows/s., 1.53 GB/s.)
Peak memory usage: 151.51 MiB.从上述 JuiceFS 的数据中我们可以看出JuiceFS 确实能够缓存数据以便于后续读取时提供更快的访问速度达到了最高读取速度1.53 GB/s对于我们的数据集而言是每秒 1755 万行。JuiceFS 缓存的默认设置是 100 GB并使用 2-random 策略进行数据块的淘汰。
现在让我们对 tripdata_s3 表执行相同的操作。
SELECT * FROM tripdata_s3 FORMAT Null SETTINGS max_threads 8, max_insert_threads 8, input_format_parallel_parsing 0;以下是我的结果
0 rows in set. Elapsed: 256.445 sec. Processed 1.31 billion rows, 114.15 GB (5.11 million rows/s., 445.11 MB/s.)
Peak memory usage: 220.41 MiB.
0 rows in set. Elapsed: 176.409 sec. Processed 1.31 billion rows, 114.15 GB (7.43 million rows/s., 647.05 MB/s.)
Peak memory usage: 280.11 MiB.
0 rows in set. Elapsed: 195.219 sec. Processed 1.31 billion rows, 114.15 GB (6.72 million rows/s., 584.71 MB/s.)
Peak memory usage: 282.05 MiB.
0 rows in set. Elapsed: 252.489 sec. Processed 1.31 billion rows, 114.15 GB (5.19 million rows/s., 452.08 MB/s.)
Peak memory usage: 281.88 MiB.
0 rows in set. Elapsed: 238.748 sec. Processed 1.31 billion rows, 114.15 GB (5.49 million rows/s., 478.10 MB/s.)
Peak memory usage: 279.51 MiB.S3 磁盘不使用任何缓存因此每次查询都需要通过 S3 API 调用来读取整个数据集。从读取速度的不一致性可以看出这一点速度范围从 445 MB/s 到 647 MB/s或者是 511 万到7 43 万行/秒。由于 JuiceFS 默认使用缓存因此它的速度比无缓存的 S3 磁盘要快。
最后让我们从 tripdata_s3_cache 中读取所有数据。
SELECT * FROM tripdata_s3_cache FORMAT Null SETTINGS max_threads 8, max_insert_threads 8, input_format_parallel_parsing 0;这是我获得的结果
0 rows in set. Elapsed: 252.166 sec. Processed 1.31 billion rows, 114.15 GB (5.20 million rows/s., 452.66 MB/s.)
Peak memory usage: 4.12 GiB.
0 rows in set. Elapsed: 155.952 sec. Processed 1.31 billion rows, 114.15 GB (8.41 million rows/s., 731.93 MB/s.)
Peak memory usage: 7.94 GiB.
0 rows in set. Elapsed: 144.535 sec. Processed 1.31 billion rows, 114.15 GB (9.07 million rows/s., 789.75 MB/s.)
Peak memory usage: 8.40 GiB.
0 rows in set. Elapsed: 141.699 sec. Processed 1.31 billion rows, 114.15 GB (9.25 million rows/s., 805.55 MB/s.)
Peak memory usage: 8.58 GiB.
0 rows in set. Elapsed: 142.764 sec. Processed 1.31 billion rows, 114.15 GB (9.18 million rows/s., 799.54 MB/s.)
Peak memory usage: 8.53 GiB.与无缓存的 S3 磁盘相比s3_cache 磁盘提高了性能。我们达到了 805MB/s 的速度相当于每秒处理 925 万行。然而这仍然慢于默认设置下的 JuiceFS。
现在让我们再试一次使用 s3_cache 磁盘但这次我们将缓存大小设置为 50GB而不是最初的 10GB。我们通过修改 s3_cache 磁盘的定义来实现这一点。
s3_cachetypecache/typedisks3/diskpath/s3_cache//pathmax_size50Gi/max_size/s3_cache在修改了 clickhouse_cluster.yaml 并重新应用之后这将重启容器和服务器接着我们可以执行 SELECT 查询。
SELECT * FROM tripdata_s3_cache FORMAT Null SETTINGS max_threads 8, max_insert_threads 8, input_format_parallel_parsing 0;当缓存设置为 50GB 时结果如下
0 rows in set. Elapsed: 643.662 sec. Processed 1.31 billion rows, 114.15 GB (2.04 million rows/s., 177.34 MB/s.)
Peak memory usage: 4.23 GiB.
0 rows in set. Elapsed: 51.423 sec. Processed 1.31 billion rows, 114.15 GB (25.49 million rows/s., 2.22 GB/s.)
Peak memory usage: 417.64 MiB.
0 rows in set. Elapsed: 51.678 sec. Processed 1.31 billion rows, 114.15 GB (25.37 million rows/s., 2.21 GB/s.)
Peak memory usage: 416.17 MiB.
0 rows in set. Elapsed: 50.358 sec. Processed 1.31 billion rows, 114.15 GB (26.03 million rows/s., 2.27 GB/s.)
Peak memory usage: 418.08 MiB.
0 rows in set. Elapsed: 51.055 sec. Processed 1.31 billion rows, 114.15 GB (25.68 million rows/s., 2.24 GB/s.)
Peak memory usage: 410.55 MiB.在比较不同缓存大小的 S3 磁盘时我们发现当缓存大小为 50GB 时整个数据集可以被缓存其读取速度比 10GB 缓存快了将近 1GB/s。对于 S3 磁盘而言选择更大的缓存显然是更理想的选择。在我们的测试中使用 50GB 的缓存比使用 JuiceFS 的默认设置具有更快的读取性能。 编者注作者使用的是默认设置调优 JuiceFS 挂载参数后性能可能会进一步提升。 现在我们可以比较我们测试过的所有磁盘的整体平均读取性能。 如图所示JuiceFS 在默认设置下表现相当不错。它的速度快于没有缓存的 S3 磁盘和 10GB 缓存的 S3 磁盘但慢于 50GB 缓存的 S3 磁盘。
潜在问题
虽然我们已经实现了JuiceFS 与 ClickHouse 的结合但这是一个测试环境在生产环境中使用它仍然面临挑战。以下是一些可能出现的问题
管理 JuiceFS 的元数据存储增加了复杂性。在本地磁盘的路径中不展开宏Marcros。因此如果你想在不同的路径上为每个副本保留数据以便在移除副本时容易清理孤立数据每个副本必须有一个自定义的存储配置文件。ClickHouse 不支持本地磁盘的零拷贝复制这意味着每个副本都会创建一个重复的副本。从 23.5 版本开始ClickHouse 不再支持本地类型磁盘的缓存因此你只能依赖 JuiceFS 的缓存。使用 JuiceFS 和 ClickHouse 的整体可靠性需要进行测试。
上述五点中的三点与 ClickHouse 相关可进一步在 ClickHouse 中改进例如为本地磁盘的路径支持宏展开为已实现复制的磁盘提供零拷贝复制的通用支持以及恢复对 ClickHouse 作为本地磁盘实际上是通过分布式文件系统挂载的磁盘的缓存支持。
03 结论
目前我们对将 JuiceFS 与 ClickHouse 结合使用的探索暂告一段落。在 Kubernetes 中部署JuiceFS 虽然简单但部署独立的元数据存储增加了复杂性。然而一旦一切就绪使用clickhouse-operator 来管理 CSI 驱动非常简单直接。由于 JuiceFS 磁盘可以当作本地磁盘使用ClickHouse 的配置也非常简单。磁盘的整体性能很好我们没有进行性能优化仅使用了默认设置。
将 S3 存储作为与 POSIX 兼容的文件系统使用的想法非常吸引人因为它能让任何应用程序无缝集成 S3 存储。但是没有元数据存储S3 上的数据将无法被访问这使得元数据存储成为了基础设施的关键组成部分。如果我们没有元数据存储那么所有的数据也将丢失。但是在 S3 上使用 ClickHouse 时也需要依赖本地元数据来映射 S3 中的对象因此元数据存储必不可缺。使用 JuiceFS 和 ClickHouse 的整体可靠性还需要进一步验证我们需要找到应对潜在问题的办法。我们已经证明了集成 ClickHouse 与 JuiceFS 的可行性JuiceFS 非常值得进一步探索。
希望这篇内容能够对你有一些帮助如果有其他疑问欢迎加入 JuiceFS 社区与大家共同交流。