Kubernetes Informer數(shù)據(jù)存儲(chǔ)Index與Pod分配流程解析
確立目標(biāo)
- 理解Informer的數(shù)據(jù)存儲(chǔ)方式
- 大致理解Pod的分配流程
理解Informer的數(shù)據(jù)存儲(chǔ)方式 代碼在k8s.io/client-go/tools/cache/controller
Process 查看消費(fèi)的過(guò)程
func (c *controller) processLoop() { for { // Pop出Object元素 obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process)) if err != nil { if err == ErrFIFOClosed { return } if c.config.RetryOnError { // 重新進(jìn)隊(duì)列 c.config.Queue.AddIfNotPresent(obj) } } } } // 去查看Pop的具體實(shí)現(xiàn) 點(diǎn)進(jìn)Pop 找到fifo.go func (f *FIFO) Pop(process PopProcessFunc) (interface{}, error) { f.lock.Lock() defer f.lock.Unlock() for { // 調(diào)用process去處理item,然后返回 item, ok := f.items[id] delete(f.items, id) err := process(item) return item, err } } // 然后去查一下 PopProcessFunc 的定義,在創(chuàng)建controller前 share_informer.go的Run()里面 cfg := &Config{ Process: s.HandleDeltas, } func (s *sharedIndexInformer) HandleDeltas(obj interface{}) error { s.blockDeltas.Lock() defer s.blockDeltas.Unlock() for _, d := range obj.(Deltas) { switch d.Type { // 增、改、替換、同步 case Sync, Replaced, Added, Updated: s.cacheMutationDetector.AddObject(d.Object) // 先去indexer查詢(xún) if old, exists, err := s.indexer.Get(d.Object); err == nil && exists { // 如果數(shù)據(jù)已經(jīng)存在,就執(zhí)行Update邏輯 if err := s.indexer.Update(d.Object); err != nil { return err } isSync := false switch { case d.Type == Sync: isSync = true case d.Type == Replaced: if accessor, err := meta.Accessor(d.Object); err == nil { isSync = accessor.GetResourceVersion() == oldAccessor.GetResourceVersion() } } } // 分發(fā)Update事件 s.processor.distribute(updateNotification{oldObj: old, newObj: d.Object}, isSync) } else { // 沒(méi)查到數(shù)據(jù),就執(zhí)行Add操作 if err := s.indexer.Add(d.Object); err != nil { return err } // 分發(fā) Add 事件 s.processor.distribute(addNotification{newObj: d.Object}, false) } // 刪除 case Deleted: // 去indexer刪除 if err := s.indexer.Delete(d.Object); err != nil { return err } // 分發(fā) delete 事件 s.processor.distribute(deleteNotification{oldObj: d.Object}, false) } } return nil }
Index 掌握Index數(shù)據(jù)結(jié)構(gòu)
Index
的定義為資源的本地存儲(chǔ),保持與etcd中的資源信息一致。
// 我們?nèi)タ纯碔ndex是怎么創(chuàng)建的 func NewSharedIndexInformer(lw ListerWatcher, exampleObject runtime.Object, defaultEventHandlerResyncPeriod time.Duration, indexers Indexers) SharedIndexInformer { realClock := &clock.RealClock{} sharedIndexInformer := &sharedIndexInformer{ processor: &sharedProcessor{clock: realClock}, // indexer 的初始化 indexer: NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers), listerWatcher: lw, objectType: exampleObject, resyncCheckPeriod: defaultEventHandlerResyncPeriod, defaultEventHandlerResyncPeriod: defaultEventHandlerResyncPeriod, cacheMutationDetector: NewCacheMutationDetector(fmt.Sprintf("%T", exampleObject)), clock: realClock, } return sharedIndexInformer } // 生成一個(gè)map和func組合而成的Indexer func NewIndexer(keyFunc KeyFunc, indexers Indexers) Indexer { return &cache{ cacheStorage: NewThreadSafeStore(indexers, Indices{}), keyFunc: keyFunc, } // ThreadSafeStore的底層是一個(gè)并發(fā)安全的map,具體實(shí)現(xiàn)我們暫不考慮 func NewThreadSafeStore(indexers Indexers, indices Indices) ThreadSafeStore { return &threadSafeMap{ items: map[string]interface{}{}, indexers: indexers, indices: indices, } }
distribute 信息的分發(fā)distribute
// 在上面的Process代碼中,我們看到了將數(shù)據(jù)存儲(chǔ)到Indexer后,調(diào)用了一個(gè)分發(fā)的函數(shù) s.processor.distribute() // 分發(fā)process的創(chuàng)建 func NewSharedIndexInformer() SharedIndexInformer { sharedIndexInformer := &sharedIndexInformer{ processor: &sharedProcessor{clock: realClock}, } return sharedIndexInformer } // sharedProcessor的結(jié)構(gòu) type sharedProcessor struct { listenersStarted bool // 讀寫(xiě)鎖 listenersLock sync.RWMutex // 普通監(jiān)聽(tīng)列表 listeners []*processorListener // 同步監(jiān)聽(tīng)列表 syncingListeners []*processorListener clock clock.Clock wg wait.Group } // 查看distribute函數(shù) func (p *sharedProcessor) distribute(obj interface{}, sync bool) { p.listenersLock.RLock() defer p.listenersLock.RUnlock() // 將object分發(fā)到 同步監(jiān)聽(tīng) 或者 普通監(jiān)聽(tīng) 的列表 if sync { for _, listener := range p.syncingListeners { listener.add(obj) } } else { for _, listener := range p.listeners { listener.add(obj) } } } // 這個(gè)add的操作是利用了channel func (p *processorListener) add(notification interface{}) { p.addCh <- notification }
理解一個(gè)pod的被調(diào)度的大致流程
Scheduler
在前面,我們了解了Pod調(diào)度算法的注冊(cè)和Informer機(jī)制來(lái)監(jiān)聽(tīng)kube-apiserver上的資源變化,這一次,我們就將兩者串聯(lián)起來(lái),看看在kube-scheduler中,Informer監(jiān)聽(tīng)到資源變化后,如何用調(diào)度算法將pod進(jìn)行調(diào)度。
// 在setup()中找到scheduler // 在運(yùn)行 kube-scheduler 的初期,我們創(chuàng)建了一個(gè)Scheduler的數(shù)據(jù)結(jié)構(gòu),回頭再看看有什么和pod調(diào)度算法相關(guān)的 type Scheduler struct { SchedulerCache internalcache.Cache Algorithm core.ScheduleAlgorithm // 獲取下一個(gè)需要調(diào)度的Pod NextPod func() *framework.QueuedPodInfo Error func(*framework.QueuedPodInfo, error) StopEverything <-chan struct{} // 等待調(diào)度的Pod隊(duì)列,我們重點(diǎn)看看這個(gè)隊(duì)列是什么 SchedulingQueue internalqueue.SchedulingQueue Profiles profile.Map scheduledPodsHasSynced func() bool client clientset.Interface } // Scheduler的實(shí)例化函數(shù) 在最新的版本中少了create這一層 直接是進(jìn)行里面的邏輯 func New(){ var sched *Scheduler switch { // 從 Provider 創(chuàng)建 case source.Provider != nil: sc, err := configurator.createFromProvider(*source.Provider) sched = sc // 從文件或者ConfigMap中創(chuàng)建 case source.Policy != nil: sc, err := configurator.createFromConfig(*policy) sched = sc default: return nil, fmt.Errorf("unsupported algorithm source: %v", source) } } // 兩個(gè)創(chuàng)建方式,底層都是調(diào)用的 create 函數(shù) func (c *Configurator) createFromProvider(providerName string) (*Scheduler, error) { return c.create() } func (c *Configurator) createFromConfig(policy schedulerapi.Policy) (*Scheduler, error){ return c.create() } func (c *Configurator) create() (*Scheduler, error) { // 實(shí)例化 podQueue podQueue := internalqueue.NewSchedulingQueue( lessFn, internalqueue.WithPodInitialBackoffDuration(time.Duration(c.podInitialBackoffSeconds)*time.Second), internalqueue.WithPodMaxBackoffDuration(time.Duration(c.podMaxBackoffSeconds)*time.Second), internalqueue.WithPodNominator(nominator), ) return &Scheduler{ SchedulerCache: c.schedulerCache, Algorithm: algo, Profiles: profiles, // NextPod 函數(shù)依賴(lài)于 podQueue NextPod: internalqueue.MakeNextPodFunc(podQueue), Error: MakeDefaultErrorFunc(c.client, c.informerFactory.Core().V1().Pods().Lister(), podQueue, c.schedulerCache), StopEverything: c.StopEverything, // 調(diào)度隊(duì)列被賦值為podQueue SchedulingQueue: podQueue, }, nil } // 再看看這個(gè)調(diào)度隊(duì)列的初始化函數(shù),點(diǎn)進(jìn)去podQueue,從命名可以看到是一個(gè)優(yōu)先隊(duì)列,它的實(shí)現(xiàn)細(xì)節(jié)暫不細(xì)看 // 結(jié)合實(shí)際情況思考下,pod會(huì)有重要程度的區(qū)分,所以調(diào)度的順序需要考慮優(yōu)先級(jí)的 func NewSchedulingQueue(lessFn framework.LessFunc, opts ...Option) SchedulingQueue { return NewPriorityQueue(lessFn, opts...) }
SchedulingQueue
// 在上面實(shí)例化Scheduler后,有個(gè)注冊(cè)事件 Handler 的函數(shù):addAllEventHandlers(sched, informerFactory, podInformer) informer接到消息之后觸發(fā)對(duì)應(yīng)的Handler func addAllEventHandlers( sched *Scheduler, informerFactory informers.SharedInformerFactory, podInformer coreinformers.PodInformer, ) { /* 函數(shù)前后有很多注冊(cè)的Handler,但是和未調(diào)度pod添加到隊(duì)列相關(guān)的,只有這個(gè) */ podInformer.Informer().AddEventHandler( cache.FilteringResourceEventHandler{ // 定義過(guò)濾函數(shù):必須為未調(diào)度的pod FilterFunc: func(obj interface{}) bool { switch t := obj.(type) { case *v1.Pod: return !assignedPod(t) && responsibleForPod(t, sched.Profiles) case cache.DeletedFinalStateUnknown: if pod, ok := t.Obj.(*v1.Pod); ok { return !assignedPod(pod) && responsibleForPod(pod, sched.Profiles) } utilruntime.HandleError(fmt.Errorf("unable to convert object %T to *v1.Pod in %T", obj, sched)) return false default: utilruntime.HandleError(fmt.Errorf("unable to handle object in %T: %T", sched, obj)) return false } }, // 增改刪三個(gè)操作對(duì)應(yīng)的Handler,操作到對(duì)應(yīng)的Queue Handler: cache.ResourceEventHandlerFuncs{ AddFunc: sched.addPodToSchedulingQueue, UpdateFunc: sched.updatePodInSchedulingQueue, DeleteFunc: sched.deletePodFromSchedulingQueue, }, }, ) } // 牢記我們第一階段要分析的對(duì)象:create nginx pod,所以進(jìn)入這個(gè)add的操作,對(duì)應(yīng)加入到隊(duì)列 func (sched *Scheduler) addPodToSchedulingQueue(obj interface{}) { pod := obj.(*v1.Pod) klog.V(3).Infof("add event for unscheduled pod %s/%s", pod.Namespace, pod.Name) // 加入到隊(duì)列 if err := sched.SchedulingQueue.Add(pod); err != nil { utilruntime.HandleError(fmt.Errorf("unable to queue %T: %v", obj, err)) } } // 在實(shí)例化Scheduler的地方 // 入隊(duì)操作我們清楚了,那出隊(duì)呢?我們回過(guò)頭去看看上面定義的NextPod的方法實(shí)現(xiàn) func MakeNextPodFunc(queue SchedulingQueue) func() *framework.QueuedPodInfo { return func() *framework.QueuedPodInfo { // 從隊(duì)列中彈出 podInfo, err := queue.Pop() if err == nil { klog.V(4).Infof("About to try and schedule pod %v/%v", podInfo.Pod.Namespace, podInfo.Pod.Name) return podInfo } klog.Errorf("Error while retrieving next pod from scheduling queue: %v", err) return nil } }
scheduleOne
// 了解入隊(duì)和出隊(duì)操作后,我們看一下Scheduler運(yùn)行的過(guò)程 func (sched *Scheduler) Run(ctx context.Context) { if !cache.WaitForCacheSync(ctx.Done(), sched.scheduledPodsHasSynced) { return } sched.SchedulingQueue.Run() // 調(diào)度一個(gè)pod對(duì)象 wait.UntilWithContext(ctx, sched.scheduleOne, 0) sched.SchedulingQueue.Close() } // 接下來(lái)scheduleOne方法代碼很長(zhǎng),我們一步一步來(lái)看 func (sched *Scheduler) scheduleOne(ctx context.Context) { // podInfo 就是從隊(duì)列中獲取到的pod對(duì)象 podInfo := sched.NextPod() // 檢查pod的有效性 if podInfo == nil || podInfo.Pod == nil { return } pod := podInfo.Pod // 根據(jù)定義的 pod.Spec.SchedulerName 查到對(duì)應(yīng)的profile prof, err := sched.profileForPod(pod) if err != nil { klog.Error(err) return } // 可以跳過(guò)調(diào)度的情況,一般pod進(jìn)不來(lái) if sched.skipPodSchedule(prof, pod) { return } // 調(diào)用調(diào)度算法,獲取結(jié)果 scheduleResult, err := sched.Algorithm.Schedule(schedulingCycleCtx, prof, state, pod) if err != nil { /* 出現(xiàn)調(diào)度失敗的情況: 這個(gè)時(shí)候可能會(huì)觸發(fā)搶占preempt,搶占是一套復(fù)雜的邏輯,后面我們專(zhuān)門(mén)會(huì)講 目前假設(shè)各類(lèi)資源充足,能正常調(diào)度 */ } metrics.SchedulingAlgorithmLatency.Observe(metrics.SinceInSeconds(start)) // assumePod 是假設(shè)這個(gè)Pod按照前面的調(diào)度算法分配后,進(jìn)行驗(yàn)證 assumedPodInfo := podInfo.DeepCopy() assumedPod := assumedPodInfo.Pod // SuggestedHost 為建議的分配的Host err = sched.assume(assumedPod, scheduleResult.SuggestedHost) if err != nil { // 失敗就重新分配,不考慮這種情況 } // 運(yùn)行相關(guān)插件的代碼先跳過(guò) 比如一些搶占插件 // 異步綁定pod go func() { // 有一系列的檢查工作 // 真正做綁定的動(dòng)作 err := sched.bind(bindingCycleCtx, prof, assumedPod, scheduleResult.SuggestedHost, state) if err != nil { // 錯(cuò)誤處理,清除狀態(tài)并重試 } else { // 打印結(jié)果,調(diào)試時(shí)將log level調(diào)整到2以上 if klog.V(2).Enabled() { klog.InfoS("Successfully bound pod to node", "pod", klog.KObj(pod), "node", scheduleResult.SuggestedHost, "evaluatedNodes", scheduleResult.EvaluatedNodes, "feasibleNodes", scheduleResult.FeasibleNodes) } // metrics中記錄相關(guān)的監(jiān)控指標(biāo) metrics.PodScheduled(prof.Name, metrics.SinceInSeconds(start)) metrics.PodSchedulingAttempts.Observe(float64(podInfo.Attempts)) metrics.PodSchedulingDuration.WithLabelValues(getAttemptsLabel(podInfo)).Observe(metrics.SinceInSeconds(podInfo.InitialAttemptTimestamp)) // 運(yùn)行綁定后的插件 prof.RunPostBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost) } }() }
ScheduleResult 調(diào)度計(jì)算結(jié)果
// 調(diào)用算法下的Schedule func New(){ scheduleResult, err := sched.Algorithm.Schedule(schedulingCycleCtx, prof, state, pod) } func (c *Configurator) create() (*Scheduler, error) { algo := core.NewGenericScheduler( c.schedulerCache, c.nodeInfoSnapshot, extenders, c.informerFactory.Core().V1().PersistentVolumeClaims().Lister(), c.disablePreemption, c.percentageOfNodesToScore, ) return &Scheduler{ Algorithm: algo, }, nil } // genericScheduler 的 Schedule 的實(shí)現(xiàn) func (g *genericScheduler) Schedule(ctx context.Context, prof *profile.Profile, state *framework.CycleState, pod *v1.Pod) (result ScheduleResult, err error) { // 對(duì) pod 進(jìn)行 pvc 的信息檢查 if err := podPassesBasicChecks(pod, g.pvcLister); err != nil { return result, err } // 對(duì)當(dāng)前的信息做一個(gè)快照 if err := g.snapshot(); err != nil { return result, err } // Node 節(jié)點(diǎn)數(shù)量為0,表示無(wú)可用節(jié)點(diǎn) if g.nodeInfoSnapshot.NumNodes() == 0 { return result, ErrNoNodesAvailable } // Predict階段:找到所有滿(mǎn)足調(diào)度條件的節(jié)點(diǎn)feasibleNodes,不滿(mǎn)足的就直接過(guò)濾 feasibleNodes, filteredNodesStatuses, err := g.findNodesThatFitPod(ctx, prof, state, pod) // 沒(méi)有可用節(jié)點(diǎn)直接報(bào)錯(cuò) if len(feasibleNodes) == 0 { return result, &FitError{ Pod: pod, NumAllNodes: g.nodeInfoSnapshot.NumNodes(), FilteredNodesStatuses: filteredNodesStatuses, } } // 只有一個(gè)節(jié)點(diǎn)就直接選用 if len(feasibleNodes) == 1 { return ScheduleResult{ SuggestedHost: feasibleNodes[0].Name, EvaluatedNodes: 1 + len(filteredNodesStatuses), FeasibleNodes: 1, }, nil } // Priority階段:通過(guò)打分,找到一個(gè)分?jǐn)?shù)最高、也就是最優(yōu)的節(jié)點(diǎn) priorityList, err := g.prioritizeNodes(ctx, prof, state, pod, feasibleNodes) host, err := g.selectHost(priorityList) return ScheduleResult{ SuggestedHost: host, EvaluatedNodes: len(feasibleNodes) + len(filteredNodesStatuses), FeasibleNodes: len(feasibleNodes), }, err } /* Predict 和 Priority 是選擇調(diào)度節(jié)點(diǎn)的兩個(gè)關(guān)鍵性步驟, 它的底層調(diào)用了各種algorithm算法。我們暫時(shí)不細(xì)看。 以我們前面講到過(guò)的 NodeName 算法為例,節(jié)點(diǎn)必須與 NodeName 匹配,它是屬于Predict階段的。 在新版本中 這部分算法的實(shí)現(xiàn)放到了extenders,邏輯是一樣的 */
Assume 初步推算
func (sched *Scheduler) assume(assumed *v1.Pod, host string) error { // 將 host 填入到 pod spec字段的nodename,假定分配到對(duì)應(yīng)的節(jié)點(diǎn)上 assumed.Spec.NodeName = host // 調(diào)用 SchedulerCache 下的 AssumePod if err := sched.SchedulerCache.AssumePod(assumed); err != nil { klog.Errorf("scheduler cache AssumePod failed: %v", err) return err } if sched.SchedulingQueue != nil { sched.SchedulingQueue.DeleteNominatedPodIfExists(assumed) } return nil } // 回頭去找 SchedulerCache 初始化的地方 func (c *Configurator) create() (*Scheduler, error) { return &Scheduler{ SchedulerCache: c.schedulerCache, }, nil } func New() (*Scheduler, error) { // 這里就是初始化的實(shí)例 schedulerCache schedulerCache := internalcache.New(30*time.Second, stopEverything) configurator := &Configurator{ schedulerCache: schedulerCache, } } // 看看AssumePod做了什么 func (cache *schedulerCache) AssumePod(pod *v1.Pod) error { // 獲取 pod 的 uid key, err := framework.GetPodKey(pod) if err != nil { return err } // 加鎖操作,保證并發(fā)情況下的一致性 cache.mu.Lock() defer cache.mu.Unlock() // 根據(jù) uid 找不到 pod 當(dāng)前的狀態(tài) 看看被調(diào)度了沒(méi)有 if _, ok := cache.podStates[key]; ok { return fmt.Errorf("pod %v is in the cache, so can't be assumed", key) } // 把 Assume Pod 的信息放到對(duì)應(yīng) Node 節(jié)點(diǎn)中 cache.addPod(pod) // 把 pod 狀態(tài)設(shè)置為 Assume 成功 ps := &podState{ pod: pod, } cache.podStates[key] = ps cache.assumedPods[key] = true return nil }
Bind 實(shí)際綁定
func (sched *Scheduler) bind(ctx context.Context, prof *profile.Profile, assumed *v1.Pod, targetNode string, state *framework.CycleState) (err error) { start := time.Now() // 把 assumed 的 pod 信息保存下來(lái) defer func() { sched.finishBinding(prof, assumed, targetNode, start, err) }() // 階段1: 運(yùn)行擴(kuò)展綁定進(jìn)行驗(yàn)證,如果已經(jīng)綁定報(bào)錯(cuò) bound, err := sched.extendersBinding(assumed, targetNode) if bound { return err } // 階段2:運(yùn)行綁定插件驗(yàn)證狀態(tài) bindStatus := prof.RunBindPlugins(ctx, state, assumed, targetNode) if bindStatus.IsSuccess() { return nil } if bindStatus.Code() == framework.Error { return bindStatus.AsError() } return fmt.Errorf("bind status: %s, %v", bindStatus.Code().String(), bindStatus.Message()) }
Update To Etcd
// 這塊的代碼我不做細(xì)致的逐層分析了,大家根據(jù)興趣自行探索 func (b DefaultBinder) Bind(ctx context.Context, state *framework.CycleState, p *v1.Pod, nodeName string) *framework.Status { klog.V(3).Infof("Attempting to bind %v/%v to %v", p.Namespace, p.Name, nodeName) binding := &v1.Binding{ ObjectMeta: metav1.ObjectMeta{Namespace: p.Namespace, Name: p.Name, UID: p.UID}, Target: v1.ObjectReference{Kind: "Node", Name: nodeName}, } // ClientSet就是訪(fǎng)問(wèn)kube-apiserver的客戶(hù)端,將數(shù)據(jù)更新上去 err := b.handle.ClientSet().CoreV1().Pods(binding.Namespace).Bind(ctx, binding, metav1.CreateOptions{}) if err != nil { return framework.NewStatus(framework.Error, err.Error()) } return nil }
站在前人的肩膀上,向前輩致敬,Respect!
Summary
Informer
依賴(lài)于Reflector
模塊,它有個(gè)組件為 xxxInformer,如podInformer
- 具體資源的
Informer
包含了一個(gè)連接到kube-apiserver
的client
,通過(guò)List
和Watch
接口查詢(xún)資源變更情況
檢測(cè)到資源發(fā)生變化后,通過(guò)Controller
將數(shù)據(jù)放入隊(duì)列DeltaFIFOQueue
里,生產(chǎn)階段完成
在DeltaFIFOQueue
的另一端,有消費(fèi)者在不停地處理資源變化的事件,處理邏輯主要分2步
- 將數(shù)據(jù)保存到本地存儲(chǔ)Indexer,它的底層實(shí)現(xiàn)是一個(gè)并發(fā)安全的threadSafeMap
- 有些組件需要實(shí)時(shí)關(guān)注資源變化,會(huì)實(shí)時(shí)監(jiān)聽(tīng)listen,就將事件分發(fā)到對(duì)應(yīng)注冊(cè)上來(lái)的listener上,自行處理
distribute
將object分發(fā)到同步監(jiān)聽(tīng)或者普通監(jiān)聽(tīng)的列表,然后被對(duì)應(yīng)的handler處理
- Pod的調(diào)度是通過(guò)一個(gè)隊(duì)列
SchedulingQueue
異步工作的 - 監(jiān)聽(tīng)到對(duì)應(yīng)pod事件后,放入隊(duì)列
- 有個(gè)消費(fèi)者從隊(duì)列中獲取pod,進(jìn)行調(diào)度
單個(gè)pod的調(diào)度主要分為3個(gè)步驟:
- 根據(jù)Predict和Priority兩個(gè)階段,調(diào)用各自的算法插件,選擇最優(yōu)的Node
Assume
這個(gè)Pod被調(diào)度到對(duì)應(yīng)的Node,保存到cache,加鎖保證一致性。- 用extender和plugins進(jìn)行驗(yàn)證,如果通過(guò)則綁定
Bind
綁定成功后,將數(shù)據(jù)通過(guò)client向kube-apiserver發(fā)送,更新etcd
以上就是Kubernetes Informer數(shù)據(jù)存儲(chǔ)Index與Pod分配流程解析的詳細(xì)內(nèi)容,更多關(guān)于Kubernetes Informer數(shù)據(jù)存儲(chǔ)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 一文詳解基于Kubescape進(jìn)行Kubernetes安全加固
- kubernetes數(shù)據(jù)持久化PV?PVC深入分析詳解
- kubernetes數(shù)據(jù)持久化StorageClass動(dòng)態(tài)供給實(shí)現(xiàn)詳解
- Kubernetes?controller?manager運(yùn)行機(jī)制源碼解析
- Kubernetes scheduler啟動(dòng)監(jiān)控資源變化解析
- Kubernetes ApiServer三大server權(quán)限與數(shù)據(jù)存儲(chǔ)解析
- IoT邊緣集群Kubernetes?Events告警通知實(shí)現(xiàn)示例
相關(guān)文章
Kubernetes?controller?manager運(yùn)行機(jī)制源碼解析
這篇文章主要為大家介紹了Kubernetes?controller?manager運(yùn)行機(jī)制源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Kubernetes scheduler啟動(dòng)監(jiān)控資源變化解析
這篇文章主要為大家介紹了Kubernetes scheduler啟動(dòng)監(jiān)控資源變化解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Citrix Xenserver 7怎么安裝?Xenserver 7.0安裝詳細(xì)圖文教程(附下載地址)
XenServer 7.0正式版已近發(fā)布了,今天腳本之家 小編為大家?guī)?lái)了Xenserver 7安裝詳細(xì)圖文教程,希望對(duì)大家有所幫助2017-12-12kubectl中g(shù)et命令及使用示例總結(jié)
這篇文章主要為大家介紹了kubectl中g(shù)et命令及使用示例的總結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03Dashboard管理Kubernetes集群與API訪(fǎng)問(wèn)配置
這篇文章介紹了Dashboard管理Kubernetes集群與API訪(fǎng)問(wèn)配置的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04了解Kubernetes中的Service和Endpoint
這篇文章介紹了Kubernetes中的Service和Endpoint,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04Kubernetes(K8S)入門(mén)基礎(chǔ)內(nèi)容介紹
這篇文章介紹了Kubernetes(K8S)的入門(mén)基礎(chǔ)內(nèi)容,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-03-03k8s集群部署時(shí)etcd容器不停重啟問(wèn)題以及處理詳解
一次在k8s集群中創(chuàng)建實(shí)例發(fā)現(xiàn)etcd集群狀態(tài)出現(xiàn)連接失敗狀況,導(dǎo)致創(chuàng)建實(shí)例失敗,下面這篇文章主要給大家介紹了關(guān)于k8s集群部署時(shí)etcd容器不停重啟問(wèn)題以及處理的相關(guān)資料,需要的朋友可以參考下2023-01-01