关于云存储:小团队如何妙用-JuiceFS

90次阅读

共计 7594 个字符,预计需要花费 19 分钟才能阅读完成。

早些年还在 ENJOY 的时候, 就曾经在用 JuiceFS, 并且一路随同着我工作过的四家小公司, 这玩意对我来说, 曾经成了理所应当不可或缺的基础设施, 对于我服务过的小团队而言, 更是实实在在的好帮手. 趁着最近的征文活动, 持续拓展一下我的小团队系列, 介绍下多年来咱们团队都在如何应用 JuiceFS.

不过这里讲到的用处, 恐怕都不算什么 ” 妙用 ”, JuiceFS 是一个大社区了, 这些用法恐怕早被玩的滚瓜烂熟, 不过不妨, 本文其实是外部我的项目文档的拓展, 次要记录一些保护过程的心得体会.

妙用: 容器共享存储

尽管曾经有了 CSI 反对, 但咱们始终以来都将 Juicefs 挂载到所有 Kubernetes 节点的 /jfs, 这样一来, 所有容器利用都能轻松以 hostPath 形式来挂载宿主机目录, 而后就有了共享存储了. 此法的一些须要留神的中央:

  • jfs.mount 必须先于容器服务启动, 毕竟二者建设依赖关系了. 以 docker 为例, 能够这么写:
# /etc/systemd/system/docker.service.d/12-after-jfs.conf
[Unit]
After=jfs.mount
  • 要挂载的目录必须先手动创立进去, 设置好权限(使之与容器过程 uid 匹配), 这点其实不太不便, 如果你的团队受不了这番折腾, 就间接用 CSI 吧.
  • 集群扩容存在肯定不便之处, 毕竟此法要求所有节点都挂载好 JuiceFS. 万一 Kubernetes 集群冗余不够了, 须要退出新节点, 还须要多做一步挂载 JuiceFS, 能力退出集群. 在极限状况下, 这样的设计还挺耽误时间的, 又多了一个间接用 CSI 的理由.

讲了这么多问题, 那看上去的确应该首选 CSI 而不是 hostPath 了, 不过 hostPath 就胜在治理更简略, 推理更加直白, 因为按照咱们的应用常规, 都会采纳相似 /jfs/[appname]-[cluster] 的命名, 比拟高深莫测, 对于不相熟 Kubernetes PV 那一套的共事而言, 做事件也更加不便一些.

妙用: 网盘

有了一个得心应手存文件的中央, 自然而然的念头就是如何不便地把文件分享进来. 大家都晓得 JuiceFS 是能够在各个平台挂载的 (甚至 Windows 上也很好用了), 但这并不是我要介绍的, 因为让用户各自在本地挂载 JuiceFS, 操作难度很大(何况还有平安问题). 我的意思是你能够简略搭建一个 web 服务, 挂载 JuiceFS, 而后将文件暴露出下载入口.

做这件事真的特地简略, 我过后从萌发想法到搭建结束不过 5 分钟, 多亏了 lain, 只须要这样一份简短的 values.yaml, 就能用 Python 拉起来 http.server:

appname: jfs-http-server

volumes:
  - name: jfs-data
    hostPath:
      path: "/jfs"
      type: Directory

volumeMounts:
  - name: jfs-data
    mountPath: /jfs

deployments:
  web:
    replicaCount: 1
    image: python:latest
    podSecurityContext: {}
    resources:
      limits:
        cpu: 1000m
        memory: 80M
      requests:
        cpu: 10m
        memory: 80M
    command: ["python", '-m', 'http.server']
    workingDir: /jfs
    containerPort: 8000

ingresses:
  - host: jfs
    deployName: web
    paths:
      - /

稍有开发教训的人就能看明确, 这是用社区的 python:latest 镜像, 运行 http.server, 而后挂载了宿主机的 /jfs 目录. 服务上线当前, 拜访 jfs.example.com 就能浏览和下载 jfs 下的所有文件了.

