本文提供了一种简单易行的在类unix系统中,以service的方式运行python程序的方法。
当我们远程联机部署并启动一个python程序后,我们常常希望脱机后,该程序仍然能继续在后台运行。有好几种方法可以实现这一想法:
- 专门的、跨平台的daemonize程序。windows和unix系统(含linux)的服务管理方式是不一样的,所以将一个python程序转化为服务程序,并能跨平台使用还是一件比较麻烦的事,因此有人做了相关的工作。
- 在Unix/Linux下修改init script。这个比较复杂,而且不同的Linux子系统、甚至同一系统的不同版本,init script的管理方式也在演进。
- 在Unix/Linux下通过nohup python script.py &来运行。本文在此基础上提供一个完整的方案,使得我们可以通过仿真service运行的方式来启动、停止和重启服务:
pyservice start $service_name --env=$env_name --path=/path/to/service.py
我们生成了一个名为pyservice的bash脚本,将它部署到/usr/bin目录下后,就可在任何地方随时运行上面的命令了。
nohup命令
nohup命令是这个脚本的核心。通过nohup $cmd &方式来运行的程序,在控制台断开后,程序将仍然保持运行。 nohup命令会将程序的控制台输出、错误信息输出到nohup.out文件中。 现在创建一个测试程序:
#test.py
import time
print("enter test")
for i in range(0, 100):
time.sleep(1)
print(i)
print("leave test")程序很简单。我们接下来通过nohup命令运行:
nohup python test.py &
当前目录下会生成一个nohup.out文件,里面记录着:
enter test
1
2
...
100
leave test现在,我们来一步步将它转化为一个”真正“的服务程序。使得你可以能通过pyservice start test来启动它,pyservice stop test来停止它。
运行python程序的一般需求
我们一般使用虚拟环境来部署和运行python程序,以保证程序一次编写,处处运行。我们在开发时,很可能为该程序创建了一个恰当的虚拟环境,并写好了requirements.txt。
我们的pyservice虽然不是安装程序,但它也应该遵循这种一般要求。此外,很可能需要将源程序目录添加到PYTHONPATH中去。显然易见,我们需要给每个服务指定一个入口(可运行的python脚本),如果我们不想创建一个注册表(正如windows服务和Linux init script所做的那样),那么我们需要在启动服务时指定脚本路径。
所以,最全格式的命令应该如下1所示:
1. pyservice start test --env=test --path=/usr/script/test.py
2. pyservice start test
3. pyservice stop test
4. pyservice restart test当我们执行命令二,即pyservice start test时,pyservice如何知道启动哪一个脚本(以及在哪里呢)?这里我们需要进行一些约定,如果你愿意遵守这些约定,则你可以通过简短的命令格式来启动停止程序,就象它是一个真正的服务一样。如果你不愿意,也关系,命令格式1仍然保留了这种灵活性。
如果你希望以命令2的格式来启动服务,需要遵守以下约定: 1. 服务应该部署到/apps/service_name.py 2. 运行该服务的conda虚拟环境名为$service_name。pyservice将自动激活它。
此外,为了能够通过服务名来停止程序的运行,我们在启动命令行中加上了–service=$service这样的标记,这样pyservice stop $service就容易实现了:
bash ps -ef | grep "service=$service" | grep -v grep | awk ‘{print $2}’ | xargs kill -9 `pyservice的实现
#!/usr/bin/env bash
help(){
IFS='' read -r -d '' String <<"EOF"
\n
===========\n
Pyservice - script to run a python app as service, using nohup\n
\n
usage:
pyservice cmd service_name --env=conda_virtual_env_name --path=path/to/python_script.py\n
\n
example: pyservice start service_name --env=stock --path=/apps/stock/app.py\n
\n
args:\n
cmd: required. Can be either of 'start', 'stop', 'restart'\n
service_name: required and must unique. Name of the service, we'll use this one to find process to kill\n
env: optional. If provided, the app will running in this env.\n
path: optional. If ignored, we'll start /apps/$service_name/app.py\n
\n
EOF
echo -e $String
echo Error: $1
echo
}
check_running(){
# check if service is running by --service=$service_name
local service=$1
local result=`ps -ef | grep "service=$service" | grep -v grep | awk '{print $2}'`
if [[ -z $result ]]
then
return 1
else
return 0
fi
}
switch_env(){
if ! [ -z $env ]
then
source activate $env
prompt=`printenv |grep CONDA_PROMPT_MODIFIER`
if [[ $prompt != *"$env"* ]]; then
echo "failed to switch conda env $env, exited"
exit -1
fi
fi
}
start_service(){
# $path, $env are global parameters parsed by argparse
local service=$1
if check_running $service
then
echo "service " $service " is already running. To restart it, please use 'restart'"
else
# if path is not passed in, then we'll compose one
if [ -z $path ]
then
path="/apps/$service/app.py"
fi
echo "starting service " $@
if [ -z $env ]
then
env=$service
fi
switch_env $env
# add $path to PYTHONPATH
DIR=$(dirname "${path}")
export PYTHONPATH="$DIR:$PYTHONPATH"
# run the program
cmd="python $path --service=$service "
nohup $cmd &
echo "Done."
fi
}
stop_service(){
local service=$1
if ! check_running $service
then
echo "service $service is not running."
else
echo "killing service " $@
ps -ef | grep "service=$service" | grep -v grep | awk '{print $2}' | xargs kill -9
fi
}
main(){
source /usr/bin/argparse.sh
parse_args "$@"
local cmd=$1
local service=$2
support_cmd="start stop restart"
if ! [[ " $support_cmd " =~ .*\ $cmd\ .* ]]
then
help "cmd not provided or cmd is not one of 'start', 'stop', 'restart'"
fi
if [[ $service == -* ]]
then
help "Please provide service name"
fi
case $cmd in
start)
start_service $service
;;
stop)
stop_service $service
if check_running $service
then
echo "failed to stop $service"
else
echo "killed."
fi
;;
restart)
stop_service $service
echo "WARNING: THIS WILL START SERVICE $service at /apps/$service/app.py with --env=$service, (Y)es or (N)o?"
read choice
if [[ $choice == 'Y' || $choice == 'y' ]]
then
start_service $service
fi
;;
*)
help "cmd not provided or cmd is not one of 'start', 'stop', 'restart'"
;;
esac
}
main "$@"为了解析longopts,使用了一个第三方的小脚本, argparse.sh。这个脚本使得我们在程序中的任何地方都可以$path, $env来获利传入的参数值(这里path, env是定义的长选项)。
argparse.sh也需要部署到/usr/bin/目录下。
切换conda env
脚本的其它地方都很简单。conda env切换后,需要做一个测试,这里使用了
prompt=`printenv |grep CONDA_PROMPT_MODIFIER`然后再通过[[ $prompt != *"$env"* ]]来判断当前环境是不是$env。如果是,则可以(粗略地)认为切换成功。在你的环境中,安装完成后,可能没有这个环境变量,可以做适当修改(比如替换成CONDA_PREFIX):

pyservice restart
这个命令执行时,会有一个提示:
WARNING: THIS WILL START SERVICE $service at /apps/$service/app.py with --env=$service, (Y)es or (N)o?因为此时没有–path和–env,在重新启动时,它只能按我们预设的部署方案去寻找应用程序,但这可能引起不期望的结果。如果是这样,输入n结束,再通过长格式的pyservice start …来启动。