方式1:nginx做负载a、b,流量全切到a或者b
方式2:代码形式
//go:build darwin || linux
package main
import (
"context"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
)
var version = "v1"
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
exe, err := os.Executable()
if err != nil {
return err
}
// Preserve this path across generations, including atomic binary replacements.
binary := os.Getenv("SSE_BINARY")
if binary == "" {
binary = exe
}
binary, err = filepath.Abs(binary)
if err != nil {
return err
}
if os.Getenv("SSE_INHERITED") != "1" {
return supervise(binary)
}
f := os.NewFile(3, "listener")
ln, err := net.FileListener(f)
f.Close()
if err != nil {
return err
}
defer ln.Close()
sigs := make(chan os.Signal, 4)
signal.Notify(sigs, syscall.SIGUSR2, syscall.SIGTERM, syscall.SIGINT)
defer signal.Stop(sigs)
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "version=%s pid=%d\n", version, os.Getpid())
})
mux.HandleFunc("/sse", stream)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, page)
})
srv := &http.Server{Handler: mux, ReadHeaderTimeout: 10 * time.Second}
serveErr := make(chan error, 1)
go func() { serveErr <- srv.Serve(ln) }()
ready := os.NewFile(4, "ready")
if _, err := ready.Write([]byte{1}); err != nil {
ready.Close()
return err
}
ready.Close()
log.Printf("READY version=%s pid=%d addr=%s binary=%s", version, os.Getpid(), ln.Addr(), binary)
for {
select {
case err := <-serveErr:
return err
case sig := <-sigs:
if sig == syscall.SIGUSR2 {
continue
}
log.Printf("DRAIN version=%s pid=%d: waiting for existing SSE", version, os.Getpid())
// Deliberately do not cancel request contexts on the deployment signal.
err := srv.Shutdown(context.Background())
log.Printf("EXIT version=%s pid=%d", version, os.Getpid())
return err
}
}
}
type child struct {
cmd *exec.Cmd
done chan error
}
type exitEvent struct {
pid int
err error
}
func supervise(binary string) error {
addr := os.Getenv("ADDR")
if addr == "" {
addr = "127.0.0.1:8080"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
defer ln.Close()
sigs := make(chan os.Signal, 8)
signal.Notify(sigs, syscall.SIGUSR2, syscall.SIGTERM, syscall.SIGINT)
defer signal.Stop(sigs)
exits := make(chan exitEvent, 32)
children := map[int]*child{}
launch := func() (*child, error) { return startWorker(ln, binary, exits) }
current, err := launch()
if err != nil {
return err
}
children[current.cmd.Process.Pid] = current
pidFile := binary + ".pid"
if err := os.WriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())+"\n"), 0644); err != nil {
_ = current.cmd.Process.Signal(syscall.SIGTERM)
<-current.done
return err
}
defer os.Remove(pidFile)
log.Printf("SUPERVISOR pid=%d worker_pid=%d; stays in foreground", os.Getpid(), current.cmd.Process.Pid)
stopping := false
var failure error
stopAll := func() {
stopping = true
_ = ln.Close()
for _, c := range children {
_ = c.cmd.Process.Signal(syscall.SIGTERM)
}
}
for len(children) > 0 {
select {
case sig := <-sigs:
if stopping {
continue
}
if sig != syscall.SIGUSR2 {
stopAll()
continue
}
next, err := launch()
if err != nil {
log.Printf("UPGRADE FAILED; current worker retained: %v", err)
continue
}
children[next.cmd.Process.Pid] = next
old := current
current = next
log.Printf("UPGRADE READY old_pid=%d new_pid=%d supervisor_pid=%d", old.cmd.Process.Pid, next.cmd.Process.Pid, os.Getpid())
_ = old.cmd.Process.Signal(syscall.SIGTERM)
case e := <-exits:
if _, ok := children[e.pid]; !ok {
continue
}
delete(children, e.pid)
log.Printf("WORKER EXIT pid=%d err=%v", e.pid, e.err)
if !stopping && e.pid == current.cmd.Process.Pid {
failure = fmt.Errorf("active worker exited unexpectedly: pid=%d err=%v", e.pid, e.err)
stopAll()
}
}
}
return failure
}
func startWorker(ln net.Listener, binary string, exits chan<- exitEvent) (*child, error) {
raw, err := ln.(*net.TCPListener).SyscallConn()
if err != nil {
return nil, err
}
var fd int
var dupErr error
if err = raw.Control(func(original uintptr) {
fd, dupErr = syscall.Dup(int(original))
if dupErr == nil {
syscall.CloseOnExec(fd)
}
}); err != nil {
return nil, err
}
if dupErr != nil {
return nil, dupErr
}
// TCPListener.File changes shared socket blocking flags; raw Dup preserves them.
f := os.NewFile(uintptr(fd), "listener")
defer f.Close()
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
defer r.Close()
defer w.Close()
cmd := exec.Command(binary, os.Args[1:]...)
for _, e := range os.Environ() {
if !strings.HasPrefix(e, "SSE_INHERITED=") && !strings.HasPrefix(e, "SSE_BINARY=") {
cmd.Env = append(cmd.Env, e)
}
}
cmd.Env = append(cmd.Env, "SSE_INHERITED=1", "SSE_BINARY="+binary)
cmd.ExtraFiles = []*os.File{f, w}
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
// Ctrl+C goes to the supervisor, which drains all workers itself.
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if err := cmd.Start(); err != nil {
return nil, err
}
w.Close()
c := &child{cmd: cmd, done: make(chan error, 1)}
go func() {
err := cmd.Wait()
c.done <- err
exits <- exitEvent{pid: cmd.Process.Pid, err: err}
}()
ready := make(chan error, 1)
go func() {
var b [1]byte
_, err := io.ReadFull(r, b[:])
if err == nil && b[0] != 1 {
err = fmt.Errorf("invalid readiness byte")
}
ready <- err
}()
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
select {
case err = <-ready:
if err == nil {
return c, nil
}
case <-timer.C:
err = fmt.Errorf("new process readiness timeout")
}
_ = cmd.Process.Kill()
<-c.done
return nil, err
}
func stream(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", 500)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Accel-Buffering", "no")
seconds := 60
if n, err := strconv.Atoi(r.URL.Query().Get("seconds")); err == nil && n > 0 && n <= 600 {
seconds = n
}
log.Printf("SSE START version=%s pid=%d seconds=%d", version, os.Getpid(), seconds)
defer log.Printf("SSE END version=%s pid=%d", version, os.Getpid())
// This is a simulation. Real upstream AI requests should use r.Context()
// plus a bounded request timeout, not a signal-canceled global context.
for i := 0; i <= seconds; i++ {
_ = http.NewResponseController(w).SetWriteDeadline(time.Now().Add(10 * time.Second))
if _, err := fmt.Fprintf(w, "data: version=%s pid=%d token=%d/%d\n\n", version, os.Getpid(), i, seconds); err != nil {
return
}
flusher.Flush()
if i == seconds {
break
}
select {
case <-r.Context().Done():
return
case <-time.After(time.Second):
}
}
fmt.Fprint(w, "event: done\ndata: completed\n\n")
flusher.Flush()
}
const page = `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>Go SSE 热更新测试</title>
<style>body{font:16px system-ui;max-width:850px;margin:40px auto;padding:0 20px}button{padding:10px 16px;margin-right:12px}pre{background:#f2f4f7;padding:20px;height:420px;overflow:auto}</style>
<h1>Go SSE 热更新测试</h1><p>先启动旧流,发布新版后再启动新流;每条输出都有版本和进程 PID。流持续 60 秒。</p>
<button onclick="start()">新建一条 SSE</button><button onclick="health()">查看当前版本</button><pre id="log"></pre>
<script>let count=0;const box=document.getElementById('log');function line(s){box.textContent+=s+'\n';box.scrollTop=box.scrollHeight}async function health(){line(await(await fetch('/health')).text())}function start(){const id=++count;const es=new EventSource('/sse');es.onmessage=e=>line('连接 '+id+' | '+e.data);es.addEventListener('done',()=>{line('连接 '+id+' 正常结束');es.close()});es.onerror=()=>{line('连接 '+id+' 发生错误,停止自动重连以便检查');es.close()}}health();</script></html>`
使用方式
窗口A
go build -o server .
新起一个窗口
go build -ldflags '-X main.version=v2' -o server.new .
mv -f server.new server
kill -USR2 "$(cat server.pid)"