自定义存储层插件

Clusterpedia 可以通过存储层来使用不同的存储组件,例如 MySQL/PostgreSQL, Memory, Elasticsearch

当前,Clusterpedia 内置了两个存储层:

尽管 Clusterpedia 已经默认支持了关系型数据库和内存,但是用户的需求往往是多变和复杂的,固定的存储层可能无法满足不同用户对于存储组件和性能的要求,于是 Clusterpedia 支持通过插件的方式来接入用户自己实现的存储层,我们称这种方式为 自定义存储层插件,简称为 存储插件

通过存储插件,用户可以做到以下事情:

  • 使用任意的存储组件,例如 ElasticsearchRedisGraphEtcd,甚至是 MessageQueue 也没有问题
  • 允许用户针对自己的业务来优化资源的存储格式和查询性能
  • 针对存储组件的特性来实现更高级的检索功能

Clusterpedia 也维护了一些存储插件,用户可以根据需求选用:

存储插件是通过 Go Plugin 的方式来让 Clusterpedia 组件加载,相比 RPC 或者其他方式,这种方式在没有任何损耗的前提下提供非常灵活的插件接入。

关于 Go Plugin 对性能的影响可以简单参考 https://github.com/uberswe/goplugins

众所周知,Go Plugin 在开发和使用上都比较繁琐,不过 Clusterpedia 通过一些机制来巧妙的优化了存储插件的使用和开发,并且提供了 clusterpedia-io/sample-storage 插件作为参考。

下面我们以 clusterpedia-io/sample-storage 为例,分别介绍:

使用 自定义存储层插件

存储插件的使用可以大致分为三种方式:

  1. 运行 Clusterpedia 组件二进制并加载 存储插件
  2. 使用基础 Chart —— clusterpedia-core 来设置存储插件镜像和配置存储层
  3. 使用 Clusterpedia 高级 Chart,无需关心存储插件的设置

通过本地运行组件二进制,我们可以更加了解 Cluserpedia 组件是如何加载和运行 存储插件

用户在真正使用时通常是利用已经构建好的存储插件镜像,或者是直接部署 Clusterpedia 高级 Chart

本地运行

插件构建

存储插件实际是一个 .so 后缀的动态链接库,Clusterpedia 组件在启动时可以加载存储插件,并根据指定的存储层名称来使用具体的存储插件

我们以 clusterpedia-io/sample-storage 为例,构建出一个存储插件二进制

$ git clone --recursive https://github.com/clusterpedia-io/sample-storage.git && cd sample-storage
$ make build-plugin

使用 file 命令查看存储插件信息

$ file ./plugins/sample-storage-layer.so
./plugins/sample-storage-layer.so: Mach-O 64-bit dynamically linked shared library x86_64

Clusterpedia 的 ClusterSynchro ManagerAPIServer 组件可以通过环境变量和命令参数来加载和使用存储插件:

  • STORAGE_PLUGINS=<plugins dir> 环境变量设置插件所在目录,Clusterpedia 会将该目录下所有插件都加载到组件中
  • --storage-name=<storage name> 插件命令参数,设置存储层名称
  • --storage-config=<storage config path> 插件命令参数,设置存储层配置

组件构建

本地运行时,为了保证依赖一致,需要在本地通过 make build-components 命令构建 clusterpedia 组件

$ # 在 sample-storage 目录
$ make build-components
$ ls -al ./bin
-rwxr-xr-x   1 icebergu  staff  90707488 11  7 11:15 apiserver
-rwxr-xr-x   1 icebergu  staff  91896016 11  7 11:16 binding-apiserver
-rwxr-xr-x   1 icebergu  staff  82769728 11  7 11:16 clustersynchro-manager
-rwxr-xr-x   1 icebergu  staff  45682000 11  7 11:17 controller-manager

关于存储插件与 Clusterpedia 组件的构建可以查看 开发自定义存储层插件

存储插件运行时的配置文件

在运行 clusterpedia 前还需要准备存储插件在运行时的配置文件,sample-storage 提供了它的配置示例 example-config.yaml

在运行时 clusterpedia 组件时,通过 --storage-config=./config.yaml 来指定运行时配置文件

# example-config.yaml
type: mysql
host: 127.0.0.1
port: "3306"
user: root
password: dangerous0
database: clusterpedia
log:
  stdout: true
  colorful: true
  slowThreshold: 100ms

用户需要根据选择的存储层来配置运行时配置

运行 clusterpedia clustersynchro manager

