龙空技术网

一文详解 | K8S 上多租户、高性能的分布式告警系统实践

青云QingCloud 79

前言:

现时兄弟们对“k8s多租户管理”可能比较注意,姐妹们都想要分析一些“k8s多租户管理”的相关文章。那么小编也在网摘上网罗了一些有关“k8s多租户管理””的相关内容,希望咱们能喜欢,咱们一起来学习一下吧!

技术内容提供者青云QingCloud 软件工程师马丹

Kubernetes 已经成为事实上的编排平台的领导者,下一代分布式架构的王者,其在自动化部署、扩展性、以及管理容器化的应用中已经体现出独特的优势,在企业中应用落地已经成为一种共识。

首先,KubeSphere® 作为 K8S 的发行版,其告警有什么需求?

第二,在需求下,告警怎么实现?它跟 K8S 基于 Prometheus 的告警有怎样的区别?

第三,在我们的实现下,我们的架构是怎样的、性能如何以及如何实现 KubeSphere® 多租户需求的实现。

最后进行总结。

KubeSphere® 告警需求

KubeSphere® 作为 Kubernetes 的发行版,我们知道它支持多租户,可以通过图形界面配置和观察告警状态我们知道它有图形界面的。告警同样有界面配置告警、管理告警、查看告警状态的需求。我们第一版做的告警主要是基于监控指标的告警,就像刚刚看到监控系统得到监控值,我们对它进行判断,大于某个阈值或者小于某个阈值,判断周期连续超过多少次就会产生告警。

在监控过程中,它跟其他的不太一样。我们监控对象可能是动态变化的,比如 Node 的个数可能会在运行过程中增减,假如监控一个 Pod 或者一个 Container 的话,其对象也会在运行过程中动态变化。我们告警指定要有灵活的方式,不但可以通过指定某几个要监控对象的具体名称,也可以通过像 Label Selector 批量或者按属性指定的方式指定它。

同样的,每个对象有不同的监控指标,这是依据其资源类型来的。不同的监控指标、扫描周期,比如几分钟看它的值,判断规则大于、等于、小于等,达到规则的连续次数等,这些都可以自定义。

在告警产生后,如果不加管控,假如一分钟扫描一次,最原始的告警做法是扫描超了就发生告警,通知的用户的邮箱、短信会泛滥。我们对通知发送者,假如一个用户的告警只希望在上班时间段发送,下班时间段他可以等到上班时一并发送。发送方式,不同等级的告警是怎么重复的?比如几分钟重复一次,连续发多少次就不发了,这些都可以自定义。

我们这套告警系统本质上是往通用告警系统的方向做,它的 API 并不是跟 KubeSphere® 的多租户相融的。作为 KubeSphere® 要满足多租户的管理要求,我们需要对它进行封装,如何封装它原来的 API。就像我们刚刚封装 Prometheus 的监控系统一样,把它封装成一套符合多租户管理要求的 API。这是我们告警系统的需求。

Prometheus 本身也是带有告警的,我们在这当中为什么没有用它,我可以稍微介绍一下。

Prometheus Alert Manager

Prometheus 的执行规则、告警规则并不是由 Alertmanager 执行的,而是由 Prometheus 执行的。它也是将告警规则一条条写成 Yaml 的文件,更新为 ConfigMap,在 Prometheus 里执行。它一样会指定监控指标、阈值和扫描周期等参数。

当告警发生后,Alertmanager 起到的作用是 Prometheus Server 有告警发生,它会把告警消息推送给 Alertmanager,Alertmanager 更多地类似对通知进行管理的角色,它会将正式告警的通知发送到很多地方。

这是典型的 Prometheus 原版的 Alertmanager 配置方式。左边几乎看不清楚的是其配置文件,我们会在 Yaml 里写上每一条告警的规则,右边是管理界面,他也可以看到告警的规则和状态。

