什么是优雅重启

在不停机的情况下,就地部署一个应用程序的新版本或者修改其配置的能力已经成为现代软件系统的标配。这篇文章讨论优雅重启一个应用的不同方法,并且提供一个功能独立的案例来深挖实现细节。如果你不熟悉 Teleport 话,Teleport 是我们使用 Golang 针对弹性架构设计的 SHH 和 Kubernetes 特权访问管理解决方案。使用 Go 建立和维护服务的开发者和网站可靠性工程师(SRE)应该对这篇文章有兴趣。

SO_REUSEPORT vs 复制套接字的背景

为了推进 Teleport 高可用的工作,我们最近花了些时间研究如何优雅重启 Teleport 的 TLS 和 SSH 的端口监听器(GitHub issue #1679)。我们的目标是能够更新一个 Teleport 二进制文件而不需要让实例停止服务。

Marek Majkowski 在他的博客文章《为什么一个 NGINX 工作线程会承担所有负载?》 讨论了两种普遍的方法。这些方法可以被如下概括:

  • 你可以在套接字上设置 SO_REUSEPORT ,从而让多个进程能够被绑定到同一个端口上。利用这个方法,你会有多个接受队列向多个进程提供数据。
  • 复制套接字,并把它以文件的形式传送给一个子进程,然后在新的进程中重新创建这个套接字。使用这种方法,你将有一个接受队列向多个进程提供数据。]

在我们初期的讨论中,我们了解到几个关于 SO_REUSEPORT 的问题。我们的一个工程师之前使用这个方法,并且注意到由于其多个接受队列,有时候会丢弃挂起的 TCP 连接。除此之外,当我们进行这些讨论的时候,Go 并没有很好地支持在一个 net.Listener 上设置 SO_REUSEPORT。然而,在过去的几天中,在这个问题上有了进展,看起来像 Go 不久就会支持设置套接字属性

第二种方法也很吸引人,因为它的简单性以及大多数开发人员熟悉的传统Unix 的 fork/exec 产生模型,即将所有打开文件传递给子进程的约定。需要注意的一点,os/exec 包实际上不赞同这种用法。主要是出于安全上的考量,它只传递 stdin , stdoutstderr 给子进程。然而, os 包确实提供较低级的原语,可用于将文件传递给子程序,这就是我们想做的。

使用信号切换套接字进程所有者

在我们看源码之前,了解一些这个方法如何工作的细节是值得的。

启动一个全新的 Teleport 程序后,该进程会在绑定的端口上创建一个监听套接字接受所有入站流量。对于 Teleport,入口流量就是 LTS 和 SSH 流量。我们添加了一个处理 SIGUSR2 信号的句柄,该句柄让 Teleport 复制监听套接字,然后生成一个新的进程,同时将监听套接字以文件的形式和这个套接字的元数据以环境变量的形式传入给该进程。一旦新的进程开始,他会依据传进来的文件和元数据重建这个套接字,并且处理它所获得的流量。

应该注意的是,当一个套接字被复制时,入栈流量会在两个套接字之间以轮询的方式进行负载均衡。如下图所示,这就意味着有一段时间,两个 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 中看到这一点,我们邀请你瞧瞧我们的参考 AWS SSH 和 Kubernetes 堡垒机部署,里面包含了一个 ansible 脚本,该脚本利用就地优雅重启实现无停机更新 Teleport 二进制文件。

Golang 优雅重启案例源代码

package mainimport ("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 listenererr := 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.filelistenerFile := 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,3files := []*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 := <-signalCh: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 stringflag.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: %vn", err)return}fmt.Printf("Exiting.n")
}

如果你读到了这里

Teleport 是一个开源软件,你可以免费地在 GitHub 上深入了解它。如果你对 Teleport 或是其他类似的分布式系统软件的工作有兴趣,我们时刻期待着优秀的软件工程师


