Das Online-Empfehlungssystem für Videoinhalte, an dem wir arbeiten, ist eine geschlossene kommerzielle Entwicklung und technisch gesehen ein Multikomponenten-Cluster aus proprietären und Open-Source-Komponenten. Der Zweck des Schreibens dieses Artikels besteht darin, die Implementierung des Docker-Swarm-Clustering-Systems für eine Staging-Site zu beschreiben, ohne den etablierten Arbeitsablauf unserer Prozesse in einer begrenzten Zeit zu stören. Die Ihnen präsentierte Erzählung ist in zwei Teile gegliedert. Der erste Teil beschreibt CI/CD vor der Verwendung von Docker Swarm und der zweite Teil beschreibt den Prozess seiner Implementierung. Wer kein Interesse daran hat, den ersten Teil zu lesen, kann getrost zum zweiten übergehen.
Teil I
Damals, in einem fernen, fernen Jahr, galt es, den CI/CD-Prozess so schnell wie möglich einzurichten. Eine der Bedingungen war, Docker nicht zu verwenden für den Einsatz entwickelte Komponenten aus mehreren Gründen:
- für einen zuverlässigeren und stabileren Betrieb von Komponenten in der Produktion (das ist eigentlich die Anforderung, keine Virtualisierung zu verwenden)
- führende Entwickler wollten nicht mit Docker arbeiten (komisch, aber so war es)
- entsprechend den ideologischen Überlegungen der F&E-Leitung
Infrastruktur, Stack und ungefähre Anfangsanforderungen für MVP wurden wie folgt dargestellt:
- 4 Intel® X5650-Server mit Debian (eine weitere leistungsstarke Maschine ist vollständig entwickelt)
- Die Entwicklung eigener benutzerdefinierter Komponenten erfolgt in C++, Python3
- Wichtigste verwendete Tools von Drittanbietern: Kafka, Clickhouse, Airflow, Redis, Grafana, Postgresql, Mysql, …
- Pipelines zum separaten Erstellen und Testen von Komponenten für Debug und Release
Eine der ersten Fragen, die in der Anfangsphase geklärt werden muss, ist, wie benutzerdefinierte Komponenten in einer beliebigen Umgebung (CI/CD) bereitgestellt werden.
Wir haben uns entschieden, Komponenten von Drittanbietern systematisch zu installieren und systematisch zu aktualisieren. Benutzerdefinierte Anwendungen, die in C++ oder Python entwickelt wurden, können auf verschiedene Arten bereitgestellt werden. Darunter zum Beispiel: Systempakete erstellen, sie an das Repository der erstellten Images senden und sie dann auf Servern installieren. Aus einem unbekannten Grund wurde eine andere Methode gewählt, nämlich: Mit CI werden ausführbare Anwendungsdateien kompiliert, eine virtuelle Projektumgebung erstellt, Py-Module aus „requirements.txt“ installiert und alle diese Artefakte werden zusammen mit Konfigurationen, Skripten usw. gesendet begleitende Anwendungsumgebung bis hin zu Servern. Anschließend werden Anwendungen als virtueller Benutzer ohne Administratorrechte gestartet.
Als CI/CD-System wurde Gitlab-CI gewählt. Die resultierende Pipeline sah etwa so aus:
Strukturell sah gitlab-ci.yml so aus
---
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
Bemerkenswert ist, dass der Zusammenbau und Test auf einem eigenen Image erfolgt, auf dem bereits alle notwendigen Systempakete installiert und weitere Einstellungen vorgenommen wurden.
Obwohl jedes dieser Skripte in Jobs auf seine Art interessant ist, werde ich natürlich nicht darüber sprechen. Die Beschreibung jedes einzelnen von ihnen wird viel Zeit in Anspruch nehmen und das ist nicht der Zweck des Artikels. Ich möchte Ihre Aufmerksamkeit nur auf die Tatsache lenken, dass die Bereitstellungsphase aus einer Folge von Aufrufskripten besteht:
- createconfig.py - Erstellt eine Settings.ini-Datei mit Komponenteneinstellungen in verschiedenen Umgebungen für die spätere Bereitstellung (Vorproduktion, Produktion, Tests, ...)
- install_venv.sh – Erstellt eine virtuelle Umgebung für Py-Komponenten in einem bestimmten Verzeichnis und kopiert sie auf Remote-Server
- Prepare_init.d.py – bereitet Start-Stopp-Skripte für die Komponente basierend auf der Vorlage vor
- Deploy.py - Zerlegt und startet neue Komponenten neu
Zeit verging. Die Inszenierungsphase wurde durch Vorproduktion und Produktion ersetzt. Unterstützung für das Produkt auf einer weiteren Distribution (CentOS) hinzugefügt. Fünf leistungsstärkere physische Server und ein Dutzend virtuelle Server hinzugefügt. Und es wurde für Entwickler und Tester immer schwieriger, ihre Aufgaben in einer Umgebung zu testen, die mehr oder weniger betriebsbereit war. Zu diesem Zeitpunkt wurde klar, dass es ohne ihn unmöglich war ...
Teil II
Unser Cluster ist also ein spektakuläres System aus ein paar Dutzend separaten Komponenten, die nicht von Dockerfiles beschrieben werden. Sie können es im Allgemeinen nur für die Bereitstellung in einer bestimmten Umgebung konfigurieren. Unsere Aufgabe besteht darin, den Cluster in einer Staging-Umgebung bereitzustellen, um ihn vor den Tests vor der Veröffentlichung zu testen.
Theoretisch können mehrere Cluster gleichzeitig laufen: so viele, wie es Aufgaben im abgeschlossenen Zustand oder kurz vor dem Abschluss gibt. Die Kapazitäten der uns zur Verfügung stehenden Server ermöglichen es uns, auf jedem Server mehrere Cluster zu betreiben. Jeder Staging-Cluster muss isoliert sein (es darf keine Überschneidungen bei Ports, Verzeichnissen usw. geben).
Unsere wertvollste Ressource ist unsere Zeit, und wir hatten nicht viel davon.
Für einen schnelleren Start haben wir uns aufgrund seiner Einfachheit und Architekturflexibilität für Docker Swarm entschieden. Als erstes haben wir einen Manager und mehrere Knoten auf den Remote-Servern erstellt:
$ 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
Als nächstes erstellen Sie ein Netzwerk:
$ docker network create --driver overlay --subnet 10.10.10.0/24 nw_swarm
Als nächstes haben wir Gitlab-CI- und Swarm-Knoten im Hinblick auf die Fernsteuerung von Knoten von CI verbunden: Installieren von Zertifikaten, Festlegen geheimer Variablen und Einrichten des Docker-Dienstes auf dem Steuerungsserver. Dieses hier
Als nächstes haben wir Jobs zur Stapelerstellung und -zerstörung zu .gitlab-ci .yml hinzugefügt.
Zu .gitlab-ci .yml wurden einige weitere Jobs hinzugefügt
## 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
Aus dem obigen Codeausschnitt können Sie ersehen, dass Pipelines zwei Schaltflächen (deploy_staging, stop_staging) hinzugefügt wurden, die eine manuelle Aktion erfordern.
Der Stack-Name stimmt mit dem Branch-Namen überein und diese Eindeutigkeit sollte ausreichend sein. Dienste im Stapel erhalten eindeutige IP-Adressen sowie Ports, Verzeichnisse usw. wird isoliert, aber von Stapel zu Stapel gleich (da die Konfigurationsdatei für alle Stapel gleich ist) – was wir wollten. Wir stellen den Stack (Cluster) mithilfe von bereit docker-compose.yml, was unseren Cluster beschreibt.
docker-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
Hier ist zu erkennen, dass die Komponenten über ein Netzwerk (nw_swarm) verbunden sind und untereinander verfügbar sind.
Systemkomponenten (basierend auf Redis, MySQL) werden vom allgemeinen Pool benutzerdefinierter Komponenten getrennt (in Plänen werden benutzerdefinierte Komponenten als Dienste unterteilt). Die Bereitstellungsphase unseres Clusters ähnelt der Übergabe von CMD an unser einziges großes konfiguriertes Image und unterscheidet sich im Allgemeinen praktisch nicht von der in Teil I beschriebenen Bereitstellung. Ich werde die Unterschiede hervorheben:
- Git-Klon... - Holen Sie sich die für die Bereitstellung erforderlichen Dateien (createconfig.py, install_venv.sh usw.)
- locken... && entpacken... - Build-Artefakte (kompilierte Dienstprogramme) herunterladen und entpacken
Es gibt nur ein noch unbeschriebenes Problem: Komponenten, die über eine Weboberfläche verfügen, sind über die Browser der Entwickler nicht zugänglich. Wir lösen dieses Problem mithilfe eines Reverse-Proxys, also:
In .gitlab-ci.yml fügen wir nach der Bereitstellung des Cluster-Stacks die Zeile zur Bereitstellung des Balancers hinzu (der beim Festschreiben nur seine Konfiguration aktualisiert (erstellt neue Nginx-Konfigurationsdateien gemäß der Vorlage: /etc/nginx/conf). d/${CI_COMMIT_REF_NAME}.conf) – siehe docker-compose-nginx.yml-Code)
- 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
Aktualisieren Sie auf den Entwicklungscomputern /etc/hosts; Verschreiben Sie die URL für Nginx:
10.50.173.106 staging_BRANCH-1831_cluster.dev
Daher wurde die Bereitstellung isolierter Staging-Cluster implementiert und Entwickler können diese nun in beliebiger Anzahl ausführen, um ihre Aufgaben zu überprüfen.
Zukunftspläne:
- Trennen Sie unsere Komponenten als Dienste
- Haben Sie für jede Docker-Datei
- Erkennen Sie automatisch weniger belastete Knoten im Stapel
- Geben Sie Knoten nach Namensmuster an (anstatt wie im Artikel die ID zu verwenden).
- Fügen Sie eine Überprüfung hinzu, ob der Stapel zerstört ist
- ...