大规模分布式集中化运维方案设计

版权声明:

本文所有内容均为原创,文章版权归本人所有,任何机构和个人均不得以本文版权为由要求其他机构或个人付费,作者姓名md5值: 97527f2994a8362145f116d6a5189902,本人授权国内所有平台和个人免费转发与使用,转发请注明出处,并附带本条版权声明,禁止用于商业目的。

一、前言

1. 写作背景

2021年,本人入职了某业务规不算太大,但运维工作极度复杂的企业,按行业内通用运维方案来评估的话,此公司需要招至少50名运维工程师方可满足业务需求,但本人在此公司担任运维总监期间,仅招了平均5个人左右就完成了所有运维工作,那么我是怎么做到的呢,本文就来给大家分享一下我当时设计的运维方案。

因能力有限,文中所述方案必然存在某些不足,也欢迎大家积极进行批评指正。

2. 写作风格

本文着重讲解架构设计原理,即:如何用简单的方案实现所需功能,而非纯配置文件或代码的展示,故全文以文字叙述为主,配以设计图,以及少量的代码,不会大量粘贴配置文件,有些细节一笔带过并未完全交代,展示的代码也是修改后才截图,并未经过调试,不保证能直接执行成功。故请各位读者将本文视作参考方案,而非操作手册。

3. 自我介绍

大家好,我是一名有着15年运维经验的互联网老兵,本科毕业于某211大学的计算机专业,毕业后一直从事运维工作从未改行,工作中一直坚持走技术路线,多年来与时俱进,紧跟运维行业趋势,对与运维相关的各种技术几乎都有涉猎。高中时期(2003年左右)就开始自学编程,2009年互联网尚未兴起时就已考得RHCE证书(当时此证含金量还是很高的),4年后又考取了RHCA(此时证书已经烂大街了),近十年来在所任职的企业几乎都是一个人搞定了公司的全部运维工作,擅长利用各种工具加自研脚本的方式配合实现各类自动化。

4. 企业概况

我任职的这家公司的产品基本都是ToB的,但功能是ToC的,客户企业购买我方产品后部署到他们的机房,最终提供给他们的客户使用,这是产品的模式(类似于私有云产品这种场景)。故业务系统开发完毕后,我们要到很多客户的服务器中做本地化部署,每个客户根据其用户量的多少来预估需要多少服务器资源,每家客户的机器规模都不一样。部署完毕后也依然由我们提供运维服务,他们自己的运维团队是不碰这些机器的,所有的运维工作都由我们自己负责。

公司的产品线很多,但每个产品都是这种模式,所以在日常的管理中,我们将每个客户的系统都称之为一个项目。我们实际只有20个左右的产品,但是最终部署到了100家左右的客户的机房内,比如产品一可能部署了10套,产品二部署了5套等等,20个产品线累计部署到了100个客户机房内,我们为了管理方便,每个客户机房我们都称之为是一个项目。

项目服务器通常由客户企业提供,而每个项目都是不同的甲方,不同的客户使用的云平台也不一样,有自建机房用物理机的,有采买私有云的,有用公有云的(什么云都有),也有少量客户自己没机房,由我们提供服务器,他们付费使用,此类客户我们通常是在我们的公有云平台内新建一个VPC部署其业务。而这些客户们对于我们运维团队来说,就相当于是有多个不同的机房,机房之间互相完全隔离,数据互相不通,也禁止数据互通,毕竟不是同一个客户,不能混淆到一起,所以这就给我们的运维工作带来了极大的挑战,运维所需的各种自动化,配置管理,监控报警等,在每个项目中都需要搭建与维护一套,而该企业有近百个项目,即便是修改一项很简单的配置,也会因需要在每个项目都改一遍而造成工作量剧增。例如部署一个监控这么简单的任务,很可能好几个人一个月干不完,因为即便搭建顺利,部署一套基本也得一人日左右,而实际肯定不会那么顺利,很可能这个项目搭建成功了,那个项目因各种原因失败了,毕竟每个机房的网络环境与策略都不一样,实际操作中不一定遇到什么限制。而日常工作中每天都有大量的这种任务,如果按前述方法去执行的话,我们处理需求的速度将永远追不上需求产生的速度,并远远被需求甩在后面。

二、业务环境概览

1. 网络环境概述

先看图:

如上图所示,该企业所有甲方客户的机房几乎都是这样的架构:

A. 纯内网区:该区域所有服务器均无法访问互联网,但是可以和DMZ区互访(需申请权限)
B. DMZ区:与互联网可互访,申请权限后也可以访问内网区的一些服务端口。

每家云/甲方对这两个区的起的名字都不一样,但根据网络权限与是否能访问互联网来划分的话,基本都是如上架构,大部分互联网企业通常也都是这样的架构,区别不太大,可以算是全行业通用的方案吧。

2. 业务部署位置

因信息敏感度不同,有的项目要求服务必需部署在纯内网区,只在DMZ区提供若干台服务器当代理;而有的项目保密级别很低或者不需要保密,此类项目的服务器都在DMZ区,和你使用公有云没区别,事实上这类项目的机房里根本没有内网区。

3. 技术栈选型

由于产品线过多,为了避免到处搭建各种服务,我们所有的业务均采用容器化方式部署。少量业务直接以docker run的方式手动运行,大部分业务均采用在k8s内部署的方式,容器仓库使用自建harbor。K8S的存储既使用了云平台提供的NAS,也有自建的NFS,自建的GlusterFS等,存储方案的演变路径是:NAS + NFS共存 --> GlusterFS --> CEPH

数据库是MySQL和Redis,由于Redis搭建和使用都很简单,所以运维自动化系统也大量使用了Redis作为存储,仅流程及工单等功能部使用了MySQL,资产管理(CMDB)最初用的是MongoDB,后来为了减少维护量,也改成了MySQL。

日志管理采用通用的ELK方案。

监控平台是Prometheus+Grafana,但大部分功能都是脚本实现的,并非使用Prometheus实现,原因在后面会详述。

其他数据库或中间件我们还有很多,但由于改造量不多或没有,对提高运维工作效率没有什么增益,我们仅使用了其基本功能,所以这类软件的使用情况在本文中就不再赘述了,仅在这里提一句。

三、运维工作痛点

我入职时,公司已有近10个项目已上线,入职一周后,发现公司运维工作存在大量的问题,已入职的几个运维工程师们由于各种因素的限制,导致工作效率很低,但每个人却又都很忙,不是假装忙,是真的忙。而这些痛点也并非此公司专有,大部分公司多多少少都有这样的问题,大家也可以对照着看你的公司是否也存在这样的问题。

1. 服务器登录步骤很繁琐

几乎所有企业登录服务器都需要VPN和堡垒机,我们也不例外。但我们与大家不同的地方是,我们的服务器并非全部由我们自己创建与维护,而是由各个不同的客户提供,故而VPN等软件也由客户提供,所以我们在登录不同项目的服务器时,就需要登录对应的VPN,而登录之前,则需要退出其他项目的VPN,多个VPN是无法同时使用的,更何况,有些不同的客户的VPN却是相同的软件,只是配置文件不同而已。这就导致了我们无论因哪个项目的需求要处理,都需要进行VPN切换,并且我们在工作中发现,有些VPN软件(例如深信服的Easy Connect)在多次切换后会导致办公电脑彻底断网,哪也访问不了,只能重启办公电脑来解决,但运维的工作你们是知道的,平时需要打开大量的工具和网页,每次重启都要重新打开和登陆这些工具,这是很麻烦的。

2. VPN账号冲突与密码过期

每个客户通常会提供2-5个VPN账号,并不会提供几十个,这导致在某项目有多个问题需要处理时,我们的运维工程师会“争抢”VPN,如果某人登陆了其他人正在使用的账号,就会导致他人的连接突然中断,如果是在执行某需要长时间执行的命令,他会因突然断网导致命令执行失败,此时不得不重新登陆执行,而这个命令若具有破坏性,则突然的断网很可能会导致某些功能异常,所以最初时我们为了避免此类情况发生,每次登陆VPN之前都需要问所有其他人,某某VPN账号是否有人在使用,必须所有人都反馈未使用以后,此人才敢登陆,这极大了浪费了我们的时间和沟通成本。

有的时候,某个客户的项目可能长时间没更新需求,于是我们就不怎么登陆他们项目的VPN,等再登陆时提示我们密码过期了,需要改密码,改密码又得找客户,他们内部还得走流程,等改完密码已经好几个小时过去了,如果只是上线那到没什么,上线晚点开始就行了,但如果是有报警需要处理呢?这就人为引入了巨大的隐患。

3. 服务器密码混乱

每台机器的root密码都不一样,每次登陆都要先查密码,而密码是以明文记录在wiki某处的,这也不太安全。其次,并非所有服务器都允许root直接登陆,PermitRootLogin有的设置的是yes有的是no,没有规律,应该都是手动临时修改造成的,我们还有3个普通账号,这3个账号至少有一个是有sudo权限的,所以当root无法登陆时,需要使用那个有sudo权限的普通账号登陆,然后再切到root,但几个普通账号里谁有sudo权限也不一定,也需要测试。而无论是root的密码还是这几个普通账号的密码也都有多个,互相混用,哪个账号是哪个密码wiki里记录的并不精确,每次有人登陆失败都可能把密码改成其他的,但是wiki里并未及时更新,时间一长,密码基本就混乱无章了,虽然我们有所有密码的列表,但是哪个是正确的,我们每次都要一个个试过才知道。

4. 服务器登陆耗时过长

由于前3个痛点,我们每次登陆一台服务器都要经历人工独占VPN账号,改VPN密码(不一定每次都有),测试root密码,测试普通账号密码等过程,极其耗费时间,登陆一台机器经常耗时20分钟以上,5分钟以下的情况很少。当业务系统出现告警需要处理时,这是个极大的隐患。

5. 装包困难

当我们费尽精力终于登到某台机器后,经常发现一执行某命令就报命令不存在,command not found, 因为很多客户的系统都是最小化安装,很多常用的运维工具都不存在,连netstat都没有,排查故障时经常受阻。于是只能先安装工具,但很多客户的服务器是部署在纯内网区的,无法使用外部yum源,而他们内网也没有镜像源,我们自己下载好全传进去得好几天(受限于带宽),所以只能缺什么传什么,上传rpm包也是要先传到堡垒机,再scp到最终使用的机器,scp时会重复经历痛点4中描述的场景,需要反复试密码,等我们终于把包传进去以后,安装时又会开始提示包依赖,装这个包需要先装另一个或几个包,于是我们又要去下载被依赖的那些包,再重复上面的过程。再次传进来之后,也许还有会有新依赖要处理。一些依赖关系比较多的软件,安装过程极度痛苦,耗时1小时以上是常态,此时我们尚未处理真正的需求,一直在解决工具的问题。

6. 业务异常时没有监控数据辅助判断

此项痛点分两种情况:

情况一:“没有”监控数据时,并非真的没有,而是有的项目,不允许外部地址访问内部工具地址,也就是说,监控数据已有,但只有在他们内网中才能访问,我们看不到,VPN地址也没有权限,有的客户经过权限申请后可以给我们开通这个权限,有的客户则不给开,这是最坏的情况,而若没有监控数据辅助,我们将很难判断异常的原因。

情况二:也有很多项目,的确是没有监控数据,因为部署一个监控需要很多时间,如果项目少,部署则很快,但我们的项目太多了,根本没那么多时间去部署到所有地方,运维工程师们每天处理各类报警和上线申请就已经很忙了,工作早已饱和。

7. 安全隐患多,整改不彻底

服务器在上线前,通常要做很多安全类优化,例如:

1. 禁止root直接登陆
2. 配置pam模块,使得账号密码连续输入错误5次后就锁定半小时
3. 禁用不常用的服务
4. 不常用用户的登陆shell不允许使用/bin/bash,必需改成/sbin/nologin

等等等等,整改项很多,不一一列举。

每一项修改,都需要耗费大量的人力和时间,在不考虑服务器登陆困难的情况下,完成一项整改也需要很多时间。整改时也可能会因工作疏漏导致遗漏若干台机器,这不是不可能,当你每天都饱和式工作,不到一周你就会头昏脑涨,遗漏是正常的,不遗漏反而不正常。

每次安全检查,都会因上一次的遗漏造成再一次的全盘梳理,即便上一次全部整改完毕,后期业务因规模增长,又新增了几台服务器,那么这几台服务器也很可能发生遗漏,毕竟整改项不是一项,而是几十项,即便有整改项列表,那也不一定能改全,可况当时根本没有。

本次确认已完成的整改,也可能在以后因版本升级造成又不再符合安全要求,例如当OpenSSH的最新版是9.2时,你可以把系统自带的7.2升级到9.2,以解决7.2的安全漏洞问题,而当9.2爆出安全漏洞时,你又需要立刻升到9.3,此前的整改成果就又归零了。

8. 服务器总体使用率没有获取途径

客户方经常会询问服务器的使用率,如果使用率过低,客户则会要求进行缩容以节省成本。每次客户询问当前的总体使用率都是个大难题,每次都要去所有机器查一遍,并手动做统计,监控软件里的数据需要写代码获取,普通运维也就写个简单的shell脚本,这种需要研究API接口的,很多普通运维是搞不定的,而且我们当时的很多项目根本没有监控,所以为了让需求更简单一些,所有项目我们都是手动统计,free看内存,top看cpu,df看硬盘等。而若发生降配的需求,大概率会把两个不同功能的机器合并,机器混用,但软件1部署在机器1中时,我们曾经处理过软件1的包依赖问题,当机器合并后,机器2中并没有软件1需要的包,当时处理过的包依赖问题现在又要处理一遍。

9. 没有资产系统,不知道有多少资源

售后团队由于经常要向甲方汇报业务系统的各种概况,经常需要向运维询问各种数据,总使用率是其一,还有就是我们目前使用了多少机器,有时候还要根据机器的类型,单独分别计算每类服务器的使用率,这些需求都因我们没有基础数据而导致每次都要手动统计,极其消耗人力。最初我们有6个运维,项目不到10个,所以安排不同的运维去执行是可以的,但后来项目增长的太快了,仅靠运维人工去处理已经不现实了,而且手动统计也经常会出错,给甲方造成不好的印象,认为我们公司的能力有问题,连这种基础数据都算不准。

10. 各类信息没有统一收集与展示的工具

例如我们有大量的nginx服务,版本不一,启用的参数也不一样,信息安全组发现某版本爆出安全漏洞后,提整改需求给运维,运维首先就不知道我们有多少台nginx服务器,每台服务器都是什么版本,用了什么参数等等,此整改需求也和统计总使用率一样,都需要一台台登陆处理,最初我们只有几百台服务器,人工处理还应付的过来,后来我们的机器增长到数千台,这种方式也明显无法满足需求了。

11. 没有工单系统,上线申请信息混乱

最初每个上线申请,都是在运维支持群里发布,运维工程师按申请的时间顺序执行,因为项目较多,上线也很多,这也导致了群聊的消息很多,每个上线若有疑问,运维还需要和研发进行沟通,进一步造成群聊消息变多,运维每做完一个上线后,都需要向前翻聊天记录,找到下一个尚未执行的申请,然后回复申请人说我现在开始执行,这样也可以告诉其他运维,此上线已有人执行,你上一个做完了以后继续找我后面的,如若这个运维遗漏了一个,那么下一个运维大概率也不会发现,肯定会从上一个运维执行的那个上线的位置往后翻聊天记录,一段时间后,被遗忘的那个上线的申请人就会找过来,说我的上线被漏掉了,这就显得我们运维很不认真,但实际也不是不认真,可能那个运维当时真的是眼花了吧,毕竟当时的那个环境,每天都太累了,好在我们团队当时有个女运维特别认真和细心,每次都会仔细翻看聊天记录,所以真正被我们漏掉的上线并不多,但这也不表示我们的状态是合理的,整改还是需要的。

再就是,研发提供的上线信息经常给不全,比如项目1的成员申请上线,他不一定会写项目1,他平时只负责项目1,所以日常工作中没必要提项目1,但是运维不一样,运维负责支持全公司所有项目,所以你若不提,我们将不知道你要给哪个项目升级,毕竟很多不同的客户是购买了我们相同的产品,你是想给哪个客户升级呢?你不说我们肯定是不知道的。还有就是各种IP地址之类的信息,要么给错了,要么给漏了,这都会造成升级异常,异常后运维和研发都各自蒙B不知为何,一起排查良久后才可能会发现是漏了机器或者升错了。

12. 运维痛点小结

运维工作中的痛点还有很多,这里就不再举更多的例子了,其他痛点在解决后,提高的效率并没有那么显著,此类整改在本文中就不提了。

另外搭建各种数据库和中间件没有自动化的场景并未当作痛点来陈述,虽然这也是存在的,毕竟所有企业最初的时候都没有这些功能,所以我没有把这种情况当作痛点,而实际上这也算。

下面就开始讲述一下我们都做了哪些改进。

四、运维方案设计

1. 运维机设计

每家互联网企业都有那么一台机器用作运维机,只是大家起的名字各不相同而已,有的企业叫运维机,有的企业叫堡垒机,有的叫中控机,但无论叫什么,这台机器的功能都只有一个,那就是用于登陆本项目的所有服务器,并实现人员的权限控制与管理等功能。本人在任职的这家企业也设计了一个运维机机制,但与前述通用方案又有很大的不同。

因为我们的项目很多,任意两个项目之间没有任何联系,也不允许数据互通,所以我不能设计一个可以登陆所有服务器的所谓运维机。再考虑到监控发报警等需求,有的项目直接连企业微信/钉钉是连不上的,网络不通,所以这台运维机不能直接使用JumpServer等开源堡垒机方案,本机还需要承担很多其他功能,所以这台机器其实就是一台Linux虚拟机,这台机器里面实现的功能很多,这个在后面会详说。

于是,我的运维机方案就变成了如下的架构:

其中:

项目1(左侧红字部分),业务服务均部署在内网区,只在DMZ区中有少量代理机。
项目2(右侧蓝字部分),业务服务均部署在DMZ区,内网区无服务器。
项目N:图中没画那么多,只画了两个场景作为代表。

这里的设计的方案是每个项目都有一台自己的运维机,每个项目的服务器均由本项目的运维机管理,图中是只画了两个项目,实际是100个。而这里的这100个运维机,就有点传统的运维机的意思了,也就是前面说的那个类似JumpServer的功能,但实际又不是,它只是一台Linux,而我们真正的带审计功能的堡垒机则是最顶端的那台堡垒机(黑字),运维团队在工作时第一步要登录的也并不是某个项目的运维机,而是连这个带审计的堡垒机,这台堡垒机中并不包含任何项目的业务服务器,只包含我们的入口运维机,故运维登陆此堡垒机后,就可以在审计模式下登陆各项目的运维机了,各项目的运维机里面也是有JumpServer的,而每个项目的运维机都可以登陆自己项目的所有服务器,所以通过二级跳转,最终就实现了一台堡垒机登陆所有项目所有服务器的功能,同时各项目的运维机之间,服务器与服务器之间,也都没有任何数据交互,同时也实现了审计功能。

这么一看好像我们的场景也没有我前面说的那么复杂,其实不然,下面我就说一下为什么实际环境很复杂:

1.1 解决因客户网络限制造成的困境

每个客户的网络限制都不一样,有的允许我们的堡垒机连那台运维机,更多的客户是不允许的,因为那台”运维机”是我们自己定义的名字,但对于客户公司来说,这台机器与其他机器没任何区别,都是我们使用的业务服务器之一,只是我们自己把它当做运维机不部署业务代码而已。

有的客户同意给我们开通22/TCP权限,于是登陆很方便。但更多的企业是不给我们开放的,他们要求我们登陆必需使用他们提供的VPN账号,而VPN账号数量有限,远少于我们运维工程师的人数,当有工作需求时,我们有几个账号最多就只能登陆几个人,其他人只能看着却无法登陆。再加上我们的客户很多,日常工作中需要频繁地在各个客户的VPN之间切换(断开这个,再连那个),每家的VPN软件还都不一样,在这个场景下,我们最初平均登陆一台服务器需要15分钟,这一点都不夸张,5分钟登进去已经算快的了。

为了解决这个问题,于是,我们引入了如下的方案:

1. 在我们自己公司的公有云帐号里创建一台虚拟机,当作入口运维机,本机前端有公网IP,这里简称为IP-A,后续再提到IP-A也都是指这个公网IP,而这个入口运维机我们则简称为VM-A,后续也会用VM-A来代指这台虚拟机。

2. 每个项目的运维机(简称为VM-B)都申请访问VM-A的多个端口,例如40000-40010等11个端口,VM-A访问各项目VM-B的权限是拿不全的,但是VM-B访问VM-A的权限则是可以的。为了进一步简化方案,那些可以给我们开22权限的项目我们也当做不给开,因为若按是否给开通22权限来定方案的话,我们将不得不维护两套方案,所以我们按照”木桶原理”的方式,无论你是否给我开22权限,我都当你不给开,我只使用没有22权限的方案,即:申请VM-B们访问VM-A的40000:40010端口。这样,我们的方案就变成了只有一个,未来无论新增多少项目,我们都不会因项目的增多而造成方案变更或工作量增加。

3. VM-A的防火墙要严格设置,只允许各项目VM-B的出口IP访问这11个端口。

4. 每个项目分配一个固定端口,使用SSH反向代理的方式在VM-A内监听该端口,SSH命令类似如下:在某项目的VM-B内执行:ssh -CNfR 0.0.0.0:40001:127.0.0.1:22 IP-A,而实际配置中,我们并不是直接连VM-A,而是在他的前面放了一个负载均衡器,并使用这11个端口分别代理到不同的服务器实现不同的功能, IP-A实际则是负载均衡器的IP,而VM-A本身并没有公网IP,它是一台纯内网服务器。在负载均衡器的配置中,我们实际是使用40000端口作为22/ssh服务的前端端口,后端服务器配置成VM-A的22,TCP模式,所以最终执行的真正命令是:ssh -p 40000 -CNfR 0.0.0.0:40001:127.0.0.1:22 IP-A,还有部分项目的运维机由于是在内网区,所以直连VM-A的40000端口是不通的,此时我们会先配运维机VM-B到DMZ区代理机的ssh代理功能,先打通第一段,然后再连就可以了。

这里的40001是分配给这个项目的ssh端口,第二个项目使用40002端口,以此类推,每个项目有一个端口。此例中,执行命令回车后,需要输入VM-A的root密码,随后命令返回,无任何输出,但它已经在VM-A启动了一个sshd进程,监听的就是127.0.0.1的40001端口,当你在VM-A内执行ssh localhost -p 40001时,你会发现你登录到了VM-B中了,在VM-B中执行ps aux | grep ssh你也能看见上面你执行过的那条ssh反代的命令。

5. 有了这样的代理功能后,我们再为每个项目起一个别名alias,用于代替不便记忆的端口号,假设有两个项目分别叫原神与王者荣耀,被分配了40001和40002端口,那么我们会在.bashrc中配置两个alias,内容如下:

alias yuanshen=’ssh localhost -p 40001’
alias wangzherongyao=’ssh localhost -p 40002’

这样当我们想登陆王者荣耀的运维机时,只需要敲wangzherongyao并按回车就可以登陆进去了,敲命令时也不用敲全,输入前缀后按tab键就可以自动补全了。

6. VM-A登陆各VM-B时依然需要输入各VM-B的密码,所以我们先在VM-A中执行:

ssh-keygen -t rsa生成公钥私钥对,再执行ssh-copy-id -p 40001 localhost,然后输入VM-B的root密码,这样就可以将公钥部署到目标运维机VM-B中了,您也可以手动将~/.ssh/id_rsa.pub文件的内容复制到目标机的~/.ssh/authorazed_keys文件中,然后下次再敲wangzherongyao时就不需要密码,直接就进入目标运维机VM-B了。

7. 各项目的VM-B中也通过如上方式生成公钥私钥对,并将公钥复制到本项目的所有服务器内,于是每个项目的运维机就都可以实现免密登陆自己项目的服务器了。此时若某项目有上线需求等,运维工程师只需要先连我们的堡垒机,通过堡垒机进入VM-A,然后敲目标项目的别名缩写即可进入该项目的运维机了,然后再ssh到最终的IP,整个过程不会超过10秒。

于是,我们可以得到如下模式的运维机设计架构:

