flask框架的自动重载技术

流行的python web框架都支持自动重载技术,虽然该技术不能应用于生产环境,但在开发测试过程中,极大的提升了开发人员的工作效率,启动服务以后,如果对源码进行了修改,框架会自动重新加载所有代码,这样,就免去了反复停止启动服务的麻烦。

本文将带你研究这种自动重载技术,探究其内部实现的原理

reload---python模块重载技术

python 内置函数reload可以将一个模块重新导入,通过学习理解这个函数,你可以知晓框架重载的一般原理和过程,新建两个python脚本

├── __init__.py
├── conf.py
└── main.py

main.py的内容为

import time
import os
import conf
from threading import Thread
from imp import reload

def run():
    """
    主流程
    :return:
    """
    while True:
        print(conf.ip)
        time.sleep(5)


def watch():
    """
    监控文件变化
    :return:
    """
    last_modify_time = get_file_modify_time('./conf.py')
    while True:
        modify_time = get_file_modify_time('./conf.py')
        if modify_time != last_modify_time:
            last_modify_time = modify_time
            reload(conf)

        time.sleep(3)


def get_file_modify_time(filename):
    return time.ctime(os.stat(filename).st_mtime)


t = Thread(target=watch)
t.start()       # 启动文件监控

run()    # 启动主流程

conf.py的内容为

ip = '192.168.0.1'

在main.py脚本中,run函数启动后,每5秒钟会打印一次conf模块下的ip值,同时,watch函数会在另外一个线程中监控conf.py的脚本是否发生变化,一旦发现conf.py脚本的最后修改时间由变化,则使用reload函数重新加载conf模块。

启动main.py脚本后,等待它输出几次ip的值以后,修改conf.py里ip变量的值,你会发现,main.py里输出的值也随之发生变化,这说明,reload函数重新导入了conf模块,因此conf.ip的值发生了改变。

上面的例子虽然简单,但它揭示了python web框架自动重载的一般原理,即监控文件变化,重新载入模块。

flask 自动重载技术

简单的重载示例代码

为了演示自动重载技术,我写了一个简单的服务
脚本目录为 /Users/zhangdongsheng/experiment/test/test.py

from flask import Flask

app = Flask(__name__)


@app.route('/', methods=['GET'])
def index():
    return 'ok'


app.run(debug=True)

启动服务后,你可以修改index函数的返回值,可以看到flask会自动重新加载脚本

run_with_reloader

通过跟踪run方法,我们得到以下函数调用链

run --> run_simple --> run_with_reloader --> 

先来看run_with_reloader

def run_with_reloader(main_func, extra_files=None, interval=1,
                      reloader_type='auto'):
    """Run the given function in an independent python interpreter."""
    import signal
    reloader = reloader_loops[reloader_type](extra_files, interval)
    signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
    try:
        if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
            t = threading.Thread(target=main_func, args=())
            t.setDaemon(True)
            t.start()
            reloader.run()
        else:
            sys.exit(reloader.restart_with_reloader())
    except KeyboardInterrupt:
        pass

在run_simple函数中,定义了一个inner函数,这个函数就是真正的启动服务的函数,inner函数作为参数传给run_with_reloader,参数名为main_func

在run_with_reloader 函数中,先要判断环境变量run_with_reloader 是否为true,如果不等于字符串true,则执行代码

sys.exit(reloader.restart_with_reloader())

这是理解flask自动重载的关键所在,sys.exit意味着进程退出,但在退出前,先执行了restart_with_reloader方法,reloader对象的类型是StatReloaderLoop,来看一下restart_with_reloader方法都做了哪些事情

    def restart_with_reloader(self):
        """Spawn a new Python interpreter with the same arguments as this one,
        but running the reloader thread.
        """
        while 1:
            _log('info', ' * Restarting with %s' % self.name)
            args = _get_args_for_reloading()
            new_environ = os.environ.copy()
            new_environ['WERKZEUG_RUN_MAIN'] = 'true'

            # a weird bug on windows. sometimes unicode strings end up in the
            # environment and subprocess.call does not like this, encode them
            # to latin1 and continue.
            if os.name == 'nt' and PY2:
                for key, value in iteritems(new_environ):
                    if isinstance(value, text_type):
                        new_environ[key] = value.encode('iso-8859-1')

            exit_code = subprocess.call(args, env=new_environ,
                                        close_fds=False)
            if exit_code != 3:
                return exit_code

_get_args_for_reloading() 函数返回的结果是

['/usr/local/bin/python3.6', '/Users/zhangdongsheng/experiment/test/test.py']

前面是python的解释器位置,后面是我实验代码的脚本位置, 通过subprocess.call方法,再一次的启动了服务脚本,这就意味着我的test.py脚本再次被执行,而之前执行时所启动的进程即将在执行sys.exit时结束。

另一个关键点是在这个方法中,将环境变量WERKZEUG_RUN_MAIN设置为true,这样,当代码再次执行到run_with_reloader时,if条件语句就成立了

reloader.run()

由于if条件成立,这次,会启动一个线程来执行main_func函数,同时,执行reloader.run()

class StatReloaderLoop(ReloaderLoop):
    name = 'stat'

    def run(self):
        mtimes = {}
        while 1:
            for filename in chain(_iter_module_files(),
                                  self.extra_files):
                try:
                    mtime = os.stat(filename).st_mtime
                except OSError:
                    continue

                old_time = mtimes.get(filename)
                if old_time is None:
                    mtimes[filename] = mtime
                    continue
                elif mtime > old_time:
                    self.trigger_reload(filename)
            self._sleep(self.interval)

代码意图很明显,就是要检查服务所依赖的每一个模块是否有变化,一旦有变化,就执行trigger_reload方法,trigger_reload方法会记录输出重载的脚本名称,同时执行sys.exit(3),进程要退出,退出?进程退出了还怎么重载啊?

回到restart_with_reloader方法中,注意看最后一段

exit_code = subprocess.call(args, env=new_environ,
                                close_fds=False)
if exit_code != 3:
    return exit_code

当前进程是由subprocess.call创建的,如果通过sys.exit(3)退出,那么就会返回状态码3赋值给exit_code, 可是只有在exit_code不等于3的情况下,才会执行return,当前进程退出是由于文件被修改了,exit_code=3,那么restart_with_reloader方法就不会被退出,而是继续执行while循环,再一次执行subprocess.call方法启动服务。

通过前面的分析,你应该大值理解flask框架的自动重载原理了,简单总结一下

  1. 服务的进程是通过subprocess.call创建出来的,且服务是通过一个线程来执行的
  2. 当服务进程检测到文件被修改时,退出服务进程并返回状态码3
  3. 创建服务进程的程获得状态码后,发现状态码为3,再次执行subprocess.call方法创建服务进程

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案