读源码-Gunicorn篇-3-配置

本节说明

上一节我们梳理了 gunicorn 服务启动的流程,本章节我们来研究一下它的配置是如何处理的。

来源

从官方文档中我们可以得知,gunicorn 会从 5 个地方读取配置数据,按优先级从低到高分别是:

  1. 从环境变量中读取
  2. 从框架特定的配置文件中读取(目前仅支持 Paster 应用)
  3. 从当前工作目录中的 gunicorn.conf.py 文件中读取,或通过命令行参数指定这个文件
  4. 从名称为 GUNICORN_CMD_ARGS 的环境变量中读取
  5. 命令行启动时指定的参数配置

这几个位置读取到的配置优先级依次增加,不同位置配置的相同参数,优先级高的会覆盖优先级低的。

需要注意的是如果像 GUNICORN_CMD_ARGS 和命令行中都指定了配置文件的路径,则只会解析使用命令行中指定的文件,而不是都解析后进行覆盖合并。

了解了配置的来源及优先级,我们来看下 gunicorn 是怎么做的。

加载

在上一节中,我们看到在创建 WSGIApplication 对象时,调用了 BaseApplication.do_load_config 方法,今天我们就从这个方法入手,看下 gunicorn 是如何处理配置的。

BaseApplication.do_load_config BaseApplication.load_default_config
1
2
3
4
5
6
7
8
9
10
11
def do_load_config(self):
try:
self.load_default_config()
self.load_config()
except Exception as e:
print("\nError: %s" % str(e), file=sys.stderr)
sys.stderr.flush()
sys.exit(1)

def load_default_config(self):
self.cfg = Config(self.usage, prog=self.prog)

可以看到 do_load_config 中先加载了默认配置,之后又加载了用户配置。加载默认配置就是简单地生成了一个 Config 对象保存到 cfg 属性中,而 Config 对象中处理了所有的默认配置,然后加载了环境变量,如下:

Config.__init__
1
2
3
4
5
def __init__(self, usage=None, prog=None):
self.settings = make_settings()
self.usage = usage
self.prog = prog or os.path.basename(sys.argv[0])
self.env_orig = os.environ.copy()

之后执行了 load_config 来加载用户配置,这个方法最终会调用 Application.load_config。

Application.load_config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def load_config(self):
# parse console args
parser = self.cfg.parser()
args = parser.parse_args()

# optional settings from apps
cfg = self.init(parser, args, args.args)

# set up import paths and follow symlinks
self.chdir()

# Load up the any app specific configuration
if cfg:
for k, v in cfg.items():
self.cfg.set(k.lower(), v)

env_args = parser.parse_args(self.cfg.get_cmd_args_from_env())

if args.config:
self.load_config_from_file(args.config)
elif env_args.config:
self.load_config_from_file(env_args.config)
else:
default_config = get_default_config_file()
if default_config is not None:
self.load_config_from_file(default_config)

# Load up environment configuration
for k, v in vars(env_args).items():
if v is None:
continue
if k == "args":
continue
self.cfg.set(k.lower(), v)

# Lastly, update the configuration with any command line settings.
for k, v in vars(args).items():
if v is None:
continue
if k == "args":
continue
self.cfg.set(k.lower(), v)

# current directory might be changed by the config now
# set up import paths and follow symlinks
self.chdir()

在 load_config 方法中,先生成了一个解析参数的 parser,之后调用 init 方法去读了 Paster 应用的配置,然后又解析了环境变量 GUNICORN_CMD_ARGS 里的参数。

之后就是判断命令行参数中有没有指定配置文件,如果没有那 GUNICORN_CMD_ARGS 中有没有指定,如果也没有再看有没有默认的配置文件。这几个按顺序先有哪个加载哪个,后续的就不再处理了。都加载完成后,先将环境变量中读到的配置覆盖到默认配置,之后再将命令行读到的配置进行覆盖。

至此,所有的配置就加载完成了!可以看出,gunicorn 就是按照文档所写的顺序对配置进行覆盖的。

默认参数的实现与加载

我们再来看下默认配置是如何处理的。

在 config.py 文件中,gunicorn 设计了一个基类 Setting,其他所有的配置项比如像 ConfigFile 都继承自这个类,同时还实现了一个 SettingMeta 类,这个类是个元类,我们看下它的实现:

SettingMeta
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SettingMeta(type):
def __new__(cls, name, bases, attrs):
super_new = super().__new__
parents = [b for b in bases if isinstance(b, SettingMeta)]
if not parents:
return super_new(cls, name, bases, attrs)

attrs["order"] = len(KNOWN_SETTINGS)
attrs["validator"] = staticmethod(attrs["validator"])

new_class = super_new(cls, name, bases, attrs)
new_class.fmt_desc(attrs.get("desc", ""))
KNOWN_SETTINGS.append(new_class)
return new_class

def fmt_desc(cls, desc):
desc = textwrap.dedent(desc).strip()
setattr(cls, "desc", desc)
setattr(cls, "short", desc.splitlines()[0])

在 config.py 文件中还有这样一行:

1
Setting = SettingMeta('Setting', (Setting,), {})

这行代码是全局的,在导入 config.py 文件时就会执行,它会将 SettingMeta 作为 Setting 类的元类重新生成一个 Setting 类对象,而所有的配置项类(也就是 Setting 的子类)都是在这行后边定义的,保证了所有配置项继承的都是 修改过 的 Setting 类,这样这些配置项类在创建的时候,就都会走一遍由 SettingMeta 创建的流程。

那么 SettingMeta 类做了什么呢?它做了下面几件事:

  1. 对每个配置项类添加了一个序号 order 用于后续对配置项的分组和排序
  2. 给每个配置项类挂上一个 validator 并设置为静态方法
  3. 将每个配置项类添加到全局的已知配置列表 KNOWN_SETTINGS 中

这样做有什么用呢?

我们看,在 Config.__init__ 方法中,调用了 make_settings,它从 KNOWN_SETTINGS 读取所有的配置项类,之后使用名称作为 key 创建了所有配置项类的词典,然后在 Config.parser 方法中,将每个配置项作为一个参数添加到 parser 的支持参数列表,再用这个 parse 去解析了每个来源的参数,之后再按顺序一个个 set 到 Config 对象的 settings 中,完成所有配置项的解析。

这样在后续如果需要支持新的配置项,只需要写一个新的配置项类,继承 Setting 类即可,因为它的元类是 SettingMeta,它会自动被加入到全局的支持参数的列表,会自动被加入到 parser 中进行解析,能保证服务运行起来时,Config 对象中一定能获取到这个配置项的 key,即使它未配置。

而这一切,都不用再多写其它的东西,只需要继承 Setting 类就好了。

结尾

好了,这一节我们讨论了 gunicorn 的配置是如何加载的,以及配置项的实现,基本上就这么多。

下一个章节,我们来看一下 worker 进程是如何加载的,worker 进程的生命周期,以及当一个请求进来时,是如何交给 worker 进行处理的。

就这样。