Rainbond Helm 应用商店对接管理实现分析

Rainbond Helm 应用商店对接管理实现分析

本文作者:Rainbond 项目维护者:黄润豪

Rainbond 是一个完全开源,简单易用的云原生应用管理平台。除了支持内置的本地组件库, 云原生应用商店, 还支持 Helm 应用商店. 用户把常用的 Helm 仓库对接到 Rainbond 后, 可以简单, 方便地对Helm应用进行配置和安装。

Rainbond Helm 应用商店的功能

添加 Helm 应用商店

当前版本支持基于Helm v3协议添加外部应用商店。

Rainbond Helm 应用商店对接管理实现分析

Helm 应用列表

Rainbond Helm 应用商店对接管理实现分析

搜索 Helm 应用

Rainbond Helm 应用商店对接管理实现分析

Helm 应用商店实现

在开始介绍基本原理时, 我们先简单介绍下 Helm 仓库, 让大家对 Helm 仓库有基础的理解.

Helm 仓库

Helm 仓库的应用模板列表索引会放在 index.yaml 里面,可以通过 <仓库地址>/index.yaml 获取,

比如: https://openchart.goodrain.com/goodrain/rainbond/index.yaml

index.yaml 详情如下:

apiVersion: v1
entries:
  nfs-client-provisioner:
  - apiVersion: v1
    appVersion: 3.1.0
    created: "2021-07-08T08:20:40Z"
    description: nfs-client is an automatic provisioner that used your *already configured*
      NFS server, automatically creating Persistent Volumes.
    digest: 1197847e6ee3720e4f7ee493985fce3703aa42813fa409dc6c0ebaa4fef3f175
    home: https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client
    keywords:
    - nfs
    - storage
    maintainers:
    - email: bart@verwilst.be
      name: verwilst
    name: nfs-client-provisioner
    sources:
    - https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client
    urls:
    - charts/nfs-client-provisioner-1.2.8.tgz
    version: 1.2.8
  rainbond-console:
  - apiVersion: v2
    appVersion: 1.16.0
    created: "2021-07-08T08:36:05Z"
    description: Goodrain Rainbond-console Helm chart for Kubernetes
    digest: 5ecba097ee7425d16a1a4512c6bb82ca3e7d04b8491d098226c4a9517e3507be
    home: https://github.com/goodrain/rainbond
    icon: https://www.rainbond.com/images/rainbondlog.png
    name: rainbond-console
    sources:
    - https://github.com/goodrain/rainbond/
    type: application
    urls:
    - charts/rainbond-console-5.3.1.tgz
    version: 5.3.1
generated: "2021-07-08T08:36:12Z"
serverInfo: {}

可以看到 index.yaml 记录了应用模板列表, 每个应用模板的元数据, 包括 版本号(version), 说明(description), charts 包地址(urls) 等等.

基本原理

Rainond Helm 应用商店的基本原理: 识别 Helm 仓库, 获得 index.yaml, 解析得到每个应用模板, 并转化为 Rainbond 模型(Golang 结构体).

┌────────────────┐        ┌───────────────────────┐
│                │        │                       │
│   index.yaml   ├───────►│ application templates │
│                │        │                       │
└────────────────┘        └───────────────────────┘

添加 Helm 应用商店

添加Helm 应用商店需要把 Helm 应用商店的元数据记录下来, 持久化到数据库中. 在正式添加前, 还需要检查商店的可用心.

代码如下:

func (a *appStoreRepo) Create(ctx context.Context, appStore *domain.AppStore) error {
    // Check the availability of the app store.
    if err := a.isAvailable(ctx, appStore); err != nil {
        return err
    }
    return a.appStoreDao.Create(&model.AppStore{
        Name:     appStore.Name,
        URL:      appStore.URL,
        Branch:   appStore.Branch,
    })
}

Helm 应用列表

Helm 应用商店的主要逻辑:

  • 建立 HTTP 请求去获取 index.yaml 文件
  • 将 HTTP 响应反序列化到结构体 IndexFile 中. IndexFile 是 Helm 官方定义的结构体.
  • 遍历 IndexFile 的 Entries, 生成我们想要的应用模板列表(appTemplates)

代码如下:

func (h *helmAppTemplate) fetch(ctx context.Context, appStore *domain.AppStore) ([]*domain.AppTemplate, error) {
      // 建立 HTTP 请求去访问 index.yaml
    req, err := http.NewRequest("GET", appStore.URL+"/index.yaml", nil)
    ...
    body, err := ioutil.ReadAll(resp.Body)
    ...
    jbody, err := yaml.YAMLToJSON(body)
    ...
      // 解析返回结果, 反序列化到结构体 hrepo.IndexFile 中
    var indexFile hrepo.IndexFile
    if err := json.Unmarshal(jbody, &indexFile); err != nil {
        return nil, errors.Wrap(err, "read index file")
    }
    ...
      // 解析成应用列表
    var appTemplates []*domain.AppTemplate
    for name, versions := range indexFile.Entries {
        appTemplate := &domain.AppTemplate{
            Name:     name,
            Versions: versions,
        }
        appTemplates = append(appTemplates, appTemplate)
    }
    return appTemplates, nil
}

应用列表的缓存

为了加快响应速度, 应用模板列表会被缓存起来, 目前支持的缓存是内存.

代码如下:

appTemplates, err := s.appTemplater.Fetch(ctx, appStore)
if err != nil {
  return nil, err
}
appStore.AppTemplates = appTemplates
s.store.Store(appStore.Key(), appStore)

防止缓存击穿

