从Kubernetes的生活中看:我们如何将DBMS(不仅是从评论环境)删除为静态



注意:本文并不声称是最佳实践。它描述了使用Kubernetes和Helm进行基础架构任务的特定实施的经验,这对于解决相关问题很有用。

对于开发人员和系统工程师而言,在CI / CD中使用审阅环境可能非常有用。让我们首先同步有关它们的一般想法:

  1. 可以从开发人员定义的Git存储库中的独立分支(所谓的功能分支)中创建评论环境。
  2. 它们可以具有单独的DBMS实例,队列处理器,缓存服务等。-总的来说,一切都是为了充分再现生产环境。
  3. 它们允许并行开发,从而大大加速了应用程序中新功能的发布。同时,每天可能需要数十个这样的环境,这就是为什么它们的创建速度至关重要的原因。

在第二点和第三点的交汇处,经常会遇到困难:由于基础结构非常不同,因此其组件可以长时间部署。例如,此时间包括从已经准备好的备份*中还原数据库。这篇文章是关于我们曾经去解决这个问题的迷人方式。

*顺便说一句,特别是在这种情况下,关于大型数据库转储,我们已经在材料中写过有关加速引导数据库的信息。)

问题和解决方法


在其中一个项目中,我们的任务是“为开发人员和质量检查工程师创建一个单一的入口点”。此公式在技术上隐藏了以下内容:

  1. 为了简化质量检查工程师和其他一些员工的工作,请在单独的静态环境中删除审阅中使用的所有数据库(和相应的虚拟主机)。由于项目中普遍存在的原因,这种与他们互动的方式是最佳的。
  2. 减少审查环境的创建时间。从头开始暗示了它们创建的整个过程。包括数据库克隆,迁移等。

从实现的角度来看,主要问题归结为确保创建和删除审阅环境时的幂等性。为此,我们首先通过将PostgreSQL,MongoDB和RabbitMQ服务迁移到静态环境,从而更改了创建审阅环境的机制。静态是指不会根据用户的请求创建的“永久”环境(与审阅环境一样)。

重要!静态环境下的方法远非理想 -有关其特定缺点,请参见本文结尾。但是,我们将详细分享这种经验,因为它或多或少可以适用于其他任务,并且同时在讨论基础结构设计问题时也可以作为一个论据。

因此,执行中的动作顺序为:

  • 创建审阅环境时,应执行以下操作一次:在两个DBMS(MongoDB和PostgreSQL)中创建数据库,从备份/模板还原数据库,以及在RabbitMQ中创建vhost。这将需要一种方便的方式来加载当前转储。(如果您之前有审查环境,那么很可能已经有一个现成的解决方案。)
  • 查看环境完成后,必须删除RabbitMQ中的数据库和虚拟主机。

在我们的案例中,基础架构在Kubernetes(使用Helm)的框架内运行。因此,对于执行上述任务,Helm hooks非常它们既可以在Helm版本中创建所有其他组件之前,也可以在删除它们之后执行。因此:

  • 对于初始化任务,我们将pre-install在创建发行版中的所有资源之前使用挂钩启动它;
  • 对于删除任务,使用一个hook post-delete

让我们继续执行细节。

实际实施


在原始版本中,该项目仅使用一个Job,由三个容器组成。当然,这并不完全方便,因为结果是一个很大的清单,很难读懂。因此,我们将其分为三个小工作。

以下是PostgreSQL的清单,另外两个清单(MongoDB和RabbitMQ)的清单结构相同:

{{- if .Values.global.review }}
---
apiVersion: batch/v1
kind: Job
metadata:
  name: db-create-postgres-database
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "5"
spec:
  template:
    metadata:
      name: init-db-postgres
    spec:
      volumes:
      - name: postgres-scripts
        configMap:
          defaultMode: 0755
          name: postgresql-configmap
      containers:
      - name: init-postgres-database
        image: private-registry/postgres 
        command: ["/docker-entrypoint-initdb.d/01-review-load-dump.sh"]
        volumeMounts:
        - name: postgres-scripts
          mountPath: /docker-entrypoint-initdb.d/01-review-load-dump.sh
          subPath: review-load-dump.sh
        env:
{{- include "postgres_env" . | indent 8 }}
      restartPolicy: Never
{{- end }}

对清单内容的评论:

  1. Job review-. review CI/CD Helm- (. if .Values.global.review ).
  2. Job — , ConfigMap. , , . , hook-weight.
  3. cURL , PostgreSQL, .
  4. PostgreSQL : , shell- .

PostgreSQL