1.2 解决如上方案中额外引入的合规与审计问题

根据上述方案可知,我们仅在自己控制的入口JumpServer中有审计日志和录像,客户的服务器中并没有,若此客户的场景对合规要求比较严格,则我们必须把录像保存到客户的服务器中,所以我们的JumpServer就变成了两层嵌套的模式,即:在每个项目的运维机中也部署一套JumpServer,只用于管理本项目的服务器。

参数设计如下:

客户运维机VM-B中sshd监听4个2端口(2222/TCP)
客户运维机VM-B中的JumpServer监听5个2端口(22222/TCP)

细节说明:

从VM-A登录到VM-B们,以及从VM-B(1)登陆项目(1)的所有服务器时,既可以使用root用户,也可以使用具有sudo权限的普通用户,前述所有步骤中,包括生成公私钥等操作,都可以使用root,也都可以使用普通账号,对安全要求高的项目可以使用普通账号,要求不高的可以使用root,具体用谁可具体项目具体分析。

登陆VM-A必须通过入口JumpServer,所以无论你通过VM-A登陆了哪个项目的哪台机器,所有操作都会被记录在此JumpServer的VM-A资产的历史记录中。虽然不会单独为每个机器都单独记录,但此情形已实现了审计功能,至于是否按机器拆分意义并不大,假设某天出现了什么故障,怀疑是某天什么时间做升级导致的,想找到执行人询问,只需要根据时间戳,调出该范围内的录像,依然可以查到是谁在那个时间通过VM-A登陆了那台机器。

通常情况下,只要某运维不是想故意搞破坏,你不需要查录像也能得到答案,问一下是谁操作的,那个执行人自己就说了,除非是时间久远大家都不记得了,此时才需要一点点查录像,但这种场景毕竟很少,所以这不会造成工作量增加,反而会因为只需要在JumpServer配置一台机器而大幅减少工作量,并不会因为机器的增减而导致运维需要频繁地更新资产信息或研究API接口开发同步脚本。只有新项目上线时,才需要在入口JumpServer中添加此项目的运维机,仅一台。某项目整体下线时(通常是客户不合作了)才需要删除一台,工作量约等于没有(事实上我们最终的方案连这步也没有,我们是在CMDB中实现的,这个后面再说)

这个嵌套JumpServer的方案,大致就如下方架构图所示:

使用方法:

1. 运维工程师登陆【入口JumpServer】,登陆后可以看到只有一台机器,名字叫VM-A,此vm在【入口JumpServer】中登记的信息是:IP-A:40000/TCP,其中40000/TCP是负载均衡器监听的 端口,轮循到了VM-A的2222/TCP端口,这里其实也可以直接使用VM-A的内网IP+2222端口,但是为了管理统一以及方便本机迁移重建等场景,最终还是使用了前端的LB的IP+端口,没有直连本机内网IP。

2. 登录到VM-A以后,运维工程师有两种方式登录到图中项目的运维机VM-B:

方法一: ssh -p 2222  username@VM-B的IP,此时是直连虚拟机的sshd进程,此虚拟机中的JumpServer未生效,适合代码使用。
方法二: ssh -p 22222 username@VM-B的IP,此时连的是VM-B中的JumpServer,适合人使用。
          登录到JumpServer后再输入VM-B的IP才是登陆到了VM-B内

3.当客户对审计要求很高时,我们优先选择将操作录像记录到客户的堡垒机中,故使用上述方法二,

此时入口JumpServer也会有一份录像,共两份。当客户对审计要求不高时,我们优先选择方法一,只在入口JumpServer中存放录像,这样可以减少对VM-B的磁盘占用,节省资源。此时可规避JumpServer要求输入序号等步骤,可以实现大量自动化功能。

4.自动化脚本直连各运维机的2222/sshd(通过本地40000+N端口转发的方式),部署在VM-A中,于是可以循环连所有项目的运维机,用于采集各类监控数据(说明:但最终方案并不是使用ssh)

5. 当入口JumpServer故障或者VM-A到VM-B的网络中断且短时间内无法恢复时,运维工程师无法从VM-A 登陆到VM-B,此时我们依然可以采取最原始的方式:使用客户提供的VPN,然后就可以直连VM-B的sshd或JumpServer了,此时若直连sshd,相当于又没有审计了,因为两层JumpServer都被绕过去了,所以本机的sshd的防护策略要设置为仅允许本机登陆,不允许VPN的地址登陆,或者改成只监听127.0.0.1也可以,所以此时运维在使用VPN登陆时,只能通过部署在VM-B中的JumpServer登陆VM-B自己,于是有了强制审计。

1.3 其他安全性改进的小细节

客户运维机VM-B中的JumpServer的录像是保存在本机中的,所以理论上运维是可以自己删除的,如果客户的权限限制要求特别严的话,可以将VM-A到VM-B连接时使用的用户改成普通用户,不给普通运维工程师root权限,root权限仅在企业管理层手中(例如运维总监,CIO,CDO等职位),普通运维工程师使用普通账号登陆到运维机VM-B中以后,若想登陆本项目的其他服务器,可以使用ssh root@IP的方式指定用户名,这样普通运维就可以得到除运维机以外的所有服务器的root权限了,同时又无法在运维机里删录像。

1.4 进一步提高SSH登陆速度的改进措施

虽然此时我们可以通过【VM-A --> VM-B --> 最终服务器】的两级跳转登陆了,但我依然嫌弃登陆速度慢,为了进一步简化操作,我们还可以做以下这些优化。

1.4.1 省略ssh命令的用户名

如果为了安全,运维机上使用了普通用户,登陆其他服务器时需要使用ssh root@IP来指定用户名,但我并不想每次都输入“root@“这5个字符,我懒,于是我们可以在ssh的客户端配置文件(/etc/ssh/ssh_config)中添加如下配置:

Host *
  User root

这样配置以后,即便你使用非root账号执行ssh IP,那么ssh也会使用root去登陆此IP

于是普通账号就不需要每次都输入烦人的’root@’了。

1.4.2 取消每次登陆时的fingerprint检查

首次登陆某服务器时,sshd会询问你是否信任和保存它的fingerprint,输出类似如下:

The authenticity of host '[IP]:端口 ([IP]:端口)' can't be established.
ECDSA key fingerprint is SHA256:xxxxxxxxxxxxxxxx一长串,中间省略xxxxxxxxxxxxx
ECDSA key fingerprint is MD5:5f:f1:68:中间很长,省略
Are you sure you want to continue connecting (yes/no)? 

然后你需要输入yes,第二次登陆时就不会再问你了,如果这台机器重装了系统,或者机器删除了,新建的一台机器复用了相同的IP,你再登陆时就会因fingerprint不匹配而登陆失败,所以,我们添加如下的配置,让ssh每次不询问,依然是在/etc/ssh/ssh_config中添加:

Host *
  User root
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null

这样既可。

新增的StrictHostKeyChecking 表示不检查,UserKnownHostsFile 设置为/dev/null会让ssh客户端每次都丢弃获取到的fingerprint信息,这样就不会产生冲突的情况了。

1.4.3 不指定端口号的情况下使用非标端口

如果该项目的安全要求特别特别特别特别严,sshd使用22端口也是不可以的,我们的例子中,sshd监听的是2222端口,所以我们登陆某IP时,要使用-p 2222来指定端口,但是这样我也嫌麻烦,于是又可以在/etc/ssh/ssh_config中继续添加配置,此时我们的配置就变成了:

Host *
  User root
  Port 2222
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null

此时你只需要执行ssh IP就可以登陆目标服务器了,不需要指定端口,默认就是2222

经过以上的改进,你直接执行ssh IP最终就会以免密方式以root身份及2222端口登录到目标服务器。

1.4.4 实现只输入IP地址就完成登陆的功能

我还要继续优化,每次登陆某台机器,只有IP是变化的,ssh命令是不变的,我连这三个字母也不想敲,我很懒,我是否可以直接输入IP就登陆到对应的机器内呢?答案是可以的。

方案是:我们可以使用bash的内置handler功能,即:command_not_found_handle函数,bash的设计是,当你执行的命令不存在时,它会把你执行的命令作为参数传给这个函数,而这个函数的默认行为则是输出【bash: 参数: command not found】这句话,所以当你输入某命令,例如不存在的abcd命令后,你就会在命令行看见如下输出:bash: abcd: command not found,这是redhat系列操作系统的默认行为,在redhat系列系统中,这个函数默认是未定义的,而在ubuntu系列中,此函数是有定义的,ubuntu会把你输入的命令作为参数,传递给一个叫command-not-found的脚本,此脚本会使用apt命令去软件源中查询哪个deb包中包含这个命令,然后它会输出一条安装命令来建议你安装这个包,假设你输入netstat后报command not found,然后ubuntu系统就会把netstat这个命令作为参数1($1)传递给command_not_found_handle函数,然后此函数会调用command-not-found脚本来查询哪个deb包里有netstat这个命令,查到的结果是net-tools这个包里包含,于是ubuntu系统中就会输出一句建议你使用apt install net-tools命令来安装。我们在ubuntu22/jammy中查询该函数的定义可以看到如下输出:

ubuntu22]# type command_not_found_handle
command_not_found_handle is a function
command_not_found_handle ()
{
    if [ -x /usr/lib/command-not-found ]; then
        /usr/lib/command-not-found -- "$1";
        return $?;
    else
        if [ -x /usr/share/command-not-found/command-not-found ]; then
            /usr/share/command-not-found/command-not-found -- "$1";
            return $?;
        else
            printf "%s: command not found\n" "$1" 1>&2;
            return 127;
        fi;
    fi
}

于是,我们想实现这个需求,就可以用这个函数做文章,此函数我们可以这样编写:

function command_not_found_handle()
{
    command=$1
    if echo ${command} | grep -qP ‘^(\d{1,3}\.){3}\d{1,3}$’; then
        ssh ${command}
    else
        echo “bash: ${command}: command not found” >&2
        return 127
    fi
}

此函数会把不存在的那个命令作为参数1($1)传递给这个函数,函数中对这个命令做判断,是否符合IP地址的正则表达式,如果匹配,表示这是一个IP地址,那么我就直接ssh这个IP,如果它不符合IP的正则,那么就输出和系统默认行为一样的内容,并要确保错误信息输出到stderr,返回值设为127,与系统保持一致。于是乎,当我们敲一个IP地址后就会直接登陆进去了,而输入其他不存在的命令时则会报command not found,与操作系统的默认行为完全一致。

还能继续提速吗?能!

1.4.5 两级SSH跳转合并成一级

目前为止,我们登陆一台服务器,需要先登陆入口运维机VM-A,再从VM-A登陆该项目的运维机VM-B,最后再从VM-B登陆目标机,我们能否直接从VM-A登陆到最终的服务器呢?答案依旧是可以,我们可以使用ssh的跳板功能(ProxyJump),既然我们登陆VM-B是通的,那么就可以把它配成SSH跳板,假设项目1的服务器的网段是192.168.1.0/24,那么我们就可以在/etc/ssh/ssh_config文件里加入如下配置:

Host 192.168.1.*
  ProxyJump 127.0.0.1
  Port 40001

这里我们设置是通过本机的40001端口进行跳转,而这个40001端口我们早就用ssh反向代理功能配置成了转发给VM-B的ssh端口,所以这样配置后,在入口运维机执行ssh 192.168.1.XXX就会直接登陆进去了,使用此功能,需要把VM-B的SSH的转发选项打开,否则是无法使用的。

这个功能也是基于ssh的功能做的,所以只要ssh进程不挂,此功能就可以一直正常使用,我们再假设第二个项目的网段是192.168.8.0/24,那么继续在/etc/ssh/ssh_config添加如下配置:

Host 192.168.2.*
  ProxyJump 127.0.0.1
  Port 40002

然后就又可以在VM-A内直接以ssh 192.168.2.XXX的方式登陆项目2的服务器了,此时ssh这三个字母是可以省略的,我们前面已经配了command_not_found_handle函数,所以截止到此刻,我们在入口运维机VM-A中,直接敲目标服务器的IP就可以直接免密登陆进去了。

无论有多少个项目,只要在ssh客户端配置文件里把这段加进去就可以了,需要注意的是,运维机本身不能通过这种方式跳转,因为大家就是通过运维机进行的跳转,运维机若是自己通过自己跳转就会卡死在那里,变成死循环了,假设项目1的运维机IP是192.168.1.88,那么该项目的完整配置就是:

Host 192.168.1.* ! 192.168.1.88
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null
  ProxyJump 127.0.0.1
  Port 40001

这是一个项目的ssh客户端完整的配置,配置中我们排除了运维机自己的IP,所以登陆这台运维机要使用前面说的ssh反向代理功能,即使用:ssh localhost -p 40001命令(有alias)

如果两个项目的网段相同怎么配置呢?答案是:无法配置。如果IP暂时没有相同的,那么你可以一个IP一个IP地配,但如果有IP冲突就没办法了,只能先登陆VM-B,然后再登陆最终的目标机,如果网段冲突的俩项目的IP比较多,即便没有冲突,也依然不建议这样配,因为需要配的IP太多了,即时今天没冲突,将来也有可能有,所以这个场景下还是先登陆VM-B吧。

1.4.6 实现通过ssh+主机名登陆及命令自动补全

我们继续优化,目前为止我们的所有优化都是通过IP地址登陆,但有的时候,我知道某台机器的主机名,但IP我记不住,我不想每次都查IP,能否通过主机名登陆呢?答案继续是可以,如果不可以就不会有这一节了对吧?

实现的原理也简单,我们自己实现一个主机名(不是域名)到IP地址的转换就行了 ,我们在后续章节中会介绍我们的监控数据采集脚本,这个脚本会把每个项目的每一台服务器的信息都存储在运维机VM-B的Redis内,使用的key是hash类型,hkey是机器的IP,hvalue里有很多数据,其值是个json串,串里有一个字段就是那台机器的主机名,脚本的采集频率是1分钟一次,IP地址肯定是不变的,如果谁改了主机名,1分钟以后这里的主机名就会更新了。

于是,我们可以使用这里的数据,实现一个主机名到IP的转换,我们可以写一个函数,名字叫sssh,比ssh多一个s,函数的定义可以放到/etc/profile里面。函数的内容是:

一共就9行,第9行我们先忽略。

我们可以看到第3行要求我们输入一个主机名,第4行则根据这个主机名,调用一个叫sssh.py的脚本去查询这个主机名对应的IP地址,查询方法就是去运维机VM-B的Redis里去查,第5行输出一下查到的地址,第6行就使用这个IP登陆了。

很多机器的主机名很长,也记不住,那怎么办呢?我们实现一个自动补全就行了,比如我们说的k8s节点,你输入一个字母k,然后双击tab键,那么所有以字母k开头的主机名就都列出来了,但可能会有很多,例如k8s-master-xxx(3台左右),k8s-node-xxx(也许有几十上百台),而如果你登陆的机器是kafka-xxx怎么办呢?你输入kafka然后按两次tab键,那么自动列出来的主机名就都是kafka开头的了,用法和系统里的各种命令补全方法是一模一样的,只不过我们这里补全的是我们的业务服务器主机名。

功能效果描述完了,那具体如何配置呢?实现的方法,就是上图中的第9行,使用bash的complete功能,我们设置了【让sssh这个函数使用__sssh这个函数】进行命令补全,我们剩下要做的,就是编写这个__sssh函数,内容也不长,见下图:

脚本一共就8行,其中_get_comp_words_by_ref是bash规定的固定写法,大部分都是固定写法,我们唯一要做的,就是在第6行输出我们的所有服务器的主机名列表,然后bash就会自己完成补全功能了。

大家可能注意到了,第6行的脚本和前面那个根据主机名获取IP地址的脚本是同一个,只不过这里没加参数,上一个使用的地方加了参数,没错,他们就是同一个脚本。此脚本会去VM-B的redis里获取本项目的所有服务器的主机名和IP的对应关系,如果不加参数,就直接返回所有主机名,如果加了参数,那么就只返回该参数(主机名)对应的IP地址。这样,我们便实现了使用sssh命令+主机名(含主机名自动补全功能)进行登录的配置了。

前面两个截图中都使用的那个sssh.py的代码这里就不展示了,那个脚本里其实还有一级转换,有的服务器是有公网IP或者内部的EIP的,跨VPC登录时(例如DMZ区登录纯内网区),ssh登录使用的IP并不是服务器内的内网IP,而是这台机器的EIP,而我们的监控采集脚本是在操作系统里运行的,它采集到的IP地址则是我们的真正内网IP,此时从VM-B登录这个IP有可能是不通的,所以我们使用sssh.py获取到某个主机名对应的内网IP后,还会把该内网IP转换成这台机器的EIP,然后再登陆,没有EIP的项目就不需要这个步骤,总之sssh.py会返回一个从VM-B可以登陆进去的IP地址。

至此,

我们实现了在VM-A或VM-B只敲IP就完成登陆的需求,也实现了在VM-B使用sssh+hostname的方式登陆,这些登陆都是免密的,使用的用户无论是不是root,最终登陆用的都是root,不写端口号的前提下也会自动添加2222或22222端口,在日常工作需要频繁登陆很多服务器的情况下,这些改进措施是可以累计节省大量的时间,也直接大幅提高了整个团队所有人的效率,不是间接。

1.5 运维机功能总结

通过第一章,我们知道了运维机有大量的基于ssh的配置用于提高运维团队的工作效率,每个项目的这台机器也是我们这个项目的yum源(基于nginx搭建),也是这个项目的时间服务器,JumpServer,监控汇总数据库的所在地,自动化管理(puppet)的Server端,harbor的代理端等等,这些功能都会在后面讲解,这里只需要知道一下,我们的所有运维管理及自动化功能都在这台机器上。

每个项目上线后的第一件事也是先配这台机器,这台机器配好以后,其他机器的初始化就是5秒钟的事,执行一条curl http://puppet/init.sh | bash就OK了。

后来为了更简化,我还继续设计了改进方案,即:在我们自己的公有云里面创建3台云主机,搭建一套3master节点的k8s,然后所有项目的运维机都作为这套k8s的node节点加入到集群内,于是,这套k8s就成了运维机专用k8s了,而我们前述的所有服务,都可以在镜像里配好,然后部署成daemonset模式就可以了,以后再有新项目上线,只要把运维机加入到集群内,那么运维机内就会有我们需要的所有服务,比如puppet,比如yum的nginx,比如jumpserver等等。但不同的项目的服务器无法访问其他项目的运维机,网也不通,所以,我们的所有服务都采用nodePort的模式,这样所有服务器依然只需要访问自己项目运维机的内网IP,就可以访问到运行在自己项目运维机里面的docker镜像的监听端口了,而代码部分则依然放在宿主机磁盘内,不然每次更新代码都打包镜像太麻烦了,而且各运维机拉镜像的耗时也会很长,所以代码继续采用以前的方式,只要在k8s内配置hostPath,将宿主机的目录映射到镜像里就OK了,而服务运行期间产生的各种日志、证书请求文件等,最终也都会写入宿主机硬盘,并不在镜像里。

改成k8s模式后,我们在VM-A里面的那个redis其实就可以部署在k8s内了,这样所有项目的运维机就都可以使用service_name.namespace访问到,也就不需要转储脚本,而是可以在k8s内部直接访问了,但这个数据要放到我们的公有云内,不能放在任何一个项目的运维机里,以免项目下线造成被动迁移,这也简单,把master改成允许调度,并在statefulset里面配置节点绑定,让它运行在我们的master节点里就OK了。

此方案只停留在设想阶段,尚未推进,后来就离职了,但这也是个更好的解决方案。

2. 监控系统设计

2.1 解决因客户项目过多而造成的搭建配置任务过多的困境

前面我们说过,搭建一套监控平台并不是个可以很快完成的任务,如果只是搭建服务端那是可以很快的,我们可以使用脚本自动安装的方式进行。但是日常工作中耗费运维工程师大量时间的场景并不是搭建步骤,而是后期的配置步骤,我们搭建完毕后,通常需要立刻登陆进去进行各种配置,这个配置是个繁琐的过程,而100个项目每个都要这样操作一遍,这就不是一个很小的工作量了,同时我们还得集中精神来确保我们在每个项目里的操作都一样。

并且,即便我们做到了,那将来第101个项目上线后,我们又得重复做一遍这个操作,没谁能保证自己每次都不漏,而特意为这种操作写个操作文档也不现实,就像是没人会去仔细阅读100页的微信使用帮助一样,因为这个操作太基础了,谁都会做,不需要看别人的文档指引,但所有人都无法保证每次的操作都一样。

我们的日常工作也不是只有搭建监控这一个任务,如果只有这一个,我们不存在记不住的情况,实际情况中,这类的工作很多,每一种都需要在新项目上线时做一遍,这将会是个O(n的平方)级别复杂度的任务,做为管理者,你不能要求你的员工在任意时间都能准确记住所有事,技术者要有这样一种觉悟,那就是:人的记忆是不可靠的,代码才行。

于是,为了解决这一痛点,我在经历了监控软件选型的短暂犹豫后,最终选择了自己写代码的方式去实现,不依赖任何开源工具,比如prometheus,比如zabbix,等等等等,我一个都不用。

自己若开发一套能采集所有项目监控数据的单一程序,这也不是个简单的任务,我们会遇到各种兼容问题与网络问题,由其是网络问题,好在我们还有运维部署自动化的帮助,这个自动化的实现方式我们在后面再详说,这里只给一个结论,那就是,通过我们的自动化配置管理工具,任何文件都可以通过“只上传一次”就实现自动部署到所有项目的所有服务器内,于是,有这种工具的背景下,我们的监控采集脚本的功能就变成非常简单,它只需要能采集到本机的各类基础数据就行了,不需要考虑其他项目,甚至连自己项目的其他服务器也不需要考虑,它只要能采集到自己的数据就行了,大家只要把各自采集到的数据存到同一个地方,那就可以实现集中化存储及报警的逻辑判断了。

2.2 自研监控采集脚本的实现原理

上一小节我们提到了我们有自动化配置管理的工具,这种工具有很多开源解决方案,常用来互相比较的有puppet/ansible/saltstack/chef,我们使用的是puppet方案,因为这个工具在配置时比较灵活,并且代码即注释,逻辑清晰,一看就懂。puppet只需要客户端访问服务端的权限即可,不需要过多的权限,例如ansible需要ssh权限,saltstack需要访问minion端口的权限等等。在强监管的场景下,puppet方案也许不是最优的,但绝对是阻力最小的。

使用puppet时,我们在每一个被它管理的服务器的/etc/hosts中都要加一条解析,将puppet这个名字解析到puppet服务器的IP,我们的运维机VM-B们就是puppet的server端,所以只要某台服务器作为客户端加入到了puppet自动化中,那么这台机器的/etc/hosts中就一定存在一条解析记录将puppet这个域名解析到了运维机VM-B的内网IP。

于是,我们的监控数据采集脚本在获取到各类数据后,将数据直接存入域名为puppet的这台机器的数据库中就行了,不同项目的运维机的IP各不相同,但每个项目的所有服务器都会在/etc/hosts中将这个域名解析到自己项目的运维机,所以我们的这个采集脚本就直接连puppet这个域名就行了,无须考虑其他。

在此情况下,我们每一台VM-B的运维机中,都有自己项目的所有监控数据,任意两个项目的数据也没有互通,即实现了功能,也满足了合规要求。

运维机VM-B们当中,用于存监控数据的数据库是Redis,并不是关系型数据库,前面我们提过,我们的工具大量依赖Redis,那么我为什么选用Redis呢?

原因如下:

运维机VM-B硬盘资源有限,无法长期存放过多监控数据,故我们在运维机只存上一分钟的数据,下一分钟再采集时,就会把上一分钟的数据覆盖,此监控项只使用了一个Redis Key,所以看不出使用Redis的优势,但我们同时还有很多其他类型的监控功能,每分钟都会产生一个新KEY,以固定前缀+下划线+时间的方式命名,但这种KEY通常只需要保留几分钟,所以此时选用Redis,可以充分利用他的KEY可以自动过期删除的特性,而且也不用像关系型数据库那样建表,不然每个监控类型我们都需要建一张专用表,100个项目依次建表也是个累人的活儿,第101个项目上线后也需要把其他运维机中的所有表都导一份表结构过来,这很费时间,对于我们这种自动化狂热支持者来说,这是无法容忍的,而Redis恰巧可以帮我们解决这个问题。

