Extrém rendszeradminisztráció: djbware és társai

A Unix/Linux szerverek üzemeltetése wikiből
Előadó: Korn András (BME TMIT)
E-mail: korn.andras @ tmit . bme . hu
Az előadásvázlat URL-je:
https://unixlinux.tmit.bme.hu/index.php/Extrém_rendszeradminisztráció:_djbware_és_társai
  • Mi a djbware?
    • Daniel J. Bernstein (http://cr.yp.to/)
    • Nem szabad szoftver, hanem "licence-free software" (kivéve, amelyik public domain)
    • Fő tervezési szempontjai:
      • Egyszerűség és modularitás
        • Ezekre szélsőséges mértékben törekszik ("Unix-filozófia")
      • A konfiguráció könnyen feldolgozható legyen
        • Akár azon az áron is, hogy így több ész kell a megírásához
    • Tágabb értelemben djbware az is, amit nem DJB írt
  • Ismertebb djbware-ek:
    • qmail
    • djbdns
    • daemontools
  • De: ez itt egy szabadszoftveres konferencia
  • Úgyhogy a témánk a runit
    • A daemontools szabad reimplementációja

Tartalomjegyzék

1 A runit motivációja

  • Cseréljük le a System V initet!
  • Hogy mi a baj vele?
    • Maga az init program túl bonyolult, túl sok dolgot csinál
      • Pl. van benne "UPS-kezelés"
      • utmp/wtmp-kezelés
      • A respawnt is csinálhatná külső program
      • Feleslegesen lassú az elindítás/leállás
        • Sok feladat párhuzamosítható lenne
        • Ráadásul néhány disztribúcióban a csicsázás teszi ki a kód 50+%-át
    • Nem indítja újra a szolgáltatásokat, ha kilépnek
      • kivéve, ha az inittabból futtatjuk őket
      • de akkor nem tudjuk őket szelektíven leállítani
    • Az initscript örökli az őt indító shell környezetét
      • Vagyis parancssorból indítva a szolgáltatást nem biztos, hogy ugyanazt csinálja majd, mint bootkor
    • Nem triviális leállítani egy szolgáltatást, vagy signalt küldeni neki

1.1 Szolgáltatások leállítása á la init

  • Szolgáltatás leállítása: általában signalküldés
  • Csakhogy: melyik PID-nak?
  • Hja kérem, nem kellett volna daemonizálódni...
  • "Mi a gond? Ott a pidfile..."
    • Több processzből álló, processz poollal dolgozó szolgáltatás esetén elavulttá válhat
      • Főleg, ha eleve a start-stop-daemon hozta létre
    • Ha a pidfile helye konfigolható, indítás óta megváltozhatott a konfig, és talán már nem aktuális a pidfile elérési útja
    • Összetett configfile esetén az initscript esetleg nem találja meg az érvényes pidfile-beállítást (pl. apache2)
    • Vagyis: ha a pidfile-ból ki is olvasunk egy természetes számot, meg kell(ene) győződni arról, hogy az valóban a leállítandó szolgáltatáshoz tartozó processz PID-ja-e
      • Itt viszont versenyhelyzetbe kerülhetünk, mert a szám kiolvasása, hitelesítése és a signal elküldése között az eredeti processz kiléphet, és másik kaphatja meg a PID-ját
    • Még akkor is fennáll a versenyhelyzet, ha nem hitelesítjük a PID-t sehogy (ez amúgy rossz ötlet), mert a kiolvasás-signalküldés nem atomi művelet
      • a kiolvasás és a signalküldés között eltűnhet az eredeti processz és létrejöhet ugyanolyan PID-val másik
      • vagy csak eltűnhet az, aminek a signalt küldeni akartuk, de az is elég baj

Nézzünk erre egy példát (apache2, Ubuntu)!

ENV="env -i LANG=C PATH=/usr/local/bin:/usr/bin:/bin"
APACHE2="$ENV /usr/sbin/apache2"
APACHE2CTL="$ENV /usr/sbin/apache2ctl"
  • Környezettisztítás, de hasztalan:
  • Pl. mi lesz az ulimitekkel?
  • LD_*?
apache_stop() {
  PID=""
  PIDFILE=""
  AP_CONF=/etc/apache2/apache2.conf

  # apache2 allows more than PidFile entry in the config but only the
  # last found in the config is used; we attempt to follow includes
  # here, but only first-level includes are supported, not nested ones

  for i in $AP_CONF `awk '$1 ~ /^\s*[Ii]nclude$/ && $2 ~ /^\// {print $2}' $AP_CONF`; do
    PIDFILE=`grep -i ^PidFile $i | tail -n 1 | awk '{print $2}'`
    if [ -e "$PIDFILE" ]; then
      PID=`cat $PIDFILE`
    fi
  done

Vagyis, ha többszintű include-rendszer volt, már itt elvéreztünk.

  errors=`$APACHE2 -t 2>&1`

Vajon hibátlan-e a config?

  • Az apache hibás configgal nem indul
  • Vagyis ha a config hibás, de az apache fut, akkor a configot indítás után módosítottuk
  • Vagyis a megtalált PidFile direktíva kerülhetett oda az indítás után
  • Ha indítás óta módosítottuk a configot, de nem hibás, a script sehonnan se tudja meg
  if [ $? = 0 ]; then
    # if the config is ok then we just stop normally

    if [ -n "$PID" ]; then
      $APACHE2CTL stop

Ez vajon mit csinál? Tömören:

open("/var/run/apache2.pid", O_RDONLY)  = 4
read(4, "9155\n", 13)                   = 5
read(4, "", 8)                          = 0
close(4)                                = 0
kill(9155, SIG_0)                       = 0
kill(9155, SIGTERM)                     = 0

Vagyis majdnem vakon bízik a pidfile-ban, és itt is van versenyhelyzet.

Vissza a scriptre:

      CNT=0
      while [ 1 ]; do
        CNT=$(expr $CNT + 1)
        [ ! -d /proc/$PID ] && break
  • Ha kilépett a pidfájlban szereplő PID, mi is kilépünk
  • De: lehet hogy kilépett, és közben már indult azzal a PID-val más
  • Ekkor esetleg sosem lépünk ki, vagy mégis?
        if [ $CNT -gt 60 ]; then
          if [ "$VERBOSE" != "no" ]; then
            echo " ... failed!"
            echo "Apache2 failed to honor the stop command, please investigate the situation by hand."
          fi
          return 1
        fi
        sleep 1
      done
  • Ha elbukjuk a versenyhelyzetet, azért még kiírunk egy félrevezető hibaüzenetet
    else
      if [ "$VERBOSE" != "no" ]; then
        echo -n " ... no pidfile found! not running?"
      fi
    fi
  • Nem volt pidfile, jaj-jaj!
  • Ki is lépünk, nem csinálunk semmit

Lássuk, mi a helyzet, ha hibás volt a config:

  else
    [ "$VERBOSE" != "no" ] && echo "$errors"

    # if we are here something is broken and we need to try
    # to exit as nice and clean as possible

    # if pidof is null for some reasons the script exits automagically
    # classified as good/unknown feature

    PIDS=`pidof apache2` || true

    REALPID=0
    # if there is a pid we need to verify that belongs to apache2
    # for real
    for i in $PIDS; do
      if [ "$i" = "$PID" ]; then
        # in this case the pid stored in the
        # pidfile matches one of the pidof apache
        # so a simple kill will make it
        REALPID=1
      fi
    done

    if [ $REALPID = 1 ]; then
      # in this case everything is nice and dandy
      # and we kill apache2
      kill $PID

Mi történt?

  • Kerestünk apache2 nevű processzeket
    • Amúgy a pidof(8) is írja, hogy teljes path kéne
    • Lehet, hogy több apache2 példányunk is van, ekkor itt az összeset megtaláljuk
  • Ha a megtalált PID-k közül valamelyik szerepelt a pidfile-ban, kilőjük
  • De: hibás volt a config!
    • Hibás configgal az apache el se indul, ugye
    • Tehát a futó apache nem azzal a configgal indult
    • Tehát ki tudja, mit olvastunk ki a pidfile-ból
    • Arról nem is beszélve, hogy mikor; azóta már egy csomó idő eltelt
      • Processzek léphettek ki és születhettek
      • A pidfile tartalma is megváltozhatott
      • Ráadásul a pidof és a kill között is eltelik valamennyi idő
      • A múltban élünk...

Na és mi van, ha a pidof nem talál áldozatjelöltet?

    else
      # this is the worst situation... just kill all of them
      #for i in $PIDS; do
      #       kill $i
      #done
      # Except, we can't do that, because it's very, very bad
      if [ "$PIDS" ] && [ "$VERBOSE" != "no" ]; then
        echo " ... failed!"
        echo "You may still have some apache2 processes running.  There are"
        echo "processes named 'apache2' which do not match your pid file,"
        echo "and in the name of safety, we've left them alone.  Please review"
        echo "the situation by hand."
      fi
      return 1
    fi
  fi
}
  • Itt azért győzött a józanság szava... egyelőre :)
  • A gond a daemonizálódás
  • Ha az apache2 szépen az előtérben maradna, a szülője
    • tudná a PID-ját és
    • tudna neki signalt küldeni.

