kubelet监控静态Pod

获取静态Pod路径

静态Pod路径默认是空。

当静态Pod路径是空时,路径会被设置成/etc/kubernetes/manifests。

检测周期

/var/lib/kubelet/config.yaml配置了FileCheckFrequency值是20s即List的间隔周期是20s。

List和Watch流程

doWatch函数的重试回退流程

主流程

pkg/kubelet/config/file_linux.go
startWatch函数

如果watch失败,那么看错误是否支持重试。
1.    支持重试,继续watch。
2.    不支持重试,进入回退流程。
创建Backoff对象后,每隔1s调用doWatch函数(如果处于回退流程中,那么需要等待,不会调用doWatch函数)。

const (
   // 回退最小时间 
   retryPeriod    = 1 * time.Second 
   // 回退最大时间
   maxRetryPeriod = 20 * time.Second 
)

func (s *sourceFile) startWatch() {
   // 创建Backoff对象 
   backOff := flowcontrol.NewBackOff(retryPeriod, maxRetryPeriod)
   backOffId := "watch"

   go wait.Forever(func() {
      if backOff.IsInBackOffSinceUpdate(backOffId, time.Now()) {
         return
      }

      if err := s.doWatch(); err != nil {
         klog.Errorf("Unable to read config path %q: %v", s.path, err)
         if _, retryable := err.(*retryableError); !retryable {
            backOff.Next(backOffId, time.Now())
         }
      }
   }, retryPeriod)
}

创建Backoff对象

staging/src/k8s.io/client-go/util/flowcontrol/backoff.go
NewBackOff函数

type Duration int64

type Backoff struct {
   sync.Mutex
   Clock           clock.Clock
   defaultDuration time.Duration
   maxDuration     time.Duration
   perItemBackoff  map[string]*backoffEntry
}

type backoffEntry struct {
   // 回退间隔时间 
   backoff    time.Duration
   // 回退开始时间
   lastUpdate time.Time 
}

type RealClock struct{}

// 参数值分别是1s和20s
func NewBackOff(initial, max time.Duration) *Backoff {
   return &Backoff{
      perItemBackoff:  map[string]*backoffEntry{},
      Clock:           clock.RealClock{},
      defaultDuration: initial,
      maxDuration:     max,
   }
}

判断是否在回退流程中

staging/src/k8s.io/client-go/util/flowcontrol/backoff.go
IsInBackOffSinceUpdate函数

如果函数返回false,那么说明还在回退流程中;否则,说明回退流程已经结束。

1.    加锁。
2.    获取map里面key是“watch”对应的value,即backoffEntry。
3.    如果没有该key,那么返回false。
4.    如果当前时间与回退开始时间差值>2倍maxDuration,那么返回false。(此步多余,第5步已经覆盖了,2倍maxDuration>backoff)
5.    如果当前时间与回退开始时间差值<回退间隔时间,那么返回false;否则,返回true。
6.    释放锁。

func (p *Backoff) IsInBackOffSinceUpdate(id string, eventTime time.Time) bool {
   p.Lock()
   defer p.Unlock()
   entry, ok := p.perItemBackoff[id]
   if !ok {
      return false
   }
   if hasExpired(eventTime, entry.lastUpdate, p.maxDuration) {
      return false
   }
   return eventTime.Sub(entry.lastUpdate) < entry.backoff
}

插入或更新回退间隔时间、更新回退开始时间

staging/src/k8s.io/client-go/util/flowcontrol/backoff.go
Next函数

1.    加锁。
2.    获取map里面key是“watch”对应的value,即backoffEntry。
3.    如果没有该key或者当前时间与回退开始时间差值>2倍maxDuration,那么初始化(插入key-“watch”对应的backoffEntry);否则,backoffEntry的回退间隔时间backoff翻倍,最大值是maxDuration即20s。
4.    更新backoffEntry里面的lastUpdate即回退开始时间。
5.    释放锁。

// 当前时间与回退开始时间差值最大是2倍maxDuration。
func hasExpired(eventTime time.Time, lastUpdate time.Time, maxDuration time.Duration) bool {
   return eventTime.Sub(lastUpdate) > maxDuration*2
}

