如何在 Kubernetes 中使用 ClickHouse 和 JuiceFS

avatar
作者
筋斗云
阅读量:0

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`.deb"   hetzner-k3s -v   hetzner-k3s version v0.1.9 

Kubernetes 集群配置如下:使用位于美国东部(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.4+k3s1 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: cpx31   instance_count: 1 worker_node_pools: - name: clickhouse   instance_type: cpx31   instance_count: 3  

现在,我将使用以下命令创建我的 Kubernetes 集群:

hetzner-k3s create-cluster -c k3s_cluster.yaml  

在集群设置完成后,检查所有节点是否都已启动并正常工作。请记住将 kubectl 指向我们集群的 kubeconfig 文件,该文件为集群创建,并位于与 k3s_cluster.yaml 相同的目录中。

kubectl --kubeconfig ./kubeconfig get nodes   NAME                                             STATUS   ROLES                       AGE     VERSION clickhouse-cloud-cpx31-master1                   Ready    control-plane,etcd,master   2m32s   v1.29.4+k3s1 clickhouse-cloud-cpx31-pool-clickhouse-worker1   Ready    <none>                      2m20s   v1.29.4+k3s1 clickhouse-cloud-cpx31-pool-clickhouse-worker2   Ready    <none>                      2m21s   v1.29.4+k3s1 clickhouse-cloud-cpx31-pool-clickhouse-worker3   Ready    <none>                      2m22s   v1.29.4+k3s1 

安装 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.yaml 
serviceaccount/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/name=juicefs-csi-driver 
NAME                                     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-service   namespace: redis   labels:     app: redis spec:   ports:     - port: 6379   clusterIP: None   selector:     app: redis 

接下来,我们需要创建简单的配置文件,这些文件将支持 Redis 主节点和从节点。

--- apiVersion: v1 kind: ConfigMap metadata:   name: redis-config   namespace: redis   labels:     app: redis data:   master.conf: |     maxmemory 1024mb     maxmemory-policy allkeys-lru     maxclients 20000     timeout 300     appendonly no     dbfilename dump.rdb     dir /data   secondary.conf: |     slaveof redis-0.redis.redis 6379     maxmemory 1024mb     maxmemory-policy allkeys-lru     maxclients 20000     timeout 300     dir /data 

我将使用 StatefulSet 来定义 Redis 集群。需要注意的是,主节点和从节点将有不同的配置,其中 Pod 的序号索引用来确定主节点和从节点。在这次测试中,我们可以只使用 1 个副本,并且只为 /data 和 /etc 文件夹使用 10Gi 的卷。

--- apiVersion: apps/v1 kind: StatefulSet metadata:   name: redis   namespace: redis spec:   serviceName: "redis-service"   replicas: 1   selector:     matchLabels:       app: redis   template:     metadata:       labels:         app: redis     spec:       initContainers:       - name: init-redis         image: redis:7.2.4         command:         - bash         - "-c"         - |           set -ex           # Generate redis server-id from pod ordinal index.           [[ `hostname` =~ -([0-9]+)$ ]] || exit 1           ordinal=${BASH_REMATCH[1]}           # Copy appropriate redis config files from config-map to respective directories.           if [[ $ordinal -eq 0 ]]; then             cp /mnt/master.conf /etc/redis-config.conf           else             cp /mnt/slave.conf /etc/redis-config.conf           fi         volumeMounts:         - name: redis-claim           mountPath: /etc         - name: config-map           mountPath: /mnt/       containers:       - name: redis         image: redis:7.2.4         ports:         - containerPort: 6379           name: redis         command:           - redis-server           - "/etc/redis-config.conf"         volumeMounts:         - name: redis-data           mountPath: /data         - name: redis-claim           mountPath: /etc       volumes:       - name: config-map         configMap:           name: redis-config                     volumeClaimTemplates:   - metadata:       name: redis-claim     spec:       accessModes: [ "ReadWriteOnce" ]       resources:         requests:           storage: 10Gi   - metadata:       name: redis-data     spec:       accessModes: [ "ReadWriteOnce" ]       resources:         requests:           storage: 10Gi 

我还将为其创建一个专用的命名空间:

kubectl --kubeconfig kubeconfig create namespace redis   namespace/redis created 

现在,可以创建 Redis 集群了。

kubectl --kubeconfig kubeconfig apply -n redis -f redis_cluster.yaml   service/redis-service created configmap/redis-config created statefulset.apps/redis created 

让我们一起检查一下创建的资源。

 kubectl --kubeconfig kubeconfig get configmap -n redis  NAME               DATA   AGE kube-root-ca.crt   1      3m22s redis-config       2      57s  kubectl --kubeconfig kubeconfig get svc -n redis  NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE redis-service   ClusterIP   None         <none>        6379/TCP   88s  kubectl --kubeconfig kubeconfig get statefulset -n redis NAME    READY   AGE redis   1/1     96s  kubectl --kubeconfig kubeconfig get pods -n redis  NAME      READY   STATUS    RESTARTS   AGE redis-0   1/1     Running   0          115s  kubectl --kubeconfig kubeconfig get pvc -n redis  NAME                  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/bash 
bash-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-secret   namespace: default   labels:     juicefs.com/validate-secret: "true" type: Opaque stringData:   name: ten-pb-fs   metaurl: redis://redis-service.redis:6379/1   storage: s3   bucket: https://<BUCKET>.s3.<REGION>.wasabisys.com   access-key: <ACCESS_KEY>   secret-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-secret   csi.storage.k8s.io/provisioner-secret-namespace: default   csi.storage.k8s.io/node-publish-secret-name: juicefs-secret   csi.storage.k8s.io/node-publish-secret-namespace: default 

按照以下步骤创建数据存储。

kubectl --kubeconfig kubeconfig apply -f juicefs-sc.yaml  secret/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-pvc   namespace: default spec:   accessModes:     - ReadWriteMany   volumeMode: Filesystem   storageClassName: juicefs   resources:     requests:       storage: 10P 

让我们应用它。

kubectl --kubeconfig kubeconfig apply -f juicefs-pvc-sc.yaml   persistentvolumeclaim/juicefs-pvc created 

通过使用以下的 juicefs-app.yaml 文件创建一个简单的 pod 来进行测试。

apiVersion: v1 kind: Pod metadata:   name: juicefs-app   namespace: default spec:   containers:     - name: juicefs-app        image: centos       command:       - sleep       - "infinity"       volumeMounts:       - mountPath: /data         name: data   volumes:     - name: data       persistentVolumeClaim:         claimName: juicefs-pvc 启动 pod 并检查 /data 是否成功挂载。  kubectl --kubeconfig kubeconfig apply -f juicefs-app.yaml  pod/juicefs-app created  kubectl --kubeconfig kubeconfig exec -it -n default juicefs-app -- /bin/bash  [root@juicefs-app /]# ls /data [root@juicefs-app /]# 

我们的 pod 中已经挂载了 /data。JuiceFS 已经可以使用了!

运行 JuiceFS 提供的基准测试

将 JuiceFS 客户端安装到容器中,然后运行基准测试以检查文件系统是否按预期工作。

curl -sSL https://d.juicefs.com/install | sh - 

运行只使用1个线程的基准测试。

juicefs bench -p 1 /data   Write big blocks: 1024/1024 [==============================================================]  96.9/s   used: 10.566310616s  Read 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 /data   Write big blocks: 4096/4096 [==============================================================]  267.1/s   used: 15.333303679s  Read big blocks: 4096/4096 [==============================================================]  112.3/s   used: 36.469782933s Write small blocks: 400/400 [==============================================================]  16.5/s    used: 24.257067969s  Read 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 /data   Write big blocks: 16384/16384 [==============================================================]  307.2/s  used: 53.335911256s  Read big blocks: 16384/16384 [==============================================================]  331.4/s  used: 49.440177355s Write small blocks: 1600/1600 [==============================================================]  48.9/s   used: 32.723927882s  Read 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 文件夹中。

<clickhouse>     <storage_configuration>       <disks>         <juicefs>           <type>local</type>           <path>/juicefs-disk/juicefs-data/</path>         </juicefs>       </disks>     </storage_configuration> </clickhouse> 

这个磁盘对应于下面定义的卷挂载路径 /juicefs-disk/。

volumeMounts:   - name: juicefs-storage-vc-template     mountPath: /juicefs-disk 

juicefs-storage-vc-template 是一个卷声明模板,它使用了我们为JuiceFS创建的 juicefs StorageClass。

volumeClaimTemplates:   - name: juicefs-storage-vc-template     spec:       accessModes:         - ReadWriteOnce       volumeMode: Filesystem       storageClassName: juicefs       resources:         requests:           storage: 10P 

这是完整的 clickhouse_cluster.yaml 定义。

apiVersion: "clickhouse.altinity.com/v1" kind: "ClickHouseInstallation" metadata:   name: "ch-juicefs" spec:   configuration:     files:       storage.xml: |         <clickhouse>            <storage_configuration>                <disks>                    <s3>                        <type>s3</type>                        <endpoint>https://BUCKET.s3.REGION.wasabisys.com/s3-data/{replica}/</endpoint>                        <access_key_id>ACCESS_KEY</access_key_id>                        <secret_access_key>SECRET_KEY</secret_access_key>                    </s3>                    <s3_cache>                        <type>cache</type>                        <disk>s3</disk>                        <path>/s3_cache/</path>                        <max_size>10Gi</max_size>                    </s3_cache>                    <juicefs>                        <type>local</type>                        <path>/juicefs-disk/juicefs-data/</path>                    </juicefs>                </disks>                <policies>                    <s3>                        <volumes>                            <main>                                <disk>s3</disk>                            </main>                        </volumes>                    </s3>                    <s3_cache>                        <volumes>                            <main>                                <disk>s3_cache</disk>                            </main>                        </volumes>                    </s3_cache>                    <juicefs>                        <volumes>                            <main>                                <disk>juicefs</disk>                            </main>                        </volumes>                    </juicefs>                </policies>            </storage_configuration>         </clickhouse>     clusters:       - name: "ch-juicefs"         templates:           podTemplate: pod-template-with-volumes         layout:           shardsCount: 1           replicasCount: 1    templates:     podTemplates:       - name: pod-template-with-volumes         spec:           containers:             - name: clickhouse               image: altinity/clickhouse-server:23.8.11.29.altinitystable               volumeMounts:                 - name: data-storage-vc-template                   mountPath: /var/lib/clickhouse                 - name: log-storage-vc-template                   mountPath: /var/log/clickhouse-server                 - name: juicefs-storage-vc-template                   mountPath: /juicefs-disk     volumeClaimTemplates:       - name: juicefs-storage-vc-template         spec:           accessModes:             - ReadWriteOnce           volumeMode: Filesystem           storageClassName: juicefs           resources:             requests:               storage: 10P       - name: data-storage-vc-template         spec:           accessModes:             - ReadWriteOnce           resources:             requests:               storage: 100Gi       - name: log-storage-vc-template         spec:           accessModes:             - ReadWriteOnce           resources:             requests:               storage: 10Gi 

为了简化操作,我选择了一个只有一个分片和副本的集群。现在,让我们应用 clickhouse_cluster.yaml 来创建这个集群。

kubectl --kubeconfig kubeconfig apply -f clickhouse_cluster.yaml  clickhouseinstallation.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/          │          64848461824105089261568648484618240local00000 │            │ │ juicefs  │ /juicefs-disk/juicefs-data/   │     1125830005723136112583000573132811258300057231360local00000 │            │ │ s3       │ /var/lib/clickhouse/disks/s3/ │ 1844674407370955161518446744073709551615184467440737095516150 │ s3    │            00010 │            │ │ s3_cache │ /var/lib/clickhouse/disks/s3/ │ 1844674407370955161518446744073709551615184467440737095516150 │ s3    │            00010 │ /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_policy='<NAME>'; 

这里有一些关于表定义的简短示例。

CREATE TABLE IF NOT EXISTS tripdata_juicefs (   ... SETTINGS storage_policy='juicefs';   CREATE TABLE IF NOT EXISTS tripdata_s3 (   ... SETTINGS storage_policy='s3';   CREATE TABLE IF NOT EXISTS tripdata_s3_cache (   ... SETTINGS storage_policy='s3_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_threads=8, max_insert_threads=8, input_format_parallel_parsing=0;  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_threads=8, max_insert_threads=8, input_format_parallel_parsing=0;  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_threads=8, max_insert_threads=8, input_format_parallel_parsing=0;   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_cache>      <type>cache</type>      <disk>s3</disk>      <path>/s3_cache/</path>      <max_size>50Gi</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 的结合,但这是一个测试环境,在生产环境中使用它仍然面临挑战。以下是一些可能出现的问题:

  1. 管理 JuiceFS 的元数据存储增加了复杂性。
  2. 在本地磁盘的路径中不展开宏(Marcros)。因此,如果你想在不同的路径上为每个副本保留数据,以便在移除副本时容易清理孤立数据,每个副本必须有一个自定义的存储配置文件。
  3. ClickHouse 不支持本地磁盘的零拷贝复制,这意味着每个副本都会创建一个重复的副本。
  4. 从 23.5 版本开始,ClickHouse 不再支持本地类型磁盘的缓存,因此你只能依赖 JuiceFS 的缓存。
  5. 使用 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 社区与大家共同交流。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!