WIADOMOŚCI

Optymalizacja wdrażania w wielu centrach danych – Docker

Published on:26 / April / 2016

Wykorzystanie rejestru Docker do wdrażania usług w trakcie produkcji

 
W Infobip prowadzimy ponad 400 usług w 6 centrach danych. Zespoły programistów wdrażają usługi co kilka minut – obecnie około 90 wdrożeń dziennie.
 
Aby zapewnić spójny i ustandaryzowany interfejs wdrażania, wprowadziliśmy wdrażanie usług jako kontenerów Docker. Zespoły mogą tworzyć pakiety usług jako obrazy Docker i wdrażać je w ten sam sposób, niezależnie od technologii lub języka użytego do stworzenia usługi.
 
Ponieważ obrazy Docker mogą być dość duże (setki megabajtów), już na wczesnym etapie wiedzieliśmy, że efektywna dystrybucja obrazów Docker do wszystkich centrów danych będzie stanowić wyzwanie.
 

Efektywna dystrybucja obrazów usług Docker do wszystkich centrów danych

 
Podczas wdrażania pakietu usług w formie obrazu Docker jako kontenera Docker na maszynie centrum danych chcemy wykorzystywać zassanie docker do pobrania obrazów Docker na tę maszynę i uruchomienia kontenera.
 
Przechowujemy obrazy Docker naszych aplikacji w prywatnym rejestrze centralnym Docker – wykorzystujemy do tego Artifactory, który działa jako rejestr Docker. Wybór ten był oczywisty, ponieważ Artifactory wykorzystujemy już do przechowywania wszystkich innych artefaktów.
 
Moglibyśmy przeprowadzać zassanie docker bezpośrednio z centralnego repozytorium Artifactory Docker przy każdym wdrożeniu, ale wiązałoby się to ze znaczącym obciążeniem krzyżowym centralnej sieci danych podczas przesyłania tego samego obrazu Docker na różne maszyny.
 
Dla ilustracji: jeden obraz Docker aplikacji Java Infobip składa się z 2 warstw: warstwy podstawowej –około 160 MB (instalacja alpine linux + oracle jdk) oraz warstwy obrazu aplikacji – 60 MB (aplikacja Spring Java).
 
 
Aby wdrożyć tę samą aplikację na 10 różnych maszynach w tym samym centrum danych, musielibyśmy przesłać łącznie 10*60 MB do centrów danych (z centralnego repozytorium na maszyny w zdalnych centrach danych) – i to w najlepszym scenariuszu, w którym zakładamy, że obraz podstawowy Java Docker znajduje się już na maszynie docelowej.
 
Takie dodatkowe obciążenie sieci można zminimalizować, wykorzystując rejestr Docker lokalnego centrum danych w roli pośredniej pamięci podręcznej centralnego prywatnego repozytorium Docker.
 
Przy wykorzystaniu rejestru pośredniej pamięci podręcznej w postaci lokalnego centrum danych Docker w tym samym scenariuszu obejmującym 10 centrów danych tylko warstwa aplikacji Docker (60 MB) zostałaby przesłana przez centra danych; pozostałe 9 maszyn otrzymałoby warstwę aplikacji z pamięci podręcznej w lokalnym rejestrze (w tym scenariuszu zakłada się, że tych 10 maszyn zostało wdrożonych sekwencyjnie, tak jak odbywa się to obecnie).
 
 
Moglibyśmy także zhackować przepływ obrazu Docker eksport-transport- pamięć podręczna-import, ale postanowiliśmy wypróbować otwartoźródłowy rejestr Docker, który ma wbudowaną funkcję pośrednika i pamięci podręcznej.
 

Wypróbowanie rejestru Docker

 
Uruchomienie prywatnego rejestru Docker jest proste. Poniżej przedstawiono opis tego procesu: https://docs.Docker.com/registry
 