func (p *Backoff) initEntryUnsafe(id string) *backoffEntry {
   // 起始回退间隔时间是1s。 
   entry := &backoffEntry{backoff: p.defaultDuration}
   p.perItemBackoff[id] = entry
   return entry
}

func (p *Backoff) Next(id string, eventTime time.Time) {
   p.Lock()
   defer p.Unlock()
   entry, ok := p.perItemBackoff[id]
   // 只有第一次回退或者当前时间与回退开始时间差值超过2倍maxDuration即40s,才会把回退间隔时间重置成1s。
   if !ok || hasExpired(eventTime, entry.lastUpdate, p.maxDuration) {
      entry = p.initEntryUnsafe(id)
   } else {
      delay := entry.backoff * 2 // exponential
      // 确保回退间隔时间最大是20s。
      entry.backoff = time.Duration(integer.Int64Min(int64(delay), int64(p.maxDuration)))
   }
   entry.lastUpdate = p.Clock.Now()
}

回退间隔时间的变化(不是单调递增,而是先增后保持不变)

1s

2s

4s

8s

16s

20s

...

20s

一开始,呈现2的指数级变化,直到16s。最后,一直20s。
除非当前时间与回退开始时间差值超过2倍maxDuration即40s,从而重置回退间隔时间为1s。

自己动手写Demo

代码

go.mod

require (
	github.com/fsnotify/fsnotify v1.4.9
)

main.go

package main

