วิธีหยุด http.ListenAndServe ()


91

ฉันใช้ไลบรารี Mux จาก Gorilla Web Toolkit พร้อมกับเซิร์ฟเวอร์ Go http ที่ให้มา

ปัญหาคือในแอปพลิเคชันของฉันเซิร์ฟเวอร์ HTTP เป็นเพียงส่วนประกอบเดียวและจำเป็นต้องหยุดและเริ่มต้นตามดุลยพินิจของฉัน

เมื่อฉันเรียกhttp.ListenAndServe(fmt.Sprintf(":%d", service.Port()), service.router)มันว่าบล็อกและดูเหมือนว่าฉันจะหยุดเซิร์ฟเวอร์ไม่ให้ทำงานไม่ได้

ฉันทราบว่าปัญหานี้เคยเป็นปัญหาในอดีตยังคงเป็นเช่นนั้นอยู่หรือไม่? มีโซลูชันใหม่ ๆ หรือไม่?

คำตอบ:


92

เกี่ยวกับการปิดระบบอย่างสง่างาม (แนะนำใน Go 1.8) ตัวอย่างที่เป็นรูปธรรมอีกเล็กน้อย:

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
ใช่คุณลักษณะนี้คือ Shutdown () ซึ่งฉันกำลังสาธิตการใช้งานที่เป็นรูปธรรมที่นี่ ขอบคุณฉันน่าจะชัดเจนกว่านี้ตอนนี้ฉันเปลี่ยนหัวข้อเป็น: "เกี่ยวกับการปิดระบบอย่างนิ่มนวล (แนะนำใน Go 1.8) เป็นตัวอย่างที่ชัดเจนกว่านี้:"
joonas.fi

เมื่อฉันผ่านnilไปฉันจะได้รับsrv.Shutdown panic: runtime error: invalid memory address or nil pointer dereferenceการส่งผ่านcontext.Todo()แทนใช้งานได้
Hubro

1
@Hubro มันแปลกมากฉันเพิ่งลองทำกับ Golang เวอร์ชันล่าสุด (1.10) และมันก็ใช้ได้ดี บริบทพื้นหลัง () หรือบริบท TODO () ใช้ได้ผลและถ้ามันเหมาะกับคุณก็ดี :)
joonas.fi

1
@ newplayer65 มีหลายวิธีในการทำเช่นนั้น วิธีหนึ่งคือการสร้างการซิงค์ WaitGroup ใน main () เรียก Add (1) จากนั้นส่งตัวชี้ไปที่ startHttpServer () และเรียก waitGroup ที่เลื่อนออกไป Done () ที่จุดเริ่มต้นของ goroutine ที่มีการเรียกไปที่ ListenAndServe () จากนั้นโทรไปที่ waitGroup.Wait () ที่ส่วนท้ายของ main () เพื่อรอให้ goroutine ทำงานเสร็จ
joonas.fi

1
@ newplayer65 ฉันดูรหัสของคุณ การใช้ช่องเป็นทางเลือกที่ดีน่าจะดีกว่าคำแนะนำของฉัน รหัสของฉันส่วนใหญ่จะแสดงให้เห็นถึงการปิดระบบ () - ไม่ใช่การแสดงรหัสคุณภาพการผลิต :) Ps โลโก้ "เซิร์ฟเวอร์โกเฟอร์" ของโครงการของคุณคือ adorbs! : D
joonas.fi

73

ดังที่กล่าวไว้ในyo.ian.gคำตอบของ Go 1.8 ได้รวมฟังก์ชันนี้ไว้ใน lib มาตรฐาน

ตัวอย่างน้อยที่สุดสำหรับ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.

คำตอบเดิม - Pre Go 1.8:

สร้างจากคำตอบของ Uvelichitel

คุณสามารถสร้างเวอร์ชันของคุณเองListenAndServeซึ่งจะส่งคืนio.Closerและไม่บล็อก

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
}

รหัสเต็มมีที่นี่

เซิร์ฟเวอร์ HTTP จะปิดลงพร้อมกับข้อผิดพลาด accept tcp [::]:8080: use of closed network connection


ฉันสร้างแพ็คเกจที่สร้างต้นแบบสำหรับคุณgithub.com/pseidemann/finish
pseidemann

