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,有更多惊喜等着你!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/528135.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ai怎么让图片任意变形_想一键提取图片文字,有什么好的文字识别软件/APP推荐吗?...

工作也有一段时间了,有时候需要把图片中的文字提取出来,转换成电子档式,一开始的时候可能是傻吧,也没想到借用工作减少工作量,就知道埋头拼命敲敲打打,文字比较少的时候还好,多了简直...得颈椎病…

MySql和Oracle数据库的区别?

查询当前所有的表 SQL> select * from tab; SQL> select * from cat where table_type’TABLE’;//可能会有view SQL>select * from user_tables; mysql> show tables; c:/mysql/bin>mysqlshow 库名 显示当前连接用户(库) SQL> sh…

隐藏功能_IOS 14.2 隐藏功能

FaceTime隐藏功能现身,只要设备运行 iOS 14.2 系统,iPhone 8 或更新的机型都可以以 1080p 分辨率进行视频通话,1080p什么概念,平时看视频应该最有感触。FaceTime 是什么?可能有些小伙伴压根儿就没听说过这个功能。Face…

SSH框架(Struts+Spring+Hibernate)

在SSH 的组合框架模式中,三者各自的作用? Struts 是一个很好的MVC框架,主要技术是Servlet和Jsp。Struts的MVC设计模式可以使我们的逻辑变得很清晰,让我们写的程序层次分明。基于Struts开发可以简化开发难度,提高开发效…

mapbox矢量切片标准_Cesium 加载矢量切片(MapBox Vector Tile)

矢量切片(vector tile)是当前 WebGIS 较热技术,国内的高德、百度等在线地图都使用了矢量切片技术。相较于传统栅格切片,矢量切片好处很多。简单几点就是:轻量、客户端渲染、还可加密(栅格切片容易被爬取)。矢量切片格式一般有 GeoJSON、TopoJ…

d630无电池升级bios_太重要,你想要的电脑BIOS全面解读与设置(下)

设置意外断电后恢复状态通常在电脑意外断电后,需要重新启动电脑,但在 BIOS 中可以对断电恢 复进行设置,一旦电源恢复,电脑将自动启动。下面就在 UEFIBIOS 中设置电 脑的自动断电后重启,具体操作步骤如下。 STEP 1 选择…

MySQL的乱码解决方案

1.如果第一次安装MySQL数据库: 在选择编码时,选择最后一项,并指定编码为utf8或者gbk(这两种编码支持中文,是我们比较常用的) 如果已经按照完成了MySQL数据库,那么可以进行重新配置修改: 在开始菜单中找到: 然后重复第…

pg数据库开启远程连接_疫情之下,开启在家办公模式,远程连接工具篇之向日葵...

1月30号本来就要返程去上班了,接到公司通知,根据当前疫情的形势,假期延长到3号,退车票,候补抢票一顿操作,将票改到了3号,3号又接到通知假期延长到10号。作为一个一线的销售人员,工作…

谈谈对MVC的理解(View+Model+Controller)

1) 什么是MVC? MVC是一种设计思想,根据职责不同将程序中的组件分成以下3个部分。 V(View视图):负责与用户交互。将数据展现,或者是接收数据 M(Model模型):负责业务处理。…

吃下去的东西老是往上翻上来_食管裂孔疝的这个诱因你知道吗?医生:不要再吃过烫的食物...

食管裂孔疝的忌吃辣门诊遇见一个爱吃热、辣食物的患者。她自述饭后老是反酸水呕吐,本来以为只是普通胃病,最近到我们医院一查,结果让她大吃一惊,本在腹腔的胃竟然有部分“跑”进了胸腔!两年多前,这个女士发…

物理搬砖问题_全职业通用,搬砖市场装备

更多原创文章可关注微信公众号:地下城勤帝 查看大家好,我是勤帝,我只写大家能看懂的文章搬砖市场的角色更新了一些,装备都有所不同,今天我给大家一个可以通用的装备,这套装备虽然成型不快,但是…

Oracle中表连接的方式有哪些

内连接(自然连接) 等值连接、自然连接和不等值连接 {内连接是join 关键字连接两个表,语法为 table1 join table2 on 条件。 根据使用的比较方式不同,内连接又分为等值连接、自然连接和不等值连接。 等值连接:所谓等值连接&…

快手通过标签添加的我_快手怎么上热门?快手短视频推荐指标有哪些?

“快手怎么才能上热门?快手上热门都有哪些技巧?”其实这些问题,除了通过提高短视频质量,还可以通过分析推荐指数的方式解决。运营者想要有效提升短视频的播放量,想让快手短视频快速上热门,还可以从推荐指数…

JAVA的数据类型有哪些?

⑴基本数据类型(八种): 整型: byte 1字节, short 2字节, int 4字节, long 8字节 浮点型: float 4字节, double 8字节 【float 类型共 32位(不 int相同),其中 1位为符号位…

python机器学习算法.mobi_机器学习之ID3算法详解及python代码实现

在生活中我们经常会用到决策树算法,最简单的就是二叉树了;相信大家也会又同样的困扰,手机经常收到各种短信,其中不乏很多垃圾短信、此时只要设置这类短信为垃圾短信手机就会自动进行屏蔽、减少被骚扰的次数,同时正常短…

java中的4种访问制权限有哪些?分别作用范围是什么?

(1).public:最大访问控制权限,对所有的类都可见。 (2).protect:修饰的,在类内部、同一个包、子类中能访问 (3).default:包访问权限,即同一个包中的类可以可见。默认不显式指定访问控制权限时就是default包…

打docker镜像_从安全到镜像流水线,Docker 最佳实践与反模式一览

作者 | Timothy Mugayi译者 | 弯月,责编 | 夕颜封图 | CSDN付费下载自视觉中国出品 | CSDN(ID:CSDNnews)在使用Docker的大部分时间里,我们并不关心其内部的工作原理。仅凭启动一个Docker容器并且让应用程序运行良好,并不能说明你已经实现了一…

JAVA重载和重写的区别?

重载:方法名相同,参数不同(参数类型或者长度) 重载和修饰符和返回类型无关。 一是方法的参数列表必须改变,包括参数的类型,参数的个数多少,参数顺序。 二是重载对返回类型,访问修饰…

详细关闭iiop方法_疏通暖气片堵塞的方法,看完你就知道了!

冬季几乎家家户户都会安装暖气片来进行采暖,但在使用过程中,通常会出现暖气片被堵,用户不知道被堵的原因又不知从哪下手,今天金旗舰旗哥带大家来了解一下暖气片被堵塞的原因及疏通方法。一、堵塞暖气片的原因:1、暖气片…

谈谈对集合框架的理解?

集合框架包括集合不映射(Collection and Map) List 元素有先后次序的集合, 元素有 index 位置, 元素可以重复,继承自 Collection 接口,实现类: ArrayList, Vector, LinkedList List 表示有先后次序的对象集合 ArrayList是使用变长数组算法…