配置文件是反模式

反模式,一般指一些已经形成固定套路的错误做法,至于配置文件,则是因为看到某个微信群中有人抱怨redis的官方docker镜像没有暴露 redis.conf ,我当时不方便讨论,只是说了自己的观点——“配置文件是反模式”。

实际上,这个问题我大约两年前在文章中提到过——

...(后来)大家形成共识——不用环境变量来约束和客制化软件的行为(虽然环境变量本来就是做这个的)。这样的后果是,c/c++、java、python、ruby各种社区都形成了自己的配置文件习惯用法,更进一步,java社区曾经有个趋势,把配置文件本身都做的快和代码没区别了...

不知道有多少人注意这段话,曾有同事反馈说很有感触,我就略过了细节讨论。然而两年后我发现,争论不少,而其中的道理并没有讲透,本文就来补补课,专门讨论一下配置文件的问题。

什么才算配置?

不是配置的“配置”

web.xml、springXX.xml、ibatisXXX.xml......这些曾经是Java工程师熟悉的“配置”,在“约定大于配置”尚未流行的年代,这些东西非常繁琐,常常为开发人员诟病,因为它们做的事情其实和代码差不多,不用代码写其实是因为不方便用编程语言简洁的表达出来。

Ruby on Rails也有类似的文件,比如config目录下的一些文件(包括routes.rb、application.rb,以及 initializers 目录下面的文件等等),它们虽然被称为“配置”文件,但本身也只是ruby代码。从某个角度看,这些文件是为了对一些功能库进行针对性的配置,但是这种“配置”是一次性的、不变的,因而也可以看成是业务功能的编程实现。

上述这些情况有一个共同点,这些所谓的“配置”文件,内容是在开发阶段就写好的,在软件交付以后,无论是测试还是线上,它们都不会被改变,从软件开发的生命周期来看,它们和代码一致,所以这个“配置”文件其实不是配置。

真正的配置

真正的配置是那些以 dev\test\pe\real 为前缀的内容,比如数据库设置——显然,我们在开发阶段和生产环境当然不可能使用同一个数据库。

# config/database.yml
development:
  ......
  database: shelter_development

test:
  ......
  database: shelter_test

production:
  <<: *default
  host: db
  database: shelter_production

这些配置体现的是应用系统与外部环境之间的关系,每一个配置项的值,都和具体环境有关,其生命周期和代码不同,而和软件的部署运行息息相关。

配置管理

由于配置项和运行时相关,当软件系统运行起来的时候,我们必须做的一件事就是为这些配置项提供真实的参数值,比如这样:

production:
  <<: *default
  host: 192.168.16.103

我们知道,这样的内容不应该纳入源码控制,原因有三个:

  1. 线上的实际情况需要保护,直接纳入源码会导致安全风险
  2. 从线上运维来看,一旦需要进行调整(比如更换数据库),应该用比较简单的方式快速切换,“commit-push-deploy”流程对这种调整是过于繁琐的
  3. 对源代码进行的版本控制和这样的内容难以协调,比如线上有多个应用实例,配置项的值不同等等

这里的根本原因还是在于配置项和代码的生命周期不同,它们本来就是两回事。

通常的运维工作中,管理配置项和源码的版本控制系统是独立的,我见过一种做法,线上维护一套git,把所有的线上配置文件都放进去,然后用一个自动化脚本去执行分发。

如果运用docker技术,我们大概会看到这种做法:

docker run ... -v /home/admin/conf/db.yml:/usr/src/app/config/database.yml ...

出于标准化和自动化的需要,这行代码大约会在某个capistrano脚本或者jenkins任务中固化下来,实际上这也是本文最开始那个问题所得到的建议答案。

这个做法是可行的,遗憾的是这并不漂亮,我们在配置项中需要反映的是环境的特殊性(在这里,指的是数据库服务器的host),而对每个这样的配置项,运维的工具需要管理的信息至少包括4个:

  • 配置项名称: host
  • 配置项的值: 192.168.16.103
  • 配置文件的位置: /home/admin/conf/db.yml
  • 配置文件的目标挂载点: /usr/src/app/config/database.yml

显然,前两个才是我们要关心的信息,后两条并不重要。

因为这里出现了“间接性”,由于我们把配置项的名-值绑定放在了文件中,因此运维工具并不能直接管理这个绑定关系,而是迂回的去管理那个文件,并由此引入了挂载点的概念。

这似乎是个小问题,其实不然,这种间接管理除了反直觉以外,至少还有两个后果:

  1. 配置项统一管理的困难,无论你使用什么运维工具,工具并不知道自己管理了哪些配置项,这使得我们缺乏一个全局视图,未来要进行配置项的优化就会比较麻烦——这个后果是,很多团队要再额外开发一个“配置管理系统”,然后让这个管理系统负责维护挂载点和文件实际位置的绑定关系。
  2. 配置项管理不能标准化。显然,一个 java 应用中的 db.properties 和 Rails 应用中的 database.yml 的文件在本质上具有相同的数据结构,但使用起来却大相径庭,于是,我们上面所说的那个“配置管理系统”需要理解五花八门的配置文件格式,而且不但要能读取,还要能——不丢失信息的进行写入(于是还要处理配置文件中的comment内容等很多问题)

所以使用配置文件并不是一个好主意,更好的做法是使用环境变量——

production:
  <<: *default
  host: <%= ENV['db_host'] %>

此时,在docker中启用容器就会是这样:

docker run ... -e db_host='192.168.16.103' ...

乍一看,这么做显得有些落后,特别是配置项较多的时候显得比较麻烦,不过实践中我们并不会直接这么做,借助运维工具,这些配置项可以系统的进行管理——去除了文件这个中间环节以后,配置管理这件事就会变得很简单,一个标准的开源软件就可以承担了。

当然,这里应该管理那些真正的配置项,那些不是配置的“配置”无需这么做。

配置的本质

接下来,让我们深入一步,讨论一下配置这件事的本质是什么。所谓软件,本质上其实就是一个程序而已,一个简单的程序代码可以是这样运行的:

def area
  5 ** 2 * Math::PI
end

我们用它来计算一个半径为5的圆面积,执行一下

irb(main):001:0> area
=> 78.53981633974483

不过,这段代码只能计算固定半径,我们一般会改成这样:

def area r
  r ** 2 * Math::PI
end

执行一下

irb(main):001:0> area 5
=> 78.53981633974483
irb(main):002:0> area 7
=> 153.93804002589985

修改前后的差别是添加了函数的参数,程序设计课老师会告诉我们,函数里的 r 是形式参数,调用时我们用数字 5 或者 7 来代替这个 r ,调用时的参数值也叫做实际参数。

这个例子很简单,然而蕴含着一个很重要的思路——参数化,核心是把我们能确定的东西固化为某种产品单元(函数、类、程序等等),然后通过参数让其具备适应能力,能够灵活适应多种不同的运行上下文。

所以,参数赋值过程其实就是产品单元得到运行上下文的过程,我们也可以看做是对环境的绑定,按照这个思路,函数参数化就是带参数的函数,类的参数化就是携带实例变量的对象实例,而应用程序的参数化就是我们上面所说的配置项,正是在这个意义上,我们才能更好的理解UNIX的先驱们为什么要搞一个环境变量的机制。

强调一遍:所谓配置项就是对应用软件进行参数化,环境变量设定就是实参对形参的绑定

程序和进程的关系非常像类和实例的关系,每当一个程序被“实例化”——也就是启动运行一个进程时,操作系统会对环境变量进行绑定,这样,这个进程就会按照启动时所做的设定调整自己的运行逻辑,以适应外在环境。

可能有人会注意到,环境变量的绑定是进程启动时进行的,而运行过程中一般不能改变它的值,这是不是设计上的不足呢?

并不是,可以设想一下相反的设计,如果环境变量的绑定是随时进行的,那么我们将面对一个行为方式不确定的软件系统,一旦进程运行出现问题,我们要分析的现场,会包括很多不确定其实际值的参数,这对问题的分析常常是个灾难。

配置看似是一个小问题,然而当我们认真考察以后,对系统的架构、组件之间、组件与外部环境之间的关系会有更深入的理解,我一直很喜欢《三体》里章北海父亲临终前的建议“多想想”,是的,遇到问题多想想,想清楚再动手,会事半功倍。

思考题:配置问题与“开闭原则(Open/Closed Principle)”有什么关系?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • Ubuntu的发音 Ubuntu,源于非洲祖鲁人和科萨人的语言,发作 oo-boon-too 的音。了解发音是有意...
    萤火虫de梦阅读 98,451评论 9 468
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,289评论 6 344
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,292评论 18 399
  • 我和我的祖国 9月,随着省委组织的“拥戴核心合作共进”主题教育培训班来到宁夏,倾听专家讲解,追寻红...
    小夕RY阅读 591评论 0 0