Golang支持平滑升级的HTTP服务

2024-09-08 13:48

本文主要是介绍Golang支持平滑升级的HTTP服务,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前段时间用Golang在做一个HTTP的接口,因编译型语言的特性,修改了代码需要重新编译可执行文件,关闭正在运行的老程序,并启动新程序。对于访问量较大的面向用户的产品,关闭、重启的过程中势必会出现无法访问的情况,从而影响用户体验。

使用Golang的系统包开发HTTP服务,是无法支持平滑升级(优雅重启)的,本文将探讨如何解决该问题。

一、平滑升级(优雅重启)的一般思路

一般情况下,要实现平滑升级,需要以下几个步骤:

  1. 用新的可执行文件替换老的可执行文件(如只需优雅重启,可以跳过这一步)
  2. 通过pid给正在运行的老进程发送 特定的信号(kill -SIGUSR2 $pid)
  3. 正在运行的老进程,接收到指定的信号后,以子进程的方式启动新的可执行文件并开始处理新请求
  4. 老进程不再接受新的请求,等待未完成的服务处理完毕,然后正常结束
  5. 新进程在父进程退出后,会被init进程领养,并继续提供服务

二、Golang Socket 网络编程

Socket是程序员层面上对传输层协议TCP/IP的封装和应用。Golang中Socket相关的函数与结构体定义在net包中,我们从一个简单的例子来学习一下Golang Socket 网络编程,关键说明直接写在注释中。

1、服务端程序 server.go

package mainimport ("fmt""log""net""time"
)func main() {// 监听8086端口listener, err := net.Listen("tcp", ":8086")if err != nil {log.Fatal(err)}defer listener.Close()for {// 循环接收客户端的连接,没有连接时会阻塞,出错则跳出循环conn, err := listener.Accept()if err != nil {fmt.Println(err)break}fmt.Println("[server] accept new connection.")// 启动一个goroutine 处理连接go handler(conn)}
}func handler(conn net.Conn) {defer conn.Close()for {// 循环从连接中 读取请求内容,没有请求时会阻塞,出错则跳出循环request := make([]byte, 128)readLength, err := conn.Read(request)if err != nil {fmt.Println(err)break}if readLength == 0 {fmt.Println(err)break}// 控制台输出读取到的请求内容,并在请求内容前加上hello和时间后向客户端输出fmt.Println("[server] request from ", string(request))conn.Write([]byte("hello " + string(request) + ", time: " + time.Now().Format("2006-01-02 15:04:05")))}
}

2、客户端程序 client.go

package mainimport ("fmt""log""net""os""time"
)func main() {// 从命令行中读取第二个参数作为名字,如果不存在第二个参数则报错退出if len(os.Args) != 2 {fmt.Fprintf(os.Stderr, "Usage: %s name ", os.Args[0])os.Exit(1)}name := os.Args[1]// 连接到服务端的8086端口conn, err := net.Dial("tcp", "127.0.0.1:8086")checkError(err)for {// 循环往连接中 写入名字_, err = conn.Write([]byte(name))checkError(err)// 循环从连接中 读取响应内容,没有响应时会阻塞response := make([]byte, 256)readLength, err := conn.Read(response)checkError(err)// 将读取响应内容输出到控制台,并sleep一秒if readLength > 0 {fmt.Println("[client] server response:", string(response))time.Sleep(1 * time.Second)}}
}func checkError(err error) {if err != nil {log.Fatal("fatal error: " + err.Error())}
}

3、运行示例程序

# 运行服务端程序
go run server.go# 在另一个命令行窗口运行客户端程序
go run client.go "tabalt"

三、Golang HTTP 编程

HTTP是基于传输层协议TCP/IP的应用层协议。Golang中HTTP相关的实现在net/http包中,直接用到了net包中Socket相关的函数和结构体。

我们再从一个简单的例子来学习一下Golang HTTP 编程,关键说明直接写在注释中。

1、http服务程序 http.go

package mainimport ("log""net/http""os"
)// 定义http请求的处理方法
func handlerHello(w http.ResponseWriter, r *http.Request) {w.Write([]byte("http hello on golang\n"))
}func main() {// 注册http请求的处理方法http.HandleFunc("/hello", handlerHello)// 在8086端口启动http服务,会一直阻塞执行err := http.ListenAndServe("localhost:8086", nil)if err != nil {log.Println(err)}// http服务因故停止后 才会输出如下内容log.Println("Server on 8086 stopped")os.Exit(0)
}

2、运行示例程序

# 运行HTTP服务程序
go run http.go# 在另一个命令行窗口curl请求测试页面
curl http://localhost:8086/hello/# 输出如下内容:
http hello on golang

四、Golang net/http包中 Socket操作的实现

从上面的简单示例中,我们看到在Golang中要启动一个http服务,只需要简单的三步:

  1. 定义http请求的处理方法
  2. 注册http请求的处理方法
  3. 在某个端口启动HTTP服务

而最关键的启动http服务,是调用http.ListenAndServe()函数实现的。下面我们找到该函数的实现:

func ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}return server.ListenAndServe()
}

