工欲善其事,必先利其器,我们以 CSDN 技能树的分层构架和工具链构建为例子,展示投资工具链在软件开发中的好处。流畅的工具链,是软件开发团队效能的关键之一。
CSDN技能树是一个在线的领域技术学习社区,我们以最新发布的“网络技能树”为例,先简单介绍下它的核心功能。
技能树首页左侧包含了技能树的章/节结构,中间展示了用户在每个章节的学习进度,右侧则是技能树的用户答题榜单和技能树贡献者榜单。我们点开“路由表的工作原理”一节,可以看到每个节是一个“节社区”,每个“节社区”由“参考资料”、“练习题”、“交流讨论”、“我的笔记”四个频道构成。
练习题视图和答题页面如下:
为了说明工程构建上的设计,需要先对CSDN技能树的构架有一个基础的了解,技能树工程开发在构架上分为3层来解除耦合:
其中,技能树数据层服务大体上包含一组核心API:
API
API
API
其中,技能树用户业务层服务提供:
上述是技能树社区的大致构架分层,我们不打算把每层在工程上如何做“编译/构建”都展开讲,我们会屏蔽知识,重点放在“技能树数据层”来重点展示工程上如何做好“编译/构建”。
其中,“技能树数据层”又分为两大部分:
我们分别讲解这两部分如何做好“编译/构建”工作
。
正如前面提到的,技能树数据服务按顺序,分别在如下几个维度来建立规范化的编译/构建
项目采用git做源代码版本管理,项目的源代码包含三个分支:
dev
test
pro
通常来说,test
分支代码从 dev
分支单线合并过来,然后去测试服务器上测试,测试通过后才能合并到pro
分支,合并路线只能是:dev
->test
->pro
.
项目的环境配置采用阿波罗配置中心服务:https://gitcode.net/mirrors/ctripcorp/apollo/-/blob/master/docs/zh/README.md
项目为不同环境配置了3套不同的配置
不同环境配置,主要配置了不同环境下依赖的数据库、缓存、仓库…等各种账号信息。以及一些不同环境下服务功能的开关配置等。配置会做一些访问限制,杜绝不同环境的配置被错误的使用,例如:
项目编译和构建之后,最终是要运行的。在理解项目的其他配置之前,首先需要理解项目运行的服务器配置。
技能树服务的服务器包含了三类:
BUG
。技能树数据服务使用一台低配的云主机来做测试环境服务器来规避这个问题,j尽早发现问题。test
分支代码,只允许使用测试环境配置。pro
分支代码,只允许使用正式环境配置。微服务代码的组织方式,可能每个服务都是一个独立的project
,每个project
都需要独立的git
仓库,每个服务都需要独立的编译/构建/部署。这种碎片式的方式会带来开发和源代码管理上的碎片化。
我们做微服务管理的方式和通常的做法有一些差异,但是是一种便利的方式。我们在一个git
仓库里管理好整个系统的微服务。
代码上的组织是这样的:
这样的方式,我们可以灵活组合满足各种不同需求的微服务。通过仔细设计的命令行参数,可以精确地启动目标微服务。
这种方式的优点是:
config
+options
根据上面的讨论,环境上有3个维度:
dev/test/pro
)dev/test/pro
)dev/test/pro
)它们构成了如下的组合:
config | 代码分支 | 环境配置 | 服务器 | 允许运行 |
---|---|---|---|---|
dev | dev | dev | dev | yes |
test | test | test | test | yes |
pro | pro | pro | pro | yes |
我们让程序都是通过命令行启动的。这样通过指定命令行参数--config
来指定:
--config dev
: 此时启动的时候会使用 dev
环境配置,只能在开发机器使用,严格来说也要检查代码的分支是否是dev
,但是有时候我们会需要在开发环境跑下test
和pro
分支的代码是否正常。--config test
: 此时启动的时候会使用test
环境配置,只能在测试服务器使用,同时检查代码的分支是否是test
。--config pro
: 此时启动的时候会使用pro
环境的配置,只能在正式服务器使用,同时检查代码的分支是否是pro
。命令行—config
指定的环境配置会被解析到一个叫confi
g的key-value
字典。而命令行的其他参数会被解析到一个叫 options
的key-value字典里。这样,程序启动后有两个重要的配置信息,构成了程序运行的控制上下文:
config
options
根据上面的讨论,微服务需要通过命令行来精细控制。这是怎么做到的呢?事实上,我们设计了一种有层次的命令行参数指定方式,直接通过例子说明,假设程序是 xxx
,下面的例子说明了给xxx
程序传递的命令和行为,以开发环境配置为例:
python main.py server.main --clusetr dev
python main.py server.skill_tree.main --clusetr dev
python main.py server.skill_tree.match --clusetr dev
可以看到我们通过指定 aaa.bbb.ccc.ddd
这样的格式,可以让命令行程序精细指定启动哪一个粒度的服务。我们把这个叫做“使用.分隔的命令行路由”
实际上,有了命令行路由,我们顺便解决了很多场景的控制问题:
python main.py test.skill_tree.basic.test_get_tree_name
python main.py test.skill_tree.view.test_get_tree_name
python main.py test.tag.test_classfier
test_xxx(config, options)
签名风格的测试函数即可。always_run
的定时服务,定时调用api
单元测试程序并根据情况通过机器人发送警告信息。—config
即可控制不同环境的数据管理。python main.py dataset.minist.build
python main.py dataset.minist.upload
python main.py dataset.minist.download
python main.py model.tag.train
python main.py model.tag.download
python main.py model.tag.upload
每个技术栈都有对应的管理软件。例如 python
的包管理器是 pip
,就是在项目根目录下配置一个requirements.txt
的配置文件,里面指定好各种依赖的包版本,然后使用pip install -r requirements.txt
来在不同环境安装依赖包即可。
但是这里有一个痛点问题:python
的依赖包解析问题。例如:
A
包依赖B
包,B
包内部依赖C
包D
包依赖C
包此时,A
间接依赖的C
包,和D
依赖的C
包的版本要求可能不同,因此在包配置多了之后,可能出现包管理软件无法正确解析包版本依赖的问题。有时候这是隐性的问题:在你的开发机器上由于某种巧合,包配置并没有在你的机器上产生包依赖冲突。但是上线的时候,发现服务器上总是不能正确解析包依赖。
因此,项目要需要针对不同的技术栈,使用对应的包冲突解决方式。一种典型的方式就是“预先检查”。例如,python
提供了一个叫做“pip-compile”
的工具。你可以配置一个包依赖输入文件requirements.in
,通过pip-compile requirements.in
来预先检查,工具会告诉你哪些包版本冲突了,详细告诉你冲突来自哪里。于是开发者可以通过精细调整顺序和版本,来解决冲突。完整解决冲突后,工具会生成一个安全的requirements.txt
文件。这样就避免了你在线上环境需要解决包依赖问题。这也是一个“编译问题”:确保通过编译来提前解决线上环境的冲突问题。
当然,如果使用了docker
,这些问题也就在虚拟化层就解决了。但是虚拟化有性能损耗,并非所有开发目前都适合。有时候 keep it simple
是最合适的。
技能树除了内部的服务,还有另外一个重要的部分是,由社区贡献者共同构建每个领域的技能树。我们是怎么做到外部贡献者贡献的数据和内部服务之间的无缝集成呢?
首先,我们建立了一个“技能森林”的索引仓库:
https://gitcode.net/csdn/skill_tree
但是每个具体领域的技能树独立一个编辑仓库,例如这个网络技能树的开放构建仓库:
https://gitcode.net/csdn/skill_tree_network
通过每个领域技能树独立一个编辑仓库的方式,解除了不同技能树设计编辑的解耦。外部用户会通过在VSCode
里,用约定的格式编辑技能树的章/节目录结构,并且在节目录下按模版添加选择题习题markdown
文件
社区贡献者,在对应的领域技能树编辑仓库里,可以放心的提交,因为仓库的数据没有被使用之前是静态的数据:只是由git
仓库有版本地管理。这是技能树在社区用户“构建”这个步骤提供的解决方案:使用一套经过设计的模版,可以在VSCode
里便利地构建,安全地提交。
每个技能树编辑仓库下,提供了一个python
构建脚本 main.py
. 这个脚本的作用是:
config.json
文件,同时为每个config.json
生成一个node_id
。markdown
文件xxx.md
生成一个同名的xxx.json
文件,同时生成习题的id: exercises_id
这么做有什么好处呢?大部分情况下,生成的id
是不会被编辑者改动的。因此用户重命名文件和文件夹、移动文件和文件夹,文件id
都是存在的。我们的工具链就可以根据这些id在内部做匹配,从而做到文件文件夹的位置变动跟踪。这些跟踪就可以进一步在安全的管道里,通过内部的技能树服务API
,同步到数据库。
如果在核心章节习题都提交的差不多的时候。技能树的结构数据(章/节、习题)就会被技能树管道工具来处理。技能树管道工具目前是一个内部仓库,会在内部提供一组操作命令:
python main.py skill_tree.debug.network.import --cluster dev
python main.py skill_tree.debug.network.section.init --cluster dev
python main.py skill_tree.debug.network.section.post --cluster dev
python main.py skill_tree.debug.network.sync --cluster dev
python main.py skill_tree.debug.network.mount --cluster dev
python main.py skill_tree.debug.network.deploy --cluster dev
其中命令路由的第二个子路由debug
表示,这是在测试环境调试技能树。如果测试环境数据都没有问题,就可以对生产环境重复上述过程。例如:
python main.py skill_tree.release.network.import --cluster pro
注意,管道工具的 --cluster
依然是用来控制管道工具的环境配置依赖,里面配置了每个不同技能树相关的账号依赖等重要信息。有了 cluster
参数,为什么还需要在命令路由里有 debug
/release
区分呢?这个是为了
dev
/test
环境合并看成一个debug
测试维度。pro
环境看成是 release
环境。构建中的重要的元数据,都会在管道工具仓库里保存并提交到git
。便于后续的更新操作对数据的校验、去重。
可以看到每个子命令内部,都会有相关的访问技能树数据服务api的操作。管道工具的每个子命令对各种数据格式转换、资源创建等做了细致的错误处理。其基本原则是遇错就立刻退出,而不是隐藏BUG
。
如果在开发/测试环境,管道工具将社区贡献者一起贡献的技能树数据成功的转换、构建、并成功的在测试环境预览界面上完成验证。那么就可以去预发环境上再测试下,最后在正式环境上完成构建/编译和部署。
到目前为止,我们并没有提到“自动化”。一方面我们在项目实践中是根据需求按需演化,不同阶段需要解决不同的问题。全流程的自动化编译/构建/部署/上线并非是一个急需问题,我们目前的方式在实践中:
工程上的两个重要的部分流程和工具,我们对源码、仓库、服务、数据、模型的组织是反碎片化的,工具是半自动化的。有着良好的命令行设计,自动化只是在自动化系统里把这些命令集成进去而已。
本节我们对项目中的工具链设计做了分析。实际上,工具链的设计体现了我们对项目的分层构架的理解,对程序、数据、环境、树状结构API
的理解。工具解决工程中繁杂的碎片问题,从而软件开发团队可以规避大量的碎片问题,流畅地迭代。