24

Go 1.8 จะรวมถึงการปิดระบบที่สง่างามและมีประสิทธิภาพพร้อมใช้งานผ่านServer::Shutdown(context.Context)และServer::Close()ตามลำดับ

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

srv.Shutdown(context)

การคอมมิตที่เกี่ยวข้องสามารถพบได้ที่นี่


7
ขออภัยที่จู้จี้จุกจิกและฉันรู้ว่ารหัสของคุณเป็นเพียงตัวอย่างการใช้งาน แต่ตามกฎทั่วไป: go func() { X() }()ตามด้วยY()การตั้งสมมติฐานที่ผิดให้กับผู้อ่านที่X()จะดำเนินการก่อนหน้าY()นี้ Waitgroups ฯลฯ ตรวจสอบให้แน่ใจว่าข้อบกพร่องของเวลาเช่นนี้จะไม่กัดคุณเมื่อคาดหวังน้อยที่สุด!
colm.anseo

20

คุณสามารถสร้าง net.Listener

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

ซึ่งคุณสามารถทำได้ Close()

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

และhttp.Serve()กับมัน

http.Serve(l, service.router)

1
ขอบคุณ แต่นั่นไม่ได้ตอบคำถามของฉัน ฉันกำลังถามเกี่ยวกับhttp.ListenAndServeเหตุผลที่เฉพาะเจาะจง นั่นคือวิธีที่ฉันใช้ไลบรารี GWT MUX ฉันไม่แน่ใจว่าจะใช้ net.listen อย่างไร ..
jim

6
คุณใช้ http.Serve () แทน http.ListenAndServe () ในลักษณะเดียวกันกับไวยากรณ์เดียวกันกับ Listener ของตัวเอง http.Serve (net.Listener, gorilla.mux.Router)
Uvelichitel

ขอบคุณมากขอบคุณ ฉันยังไม่ได้ทดสอบ แต่น่าจะใช้ได้
jim

1
ช้าไปหน่อย แต่เราใช้แพ็คเกจมารยาทสำหรับกรณีการใช้งานนี้ เป็นการแทนที่แบบดร็อปอินสำหรับแพ็กเกจ http มาตรฐานที่อนุญาตให้ปิดระบบได้อย่างสง่างาม (เช่นเสร็จสิ้นคำขอที่ใช้งานอยู่ทั้งหมดในขณะที่ปฏิเสธคำขอใหม่จากนั้นจึงออก)
Kaedys

13

เนื่องจากไม่มีคำตอบก่อนหน้านี้บอกว่าทำไมคุณถึงทำไม่ได้ถ้าคุณใช้ http.ListenAndServe () ฉันจึงเข้าไปในซอร์สโค้ด v1.8 http และนี่คือสิ่งที่กล่าวว่า:

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

ดังที่คุณเห็นฟังก์ชัน http.ListenAndServe ไม่ส่งคืนตัวแปรเซิร์ฟเวอร์ ซึ่งหมายความว่าคุณไม่สามารถไปที่ 'เซิร์ฟเวอร์' เพื่อใช้คำสั่งปิดเครื่องได้ ดังนั้นคุณต้องสร้างอินสแตนซ์ 'เซิร์ฟเวอร์' ของคุณเองแทนที่จะใช้ฟังก์ชั่นนี้เพื่อให้การปิดระบบเป็นไปอย่างราบรื่น


2

คุณสามารถปิดเซิร์ฟเวอร์ได้โดยปิดบริบท

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
}

และเมื่อใดก็ตามที่คุณพร้อมที่จะปิดให้โทร:

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

"การปิดเซิร์ฟเวอร์ไม่ใช่สิ่งที่ไม่ดี ffs Go ... " :)
Paul Knopf

สิ่งหนึ่งที่ควรสังเกตคือสำหรับการปิดเครื่องอย่างสง่างามก่อนที่จะออกคุณต้องรอให้ Shutdown กลับมาซึ่งดูเหมือนจะไม่เกิดขึ้นที่นี่
Marcin Bilski