import (
	"encoding/json"
	"fmt"
	"github.com/fsnotify/fsnotify"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

func main() {
	change := make(chan Info, 50)
	// 监控/root/test目录,间隔20s list,间隔1s watch
	s := NewSourceFile("/root/test", change)
	s.startListAndWatch()

	for info := range change {
			switch info.Op {
			case ADD:
				fmt.Printf("ADD: {%s}\n", info.String())
			case MODIFY:
				fmt.Printf("UPDATE: {%s}\n", info.String())
			case DELETE:
				fmt.Printf("REMOVE: {%s}\n", info.String())
			}

			fmt.Printf("latest info is %v\n", s.cache)
	}
}

type Info struct {
	Name string
	Age int
	Op InfoOp
}

type InfoOp int

const (
	ADD InfoOp = iota
	MODIFY
	DELETE
)

func (i *Info) String() string {
	return i.Name + "," +strconv.Itoa(i.Age)
}

type SourceFile struct {
	path           string
	cache map[string]int
	mutex sync.Mutex
	change chan Info
}

func NewSourceFile(path string, change chan Info) SourceFile {
	return SourceFile{
		path: path,
		cache: make(map[string]int),
		change: change,
	}
}

func (s *SourceFile) startListAndWatch() {
	if err := s.listConfig(); err != nil {
		fmt.Printf("Unable to read config path %q: %v\n", s.path, err)
	}

	go func() {
		listTicker := time.NewTicker(5 * time.Second)
		defer listTicker.Stop()

		for range listTicker.C {
			if err := s.listConfig(); err != nil {
				fmt.Printf("Unable to read config path %q: %v\n", s.path, err)
			}
		}
	}()

	go func() {
		watchTicker := time.NewTicker(time.Second)
		defer watchTicker.Stop()

		for range watchTicker.C {
			// 启动fsnotify,直到监听目录下文件有变化才停止,监听时间可能超过1s
			// watch可能会监听不到一些变化,需要list来确保正确性
			if err := s.doWatch(); err != nil {
				fmt.Printf("Unable to read config path %q: %v\n", s.path, err)
			}
		}
	}()
}

func (s *SourceFile) doWatch() error {
	w, err := fsnotify.NewWatcher()
	if err != nil {
		return fmt.Errorf("unable to create inotify: %v", err)
	}
	defer w.Close()

	err = w.Add(s.path)
	if err != nil {
		return fmt.Errorf("unable to create inotify for path %q: %v", s.path, err)
	}

	for {
		select {
		case event := <-w.Events:
			if err = s.produceWatchEvent(event); err != nil {
				return fmt.Errorf("error while processing inotify event (%+v): %v", event, err)
			}
		case err = <-w.Errors:
			return fmt.Errorf("error while watching %q: %v", s.path, err)
		}
	}
}

func (s *SourceFile) addCache(info Info) {
	s.mutex.Lock()
	defer s.mutex.Unlock()

	if age, ok := s.cache[info.Name]; !ok {
		info.Op = ADD
		s.change <- info
	} else if age != info.Age {
		info.Op = MODIFY
		s.change <- info
	}

	s.cache[info.Name] = info.Age
}

func (s *SourceFile) deleteCache(keys []string) {
	s.mutex.Lock()
	defer s.mutex.Unlock()

	for _, key := range keys {
		if age, ok := s.cache[key]; ok {
			info := Info{
				Name: key,
				Age: age,
				Op: DELETE,
			}
			s.change <- info
			delete(s.cache, key)
		}
	}
}

func (s *SourceFile) produceWatchEvent(e fsnotify.Event) error {
	// 不处理隐藏文件,编辑文件时会产生隐藏文件,例如.a.log.swp
	if strings.HasPrefix(filepath.Base(e.Name), ".") {
		return nil
	}

	switch {
	case (e.Op & fsnotify.Create) > 0 || (e.Op & fsnotify.Write) > 0:
		if info, err := s.extractFromFile(e.Name); err != nil {
			return fmt.Errorf("can't read config file %q: %v", e.Name, err)
		} else {
			s.addCache(info)
			return nil
		}
	case (e.Op & fsnotify.Remove) > 0:
		// 文件名是key,减少从文件名到key的一层映射
		s.deleteCache([]string{strings.Split(e.Name, ".")[0]})
		return nil
	default:
		return nil
	}
}

func (s *SourceFile) listConfig() error {
	infos, err := s.extractFromDir(s.path)
	if err != nil {
		return err
	}

	deleteKeys := make([]string, 0)
	for name, _ := range s.cache {
		find := false
		for _, info := range infos {
			if info.Name == name {
				s.addCache(info)
				find = true
			}
		}
		if !find {
			deleteKeys = append(deleteKeys, name)
		}
	}

	if len(s.cache) == 0 {
		for _, info := range infos {
			s.addCache(info)
		}
	}
	s.deleteCache(deleteKeys)

	return nil
}

func (s *SourceFile) extractFromDir(path string) ([]Info, error) {
	dirents, err := filepath.Glob(filepath.Join(path, "[^.]*"))
	if err != nil {
		return nil, fmt.Errorf("glob failed: %v", err)
	}

	infos := make([]Info, 0)
	if len(dirents) == 0 {
		fmt.Printf("there is nothing in %s\n", path)
		return infos, nil
	}

	for _, path := range dirents {
			info, err := s.extractFromFile(path)
			if err != nil {
				fmt.Printf("Can't read info file %q: %v\n", path, err)
				return nil, err
			} else {
				infos = append(infos, info)
			}
	}
	return infos, nil
}

func (s *SourceFile) extractFromFile(filename string) (info Info, err error) {
	file, err := os.Open(filename)
	if err != nil {
		return info, err
	}
	defer file.Close()

	data, err := ioutil.ReadAll(file)
	if err != nil {
		return info, err
	}

	err = json.Unmarshal(data, &info)
	return info, err
}

编译并执行二进制文件

cd kubelet-monitor-manifests-demo
go build main.go
./main

测试数据

/root/test目录下放入a.log和b.log,内容分别如下:

[root@192 test]# cat a.log
{"name": "a", "age": 10}
[root@192 test]# cat b.log
{"name": "b", "age": 100}

效果

流程图

demo流程
与kubelet相比,缓存对比更简单

kubelet类似流程

缓存1的全量数据是最新的,缓存2的全量数据是旧的。
缓存1和缓存2对比方式:
遍历缓存1中所有的key,如果缓存1中的key在缓存2中没有,那么说明该info需要增加;如果缓存1中的key在缓存2中有而且value发生了变化,那么该info需要更新。
遍历缓存2中所有的key,如果缓存2中的key在缓存1中没有,那么说明该info需要删除。
把变化的信息发送到缓冲通道,交给数据处理中心。

posted on 2022-04-21 07:49  王景迁  阅读(160)  评论(0编辑  收藏  举报

导航