$ STORAGE_PLUGINS=./plugins ./bin/clustersynchro-manager --kubeconfig ~/.kube/config \
    --storage-name=sample-storage-layer \
    --storage-config ./config.yaml

运行 clusterpedia apiserver

可以选择不使用自己生成的证书,这时需要在运行 apiserver 忽略掉 --client-ca-file ca.crt 参数

$ openssl req -nodes -new -x509 -keyout ca.key -out ca.crt
$ openssl req -out client.csr -new -newkey rsa:4096 -nodes -keyout client.key -subj "/CN=development/O=system:masters"
$ openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -sha256 -out client.crt

运行 apiserver

$ STORAGE_PLUGINS=./plugins ./bin/apiserver --client-ca-file ca.crt  --secure-port 8443 \
    --kubeconfig ~/.kube/config \
    --authentication-kubeconfig ~/.kube/config \
    --authorization-kubeconfig ~/.kube/config \
    --storage-name=sample-storage-layer \
    --storage-config ./config.yaml

存储插件镜像 + Helm Charts

Clusterpeida 已经提供了多个 Charts:

如果用户没有存储插件需求,默认存储层和关系型数据库已经可以满足需求的话,可以直接选用 charts/clusterpedia,开箱即用

clusterpedia-mysqlclusterpedia-postgresqlclusterpedia-elasticsearch 便是基于 clusterpedia-core 实现的高级 Charts,在这些 Charts 中通过默认配置 clusterpedia-core 的存储插件镜像和存储层配置来为用户屏蔽掉存储插件的复杂概念,开箱即用

尽管我们在使用中通常会直接使用高级 Charts,但是知道如何使用 clusterpedia-core 来设置存储插件镜像,可以让我们更好的理解插件镜像是如何工作的。

clusterpedia-core

我们以 clusterpedia-io/sample-storage 为例,使用 ghcr.io/clusterpedia-io/clusterpedia/sample-storage-layer 插件镜像来部署 Clusterpedia。

clusterpedia-core 不涉及任何存储组件的部署安装,所以用户需要根据已部署的存储组件来配置存储层

# myvalues.yaml
storage:
  name: "sample-storage-layer"
  image:
    registry: ghcr.io
    repository: clusterpedia-io/clusterpedia/sample-storage-layer
    tag: v0.0.0-v0.6.0
  config:
    type: "mysql"
    host: "10.111.94.196"
    port: 3306
    user: root
    password: dangerous0
    database: clusterpedia

storage.name 配置存储镜像插件的存储层名称

clusterpedia-core 会将 storage.image 定义的插件镜像中的存储插件复制到组件的插件目录下

