go-sse不间断更新

方式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)"
posted on 2026-10-19 17:19  朝阳1  阅读(3)  评论(0)    收藏  举报