You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

260 lines
19 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 28 | Ops三部曲之一配置管理
你好,我是四火。
欢迎进入第五章:寻找最佳实践。本章我们会讲到 Ops由于 Ops 的范围实在太广了,因此从今天开始,接连三讲,我们会讨论 Ops 的三个常见话题,今天要谈的就是其中的第一个——配置管理。
我们总在谈论 Ops所谓 Ops指的就是 Operations如果你专门去搜索你恐怕也很难找到一个准确的定义。在中文翻译上看“运维”这个词用得比较多但是我认为 Ops 的含义明显更为广泛。我的理解是,和实际的软件开发和质量保证相对应的,环境搭建、配置管理、自动化流程、部署发布,等等的实践活动,无论线上还是线下,无论它们是在开发测试环节还是产品发布环节,都可以算作 Ops。
当然,**我们并不需要执着去找出 Ops 和开发、测试的分界线,它们实际是互有重叠的**,我觉得[维基百科](https://zh.wikipedia.org/wiki/DevOps#/media/File:Devops.svg)上的这张图就很好地展示了这一点。
![](https://static001.geekbang.org/resource/image/e0/46/e01e10d70ba403368ee9f9973d87f646.jpg)
我想很多程序员不喜欢 Ops也不愿意谈 Ops这都可以理解毕竟和开发本身比起来它没有那么多的创造性有时甚至属于“重要但无趣”的工作。但是我必须要强调的是不只对于全栈工程师而言Ops 能力对于每一个通常意义上的软件工程师来说,都是必须要锻炼的重要方面。
需要明确的是,我今天要讲的配置管理,我认为就是 Ops 的一个重要部分,它关注的是软件代码以各种形式,在项目的各个阶段存在的配置。几乎每个做运维的人都要经常去修改大量的线上配置,无论是来自操作系统还是应用本身。这个话题很少有人谈及,但却存在于每个程序员的日常工作中。
## 常见的配置方式
在实际的项目开发过程中,当我们讲到“配置”的时候,其实隐含了许多不同的方式,这些方式有时候看起来差不多,实际却有着最适合的场景。
### 1.源代码中的常量
代码常量是一种最典型的配置方式,它的特点是定义和使用方便,且只对开发人员友好。每次变更的时候,都需要改动代码,经过一定的测试和 Code Review之后还要通过指定流程的部署才能发布到产品线上。
通常来说,项目中常量类的定义有着明确的规约,比如这样的示例代码:
```
public final class ProductConstants {
private ProductConstants() {}
public static int MAX_NUMBER = 9999;
public static String DEFAULT_CODE = "123";
}
```
你看,上面的常量类包含的要点有:
* 类名直白且具体,这个 Product 修饰很重要,避免了过于一般化的常量类定义,从而限制了它的职责范围;而这个 Constants 则说明了类的属性,一眼就知道它是干什么的。
* 被定义为无法实例化的(即将构造器私有化),无法被继承的(即使用 final 修饰)。
* 常量都使用 public static 修饰,这样就可以直接访问,而不需要实例化这个类。
值得一提的是,在我参与过的项目中,有的程序员为了强制编码的时候只能定义 static 属性来表示常量,会使用 interface 这样的方式,即使用接口来存放常量。效果自然是能达到的,但是这个方法有些投机取巧(毕竟和 interface 关键字所应该表示的含义有明显偏差了),我认为这是一种反模式。
### 2.代码中的配置文件
代码中的配置文件也很常见,它们从 Java、Python 这样的编程语言中脱离出来,但是依然作为源代码的一部分而存在。那为什么要这样做呢?
简单来说,就是为了“解耦”。
而就是这个“解耦”,带来了诸多好处。例如最小职责,单一职责,即配置文件做且只能做配置;再例如可以将同一类的资源放在更合适的地方统一管理了。
举例来说:
```
MessagesBundle_en_US.properties
MessagesBundle_fr_CN.properties
MessagesBundle_de_DE.properties
```
你看,这样的 i18nInternationalization的特定语言的资源文件就从编程语言的代码中独立出来了更容易统一管理和维护。
这种把相关代码或配置单独拿出来的方式,不知道你能否联想到我们在 [\[第 10 讲\]](https://time.geekbang.org/column/article/143834) 中介绍的 MVC 控制器各个子功能的配置方式,当时不但讨论了横向和纵向两种实现形式,还讨论了各自的优劣,如果忘记了的话,你可以阅读回顾一下。比如 URL 映射,既可以通过注解的方式,和实际的 Controller 代码放在一起;也可以通过 XML 配置的方式,从 Java 代码中拿出去。
### 3.环境配置文件和环境变量
接着我们来讲讲环境下的配置。这里的“环境”Environment指的是代码部署的环境可以是用于开发的笔记本电脑也可以是生产线上的机器。对于不同的环境虽说代码是同一份但是可以通过不同的环境配置文件和环境变量让代码执行不同的逻辑。
举一个环境配置文件的例子,例如环境下有这样一个文件,定义了这个环境所属的“地区”,而通过这个地区,可以从代码中寻找到相应正确的配置文件来读取配置:
```
/etc/region
```
在某些系统中,这样的配置可以通过一个 web 工具指定,并通过一定的部署工具将其同步到目标环境中去。
还有一种类似的方式是环境变量,环境变量和环境配置文件,有时允许同时存在,但是有一定的优先级顺序。比如说,如果环境变量使用 REGION 也指定了上述文件中的 region发生了冲突这就要根据优先级的关系来决定使用哪一个配置通常来说环境变量比环境配置文件优先级更高。
那为什么要允许配置不同方式的覆写呢?这是为了给应用的部署赋予灵活性,举例来说,/etc/region 已经确定了值为 EU欧洲那么如果需要在该物理机器上部署两份环境 ,第二份就可以通过环境变量 REGION 强制指定为 NA北美
### 4.运行参数
运行参数,可以说是最为具体的一种配置方式,真正做到了应用级别,即一个应用可以配置一个参数。这种情况其实对于开发人员来说非常熟悉,比如在项目中应用 [log4j2](https://logging.apache.org/log4j/2.x/) 这个日志工具时,启动应用的 java 命令上,增加如下参数:
```
-Dlog4j.configurationFile=xxx/log4j2.xml
```
通过这样的运行参数指定的方式,指定 log4j2 配置文件的位置。
当然,也可以通过规约优于配置的方式,不显式指定位置,而在 classpath 中放置一个名为 log4j2.component.properties 的文件,里面指定类似的配置,而 log4j2 在系统启动的时候可以自动加载。
### 5.配置管理服务
常见的配置方式中,我们最后来讲一下配置管理服务。尤其对一个较大的系统来说,配置管理是从整个系统的层面上抽取并统一管理配置项的方式。通常来说,这样的配置管理系统会被包装成一个服务,当然,也有少数是单纯放到数据库的某张表里,不过这种数据库访问层面的耦合通常并不推荐。
一旦配置管理成为了独立的服务,就说明这个系统已经复杂到一定程度了,通常也意味着这个服务的用户,不再只是开发人员了,往往还有运维人员,甚至是一些非技术的管理人员。
## 配置的层级关系
资源文件,本质上也算是代码的一部分,**通过合理的设计,可以让资源文件具备编程语言代码一般的继承关系。**比如这样的配置文件组织结构:
```
conf/rules.conf
conf/CN/rules.conf
conf/CN/Zhejiang/rules.conf
conf/US/rules.conf
```
conf 目录下rules.conf 文件就像是基类,存放了默认的规则配置;下面的 CN 目录下的 rules.conf 则存放了中国区的增量配置,就像是子类,里面的配置项优先级高于“基类”的配置,起到一个有选择性地覆写的作用;而再下面的 Zhejiang 目录下的 rulels.conf 则表示浙江省的规则配置,优先级更高。
在这种方式下,配置代码不但清晰易懂,而且减少了重复,易于维护。
## 规约优于配置
不知道你是否还记得我们在 [\[第 10 讲\]](https://time.geekbang.org/column/article/143834) 中介绍过的“终极偷懒大法”——规约优于配置CoCConvention over Configuration。在这种方式下系统和配置的用户会建立“隐性的契约”通过遵从一定的规则比如命名规则达到自动应用配置的目的。
当时我们举了一个 Spring 的 ControllerClassNameHandlerMapping 的例子来说明,现在我们来举另外一个 Grails 的例子。这里使用 [Grails](https://grails.org/) 举例,是因为 Grails 是我接触过的应用 CoC 原则应用得最好的 Web 应用框架了,使用它搭建起一个 Web 应用极其简洁。如果你使用过 Spring Boot 并对它印象还不错的话,你可以尝试这个将快速和简洁履行得更为彻底的 Grails。
```
class BooksController {
def index() { ... }
}
```
你看,如此简单的控制器定义,就可以自动把路径为 /books 的请求映射到 index 方法上,通过规约将 BooksController 映射到 /books 上,而 index 又代表了默认的 GET 方法的访问。
## 配置模板
对于某些复杂或灵活的软件系统来说,配置会变成实际上的 DSLDomain Specific Language复杂程度可以不亚于一门编程语言写的代码。于是有一种常见的帮助使用者理解和修改配置的方法就出现了它就是创建配置模板写好足量的配置默认值和配置说明这样使用者就可以复制一份模板并在其之上按需修改比如 Nginx 的配置模板。
Nginx 是一个高性能的反向代理服务器,反向代理可以挡在 Web 服务器前面响应用户的请求,根据不同的规则来处理请求,比如认证、限流、映射到其它路径,访问特定的服务器等等,将这些复杂的访问逻辑和服务器节点都隐藏起来,给用户提供一个单一的 IP 地址。
下面我们来动动手,亲自配置一下 Nginx 并让它工作起来。
在 Mac 上你可以使用 brew 来安装 Nginx或者去官网上[找一个适合的版本](http://nginx.org/en/download.html)下载安装:
```
brew install nginx
```
在安装成功以后,你应该能看到命令行输出了默认 Nginx 配置文件的位置,例如 /usr/local/etc/nginx/nginx.conf。现在打开看一下你会发现它本质上就是一个配置模板有些配置选项为了便于程序员理解和使用使用注释包围起来了。在同一路径下还有一个 nginx.conf.default 作为备份和参考。
还记得在我们在 [\[第 12 讲\]](https://time.geekbang.org/column/article/143909) 中自己动手配置的过滤器吗?确认一下它还能正确运行:
```
catalina run
```
接着访问 [http://localhost:8080/books?category=art](http://localhost:8080/books?category=art),你应该能看到类似如下的输出:
```
Category name: art, date: 2019-10-4 Count: 1
```
好,如果能正确运行,我们继续往下;否则,请回头看看那一讲是怎样把这一个过滤器配置起来的。现在我们根据前面提示的 Nginx 配置文件的路径来稍加修改,比如,把 http 部分的 #access\_log 开始的一行修改为类似如下路径:
```
access_log /logs/nginx_access.log;
```
接着打开一个新命令行窗口运行:
```
tail -f /logs/nginx_access.log
```
这样就可以监视 Nginx 的请求访问日志了。
继续修改配置文件,在接下去的 server 部分,将开头部分的 listern、server\_name 修改为如下配置,表示同时捕获访问 localhost 的 80 端口和 9000 端口的请求:
```
listen 80;
listen 9000;
server_name localhost;
```
紧接着,在它下方增加路径映射的配置:
```
location ~ /books.* {
proxy_pass http://localhost:8080;
}
```
这表示如果 URI 是以 /books 开头,就映射到 8080 的端口上面去。
好了,如果是第一遍启动 Nginx你可以直接执行 sudo nginx如果已经启动但是修改了配置文件你可以重新加载 sudo nginx -s reload。
现在,去访问如下两个链接,你都应该看到前面见到的那个熟悉的页面:
```
http://localhost/books?category=art
http://localhost:9000/books?category=art
```
这证明端口映射成功了,并且,切换回访问日志的那个窗口,你应该可以看到类似这样的访问日志:
```
127.0.0.1 - - [04/Oct/2019:20:36:43 -0700] "GET /books?category=art HTTP/1.1" 200 46 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36"
```
## 总结思考
今天我们介绍了 Ops 中配置管理的一些常见的方式,以及一些配置文件常见的组织形式。内容本身并不复杂,也没有介绍配置文件的格式(它在后面的文章中会有介绍),但是配置管理确实是程序员每天都在打交道的对象,自然全栈工程师也不例外,遵从好的实践可以养成良好的 Ops 习惯。
现在,提问时间又到了,我来提两个问题吧:
* 我们今天比较了常见的配置方式,其中一个是使用源代码中的常量,另一个是使用 Web 应用的运行参数,你觉得这两个各有什么优劣,各适合怎样的场景?
* 有程序员朋友认为,大型 Web 应用应该尽量少用代码层面的配置,而是把这些变化的部分放到独立的配置服务中,这样软件会比较灵活,修改简便,适合各式各样的场景。你觉得这个说法对吗?
好,今天的正文内容就到这里,下面是选修课堂。今天的选修课堂我想继续顺着 Ops 往下谈,来讲一讲程序员的“独立性”,而 Ops恰好就是独立性的一大标志。
## 选修课堂:程序员的“独立性”
几乎所有的软件工程师,都会写代码,都会做测试,但是做项目的“独立性”是大相径庭的,这也是容易让人忽略的部分,但却是一个“程序员”向“工程师”蜕变的标志。“独立性”在一定程度上决定了软件工程师的单兵作战能力,而对于全栈工程师来说,尤其如此。我们在技术上精进的同时,也需要提升独立完成项目的能力。而今天我们开始接触的 Ops恰恰是程序员独立性提升的一个重要部分。我认为软件工程的“独立性”可以分成这样几个阶段再次强调一下这并不是从编程能力上来分的
### 第一阶段:编码工作者
拿到详细的设计文档,上面连许多方法接口都定义好了。写一些实现,调用一些既定的 API然后根据设计文档来实现测试。这种情况很常见比如在某些外包公司就是如此。编码能力得到了一定的锻炼但这扼杀了大部分的创造力长此以往也许最终只能成为一个熟练工。我相信短期内这是一个可行的选择但是从程序员成长的角度来看这个阶段是一定要迈过去的。
### 第二阶段:需求的独立实现者
拿到了粗略的设计文档需求和业务也已经大致描述清楚接下去要做的就是发挥聪明才智把软件设计好把代码写好通过测试。在具备简单沟通的基础上这项工作可以在安静和独立的环境中完成因为项目经理、架构师和产品经理已经把那些复杂的技术或业务难题搞定了。这样的环境下可以诞生许许多多代码设计优秀、实现逻辑清晰简洁的程序员但是这始终只是在做一个“残缺”的项目而已。在大厂无论国内外很多程序员新手都是从这个阶段开始的。那从这个阶段开始Ops 的工作就显山露水了,每天大量的安装、配置、部署工作,无论是在开发还是测试环境。
### 第三阶段:项目沟通者和管控者
程序员要和产品经理,甚至客户澄清需求;需要自行分析可行性,明确项目中的技术和业务难点;参与决定和管理迭代周期和计划表;组织和参与项目组内运作跟踪会议。编码以外的事情会占用相当多的时间,而且这些时间往往会用在各种沟通上。到了这个阶段的程序员,通常已经成为了团队中的顶梁柱。
### 第四阶段:从做项目到做产品
从做项目到做产品区别是什么做项目只需要做好一次或者很少的几次交付就可以了而产品则是要倾注心血于它的整个生命周期。做项目需要更多倾听用户需求但是做产品更注重思考思考用户的痛点和产品的定位远重于倾听用户表述要把更多的精力花在产品定义、设计思考怎样把技术、业务落地到产品实现上。在发布以后如果幸运的话产品会有一个漫长的迭代和维护周期Ops 工作也很可能成为你的重心,你会把主要的时间都投入到这里。
### 第五阶段:产品成长的见证人
也许很少人能够参与从零开始,经过创意、市场分析到产品设计的过程。在明确要做什么之前,程序员有大量的时间会花在产品探索性的工作上面。也许会做很多的产品原型,也许某些版本和功能在 A/B 测试之后就被放弃了,更有些产品在流传开来以前就销声匿迹了,或者很快就死在了抄袭和山寨手里。产品的更迭和换代总是千辛万苦,而看得到的部分往往如此简单,但是谁又知道它的历史有多曲折呢?
好,到这里,我想问一下,正在阅读的你,处于程序员“独立性”的哪个阶段呢?
## 扩展阅读
* 当 Operation 和 Development 相遇DevOps 就是它们碰撞产生的火花。你可能已经听过 DevOps 无数次了,但是,如果你并不是很清楚它意味着什么的话,那么我推荐你阅读[什么是 DevOps](https://aws.amazon.com/cn/devops/what-is-devops/?nc1=h_ls)以及 [DevOps: Breaking the Development-Operations barrier](https://www.atlassian.com/devops) 这两篇文章。
* 文中简单介绍了反向代理,而它是几乎每个基于 Web 的全栈工程师都会接触的,维基百科的[页面](https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86)介绍了它常见的功能。
* 关于 Nginx对于它配置的具体含义如果想了解的话除了[官方的文档例子](https://www.nginx.com/resources/wiki/start/topics/examples/full/)以外,[nginxconfig.io](https://nginxconfig.io/) 这个网站可以通过简单的配置,直观、清晰地生成和比较相应的配置文件。
* 有一篇 [Top 5 configuration management tools](https://opensource.com/article/18/12/configuration-management-tools) 文章介绍了 5 种常见的配置管理工具,推荐阅读,中文译文在[这里](https://linux.cn/article-10497-1.html)。