成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

九張圖帶你理解 Kubernetes Controller 工作機制

系統 Linux
此文就是通過對代碼的簡單分析,以及一些經驗總結,來描述 k8s controller 管理資源的主要流程。

圖片

一、Introduction

起因:工作上要重構一個現有的組件管理工具,要求實現全生命周期管理,還有上下游解耦,我心里一想這不就是 k8s controller 嘛!所以決定在動手前先學習一下 k8s 的先進理念。

此文就是通過對代碼的簡單分析,以及一些經驗總結,來描述 k8s controller 管理資源的主要流程。

二、Concepts

resource: 資源,k8s 中定義的每一個實例都是一個資源,比如一個 rs、一個 deployment。資源有不同的 kind,比如 rs、deployment。資源間存在上下游關系。

注意:下文中提到的所有“資源”,都是指 k8s 中抽象的資源聲明,而不是指 CPU、存儲等真實的物理資源。

高度抽象 k8s 的話其實就三大件:

  • apiserver: 負責存儲資源,并且提供查詢、修改資源的接口
  • controller: 負責管理本級和下級資源。比如 deploymentController 就負責管理 deployment 資源 和下一級的 rs 資源。
  • kubelet: 安裝在 node 節點上,負責部署資源

controller 和 kubelet 都只和 apiserver 通訊。controller 不斷監聽本級資源,然后修改下級資源的聲明。kubelet 查詢當前 node 所應部署的資源,然后執行清理和部署。

圖片

1、術語

  • ??metadata??: 每一個資源都有的元數據,包括 label、owner、uid 等
  • ??UID??: 每一個被創建(提交給 apiserver)的資源都有一個全局唯一的 UUID。
  • ??label??: 每個資源都要定義的標簽
  • ??selector??: 父資源通過 labelSelector 查詢歸其管理的子資源。不允許指定空 selector(全匹配)。
  • owner: 子資源維護一個 owner UID 的列表??OwnerReferences??, 指向其父級資源。列表中第一個有效的指針會被視為生效的父資源。selector 實際上只是一個 adoption 的機制, 真實起作用的父子級關系是靠 owner 來維持的, 而且 owner 優先級高于 selector。
  • replicas: 副本數,pod 數
  • 父/子資源的相關:
  • orphan: 沒有 owner 的資源(需要被 adopt 或 GC)
  • adopt: 將 orphan 納入某個資源的管理(成為其 owner)
  • match: 父子資源的 label/selector 匹配
  • release: 子資源的 label 不再匹配父資源的 selector,將其釋放
  • RS 相關:
  • saturated: 飽和,意指某個資源的 replicas 已符合要求
  • surge: rs 的 replicas 不能超過 spec.replicas + surge
  • proportion: 每輪 rolling 時,rs 的變化量(小于 maxSurge)
  • fraction: scale 時 rs 期望的變化量(可能大于 maxSurge)

三、Controller

  • sample-controller@a40ea2c/controller.go
  • kubernetes@59c0523b/pkg/controller/deployment/deployment_controller.go
  • kubernetes@59c0523b/pkg/controller/controller_ref_manager.go

控制器,負責管理自己所對應的資源(resource),并創建下一級資源,拿 deployment 來說:

  1. 用戶創建 deployment 資源
  2. deploymentController 監聽到 deployment 資源,然后創建 rs 資源
  3. rsController 監聽到 rs 資源,然后創建 pod 資源
  4. 調度器(scheduler)監聽到 pod 資源,將其與 node 資源建立關聯

(node 資源是 kubelet 安裝后上報注冊的)

圖片

理想中,每一層管理器只管理本級和子兩層資源。但因為每一個資源都是被上層創建的, 所以實際上每一層資源都對下層資源的定義有完全的了解,即有一個由下至上的強耦合關系。

比如 ??A -> B -> C -> D?? 這樣的生成鏈,A 當然是知道 D 資源的全部定義的, 所以從理論上說,A 是可以去獲取 D 的。但是需要注意的是,如果出現了跨級的操作,A 也只能只讀的獲取 D,而不要對 D 有任何改動, 因為跨級修改數據的話會干擾下游的控制器。

k8s 中所有的控制器都在同一個進程(controller-manager)中啟動, 然后以 goroutine 的形式啟動各個不同的 controller。所有的 contorller 共享同一個 informer,不過可以注冊不同的 filter 和 handler,監聽自己負責的資源的事件。