2 Megmentőnk, a runit

  • Ötlet: válasszuk szét az init feladatait!
  1. Rendszerindítás, futtatás, leállítás
  2. Szolgáltatások futtatása
  3. utmp-piszkálás
  4. Runlevel-váltás
  5. Naplózás (bár ilyet az init nem nagyon csinál)

Így fest majd a processzfánk:

runit-+-events/0
[...]
      |-runsvdir-+-runsv-+-dnscache
      |          |       `-svlogd
      |          |-runsv-+-run---sleep
      |          |       `-svlogd
      |          |-runsv-+-qmail-send-+-qmail-clean
      |          |       |            |-qmail-lspawn
      |          |       |            `-qmail-rspawn
      |          |       `-svlogd
      |          |-runsv-+-socklog
      |          |       `-svlogd]
      |          |-runsv---dd
      |          |-runsv---klogd
      |          |-runsv---cron
      |          |-runsv-+-munin-node
      |          |       `-svlogd
      |          |-8*[runsv---getty]
      |          |-runsv-+-ntpd
      |          |       `-svlogd
      |          |-runsv---sshd---sshd---zsh---screen
      |          |-runsv-+-smartd
      |          |       `-svlogd
      |          |-runsv---mdadm
      |          |-runsv---pound---pound---2*[{pound}]
      |          `-runsv-+-svlogd
      |                  `-tcpserver
