netzstaub

beatz & funkz

Sunday, March 20, 2005

Process Watch

Dividuum war diese Woche ganz schön fleissig, und ich werde hier mal kurz über die Software bloggen die er geschrieben hat. Wir wollten demnächst die Welt mit unser Streaming-Software übernehmen. Doch dazu braucht man auch Software, die den Zustand der ganzen Tools, die für erfolgreiches Streaming notwendig sind, überwacht. Wie überwacht man am besten Encoder, HTTP-Server, Mitschneide-Prozesse, Umkodiersoftware, usw…? Am Besten mit eigener Process-Watch Software, den anderen Lösungen ist ja nicht zu trauen. Klar, es gibt schon init-Skripte. Doch wer will sowas hässliches schreiben wie start-stop-daemon zu verwenden? Und wie prüft man nachher nach, das die Software auch läuft? Integrieren von Pipe-basierten Shellskripten ist da auch nicht wirklich einfach. Genauso ist es mit supervise von djb z.B. Da ist die User-Schnittstelle ganz schön anstrengend, selbst die Logs kann man nicht als Mensch parsen.

Die Software von Dividuum heisst “Process-Watch”, abgekürzt “pw.rb”, und ist natürlich in Ruby geschrieben. Sie liest ein Config-File in YAML ein, und verwendet den Webserver Webrick zur Darstellung des Prozess-status. Die Software ist in unserem Subversion Repository unter svn://bl0rg.net/pw/ zu finden. Zuerst eine kleine Übersicht von YAML.

YAML

YAML heisst YAML Ain’t Markup Language, und ist so eine Art Sprache, die man benutzen kann, um Daten zu serialisieren. Im Gegensatz zu XML, mit der YAML oft verglichen wird, ist das explizite Ziel von YAML auch noch von Menschen lesbar und editierbar zu sein. Dazu verwendet YAML eine Mischung von speziellen Zeichen und Indentierung. Weiterhin kann man YAML auch in einem Stream-Context verwenden. Wenn man also nur eine Kommunikationspipe hat, kann man darüber mit YAML getrennte Dokumente übermitteln. In dem Fall von Process-Watch werden nur die “einfachen” Features von YAML unterstützt. Eine Beispielskonfigurationsdatei sieht so aus:

Sleeper:
    command: sleep 5

Test:
    command: sleep 10
    graceTime: 2
    
Top:
    command: top 1>&2
    graceTime: 3
    activateOnStartup: false

Diese Konfigurationsdatei lässt sich ungefähr so lesen: Es gibt 3 Knoten in der Datei. Die Knoten heissen “Sleeper”, “Test” und “Top”, und sind Kommandos, die Process-Watch überwachen soll. Jeder Knoten ist ein Mapping, d.h. z.B. eine Hashtabelle. Der Key “command” ist jeweils das Kommando, das ausgeführt werden muss. Der Parameter “graceTime” ist dazu da, einem Prozess bevor er gekillt wird ein bisschen Zeit zu geben. Ein Prozess wird gekillt, wenn in der Steueroberfläche ein Stop-Kommando abgesetzt wird, oder Process-Watch allgemein gestoppt wird. Der Parameter “activateOnStartup” ist dazu da, um ein Prozess nicht gleich beim Starten von Process-Watch zu starten. Wie man sehen kann, sind die “command”-Parameter vollwertige Shellkommandos, mit Pipes, usw… Daher kann man z.B. Programme wie unseren FEC-Streamclient gleich mit mpg123 über Process-Watch starten:

poc:
    command: ~/progs/poc/pob-fec | mpg123 -
    maxLogItems: 30
    graceTime: 5

Hier sieht man auch gleich an dem “maxLogItems”-Parameter, dass Process-Watch nicht nur Programme ausführen kann, sondern auch ihre Ausgaben mitloggen kann, und über die Kontrollschnittstelle anzeigen kann.

Process-Watch im Einsatz

Ganz “user”-friendly ist Process-Watch noch nicht, und im Moment ist der Name der Konfigurationsdatei (“config.yaml”) noch fest verdrahtet. Wenn man jetzt Process-Watch mit der obigen Datei startet, bekommt man folgende Konfigurationsoberfläche auf dem Port 8080:

Pw1-1

Mit “start” und “stop” kann man hier die Programme starten und stoppen, mit “log” kann man den Output des Programms einsehen. Unten kann man auch noch die Konfiguration neuladen (wenn man z.B. neue Programme eingefügt hat). Process-Watch geht dabei recht klever vor, indem es nicht nur die Konfiguration dumm neulädt, sondern erkennt, welche Programme dazu gekommen sind, und welche gekillt werden müssen, und bei welchen sich die Parameter geändert haben.

Process-Watch Internals