(informer 是對 apiserver 的封裝,是 controller 查詢、訂閱資源消息的組件,后文有介紹)注:如果是用戶自定義 controller(CRD)的話,需要以單獨進程的形式啟動,需要自己另行實例化一套 informer, 不過相關代碼在 client-go 這一項目中都做了封裝,編寫起來并不會很復雜。

控制器的核心代碼可以概括為:

for {
for {
// 從 informer 中取出訂閱的資源消息
key, empty := queue.Get()
if empty {
break
}

defer queue.Done(key)

// 處理這一消息:更新子資源的聲明,使其匹配父資源的要求。
// 所有的 controller 中,這一函數都被命名為 `syncHandler`。
syncHandler(key)
}

// 消息隊列被消費殆盡,等待下一輪運行
time.sleep(time.Second)
}
  1. 通過 informer(indexer)監聽資源事件,事件的格式是字符串??<namespace>/<name>??
  2. 控制器通過 namespace 和 name 去查詢自己負責的資源和下級資源
  3. 比對當前資源聲明的狀態和下級資源可用的狀態是否匹配,并通過增刪改讓下級資源匹配上級聲明。比如 deployments 控制器就查詢 deployment 資源和 rs 資源,并檢驗其中的 replicas 副本數是否匹配。

controller 內包含幾個核心屬性/方法:

  • informer: sharedIndexer,用于獲取資源的消息,支持注冊 Add/Update/Delete 事件觸發,或者調用??lister?? 遍歷。
  • clientset: apiserver 的客戶端,用來對資源進行增刪改。
  • syncHandler: 執行核心邏輯代碼(更新子資源的聲明,使其匹配父資源的要求)。

1、syncHandler

syncHandler 像是一個約定,所有的 controller 內執行核心邏輯的函數都叫這個名字。該函數負責處理收到的資源消息,比如更新子資源的聲明,使其匹配父資源的要求。

以 deploymentController 為例,當收到一個事件消息,syncHandler 被調用后:

注:

  • ??de??: 觸發事件的某個 deployment 資源
  • ??dc??: deploymentController 控制器自己
  • ??rs??: replicaset,deployment 對應的 replicaset 子資源

注:事件是一個字符串,形如 ??namespace/name??,代表觸發事件的資源的名稱以及所在的 namespace。因為事件只是個名字,所以 syncHandler 需要自己去把當前觸發的資源及其子資源查詢出來。這里面涉及很多查詢和遍歷,不過這些查詢都不會真實的調用 apiserver,而是在 informer 的內存存儲里完成的。

graph TD

A1[將 key 解析為 namespace 和 name] --> A2[查詢 de]
A2 --> A3[查詢關聯子資源 rs]
A3 --> A31{de 是否 paused}
A31 --> |yes| A32[調用 dc.sync 部署 rs]
A31 --> |no| A4{是否設置了 rollback}
A4 --> |yes| A41[按照 annotation 設置執行 rollback]
A4 --> |no| A5[rs 是否匹配 de 聲明]
A5 --> |no| A32
A5 --> |yes| A6{de.spec.strategy.type}
A6 --> |recreate| A61[dc.rolloutRecreate]
A6 --> |rolling| A62[dc.rolloutRolling]

圖片

查詢關聯子資源

  • kubernetes@59c0523b/pkg/controller/deployment/deployment_controller.go:getReplicaSetsForDeployment

k8s 中,資源間可以有上下級(父子)關系。

理論上 每一個 controller 都負責創建當前資源和子資源,父資源通過 labelSelector 查詢應該匹配的子資源。

一個 deployment 的定義:

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80

上文中講到 syncHandler 的時候,提到需要“查詢關聯子資源”。其實這一步驟很復雜,不僅僅是查詢,還包含對現有子資源進行同步(修改)的操作。簡而言之,這一步驟實際上做的是通過對 owner、label 的比對,確認并更新當前真實的父子資源關系。

對用戶呈現的資源關聯是依靠 label/selector。但實際上 k8s 內部使用的是 owner 指針。(owner 指針是資源 metadata 內用來標記其父資源的 OwnerReferences)。

查詢匹配子資源的方法是:

  1. 遍歷 namespace 內所有對應類型的子資源 (比如 deployment controller 就會遍歷所有的 rs)
  2. 匹配校驗 owner 和 label

(父是當前操作的資源,子是查詢出的子資源)

還是用 deployment 舉例,比如此時收到了一個 deployment 事件,需要查詢出該 de 匹配的所有 rs:

graph LR