2.3 自研监控报警脚本的实现原理

在我们自己的入口运维机VM-A中,会每分钟读取一轮所有项目运维机VM-B的Redis,将采集到的数据转存至某外部数据库及入口运维机VM-A的Redis内,每分钟的数据都会存一份,而各运维机VM-B中永远只有一份最新版。这里转存的数据只有CPU内存使用率这类信息,不含客户的敏感数据,所以不涉及合规问题。如果客户很较真,连CPU内存使用率的数据也不让往外导出的话,那就在循环里将这个客户的运维机排除一下就行了。于是我们就可以使用这个数据库做为唯一的数据源,在它的前端搭建各种服务或实现各种功能了。

这里说一下我们在入口运维机VM-A中如何读取所有VM-B的Redis,前面我们说过,每个项目会从1开始编号,为其分配固定的端口用于配置转发功能,例如第一个项目会使用40001端口转发2222/SSH,使用50001转发22222/JumpServer,所以,我们也会使用30001端口转发运维机VM-B中的6379/Redis端口,于是就可以用一个脚本在VM-A中读取所有项目的监控数据了,读取后即可存入我们的后端数据库(通常是MySQL)。

而读取过程中,我们可以顺便判断一下各种资源的使用率是否超过80%,如果超过了,就把对应的信息追加到一个列表中,追加的信息可以包括项目名,运维机的拼音缩写(例如前面提到的yuanshen, wangzherongyao等等一键SSH登录的alias),把这些处理故障时需要的信息全部追加进去。而等这个循环结束时,只需要判断一下这个列表的长度是否大于0即可实现报警,若大于0,则说明至少有一台服务器的资源是存在大于80%的情况的,然后我们去处理就行了,此时不会因为同时出现大量报警而导致收到一堆提醒,使用此脚本时我们只会发送一条报警,而这条报警里包含所有有隐患的监控项。于是,我们巧妙地避免了信息洪水场景下容易漏看报警的痛点。

需要说一下的是,此脚本后来随着环境的演变,最终拆成了两个脚本,一个专门用于检测是否需要发报警,另一个用于将各运维机VM-B的Redis中的数据转存到MySQL及入口运维机VM-A的Redis中,转存数据与发送报警的功能也做了拆分,每一个脚本的功能都进一步简化。由于Redis KEY的名字在各VM-B中是相同的,所以存到同一个Redis后,我们使用了其Redis的代理端口做为后缀。

这样拆分是因为有时候VM-A到VM-B的网络会临时中断,导致所有脚本中都要做异常处理,所有展示页面也都需要处理,这个工作量会很大,部分检测频率很高的监控脚本也会因网络中断而卡住,造成脚本执行超时。还会导致脚本代码过长(超过200行了),这与我们一直秉承的简洁的方式不匹配,故而拆成了两个100行的脚本,这样无论是谁来看脚本代码都能轻易地看懂。

2.4 监控数据画成趋势图的实现方案

监控仅有报警是不够的,我们通常是要画走势图的,前3个小节中,我们并未提到如何画图,这里便详细进行描述。

目前,我们已知所有项目的所有服务器的监控信息,都已经存入相同的一个数据库内了。

此时,相信我不讲大家也知道该如何做,我们只需要搭建一套Grafana之类的软件,并将此MySQL作为数据源,就可以方便地查看所有项目的数据了。

如果想在图像里按项目进行区分,我们可以使用PromSQL的语法来进行筛选,因为每个IP是哪个项目的机器,这些信息我们都会存入Redis,最终都会进入到这个MySQL,所以我们就可以很方便地按项目进行筛选。

我们也曾提过,我们的某一条产品线,可能会因为有多个客户购买导致在不同的地方部署了多套,所以这个产品线的研发团队有时候会需要看所有项目的某服务的监控聚合图,此时我们便可以根据服务名来进行筛选,而不是通过项目名,于是,我们也无需再开发什么工具,工作量大幅减少了。

有些时候,Grafana的功能也许无法满足我们的展示需求,此时我们便可以使用HighCharts/Echarts等开源js图表的方案进行自己画图,而数据则可以直接读MySQL,一条简单的不能再简单的select xxx from table where就可以了,无需研究一堆接口,无需研究数据怎么同步、合并,等等等等,我们的数据库中有一切你需要的数据。

至此,我们的图像已经可以随意的画了,但工具地址是我们公司内部的地址,如果客户想看,那我们是不能给他们看的,因为这里包含我们自己的大量敏感信息,也包括其他客户的信息,何况网络也不通,那么如何在客户需要时,也给他们一个可以看图的地址呢? 做法也简单,在每个项目里,也都部署一套Promethues+Grafana就行了,但不需要做过多的配置,有就行,毕竟100个客户累计一整年也不见得会有人要求看,我们部署这个只是为了万一有需要时我们得能拿得出来,而这俩服务的搭建,我们早在k8s内实现自动化了,项目刚上线时,我们就已经使用备好的yaml文件完成了部署,客户什么时候突然想看什么图,我们现场现做即可,没要求时就不做,反正数据在prometheus里,想看的话随时可以画图。如果客户只是想看到图像,而不是想访问这个链接,那么连这个画图的操作都不需要做,我们只需要在外部的那个唯一的Granfana里选取他们项目的图表,然后截图发给他们就行了,连画图的步骤都省了。

2.5 此方案带来的额外好处

使用以上方案,我们还会有其他效率上的收获,或者我们反过来说,正因为会有那些收获,所以我才设计了这样的方案。

我们先看下监控报警软件的发展历史,在过去的这20年里,监控方案也经过了几轮迭代,从最初的Nagios(含变种Icinga)+Cacti,到后来的Zabbix,再到Prometheus,方案有很多,我们现在一个个看看他们的实现原理:

Nagios是在每一个监控项里都实现了数据采集与判断的逻辑,同时实现报警(通过退出码来定),而到了Zabbix时代“监控项”的含义变了,它只是采集了数据,至于这个数据是否符合预期是用触发器实现的,所以zabbix的“监控项”改名叫“采集项”其实更准确。Prometheus的原理和zabbix一样,也是仅采集不报警,报警使用alert_manager,还有所有同时期的各种exporter,也都是采用这样的架构,国产的夜莺等监控系统也是同样的原理,即:将采集功能与报警功能分开。

但目前为止,这些工具都只是将采集与告警做了分离,并没有将告警与发送进行分离,什么意思呢?无论你用的是哪个开源软件?他的报警逻辑部分的那个组件,在检测到某个数据需要报警时,他都是使用自己的功能发送报警,但我们需要考虑的是,如果客户的环境是纯内网区,报警发不出来怎么办?

于是,我们在真实环境中,根本不使用它的报警功能,而是仅用他们采集数据和画图,事实上我们只部署了prometheus本身,并未配置alert_manager,所有的报警都用自研脚本实现,而脚本也不是在各个项目里面直接发送,而且把要发送的信息以结构化的方式存到本项目运维机VM-B的Redis中的一个队列(list)中,而真正的报警脚本则在入口运维机VM-A中,它会每隔1分钟循环读取所有项目运维机的Redis,看看这个固定名字的Redis KEY中是否有内容,直接用redis list的rpop方式就可以了,如果有,就发报警,无论你使用企业微信接收还是钉钉,都在这一个地方处理就可以了,如果你从企业微信更换至钉钉,或者反过来,也只需要改这个一个脚本就行了,同时不需要考虑网络问题,因为你的VM-A是可以直达互联网的。

如果有些客户的信息敏感度比较高,他们不希望把这些信息发到企业微信或钉钉,我们则可以在这个报警脚本中将这个项目排除,而如何发出报警则可以根据客户的要求单独做,要么使用他们内部的短信通道,要么使用他们公司内部的IM软件等,而这个专用的报警脚本只需要部署在对应项目的运维机里,定期处理那个Redis队列就行了。

2.6 监控系统架构图

我们连讲了5节了,都是原理性的文字描述,是时候放出我们的架构图了,翠花,上图!

现在我们来解释一下:

1. 左下角蓝色部分是项目1,有若干台服务器,我们在图中只画了两台机器做演示。这两台机器中都有我们2.2节中的那个采集脚本,采集到的数据会存入此项目1运维机的Redis中,Redis KEY类型是hash,hkey是IP,hvalue是监控数据,同一个项目中IP不会相同,所以用IP做为HKEY可以避免冲突,项目1在示例中对报警方式没要求。

2. 项目1右侧橙色部分是项目2,与项目1类似,也只画了两台服务器当演示用,但项目2要求必需使用他们企业内部的报警通道。

3. 两个项目上面红色字体的是我们的入口运维机VM-A,这台机器里面部署了我们2.3节中提到的那两个脚本,这俩脚本会将数据存入数据库,以及判断是否需要报警。

4. 右下角则是报警逻辑,用的依然是2.3节中提过的脚本,如果判断结果是需要报警,则会调用公司使用的IM软件进行告警,想要更换也很容易。如果某项目不想将报警信息外发,例如这里的项目2,那么则会使用项目2服务器右侧的那个粉色文字描述的逻辑,在项目2运维机里部署一个为其开发的专用报警工具,直接读取此项目运维机Redis里的数据,并使用他们的报警通道就行了。

5. 现在请看报警功能(紫字)的右上角部分,若检测到需要报警,那么会将什么时候哪个项目的哪台机器发生了什么类型的报警以及报警内容等均存入数据库。

6. 图中画了两个数据库,这是逻辑上的两个,而实际中可以使用同一个,用不同的表就行了,在我们的真实环境中,这俩库确实是同一个库的两张表,这里是为了让逻辑看起来更清晰,所以画成了两个。

7. 两个绿色数据库的上方有两个绿框,分别就是我们的公用grafana及内部各种展示工具了(例如我们之前提过的使用highcharts/echats自己做的图表)

8. 如果某个产品线的研发团队想知道他们的代码在所有客户机房中的报警统计信息,那么就可以使用图中最右上角黑字那里的脚本来统计分析,统计方法也很简单,一条SQL语句就够了:

select 组件字段,count(*) from 报警记录的表 group by 组件字段 order by count(*) desc;

这样就可以查到哪些组件经常挂,哪些组件一直很稳定了。如果某个组件报警特别多,还可以用这个方式查询到底是哪个项目经常挂:

select 项目名,count(*) from 报警记录的表 where 组件=”想查的组件” group by 项目名;

如果查询结果发现某个项目有大量报警,其他项目几乎没有,那说明可能是这个项目的网络环境有问题,需要运维去查机房的环境是否有异常,而如果报警次数比较平均,那就说明与客户机房无关,而是代码有问题了,此时研发就需要去查代码。这些数据也不必每次都手动查询,编写一个简单的网页,读库,然后自动计算和展示就行了。

9. 其实各项目的prometheus也可以通过ssh反代的方式,在入口运维机VM-A内监听一个端口,然后就可以作为数据源配置到公用Grafana内了,正如上图左上角的部分所示。但是这样会给VM-A到VM-B之间的网络造成大量的带宽消耗,网络也不稳定,所以不应该长期使用,而是偶尔使用时临时配置,端口随便选一个未使用的就行,当天用完当天就取消,这样就可以实现各种突发的临时需求了。

根据以上信息,我们还额外知道了每个项目里其实也都有一套Prometheus,这也是我们在运维机VM-B的Redis里只存一分钟的数据的第二个原因,因为Prometheus里已经有180天内的数据了,运维机里就没必要再存那么多了。

以上便是监控系统的整体逻辑,很简单,只有3个脚本,而每个脚本的内容也都不长,不到100行,我们仅用了3个简单的脚本,就实现了所有的功能,并且无论是100个项目,还是1000个项目,我们都不会增加任何工作量,当新项目上线后,通过puppet自动化,会将此项目的每一台服务器都部署2.2采集脚本,并将数据存在此项目的运维机Redis中,然后VM-A中的脚本就会循环完成后面的逻辑了,运维机的列表也是存在数据库中的,故VM-A中的这个脚本也无需做任何修改,他内部只有一个循环,不会受项目数量增减的影响。

最后,我们上代码吧,看看真实的用法,当然,脚本是有修改和删减的,一是为了删除敏感信息,二是因为脚本里还有其他功能尚未讲解。这里可以小小透漏一下,我们的报警部分,最终并不是只发消息,而是会同时在工单系统中创建一个处理报警的工单,这个后面再说。

2.7 监控系统真实脚本演示

2.7.1 数据采集脚本

这个脚本的写法很简单,并不太长,例如我曾经使用过的脚本大致长这个样子,自2017年左右开始,一直用到了现在,几乎没怎么改过,只有换工作后会根据新公司的环境稍作修改,核心采集部分代码几乎没变过。因截图长度的限制,脚本代码分成了两张图片:

图片1:代码的前半部分

释义:

1. 第一行from devops这是我们的公用函数库,里面有连接redis之类的功能代码。

2. 每个项目的所有服务器的监控数据都会放到【资源使用率】这个Redis Key中。

3. 每台机器的数据,hkey是其IP地址,hvalue是此脚本中的hostinfos这个字典(dict)。

4. 我们会将需要采集的数据都放到这个dict中,最终json.dumps后hset到redis中。

5. 您可能注意到了我们采集了/etc/motd文件的内容,这个文件里的内容会在你ssh登陆后自动显示,我们通常会在这里面写一些只有本机才有的特殊情况,这样每次登陆时都会看到提醒,把这个信息采集出来会进到我们的资产系统(CMDB)内,资产的备注部分就会显示这里的数据。所以如果某台机器有什么特殊情况,只要我们写到这里,无论哪个运维登陆都能看见,所有运维和研发在CMDB内也能看到,数据唯一且实时更新(1分钟一次)。

图片2:代码的后半部分

释义:

1.前三段主要是在采集TCP连接数,磁盘IO信息,网络带宽等数据,如果你的场景需要采集更多其他数据,继续在这里新增采集项就行了。

2.采集的最后我们增加了一句hostinfos[‘update_time’]=current_time登记本条信息的采集时间,这是为了让大家在监控系统内看到这条数据是什么时间采集的,默认是1分钟一次,所以不会有哪条数据的采集时间在当前时间的2分钟以前,如果有,我们会把页面里的那行文字的颜色改成橙色,以此来告诉使用者,这条数据上一分钟因故未更新,你看到的是脏数据,不是最新的。而运维机VM-A也有另外一个脚本会对所有服务器(不区分项目)的这个字段进行检查,如果发现时间间隔超过120秒,则会有报警发出(实际也是在ticket系统中自动创建一个报警处理工单),当然报警信息也会发,但由于不是紧急故障,所以不会与那些紧急类的报警发到同一个群,各种场景发送的群是不一样的,部分优先级很低的报警也会采用邮件,不使用IM软件。

3.如果某一台服务器下线删除了,它的信息是不会自动从redis的这个hash中消失的,所以我们以主机名做判断,如果是运维机在执行这个采集脚本(运维机自己也需要被监控),则循环判断一下目前所有已登记进来的机器的时间戳是否已超过1小时未更新,如果是,说明这机器已经没了,那么就自动将其删除。代码里的rc是redis client的缩写。

2.7.2 数据转储脚本

数据转储脚本实现的功能是:

功能一:将VM-B们的Redis中的资源使用率的KEY原封不动的在VM-A中存一份,KEY的名字会加上Redis的转发端口做为后缀,最终真正的监控报警脚本只会读取VM-A的Redis中的那些KEY,不会去连各个VM-B,以此来规避网络中断带来的报警脚本卡住的风险。

功能二:使用Redis KEY中的json格式的数据, 生成MySQL命令并执行,将此数据存入我们的关系型数据库中。

脚本内容大致如下(有删减):

图中脚本只有27行,实际的真实脚本有70行左右,也不长。

被删除的那些行的功能包括:中文编码的转换,异常的处理等等,这里凡是有可能报异常的语句在实际脚本中都是放在了try里面,并不是直接这样写的,但是那些语句对理解这个脚本的逻辑没什么帮助,所以在截图之前,我把那些代码行删掉了,这样这个脚本看起来就简单多了,不过即便我不删,相信你也看得懂,毕竟还不到100行。

存监控数据的Redis KEY在所有项目的运维机VM-B中都叫【资源使用率】,转存到入口运维机VM-A的Redis后,分成了多个KEY,有多少个项目就有多少个KEY,这样分KEY也是有原因的,等我们讲到运维管理平台时再说这个事。这里我们只需要知道,这些KEY到了VM-A中以后,分别叫:【资源使用率_30001】【资源使用率_30002】等等,以此类推。

2.7.3 监控报警脚本

这个脚本我们只截取一部分,释义如下:

1. 整个脚本的逻辑都处于一个循环内,由于循环体很长,实际的脚本是用了函数,这里为了演示逻辑,把函数删掉了,直接把功能代码放到了循环体内,由于截图里的内容是为了演示用,而不是最真实的原版代码,所以不确保能执行成功,出现手误打错变量名也是可能的。

2. 代码第22-23行是根据资产系统里的数据,获取当前正在检测哪个云上的哪个项目的使用情况,如果有需要报警的条目,则云平台名称与项目名称都会像第30行里写的那样,填进最终的报警信息里,这样运维工程师看见后就可以知道是哪个项目的哪个机器在报警了。

3. 第32-35行是在随机选取运维工程师和数据库工程师,因为脚本的后面会为报警项自动创建工单,此工单需要有执行人,而执行人就是在这里进行选取的,选人的函数会读取值班表,判断当前谁在岗,然后从在岗的人里面选一个(我们是7x24排班,所以不一定谁在)。(创建工单的接口还会根据报警的类型自动进行计数和统计,用于定期发运维监控报表)

4. 从37行起到最后就是在判断硬盘/内存/CPU的使用情况了,判断一下是否需要报警,需要注意看一下第47,57行,这里调用了一个叫gen_mon_task的函数,这个函数就是调用ticket系统接口创建工单的函数,三个单词的全程是generate monitor task,此函数会根据此工单是否已存在来返回不同的值,如果工单已创建,下一轮监控检测时就不会重复创建工单了,因为监控脚本是每分钟都执行的,但不能每分钟都创建一个新工单,所以会在这里做判断,还需要交代一个小细节,如果工单已存在,那么他的内容是会进行更新的,比如/data分区首次报警时使用率是80%,下一轮检测时变成了82%,那么工单的内容也会跟着变成82%,当然了,这个修改也会在对应的工单下方以变更日志的形式展示,此工单的执行人会看见是报警脚本修改了工单的内容,修改前是什么,修改后是什么,这些都会记录,这个功能我们将来在运维管理平台中说明,这里就不说太多的细节了。

5. 我们看到第54行有个设置某个KEY的值为80的语句,第56行又将其过期时间为299秒的语句(5分钟-1秒=299秒),这个是用于设置报警阈值,默认我们是80%以上才会报警,但有的时候有的机器超过80%可以忽略,90%再报警就行了,所以我们的运维管理平台内有个页面会去统一展示这个Redis KEY的值,并允许修改,允许的修改范围是70-100,如果设置为100就表示这个监控项永远不报警,因为它的使用率永远不会超过100%,如果这台机器的使用率低于设置的值了,说明此报警已经恢复了,此时监控脚本则会把这个KEY删除,它自然也会从阈值管理页面中消失,所以管理页面里你能设置报警阈值的只有那些已经超过80%的监控项,当它报警了,你觉得这个情况可以忽略,于是就可以手动调高它的报警阈值,有了这个功能以后,我们就不需要在脚本做各种差异化设置了,也不用像直接使用alert_manager那样,做一堆判断,配置文件上千行,光看着都头大。设置阈值时不仅可以设置阈值的比例,还可以设置生效的时间范围,这个也后面再说。

6. 第68和72行我们看到我们还在contents这个列表里追加了几个链接,这个链接就是我们前面提到过的自己用highcharts/echats做的图像展示,我们只有这样自己做工具,才能拥有精确的链接地址,于是报警发生时,我们直接把监控图的链接带进去,然后直接点击报警信息就可以看到他的走势了,页面里我们还可以选择时间范围,精细度等指标,这些也依然放到运维管理平台章节细讲。

7. 以上便是报警的逻辑,如果所有服务器都正常,设置报警阈值的页面里会是空的,代码中contents这个列表也是空的,contents == [] ==> True,而如果contents不为空,我们将会把它的内容,根据业务的逻辑,发送到对应的聊天群里,以这种方式通知相关人员,通常每个产品线都会有一个单独的群,按产品线划分,不是按项目分。而这个报警已经按释义4中说的那样,自动生成工单了,而且也设置了执行人。当该产品的研发在对应的群里看到报警后,就可以主动找这名运维执行人一起排查错误了,这个过程不会影响任何其他人,其他运维的工作不会受到打扰。该运维工程师处理的这个报警的任务,也会自动出现在Ta的工作周报中(工作周报也是运维管理平台中的功能,工单类内容会自动填,无须运维工程师自己动手写)。

以上便是监控报警脚本的全部逻辑。

3. 资产管理系统(CMDB) 方案设计

每家稍微具备一定规模的企业,IT运维团队都要使用一套资产系统,用于登记各种资产信息。运维要登记统计的,通常就是我们的服务器信息,但我这里设计的资产系统,并非大家熟知的实物资产与软件资产的管理系统,而更像是数据的管理系统,所有的数据都可以称之为是资产,数字资产,而这个系统要实现的功能,就是各类数据的增删改查。

任意两个类型的资产在本系统中所需的功能都是完全一样的,所以在代码层面,只需要按类型分成不同的表即可,无需开发过多功能,所以这个系统的组成就简单的只包含如下几个组件:

1. 资产展示页面(可按传入的资产类型展示对应的资产)
2. 资产新增(含修改和删除)功能页面。
其实到这里,增删改查功能已经全有了。
3. 资产信息修改日志展示页面,可以全展示,也可以按资产ID只展示某资产。

没了,功能的设计就这么多,超级简洁,所以代码量也很少,我当时不到半天时间就写完了,后来还在github里建了一个项目叫SimpleCMDB,把所有代码都上传进去了,大家有兴趣的话可以自己去搜。

最早的版本中,这个资产系统使用的数据库是MongoDB,选它是因为不用为每个资产都单独建表,我一共就用了两张表,第一张存资产数据,第二张表存资产字段。某一类型的资产的增改页面会显示哪些字段完全取决于它在第二张表里设置了多少个字段,如果在表2中新增一个字段,那么增删改查页面都会自动出现此字段,如果某字段从表2中被删除,那么所有页面也都无法显示和删改此字段,但此字段的数据依然存在,并没有被删除,只是看不见了而已,如果把该字段再重新加回表2,那么显示页面依然能看见以前登记的数据,并不会像MySQL那样,删除一列后,所有数据都跟着没了。

资产数据的表(表1)其实也可以分成多张表,用【asset_资产类型】来区分就行了,但我当时为了图省事,资产类型用的都是中文,所以无法用于表名(mongodb::collection name),所以就直接用一张表了,但这并不影响使用,除非资产数据量很大,此时分表很有必要,否则只用一个表也可以,但这并不是一个好习惯。所以虽然我只用了一张表,依然建议大家要分表,万一以后数据突然大涨了呢,我当时只是图快了。

在此设计方案下,如果想新增一个资产类型,只需要在类型管理页面进行修改即可,此时相当于只在修改表2,在里面新增了类似如下的内容:NewType:[‘Column1’,’Column2’,’Column3’]等等, 我实际中用的都是中文,对不起。

后来,我把这套系统做了修改,发布了2.0版,这次是按资产类型进行了分表,不再是把所有数据都放在同一个表中了,而且数据库也从MongoDB改成了MySQL,这样改是为了让各类监控脚本的复杂度降低,毕竟代码连接MySQL要比连MongoDB简单的多,由其是安装MongoDB的Python开发包是个很让人头疼的活儿,Python2和Python3还不一样,CentOS8默认不支持Python2, CentOS7默认不支持Python3,你说这烦不烦人,所以辛苦我一人,可以让团队所有人都更轻松,于是,我就改了,从此每新增一个类型的数字资产我们都需要新建一张表,但这也不费多少时间。

每张表的结构都是类似的,都包含以下字段:

