http.ListenAndServe () 중지 방법


91

번들 Go http 서버와 함께 Gorilla Web Toolkit의 Mux 라이브러리를 사용하고 있습니다.

문제는 내 응용 프로그램에서 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

내가 전달하는 경우 nilsrv.Shutdown나는 얻을 panic: runtime error: invalid memory address or nil pointer dereference. context.Todo()대신 통과 가 작동합니다.
Hubro

1
@Hubro 그거 이상합니다. 최신 Golang 버전 (1.10)에서 이것을 시도했지만 정상적으로 실행되었습니다. context.Background () 또는 context.TODO () 물론 작동하며 그것이 당신을 위해 작동한다면 좋습니다. :)
joonas.fi

1
@ newplayer65에는 여러 가지 방법이 있습니다. 한 가지 방법은 main ()에 sync.WaitGroup을 만들고 Add (1)을 호출 한 다음 포인터를 startHttpServer ()에 전달하고 defer waitGroup.Done ()을 호출하는 goroutine을 ListenAndServe (). 그런 다음 main () 끝에 waitGroup.Wait ()를 호출하여 goroutine이 작업을 완료 할 때까지 기다리십시오.
joonas.fi

1
@ newplayer65 나는 당신의 코드를 보았다. 채널을 사용하는 것이 제 제안보다 좋을 것입니다. 내 코드는 주로 Shutdown ()을 보여주기위한 것이 었습니다. 프로덕션 품질 코드를 보여주지 않았습니다. :) 추신 프로젝트의 "server gopher"로고는 adorbs입니다! : D
joonas.fi

70

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()독자에게 잘못된 가정을합니다 . Waitgroups 등은 이와 같은 타이밍 버그가 최소한 예상 할 때 당신을 물지 않도록 보장합니다! X()Y()
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.ListenAndServe () 대신에 http.Serve ()를 사용합니다. 리스너와 동일한 구문으로 정확히 동일한 방식으로 사용합니다. http.Serve (net.Listener, gorilla.mux.Router)
Uvelichitel 2016 년

아, 고마워. 아직 테스트하지는 않았지만 작동해야합니다.
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 함수는 서버 변수를 반환하지 않습니다. 즉, Shutdown 명령을 사용하기 위해 '서버'에 액세스 할 수 없습니다. 따라서 정상적인 종료를 구현하려면이 기능을 사용하는 대신 고유 한 '서버'인스턴스를 만들어야합니다.


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 이동하지 않습니다 ...":
폴 크노프

주목할 가치가있는 한 가지는 정상적인 종료를 위해 종료하기 전에 여기서 발생하지 않는 것으로 보이는 종료가 반환 될 때까지 기다려야한다는 것입니다.
Marcin Bilski

의 사용 ctx으로는 server.Shutdown올바르지 않습니다. 컨텍스트가 이미 취소되었으므로 완전히 종료되지 않습니다 . server.Close불결한 종료를 요구했을 수도 있습니다 . (완전 종료를 들어이 코드는 광범위하게 다시 일을 할 필요가있다.
데이브 C에게

0

또한 이것은 사용 해결할 수있다 context.Context사용 a net.ListenConfig. 제 경우에는 sync.WaitGroup또는 http.ServerShutdown()호출 을 사용하고 싶지 않았고 대신 a 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

응용 프로그램이 서버 일 뿐이고 다른 기능을 수행하지 않는 경우 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
프로덕션 앱에 / shutdown 핸들러가 없다는 것이 분명하기 때문에 무엇보다 혀가 혀를 내 밀었습니다. :) 모든 것이 내부 툴링에 사용됩니다. 그 외에, 갑자기 등을 디스크에 기록하는 동안, 더 나쁜, 데이터베이스 트랜잭션을 통해 연결 또는 충돌 중도를 떨어 뜨리거나하지 않도록 서버를 정상적으로 종료하는 방법이있다
마르신 Bilski

확실히, 투표자가 상상력이없는 경우는있을 수 없습니다. 내가 너무 많은 상상력을 가정하는 것임에 틀림 없다. 내 오류를 수정하기 위해 응답 (예제 포함)을 업데이트했습니다.
greg.carter
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.