如何以service的方式运行python程序.md

478 阅读3分钟
原文链接: zhuanlan.zhihu.com

本文提供了一种简单易行的在类unix系统中,以service的方式运行python程序的方法。

当我们远程联机部署并启动一个python程序后,我们常常希望脱机后,该程序仍然能继续在后台运行。有好几种方法可以实现这一想法:

  1. 专门的、跨平台的daemonize程序。windows和unix系统(含linux)的服务管理方式是不一样的,所以将一个python程序转化为服务程序,并能跨平台使用还是一件比较麻烦的事,因此有人做了相关的工作。
  2. 在Unix/Linux下修改init script。这个比较复杂,而且不同的Linux子系统、甚至同一系统的不同版本,init script的管理方式也在演进。
  3. 在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 …来启动。