asset_id 资产ID(全局唯一)
asset_name 资产名称
column1 字段1
...每个类型的资产会分别设置其需要的字段,不同类型资产的字段不一样,怀念MongoDB
columnN 字段N
comment 备注说明
create_time 创建时间
update_time 修改时间
expire_time 过期时间(当数据有效期已过但暂时不能删,这种场景会使用本字段,偶尔用)
delete_time 删除时间

每个类型的资产会分别设置其需要的字段,例如:

物理机资产:我们会登记品牌、型号,产权,价格,买入渠道,CPU,内存,硬盘,GPU,RAID模式,管理IP,机架位置等信息

虚拟机信息:登记CPU/内存/硬盘等常用信息,以及所属云平台,所属账号,归属的项目,使用人,其在公有云中的资产ID及地域可用区等等信息。

还有很多类型,例如机房、机柜、云平台、供应商、联系人、等等等等,我们都将其视为资产类型之一并单独建表,然后在系统里对数据进行增删改查,然后各类型资产可以相互引用对方的资产ID,例如某公有云的商务若是换人了,直接在联系人中修改即可,然后机房页面的商务联系人字段就自动显示新数据了,所有类型资产的数据互相组合成了一套庞大的资产系统,但这个系统的代码量却没有多少,即便我花时间把数据库改成了MySQL,也只是给我一个人增加了不到一周的工作量。

3.1 服务器资产管理

我们的服务器资产,既包含自有物理机,也包含自有云主机。还包含客户的物理机,客户的云主机,此时我们如何管理呢?

我们的物理机是需要单独管理的,而我们的虚拟机、客户的虚拟机、客户的物理机,这3种类型对我们运维来说是没区别的,都是业务系统,至于你是不是物理机我们并不关心,我们只需要知道操作系统里面有什么就行了,只有我们自己的物理机才需要考虑硬件的信息,而客户的物理机就当它是虚拟机好了,所以,我们的服务器资产,便按这种思路,分成了两类资产:虚拟机+物理机

自有的物理机我们就不说了,这个场景毕竟简单,我们在这里说说虚拟机(含客户的物理机)

这些机器的列表是频繁变化的,每个项目今天新增10台机器,明天下线5台机器,这些数据是一直变的,无论如何登记,总免不了遗漏,如果设计一套申请与归还(含删除)的流程,那又是很大的工作量,只要有一处错漏,我们的所有统计数据就会都不准确,相信很多企业的资产系统里都有很多已经不再存在的机器了吧?要么忘删了,要么删除失败了。有时候还会发现有一台机器是存在的,但资产系统里却看不见,我不希望这种情况发生,所以,我自然不打算采取这种方式。

还记得我们在监控系统设计中提到的数据采集脚本吗?以及我们提到过的配置管理自动化。

我们所有的服务器,无论什么云、什么机房、什么项目,机器上线使用之前都会用我们的自动化部署2.2数据采集脚本,而这个脚本会把各项目的信息采集到本项目运维机VM-B的Redis内,然后入口运维机VM-A每分钟会循环读取所有项目的运维机VM-B的Redis,将各项目的数据读出来,然后转存到我们的那个MySQL库中,同时也会在入口运维机VM-A的Redis中存一份,Redis KEY使用的是【资源使用率_Redis转发端口】,我们有这个同步机制,想起来了吗?

现在回忆完毕,新增信息如下:我们的同步脚本,不但在这两个地方存了数据,还会调用资产系统的API接口,将同步过来的数据向资产系统也存一份,以【云平台+项目名+IP地址】作为唯一联合索引,数据存在就更新,不存在则创建,MySQL的语法是insert into .... on duplicate key update,其他数据库中有upsert类似的语法,我们以这种方式,将数据全部自动登记到CMDB中,所以永远不会有任何遗漏,且新机器跑完自动化以后不到5分钟,他的信息就已经出现在资产系统中了。

我们的自动化以及数据转储脚本的同步状态都是有监控的,所以只要这个脚本没报错,资产系统里面就绝对不会缺数据,但我们有可能会有多余的数据,当某台服务器下线时,他并不会自动从库里消失。所以在运维机VM-B的Redis中,我们在同步脚本的最后做了判断,如果某台机器长时间不更新,我们会将其从redis的hash类型的key中hdel,删除后,这台机器就不会继续更新我们资产系统里面的数据了,那么,它的update_time字段也将不再变化,所以,我们在后台跑个简单的crontab,如果update_time字段超过一段时间没变化,就自动delete from table就行了,我们设置的时间是1天不更新再删除。误删场景:如果某台服务器关机维护超过1天,它相当于被误删了,那也没关系,等它重新上线后,自动脚本会将它重新加回来的,而他以前采集到的监控数据是不会消失的,因为数据在监控的表里,不在资产的表里。顺便说一句,资产系统和监控系统的数据库也是同一个库的实例,使用了不同的库而已。

当服务器真的下线时,他的监控数据也会在数据库里继续存着,不会立刻删除,以便随时复查老旧数据用于各种场景,我们对监控数据的删除有单独的逻辑,也是不区分项目,不区分机房等,只根据update_time字段,每天删除180天以前的数据,所以某台服务器今天下线了,那么它的监控数据每天都会被删一点,删的是180天以前记录的,直到180天以后,这台机器的数据才会永久消失。如果某个项目要求保留的时间不一样,那么在删除语句里增加一个where判断就行了,也是几分钟的工作量。

还要说一下的是,删180天以前的监控数据的脚本,删1天无更新的资产的脚本,这俩脚本实际是同一个,这个脚本里还有删除其他数据的逻辑代码,目前尚未提到过,但所有代码加起来也不超过20行,并不复杂,脚本内容大改长这个样子:

delete from asset_vm_资产表 where update_time > 1天;
delete from 监控数据表 where 项目名 != ‘项目1’ and update_time > 180天
delete from 监控数据表 where 项目名  = ‘项目1’ and update_time > 365天

后两条语句实现了【项目1】的监控数据保留1年,所有其他项目的数据保留180天。

代码我就不贴了,里面基本就是如上语句的列表。

下面我们看一下效果吧,截几张图瞅瞅:

不贴图了,很多信息都需要屏蔽后才能截图,都打马也看不出什么效果了,页面的模样你们自己脑补吧。整体结构是页面顶端是筛选框,下面是个大表格,表格第一行是标题,标题的名字可点击,点谁就按谁排序,大概这个样子。涉及外部信息时,某格子里的文字则是链接,点击后会跳到对应的页面。

3.2 各类元数据的管理

前面我们说过,每一种数据,我们都当做是一种数字资产,创建一个表,然后用上述的通用CMDB工具进行管理,于是,我们的资产登记便变得异常简洁,我们通过自动化方式登记了大量可通过代码采集到的数据,也使用手工方式登记了很多无法用代码获取的数据,举例来看:

手动(Manually)登记的部分:

M-3.2.1 机房信息

我们登记了机房的地理位置,总带宽信息,付费日期等,展示页面还会从联系人的资产表中自动加载本机房的联系人信息,服务器数量一列会从服务器资产表计算本机房有多少台服务器,然后显示在这里,于是我们在本页面就可以看到本机房的所有数据,其中服务器数量之类的数字型的数据都是访问时实时计算的,文字类的信息则由运维手动更新维护。

M-3.2.2 VPN账号

每个机房都使用什么VPN软件,拨号地址是什么,用户名密码是什么,最后更新日期是哪天等等,如果一个月不登录密码可能会过期,下次使用必须找客户改密码,这样很麻烦,于是我们就可以使用这个字段来判断,如果快满一个月了,就自动创建一个工单,要求运维工程师登陆一下这个账号,仅登陆一下就行,工单点击完成后自动更新此字段,下个月再自动创建下个月的登录任务,这样就不会出现账号密码频繁过期的情况了。也不需要人工进行定期或不定期的统计,代码全自动追踪及安排任务,连运维团队管理者也不需要参与催促,全靠代码完成。

M-3.2.3 供应商及联系人等类型

登记他们的微信,手机号,QQ等信息,他们提供哪个我们就登记哪个,如果给了名片,还可以拍照上传,这里显示一个名片的图片。

M-3.2.4 数据库信息

这部分信息是手动登记的,不是自动,这一点也许大家会很意外。因为数据库服务器并不是各自独立存在,而是多台服务器一起组成集群,那么谁和谁是一组,用代码判断很困难,比如我给你5000台机器让你用代码分析哪些机器是MySQL,哪些是Redis,而且用户名+密码与IP的关系也没有,代码是很难实现的,而这个信息又不常变,所以我们采用手动登记的方式。

于是,我们又可以继续使用这里登记的信息做其他自动化了,比如备份,我们可以在这里设置备份的频率和时间,备份使用的用户名和密码等信息,每一套集群/副本集的账号密码都是本资产页面里的一列,所以备份脚本就可以只写一个,只要循环处理这个资产列表就行了,IP+用户名+密码都是变量,所以我们的数据备份工作,最终就只由一个脚本来完成,而不是到处都有到处都不一样,进一步简化了工作。当然了,不同类型的数据库的备份脚本是不能通用的,实际工作中我们有3个脚本,分别用于备份MySQL,MongoDB,PostgreSQL。

有了这个资产数据以后,我们还可以在运维管理系统里面提供数据库查询工具,研发人员只需要在数据库集群里选择即可,不需要他们自己整理和提供IP之类的信息。只读账号在资产系统里面也已经登记好了,但直连数据库是连不上的,需要配转发,而且查生产库也需要审批,所以运维工程师可以在收到这个工单后,确认当前网络无异常后,点击审批同意,后台脚本就会自动配一个转发,然后把转发的端口登记到Redis中,然后点击同意,此时研发就可以连到生产库进行查数据了,此功能不可以长期开放,申请时是需要填有效期的,所以自动配转发的脚本要给转发命令加一个超时参数,通常是使用timeout -s kill XXX秒,到期就自动断开,而那个记录转发IP端口的Redis KEY也可以设置同样的过期时间,时间到了KEY就自动消失了,查询工具检测不到对应的KEY就会认为此人没有查询权限,页面直接返回您没有查询权限或者权限已过期。整个过程完全无人参与,仅需运维与研发上级进行工单审批,运维审批时看一下网络状况就行了,比如若该项目此时的带宽占用比较高,在无法确认研发会查多少数据的情况下就可以暂不审批,后期也可以通过限速的方式实现,但这都是后话了。

M-3.2.5 其他手动登记的信息

各种平台的管理员账号,密码,绑定手机号,手机实名的人是谁,各种备案的信息,调用云平台API接口使用的只读API KEY,等等等等吧。手动登记的资产信息并不多,主要就只有上面提到的几个,大部分数据都是通过代码自动采集和登记的。

自动(Automatically)登记的部分:

自动登记的信息有很多,我列一些,以及简单说一下这些数据采集登记后我们用它做了什么其他功能。

A-3.2.6 调用公有云API接口使用的KEY

所有公有云都有API,我们从资产系统里拿到各个云的KEY,循环去各平台获取资产信息,其中也包括云主机的信息,我们前面说的2.2小节里的数据采集脚本只能采集到操作系统里面的信息,而操作系统之外也有一些信息需要登记,例如本机的硬盘类型是机械盘还是SSD,所属的VPC是哪个,属于哪个账号等等,这些信息则是在这里获取的,登记到同一张表的同一行数据内,不同的字段。所有字段都会在资产页面显示,可以筛选显示哪些字段。

A-3.2.7 域名列表

研发团队经常想知道哪个域名解析到了哪些IP,或者想动某台服务器,想知道哪些域名解析到这个IP了,于是我们就可以使用DNS的API接口,把所有域名和IP的对应关系抓取下来登记到这个页面里,研发随时可以自助查询,不需要运维参与。

A-3.2.8 操作系统中的证书

很多业务都需要使用证书文件,例如操作系统的/etc/pki目录中有很多证书文件,k8s也有很多证书文件,nginx配置web服务时也使用了很多公司域名的证书文件,这些证书早晚是会过期的,过期后哪些服务器进行了更换哪些没换呢?很多企业都有这个痛点吧,经常突然哪一天某个系统就不能用了,一查发现是证书过期了,而新证书早就买了,只不过这台机器里的证书文件是旧的,没有替换或忘记了替换,每次遇到这种场景,都需要一台台排查,耗时耗力又容易遗漏,既然第一次更换时有遗漏,第二次梳理时也同样可能遗漏,就算没遗漏,如果是在过期前一天才发现呢?更换根本来不及,所以我写了一个也很短的脚本,循环读取几个固定放证书的目录,检测过期时间,并登记到资产系统中,然后就可以使用第二个更简单的脚本来循环检测这个列表,看看这个列表里的证书是否即将过期,如果是,调用ticket系统接口,自动创建工单,然后就会有运维工程师进行跟进更换,更新完毕后,下一轮脚本运行时,它的数据就会自动更新到资产系统里了,报警也会立时消失。而资产展示页面中,我们则可以按过期时间排序,这样便可以一眼看到所有服务器中的所有证书的最早到期时间是哪天。整个过程也全部是自动化,无需人工参与。

3.2.8的这个脚本里没有敏感信息,所以我们来看看真实的代码吧:

这就是我真实使用的检测代码,截图前我只改了几行参数处理的部分,脚本一共就只有37行,还包含了空行,短吧?

下面我们来做一下释义:

1. 第8-9行我们可以看到我们只配了k8s的目录,并没有配nginx的目录,并不是nginx有第二个脚本,如果也使用此脚本来监控nginx的域名证书的话,我们是没必要写成两份的,前面我说过我是个自动化狂热分子,我是不会干这种事的,至于nginx证书怎么监控的我们后面再说,这里您只需要知道只有k8s的证书需要这样监控,所以我们才这样配置。

2. 并不是每一台机器都是k8s的机器,所以第12行做了判断,如果不是k8s节点,那么此目录就不存在,那我们就退出脚本好了,脚本和没运行没区别,所以此脚本可以部署到企业的所有服务器内定期运行,无论它是不是k8s节点。如果某台机器曾经是k8s节点,但是现在不是了,此时这个目录存在,且含有旧证书,监控能检测到,但这属于“误报”,怎么处理?首先,你机器改功能不重装系统就不是个合格的做法,否则是不存在这种情况的,第二,如果这种情况已经发生了,报警时ssh进去把那个目录整目录rm -rf就行了,反正这目录里的文件也已经没用了,所以,用这种方式,基本就可以实现一个脚本检测全公司所有业务的所有服务器了。

3. 第32行我们可以看到,我们是把检测结果写到了redis中,实际就是运维机VM-B的Redis,我们第4行中import的那个devops模块里会使用redis.Redis(host=’puppet’)的方式连到运维机的Redis,所以各种采集脚本直接使用redis connection client就行了(代码里此变量名简称为rc),这也是应用了我们在监控系统设计章节里讲过的【采集与告警相分离】的策略。一是可以规避各项目的网络限制问题,二是可以在报警脚本中循环检测所有项目的信息,最终在有异常情况时只发一条报警,而不是呼啦啦发一堆,如果单独发,假设你的k8s集群有100个node节点,你就会收到100条报警信息,下一轮检测时又会发100条信息,你会被这种垃圾信息淹没的。

现在,所有项目的k8s证书的过期时间及文件路径我们都采集好了,并登记到了资产系统内,我们既可以在运维管理平台网站内肉眼看到,也可以使用报警脚本进行检测及告警和自动建工单,那么这个代码长什么样呢?

看图:

依然是很短,只有53行,我们再来做一下释义:

1.第6行,我们设置如果有效期有少于4000天(约为10.96年)的证书存在就报警,这是因为我们把所有证书的默认有效期都从1年改成了100年,所以不存在有效期少于11年的证书,4000天这个告警阈值是我随便写的,如果你的公司只使用默认的1年证书,你可以把这个值改小,例如你希望还剩3个月到期时再报警,那就可以把它改成90,我这里的4000只适用于我们这种改过默认有效期的场景。

2.第33至49行其实就是报警功能了,之前我们一直说有独立的发告警功能,但是从来没展示过报警代码,这里是首次展示,内容依旧很短,就是个固定格式的函数,这个函数会去调用企业微信/钉钉/飞书等的接口,所有的报警脚本都调这个函数就可以了,脚本里还使用了msg_from字段,把每一条报警信息是哪台机器的哪个脚本报出来的也记录进去了。

3.第36行我说没统计哪个IP属于哪套K8S,是因为当时太忙,着急把功能写完然后上线部署,所以就没写,后来就忘了,因为首次报警后我们把证书改完,以后就再也没发生过证书即将到期或已过期的情况,而新搭建的k8s默认都是100有效期的证书,所以这脚本直到我离职都没再发出过任何告警,所以我也就不记得了这里还留下过一个小缺陷没完善。

4.整个脚本都是监控的主逻辑代码,如果有证书即将过期,按图里的设置,我们将会把这条信息发到【监控报警】这个群里。一套k8s的node节点可能有数百个,当发生报警时,没必要把所有机器IP都列出来,所以我在第34行进行了截取,如果超过20台,那么就只显示20台,以免报警内容太长,这也会造成垃圾信息污染,且企业微信/钉钉等IM软件对信息长度都有限制,过长依然会被截断。如果一个IP都不写也不合适,因为有可能某套k8s只有个别某几台机器忘记更换了,并不是所有节点,所以使用这种模式,谁过期了就把谁列出来,被列出来的IP如果超过20个就只显示20个,更换证书的过程中,监控脚本会根据crontab配置的频率定期再次检测,如果某些IP已经更换了,那么下一轮检测时他就不会出现在列表里了,工单的内容也会被自动更新,这个功能特性我们前面也提过。

于是,运维工程师就可以根据工单内容的变化来判断他的更新进度到了哪里,是否有遗漏等等,于是就不用手动统计,也不会出现遗漏了。这个监控+报警+建工单(含安排运维执行)+更新进度,整个流程,依然是全自动,无需人工参与,只有此工单的运维执行人需要跟进。而团队管理者,例如当时的运维总监我,只需要根据这些工单的内容,以及自动生成的周报,就可以知道大家都在忙什么工作,以及进度到哪里了,编写团队周报时也可以非常精准地描述进度及预估工期,再上一级的领导通常都是不懂技术的,而用这种方式写出来的团队周报,内容清晰,数据准确,你的上级想挑毛病都难。

A-3.2.9 域名证书(本地文件)

我从2018年起就在公司推广全站https了,这是google带的头,我跟进的还算比较快。我们的很多域名,都会使用域名证书,无论是nginx方案,还是apache/IIS方案,或是其他,基本都要部署证书文件。这个证书文件在磁盘中到处都有,和k8s的情况一样,不一定哪台机器的哪个证书哪天就过期了,而一台台手动更换则非常耗时耗力又容易遗漏,所以我并不推崇这种工作方式。

前面我们提过,我们使用puppet实现配置管理的自动化,若你的公司使用ansible/saltstack等方案也是可以实现的。我的方法是,在puppet里面专门建一个名字叫certs的模块,用来部署域名证书,证书的更新入口有且仅有这一个,这里的证书文件的有效期更新到哪一天,所有服务器的证书文件就会跟着更新到哪一天,我不需要去每一台机器里查,我只要把这里更新了,然后就喝茶看监控就行了,只要没有哪台机器报puppet更新失败,那么一轮循环结束后,所有服务器中的证书文件就都已经更新了,而此时你的这杯茶可能才刚喝了一半。

配置代码也极简单,只有20行左右,真实代码如下:

这里没有贴nginx模块的代码,nginx模块会确保把nginx装好,依赖关系的设置会确保此时/etc/nginx/certs目录一定是存在的,故certs模块中直接用require=>Package[‘nginx’] 来依赖nginx装包就可以了,然后可以用notify=>Service[‘nginx’]来重新加载配置文件,使新证书生效,整个过程都是全自动,只有上传证书文件时是手动处理,后续所有步骤都是自动,证书文件每年才更新一次,所以就没做自动化,手动传一下也就十几秒的事,这时候我就不追求全自动了,以免代码缺陷导致所有服务器的证书文件都被更新错了。

还有一些域名站点是部署在k8s内的,k8s中的所有域名证书都在secret内,名字在所有地方都是一样的,故我们可以在运维机中用一个脚本来循环检测或更新这个secret,然后就可以实现更新k8s中的域名证书文件了,代码如下:

我们每个项目的k8s的master节点(之一)的 /etc/kubernetes/admin.conf这个文件都会拷贝到自己运维机的~/.kube/config,于是我们就可以在运维机执行kubectl命令来控制k8s集群了,而我们的很多k8s相关的监控,也都是用这种方式实现的,即:在运维机使用kubectl命令输出json格式,提取我们需要的信息,登记在运维机VM-B的Redis内,然后使用VM-A中的监控脚本,循环巡检所有项目运维机VM-B的Redis里的这个KEY,判断是否有非预期的值,若有则报警+自动建工单,使用这种方式,像监控CPU内存那样,用一个很短的、仅有几十行的小脚本,实现监控100个项目的100个集群的状态,同时也不会在有异常时造成报警信息轰炸,也能自动建工单跟进进度,这个详细做法这里就不提了,我们继续说回域名证书监控。

域名证书只在这两个地方有,其他地方都没有,监控和更新的做法分别是:

顺便说一下,puppet server是部署在运维机中的,所有运维管理和自动化功能都部署在运维机中,所以上面表格2里面的那个脚本在运行时是不需要去外面下载证书的,也不需要手动把证书传进来,因为puppet更新之后,puppet的代码目录里就会有一份最新的证书文件,而我们这个脚本3,直接读取这个路径下的文件再调用kubectl来更新就行了。

路径举例(路径是固定的):

/etc/puppetlabs/code/environments/production/modules/certs/files/www.abc.com.key

因为路径是固定的,所以我们上面表格里的脚本1还可以再配一份,用于监控这个puppet配置目录下的证书文件是否快到期,脚本只需要把/etc/nginx/certs替换成

/etc/puppetlabs/code/environments/production/modules/certs/files/

就可以了,如果快到期了,就可以发提醒和建工单提醒我们要更新证书,而一旦我们更新了puppet server里面的证书,所有的被管理节点都会跟着更新。

第二个证书目录的监控只需要改一下脚本里目录的路径,所以,我们并不是复制粘贴了一份代码来改,而是用sys.argv[1]传参的方式,如果有第3个目录要监控,那就继续添加就行了。

不同项目在创建工单时会按项目来判断是否工单已存在,多个项目存在同样的问题时是会创建多个工单的,但这个puppet中的证书的监控不需要分开建多个工单,因为所有的项目的运维机VM-B中的puppet代码都是每分钟一次从代码库(gitlab/svn等)同步过来的,所以如果pupppet中的证书快过期了,那么所有项目都会过期,如果没有,那所有项目旧都没有过期,所以这个监控,我们只需要建一个工单就行了。

实现只建一个工单的逻辑也不复杂,我们判断工单是否存在的依据是标题是否一致,所有工单都以【云平台】【项目名】开头,所以这个监控中,把这个前缀去掉就够了,这样即便每个项目都去建了一个工单,但从第二个项目起就会因同名工单已存在而被工单系统忽略,所以,我们并没有增加多少工作量,只需要十几秒从脚本里删掉几十个字符就可以了,你看看,我们的所有方案都是一个人几分钟的工作量,不需要大量的人参与统计、梳理、修改、开会、汇报、进度跟进等等等等一系列在很多公司长期存在的问题。

A-3.2.10 域名证书(网络获取)

在上一小节中,我们分享了如何监控本机磁盘上的所有证书文件是否已过期或即将过期,并实现了自动更换和配置重加载,但毕竟很多企业并没有像我们这样有统一的目录结构设计,还有些服务可能是把证书文件打包到了容器内直接使用,而监控容器里的证书文件则会很麻烦,还有很多企业因历史遗留问题导致很多人甚至不知道公司的某某处部署过证书文件,只有等证书过期导致服务异常时才发现,我自己这边也存在某些服务器因为某些原因造成配置重加载失败的问题,重加载失败时,已有的nginx进程并没挂,所以我们的监控是不会发出任何报警的,但此时若新证书未生效,那岂不是又会出运维故障了吗?所以,我们还有备用方案,这也是更加精准的监控方案。

这个方案就是:

不监控本地磁盘上的文件,而是通过访问https://域名:端口的方式来获取正在使用的证书文件,毕竟磁盘上的文件是否过期只能证明它自己是否过期,无法证明正在使用的是否已过期,所以我们直接读取当前正在使用的证书,得到的数据将会更加的准确,检测代码也不长,和检测本地磁盘中的证书的脚本长的也差不多,废话不说,我们直接上代码:

