我们正在开发的在线视频内容推荐系统是一个封闭的商业开发,从技术上讲是一个由专有和开源组件组成的多组件集群。 写这篇文章的目的是描述在有限时间内不打乱我们流程既定工作流程的情况下,针对暂存站点实现 docker swarm 集群系统。 呈现给您注意的叙述分为两部分。 第一部分介绍了使用docker swarm之前的CI/CD,第二部分介绍了其实现过程。 那些对阅读第一部分不感兴趣的人可以安全地继续阅读第二部分。
第一部分
回到遥远的过去,我们需要尽快建立 CI/CD 流程。 条件之一是不使用Docker 用于部署 开发组件有几个原因:
- 为了生产中的组件更可靠、更稳定的运行(实际上就是不使用虚拟化的要求)
- 领先的开发人员不想使用 Docker(很奇怪,但事实就是如此)
- 研发管理的思想基础
MVP 的基础设施、堆栈和大致初始要求如下:
- 4 台采用 Debian 的 Intel® X5650 服务器(已完全开发出一台更强大的机器)
- 自己的定制组件的开发是用C++、Python3进行的
- 主要使用的第 3 方工具:Kafka、Clickhouse、Airflow、Redis、Grafana、Postgresql、Mysql 等
- 用于单独构建和测试组件以进行调试和发布的管道
在初始阶段需要解决的首要问题之一是如何在任何环境(CI / CD)中部署自定义组件。
我们决定系统地安装第三方组件并系统地更新它们。 用 C++ 或 Python 开发的自定义应用程序可以通过多种方式进行部署。 其中,例如:创建系统包,将其发送到构建镜像的存储库,然后将其安装到服务器上。 由于未知原因,选择了另一种方法,即:使用 CI,编译应用程序可执行文件,创建虚拟项目环境,从requirements.txt 安装 py 模块,并将所有这些工件与配置、脚本和附带应用环境到服务器。 接下来,应用程序以没有管理员权限的虚拟用户身份启动。
选择 Gitlab-CI 作为 CI/CD 系统。 最终的管道看起来像这样:
从结构上讲,gitlab-ci.yml 看起来像这样
---
variables:
# минимальная версия ЦПУ на серверах, где разворачивается кластер
CMAKE_CPUTYPE: "westmere"
DEBIAN: "MYREGISTRY:5000/debian:latest"
before_script:
- eval $(ssh-agent -s)
- ssh-add <(echo "$SSH_PRIVATE_KEY")
- mkdir -p ~/.ssh && echo -e "Host *ntStrictHostKeyChecking nonn" > ~/.ssh/config
stages:
- build
- testing
- deploy
debug.debian:
stage: build
image: $DEBIAN
script:
- cd builds/release && ./build.sh
paths:
- bin/
- builds/release/bin/
when: always
release.debian:
stage: build
image: $DEBIAN
script:
- cd builds/release && ./build.sh
paths:
- bin/
- builds/release/bin/
when: always
## testing stage
tests.codestyle:
stage: testing
image: $DEBIAN
dependencies:
- release.debian
script:
- /bin/bash run_tests.sh -t codestyle -b "${CI_COMMIT_REF_NAME}_codestyle"
tests.debug.debian:
stage: testing
image: $DEBIAN
dependencies:
- debug.debian
script:
- /bin/bash run_tests.sh -e codestyle/test_pylint.py -b "${CI_COMMIT_REF_NAME}_debian_debug"
artifacts:
paths:
- run_tests/username/
when: always
expire_in: 1 week
tests.release.debian:
stage: testing
image: $DEBIAN
dependencies:
- release.debian
script:
- /bin/bash run_tests.sh -e codestyle/test_pylint.py -b "${CI_COMMIT_REF_NAME}_debian_release"
artifacts:
paths:
- run_tests/username/
when: always
expire_in: 1 week
## staging stage
deploy_staging:
stage: deploy
environment: staging
image: $DEBIAN
dependencies:
- release.debian
script:
- cd scripts/deploy/ &&
python3 createconfig.py -s $CI_ENVIRONMENT_NAME &&
/bin/bash install_venv.sh -d -r ../../requirements.txt &&
python3 prepare_init.d.py &&
python3 deploy.py -s $CI_ENVIRONMENT_NAME
when: manual
值得注意的是,组装和测试是在其自己的映像上进行的,其中已经安装了所有必要的系统软件包并进行了其他设置。
虽然jobs中的这些脚本各有各的有趣,但是我当然不会谈论它们,每一个的描述都会花费很多时间,这也不是本文的目的。 我只会提请您注意部署阶段由一系列调用脚本组成的事实:
- 创建配置.py - 创建一个settings.ini 文件,其中包含各种环境中的组件设置,以供后续部署(预生产、生产、测试...)
- install_venv.sh - 在特定目录中为py组件创建虚拟环境并将其复制到远程服务器
- 准备初始化.d.py — 根据模板为组件准备启停脚本
- 部署文件 - 分解并重新启动新组件
时间飞逝。 舞台阶段被预制作和制作所取代。 在另一发行版 (CentOS) 上添加了对该产品的支持。 添加了 5 台更强大的物理服务器和十几台虚拟服务器。 对于开发人员和测试人员来说,在或多或少接近工作状态的环境中测试他们的任务变得越来越困难。 这时候才明白,没有他是不可能的……
第二部分
因此,我们的集群是一个由数十个独立组件组成的壮观系统,而 Dockerfile 并未描述这些组件。 一般情况下,您只能将其配置为部署到特定环境。 我们的任务是在预发布测试之前将集群部署到临时环境中进行测试。
理论上,可以有多个集群同时运行:与处于已完成状态或接近完成状态的任务一样多。 我们可以使用的服务器的容量允许我们在每台服务器上运行多个集群。 每个暂存集群必须是隔离的(端口、目录等不能有交集)。
我们最宝贵的资源是我们的时间,而我们的时间并不多。
为了更快地启动,我们选择了 Docker Swarm,因为它简单且架构灵活。 我们做的第一件事是在远程服务器上创建一个管理器和几个节点:
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
kilqc94pi2upzvabttikrfr5d nop-test-1 Ready Active 19.03.2
jilwe56pl2zvabupryuosdj78 nop-test-2 Ready Active 19.03.2
j5a4yz1kr2xke6b1ohoqlnbq5 * nop-test-3 Ready Active Leader 19.03.2
接下来,创建一个网络:
$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm
接下来,我们连接了 Gitlab-CI 和 Swarm 节点,从 CI 远程控制节点:安装证书、设置秘密变量以及在控制服务器上设置 Docker 服务。 这个
接下来,我们将堆栈创建和销毁作业添加到 .gitlab-ci .yml。
.gitlab-ci .yml 中添加了更多工作
## staging stage
deploy_staging:
stage: testing
before_script:
- echo "override global 'before_script'"
image: "REGISTRY:5000/docker:latest"
environment: staging
dependencies: []
variables:
DOCKER_CERT_PATH: "/certs"
DOCKER_HOST: tcp://10.50.173.107:2376
DOCKER_TLS_VERIFY: 1
CI_BIN_DEPENDENCIES_JOB: "release.centos.7"
script:
- mkdir -p $DOCKER_CERT_PATH
- echo "$TLSCACERT" > $DOCKER_CERT_PATH/ca.pem
- echo "$TLSCERT" > $DOCKER_CERT_PATH/cert.pem
- echo "$TLSKEY" > $DOCKER_CERT_PATH/key.pem
- docker stack deploy -c docker-compose.yml ${CI_ENVIRONMENT_NAME}_${CI_COMMIT_REF_NAME} --with-registry-auth
- rm -rf $DOCKER_CERT_PATH
when: manual
## stop staging stage
stop_staging:
stage: testing
before_script:
- echo "override global 'before_script'"
image: "REGISTRY:5000/docker:latest"
environment: staging
dependencies: []
variables:
DOCKER_CERT_PATH: "/certs"
DOCKER_HOST: tcp://10.50.173.107:2376
DOCKER_TLS_VERIFY: 1
script:
- mkdir -p $DOCKER_CERT_PATH
- echo "$TLSCACERT" > $DOCKER_CERT_PATH/ca.pem
- echo "$TLSCERT" > $DOCKER_CERT_PATH/cert.pem
- echo "$TLSKEY" > $DOCKER_CERT_PATH/key.pem
- docker stack rm ${CI_ENVIRONMENT_NAME}_${CI_COMMIT_REF_NAME}
# TODO: need check that stopped
when: manual
从上面的代码片段中,您可以看到Pipelines中添加了两个按钮(deploy_staging、stop_staging),需要手动操作。
堆栈名称与分支名称相匹配,并且这种唯一性应该足够了。 堆栈中的服务接收唯一的 IP 地址、端口、目录等。 将被隔离,但堆栈之间是相同的(因为所有堆栈的配置文件都是相同的) - 这是我们想要的。 我们使用以下方式部署堆栈(集群) 泊坞窗,compose.yml,它描述了我们的集群。
泊坞窗,compose.yml
---
version: '3'
services:
userprop:
image: redis:alpine
deploy:
replicas: 1
placement:
constraints: [node.id == kilqc94pi2upzvabttikrfr5d]
restart_policy:
condition: none
networks:
nw_swarm:
celery_bcd:
image: redis:alpine
deploy:
replicas: 1
placement:
constraints: [node.id == kilqc94pi2upzvabttikrfr5d]
restart_policy:
condition: none
networks:
nw_swarm:
schedulerdb:
image: mariadb:latest
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
MYSQL_DATABASE: schedulerdb
MYSQL_USER: ****
MYSQL_PASSWORD: ****
command: ['--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci', '--explicit_defaults_for_timestamp=1']
deploy:
replicas: 1
placement:
constraints: [node.id == kilqc94pi2upzvabttikrfr5d]
restart_policy:
condition: none
networks:
nw_swarm:
celerydb:
image: mariadb:latest
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
MYSQL_DATABASE: celerydb
MYSQL_USER: ****
MYSQL_PASSWORD: ****
deploy:
replicas: 1
placement:
constraints: [node.id == kilqc94pi2upzvabttikrfr5d]
restart_policy:
condition: none
networks:
nw_swarm:
cluster:
image: $CENTOS7
environment:
- CENTOS
- CI_ENVIRONMENT_NAME
- CI_API_V4_URL
- CI_REPOSITORY_URL
- CI_PROJECT_ID
- CI_PROJECT_URL
- CI_PROJECT_PATH
- CI_PROJECT_NAME
- CI_COMMIT_REF_NAME
- CI_BIN_DEPENDENCIES_JOB
command: >
sudo -u myusername -H /bin/bash -c ". /etc/profile &&
mkdir -p /storage1/$CI_COMMIT_REF_NAME/$CI_PROJECT_NAME &&
cd /storage1/$CI_COMMIT_REF_NAME/$CI_PROJECT_NAME &&
git clone -b $CI_COMMIT_REF_NAME $CI_REPOSITORY_URL . &&
curl $CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/artifacts/$CI_COMMIT_REF_NAME/download?job=$CI_BIN_DEPENDENCIES_JOB -o artifacts.zip &&
unzip artifacts.zip ;
cd /storage1/$CI_COMMIT_REF_NAME/$CI_PROJECT_NAME/scripts/deploy/ &&
python3 createconfig.py -s $CI_ENVIRONMENT_NAME &&
/bin/bash install_venv.sh -d -r ../../requirements.txt &&
python3 prepare_init.d.py &&
python3 deploy.py -s $CI_ENVIRONMENT_NAME"
deploy:
replicas: 1
placement:
constraints: [node.id == kilqc94pi2upzvabttikrfr5d]
restart_policy:
condition: none
tty: true
stdin_open: true
networks:
nw_swarm:
networks:
nw_swarm:
external: true
在这里您可以看到组件通过一个网络 (nw_swarm) 连接并且彼此可用。
系统组件(基于redis、mysql)与通用自定义组件池分离(在计划中,自定义组件被划分为服务)。 我们集群的部署阶段看起来就像将 CMD 传递到我们的一个大型配置映像中,一般来说,实际上与第一部分中描述的部署没有什么不同。我将重点介绍差异:
- git 克隆... - 获取部署所需的文件(createconfig.py、install_venv.sh 等)
- 卷曲... && 解压... - 下载并解压构建工件(编译实用程序)
只有一个尚未描述的问题:开发人员的浏览器无法访问具有 Web 界面的组件。 我们使用反向代理来解决这个问题,因此:
在.gitlab-ci.yml中,部署集群堆栈后,我们添加部署平衡器的行(在提交时,仅更新其配置(根据模板创建新的nginx配置文件:/etc/nginx/conf.d) d/${CI_COMMIT_REF_NAME}.conf) - 请参阅 docker-compose-nginx.yml 代码)
- docker stack deploy -c docker-compose-nginx.yml ${CI_ENVIRONMENT_NAME} --with-registry-auth
docker-compose-nginx.yml
---
version: '3'
services:
nginx:
image: nginx:latest
environment:
CI_COMMIT_REF_NAME: ${CI_COMMIT_REF_NAME}
NGINX_CONFIG: |-
server {
listen 8080;
server_name staging_${CI_COMMIT_REF_NAME}_cluster.dev;
location / {
proxy_pass http://staging_${CI_COMMIT_REF_NAME}_cluster:8080;
}
}
server {
listen 5555;
server_name staging_${CI_COMMIT_REF_NAME}_cluster.dev;
location / {
proxy_pass http://staging_${CI_COMMIT_REF_NAME}_cluster:5555;
}
}
volumes:
- /tmp/staging/nginx:/etc/nginx/conf.d
command:
/bin/bash -c "echo -e "$$NGINX_CONFIG" > /etc/nginx/conf.d/${CI_COMMIT_REF_NAME}.conf;
nginx -g "daemon off;";
/etc/init.d/nginx reload"
ports:
- 8080:8080
- 5555:5555
- 3000:3000
- 443:443
- 80:80
deploy:
replicas: 1
placement:
constraints: [node.id == kilqc94pi2upzvabttikrfr5d]
restart_policy:
condition: none
networks:
nw_swarm:
networks:
nw_swarm:
external: true
在开发计算机上,更新 /etc/hosts; 指定 nginx 的 url:
10.50.173.106 staging_BRANCH-1831_cluster.dev
因此,隔离的临时集群的部署已经实现,开发人员现在可以运行足以检查其任务的任意数量的集群。
未来的计划:
- 将我们的组件分离为服务
- 每个 Dockerfile 都有
- 自动检测堆栈中负载较少的节点
- 按名称模式指定节点(而不是像文章中那样使用 id)
- 添加堆栈是否被破坏的检查
- ...