首页 文章 精选 留言 我的

精选列表

搜索[k8s],共4073篇文章
优秀的个人博客,低调大师

k8s源码Client-go中Reflector解析

摘要:通过本文,可以了解Reflector通过ListWatcher从Kubernetes API中获取对象的流程,以及存储到store中,后续会对DeltaFIFO进行源码研读,通过结合informer,来加深对整个informer的理解。 本文分享自华为云社区《Client-go源码分析之Reflector》,作者: kaliarch 。 一 背景 Reflector 是保证 Informer 可靠性的核心组件,在丢失事件,收到异常事件,处理事件失败等多种异常情况下需要考虑的细节很多。单独的listwatcher缺少重新连接和重新同步机制,有可能出现数据不一致问题。其对事件响应是同步的,如果执行复杂的操作会引起阻塞,需要引入队列。 二 Reflector Reflector可以成为反射器,将etcd中的数据反射到存储(DeltaFIFO)中。Reflector通过其内部的List操作获取所有资源对象数据,保存到本地存储,之后Watch监视资源变化,触发对应事件处理,例如Add、Update、Delete等。 Reflector 结构体的定义位于 staging/src/http://k8s.io/client-go/tools/cache/reflector.go下面: // k8s.io/client-go/tools/cache/reflector.go type Reflector struct { // name 标识这个反射器的名称,默认为 文件:行数(比如reflector.go:125) // 默认名字通过 k8s.io/apimachinery/pkg/util/naming/from_stack.go 下面的 GetNameFromCallsite 函数生成 name string // 期望放到 Store 中的类型名称,如果提供,则是 expectedGVK 的字符串形式 // 否则就是 expectedType 的字符串,它仅仅用于显示,不用于解析或者比较。 expectedTypeName string // An example object of the type we expect to place in the store. // Only the type needs to be right, except that when that is // `unstructured.Unstructured` the object's `"apiVersion"` and // `"kind"` must also be right. // 放到 Store 中的对象类型 expectedType reflect.Type // 如果是非结构化的,期望放在 Sotre 中的对象的 GVK expectedGVK *schema.GroupVersionKind // 与 watch 源同步的目标 Store store Store // 用来执行 lists 和 watches 操作的 listerWatcher 接口(最重要的) listerWatcher ListerWatcher // backoff manages backoff of ListWatch backoffManager wait.BackoffManager resyncPeriod time.Duration // ShouldResync 会周期性的被调用,当返回 true 的时候,就会调用 Store 的 Resync 操作 ShouldResync func() bool // clock allows tests to manipulate time clock clock.Clock // paginatedResult defines whether pagination should be forced for list calls. // It is set based on the result of the initial list call. paginatedResult bool // Kubernetes 资源在 APIServer 中都是有版本的,对象的任何修改(添加、删除、更新)都会造成资源版本更新,lastSyncResourceVersion 就是指的这个版本 lastSyncResourceVersion string // 如果之前的 list 或 watch 带有 lastSyncResourceVersion 的请求中是一个 HTTP 410(Gone)的失败请求,则 isLastSyncResourceVersionGone 为 true isLastSyncResourceVersionGone bool // lastSyncResourceVersionMutex 用于保证对 lastSyncResourceVersion 的读/写访问。 lastSyncResourceVersionMutex sync.RWMutex // WatchListPageSize is the requested chunk size of initial and resync watch lists. // If unset, for consistent reads (RV="") or reads that opt-into arbitrarily old data // (RV="0") it will default to pager.PageSize, for the rest (RV != "" && RV != "0") // it will turn off pagination to allow serving them from watch cache. // NOTE: It should be used carefully as paginated lists are always served directly from // etcd, which is significantly less efficient and may lead to serious performance and // scalability problems. WatchListPageSize int64 } // NewReflector 创建一个新的反射器对象,将使给定的 Store 保持与服务器中指定的资源对象的内容同步。 // 反射器只把具有 expectedType 类型的对象放到 Store 中,除非 expectedType 是 nil。 // 如果 resyncPeriod 是非0,那么反射器会周期性地检查 ShouldResync 函数来决定是否调用 Store 的 Resync 操作 // `ShouldResync==nil` 意味着总是要执行 Resync 操作。 // 这使得你可以使用反射器周期性地处理所有的全量和增量的对象。 func NewReflector(lw ListerWatcher, expectedType interface{}, store Store, resyncPeriod time.Duration) *Reflector { // 默认的反射器名称为 file:line return NewNamedReflector(naming.GetNameFromCallsite(internalPackages...), lw, expectedType, store, resyncPeriod) } // NewNamedReflector 与 NewReflector 一样,只是指定了一个 name 用于日志记录 func NewNamedReflector(name string, lw ListerWatcher, expectedType interface{}, store Store, resyncPeriod time.Duration) *Reflector { realClock := &clock.RealClock{} r := &Reflector{ name: name, listerWatcher: lw, store: store, backoffManager: wait.NewExponentialBackoffManager(800*time.Millisecond, 30*time.Second, 2*time.Minute, 2.0, 1.0, realClock), resyncPeriod: resyncPeriod, clock: realClock, } r.setExpectedType(expectedType) return r } 三 流程 Reflector.Run()调用ListAndWatch(), 启动一个子协程(goroutine)执行List,主协程阻塞等待List执行完成。 Meta.ExtractList(list)把List结果转化成runtime.Object数组。 r.syncWith(items, resourceVersion)写入DeltaFIFO,全量同步到Indexer。 r.resyncChan()也是在一个子协程里执行。 循环执行r.ListerWatcher.Watch(optiopns)。 r.WatchHandler增量同步runtime.Object到indexer。 List只做一次全量同步,watch持续做增量同步。 四 Reflector关键方法 4.1 构造方法 // NewReflector 创建一个新的反射器对象,将使给定的 Store 保持与服务器中指定的资源对象的内容同步。 // 反射器只把具有 expectedType 类型的对象放到 Store 中,除非 expectedType 是 nil。 // 如果 resyncPeriod 是非0,那么反射器会周期性地检查 ShouldResync 函数来决定是否调用 Store 的 Resync 操作 // `ShouldResync==nil` 意味着总是要执行 Resync 操作。 // 这使得你可以使用反射器周期性地处理所有的全量和增量的对象。 func NewReflector(lw ListerWatcher, expectedType interface{}, store Store, resyncPeriod time.Duration) *Reflector { // 默认的反射器名称为 file:line return NewNamedReflector(naming.GetNameFromCallsite(internalPackages...), lw, expectedType, store, resyncPeriod) } // NewNamedReflector 与 NewReflector 一样,只是指定了一个 name 用于日志记录 func NewNamedReflector(name string, lw ListerWatcher, expectedType interface{}, store Store, resyncPeriod time.Duration) *Reflector { realClock := &clock.RealClock{} r := &Reflector{ name: name, listerWatcher: lw, store: store, backoffManager: wait.NewExponentialBackoffManager(800*time.Millisecond, 30*time.Second, 2*time.Minute, 2.0, 1.0, realClock), resyncPeriod: resyncPeriod, clock: realClock, } r.setExpectedType(expectedType) return r } //新建Indexer和reflector func NewNamespaceKeyedIndexerAndReflector(lw ListerWatcher, expectedType interface{}, resyncPeriod time.Duration) (indexer Indexer, reflector *Reflector) { indexer = NewIndexer(MetaNamespaceKeyFunc, Indexers{NamespaceIndex: MetaNamespaceIndexFunc}) reflector = NewReflector(lw, expectedType, indexer, resyncPeriod) return indexer, reflector } 4.2 Run方法 // Run 重复使用反射器的 ListAndWatch 来获取所有对象和后续增量。当 stopCh 关闭时,运行将退出 func (r *Reflector) Run(stopCh <-chan struct{}) { klog.V(2).Infof("Starting reflector %s (%s) from %s", r.expectedTypeName, r.resyncPeriod, r.name) wait.BackoffUntil(func() { if err := r.ListAndWatch(stopCh); err != nil { r.watchErrorHandler(r, err) } }, r.backoffManager, true, stopCh) klog.V(2).Infof("Stopping reflector %s (%s) from %s", r.expectedTypeName, r.resyncPeriod, r.name) } 4.3 ListWatch List部分逻辑:设置分页参数;执行list方法;将list结果同步进DeltaFIFO队列中,其实是调用store中的Replace方法。 定时同步:定时同步以协程的方式运行,使用定时器实现定期同步,Store中的Resync操作。 Watch部分逻辑:在for循环里;执行watch函数获取resultchan;监听resultchan中数据并处理; // ListAndWatch 函数首先列出所有的对象,并在调用的时候获得资源版本,然后使用该资源版本来进行 watch 操作。 // 如果 ListAndWatch 没有初始化 watch 成功就会返回错误。 func (r *Reflector) ListAndWatch(stopCh <-chan struct{}) error { klog.V(3).Infof("Listing and watching %v from %s", r.expectedTypeName, r.name) var resourceVersion string options := metav1.ListOptions{ResourceVersion: r.relistResourceVersion()} // 1.List部分逻辑:设置分页参数;执行list方法;将list结果同步进DeltaFIFO队列中; if err := func() error { initTrace := trace.New("Reflector ListAndWatch", trace.Field{"name", r.name}) defer initTrace.LogIfLong(10 * time.Second) var list runtime.Object var paginatedResult bool var err error listCh := make(chan struct{}, 1) panicCh := make(chan interface{}, 1) go func() { defer func() { if r := recover(); r != nil { panicCh <- r } }() // Attempt to gather list in chunks, if supported by listerWatcher, if not, the first // list request will return the full response. pager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) { return r.listerWatcher.List(opts) })) switch { case r.WatchListPageSize != 0: pager.PageSize = r.WatchListPageSize case r.paginatedResult: // We got a paginated result initially. Assume this resource and server honor // paging requests (i.e. watch cache is probably disabled) and leave the default // pager size set. case options.ResourceVersion != "" && options.ResourceVersion != "0": // User didn't explicitly request pagination. // // With ResourceVersion != "", we have a possibility to list from watch cache, // but we do that (for ResourceVersion != "0") only if Limit is unset. // To avoid thundering herd on etcd (e.g. on master upgrades), we explicitly // switch off pagination to force listing from watch cache (if enabled). // With the existing semantic of RV (result is at least as fresh as provided RV), // this is correct and doesn't lead to going back in time. // // We also don't turn off pagination for ResourceVersion="0", since watch cache // is ignoring Limit in that case anyway, and if watch cache is not enabled // we don't introduce regression. pager.PageSize = 0 } list, paginatedResult, err = pager.List(context.Background(), options) if isExpiredError(err) || isTooLargeResourceVersionError(err) { r.setIsLastSyncResourceVersionUnavailable(true) // Retry immediately if the resource version used to list is unavailable. // The pager already falls back to full list if paginated list calls fail due to an "Expired" error on // continuation pages, but the pager might not be enabled, the full list might fail because the // resource version it is listing at is expired or the cache may not yet be synced to the provided // resource version. So we need to fallback to resourceVersion="" in all to recover and ensure // the reflector makes forward progress. list, paginatedResult, err = pager.List(context.Background(), metav1.ListOptions{ResourceVersion: r.relistResourceVersion()}) } close(listCh) }() select { case <-stopCh: return nil case r := <-panicCh: panic(r) case <-listCh: } if err != nil { return fmt.Errorf("failed to list %v: %v", r.expectedTypeName, err) } // We check if the list was paginated and if so set the paginatedResult based on that. // However, we want to do that only for the initial list (which is the only case // when we set ResourceVersion="0"). The reasoning behind it is that later, in some // situations we may force listing directly from etcd (by setting ResourceVersion="") // which will return paginated result, even if watch cache is enabled. However, in // that case, we still want to prefer sending requests to watch cache if possible. // // Paginated result returned for request with ResourceVersion="0" mean that watch // cache is disabled and there are a lot of objects of a given type. In such case, // there is no need to prefer listing from watch cache. if options.ResourceVersion == "0" && paginatedResult { r.paginatedResult = true } r.setIsLastSyncResourceVersionUnavailable(false) // list was successful initTrace.Step("Objects listed") // listMetaInterface, err := meta.ListAccessor(list) if err != nil { return fmt.Errorf("unable to understand list result %#v: %v", list, err) } // 获取资源版本号 resourceVersion = listMetaInterface.GetResourceVersion() initTrace.Step("Resource version extracted") // 将资源对象转换为资源列表,讲runtime.Object 对象转换为[]runtime.Object对象 items, err := meta.ExtractList(list) if err != nil { return fmt.Errorf("unable to understand list result %#v (%v)", list, err) } initTrace.Step("Objects extracted") // 将资源对象列表中的资源和版本号存储在store中 if err := r.syncWith(items, resourceVersion); err != nil { return fmt.Errorf("unable to sync list result: %v", err) } initTrace.Step("SyncWith done") r.setLastSyncResourceVersion(resourceVersion) initTrace.Step("Resource version updated") return nil }(); err != nil { return err } // 2.定时同步:定时同步以协程的方式运行,使用定时器实现定期同步 resyncerrc := make(chan error, 1) cancelCh := make(chan struct{}) defer close(cancelCh) go func() { resyncCh, cleanup := r.resyncChan() defer func() { cleanup() // Call the last one written into cleanup }() for { select { case <-resyncCh: case <-stopCh: return case <-cancelCh: return } // 如果ShouldResync 为nil或者调用返回true,则执行Store中的Resync操作 if r.ShouldResync == nil || r.ShouldResync() { klog.V(4).Infof("%s: forcing resync", r.name) // 将indexer的数据和deltafifo进行同步 if err := r.store.Resync(); err != nil { resyncerrc <- err return } } cleanup() resyncCh, cleanup = r.resyncChan() } }() // 3.在for循环里;执行watch函数获取resultchan;监听resultchan中数据并处理; for { // give the stopCh a chance to stop the loop, even in case of continue statements further down on errors select { case <-stopCh: return nil default: } timeoutSeconds := int64(minWatchTimeout.Seconds() * (rand.Float64() + 1.0)) options = metav1.ListOptions{ ResourceVersion: resourceVersion, // We want to avoid situations of hanging watchers. Stop any wachers that do not // receive any events within the timeout window. TimeoutSeconds: &timeoutSeconds, // To reduce load on kube-apiserver on watch restarts, you may enable watch bookmarks. // Reflector doesn't assume bookmarks are returned at all (if the server do not support // watch bookmarks, it will ignore this field). AllowWatchBookmarks: true, } // start the clock before sending the request, since some proxies won't flush headers until after the first watch event is sent start := r.clock.Now() w, err := r.listerWatcher.Watch(options) if err != nil { // If this is "connection refused" error, it means that most likely apiserver is not responsive. // It doesn't make sense to re-list all objects because most likely we will be able to restart // watch where we ended. // If that's the case begin exponentially backing off and resend watch request. if utilnet.IsConnectionRefused(err) { <-r.initConnBackoffManager.Backoff().C() continue } return err } if err := r.watchHandler(start, w, &resourceVersion, resyncerrc, stopCh); err != nil { if err != errorStopRequested { switch { case isExpiredError(err): // Don't set LastSyncResourceVersionUnavailable - LIST call with ResourceVersion=RV already // has a semantic that it returns data at least as fresh as provided RV. // So first try to LIST with setting RV to resource version of last observed object. klog.V(4).Infof("%s: watch of %v closed with: %v", r.name, r.expectedTypeName, err) default: klog.Warningf("%s: watch of %v ended with: %v", r.name, r.expectedTypeName, err) } } return nil } } } 4.4 LastSyncResourceVersion 获取上一次同步的资源版本 func (r *Reflector) LastSyncResourceVersion() string { r.lastSyncResourceVersionMutex.RLock() defer r.lastSyncResourceVersionMutex.RUnlock() return r.lastSyncResourceVersion } 4.5 resyncChan 返回一个定时通道和清理函数,清理函数就是停止计时器。这边的定时重新同步是使用定时器实现的。 func (r *Reflector) resyncChan() (<-chan time.Time, func() bool) { if r.resyncPeriod == 0 { return neverExitWatch, func() bool { return false } } // The cleanup function is required: imagine the scenario where watches // always fail so we end up listing frequently. Then, if we don't // manually stop the timer, we could end up with many timers active // concurrently. t := r.clock.NewTimer(r.resyncPeriod) return t.C(), t.Stop } 4.6 syncWith 将从apiserver list的资源对象结果同步进DeltaFIFO队列中,调用队列的Replace方法实现。 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) } 4.7 watchHandler watch的处理:接收watch的接口作为参数,watch接口对外方法是Stop和Resultchan,前者关闭结果通道,后者获取通道。 func (r *Reflector) watchHandler(start time.Time, w watch.Interface, resourceVersion *string, errc chan error, stopCh <-chan struct{}) error { eventCount := 0 // Stopping the watcher should be idempotent and if we return from this function there's no way // we're coming back in with the same watch interface. defer w.Stop() loop: for { select { case <-stopCh: return errorStopRequested case err := <-errc: return err case event, ok := <-w.ResultChan(): if !ok { break loop } if event.Type == watch.Error { return apierrors.FromObject(event.Object) } if r.expectedType != nil { if e, a := r.expectedType, reflect.TypeOf(event.Object); e != a { utilruntime.HandleError(fmt.Errorf("%s: expected type %v, but watch event object had type %v", r.name, e, a)) continue } } // 判断期待的类型和监听到的事件类型是否一致 if r.expectedGVK != nil { if e, a := *r.expectedGVK, event.Object.GetObjectKind().GroupVersionKind(); e != a { utilruntime.HandleError(fmt.Errorf("%s: expected gvk %v, but watch event object had gvk %v", r.name, e, a)) continue } } // 获取事件对象 meta, err := meta.Accessor(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event)) continue } newResourceVersion := meta.GetResourceVersion() // 对事件类型进行判断,并进行对应操作 switch event.Type { case watch.Added: err := r.store.Add(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to add watch event object (%#v) to store: %v", r.name, event.Object, err)) } case watch.Modified: err := r.store.Update(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to update watch event object (%#v) to store: %v", r.name, event.Object, err)) } case watch.Deleted: // TODO: Will any consumers need access to the "last known // state", which is passed in event.Object? If so, may need // to change this. err := r.store.Delete(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to delete watch event object (%#v) from store: %v", r.name, event.Object, err)) } case watch.Bookmark: // 表示监听已在此处同步,只需更新 // A `Bookmark` means watch has synced here, just update the resourceVersion default: utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event)) } *resourceVersion = newResourceVersion r.setLastSyncResourceVersion(newResourceVersion) if rvu, ok := r.store.(ResourceVersionUpdater); ok { rvu.UpdateResourceVersion(newResourceVersion) } eventCount++ } } watchDuration := r.clock.Since(start) if watchDuration < 1*time.Second && eventCount == 0 { return fmt.Errorf("very short watch: %s: Unexpected watch close - watch lasted less than a second and no items received", r.name) } klog.V(4).Infof("%s: Watch close - %v total %v items received", r.name, r.expectedTypeName, eventCount) return nil } 4.8 relistResourceVersion relistResourceVersion 函数获得反射器 relist 的资源版本,如果资源版本非 0,则表示根据资源版本号继续获取,当传输过程中遇到网络故障或者其他原因导致中断,下次再连接时,会根据资源版本号继续传输未完成的部分。可以使本地缓存中的数据与Etcd集群中的数据保持一致,该函数实现如下所示: // 如果最后一次relist的结果是HTTP 410(Gone)状态码,则返回"",这样relist将通过quorum读取etcd中可用的最新资源版本。 // 返回使用 lastSyncResourceVersion,这样反射器就不会使用在relist结果或watch事件中watch到的资源版本更老的资源版本进行relist了 func (r *Reflector) relistResourceVersion() string { r.lastSyncResourceVersionMutex.RLock() defer r.lastSyncResourceVersionMutex.RUnlock() if r.isLastSyncResourceVersionUnavailable { // 因为反射器会进行分页List请求,如果 lastSyncResourceVersion 过期了,所有的分页列表请求就都会跳过 watch 缓存 // 所以设置 ResourceVersion="",然后再次 List,重新建立反射器到最新的可用资源版本,从 etcd 中读取,保持一致性。 return "" } if r.lastSyncResourceVersion == "" { // 反射器执行的初始 List 操作的时候使用0作为资源版本。 return "0" } return r.lastSyncResourceVersion } 五 整体流程 // k8s.io/client-go/informers/apps/v1/deployment.go // NewFilteredDeploymentInformer 为 Deployment 构造一个新的 Informer。 // 总是倾向于使用一个 informer 工厂来获取一个 shared informer,而不是获取一个独立的 informer,这样可以减少内存占用和服务器的连接数。 func NewFilteredDeploymentInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.AppsV1().Deployments(namespace).List(context.TODO(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.AppsV1().Deployments(namespace).Watch(context.TODO(), options) }, }, &appsv1.Deployment{}, resyncPeriod, indexers, ) } 从上面代码我们就可以看出来当我们去调用一个资源对象的 Informer() 的时候,就会去调用上面的 NewFilteredDeploymentInformer 函数进行初始化,而在初始化的使用就传入了 cache.ListWatch 对象,其中就有 List 和 Watch 的实现操作,也就是说前面反射器在 ListAndWatch 里面调用的 ListWatcher 的 List 操作是在一个具体的资源对象的 Informer 中实现的,比如我们这里就是通过的 ClientSet 客户端与 APIServer 交互获取到 Deployment 的资源列表数据的,通过在 ListFunc 中的 client.AppsV1().Deployments(namespace).List(context.TODO(), options) 实现的。 获取到了全量的 List 数据过后,通过 listMetaInterface.GetResourceVersion() 来获取资源的版本号,ResourceVersion(资源版本号)非常重要,Kubernetes 中所有的资源都拥有该字段,它标识当前资源对象的版本号,每次修改(CUD)当前资源对象时,Kubernetes API Server 都会更改 ResourceVersion,这样 client-go 执行 Watch 操作时可以根据ResourceVersion 来确定当前资源对象是否发生了变化。 然后通过 meta.ExtractList 函数将资源数据转换成资源对象列表,将 runtime.Object 对象转换成 []runtime.Object 对象,因为全量获取的是一个资源列表。 接下来是通过反射器的 syncWith 函数将资源对象列表中的资源对象和资源版本号存储在 Store 中,这个会在后面的章节中详细说明。 最后处理完成后通过 r.setLastSyncResourceVersion(resourceVersion) 操作来设置最新的资源版本号,其他的就是启动一个 goroutine 去定期检查是否需要执行 Resync 操作,调用存储中的 r.store.Resync() 来执行。 紧接着就是 Watch 操作了,Watch 操作通过 HTTP 协议与 APIServer 建立长连接,接收Kubernetes API Server 发来的资源变更事件,和 List 操作一样,Watch 的真正实现也是具体的 Informer 初始化的时候传入的,比如上面的 Deployment Informer 中初始化的时候传入的 WatchFunc,底层也是通过 ClientSet 客户端对 Deployment 执行 Watch 操作 client.AppsV1().Deployments(namespace).Watch(context.TODO(), options) 实现的。 获得 watch 的资源数据后,通过调用 r.watchHandler 来处理资源的变更事件,当触发Add 事件、Update 事件、Delete 事件时,将对应的资源对象更新到本地缓存(DeltaFIFO)中并更新 ResourceVersion 资源版本号。 这就是 Reflector 反射器中最核心的 ListAndWatch 实现,从上面的实现我们可以看出获取到的数据最终都流向了本地的 Store,也就是 DeltaFIFO,所以接下来我们需要来分析 DeltaFIFO 的实现。 六 小试牛刀 package main import ( "fmt" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/util/homedir" "path/filepath" "time" ) func Must(e interface{}) { if e != nil { panic(e) } } func InitClientSet() (*kubernetes.Clientset, error) { kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config") restConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig) if err != nil { return nil, err } return kubernetes.NewForConfig(restConfig) } // 生成listwatcher func InitListerWatcher(clientSet *kubernetes.Clientset, resource, namespace string, fieldSelector fields.Selector) cache.ListerWatcher { restClient := clientSet.CoreV1().RESTClient() return cache.NewListWatchFromClient(restClient, resource, namespace, fieldSelector) } // 生成pods reflector func InitPodsReflector(clientSet *kubernetes.Clientset, store cache.Store) *cache.Reflector { resource := "pods" namespace := "default" resyncPeriod := 0 * time.Second expectedType := &corev1.Pod{} lw := InitListerWatcher(clientSet, resource, namespace, fields.Everything()) return cache.NewReflector(lw, expectedType, store, resyncPeriod) } // 生成 DeltaFIFO func InitDeltaQueue(store cache.Store) cache.Queue { return cache.NewDeltaFIFOWithOptions(cache.DeltaFIFOOptions{ // store 实现了 KeyListerGetter KnownObjects: store, // EmitDeltaTypeReplaced 表示队列消费者理解 Replaced DeltaType。 // 在添加 `Replaced` 事件类型之前,对 Replace() 的调用的处理方式与 Sync() 相同。 // 出于向后兼容的目的,默认情况下为 false。 // 当为 true 时,将为传递给 Replace() 调用的项目发送“替换”事件。当为 false 时,将发送 `Sync` 事件。 EmitDeltaTypeReplaced: true, }) } func InitStore() cache.Store { return cache.NewStore(cache.MetaNamespaceKeyFunc) } func main() { clientSet, err := InitClientSet() Must(err) // 用于在processfunc中获取 store := InitStore() // queue DeleteFIFOQueue := InitDeltaQueue(store) // 生成podReflector podReflector := InitPodsReflector(clientSet, DeleteFIFOQueue) stopCh := make(chan struct{}) defer close(stopCh) go podReflector.Run(stopCh) //启动 ProcessFunc := func(obj interface{}) error { // 最先收到的事件会被最先处理 for _, d := range obj.(cache.Deltas) { switch d.Type { case cache.Sync, cache.Replaced, cache.Added, cache.Updated: if _, exists, err := store.Get(d.Object); err == nil && exists { if err := store.Update(d.Object); err != nil { return err } } else { if err := store.Add(d.Object); err != nil { return err } } case cache.Deleted: if err := store.Delete(d.Object); err != nil { return err } } pods, ok := d.Object.(*corev1.Pod) if !ok { return fmt.Errorf("not config: %T", d.Object) } fmt.Printf("Type:%s: Name:%s\n", d.Type, pods.Name) } return nil } fmt.Println("Start syncing...") wait.Until(func() { for { _, err := DeleteFIFOQueue.Pop(ProcessFunc) Must(err) } }, time.Second, stopCh) } 七 总结 通过本文,可以了解Reflector通过ListWatcher从Kubernetes API中获取对象的流程,以及存储到store中,后续会对DeltaFIFO进行源码研读,通过结合informer,来加深对整个informer的理解。 参考链接 https://blog.csdn.net/u013276277/article/details/108592288 https://www.jianshu.com/p/1daeae7b6970 点击关注,第一时间了解华为云新鲜技术~