上面的class类,与监控本地磁盘文件时使用的那个类是一模一样的,一个字符都不差,所以,估计你也想到了,我实际使用中,这个class代码是在一个单独的文件中的,监控本地和监控https地址的两个脚本都会去import那个类来使用,而这个监控网络地址的脚本,只需要向截图中最后一行那样,就可以获取证书的过期时间了,是不是超级简单呢?

上面我使用了https://www.abc.com:443这个做演示,而实际的使用中,这里则是一个循环,循环处理所有我们的业务地址,而这个使用了https证书的业务域名+端口的列表,在我们的CMDB中也是一个类型的资产,这个脚本只需要去资产系统里读表,然后循环检测就行了,以后无论是新业务上线还是老业务下线,都只需要修改资产系统,而不必修改监控脚本。

A-3.2.11 K8S的各类信息

我们在A-3.2.9中曾顺便提到过一次,我们每个项目的k8s的master节点之一的/etc/kubernetes/admin.conf文件都会复制到对应项目运维机VM-B的~/.kube/config,这使得我们可以在运维机使用kubectl命令获取到各种k8s的信息,或用于监控,或用于资产统计,或用于生成自动报表等等,本小节我们就来说一说可以使用这个架构来做哪些功能和工具。

我们最常用的k8s部署模式就是使用deployment了,每个deployment都会配置这个服务使用哪个镜像,资源使用配额,反亲和等设置,于是我们就可以使用类似如下的脚本来获取所有项目中k8s的各种信息:

这个脚本中,我们只获取了每个deployment使用的镜像是什么,我们以这个功能为例做演示。截图里这个脚本我只截了十几行,完整的脚本一共也只有30多行(含注释和空行)。

通过代码可知,我们获取到所有namespace下所有deployment使用的image的地址,然后登记到运维机VM-B的Redis中,然后我们就可以把这些信息全部同步到运维管理平台内进行统一展示和筛选了,例如某个产品线的研发团队想知道他们开发的xxx服务在各个项目里都是什么版本,于是只需要在这个页面筛选一下,把deployment名称选成xxx,项目名不限,云平台不限,这样研发工程师就可以知道当前的版本升级推进情况了。而销售团队通常不关心某一个服务在每个项目里是啥版本,他可能更关心某个项目里的所有服务都各自是什么版本,此时他只需要选择项目名,而不选deployment的名字,就可以拿到他想要的列表了。

statefulset/daemonset同理,只需要把脚本里的kubectl get deploymnet改成kubectl get statefulset/daemonset就行了,于是又是一个很短的脚本一键采集了所有项目的所有服务的信息,也包括k8s自身的服务,例如etcd, coredns,flannel/calico等等。

而我们采集到这些信息以后,就可以做各种监控报警和报表统计类的工作了,例如某一天业务部门反馈新版本上线后某客户的某功能不正常,其他客户都正常,此时排查很困难,但如果到这个资产系统的页面看一下这个服务的版本信息,你可能会发现30个客户里有29个客户的版本都是1.2,只有一个客户的版本是1.1,而刚好这个客户就是有问题的那个客户,这很明显就是业务方忘记了申请给这个客户升级造成的,通过版本号的横向对比,可以帮业务及研发团队解决掉很多莫名其妙的问题。

有些服务需要限制它的资源使用,不能让它无限占用宿主机的计算资源,所以我们通常都会在yaml里配置request和limit,每个项目都配一遍难免有遗漏,或者所有项目都配了,但由于客户给的资源的多少不一样,所以我们配的参数也是不一样的,研发团队在排查bug时,很是希望知道每个项目都给了多少内存多少CPU,出故障的那个项目是不是内存给少了造成的等等。于是,在这个页面里,他可以查看这个服务在所有项目里的资源配额情况,如果刚好这个客户给的资源少,而临时调大配额后,故障就消失了,那就说明这是资源不足造成的了,而若调成和其他项目一样,或者本项目中它的配额并不是所有项目中最低的,但它反而有故障,那就说明不是资源限制导致的了,此时研发需要仔细去查bug,而前述情况,基本通过调整资源限制就可以解决故障了,也间接帮研发团队节省了大量的时间和精力。

POD的反亲和配置也是我们的刚需,我们并不希望同一个服务的所有pod都被调度到同一台机器内,k8s本身有这个功能,我们不需要自己写代码判断,但是这个功能是否启用了,我们则是需要判断的,我们判断的方式也异常简单,只需要看对应的参数在yaml里是否存在就可以了,如果没有就发个报警和建工单要求运维增加反亲和配置。

还有我们的k8s运行状态是否正常,如果挂了,你如何得知的?也简单,直接执行kubectl get nodes就可以了,看看是否有返回,当然了,在脚本里肯定是需要添加-o json参数的,而这个脚本也不只有这个功能,你还可以根据他返回的数据判断其他异常情况,例如是否有某个node/worker节点被禁用了,如果有,是谁禁用的?他/她是否在运维内部通报过这件事的原因?我们经常会因某临时需求临时禁用某节点,如果未及时放开很可能造成本机长期不被调度,浪费了资源不说,新服务上线时很可能会因为资源不足而创建pod失败,此时运维若去无脑扩容,则会给企业带了成本上的损失。而这个监控脚本若存在,在检测到有节点被禁用的情况下,自动建一个工单,要求恢复取消禁用,收到工单的运维肯定会立刻在运维内部询问这个节点是谁禁用的,为什么,如果很快可以解禁,他等时间到了uncordon一下就行了,而如果时间过长,Ta则可以把该工单的执行人改成那个禁用了节点的运维,相当于谁做了禁用,那么就由谁负责取消禁用,解禁的操作又工单进行自动监控和跟进,只要此node/worker不被解禁,哪怕你点击了完成工单,下一轮监控脚本运行时又会自动重建此工单,除非你真的解禁了本节点。于是,我们就永远不会出现节点被长期禁用、浪费资源的情况了。

我们再贴一段代码吧,其实我不太想贴的,因为实在是太短了,男人嘛。

这个代码一共就13行。

如果k8s集群挂了,那么第7行的命令执行是不会成功的,所以输出的outputs一定空的,我们在脚本里,是在第11行把它使用的redis key删除了,我们在入口运维机VM-A的报警脚本的逻辑则是,如果此key不存在了,说明这个项目执行kubectl get nodes失败,此时需要报最高级别的报警,通报说k8s集群异常。而如果命令执行成功,那么就在第13行把获取到的信息存到运维机VM-B的redis内。

我们所有监控的设计逻辑都是在各项目的运维机VM-B内做采集,把采集到的的数据存到自己运维机的Redis内,然后入口运维机VM-A中的监控脚本循环处理所有项目运维机的Redis中的数据,检测到异常的话,只发一条报警,只建一个工单,规避了客户网络不通的限制的同时,也大幅减少了相同的报警信息的数量。

其他场景还有很多,凡是yaml文件里可以配的配置项,只要你需要监控或者需要在多项目之间做对比,就可以使用如上方式进行采集,采集失败就报警,采集成功就在运维管理平台内统一展示,并允许用户按需进行筛选和对比,你的网页只需要提供一些简单的查询和筛选功能,以及不一致时变色及高亮显示就够了,由于这些功能都完全一致,只是监控和展示的点不一样,所以就不一一列举了。

我们额外再介绍一个单独的例子吧。

这个是用于监控deployment中所有pod的资源使用量(不是率)对比的,什么意思呢?有的时候,代码由于bug或其他缺陷,可能跑着跑着就卡死了,是假死,不是挂了,如果挂了pod会自动重启,用户是无感的,但若代码假死,pod不自动杀它,它又可以接收到用户请求,此时就会有1/N的用户受到影响,N是副本的个数,此时该POD的资源使用量要远大于其他pod,例如正常的pod使用了10m的cpu,200M的内存,但这个异常的POD很可能把CPU和内存使用到limit的上限,于是,我们只需要判断所有pod的使用量是否趋于一致(方差不大)就可以判断是否有假死的情况发生了。

这个判断的逻辑就稍微有点复杂了,这个监控脚本也是所有监控脚本里行数最长的,我们上代码吧,这回您可能需要费点脑细胞仔细看代码了,这个不像之前的那么简单:

代码第1/3部分:

释义:

1.第6行设置的deployment列表是比较重要的服务,这些服务的信息我们每分钟都会存1份,用来画图的,方案就是使用我们之前提到过的highcharts/echats方式,我们可以把同一个服务在不同客户的数据画在同一张图里,用于对比判断故障的原因等等。

2.第7行标识这些服务不需要监控,检测时会被忽略,通常是系统或运维使用的服务。

3.第18行获取所有pod的使用数据。

4.第24-48行是在获取每一个pod的使用量明细,最终在第50行存储到redis内用于后续的画图等功能。

代码第2/3部分:

释义:

1. 52行起,直到脚本结束(部分代码在截图3中),都是在判断pod的资源使用情况。

2. 第63-64行在计算本服务所有pod的平均cpu内存使用量。65-66行在计算最大值。

3. 67-72行将数据存档,用于其他功能。

4. 73行表示如果这个deployment的pod副本数少于3个就不监控,此时对比没意义。

5. 74-75行在计算使用量第2多和第3多的两个pod的平均使用量(61-62行尾部已排序)

6. 76行表示,如果使用量最大的那个POD,比【使用量第2大和第3大的POD的平均使用量】的3倍还大,且CPU大于1核时就认为是异常,例如假设cpu使用情况是:cpus=[0.11, 0.2, 0.31, 0.45, 0.47, 7], 此时cpus[-2]和cpus[-3]分别是0.47和0.45,平均值是0.46,而最大值cpus[-1]是7(核),远比0.46的3倍大,此时该pod大概率是不正常,所以要报警。限制1核以上再报警是为了避免最大值也很小时的误报(比例大),例如cpus=[0.01, 0.01, 0,03, 0.4],此时第2大和第3大分别是0.03和0.01,平均值是0.02,最大的是0.4,是对比用的平均值的20倍了,但这么小的用量肯定没挂,所以加了这个限制。

7. 第88-99行是判断内存的情况,和判断cpu的逻辑一样,这里就不解释了。

代码第3/3部分:

释义:

1.第100行是在设置每小时的第21分钟和第51分钟进行报警,脚本是每分钟运行一次的,但不必每分钟都报警,半小时一次就够了,21分和51分这俩数字可以随便选,我们是根据运维机VM-B里所有crontab的执行时间的分布,挑了一个负载相对较低的分钟值。

2.101行是在获取这个有问题的pod的日志,tail了它日志的最后100行。

3.102-113行就是发报警的逻辑了,这里我们可以看到,我们发报警并不是直连企业微信/钉钉,而是把报警信息以json结构化数据的形式在redis队列中入栈,然后入口运维机VM-A中的脚本再去循环处理这个队列。

4.第114行是把日志的内容写进另一个队列,因为报警发文字(send_text)与发文件(send_media)的处理代码是不一样的。

如此,我们便实现了当某个POD使用量异常时的报警监控了,此时也许该POD还没有完全没响应,用户受到的影响也还不大,尚未达到1/N,而我们的监控已经把报警信息发出来了,而运维看到后,通常只需要把这个POD删了让它自动重建就可以了,这个步骤也可以在脚本中实现自动删除,但为了保留现场我们并没有这样做,真正的业务场景下,我们则可以根据服务名来判断到底是自动处理还是手动处理。

收到的报警信息大致如下:

以下deployment检测到部分pod的CPU/内存使用量远大于其他pod
云平台:某某云
K8S: 某某业务某某集群
namespace: 业务简称
pod_name(最大的那个): pod_name-xxxxxx-随机字符
CPU使用量列表:0.1m, 0.2m, 0.22m, 0.25m, 7
忽略列表(这些服务不监控):
logstash, apiserver

收到报警几秒后,这个pod的日志文件的最后100行也会以文件的形式自动发到这个群里。

3.3 资产管理系统小结

从我们分享的内容可以看到,我们的CMDB并不只是一个资产管理系统,而是与我们的监控系统密切相关的,我们的绝大部分的监控,都依赖资产系统中的数据,这样便可以给我们带来很多好处,例如:

1. 信息采集脚本无需考虑如何报警,数据如何展示,只采集即可。

2. 监控脚本无需考虑有多少项目,数据怎么获取,只需要从CMDB内读取即可。

3. 运维管理平台同监控脚本一样,无需考虑其他,只需做好展示、筛选、对比等功能即可。

4. 运维团队无需在项目上线下线及服务器数量变化时到处修改数据,所有脚本和展示页面的数据来源都是CMDB,采集脚本的存储目的地也是CMDB,所有工作皆围绕CMDB开展。

5. 项目信息本身也是资产类型之一,资产系统里面也有一个分类叫项目,这个项目里可以登记该项目部署在哪个云,研发负责人是谁,销售负责人是谁等等,还可以登记该项目的专用群的群聊机器人是哪个,发报警时直接读取对应字段就可以了,报警脚本也不需要判断这个报警信息要发给谁,MySQL表结构里面的groupname字段写的是谁就发给谁。

6. 群聊机器人也是资产类型之一,上面说的groupname是群聊的名字,有固定的英文ID(不变),也有随时可以改的名字(就是群聊的名称,例如XX项目沟通群这种字样),报警脚本可以根据不变的英文ID(资产编号)获取到webhook地址,这个地址随时可以改,随便改,只要改完登记到群聊机器人的资产页面即可,报警脚本会自动完成groupname到webhook地址的转换。

7. 普通的报警直接发,不at任何人,重要的报警自动at研发负责人,XX负责人等等,就看项目资产里都登记了哪些人,登记了谁就at谁,如果项目负责人换了,在资产系统里修改就行了,监控脚本不需要改,所以永远不会出现报警只发给了某已离职人员、导致没人看见报警的情况。如果所有负责人的字段都未填写或已删空,那么报警脚本有个兜底策略是发给运维团队内部专用的监控报警群,所以永远不会有报警信息丢失。

8. 第1大章里面介绍了运维机的设计,我们曾提过我们使用了2000X,3000X,4000X,5000X等端口用于为不同的服务配转发,这里的X是几,也是在资产系统里登记,运维机也是资产类型之一。在入口运维机VM-A中是使用哪个别名alias登陆这个运维机的,也是在这里设置,后台有脚本每分钟会修改一次VM-A的配置,只要CMDB内修改了,1分钟以后新设置就生效了。某个项目临时下线时,把运维机从启用状态改成禁用,那么所有的监控脚本及所有的展示页面就都不读取这个项目的信息了,也是什么代码都不用改。

还有很多,等等等等,由此可见,我们以资产系统CMDB为核心,用了很少的工作量,完成了上百个项目的全自动化运维,同时此模式可以无限扩展可维护项目的数量上限,无论100个项目还是1000个,这对我们运维团队来说都没任何区别。

最后我们来一张以CMDB为核心视角的架构设计图:

在这个模式下,运维在每个项目上线时,只需要把新项目的运维机配好即可,剩下的一切均由自动化搞定,运维无需进行任何操作。项目下线时,也只需要把该项目的运维机从资产系统的运维机管理页面删除或禁用即可。

我在前言部分说过,我仅带了平均5个左右的运维,就完成了50个运维的工作量。本文看到这里,相信您已经知道我是如何做到的了吧,而做到这些,只需要20个左右的脚本配合自动化即可,这些脚本在前文中大部分都有演示,而每个脚本也都不长。

在明显解放了人力的情况下,运维团队的工作就不再是应急救火和到处统计整改和开会,而是变成了系统优化与改进,比如我们每发现一个配置项有可能不合理,我们就写一个简单的脚本去检测,脚本的模式依然是只采集,然后再在入口运维机VM-A写一个循环处理的脚本用于发报警和自动建工单,运维管理平台内也会为这个检查项编写一个展示页面,而这种工作,我们每做一个就永久性消失一个,每解决一个问题,所有项目都会永久不再出现该问题,即便是新上线的项目,也都会因为自动化的存在,从上线当天起就不存在以上任何问题和隐患。

我的团队在短短几个月时间内,用这种模式做了很多优化与改进,后来,后来,我们每天闲得慌,没活可干你知道是什么感觉吗?

3.4 IT资产管理系统

后来该运维资产管理系统的应用范围扩展到了IT团队的资产管理,因为运维与IT有些工作是重合的,所以IT资产系统也接入了进来。我们在这里实现了很多有形资产的管理,包括:机柜,交换机,服务器,RAID存储,内存(配件),硬盘(配件),路由器,AP设备,台式机,笔记本,鼠标,键盘,优盘,移动硬盘,手机,电话机,投影仪,电视大屏,打印机,等等等等,所有IT资产都在这里面进行了登记。还在IT资产管理功能内,实现了资产从采购入库到损坏报废的全流程功能。

需要单独说一下的事情是:

我们的配件(内存和硬盘)是单独管理的,每个内存条或每一块硬盘都有单独的资产编号,每个配件被安装在了哪台服务器内都有登记,每台服务器的详情页面也都有它在使用的配件列表。每一个配件也都有它自己的生命周期管理。2019年左右听说腾讯是这么做的,所以我也学了他们的做法,这种方式确实是合理的。

当一些老旧过保服务器(我们设置的是:购买时间>=8年)发生故障时,我们通常不会采购同型号进行替换,一般都是买更(gèng)新的型号,而故障的机器,会把未损坏的配件拆除重新入配件库,用于将来其他服务器配件故障时的更换,而配件管理页面则会详细记录配件的属性,例如内存条会登记购买日期,购买渠道,品牌,容量等信息,最重要的信息是它是第几代产品(DDR3还是DDR4等等),不是同一代是无法混用的,某服务器DDR4内存坏了一条,你去备件库里拿一条DDR3更换是换不了的。硬盘则会记录它是SSD还是机械硬盘,如果是机械盘,还会记录转速是5.4K还是7.2K还是10K、15K等信息,转速不同的硬盘也不会混用,用这种方法,充分发挥了每一个配件的作用,不浪费任何残值。

配件的更换也是由流程触发,比如某台机器DIMM1的内存条故障需要更换,IT工程师则会创建一个工单,用配件B(在库房呢)替换配件A(故障的那个),审批通过后就会按流程里规定的时间去更换,点击完成后,该服务器DIMM1关联的内存条资产就自动变成了配件B,而配件A则变成了【损坏】状态,未损坏时的更换(通常指扩容)则会把配件状态从【使用中】改成【闲置】,只有坏了的才会变成【损坏】,每年年初会统一报废上一自然年所有损坏的硬件资产,也包含这里说的配件。硬件故障时会自动发通知给本机所有使用人(若为虚拟化,则从虚拟机的使用人里加载)

4. 运维管理平台

每一家稍微有一点规模的企业,肯定也都有内部的运维管理平台,每家起的名字也都不一样,有叫运维管理系统的,有叫运维开发平台的,有叫MIS系统的,有叫流程管理系统的,等等等等吧,名字很多种,但功能却不太统一,很多企业内部都有多套系统,不同的系统实现不同的功能,而我则是把所有功能都放到了同一个地方,这样全公司就只有一个入口,不管你想查什么信息,都到这里来就了。

按功能设置,我把我的运维管理平台分成了以下几个大类:

1. 资产系统
2. 监控系统
3. 工单系统
4. 数据报表
5. 数据库
6. 权限管理
7. 运维排班
8. 规范制度

每一个大类在一级导航栏里都是一个按钮,每个大类下均有多个小类,用于实现不同的功能。

从用户体验的角度出发,我的导航设置只有两级,全平台都没有三级导航,以免点开一级还有一级影响用户心态,”点不完,根本点不完”。导航的第一个大按钮是【首页】,并不是【资产系统】,首页里显示的内容是更新日志,按时间倒排,每个人扫码登录后看见的第一个信息就是运维管理平台做了哪些更新,以免使用人不知道我们提供了某某功能,登陆后默认就进首页,而不是用户在浏览器输入的地址,此做法是为了强制让用户查看运维管理平台有哪些变更,而不是三天两头去开会、去同步信息、去培训他们什么的。从第二项起,才是各种功能的分类按钮。

下面我们便按章节来分章介绍我们都实现了什么功能,怎么实现的,这些功能又是如何与其他功能协作的。

所有在这里介绍的功能,都是大幅提高工作效率的功能,一般性功能就不介绍了,本文中省略。

4.1 资产系统

这个【资产系统】就是我们前面所说的资产管理系统(CMDB),由于是在导航栏里,为了让文字个数相等,所以就简称成了【资产系统】,没办法,我是个强迫症。

资产系统前面提过很多次了,最早的版本我是用的MongoDB做存储,该版比MySQL版多一个管理页面,那就是资产属性的设置页面,这个页面可以设置每一类资产有哪些属性(字段),这些字段会自动出现在该类型资产的增删改查页面。属性管理页面实现的功能是对某类型资产的属性的增删改查设置(相当于MySQL的改表结构的功能)

存储改成MySQL以后,此功能就废弃了,因为某个类型的资产的所有字段都是在表结构里面定义了,但增删改查页面我也并没有为每个类型依次写展示和修改页面,而是通过MySQL系统库读它的表结构,我的所有表的comment字段都有设置,设置的值就是该字段的名称,例如asset_id字段我会这样定义: char asset_id(32) not null comment ‘资产编号’, 然后在所有页面里,asset_id这个字段都会显示成【资产编号】,显示的内容都在表结构的comment里面,设置成什么页面就显示什么,而获取这些信息,只需要在information_schema库的columns表读取就行了,语句大致如下

select column_name,column_comment from information_schema.columns where table_name='表名’; 

所以,无论有多少种资产类型,我都不需要额外写新代码(不是完全不用,修改页面需要额外写一点,但不多,展示与删除页面不需要)

所以,我们的导航栏里仅需要循环输出我们的资产类型与资产名称的对应关系做二级导航就够了,Python伪代码大致如下:

for asset_type, asset_name in asset_list.items():
    print “<a href=/asset_show?t=%s>%s</a>” % ( asset_type, asset_name)

前端开发的同事也不用每次都改代码了,虽然即便修改、这个修改量也不大。但能简则简,重复的事情尽量不做第二遍。

小结一下:

资产系统(CMDB)里,虽然种类很多,数据很多,但展示管理的代码却不多。所以这一章节,其实也没什么可讲的了,毕竟逻辑和架构都设计得极其简单,而这个资产系统的真正用武之地,则是监控与数据报表。监控功能我们在前面都说过了,而数据报表功能,我们将在后面的【4.4 数据报表】中再进行详述。

贴几张资产系统的截图吧,下图是云主机的页面(客户虚拟机及物理机+本公司虚拟机)

(数据是编的,脚本自动生成的随机数据,用于演示)

表格上方有每一列的checkbox框,取消勾选时,下方表格里就不显示该列了,勾选哪个框就显示哪一列,默认是都显示。【其他信息】这一列登记的内容就是我们在2.7节讲解监控脚本代码时提到的/etc/motd文件的内容,如果本机的motd文件是空的,那么这里的这一列也是空的。【备注】一列是运维纯手动修改时使用的备注功能。

真正的管理平台页面内,还有所属项目及所属产品线等几列,表格的上方是可以按这些列进行筛选的(下拉框),用户可以自己选择查看哪个项目或哪个产品、哪个云的机器列表等等,自由度很高,查询条件随意组合。

您可能还注意到了序号为2的资产有两次变更(修改)记录,那个【2次】的文字是链接,点击后就会跳到这个资产的修改记录页面,此页面内容大致如下:

日志显示:

我在2024-03-11 15:08:33将公网IP字段从”空”改成了”11.11.11.11”
又在2024-03-26 16:13:12把公网IP字段从”11.11.11.11”改成了”22.22.22.22”

写文档时我用date命令把系统日期设置为了2024-03-26,所以第一行文字是橙色(“今天”),第二行是浅蓝色(15天前),这种变色方式可以一眼看出该资产最近是否有属性变更。

以上是资产被手动修改后的日志记录,自动脚本每分钟都会更新这个页面的数据,脚本做的修改也会自动记录日志。记录日志的函数是同一个,无论手动还是自动,都是用同一个函数进行记日志的,所以不会漏,脚本做了内容修改时,执行人一列填的是脚本的全路径。

