设计服务端软件配置的 4 条建议

在设计和开发服务端(后端)软件时,配置文件是一个绕不开的话题。

配置文件是一种用于存放各类可配置项的特殊文件。每个软件都会预设一些默认配置,但这些默认值不可能适用于所有情况。因此,到了不同的环境中,我们常常需要用配置文件对其进行扩展和修改。

拿版本控制软件 git 举例。大部分用户的 home 目录(~)都存放着一份 .gitconfig 配置文件,里面写着自己的用户名和邮箱地址:

[user]
    name = piglei
    email = piglei2007@gmail.com

就像软件的任何一个主要功能一样,配置文件也会极大的影响软件的使用体验。良好的设计能让软件变得更易用,糟糕的设计则会带来许多意想不到的问题,将用户拒之门外。

在这篇文章中,我整理了 4 条关于“软件配置文件”的设计建议,希望能对你有所帮助。

1. 最好不给配置也能用

在网络世界里,每过一天,人们的耐心似乎就又比前一天减少了一丁点。一个制作精良的短视频,如果 3 秒钟之内无法抓住你,你的右手拇指就会条件反射般将它划走。

现在,假设你开发了一个非常有用的工具软件,并发布到了网上。软件的功能非常全面,所有人在使用它之前,需要编辑一份包含 20 项配置的配置文件。你猜,有多大比例的潜在用户会直接掉头走掉?

当我们想要一件东西时,总是一刻也不想等。因此,初始的配置过程麻烦与否,会强烈影响人们尝试软件的决心。在这方面,我认为最好的体验是:无需提供任何配置,便能直接使用软件 80% 以上的功能。

假如无法做到这一点,我们也应该试着从以下几个方面着手,尽量降低用户的配置成本,压缩从“开始安装”到“可使用”之间的等待时间。

1.1 预设合理的默认值

为了将必须由用户提供的配置项,压缩到最少。你得给软件的所有配置项预设一个足够合理的默认值。这些默认值应当能让尽可能多的用户满意。

举个例子,假如你的软件依赖一个可用的 Redis 服务。那么请将该项配置的默认值设为约定俗成的缺省地址: redis_url: redis://localhost:6379/0。这样,当用户的机器上刚好跑着一个 Redis 服务时,便可免去调整这个配置项的麻烦。

1.2 延迟部分配置项

软件复杂到一定程度后,可配置项也常常会多到令人咋舌。许多配置在开发者的眼里,常常都显得特别关键,无法通过预设默认值来简化。此时,想让用户少填一些配置,似乎成了不可能完成的任务。

面对这种情况,我们可以尝试把配置项拆分为两种类型:最少配置项其他配置项。用户在使用软件时,只需提供一份包含最少配置项的文件。剩下的配置,可延迟到使用软件时,通过友好的交互界面要求用户填写。

举个例子,你提供的软件包含一个 Web 站点。要使用它,用户只需提供 1 个配置项:MySQL 数据库地址即可。之后,他可以通过浏览器访问站点,逐步补完其他功能所需的剩余配置项。

2. 永远别默认“管理员密码”

在给配置项预设默认值时,有一点很容易做过头——将关键的安全类配置设置固定的默认值。举个例子,你的项目在第一次启动时,会创建一个具有最高权限的内置管理员角色。为了降低用户的配置成本,该角色的密码被硬编码了一个默认值:

# 重要:请在你的配置文件里修改该默认密码,否则将会带来严重
# 的安全风险。
SUPER_USER_PASSWORD = "proje2p#admin@321"

一旦系统设置了一个默认密码,那么 90% 以上的用户绝对不会去修改它——无论提醒文字说的多危险都没用。当软件传播开来后,这个固定密码会给许多黑客提供便利,带来灾难性的后果。

除了密码以外,任何秘钥类的配置也都不应该提供固定的默认值。你应该总是要求用户手动配置它们。或者,在软件第一次启动时,生成一个随机值也行。做法如下所示:

# 成功安装软件后,弹出提示文字:

恭喜,你已经成功安装了 nice_software。因为你没有在配置里提供
管理员密码,该值已经被设置为随机值:fuiwe2shdvwi23

在之后的流程里,你可以再次要求用户输入该随机密码进行验证,并提供一个新密码。这样也能有效降低安全风险。