这里创建了一个Server的对象,并调用它的ListenAndServe()方法,我们再找到结构体Server的ListenAndServe()方法的实现:

func (srv *Server) ListenAndServe() error {addr := srv.Addrif addr == "" {addr = ":http"}ln, err := net.Listen("tcp", addr)if err != nil {return err}return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

从代码上看到,这里监听了tcp端口,并将监听者包装成了一个结构体 tcpKeepAliveListener,再调用srv.Serve()方法;我们继续跟踪Serve()方法的实现:

func (srv *Server) Serve(l net.Listener) error {defer l.Close()var tempDelay time.Duration // how long to sleep on accept failurefor {rw, e := l.Accept()if e != nil {if ne, ok := e.(net.Error); ok && ne.Temporary() {if tempDelay == 0 {tempDelay = 5 * time.Millisecond} else {tempDelay *= 2}if max := 1 * time.Second; tempDelay > max {tempDelay = max}srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)time.Sleep(tempDelay)continue}return e}tempDelay = 0c, err := srv.newConn(rw)if err != nil {continue}c.setState(c.rwc, StateNew) // before Serve can returngo c.serve()}
}

可以看到,和我们前面Socket编程的示例代码一样,循环从监听的端口上Accept连接,如果返回了一个net.Error并且这个错误是临时性的,则会sleep一个时间再继续。 如果返回了其他错误则会终止循环。成功Accept到一个连接后,调用了方法srv.newConn()对连接做了一层包装,最后启了一个goroutine处理http请求。

五、Golang 平滑升级(优雅重启)HTTP服务的实现

我创建了一个新的包gracehttp来实现支持平滑升级(优雅重启)的HTTP服务,为了少写代码和降低使用成本,新的包尽可能多地利用net/http包的实现,并和net/http包保持一致的对外方法。现在开始我们来看gracehttp包支持平滑升级 (优雅重启)Golang HTTP服务涉及到的细节如何实现。

1、Golang处理信号

Golang的os/signal包封装了对信号的处理。简单用法请看示例:

package mainimport ("fmt""os""os/signal""syscall"
)func main() {signalChan := make(chan os.Signal)// 监听指定信号signal.Notify(signalChan,syscall.SIGHUP,syscall.SIGUSR2,)// 输出当前进程的pidfmt.Println("pid is: ", os.Getpid())// 处理信号for {sig := <-signalChanfmt.Println("get signal: ", sig)}
}

2、子进程启动新程序,监听相同的端口

在第四部分的ListenAndServe()方法的实现代码中可以看到,net/http包中使用net.Listen函数来监听了某个端口,但如果某个运行中的程序已经监听某个端口,其他程序是无法再去监听这个端口的。解决的办法是使用子进程的方式启动,并将监听端口的文件描述符传递给子进程,子进程里从这个文件描述符实现对端口的监听。

具体实现需要借助一个环境变量来区分进程是正常启动,还是以子进程方式启动的,相关代码摘抄如下:

// 启动子进程执行新程序
func (this *Server) startNewProcess() error {listenerFd, err := this.listener.(*Listener).GetFd()if err != nil {return fmt.Errorf("failed to get socket file descriptor: %v", err)}path := os.Args[0]// 设置标识优雅重启的环境变量environList := []string{}for _, value := range os.Environ() {if value != GRACEFUL_ENVIRON_STRING {environList = append(environList, value)}}environList = append(environList, GRACEFUL_ENVIRON_STRING)execSpec := &syscall.ProcAttr{Env:   environList,Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), listenerFd},}fork, err := syscall.ForkExec(path, os.Args, execSpec)if err != nil {return fmt.Errorf("failed to forkexec: %v", err)}this.logf("start new process success, pid %d.", fork)return nil
}func (this *Server) getNetTCPListener(addr string) (*net.TCPListener, error) {var ln net.Listenervar err errorif this.isGraceful {file := os.NewFile(3, "")ln, err = net.FileListener(file)if err != nil {err = fmt.Errorf("net.FileListener error: %v", err)return nil, err}} else {ln, err = net.Listen("tcp", addr)if err != nil {err = fmt.Errorf("net.Listen error: %v", err)return nil, err}}return ln.(*net.TCPListener), nil
}

3、父进程等待已有连接中未完成的请求处理完毕

这一块是最复杂的;首先我们需要一个计数器,在成功Accept一个连接时,计数器加1,在连接关闭时计数减1,计数器为0时则父进程可以正常退出了。Golang的sync的包里的WaitGroup可以很好地实现这个功能。

然后要控制连接的建立和关闭,我们需要深入到net/http包中Server结构体的Serve()方法。重温第四部分Serve()方法的实现,会发现如果要重新写一个Serve()方法几乎是不可能的,因为这个方法里调用了好多个不可导出的内部方法,重写Serve()方法几乎要重写整个net/http包。

幸运的是,我们还发现在 ListenAndServe()方法里传递了一个listener给Serve()方法,并最终调用了这个listener的Accept()方法,这个方法返回了一个Conn的示例,最终在连接断开的时候会调用Conn的Close()方法,这些结构体和方法都是可导出的!

我们可以定义自己的Listener结构体和Conn结构体,组合net/http包中对应的结构体,并重写Accept()和Close()方法,实现对连接的计数,相关代码摘抄如下:

type Listener struct {*net.TCPListenerwaitGroup *sync.WaitGroup
}func (this *Listener) Accept() (net.Conn, error) {tc, err := this.AcceptTCP()if err != nil {return nil, err}tc.SetKeepAlive(true)tc.SetKeepAlivePeriod(3 * time.Minute)this.waitGroup.Add(1)conn := &Connection{Conn:     tc,listener: this,}return conn, nil
}func (this *Listener) Wait() {this.waitGroup.Wait()
}type Connection struct {net.Connlistener *Listenerclosed bool
}func (this *Connection) Close() error {if !this.closed {this.closed = truethis.listener.waitGroup.Done()}return this.Conn.Close()
}

4、gracehttp包的用法

gracehttp包已经应用到每天几亿PV的项目中,也开源到了github上:github.com/tabalt/gracehttp,使用起来非常简单。

如以下示例代码,引入包后只需修改一个关键字,将http.ListenAndServe 改为 gracehttp.ListenAndServe即可。

package mainimport ("fmt""net/http""github.com/tabalt/gracehttp"
)func main() {http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "hello world")})err := gracehttp.ListenAndServe(":8080", nil)if err != nil {fmt.Println(err)}
}