A(遍歷 namespace 內所有 rs) --> A1{子.owner == nil}
A1 --> |false| A2{子.owner == 父.uid}
A2 --> |false| A21[skip]
A2 --> |true| A3{labels matched}
A3 --> |true| A5
A3 --> |false| A31[release]
A1 --> |true| A4{labels matched}
A4 --> |false| A21
A4 --> |true| A41[adopt]
A41 --> A5[標記為父子]

圖片

如上圖所示,其實只有兩個 case 下,rs 會被視為是 de 的子資源:

  1. rs owner 指向 de,且 labels 匹配
  2. rs owner 為空,且 labels 匹配

注意:如果 rs owner 指向了其他父資源,即使 label 匹配,也不會被視為當前 de 的子資源

dc.sync

  • kubernetes@59c0523b/pkg/controller/deployment/sync.go:sync

這是 deployment controller 中執行“檢查和子資源,令其匹配父資源聲明”的一步。準確的說:

  1. dc.sync: 檢查子資源是否符合父資源聲明
  2. dc.scale: 操作更新子資源,使其符合父資源聲明
graph TD

A1[查詢 de 下所有舊的 rs] --> A2{當前 rs 是否符合 de}
A2 --> |no| A21[newRS = nil]
A2 --> |yes| A22[NewRS = 當前 rs]
A22 --> A23[將 de 的 metadata 拷貝給 newRS]
A23 --> A231[newRS.revision=maxOldRevision+1]
A231 --> A3[調用 dc.scale]
A21 --> A33
A3 --> A31{是否有 active/latest rs}
A31 --> |yes| A311[dc.scaleReplicaSet 擴縮容]
A31 --> |no| A32{newRS 是否已飽和}
A32 --> |yes|A321[把所有 oldRS 清零]
A32 --> |no|A33{de 是否允許 rolling}
A33 --> |no|A331[return]
A33 --> |yes|A34[執行滾動更新]

圖片

滾動更新的流程為:

(??if deploymentutil.IsRollingUpdate(deployment) {...}?? 內的大量代碼,實際做的事情就是按照 deployment 的要求更新 rs 的 replicas 數。不過每次變更都涉及到對 rs 和 deployment 的 maxSurge 的檢查,所以代碼較為復雜。)

  1. 計算所有 RS replicas 總和??allRSsReplicas??。
  2. 計算滾動升級過程中最多允許出現的副本數??allowedSize???。??allowedSize = de.Spec.Replicas + maxSurge??
  3. ??deploymentReplicasToAdd = allowedSize - allRSsReplicas??
  4. 遍歷所有當前 rs,計算每一個 rs 的 replicas 變化量(proportion), 計算的過程中需要做多次檢查,不能溢出 rs 和 deployment 的 maxSurge。
  5. 更新所有 rs 的 replicas,然后調用??dc.scaleReplicaSet?? 提交更改。

四、Object

  • apimachinery@v0.0.0-20210708014216-0dafcb48b31e/pkg/apis/meta/v1/meta.go
  • apimachinery@v0.0.0-20210708014216-0dafcb48b31e/pkg/apis/meta/v1/types.go

ObjectMeta 定義了 k8s 中資源對象的標準方法。

雖然 resource 定義里是通過 labelSelector 建立從上到下的關聯, 但其實內部實現的引用鏈是從下到上的。每一個資源都會保存一個 Owner UID 的 slice。

每個資源的 metadata 中都有一個 ??ownerReferences?? 列表,保存了其父資源(遍歷時遇到的第一個有效的資源會被認為是其父資源)。

type ObjectMeta struct {
OwnerReferences []OwnerReference `json:"ownerReferences,omitempty" patchStrategy:"merge" patchMergeKey:"uid" protobuf:"bytes,13,rep,name=ownerReferences"`
}

判斷 owner 靠的是比對資源的 UID

func IsControlledBy(obj Object, owner Object) bool {
ref := GetControllerOfNoCopy(obj)
if ref == nil {
return false
}

// 猜測:UID 是任何資源在 apiserver 注冊的時候,由 k8s 生成的 uuid
return ref.UID == owner.GetUID()
}

五、Informer

  • A deep dive into Kubernetes controllers[1]
  • client-go@v0.0.0-20210708094636-69e00b04ba4c/informers/factory.go

Informer 也經歷了兩代演進,從最早各管各的 Informer,到后來統一監聽,各自 filter 的 sharedInformer。

