Comment arrêter http.ListenAndServe ()


91

J'utilise la bibliothèque Mux de Gorilla Web Toolkit avec le serveur Go http fourni.

Le problème est que dans mon application, le serveur HTTP n'est qu'un composant et il est nécessaire de s'arrêter et de démarrer à ma discrétion.

Quand je l'appelle http.ListenAndServe(fmt.Sprintf(":%d", service.Port()), service.router)bloque et je n'arrive pas à empêcher le serveur de fonctionner.

Je sais que cela a posé problème dans le passé, est-ce toujours le cas? Existe-t-il de nouvelles solutions?

Réponses:


92

En ce qui concerne l'arrêt progressif (introduit dans Go 1.8), un exemple un peu plus concret:

package main

import (
    "context"
    "io"
    "log"
    "net/http"
    "sync"
    "time"
)

func startHttpServer(wg *sync.WaitGroup) *http.Server {
    srv := &http.Server{Addr: ":8080"}

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        io.WriteString(w, "hello world\n")
    })

    go func() {
        defer wg.Done() // let main know we are done cleaning up

        // always returns error. ErrServerClosed on graceful close
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            // unexpected error. port in use?
            log.Fatalf("ListenAndServe(): %v", err)
        }
    }()

    // returning reference so caller can call Shutdown()
    return srv
}

func main() {
    log.Printf("main: starting HTTP server")

    httpServerExitDone := &sync.WaitGroup{}

    httpServerExitDone.Add(1)
    srv := startHttpServer(httpServerExitDone)

    log.Printf("main: serving for 10 seconds")

    time.Sleep(10 * time.Second)

    log.Printf("main: stopping HTTP server")

    // now close the server gracefully ("shutdown")
    // timeout could be given with a proper context
    // (in real world you shouldn't use TODO()).
    if err := srv.Shutdown(context.TODO()); err != nil {
        panic(err) // failure/timeout shutting down the server gracefully
    }

    // wait for goroutine started in startHttpServer() to stop
    httpServerExitDone.Wait()

    log.Printf("main: done. exiting")
}

1
Oui, la fonctionnalité est Shutdown (), dont je montre l'utilisation concrète ici. Merci, j'aurais dû être plus clair, j'ai changé le titre maintenant: "En ce qui concerne l'arrêt gracieux (introduit dans Go 1.8), un exemple un peu plus concret:"
joonas.fi

Quand je passe nilà srv.Shutdownje reçois panic: runtime error: invalid memory address or nil pointer dereference. Passer à la context.Todo()place fonctionne.
Hubro

1
@Hubro c'est bizarre, je viens de l'essayer sur la dernière version de Golang (1.10), et ça a bien fonctionné. context.Background () ou context.TODO () fonctionne bien sûr et si cela fonctionne pour vous, tant mieux. :)
joonas.fi

1
@ newplayer65 il y a plusieurs façons de faire cela. Une façon pourrait être de créer sync.WaitGroup dans main (), d'appeler Add (1) dessus et de lui passer un pointeur pour startHttpServer () et d'appeler defer waitGroup.Done () au début de la goroutine qui a un appel à la ListenAndServe (). puis appelez simplement waitGroup.Wait () à la fin de main () pour attendre que le goroutine ait terminé son travail.
joonas.fi

1
@ newplayer65 J'ai regardé votre code. Utiliser un canal est une bonne option, probablement meilleure que ma suggestion. Mon code était principalement de démontrer Shutdown () - pas de mettre en valeur le code de qualité de production :) Ps le logo "server gopher" de votre projet est adorbe! : D
joonas.fi

70

Comme mentionné dans yo.ian.gla réponse de. Go 1.8 a inclus cette fonctionnalité dans la bibliothèque standard.

Exemple minimal pour pour Go 1.8+:

    server := &http.Server{Addr: ":8080", Handler: handler}

    go func() {
        if err := server.ListenAndServe(); err != nil {
            // handle err
        }
    }()

    // Setting up signal capturing
    stop := make(chan os.Signal, 1)
    signal.Notify(stop, os.Interrupt)

    // Waiting for SIGINT (pkill -2)
    <-stop

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := server.Shutdown(ctx); err != nil {
        // handle err
    }

    // Wait for ListenAndServe goroutine to close.

Réponse originale - Pre Go 1.8:

S'appuyant sur la réponse d'Uvelichitel .

Vous pouvez créer votre propre version ListenAndServedont renvoie un io.Closeret ne bloque pas.