相比 jfs, 这一节仿佛更是在给 lain 做广告, 不过在 DevOps 的世界里, 好用的货色总会相互吸引, 如果各位的团队也履行 DevOps, 欢送参考 lain.

妙用: 在 JupyterLab 进行 ad hoc 编程

咱们团队常常要和数据打交道, 不仅是做数据报表, 可视化剖析, 有时也心愿在能摸到数据的中央验证一些开发思路. 总不能让大家都本地连接线上数据库吧, 既不不便也不平安, 大伙也不肯定都善于在本地折腾这方面的工具. 因而我部署了 JupyterLab, 在里边做了大量易用性改善, 也就是内置了很多公司外部数据库的快捷方式, 让所有开发者 / 数据工程师都能便捷地应用封装好的 Python 库来做数据分析, 甚至间接利用 bokeh 来交付数据可视化的工作.

不言而喻, 在 Jupyter 里写的代码也是要进入版本治理的, 辛苦写的代码可千万不能丢了. 因而我间接将 JupyterLab 的工作目录设置为 JuiceFS, 所有的 notebook 就都寄存于 JuiceFS 下了. 用 lain 部署 JupyterLab 非常简略, 上面就是用到的 values.yaml:

appname: lab

env:
  SHELL: zsh
  IPYTHONDIR: /lain/app

volumes:
  - name: jfs
    hostPath:
      path: "/jfs/lab"
      type: Directory

volumeMounts:
  - name: jfs
    mountPath: /jfs/lab

deployments:
  web:
    replicaCount: 1
    podSecurityContext: {'runAsUser': 0}
    terminationGracePeriodSeconds: 70
    resources:
      limits:
        cpu: 2
        memory: 4Gi
      requests:
        cpu: 100m
        memory: 1Gi
    command: ['jupyter', 'lab', '--allow-root', '--collaborative', '--no-browser', '--config=/lain/app/jupyter_notebook_config.py']
    containerPort: 8888
    workingDir: /jfs/lab/notebooks

ingresses:
  - host: lab
    deployName: web
    paths:
      - /

build:
  base: lain:latest
  prepare:
    script:
      - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv E0C56BD4
      - echo "deb https://repo.clickhouse.tech/deb/stable/ main/" | tee /etc/apt/sources.list.d/clickhouse.list
      - apt-get update
      - apt-get install -y apt-transport-https ca-certificates dirmngr clickhouse-client=20.12.8.5 clickhouse-common-static=20.12.8.5
      - apt-get clean
      - pip3 install -r requirements.txt
  script:
    - pip3 install -r requirements.txt

光有 Jupyter 其实用处不大, 所以我上边提到, 要做好易用性建设, 比方说封装好各种 database client:

from os import environ

import pandas as pd
import pymysql
from IPython.core.display import display