所有的 controller 都在一個 controller-manager 進程內,所以完全可以共享同一個 informer, 不同的 controller 注冊不同的 filter(kind、labelSelector),來訂閱自己需要的消息。

簡而言之,現在的 sharedIndexer,就是一個統一的消息訂閱器,而且內部還維護了一個資源存儲,對外提供可過濾的消息分發和資源查詢功能。

sharedIndexer 和 sharedInformer 的區別就是多了個 threadsafe 的 map 存儲,用來存 shared resource object。

現在的 informer 中由幾個主要的組件構成:

  • reflecter:查詢器,負責從 apiserver 定期輪詢資源,更新 informer 的 store。
  • store: informer 內部對資源的存儲,用來提供 lister 遍歷等查詢操作。
  • queue:支持 controller 的事件訂閱。

圖片

各個 controller 的訂閱和查詢絕大部分都在 sharedIndexer 的內存內完成,提高資源利用率和效率。

一般 controller 的消息來源就通過兩種方式:

  1. lister: controller 注冊監聽特定類型的資源事件,事件格式是字符串,??<namespace>/<name>??
  2. handler: controller 通過 informer 的??AddEventHandler??? 方法注冊??Add/Update/Delete?? 事件的處理函數。

這里有個值得注意的地方是,資源事件的格式是字符串,形如 ??<namespace>/<name>??,這其中沒有包含版本信息。

那么某個版本的 controller 拿到這個信息后,并不知道查詢出來的資源是否匹配自己的版本,也許會查出一個很舊版本的資源。

所以 controller 對于資源必須是向后兼容的,新版本的 controller 必須要能夠處理舊版資源。這樣的話,只需要保證運行的是最新版的 controller 就行了。

圖片

1、Queue

controller 內有大量的隊列,最重要的就是注冊到 informer 的三個 add/update/delete 隊列。

RateLimitingQueue

  • client-go@v0.0.0-20210708094636-69e00b04ba4c/util/workqueue/rate_limiting_queue.go

實際使用的是隊列類型是 RateLimitingQueue,繼承于 Queue。

Queue

  • client-go@v0.0.0-20210708094636-69e00b04ba4c/util/workqueue/queue.go
type Interface interface {
// Add 增加任務,可能是增加新任務,可能是處理失敗了重新放入
//
// 調用 Add 時,t 直接插入 dirty。然后會判斷一下 processing,
// 是否存在于 processing ? 返回 : 放入 queue
Add(item interface{})
Len() int
Get() (item interface{}, shutdown bool)
Done(item interface{})
ShutDown()
ShuttingDown() bool
}


type Type struct {
// queue 所有未被處理的任務
queue []t

// dirty 所有待處理的任務
//
// 從定義上看和 queue 有點相似,可以理解為 queue 的緩沖區。
// 比如調用 Add 時,如果 t 存在于 processing,就只會插入 dirty,不會插入 queue,
// 這種情況表明外部程序處理失敗了,所以再次插入了 t。
dirty set

// processing 正在被處理的任務
//
// 一個正在被處理的 t 應該從 queue 移除,然后添加到 processing。
//
// 如果 t 處理失敗需要重新處理,那么這個 t 會被再次放入 dirty。
// 所以調用 Done 從 processing 移除 t 的時候需要同步檢查一下 dirty,
// 如果 t 存在于 dirty,則將其再次放入 queue。
processing set

cond *sync.Cond

shuttingDown bool

metrics queueMetrics

unfinishedWorkUpdatePeriod time.Duration
clock clock.Clock
}

隊列傳遞的資源事件是以字符串來表示的,格式形如 ??namespace/name??。

正因為資源是字符串來表示,這導致了很多問題。其中對于隊列的一個問題就是:沒法為事件設置狀態,標記其是否已完成。為了實現這個狀態,queue 中通過 queue、dirty、processing 三個集合來表示。具體實現可以參考上面的注釋和代碼。

另一個問題就是資源中沒有包含版本信息。

那么某個版本的 controller 拿到這個信息后,并不知道查詢出來的資源是否匹配自己的版本,也許會查出一個很舊版本的資源。

所以 controller 對于資源必須是向后兼容的,新版本的 controller 必須要能夠處理舊版資源。這樣的話,只需要保證運行的是最新版的 controller 就行了。

六、GC

  • Garbage Collection[2]
  • Using Finalizers to Control Deletion[3]
  • kubernetes@59c0523b/pkg/controller/garbagecollector/garbagecollector.go

1、Concepts

我看到