func ListenAndServeWithClose(addr string, handler http.Handler) (io.Closer,error) {

    var (
        listener  net.Listener
        srvCloser io.Closer
        err       error
    )

    srv := &http.Server{Addr: addr, Handler: handler}

    if addr == "" {
        addr = ":http"
    }

    listener, err = net.Listen("tcp", addr)
    if err != nil {
        return nil, err
    }

    go func() {
        err := srv.Serve(tcpKeepAliveListener{listener.(*net.TCPListener)})
        if err != nil {
            log.Println("HTTP Server Error - ", err)
        }
    }()

    srvCloser = listener
    return srvCloser, nil
}

Code complet disponible ici .

Le serveur HTTP se fermera avec l'erreur accept tcp [::]:8080: use of closed network connection


J'ai créé un package qui fait le passe
partout

24

Go 1.8 inclura un arrêt progressif et forcé, disponible via Server::Shutdown(context.Context)et Server::Close()respectivement.

go func() {
    httpError := srv.ListenAndServe(address, handler)
    if httpError != nil {
        log.Println("While serving HTTP: ", httpError)
    }
}()

srv.Shutdown(context)

Le commit correspondant peut être trouvé ici


7
désolé d'être pointilleux, et je sais que votre code n'était qu'un exemple d'utilisation, mais en règle générale: go func() { X() }()suivi de Y()fait la fausse hypothèse au lecteur qui X()s'exécutera avant Y(). Les groupes d'attente, etc. veillent à ce que des bogues de synchronisation comme celui-ci ne vous mordent pas au moment le moins attendu!
colm.anseo

20

Vous pouvez construire net.Listener

l, err := net.Listen("tcp", fmt.Sprintf(":%d", service.Port()))
if err != nil {
    log.Fatal(err)
}

que tu peux Close()

go func(){
    //...
    l.Close()
}()

et http.Serve()dessus

http.Serve(l, service.router)

1
merci mais cela ne répond pas à ma question. Je pose des questions http.ListenAndServepour des raisons spécifiques. Voilà comment j'utilise la bibliothèque GWT MUX, je ne sais pas comment utiliser net.listen pour cela ..
jim

6
Vous utilisez http.Serve () au lieu de http.ListenAndServe () exactement de la même manière avec la même syntaxe uniquement avec votre propre écouteur. http.Serve (net.Listener, gorilla.mux.Router)
Uvelichitel

Ah super, merci. Je n'ai pas encore testé mais devrait fonctionner.
jim le

1
Un peu tard, mais nous avons utilisé le package manners pour ce cas d'utilisation. C'est un remplacement instantané du package http standard qui permet un arrêt progressif (c'est-à-dire qui termine toutes les requêtes actives tout en en refusant de nouvelles, puis se termine).
Kaedys le

13

Étant donné qu'aucune des réponses précédentes ne dit pourquoi vous ne pouvez pas le faire si vous utilisez http.ListenAndServe (), je suis entré dans le code source http v1.8 et voici ce qu'il dit:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

Comme vous pouvez le voir, la fonction http.ListenAndServe ne renvoie pas la variable serveur. Cela signifie que vous ne pouvez pas accéder au «serveur» pour utiliser la commande Shutdown. Par conséquent, vous devez créer votre propre instance de «serveur» au lieu d'utiliser cette fonction pour que l'arrêt progressif soit implémenté.


2

Vous pouvez fermer le serveur en fermant son contexte.

type ServeReqs func(ctx context.Context, cfg Config, deps ReqHandlersDependencies) error

var ServeReqsImpl = func(ctx context.Context, cfg Config, deps ReqHandlersDependencies) error {
    http.Handle(pingRoute, decorateHttpRes(pingHandlerImpl(deps.pingRouteResponseMessage), addJsonHeader()))

    server := &http.Server{Addr: fmt.Sprintf(":%d", cfg.port), Handler: nil}

    go func() {
        <-ctx.Done()
        fmt.Println("Shutting down the HTTP server...")
        server.Shutdown(ctx)
    }()

    err := server.ListenAndServeTLS(
        cfg.certificatePemFilePath,
        cfg.certificatePemPrivKeyFilePath,
    )

    // Shutting down the server is not something bad ffs Go...
    if err == http.ErrServerClosed {
        return nil
    }

    return err
}

Et chaque fois que vous êtes prêt à le fermer, appelez:

ctx, closeServer := context.WithCancel(context.Background())
err := ServeReqs(ctx, etc)
closeServer()

"Arrêter le serveur n'est pas quelque chose de mal ffs Go ..." :)
Paul Knopf