除了密码和秘钥外,Web 服务启动时默认绑定的网卡设备,也很容易制造出安全漏洞。假如某软件在启动时,默认绑定所有的网卡接口,那么当用户在携带公网 IP 的机器上安装软件时,服务可能会在毫无准备的情况下被暴露给外网,风险极高。具体可参考 Redis 配置文件中的例子

3. 描述性文本格式优先

在一些使用了动态编程语言(比如 Python、Ruby、Lua 等)的项目中,常常会出现一种独特的配置文件格式:源代码文件

举个例子,假如你有一个项目,是用 Django 框架开发的,那么你在分发项目时,完全可以支持用户提供一个 Python 脚本来作为配置文件。

# 配置文件 my_settings.py 内容
CONTACT_USERNAME = 'contact_us'
CREATE_DEFAULT_ADMIN = True
ATTACHMENTS_DIR = '~/.attachments'

# 启动项目
$ start_project -s ./my_settings.py

第一眼看上去,这种做法非常不错。如果你四处转转,甚至能发现不少开源项目都在这么干——比如 Sentry 在使用 PythonGitLab 在使用 Ruby。在灵活性方面,源码和常见的描述性文本文件(YAML、JSON 等)有着天壤之别。采用源代码格式,意味着用户能通过编写代码,折腾出令人眼花缭乱的复杂配置。

但问题是,对于配置文件来说,灵活性(或者说“可编程性”) 并不是最重要的评价指标。要评价一份配置文件设计的好坏,是否“简单易懂”、是否“易于修改”,每个配置项是否有详尽的说明才是最重要的。而对比源代码和普通描述性文本文件,你会发现前者在这些维度上并不占优。

比如,不论你选了用哪门编程语言作为配置文件格式。当用户想使用你的软件时,就非得了解一些基本语法和数据结构不可。这很可能就会把一些不热衷于此的人拒之门外。

用源代码作为配置,在某些角度上还会鼓励用户把复杂的逻辑塞进配置文件里。有时这不算什么问题,但也有些时候——尤其是当用户需要同时管理许多份配置文件时,这些藏在配置里的复杂代码逻辑,会让维护变得尤其困难。

如上所述,在满足项目需求的前提下,你应该尽量选择简单的描述性文本格式作为配置文件——YAML、TOML、JSON 都行。它们虽然不如代码灵活,但胜在上手简单。

另外,即使同为文本格式,灵活性也各有差异。当你需要从多种格式中挑选一个时,选更简单、更灵活的那个就行。也就是说,如果 TOML 和 YAML 都可以,那就用 TOML 吧。

配置文件结构

在采用了文本格式作为配置文件后,一般项目的配置会形成类似下图这种三层结构:

说明:

  1. 最左侧的主配置模块:由项目开发者维护,负责提供默认配置,同时也包含绝大多数“智能”的配置生成与简化逻辑
  2. 中间部分是用户在使用软件时,需要提供的个性化配置文件
  3. 如环境异常复杂,用户亦可针对具体场景,开发工具来辅助管理和操作配置文件,降低维护成本

4. 部分或完全支持环境变量

The 12-factor App 是一个非常流行的应用架构建议。它推荐使用环境变量来作为应用配置来源,并认定这就是最好的配置管理模式——比任何文件格式都要好。

和配置文件比起来,环境变量确实有着一些天生的优势:比如门槛低、易于修改、操作系统无关性,等等。在 Kubernetes 等云原生平台上,使用环境变量配置服务尤其方便。

正因如此,除了支持从文件中读取配置项以外,你的软件最好也允许用户通过环境变量来修改配置项。

一个示例的配置文件 sample.TOML

contact_username = 'piglei'

同样的配置项,也可通过环境变量来设置:

export CONTACT_USERNAME="piglei"

要实现这个功能,你可以直接在项目中读取环境变量的值:

# config.py
import os

# 优先读取环境变量,而后是配置文件
contact_username = os.environ.get('CONTACT_USERNAME', toml_config['contact_username'])

如果你觉得手动读取环境变量太麻烦,每种编程语言一般都有各自流行的配置管理库,其中一些就支持同时管理文件和环境变量里的配置。比如 Python 的 dynaconf、Go 的 viper 模块,都非常好用。

总结

本文分享了一些设计服务端软件配置的经验之谈,我认为其中最重要的一点,就是认识到:用户总是希望立马用上软件,而不是先折腾半天配置。

希望我们能更积极地思考如何优化配置设计,为软件增添更多色彩。