读源码-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 去解析了每个来源的参数,之后再按顺序一个个 setConfig 对象的 settings 中,完成所有配置项的解析。

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

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

结尾

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

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

就这样。