测试平滑升级(优雅重启)的效果,可以参考下面这个页面的说明:
https://github.com/tabalt/gracehttp#demo

这篇关于Golang支持平滑升级的HTTP服务的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1148278

相关文章

【区块链 + 人才服务】可信教育区块链治理系统 | FISCO BCOS应用案例

伴随着区块链技术的不断完善,其在教育信息化中的应用也在持续发展。利用区块链数据共识、不可篡改的特性, 将与教育相关的数据要素在区块链上进行存证确权,在确保数据可信的前提下,促进教育的公平、透明、开放,为教育教学质量提升赋能,实现教育数据的安全共享、高等教育体系的智慧治理。 可信教育区块链治理系统的顶层治理架构由教育部、高校、企业、学生等多方角色共同参与建设、维护,支撑教育资源共享、教学质量评估、

BUUCTF靶场[web][极客大挑战 2019]Http、[HCTF 2018]admin

目录   [web][极客大挑战 2019]Http 考点:Referer协议、UA协议、X-Forwarded-For协议 [web][HCTF 2018]admin 考点:弱密码字典爆破 四种方法:   [web][极客大挑战 2019]Http 考点:Referer协议、UA协议、X-Forwarded-For协议 访问环境 老规矩,我们先查看源代码

【区块链 + 人才服务】区块链集成开发平台 | FISCO BCOS应用案例

