CARP bleibt hängen

CARP ist eine coole Lösung, wenn man mehrere Systeme hat, die sich eine IP teilen sollen. Aber von gestern zu heute war da ein Wurm drin. Ein Server hat sich geweigert, wieder seine Aufgabe als CARP-Master zu erfüllen. Ich stand kurz vor dem Reboot des Systems.

Aus und vorbei

netzplan-animiert

Beim Blick auf die Auslastung der Leitungen zeigte sich eine relativ hohe Last auf einem der Server des NAT-Clusters während eine andere Maschine es sich gemütlich machte.

Eigentlich war nichts auffällig. Der Teufel steckte offenbar im Detail.

Konnte es sein, dass sich die Balancierung des CARP verschoben hatte, so dass ein Teil der Kunden auf einen andere Maschine geleitet wurden?

Wenn ja, müsste sich das im CARP-Status der fraglichen Maschine zeigen.

# for i in 0 1 2 3; do ifconfig carp$i; done
carp0: flags=49<UP,LOOPBACK,RUNNING> metric 0 mtu 1500
        inet 100.100.0.1 netmask 0xffff8000 
        carp: BACKUP vhid 1 advbase 1 advskew 200
carp1: flags=49<UP,LOOPBACK,RUNNING> metric 0 mtu 1500
        inet 100.100.0.1 netmask 0xffff8000 
        carp: BACKUP vhid 2 advbase 1 advskew 100
carp2: flags=49<UP,LOOPBACK,RUNNING> metric 0 mtu 1500
        inet 100.100.0.1 netmask 0xffff8000 
        carp: BACKUP vhid 3 advbase 1 advskew 133
carp3: flags=49<UP,LOOPBACK,RUNNING> metric 0 mtu 1500
        inet 100.100.0.1 netmask 0xffff8000 
        carp: BACKUP vhid 4 advbase 1 advskew 166

Ja, die Kiste hat schlicht keinen Bock mehr. Aber warum?

Ich schaue mir mal den CARP-Status aller Maschinen des Clusters in der Übersicht an.

$ for i in 1 2 3 4; do
    echo server$i;
    ssh root@server$i sysctl net.inet.carp;
    for k in 0 1 2 3; do
      ssh root@server$i ifconfig carp$k | fgrep advbas;
    done;
  done

server1
net.inet.carp.allow: 1
net.inet.carp.preempt: 1
net.inet.carp.log: 1
net.inet.carp.arpbalance: 1
net.inet.carp.suppress_preempt: 0
        carp: MASTER vhid 1 advbase 1 advskew 100
        carp: MASTER vhid 2 advbase 1 advskew 133
        carp: BACKUP vhid 3 advbase 1 advskew 166
        carp: BACKUP vhid 4 advbase 1 advskew 200
server2
net.inet.carp.allow: 1
net.inet.carp.preempt: 1
net.inet.carp.log: 1
net.inet.carp.arpbalance: 1
net.inet.carp.suppress_preempt: 1
        carp: BACKUP vhid 1 advbase 1 advskew 200
        carp: BACKUP vhid 2 advbase 1 advskew 100
        carp: BACKUP vhid 3 advbase 1 advskew 133
        carp: BACKUP vhid 4 advbase 1 advskew 166
server3
net.inet.carp.allow: 1
net.inet.carp.preempt: 1
net.inet.carp.log: 1
net.inet.carp.arpbalance: 1
net.inet.carp.suppress_preempt: 0
        carp: BACKUP vhid 1 advbase 1 advskew 166
        carp: BACKUP vhid 2 advbase 1 advskew 200
        carp: MASTER vhid 3 advbase 1 advskew 100
        carp: BACKUP vhid 4 advbase 1 advskew 133
server4
net.inet.carp.allow: 1
net.inet.carp.preempt: 1
net.inet.carp.log: 1
net.inet.carp.arpbalance: 1
net.inet.carp.suppress_preempt: 0
        carp: BACKUP vhid 1 advbase 1 advskew 133
        carp: BACKUP vhid 2 advbase 1 advskew 166
        carp: BACKUP vhid 3 advbase 1 advskew 200
        carp: MASTER vhid 4 advbase 1 advskew 100

Die virtuelle Host-ID (vhid) zwei ist falsch. Server2 ist nicht für diese vhid zuständig, obwohl er eine kleinere advskew hat.