[...]

Vagyis:

  • A runsvdir nézi, milyen szolgáltatások vannak
  • A runsv futtat egy-egy szolgáltatást
    • A szolgáltatás-leállítás pl. úgy megy, hogy az adott runsv-hez tartozó FIFO-ba írunk egy parancsot
  • Az svlogd naplózza egy-egy szolgáltatás üzeneteit

Előnyök a "sima" inithez képest:

  • A szolgáltatások
    • tiszta környezettel indulnak
    • újraindulnak, ha kilépnek
    • opcionálisan megbízható naplózást kapnak
      • naplóüzenet nem vész el naplózás közben
      • méret alapján rotált logok
      • megbízható utófeldolgozás lehetősége
      • akkor is működik, ha a szolgáltatás chrootban fut
      • a logolás és a szolgáltatás egymástól függetlenül újraindítható/leállítható
    • kiválasztott felhasználók sudo nélkül is menedzselhetik őket
    • könnyedén lekérdezhető a státuszuk
    • könnyedén küldhető nekik signal
    • könnyedén biztosíthatjuk ezeket a tulajdonságokat a felhasználók saját "szolgáltatásainak" - pl.:
      • screen, benne irssi
      • eggdrop bot (a régebbi verziók mindenképpen a háttérbe akarták rakni magukat; az újaknak már van "-n" kapcsolója, amitől nem)
      • fetchmail
      • signify
      • akár apache vagy egyéb
  • Tetszőleges számú és nevű runlevelünk lehet