无论是左边的编辑方式还是右边的界面,它首先不是多租户的,原版有一些局限。它的局限主要是 ConfigMap 加载,一是 ConfigMap 容量有限,作为多租户系统有很大的集群,会有成千上万条告警。它的容量有限,带来告警使用受到限制。比较难支持 Label Selector 这种方式,指定监控对象。当它实时变化时要动态地更改其规则。在多租户环境下会有很多用户同时在线,他们会频繁登录改操作。ConfigMap 通过映射到文件,再重新通过获取文件的变化来加载的,它响应肯定不如我们直接用一套自己的机制执行来得及时。

我们这套告警系统当时开发的目的不光是给 KubeSphere®,KubeSphere® 是第一个告警系统使用的场合,我们还会给后续的产品使用我们的告警。在通用告警系统上,像 Prometheus 这一套难以实现它。所以我们架构实现了一套告警系统,就是中间最大的 Alert Service 这一部分。

KubeSphere® 告警系统架构

从刚才的需求可以看出我们的告警主要包括几部分:

首先,我们的 Alertmanager 实现的是跟用户进行交互,它提供了一套 CRUD 的接口,接受用户过来的增、删、改、查等具体告警需求。最关键的部分是 Manager、Executor、Watch,这三个角色,Manager 负责跟用户交互,Executor 负责具体告警用户发过来新的告警,其使用和执行在这当中。Watch 是对告警集群的状态进行健康检查。

我们这个地方的告警并不是单体应用,它要考虑到横向扩展和分布式可扩展性,它对可靠性有要求。如果 Executor 放在 K8S 里执行,一个 Executor 就是一个 Pod,它的个数是可以动态跟踪的。像告警需求比较大的集群,我们 Executor 可以细化到时刻或者更多,比较少的可能一两个就够了。动态跟踪过程中如何有效地对 Executor 执行告警状态有一个监控,当 Executor 失效后进行灾难恢复、告警状态、任务迁移,就是由 Watch 实现的。

这套系统要实现这些就需要外部组件的配合,首先有 etcd,我们使用 etcd 来进行健康状态的注册、服务与发现。所有的 Executor 在运行时会定期向 etcd 发送自己的健康状态,当 Executor 被停止或者以别的方式并丢掉,或者它运行的物理宕机了,etcd 里不会被长久保持,会被 Watch 某个掉线,它会实时将 Executor 的任务分配到其他的 Executor 上。

在告警创建过程中,所有的告警状态都是维护在 MySQL 数据库中,这个数据库要求可以动态扩展的。

我们这边的告警状态是以 MySQL 数据库中存储的为准,当一个用户创建一条新的告警时,Alertmanager 接到这条告警需求会先在数据库里创建一条告警,将用户的规则信息、额外的信息写到数据库里,并且将这条告警置于 Waiting 状态,这时候并没有被实际执行到,这时候 Alertmanager 会在 Redis 里 Push 一个任务,Redis 通过消费队列的分配,这时候所有的 Executor 在消费队列,它会被动态分配到所有 Watch 消息队列、Redis 消息队列的 Executor 上。Executor 引用告警并执行后,会向 MySQL 将告警的状态转成 Running。

我们在运行过程中会记载它的状态,达到阈值多少次?是否发送告警?当它一直发送的时候,告警要合并多少条等临时信息,都是由 Executor 提交到 MySQL 里。

同样的,Watch 一方面通过 etcd 的 Watch 来监控 Executor 状态。另一方面,它要对 MySQL 里的告警状态做定期定时检查。好比有一些情况,每一个告警我们开了一个 Go 协程,它可能有很多个协程,可能整个进程并没有死,但是某几个协程因为什么原因导致并没有及时更新告警状态,更新时间不足的告警同样会被我们 Watch 检查,让它得到及时的执行。

无论是 Executor 挂掉了或者某些告警挂掉了,Watch 都会将这条告警在 MySQL 里置为 Waiting 的状态,并且重新将告警 ID Push 到 Redis 队列中,由其他健康的 Executor 继续迁移任务并且执行。

Executor 也要对自我运行的状态进行健康检查。假如 Executor 中间出现了失联等现场,它的任务已经被取消了,它并不知道,但它有办法检查到。虽然 Check 是定期检查,但这是批量的,不会对数据库造成太大的压力。去检查哪些是它认为在执行,实际上在别的数据库里也执行的。它应该把不该自己执行的给 Kill 掉,实现告警完整的运行。

我们为了通用性,Alertmanager 暴露的是一套通用的 gRPC 接口,这是一个内部接口。我们为了让它能够跟外面的界面实现多租户,我们有 KubeSphere® 或者其他系统将内部接口转换成其他系统的接口。比如在我们 KubeSphere® 中,后面我们会有具体的 API 的图,讲到它的接口转换前后的样子。

Adapter 起的作用是在 KubeSphere® 当中,我们的告警是基于 Prometheus 封装的,刚刚 Ben 讲的监控系统。它每次告警的读取都是通过监控系统来的,Adapter 就是为了适应不同的监控系统,这样的话我们 Executor 的代码就不用变,它跟 Adapter 之间是同一个接口,再由 Adapter 将它的接口翻译成其他不同的读取监控指标的接口。同样的,Notification Service,当告警发生后我要发送一条具体的告警,我从这个发出。这就是我们告警系统的架构。

KubeSphere® 告警系统特点

这样一套告警系统的架构主要实现的指标,首先它要有通用的、指标读取、通知发送、API 等组件都是通过内部标准 API 交互,要可转换,要针对不同的业务系统定制插件就可以完成适配。

要有高可用和动态伸缩,它的业务分配策略是刚才我们看到的,我们通过 MySQL 数据库中进行一致性,再通过 Redis 队列进行任务的分配。集群健康检查是通过 Watch 来进行的,有一个问题,目前在 K8S 中执行,我们由 K8S 保证集群中的 Watch 可以用。在其他的地方,我们也要保证在这个集群有多个 Executor,并且被一个 Watch 管理,管理者本身要有一个健康的管理。

微服务架构,我们一个服务过来,API 完成后立刻将任务置于某个状态,再由下面具体的执行者执行,这是一套微服务架构。基础 API 是针对告警的某一条规则或者告警资源来的,我们会封装出适合界面使用的,一次调用就可以完成大部分功能的 API,实现 API 的可扩展性。

KubeSphere® 告警系统指标

我们实现这套告警系统后对它进行了实际的测试,我们测试环境是两台 8 核 16G K8S 集群,Executor Pod 设置为两个,我们用专门的测试程序写的测试。在 5 分钟内创建 1 万条告警,每条告警有一条规则,要从后面的数据库观察,在 5 分钟内进入 adding 或者 Running状态。10 分钟内不要有 adding,所有都要被 Executor 引用掉、执行掉,进入运行状态。在运行后或者创建过程中,通过我的测试程序在 5 分钟内删除 1 万条告警,看能否都准确删除掉,不要在数据库里有所反应,也不在 Executor 的报告中有所反应。

检查,1 万条告警运行时大概占用了多少硬件资源,再检查 1 万条告警里面每个都有一条规则,一分钟执行一次,它真的是一分钟就发送一次读取监控指标的请求吗?这都是要检查的。

然后是对高可用的检查,现在我们有两个 Executor,我手动通过程序把它 Scale Down 到一个,另一个 Pod 很快就会被一次性踢掉。Watch 是不是真的把属于 Pod 的任务执行到 Running 状态,adding 的状态是否真的通过 Redis 到了另一个 Pod 迁移,Scale Up10 个 Pod。目前的 Scale Up 是这样的,它已经在运行的,不会再运行了。如果创建新的话,现在有 10 个 Pod、10 个 Executor 在获取 Redis,创建的时候同样有机会可以分到。

