第十四章 Odoo的启动

启动脚本

我们先来看一下Odoo的启动脚本:

__import__('os').environ['TZ'] = 'UTC'
import odoo

if __name__ == "__main__":
    odoo.cli.main()

可以看出 odoo的启动做了两件事:

  • 设置环境变量的时区为UTC
  • 调用cli模块的main方法,启动程序。

Cli模块

Cli模块包含了odoo启动和命令行的一系列支持功能,原生支持的命令行命令有如下几个:

  • help: 帮助
  • cloc: 用来统计代码行数和字数的工具
  • deploy: 发布模块的工具
  • scaffold: 创建第三方模块的脚手架程序
  • populate: 自动化生成测试数据的工具
  • server: 启动的默认方法,启动主程序
  • shell: odoo的shell环境
  • start: 快速启动odoo服务器的命令

与启动相关的核心模块是Command.py文件。odoo脚本启动后,会调用command.py文件中的main方法,执行odoo的启动程序。

main方法的作用是分析命令行中的参数,匹配正确的命令,执行对应的操作。因为默认的命令是server,因此默认情况下,我们就启动了odoo的主服务进程。

主进程的条件检查

在真正启动odoo进程前,系统还会对进程的环境进行检查,其中包括如下几个条件:

  • root用户:Linux环境下使用root用户是具有潜在的危险因素,因此系统不允许使用root用户进行启动
  • postgres: 同样的,对于数据库的用户,也同样不允许使用postgres账号。

对于系统中csv文件的大小同样有限制,其最大可接收的大小为500M

条件检查完成之后,系统会创建进程文件,然后真正的启动进程。

主进程

主进程的启动类型根据是否配置了多进程选项会有所不同,具体来说分为以下几种类型:

  • GeventServer: 使用Gevent协程的Server。
  • PreforkServer: Gunicorn驱动的多进程实例。
  • ThreadedServer: 多线程模式驱动的进程。

即,如果配置文件中使用了多Worker,那么将使用Gunicorn驱动的多进程实例运行,否则使用单进程运行,默认情况下单进程使用多线程模式驱动,如果启动参数中指定了gevent参数,那么使用Gevent驱动的单进行驱动。

另外,单元测试只能在单进程模式下运行。

线程模式下的启动

线程模式下的启动会同时启动HttpServer和定时任务线程CronThread。线程模式使用多线程方式调用http_thread方法:

def http_thread(self):
    def app(e, s):
        return self.app(e, s)
    self.httpd = ThreadedWSGIServerReloadable(self.interface, self.port, app)
    self.httpd.serve_forever()

内部使用了WSGIServer作为Web服务器启动。

Gevent模式下的启动

Gevent模式下同样使用的是WSGISever,不同的是使用gevent协程方式启动。

 self.httpd = WSGIServer(
    (self.interface, self.port), self.app,
    log=logging.getLogger('longpolling'),
    error_log=logging.getLogger('longpolling'),
    handler_class=ProxyHandler,
)
_logger.info('Evented Service (longpolling) running on %s:%s', self.interface, self.port)

Gevent模式默认工作在8072端口,也就是长连接端口,顾名思义,GeventServer是用来处理长连接请求的服务,单进程模式下一般不使用此模式。

PreforkServer模式下的启动

PreforkServer模式下,进程会fork出多个进程,然后将多个进程的父进程指定为第一个进程,然后调用worker的run方法启动。

PreforkServer采用gunicorn驱动,启动之后会执行下面四个任务:

  • self.process_signals()
  • self.process_zombie()
  • self.process_timeout()
  • self.process_spawn()

处理完进进程信号、僵死进程、超时进程之后开始孵化新的进程。在孵化新进程过程中,会创建一个GeventServer用来处理长连接的任务。这点我们可以通过查看进程列表证实:

progress

TIP

如果我们在测试环境下没有配置反向代理启动了多workder 模式,那么我们会碰到下面的错误:

```python
Traceback (most recent call last):
File "/mnt/hgfs/Code/odoo/odoo14/odoo/http.py", line 639, in _handle_exception
    return super(JsonRequest, self)._handle_exception(exception)
File "/mnt/hgfs/Code/odoo/odoo14/odoo/http.py", line 315, in _handle_exception
    raise exception.with_traceback(None) from new_cause
Exception: bus.Bus unavailable
```

这个错误的原因是因为IM模块的消息总线机制使用的是长连接机制,虽然后台启动了长连接进程,但是我们web端并没有将longpolling的请求转发给监听在8072端口上的长连接进程。消息总线的分发机只支持在单worker或者gevent模式下运行,对于普通的多workder进程来说,无法获取到dispatch对象,因此会引发上面的错误。

解决这个问题的方法也很简单,配置前端反向代理服务器,将longpolling的请求打到gevent 进程上即可。

```sh
location /longpolling {
    proxy_pass http://127.0.0.1:8072;
}
location / {
    proxy_pass http://127.0.0.1:8069;
}
```

主进程启动之后,odoo就进入到了监听模式。odoo处理HTTP请求的入口是http.py文件中的Root类。

def application_unproxied(environ, start_response):
    """ WSGI entry point."""
    # cleanup db/uid trackers - they're set at HTTP dispatch in
    # web.session.OpenERPSession.send() and at RPC dispatch in
    # odoo.service.web_services.objects_proxy.dispatch().
    # /!\ The cleanup cannot be done at the end of this `application`
    # method because werkzeug still produces relevant logging afterwards
    if hasattr(threading.current_thread(), 'uid'):
        del threading.current_thread().uid
    if hasattr(threading.current_thread(), 'dbname'):
        del threading.current_thread().dbname
    if hasattr(threading.current_thread(), 'url'):
        del threading.current_thread().url

    with odoo.api.Environment.manage():
        result = odoo.http.root(environ, start_response)
        if result is not None:
            return result

我们来看一下Root的核心方法:

def __call__(self, environ, start_response):
    """ Handle a WSGI request
    """
    if not self._loaded:
        self._loaded = True
        self.load_addons()
    return self.dispatch(environ, start_response)

从中我们可以看出,odoo会判断当前进程是否加载的addons模块,没有加载则启动加载程序,最后再进入监听模式。

Root类的核心是dispatch方法,它负责给请求挂载session、绑定数据库、设置语言环境,以及处理一些请求中的异常。

接下来的部分就是我们很熟悉的Request部分的内容了。

总结

我们可以把这个流程总结成为一张更为直观的图:

lc

results matching ""

    No results matching ""