前言
本文写就于 2022 年,是本人在 CMDB 领域的钻研探索实践之后的一些经验分享,全文约计 1.7w 字,凝聚了我全部的心血,从平台架构的设计,到项目周期的管理,甚至项目完成之后的推广,都有所涉猎,如果你也在折腾 CMDB,那么本文是你不可错过的入门必读之作。
我们的运维团队汇聚了一群优秀的,富有想法的年轻人。每次周会里边,当我们完成了各自日常工作的交流沟通之后,总是会跳出当下,天南海北,胡言乱语地头脑风暴一番。
在这些看似 闲笔 的沟通当中,我们进行了大量的运维规范以及最佳实践的讨论,而当我们进行地讨论越多,越能够发现一个问题,无论是对于一个全流程(指一个项目的整个生命周期)命名的规范约束落地,还是对发布平台以应用维度的主机列表维护,都步步绕不开一个能够真正解决运维痛点的 CMDB 平台。
注意,这里在 CMDB 这个名词的前边加了一个定语,真正解决运维痛点。
事实上在这之前,我们并非没有做过 CMDB 平台的建设,只不过由于我们以往对这一概念的认知不足,完全交由运维开发同学,按他的一些经验想法开发出来的 CMDB,却总是难以真正在实际运维生产当中落地,简而言之就是,CMDB 平台做过,但都失败了。
我想,失败的原因大概有如下三点:
何不食肉糜 的感觉,当然这里没有丝毫揶揄开发的意思,真正好的平台其实还是需要开发来把想法实现,只是从客观事实呈现的效果来看,的确会走向这样一个窘境。何不食肉糜 的感觉。人言说鞋子合不合适只有脚知道,如果一个平台的最终使用者从来没有参与过平台的建设,那么最终这个平台也的确很难做到令其满意。实际上,在公司里边造的轮子,通常很难会被判定为失败,这里边包含很多现实的原因,公司能不能够勇敢地站出来知错认错改错,通常取决于领导人的魄力。很多时候阻碍我们成长发展的,正是那些被太多本不应掺杂其中的表面因素给影响的,可能我们司空见惯了实事求是这个词儿,从而觉得这个词儿的落地也是顺理成章的事情,但,我们的确经常会因为没能做到实事求是,以至于鞋子不合脚也会说服自己让脚去适应鞋子,而忘记了穿一双鞋子的本质是为了让脚舒服。
没错,有时候阻碍我们成功的唯一因素,可能就是实事求是。当然这里并不是哲学思想研讨会,我不会对这一理论做过多深入分析,简单一提算是抛砖引玉,让我们在意识中埋下一条行事准则,接下来还是回到 CMDB 平台建设的分享上来。
对于建设 CMDB 平台而言,实事求是的指导意义就在于,我们能够认真审视上边提到的失败的每一个原因,而我也会在接下来的内容当中,时不时地回来过剖析,实际推进设计,开发平台的过程与以上问题的碰撞。
这里我要感谢我的领导,能够勇于认识到以往平台建设的问题,并勇于启用我作为新阶段平台建设的主负责。
之所以说领导启用我是一种勇敢,是因为我在这之前并没有任何 CMDB 平台建设的经验心得,这次受命,则完全是从零基础开始。
因此这里也提前与大家露个怯,我并不算是 CMDB 领域的专家,仅仅是经历过一个平台建设,收获了一些心得,不揣愚昧与大家做一个交流分享,每家公司的运维架构以及实际需求场景都各不相同,这里仅是针对一些比较有共性的底层设计,以及平台建设过程与实际落地应用之间的问题,做一些分享。
当我接到这个任务之后,首先去了解了一下 CMDB 的概念,CMDB(Configuration Management Database)即配置管理数据库,或者叫资产管理系统。网上还有从这个概念发散出来的一大堆名词概念,一番查阅之后,我如坠五里雾之中,变得更加迷茫与模糊,好在之前一直有关注行业内一些前沿动态,多少了解一些开源的 CMDB 平台,当我把这些平台部署起来,真切体验之后,才慢慢形成了一些个人对 CMDB 平台的理解和方向上的把控。
在这里要首先感谢这些开源项目,正是他们将自己在这个领域的超前认知以及凝聚心血的智慧开源出来,才使得我们能够站在巨人的肩膀上思索当下的问题。
我们的 CMDB 平台建设,底层设计思路很多地方参考如上两个平台,但也并非只是简单地拾人牙慧,我在一开始部署体验这些平台,阅读了其中全部文档之后,又结合了自己业务中的实际场景和问题,做了重新的梳理设计,这些内容在后边都会一一呈现。
最后表明一下我个人心中对 CMDB 的理解:
能够做好这两点,我想,这个平台距离真正解决运维痛点,也就不远了。
本节内容也将会围绕这些问题,平铺展开。
事实上我在组织部门成员第一次碰面会之前,就遇到过其他开发同学问过这样的问题:CMDB 好干啥?
我当时用运维实际日常工作的一个小场景对此做了解释,大家也可以看看自己在日常运维工作中有没有这样的痛点。
我当时这样回答:举个例子,你有没有遇到过这样一个问题,你知道客户端服务所在主机,现在的需求是要在服务端入口对客户端请求加白名单,但是客户端主机走的是统一的 NAT 出口,这个操作的流程会是什么样呢?
事实上运维同学在日常工作中,需要面对的类似问题数不胜数,很多问题都是这种很繁琐的重复性工作,CMDB 平台就应该在提供资源对象的基础属性之外,还要考虑资源之间的关系,这两点,都需要通过抽象出来的模型进行维护,后边会详述。
一个平台的建设, 设计是第一步,开发才是第二步,尽管项目启动之后,参与其中的开发人员已经开始催促你拿出方案,但我劝你,别着急,你需要冷静地思考,耐心地梳理,把你想要的东西和样子想清楚,并把你该做的准备工作做充分,否则你一定会被后续开发过程中遇到的诸多问题干扰,从而在大平台的小细节中迷失。这是作为项目 PO 必须在一开始就要注意的。
为了便于大家快速领会整个平台设计的方向,这里先将我们落地下来的页面左侧导航陈列与此:
这个导航将会作为后续内容展开的参照。
现在很多 CMDB 平台的最佳实践,至少都同步到了这样一个共识:需将资源进行抽象,通过模型表达资源的属性及关系。
乍一听可能会有一些懵,这里我可以从个人所经历的资产管理的几个阶段来做一个说明。
最早的时候,我与小伙伴所管理的服务器资源(其他资源则大多疏于登记管控),都是基于 Excel 来进行管理的,彼时表格里边大概会这样来对服务器资源进行登记:

表格维护,缺点多多,相信经历过的同学必有同感,维护麻烦与数据准确性是表格维护的最大问题。那时候我与几个运维小伙伴将这些 Excel 维护在了 SVN 当中,每个人更新了表格内容之后,就通过小乌龟将内容同步上去,想来这应该算是远古时期的手工时代了,现在公有云时代的运维,恐怕没有几个人会甘心做这样的手工活了。
当时有不少服务器是在自建机房的 kvm 中运行,因此就先以单台宿主机服务器作为大分类,然后逐个记录运行在服务器上的虚拟机,后边的则是一些相对重要的虚拟机属性信息。如果想要获取虚拟机更多的信息,就只能登陆到主机上凭经验查找确认了。
再往后会有一些运维同学自学了开发,或者是部门有专门的运维开发同学,也做过 CMDB 平台的建设,只不过,大多是服务器资源数据在云厂商那里平搬到本地数据库,然后在 web 页面提供了一个可供搜索的框而已,看似跨出了很大一步,实则对实际很多应用场景,都是没有太多帮助的。
这算是半自动时代,资源的字段属性在数据库,乃至在前后端代码内,都是固定了的,如果你想要新增一个你关心的字段,则需要牵动前后端开发者,乃至 DBA 同学,一同来做一波变更,从而满足这样一个需求。还有一个问题是,这种单资源对象的管理,通常都没有把不同资源之间的关系维护起来,从而很难在实际运维工作中解决掉运维的痛点。
上边一些描述中,需要大家注意一些关键词,这些关键词也组成了新平台的纲领。
模型是对 CMDB 平台所有资源的抽象,所有资源都应该基于模型来构筑,模型的核心功能在于:定义资源对象的基础属性,配置资源对象的关联关系。
最终,资源仓库对资源进行展示的时候将会依据模型中资源模型定义的字段以及关系进行呈现。
模型设计是 CMDB 平台建设的重要环节,如果这一个环节工作做好了,那么后续的开发以及使用都将会变得顺遂起来。而这个建设过程如果没有比较好的方法论支撑,又是很容易陷入焦灼之中的。
模型的产生过程,大概分如下几个步骤:
因为我们的所有业务全部运行在公有云之上,于是我就登录上云平台,将常见资源一一巡查了一波,首先整理出了一张模型草图:

当你读这篇文章,打算建设一个 CMDB 平台,那么梳理出来这样一张图是必不可少的一个步骤,它不仅清晰地展现了 CMDB 平台你想要的内容和样子,最重要的是为后续平台推进奠定了重要的基础。
我来简单说明下这张图,首先图里呈现了我们前边提到过的,CMDB 平台的使命,即将右上角云上的资源与左边我们本地的资源数据,通过关系(下边会详细介绍关系的设计)给串联起来了。有了这样的关系链之后,我们就能够在平台上一目了然地看到一个应用,它的某个环境所涉及到的 GitLab 仓库地址,相关的开发测试运维人员,以及其他中间件,云上各种资源。
一个平台能够做到这一步,其实就已经非常厉害了,通常互联网公司中人员流动都比较大,新人入职花费精力最大的莫过于要去了解每个应用以及它背后隐藏着的一系列相关资源,当我们的平台能够弥补这块儿的空白,就实现了很大的一个价值。
另外,图中还提到过的其他扩展能力,主要需要考虑与其他平台打通的问题,以及成本核算方面的建设,当我们基础建设完成之后,会再添加任务核算出每个应用的花费,然后再结合上该应用的监控数据进行比对,从而在平台上自动提供给我们一个应用成本与产出之间的比例,以便运维同学对其进行评估。
当你梳理完资源大纲之后,就可以梳理每个资源的具体字段以及关系了,建议这里使用表格进行统一梳理与维护,这也是从赵班长一次直播分享当中学习到的经验。
Excel 样式大概如下:

通常一个资源分三组进行维护:
因为云上资源的字段都是已经固定下来的,因此只需要在资源详情页面对字段进行整理登记即可,这块儿基本上也不需要过多讨论,顶多对一些资源字段的取舍上做一个讨论,因为有一些云上资源的字段对我们来说意义并不大。
如果是本地新增的模型,那么字段有哪些,如何定,则都是需要大家好好讨论的,这块儿我在后边讲应用视图设计的时候会单独介绍应用的字段。
项目一开始,在技术评审会议上,我们内部对于 CMDB 平台底层存储选型问题也做过专门的讨论。
讨论的核心在于:选择 MySQL 还是 MongoDB。
通常运维平台都是以 MySQL 作为后端存储的,大家用起来也会更加轻车熟路一些。之所以这里要引入 MongoDB,是因为 CMDB 平台的一些特殊性,也正是基于这些特殊性,促使我们在讨论之后,决定选型存储为 MongoDB。
第一个要点:动态字段,我们给资源进行建模,然后在平台上来维护这个资源的字段,因此每个资源模型都是在平台新建的,模型的字段也是新建的,这就要求这些内容落到库中也是动态灵活的。以往使用 MySQL 的思路可能一种资源就是一张表,字段也是一开始就约定好无法随意更改的,这其实是无法满足当前这种需求的。当然,我们也可以在 MySQL 中存一个 json,但是使用起来其实会非常别扭,实际开发过程中也会遇到不少现实的难题。MongoDB 的存储格式为 BSON(一种 json 的扩展),刚好契合这种需求场景,这也是选型 MongoDB 的最重要原因。
第二个要点:全文搜索,前边说过,CMDB 平台使用场景中,有占八成都是查询方面的,通常我们会拿到一个 IP,或者一个关键词,你还不能确定这个信息归属于哪个资源类别,没办法直接去搜索,因此全文搜索也是一个必须要支持的功能。如果选型 MySQL,那么就需要再把数据同步到 ES 或者 MongoDB 来实现,既然 MongoDB 天生支持这种全文模糊搜索,那么为什么不直接选型 MongoDB 呢,是的,就它了!
其实这个本来不值得多说,语言只是工具,用任何语言栈来实现都是没有问题的,只是说明一下我们之前使用的技术栈,从而在后边对一些具体的知识点分享时大家能心里有数。
任何一个平台的建设,都是需要时间的,这一点对于我们项目负责者要提前意识到,更重要的是还需要让你的领导意识到。
所有一旦口渴马上就要喝茶,一旦饿了马上就要把饭吃到嘴里,一旦提出建设平台的想法,只给了一周两周就要看到成品,这些都是小孩子未曾开蒙的行为。行军打仗,首要考虑的就是粮草与天象,负责项目,亦要重视项目拆解与排期。
通常来说,要建设一个基本可用的 CMDB 平台,前后端的开发,以及运维同学的协同配置接入使用,至少需要三期规划,如果开发人员资源不很充足的话,那么最少也需要半年的时间来建设。
接下来我将详细介绍这几个阶段开发推进的流程与细节,大多地方先通过截图让大家有个整体认识,然后将功能细节用任务列表的形式进行解释与说明。
一个大的平台,必然是由无数个小的功能点堆砌而成的,没有人能够把每一个功能点的详情记在脑海中,我也一样,所以我在负责该项目的时候,当我与小伙伴们一起把功能点聊清楚之后,马上就会拆分成每一个功能细节,并用任务列表记录下来。再拆分成前后端开发的具体接口列表,分配给开发者落地到 mock 平台。然后再将这些内容与各位开发者二次确认,以免出现需求理解偏差的问题。在最后项目结期的时候,这个任务列表也会成为我验收项目的一个参考凭据,以免因为忘记某个细节而漏测某个功能点。
项目的开发,在每一期未动工之前,无论费多少口舌,开多少次会议,只要能够把需求对齐到开发者同学的心里,都是值得的。需求理解偏差这种问题,发现越早,提出越早,调整越早,就越好,这也是作为项目负责人,需要在项目推进过程中不断关注并跟进测试的,否则,等到最后做出来却发现货不对板,然后再进行返工,就会对项目推进的整体质量,以及开发者的心气儿都产生极大的影响。
所以,不要着急动工,一旦动工,就一定是想好了。
完成模型管理这一功能的落地,要求这一期结束之后,能够完成一个资源模型的增删改查,以及能够在资源仓库手动添加删除资源数据。
建议排期为两个月,这一期看似只是完成了模型管理和资源仓库两个内容,但实际上里边模型的动态字段,复杂的交互逻辑对于前后端的考验都是非常大的。
加之这是项目的开篇阶段,每个开发人员都还不熟悉里边的各种概念,需要一点时间来磨合。我们经历的过程也正是如此,后端开发者,前端开发者,在前期都曾不止一次被里边的概念绕晕,从而导致需求与实现错位,但当我们后边接触越来越多,每个陌生的概念慢慢都变成大家的常识,每个功能点大家也都能参与其中,一起思考,讨论,共建,这个时候才能真正进入快速开发的阶段。
样式示例:

功能细节:
样式示例:

功能细节:
字段管理是整个模型管理当中比较复杂的一块儿内容,当然,这个复杂是针对前后端开发者而言的,其实这种开发上的复杂,也正是为了达到使用上的简单。
点击添加字段之后的页面示例:

关于字段的类型,不同的平台可能提供的有不同的类别,像蓝鲸 CMDB 中模型的字段类型有如下几种(参考: https://bk.tencent.com/docs/document/6.0/152/6954):
我根据日常使用场景中经常遇到过的字段类型,将字段类型压缩成了 6 种,以减少运维人员的接入认知成本。分别为:
每一种类型都会有不同的规则约束,也就造成了每种类型的信息表单是不一样的,前端需要根据选择类型的不同,进行表单的动态变化。其中的规则约束基本上都能见名知意,因此不再一一解释。
这里将不同的字段类型表单示例做个分享。
字符串类型

数字类型

日期类型

下拉选项

密码类型

关系类型

关系是比较复杂的一个类型逻辑,不过前边我们已经有过铺垫,这里将所有的资源之间的关系拆分为从属和连接两种,下边再来拆分看两种关系的不同表单效果。
从属关系

连接关系

两种关系在这里可能看起来并不够直观,后边在介绍资源仓库的时候,我将通过实例进行详细的讲解。
功能细节:
资源仓库是所有资源落库之后对外呈现的一个窗口,也是将要开放给运维以及普通开发者的一个数据集合页面。
首页展示

功能细节:
为了说明功能点,我这里在其他分类中创建三个测试模型,以及一些测试数据,分别如下:
在模型管理中其他分类下,创建了一个 CMDB 测试 的模型分组。
以上是三个模型以及基本属性的字段信息。
因为关系属性相对抽象,所以这里单独表达:
此时我们需要在教师和学生两个模型的关系属性中分别创建一个从属类型的关系,从属模型为班级,如下图:

然后在教师与学生两个模型中任选其一创建一个连接类型的关系,之所以任选其一,是因为在模型管理的功能细节里已经说过,连接关系在某一个模型中创建之后,另一方也需要自动创建这条关系。如下图:

创建测试数据如下:
[
{
"_id": "6279d9e2d440eaf8ddac1255",
"class_identify": "3nian1ban",
"class_name": "3年1班",
"create_at": 1652152802,
"model_identify": "class",
"modify_at": 1652152802
},
{
"_id": "6279d9f4d440eaf8ddac1256",
"class_identify": "3nian2ban",
"class_name": "3年2班",
"create_at": 1652152820,
"model_identify": "class",
"modify_at": 1652152820
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"_id": "6279db9cd440eaf8ddac1257",
"create_at": 1652153244,
"model_identify": "teacher",
"modify_at": 1652153244,
"teacher_identify": "lilaoshi",
"teacher_name": "李老师",
"teacher_xueke": "语文"
},
{
"_id": "6279dbb2d440eaf8ddac1258",
"create_at": 1652153266,
"model_identify": "teacher",
"modify_at": 1652153266,
"teacher_identify": "wanglaoshi",
"teacher_name": "王老师",
"teacher_xueke": "数学"
},
{
"_id": "6279dbc9d440eaf8ddac1259",
"create_at": 1652153289,
"model_identify": "teacher",
"modify_at": 1652153289,
"teacher_identify": "zhanglaoshi",
"teacher_name": "张老师",
"teacher_xueke": "英语"
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[
{
"_id": "6279dc3dd440eaf8ddac125a",
"create_at": 1652153405,
"model_identify": "student",
"modify_at": 1652153405,
"student_identify": "jia",
"student_name": "甲",
"student_xuehao": 1
},
{
"_id": "6279dc47d440eaf8ddac125b",
"create_at": 1652153415,
"model_identify": "student",
"modify_at": 1652153415,
"student_identify": "yi",
"student_name": "乙",
"student_xuehao": 2
},
{
"_id": "6279dc52d440eaf8ddac125c",
"create_at": 1652153426,
"model_identify": "student",
"modify_at": 1652153426,
"student_identify": "bing",
"student_name": "丙",
"student_xuehao": 3
},
{
"_id": "6279dc5dd440eaf8ddac125d",
"create_at": 1652153437,
"model_identify": "student",
"modify_at": 1652153437,
"student_identify": "ding",
"student_name": "丁",
"student_xuehao": 4
},
{
"_id": "6279dcaad440eaf8ddac125e",
"create_at": 1652153514,
"model_identify": "student",
"modify_at": 1652153514,
"student_identify": "wu",
"student_name": "戊",
"student_xuehao": 5
},
{
"_id": "6279dcb6d440eaf8ddac125f",
"create_at": 1652153526,
"model_identify": "student",
"modify_at": 1652153526,
"student_identify": "ji",
"student_name": "己",
"student_xuehao": 6
},
{
"_id": "6279dcc3d440eaf8ddac1260",
"create_at": 1652153539,
"model_identify": "student",
"modify_at": 1652153539,
"student_identify": "geng",
"student_name": "庚",
"student_xuehao": 7
},
{
"_id": "6279dccdd440eaf8ddac1261",
"create_at": 1652153549,
"model_identify": "student",
"modify_at": 1652153549,
"student_identify": "xin",
"student_name": "辛",
"student_xuehao": 8
},
{
"_id": "6279dcd8d440eaf8ddac1262",
"create_at": 1652153560,
"model_identify": "student",
"modify_at": 1652153560,
"student_identify": "ren",
"student_name": "壬",
"student_xuehao": 9
},
{
"_id": "6279dce5d440eaf8ddac1263",
"create_at": 1652153573,
"model_identify": "student",
"modify_at": 1652153573,
"student_identify": "gui",
"student_name": "癸",
"student_xuehao": 10
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
然后将数据按照模型中设计的关系关联起来。
这个时候,我们就可以看到数据之间的一些联动关系了。
从资源仓库点击老师这个模型进入详情列表:

此时看一下李老师的信息:

然后看关系管理中的从属关系:

再看连接关系:

既然可以点击跳转,那我们点击一下学生甲,会在新窗口展示甲的基本信息:

以及同学甲的从属关系:

和甲的连接关系:

可以看到甲同学所关联的两位老师,最终效果正如我们前边设想的一样。
同理,通过一个内网服务器的 IP,找到它的出口 NAT,就是通过这样的方式来实现的。
其他教师与学生的关系大都类此,因此不再重复截图呈现。
完成云资源基于平台定时任务的落库工作,要求这一期结束之后,云资源能够自动落库,资源关系也要自动维护。
建议排期为两个月,这一阶段的前后端开发任务比例并不协调,大概是三七开的一个比例。有了第一期模型管理以及资源仓库之后,后端开发者就可以照着云资源的 SDK 编写同步逻辑了。这时就非常考验项目负责人,要把控好不同阶段的工作规划及分配,比如这期前端完成了定时任务的交互页面之后,可以提前着手第三期的标签管理应用视图的开发工作,因为第三期的工作内容,前后端任务比例又反过来了。
这些都是项目 PO 必须要在项目推进过程中考虑的。
上边数据仓库的示例中,数据的创建,数据之间关系的维护,全部都是通过手工操作来完成的,非常地繁琐。第二期首先要完成的就是定时任务的管理页面,从而让程序能够自动将云上资源自动获取并落地到本地。
样式示例:

添加任务,云上资源与其他资源一致,不同的是云上资源多了一个扫描可用区的条件选项。

功能细节:
当前后端协同完成定时任务接口与页面之后,后端同学就可以开始一一编写云上资源的同步任务了。这个操作以及编码的步骤,我整理如下:
申明
原创文章eryajf,未经授权,严禁转载,侵权必究!此乃文中随机水印,敬请读者谅解。
样式示例:

功能细节:
此处是一个单独的页面提供一个搜索框,输入英文或者数字,将会对数据表进行全局搜索。
添加标签管理的能力,完成应用视图。要求这一期结束之后,CMDB 平台进入一个基本可用的状态。
建议排期为两个月,这一阶段首先要完成的是覆盖全局的标签管理能力,然后再完成应用视图的页面,注意应用的环境管理将借助标签的能力进行二次归纳,这一点会在后边讲到。
我了解过市面上大部分开源的,商业收费的 CMDB 平台,几乎没有一家,能够真正将应用从环境的维度来管理,给做好的,这的确会是一个难题,后边我也会介绍我们在这个过程中的思考,以及为什么选择借助标签来实现环境的管理的。
标签也是 CMDB 平台一大重要的设计,许多时候,我们会依赖标签对一批或者某些数据进行标识与管理。
这里先简单说明下标签管理的核心逻辑:
标签管理分标签键和标签值两种,标签键是唯一的,标签值不必唯一,一个标签键可以关联多个标签值,一个标签值可以关联多个资源数据(这里就要求标签值是要与资源仓库的全局进行打通,从而能够提供给用户选择要关联的资源数据)。 这种设计逻辑完成之后,我们就可以通过标签的键值对,过滤搜索我们关心的数据了。
样式示例

功能细节:
为了说明功能点,这里再次创建一些测试数据:

点击查看可以进入到标签的详情:

点击左侧+号可以展开标签值关联的数据,我们展开一下 yuwen 看下:

功能细节:
绑定资源的页面视图如下:

功能细节:
同理,外边标签搜索页面与绑定资源页面类似:

功能细节
应用视图涉及到内部一些信息,因此这里就拿原型图来做展示及说明。
样式示例

功能细节
主机列表示例如下

这里主要是通过标签中的环境标识,来区分主机列表中的环境,从而过滤出应用对应不同环境的主机列表。
这一小节,我们聊聊,一个平台如何渐进式地走入运维组小伙伴们的内心,并夺走他们日常工作的主要关注的。
想要博得大家的关注,我想到了三点:自身硬,常吹风,强约束。
这里将一个资源对象在平台上的维护流程以及一些注意点完整罗列,以便于读者参考时可以方便地了解上手。
操作之前,首先清空所有的测试数据。
资产模型 分类下创建 公有云 的分组组织模型 分类下创建两个组如下
内部组织外部组织所创建的模型以及分组关系如下:
模型大分类 模型分组 模型 模型字段分组 模型字段目前先创建这些内容,能够保证云资源的交互可用。
注意: 创建模型的时候需要注意一个细节,模型的 identify 尽量与腾讯云资源的 identify 一致,以便于后续核对排查。
其他模型亦按照上边的姿势进行扩展配置同步。
标签是一个全局数据可关联过滤的系统,目前主要场景会在应用管理界面的环境区分中使用。
标签是一个独立的 kv 系统,一个标签由一个键多个值组成,键作为一种维度的表意(要求全局唯一),值作为该维度的参数,实际操作中将值与数据进行绑定。
通过在标签值管理页面,将值关联上所有的资源数据,从而在标签的维度,将所有的数据分成三种环境。
注意: 虽然一条数据能够绑定同一个标签键的不同标签值,但是这里作为环境区隔的时候,并不建议一条数据绑定给多个标签值。换言之,也就是一个资源实例,不能即属于测试环境,又属于其他环境,以做到资源的环境隔离。
在所有云上动作还未集成到平台的时候,可以借助云的一些能力,将数据落实到本地,比如购买云主机的页面,可让腾讯云调配,标签是必填项,那么一台主机在购买的时候,就能做好环境归属的标识工作了。后续集成到平台,则平台的购买流程,同样需要考虑将标签纳入到购买的标准流程中。
云上的资源我们通过定时任务来进行同步,但是,如果确保本地的数据,对比云上的数据,是准确的,这其实是一个值得思考的问题。
在与小伙伴们讨论分析之后,对于我们现阶段的平台而言,可能将同步任务分割成全量与增量两种比较合适。
在我们的工作场景中,主机的上下线操作是非常频繁的,如果同步任务不能考虑到这种操作毛刺,那么就很容易造成数据不准的问题。于是我们宁愿本地存留一些脏数据,由运维手动来二次确认,也不要简单粗暴地直接将数据删除。
吃透了本节内容的同学可能会发现一个细节,这个平台设计上,如果想要删除一个模型分组,其实是非常难的,我整理了一下,大概需要如下步骤:
想要删除一个模型模型分组,首先需要确保该分组下没有模型,然后确保模型下没有模型字段分组,然后确保模型字段分组下没有字段,然后确保模型字段没有数据了,然后才能将模型分组删除。
这么做的目的是:数据的添加或者移除,都要被平台约束,尤其是删除,平台强约束,是保障数据准确的第一个基石。模型分组到最后一条条数据,就是这样的上下关系,如果强行将模型分组删除,就好比直接把一个蜂群的蜂王给干掉了,会让最底层的一条条数据变得无根可依。
市面上大多数 CMDB 平台,对于应用管理的维度,很少有达到环境这一层的,仅见过一两个产品引入了这一概念,却设计不佳,使用麻烦,让人不明所以。
那么应用的环境到底是否重要呢,实际上是非常重要的,我们在制定一些其他规范的时候,环境是一个很重要的参考角度,比如我们制定日志规范,一个应用的所有日志都会在一个集群当中,环境标识就会拼接在应用名后边作为应用对应的日志索引在日志平台进行隔离。
那么,在 CMDB 平台上,应用的环境如何表示?
一开始我想的是,走模型关联的思路,因为上边在模型设计当中讲过,模型可以通过从属和连接两种关系表示资源之间的关系,于是,我们可以这样做:每个应用创建的时候都会再自动创建这个应用关联的三个环境模型,每个环境都会关联云主机等资源,那么就会存在一个问题,一个云主机的模型,最终会被上百个环境进行关联,这是无法接受的。
最后我们决定,走标签搭桥的思路来解决这个问题。
在模型之间,让应用模型直接与其他资源模型进行关联,换言之,资源会首先以应用维度进行区隔,比如 api 应用测试预发正式环境都是两台主机,那么 api 关联的主机资源就是 6 台主机。
然后创建一个环境标识的标签键,标签值有 test,pre,prod,三个标签值去绑定对应环境的资源。
那么一个应用的某个环境的主机列表就可以通过这层关系加上标签过滤给呈现出来了。
在过往工作过程中,我们发现,有一些应用,说起来已经上线一两周了,然而还会有开发者来找,因为日志没配置采集,不管当事运维是有意还是无意,终究带来的后果是还需要再指派人二次涉足这个应用。有一些应用,明明已经下线了,但是 GitLab 仓库这个项目还好端端躺在那儿,发的 Job 也还在 Jenkins 列表里,甚至主机上进程还在运行着,域名也没有删除,时间久了之后,就没有人能知道这个项目到底是否下线了,这种混淆运维视线的情况,其实最为让人痛心疾首,但是在以往工作中,这些情况却时常存在。
为了应对这种问题,我们甚至专门针对应用上下线,涉及到的各方依赖,做成了流程图来期望能够改善如上描述的问题,但事实证明后来都是收效甚微的。还是那句话,将规范约定交给平台来约束,而不应该单纯靠人的自觉。
比如,我们下线的流程图如下:

所以,在做平台之初我就一直再考虑,如何才能将这样的流程,结合 CMDB 平台,固化下来,其实就是,应用的生命周期管理。
CMDB 平台上大约有 80% 的数据是自动任务同步上来的,不需要人力介入,还有 20% 则是需要人为介入维护的,其中就包括应用,不过,倒是不必像创建普通数据那样创建应用,而应该把应用的创建集成在工单系统当中来进行流转。
当业务方需要新增一个应用时,要先在工单中提出申请,表单里的内容就是应用对应的字段,提交之后由 leader 进行审核,这一环节主要是核对应用名是否符合规范,应用资源需求是否合理,如果通过之后,将自动把项目在 GitLab 中创建好,然后开发者就可以开始编码了,此时应用的状态为待上线。运维方则可以开始资源准备,环境准备的步骤,这些都在这个应用的上线流程当中,流程还有其他一些配套的步骤,当所有工作做完之后,由运维操作,将应用的状态置为运行中,但是这一个动作会触发一个检测脚本,从而预检一个项目是否还有配套流程没有安排上,如果有步骤未配置到位,则该应用状态无法改为运行中,而不在运行中就意味着,这个应用的上线项目周期是不完整的,自然在产品那里验收也是无法通过的。只有通过这样的强约束倒逼,才能真正将一个应用的各个环节落实到位。
同理,当业务方确认一个应用需要下线,仍旧是先提交应用下线工单,相关配套的下线,就可以做成一个一个的运维脚本来扩展,平台只需要做成一个 checklist,由运维进行操作与核查,只有这些清单都核查过了,方可将该应用置为已下线状态,注意,应用下线,不代表直接将应用从库里删除,一些应用的数据,在很久之后可能还会有查阅的需求,所以应用下线就只需要把应用状态置为已下线即可。
本来不打算再分享编程方面的内容,因为自己也属于半路出家自学的 go 语言,很担心自己的一知半解分享之后误人子弟。但转念又感觉到,对比起来,怯于分享似乎问题更大,文章本身就是写出来供人品评的,如果连分享交流的勇气都没了,前进自然也是困难的。
MongoDB 默认的 ID 并不像是 MySQL 那样的自增 ID,如果想要实现自增 ID,则需要借助于另一张表存放该表的 ID,每次存数据的时候,再通过 findAndModify 方法对这个 ID 进行获取并加 1。
$ db.ids.save({name:"user", next_id:NumberInt("0")});
1
创建一个集合,专门存储其他表的自增信息,默认数字类型是int64,这里通过NumberInt()方法将其指定为int32。
查询该条记录:
$ db.getCollection('ids').find({})
{
"_id" : ObjectId("6234313fb503f6bf2433f4e4"),
"name" : "user",
"next_id" : 0
}
1
2
3
4
5
6
新增用户的时候,直接调用获取 ID:
$ db.user.save({
_id: NumberInt(db.ids.findAndModify({
update:{$inc:{'next_id':NumberInt("1")}},
query:{"name":"user"},
new:true
}).next_id),
username: "eryajf",
site:"https://wiki.eryajf.net"
});
1
2
3
4
5
6
7
8
9
注:因为 findAndModify 是一个方法完成更新查找两个操作,所以具有原子性,多线程不会冲突。
然后查询该条记录:
$ db.getCollection('user').find({})
{
"_id" : 1,
"username" : "eryajf",
"site" : "https://wiki.eryajf.net"
}
1
2
3
4
5
6
golang 的实现与语句差不多,这里只是做示例展示,其中的GetDataID("user")在实际生产使用过程中,需要确保对其错误进行处理。
package main
import (
"context"
"fmt"
"learnmongo/public"
"log"
"math/rand"
"strconv"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var DB *mongo.Database
func init() {
uri := "mongodb://root:123456@127.0.0.1:27017"
client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
if err != nil {
panic(err)
}
DB = client.Database("class")
}
type UserMongo struct {
ID int32 `bson:"_id"`
Name string `bson:"name"`
Phone string `bson:"phone"`
Email string `bson:"email"`
}
func main() {
defer func() {
if err := public.InitDb().Disconnect(context.TODO()); err != nil {
panic(err)
}
}()
fmt.Println("start")
table := DB.Collection("user") // 指定表名为user
_, err := table.InsertMany(context.TODO(), []interface{}{
UserMongo{
ID: GetDataID("user"),
Name: "eryajf" + strconv.Itoa(rand.Intn(11)),
Phone: strconv.Itoa(rand.Intn(11)),
Email: strconv.Itoa(rand.Intn(5)) + "@qq.com",
},
UserMongo{
ID: GetDataID("user"),
Name: "liql" + strconv.Itoa(rand.Intn(11)),
Phone: strconv.Itoa(rand.Intn(11)),
Email: strconv.Itoa(rand.Intn(5)) + "@qq.com",
},
})
if err != nil {
fmt.Print(err)
return
}
fmt.Println("end「👋」")
}
func GetDataID(collname string) int32 {
table := DB.Collection("ids") // 指定表名为ids表
var result struct {
Name string `json:"name" bson:"name"`
NextID int32 `json:"next_id" bson:"next_id"`
}
table.FindOneAndUpdate(
context.TODO(),
bson.M{"name": collname},
bson.M{"$inc": bson.M{"next_id": 1}}).Decode(&result)
return result.NextID
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
自增 ID 从视觉上更加直观,与 DB 交互查询的时候也更加简便,某些场景中是可取的方案。CMDB 平台中数据存储标签管理都用上了这个能力。
全局搜索的能力主要依赖索引来实现,接下来我们通过一些示例及说明来了解这一功能。
在开始学习了解之前,先准备一些测试数据如下:
db.datas.insert(
[
{"datas_identify":"eryajf","data":[{"name":"aaa","identify":"aaa-1","create_time":"2020-01-01"}]},
{"datas_identify":"eryajf","data":[{"name":"bbb","identify":"bbb-1","create_time":"2021-01-01"}]},
{"datas_identify":"eryajf","data":[{"name":"ccc","identify":"ccc-1","create_time":"2022-01-01"}]},
{"datas_identify":"eryajf","data":[{"name":"ddd","identify":"ddd-1","create_time":"2023-01-01"}]},
{"datas_identify":"eryajf","data":[{"name":"eee","identify":"eee-1","create_time":"2024-01-01"}]},
{"datas_identify":"liql","data":[{"name":"fff","identify":"fff-1","create_time":"2024-01-01"}]},
{"datas_identify":"liql","data":[{"name":"ggg","identify":"ggg-1","create_time":"2026-01-01"}]},
{"datas_identify":"liql","data":[{"name":"hhh","identify":"hhh-1","create_time":"2027-01-01"}]},
{"datas_identify":"liql","data":[{"name":"iii","identify":"iii-1","create_time":"2028-01-01"}]},
{"datas_identify":"liql","data":[{"name":"aaa","identify":"aaa-1","create_time":"2029-01-01"}]}])
1
2
3
4
5
6
7
8
9
10
11
12
常规情况下,我们可以针对具体字段进行查询:
db.getCollection('datas').find({"datas_identify":"eryajf"})
db.getCollection('datas').find({"data.name":"aaa"})
1
2
这种针对指定字段的查询没有问题,不过如果想要实现一个智能识别的,或者针对当前数据表能够全表搜索的,就需要用到索引了。
MongoDB 可以对任意字段建立全文索引(text index),但需要注意:1 个 collection 中只能包含至多 1 个全文索引,且只能对 String 或 String 数组的字段建立文本索引。
我们可以通过如下命令创建一个文本索引:
db.datas.createIndex({datas_identify: "text"})
1
这个语句表示将datas_identify字段添加为全文索引,当然也可以指定多个字段,方法如下:
db.datas.createIndex({datas_identify: "text", name: "text"})
1
执行完毕之后,可以通过如下命令查看当前集合的索引:
$ db.datas.getIndexes()
/* 1 */
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"_fts" : "text",
"_ftsx" : 1
},
"name" : "datas_identify_text",
"weights" : {
"datas_identify" : 1
},
"default_language" : "english",
"language_override" : "language",
"textIndexVersion" : 3
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
查询的语句格式如下:
{
$text:
{
$search: <string>,
$language: <string>,
$caseSensitive: <boolean>,
$diacriticSensitive: <boolean>
}
}
1
2
3
4
5
6
7
8
9
$search:后面跟的是将要搜索的关键字。$language:指定搜索的语言。$caseSensitive:设置是否区分大小写。$diacriticSensitive设置是否区别发音符号。那么查询 datas_identify为eryajf的方式如下:
$ db.datas.find({ $text: {$search: "eryajf"}})
1
MongoDB 可以给一个数组添加索引,从而提高指定数组的检索效率。
创建一个数组的索引:
$ db.datas.createIndex({"data.name":"text","data.identify":"text","datas_identify":"text"})
1
然后直接进行检索:
$ db.datas.find({ $text: {$search: "bbb"}})
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd6"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "bbb",
"identify" : "bbb-1",
"create_time" : "2021-01-01"
}
]
}
$ db.datas.find({ $text: {$search: "aaa"}})
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebde"),
"datas_identify" : "liql",
"data" : [
{
"name" : "aaa",
"identify" : "aaa-1",
"create_time" : "2029-01-01"
}
]
}
/* 2 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd5"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "aaa",
"identify" : "aaa-1",
"create_time" : "2020-01-01"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
这是在明确知道数组内字段的情况下的方案,在一些实际应用场景中,一个数组内的字段有可能是不固定的,这种时候就没办法对指定字段进行索引了。这个时候就需要用到全文索引了。
建立一个通配符全文索引的方法是:
$ db.datas.createIndex({"$**":"text"})
1
全文索引创建完毕之后,我们可以对整个集合进行检索。
测试效果如下:
$ db.datas.find({ $text: { $search: "aaa" } })
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebde"),
"datas_identify" : "liql",
"data" : [
{
"name" : "aaa",
"identify" : "aaa-1",
"create_time" : "2029-01-01"
}
]
}
/* 2 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd5"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "aaa",
"identify" : "aaa-1",
"create_time" : "2020-01-01"
}
]
}
# ============ #
$ db.datas.find({ $text: { $search: "2022" } })
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd7"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "ccc",
"identify" : "ccc-1",
"create_time" : "2022-01-01"
}
]
}
# ============ #
$ db.datas.find({ $text: { $search: "2024" } })
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebda"),
"datas_identify" : "liql",
"data" : [
{
"name" : "fff",
"identify" : "fff-1",
"create_time" : "2024-01-01"
}
]
}
/* 2 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd9"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "eee",
"identify" : "eee-1",
"create_time" : "2024-01-01"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
基于全文索引的搜索,有一些需要注意的点,这里做一下记录。
如果查询中有通配符,使用如下方式转义
$ db.datas.find({ $text: { $search: "\"10.6.6.66\"" } })
1
多条件逻辑或
$ db.datas.find({ $text: { $search: "2024 bbb" } })
1
多条件逻辑与
$ db.datas.find({ $text: { $search: '"2024" "liql"' } })
1
如果想要排除某个结果,则用-查询
$ db.datas.find({ $text: { $search: '"2024" -liql' } })
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd9"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "eee",
"identify" : "eee-1",
"create_time" : "2024-01-01"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
golang 检索时的代码
type Data struct {
DatasIdentify string `json:"datas_identify"`
Data []struct {
Name string `json:"name"`
Identify string `json:"identify"`
CreateTime string `json:"create_time"`
} `json:"data"`
}
func FindTest() {
filters := bson.D{}
filter := bson.E{Key: "datas_identify", Value: "eryajf"}
searchFilter := bson.E{Key: "$text", Value: bson.M{"$search": "2022"}}
filters = append(filters, filter, searchFilter)
datas, err := ListData(filters, options.FindOptions{})
if err != nil {
fmt.Printf("get data failed: %v\n", err)
}
for _, v := range datas {
fmt.Println(v)
}
}
// ListData 获取数据列表
func ListData(filter bson.D, options options.FindOptions) ([]*Data, error) {
table := DB.Collection("datas")
cus, err := table.Find(ctx, filter, &options)
if err != nil {
fmt.Printf("find data failed: %v\n", err)
}
defer func(cus *mongo.Cursor, ctx context.Context) {
err := cus.Close(ctx)
if err != nil {
return
}
}(cus, ctx)
list := make([]*Data, 0)
for cus.Next(ctx) {
data := new(Data)
err := cus.Decode(&data)
if err != nil {
fmt.Printf("decode data failed: %v\n", err)
}
list = append(list, data)
}
return list, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
以上就是 MongoDB 中索引的一些实践,一些内容需要结合实际场景进行使用。
以上,便是我个人对于 CMDB 平台建设的全部思考,但愿这对您是有所帮助的。
只有真正身处其中的人才能体会负责一个平台建设的苦辣酸甜,当平台落地建成之后,这种价值与影响是长久且深远的,祝福每一位,在平台建设之路上顺遂平坦!
我们的团队基于各个领域的实践,汇集成了一本《高性能之道--SRE 视角下的运维架构实践》 (opens new window),欢迎大家购买品读。
前言
本文写就于 2022 年,是本人在 CMDB 领域的钻研探索实践之后的一些经验分享,全文约计 1.7w 字,凝聚了我全部的心血,从平台架构的设计,到项目周期的管理,甚至项目完成之后的推广,都有所涉猎,如果你也在折腾 CMDB,那么本文是你不可错过的入门必读之作。
我们的运维团队汇聚了一群优秀的,富有想法的年轻人。每次周会里边,当我们完成了各自日常工作的交流沟通之后,总是会跳出当下,天南海北,胡言乱语地头脑风暴一番。
在这些看似 闲笔 的沟通当中,我们进行了大量的运维规范以及最佳实践的讨论,而当我们进行地讨论越多,越能够发现一个问题,无论是对于一个全流程(指一个项目的整个生命周期)命名的规范约束落地,还是对发布平台以应用维度的主机列表维护,都步步绕不开一个能够真正解决运维痛点的 CMDB 平台。
注意,这里在 CMDB 这个名词的前边加了一个定语,真正解决运维痛点。
事实上在这之前,我们并非没有做过 CMDB 平台的建设,只不过由于我们以往对这一概念的认知不足,完全交由运维开发同学,按他的一些经验想法开发出来的 CMDB,却总是难以真正在实际运维生产当中落地,简而言之就是,CMDB 平台做过,但都失败了。
我想,失败的原因大概有如下三点:
何不食肉糜 的感觉,当然这里没有丝毫揶揄开发的意思,真正好的平台其实还是需要开发来把想法实现,只是从客观事实呈现的效果来看,的确会走向这样一个窘境。何不食肉糜 的感觉。人言说鞋子合不合适只有脚知道,如果一个平台的最终使用者从来没有参与过平台的建设,那么最终这个平台也的确很难做到令其满意。实际上,在公司里边造的轮子,通常很难会被判定为失败,这里边包含很多现实的原因,公司能不能够勇敢地站出来知错认错改错,通常取决于领导人的魄力。很多时候阻碍我们成长发展的,正是那些被太多本不应掺杂其中的表面因素给影响的,可能我们司空见惯了实事求是这个词儿,从而觉得这个词儿的落地也是顺理成章的事情,但,我们的确经常会因为没能做到实事求是,以至于鞋子不合脚也会说服自己让脚去适应鞋子,而忘记了穿一双鞋子的本质是为了让脚舒服。
没错,有时候阻碍我们成功的唯一因素,可能就是实事求是。当然这里并不是哲学思想研讨会,我不会对这一理论做过多深入分析,简单一提算是抛砖引玉,让我们在意识中埋下一条行事准则,接下来还是回到 CMDB 平台建设的分享上来。
对于建设 CMDB 平台而言,实事求是的指导意义就在于,我们能够认真审视上边提到的失败的每一个原因,而我也会在接下来的内容当中,时不时地回来过剖析,实际推进设计,开发平台的过程与以上问题的碰撞。
这里我要感谢我的领导,能够勇于认识到以往平台建设的问题,并勇于启用我作为新阶段平台建设的主负责。
之所以说领导启用我是一种勇敢,是因为我在这之前并没有任何 CMDB 平台建设的经验心得,这次受命,则完全是从零基础开始。
因此这里也提前与大家露个怯,我并不算是 CMDB 领域的专家,仅仅是经历过一个平台建设,收获了一些心得,不揣愚昧与大家做一个交流分享,每家公司的运维架构以及实际需求场景都各不相同,这里仅是针对一些比较有共性的底层设计,以及平台建设过程与实际落地应用之间的问题,做一些分享。
当我接到这个任务之后,首先去了解了一下 CMDB 的概念,CMDB(Configuration Management Database)即配置管理数据库,或者叫资产管理系统。网上还有从这个概念发散出来的一大堆名词概念,一番查阅之后,我如坠五里雾之中,变得更加迷茫与模糊,好在之前一直有关注行业内一些前沿动态,多少了解一些开源的 CMDB 平台,当我把这些平台部署起来,真切体验之后,才慢慢形成了一些个人对 CMDB 平台的理解和方向上的把控。
在这里要首先感谢这些开源项目,正是他们将自己在这个领域的超前认知以及凝聚心血的智慧开源出来,才使得我们能够站在巨人的肩膀上思索当下的问题。
我们的 CMDB 平台建设,底层设计思路很多地方参考如上两个平台,但也并非只是简单地拾人牙慧,我在一开始部署体验这些平台,阅读了其中全部文档之后,又结合了自己业务中的实际场景和问题,做了重新的梳理设计,这些内容在后边都会一一呈现。
最后表明一下我个人心中对 CMDB 的理解:
能够做好这两点,我想,这个平台距离真正解决运维痛点,也就不远了。
本节内容也将会围绕这些问题,平铺展开。
事实上我在组织部门成员第一次碰面会之前,就遇到过其他开发同学问过这样的问题:CMDB 好干啥?
我当时用运维实际日常工作的一个小场景对此做了解释,大家也可以看看自己在日常运维工作中有没有这样的痛点。
我当时这样回答:举个例子,你有没有遇到过这样一个问题,你知道客户端服务所在主机,现在的需求是要在服务端入口对客户端请求加白名单,但是客户端主机走的是统一的 NAT 出口,这个操作的流程会是什么样呢?
事实上运维同学在日常工作中,需要面对的类似问题数不胜数,很多问题都是这种很繁琐的重复性工作,CMDB 平台就应该在提供资源对象的基础属性之外,还要考虑资源之间的关系,这两点,都需要通过抽象出来的模型进行维护,后边会详述。
一个平台的建设, 设计是第一步,开发才是第二步,尽管项目启动之后,参与其中的开发人员已经开始催促你拿出方案,但我劝你,别着急,你需要冷静地思考,耐心地梳理,把你想要的东西和样子想清楚,并把你该做的准备工作做充分,否则你一定会被后续开发过程中遇到的诸多问题干扰,从而在大平台的小细节中迷失。这是作为项目 PO 必须在一开始就要注意的。
为了便于大家快速领会整个平台设计的方向,这里先将我们落地下来的页面左侧导航陈列与此:
这个导航将会作为后续内容展开的参照。
现在很多 CMDB 平台的最佳实践,至少都同步到了这样一个共识:需将资源进行抽象,通过模型表达资源的属性及关系。
乍一听可能会有一些懵,这里我可以从个人所经历的资产管理的几个阶段来做一个说明。
最早的时候,我与小伙伴所管理的服务器资源(其他资源则大多疏于登记管控),都是基于 Excel 来进行管理的,彼时表格里边大概会这样来对服务器资源进行登记:

表格维护,缺点多多,相信经历过的同学必有同感,维护麻烦与数据准确性是表格维护的最大问题。那时候我与几个运维小伙伴将这些 Excel 维护在了 SVN 当中,每个人更新了表格内容之后,就通过小乌龟将内容同步上去,想来这应该算是远古时期的手工时代了,现在公有云时代的运维,恐怕没有几个人会甘心做这样的手工活了。
当时有不少服务器是在自建机房的 kvm 中运行,因此就先以单台宿主机服务器作为大分类,然后逐个记录运行在服务器上的虚拟机,后边的则是一些相对重要的虚拟机属性信息。如果想要获取虚拟机更多的信息,就只能登陆到主机上凭经验查找确认了。
再往后会有一些运维同学自学了开发,或者是部门有专门的运维开发同学,也做过 CMDB 平台的建设,只不过,大多是服务器资源数据在云厂商那里平搬到本地数据库,然后在 web 页面提供了一个可供搜索的框而已,看似跨出了很大一步,实则对实际很多应用场景,都是没有太多帮助的。
这算是半自动时代,资源的字段属性在数据库,乃至在前后端代码内,都是固定了的,如果你想要新增一个你关心的字段,则需要牵动前后端开发者,乃至 DBA 同学,一同来做一波变更,从而满足这样一个需求。还有一个问题是,这种单资源对象的管理,通常都没有把不同资源之间的关系维护起来,从而很难在实际运维工作中解决掉运维的痛点。
上边一些描述中,需要大家注意一些关键词,这些关键词也组成了新平台的纲领。
模型是对 CMDB 平台所有资源的抽象,所有资源都应该基于模型来构筑,模型的核心功能在于:定义资源对象的基础属性,配置资源对象的关联关系。
最终,资源仓库对资源进行展示的时候将会依据模型中资源模型定义的字段以及关系进行呈现。
模型设计是 CMDB 平台建设的重要环节,如果这一个环节工作做好了,那么后续的开发以及使用都将会变得顺遂起来。而这个建设过程如果没有比较好的方法论支撑,又是很容易陷入焦灼之中的。
模型的产生过程,大概分如下几个步骤:
因为我们的所有业务全部运行在公有云之上,于是我就登录上云平台,将常见资源一一巡查了一波,首先整理出了一张模型草图:

当你读这篇文章,打算建设一个 CMDB 平台,那么梳理出来这样一张图是必不可少的一个步骤,它不仅清晰地展现了 CMDB 平台你想要的内容和样子,最重要的是为后续平台推进奠定了重要的基础。
我来简单说明下这张图,首先图里呈现了我们前边提到过的,CMDB 平台的使命,即将右上角云上的资源与左边我们本地的资源数据,通过关系(下边会详细介绍关系的设计)给串联起来了。有了这样的关系链之后,我们就能够在平台上一目了然地看到一个应用,它的某个环境所涉及到的 GitLab 仓库地址,相关的开发测试运维人员,以及其他中间件,云上各种资源。
一个平台能够做到这一步,其实就已经非常厉害了,通常互联网公司中人员流动都比较大,新人入职花费精力最大的莫过于要去了解每个应用以及它背后隐藏着的一系列相关资源,当我们的平台能够弥补这块儿的空白,就实现了很大的一个价值。
另外,图中还提到过的其他扩展能力,主要需要考虑与其他平台打通的问题,以及成本核算方面的建设,当我们基础建设完成之后,会再添加任务核算出每个应用的花费,然后再结合上该应用的监控数据进行比对,从而在平台上自动提供给我们一个应用成本与产出之间的比例,以便运维同学对其进行评估。
当你梳理完资源大纲之后,就可以梳理每个资源的具体字段以及关系了,建议这里使用表格进行统一梳理与维护,这也是从赵班长一次直播分享当中学习到的经验。
Excel 样式大概如下:

通常一个资源分三组进行维护:
因为云上资源的字段都是已经固定下来的,因此只需要在资源详情页面对字段进行整理登记即可,这块儿基本上也不需要过多讨论,顶多对一些资源字段的取舍上做一个讨论,因为有一些云上资源的字段对我们来说意义并不大。
如果是本地新增的模型,那么字段有哪些,如何定,则都是需要大家好好讨论的,这块儿我在后边讲应用视图设计的时候会单独介绍应用的字段。
项目一开始,在技术评审会议上,我们内部对于 CMDB 平台底层存储选型问题也做过专门的讨论。
讨论的核心在于:选择 MySQL 还是 MongoDB。
通常运维平台都是以 MySQL 作为后端存储的,大家用起来也会更加轻车熟路一些。之所以这里要引入 MongoDB,是因为 CMDB 平台的一些特殊性,也正是基于这些特殊性,促使我们在讨论之后,决定选型存储为 MongoDB。
第一个要点:动态字段,我们给资源进行建模,然后在平台上来维护这个资源的字段,因此每个资源模型都是在平台新建的,模型的字段也是新建的,这就要求这些内容落到库中也是动态灵活的。以往使用 MySQL 的思路可能一种资源就是一张表,字段也是一开始就约定好无法随意更改的,这其实是无法满足当前这种需求的。当然,我们也可以在 MySQL 中存一个 json,但是使用起来其实会非常别扭,实际开发过程中也会遇到不少现实的难题。MongoDB 的存储格式为 BSON(一种 json 的扩展),刚好契合这种需求场景,这也是选型 MongoDB 的最重要原因。
第二个要点:全文搜索,前边说过,CMDB 平台使用场景中,有占八成都是查询方面的,通常我们会拿到一个 IP,或者一个关键词,你还不能确定这个信息归属于哪个资源类别,没办法直接去搜索,因此全文搜索也是一个必须要支持的功能。如果选型 MySQL,那么就需要再把数据同步到 ES 或者 MongoDB 来实现,既然 MongoDB 天生支持这种全文模糊搜索,那么为什么不直接选型 MongoDB 呢,是的,就它了!
其实这个本来不值得多说,语言只是工具,用任何语言栈来实现都是没有问题的,只是说明一下我们之前使用的技术栈,从而在后边对一些具体的知识点分享时大家能心里有数。
任何一个平台的建设,都是需要时间的,这一点对于我们项目负责者要提前意识到,更重要的是还需要让你的领导意识到。
所有一旦口渴马上就要喝茶,一旦饿了马上就要把饭吃到嘴里,一旦提出建设平台的想法,只给了一周两周就要看到成品,这些都是小孩子未曾开蒙的行为。行军打仗,首要考虑的就是粮草与天象,负责项目,亦要重视项目拆解与排期。
通常来说,要建设一个基本可用的 CMDB 平台,前后端的开发,以及运维同学的协同配置接入使用,至少需要三期规划,如果开发人员资源不很充足的话,那么最少也需要半年的时间来建设。
接下来我将详细介绍这几个阶段开发推进的流程与细节,大多地方先通过截图让大家有个整体认识,然后将功能细节用任务列表的形式进行解释与说明。
一个大的平台,必然是由无数个小的功能点堆砌而成的,没有人能够把每一个功能点的详情记在脑海中,我也一样,所以我在负责该项目的时候,当我与小伙伴们一起把功能点聊清楚之后,马上就会拆分成每一个功能细节,并用任务列表记录下来。再拆分成前后端开发的具体接口列表,分配给开发者落地到 mock 平台。然后再将这些内容与各位开发者二次确认,以免出现需求理解偏差的问题。在最后项目结期的时候,这个任务列表也会成为我验收项目的一个参考凭据,以免因为忘记某个细节而漏测某个功能点。
项目的开发,在每一期未动工之前,无论费多少口舌,开多少次会议,只要能够把需求对齐到开发者同学的心里,都是值得的。需求理解偏差这种问题,发现越早,提出越早,调整越早,就越好,这也是作为项目负责人,需要在项目推进过程中不断关注并跟进测试的,否则,等到最后做出来却发现货不对板,然后再进行返工,就会对项目推进的整体质量,以及开发者的心气儿都产生极大的影响。
所以,不要着急动工,一旦动工,就一定是想好了。
完成模型管理这一功能的落地,要求这一期结束之后,能够完成一个资源模型的增删改查,以及能够在资源仓库手动添加删除资源数据。
建议排期为两个月,这一期看似只是完成了模型管理和资源仓库两个内容,但实际上里边模型的动态字段,复杂的交互逻辑对于前后端的考验都是非常大的。
加之这是项目的开篇阶段,每个开发人员都还不熟悉里边的各种概念,需要一点时间来磨合。我们经历的过程也正是如此,后端开发者,前端开发者,在前期都曾不止一次被里边的概念绕晕,从而导致需求与实现错位,但当我们后边接触越来越多,每个陌生的概念慢慢都变成大家的常识,每个功能点大家也都能参与其中,一起思考,讨论,共建,这个时候才能真正进入快速开发的阶段。
样式示例:

功能细节:
样式示例:

功能细节:
字段管理是整个模型管理当中比较复杂的一块儿内容,当然,这个复杂是针对前后端开发者而言的,其实这种开发上的复杂,也正是为了达到使用上的简单。
点击添加字段之后的页面示例:

关于字段的类型,不同的平台可能提供的有不同的类别,像蓝鲸 CMDB 中模型的字段类型有如下几种(参考: https://bk.tencent.com/docs/document/6.0/152/6954):
我根据日常使用场景中经常遇到过的字段类型,将字段类型压缩成了 6 种,以减少运维人员的接入认知成本。分别为:
每一种类型都会有不同的规则约束,也就造成了每种类型的信息表单是不一样的,前端需要根据选择类型的不同,进行表单的动态变化。其中的规则约束基本上都能见名知意,因此不再一一解释。
这里将不同的字段类型表单示例做个分享。
字符串类型

数字类型

日期类型

下拉选项

密码类型

关系类型

关系是比较复杂的一个类型逻辑,不过前边我们已经有过铺垫,这里将所有的资源之间的关系拆分为从属和连接两种,下边再来拆分看两种关系的不同表单效果。
从属关系

连接关系

两种关系在这里可能看起来并不够直观,后边在介绍资源仓库的时候,我将通过实例进行详细的讲解。
功能细节:
资源仓库是所有资源落库之后对外呈现的一个窗口,也是将要开放给运维以及普通开发者的一个数据集合页面。
首页展示

功能细节:
为了说明功能点,我这里在其他分类中创建三个测试模型,以及一些测试数据,分别如下:
在模型管理中其他分类下,创建了一个 CMDB 测试 的模型分组。
以上是三个模型以及基本属性的字段信息。
因为关系属性相对抽象,所以这里单独表达:
此时我们需要在教师和学生两个模型的关系属性中分别创建一个从属类型的关系,从属模型为班级,如下图:

然后在教师与学生两个模型中任选其一创建一个连接类型的关系,之所以任选其一,是因为在模型管理的功能细节里已经说过,连接关系在某一个模型中创建之后,另一方也需要自动创建这条关系。如下图:

创建测试数据如下:
[
{
"_id": "6279d9e2d440eaf8ddac1255",
"class_identify": "3nian1ban",
"class_name": "3年1班",
"create_at": 1652152802,
"model_identify": "class",
"modify_at": 1652152802
},
{
"_id": "6279d9f4d440eaf8ddac1256",
"class_identify": "3nian2ban",
"class_name": "3年2班",
"create_at": 1652152820,
"model_identify": "class",
"modify_at": 1652152820
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"_id": "6279db9cd440eaf8ddac1257",
"create_at": 1652153244,
"model_identify": "teacher",
"modify_at": 1652153244,
"teacher_identify": "lilaoshi",
"teacher_name": "李老师",
"teacher_xueke": "语文"
},
{
"_id": "6279dbb2d440eaf8ddac1258",
"create_at": 1652153266,
"model_identify": "teacher",
"modify_at": 1652153266,
"teacher_identify": "wanglaoshi",
"teacher_name": "王老师",
"teacher_xueke": "数学"
},
{
"_id": "6279dbc9d440eaf8ddac1259",
"create_at": 1652153289,
"model_identify": "teacher",
"modify_at": 1652153289,
"teacher_identify": "zhanglaoshi",
"teacher_name": "张老师",
"teacher_xueke": "英语"
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[
{
"_id": "6279dc3dd440eaf8ddac125a",
"create_at": 1652153405,
"model_identify": "student",
"modify_at": 1652153405,
"student_identify": "jia",
"student_name": "甲",
"student_xuehao": 1
},
{
"_id": "6279dc47d440eaf8ddac125b",
"create_at": 1652153415,
"model_identify": "student",
"modify_at": 1652153415,
"student_identify": "yi",
"student_name": "乙",
"student_xuehao": 2
},
{
"_id": "6279dc52d440eaf8ddac125c",
"create_at": 1652153426,
"model_identify": "student",
"modify_at": 1652153426,
"student_identify": "bing",
"student_name": "丙",
"student_xuehao": 3
},
{
"_id": "6279dc5dd440eaf8ddac125d",
"create_at": 1652153437,
"model_identify": "student",
"modify_at": 1652153437,
"student_identify": "ding",
"student_name": "丁",
"student_xuehao": 4
},
{
"_id": "6279dcaad440eaf8ddac125e",
"create_at": 1652153514,
"model_identify": "student",
"modify_at": 1652153514,
"student_identify": "wu",
"student_name": "戊",
"student_xuehao": 5
},
{
"_id": "6279dcb6d440eaf8ddac125f",
"create_at": 1652153526,
"model_identify": "student",
"modify_at": 1652153526,
"student_identify": "ji",
"student_name": "己",
"student_xuehao": 6
},
{
"_id": "6279dcc3d440eaf8ddac1260",
"create_at": 1652153539,
"model_identify": "student",
"modify_at": 1652153539,
"student_identify": "geng",
"student_name": "庚",
"student_xuehao": 7
},
{
"_id": "6279dccdd440eaf8ddac1261",
"create_at": 1652153549,
"model_identify": "student",
"modify_at": 1652153549,
"student_identify": "xin",
"student_name": "辛",
"student_xuehao": 8
},
{
"_id": "6279dcd8d440eaf8ddac1262",
"create_at": 1652153560,
"model_identify": "student",
"modify_at": 1652153560,
"student_identify": "ren",
"student_name": "壬",
"student_xuehao": 9
},
{
"_id": "6279dce5d440eaf8ddac1263",
"create_at": 1652153573,
"model_identify": "student",
"modify_at": 1652153573,
"student_identify": "gui",
"student_name": "癸",
"student_xuehao": 10
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
然后将数据按照模型中设计的关系关联起来。
这个时候,我们就可以看到数据之间的一些联动关系了。
从资源仓库点击老师这个模型进入详情列表:

此时看一下李老师的信息:

然后看关系管理中的从属关系:

再看连接关系:

既然可以点击跳转,那我们点击一下学生甲,会在新窗口展示甲的基本信息:

以及同学甲的从属关系:

和甲的连接关系:

可以看到甲同学所关联的两位老师,最终效果正如我们前边设想的一样。
同理,通过一个内网服务器的 IP,找到它的出口 NAT,就是通过这样的方式来实现的。
其他教师与学生的关系大都类此,因此不再重复截图呈现。
完成云资源基于平台定时任务的落库工作,要求这一期结束之后,云资源能够自动落库,资源关系也要自动维护。
建议排期为两个月,这一阶段的前后端开发任务比例并不协调,大概是三七开的一个比例。有了第一期模型管理以及资源仓库之后,后端开发者就可以照着云资源的 SDK 编写同步逻辑了。这时就非常考验项目负责人,要把控好不同阶段的工作规划及分配,比如这期前端完成了定时任务的交互页面之后,可以提前着手第三期的标签管理应用视图的开发工作,因为第三期的工作内容,前后端任务比例又反过来了。
这些都是项目 PO 必须要在项目推进过程中考虑的。
上边数据仓库的示例中,数据的创建,数据之间关系的维护,全部都是通过手工操作来完成的,非常地繁琐。第二期首先要完成的就是定时任务的管理页面,从而让程序能够自动将云上资源自动获取并落地到本地。
样式示例:

添加任务,云上资源与其他资源一致,不同的是云上资源多了一个扫描可用区的条件选项。

功能细节:
当前后端协同完成定时任务接口与页面之后,后端同学就可以开始一一编写云上资源的同步任务了。这个操作以及编码的步骤,我整理如下:
申明
原创文章eryajf,未经授权,严禁转载,侵权必究!此乃文中随机水印,敬请读者谅解。
样式示例:

功能细节:
此处是一个单独的页面提供一个搜索框,输入英文或者数字,将会对数据表进行全局搜索。
添加标签管理的能力,完成应用视图。要求这一期结束之后,CMDB 平台进入一个基本可用的状态。
建议排期为两个月,这一阶段首先要完成的是覆盖全局的标签管理能力,然后再完成应用视图的页面,注意应用的环境管理将借助标签的能力进行二次归纳,这一点会在后边讲到。
我了解过市面上大部分开源的,商业收费的 CMDB 平台,几乎没有一家,能够真正将应用从环境的维度来管理,给做好的,这的确会是一个难题,后边我也会介绍我们在这个过程中的思考,以及为什么选择借助标签来实现环境的管理的。
标签也是 CMDB 平台一大重要的设计,许多时候,我们会依赖标签对一批或者某些数据进行标识与管理。
这里先简单说明下标签管理的核心逻辑:
标签管理分标签键和标签值两种,标签键是唯一的,标签值不必唯一,一个标签键可以关联多个标签值,一个标签值可以关联多个资源数据(这里就要求标签值是要与资源仓库的全局进行打通,从而能够提供给用户选择要关联的资源数据)。 这种设计逻辑完成之后,我们就可以通过标签的键值对,过滤搜索我们关心的数据了。
样式示例

功能细节:
为了说明功能点,这里再次创建一些测试数据:

点击查看可以进入到标签的详情:

点击左侧+号可以展开标签值关联的数据,我们展开一下 yuwen 看下:

功能细节:
绑定资源的页面视图如下:

功能细节:
同理,外边标签搜索页面与绑定资源页面类似:

功能细节
应用视图涉及到内部一些信息,因此这里就拿原型图来做展示及说明。
样式示例

功能细节
主机列表示例如下

这里主要是通过标签中的环境标识,来区分主机列表中的环境,从而过滤出应用对应不同环境的主机列表。
这一小节,我们聊聊,一个平台如何渐进式地走入运维组小伙伴们的内心,并夺走他们日常工作的主要关注的。
想要博得大家的关注,我想到了三点:自身硬,常吹风,强约束。
这里将一个资源对象在平台上的维护流程以及一些注意点完整罗列,以便于读者参考时可以方便地了解上手。
操作之前,首先清空所有的测试数据。
资产模型 分类下创建 公有云 的分组组织模型 分类下创建两个组如下
内部组织外部组织所创建的模型以及分组关系如下:
模型大分类 模型分组 模型 模型字段分组 模型字段目前先创建这些内容,能够保证云资源的交互可用。
注意: 创建模型的时候需要注意一个细节,模型的 identify 尽量与腾讯云资源的 identify 一致,以便于后续核对排查。
其他模型亦按照上边的姿势进行扩展配置同步。
标签是一个全局数据可关联过滤的系统,目前主要场景会在应用管理界面的环境区分中使用。
标签是一个独立的 kv 系统,一个标签由一个键多个值组成,键作为一种维度的表意(要求全局唯一),值作为该维度的参数,实际操作中将值与数据进行绑定。
通过在标签值管理页面,将值关联上所有的资源数据,从而在标签的维度,将所有的数据分成三种环境。
注意: 虽然一条数据能够绑定同一个标签键的不同标签值,但是这里作为环境区隔的时候,并不建议一条数据绑定给多个标签值。换言之,也就是一个资源实例,不能即属于测试环境,又属于其他环境,以做到资源的环境隔离。
在所有云上动作还未集成到平台的时候,可以借助云的一些能力,将数据落实到本地,比如购买云主机的页面,可让腾讯云调配,标签是必填项,那么一台主机在购买的时候,就能做好环境归属的标识工作了。后续集成到平台,则平台的购买流程,同样需要考虑将标签纳入到购买的标准流程中。
云上的资源我们通过定时任务来进行同步,但是,如果确保本地的数据,对比云上的数据,是准确的,这其实是一个值得思考的问题。
在与小伙伴们讨论分析之后,对于我们现阶段的平台而言,可能将同步任务分割成全量与增量两种比较合适。
在我们的工作场景中,主机的上下线操作是非常频繁的,如果同步任务不能考虑到这种操作毛刺,那么就很容易造成数据不准的问题。于是我们宁愿本地存留一些脏数据,由运维手动来二次确认,也不要简单粗暴地直接将数据删除。
吃透了本节内容的同学可能会发现一个细节,这个平台设计上,如果想要删除一个模型分组,其实是非常难的,我整理了一下,大概需要如下步骤:
想要删除一个模型模型分组,首先需要确保该分组下没有模型,然后确保模型下没有模型字段分组,然后确保模型字段分组下没有字段,然后确保模型字段没有数据了,然后才能将模型分组删除。
这么做的目的是:数据的添加或者移除,都要被平台约束,尤其是删除,平台强约束,是保障数据准确的第一个基石。模型分组到最后一条条数据,就是这样的上下关系,如果强行将模型分组删除,就好比直接把一个蜂群的蜂王给干掉了,会让最底层的一条条数据变得无根可依。
市面上大多数 CMDB 平台,对于应用管理的维度,很少有达到环境这一层的,仅见过一两个产品引入了这一概念,却设计不佳,使用麻烦,让人不明所以。
那么应用的环境到底是否重要呢,实际上是非常重要的,我们在制定一些其他规范的时候,环境是一个很重要的参考角度,比如我们制定日志规范,一个应用的所有日志都会在一个集群当中,环境标识就会拼接在应用名后边作为应用对应的日志索引在日志平台进行隔离。
那么,在 CMDB 平台上,应用的环境如何表示?
一开始我想的是,走模型关联的思路,因为上边在模型设计当中讲过,模型可以通过从属和连接两种关系表示资源之间的关系,于是,我们可以这样做:每个应用创建的时候都会再自动创建这个应用关联的三个环境模型,每个环境都会关联云主机等资源,那么就会存在一个问题,一个云主机的模型,最终会被上百个环境进行关联,这是无法接受的。
最后我们决定,走标签搭桥的思路来解决这个问题。
在模型之间,让应用模型直接与其他资源模型进行关联,换言之,资源会首先以应用维度进行区隔,比如 api 应用测试预发正式环境都是两台主机,那么 api 关联的主机资源就是 6 台主机。
然后创建一个环境标识的标签键,标签值有 test,pre,prod,三个标签值去绑定对应环境的资源。
那么一个应用的某个环境的主机列表就可以通过这层关系加上标签过滤给呈现出来了。
在过往工作过程中,我们发现,有一些应用,说起来已经上线一两周了,然而还会有开发者来找,因为日志没配置采集,不管当事运维是有意还是无意,终究带来的后果是还需要再指派人二次涉足这个应用。有一些应用,明明已经下线了,但是 GitLab 仓库这个项目还好端端躺在那儿,发的 Job 也还在 Jenkins 列表里,甚至主机上进程还在运行着,域名也没有删除,时间久了之后,就没有人能知道这个项目到底是否下线了,这种混淆运维视线的情况,其实最为让人痛心疾首,但是在以往工作中,这些情况却时常存在。
为了应对这种问题,我们甚至专门针对应用上下线,涉及到的各方依赖,做成了流程图来期望能够改善如上描述的问题,但事实证明后来都是收效甚微的。还是那句话,将规范约定交给平台来约束,而不应该单纯靠人的自觉。
比如,我们下线的流程图如下:

所以,在做平台之初我就一直再考虑,如何才能将这样的流程,结合 CMDB 平台,固化下来,其实就是,应用的生命周期管理。
CMDB 平台上大约有 80% 的数据是自动任务同步上来的,不需要人力介入,还有 20% 则是需要人为介入维护的,其中就包括应用,不过,倒是不必像创建普通数据那样创建应用,而应该把应用的创建集成在工单系统当中来进行流转。
当业务方需要新增一个应用时,要先在工单中提出申请,表单里的内容就是应用对应的字段,提交之后由 leader 进行审核,这一环节主要是核对应用名是否符合规范,应用资源需求是否合理,如果通过之后,将自动把项目在 GitLab 中创建好,然后开发者就可以开始编码了,此时应用的状态为待上线。运维方则可以开始资源准备,环境准备的步骤,这些都在这个应用的上线流程当中,流程还有其他一些配套的步骤,当所有工作做完之后,由运维操作,将应用的状态置为运行中,但是这一个动作会触发一个检测脚本,从而预检一个项目是否还有配套流程没有安排上,如果有步骤未配置到位,则该应用状态无法改为运行中,而不在运行中就意味着,这个应用的上线项目周期是不完整的,自然在产品那里验收也是无法通过的。只有通过这样的强约束倒逼,才能真正将一个应用的各个环节落实到位。
同理,当业务方确认一个应用需要下线,仍旧是先提交应用下线工单,相关配套的下线,就可以做成一个一个的运维脚本来扩展,平台只需要做成一个 checklist,由运维进行操作与核查,只有这些清单都核查过了,方可将该应用置为已下线状态,注意,应用下线,不代表直接将应用从库里删除,一些应用的数据,在很久之后可能还会有查阅的需求,所以应用下线就只需要把应用状态置为已下线即可。
本来不打算再分享编程方面的内容,因为自己也属于半路出家自学的 go 语言,很担心自己的一知半解分享之后误人子弟。但转念又感觉到,对比起来,怯于分享似乎问题更大,文章本身就是写出来供人品评的,如果连分享交流的勇气都没了,前进自然也是困难的。
MongoDB 默认的 ID 并不像是 MySQL 那样的自增 ID,如果想要实现自增 ID,则需要借助于另一张表存放该表的 ID,每次存数据的时候,再通过 findAndModify 方法对这个 ID 进行获取并加 1。
$ db.ids.save({name:"user", next_id:NumberInt("0")});
1
创建一个集合,专门存储其他表的自增信息,默认数字类型是int64,这里通过NumberInt()方法将其指定为int32。
查询该条记录:
$ db.getCollection('ids').find({})
{
"_id" : ObjectId("6234313fb503f6bf2433f4e4"),
"name" : "user",
"next_id" : 0
}
1
2
3
4
5
6
新增用户的时候,直接调用获取 ID:
$ db.user.save({
_id: NumberInt(db.ids.findAndModify({
update:{$inc:{'next_id':NumberInt("1")}},
query:{"name":"user"},
new:true
}).next_id),
username: "eryajf",
site:"https://wiki.eryajf.net"
});
1
2
3
4
5
6
7
8
9
注:因为 findAndModify 是一个方法完成更新查找两个操作,所以具有原子性,多线程不会冲突。
然后查询该条记录:
$ db.getCollection('user').find({})
{
"_id" : 1,
"username" : "eryajf",
"site" : "https://wiki.eryajf.net"
}
1
2
3
4
5
6
golang 的实现与语句差不多,这里只是做示例展示,其中的GetDataID("user")在实际生产使用过程中,需要确保对其错误进行处理。
package main
import (
"context"
"fmt"
"learnmongo/public"
"log"
"math/rand"
"strconv"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var DB *mongo.Database
func init() {
uri := "mongodb://root:123456@127.0.0.1:27017"
client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
if err != nil {
panic(err)
}
DB = client.Database("class")
}
type UserMongo struct {
ID int32 `bson:"_id"`
Name string `bson:"name"`
Phone string `bson:"phone"`
Email string `bson:"email"`
}
func main() {
defer func() {
if err := public.InitDb().Disconnect(context.TODO()); err != nil {
panic(err)
}
}()
fmt.Println("start")
table := DB.Collection("user") // 指定表名为user
_, err := table.InsertMany(context.TODO(), []interface{}{
UserMongo{
ID: GetDataID("user"),
Name: "eryajf" + strconv.Itoa(rand.Intn(11)),
Phone: strconv.Itoa(rand.Intn(11)),
Email: strconv.Itoa(rand.Intn(5)) + "@qq.com",
},
UserMongo{
ID: GetDataID("user"),
Name: "liql" + strconv.Itoa(rand.Intn(11)),
Phone: strconv.Itoa(rand.Intn(11)),
Email: strconv.Itoa(rand.Intn(5)) + "@qq.com",
},
})
if err != nil {
fmt.Print(err)
return
}
fmt.Println("end「👋」")
}
func GetDataID(collname string) int32 {
table := DB.Collection("ids") // 指定表名为ids表
var result struct {
Name string `json:"name" bson:"name"`
NextID int32 `json:"next_id" bson:"next_id"`
}
table.FindOneAndUpdate(
context.TODO(),
bson.M{"name": collname},
bson.M{"$inc": bson.M{"next_id": 1}}).Decode(&result)
return result.NextID
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
自增 ID 从视觉上更加直观,与 DB 交互查询的时候也更加简便,某些场景中是可取的方案。CMDB 平台中数据存储标签管理都用上了这个能力。
全局搜索的能力主要依赖索引来实现,接下来我们通过一些示例及说明来了解这一功能。
在开始学习了解之前,先准备一些测试数据如下:
db.datas.insert(
[
{"datas_identify":"eryajf","data":[{"name":"aaa","identify":"aaa-1","create_time":"2020-01-01"}]},
{"datas_identify":"eryajf","data":[{"name":"bbb","identify":"bbb-1","create_time":"2021-01-01"}]},
{"datas_identify":"eryajf","data":[{"name":"ccc","identify":"ccc-1","create_time":"2022-01-01"}]},
{"datas_identify":"eryajf","data":[{"name":"ddd","identify":"ddd-1","create_time":"2023-01-01"}]},
{"datas_identify":"eryajf","data":[{"name":"eee","identify":"eee-1","create_time":"2024-01-01"}]},
{"datas_identify":"liql","data":[{"name":"fff","identify":"fff-1","create_time":"2024-01-01"}]},
{"datas_identify":"liql","data":[{"name":"ggg","identify":"ggg-1","create_time":"2026-01-01"}]},
{"datas_identify":"liql","data":[{"name":"hhh","identify":"hhh-1","create_time":"2027-01-01"}]},
{"datas_identify":"liql","data":[{"name":"iii","identify":"iii-1","create_time":"2028-01-01"}]},
{"datas_identify":"liql","data":[{"name":"aaa","identify":"aaa-1","create_time":"2029-01-01"}]}])
1
2
3
4
5
6
7
8
9
10
11
12
常规情况下,我们可以针对具体字段进行查询:
db.getCollection('datas').find({"datas_identify":"eryajf"})
db.getCollection('datas').find({"data.name":"aaa"})
1
2
这种针对指定字段的查询没有问题,不过如果想要实现一个智能识别的,或者针对当前数据表能够全表搜索的,就需要用到索引了。
MongoDB 可以对任意字段建立全文索引(text index),但需要注意:1 个 collection 中只能包含至多 1 个全文索引,且只能对 String 或 String 数组的字段建立文本索引。
我们可以通过如下命令创建一个文本索引:
db.datas.createIndex({datas_identify: "text"})
1
这个语句表示将datas_identify字段添加为全文索引,当然也可以指定多个字段,方法如下:
db.datas.createIndex({datas_identify: "text", name: "text"})
1
执行完毕之后,可以通过如下命令查看当前集合的索引:
$ db.datas.getIndexes()
/* 1 */
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"_fts" : "text",
"_ftsx" : 1
},
"name" : "datas_identify_text",
"weights" : {
"datas_identify" : 1
},
"default_language" : "english",
"language_override" : "language",
"textIndexVersion" : 3
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
查询的语句格式如下:
{
$text:
{
$search: <string>,
$language: <string>,
$caseSensitive: <boolean>,
$diacriticSensitive: <boolean>
}
}
1
2
3
4
5
6
7
8
9
$search:后面跟的是将要搜索的关键字。$language:指定搜索的语言。$caseSensitive:设置是否区分大小写。$diacriticSensitive设置是否区别发音符号。那么查询 datas_identify为eryajf的方式如下:
$ db.datas.find({ $text: {$search: "eryajf"}})
1
MongoDB 可以给一个数组添加索引,从而提高指定数组的检索效率。
创建一个数组的索引:
$ db.datas.createIndex({"data.name":"text","data.identify":"text","datas_identify":"text"})
1
然后直接进行检索:
$ db.datas.find({ $text: {$search: "bbb"}})
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd6"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "bbb",
"identify" : "bbb-1",
"create_time" : "2021-01-01"
}
]
}
$ db.datas.find({ $text: {$search: "aaa"}})
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebde"),
"datas_identify" : "liql",
"data" : [
{
"name" : "aaa",
"identify" : "aaa-1",
"create_time" : "2029-01-01"
}
]
}
/* 2 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd5"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "aaa",
"identify" : "aaa-1",
"create_time" : "2020-01-01"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
这是在明确知道数组内字段的情况下的方案,在一些实际应用场景中,一个数组内的字段有可能是不固定的,这种时候就没办法对指定字段进行索引了。这个时候就需要用到全文索引了。
建立一个通配符全文索引的方法是:
$ db.datas.createIndex({"$**":"text"})
1
全文索引创建完毕之后,我们可以对整个集合进行检索。
测试效果如下:
$ db.datas.find({ $text: { $search: "aaa" } })
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebde"),
"datas_identify" : "liql",
"data" : [
{
"name" : "aaa",
"identify" : "aaa-1",
"create_time" : "2029-01-01"
}
]
}
/* 2 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd5"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "aaa",
"identify" : "aaa-1",
"create_time" : "2020-01-01"
}
]
}
# ============ #
$ db.datas.find({ $text: { $search: "2022" } })
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd7"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "ccc",
"identify" : "ccc-1",
"create_time" : "2022-01-01"
}
]
}
# ============ #
$ db.datas.find({ $text: { $search: "2024" } })
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebda"),
"datas_identify" : "liql",
"data" : [
{
"name" : "fff",
"identify" : "fff-1",
"create_time" : "2024-01-01"
}
]
}
/* 2 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd9"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "eee",
"identify" : "eee-1",
"create_time" : "2024-01-01"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
基于全文索引的搜索,有一些需要注意的点,这里做一下记录。
如果查询中有通配符,使用如下方式转义
$ db.datas.find({ $text: { $search: "\"10.6.6.66\"" } })
1
多条件逻辑或
$ db.datas.find({ $text: { $search: "2024 bbb" } })
1
多条件逻辑与
$ db.datas.find({ $text: { $search: '"2024" "liql"' } })
1
如果想要排除某个结果,则用-查询
$ db.datas.find({ $text: { $search: '"2024" -liql' } })
/* 1 */
{
"_id" : ObjectId("621b7bbff00df89221ebebd9"),
"datas_identify" : "eryajf",
"data" : [
{
"name" : "eee",
"identify" : "eee-1",
"create_time" : "2024-01-01"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
golang 检索时的代码
type Data struct {
DatasIdentify string `json:"datas_identify"`
Data []struct {
Name string `json:"name"`
Identify string `json:"identify"`
CreateTime string `json:"create_time"`
} `json:"data"`
}
func FindTest() {
filters := bson.D{}
filter := bson.E{Key: "datas_identify", Value: "eryajf"}
searchFilter := bson.E{Key: "$text", Value: bson.M{"$search": "2022"}}
filters = append(filters, filter, searchFilter)
datas, err := ListData(filters, options.FindOptions{})
if err != nil {
fmt.Printf("get data failed: %v\n", err)
}
for _, v := range datas {
fmt.Println(v)
}
}
// ListData 获取数据列表
func ListData(filter bson.D, options options.FindOptions) ([]*Data, error) {
table := DB.Collection("datas")
cus, err := table.Find(ctx, filter, &options)
if err != nil {
fmt.Printf("find data failed: %v\n", err)
}
defer func(cus *mongo.Cursor, ctx context.Context) {
err := cus.Close(ctx)
if err != nil {
return
}
}(cus, ctx)
list := make([]*Data, 0)
for cus.Next(ctx) {
data := new(Data)
err := cus.Decode(&data)
if err != nil {
fmt.Printf("decode data failed: %v\n", err)
}
list = append(list, data)
}
return list, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
以上就是 MongoDB 中索引的一些实践,一些内容需要结合实际场景进行使用。
以上,便是我个人对于 CMDB 平台建设的全部思考,但愿这对您是有所帮助的。
只有真正身处其中的人才能体会负责一个平台建设的苦辣酸甜,当平台落地建成之后,这种价值与影响是长久且深远的,祝福每一位,在平台建设之路上顺遂平坦!
我们的团队基于各个领域的实践,汇集成了一本《高性能之道--SRE 视角下的运维架构实践》 (opens new window),欢迎大家购买品读。