class MySQLClient:

    def __init__(self, config):
        config.update({
            'charset': 'utf8mb4',
            'cursorclass': pymysql.cursors.DictCursor,
            'autocommit': True,
        })
        self.config = config

    def use(self, db):
        self.config['database'] = db
        # just to make sure this db exists
        return self.execute(f'use {db}')

    def fetch(self, sql, *args, **kwargs):
        return self.execute(sql, *args, **kwargs)

    def fetchone(self, sql, *args, **kwargs):
        kwargs.update({'fetchone': True})
        return self.execute(sql, *args, **kwargs)

    def executemany(self, sql, *args, **kwargs):
        con = pymysql.connect(**self.config)
        with con.cursor() as cur:
            cur.executemany(sql, *args, **kwargs)
            res = cur.fetchall()

        con.close()
        return res

    def execute(self, sql, *args, **kwargs):
        con = pymysql.connect(**self.config)
        with con.cursor() as cur:
            fetchone = kwargs.pop('fetchone', None)
            as_pandas = kwargs.pop('as_pandas', None)
            cur.execute(sql, *args, **kwargs)
            if fetchone:
                res = cur.fetchone()
            else:
                res = cur.fetchall()

        con.close()
        if as_pandas:
            return pd.DataFrame(res)
        return res

    x = execute

    def preview(self, table_name=None, n=2):
        """
        # first, use a database
        mysql_client.use('configcenter')
        # show tables
        mysql_client.preview()
        # select example data from one table
        mysql_client.preview('post')
        # study one single column
        mysql_client.preview('post.visibility')
        """
        if not table_name:
            return self.execute('show tables', as_pandas=True)
        if '.' in table_name:
            n = max([n, 20])
            table_name, column_name = table_name.split('.')
            part = self.execute(
                f'''
                SELECT DISTINCT {column_name}, count(*) AS count
                FROM {table_name}
                GROUP BY {column_name}
                ORDER BY count DESC
                LIMIT {n}
                ''', as_pandas=True
            )
            return part

        part1 = self.execute(f'''
        SELECT `column_name`,
               `column_type`,
               `column_comment`
        FROM `information_schema`.`COLUMNS`
        WHERE `table_name` = "{table_name}"
        ''', as_pandas=True)
        display(part1)
        part2 = self.execute(
            f'''
            SELECT *
            FROM {table_name}
            ORDER BY RAND()
            LIMIT {n}
            ''', as_pandas=True
        )
        return part2


MYSQL_CONFIG = jalo(environ['MYSQL_CONFIG'])
mysql_client = mysql = my = MySQLClient(MYSQL_CONFIG)
mysql_client.use('mydatabase')

通过了这么一堆封装, 有多好用你都不敢想:

[站外图片上传中 …(image-cd21cc-1648800368731)]

就靠着相似的快捷调用, 在我任职过的团队里, 从后端工程师, 数据分析师, 乃至于产品经理, 都能间接用 JupyterLab 进行工作.

如同次要都在介绍 Jupyter 了, 挺不好意思的. 但实际上这个我的项目和 JuiceFS 关系也很严密:

  • 产生的数据报表, 或者别的 ad hoc 流程的产物, 都放在 JuiceFS 上, 间接就能不便地分享给别人(见上方 ” 网盘 ” 一节)
  • 所有的代码(在 Jupyter 的世界里, 叫 notebook) 都寄存在 JuiceFS, 用 juicefs snapshot 来做定期备份

妙用: GitLab, ClickHouse, Elasticsearch, etc.

实践上, 所有利用须要数据落盘的时候, 你都能够放到 JuiceFS 里, 只有适合的性能区间匹配就行. 在这一节里介绍一下咱们摸索过的一些用法:

  • GitLab 对磁盘 IO 的要求还是挺高的, 尤其 MR 的时候, 如果代码库很大, 最好还是视状况迁到 SSD. 但如果你是个小团队, 我的项目不多也不大, 那放在 JuiceFS 上就能享受到一系列额定的益处, 比方用 juicefs grep 全局搜寻代码仓库(找垃圾代码), 不便地用 juicefs snapshot 全量备份所有 repo data, 等等
  • 我用 ClickHouse 配合 JuiceFS CSI, 不便地拉起 CH 集群, 这点在 小团队如何保护 Sentry 有更具体介绍, 不反复

妙用: CI

就以 GitLab CI 为例吧, 给 Runner 设置好挂载目录:

  [runners.docker]
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/jfs:/jfs:rw", "/cache"]

没想到仅仅是把 JuiceFS 挂载到 CI Runner 里, 能关上这么多可能性, 下边的这些案例每一个都没有多 ” 神 ”, 但都是因为有了 JuiceFS, 事件变得特地不便和易保护:

公布构建产物 (Artifacts)

原本 jfs 就是用来存文件的, 将构建产物 (比方说安卓打包) 扔到 jfs, 再配合上文 ” 网盘 ” 一节介绍的文件分享, 就能轻松做出下载链接了. 如果你团队外部须要把构建产物公布给非技术人员, JuiceFS + Python http.server 会是一个很好的配合.

继续部署