3 Architektúra

  • Minden szolgáltatáshoz tartozik egy könyvtár (directory), amiben:
    • Megvan az a program (általában script), ami az adott szolgáltatást elindítja és futtatja (és run névre hallgat); addig nem léphet ki, amíg a szolgáltatás nem lépett ki
    • Lehet egy down nevű file, ami azt jelzi, hogy a szolgáltatás normális állapota az, hogy nem fut
    • Lehet egy log nevű könyvtár, ha a szolgáltatáshoz naplózást is szeretnénk - l. később
    • Lehet egy check nevű program, ami 0-ás visszatérési értékkel lép ki, ha a szolgáltatás működik (ami nem azonos azzal, hogy fut), egyébként nullától különbözővel
    • Lehet egy finish nevű program, amit abban az esetben kell lefuttatni, ha a run kilép
    • A runit létrehoz egy supervise könyvtárat (ebben van többek között a parancs-FIFO)
  • A runsvdir-nek a parancssorban bootkor megadtuk, melyik könyvtárat figyelje (pl. /var/service)
  • Ebbe a könyvtárba besymlinkeljük a futtatandó szolgáltatások könyvtárát (pl. /var/service/apache2 -> /etc/runit/sv/apache2)
  • A runsvdir ezt észreveszi, és elindítja a runsv-t (újra is indítja, ha kilép)
  • A runsv elindítja a szolgáltatáshoz tartozó run scriptet
  • Ez a script futtatja a szolgáltatást (exec apache2 -DNO_DETACH jelleggel)
  • Így a script pontosan akkor lép ki, amikor a szolgáltatás
  • Ekkor a runsv újraindíthatja (ill. ott a finish, amit előtte lefuttathat)
  • s.í.t.

Egyebek:

  • A run a runsv-től, az a runsvdir-től, az pedig a runit-től (1-es PID) örökli az állapotteret, nem a shelltől
  • Ha nem akarjuk, nem indítja újra a szolgáltatásokat
  • Van initscript-emulátor (/usr/bin/sv); a /etc/init.d/apache2 lehet erre mutató symlink
  • Runlevel-váltás: kicseréljük a runsvdir könyvtárát (symlink-zsonglőrködés); eszköze a runsvchdir

4 Telepítés

  1. apt-get install runit-run
  2. vim /etc/runit/{1,2,3} (indító- és leállítóscriptek)
  3. szolgáltatások symlinkelése (legalább egy getty legyen; a /etc/runit/sv-ben van, csak a symlinket kell létrehoznunk)
    • ln -s /etc/runit/sv/* /var/service
    • ha távolról akarjuk adminisztrálni a gépet, nem árt ssh service-t is csinálni...
  4. sv status getty-1 (nézzük meg, próbálja-e futtatni - nem fog menni, mert az init még futtat egyet a tty1-en)
  5. vim /etc/inittab (kapcsoljuk ki az inites gettyket)
  6. init q (győződjünk meg róla, hogy a runitos gettyk elindultak)
  7. /sbin/init.sysv 6 (reboot)
  • A szolgáltatásokkal áttérhetünk fokozatosan is; a /etc/runit/1 tartalmazhatja az /etc/init.d/rc 2 parancsot.
  • A naplózással is érdemes áttérni (legalábbis syslogd-ről; syslog-ng-ről nem minden esetben)

5 Ami kimaradt

  • Naplózás (socklog, svlogd)
  • A runit többi komponense (pl. chpst)
  • run-script HOWTO
  • Egyéb djbware

6 Ajánlott irodalom

Személyes eszközök