Intern ist Process-Watch recht klar aufgebaut. Es gibt ein Master-Objekt, dass die laufenden Prozesse kontrolliert, und auch Anfragen von dem Webserver abarbeitet. Für die einzeln gestartete Prozess sind “WatchProcess”-Objekte zuständig. Das Hauptprogramm (die “main”-Routine, sozusagen) instanziiert ein “Master”-Objekt, einen HTTP Server, und biegt das “INT” Signal um, um Process-Watch sauber zu stoppen. Nachdem der Webserver instanziiert wurde, wird für die Url “/” ein Handler angegeben, der die Anfrage an das Master-Objekt weiterleitet, indem es bei ihm die Methode “statusRequest” aufruft. Anschliessend wird der Webserver-thread gestartet, und alle 0.5 Sekunden wird dann der Master gepollt. Bei diesem Schritt überprüft er dass jeder Prozess richtig läuft, usw… Hier dieses Hauptprogramm:

done = false

trap("INT") do 
    done = true
end

master = Master.new("config.yaml")
http   = WEBrick::HTTPServer.new(:Port => 8080)

http.mount_proc("/") do |req, res|
    master.statusRequest(req, res)
end

httpThread = Thread.new do 
    http.start
end

until done do
    master.iteration
    sleep 0.5
end

http.shutdown 
httpThread.join
master.activateNewConfig({})

Wie man sieht, wird hier die “eigentliche” Arbeit in der Methode “iteration” von dem Masterobjekt gemacht. Diese Routine sieht wie folgt aus:

    def iteration
        @masterMutex.synchronize do 
            fdlist = {}
            @processes.values.each do |process|
                fdlist[process.stderr] = process if process.stderr
            end
            readable, _, _ = *IO.select(fdlist.keys, nil, nil, 0)
            if readable
                readable.each do |fd| 
                    fdlist[fd].onStderrReadable
                end
            end
            
            collectDeadChilds
            checkProcessConfig
        end
    end

Process-Watch benutzt 2 Threads (der Master-thread, und der Webserver-thread), und deswegen wird zur Synchronisation hier in “iteration” das Mastermutex reserviert. Innerhalb des Mutex geht dann der Masterprozesse alle gestarteten Prozesse durch (die in dem Array “@processes” gespeichert sind), und checkt, ob bei einem Prozess was von Stderr gelesen werden kann. Ist dies der Fall, dann stehen Daten für das Log bereit, und die Methode “onStderrReadable” wird bei dem “WatchProcess” Objekt aufgerufen. Nachdem die Logfiles aller gestarteten Prozesse aufgefrischt wurden, wird in der Method “collectDeadChilds” nachgeguckt, welche Subprozesse gestorben sind, und sammelt ihre Leichen auf. Schliesslich wird verifiziert, ob die Konfiguration neugeladen werden muss, was durch die “checkProcessConfig” Methode erledigt wird. Bei dem ersten Ausführen von “iteration” ist noch gar keine Konfiguration geladen worden, so dass diese Methode auf jedem Fall aufgerufen wird.

“checkProcessConfig” tut nichts anderes, als die YAML Konfigurationsdatei zu laden, und nachher “activateNewConfig” aufzurufen, die sich darum kümmert, die neuen Prozesse zu erkennen, die nicht-mehr existierenden zu identifizieren, und die modifizierten Parameter der gleich-bleibenden Prozesse zu übernehmen. Der YAML-Reader benutzt die YAML-Library für Ruby, und ist verheerend einfach 🙂

def readYaml(file)
    begin 
        cfgfile = File.open(file)
        config  = YAML::load(cfgfile.read)
    rescue SystemCallError => e
        puts "error while reading configuration file #{file}: #{e}"
    rescue ArgumentError => e
        puts "error while parsing configuration file #{file}: #{e}"
    ensure
        cfgfile.close
    end
    config
end

def checkProcessConfig(reload = @mustReload)
    return unless reload
    @mustReload = false

    config = readYaml(@config)
    unless config
        log "could not read configuration"
        return false
    end
    activateNewConfig(config)
end

Die neuen und alten Prozesse werden über ihren Namen identifiziert, also die Keys vom ersten Mapping in der YAML-Datei. Für jeden neuen Prozess wird ein neues “WatchProcess” Objekt instanziiert, was sich dann auch darum kümmert, zu Forken und den neuen Prozess auszuführen (und auch Stderr umzuleiten, damit die Nachrichten, die darüber kommen mitloggen zu können). Das ganze Unix-Handling ist ziemlich ekelhaft, und ich werde hier nicht näher drauf eingehen. Vermerken, dass Unix wirklich stinkt, muss ich allerdings schon.

posted by manuel at 6:19 pm  

1 Comment »

  1. ขอโปรคับๆๆๆๆๆๆๆๆๆ

    Comment by weerayut butgunha — February 14, 2009 @ 12:38 pm

RSS feed for comments on this post.

Leave a comment

Powered by WordPress