目前所谓的分配方法是自然分配,没有进行特别的判断。大家谁的手速快,抢的队列及时,基本上反映了硬件状态比较空闲。

后面是我们对实际运行时的指标进行的检查,我们主要检查了两个 Pod CPU 和内存的占用总和。差不多在 2.5 Core 和 1.5G 的样子,这样的话折合起来一条告警规则占用的内存大概 150K 左右,这是一条规则。规则的增加,它会有所增加,但基本是可控的,基本满足我们第一步的设计指标。

这是检查它是否真的 1 万条一分钟的告警,每一分钟都精确地执行,我们特意打了一条 Log 信息,这条信息在实际使用执行过程中应该删除,它会极大的消耗系统的资源。左边上升的过程是创建的过程,中间会一段时间不太稳定,它可能这一分钟少几个,下一分钟补上几个。最终稳定后,它可以在日志中看到一分钟就是 1 万条 Monitor 被发送。

告警系统 API

刚才我们看到告警 API 分为基础 API 和扩展的多租户 API,这个 API 是 gRPC 接口的,为了方便它的观察。可见它是不带权限管理、不带资源的。

如何实现它的多租户?这条 API 肯定不够的,这是一个通用的东西,放在其他的系统没有租户,没有权限也一样能用。

我们的 KubeSphere® 多租户权限管理是一个三级的 RBAC,基于角色的多租户管理。我们的用户都有一个 Rules,不同的 Rules 对不同的 Resource,有不同的网络权限。只要 API 的路径,比如是 Cluster 级别还是 WorkSpace 级别,它相当于是 K8S 的 NameSpace。它的 API 有一个这样的级别限制在里面,在访问 API 时,只要我们是符合路径要求的 API。我们的 API 先进行 KubeSphere® 的 API Gateway,再转发到我们的 API 上。

API Gateway 是暴露了外部的服务,而我们是内部服务。这样实现了从一个通用的 API 包装成一个专用的,支持多租户权限管理的 API。这是经过包装后的 API,可见左边开始带上了一些路径,所有的界面或者其他的在调用过程中都先经过 KubeSphere® API Gateway 的权限检查,假如 Log in 的用户并不具备权限的话,它直接会被拒绝掉,这样就实现我们的定制 API,并且有权限的管理。

同时,定制 API 有时候界面上他不希望创建告警时要很零碎的操作,先在这个表面操作几下,后在那个表面操作几下。我们在这里可以把流程一次性封装完,它只要调一条非常方便的 API,传输一个大的数据结构,这边直接创建好这条告警。

告警系统是我们目前实现的第一版,它还有很多可以改进的地方。就功能来说,Prometheus 本身带着告警功能也应该为我们所接受和使用的。我们也在考虑有很多第三方的告警消息也可以直接产生告警,它一起到我这边发送的地方做发送方式的汇总。我们目前看到的是基于监控指标、监控值的告警,以后我们会做基于日志的告警。某种日志,比如错误日志、访问日志过大时,我们也会对它进行规则的检查,并且产生一些告警消息。

架构方面,可以看到这套告警由高可用和这样一套架构,目前只是执行告警,实际上这里面执行的东西是不是告警,关系不是很大。我们希望进一步介绍通用的用户调度框架剥离出来,告警只作为其一个具体任务执行。这个告警规则目前的判断都是我们写在代码里,后来进行判断的。如果可以引入规则引擎的话,它的功能会更强,类似语言那样的。

我们下一步的改进是将告警中的执行和具体的业务分开,它的调度框架具体执行什么,是不是在 K8S 上,关系都不大,都可以实现通用调度的能力,可以用在很多系统上。告警规则本身如果结合给它,它就是一个告警。假如有别的规则,可以形成计费系统这样的东西。告警规则,后面我们会基于规则引擎进行改造。

我们的告警是开源项目,在 KubeSphere® 这个大项目下,欢迎大家的关注。

标签: #k8s多租户管理 #k8s 多租户