随着系统功能越来越多,系统的配置也越来越多,配置管理成为了一个重要的问题。做过线上运维的同学们一定对配置的复杂性有深刻体会,多少次加班都是因为一个配置不对而导致系统无法正常工作!配置问题由于难以建立有效的自动化测试而难以检测,常常使得我们不得不花费数小时甚至数天来调试才能找到配置上的问题。
对于分布式计算,这个问题变得更加突出了,熟悉分布式大数据处理的同学们对于分布式任务的复杂配置一定深有感触。分布式系统本身的复杂性常常使得单个组件的配置就有上百个。而在微服务架构流行的当下,我们的系统越来越多以分布式的形式出现,系统的配置管理问题也越来越突出。
本文尝试分享一下我们在构建机器学习平台时对于配置管理方面的设计实践。
模块自有配置导致的混乱
结合上文中关于平台架构的分析,我们看到系统拆分了一个名为connector
的通用模块,负责实现和外部系统进行对接。这一模块会同时支持Rest API
及ml
两个应用层模块。
由于Rest API
及ml
两个模块会连接同一个大数据集群进行数据操作,所以它们的不少配置是一样的。比如某一租户运行任务的缓存路径,读写的hbase
的数据库名字等。一个简单的想法就是将这些与connector
相关的共享配置放到connector
模块中去,然后将无法共享的配置放到Rest API
及ml
各自的模块中去。
在刚开始时,我们确实是这样做的。但是随着系统功能慢慢的变得复杂,配置项越来越多,这样简单粗暴的配置设计越来越难以支撑系统的快速演进。主要问题表现在:1. 开发人员在新增配置的时候,对于配置放哪里(至少有3个地方)一直存在争议;2. 为了支持多个环境并避免配置重复,不得不自己实现类似Spring profile
的配置加载机制;3. 某一个功能用到的配置可能分散到多个地方,出现问题的时候,需要从多个地方去识别配置问题;4. 由于配置会打包到jar
包中,我们不得不为每个环境生成不同的包。
统一的配置管理设计
那么,在一个多模块的复杂系统中我们要怎么做才能有效的管理配置呢?
分析上述提到的痛点可以发现,对于一个好的配置管理设计,一个重要的点是配置要能集中进行管理。
在我们的系统中配置应该集中到哪里呢?稍加分析就能知道,配置当然要集中在某个最外层的应用层模块进行,因为各个服务实例的组装就是在这里完成的。在上面的这个场景中,我们有两个应用层的模块,即Rest API
及ml
。实际上ml
模块并不是可以独立运行的模块,它的内部是实现了特征处理Pipeline
和各种机器学习算法,以spark
应用的形式提供出来。这些分布式应用的调度却来自Rest API
,也就是Rest API
是一个驱动器,是更偏应用层的一个模块。对于ml
模块中需要的配置,我们就可以在调度其运行时通过参数的方式传入。于是,我们的配置就应该集中到Rest API
模块中。
改造之后的配置如下图所示:
恰好Rest API
模块是基于Spring Boot
构建的,这样一来,就可以完全借助于Spring
为我们提供的配置管理工具来实现配置管理了。
实现了这一步骤之后,整个配置设计就变得简单易懂,上面提到的配置管理问题也被有效的解决了。除了Rest API
模块会根据不同的环境构建不同的包,其他的包全部都是可以按版本独立发布且可在各个环境复用的。至于Rest API
模块,其实我们也可以很容易实现一个包支持多个环境,只需要我们将配置独立出来作为一个外部文件来管理即可。
为了实现这一架构调整,我们在原有系统上面进行了较大的重构。在重构过程中,有几点设计上面的经验是比较有价值的:
- 从依赖倒置的设计思想来看,作为一个通用的模块,其配置设计应该以一个接口的形式进行定义,然后在模块内部所有需要使用此配置的地方引用该接口
- 对模块内某一个具体类的配置设计,应该考虑最小信息原则,仅支持类自身需要知道的几个配置,而尽量不要直接引用一个大而全的配置对象
- 在模块内部,可以考虑提供一个配置接口的实现,以便提供一些合理的默认配置
- 在配置设计上需要尽量保持配置项的独立,避免设计一些本可以通过原子配置计算得到的配置,这样可以减少配置数量
微服务中的配置
除了要考虑模块中的配置设计,另一个要考虑的配置问题就是各个微服务中的配置设计。
由于微服务需要独立的为外部系统提供服务,我们无法将其当做一个模块来对待,所以配置是不得不存在的。
Spring Cloud Config
为我们提供了一个中心化管理微服务配置的思路,它考虑了支持配置文件的集中加载、版本管理(可以通过g