优秀的个人博客,低调大师

跟我学k8s:Deployment 控制器

### 今日课程推荐: Kubernetes容器编排技术零基础入门和企业实践:https://edu.51cto.com/course/24884.html ### 开篇 今天我们来说说Deployment, Deployment一个非常重要的功能就是实现了 Pod 的“水平扩展/收缩”,比如我们应用更新了,我们只需要更新我们的容器镜像,然后修改 Deployment 里面的 Pod 模板镜像,那么 Deployment 就会用滚动更新(Rolling Update)的方式来升级现在的 Pod,这个能力是非常重要的,因为对于线上的服务我们需要做到不中断服务,所以滚动更新就成了必须的一个功能。我们可以通俗的理解就是每个 Deployment 就对应集群中的一次部署,这样就更好理解了。 ### Deployment Deployment 资源对象的格式和 ReplicaSet 几乎一致,如下资源对象就是一个常见的 Deployment 资源类型:(nginx-deploy.yaml) ``` apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy namespace: default spec: replicas: 3 # 期望的 Pod 副本数量,默认值为1 selector: # Label Selector,必须匹配 Pod 模板中的标签 matchLabels: app: nginx template: # Pod 模板 metadata: labels: app: nginx spec: containers: - name: nginx image: nginx ports: - containerPort: 80 ``` 我们这里只是将类型替换成了 Deployment,我们可以先来创建下这个资源对象: ``` $ kubectl apply -f nginx-deploy.yaml deployment.apps/nginx-deploy created $ kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE nginx-deploy 3/3 3 3 58s ``` 创建完成后,查看 Pod 状态: ``` $ kubectl get pods -l app=nginx NAME READY STATUS RESTARTS AGE nginx-deploy-85ff79dd56-7r76h 1/1 Running 0 41s nginx-deploy-85ff79dd56-d5gjs 1/1 Running 0 41s nginx-deploy-85ff79dd56-txc4h 1/1 Running 0 41s ``` 到这里我们发现和之前的 RS 对象是否没有什么两样,都是根据spec.replicas来维持的副本数量,我们随意查看一个 Pod 的描述信息: ``` $ kubectl describe pod nginx-deploy-85ff79dd56-txc4h Name: nginx-deploy-85ff79dd56-txc4h Namespace: default Priority: 0 PriorityClassName: Node: ydzs-node1/10.151.30.22 Start Time: Sat, 16 Nov 2019 16:01:25 +0800 Labels: app=nginx pod-template-hash=85ff79dd56 Annotations: podpreset.admission.kubernetes.io/podpreset-time-preset: 2062768 Status: Running IP: 10.244.1.166 Controlled By: ReplicaSet/nginx-deploy-85ff79dd56 ...... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled default-scheduler Successfully assigned default/nginx-deploy-85ff79dd56-txc4h to ydzs-node1 Normal Pulling 2m kubelet, ydzs-node1 Pulling image "nginx" Normal Pulled 117s kubelet, ydzs-node1 Successfully pulled image "nginx" Normal Created 117s kubelet, ydzs-node1 Created container nginx Normal Started 116s kubelet, ydzs-node1 Started container nginx ``` 我们仔细查看其中有这样一个信息Controlled By: ReplicaSet/nginx-deploy-85ff79dd56,什么意思?是不是表示当前我们这个 Pod 的控制器是一个 ReplicaSet 对象啊,我们不是创建的一个 Deployment 吗?为什么 Pod 会被 RS 所控制呢?那我们再去看下这个对应的 RS 对象的详细信息如何呢: ``` $ kubectl describe rs nginx-deploy-85ff79dd56 Name: nginx-deploy-85ff79dd56 Namespace: default Selector: app=nginx,pod-template-hash=85ff79dd56 Labels: app=nginx pod-template-hash=85ff79dd56 Annotations: deployment.kubernetes.io/desired-replicas: 3 deployment.kubernetes.io/max-replicas: 4 deployment.kubernetes.io/revision: 1 Controlled By: Deployment/nginx-deploy Replicas: 3 current / 3 desired Pods Status: 3 Running / 0 Waiting / 0 Succeeded / 0 Failed ...... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulCreate 4m52s replicaset-controller Created pod: nginx-deploy-85ff79dd56-7r76h Normal SuccessfulCreate 4m52s replicaset-controller Created pod: nginx-deploy-85ff79dd56-d5gjs Normal SuccessfulCreate 4m52s replicaset-controller Created pod: nginx-deploy-85ff79dd56-txc4h ``` 其中有这样的一个信息:Controlled By: Deployment/nginx-deploy,明白了吧?意思就是我们的 Pod 依赖的控制器 RS 实际上被我们的 Deployment 控制着呢,我们可以用下图来说明 Pod、ReplicaSet、Deployment 三者之间的关系: ![deployment.png](https://s2.51cto.com/images/20210610/1623293371427285.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 通过上图我们可以很清楚的看到,定义了3个副本的 Deployment 与 ReplicaSet 和 Pod 的关系,就是一层一层进行控制的。ReplicaSet 作用和之前一样还是来保证 Pod 的个数始终保存指定的数量,所以 Deployment 中的容器 restartPolicy=Always 是唯一的就是这个原因,因为容器必须始终保证自己处于 Running 状态,ReplicaSet 才可以去明确调整 Pod 的个数。而 Deployment 是通过管理 ReplicaSet 的数量和属性来实现水平扩展/收缩以及滚动更新两个功能的。 ### 水平伸缩 水平扩展/收缩的功能比较简单,因为 ReplicaSet 就可以实现,所以 Deployment 控制器只需要去修改它缩控制的 ReplicaSet 的 Pod 副本数量就可以了。比如现在我们把 Pod 的副本调整到 4 个,那么 Deployment 所对应的 ReplicaSet 就会自动创建一个新的 Pod 出来,这样就水平扩展了,我们可以使用一个新的命令 kubectl scale 命令来完成这个操作: ``` $ kubectl scale deployment nginx-deploy --replicas=4 deployment.apps/nginx-deployment scaled ``` 扩展完成后可以查看当前的 RS 对象: ``` $ kubectl get rs NAME DESIRED CURRENT READY AGE nginx-deploy-85ff79dd56 4 4 3 40m ``` 可以看到期望的 Pod 数量已经变成 4 了,只是 Pod 还没准备完成,所以 READY 状态数量还是 3,同样查看 RS 的详细信息: ``` $ kubectl describe rs nginx-deploy-85ff79dd56 Name: nginx-deploy-85ff79dd56 Namespace: default Selector: app=nginx,pod-template-hash=85ff79dd56 ...... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulCreate 40m replicaset-controller Created pod: nginx-deploy-85ff79dd56-7r76h Normal SuccessfulCreate 40m replicaset-controller Created pod: nginx-deploy-85ff79dd56-d5gjs Normal SuccessfulCreate 40m replicaset-controller Created pod: nginx-deploy-85ff79dd56-txc4h Normal SuccessfulCreate 17s replicaset-controller Created pod: nginx-deploy-85ff79dd56-tph9g ``` 可以看到 ReplicaSet 控制器增加了一个新的 Pod,同样的 Deployment 资源对象的事件中也可以看到完成了扩容的操作: ``` $ kubectl describe deploy nginx-deploy Name: nginx-deploy Namespace: default ...... OldReplicaSets: NewReplicaSet: nginx-deploy-85ff79dd56 (4/4 replicas created) Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ScalingReplicaSet 43m deployment-controller Scaled up replica set nginx-deploy-85ff79dd56 to 3 Normal ScalingReplicaSet 3m16s deployment-controller Scaled up replica set nginx-deploy-85ff79dd56 to 4 ``` ### 滚动更新 如果只是水平扩展/收缩这两个功能,就完全没必要设计 Deployment 这个资源对象了,Deployment 最突出的一个功能是支持滚动更新,比如现在我们需要把应用容器更改为 nginx:1.21.0 版本,修改后的资源清单文件如下所示: ``` apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy namespace: default spec: replicas: 3 selector: matchLabels: app: nginx minReadySeconds: 5 strategy: type: RollingUpdate # 指定更新策略:RollingUpdate和Recreate rollingUpdate: maxSurge: 1 maxUnavailable: 1 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.21.0 ports: - containerPort: 80 ``` 后前面相比较,除了更改了镜像之外,我们还指定了更新策略: ``` minReadySeconds: 5 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 1 ``` 1. minReadySeconds:表示 Kubernetes 在等待设置的时间后才进行升级,如果没有设置该值,Kubernetes 会假设该容器启动起来后就提供服务了,如果没有设置该值,在某些极端情况下可能会造成服务不正常运行,默认值就是0。 2. type=RollingUpdate:表示设置更新策略为滚动更新,可以设置为Recreate和RollingUpdate两个值,Recreate表示全部重新创建,默认值就是RollingUpdate。 3. maxSurge:表示升级过程中最多可以比原先设置多出的 Pod 数量,例如:maxSurage=1,replicas=5,就表示Kubernetes 会先启动一个新的 Pod,然后才删掉一个旧的 Pod,整个升级过程中最多会有5+1个 Pod。 4. maxUnavaible:表示升级过程中最多有多少个 Pod 处于无法提供服务的状态,当maxSurge不为0时,该值也不能为0,例如:maxUnavaible=1,则表示 Kubernetes 整个升级过程中最多会有1个 Pod 处于无法服务的状态。 现在我们来直接更新上面的 Deployment 资源对象: ``` $ kubectl apply -f nginx-deploy.yaml ``` **record 参数** 我们可以添加了一个额外的 --record 参数来记录下我们的每次操作所执行的命令,以方便后面查看。 更新后,我们可以执行下面的 kubectl rollout status 命令来查看我们此次滚动更新的状态: ``` $ kubectl rollout status deployment/nginx-deploy Waiting for deployment "nginx-deploy" rollout to finish: 2 out of 3 new replicas have been updated... ``` 从上面的信息可以看出我们的滚动更新已经有两个 Pod 已经更新完成了,在滚动更新过程中,我们还可以执行如下的命令来暂停更新: ``` $ kubectl rollout pause deployment/nginx-deploy deployment.apps/nginx-deploy paused ``` 这个时候我们的滚动更新就暂停了,此时我们可以查看下 Deployment 的详细信息: ``` $ kubectl describe deploy nginx-deploy Name: nginx-deploy Namespace: default CreationTimestamp: Sat, 16 Nov 2019 16:01:24 +0800 Labels: Annotations: deployment.kubernetes.io/revision: 2 kubectl.kubernetes.io/last-applied-configuration: {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"name":"nginx-deploy","namespace":"default"},"spec":{"minReadySec... Selector: app=nginx Replicas: 3 desired | 2 updated | 4 total | 4 available | 0 unavailable StrategyType: RollingUpdate MinReadySeconds: 5 RollingUpdateStrategy: 1 max unavailable, 1 max surge ...... OldReplicaSets: nginx-deploy-85ff79dd56 (2/2 replicas created) NewReplicaSet: nginx-deploy-5b7b9ccb95 (2/2 replicas created) Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ScalingReplicaSet 26m deployment-controller Scaled up replica set nginx-deploy-85ff79dd56 to 4 Normal ScalingReplicaSet 3m44s deployment-controller Scaled down replica set nginx-deploy-85ff79dd56 to 3 Normal ScalingReplicaSet 3m44s deployment-controller Scaled up replica set nginx-deploy-5b7b9ccb95 to 1 Normal ScalingReplicaSet 3m44s deployment-controller Scaled down replica set nginx-deploy-85ff79dd56 to 2 Normal ScalingReplicaSet 3m44s deployment-controller Scaled up replica set nginx-deploy-5b7b9ccb95 to 2 Deployment RollingUpdate ``` ![deploymentrollupdate.png](https://s2.51cto.com/images/20210610/1623293719449640.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) 我们仔细观察 Events 事件区域的变化,上面我们用 kubectl scale 命令将 Pod 副本调整到了 4,现在我们更新的时候是不是声明又变成 3 了,所以 Deployment 控制器首先是将之前控制的 nginx-deploy-85ff79dd56 这个 RS 资源对象进行缩容操作,然后滚动更新开始了,可以发现 Deployment 为一个新的 nginx-deploy-5b7b9ccb95 RS 资源对象首先新建了一个新的 Pod,然后将之前的 RS 对象缩容到 2 了,再然后新的 RS 对象扩容到 2,后面由于我们暂停滚动升级了,所以没有后续的事件了,大家有看明白这个过程吧?这个过程就是滚动更新的过程,启动一个新的 Pod,杀掉一个旧的 Pod,然后再启动一个新的 Pod,这样滚动更新下去,直到全都变成新的 Pod,这个时候系统中应该存在 4 个 Pod,因为我们设置的策略maxSurge=1,所以在升级过程中是允许的,而且是两个新的 Pod,两个旧的 Pod: ``` $ kubectl get pods -l app=nginx NAME READY STATUS RESTARTS AGE nginx-deploy-5b7b9ccb95-k6pkh 1/1 Running 0 11m nginx-deploy-5b7b9ccb95-l6lmx 1/1 Running 0 11m nginx-deploy-85ff79dd56-7r76h 1/1 Running 0 75m nginx-deploy-85ff79dd56-txc4h 1/1 Running 0 75m ``` 查看 Deployment 的状态也可以看到当前的 Pod 状态: ``` $ kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE nginx-deploy 4/3 2 4 75m ``` 这个时候我们可以使用kubectl rollout resume来恢复我们的滚动更新: ``` $ kubectl rollout resume deployment/nginx-deploy deployment.apps/nginx-deploy resumed $ kubectl rollout status deployment/nginx-deploy Waiting for deployment "nginx-deploy" rollout to finish: 2 of 3 updated replicas are available... deployment "nginx-deploy" successfully rolled out ``` 看到上面的信息证明我们的滚动更新已经成功了,同样可以查看下资源状态: ``` $ kubectl get pod -l app=nginx NAME READY STATUS RESTARTS AGE nginx-deploy-5b7b9ccb95-gmq7v 1/1 Running 0 115s nginx-deploy-5b7b9ccb95-k6pkh 1/1 Running 0 15m nginx-deploy-5b7b9ccb95-l6lmx 1/1 Running 0 15m $ kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE nginx-deploy 3/3 3 3 79m ``` 这个时候我们查看 ReplicaSet 对象,可以发现会出现两个: ``` $ kubectl get rs -l app=nginx NAME DESIRED CURRENT READY AGE nginx-deploy-5b7b9ccb95 3 3 3 18m nginx-deploy-85ff79dd56 0 0 0 81m ``` 从上面可以看出滚动更新之前我们使用的 RS 资源对象的 Pod 副本数已经变成 0 了,而滚动更新后的 RS 资源对象变成了 3 个副本,我们可以导出之前的 RS 对象查看: ``` $ kubectl get rs nginx-deploy-85ff79dd56 -o yaml apiVersion: apps/v1 kind: ReplicaSet metadata: annotations: deployment.kubernetes.io/desired-replicas: "3" deployment.kubernetes.io/max-replicas: "4" deployment.kubernetes.io/revision: "1" creationTimestamp: "2019-11-16T08:01:24Z" generation: 5 labels: app: nginx pod-template-hash: 85ff79dd56 name: nginx-deploy-85ff79dd56 namespace: default ownerReferences: - apiVersion: apps/v1 blockOwnerDeletion: true controller: true kind: Deployment name: nginx-deploy uid: b0fc5614-ef58-496c-9111-740353bd90d4 resourceVersion: "2140545" selfLink: /apis/apps/v1/namespaces/default/replicasets/nginx-deploy-85ff79dd56 uid: 8eca2998-3610-4f80-9c21-5482ba579892 spec: replicas: 0 selector: matchLabels: app: nginx pod-template-hash: 85ff79dd56 template: metadata: creationTimestamp: null labels: app: nginx pod-template-hash: 85ff79dd56 spec: containers: - image: nginx imagePullPolicy: Always name: nginx ports: - containerPort: 80 protocol: TCP resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File dnsPolicy: ClusterFirst restartPolicy: Always schedulerName: default-scheduler securityContext: {} terminationGracePeriodSeconds: 30 status: observedGeneration: 5 replicas: 0 ``` 我们仔细观察这个资源对象里面的描述信息除了副本数变成了replicas=0之外,和更新之前没有什么区别吧?大家看到这里想到了什么?有了这个 RS 的记录存在,是不是我们就可以回滚了啊?而且还可以回滚到前面的任意一个版本,这个版本是如何定义的呢?我们可以通过命令 rollout history 来获取: ``` $ kubectl rollout history deployment nginx-deploy deployment.apps/nginx-deploy REVISION CHANGE-CAUSE 1 2 ``` 其实 rollout history 中记录的 revision 是和 ReplicaSets 一一对应。如果我们手动删除某个 ReplicaSet,对应的rollout history就会被删除,也就是说你无法回滚到这个revison了,同样我们还可以查看一个revison的详细信息: ``` $ kubectl rollout history deployment nginx-deploy --revision=1 deployment.apps/nginx-deploy with revision #1 Pod Template: Labels: app=nginx pod-template-hash=85ff79dd56 Containers: nginx: Image: nginx Port: 80/TCP Host Port: 0/TCP Environment: Mounts: Volumes: ``` 假如现在要直接回退到当前版本的前一个版本,我们可以直接使用如下命令进行操作: ``` $ kubectl rollout undo deployment nginx-deploy ``` 当然也可以回退到指定的revision版本: ``` $ kubectl rollout undo deployment nginx-deploy --to-revision=1 deployment "nginx-deploy" rolled back ``` 回滚的过程中我们同样可以查看回滚状态: ``` $ kubectl rollout status deployment/nginx-deploy Waiting for deployment "nginx-deploy" rollout to finish: 1 old replicas are pending termination... Waiting for deployment "nginx-deploy" rollout to finish: 1 old replicas are pending termination... Waiting for deployment "nginx-deploy" rollout to finish: 1 old replicas are pending termination... Waiting for deployment "nginx-deploy" rollout to finish: 2 of 3 updated replicas are available... Waiting for deployment "nginx-deploy" rollout to finish: 2 of 3 updated replicas are available... deployment "nginx-deploy" successfully rolled out ``` 这个时候查看对应的 RS 资源对象可以看到 Pod 副本已经回到之前的 RS 里面去了。 ``` $ kubectl get rs -l app=nginx NAME DESIRED CURRENT READY AGE nginx-deploy-5b7b9ccb95 0 0 0 31m nginx-deploy-85ff79dd56 3 3 3 95m ``` 不过需要注意的是回滚的操作滚动的revision始终是递增的: ``` $ kubectl rollout history deployment nginx-deploy deployment.apps/nginx-deploy REVISION CHANGE-CAUSE 2 3 ``` ### 保留旧版本 在很早之前的 Kubernetes 版本中,默认情况下会为我们暴露下所有滚动升级的历史记录,也就是 ReplicaSet 对象,但一般情况下没必要保留所有的版本,毕竟会存在 etcd 中,我们可以通过配置 spec.revisionHistoryLimit 属性来设置保留的历史记录数量,不过新版本中该值默认为 10,如果希望多保存几个版本可以设置该字段。

