英文:
Worker and HTTP server graceful shutdown
问题
我正在尝试创建一个独立启动并监听终止事件的工作程序和HTTP服务器,并在完成后优雅地退出。
由于某种原因,工作程序启动了,但是HTTP服务器直到发送SIGTERM事件后才启动。只有在发送sigterm事件后,HTTP服务器才会启动。以下是代码中的问题所在:
输出
https://gosamples.dev is the best
https://gosamples.dev is the best
https://gosamples.dev is the best
^C2023/05/27 15:07:52 Listening on HTTP server port:
进程以退出代码0结束
代码
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
<-signals
cancel()
}()
var wg sync.WaitGroup
wg.Add(1)
go func() {
if err := myWorker(ctx); err != nil {
cancel()
}
wg.Done()
}()
wg.Add(1)
go func() {
if err := startServer(ctx); err != nil {
cancel()
}
wg.Done()
}()
wg.Wait()
}
func myWorker(ctx context.Context) error {
shouldStop := false
go func() {
<-ctx.Done()
shouldStop = true
}()
for !shouldStop {
fmt.Println("https://gosamples.dev is the best")
time.Sleep(1 * time.Second)
}
return nil
}
func startServer(ctx context.Context) error {
var srv http.Server
go func() {
<-ctx.Done() // 等待上下文完成
// 关闭服务器
if err := srv.Shutdown(context.Background()); err != nil {
// 关闭监听器时出错或上下文超时:
log.Printf("HTTP server Shutdown: %v", err)
}
}()
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
// 启动或关闭监听器时出错:
return fmt.Errorf("HTTP server ListenAndServe: %w", err)
}
log.Printf("Listening on HTTP server port: %s", srv.Addr)
http.HandleFunc("/readiness", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
http.HandleFunc("/liveness", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
return nil
}
英文:
I am trying to create a worker and an HTTP server which start independently, and listen for terminations and exit gracefully upon completion.
For some reason, the worker starts, but the HTTP server does not, until the SIGTERM event is sent. Only after the sigterm event is sent, then the http server starts. Where is the problem with the following?
Output
https://gosamples.dev is the best
https://gosamples.dev is the best
https://gosamples.dev is the best
^C2023/05/27 15:07:52 Listening on HTTP server port:
Process finished with the exit code 0
Code
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
<-signals
cancel()
}()
var wg sync.WaitGroup
wg.Add(1)
go func() {
if err := myWorker(ctx); err != nil {
cancel()
}
wg.Done()
}()
wg.Add(1)
go func() {
if err := startServer(ctx); err != nil {
cancel()
}
wg.Done()
}()
wg.Wait()
}
func myWorker(ctx context.Context) error {
shouldStop := false
go func() {
<-ctx.Done()
shouldStop = true
}()
for !shouldStop {
fmt.Println("https://gosamples.dev is the best")
time.Sleep(1 * time.Second)
}
return nil
}
func startServer(ctx context.Context) error {
var srv http.Server
go func() {
<-ctx.Done() // Wait for the context to be done
// Shutdown the server
if err := srv.Shutdown(context.Background()); err != nil {
// Error from closing listeners, or context timeout:
log.Printf("HTTP server Shutdown: %v", err)
}
}()
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
// Error starting or closing listener:
return fmt.Errorf("HTTP server ListenAndServe: %w", err)
}
log.Printf("Listening on HTTP server port: %s", srv.Addr)
http.HandleFunc("/readiness", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
http.HandleFunc("/liveness", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
return nil
}
答案1
得分: 5
如果我正确阅读了你的代码,你是在定义路由处理程序之前启动服务器。这意味着当服务器启动时,它不知道你的 /readiness
和 /liveness
端点,因为你还没有添加它们。结果,服务器启动了,但它不执行任何操作,因为它没有处理的路由。
另外,你没有在 http.Server
实例中定义 Addr
字段。ListenAndServe()
使用调用它的 http.Server
实例中定义的地址。如果该字段为空,则默认为 ":http"
,但你的代码中没有明确说明,这可能会导致混淆。
我将
srv.ListenAndServe
移到了 startServer 的最后。我错过了什么?
问题不在于 srv.ListenAndServe
在函数中的位置,而在于如何配置 http.Server
以及何时设置 HTTP 处理程序。
在原始代码中,你是在服务器启动后设置 HTTP 处理程序的。处理程序需要在启动服务器之前设置,因为一旦服务器运行,它将不会接收到后面定义的任何新处理程序。
而且日志语句 log.Printf("Listening on HTTP server port: %s", srv.Addr)
在 srv.ListenAndServe()
之后,而这是一个阻塞调用。这意味着日志语句只会在服务器停止后运行,这就是为什么你只在发送 SIGTERM 信号后看到它的原因。
尝试按照以下方式重新组织你的 startServer
函数:
func startServer(ctx context.Context) error {
srv := &http.Server{
Addr: ":8080", // 定义服务器监听的地址
}
http.HandleFunc("/readiness", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
http.HandleFunc("/liveness", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
go func() {
<-ctx.Done() // 等待上下文完成
// 关闭服务器
if err := srv.Shutdown(context.Background()); err != nil {
// 关闭监听器时出错,或者上下文超时:
log.Printf("HTTP server Shutdown: %v", err)
}
}()
log.Printf("Listening on HTTP server port: %s", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
// 启动或关闭监听器时出错:
return fmt.Errorf("HTTP server ListenAndServe: %w", err)
}
return nil
}
在修改后的 startServer
函数中,服务器现在知道你的 /readiness
和 /liveness
端点,因为它们在服务器启动之前就定义了。HTTP 处理程序在服务器启动之前设置,并且日志语句在服务器启动之前打印。这应该解决你的问题,并允许服务器按预期启动和处理请求。此外,现在服务器知道要监听的地址,因为 Addr
被明确定义了。
英文:
If I read your code correctly, you start the server before you are defining your route handlers. This means that when the server starts, it does not know about your /readiness
and /liveness
endpoints because you have not added them yet. As a result, the server starts, but it does not do anything because it has no routes to handle.
Then, you do not define the Addr
field in your http.Server
instance. ListenAndServe()
uses the address defined in the Addr
field of the http.Server
instance it is called on. If it is empty, it defaults to ":http"
, but this is not explicitly stated in your code which could lead to confusion.
> I moved srv.ListenAndServe
to the very end of startServer. What did I missed?
The issue isn't where srv.ListenAndServe
is located in the function, but rather how the http.Server
is configured and when the HTTP handlers are set up.
In the original code, you are setting up the HTTP handlers after the server has started. Handlers need to be set up before starting the server because once the server is running, it will not pick up any new handlers that are defined later.
And the log statement log.Printf("Listening on HTTP server port: %s", srv.Addr)
is after srv.ListenAndServe()
, which is a blocking call. This means that the log statement will only run after the server has stopped, which is why you only see it after sending the SIGTERM signal.
Try and reorganize your startServer
function like this:
func startServer(ctx context.Context) error {
srv := &http.Server{
Addr: ":8080", // Define the address where you want the server to listen
}
http.HandleFunc("/readiness", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
http.HandleFunc("/liveness", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
go func() {
<-ctx.Done() // Wait for the context to be done
// Shutdown the server
if err := srv.Shutdown(context.Background()); err != nil {
// Error from closing listeners, or context timeout:
log.Printf("HTTP server Shutdown: %v", err)
}
}()
log.Printf("Listening on HTTP server port: %s", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
// Error starting or closing listener:
return fmt.Errorf("HTTP server ListenAndServe: %w", err)
}
return nil
}
In this modified version of your startServer
function, the server now knows about your /readiness
and /liveness
endpoints because they are defined before the server starts.
The HTTP handlers are set up before the server starts, and the log statement is printed before the server starts. This should solve your problem and allow the server to start and handle requests as expected. Also, now the server knows where to listen as Addr
is explicitly defined.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论