Docker Swarm: jak přesunout tasky na jiné nodes bez downtime
Jednou za čas je potřeba aplikovat systémový update, změnit nastavení docker daemonu, nebo z jiného důvodu na chvíli vypnout node ve swarmu. Jak to udělat bez downtime?
Součástí Docker CLI je příkaz docker node update, který nám pomocí parametru --availability umožňuje ovládat, jak jednotlivé nodes ve swarmu přijímají nové tasky. Pokud chceme node vyprázdnit, stačí nám použít příkaz docker node update NODE_NAME --availability=drain. Přechodem ze stavu active do drain se novým "cílem" pro tento node stane mít 0 běžících tasků. Swarm toto zařídí jednoduše tak, že všechny běžící tasky ukončí a přesune je jinam. Pokud jsou ale tyto tasky jedinými instancemi svých services, je výsledkem krátký downtime. Jak tedy zařídit, aby se při přesouvání tasků použilo naše nastavení pro rolling updates?
Klíčem je nevynutit přesunutí tasků nastavením availability=drain, ale místo toho použít třetí možnost, availability=pause. V tomto stavu node nepřijímá nové tasky, ale zároveň se nesnaží "násilím" přesunout ty stávající pryč. Potom stačí všechny services, jejichž instance běží na nodu, který chceme vyprázdnit, přesunout stejným způsobem, jako při nasazení nové verze.
Krok po kroku
- Nejprve zkontrolujeme, v jakém stavu cluster je.
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
o81cq30ogz2x34goarbs6xamo * manager1 Ready Active Leader 20.10.7
kv9gwot1u35id1bzmaktl4lhw worker1 Ready Active 20.10.7
px15r84ksn8uqe02wls5egopl worker2 Ready Active 20.10.7
rrsb7disoubuckfz2h7e47hu6 worker3 Ready Active 20.10.7
2. Potřebujeme "vyprázdnit" worker1, nastavíme tedy availability na pause.
$ docker node update worker1 --availability=pause
node1
3. Můžeme se přesvědčit, že stav se změnil, ale všechny služby jedou dál.
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
o81cq30ogz2x34goarbs6xamo * manager1 Ready Active Leader 20.10.7
kv9gwot1u35id1bzmaktl4lhw worker1 Pause Active 20.10.7
px15r84ksn8uqe02wls5egopl worker2 Ready Active 20.10.7
rrsb7disoubuckfz2h7e47hu6 worker3 Ready Active 20.10.7
4. Vyfiltrujeme si tasky, které běží na worker1, a pro každý najdeme ID jeho service.
$ SERVICE_IDS=($(docker node ps worker1 --filter desired-state=running --format '{{ .ID }}' | xargs docker inspect --format '{{ .ServiceID }}'))
5. Pro každou service spustíme force-update. Tím vynutíme přesunutí jejích tasků na nodes, které aktuálně přijímají nové tasky. Tento update ale respektuje naše nastavení rolling updates.
$ for s in ${SERVICE_IDS[@]}; do
docker service update --force "$s"
done
6. Až proces doběhne (v závislosti na rychlosti přenasazení to může chvíli trvat), můžeme bezpečně provést maintenance, restartovat server kvůli updatu kernelu, atp.
7. Na závěr stačí přepnout node zpátky do stavu active.
$ docker node update worker1 --availability=active
Celý proces můžeme ještě zabalit do jednoduchého skriptu, abychom si příště ušetřili práci: ssh manager1 "bash -s" -- < drain.sh NODE_NAME
#!/bin/bash
if [[ "$#" -ne 1 ]]; then
echo "Invalid arguments. Usage: $0 NODE_NAME"
exit 1
fi
NODE="$1"
# check if node exists
docker node ps $NODE > /dev/null
if [[ $? -ne 0 ]]; then
echo "Invalid NODE_NAME"
exit 1
fi
# set availability to pause - this prevents the node from receiving new tasks
echo "Current swarm state:"
docker node ls
docker node update "$NODE" --availability=pause
echo "Node availability updated. New swarm state:"
docker node ls
echo
# get tasks running on the node and find the services they belong to
echo "Getting services..."
SERVICE_IDS=($(docker node ps "$NODE" --filter desired-state=running --format '{{ .ID }}' | xargs docker inspect --format '{{ .ServiceID }}'))
echo "Found ${#SERVICE_IDS[@]} services."
# iterate through services and force-update each of them
for s in ${SERVICE_IDS[@]}; do
echo
SERVICE_NAME=$(docker service inspect "$s" --format '{{ .Spec.Name }}')
echo "Processing service: $SERVICE_NAME"
docker service update --force "$s" > /dev/null
echo "Done."
done