随着区块链技术的快速发展,越来越多的企业开始将其应用于实际业务中。然而,区块链技术的专业性使得其集成开发成为一项挑战。针对此,广东中创智慧科技有限公司基于国产开源联盟链 FISCO BCOS 推出了区块链集成开发平台。该平台基于区块链技术,提供一套全面的区块链开发工具和开发环境,支持开发者快速开发和部署区块链应用。此外,该平台还可以提供一套全面的区块链开发教程和文档,帮助开发者快速上手区块链开发。

【Linux】应用层http协议

一、HTTP协议 1.1 简要介绍一下HTTP        我们在网络的应用层中可以自己定义协议,但是,已经有大佬定义了一些现成的,非常好用的应用层协议,供我们直接使用,HTTP(超文本传输协议)就是其中之一。        在互联网世界中,HTTP(超文本传输协议)是一个至关重要的协议,他定义了客户端(如浏览器)与服务器之间如何进行通信,以交换或者传输超文本(比如HTML文档)。

macOS升级后SVN升级

问题 svn: error: The subversion command line tools are no longer provided by Xcode. 解决 sudo chown -R $(whoami) /usr/local/Cellar brew install svn

如何确定 Go 语言中 HTTP 连接池的最佳参数?

确定 Go 语言中 HTTP 连接池的最佳参数可以通过以下几种方式: 一、分析应用场景和需求 并发请求量: 确定应用程序在特定时间段内可能同时发起的 HTTP 请求数量。如果并发请求量很高,需要设置较大的连接池参数以满足需求。例如,对于一个高并发的 Web 服务,可能同时有数百个请求在处理,此时需要较大的连接池大小。可以通过压力测试工具模拟高并发场景,观察系统在不同并发请求下的性能表现,从而

Anaconda 中遇到CondaHTTPError: HTTP 404 NOT FOUND for url的问题及解决办法

最近在跑一个开源项目遇到了以下问题,查了很多资料都大(抄)同(来)小(抄)异(去)的,解决不了根本问题,费了很大的劲终于得以解决,记录如下: 1、问题及过程: (myenv) D:\Workspace\python\XXXXX>conda install python=3.6.13 Solving environment: done.....Proceed ([y]/n)? yDownloa

基于SpringBoot的宠物服务系统+uniapp小程序+LW参考示例

系列文章目录 1.基于SSM的洗衣房管理系统+原生微信小程序+LW参考示例 2.基于SpringBoot的宠物摄影网站管理系统+LW参考示例 3.基于SpringBoot+Vue的企业人事管理系统+LW参考示例 4.基于SSM的高校实验室管理系统+LW参考示例 5.基于SpringBoot的二手数码回收系统+原生微信小程序+LW参考示例 6.基于SSM的民宿预订管理系统+LW参考示例 7.基于

构建高性能WEB之HTTP首部优化

0x00 前言 在讨论浏览器优化之前,首先我们先分析下从客户端发起一个HTTP请求到用户接收到响应之间,都发生了什么?知己知彼,才能百战不殆。这也是作为一个WEB开发者,为什么一定要深入学习TCP/IP等网络知识。 0x01 到底发生什么了? 当用户发起一个HTTP请求时,首先客户端将与服务端之间建立TCP连接,成功建立连接后,服务端将对请求进行处理,并对客户端做出响应,响应内容一般包括响应

Golang进程权限调度包runtime

关于 runtime 包几个方法: Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行GOMAXPROCS:设置最大的可同时使用的 CPU 核数Goexit:退出当前 goroutine(但是defer语句会照常执行)NumGoroutine:返回正在执行和排队的任务总数GOOS:目标操作系统NumCPU:返回当前系统的 CPU 核数量 p