Ebenso auffällig ist die Aussage, das dieser Server2 suppress_preempt auf einem anderen Wert stehen hat. Dieser Wert gibt an, dass der Server glaubt, er sei nicht bereit CARP aktiv zu machen. Unglücklicherweise ist der Wertread-only.

Ursachenforschung

Server2 glaubt also in einem Fehlerzustand zu sein. Aber welcher kann das sein? Und wie kann man ihn aus dem Weg räumen?

Die Man-Page war jedenfalls nicht hilfreich. Sie sagt, ein Interface müsse down sein. Aber das ist offensichtlich nicht der Fall.

Eine Suche im Sourcecode offenbart, dass dieser Fehlerzustand ausschließlich in der Datei ip_carp.c existiert und nur dort behandelt wird.

[lutz@server2 /usr/src/sys]# fgrep -lR suppress_preempt .
./netinet/ip_carp.c

Das macht es schon einmal sehr einfach.

int carp_suppress_preempt = 0;
SYSCTL_INT(_net_inet_carp, OID_AUTO, suppress_preempt, CTLFLAG_RD,
    &carp_suppress_preempt, 0, "Preemption is suppressed");

Es gibt also eine globale Variable des Kernels, deren Wert per sysctl ausgelesen werden darf. Nach dem Booten ist die 0.