การใช้ctxto ของคุณserver.Shutdownไม่ถูกต้อง บริบทถูกยกเลิกไปแล้วดังนั้นจึงไม่สามารถปิดได้อย่างสมบูรณ์ คุณอาจเรียกร้องserver.Closeให้ปิดระบบที่ไม่สะอาด (สำหรับการปิดระบบใหม่ทั้งหมดรหัสนี้จะต้องมีการทำงานใหม่อย่างกว้างขวาง
Dave C

0

เป็นไปได้ที่จะแก้ปัญหานี้โดยcontext.Contextใช้ไฟล์net.ListenConfig. ในกรณีของฉันฉันไม่ต้องการที่จะใช้sync.WaitGroupหรือhttp.Server's Shutdown()โทรและแทนที่จะพึ่งพา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
}

-7

สิ่งที่ฉันทำในกรณีเช่นนี้ที่แอปพลิเคชันเป็นเพียงเซิร์ฟเวอร์และไม่มีฟังก์ชันอื่นใดที่ติดตั้งhttp.HandleFuncสำหรับรูปแบบเช่น/shutdown. สิ่งที่ต้องการ

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

ไม่ต้องใช้ 1.8 แต่ถ้า 1.8 พร้อมใช้งานโซลูชันนั้นสามารถฝังที่นี่แทนการos.Exit(0)โทรถ้าต้องการฉันเชื่อ

รหัสสำหรับดำเนินการล้างข้อมูลทั้งหมดจะถูกปล่อยให้เป็นแบบฝึกหัดสำหรับผู้อ่าน

เครดิตพิเศษหากคุณสามารถบอกได้ว่ารหัสการล้างข้อมูลนั้นควรวางไว้ที่ใดอย่างสมเหตุสมผลที่สุดเพราะฉันไม่แนะนำให้ทำที่นี่และวิธีการที่จุดสิ้นสุดนี้จะทำให้เกิดการเรียกใช้รหัสนั้น

เครดิตพิเศษเพิ่มเติมหากคุณสามารถพูดได้ว่าการos.exit(0)โทรนั้น(หรือกระบวนการใดก็ตามที่คุณเลือกใช้) ให้ไว้ที่นี่เพื่อเป็นภาพประกอบเท่านั้นจะได้รับการจัดวางอย่างสมเหตุสมผลที่สุด

แต่เครดิตแม้พิเศษมากขึ้นถ้าคุณสามารถอธิบายได้ว่าทำไมนี้กลไกของการส่งสัญญาณ HTTP กระบวนการเซิร์ฟเวอร์ควรได้รับการพิจารณาข้างต้นกลไกอื่น ๆ ทุกคนคิดว่าสามารถทำงานได้ในกรณีนี้


แน่นอนฉันตอบคำถามตามที่ถามโดยไม่มีการตั้งสมมติฐานเพิ่มเติมเกี่ยวกับลักษณะของปัญหาและโดยเฉพาะอย่างยิ่งไม่มีสมมติฐานเกี่ยวกับสภาพแวดล้อมการผลิตใด ๆ แต่เพื่อความจรรโลงใจของฉันเอง @MarcinBilski ข้อกำหนดใดที่ทำให้โซลูชันนี้ไม่เหมาะสมกับสภาพแวดล้อมการผลิตหรืออย่างอื่น?
greg.carter

2
หมายถึงแก้มลิ้นมากกว่าอะไรเพราะชัดเจนว่าคุณไม่มีตัวจัดการ / ปิดเครื่องในแอปที่ใช้งานจริง :) อะไรก็ตามที่เป็นเครื่องมือภายในฉันเดา นอกจากนี้ยังมีวิธีการปิดเซิร์ฟเวอร์อย่างสง่างามเพื่อที่จะไม่ทำให้การเชื่อมต่อหยุดลงอย่างกะทันหันหรือขัดข้องระหว่างการทำธุรกรรมฐานข้อมูลหรือที่แย่กว่านั้นคือขณะเขียนลงดิสก์เป็นต้น
Marcin Bilski

แน่นอนว่าไม่สามารถเป็นไปได้ที่ผู้มีสิทธิเลือกตั้งจะไม่สามารถจินตนาการได้ คงเป็นเรื่องที่ฉันใช้จินตนาการมากเกินไป ฉันได้อัปเดตการตอบกลับ (รวมถึงตัวอย่าง) เพื่อแก้ไขข้อผิดพลาดของฉัน
greg.carter
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.