最有趣的是清单中已经提到的shell脚本(review-load-dump.sh)。在PostgreSQL中还原数据库有哪些常规选项?

  1. 从备份中“标准”恢复;
  2. 使用模板进行恢复

在我们的案例中,两种方法之间的差异主要在于为新环境创建数据库的速度。首先,我们加载数据库转储并使用还原它pg_restore与我们相比,这发生的速度比第二种方法慢,因此做出了相应的选择。

使用第二个选项(使用模板进行恢复),您可以在物理级别克隆数据库,而无需在另一个环境中从容器远程向数据库发送数据-这样可以减少恢复时间。但是,有一个局限性:您无法克隆保留活动连接的数据库。由于我们将舞台用作静态环境(而不是单独的审阅环境),因此我们需要创建第二个数据库并将其转换为模板,并每天进行更新(例如,上午)。为此准备了一个小的CronJob:

---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: update-postgres-template
spec:
  schedule: "50 4 * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  startingDeadlineSeconds: 600
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          imagePullSecrets:
          - name: registrysecret
          volumes:
          - name: postgres-scripts
            configMap:
              defaultMode: 0755
              name: postgresql-configmap-update-cron
          containers:
          - name: cron
            command: ["/docker-entrypoint-initdb.d/update-postgres-template.sh"]
          image: private-registry/postgres 
            volumeMounts:
            - name: postgres-scripts
              mountPath: /docker-entrypoint-initdb.d/update-postgres-template.sh
              subPath: update-postgres-template.sh
            env:
{{- include "postgres_env" . | indent 8 }}

包含脚本的完整ConfigMap清单很可能没有多大意义(如果不是这种情况,请在注释中进行报告)。相反,我将提供最重要的内容-bash脚本:

#!/bin/bash -x

CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"

psql -d "${CREDENTIALS}" -w -c "REVOKE CONNECT ON DATABASE ${POSTGRES_DB_TEMPLATE} FROM public"
psql -d "${CREDENTIALS}" -w -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '${POSTGRES_DB_TEMPLATE}'"

curl --fail -vsL ${HOST_FORDEV}/latest_${POSTGRES_DB_STAGE}.psql -o /tmp/${POSTGRES_DB}.psql

psql -d "${CREDENTIALS}" -w -c "ALTER DATABASE ${POSTGRES_DB_TEMPLATE} WITH is_template false allow_connections true;"
psql -d "${CREDENTIALS}" -w -c "DROP DATABASE ${POSTGRES_DB_TEMPLATE};" || true
psql -d "${CREDENTIALS}" -w -c "CREATE DATABASE ${POSTGRES_DB_TEMPLATE};" || true
pg_restore -U ${POSTGRES_USER} -h ${POSTGRES_HOST} -w -j 4 -d ${POSTGRES_DB_TEMPLATE} /tmp/${POSTGRES_DB}.psql

psql -d "${CREDENTIALS}" -w -c "ALTER DATABASE ${POSTGRES_DB_TEMPLATE} WITH is_template true allow_connections false;"

rm -v /tmp/${POSTGRES_DB}.psql

您可以从一个模板一次还原多个数据库,而不会发生任何冲突。最主要的是应该禁止数据库连接,并且数据库本身应该是模板。这是倒数第二个步骤。

包含用于还原数据库的Shell脚本的清单如下所示:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgresql-configmap
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  review-load-dump.sh: |
    #!/bin/bash -x
    
 
 
    CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"

    if [ "$( psql -d "${CREDENTIALS}" -tAc "SELECT CASE WHEN EXISTS (SELECT * FROM pg_stat_activity WHERE datname = '${POSTGRES_DB}' LIMIT 1) THEN 1 ELSE 0 END;" )" = '1' ]
      then
          echo "Open connections has been found in ${POSTGRES_DB} database, will drop them"
          psql -d "${CREDENTIALS}" -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '${POSTGRES_DB}' -- AND pid <> pg_backend_pid();"
      else
          echo "No open connections has been found ${POSTGRES_DB} database, skipping this stage"
    fi

    psql -d "${CREDENTIALS}" -c "DROP DATABASE ${POSTGRES_DB}"

    if [ "$( psql -d "${CREDENTIALS}" -tAc "SELECT 1 FROM pg_database WHERE datname='${POSTGRES_DB}'" )" = '1' ]
      then
          echo "Database ${POSTGRES_DB} still exists, delete review job failed"
          exit 1
      else
          echo "Database ${POSTGRES_DB} does not exist, skipping"
    fi


    psql ${CREDENTIALS} -d postgres -c 'CREATE DATABASE ${POSTGRES_DB} TEMPLATE "loot-stage-copy"'

显然,他们参与了这里hook-delete-policy这些策略的应用细节在此处编写在给定的清单中,我们使用before-hook-creation,hook-succeeded它来满足以下要求:在创建新的挂钩之前删除先前的对象,并仅在挂钩成功时才删除。

我们将在此ConfigMap中删除数据库:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgresql-configmap-on-delete
  annotations:
    "helm.sh/hook": "post-delete, pre-delete"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation
data:
  review-delete-db.sh: |
    #!/bin/bash -e

    CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"

    psql -d "${CREDENTIALS}" -w postgres -c "DROP DATABASE ${POSTGRES_DB}"

尽管我们将其移至单独的ConfigMap,但可以将其放置在常规ConfigMap中command毕竟,它可以做成单线而不会使清单本身的外观复杂化。

如果PostgreSQL模板的选项由于某种原因不合适或不合适,则可以使用backup返回上述的“标准”恢复路径该算法将很简单:

  1. 每天晚上,都会进行数据库备份,以便可以从群集的本地网络下载它。
  2. 在创建审阅环境时,将从转储中加载和还原数据库。
  3. 部署转储后,将执行所有其他操作。

在这种情况下,恢复脚本将大致如下所示:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgresql-configmap
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  review-load-dump.sh: |
    #!/bin/bash -x

    CREDENTIALS="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/postgres"
    psql -d "${CREDENTIALS}" -w -c "DROP DATABASE ${POSTGRES_DB}" || true
    psql -d "${CREDENTIALS}" -w -c "CREATE DATABASE ${POSTGRES_DB}"

    curl --fail -vsL ${HOST_FORDEV}/latest_${POSTGRES_DB_STAGE}.psql -o /tmp/${POSTGRES_DB}.psql

    psql psql -d "${CREDENTIALS}" -w -c "CREATE EXTENSION ip4r;"
    pg_restore -U ${POSTGRES_USER} -h ${POSTGRES_HOST} -w -j 4 -d ${POSTGRES_DB} /tmp/${POSTGRES_DB}.psql
    rm -v /tmp/${POSTGRES_DB}.psql

该过程对应于上面已经描述的过程。唯一的更改是在添加所有工作之后删除psql文件。

注意:在恢复脚本和卸载脚本中,每次都会删除数据库。这样做是为了避免在重新创建审阅期间可能发生的冲突:您需要确保确实删除了数据库。同样,可以通过--clean在实用程序中添加一个标志来解决此问题pg_restore,但要小心:此标志仅清除转储本身中那些元素的数据,因此在本例中此选项不起作用。

结果,我们得到了一个需要进一步改进的工作机制(直到用更优雅的代码替换Bash脚本)。我们将把它们放在本文的讨论范围之外(尽管当然欢迎对此主题发表评论)。

Mongodb


下一个组件是MongoDB。这样做的主要困难在于,对于该DBMS,名义上存在复制数据库的选项(例如在PostgreSQL中),因为:

  1. 他已经过时了
  2. 根据我们的测试结果,我们发现数据库恢复时间与通常的恢复时间没有太大的不同mongo_restore但是,我注意到测试是作为一个项目的一部分进行的-在您的情况下,结果可能会完全不同。

事实证明,在数据库容量很大的情况下,可能会出现一个严重的问题:我们节省了在PgSQL中恢复数据库的时间,但同时又在Mongo中恢复了很长时间。在撰写本文时,在现有基础架构的框架内,我们看到了三种方式(顺便说一下,它们可以组合在一起):

  1. 例如,如果您的DBMS位于网络文件系统上(对于没有生产环境的情况),则恢复可能会花费很长时间然后,您可以简单地将DBMS从舞台转移到单独的节点并使用本地存储。由于这不是正式产品,因此创建评论的速度对我们而言更为关键。
  2. 您可以将每个作业恢复带到一个单独的容器中,从而允许您预先执行迁移和依赖于DBMS操作的其他过程。因此,我们通过提前完成它们来节省时间。
  3. 有时,您可以通过删除旧的/不相关的数据来减小转储的大小-在一定程度上足以仅保留数据库结构。当然,这不适用于需要完全转储的情况(例如,对于QA测试任务)。

如果不需要快速创建审阅环境,则可以忽略所有描述的困难。

我们无法像PgSQL一样复制数据库,将采用第一种方法,即 从备份中进行标准恢复。该算法与PgSQL相同。如果查看清单,这很容易看到:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mongodb-scripts-on-delete
  annotations:
    "helm.sh/hook": "post-delete, pre-delete"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation
data:
  review-delete-db.sh: |
    #!/bin/bash -x

    mongo ${MONGODB_NAME} --eval "db.dropDatabase()" --host ${MONGODB_REPLICASET}/${MONGODB_HOST}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mongodb-scripts
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  review-load-dump.sh: |
    #!/bin/bash -x

    curl --fail -vsL ${HOST_FORDEV}/latest_${MONGODB_NAME_STAGE}.gz -o /tmp/${MONGODB_NAME}.gz

    mongo ${MONGODB_NAME} --eval "db.dropDatabase()" --host ${MONGODB_REPLICASET}/${MONGODB_HOST}
    mongorestore --gzip --nsFrom "${MONGODB_NAME_STAGE}.*" --nsTo "${MONGODB_NAME}.*" --archive=/tmp/${MONGODB_NAME}.gz --host ${MONGODB_REPLICASET}/${MONGODB_HOST}

这里有一个重要的细节。在我们的例子中,MongoDB在集群中,您需要确保连接始终发生在Primary节点上。例如,如果您指定仲裁中的第一台主机,则一段时间后,它可能会从“主要”切换到“辅助”,这将阻止创建数据库。因此,您不需要连接到一台主机,而是立即连接到ReplicaSet,列出其中的所有主机。仅出于这个原因,您需要将MongoDB设置为StatefulSet,以便主机名始终相同(更不用说MongoDB本质上是有状态的应用程序)。使用此选项,可以确保连接到主节点。

对于MongoDB,我们还会在创建审阅之前删除数据库-这样做的原因与PostgreSQL中相同。

最后一个细微差别:由于要检查的数据库与舞台处于同一环境中,因此克隆的数据库需要一个单独的名称。如果转储不是BSON文件,则会发生以下错误:

the --db and --collection args should only be used when restoring from a BSON file. Other uses are deprecated and will not exist in the future; use --nsInclude instead

因此,在上述示例中,使用--nsFrom--nsTo

我们没有遇到其他恢复问题。最后,我只补充说copyDatabaseMongoDB 的文档在这里可用,以防您尝试此选项。

Rabbitmq


我们需求列表中的最后一个应用是RabbitMQ。它很简单:您需要代表应用程序将连接到的用户创建一个新的虚拟主机。然后删除它。

创建和删除虚拟主机的宣言:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: rabbitmq-configmap
  annotations:
    "helm.sh/hook": "pre-install"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
data:
  rabbitmq-setup-vhost.sh: |
    #!/bin/bash -x

    /usr/local/bin/rabbitmqadmin -H ${RABBITMQ_HOST} -u ${RABBITMQ_USER} -p ${RABBITMQ_PASSWORD} declare vhost name=${RABBITMQ_VHOST}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: rabbitmq-configmap-on-delete
  annotations:
    "helm.sh/hook": "post-delete, pre-delete"
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation
data:
  rabbitmq-delete-vhost.sh: |
    #!/bin/bash -x

    /usr/local/bin/rabbitmqadmin -H ${RABBITMQ_HOST} -u ${RABBITMQ_USER} -p ${RABBITMQ_PASSWORD} delete vhost name=${RABBITMQ_VHOST}

在RabbitMQ中遇到了很大的困难,我们(到目前为止?)还没有遇到过。通常,相同的方法可以应用于对数据没有严格要求的任何其他服务。

缺点


为什么这个决定不声称是“最佳实践”?

  1. 它以阶段环境的形式证明了单点故障。
  2. 如果舞台环境中的应用程序仅在一个副本中运行,那么我们将更加依赖于该应用程序在其上运行的主机。因此,随着查看环境数量的增加,节点上的负载会成比例地增加,而无法平衡该负载。

考虑到特定项目的基础架构的功能,不可能完全解决这两个问题,但是,可以通过群集(添加新节点)和垂直扩展来最大程度地减少潜在的损害。

结论


随着应用程序的发展以及开发人员数量的增加,审核环境的迟早会增加负荷,并向其中添加新的要求。对于开发人员来说,尽快交付生产中的下一个更改很重要,但是要实现这一点,我们需要使开发“平行”的动态审查环境。结果,基础架构上的负载不断增加,创建此类环境的时间也在增加。

这篇文章是根据真实而具体的经验写的。仅在特殊情况下,我们才会在静态环境中隔离任何服务,在此专门针对他。由于具有从头开始快速创建审阅环境的能力,这种必要的措施使我们能够加快应用程序的开发和调试。

当我们开始执行此任务时,它看起来非常简单,但是在我们进行这项工作时,我们发现了许多细微差别。在最后一篇文章中聚集了他们:尽管它们不是通用的,但是它们可以作为他们自己在加速审核环境中做出决定的基础/灵感的榜样。

聚苯乙烯


另请参阅我们的博客:


All Articles