Skonfigurowanie rejestru Docker do roli pośredniej pamięci podręcznej również nie jest skomplikowane, istnieje też przewodnik opisujący ten proces, w związku z czym mogliśmy szybko uruchomić odbicie rejestru Docker w trybie zassania z pamięci podręcznej. Okazało się jednak, że nie działa to tak, jak mieliśmy nadzieję :(
 
Jak stwierdzono w tym dokumencie, obecnie nie ma możliwości odzwierciedlenia innego prywatnego rejestru:
 
 
Ponieważ naprawdę podobał nam się pomysł zaoszczędzenia przepustowości i skrócenia czasu wdrażania, byliśmy zdeterminowani, żeby jakoś obejść tę brakującą funkcję rejestru Docker.
 

Rozwiązanie

 
Artifactory umożliwia API Docker kontakt ze wszystkimi hostowanymi przez siebie repozytoriami Docker – na przykład, aby uzyskać dostępne znaczniki obrazów busybox przechowywanych w lokalnym repozytorium Artifactory-Docker, możemy wysłać następujące żądanie:
 
 $ curl -u mzagar http://artifactory:8081/artifactory/api/docker/docker-local/v2/busybox/tags/list 
Enter host password for user 'mzagar': 
{ 
"name" : "busybox", 
"tags" : [ "latest" ] 
} 
 
Ponieważ rejestr Docker może obecnie odzwierciedlać jedynie centralny publiczny Docker Hub, wpadliśmy na pomysł, aby przechwytywać każde żądanie HTTP, które odbicie rejestru Docker wykonywałoby na zdalny adres URL rejestru Docker i przeformułowywałoby zgodnie z adresem URL API Artifactory Docker.
 
Po prostu sprawilibyśmy, żeby odbicie rejestru Docker myślało, że kontaktuje się z Docker Central Hub, a tak naprawdę kontaktowało się z repozytorium Artifactory Docker.
 

Trochę magii HAProxy

 
Aby przeformułować żądanie HTTP wysyłane przez rejestr Docker do centralnego rejestru Docker, wykorzystaliśmy HAProxy – oczywiście uruchomiony jako kontener Docker.
 
Oto konfiguracja HAProxy, która określa, jak przeprowadzić przeformułowanie:
haproxy.cfg
 
 global 
log 127.0.0.1 local0 
log 127.0.0.1 local1 notice 

defaults 
log global 
mode http 
option httplog 
option dontlognull 
option forwardfor 
timeout connect 5000ms 
timeout client 60000ms 
timeout server 60000ms 
stats uri /admin?stats 

frontend docker 
bind *:80 
mode http 
default_backend artifactory 

backend artifactory 
reqirep ^([^\ ]*)\ /v2/(.*) \1\ /artifactory/api/docker/docker-local/v2/\2 
http-request add-header Authorization Basic\ %[env(BASIC_AUTH_PASSWORD)] 
server artifactory ${ARTIFACTORY_IP}:${ARTIFACTORY_PORT} check 
 
Każde żądanie /v2/* wysłane przez odbicie Docker zostaje przeformułowane na /artifactory/api/docker/docker- local/v2/* i wysłane do naszego serwera Artifactory.
 
Wykorzystujemy zmienne środowiskowe w celu określenia IP Artifactory i portu oraz dodania stałego nagłówka uwierzytelnienia w celu uwierzytelnienia jako ważnego użytkownika Artifactory.
 
Ta konfiguracja ujawnia także statystyki HAProxy dla serwera front-end i back-end, co umożliwia wygodne sprawdzenie, czy Artifactory działa i jest dostępny oraz ile ruchu generuje odbicie.
 

Podłączenie do rejestru Docker

 
Następnie musimy podłączyć rejestr Docker, aby kontaktował się z HAProxy, który z kolei następnie przekieruje żądanie HTTP tam, gdzie chcemy i tak, jak chcemy – my zrobiliśmy to przy użyciu docker-compose. Oto jak wygląda kompletny plik docker-compose.yml:
docker-compose.yml
 
 haproxy: 
image: haproxy:latest 
container_name: hap 
restart: always 
ports:
- 80:80 
volumes:
- ${WORK}/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg 
environment:
- ARTIFACTORY_IP=${ARTIFACTORY_IP}
- ARTIFACTORY_PORT=${ARTIFACTORY_PORT}
- BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD} 
log_driver: "json-file" 
log_opt:
max-size: "10m"
max-file: "10" 

mirror:
image: registry:2.4.0
container_name: registry restart: always ports: - 5000:5000
volumes:
- ${WORK}/docker-registry:/var/lib/registry
- ${REGISTRY_CERTIFICATE_FOLDER}:/certs
environment:
- REGISTRY_HTTP_TLS_CERTIFICATE=${REGISTRY_HTTP_TLS_CERTIFICATE}
- REGISTRY_HTTP_TLS_KEY=${REGISTRY_HTTP_TLS_KEY}
command: serve /var/lib/registry/config.yml
links:
- haproxy:haproxy
log_driver: "json-file"
log_opt:
max-size: "10m"
max-file: "10" 
 
Wykorzystujemy dużo zmiennych środowiskowych, aby zachować elastyczność przy uruchamianiu tego duetu aplikacji. Compose oczekuje haproxy.cfg w folderze HAProxy i pliku config.yml rejestru Docker w folderze docker-registry odpowiadającym danemu folderowi ROBOCZEMU.
 
Zastosujemy prosty skrypt bash, aby wyposażyć docker-compose we wszystkie niezbędne zmienne środowiskowe:
 
run-mirror.sh
 
 #!/bin/sh  

export WORK=`pwd` 
export ARTIFACTORY_IP='10.10.10.10' 
export ARTIFACTORY_PORT='8081' 
export REGISTRY_CERTIFICATE_FOLDER='/etc/ssl/example' 
export REGISTRY_HTTP_TLS_CERTIFICATE='/certs/cert.pem' 
export REGISTRY_HTTP_TLS_KEY='/certs/cert.pem' 
export BASIC_AUTH_PASSWORD='basicauthbase64encodedstring==' 

docker-compose up --force-recreate
 
Po uruchomieniu docker-compose odbicie jest dostępne pod adresem mirror.ib-ci.com:5000 – najpierw sprawdzamy zawartość odbicia:
 
 $ ./run-mirror.sh 
$ curl https://mirror.example.com:5000/v2/_catalog {"repositories":[]} 
 
Tak jak się spodziewaliśmy, pamięć podręczna odbicia jest pusta. Teraz zasysamy obraz busybox z naszego centralnego repozytorium Docker i mierzymy, jak długo trwa pobieranie obrazu:
 
 $ time docker pull mirror.example.com:5000/busybox 
Using default tag: 
latest latest: Pulling from busybox 
9d7588d3c063: Pull complete 
a3ed95caeb02: Pull complete 
Digest: sha256:000409ca75cd0b754155d790402405fdc35f051af1917ae35a9f4d96ec06ae50 
Status: Downloaded newer image for mirror.example.com:5000/busybox:latest 
real 0m 3.66s 
user 0m 0.02s 
sys 0m 0.00s 
 
Pierwsze pobranie obrazu zajęło 3,5 sekundy.
 
Widzimy, że obraz busybox jest teraz przechowywany w pamięci podręcznej odbicia Docker.
 
 $ curl https://mirror.example.com:5000/v2/_catalog 
{"repositories":["busybox"]} 
 
Usuńmy lokalny obraz busybox i ponownie go zassijmy – spodziewamy się, że drugie zassanie przebiegnie znacznie szybciej niż pierwsze, ponieważ obraz znajduje się już w pamięci development teams. podręcznej i odbicie nie musi go pobierać z centralnego repozytorium.
 
$ docker rmi mirror.example.com:5000/busybox 
Untagged: mirror.example.com:5000/busybox:latest 
Deleted: sha256:a84c36ecc374f680d00a625d1f0ba52426a536775ee7277f21728369dc42499b 
Deleted: sha256:1a879e2f481d67c4537144f80f5f6d776542c7d3a0bd7721fdf6aa1ec024af24 
Deleted: sha256:a193ed10c686545c776af2bb8cfe20d3e5badf5c936fbf0e8f389d769018a3f9 

$ time docker pull mirror.example.com:5000/busybox 
Using default tag: latest 
latest: Pulling from busybox 
9d7588d3c063: Pull complete 
a3ed95caeb02: Pull complete 
Digest: sha256:000409ca75cd0b754155d790402405fdc35f051