Bagaimana menghentikan http.ListenAndServe ()

91

Saya menggunakan pustaka Mux dari Gorilla Web Toolkit bersama dengan server http Go yang dibundel.

Masalahnya adalah bahwa dalam aplikasi saya, server HTTP hanya satu komponen dan diperlukan untuk berhenti dan memulai menurut kebijaksanaan saya.

Ketika saya menyebutnya http.ListenAndServe(fmt.Sprintf(":%d", service.Port()), service.router)blok dan sepertinya saya tidak bisa menghentikan server agar tidak berjalan.

Saya sadar ini telah menjadi masalah di masa lalu, apakah masih demikian? Apakah ada solusi baru?

jim
sumber

Jawaban:

92

Mengenai shutdown yang anggun (diperkenalkan di Go 1.8), contoh yang sedikit lebih konkret:

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")
}
joonas.fi
sumber
1
Ya, fiturnya adalah Shutdown (), yang penggunaan konkritnya saya tunjukkan di sini. Terima kasih, saya seharusnya lebih jelas, saya mengubah judul menjadi ini sekarang: "Mengenai pematian yang anggun (diperkenalkan di Go 1.8), contoh yang sedikit lebih konkret:"
joonas.fi
Ketika saya lolos nilke srv.Shutdownsaya dapatkan panic: runtime error: invalid memory address or nil pointer dereference. Mengoper context.Todo()malah berhasil.
Hubro
1
@Hubro itu aneh, saya baru saja mencoba ini di versi Golang terbaru (1.10), dan itu berjalan dengan baik. konteks.Background () atau context.TODO () tentu saja berfungsi dan jika berhasil untuk Anda, bagus. :)
joonas.fi
1
@ newplayer65 ada beberapa cara untuk melakukan itu. Salah satu caranya adalah dengan membuat sync.WaitGroup di main (), panggil Add (1) di atasnya dan teruskan pointer ke sana ke startHttpServer () dan panggil defer waitGroup.Done () di awal goroutine yang memiliki panggilan ke ListenAndServe (). lalu panggil waitGroup.Wait () di akhir main () untuk menunggu goroutine menyelesaikan tugasnya.
joonas.fi
1
@ newplayer65 Saya melihat kode Anda. Menggunakan saluran adalah pilihan yang bagus, mungkin lebih baik daripada saran saya. Kode saya terutama untuk mendemonstrasikan Shutdown () - bukan menampilkan kode kualitas produksi :) Untuk logo "server gopher" proyek Anda adalah adorbs! : D
joonas.fi
70

Seperti yang disebutkan dalam yo.ian.gjawaban. Go 1.8 telah memasukkan fungsionalitas ini dalam lib standar.

Contoh minimal untuk 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.

Jawaban Asli - Pre Go 1.8:

Membangun jawaban Uvelichitel .

Anda dapat membuat versi Anda sendiri ListenAndServeyang mengembalikan io.Closerdan tidak memblokir.

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
}

Kode lengkap tersedia di sini .

Server HTTP akan ditutup dengan kesalahan accept tcp [::]:8080: use of closed network connection

John S Perayil
sumber
Saya membuat paket yang sesuai untuk Anda github.com/pseidemann/finish
pseidemann
24

Go 1.8 akan mencakup shutdown yang anggun dan paksa, tersedia melalui Server::Shutdown(context.Context)dan Server::Close()masing - masing.

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

srv.Shutdown(context)

Komit yang relevan dapat ditemukan di sini

yo.ian.g
sumber
7
maaf pilih-pilih, dan saya tahu kode Anda adalah murni contoh penggunaan, tetapi sebagai aturan umum: go func() { X() }()diikuti dengan Y()membuat asumsi palsu kepada pembaca yang X()akan dieksekusi sebelumnya Y(). Waitgroups dll. Memastikan kesalahan waktu seperti ini tidak mengganggu Anda saat tidak diharapkan!
colm.anseo
20

Anda bisa membangun net.Listener

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

yang Anda bisa Close()

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

dan http.Serve()di atasnya

http.Serve(l, service.router)
Uvelichitel
sumber
1
terima kasih tapi itu tidak menjawab pertanyaan saya. Saya bertanya tentang http.ListenAndServealasan tertentu. Begitulah cara saya menggunakan perpustakaan GWT MUX, saya tidak yakin cara menggunakan net.listen untuk itu ..
jim
6
Anda menggunakan http.Serve () daripada http.ListenAndServe () dengan cara yang persis sama dengan sintaks yang sama hanya dengan Listener sendiri. http.Serve (net.Listener, gorilla.mux.Router)
Uvelichitel
Ah bagus, terima kasih. Saya belum menguji tetapi harus bekerja.
jim
1
Agak terlambat, tapi kami telah menggunakan paket tata krama untuk kasus penggunaan ini. Ini adalah pengganti drop-in untuk paket http standar yang memungkinkan shutdown yang anggun (mis. Menyelesaikan semua permintaan aktif sambil menolak yang baru, lalu keluar).
Kaedys
13

