本文是 从 Flask 到 Gin 系列的第 6 篇,也是最后一篇。


到这里,重点的内容就讲得差不多了。似乎就剩下怎么部署和维护服务器了。

本文主要介绍如何部署 App,以及实现优雅重启。

部署代码

Fabric 是我一直都在使用的部署工具。它是很成熟的工具,网上能找到大量的介绍文章。需要注意的是两点:

  1. 使用最新的 2.x 版本(支持 Python3)。
  2. 早期一点的版本(2019Q4 之前)由于依赖库 Paramiko 不支持 MacOS 生成的 BEGIN OPENSSH PRIVATE KEY 开头的私钥格式,导致在 MacOS 上使用新的 OPENSSH 私钥会报错,现在这个 BUG 已经修复,确保 Paramiko 库使用 2.7.1 版本以上即可。

Flask App 的部署,直接把 Python 源码用 Frbric transfers.rsync 上传到服务器上就可以了,当然你也可以直接自己写个 shell 脚本调用 rsync 来解决。

Gin 的部署,将编译后的可执行文件使用 Fabric.connection.Connection.put 上传到服务器即可。

Flask App 的启动

三年前,我写过一篇 部署Flask + uWSGI + Nginx 完整地介绍过如何部署 Flask 实现的服务,而且后续的一年一直在更新这篇文章。这几年我用 Flask 和 uWSGI 写了很多服务,采用的都是文中提到的方法。

因此,Flask App 部署和启动,可以直接参考上文来处理。本文就不再花费笔墨介绍了。

Gin App 的启动

Gin 生成的 App 就是一个可执行文件而已,如果是临时使用,可以用 nohup/tmux/screen 保证后台启动;如果是正式使用,可以用 systemd 或者 Supervisor

uWSGI + Flask 优雅重启

优雅重启,也叫热更新、平滑升级等等,表达的都是同一个意思,英文一般使用 Graceful upgrade 或者 Zero Downtime。

uWSGI + Flask 的优雅重启很简单,使用 rsync 更新 Python 源码,然后向 uWSGI 的 master 进程发送 SIGHUP 即可实现优雅重启。

要发送信号,可以使用 Fabric.connection.Connection.run

近期我部署的大量的 Flask APP 实例用了另外一种方式:负载均衡 + uWSGI http 服务。这套方案不再需要 Nginx 支持,而是直接用负载均衡服务将请求转发到 uWSGI 的 http 端口。如果实例规模进一步扩大,使用这种方式也容易移植到 API Gateway。

在这种新的部署方式下,我采用了 uWSGI 的 Maser FIFO 来实现优雅重启。从发送控制信号到 uWSGI 进程,改为了直接写入 FIFO 文件来实现优雅重启。这样就不需要关注 master 进程 pid 的变化,或许这样更“优雅”一点?

Gin App 优雅重启

对于 Gin 的 App,优雅重启需要自己来实现。Golang 1.8 新增了 Server.Shutdown 之后,要实现优雅重启已经相当简单了。

我直接拿来主义使用 zerdown 这个 package,main.go 的内容:

package main

import (
	"fmt"
    "github.com/douglarek/zerodown"
	"mjp/models"
	"mjp/routers"
	"mjp/util"

	"github.com/gin-gonic/gin"
)

func main() {
	defer func() {
		if e := recover(); e != nil {
			fmt.Printf("Panicing in main: %s\n", e)
		}
	}()
	models.AutoMigrate()
	r := gin.Default()
	routers.Init(r)
	conf := util.ConfigInstance()
	zerodown.ListenAndServe(conf.Address, r)
}

上面的代码只是提供了优雅重启的功能,要实现优雅重启,还需要知道进程的 pid,向这个进程发送 USR2 信号。

在 Fabric 中获得进程 PID 和发送信号

依然是依赖 Fabric.connection.Connection.run 的远程执行能力,下面的代码片段位于 fabfile.py 中:

import re
from fabric import task, Connection
from invoke.exceptions import Exit

def get_pid(conn):
    address = '127.0.0.1:5005'
    command = 'lsof -i @%s | tail -1' % address
    result = conn.run(command, warn=False, hide=True)
    if result.stdout == '':
        return None
    return re.split(r'\s+', result.stdout)[1]

@task
def reload(c):
    """ 重载服务
    """
    if not isinstance(c, Connection):
        raise Exit('Use -H to provide a host!')
    pid = get_pid(c)
    if pid is None:
        raise Exit('进程不存在,不能重载。')
    result = c.run('kill -USR2 %s' % pid, warn=False)
    if result.ok:
        new_pid = get_pid(c)
        print('重载进程 %s 成功,新的 pid 为 %s' % (pid, new_pid))

参考


本系列到此结束,阅读系列所有文章:从 Flask 到 Gin

全文完