【资产编号】这列默认会显示资产名称,未设置名称时才显示那个英文的唯一ID,它也是MySQL表里的唯一主键。这个文字也是个链接,点击后能跳转到这个资产的修改页面,如果看日志时发现数据改错了,那么点一下跳过去重改一下就行了,修改页面也会显示本资产的修改记录,修改页面大致长这个样子:

可以看到这条记录的唯一资产ID是【vm-40005-172.17.111.222】,编号以中横线分隔,第一段【vm】是资产类型,第二段就是我们前面提到过的2222/SSH服务的转发端口,这里这台机器我将其设置为了项目5的某台服务器,IP是172.17.111.222,所以它的转发端口就是40005,用这种编号规则,可以确保这张表里不会有主键冲突。而公有云上面的机器则会使用云平台中的资产ID作为编码,当然也会加上一个前缀,以免不同云的编码重复(概率极低,但不表示不可能,所以也需要加个前缀端口),所有类型的资产页面都是这样的,这里就不再演示更多的了,某字段若涉密(例如密码或K8S的Token字段),则会调用一个函数进行权限校验,没权限的人是看不到的。

4.2 监控系统

监控系统也是运维管理平台内比较重要的功能之一,所以导航里排序我把它放到了第二位。

它本身并不是什么监控的功能,而是一些监控数据的展示及管理页面。

例如我们前面说过很多次的,某某检查项我们需要判断它的当前值是否符合预期,所以会写个小脚本1进行信息采集,然后再写个小脚本2进行报警和建工单,而在运维管理平台内,则会为小脚本1采集到的数据做统一展示,以及一些数据的聚合分析等等,比如nginx版本的统计页面,我们会在表格顶端统计当前共有多少台服务器里有nginx,版本1有多少台,占比多少。版本2有多少台,占比多少等等等等,每一个监控项,我们都会针对它的整改需求,采集分析统计展示相应的属性。这个展示页面也是通用的,实际并不需要每次都新写。

一些与时间有关的采集项,例如证书的过期时间,域名续费及年度备案等信息,我们则会按时间排序,先到期的在上面,后到期的在下面,页面点进来就能一眼分析出未来几个月是否有相应的任务要处理。

这个监控数据展示页面由于也是基于资产系统(CMDB)做的,所以就是以【监控项的类别】作为【资产类型】的参数登记到了资产系统内,展示时也是直接使用资产系统的展示页面。

唯一例外的是各项目服务器的资源使用率页面,这个是单独编写的,因为需要对使用率进行筛选,使用率超过80%背景色变红,超过90%变黑等等,类似下图的样子:

此截图依然是阉割版,实际版本中有更多选项可筛选,表格里还有项目名等信息,这里删减掉了,点击【查看图像】按钮,还可以看到本机的所有监控项的图表,截图如下(多张图):

点击上面的【查看图像】按钮就会跳转到下面的页面,页面里包括本机的所有图像,因显示器大小限制,我是截了3张图依次粘贴到这里的,但实际他们是在同一个页面里。页面顶端就是筛选表格,进入本机后,依然可以根据项目、云平台等信息进行筛选,表格依然是做了删减后才截的图。右侧表格是本机的硬件属性信息,也是大量删减后截的图。

最右侧有一栏目叫【CPU性能】,其值是XX秒,这个是我用C语言写了一个循环几十亿次的加法运算的程序,然后用系统内的time命令看这个二进制文件执行完需要多少秒,用于评估CPU的计算能力,时间越小说明计算越快,性能越强。这个数据通常在服务器刚拿到手时、没有启动任何业务进程时执行,此时的计算不会受其他程序的影响,同时此程序也不会与业务进程争抢时间片,时间计算出来后就会自动登记到资产系统内,并将永远不变。

这个时间越小误差越大,所以我在首次编写代码后的调试时,不停地增加循环的次数,最终的版本印象中是循环了几十亿次,我的标准是让这个值变成3秒以上,如此可以减少误差。

此程序在使用了1年多以后,有一天有一个新项目上线,跑完系统初始化以后,我访问上方的监控页面查看本项目的机器配置是否与申请的配置相符,页面内的数据都是脚本自动采集的,所以访问这个页面来对比就可以了,比如/data盘一列若为空,则说明这台机器没有给数据盘(若给了我们会自动建分区,分区不存在就说明没给),而此时我发现这个项目的所有服务器的CPU性能一列都在16秒以上,计算耗时比所有其他项目多3倍,然后我就去问甲方(某政府项目),他们反馈说这批机器是使用了国产CPU,所以性能稍差(鼓励打气:国产CPU加油!),然后我就要求升配增加CPU核数了,否则后期业务运行肯定受到影响,比如数据库的查询肯定会因CPU性能不足而查询缓慢。通过此方式,我们在故障发生之前就将其避免了,而不是真正故障后再去救火,而CPU性能不足造成的查询缓慢,DBA可能查一周都不见得能反映过来是CPU性能造成的,像这样的避免故障和提高效率的小创意还有很多很多,所有创意集合到一起,组成了我们高效的自动化运维系统。

截图2:系统负载和出入口带宽数据(也是脚本生成的假数据)

截图3:磁盘IO和TCP连接明细数据(依然是脚本生成的假数据,看着挺像真的)

此外还有图像聚合功能,通过管理页面的设置,可以把不同项目、不同服务器的不同监控项放在一起成为一个组合,创建人可以给这个组合起个名字,然后在聚合页面访问这个组合图链接,就可以在同一个图表里看到不同的画线了,这个功能是使用人全自助、全自动模式,不需要运维去配Grafana,后期有新项目上线后,使用人(通常是研发)可以继续把新项目的某个图加到他的组合内进行聚合展示,运维全程不需要参与。

创建组合页面长这个样子:

创建好的组合在首页有列表,可以点那个【链接】去看聚合图,也可以修改已有组合。

页面比较朴素,没什么js/css代码,因为这些功能都是纯运维自己写的,不是运维开发写的,但这并不影响使用,工作繁忙时,功能最重要,等有空了再考虑美化的事。

创建组合时会自动生成【IP数】乘以【图形类型数】个图表,乘法的关系,2个IP+3个类型,最终就是6张图,修改时则会列出这6张图,可以按图删除,只删1个留5个也是可以的。

上面新建的组合的修改页面如下:

组合图像信息的存储依然是Redis,不是MySQL。

KEY的名字就是【监控聚合图_<用户起的名字的md5>】,而创建页面中的有效期设置,其实就是这个Redis的KEY的过期时间,时间到了以后KEY自动就消失了,管理页面自然也就看不到了。这个KEY是个hash类型,所有与这个组合有关的信息都可以新增一列hkey,后期可以继续扩展其功能。

4.3 工单系统

终于说到工单了,工单系统其实就是流程的代码化,我当时设计这个工单系统时,表面上是为了解决第三章第11条里提到的那些痛点(运维支持群里消息多且混乱那个事),但实际我的最终目的依然是做自动化,如果工作全靠手动处理,即便有工单系统速度也快不了,这样顶多是流程不乱了,但工单申请人依然要排队等待运维去执行,所以自动化还是得做,在这个背景下,我设计了如下模式的工单系统。

1. 工单可以由员工手动创建,也可以由监控脚本自动创建(监控功能使用,前面大量提过)

2. 工单有类型,不同工单有不同的自动化脚本在后台处理,暂未实现自动化的手动处理。

3. 不同类型的工单会从资产系统中加载不同的信息用于各类输入框下拉框,实现自动填表。凡是资产系统里面登记了的数据就不应该由员工手动填写,以免写错或写漏。

4. 工单自动化类型有3种:全自动,半自动,手动。全自动类型的工单,参与流程的员工只需要参与审批,无需任何人做其他事情。半自动类型的工单,工单内容的一部分可以实现自动化,此部分和全自动的模式完全一样,暂未实现自动化的部分要么是无法实现要么是功能尚未开发完毕,此部分手动处理。工单审批通过后,执行人是自动化脚本,脚本把自动的部分处理完毕后,将工单模式改成手动,然后随机挑选一名在岗运维去执行。纯手动的工单场景也和前述一样,要么自动功能尚未开发,要么无法实现自动。自动工单的处理若失败,脚本会把工单改成手动模式,并选取运维进行人工处理。运维工程师的优先级也高于脚本,如果运维把某自动化工单的类型改成了手动,那么脚本是不会去执行的。

5. 工单种时间限制,工单分配多少多少时间后若未完成会有机器人进行提醒,到期未完成的工单,运维团队管理者要进行跟进及问明原因,根据逾期原因来定工单无责延期还是扣除绩效。通常紧急类的报警工单是不允许逾期的,一般类的随便,但如果延期次数过多依然要干预。临时配置类的工单(例如申请临时权限),根据生效时间范围,后台脚本会在申请的内容过期前自动重新打开此工单,申请人确认权限可以回收后,工单再次流转到运维节点进行权限回收,避免各种临时的申请由于未统计或统计不全变成了永久权限。

6. 工单页面按时间排序显示当前的工单列表,表头统计每个人名下有几个未完成工单,每名运维接收到新工单后会提醒运维有新工单,工单标题及申请人是谁等信息,而申请人则会收到工单由哪名运维执行,这名运维名下有几个尚未完成的工单(你要在这后面排队)等信息,于是申请人就不用一直问工单做没做,做到哪了等等,看执行人名下的工单变化情况就能预估出自己的工单何时会执行了,每个申请人只能看自己的工单内容,其他人的只能看见标题。

7. 运维工程师可以看到所有工单的内容,并可以根据工单的复杂程度,自己的繁忙程度等更换工单的执行人,以此确保运维团队内部工单不会过度集中,处理完成后的工单会进行自动存档以及自动出现在周报里。

8. 每个工单的执行人都在创建时由代码随机选取,运维团队由于有7x24排班,任一时刻不一定谁在,排班功能会把每天每小时都有谁在线提前计算好,工单选取执行人时会根据此数据选人执行,不会选取已下班的人,避免大家下班后加班,代码选取运维执行人时也有一定的策略,根据每个人的特点,在不同类型工单下,为每个人设置不同的概率,以便每个人都能发挥其专长,同时还要限定每个人每个月都至少做一次某类型的工单,不能因为A擅长某个工作,就永远不让B执行,长此以往B将失去处理这个类型工单的能力,运维团队的所有人都要互备,平时工作可以有侧重点,但不允许有盲点。在这个模式下,我的团队里所有人都知道所有事,平时无论什么时候谁找运维,找哪个运维,他都能得到他想要的信息。而运维团队成员的离职也不会造成我团队能力的缺失或技术细节的丢失。

9. 创建工单时,若该工单是全自动类型,那么后台脚本在处理时需要哪些信息,就在工单里填写哪些信息,信息的读取和筛选由工单系统完成,而非后台脚本完成,一是为了避免后台脚本出bug造成意外,二是工单系统自动算好可以节省后台脚本的处理时间,提高处理速度,三是申请人和所有审批人都可以看到这些信息,万一有错误的数据可以暂不审批,以免引发非预期故障。

10. 工单的类型有:工单,监控,通知,任务。前面说的基本都是【工单】和【监控】的类型,我们还有【通知】的类型,这个功能相当于是企业微信/钉钉等软件里的待办,这里我会用这个类型的工单给团队每一个人建一个【通知】类型的工单,并规定完成时间,以此来确保所有人都能按时看到通知的内容,如果不手动点击完成,机器人则会一直不停地发消息进行提醒,大部分【通知】都是全选所有人,给每个人建一个【通知】类的工单,少量场景只勾选几个人。而【任务】类型的工单,通常是学习调研类的,需要很多时间来处理,相当于是OKR中的O,【任务】的执行期限通常是以周或月为单位,例如可以建一个【任务】给某个擅长k8s的运维,要求他做一个把flannel换成calico的方案,还可以建一个将MySQL-5.7升级到8.0的方案设计的【任务】给某个DBA,等等等等。

11. 工单系统还设计了依赖关系,每个工单可以依赖其他多个工单,例如某研发申请新搭建一台数据库,那么工单提交后,工单系统在创建他的工单的同时,还会根据他申请的机器的配置信息,自动创建一个【创建云主机】的工单,并将此工单设为依赖创建云主机的工单,创建云主机的工单有后台脚本自动处理,也可以改成由运维手动处理(当自建虚拟化时可以这样干),而搭建数据库的这个工单的执行人则会选取某DBA负责。这是依赖关系的作用,代码量不高,只在表结构里有个字段,填了谁就依赖谁,不填就没依赖,很简单。只有被依赖的工单都变成已完成状态,此工单才会显示及发机器人消息给申请人和执行人进行提醒。申请人在所有阶段都能看见自己的工单,运维总监也能一直看到所有的工单,用于评估未来的工作量或对工作安排进行调整。

这个工单系统是我写的所有工具中代码量最高的,一共800多行,所以这里就不贴代码了,我贴几张设计图吧,是Excel表格的设计表,但我也采用截图的方式,不复制粘贴里面的文字了。

截图一:工单编号规则

这只是最初的设计图,因时间等因素,最终的版本和这里并不太一致,很多自动化功能也没来得及做完。

截图二:表结构设计

这个也不是最终版,最终版我没存档,找不到了,这个截图是很多年以前我第一次设计这个工单功能时的存档,而且也不是最终版,我是从某老东家的工作文件备份文件夹里翻出来的老古董。【备注说明】这列是我写本文时后加的,老古董里没有这一列,但用法是一样的。

基本就是以上的设计思路了,这个工单系统我也开发过两个版本,在两家不同的企业任职时开发的,根据企业场景的不同,实现的功能方案也不太一样,就比如我在那个老古董公司任职时,执行人这列是直接写死的是我,因为当时是我一个人搞定了公司运维所有的事情,而第二家公司我是运维总监岗,带了一个20人编制的团队,所以才需要选取执行人的功能,以前是没这种需求的。最终页面的长像也完全不一样,第一家公司的版本更像是专用版,有固定的模式,而第二家公司则是通用版,但会给工单分类,只不过由于自动脚本没时间写,所以这部分代码最终也没上线,运维团队使用的一直是通用版,也就是前面设计时提到的未分类场景,这个功能只要招一个运维开发就可以很快完成,但当时是我自己在搞,没时间,所以就废弃了。

有了这个工单系统后,无论是响应研发团队或销售团队的需求,还是日常工作的管理,还是监控的处理,还是各类任务的跟进,我都可以在这一个页面自助看见所有的进度信息,再也不用天天问这个问那个了,其他团队的人也不再需要总找我给他安排运维做什么什么事情,感觉整个世界一下子清静了。

4.4 数据报表

很多数据我们都需要统计,公司的其他部门也经常需要找运维要各种各样的数据,频率很高,每种数据统计起来都不简单,而追求极致自动化的我自然是不愿意每次都手动做这种事的,所以,凡是我们运维团队或者其他业务部门需要的数据,我都基于资产系统做数据登记及增删改查功能,然后编写各种功能的网页来展示这些数据或数据的统计结果。

这个数据报表栏目,就是用来展示各种统计数据的。

例如我们之前提过的各种软件的整改进度,版本号统计等等,这些信息既可以在【监控系统】里设置一个二级导航,也可以在这里设一个,因为放哪都合理,事实上我确实两个地方都放了同一个链接。

最常用的报表应该就是服务器资源统计及使用率统计了,无论是我们自己,还是甲方,还是我们的销售、售后,都需要频繁地看这个数据,但手动统计太费时间也太累,数据更新也不及时,所以我在这里做了一个实时计算的报表页面,还记得我们的项目运维机VM-B里面的Redis吗?这个Redis内有每个项目的所有服务器上一分钟的资源总数及用量的数据,而再过1分钟,入口运维机VM-A中的脚本就会把所有项目VM-B的Redis中的【资源使用率】这个KEY转储到VM-A的Redis中,并在【资源使用率】这个KEY后面加上【_PORT】下划线+转发端口号,于是,我们的入口运维机VM-A的Redis中,就有了所有项目所有服务器的信息,而这个信息,只比最新数据慢两分钟,于是我们的报表页面就可以读这个Redis并循环处理这些KEY了,或统计到一起,或按项目单独查看等等,下面看一下报表页面的效果:

这里的数据依然是我用脚本生成的假数据,有一格的内存使用率竟然是负数,这是造假造成的bug,我懒得查原因,直接用这个图吧。图中我用了6个项目的数据来展示本功能,我们在这里可以一眼看出每个项目的总资源是多少,使用率是多少,既可以看数字表格,也可以看柱状图,也可以都看,就看项目的多少了,如果项目很多,同一个屏幕内都显示就不够了,此时可以分开看。性能一列我忘记准备假数据了,所以都是0,但不影响你理解此页面的功能,所以我就不费精力去做了。

需要说一下的是:

CPU性能数据我是用C语言写了一个循环几十亿次的加法运算,前面提过。
内存性能计算是用的mbw memory benchmark v1.5, https://github.com/raas/mbw
硬盘性能计算是用的dd命令,在磁盘上生成一个100M的文件看IO速率。

其他类型的数据报表就数不胜数了,我就不一一截图了,因为此刻的我手里没图,本文中的每一张图我都需要改代码,让网页在本地虚拟机能运行,让网页在没有任何数据的情况下能正常显示表格,每一个报表页面都需要读很多数据,只要有一个数据是空的,表格就不正常,所以我需要生成很多假数据才能让一个页面显示正常,我前面的所有截图几乎都是这样做出来的,很多截图我也都说做了删减,删减并不是为了保密,没什么可保密的,我删减是因为那些数据造出来太费劲了,还不如直接不显示了,毕竟那些数据是和业务相关,又不影响功能展示,所以我就干脆删掉了。

下面列一下我们都有哪些数据报表吧:

1. 资产数量、总配置及当前使用率的统计,也就是上面那张图的功能,这个能截图是因为它需要的数据我在演示监控功能时已经造好了,所以稍加修改就能显示。

2. 每日、每周、每月、每季、每年的工单量,报警数量统计,总数统计,分类统计,按故障类型统计,按执行人统计(绩效考核用)等等。

3. 各类故障的影响时间,资产系统里有一个类型是SLA,这里登记的是所有故障的详细信息,处理过程等,也包含故障的开始时间和恢复时间,SLA报表页面则会根据这里登记的时间来按年自动计算每一年的可用率是几个9,故障发生后,处理故障的运维只需要把故障处理过程登记到资产页面的SLA分类中就可以了,后续的所有工作都是自动化,自动计算SLA,自动生成报表,自动发布月度、季度性的可用性报告等等。展示页面和报表既可以按全公司的维度来统计SLA,也可以按项目,也可以按产品线,方式很自由。

4. 数据库备份文件及binlog的统计信息,每个库(含所属项目及IP等信息)的备份文件有多少个,存储路径,文件大小,保存天数(管理页面可修改)等数据的展示,业务团队可以在这里看到他们使用的库都在什么时刻有备份,如果想查询某个备份文件里面的数据,可以点击【恢复此数据到临时库】功能,然后后台脚本会在某个【临时恢复专用】的库中建一个新库,库名加上下划线+时间戳+随机字符,确保不会重名,然后恢复数据到此库内,恢复完毕后业务研发就可以进去查库了,这个库的有效期为1-4小时可选,时间到了会自动删库,如果需要继续访问,则需要重新申请。标记为敏感库的库在恢复完成后会由DBA删除敏感字段,然后才会把账号密码告诉申请人,非敏感库会直接自动发密码。Binlog则是解析成文本后用于搜索功能,这个是用awk写的,这里就不多说了。

5. 服务器成本统计,给财务用的。按项目或按云来计算每个月的成本是多少,这个我手里还真就有数据,给你们截个图吧,我说一下,这些截图里的数据。。。都是真实数据,数据信息更新于不到1小时以前(2024-03-30下午截的图),我的虚拟机里每小时都会跑一遍这些数据抓取脚本,当年测试时配的,离职后也没删,而我的这位老东家的权限管理也是有点不到位,我都离职这么久了,API KEY还是没改,导致我一直能获取到他们的数据,这算不算偷?我要不要提醒他们一下呢?毕竟我当时是公司的P8运维架构师。要不把我再招回去给你们做一波整改吧(开玩笑的),祝你们越来越好。

截图一:所有云平台、所有账号、所有项目、所有产品的总消费走势,也就是公司的总成本

截图二:按项目筛选,不限云平台和云产品,就是项目的成本,运营应该喜欢看这个数据

截图三:按云产品统计,可以看到我们所有项目在每个云产品上每个月都花多少钱

图片都打码了,红色抹掉的部分是真实项目的名字,只能抹掉了。平时看数据时可以根据下拉框自由选择查看,我们在两个公有云平台累计注册了7个账号,并非每个平台只有1个账号,而是多个。

截图四:某个账号的所有云产品及所有项目的明细统计(含与上个月成本的涨幅统计)

此外还有账单明细(需要打码的地方太多了)等页面,这里就不一一截图了。

下面继续说还有哪些报表。

6. 根据CPU内存统计每种配置有多少台服务器,查看配置分布。

7. 根据监控数据,按项目或按产品线计算总体资源使用率,这个前面已经有了,这里的功能则是按月均使用率建议使用率在30%以下的资源进行降配,70%以上的进行升配(平均70%-75%时,峰值通常会超过80%,有一定风险,可升配,资源即便翻倍,使用率最低也才到70%的一半,即35%,不会升完就让你降),页面展示的同时,每个月初还会自动发邮件给业务方。

等等等等,还有很多数据都可以统计,你能想到的都可以做,数据都在CMDB内,这些统计报表只需要按需求进行计算就OK了。

4.5 数据库

这里主要是提供各种查询工具和自动化工具,以及自动执行改库命令的工具等等,主要做了MySQL和Redis的,其他库由于没需求所以没做。

4.5.1 MySQL用户及权限查询

这个主要是为了方便DBA查看,在页面点一下下拉框筛选就可以了,不用去库里查,非DBA没权限。页面显示的是mysql库里面的db,user,tables_priv,columns_priv等几个表的数据。

4.5.2 MySQL各种状态查询

本页面主要提供mysql自带的各种show开头的命令查询,并将一些常用的或感兴趣的字段上移,如果某些值不符合预期还会变红显示,截图里并未这样显示,因为需要单独设置,我目前没有数据源,所以临时改成了直接输出所有返回项。

4.5.3 MySQL查询工具

相当于是个PHPMyAdmin功能,但是能限制权限,记录查询历史等等,数据库列表可以从资产系统里自动加载,只要CMDB中做了修改,这里一刷新页面就跟着变了。

这个功能的实现原理是使用MySQL的federated引擎,相当于Oracle里面的dbLink功能,也可以看做是MySQL概念里面的视图(view)功能(远程版)

我们的所有数据库都在客户机房,研发同事申请不了权限,即使我们建了账号他们也无法访问,网是不通的,VPN也不能给他们,运维机更是不可能允许他们登陆,所以这个功能会在权限申请审批通过后,使用前面大量使用的ssh反向代理功能,建立一条临时的转发隧道,根据申请权限的时长设置隧道的超时断开时间,隧道临时打通后,就会在一个办公网可访问的数据库内创建一个federated引擎的临时表,建表语句是代码每次重新生成,语句的末尾大致如下:

sql_desc[-1] =") engine=FEDERATED connection='mysql://%s:%s@%s/%s/%s'" % \
   (fed_dbuser,fed_dbpass,remote_dbhost,remote_dbname,tbname)

这是真实使用的代码的截取,这样通过前述的临时ssh隧道,指向最终的目标库,如果目标表在资产系统内登记了某些字段包含敏感数据,那么在创建federated表时会删除这些字段不映射,最终的效果就是建了一个跨机房跨库版的视图(view),研发使用的账号也是临时账号,虽然ssh隧道会在到期后自动断开,但账号也不会一直留着(以免成为脏数据),每次生成的用户名密码其实都是随机的,账号过期的逻辑使用了mysql库的tables_priv表的timestamp字段,这个字段的默认值是一堆0,我把他改成了过期时间的时间戳,授权脚本每分钟自动执行一次,用来检测是否有人申请访问某库,如果有就建一个临时账号给他,并会delete from这个表,where 现在时间 > timestamp字段,用这样的方式,把所有已过期的账号删除,最后再flush privileges; 这样一来,所有到期的账号都会在过期1分钟之内被删除。