GC 的第一印象是一個像語言 runtime 里的回收資源的自動垃圾收集器。但其實 k8s 里的 GC 的工作相對比較簡單,更像是只是一個被動的函數調用,當用戶試圖刪除一個資源的時候, 就會把這個資源提交給 GC,然后 GC 執行一系列既定的刪除流程,一般來說包括:

  1. 刪除子資源
  2. 執行刪除前清理工作(finalizer)
  3. 刪除資源

k8s 的資源間存在上下游依賴,當你刪除一個上游資源時,其下游資源也需要被刪除,這被稱為??級聯刪除 cascading deletion??。

刪除一個資源有三種策略(??propagationPolicy/DeletionPropagation??):

  • ??Foreground??(default): Children are deleted before the parent (post-order)
  • ??Background??: Parent is deleted before the children (pre-order)
  • ??Orphan??: 忽略 owner references

可以在運行 ??kubectl delete --cascade=?????? 的時候指定刪除的策略,默認為 ??foreground??。

2、Deletion

k8s 中,資源的 metadata 中有幾個對刪除比較重要的屬性:

  • ??ownerRerences??: 指向父資源的 UID
  • ??deletionTimestamp??: 如果不為空,表明該資源正在被刪除中
  • ??finalizers??: 一個字符串數組,列舉刪除前必須執行的操作
  • ??blockOwnerDeletion??: 布爾,當前資源是否會阻塞父資源的刪除流程

每一個資源都有 ??metadata.finalizers???,這是一個 ??[]string??, 內含一些預定義的字符串,表明了在刪除資源前必須要做的操作。每執行完一個操作,就從 finalizers 中移除這個字符串。

無論是什么刪除策略,都需要先把所有的 finalizer 逐一執行完,每完成一個,就從 finalizers 中移除一個。在 finalizers 為空后,才能正式的刪除資源。

foreground、orphan 刪除就是通過 finalizer 來實現的。

const (
FinalizerOrphanDependents = "orphan"
FinalizerDeleteDependents = "foregroundDeletion"
)

圖片

注:有一種讓資源永不刪除的黑魔法,就是為資源注入一個不存在的 finalizer。因為 GC 無法找到該 finalizer 匹配的函數來執行,就導致這個 finalizer 始終無法被移除, 而 finalizers 為空清空的資源是不允許被刪除的。

3、Foreground cascading deletion

  1. 設置資源的??deletionTimestamp???,表明該資源的狀態為正在刪除中(??"deletion in progress"??)。
  2. 設置資源的??metadata.finalizers??? 為??"foregroundDeletion"??。
  3. 刪除所有??ownerReference.blockOwnerDeletion=true?? 的子資源
  4. 刪除當前資源

每一個子資源的 owner 列表的元素里,都有一個屬性 ??ownerReference.blockOwnerDeletion???,這是一個 ??bool??, 表明當前資源是否會阻塞父資源的刪除流程。刪除父資源前,應該把所有標記為阻塞的子資源都刪光。

在當前資源被刪除以前,該資源都通過 apiserver 持續可見。

4、Orphan deletion

觸發 ??FinalizerOrphanDependents??,將所有子資源的 owner 清空,也就是令其成為 orphan。然后再刪除當前資源。

5、Background cascading deletion

立刻刪除當前資源,然后在后臺任務中刪除子資源。

graph LR

A1{是否有 finalizers} --> |Yes: pop, execute| A1
A1 --> |No| A2[刪除自己]
A2 --> A3{父資源是否在等待刪除}
A3 --> |No| A4[刪除所有子資源]
A3 --> |Yes| A31[在刪除隊列里提交父資源]
A31 --> A4

圖片

foreground 和 orphan 刪除策略是通過 finalizer 實現的 因為這兩個策略有一些刪除前必須要做的事情:

  • foreground finalizer: 將所有的子資源放入刪除事件隊列
  • orphan finalizer: 將所有的子資源的 owner 設為空

而 background 則就是走標準刪除流程:刪自己 -> 刪依賴。

這個流程里有一些很有趣(繞)的設計。比如 foreground 刪除,finalizer 里把所有的子資源都放入了刪除隊列, 然后下一步在刪除當前資源的時候,會發現子資源依然存在,導致當前資源無法刪除。實際上真正刪除當前資源(父資源),j是在刪除最后一個子資源的時候,每次都會去檢查下父資源的狀態是否是刪除中, 如果是,就把父資源放入刪除隊列,此時,父資源才會被真正刪除。

6、Owner References

