工作进程和HTTP服务器的优雅关闭

huangapple go评论74阅读模式
英文:

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 (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;log&quot;
    &quot;net/http&quot;
    &quot;os&quot;
    &quot;os/signal&quot;
    &quot;sync&quot;
    &quot;syscall&quot;
    &quot;time&quot;
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go func() {
        signals := make(chan os.Signal, 1)
        signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
        &lt;-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() {
        &lt;-ctx.Done()
        shouldStop = true
    }()

    for !shouldStop {
        fmt.Println(&quot;https://gosamples.dev is the best&quot;)
        time.Sleep(1 * time.Second)
    }

    return nil
}

func startServer(ctx context.Context) error {
    var srv http.Server

    go func() {
        &lt;-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(&quot;HTTP server Shutdown: %v&quot;, err)
        }
    }()

    if err := srv.ListenAndServe(); err != http.ErrServerClosed {
        // Error starting or closing listener:
        return fmt.Errorf(&quot;HTTP server ListenAndServe: %w&quot;, err)
    }

    log.Printf(&quot;Listening on HTTP server port: %s&quot;, srv.Addr)

    http.HandleFunc(&quot;/readiness&quot;, func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
    })
    http.HandleFunc(&quot;/liveness&quot;, 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 &quot;:http&quot;, 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(&quot;Listening on HTTP server port: %s&quot;, 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 := &amp;http.Server{
		Addr: &quot;:8080&quot;, // Define the address where you want the server to listen
	}

	http.HandleFunc(&quot;/readiness&quot;, func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
	})
	http.HandleFunc(&quot;/liveness&quot;, func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
	})

	go func() {
		&lt;-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(&quot;HTTP server Shutdown: %v&quot;, err)
		}
	}()

	log.Printf(&quot;Listening on HTTP server port: %s&quot;, srv.Addr)

	if err := srv.ListenAndServe(); err != nil &amp;&amp; err != http.ErrServerClosed {
		// Error starting or closing listener:
		return fmt.Errorf(&quot;HTTP server ListenAndServe: %w&quot;, 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.

huangapple
  • 本文由 发表于 2023年5月28日 03:08:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/76348588.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定