exec go 重启_无停机优雅重启 Go 程序
什么是优雅重启
在不停机的情况下,就地部署一个应用程序的新版本或者修改其配置的能力已经成为现代软件系统的标配。这篇文章讨论优雅重启一个应用的不同方法,并且提供一个功能独立的案例来深挖实现细节。如果你不熟悉 Teleport 话,Teleport 是我们使用 Golang 针对弹性架构设计的
SO_REUSEPORT vs 复制套接字的背景
为了推进 Teleport 高可用的工作,我们最近花了些时间研究如何优雅重启 Teleport 的 TLS 和 SSH 的端口监听器
Marek Majkowski 在他的博客文章你可以在套接字上设置 SO_REUSEPORT ,从而让多个进程能够被绑定到同一个端口上。利用这个方法,你会有多个接受队列向多个进程提供数据。
复制套接字,并把它以文件的形式传送给一个子进程,然后在新的进程中重新创建这个套接字。使用这种方法,你将有一个接受队列向多个进程提供数据。]
在我们初期的讨论中,我们了解到几个关于 SO_REUSEPORT 的问题。我们的一个工程师之前使用这个方法,并且注意到由于其多个接受队列,有时候会丢弃挂起的 TCP 连接。除此之外,当我们进行这些讨论的时候,Go 并没有很好地支持在一个 net.Listener 上设置 SO_REUSEPORT。然而,在过去的几天中,在这个问题上有了进展,看起来像
第二种方法也很吸引人,因为它的简单性以及大多数开发人员熟悉的传统Unix 的 fork/exec 产生模型,即将所有打开文件传递给子进程的约定。需要注意的一点,os/exec 包实际上不赞同这种用法。主要是出于安全上的考量,它只传递 stdin , stdout 和 stderr 给子进程。然而, os 包确实提供较低级的原语,可用于将文件传递给子程序,这就是我们想做的。
使用信号切换套接字进程所有者
在我们看源码之前,了解一些这个方法如何工作的细节是值得的。
启动一个全新的 Teleport 程序后,该进程会在绑定的端口上创建一个监听套接字接受所有入站流量。对于 Teleport,入口流量就是 LTS 和 SSH 流量。我们添加了一个处理
应该注意的是,当一个套接字被复制时,入栈流量会在两个套接字之间以轮询的方式进行负载均衡。如下图所示,这就意味着有一段时间,两个 Teleport 进程都会接受新的连接。
父进程的关闭是相同的事情,但是反过来做。一旦 Teleport 进程接受到 SIGOUIT 信号,他会开始关闭这个进程,停止接受新的连接,等待所有的现有连接断开或是超时发生。一旦入站流量被清空,这个濒死进程就会关闭它的监听套接字并且退出。这种情况下,新的进程会接管内核发送过来的所有请求。
优雅重启演练
我们基于上面的方法写了一个简单的程序,你可以自己尝试使用一下。源代码在文章的最后,你可以按照以下步骤尝试这个例子。
首先,编译和启动程序。
$ go build restart.go
$ ./restart &
[1] 95147
$ Created listener file descriptor for :8080.
$ curl http://localhost:8080/hello
Hello from 95147!
将 USR2 信号发送给初始进程。现在,当你访问这个 HTTP 入口的时候,他会返回两个不同的进程的 PID。
$ kill -SIGUSR2 95147
user defined signal 2 signal received.
Forked child 95170.
$ Imported listener file descriptor for :8080.
$ curl http://localhost:8080/hello
Hello from 95170!
$ curl http://localhost:8080/hello
Hello from 95147!
杀死初始进程后,你将只会从新的进程中获得返回。
$ kill -SIGTERM 95147
signal: killed
[1]+ Exit 1 go run restart.go
$ curl http://localhost:8080/hello
Hello from 95170!
$ curl http://localhost:8080/hello
Hello from 95170!
最后杀死新进程,访问将会被拒绝。
$ kill -SIGTERM 95170
$ curl http://localhost:8080/hello
curl: (7) Failed to connect to localhost port 8080: Connection refused
总结和示例源代码
像你看到,一旦你了解了他是如何工作的,增加优雅重启功能到 Go 写的服务中是相当简单的事情,并且有效地提高服务使用者的用户体验。如果你想在 Teleport 中看到这一点,我们邀请你瞧瞧我们的参考
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
)
type listener struct {
Addr string `json:"addr"`
FD int `json:"fd"`
Filename string `json:"filename"`
}
func importListener(addr string) (net.Listener, error) {
// 从环境变量中抽离出被编码的 listener 的元数据。
listenerEnv := os.Getenv("LISTENER")
if listenerEnv == "" {
return nil, fmt.Errorf("unable to find LISTENER environment variable")
}
// 解码 listener 的元数据。
var l listener
err := json.Unmarshal([]byte(listenerEnv), &l)
if err != nil {
return nil, err
}
if l.Addr != addr {
return nil, fmt.Errorf("unable to find listener for %v", addr)
}
// 文件已经被传入到这个进程中,从元数据中抽离文件描述符和名字,为 listener 重建/发现 *os.file
listenerFile := os.NewFile(uintptr(l.FD), l.Filename)
if listenerFile == nil {
return nil, fmt.Errorf("unable to create listener file: %v", err)
}
defer listenerFile.Close()
// Create a net.Listener from the *os.File.
ln, err := net.FileListener(listenerFile)
if err != nil {
return nil, err
}
return ln, nil
}
func createListener(addr string) (net.Listener, error) {
ln, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
return ln, nil
}
func createOrImportListener(addr string) (net.Listener, error) {
// 尝试为地址导入一个 listener, 如果导入成功,则使用。
ln, err := importListener(addr)
if err == nil {
fmt.Printf("Imported listener file descriptor for %v.\n", addr)
return ln, nil
}
// 没有 listener 被导入,这就意味着进程必须自己创建一个。
ln, err = createListener(addr)
if err != nil {
return nil, err
}
fmt.Printf("Created listener file descriptor for %v.\n", addr)
return ln, nil
}
func getListenerFile(ln net.Listener) (*os.File, error) {
switch t := ln.(type) {
case *net.TCPListener:
return t.File()
case *net.UnixListener:
return t.File()
}
return nil, fmt.Errorf("unsupported listener: %T", ln)
}
func forkChild(addr string, ln net.Listener) (*os.Process, error) {
// 从 listener 中获取文件描述符,在环境变量编码在传递给这个子进程的元数据。
lnFile, err := getListenerFile(ln)
if err != nil {
return nil, err
}
defer lnFile.Close()
l := listener{
Addr: addr,
FD: 3,
Filename: lnFile.Name(),
}
listenerEnv, err := json.Marshal(l)
if err != nil {
return nil, err
}
// 将 stdin, stdout, stderr 和 listener 传入子进程。
// 译注: 以上四个文件描述符分别为 0,1,2,3
files := []*os.File{
os.Stdin,
os.Stdout,
os.Stderr,
lnFile,
}
// 获取当前环境变量,并且传入子进程。
environment := append(os.Environ(), "LISTENER="+string(listenerEnv))
// 获取当前进程名和工作目录
execName, err := os.Executable()
if err != nil {
return nil, err
}
execDir := filepath.Dir(execName)
// 生成子进程
p, err := os.StartProcess(execName, []string{execName}, &os.ProcAttr{
Dir: execDir,
Env: environment,
Files: files,
Sys: &syscall.SysProcAttr{},
})
if err != nil {
return nil, err
}
return p, nil
}
func waitForSignals(addr string, ln net.Listener, server *http.Server) error {
signalCh := make(chan os.Signal, 1024)
signal.Notify(signalCh, syscall.SIGHUP, syscall.SIGUSR2, syscall.SIGINT, syscall.SIGQUIT)
for {
select {
case s :=
fmt.Printf("%v signal received.\n", s)
switch s {
case syscall.SIGHUP:
// Fork 一个子进程。
p, err := forkChild(addr, ln)
if err != nil {
fmt.Printf("Unable to fork child: %v.\n", err)
continue
}
fmt.Printf("Forked child %v.\n", p.Pid)
// 创建一个在 5 秒钟过去的 Context, 使用这个超时定时器关闭。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 返回关闭过程中发生的任何错误。
return server.Shutdown(ctx)
case syscall.SIGUSR2:
// Fork 一个子进程。
p, err := forkChild(addr, ln)
if err != nil {
fmt.Printf("Unable to fork child: %v.\n", err)
continue
}
// 输出被 fork 的子进程的 PID,并等待更多的信号。
fmt.Printf("Forked child %v.\n", p.Pid)
case syscall.SIGINT, syscall.SIGQUIT:
// 创建一个在 5 秒钟过去的 Context, 使用这个超时定时器关闭。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 返回关闭过程中发生的任何错误。
return server.Shutdown(ctx)
}
}
}
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from %v!\n", os.Getpid())
}
func startServer(addr string, ln net.Listener) *http.Server {
http.HandleFunc("/hello", handler)
httpServer := &http.Server{
Addr: addr,
}
go httpServer.Serve(ln)
return httpServer
}
func main() {
// Parse command line flags for the address to listen on.
var addr string
flag.StringVar(&addr, "addr", ":8080", "Address to listen on.")
// Create (or import) a net.Listener and start a goroutine that runs
// a HTTP server on that net.Listener.
ln, err := createOrImportListener(addr)
if err != nil {
fmt.Printf("Unable to create or import a listener: %v.\n", err)
os.Exit(1)
}
server := startServer(addr, ln)
// 等待复制或结束的信号
err = waitForSignals(addr, ln, server)
if err != nil {
fmt.Printf("Exiting: %v\n", err)
return
}
fmt.Printf("Exiting.\n")
}
如果你读到了这里
Teleport 是一个开源软件,你可以免费地在
本文由
欢迎关注站长公众号:polarisxu,有更多惊喜等着你!
exec go 重启_无停机优雅重启 Go 程序相关推荐
- plsql developer无监听程序_无停机优雅重启 Go 程序
什么是优雅重启 在不停机的情况下,就地部署一个应用程序的新版本或者修改其配置的能力已经成为现代软件系统的标配.这篇文章讨论优雅重启一个应用的不同方法,并且提供一个功能独立的案例来深挖实现细节.如果你不 ...
- linux 优雅重启进程,apache2 重启、停止、优雅重启、优雅停止
停止或者重新启动apache有两种发送信号的方法 第一种方法: 直接使用linux的kill命令向运行中的进程发送信号.你也许你会注意到你的系统里运行着很多httpd进程.但你不应该直接对它们中的任何 ...
- 如何重启_消费市场按下重启键,企业该如何提前布局
2020广发卡携手企业和消费者,共同按下重启键,让我们放下包袱,轻松前行. 当疫情结束后,你想做什么?也许是去见想见的人,和他一起去吃想吃的美食:也许是约上三五好友,或带着最亲的家人,出发去想去的地方 ...
- 黑苹果修复重启_修复了随机重启问题
黑苹果修复重启 The computer that had rebooted itself on Friday was fine all day and then the second I turne ...
- mysql 设置中文 重启_如何启动/停止/重启MySQL + 进入MYSQL-Go语言中文社区
Ubuntu 如何启动/停止/重启MySQL 一. 启动方式 1.使用 service 启动:service mysql start 2.使用 mysqld 脚本启动:/etc/inint.d/mys ...
- win10关机后自动重启_电脑自动关机或重启的解决办法
经常有人碰到电脑自动关机或者重启的现象来问我,怎么回事?那么今天就讲一下这种情况的解决办法.台式机的判断和解决办法如下,下次再说笔记本电脑. 先说自动关机: 电脑在正常使用中突然断电关机,这有几种原因 ...
- 黑苹果进不了系统自动重启_苹果手机频繁自动重启
如果是更新到某一个系统版本之后出现频繁重启,可能是由于系统Bug,可以尝试打开[设置]-[勿扰模式],再打开[通用]-[日期与时间],将其中的[自动设置]关闭,手动调节日期时间之后再重启.也可以卸载新 ...
- win10关机后自动重启_电脑自动关机或重启的解决办法(笔记本)
上次说了台式电脑自动关机和重启的解决办法,今天继续说说笔记本电脑自动关机或重启的解决办法: 其实笔记本电脑和台式机差不多也是那几个原因: 1.CPU散热不好,温度过高导致主板自我保护而断电,也是清理C ...
- mongodb3 重启_生化危机电影将重启,以游戏前两代剧情改编
去其糟粕 取其乐子 外媒Deadline报道,<生化危机>电影版权制作方Constantin Film筹备将该系列重启,由Johannes Roberts担任新电影编剧 ...
最新文章
- THE REAL DRAGON WARRIORS
- pytorch autograd整理
- Android - Animation(二)
- java 着色问题 回溯算法,C语言使用回溯法解旅行售货员问题与图的m着色问题
- JAVA中修改顺序表中的元素_java – 在列表中查找元素并使用stream()更改它
- matlab用lism求零输入响应,信号与系统实验报告
- 使用keras理解LSTM
- 【Machine Learning 一】监督学习与无监督学习
- 在Ubuntu和Linux 中安装虚拟机以及安装Windows 10
- free git online
- 十大办法帮助传统产业数字化转型
- 人脸生成:Beyond Face Rotation: Global and Local Perception GAN
- ubuntu16下安装mongodb 3.6
- raw socket
- 最新一代CAD技术方案------Onshape
- 服务器共享文件夹给广域网,广域网文件共享服务器
- office2003安装包下载,专业版完整版官方原版!
- Arduino和SX1278的那些事
- C++ Primer读书笔记(从后向前看)
- Flutter插件开发--获取Android手机电池信息
热门文章
- 华为:跨过时艰,向未来
- Kafka精华问答 | kafka节点之间如何备份?
- 每年一波FPGA系列新品,这次Achronix专为AI/ML应用打造……
- 面趣 | 据说这道烧脑的微软面试题很奇葩,你来试试?
- python商品总价_【Python基础 | 列表】小实验:实现显示商品,选择商品,将商品加入购物车,得到总价格...
- 并联串联混合的电压和电流_高考物理常考实验之电流表改装电压表怎么串联电阻...
- deb 中标麒麟_「图」百度网盘Linux版放出deb包客户端:新增支持Ubuntu 18.04 LTS
- 移动端小程序 腾讯地图sdk 当前位置 地址你解析 距离计算
- RuoYi-Cloud 部署篇_01(windows环境 Oracle+nginx版本)
- mysql不区分大小写设置_mysql设置不区分大小写