每個資源的 metadata 中都有一個 ??ownerReferences?? 列表,保存了其父資源(遍歷時遇到的第一個有效的資源會被認為是其父資源)。

owner 決定了資源會如何被刪除。刪除子資源不會影響到父資源。刪除父資源會導致子資源被聯動刪除。(默認 ??kubectl delete --cascade=foreground??)

七、參考資料

關于本主題的內容,我制作了一個 slides,可用于內部分享:https://s3.laisky.com/public/slides/k8s-controller.slides.html#/

1、如何閱讀源碼

核心代碼:https://github.com/kubernetes/kubernetes,所有的 controller 代碼都在 ??pkg/controller/?? 中。

所有的 clientset、informer 都被抽象出來在 https://github.com/kubernetes/client-go 庫中,供各個組件復用。

學習用示例項目:https://github.com/kubernetes/sample-controller

2、參考文章

  • Garbage Collection[4]
  • Using Finalizers to Control Deletion[5]
  • A deep dive into Kubernetes controllers[6]
  • kube-controller-manager[7]

引用鏈接

[1]

A deep dive into Kubernetes controllers: https://app.yinxiang.com/shard/s17/nl/2006464/674c3d83-f011-49b8-9135-413588c22c0f/

[2]

Garbage Collection: https://kubernetes.io/docs/concepts/workloads/controllers/garbage-collection/

[3]

Using Finalizers to Control Deletion: https://kubernetes.io/blog/2021/05/14/using-finalizers-to-control-deletion/

[4]

Garbage Collection: https://kubernetes.io/docs/concepts/workloads/controllers/garbage-collection/

[5]

Using Finalizers to Control Deletion: https://kubernetes.io/blog/2021/05/14/using-finalizers-to-control-deletion/

[6]

A deep dive into Kubernetes controllers: https://engineering.bitnami.com/articles/a-deep-dive-into-kubernetes-controllers.html

[7]

kube-controller-manager: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/

責任編輯:龐桂玉 來源: 奇妙的Linux世界
相關推薦

2024-07-03 08:28:44

HWKafkaLEO

2022-06-13 11:05:35

RocketMQ消費者線程

2020-06-28 07:39:44

Kafka分布式消息

2022-06-27 11:04:24

RocketMQ順序消息

2020-11-16 10:50:27

KubernetesIngressLinux

2022-02-28 11:10:42

ZGCG1收集器

2023-04-11 08:35:22

RocketMQ云原生

2021-04-25 10:45:59

Docker架構Job

2022-07-11 11:06:11

RocketMQ函數.消費端

2021-05-18 06:55:07

Java AQS源碼

2022-07-04 11:06:02

RocketMQ事務消息實現

2022-01-05 14:30:44

容器Linux網絡

2019-07-24 08:49:36

Docker容器鏡像

2021-11-12 08:38:26

一致性哈希算法數據結構

2021-01-28 10:55:47

Kubernetes IPLinux

2021-12-06 07:15:47

Pulsar地域復制

2022-05-09 11:15:05

RocketMQPULL 模式PUSH 模式

2020-06-03 08:19:00

Kubernetes

2015-07-13 10:23:23

Java圖解

2023-09-28 21:37:41

HashMap多線程
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲超碰在线观看 | 精产国产伦理一二三区 | 久久久久久美女 | 国产一级毛片视频 | 毛片黄片免费看 | 久久久一区二区 | 中国一级大毛片 | 亚洲资源在线 | 夜夜爽夜夜操 | h免费观看 | 精品久久久久久亚洲综合网 | 91爱爱·com | 午夜在线精品 | 日本精品视频 | 日韩三级在线观看 | caoporn视频在线 | 成人国产在线观看 | 国产探花在线精品一区二区 | 999久久久国产精品 欧美成人h版在线观看 | 欧美日韩不卡合集视频 | 一区二区三区四区视频 | 精品国产一区二区三区性色av | 一级片在线观看视频 | 一区二区在线不卡 | 久热中文字幕 | 精品在线看 | 人人人人干 | 欧美毛片免费观看 | 污书屋 | 天天爽夜夜爽精品视频婷婷 | 久久精品亚洲一区二区三区浴池 | 国产精品mv在线观看 | 亚洲精品电影网在线观看 | 国产精品日韩在线观看一区二区 | 国产精品亚洲综合 | 黄色免费网 | 人人99| 日韩欧美国产不卡 | 成人久久 | 黄色大片毛片 | 视频二区在线观看 |