# helm template clusterpedia -n clusterpedia-system -f myvalues.yaml ./clusterpedia-core
...
      initContainers:
      - name: copy-storage-plugin
        image: ghcr.io/clusterpedia-io/clusterpedia/sample-storage-layer:v0.0.0-v0.6.0
        imagePullPolicy: IfNotPresent
        command:
          - /bin/sh
          - -ec
          - cp /plugins/* /var/lib/clusterpedia/plugins/
        volumeMounts:
        - name: storage-plugins
          mountPath: /var/lib/clusterpedia/plugins
      containers:
      - name: clusterpedia-clusterpedia-core-apiserver
        image: ghcr.io/clusterpedia-io/clusterpedia/apiserver:v0.6.0
        imagePullPolicy: IfNotPresent
        command:
        - /usr/local/bin/apiserver
        - --secure-port=443
        - --storage-name=sample-storage-layer
        - --storage-config=/etc/clusterpedia/storage/config.yaml
        env:
        - name: STORAGE_PLUGINS
          value: /var/lib/clusterpedia/plugins
        volumeMounts:
        - name: storage-config
          mountPath: /etc/clusterpedia/storage
          readOnly: true
        - name: storage-plugins
          mountPath: /var/lib/clusterpedia/plugins
          readOnly: true
      volumes:
      - name: storage-config
        configMap:
          name: clusterpedia-clusterpedia-core-sample-storage-layer-config
      - name: storage-plugins
        emptyDir: {}
...

除了使用 storage.config 定义存储层的运行时配置 config.yaml 外,还可以使用已有的 configmap 以及 secret

# myvalues.yaml
storage:
  name: "sample-storage-layer"
  image:
    registry: ghcr.io
    repository: clusterpedia-io/clusterpedia/sample-storage-layer
    tag: v0.0.0-v0.6.0

  configMap: "sample-storage-config"
  componentEnv:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: "sample-storage-password"
        key: password

clusterpedia-core 为了能够作为子 Chart 被其他高级 Charts 引用,在存储层配置上是非常灵活的,但是用户实际在使用中并不一定需要直接使用 clusterpedia-core,只需要使用针对具体存储组件部署的高级 Charts 即可,例如 clusterpedia-mysqlclusterpedia-postgresql

在下节我们也会介绍如何基于 clusterpedia-core 来针对具体存储组件实现高级 Chart

开发自定义存储层插件

clusterpedia-io/sample-storage 不仅仅是一个存储插件示例,也是一个模版仓库,其中项目结构和大部分构建工具都可以在其他存储插件项目中使用

我们首先 clone sample-storage,或者根据 sample-storage 生成一个新的存储插件仓库

$ git clone --recursive https://github.com/clusterpedia-io/sample-storage.git && cd sample-storage

注意拉取仓库时,需要指定 --recursive 拉取子仓库

$ ls -al
...
-rw-r--r--   1 icebergu  staff    260 12 13 15:14 Dockerfile
-rw-r--r--   1 icebergu  staff   1836 12 13 16:03 Makefile
-rw-r--r--   1 icebergu  staff   2219 11 23 10:25 README.md
drwxr-xr-x  32 icebergu  staff   1024 11 23 10:30 clusterpedia
-rw-r--r--   1 icebergu  staff    156 11 23 10:25 example-config.yaml
-rw-r--r--   1 icebergu  staff   2376 12 13 15:33 go.mod
-rw-r--r--   1 icebergu  staff  46109 12 13 15:33 go.sum
-rw-r--r--   1 icebergu  staff    139 11 23 10:25 main.go
drwxr-xr-x  16 icebergu  staff    512 12 13 15:33 storage
drwxr-xr-x   9 icebergu  staff    288 12 13 15:33 vendor

整个项目结构分为三类

  • main.go, storage 包:自定义存储层插件的核心逻辑
  • clusterpedia 主库,用于本地开发和测试
  • DockerfileMakefile 用于项目构建和镜像打包,可以适用于任何存储插件项目

核心逻辑

main.go 是存储插件的主文件,主要用来调用 storage 包中的注册函数 —— RegisterStorageLayer

package main

import (
	plugin "github.com/clusterpedia-io/sample-storage-layer/storage"
)

func init() {
	plugin.RegisterStorageLayer()
}

storage 包中的是存储插件的核心逻辑:

  1. 实现 clusterpedia 存储层接口 storage.StorageFactory
import (
	"gorm.io/gorm"

	"github.com/clusterpedia-io/clusterpedia/pkg/storage"
)

type StorageFactory struct {
	db *gorm.DB
}

var _ storage.StorageFactory = &StorageFactory{}
  1. NewStorageFactory 函数来返回 storage.StorageFactory 实例
func NewStorageFactory(configPath string) (storage.StorageFactory, error)
  1. RegisterStorageLayer 函数将 NewStorageFactory 注册到 clusterpedia 中
const StorageName = "sample-storage-layer"

func RegisterStorageLayer() {
	storage.RegisterStorageFactoryFunc(StorageName, NewStorageFactory)
}

当用户通过 --storage-name 指定该存储层时,会自动调用注册的 NewStorageFactory 来创建 storage.StorageFactory 实例。

./bin/apiserver --storage-name=sample-storage-layer <other flags>

本地开发运行

为了方便开发测试,我们在存储插件的仓库中添加了 clusterpedia 主库作为子库

$ git submodule status
+4608c8d13101d82960525dfe39f51e4f64ed49b3 clusterpedia (v0.6.0)

并且将 go.mod 中 clusterpedia 仓库 replace 为本地子库

# go.mod
replace (
        github.com/clusterpedia-io/api => ./clusterpedia/staging/src/github.com/clusterpedia-io/api
        github.com/clusterpedia-io/clusterpedia => ./clusterpedia
)

在构建存储层镜像时,不会使用本地 cluserpedia 子库

存储插件构建

存储插件的构建分为两部分,分别是构建 clusterpedia 子库中组件和构建存储插件

$ make build-components
OUTPUT_DIR=/Users/icebergu/workspace/clusterpedia/sample-storage-layer ON_PLUGINS=true \
                /Library/Developer/CommandLineTools/usr/bin/make -C clusterpedia all
hack/builder.sh apiserver
hack/builder.sh binding-apiserver
hack/builder.sh clustersynchro-manager
hack/builder-nocgo.sh controller-manager

$ ls -al ./bin
-rwxr-xr-x   1 icebergu  staff  90724968 12 15 09:51 apiserver
-rwxr-xr-x   1 icebergu  staff  91936472 12 15 09:52 binding-apiserver
-rwxr-xr-x   1 icebergu  staff  82826584 12 15 09:52 clustersynchro-manager
-rwxr-xr-x   1 icebergu  staff  45677904 12 15 09:52 controller-manager

make build-components 命令会在调用 clusterpedia 库中的 make all,并将结果输出到存储插件项目的 ./bin 目录下

如果 clusterpedia 子库未发生更改,那么只需要构建组件即可

构建存储插件

$ make build-plugin
CLUSTERPEDIA_REPO=/Users/icebergu/workspace/clusterpedia/sample-storage/clusterpedia \
                clusterpedia/hack/builder.sh plugins sample-storage-layer.so

$ ls -al ./plugins
-rw-r--r--   1 icebergu  staff  53354352 12 15 09:47 sample-storage-layer.so

本地构建存储插件也是需要使用 clusterpedia 库的 builder.sh 脚本来构建插件二进制

关于运行存储层插件可以参考 本地运行存储插件

存储插件镜像

上文提到,存储插件在真正的部署中,是通过镜像的方式将存储插件共享给 clusterpedia。

Makefile 中提供 make image-plugin 来构建镜像,make push-images 来发布镜像

构建镜像

构建插件镜像时,我们需要使用 clusterpedia/builder 镜像作为基础镜像来构建插件,builder 镜像的版本需要和使用插件的 clusterpedia 组件的版本一致

$ BUILDER_IMAGE=ghcr.io/clusterpedia-io/clusterpedia/builder:v0.6.0 make image-plugin

clusterpedia 中维护了已发布版本的 builder 镜像,用户也可以使用自己本地构建的 builder 镜像

本地构建 builder 镜像

$ cd clusterpedia
$ make image-builder
docker buildx build \
                -t "ghcr.io/clusterpedia-io/clusterpedia"/builder-amd64:4608c8d13101d82960525dfe39f51e4f64ed49b3 \
                --platform=linux/amd64 \
                --load \
                -f builder.dockerfile . ; \

存储插件镜像的 tag 格式为 -<clusterpedia-version/commit>,例如: ghcr.io/clusterpedia-io/clusterpedia/sample-storage-layer:v0.0.0-v0.6.0

存储插件镜像可以部署在 <clusterpedia-version/commit> 版本的 Clusterpedia 中

镜像推送

make image-plugin 根据手动设置的 builder 镜像来构建存储插件镜像

而使用 make push-images 推送镜像时会自动为所有兼容版本和架构构建镜像

# Makefile
CLUSTERPEDIA_VERSIONS = v0.6.0-beta.1 v0.6.0
RELEASE_ARCHS ?= amd64 arm64

镜像构建好后,可以通过 cluserpedia-core 来使用存储插件镜像

基于 clusterpeida-core 实现高级 Chart

我们在实现自己的存储插件后,为了更加方便的使用,还是需要基于 clusterpedia-core Chart 来封装出高级 Chart

高级 Chart 需要提供以下能力:

  • 设置默认的存储插件镜像
  • 设置存储层名称
  • 支持动态设置存储层的运行时配置
  • 提供对存储组件的配置和安装

创建一个使用 sample-storage 存储插件的新 Chart —— clusterpedia-sample-mysql,这个 Chart 会使用 mysql 作为存储组件。

# Chart.yaml
dependencies:
  - name: mysql
    repository: https://charts.bitnami.com/bitnami
    version: 9.x.x
  - name: common
    repository: https://charts.bitnami.com/bitnami
    version: 1.x.x
  - name: clusterpedia-core
    repository: https://clusterpedia-io.github.io/clusterpedia-helm/
    version: 0.1.x

我们需要覆盖 clusterpedia-core 中存储层相关的设置,clusterpedia-core 提供 values.yaml动态模版两种方式来设置存储插件和存储层信息

我们在 values.yaml 覆盖存储层的静态设置,例如插件镜像存储层名称

# values.yaml
clusterpedia-core:
  storage:
    name: "sample-storage-layer"
    image:
      registry: "ghcr.io"
      repository: "clusterpedia-io/clusterpedia/sample-storage-layer"
      tag: "v0.0.0-v0.6.0"

自定义存储层的 config.yaml 和一些环境变量的设置,一般需要引用 ConfigMap 和 Secret,而这些资源的名称会根据 Chart 的 Release 名字动态变化,所以我们需要使用 动态模版 的方式来设置

clusterpedia-core 提供了三个可以覆盖的命名模版

# clusterpedia-core/templates/_storage_override.yaml
{{- define "clusterpedia.storage.override.initContainers" -}}
{{- end -}}

{{- define "clusterpedia.storage.override.configmap.name" -}}
{{- end -}}

{{- define "clusterpedia.storage.override.componentEnv" -}}
{{- end -}}

可以分别设置:

  • apiserverclustersynchro manager 组件运行前的 init containers
  • 保存存储插件需要读取的 config.yaml 配置的 ConfigMap 名称
  • 存储插件需要使用的环境变量

我们以 clusterpedia-mysql 为例,看看它的设置

# _storage_override.yaml
{{- define "clusterpedia.storage.override.initContainers" -}}
- name: ensure-database
  image: docker.io/bitnami/mysql:8.0.28-debian-10-r23
  command:
  - /bin/sh
  - -ec
  - |
    if [ ${CREARE_DATABASE} = "ture" ]; then
      until mysql -u${STORAGE_USER} -p${DB_PASSWORD} --host=${STORAGE_HOST} --port=${STORAGE_PORT} -e 'CREATE DATABASE IF NOT EXISTS ${STORAGE_DATABASE}'; do
      echo waiting for database check && sleep 1;
      done;
      echo 'DataBase OK ✓'
    else
      until mysqladmin status -u${STORAGE_USER} -p${DB_PASSWORD} --host=${STORAGE_HOST} --port=${STORAGE_PORT}; do sleep 1; done
    fi    
  env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: {{ include "clusterpedia.mysql.storage.fullname" . }}
        key: password
  envFrom:
  - configMapRef:
      name: {{ include "clusterpedia.mysql.storage.initContainer.env.name" . }}
{{- end -}}

clusterpedia-mysql 在 storage-initcontainer-env-configmap.yaml 中定义了 init container 需要的环境变量

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "clusterpedia.mysql.storage.initContainer.env.name" . }}
  namespace: {{ .Release.Namespace }}
  labels: {{ include "common.labels.standard" . | nindent 4 }}
data:
  STORAGE_HOST: {{ include "clusterpedia.mysql.storage.host" . | quote }}
  STORAGE_PORT: {{ include "clusterpedia.mysql.storage.port" . | quote }}
  STORAGE_USER: {{ include "clusterpedia.mysql.storage.user" . | quote }}
  STORAGE_DATABASE: {{ include "clusterpedia.mysql.storage.database" . | quote }}
  CREARE_DATABASE: {{ .Values.externalStorage.createDatabase | quote }}

通过 clusterpedia.storage.override.initContainers 命名模版动态设置的 init container 会被渲染到 Deployment 中:

# helm template clusterpedia -n clusterpedia-system --set persistenceMatchNode=None .
...
    spec:
      initContainers:
      - name: ensure-database
        image: docker.io/bitnami/mysql:8.0.28-debian-10-r23
        command:
        - /bin/sh
        - -ec
        - |
          if [ ${CREARE_DATABASE} = "ture" ]; then
            until mysql -u${STORAGE_USER} -p${DB_PASSWORD} --host=${STORAGE_HOST} --port=${STORAGE_PORT} -e 'CREATE DATABASE IF NOT EXISTS ${STORAGE_DATABASE}'; do
            echo waiting for database check && sleep 1;
            done;
            echo 'DataBase OK ✓'
          else
            until mysqladmin status -u${STORAGE_USER} -p${DB_PASSWORD} --host=${STORAGE_HOST} --port=${STORAGE_PORT}; do sleep 1; done
          fi          
        env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: clusterpedia-mysql-storage
              key: password
        envFrom:
        - configMapRef:
            name: clusterpedia-mysql-storage-initcontainer-env

保存存储插件运行时配置 config.yaml 的 ConfigMap 和环境变量也是在 clusterpedia-mysql 中动态配置

# _storage_override.yaml

{{- define "clusterpedia.storage.override.configmap.name" -}}
{{- printf "%s-mysql-storage-config" .Release.Name -}}
{{- end -}}

{{- define "clusterpedia.storage.override.componentEnv" -}}
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: {{ include "clusterpedia.mysql.storage.fullname" . }}
      key: password
{{- end -}}

通过 values 静态覆盖和 命名模版的动态设置,存储层配置会被设置到 APIServer 和 ClusterSynchro Manager 的 Deployment 中

通过类似 clusterpedia-mysql高级 Chart 可以为用户屏蔽掉底层存储插件的使用,达到开箱即用。