Karena tidak ada jawaban sebelumnya yang mengatakan mengapa Anda tidak dapat melakukannya jika Anda menggunakan http.ListenAndServe (), saya masuk ke kode sumber http v1.8 dan inilah yang dikatakan:

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

Seperti yang Anda lihat, fungsi http.ListenAndServe tidak mengembalikan variabel server. Ini berarti Anda tidak bisa masuk ke 'server' untuk menggunakan perintah Shutdown. Oleh karena itu, Anda perlu membuat instance 'server' Anda sendiri alih-alih menggunakan fungsi ini agar shutdown yang tepat dapat diimplementasikan.

juz
sumber
2

Anda dapat menutup server dengan menutup konteksnya.

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
}

Dan kapan pun Anda siap untuk menutupnya, panggil:

ctx, closeServer := context.WithCancel(context.Background())
err := ServeReqs(ctx, etc)
closeServer()
Lukas Lukac
sumber
"Mematikan server bukanlah sesuatu yang buruk ffs Go ..." :)
Paul Knopf
Satu hal yang perlu diperhatikan adalah, untuk shutdown yang anggun, sebelum keluar Anda harus menunggu Shutdown kembali yang tampaknya tidak terjadi di sini.
Marcin Bilski
Penggunaan Anda ctxuntuk server.Shutdownsalah. Konteksnya sudah dibatalkan sehingga tidak akan bisa dimatikan dengan bersih. Anda mungkin telah menyerukan server.Closepenutupan yang tidak bersih. (Untuk pemadaman yang bersih, kode ini perlu dikerjakan ulang secara ekstensif.
Dave C
0

Hal ini dimungkinkan untuk menyelesaikan ini dengan context.Contextmenggunakan a net.ListenConfig. Dalam kasus saya, saya tidak ingin menggunakan sync.WaitGroupatau http.Server's Shutdown()panggilan, dan bukan mengandalkan pada context.Context(yang ditutup dengan sinyal).

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
}
Sean
sumber
-6

Apa yang telah saya lakukan untuk kasus seperti itu di mana aplikasi hanya server dan tidak melakukan fungsi lain adalah menginstal http.HandleFuncuntuk pola seperti /shutdown. Sesuatu seperti

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

Ini tidak membutuhkan 1.8. Tetapi jika 1.8 tersedia, maka solusi itu dapat disematkan di sini alih-alih os.Exit(0)panggilan jika diinginkan, saya yakin.

Kode untuk melakukan semua pekerjaan pembersihan itu dibiarkan sebagai latihan untuk pembaca.

Kredit tambahan jika Anda dapat mengatakan di mana kode pembersihan itu mungkin paling masuk akal ditempatkan, karena saya tidak akan merekomendasikan melakukannya di sini, dan bagaimana titik akhir ini harus menyebabkan pemanggilan kode itu.

Lebih banyak kredit ekstra jika Anda dapat mengatakan di mana os.exit(0)panggilan itu (atau proses keluar apa pun yang Anda pilih untuk digunakan), yang diberikan di sini hanya untuk tujuan ilustrasi, akan ditempatkan paling masuk akal.

Namun bahkan lebih ekstra kredit jika Anda bisa menjelaskan mengapa ini mekanisme HTTP Server proses signaling harus dipertimbangkan di atas semua mekanisme lain seperti berpikir bisa diterapkan dalam kasus ini.

greg.carter
sumber
Tentu saja, saya menjawab pertanyaan seperti yang ditanyakan tanpa asumsi lebih lanjut tentang sifat masalah, dan khususnya, tidak ada asumsi tentang lingkungan produksi tertentu. Tetapi untuk edifikasi saya sendiri, @MarcinBilski, persyaratan apa yang membuat solusi ini tidak cocok untuk lingkungan, produksi, atau lainnya?
greg.carter
2
Mengartikannya lebih bercanda daripada apa pun karena jelas Anda tidak akan memiliki penangan / shutdown di aplikasi produksi. :) Apa pun yang berlaku untuk perkakas internal saya kira. Selain itu, ada cara untuk mematikan server dengan baik sehingga tidak tiba-tiba putus koneksi atau crash di tengah jalan melalui transaksi database atau, lebih buruk lagi, saat menulis ke disk, dll.
Marcin Bilski
Tentu saja, tidak mungkin para pemilih yang down itu tidak imajinatif. Pasti saya menganggap terlalu banyak imajinasi. Saya telah memperbarui tanggapan (termasuk contoh) untuk memperbaiki kesalahan saya.
greg.carter