Une chose à noter est que, pour un arrêt en douceur, avant de quitter, vous devez attendre le retour de Shutdown, ce qui ne semble pas se produire ici.
Marcin Bilski

Votre utilisation de ctxto server.Shutdownest incorrecte. Le contexte est déjà annulé, il ne sera donc pas arrêté proprement. Vous avez peut-être bien appelé server.Closeà un arrêt impur. (Pour un arrêt propre, ce code devra être largement retravaillé.
Dave C

0

Il est possible de résoudre ce problème en utilisant un context.Context utilisant un net.ListenConfig. Dans mon cas, je ne voulais pas utiliser l ' appel d' un sync.WaitGroupou , et plutôt me fier à un (qui était fermé avec un signal).http.ServerShutdown()context.Context

import (
  "context"
  "http"
  "net"
  "net/http/pprof"
)

func myListen(ctx context.Context, cancel context.CancelFunc) error {
  lc := net.ListenConfig{}
  ln, err := lc.Listen(ctx, "tcp4", "127.0.0.1:6060")
  if err != nil {
    // wrap the err or log why the listen failed
    return err
  }

  mux := http.NewServeMux()
  mux.Handle("/debug/pprof/", pprof.Index)
  mux.Handle("/debug/pprof/cmdline", pprof.CmdLine)
  mux.Handle("/debug/pprof/profile", pprof.Profile)
  mux.Handle("/debug/pprof/symbol", pprof.Symbol)
  mux.Handle("/debug/pprof/trace", pprof.Trace)

  go func() {
    if err := http.Serve(l, mux); err != nil {
      cancel()
      // log why we shut down the context
      return err
    }
  }()

  // If you want something semi-synchronous, sleep here for a fraction of a second

  return nil
}

-6

Ce que je l' ai fait pour les cas où l'application est que le serveur et effectuer aucune autre fonction est d' installer un http.HandleFuncpour un motif comme /shutdown. Quelque chose comme

http.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
    if <credentials check passes> {
        // - Turn on mechanism to reject incoming requests.
        // - Block until "in-flight" requests complete.
        // - Release resources, both internal and external.
        // - Perform all other cleanup procedures thought necessary
        //   for this to be called a "graceful shutdown".
        fmt.Fprint(w, "Goodbye!\n")
        os.Exit(0)
    }
})

Il ne nécessite pas 1.8. Mais si 1.8 est disponible, alors cette solution peut être intégrée ici au lieu de l' os.Exit(0)appel si cela est souhaitable, je crois.

Le code pour effectuer tout ce travail de nettoyage est laissé comme un exercice pour le lecteur.

Un crédit supplémentaire si vous pouvez dire où ce code de nettoyage pourrait être le plus raisonnablement placé, car je ne recommanderais pas de le faire ici, et comment ce coup de point final devrait provoquer l'appel de ce code.

Plus de crédit supplémentaire si vous pouvez dire où cela os.exit(0) appel (ou quelle que soit la sortie de processus que vous choisissez d'utiliser), donné ici à des fins d'illustration uniquement, serait le plus raisonnablement placé.

Encore plus de crédit supplémentaire si vous pouvez expliquer pourquoi ce mécanisme de signalisation de processus de serveur HTTP devrait être considéré au-dessus de tous les autres mécanismes de ce type pensés réalisables dans ce cas.


Bien sûr, j'ai répondu à la question telle que posée sans autre hypothèse sur la nature du problème, et en particulier, aucune hypothèse sur un environnement de production donné. Mais pour ma propre édification, @MarcinBilski, quelles exigences exactement rendraient cette solution inadaptée à tout environnement, production ou autre?
greg.carter

2
Cela signifiait plus ironique qu'autre chose, car il est clair que vous n'auriez pas de gestionnaire / shutdown dans une application de production. :) Tout est permis pour l'outillage interne, je suppose. Cela mis à part, il existe des moyens d'arrêter gracieusement le serveur afin qu'il ne supprime pas soudainement les connexions ou ne plante pas à mi-chemin d'une transaction de base de données ou, pire, lors de l'écriture sur le disque, etc.
Marcin Bilski

Certes, il ne peut pas être le cas que les électeurs de bas ne soient pas imaginatifs. Il faut que j'assume trop d'imagination. J'ai mis à jour la réponse (y compris l'exemple) pour corriger mon erreur.
greg.carter
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.