有了这个功能以后,无论哪个项目的研发想查数据,只需要业务方审批即可后续的全自动处理,此时运维没有任何工作量,DBA只需要提前在资产系统内将敏感字段标记好,后面也不会有工作内容,所有功能都是全自动处理,于是运维团队又永久消失了一个高频的查询工作内容,实现这个功能也只需要两个脚本,建表的脚本不到100行,建临时隧道的脚本不到50行,网页不到150行,总工作量也很小。

下面看一下拿到权限后的查询页面吧,这个就比较简单了,我是用单机版截的图,并不是远程表,演示环境没必要真的配个federated表出来,只用本地表演示一下页面功能吧。

截图一:查询功能首页

截图二:点击表名可以查看表结构

截图三:执行查询操作,我们这里查的是资产系统的修改记录的表,我们前面在资产展示页面看过这些数据,现在是直接查库的方式看表结构里的数据,目前一共两条。

表格上方的复选框(checkbox)功能和以前说过的一样,取消哪列的勾选,哪列在下方的表格中就不显示,默认是全勾选,全显示,输出格式默认是html,还可以输出json或csv格式的,用于给代码使用或者导出Excel使用。

截图四:上面同样的命令,输出json格式

截图五:上面同样的命令,输出csv格式

此外还有个自动执行SQL语句的功能,但页面中只有选库的下拉框和输入语句的大输入框,以及一些使用说明的文字,所以就不截图了。

4.5.4 Redis查询工具

由于Redis在项目中大量使用,所以也开发部署了一个Redis查询工具,并部署到所有项目的k8s中,工具支持string/hash/list/set/sorted_set(geo)格式数据的查询,查询时默认只显示红色字体那一列,点击【显示详细信息】的按钮后是下图这样,工具会自动去识别每一个KEY的类型,KEY的长度,TTL,并将TTL计算成到期的时间点。

点击某KEY就会直接查这个KEY,不用考虑KEY类型,工具会自动识别并调用对应的方法去读取内容,其中字符串的查询结果还支持json解码,php反序列化,unicode字符转中文等功能,图片只截一个json解码的吧,一开始都截图了,但是图片占了11页,太长了,所以又都删了。

JSON字符串查询

解码后截图如下:

字符串类型的KEY还支持基数统计(hyperloglog)查询,长度小于(20字节)时支持按位查询,基数统计时会计算pfcount,按位查询时会执行bitcount以及按位查询每一位的getbit值,由于输出很长,所以工具限制了20B以下才支持按位查询(此限制可调)

不可变集合(zset):

不可变集合是带score值的,另外GEO类型的KEY在Redis中会被识别成zset,所以zset类型的KEY页面提供按GEO查询的功能(见表格最右面的按钮)

这里的这个KEY按地理位置查询是什么也查不出来的,他只是一个普通的zset,下面的KEY则是真正按地理位置功能生成的KEY:

由于也是ZSET类型,所以按zset方式依然可以查出内容,但score部分的数据就没有实际意义了,必需按GEO的georadius和georadiusbymember等功能查询才可以,使用详情如下:

截图也都删了,只留这一个吧,以免redis部分的篇幅过长。

其他基础功能:

【info】按钮会去执行info all

【config get】按钮会去执行config get *,如果输入框里输入了值,那么就只查这个配置项,不查其他的,什么都不填时会查所有的配置信息,这也是默认行为。

【client list】按钮和redis默认行为一样,只不过做了表格展示,没有命令行那么错乱。

【cluster nodes】命令只在cluster模式生效,连接redis时,只需要提供1个IP+端口即可,其余节点会用cluster node获取后自动补全,当然了,我们这个集群的IP列表可以直接从CMDB获取,无需手动填写,下拉框里优先显示DBA给这个集群起的名字,IP列表只显示几个字符就行,查询页面才会显示完整的IP列表。

以上便是Redis的全部功能,几乎支持所有的日常查询需求,我们将此查询工具打包成docker镜像,部署到每一个项目的k8s的运维监控使用的namespace内,然后直接连【redis.业务namespace】这个k8s的内部地址就可以直接访问本项目k8s中的redis了,项目上线之前就可以部署好工具,这样在今后的工作中,研发想查数据就直接自己去查就行了,全程不需要运维和DBA参与,有什么想知道的都可以自己去查,无论是数据还是配置,这里都查得到。这样运维团队就又永久性消失一个工作量。

如果是在运维管理平台内查询,则需要确保能连得上库的IP端口,如果不通,则需要配转发或者公网IP等,连的时候连转发地址或公网IP,但显示的时候只显示内网IP,此模式只适合单机版或主从版,不支持集群版,为啥呢?因为集群版你用公网IP连到了一个内网IP端口后,若该key的slot不在这个实例内,你的请求会被redirect到另外一个节点,这个节点的地址是内网IP端口,此时你就连不上了,这也是我们把工具部署到项目k8s内的原因,这样一来,我们直接转发这个工具的访问地址就行了,而不必转发redis的多个端口。

4.5.5 MySQL备份文件管理

这个前面说过了,就是为资产系统里mysql类型的资产设置各种属性,例如备份频率,可执行备份的时间范围,备份文件保留天数,备份文件的名称路径等等,所有属性都在这个页面里设置,备份脚本从这里获取信息,然后自动执行备份及删除就备份,这样便可以一个脚本实现所有业务库的备份,不需要写一堆专用的脚本,谁也记不住在哪。而这个唯一的脚本,则可以使用自动化功能部署到所有服务器中,crontab也可以自动配好,在某个时间范围内,每隔一段时间(例如5分钟)就执行一遍,如果当前的时间与管理页面内配置的可备份的时间段不符则脚本自动退出,如果时间相符,检测锁文件,判断今日是否已执行过备份,未执行过时才执行,并在执行完毕后加锁,以免重复执行。

4.5.6 MySQL binlog文件的备份与分析

这个功能用于给研发团队提供binlog的搜索功能,有时候库里的数据不太符合研发的预期,但他们又不知道是什么时候被哪条语句改的,而binlog中是有这个数据的,但是想看还得先转成文本再搜索,而转成的文本的可读性也非常不好,所以做了这个功能,用的是awk,把binlog的解析成时间戳和语句,然后就可以在搜索功能里面使用了,这部分贴一个awk的解析脚本吧,其他的就不贴了。

代码一共就18行,没有你想象的那么长,把mysqlbinlog命令解析出来的文本保存到一个文件中,再把这个文件的全路径作为参数传给这个awk脚本就可以了,然后它就可以把文件解析好,并按时间进行切割,每小时一个文件,保存到/data/binlog_split_by_time这个目录内,搜索功能也没使用什么搜索引擎,就是一个后台脚本按用户选择的时间范围,去磁盘对应的txt文件里grep,并在网页里展示,仅此而已,非常简单。

4.5.7 MySQL表结构对比功能

选择两个表进行表结构对比,通常是同一个项目的开发环境、测试环境、生产环境等,选取两个环境进行比对,如果两个库的某个字段的定义不一致,则这个字段的显示要把背景色变红,页面大致长这样:

当某个版本的代码在测试环境一切正常,到了正常环境确不正常时,若日志里报数据库的错误,而DBA又反馈数据库运行正常,此时很可能是表结构不一致导致的,例如新版代码里使用了一个新列,这个列(字段)在测试库里已经alter table add column加上了,但是生产环境没加,所以代码肯定报错,或者是加了但是定义不一样,比如某金额字段,测试环境的定义是float(7,2),功能正常,而生产被定义成了int(7),那么生产环境的精度必然是不准的,这样计算出来的数据肯定就不对。

这种情况排查故障是很难的,也许需要很多研发、很多运维、很多DBA,一起开很多会去讨论和排查,最终用很多时间才能定位到是表结构的问题。我的例子里表结构的异常还是很明显的,但如果不明显呢?假设测试环境与生产环境对某字符串字段的定义分别是char(23)和char(22),当某条数据刚好是23个字符时,生产环境就乱码了,但又不是全部乱码,只有这一条乱码,因为它丢了一个字符,这时是很难判断的。

此时这个对比工具就派上用场了,工具看起来功能很简单,但却可以帮公司整个团队避免很多故障,或者是故障发生后快速定位原因,试想一下,如果研发、运维、DBA用了一整周的时间都没查出异常的原因,公司的损失有多少?这些人一周的工资总和是多少、公司的直接损失就是多少,还有很多隐性的损失,例如项目开发进度逾期,员工心态受到影响,甚至各岗位之间的互相质疑等。

对比也不只是网页查看,也可以用脚本进行检测监控并发邮件提醒,项目上线前、研发与DBA也应该到这里把所有表都和测试环境对比一遍,以免QA说测试环境正常同意上线,但上线后就各种莫名其妙的故障。

4.5.8 数据库功能小结

数据库的工具还有一些,我就不一一展示了,以上所说的所有工具,都会有权限控制,涉密字段加密转换等。

以上所有功能,都是为了减少研发团队找运维和DBA的次数频率,使用工具全自助处理,这样我们便可以只审批,而不用真正去做执行。

每当我们做出一个新工具,我们整个团队都是永久性消失一个工作种类,否则每天都会被要求查这个查那个,而这类所谓的“日常工作”内容就会越来越多,最后所有人都会天天忙的要死,从上班干到下班,长此以往所有人的精神状态都会非常萎靡(累的),而若有人不想坚持了而离职,那么他离职后将会有很多信息随着他的离职而丢失,因为纯手动模式下有大量的任务细节都只依赖于当时的那个执行人,而这个执行人一旦离职,所有的信息也都会跟着丢失。即便这个人与同事关系特别好,离职后也可以找他问,但离职几个月以后,记性再好的人,对这些信息的印象也会变得模糊,直到完全忘记。

这不是我希望看到的场景,所以才会做这些工具,而这些工具并不是解决某个人某次的需求,而是为了让某类需求彻底“消失”,只有这样,才是我心中真正的自动化,每天上班喝茶看监控不香吗?为什么非得手动做各种任务把自己累的要死呢?你说对吧?

我以前在应聘的时候总和人说我一个人能干50个人的活儿,但并不是真的把50个人的活儿丢给我让我一个人做,我就算加班加点每周7天都不休,最多也就能干2-3个人的活儿,我是干不了50个人的活儿的,我的做法是通过上述的那些方案设计及自动化功能,让49个人的工作量消失,最后只剩下一个人的工作量,然后我再把剩下的这一个人的任务做了,用这样的方式实现一个人顶50个人的效果,而不是真的一个人去干50个人的活儿。

4.6 权限管理

这个就比较简单了,用RBAC的方式设计权限角色,也可以叫【权限组】,每个需要授权的展示信息,都会去依赖某个权限组,依赖的函数可以加参数,用于控制不同的行为,有权限时就直接显示数据了,没权限时根据参数进行分类,有的类型直接不显示,就像没有这列似的,有的显示【没权限】字样,让你知道有这个东西,只不过你没权限,此时你可以去申请权限,运维管理平台内95%的功能页面都对所有人展示导航入口,这样设计就是为了让大家知道我们有这个功能,否则你若连这个功能的存在都不知道,又怎么去申请权限呢?

这里我吐槽几句,很多很XX的领导都会说一句话:你不知道你问啊。而我想说的是,你作为领导你得让人家知道有这个东西,然后人家才能去问这个东西咋用,各种细节等,你若是不说,别人将永远无法知道,就算他今天知道了所有功能,然后还防御性地问了你一句【我们是否还有其他内部工具是我没权限的】,然后你说没有,但是第二天又上线了一个新功能,此时他又不知道了,这怪谁?人家明明已经问过了。当他下次和你对话时你咋说?还说【你不知道你问啊】?如果你是这样的人,我敢肯定你团队的人没有谁能对整个线上环境有一个准确的完整的认识,工作全靠猜,最终肯定就是天天开会沟通各种信息。如果你是个普通员工,而你的领导是我描述的这样,那你赶紧投简历准备换工作吧,跟他混没前途。

4.7 运维排班

有些企业的业务场景是需要运维24小时值班的,而作为团队管理者,你得让全公司都知道你的团队几点有谁在岗,所以需要在运维管理平台内设置一个排班表,每个人哪天上什么班次,每个班次的上下班时间是几点到几点,这些信息必需明确地写出来让别人随时能查,否则也会天天被问,比如运维某某某在哪呢?昨天他给我处理了一个什么什么事,今天怎么一直没看到他?大致这样的场景,如果有公开的排班表就不会出现这种情况,这个人也不至于为了等某个人上班而白白浪费时间,排班表里的信息还可以每天定时用机器人发到值班群和支持群里,让群成员知道今天每个人上什么班次,我们之前讲解工单系统时也用到了这里编排的数据,每次研发提交新工单或者监控脚本自动创建监控类工单时,都会根据排班表选取一名在岗的运维或DBA去执行,而不是纯随机选,毕竟不是所有人都每时每刻都在线。

4.8 规范制度

这个这里就不细说了,放到后面的大章里讲,只有一条强调一下,每个人名下的工单,下班前必需处理完才能走,如果预计耗时很长,可以把执行人改成下一个班次的运维,俩人做了交接后才能走,这样便可以不影响业务方的进度了,每个运维也都不需要加班。

5. 配置管理(puppet)方案设计

我们前述的很多功能(监控和CMDB)都依赖各种脚本,而这些脚本都需要部署到所有服务器中,并在将来有变更需求时,要一键部署到所有项目的所有服务器,在这种需求背景下,我们肯定是要使用一套自动化工具的,我用的是puppet,选它有几下几点考虑:

原因一,其实前面说过了,因为在客户机房部署时,它需要的权限是最少的,申请网络权限时受到的阻力也最小。

第二个原因更实际,那就是我对它比对其他方案更熟,所以自然选择自己熟悉的方案。

第三个原因是,很多系统基础配置的puppet代码我早就写好了,到了每一家任职的新公司都可以直接拿来就用,而换方案则意味着所有功能都要从零开始重头做,成本高。

第四个原因,我当时的团队里没人会其他方案,所以我用哪个都一样,如果大家都会另一个方案,而不会这个,那么我会放弃前三个原因中考虑的点,而直接选用大家都会的方案,这样整个团队的总学习成本就是最小的,这个我们到团队管理章节再细讲。

综上考虑,puppet成为了我唯一的选项。

实现自动化有很多工具,常用来互相对比的有puppet/saltstack/ansible/chef,这几个都差不多,没有谁在所有方面都完全碾压另一个。

我在很多文档里都看见过作者做了很具有主观性的结论性的论断,那就是哪个哪个方案更好,其他的不行。我见过有人说saltstack极不稳定,不适合生产环境大规模使用,他推荐的是puppet。 还见过有人说saltstack可以做二次开发,特别方便,非常适合做自动化,而puppet正在逐渐被市场淘汰。

这两个人说的都不对,试想一下,如果某方案在所有方面都比另一个强,那么有谁还会使用另一个呢?另一个方案早就被淘汰了,但现在既然这些方案都有人在大量使用,那就说明他们都各自有自己的优缺点,故而选方案时没有谁是最好的,只有谁是最合适的,大家定方案时也要综合考虑多种因素才行。

由于我在工作中用的是puppet,所以我下后面就不提其他方案的做法了,其实都差不多。

5.1 服务搭建

这个老生常谈了,这是一篇方案设计的文章,向大家分享如何提高工作效率,而不是某个服务的搭建文档,所以这个步骤略过。我用了哪个版本号之类的信息也不重要。

5.2 部署方案设计

还记得我们的场景吗?每个客户都有不同的机房,不同项目的服务器是互相不允许互通的,所以我不能用同一套puppet去管理所有项目的服务器,合规上也不允许,但我又不能为每个项目都单独设计一套,这与我们的自动化理念相悖,不能用同一套,又不能用两套代码,那怎么办呢?于是,我便有了这样的设计:

1. 每个项目的确都会单独搭建一套puppet server,这个是没办法的,搭建使用的服务器就是每个项目的运维机VM-B

2. Puppet的代码并不是每个项目专用一套,而是使用共同的一套,以定期同步的方式同步到每台运维机的配置目录中,同步源可以是某台服务器,也可以是svn/git等代码库,我采用的方式是直连代码库,当然这个库默认也是不通的,还记得我们设计的40000-40010这11个端口吗?目前我们只使用了40000端口,轮循到了入口运维机VM-A的ssh端口,我们在1.1节的那个设计图里还简单标记了一下40001端口轮循到了yum服务器的80端口,40002端口指向harbor服务器的443端口,于是,我们可以使用40003端口轮循到代码库服务器的对应端口,于是,我们就可以在所有运维机VM-B中使用IP-A:40003来访问了。

3. 所有项目运维机都会使用crontab配置成每分钟执行一次更新,故每当我们提交一次代码的变更后,1分钟之内(最快1秒,最慢59秒)所有项目的运维机就都跟着变了,于是,我们就实现了一套代码管理所有服务器,而任意两个项目之间又都没有数据交互,每个项目的服务器的证书都是保存在自己项目的puppet server的ssl目录中的,每个项目的puppet的ssl目录里的文件都不一样。

于是,我们得到了如下的puppet server部署设计图:

4. 大部分功能配置在所有项目里都是一模一样的,例如脚本的部署,系统参数的优化等等,但难免有些功能在不同项目里需要设成不同的值,于是,我们在每台服务器初始化之前,都会给这台机器定义一个变量,用于判断这个服务器属于哪个项目,这个变量的术语在puppet内叫facts,在saltstack内叫grains,在chef中叫node attributes, 在ansible中也叫facts,所以我们在给每台服务器首次跑初始化之前,都会定义几个facts叫【公司名_project】【公司名_idc】,用来定义这台机器属于哪个项目,部署在哪个机房,此文件在服务器首次初始化时设置,并永远不变,而前面提过的所有监控及采集脚本,都会引用这个值,并自动出现在各类报警脚本和资产系统内,这又是一个全自动的过程。

5. 此文档从第1页起到本行结束内的所有部署都没有说明是如何进行的,如果前述所有步骤都手动也是极其耗时的,所以,我们将前面提过的所有功能,都以puppet模块的方式进行了配置,故我们的系统初始化脚本则只做了两件事,第一件事,配置一个临时的yum源,用于安装puppet的agent客户端,此时系统尚未初始化,我们在项目内网并没有yum源,运维机自己必须也跑完初始化,它的硬盘里才会有内网yum源的rpm包们,而此时我们正在配置运维机的puppet,所以此时yum源是不存在的,于是,我们才会在入口运维机VM-A定义一个40001端口指向到我们的外部yum源,用于配置运维机时临时使用,这个源只要能确保里面有puppet的agent就可以了。这个配置文件我们使用的是/etc/yum.repos.d/目录下的tmp.repo文件,并会删光所有其他文件,所以装包时不会受已有配置文件的影响。而agent装好首次运行时会自动删除tmp.repo这个临时配置文件,并使用我们自己编写的yum模块将服务器的yum源指向本项目的内网源地址(也在运维机VM-B里面)

6. 首次初始化时,运维机会引用【yunweiji】模块,其他服务器不会,而【yunweiji】这个模块会自动部署一些只有运维机才需要的功能,例如yum源的同步脚本,此脚本运行的crontab,从VM-A同步时间并将本机也配置成ntp server等等,运维机跑完初始化以后, 所有server类的功能就都在运维机里存在了,当然yum源的文件的同步是需要一段时间的,但这并不影响其他服务器的初始化,因为其他服务器在初始化时,会把yum源指向到运维机,所谓的“指向”就是把yum地址改成http://puppet/yum/repo名字等,由于每台服务器都会把puppet这个名字解析到自己项目的运维机,所以这个yum地址就变成了固定的,而运维机上提供yum服务的是nginx,nginx里面我们设置了try_files选项,如果要访问的文件在本机存在就可以直接访问,如果不存在,就会使用后面uri里面的值,而这个值就是我们的http://VM-A:40003/yum/repo名字,当然其他服务器直接访问VM-A也是不通的,只有运维机才通,所以我们在其他服务器配一条路由就行了,命令大概是:route add -host VM-A gw IP-B, 【yunweiji】模块会在运维机VM-B上开启port-forward功能。以上步骤也都是全自动,这些服务器只需要运行一下那个初始化脚本(功能只有配临时yum+装puppet agent的包+运行puppet agent,没了),自动化跑一遍以后就什么都有了。

5.3 puppet模块简介

我这里只介绍几个能让50个人工作量变成1个人工作量的配置项,其他专用性的模块就不介绍了,大家可以参考如下的做法,了解一下我是如何让49个人的工作量消失的。这就是我提高运维效率的方案与做法。

5.3.1 系统初始化入口模块【system_initial】

这个模块拆成多个模块也是可以的,但由于功能在所有项目里都相同,所以我干脆合并成了一个。以包管理和用户管理两个功能为例,如果拆成两个模块,就会分别叫packages和users,配置文件就变成了packages/manifests/init.pp和users/manifests/init.pp,而我是把所有功能都合并到了system_initial模块中,所以配置文件就变成了system_initial/manifests/packages.pp和system_initial/manifests/users.pp,其他通用功能(假设叫xxx)的配置文件就变成了system_initial/manifests/xxx.pp,而在这个模块的init.pp中我则会依次include packages, include users,include xxx等等等等。

所有node结点都会include这个模块,所以这里的所有配置项都会应用于所有服务器,那么我们所做的一些系统优化或安全整改类配置就会应用到企业的所有服务器中(不限项目),所以我们每次一个小小的修改,都会让这个整改项的梳理,统计,修改,再统计,中间夹杂的多次开会,最后的汇总,等等,这些事情都会消失。

5.3.1.1 装包模块【system_initial::packages】

这里会使用一个很长的列表,使用package资源类型,让每一台机器都把这些包装上,例如netstat,lrzsz, vim, screen, wget, rsync, bzip2, unzip, traceroute, nslookup,lsof等等等等所有常用命令所在的包,于是我们就不会出现排查问题时发现命令不存在的尴尬境地了,还记得我们在第一章的运维痛点里描述过的场景吧?

这里同时还安装了很多我们的自动化脚本需要的包,python的居多,例如python-redis,

python-requests, python-psutils,pyOpenSSL等等等等,如果需要某个新包,就继续在列表里追加就行了,puppet会自动装好,如果yum源里不存在,我们就去互联网下载这个包,然后传到自建的yum源里,然后所有项目运维机VM-B就会把这个文件同步进来了,当然了,每天的同步只有一次,且是在夜里,但白天依然会因nginx的try_files设置而可以立刻使用,除了下载速度稍微慢了点。

5.3.1.2 服务模块【system_initial::services】

专用服务我们通常是在各自的模块内配置的,例如nginx服务会在nginx模块里配置,ntp服务会在ntp模块里配置,这里的服务主要是一些不怎么需要配置的通用服务,例如我们会在这里配置crond这个服务必需是running状态,否则crontab将不生效,而系统自带的一些无用服务我们要确保他们不运行,比如gdm这个gnome桌面使用的服务就是我们不需要的,如果客户给了一个图形系统,那么我们要把这个服务关了,以免占用资源,还有就是cups这个打印机服务,我们也不需要,于是我们可以添加这样几行配置:

service { [‘gdm’,‘cups’]:
    ensure  =>  stopped, # 确保服务立刻停掉
    enable  =>  false,   # 确保重启后也生效(不会随系统启动)
}

这样无论这俩服务是否在运行,跑过一次自动化后就都停掉了,同时也设置为了开机不自启。

如果发现系统内有其他不需要的服务,就继续像这个列表内添加就行了,只要加几个字母,就可以让所有项目的所有服务器都生效了。

5.3.1.3 用户模块【system_initial::users】

为了安全,我们通常不允许一些系统用户登陆,做法是把它的登陆shell从/bin/bash改成/sbin/nologin,相信很多企业的运维工程师都被安全团队的工程师这样要求过吧,每次有这种需求,也都会经历开会,讨论,统计,执行,再统计,再开会等一系列重复性操作,刚好我们的系统有几个用户的默认登陆shell不是/sbin/nologin,所以这个工作是100%存在的,那么我不想经历上面的痛苦过程怎么办呢?这个模块就是为了避免这种场景而存在的,我们可以使用如下的配置:

$nologin_users_str= “daemon,bin,adm,uucp,lp,nobody” # 这几个是常见的可登陆用户名
$nologin_users_array = split($nologin_users_str,’,’)
user { $nologin_users_array:
    shell => ‘/sbin/nologin’,
}

这样配置后,无论以前是否可登陆,跑一遍puppet agent以后,也都变成了无法登陆。

