第十四章 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用来处理长连接的任务。这点我们可以通过查看进程列表证实:
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部分的内容了。
总结
我们可以把这个流程总结成为一张更为直观的图: