事件不是日志
一句话总结
事件是 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 在工具中如何消失。亲手增加汇总字段,看列表界面中的显示如何变化,再收集调度器产生的真实事件,最后制作一张只读取事件就能重建事故的表格。