TT Lab
开始
学习 学习路径 课程

CRD 与 Operator

事件不是日志

在 TT Lab 中继续学习

一句话总结

事件是 Operator 向用户说话的唯一位置,但它保留时间短,而且相同的原因会被合并为一条,是一种汇总机制。要顺着这个性质来使用,它才有价值。

为什么需要这个位置

开发 Operator 时会遇到这样的情况:控制器知道“镜像标签不存在,所以中止了调谐”。但这个事实只存在于控制器 Pod 的日志中。创建自定义资源的开发者,既没有查看那份日志的权限,也不知道有这样的东西。在他的界面上,只有一个什么都没有发生的资源。

你可能会想,写 status 不就行了吗?但 status 是当前状态。它能写下“现在不是 Ready”,却容纳不了“10 分钟前失败过一次,后来恢复了”。要保留过程,就需要另一个带有时间轴的位置,那就是 Event。

工作原理

事件是 API 对象,和其他对象一样存储在命名空间中。不过,它的 API 有两个。

v1 (core) events.k8s.io/v1
对象 involvedObject regarding
正文 message note
报告者 source.component reportingController、reportingInstance
时间 firstTimestamp、lastTimestamp eventTime
重复 count series.count

只是名称不同,存储是同一个。用新 API 创建的事件,用旧 API 查询时也能原样显示,只是旧 schema 中没有对应项的栏位看起来是空的。出现新 API 的原因,是为了减轻大规模集群中事件写入给 API 服务器造成的负担而整理了 schema,旧 API 则为了兼容而保留。

reason 是这个对象中最重要的字段。因为它不是给人读的句子,而是机器用来统计的键。所以惯例是用简短的 CamelCase 单词(FailedScheduling、Pulled、BackOff),如果在这里放进长句,同一个事件每次都会以不同的原因被记录,统计就会崩溃。详细内容放在 message 一侧。

相同原因的重复不是新事件。事件记录器再次遇到相同的对象、相同的原因、相同的消息时,不会再多创建一个对象,而是增加 count,并更新 lastTimestamp。在列表界面上看到的 (x12 over 3m) 就是这个结果。即使调谐循环每秒运行好几次,etcd 也不会被事件撑满,原因就在这种汇总。

type 不会被 API 检查。即使写成 Critical,对象也会被创建。但读取事件的工具都假定只有 Normal 和 Warning 两种——kubectl events --types= 根本不接受这两者之外的值。这是一个典型的例子:并没有强制,却仍应遵守惯例。

而且事件会消失。kube-apiserver 的 --event-ttl 默认值是 1 小时。如果一小时之后才开始调查,那个事件就已经没有了。

在现场相遇的样子

第一,用户界面上没有原因的 Operator。控制器日志里有明确的错误,CR 的 describe 里却什么都没有。用户提交咨询,运维人员把日志截取下来贴给他。本来三行事件就能解决的事,却要靠人来回沟通来处理。哪怕只记录调谐的开始、成功和失败,咨询也会大大减少。

第二,把事件当作审计记录引发的事故。设计时相信“谁在何时改了什么,看事件就行”,真正进入事故调查时,却因为过了一小时而什么都没有。需要长期保存的内容,应该放在 status 的条件中,或者发送到外部存储的记录里。事件是用来查看现在正在发生什么的窗口。

第三,调度器的那句话就是诊断书。Pod 起不来时,人们真正会读的是 0/3 nodes are available: 3 Insufficient cpu 这样的一行。它也是好的事件消息的范本——里面有数字,有缺少什么的名称,还能让人猜到下一步该做什么。Operator 的事件也值得以这个水平为目标。

本实验环境的局限

kwok 集群中的 Pod 是假的,所以看不到 kubelet 产生的事件(Pulling、Started、BackOff)。不过调度器是真实运行的,所以 FailedScheduling 会真正出现。另外,针对没有控制器的自定义资源的事件,要在本实验中由人亲手创建——相当于亲手做一遍 Operator 该做的工作。

下一项实验要做什么

针对自定义资源,分别用两个 API 创建事件,确认缺少对象的事件会怎样被拒绝,以及超出惯例的 type 在工具中如何消失。亲手增加汇总字段,看列表界面中的显示如何变化,再收集调度器产生的真实事件,最后制作一张只读取事件就能重建事故的表格。