下面还有几行file类型的文件配置:

$allow_login_users=”user1,user2,user3” # 这里配置几个允许登陆的用户名,可以为空
file { ‘/data/configs/allow_login_users’:
    content => “$allow_login_users\n”,
}

我们所有运维自动化的配置文件都在/data/configs目录下,而这段配置会在这个目录下建一个叫allow_login_users的文件,文件的内容就是上方那个逗号分隔的用户名列表。

然后我们就可以使用一个脚本来判断/etc/passwd文件中的用户的配置是否正确了,我们把所有可登陆的用户名提取出来,看看它在不在这个文件里,也就是白名单,如果不在就报警+建工单,如此便可以发现那些非法的登陆用户了,平时如果哪个运维临时设了一个,那么这个监控可以及时发现并建工单,此运维可以认领此工单,用户用完后由他进行关闭。这功能也可以防止黑客进来留后门,无论他设置了哪个用户允许登陆,只要不在我们的白名单里就会报警。

以上其实都是安全增强类的配置,但由于是修改用户属性,所以放到了users模块。

用户属性配置的功能还有如下这些:

为常用服务的用户配置固定的UID和GID,例如nginx,redis,mysql等用户。用此模块确保所有服务器上相同用户名的用户拥有相同的UID,因为系统是按UID判断权限的,而不是靠用户名,假设nginx用户在机器1里的UID是501,他拥有的一些文件被scp到机器2后,该文件的所有者依然是机器2中UID为501的那个用户,而机器2中nginx的UID不一定是501,所以文件拷贝到其他机器后,我们原希望文件依然归nginx所有,但却因UID不同导致了机器2中的nginx用户没权限,此时不得不执行chown nginx filename来重新设置权限,chown命令会在机器2中查询他的nginx用户的UID是什么,比如查到了是888,那么chown后,该文件的所有者就成了UID是888的那个用户,而机器2中这个用户是nginx,所以你就看到了文件所有者是nginx,为了避免这种文件权限错位的情况,我们把所有可能用到的业务用户,都从501开始编号,以确保不会有冲突。而运维工程师和DBA使用的普通账号,则从1001开始编号,同时还会创建一个UID为3000的无用账号(设置为禁止登陆),这个账号的作用是让UID3000存在,此时运维若临时useradd添加一个用户做测试的话,这个用户的UID就会从3001开始编号,如果不设这个3000,假设我们的运维有18人,那么UID编号就到了1018,如果此时某运维临时useradd了一个用户,那这个用户的UID就会是1019,此时若有第19个运维入职,我们为其设置UID为1019后,到本机执行puppet agent将会因UID冲突而报错,所以我们用了一个3000的UID将边界后移,用于各种临时测试的场景。而3000以下的UID全部在这个users.pp文件里进行管理,所有运维打开这个文件一看就知道编号编到哪了,编号规则是什么等等。

这个文件还会使用ssh_authorized_key模块为用户自动部署ssh公钥,私钥会使用【yunweiji】模块部署到运维机内,这就是我们从运维机能免密登陆其他服务器的原因。

我们还会使用user模块为root用户设密码,并自动删除未注册的ssh公钥,代码如下:

user { 'root':
home => '/root',
    password =>   '$6$1xsn4OCG$7 中间省略  HZC9Qh9v.oGT7',
    purge_ssh_keys  =>  true,
}

其中password那行会被更新到/etc/shadow的第二段密码字段,我们通过每三个月修改一次这里的配置值的方式实现了每三个月更换一次密码的要求,工作量仅需不到1分钟。

purge_ssh_keys的选项表示只有在puppet里面用ssh_authorized_key部署的ssh公钥才允许存在,如果检测到了其他公钥则会自动删除,这种方案避免了系统被人留后门,很多人登陆过某机器后都会把自己的公钥放进去以便下次登录方便,但从安全角度看这是不允许的,所以通过以上几行简单的配置,实现了即便某人违规配置也会在自动化运行时被还原,这会大幅减少我们的整改项,统计的时间消耗等等。

5.3.1.4 脚本模块【system_initial::scripts】

这个模块的内容很少,功能只有一个,递归部署本模块脚本目录下的所有文件到每一台服务器的/data/scripts目录,我们的各类脚本很多,都在scripts这个目录内,但为每一个文件都编写一个file资源是不现实的,所以我用这个模块直接部署scripts整个目录,目录里有什么就部署什么,代码如下:

file { '/data/scripts':
    ensure  => directory,
    mode    => '0755',
    source  => "puppet:///modules/${module_name}/scripts",
    recurse => true,
    purge   => false,
    force   => true,
    backup  => false,
}

依然很短,这里的purge表示删除未被此资源管理的文件,但这个选项我设置的是false,表示不删除,因为其他模块也会在目标机的这个目录里部署脚本,运维与DBA平时也会为某些具有特殊情况的服务器配置专用的脚本,脚本也会放在这个目录里,所以不能删。我们所有脚本都只能放在这个目录里,不能放其他的地方,这是规范。backup设为了false表示无需备份,因为确实没必要,就算你把某个文件删了,下次puppet运行时这个文件就被重新部署回来了(我们设置的频率是每30分钟执行一次),而这个脚本的变化则会在代码库里体现,svn/git都有这种功能,我们没必要在每一台服务器都备份每一个文件的每一个版本,这种设置会让服务器里的文件看起来更简洁,而不是一堆带下划线+时间戳的备份文件。

5.3.1.5 定时任务模块【system_initial::cron】

我们有很多公用的定时任务,也是配置在固定的文件中,文件路径是/etc/cron.d/公司名缩写,比如时间同步的定时任务,每隔几分钟执行的各种监控数据采集的脚本,各类监控脚本,他们的定时任务都在这个文件里进行配置,这个文件里的内容不会出现在crontab -l的输出里,crontab -e的功能我留给了运维工程师手动使用或临时使用等场景,而自动化功能则使用了修改起来没那么方便的磁盘文件的方式。为避免同一时刻所有服务器都执行同一个访问某资源的任务,部分脚本还使用了puppet的erb模板,将执行的时间打散到了30分钟内,效果是任意30分钟内,每分钟都有服务器在执行这个脚本,有的早,有的晚,但30分钟以后,所有服务器都会执行一轮,比如我们的puppet客户端就是这种方式运行的。

5.3.1.6 安全加固模块【system_initial::security】

这里也是使用file资源类型,分发了几个/etc/pam.d目录下配置文件,使系统的安全性得到一定的增强,通常公安部工信部的安全检查也会要求这几个文件的加固,主要实现密码长度及复杂度的限制,密码输错5次后锁定半小时等功能。

以上是几乎所有服务器都一样的配置项,所以都放到了system_initial模块里,每个地方稍微有点不太一样的是单独做了模块,其实这么分有点不太合理,最早的时候是没有system_initial模块的,后来才合并的。

5.3.2 SSH服务模块【ssh】

实现ssh功能及安全性增强,例如自动分发服务端配置文件(优化好的),修改了默认端口号,禁止root登陆等等,server端的版本设置为最新版(目前是9.3吧?),centos7/8/9及ubuntu22的默认版本都是8.x,早就爆出安全漏洞了,使用此模块,无论何时在哪创建一台新虚拟机,跑完自动化以后,openssh-server的版本都会被自动升级到9.3,再也不用被安全团队天天追着整改和统计了。

5.3.3 权限管理模块【sudo】

这里会在/etc/sudoers文件定义好各种服务器组,命令组,用户组等等,users模块会引用这里的文件sudos/files/<username>,运维、DBA、研发、安全,每个组都有预设的权限组。

这个功能配好后,今后各种岗位的人无论入职离职,就都只需要在users模块里加减用户名就行了,又消失了很多工作量。

5.3.4 系统调优模块【tuning】

通过文件的方式修改/etc/profile, /etc/security/limits.conf, /etc/sysctl.conf等文件,用于配置用户环境,资源限制,内存参数优化等等,只要跑过自动化,这些优化就都有了,如果新增优化项,只需在这里修改,所有新旧服务器就都会在30分钟内生效了。

5.3.5 域名解析模块【dns】

主要用于批量差异化配置/etc/resolv.conf,部分服务器使用host资源类型配置/etc/hosts文件

5.3.6 中间件及数据库的安装模块【nginx/mysql/redis/等等】

这不是一个模块,而是每个服务都有一个,功能都是先装包,然后部署配置文件(优化过的),最后设置服务开机自启。其中配置文件部分均只配置默认入口文件,而每台差异化的配置项会在include的目录里单独配置,这个模块会确保这个目录存在。Nginx模块还会额外部署SSL证书文件,确保所有引用了nginx模块的机器都会拉取puppet内的证书。这里的功能是只配置基础功能,不负责最后的搭建,例如mysql是不会自己去配主从的,谁和谁是一组,谁是主谁是从,这些功能由DBA手动处理,这个自动化功能会确保把mysql给他装好,目录结构等都符合我们的规范,其他的就都不管了,这样这个模块的功能就会很简洁。

5.3.7 软件源模块【yum】

这个模块会自动配置内网yum源,并部署一个脚本和为这个脚本配1小时执行一次的定时任务,这个脚本的功能是,把未在puppet::yum模块中配置的yum配置文件移走,只允许我们自己的配置的源生效。我们配置的yum源的文件路径都是/etc/yum.repos.d/公司名_xxx.repo,凡是/etc/yum.repos.d/目录下不是以公司名开头的文件,都会被mv到/etc/yum.repos.d/bak目录下,并在执行了这个操作后执行yum clean all和yum makecache,这个模块的stage也被设置了在main之前,所以首次初始化时会先配yum,后执行其他模块进行安装各种软件包。

5.3.8 时间模块【time】

安装ntp包,配置本机的时间同步功能,设置时区等等。

5.3.9 其他杂项

配置vimrc文件,把tab改成4个空格,换行时不要自动缩进,vim设置默认中文编码,设置执行过的命令的保存方案,rsyslog配置, logrotate配置,自动清理/tmp下3天未使用的文件,等等等等,各种杂七杂八的功能,凡是日常工作中会给我们造成困扰或影响我们工作效率的地方,我们都会在puppet内进行配置,并一次性永久解决所有新旧服务器里的这个问题。

其他业务相关的就不介绍了。

5.4 puppet小结

根据以上信息可知,我们并未使用puppet的高级功能,仅用了它的基础功能,这都出自我的【让配置更简洁】的运维工作理念,每个功能或脚本都不要太长,这样无论谁接手都能一眼看懂,不会出现人员离职了就没人能搞定的情况,配置文件不过长也不会出现谁因为项目1改了某文件,结果把项目2的文件也给一并更新了这种情况,这就属于人为误操作造成的故障了,为了避免这种情况发生,所有的配置都不会很复杂,就像监控脚本的功能设计一样,简洁,高效,易维护。

同时也因为配置很简单,所以这套puppet系统可以很容易地换成ansible或者saltstack,并不是永远只用这个,具体方案的选型还需要考虑团队成员的技术栈等因素。

至此,我们所有项目的所有服务器,都会在运维把机器拿到手后,立刻加入puppet自动化,跑了一遍agent以后,所有的系统优化都有了,安全加固都有了,监控脚本都有了,信息采集两分钟以后就自动出现在资产列表及监控页面里面了,各种报表和自动化工具也都会自动添加这台机器,运维团队的工作量什么都没增加,所以在这种情况下,别说是5个运维了,哪怕只有1个运维,也能干完50个人甚至100个人的活儿,所以我们的5个运维在日常工作中并不是在忙这些事,运维做的都是业务方面的工作,相当于是很多企业的业务运维团队,而一些类似基础设施团队,或叫资源团队,这些我们都不需要,一个人都不需要,甚至运维开发也不需要那么多人,因为我们需要开发的功能异常简单,开发量很少。DBA也不需要天天折腾搭建和备份的事,这些都自动化了,我们DBA的日常工作只剩下分析慢查询的原因,并协助与督促研发团队进行修改,于是我们的产品更加健壮了,效率的提高还节省了研发团队大量的时间,项目进度的推进也会大幅提高,间接为公司节省了海量成本。

6. 运维方案总结

通过前几大章我们可以看到,我们的运维方案以配置管理功能(方案任意)为中心,自动部署各种信息采集脚本,再把采集到的信息汇总到资产系统内,最终基于资产系统编写各种查询、监控、报表工具,整个架构中没有特别复杂的组件,每一个小功能都是独立且简洁的,但是大家在一起相互配合,就组成了我们的全自动系统,而且此模式的自动化系统不限项目数量,不限机房数,不限云平台,服务器规模可以无限制增长,规模越大我们系统的优势就越明显,也正是用了这样的方案,我才能在不到两个月时间内迅速解决掉公司的那十几项大痛点,并在接下来的一个季度内,在公司业务猛增好几倍的情况下,我们运维团队的工作量反而下降了不少。

6.1 本文档中与现实不符的地方简述

我在文中分享了很多方案,但实际配置中,并非与文本完全一致,有些功能是需要额外考虑企业的内部情况的,还有些实现的细节也并未交代(技术差者可能会误以为无法实现),还有我写的这些内容并非在同一家公司做的,而是2015年至2024年这十年间在不同的公司做的,每家公司都做过一部分功能,其中80%的代码都是在2015-2020年写的,并不是最近写的,近几年的代码都是在以前代码的基础上稍微改一改,让它符合新公司的环境,完全新写的虽然也有但并不多。最近几年主要是做方案设计比较多,比如这个运维机VM-A和VM-B的模式就是最近这家公司任职时设计的,以前的公司的场景比较简单,公司只有自己的机房,不需要去客户那边做交付部署,自己的机房内,内网都是互通的,所以就不需要两级跳转的方式,还有就是IT资产管理类的功能,是在上上家公司担任运维架构师时做的。而现在则是为了写文档时呈现最好的状态,所以把我过去10年做的事情都糅合到一起了,如果完全从0开始做起的话,我需要至少半年,绝不可能在2个月内完成,现在能快速搞定也是因为以前有大量的积累。如果有家企业能给我两年的时间,以上所述的所有功能我都会一个个做出来,这也是我的终极目标。

6.2 如何借鉴本文

虽然大部分企业都不是大量去客户现场交付部署的模式,但大家依然可以参考我这种方案来做,同一个云平台在不同城市的机房其实也是可以单独当作“项目”的,可以为每个城市的机房都设计一个运维机,这样就可以避免所有服务器都连同一台运维机,可以节省大量带宽,以及规避掉很多网络造成的问题,我这种分布式的方式使得跨网的访问只有VM-A到VM-B之间,而VM-B管理业务服务器则全是同机房同网段内网。如果只有一个云一个机房,那就当成是只有一个项目的场景就可以了,我的方案支持1至无限多个机房的场景。

监控的部分,如果你的公司有一个可以访问到所有服务器的监控系统,那就直接使用那些开源软件就行了,没必要非得自己写脚本。如果不能,也可以像我这样自己用几个几十行的脚本去收集。

配置管理自动化的部分,方案任意,用puppet/ansible/saltstack都差不多,我感觉没啥区别,你可以选用自己熟悉的方案。chef我没用过,不了解,所以不发表观点。

6.3 我方运维团队成员视角下的项目上线全过程

这里简单描述下我们新项目上线的全过程,看看你的场景比我们多做了多少工作。

上线前,和研发团队沟通确认服务器资源需求,然后找客户沟通要机器,机器的主机名都规划好了,会写在资源申请的Excel里面,Excel的第一页是资源列表,第二页是权限申请,比如我们需要申请VM1访问VM2的80/TCP端口的权限,用这种方式写好,虽然此时没有IP,但我们可以使用预设的主机来写权限需求,等客户把机器给我们的时候就可以直接配运维机了,配运维机顺利的话需要10-15分钟。

运维机就绪后,自动化功能其实就已经打通了,此时我们把客户给的Excel里面的IP列和密码列登机到运维管理平台的某个页面内,然后会触发一个脚本用它们的组合去尝试登陆所有服务器,总会有一对IP和密码是匹配的,所以这个脚本不需要判断密码是否正确,两层循环去遍历就行了,遍历做什么呢?登陆进去执行初始化脚本,初始化完成后,免密的配置就有了,root密码也被自动化改掉了,用户给的密码只有此时临时用了一次。执行完以后等两分钟,资产系统里就有所有服务器的信息了,这个步骤顺利的话,也不会超过10分钟,服务器初始化也是并发的,不管多少台机器,需要的总时间只取决于速度最慢的那台,而不是所有机器加起来。

监控数据采集到以后,运维人工检查配置是否正确,数据盘是否忘给了,CPU和硬盘性能如何,这些数据在初始化跑完两分钟以后就都能看到,如果有问题就一一反馈并跟进处理,如果什么问题都没有,就可以继续进行后续的各种服务的搭建了。

所有服务、中间件、k8s,数据库,等等等等,我们都有自动化安装脚本,脚本会自动部署到运维机内,所有机器全部就绪后,运维就可以登陆这个项目的运维机执行服务搭建了,由于运维机登陆所有服务器都是免密的,内网yum源也都有了,所以自动安装脚本仅需要考虑如何搭建,完全不用考虑其他基础配置,于是编写服务自动部署脚本的需求也得到了简化。这些服务搭建脚本是安排了其他运维编写的,不是我自己做的,所以本文中就不演示了,只说一下设计原理。

脚本的实现原理都差不多,都是把各种角色的IP以参数的形式传给脚本,然后脚本登陆进去执行安装和最终的配置,而运维只需要提前准备好命令,把IP信息都准备好,例如这样:

/data/scripts/install_k8s.py --master-ips=ip1,ip2,ip3 --node-ips=ip4,ip5,ip6,ip7

但这个命令也不需要手动填,我们不是有自动化吗?每个机器的主机名我们提前准备好就行,IP部分全部写主机名,等初始化跑过一遍以后,我们就可以在运维机的Redis里得到每个主机名对应的IP地址了,然后网页工具进行一下替换,直接输出最终的命令,运维只需要到这个页面里点击一下复制,然后就可以到运维机里执行了,而手动复制和执行这个操作,最终其实都可以改成自动处理,只要我们在那个登记IP列表和密码列表的页面点了提交,后续的所有步骤都可以在后台用代码实现。当然了,这些都是尚未来得及做的功能,等做完了以后,那就是完全无人参与的全自动部署了。

搭建服务需要耗费大家一段时间,但一个小时基本也够了,而且不同的服务可以由不同的运维分别处理,我们有5个人呢,所以可以并列安装5个服务。等所有服务和中间件数据库都搭好了,此时距离我们从客户那拿到资源列表,可能还不到1小时。

然后就可以通知业务方搭建业务服务了(这部分运维不管),而对于运维来说,新项目上线前的工作已经做完了,后续就是业务方部署业务代码及上线前的压测和功能测试了,但这些基本都和运维无关了,运维后续的工作则是配置各种监控和系统优化,但这些工作我们在配置管理功能里已经做好了,所以这个活儿在我们这里也是不存在的,只要我们把机器交付给业务方,我们的任务就已经全部完成了。再往后,我们的任务就是协助其他部门处理各种意外或技术分享了。

所以新项目上线我们的工作内容都有什么?其实也没有什么,我们的工作任务就是处理日常工单和编写更多的自动化工具,而每个工具的部署都意味着我们所有人都永久不用再处理这类需求,工作量会越来越少,所以我们就有大量的时间学习新技术和方案,并推进线上环境的升级改造,这种模式下,所有运维员工的技术能力都得到了提高,企业的线上环境也得到了大幅改善,这是一个双赢的良性循环,后来我们每周还会在周五做一次技术分享,每个人学一个新技术,学一两个月,学会后教给大家,这个分享会也代替了我们的周会,其实我们根本不开什么周会,因为大部分事情所有人都知道,也能在运维平台内看到,不需要开会汇报,我们团队的这种无用的会几乎也没有,此公司我在职总时间为1年半,我主动要求的开会加起来不到5次,但团队的每一件事、每个任务的各种进度及详情等我全部都知道,并没有因为不开会而导致不知道,相反,我啥都知道,而且知道的非常详细,大家也都知道,而不是只有我自己知道,这就是全自动化的好处。

再往后,如果某项目新增几台服务器,运维的工作仅需要去新机器里执行一下初始化脚本,没别的了。如果新机器的软件版本有漏洞什么的,加入自动化后,自动部署到这台机器的监控脚本会在检测到与预期不符后自动创建工单,然后运维就会去处理了。这个过程也是可以全自动的,只是我们还没做,目前需要手动进去执行一下初始化脚本。而机器下线时我们则什么都不用做,直接删就可以了,机器删除后,信息不更新了,(一段时间后)它自动就会从机器列表里消失了,所有的监控、报表等等都不再有它的信息,运维是没有工作量的。此时仅在工单系统里残留了一个机器ping不通的报警,如果是机器临时维护,运维需要在机器恢复后关闭此工单,而机器若下线删除,运维直接删除此报警就行了,什么都不用处理。

五、运维团队管理

团队的管理与实现运维方案的设计表面上没什么关系,但我还想说几句,毕竟无论你的方案设计的如何,最终都需要运维团队的成员去执行,如果你的团队执行力不行,那么再牛B的方案也得不到你想要的结果,所以让团队更团结与更认真负责也是十分重要的,所以我额外加了这章。

很多当领导的人,就像是缺领导瘾似的,当上了领导就颐指气使的,或者一天就知道瞎开会。导致大家都累的要死,心情也不好,工作自然也就糊弄你,5个小时能干完的活儿,非得说需要12个小时,然后今天干一点后摸摸鱼,剩下的留到明天处理,这样今天的工作汇报还能写这个任务的进度完成了70%,但这种糊弄的方式真的好吗?

所以我在管理团队时也是很务实,从来不做那种表面工作,我不止一次跟团队成员说,这个任务你要是3个小时能完成就3个小时干完,我不介意你另外5个小时闲着没事做,同一个任务如果你3个小时能完成,而另外一个人用10小时(加班2小时),那么我不会像很多领导那样觉得那个人工作态度好,反而会觉得他能力不行,此时我觉得优秀的人是你、不是Ta,所以你们也不要用那种方法糊弄我,而且我是做技术岗起来的,不是那种纯管理岗,你想蒙我也蒙不住。

在这种背景下,我的团队成员从来没有瞎糊弄的情况,能尽快完成的也基本都是立刻处理,不会拖着,我也是信守承诺从来不管谁是不是一直在干活,我是以结果论英雄,不完全看过程。这也是我的团队能如此高效的原因之一。

作为团队管理者,你要从员工的角度考虑问题,如果你要求团队成员不能“摸鱼”,必须一直干活,那3个小时能完成的任务Ta肯定不会只用3小时,不然那5个小时怎么解释呢?此时任务Ta肯定是拖到8小时完成,你只有从员工的角度考虑,解决了他们的后顾之忧,你的组员才会和你一条心,一起用最优的方式解决问题。

再有就是利益分配问题,很多领导有好处都自己留着,什么什么任务做的好只强调自己领导的怎么怎么好,从不说员工付出了多少努力,而一出事就问责员工,要么就是把谁谁谁开除用于向上交代,这种情况又有谁会和你同心同德呢?尊重与信任都是相互的,自己若不能为团队遮风挡雨,那就别指望团队成员在风雨来时与你同舟共济,将心比心。

当然了,为团队遮风挡雨也并不是说什么事情都给团队挡着,组员真的犯错时该罚还是得罚,而且要公开罚,这样才能尽显公平,被罚的员工也会心服,如果你一味地无论什么事情都给员工挡着,那员工对犯错也就不会有什么畏惧心理了,反正有你挡着,所以护短也得有个度。

你的团队管理能力好不好,我有一个很好的标准,此标准,不取决于你们做了什么业绩,而取决于若你离职去了新公司,在薪资不变的情况下,你的组员是否愿意跟你一起去新公司,如果组员不愿意,或者没有明确拒绝只说考虑一下(实际就是拒绝),那么你就不是一个好领导。你离职前和员工的工作配合无论如何的顺畅,那也都是职场上的客套,离职后大家基本也就各奔东西了,此时你们曾完成的业绩,如果在一个员工心里的好领导的领导下,完成任务所需的时间至少能缩短一半,因为员工不会糊弄他眼里的好领导,但是会糊弄你,你以前骄傲的一切都是你的组员在迎合你,并非出自真心。

六、尾声

此章无正文。全文完,感谢阅读。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值