优秀的个人博客,低调大师

GitOps初阶指南:将DevOps扩展至K8S

本文转自Rancher Labs 在过去十年的编程中,出现了一些革命性的转变。其中之一是源于围绕DevOps的实践,它将开发和运维团队整合到一个共享的工作流程中,此外还有持续集成和持续交付(CI/CD),通过CI/CD,Devops团队可以向代码库提供持续的更新。另一个变革来自于从单体代码库到基于云的微服务的迁移,这些微服务运行在由Kubernetes等编排平台管理的容器中。 即使有Kubernetes这样的平台来编排协调,在集群系统或云端运行的基于容器的应用程序依旧可能是复杂的、难以调配和管理的。GitOps是一套新兴的实践,旨在通过应用Devops和CI/CD世界的技术来简化这一管理任务。 GitOps的关键是基础设施即代码(IaC)的理念,它采用与DevOps用于提供应用程序一样的方法来提供基础设施。所以,不仅是应用,还有底层的主机和网络都被描述在文件中,这些文件可以像版本控制系统中的其他代码一样,然后由自动化流程来将现实世界的应用与这些文件中描述的应用进行融合。 用GitOps的说法,版本控制系统中的代码是关于应用在生产中应该是什么样子的唯一真相来源(single source of truth)。 定义GitOps Weaveworks是在GitOps概念普及方面贡献最大的公司。稍后我们会详细介绍Weaveworks在其中扮演的角色,但首先,我们先来看看该公司对GitOps的定义,它有两个方面: Kubernetes和其他云原生技术的运维模式,为统一部署、管理和监控容器化集群和应用提供了一套最佳实践。 GitOps是一条通往管理应用的开发者体验之路;在这里,端到端的CI/CD流水线和Git workflow可以同时应用于运维和开发。 换句话说,GitOps是一套特定的实践,旨在管理Kubernetes和类似的平台。随着越来越多的开发团队采用DevOps实践,并将代码迁移到云端,GitOps也将会适合更广泛的应用。但要了解GitOps的秘诀和它所能解决的问题,我们需要谈谈它的组成部分。 Git定义 在GitOps中Git指的是由Linus Torvalds在2005年开发的极为流行的分布式版本控制系统。Git是一个工具,它允许开发者团队在一个应用程序代码库上共同工作,存储各种代码分支,在将它们合并到生产代码之前,他们可以对这些代码进行修补。Git 的一个关键概念是拉取请求,即开发人员正式要求将他们正在编写的一些代码整合到代码库的另一个分支中。 Git 拉取请求为团队成员提供了一个协作和讨论的机会,然后再就是否应该将新代码添加到应用程序中达成共识。Git 还会存储旧版本的代码,如果出了问题,可以很容易地回滚到上一个好的版本,并可以让你快速查看两次修改之间的变化。Git 最为人所知的部分可能是作为GitHub 这一云端托管版本控制系统的底层,但 Git 本身也是一个开源软件,可以部署在任何地方,无论是公司内部的服务器还是你的PC。 需要注意的是,虽然我们通常认为Git是一个计算机编程工具,但实际上取决于你如何使用它。Git 很乐意将任何文本文件作为你的 “代码库”,例如,它可以被作者用来记录合作作品的编辑情况。这一点很重要,因为GitOps的核心代码库大多由声明式配置文件而非可执行代码组成。 在我们继续之前,最后要强调一件事——尽管名字中就有 “Git”,但GitOps实际上并不必要使用Git。 已经投入使用其他版本控制软件(如Subversion)的团队也可以实现GitOps。但在Devops领域,Git被广泛用于实现CI/CD,所以大多数GitOps项目最终都会使用Git。 什么是CI/CD流程? 关于CI/CD的完整解释其实不在本文讨论的范围内,但是因为CI/CD是 GitOps 工作的核心,因此我们需要对其进行简单的介绍。CI/CD中的一半持续集成是由版本控制仓库(如Git)实现的。开发者可以对代码库进行持续的小改进,而不是每隔几个月或几年就推出巨大的、单一的新版本。持续部署这一块是通过被称为流水线(pipeline)的自动化系统来实现的,这些流水线可以构建、测试和部署新的代码到生产中。 同样,我们在这里一直在谈论代码,这通常会让人联想到用C语言、Java或JavaScript等编程语言编写的可执行代码。但在GitOps中,我们所管理的 “代码” 主要是由配置文件组成的。这不是一个小细节,而是GitOps工作的核心。正如我们所说,这些配置文件是描述我们的系统应该是什么样子的 “唯一真理来源(single source of truth)”。它们是声明式的,而不是指导性的。这意味着,配置文件不会说 “启动十台服务器”,而会简单地说 “这个系统包括十台服务器”。 GitOps方程中的CI那一半允许开发人员快速推出对这些配置文件的调整和改进;当自动化软件代理竭尽全力确保应用程序的实时版本能够反映配置文件中的描述时,CD这一部分会以GitOps语言趋向于声明式模型。 GitOps和Kubernetes 正如我们所提到的,GitOps的概念最初是围绕管理Kubernetes应用而出现的。通过我们现在对GitOps的了解,让我们重温一下Weaveworks的GitOps讨论,看看他们是如何描述如何对基于GitOps原则管理的Kubernetes进行更新的。下面是对整个流程的总结: 一个开发者为一个新功能提出Git 拉取请求。 审查和批准代码,然后将其合并到主代码库中。 合并会触发 CI/CD 流水线、自动测试和重建新代码,并将其部署到仓库。 软件代理注意到更新,从仓库中提取新代码,并更新配置仓库中的配置文件(用YAML编写)。 Kubernetes集群中的软件代理根据配置文件,检测到集群已经过时,拉取更改,并部署新功能。 Weaveworks和GitOps 显然,这里的第4步和第5步做了很多繁重的工作。将Git仓库中的 "真理来源 "与现实世界中的Kubernetes应用进行神奇同步的软件代理,就是让GitOps成为可能的魔法。正如我们所说,在GitOps术语中,让实时系统更像配置文件中描述的理想系统的过程叫做融合。当实时系统和理想系统不同步时,那就是分歧。理想情况下,融合可以通过自动化流程来实现,但自动化所能做的事情是有限度的,有时人工干预是必要的。 我们在这里用通用术语描述了这个过程,但事实上,如果你真的去看Weaveworks的页面,我们提到的 “软件代理” 是该公司Weave Cloud平台的一部分。“GitOps” 这个词是由Weaveworks的CEO Alexis Richardson创造的,它的部分作用是让Weaveworks平台对已经沉浸在DevOps和CI/CD世界的开发者有吸引力。 但Weaveworks从未宣称自己垄断了GitOps,GitOps更多的是一种理念和一套最佳实践,而不是某种具体的产品。 正如提供CI/CD解决方案的公司CloudBees的博客所指出的那样,GitOps代表了一种开放的、厂商中立的模式,它是针对亚马逊、谷歌和微软等大型云厂商推出的管理型专有Kubernetes解决方案而开发的。CloudBees提供了自己的GitOps解决方案,这个领域的另一些玩家也是如此。 GitOps和DevOps Atlassian是一家为敏捷开发者制造了许多工具的公司,它有一篇关于GitOps的历史和目的的深度博文(https://www.atlassian.com/git/tutorials/gitops ),值得你花时间去了解。在他们看来,GitOps代表了作为devops的理念的逻辑延伸。具体来说,GitOps是对基础架构即代码(IaC)这一概念的阐述,而基础架构本身就是DevOps环境下产生的一种思想。在Atlassian看来,GitOps弥补了现有DevOps技术与分布式、云托管应用的特殊需求之间的关键差距,现有DevOps技术是为了解决系统管理问题而发展起来的。各个云厂商提供的自动融合是GitOps的特别之处。 虽然GitOps今天仍然专注于Kubernetes,但我们希望我们已经明确了它如何适用于更广泛的分布式、基于云的应用世界。开源安全厂商WhiteSource的一篇博文概述了GitOps的优势: 可观察性:GitOps系统为复杂的应用提供了监控、日志、跟踪和可视化功能,因此开发人员可以看到什么地方出现了故障,在哪里出现了故障。 版本控制和变更管理:很明显,这是使用Git这样的版本控制系统的一个关键优势。有缺陷的更新可以轻松回滚。 易于采用:GitOps建立在许多开发人员已经掌握的开发技能之上。 提高生产力:GitOps 可以像开发项目和 CI/CD 那样提高工作效率。 审计:有了Git,每一个操作都可以追踪到一个特定的提交,这样就可以很容易地追踪到错误的原因。 即使你不使用Kubernetes,GitOps也很有可能迟早会成为你工作流程的一部分。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册