• 1. června 2022

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

  1. 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
career

Zajímá tě Docker Swarm?

Mrkni se, jestli by ses nám nehodil.