Come interrompere http.ListenAndServe ()


91

Sto usando la libreria Mux di Gorilla Web Toolkit insieme al server Go http in bundle.

Il problema è che nella mia applicazione il server HTTP è solo un componente ed è necessario arrestarlo e avviarlo a mia discrezione.

Quando lo chiamo http.ListenAndServe(fmt.Sprintf(":%d", service.Port()), service.router)blocchi e non riesco a interrompere l'esecuzione del server.

Sono consapevole che questo è stato un problema in passato, è ancora così? Esistono nuove soluzioni?

Risposte:


92

Per quanto riguarda l'arresto grazioso (introdotto in Go 1.8), un esempio un po 'più concreto:

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
Sì, la funzionalità è Shutdown (), il cui utilizzo concreto sto dimostrando qui. Grazie, avrei dovuto essere più chiaro, ora ho cambiato il titolo in questo: "Per quanto riguarda l'arresto grazioso (introdotto in Go 1.8), un esempio un po 'più concreto:"
joonas.fi

Quando passo nila srv.Shutdownottengo panic: runtime error: invalid memory address or nil pointer dereference. Il passaggio context.Todo()invece funziona.
Hubro

1
@ Hubro è strano, l'ho appena provato sull'ultima versione di Golang (1.10) e ha funzionato bene. context.Background () o context.TODO () ovviamente funziona e se funziona per te, bene. :)
joonas.fi

1
@ newplayer65 ci sono diversi modi per farlo. Un modo potrebbe essere quello di creare sync.WaitGroup in main (), chiamare Add (1) su di esso e passarvi un puntatore a startHttpServer () e chiamare defer waitGroup.Done () all'inizio della goroutine che ha una chiamata al ListenAndServe (). quindi chiama solo waitGroup.Wait () alla fine di main () per aspettare che la goroutine abbia finito il suo lavoro.
joonas.fi

1
@ newplayer65 Ho guardato il tuo codice. L'utilizzo di un canale è una buona opzione, probabilmente migliore del mio suggerimento. Il mio codice era principalmente per dimostrare Shutdown () - non per mostrare il codice di qualità della produzione :) Ps il logo "server gopher" del tuo progetto è adorabile! : D
joonas.fi

73

Come menzionato nella yo.ian.grisposta di. Go 1.8 ha incluso questa funzionalità nella libreria standard.

Esempio minimo per 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.

Risposta originale - Pre Go 1.8:

Basandosi sulla risposta di Uvelichitel .

È possibile creare la propria versione di ListenAndServecui restituisce io.Closere non blocca.

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
}

Codice completo disponibile qui .

Il server HTTP si chiuderà con l'errore accept tcp [::]:8080: use of closed network connection


Ho creato un pacchetto che fa il boilerplate per te github.com/pseidemann/finish
pseidemann

24

Go 1.8 includerà l'arresto grazioso e forzato, disponibile rispettivamente tramite Server::Shutdown(context.Context)e Server::Close().

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

srv.Shutdown(context)

Il commit pertinente può essere trovato qui


7
mi dispiace essere pignolo, e so che il tuo codice era un utilizzo puramente esemplificativo, ma come regola generale: go func() { X() }()seguito da Y()fa il falso presupposto al lettore che X()verrà eseguito prima Y(). I gruppi di attesa ecc. Assicurano che bug temporali come questo non ti mordano quando meno te lo aspetti!
colm.anseo

20

Puoi costruire net.Listener

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

che puoi Close()

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

e http.Serve()su di esso

http.Serve(l, service.router)

1
grazie ma questo non risponde alla mia domanda. Chiedo http.ListenAndServeper motivi specifici. È così che utilizzo la libreria GWT MUX, non sono sicuro di come utilizzare net.listen per questo ..
jim

6
Si utilizza http.Serve () invece di http.ListenAndServe () esattamente allo stesso modo con la stessa sintassi solo con il proprio Listener. http.Serve (net.Listener, gorilla.mux.Router)
Uvelichitel