使用缓存时, 我们还应该考虑高并发情况下缓存击穿的问题. 缓存击穿的意思是在某个数据过期后, 有大量的并发访问该数据, 从而导致数据库压力剧增. 当然, 我们获取应用列表不是去访问数据库, 而是发出 HTTP 请求, 获取 index.yaml.

为了解决防止缓存击穿, 引入了 singleflight. singleflight 会确保同一个 key, 同一时刻, 只会有一个请求在执行. 也就是说, 对于同一个 Helm 应用商店, 在同一个时刻, 只会有一个获取 index.yaml 文件的 HTTP 请求.

代码如下:

type helmAppTemplate struct {
    singleflight.Group
}
func (h *helmAppTemplate) Fetch(ctx context.Context, appStore *domain.AppStore) ([]*domain.AppTemplate, error) {
    // single flight to avoid cache breakdown
    v, err, _ := h.Do(appStore.Key(), func() (interface{}, error) {
        return h.fetch(ctx, appStore)
    })
    appTemplates := v.([]*domain.AppTemplate)
    return appTemplates, err
}

Charts 包的缓存

每次安装应用时, Helm 会先拉取应用的 Charts 包, 缓存到本地存储中.

Helm 对 Charts 包的缓存

在 Mac 中, Helm 缓存 charts 包的路径是 $HOME/Library/Caches/helm/repository:

.
├── bitnami-charts.txt
├── bitnami-index.yaml
├── mysql-8.5.1.tgz
├── nginx-8.8.3.tgz
├── phpmyadmin-8.2.4.tgz
├── rainbond-charts.txt
├── rainbond-index.yaml
├── rainbond-operator-1.3.0.tgz
├── redis-cluster-5.0.1.tgz

所有 Helm 仓库的 chart 包都会缓存到该目录下. 需要注意的是, 该目录下的 charts 包是不区分仓库的. 也就是说, 如果 bitnami 和 rainbond 仓库都有版本为 8.5.1 的 mysql 应用, 那么 bitnami 的 mysql-8.5.1.tgz 就可能会被 rainbond 的 mysql-8.5.1.tgz 覆盖掉, 导致 bitnami 仓库的缓存失效. 详情请查看 #4618

Rainbond 对 Charts 包的缓存

Rainbond 没有直接沿用 Helm 对 Charts 包的缓存方式, 而是做了一些扩展, 使其支持区分仓库的 Charts 包的缓存:

  • 构造 Charts 包的缓存路径, 格式为 主缓存路径/仓库名/应用名/应用版本/.
  • 构造应用的包名 应用名-版本号.tgz.
  • 检查存储中时候已经有目标 Charts 包, 并检查他们的 hash 是否一样.
  • 如果缓存红没有目标 Charts 包, 则进行下载, 将Charts 包缓存起来.

代码如下:

func (h *Helm) locateChart(chart, version string) (string, error) {
    repoAndName := strings.Split(chart, "/")
    ...
    // Charts 包的路径为: 主缓存路径/仓库名/应用名/应用版本/
    chartCache := path.Join(h.settings.RepositoryCache, chart, version)
    cp := path.Join(chartCache, repoAndName[1]+"-"+version+".tgz")
    if f, err := os.Open(cp); err == nil {
        defer f.Close()
        // 检查 hash
        // check if the chart file is up to date.
        hash, err := provenance.Digest(f)
        ...
        // get digiest from repo index.
        digest, err := h.getDigest(chart, version)
        ...
        if hash == digest {
            return cp, nil
        }
    }
    cpo := &ChartPathOptions{}
    cpo.Version = version
    settings := h.settings
    // 下载 Charts 包
    cp, err := cpo.LocateChart(chart, chartCache, settings)
    ...
    return cp, err
}

接下来

目前的实现还有非常大的优化的空间, 比如应用列表缓存插件化, 应用列表缓存可过期, 清理 Charts 包缓存.

应用列表缓存插件化

应用列表缓存目前只支持内存. 随着 Helm 商店数量的越来越多, 应用模板的数量也将越来越多, 这对缓存的大小的要求将会比较高. 这场景下, 内存显然不是合适选择. 所以, 有必然将缓存抽象出来, 与内存解耦, 变得插件化, 可以轻松得对接其它的缓存, 比如 redis.

应用列表缓存可过期

细心的同学可能已经发现, 目前的内存缓存是不支持过期的, 也就是说, 在不重启应用的情况下, 缓存会越来越大. 所以, 不管是缓存在内存中, 还是缓存在 redis 中, 都非常有必要支持缓存过期. 缓存的过期策略可以是 LRU, LFU 等等, 同时结合过期时间, 清理长时间不被使用的缓存.

另外, 减少应用模板中不必要的字段, 从而减小每个应用模板的大小, 也是个不错的优化点.

清理 Charts 包缓存

Charts 包缓存在磁盘上, 不会过期, 目前也没有清理的机制, 这可能会使得磁盘的占用非常大. 接下来, 需要设计合适的方案对其进行定期的清理.

总结

本文结合代码和大家一起分析了Rainbond Helm 应用商店对接实现.

Reference

  1. Helm: https://github.com/helm/helm
  2. What is cache penetration, cache breakdown and cache avalanche?: https://www.pixelstech.net/article/1586522853-What-is-cache-penetration-cache-breakdown-and-cache-avalanche
  3. singleflight: https://pkg.go.dev/golang.org/x/sync/singleflight
上一篇:Linux CentOS重新生产后,目录下找不到网卡配置文件


下一篇:行业云的未来形态:应用程序商店风格的B2B市场