static void
carpdetach(struct carp_softc *sc, int unlock)
{
...
        if (sc->sc_suppress)
                carp_suppress_preempt--;
        sc->sc_suppress = 0;

        if (sc->sc_sendad_errors >= CARP_SENDAD_MAX_ERRORS)
                carp_suppress_preempt--;
        sc->sc_sendad_errors = 0;
...

Wenn ein CARP-Interface entfernt wird, wird der globale Zähler dekrementiert, wenn dieses Interface den Sperrzustand eingeleitet hatte.

static void
carp_input_c(struct mbuf *m, struct carp_header *ch, sa_family_t af)
{
...
        sc_tv.tv_sec = sc->sc_advbase;
        if (carp_suppress_preempt && sc->sc_advskew <  240)
                sc_tv.tv_usec = 240 * 1000000 / 256;
        else
                sc_tv.tv_usec = sc->sc_advskew * 1000000 / 256;
        ch_tv.tv_sec = ch->carp_advbase;
        ch_tv.tv_usec = ch->carp_advskew * 1000000 / 256;
...

Bei der Verarbeitung von CARP-Nachrichten wird jede CARP-Instanz als letztmöglicher Fallback angesehen, indem maximale Werte angenommen werden.

Das ist zwar beruhigend, aber ändert nichts an dem Wert selbst.

static void
carp_send_ad_locked(struct carp_softc *sc)
{
...
                advbase = sc->sc_advbase;
                if (!carp_suppress_preempt || sc->sc_advskew > 240)
                        advskew = sc->sc_advskew;
                else
                        advskew = 240;
                tv.tv_sec = advbase;
                tv.tv_usec = advskew * 1000000 / 256;
...

Bei der Generierung von CARP-Nachrichten an andere Systeme werden maximale Werte announciert, so eine Sperre vorliegt.

Damit wissen die anderen von den Problemen dieser Maschine.

...
                if (ip_output(m, NULL, NULL, IP_RAWOUTPUT, &sc->sc_imo, NULL)) {
                        SC2IFP(sc)->if_oerrors++;
                        if (sc->sc_sendad_errors < INT_MAX)
                                sc->sc_sendad_errors++;
                        if (sc->sc_sendad_errors == CARP_SENDAD_MAX_ERRORS) {
                                carp_suppress_preempt++;
                                if (carp_suppress_preempt == 1) {
                                        CARP_SCUNLOCK(sc);
                                        carp_send_ad_all();
                                        CARP_SCLOCK(sc);
                                }
                        }
                        sc->sc_sendad_success = 0;
                } else {
                        if (sc->sc_sendad_errors >= CARP_SENDAD_MAX_ERRORS) {
                                if (++sc->sc_sendad_success >=
                                    CARP_SENDAD_MIN_SUCCESS) {
                                        carp_suppress_preempt--;
                                        sc->sc_sendad_errors = 0;
                                }
                        } else
                                sc->sc_sendad_errors = 0;
                }
...

Jetzt wird es interessant (und der IPv6 Teil ist identisch).

Wenn beim Aussenden der CARP-Nachricht ein Fehler auftritt, zählt er die Fehler hoch. Überschreiten diese die MAX-Grenze, geht der globale Block an. Funktioniert es dann wieder MIN-mal hintereinander, wird die Sperre wieder aufgehoben. Die Instanz selbst wird nicht als geblockt markiert.

Soweit so logisch.

static void
carp_sc_state_locked(struct carp_softc *sc)
{
...
        if (sc->sc_carpdev->if_link_state != LINK_STATE_UP ||
            !(sc->sc_carpdev->if_flags & IFF_UP)) {
...
                if (!sc->sc_suppress) {
                        carp_suppress_preempt++;
                        if (carp_suppress_preempt == 1) {
                                CARP_SCUNLOCK(sc);
                                carp_send_ad_all();
                                CARP_SCLOCK(sc);
                        }
                }
                sc->sc_suppress = 1;
        } else {
                SC2IFP(sc)->if_flags |= sc->sc_flags_backup;
                carp_set_state(sc, INIT);
                carp_setrun(sc, 0);
                if (sc->sc_suppress)
                        carp_suppress_preempt--;
                sc->sc_suppress = 0;
        }
...

Und schließlich noch: Wenn das zugehörige Interface den Link-Status ändert, dann blockiere ebenfalls die zugehörige CARP Instanz.

Außerdem soll global blockiert werden, damit ein System, dass nicht mehr volle Konnektivität hat, sich nicht mehr aktiv um die Verarbeitung von Kundendaten bemüht.

Soweit auch logisch.

Mir erscheint die Stelle mit dem CARP_SCUNLOCK gefolgt von carp_send_ad_all aber wie eine Racecondition. Schließlich greift die Senderoutine auf die gleichen Werte zu und die Blockierung der betroffenen Instanz erfolgt erst danach der Aussendung.

Das klingt irgendwie unlogisch. Vermutlich habe ich es schlicht falsch verstanden. Es ist aber auch ziemlich uninteressant, weil neuere Kernel den Code komplett anders haben.

Fehlerbehebung

Herausgekommen ist, dass der globale Zähler nur dann wieder verschwindet, wenn entweder die Zähler von allein zurück gehen, oder die Interfaces gelöscht werden.

Na da bleibt nicht viel: Alle Interfaces manuell löschen und händisch neu anlegen.

Bei der Gelegenheit wurde auch der CARP-Loglevel erhöht. So bleibt ein letzter Blick:

$ for i in 1 2 3 4; do
    echo server$i;
    ssh root@server$i sysctl net.inet.carp;
    for k in 0 1 2 3; do
      ssh root@server$i ifconfig carp$k | fgrep advbas;
    done;
  done

server1
net.inet.carp.allow: 1
net.inet.carp.preempt: 1
net.inet.carp.log: 2
net.inet.carp.arpbalance: 1
net.inet.carp.suppress_preempt: 0
        carp: MASTER vhid 1 advbase 1 advskew 100
        carp: BACKUP vhid 2 advbase 1 advskew 133
        carp: BACKUP vhid 3 advbase 1 advskew 166
        carp: BACKUP vhid 4 advbase 1 advskew 200
server2
net.inet.carp.allow: 1
net.inet.carp.preempt: 1
net.inet.carp.log: 2
net.inet.carp.arpbalance: 1
net.inet.carp.suppress_preempt: 0
        carp: BACKUP vhid 1 advbase 1 advskew 200
        carp: MASTER vhid 2 advbase 1 advskew 100
        carp: BACKUP vhid 3 advbase 1 advskew 133
        carp: BACKUP vhid 4 advbase 1 advskew 166
server3
net.inet.carp.allow: 1
net.inet.carp.preempt: 1
net.inet.carp.log: 2
net.inet.carp.arpbalance: 1
net.inet.carp.suppress_preempt: 0
        carp: BACKUP vhid 1 advbase 1 advskew 166
        carp: BACKUP vhid 2 advbase 1 advskew 200
        carp: MASTER vhid 3 advbase 1 advskew 100
        carp: BACKUP vhid 4 advbase 1 advskew 133
server4
net.inet.carp.allow: 1
net.inet.carp.preempt: 1
net.inet.carp.log: 2
net.inet.carp.arpbalance: 1
net.inet.carp.suppress_preempt: 0
        carp: BACKUP vhid 1 advbase 1 advskew 133
        carp: BACKUP vhid 2 advbase 1 advskew 166
        carp: BACKUP vhid 3 advbase 1 advskew 200
        carp: MASTER vhid 4 advbase 1 advskew 100

Prima!

Post a comment

Verwandter Inhalt