Web 服务的进程托管

2023-10-19 09:33:27 浏览数 (1)

「入门」标签的文章是我写给新手入门者的解疑文章水文,也是给自己的知识有个地方做做梳理。如果本文对你没有帮助,可以不看。

在开发 Web 服务(或者叫 App,后文中 App 和服务概念等同)的时候,最后一步就是启动服务器运行你的 App。在大部分的教程中,这里的选择通常是 uwsgi 或者 gunicorn。你会发现,它运行起来以后,会占用你当前的一个终端会话,进入「长运行模式」,就像这样:

代码语言:javascript复制
[2020-05-23 22:54:57  0800] [13077] [INFO] Starting gunicorn 20.0.4
[2020-05-23 22:54:57  0800] [13077] [INFO] Listening at: unix:/tmp/***.socket (13077)
[2020-05-23 22:54:57  0800] [13077] [INFO] Using worker: sync
[2020-05-23 22:54:57  0800] [13084] [INFO] Booting worker with pid: 13084

这里最后一行是没有光标的,你没法执行其他命令,除非用Ctrl C退出进程。这时假如你关闭终端、关闭 SSH 连接客户端(PuTTy, Xshell 之类),Web 服务进程就立刻退出了,那不是白忙活了吗?这是因为你在终端中运行的所有进程,父进程都是当前终端会话,并且绑定了标准输入输出。很多人知道可以在命令末尾加上&把进程转为后台运行,但这样的后台进程并没有改变它的父进程,所以终端会话结束以后这个进程依然会不在。那么如何解决这个问题呢?我下面提供了三种解决方法,推荐程度也逐次提高。如果懒,就直接看第三种方案。

在后续介绍三种方案时,假定你运行服务器的命令是

代码语言:javascript复制
$ gunicorn -b :8888 -w 4 my_blog.wsgi

请根据个人情况做相应改动,教程并不是用来百分百复制粘贴的。如果是在虚拟环境中运行,只需要将虚拟环境的路径加到前面即可:

代码语言:javascript复制
$ /path/to/my/venv/bin/gunicorn -b :8888 -w 4 my_blog.wsgi

nohup

nohup 命令可以将进程变成不挂起的,(默认情况下)它会把标准输出和标准错误输入重定向到当前目录的nohup.txt文件中,并且将进程的父进程改成 1,也就是 1 号进程,这样终端退出以后,此进程将继续持续运行,我们将这种进程叫做守护进程1。使用方法如下:

代码语言:javascript复制
$ nohup gunicorn -b :8888 -w 4 my_blog.wsgi &

注意前面加上了nohup以及末尾的&

更正 nohup 并不会将进程的父进程改成 1, 只会把 SIGHUP 的设置为 ignore, 这样退出会话的时候 bash 发送的 SIGHUP 被忽略, 从而成为孤儿进程被 1 号进程接管. Thanks @Ooth-Gray

supervisor

nohup虽然能将进程转为后台运行,但它缺少一个很重要的功能:异常重启和开机自启动的功能。你重启服务器必须得记得去启动下你的服务器。所以更强大的、专门的进程管理工具就应运而生。supervisor是用 Python 写的一款进程管理器,它支持进程异常重启、日志存储,并且提供了一个命令行程序来查看、管理当前的进程。使用方法如下:

安装

代码语言:javascript复制
$ pip install supervisor

生成配置文件

代码语言:javascript复制
$ echo_supervisord_conf > /etc/supervisord.conf
$ mkdir /etc/supervisor.d

编辑/etc/supervisord.conf文件,将文件最后两行取消注释

代码语言:javascript复制
[include]
files = supervisor.d/*.ini

编写应用进程的配置/etc/supervisor.d/my_blog.ini,文件中;开头的行是注释行,如果需要该配置生效则取消注释

代码语言:javascript复制
[program:myblog]
command=/path/to/my/venv/bin/gunicorn -b :8888 -w 4 my_blog.wsgi              ; 启动进程的命令
;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
;numprocs=1                    ; number of processes copies to start (def 1)
directory=/path/to/my_blog                ; 运行命令时先切换到此目录下
;umask=022                     ; umask for process (default None)
;priority=999                  ; the relative start priority (default 999)
;autostart=true                ; start at supervisord start (default: true)
;startsecs=1                   ; # of secs prog must stay up to be running (def. 1)
;startretries=3                ; max # of serial start failures when starting (default 3)
;autorestart=unexpected        ; when to restart if exited after running (def: unexpected)
;exitcodes=0,2                 ; 'expected' exit codes used with autorestart (default 0,2)
;stopsignal=QUIT               ; signal used to kill process (default TERM)
;stopwaitsecs=10               ; max num secs to wait b4 SIGKILL (default 10)
;stopasgroup=false             ; send stop signal to the UNIX process group (default false)
;killasgroup=false             ; SIGKILL the UNIX process group (def false)
;user=myuser                   ; 启动进程的用户,推荐不要用root用户,否则注释此行
redirect_stderr=true          ; 重定向错误到输出 (默认false)
stdout_logfile=/a/path        ; 标准输出的日志地址,会将所有print到终端的输出输出到指定的文件中
;stdout_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
;stdout_logfile_backups=10     ; # of stdout logfile backups (0 means none, default 10)
;stdout_capture_maxbytes=1MB   ; number of bytes in 'capturemode' (default 0)
;stdout_events_enabled=false   ; emit events on stdout writes (default false)
;stderr_logfile=/a/path        ; stderr log path, NONE for none; default AUTO
;stderr_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
;stderr_logfile_backups=10     ; # of stderr logfile backups (0 means none, default 10)
;stderr_capture_maxbytes=1MB   ; number of bytes in 'capturemode' (default 0)
;stderr_events_enabled=false   ; emit events on stderr writes (default false)
;environment=A="1",B="2"       ; process environment additions (def no adds)
;serverurl=AUTO                ; override serverurl computation (childutils)

启动supervisord:

代码语言:javascript复制
$ supervisord

进程的查看、终止与启动

代码语言:javascript复制
$ supervisorctl status    # 查看进程状态
$ supervisorctl stop my_blog    # 终止my_blog进程
$ supervisorctl start my_blog    # 启动my_blog进程
$ supervisorctl restart my_blog    # 重新启动my_blog进程

systemd

systemd 是现在比较新的 Linux 发行版都自带的一个进程管理器2,使用自带的,就不用再费劲安装别的库了,干净又快捷,强力推荐用这个方法。使用方法也很简单,创建以下文件内容

代码语言:javascript复制
[Unit]
Description=My blog service

[Service]
Type=forking
ExecStart=gunicorn -b :8888 -w 4 my_blog.wsgi
KillMode=process
Restart=on-failure
RestartSec=3s

[Install]
WantedBy=multi-user.target

保存到/etc/systemd/system/my_blog.service。然后执行:

代码语言:javascript复制
$ systemctl enable my_blog

这样你的进程就自动加入开机自启动了,同样,systemd 也可以查看、启动、停止进程:

代码语言:javascript复制
$ systemctl status my_blog    # 查看进程状态
$ systemctl stop my_blog    # 终止my_blog进程
$ systemctl start my_blog    # 启动my_blog进程
$ systemctl restart my_blog    # 重新启动my_blog进程

就是这么 Easy!

Footnotes

  1. 关于nohup命令的作用和守护进程的定义本文只做粗浅介绍,只为提供解决的方法。如果对原理作用不清楚,推荐阅读laixintao 的这篇博文和nohup,setsid 与 disown 的不同之处 ↩
  2. 使用systemctl检查下你的系统有没有安装,如果没有,则先尝试用系统的包管理工具安装systemd,否则就只能用supervisor了。 ↩

0 人点赞