Apache Webserver und Go

This article is also available in English.

Go stellt mit dem Modul net/http einen vollständigen HTTP Server in der Standardbibliothek bereit. Dennoch kann es vorteilhaft sein, eine in Go geschriebene Web-Anwendung über einen Apache Webserver zugänglich zu machen; beispielsweise, wenn unterschiedliche Anwendungen oder Sites auf dem gleichen Server laufen sollen (in Unterverzeichnissen oder über Virtual Hosts) oder wenn weitere Konfigurationsmöglichkeiten des Apache HTTPD benötigt werden (z.B. benutzerdefiniertes Logging, Rewrite Regeln oder Authentifikation).

Eine definierte Schnittstelle zwischen Webservern und Anwendungen ist FastCGI. Dabei wird ein Socket (Unix Domain oder TCP) als Kommunikationskanal verwendet. Das Modul net/http/fcgi aus der Go Standardbibliothek stellt die Serverseite einer FastCGI Implementierung bereit.

Apache mod_fastcgi

Für den Apache Webserver gibt es FastCGI-Unterstützung in Form des Moduls mod_fastcgi (Debian-Paket libapache2-mod-fastcgi). Es stammt von Open Market Inc., dem Erfinder des FastCGI Protokolls. Das Modul unterliegt einer etwas eingeschränkten Lizenz und wird daher von Debian unter non-free geführt.

Eine einfache FastCGI Anwendung in Go sieht beispielsweise so aus:

package main

import (
    "net/http"
    "net/http/fcgi"
    "runtime"
)

var reply = []byte("Hello, World!\n")

func handler(w http.ResponseWriter, r *http.Request) {
    w.Write(reply)
}

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU()) // use all CPU cores

    err := fcgi.Serve(nil, http.HandlerFunc(handler))
    if err != nil { panic(err) }
}

Das Apache-Modul ist unter Debian standardmäßig so konfiguriert, dass es Anwendungen mit der Endung .fcgi als FastCGI Programme ansieht. Andere Anwendungen können mit der Direktive FastCgiServer explizit benannt werden. Die folgenden Zeilen innerhalb einer VirtualHost Konfiguration (z.B. bei Debian in /etc/apache2/sites-available/default) starten das Programm /usr/local/sbin/fcgiprog und leiten dorthin alle Anfragen, deren URI mit /fcgiprog beginnt.

        ...
        FastCgiServer /usr/local/sbin/fcgiprog
        RewriteRule ^/fcgiprog /usr/local/sbin/fcgiprog

Prozesse sauber beenden

Wenn der Apache HTTP Server neu gestartet wird oder seine Konfiguration neu einlesen soll, schickt das Modul mod_fastcgi ein TERM Signal an alle laufenden FastCGI-Anwendungen. Das führt bei der oben gezeigten Anwendung zu einem sofortigen Beenden des Programmes und damit zum Abbruch aller laufenden Transaktionen.

Um die Transaktionen sauber zu beenden, bevor das FastCGI Programm gestoppt wird, muss es das Signal SIGTERM abfangen und geeignet darauf reagieren. Dabei sind drei Dinge zu beachten.

  1. mod_fastcgi garantiert, dass nach dem Versand von SIGTERM kein weiterer Request an ein laufendes FastCGI-Programm geschickt wird. Allerdings können sich für eine kurze Zeitspanne noch Requests in der Verarbeitung von net/http/fcgi befinden, für die noch kein HTTP-Handler als Goroutine gestartet wurde. Um diese Fälle abzufangen, sollte im Signal Handler erstmal kurz gewartet werden (in u.g. Beispiel 100 Millisekunden).
  2. Um keine noch laufende Goroutine abzubrechen, wird mit runtime.NumGoroutine() regelmäßig geprüft, wie viele Goroutinen noch laufen. Erst wenn diese wieder dem ursprünglichen Wert vor dem Start der FastCGI Requestannahme entsprechen (plus eins für den FCGI-Handler), kann davon ausgegangen werden, dass sicher kein Request mehr verarbeitet wird.
  3. Je nach Programmstruktur kann es möglich sein, dass in der Zwischenzeit weitere Goroutinen gestartet wurden, die mit der Requestbearbeitung nichts zu tun haben; beispielsweise Synchronisationsmechanismen von Drittbibliotheken. Um in diesem Fall nicht endlos zu warten, wird die Warteschleife nach 3 Sekunden (30 Mal 100 Millisekunden) abgebrochen.

Wenn diese Punkte beachtet werden, ist es möglich, in Go geschriebene Serveranwendungen upzudaten, indem einfach das Binary ersetzt und der Apache mit apache2ctl graceful neu initialisiert wird. Selbst unter Last geht wird dabei keine Client-Verbindung abgebrochen.

// Example for a FastCGI program that terminates gracefully.
package main

import (
    "net/http"
    "net/http/fcgi"
    "os"
    "os/signal"
    "runtime"
    "syscall"
    "time"
)

// a simple request handler
var hello = []byte("Hello, World!\n")

func handler(w http.ResponseWriter, r *http.Request) {
    w.Write(hello)
}

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU())     // use all CPU cores
    n := runtime.NumGoroutine() + 1          // initial number of Goroutines

    // install signal handler
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGTERM)

    // Spawn request handler
    go func() {
        err := fcgi.Serve(nil, http.HandlerFunc(handler))
        if err != nil {
            panic(err)
        }
    }()

    // catch signal
    _ = <-c

    // give pending requests in fcgi.Serve() some time to enter the request handler
    time.Sleep(time.Millisecond * 100)

    // wait at most 3 seconds for request handlers to finish
    for i := 0; i < 30; i++ {
        if runtime.NumGoroutine() <= n {
            return
        }
        time.Sleep(time.Millisecond * 100)
    }
}

Andere FastCGI Implementierungen

Es gibt ein weiteres Apache-Modul mod_fcgid, das manchmal als Alternative zu mod_fastcgi empfohlen wird, weil es Freie Software ist und bessere Kontrolle über das zu startende Programm bieten soll. Ihm fehlt jedoch die Möglichkeit, mehrere Anfragen parallel von einem Programm beantworten zu lassen. Parallele Anfragen sind nur möglich, wenn man mehrere unabhängige Instanzen des Programms durch den Apache starten lässt. Damit kann die wichtige Eigenschaft von Go, mehrere Anfragen parallel in Goroutinen abarbeiten zu lassen, nicht genutzt werden. mod_fcgid sollte daher nicht für Go-Programme verwendet werden.