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需要删除。
把变化的信息发送到缓冲通道,交给数据处理中心。