plsql developer无监听程序_无停机优雅重启 Go 程序相关推荐

  1. exec go 重启_无停机优雅重启 Go 程序

    什么是优雅重启 在不停机的情况下,就地部署一个应用程序的新版本或者修改其配置的能力已经成为现代软件系统的标配.这篇文章讨论优雅重启一个应用的不同方法,并且提供一个功能独立的案例来深挖实现细节.如果你不 ...

  2. plsql连接本地oracle数据库,而远程主机却无法连接,出现无监听程序的解决方法(转)

    plsql连接本地oracle数据库,而远程主机却无法连接,出现无监听程序的解决方法(转) 参考文章: (1)plsql连接本地oracle数据库,而远程主机却无法连接,出现无监听程序的解决方法(转) ...

  3. PLSql连接Oracle时提示TNS:无监听程序的解决方法

    用plsql连接oracle时,总会遇到各种各样的问题,总结下当遇到TNS:无监听程序问题提示时,如何去解决 1.检查客户端配置的Ip是否有错 根据oracle client安装路径 找到 tnsna ...

  4. oracle12541 linux,PLSQL连接Linux上的oracle数据库出现,ORA-12541 TNS 无监听程序

    PLSQL连接Linux上的oracle数据库出现,ORA-12541 TNS 无监听程序 外部的PLSQL无法连接Linux上的oracle数据库,出现ORA-12541 TNS 无监听程序错误.待 ...

  5. 使用PLSql连接Oracle时报错ORA-12541: TNS: 无监听程序

    很多时候为了优化我们的启动项把oracle的服务禁止了,可是重启启动之后使用PLSQL登陆oracle时会出现无监听程序,这说明我们有一些服务没有启动.我们先查看一下oracle的服务是否启动,查看方 ...

  6. (Oracle踩坑指南)项目建立连接的各种坑 ORA-12541: TNS: 无监听程序等等

    在oracle项目建立连接中的各种坑 吐槽甲骨文一波 安装过程中遇到的问题 问题一:下载 问题二:PLSQL工具 问题三:plsql的使用 问题四:接下来到修改VisualStudio里面的web.c ...

  7. 解决因改变oracle数据库的IP地址引起的“ORA-12541:TNS:无监听程序”

    解决因改变oracle数据库的IP地址引起的"ORA-12541:TNS:无监听程序" 转载https://blog.csdn.net/amanda04/article/detai ...

  8. oracle数据库连接时报12514_连接Oracle数据库时报ORA-12541:TNS:无监听程序的图文解决教程...

    在用PL/SQL Developer等客户端工具连接oracle服务器时出现ORA-12541:TNS:无监听程序的错误,如下图: 发现原来是oracle的监听没有启动,重启监听后就连接成功了,下面跟 ...

  9. 连接linux数据库Oracle时报错ORA-12541: TNS: 无监听程序重启后提示出现ORA-01034和ORA-27101

    1 windows上安装Oracle的客户端. 2 确保linux服务器上的1521端口开放 3 看你的window机器是够能够ping同linux服务器的ip地址 4 在windows中的Oracl ...

最新文章

  1. CentOS7——vi编辑保存
  2. thinkphp mysql 预处理_thinkPHP框架中执行原生SQL语句的方法
  3. C++用库 jsoncpp 解析 JSON
  4. windows7 php的php-ssh2,windows7下安装php的php-ssh2扩展教程_PHP教程
  5. 神秘的数组初始化_I / O神秘化
  6. Conditional project or library reference in Visual Studio
  7. 吴恩达《机器学习》--- Logistic分类
  8. jmeter学习总结
  9. struts2 tutor
  10. 苹果macOS 13 Ventura 5K原生动态壁纸
  11. html中版权怎么写,HTML版权符号是怎么输入的、代表什么-京标知识产权
  12. 计算机研究生哪个子专业最容易考公务员
  13. MaNGOS开源魔兽世界服务端
  14. 超声扩散波复合成像仿真(ultrasound diverg_wave compound imaging simulation)
  15. photoshop批处理改变图片大小
  16. 基于matlab的网络通信RSRP切换仿真
  17. kali wifi不可用_Kali Linux系统解决无线网卡无法连接WIFI模块教程:
  18. 微信公众号申请到开发环境搭建
  19. 怎么确保数据在网络传输的安全性?
  20. 2021-07-24项目实训后端第四周(3)

热门文章

  1. Android studio中git密码记住的问题
  2. Android通过使用系统广播监听网络状态的改变
  3. java 类似结构体_Java中如何实现类似C++结构体的二级排序
  4. android studio 65536错误的解决
  5. 教你十分钟快速搭建springBoot项目实战
  6. jquery.timers使用说明
  7. CentOS6.5系统重启后宕机
  8. 嵌入式Linux系统运行流程图
  9. Android Resource介绍和使用
  10. svn 合并和树冲突