不是所有服务上线, 都是要更新容器的, 比方说不少前端利用的更新, 其实只是打包公布动态文件, 而这一步往往也是在 CI 里实现的, 这样一来, 前端利用的公布和 jfs 就能做一个很好的配合:

  • CI Job 将前端利用的动态文件编译好, 公布到 jfs 下带有版本号的门路
  • 更新 Nginx 配置, 将网站指向最新版的门路, 这样就算公布进来了, 需要的话还能够触发一下 CDN 预热
  • 如果要回滚的话, 也能够戳一下对应版本的 CI Job, 旧版就又部署回去了

又比方说, 咱们有一些我的项目是放到特定的服务器上运行的, 这些服务器或者在机房, 或者在办公室, 我当然能够给这些机器都做好公司内网 VPN, 而后挨个地配置出定期 git clone 更新, 但有了 jfs, 谁还用这种费劲的形式来替换数据呢? 于是咱们是这样做的:

  • 所有服务器都挂载了 jfs, 咱们的机器初始化流程里就做好了
  • 我的项目代码随着 CI 公布到 jfs, 比方说每次更新代码, 都会对 /jfs/[appname] 下的内容做笼罩
  • 服务器上监听 /jfs/[appname] 的文件变动, 或者做出每天深夜定时重启之类的, 都不便

全局缓存

GitLab CI, 或者别的各类 CI 零碎, 都有各式各样的缓存机制吧, 但有些 CI 工具能够间接做成 Global, 而不须要 Per Project, 这种时候就间接拿 JuiceFS 存一份就好, 比方说:

Trivy

咱们用 Trivy 来做容器镜像平安扫描, Trivy 须要的就是各类安全漏洞的特色数据, 扫谁都用的同一个 db, 因而我做了个 CI Job, 定期往 JuiceFS 下更新数据:

refresh_trivy_db:
  stage: schedule
  variables:
    TRIVY_CACHE_DIR: /jfs/trivycache
  rules:
    - if: '$CI_PIPELINE_SOURCE =="schedule"'
  script:
    - trivy --cache-dir $TRIVY_CACHE_DIR image --download-db-only

而后所有我的项目都能够共用这一份 jfs 下的数据, 来进行镜像漏扫了, 还不错吧:

container_scanning:
  stage: release
  rules:
    - if: '$CI_PIPELINE_SOURCE !="schedule"'
  variables:
    GIT_STRATEGY: none
    TRIVY_CACHE_DIR: /jfs/trivycache
  script:
    - trivy --cache-dir $TRIVY_CACHE_DIR image --skip-db-update=true --exit-code 0 --no-progress --severity HIGH "${IMAGE}:latest"
    - trivy --cache-dir $TRIVY_CACHE_DIR image --skip-db-update=true --exit-code 1 --severity CRITICAL --no-progress "${IMAGE}:latest"

Semgrep

Trivy 是扫镜像, Semgrep 则是扫代码的, 须要定期更新用于扫描的规定文件. 通常的姿态是现场下载规定文件, 不过因为网络问题, 国内大略更违心事后下载好这些文件, 而后间接援用. 所以轮到 JuiceFS 出场了:

# ref: https://semgrep.dev/docs/semgrep-ci/sample-ci-configs/#gitlab-ci
semgrep:
  image: semgrep-agent:v1
  script:
    - semgrep-agent
  variables:
    SEMGREP_RULES: >- # more at semgrep.dev/explore
      /jfs/semgrep/security-audit.yaml
      /jfs/semgrep/secrets.yaml
      /jfs/semgrep/ci.yaml
      /jfs/semgrep/python.yaml
      /jfs/semgrep/bandit.yaml
  rules:
    - if: $CI_MERGE_REQUEST_IID

至于更新规定文件的流程, 没啥难度, 所以这里就不赘述了.

如有帮忙的话欢送关注咱们我的项目 Juicedata/JuiceFS 哟!(0ᴗ0✿)

正文完
 0