我们在前文 一文搞懂 Kubernetes 的负载均衡 中可以看到监听节点变化的是 informer 机制,本文我们来看看informer机制是如何实现的。
informer监听configmap
我们先看看怎么使用 client-go informer
来监听configmap的变化
func ConfigWatcher(ctx context.Context, namespace, configMapName string, onChangeFunc ConfigMapOnChangeFunc) (cancel func()) {
// 获取 api server的客户端
clientSet, err := GetClient()
if err != nil {
panic(err)
}
// 实例化 configmap 的 informer
listWatcher := k8sCache.NewListWatchFromClient(
clientSet.CoreV1().RESTClient(),
"configmaps",
namespace,
fields.Everything(),
)
// 实例化 informer
informer := k8sCache.NewSharedInformer(
listWatcher,
&corev1.ConfigMap{},
0, // No resync
)
// 监听处理方法
_, err = informer.AddEventHandler(k8sCache.ResourceEventHandlerFuncs{
// 更新的方法
UpdateFunc: func(oldObj, newObj interface{}) {
log.Info().Msg("ConfigMap updated")
oldConfigMap, ok := oldObj.(*corev1.ConfigMap)
newConfigMap, ok := newObj.(*corev1.ConfigMap)
// 更新方法外部传入
err = onChangeFunc(namespace, configMapName, oldConfigMap, newConfigMap, updateConfigMap)
},
// 处理删除的方法
DeleteFunc: func(obj interface{}) {
log.Info().Msg("ConfigMap deleted")
// 更新方法由外部传入
err = onChangeFunc(namespace, configMapName, nil, nil, deleteConfigMap)
},
})
// 取消informer的方法返回给外面
stopCh := make(chan struct{})
cancel = func() {
close(stopCh)
}
// 启动 informer
go informer.Run(stopCh)
return cancel
}
这里使用了 shareIndexInformer
来初始化,与 indexInformer
的区别是可以共享 reflector
,通过增加事件处理函数来处理资源变化。
informer的实现流程
整体实现流程
ShareInformer和Informer的区别
shareIndexInformer
对事件先进行缓存、转发和处理,而 informer
则是直接对事件进行处理。**
informer在processLoop中就直接调用变更处理方法对Event进行处理
informer 实例化出 controller 并运行
func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) {
func() {
// 将锁写在函数范围内,函数执行结束立马释放锁,避免了死锁,编码的一个小技巧
s.startedLock.Lock()
defer s.startedLock.Unlock()
// 初始化DeltaFifo
fifo := NewDeltaFIFOWithOptions(DeltaFIFOOptions{})
cfg := &Config{
// 处理变更的方法
Process: s.HandleDeltas,
}
// 初始化 controller
s.controller = New(cfg)
}()
// 处理监听的事件
wg.StartWithChannel(processorStopCh, s.processor.run)
// 启动 controller
s.controller.Run(stopCh)
}
controller 通过 Reflector的Run方法来向api server拉取变更
func (c *controller) Run(stopCh <-chan struct{}) {
// 初始化 Reflector
r := NewReflectorWithOptions(
c.config.ListerWatcher,
c.config.ObjectType,
c.config.Queue,
)
// 将Reflector 赋值给 controller
c.reflectorMutex.Lock()
c.reflector = r
c.reflectorMutex.Unlock()
var wg wait.Group
// 运行reflector run 方法
wg.StartWithChannel(stopCh, r.Run)
// 每一秒循环执行 controller 的 processLoop
wait.Until(c.processLoop, time.Second, stopCh)
wg.Wait()
}
func (r *Reflector) Run(stopCh <-chan struct{}) {
wait.BackoffUntil(func() {
// 循环执行 ListAndWatch方法监听变化
if err := r.ListAndWatch(stopCh); err != nil {
//...
}
}, r.backoffManager, true, stopCh)
}
func (r *Reflector) ListAndWatch(stopCh <-chan struct{}) error {
if r.UseWatchList {
w, err = r.watchList(stopCh)
}
// 不是WatchList则走下面的list,拉取该资源下全量的内容
if fallbackToList {
err = r.list(stopCh)
}
return r.watch(w, stopCh, resyncerrc)
}
list获取对应资源的内容并不是每次都是从最开始获取,而是从relistResourceVersion 的位置开始获取,如果为空则全量拉取
func (r *Reflector) list(stopCh <-chan struct{}) error {
// 设置重新拉取的version
options := metav1.ListOptions{ResourceVersion: r.relistResourceVersion()}
var list runtime.Object
go func() {
// 通过分页进行拉取
pager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) {
return r.listerWatcher.List(opts)
}))
}()
// 获取最新版本的信息
listMetaInterface, err := meta.ListAccessor(list)
if err != nil {
return fmt.Errorf("unable to understand list result %#v: %v", list, err)
}
resourceVersion = listMetaInterface.GetResourceVersion()
// 把数据解析成items
items, err := meta.ExtractListWithAlloc(list)
//写入到 store中
if err := r.syncWith(items, resourceVersion); err != nil {
// ...
}
// 设置重新拉取资源的version
r.setLastSyncResourceVersion(resourceVersion)
return nil
}
// 同步items到 DeltaFIFO中
func (r *Reflector) syncWith(items []runtime.Object, resourceVersion string) error {
found := make([]interface{}, 0, len(items))
for _, item := range items {
found = append(found, item)
}
return r.store.Replace(found, resourceVersion)
}
这里我们先暂时不管 DeltaFIFO的实现,后续会将,这里先专注整体流程,把它当作是一个存储即可
watchList 监听变化
func (r *Reflector) watch(w watch.Interface, stopCh <-chan struct{}, resyncerrc chan error) error {
for {
if w == nil {
options := metav1.ListOptions{
ResourceVersion: r.LastSyncResourceVersion(),
}
// 监听变更,把变更扔到watch.ResultChan中
w, err = r.listerWatcher.Watch(options)
}
// 监听新增的事件Add到deltaFIFO中
err = watchHandler(start, w, r.store, r.expectedType, r.expectedGVK, r.name, r.typeDescription, r.setLastSyncResourceVersion, nil, r.clock, resyncerrc, stopCh)
}
}
通过 ListWatcher 客户端通过 HTTP chunked 方法接收API的数据并进行解码,传入到 ResultChan
中:
type StreamWatcher struct {
result chan Event
}
// NewStreamWatcher creates a StreamWatcher from the given decoder.
func NewStreamWatcher(d Decoder, r Reporter) *StreamWatcher {
sw := &StreamWatcher{
source: d,
result: make(chan Event),
}
go sw.receive()
return sw
}
// 获取Chan中的数据
func (sw *StreamWatcher) ResultChan() <-chan Event {
return sw.result
}
// 接收decode数据并传入到chan中
func (sw *StreamWatcher) receive() {
for {
action, obj, err := sw.source.Decode()
case sw.result <- Event{
Type: action,
Object: obj,
}:
}
}
}
api server通过chunked分块传输,处理客户端请求
func (s *WatchServer) HandleHTTP(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", s.MediaType)
w.Header().Set("Transfer-Encoding", "chunked")
w.WriteHeader(http.StatusOK)
// 要监听的范围
kind := s.Scope.Kind
watchEncoder := newWatchEncoder(req.Context(), kind, s.EmbeddedEncoder, s.Encoder, framer)
ch := s.Watching.ResultChan()
for {
select {
case event, ok := <-ch:
if !ok {
return
}
// api server服务端对事件进行编码
if err := watchEncoder.Encode(event); err != nil {
//...
}
if len(ch) == 0 {
flusher.Flush()
}
}
}
}
通过 http chunked 来实现数据流监听
$ curl -i http://{kube-api-server-ip}:{kube-api-server-port}/api/v1/watch/pods
HTTP/1.1 200 OK
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 02 Jan 2020 20:22:59 GMT
Transfer-Encoding: chunked
{"type":"ADDED", "object":{"kind":"Pod","apiVersion":"v1",...}}
{"type":"ADDED", "object":{"kind":"Pod","apiVersion":"v1",...}}
{"type":"MODIFIED", "object":{"kind":"Pod","apiVersion":"v1",...}}
watchHandler
则将 ResultChan
的变更读取出来之后存储到 DeltasFIFO
中
func watchHandler(start time.Time,
w watch.Interface,
store Store,
// 这里省略其他参数,我们先看watch如何将数据读取后放入到队列中
) error {
loop:
for {
select {
// 读取 Chan里面变更的数据,通过事件类型不同调用不同方法加入到队列中
case event, ok := <-w.ResultChan():
switch event.Type {
case watch.Added:
err := store.Add(event.Object)
case watch.Modified:
err := store.Update(event.Object)
case watch.Deleted:
err := store.Delete(event.Object)
}
}
return nil
}
到这里我们已经知道了客户端informer是如何从api-server获取变更
processLoop 同步变更
我们重新看一下informer的Run方法,里面同时启动了 processor
的方法
func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) {
func() {
cfg := &Config{
Process: s.HandleDeltas,
}
}()
processorStopCh := make(chan struct{})
wg.StartWithChannel(processorStopCh, s.processor.run)
}
func (c *controller) processLoop() {
for {
// 从队列中获取需要消费的数据
obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process))
}
}
// Process的值为HandleDeltas
func (s *sharedIndexInformer) HandleDeltas(obj interface{}, isInInitialList bool) error {
if deltas, ok := obj.(Deltas); ok {
return processDeltas(s, s.indexer, deltas, isInInitialList)
}
}
// 处理变更的数据
func processDeltas(
handler ResourceEventHandler,
clientState Store,
deltas Deltas,
isInInitialList bool,
) error {
for _, d := range deltas {
switch d.Type {
case Sync, Replaced, Added, Updated:
if old, exists, err := clientState.Get(obj); err == nil && exists {
handler.OnUpdate(old, obj)
} else {
handler.OnAdd(obj, isInInitialList)
}
case Deleted:
handler.OnDelete(obj)
}
}
return nil
}
// 这里是将变更的数据通过distribute分发到 addCh中
func (s *sharedIndexInformer) OnAdd(obj interface{}, isInInitialList bool) {
s.processor.distribute(addNotification{newObj: obj, isInInitialList: isInInitialList}, false)
}
distribute
将数据加入到 addCh
中
// distribute 把事件传递给所有listeners
func (p *sharedProcessor) distribute(obj interface{}, sync bool) {
for listener, isSyncing := range p.listeners {
switch {
case isSyncing:
// 同步到每一个监听器
listener.add(obj)
}
}
}
func (p *processorListener) add(notification interface{}) {
p.addCh <- notification
}
启动协程对数据进行获取和消费
func (p *sharedProcessor) run(stopCh <-chan struct{}) {
func() {
for listener := range p.listeners {
// 获取到变更的数据并进行更新
p.wg.Start(listener.run)
p.wg.Start(listener.pop)
}
p.listenersStarted = true
}()
<-stopCh
}
pop将 addCh
里的数据写入到 nextCh
中
func (p *processorListener) pop() {
var nextCh chan<- interface{}
var notification interface{}
for {
select {
case nextCh <- notification:
var ok bool
// 获取待消费的消息
notification, ok = p.pendingNotifications.ReadOne()
case notificationToAdd, ok := <-p.addCh:
if notification == nil {
// 没有等待的消息,则直接写入到nextCh中
notification = notificationToAdd
nextCh = p.nextCh
} else {
// 写入待消费的消息
p.pendingNotifications.WriteOne(notificationToAdd)
}
}
}
}
run方法消费 nextCh
里的数据
func (p *processorListener) run() {
wait.Until(func() {
for next := range p.nextCh {
// 调用更新的方法处理新旧对象的变化
switch notification := next.(type) {
case updateNotification:
p.handler.OnUpdate(notification.oldObj, notification.newObj)
case addNotification:
p.handler.OnAdd(notification.newObj, notification.isInInitialList)
case deleteNotification:
p.handler.OnDelete(notification.oldObj)
}
}
close(stopCh)
}, 1*time.Second, stopCh)
}
DeltaFIFO
从上面的代码可以看到使用了 store
的 Add
、 Replace
、 Update
、 Delete
的方法来存储变更, Pop
方法来获取Event进行处理 ,所以我们来分析这几个方法是怎么存储这些数据的。
首先要找到store的具体实现,store在最外层传入的是DeltaFIFO
func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) {
func() {
fifo := NewDeltaFIFOWithOptions(DeltaFIFOOptions{
KnownObjects: s.indexer, // 这里的实现是cache.cache,后续我们会进行分析
EmitDeltaTypeReplaced: true,
Transformer: s.transform,
})
cfg := &Config{
// 构建Reflector的时候会传入 Queue 来作为 Store
Queue: fifo,
}
}()
}
所以接下来我们分析 DeltaFIFO
的具体实现,我们先通过一张图来了解整体的流程
queueActionLocked
给队列上锁后进行的动作,这里的 actionType
包含了如下:
type DeltaType string
const(
Added DeltaType = "Added"
Updated DeltaType = "Updated"
Deleted DeltaType = "Deleted"
Replaced DeltaType = "Replaced"
Sync DeltaType = "Sync"
)
实际的处理函数则会将变化加入到队列中,然后进行去重的动作,最后唤醒消费者:
func (f *DeltaFIFO) queueActionLocked(actionType DeltaType, obj interface{}) error {
// 获取items的key
id, err := f.KeyOf(obj)
// 将新的deltas 加入到deltas中
oldDeltas := f.items[id]
newDeltas := append(oldDeltas, Delta{actionType, obj})
// Deltas进行一个去重的操作,尾部如果都是delete的对象,则认为是重复的
newDeltas = dedupDeltas(newDeltas)
if len(newDeltas) > 0 {
// 如果原先的key不存在,则加入到key的队列中
if _, exists := f.items[id]; !exists {
f.queue = append(f.queue, id)
}
// 将对象的deltas更新成最新的值
f.items[id] = newDeltas
// 如果有在等待Pop的协程则唤醒
f.cond.Broadcast()
}else{
// 这个分支不会出现,出现了的话做日志记录
}
return nil
}
Add
上面我们了解了 queueActionLocked
后,Add 、Update和Delete的操作都比较简单,我们这里直接看一个Add的实现,Update和Delete不做赘述。
func (f *DeltaFIFO) Add(obj interface{}) error {
f.lock.Lock()
defer f.lock.Unlock()
f.populated = true
return f.queueActionLocked(Added, obj)
}
Replace
该方法主要的作用是将传入的list加入到detlasFIFO中,并且将不存在List中的key全部删除
func (f *DeltaFIFO) Replace(list []interface{}, _ string) error {
keys := make(sets.String, len(list))
// 这里的action为Replaced
action := Sync
if f.emitDeltaTypeReplaced {
action = Replaced
}
// 将对象加入到items中
for _, item := range list {
// 用set保存这次加入对象的Key值,用于后面替换
key, err := f.KeyOf(item)
if err != nil {
return KeyError{item, err}
}
keys.Insert(key)
if err := f.queueActionLocked(action, item); err != nil {
//...
}
}
// 删除不是本次添加的Deltas
queuedDeletions := 0
for k, oldItem := range f.items {
// 通过key判断对象是否存在
if keys.Has(k) {
continue
}
// 获取要删除的对象并,给队列添加一个 deleted动作,相当于把对象删除了
var deletedObj interface{}
if n := oldItem.Newest(); n != nil {}
if err := f.queueActionLocked(Deleted, DeletedFinalStateUnknown{k, deletedObj}); err != nil {
return err
}
}
return nil
}
Pop
消费的时候会通过 Pop
方法来获取变更,这里传入的 PopProcessFunc
就是我们最开始看到的 HandleDeltas
func (f *DeltaFIFO) Pop(process PopProcessFunc) (interface{}, error) {
for {
// 如果队列是空则进行等待
for len(f.queue) == 0 {
f.cond.Wait()
}
// 获取队头的key,并将队头删除
id := f.queue[0]
f.queue = f.queue[1:]
// 获取deltas给到处理函数,isInInitialList是第一次Replace插入的对象个数
item, ok := f.items[id]
err := process(item, isInInitialList)
// 将detlas返回
return item, err
}
}
Indexer
cache.cache 是 indexed 的实现,在 DeltaFIFO 中用来查找已知的对象。同个对象的deltas动作在不同的 clientState 要执行的动作不同,比如Deltas虽然是 Add的类型,但是实际上对象已经在cache里面存在,则需要执行的是Update动作而不是 Add 的动作。
func processDeltas(
handler ResourceEventHandler,
clientState Store,
deltas Deltas,
//...
) error {
for _, d := range deltas {
switch d.Type {
case Sync, Replaced, Added, Updated:
**// 这里会根据对象是否存在来看新增还是更新
if old, exists, err := clientState.Get(obj); err == nil && exists {
if err := clientState.Update(obj); err != nil {
return err
}
handler.OnUpdate(old, obj)
} else {
if err := clientState.Add(obj); err != nil {
return err
}
handler.OnAdd(obj, isInInitialList)
}**
}
}
至此,恭喜你已经学习了Informer整体的处理流程,我们通过一张UML类图来回顾一下关键的对象
- 蓝色框中的内容为controller的主要对象,统筹了整个informer的处理流程
- 绿色内容为监听变化处理的对象
- 橙色内容为与API server交互的对象抽象
Go编程小技巧
wait工具包
v1.29 代码路经
staging/src/k8s.io/apimachinery/pkg/util/wait
k8s抽象了自己的wait group:
type Group struct {
wg sync.WaitGroup
}
func (g *Group) Wait() {
g.wg.Wait()
}
// 这样封装后就不需要自己提前先Add,每次执行都会先做Add的动作
func (g *Group) Start(f func()) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
f()
}()
}
// 增加了stopCh的参数,这里也可以直接使用闭包实现
func (g *Group) StartWithChannel(stopCh <-chan struct{}, f func(stopCh <-chan struct{})) {
g.Start(func() {
f(stopCh)
})
}
封装了周期性执行方法,在一定周期内会一直运行,直到 stopCh
收到信号后才停止
// 按period的时间周期执行f()
func Until(f func(), period time.Duration, stopCh <-chan struct{}) {
JitterUntil(f, period, 0.0, true, stopCh)
}
func JitterUntil(f func(), period time.Duration, jitterFactor float64, sliding bool, stopCh <-chan struct{}) {
BackoffUntil(f, NewJitteredBackoffManager(period, jitterFactor, &clock.RealClock{}), sliding, stopCh)
}
// 按一定时间周期去回退
func BackoffUntil(f func(), backoff BackoffManager, sliding bool, stopCh <-chan struct{}) {
var t clock.Timer
for {
// 如果没有退出信号则一直处于循环中
select {
case <-stopCh:
return
default:
}
// 不滑动则period包含了f()运行的时间
if !sliding {
t = backoff.Backoff()
}
func() {
defer runtime.HandleCrash()
f()
}()
// 如果是滑动则在运行后才计算回退的时间,即等待时间周期 == period
if sliding {
t = backoff.Backoff()
}
// 这里有可能会发生竞争,为了缓解该问题,在重新开始执行的时候再次检查了stopCh
select {
case <-stopCh:
if !t.Stop() {
<-t.C()
}
return
case <-t.C():
}
}
}
sync.Cond的使用
当资源未准备好的时候,可以通过 sync.Cond.Wait
进入等待,等待资源准备好了之后再通知协程启动。
我们来看一下下面模拟读写文件的例子
package concurrency
import (
"fmt"
"sync"
"time"
)
var done bool
func read(c *sync.Cond) {
c.L.Lock()
// 写入未完成之前先陷入等待
for !done {
fmt.Println("read func wait")
// 内部有unlock,等待被通知后重新唤起,所以临界资源仍然是安全的
c.Wait()
}
fmt.Println("read : ", done)
c.L.Unlock()
}
func write(c *sync.Cond) {
c.L.Lock()
time.Sleep(time.Second)
fmt.Println("write func signal")
// 写入完成标记
done = true
c.L.Unlock()
// 唤醒等待的资源
c.Broadcast()
}
通过测试来进行验证
func Test_write(t *testing.T) {
cond := sync.NewCond(&sync.Mutex{})
// 启动协程去读取,还未写入则不能读取成功
go read(cond)
go read(cond)
time.Sleep(time.Second * 1)
// 写入数据,完成后就能够读取成功
write(cond)
time.Sleep(time.Second * 3)
}
快速回顾
informer的机制总结起来其实就是通过version获取变更,然后将变更存储到队列中(DeltaFIFO)。
然后informer注册自己需要的处理方法(ResourceEventHandler),然后在循环中(processLoop)对变更进行相应的处理。
这个模式就是MQ的Producer和Consumer,最后再用一张比较简单的图来总结整个informer的流程
思考
1.为什么需要有DeltaFIFO,不能直接Watch吗?
要知道为什么需要有,可以先看看DeltaFIFO提供的功能。
它支持将变更的时间进行合并去重,避免了客户端收到重复的时间,减少了整体需要处理的事件,而直接 Watch 则需要处理所有事件。
2.Informer是如何保证事件不丢失的呢?
- Watch 机制: 监听实时产生的变更,然后将变更加入到队列中,但是这时如果应用重启则会导致在内存中的数据丢失,就需要定期重新扫描最新状态来同步以及重新启动时 list 拉取所有已有的变更来补齐状态
- Resync Period: Informer 支持设置一个定期重新同步(Resync)的时间间隔。在这个时间间隔内,Informer会主动向 API Server 查询资源对象的最新状态,以确保不会错过任何事件。虽然这并不能保证实时性,但是可以在一定程度上减小事件丢失的可能性。
写在最后
感谢你读到这里,如果想要看更多 Kubernetes 的文章可以订阅我的专栏: juejin.cn/column/7321… 。