Ah fantastico, grazie. Non l'ho ancora testato ma dovrebbe funzionare.
jim

1
Un po 'tardi, ma abbiamo utilizzato il pacchetto manners per questo caso d'uso. È un sostituto immediato per il pacchetto http standard che consente un arresto regolare (cioè termina tutte le richieste attive rifiutando quelle nuove, quindi esce).
Kaedys

13

Poiché nessuna delle risposte precedenti dice perché non puoi farlo se usi http.ListenAndServe (), sono entrato nel codice sorgente http v1.8 ed ecco cosa dice:

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

Come puoi vedere, la funzione http.ListenAndServe non restituisce la variabile del server. Ciò significa che non puoi accedere al "server" per utilizzare il comando Shutdown. Pertanto, è necessario creare la propria istanza "server" invece di utilizzare questa funzione per implementare l'arresto normale.


2

Puoi chiudere il server chiudendo il suo contesto.

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
}

E ogni volta che sei pronto per chiuderlo, chiama:

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

"Chiudere il server non è una brutta cosa. Vai ..." :)
Paul Knopf

Una cosa degna di nota è che, per un arresto grazioso, prima di uscire è necessario attendere il ritorno di Shutdown, cosa che non sembra accadere qui.
Marcin Bilski

Il tuo utilizzo di ctxa server.Shutdownnon è corretto. Il contesto è già stato cancellato, quindi non verrà chiuso in modo pulito. Potresti aver ben chiesto server.Closeun arresto impuro. (Per un arresto pulito questo codice dovrebbe essere ampiamente rielaborato.
Dave C

0

È possibile risolvere questo problema context.Contextutilizzando un file net.ListenConfig. Nel mio caso, non ho voluto usare un sync.WaitGroupo http.Server's Shutdown()chiamata, e invece affidamento su una context.Context(che è stato chiuso con un segnale).

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
}

-7

Quello che ho fatto per questi casi in cui l'applicazione è solo il server e non esegue altre funzioni è installare un http.HandleFuncper un modello simile /shutdown. Qualcosa di simile a

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)
    }
})

Non richiede 1.8. Ma se 1.8 è disponibile, allora quella soluzione può essere incorporata qui invece della os.Exit(0)chiamata, se desiderabile, credo.

Il codice per eseguire tutto il lavoro di pulizia viene lasciato come esercizio per il lettore.

Credito extra se puoi dire dove potrebbe essere posizionato più ragionevolmente il codice di pulizia, perché non consiglierei di farlo qui e come questo colpo di endpoint dovrebbe causare l'invocazione di quel codice.

Più credito extra se puoi dire dove quella os.exit(0)chiamata (o qualunque uscita di processo scegli di usare), fornita qui solo a scopo illustrativo, sarebbe più ragionevolmente collocata.

Ancora più credito extra se puoi spiegare perché questo meccanismo di segnalazione del processo del server HTTP dovrebbe essere considerato al di sopra di tutti gli altri meccanismi ritenuti funzionanti in questo caso.


Naturalmente, ho risposto alla domanda come richiesto senza ulteriori ipotesi sulla natura del problema e, in particolare, senza ipotesi su un determinato ambiente di produzione. Ma per la mia edificazione, @MarcinBilski, esattamente quali requisiti renderebbero questa soluzione non adatta a qualsiasi ambiente, produzione o altro?
greg.carter

2
Lo intendeva più ironico di ogni altra cosa perché è chiaro che non avresti un gestore / shutdown in un'app di produzione. :) Tutto va bene per gli strumenti interni, immagino. A parte questo, ci sono modi per spegnere con grazia il server in modo che non interrompa improvvisamente le connessioni o si blocchi durante una transazione di database o, peggio, durante la scrittura su disco ecc.
Marcin Bilski

Certamente, non può essere il caso che gli elettori down siano privi di fantasia. Deve essere che presumo troppa immaginazione. Ho aggiornato la risposta (incluso l'esempio) per correggere il mio errore.
greg.carter
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.