本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。
我激動人心的 Kubernetes 歷史之旅
編者按:Sascha 是 SIG Release 的成員,正在研究許多其他與容器執行時相關的議題。歡迎透過 Twitter @saschagrunert 與他聯絡。
一個關於如何使用 Kubeflow、TensorFlow、Prow 和全自動 CI/CD 流水線,對 90,000 個 GitHub 問題和拉取請求進行資料科學研究的故事。
引言
在資料科學領域工作時,選擇正確的步驟絕非易事。大多數資料科學家可能有他們自己的定製工作流,這些工作流的自動化程度可能因其工作領域而異。當嘗試大規模自動化工作流時,使用 Kubernetes 可以極大地增強效果。在這篇博文中,我將帶你踏上我的資料科學之旅,並將整個工作流整合到 Kubernetes 中。
我過去幾個月進行的研究目標是,在 Kubernetes 倉庫中找到關於數千個 GitHub 問題和拉取請求 (PR) 的任何有用資訊。最終我得到的是一個完全自動化、在 Kubernetes 中執行的持續整合 (CI) 和部署 (CD) 資料科學工作流,由 Kubeflow 和 Prow 提供支援。你可能不熟悉它們,但我會詳細解釋它們的作用。我的工作原始碼可以在 kubernetes-analysis GitHub 倉庫中找到,其中包含所有與原始碼相關的內容以及原始資料。但如何檢索我所說的資料呢?嗯,故事從這裡開始。
獲取資料
我實驗的基礎是純 JSON 格式的原始 GitHub API 資料。必要的資料可以透過 GitHub issues endpoint 檢索,該端點以 REST API 形式返回所有拉取請求以及常規問題。在第一次迭代中,我匯出了大約 91000 個問題和拉取請求,形成了一個巨大的 650 MiB 資料塊。這花費了我大約 8 小時的資料檢索時間,因為 GitHub API 確實有 速率限制。為了將這些資料放入 GitHub 倉庫,我選擇使用 xz(1)
進行壓縮。結果是一個大約 25 MiB 大小的 tarball,非常適合倉庫。
我必須找到一種定期更新資料集的方法,因為 Kubernetes 的問題和拉取請求會隨著時間的推移被使用者更新,也會建立新的。為了實現持續更新而無需一次又一次地等待 8 小時,我現在獲取 上次更新 和當前時間之間的增量 GitHub API 資料。透過這種方式,持續整合作業可以定期更新資料,而我可以使用最新可用的資料集繼續我的研究。
從工具的角度來看,我編寫了一個一體化 Python 可執行檔案,它允許我們透過專用子命令單獨觸發資料科學實驗中的不同步驟。例如,要執行整個資料集的匯出,我們可以呼叫
> export GITHUB_TOKEN=<MY-SECRET-TOKEN>
> ./main export
INFO | Getting GITHUB_TOKEN from environment variable
INFO | Dumping all issues
INFO | Pulling 90929 items
INFO | 1: Unit test coverage in Kubelet is lousy. (~30%)
INFO | 2: Better error messages if go isn't installed, or if gcloud is old.
INFO | 3: Need real cluster integration tests
INFO | 4: kubelet should know which containers it is managing
… [just wait 8 hours] …
要更新倉庫中儲存的最後一個時間戳之間的資料,我們可以執行
> ./main export --update-api
INFO | Getting GITHUB_TOKEN from environment variable
INFO | Retrieving issues and PRs
INFO | Updating API
INFO | Got update timestamp: 2020-05-09T10:57:40.854151
INFO | 90786: Automated cherry pick of #90749: fix: azure disk dangling attach issue
INFO | 90674: Switch core master base images from debian to distroless
INFO | 90086: Handling error returned by request.Request.ParseForm()
INFO | 90544: configurable weight on the CPU and memory
INFO | 87746: Support compiling Kubelet w/o docker/docker
INFO | Using already extracted data from data/data.pickle
INFO | Loading pickle dataset
INFO | Parsed 34380 issues and 55832 pull requests (90212 items)
INFO | Updating data
INFO | Updating issue 90786 (updated at 2020-05-09T10:59:43Z)
INFO | Updating issue 90674 (updated at 2020-05-09T10:58:27Z)
INFO | Updating issue 90086 (updated at 2020-05-09T10:58:26Z)
INFO | Updating issue 90544 (updated at 2020-05-09T10:57:51Z)
INFO | Updating issue 87746 (updated at 2020-05-09T11:01:51Z)
INFO | Saving data
這讓我們對專案實際移動的速度有了一個概念:在一個週六中午(歐洲時間),5 個問題和拉取請求在字面上的 5 分鐘內得到了更新!
有趣的是,Kubernetes 創始人之一 Joe Beda 建立了第一個 GitHub issue 提及單元測試覆蓋率太低。該 issue 除了標題之外沒有進一步的描述,也沒有應用像我們從最近的 issues 和 pull request 中所瞭解的更高階的標籤。但現在我們必須更深入地探索匯出的資料,才能用它做些有用的事情。
探索資料
在開始建立和訓練機器學習模型之前,我們必須瞭解資料的結構以及我們想要達到的總體目標。
為了更好地瞭解資料量,我們來看看 Kubernetes 倉庫中隨時間建立了多少問題和拉取請求
> ./main analyze --created
INFO | Using already extracted data from data/data.pickle
INFO | Loading pickle dataset
INFO | Parsed 34380 issues and 55832 pull requests (90212 items)
Python matplotlib 模組應該會彈出一個圖表,看起來像這樣
好吧,這看起來並不那麼引人注目,但讓我們對專案在過去 6 年中的增長有了一個印象。為了更好地瞭解專案開發的速度,我們可以檢視“created-vs-closed”指標。這意味著在我們的時間軸上,如果一個問題或拉取請求被建立,我們會在 y 軸上加一;如果它被關閉,我們會減去一。現在圖表看起來像這樣
> ./main analyze --created-vs-closed
在2018年初,Kubernetes專案透過光榮的fejta-bot引入了一些更強大的生命週期管理。這會自動關閉長時間處於閒置狀態的issues和拉取請求。這導致了大量issue被關閉,但拉取請求的關閉數量並不相同。例如,如果我們只檢視拉取請求的created-vs-closed指標。
> ./main analyze --created-vs-closed --pull-requests
整體影響並不那麼明顯。我們可以看到,PR 圖表中峰值數量的增加表明專案隨時間推移正在加速發展。通常,K線圖會是顯示這種波動性相關資訊的更好選擇。我還想強調的是,專案的發展似乎在 2020 年初略有放緩。
在每次分析迭代中解析原始 JSON 並不是 Python 中最快的方法。這意味著我決定將更重要的資訊,例如內容、標題和建立時間解析到專門的 問題 和 PR 類中。這些資料也將透過 pickle 序列化到倉庫中,這使得啟動速度更快,而無需依賴 JSON blob。
在我的分析中,一個拉取請求或多或少與一個問題相同,只是它包含一個釋出說明。
Kubernetes 的釋出說明寫在 PR 的描述中,放在一個單獨的 `release-note` 塊中,像這樣
```release-note
I changed something extremely important and you should note that.
```
這些釋出說明在釋出建立過程中會由 專門的釋出工程工具,如 krel
進行解析,並會成為各種 CHANGELOG.md 檔案和 釋出說明網站 的一部分。這看起來像很多魔法,但最終,整個釋出說明的質量會更高,因為它們易於編輯,並且 PR 評審者可以確保我們只記錄真正的面向使用者的更改,而沒有其他內容。
在進行資料科學時,輸入資料的質量是一個關鍵方面。我決定關注釋出說明,因為與問題和 PR 中的普通描述相比,它們似乎具有最高的總體質量。除此之外,它們易於解析,我們不需要去除 各種問題 和 PR 模板 中的文字噪音。
標籤,標籤,標籤
Kubernetes 中的問題和拉取請求在其生命週期中會應用不同的標籤。它們通常透過單個斜槓(/
)進行分組。例如,我們有 kind/bug
和 kind/api-change
,或者 sig/node
和 sig/network
。瞭解存在哪些標籤組以及它們在倉庫中的分佈情況的一種簡單方法是將其繪製成條形圖
> ./main analyze --labels-by-group
看起來 sig/
、kind/
和 area/
標籤很常見。像 size/
這樣的標籤目前可以忽略,因為這些標籤是根據拉取請求的程式碼更改量自動應用的。我們說過我們想將釋出說明作為輸入資料,這意味著我們必須檢視 PR 的標籤分佈。這意味著拉取請求上的前 25 個標籤是
> ./main analyze --labels-by-name --pull-requests
同樣,我們可以忽略諸如 lgtm
(看起來不錯)之類的標籤,因為每個現在應該合併的 PR 都必須看起來不錯。包含釋出說明的拉取請求會自動應用 release-note
標籤,這使得進一步篩選更加容易。這並不意味著每個包含該標籤的 PR 都包含釋出說明塊。該標籤可能是手動應用的,並且釋出說明塊的解析自專案開始以來並不存在。這意味著我們一方面可能會丟失相當數量的輸入資料。另一方面,我們可以專注於盡可能高的資料質量,因為正確應用標籤需要專案及其貢獻者更高的成熟度。
從標籤組的角度來看,我選擇專注於 kind/
標籤。這些標籤是 PR 作者必須手動應用的,它們在大量拉取請求中可用,並且也與面向使用者的更改相關。此外,每個拉取請求都必須選擇 kind/
,因為它是 PR 模板的一部分。
好的,只關注包含釋出說明的拉取請求時,這些標籤的分佈是怎樣的呢?
> ./main analyze --release-notes-stats
有趣的是,我們大約有 7,000 個包含釋出說明的拉取請求,但只有大約 5,000 個應用了 kind/
標籤。標籤的分佈不均,其中三分之一被標記為 kind/bug
。這把我帶到了資料科學之旅的下一個決定:我將構建一個二元分類器,為簡單起見,它只能區分 bug(透過 kind/bug
)和非 bug(未應用該標籤)。
現在主要目標是能夠根據我們已經擁有的社群歷史資料,對新傳入的釋出說明進行分類,判斷它們是否與 bug 相關。
在此之前,我建議您也玩一下 ./main analyze -h
子命令,以探索最新的資料集。您還可以檢視我在分析倉庫中提供的所有持續更新的資產。例如,這些是 Kubernetes 倉庫中排名前 25 的 PR 建立者
構建機器學習模型
現在我們對資料集有了一個概念,可以開始構建第一個機器學習模型了。在實際構建模型之前,我們必須預處理從 PR 中提取的所有釋出說明。否則,模型將無法理解我們的輸入。
進行一些初步的自然語言處理 (NLP)
首先,我們必須定義要訓練的詞彙表。我決定選擇 Python scikit-learn 機器學習庫中的 TfidfVectorizer。這個向量化器能夠接收我們的輸入文字並從中建立一個龐大的詞彙表。這就是我們所謂的詞袋模型,其 n-gram 範圍選擇為 (1, 2)
(一元詞和二元詞)。實際上,這意味著我們總是將第一個詞和下一個詞作為一個單一的詞彙表條目(二元詞)。我們也將單個詞作為詞彙表條目(一元詞)。TfidfVectorizer 能夠跳過多次出現的詞(max_df
),並要求最小數量(min_df
)才能將詞新增到詞彙表中。我最初決定不更改這些值,僅僅是因為我直覺地認為釋出說明是專案獨有的東西。
像 min_df
、max_df
和 n-gram 範圍這樣的引數可以看作是我們的一些超引數。這些引數必須在機器學習模型構建後的專門步驟中進行最佳化。這個步驟稱為超引數調優,其基本含義是,我們用不同的引數進行多次訓練,並比較模型的準確性。之後,我們選擇準確性最佳的引數。
在訓練過程中,向量化器將生成一個 data/features.json
檔案,其中包含整個詞彙表。這讓我們很好地理解了這樣一個詞彙表可能是什麼樣子
[
…
"hostname",
"hostname address",
"hostname and",
"hostname as",
"hostname being",
"hostname bug",
…
]
這在整個詞袋中產生了大約 50,000 個條目,數量相當龐大。先前對不同資料集的分析表明,根本無需考慮如此多的特徵。一些通用資料集指出,20,000 個詞彙表就足夠了,更高的數量不再影響準確性。為此,我們可以使用 SelectKBest 特徵選擇器將詞彙表縮小到只選擇最重要的特徵。然而,我仍然決定堅持使用前 50,000 個,以免對模型準確性產生負面影響。我們的資料量相對較少(大約 7,000 個樣本),每個樣本的詞數也很少(大約 15 個),這已經讓我懷疑我們是否有足夠的資料。
向量化器不僅能夠建立我們的詞袋,而且還能夠以 詞頻-逆文件頻率 (tf-idf) 格式編碼特徵。這也是向量化器得名的原因,而這種編碼的輸出是機器學習模型可以直接使用的。向量化過程的所有細節都可以在原始碼中找到。
建立多層感知器 (MLP) 模型
我決定選擇一個簡單的基於 MLP 的模型,該模型藉助流行的 TensorFlow 框架構建。由於我們沒有太多的輸入資料,我們只使用兩個隱藏層,因此模型基本上看起來像這樣
建立模型時需要考慮許多其他超引數。這裡我不會詳細討論它們,但它們對於最佳化模型中的類別數量(在我們的例子中只有兩個)也很重要。
訓練模型
在開始實際訓練之前,我們必須將輸入資料分成訓練集和驗證集。我選擇使用大約 80% 的資料進行訓練,20% 用於驗證。我們還必須打亂輸入資料,以確保模型不受排序問題的影響。訓練過程的技術細節可以在 GitHub 原始碼中找到。所以現在我們準備好最終開始訓練了
> ./main train
INFO | Using already extracted data from data/data.pickle
INFO | Loading pickle dataset
INFO | Parsed 34380 issues and 55832 pull requests (90212 items)
INFO | Training for label 'kind/bug'
INFO | 6980 items selected
INFO | Using 5584 training and 1395 testing texts
INFO | Number of classes: 2
INFO | Vocabulary len: 51772
INFO | Wrote features to file data/features.json
INFO | Using units: 1
INFO | Using activation function: sigmoid
INFO | Created model with 2 layers and 64 units
INFO | Compiling model
INFO | Starting training
Train on 5584 samples, validate on 1395 samples
Epoch 1/1000
5584/5584 - 3s - loss: 0.6895 - acc: 0.6789 - val_loss: 0.6856 - val_acc: 0.6860
Epoch 2/1000
5584/5584 - 2s - loss: 0.6822 - acc: 0.6827 - val_loss: 0.6782 - val_acc: 0.6860
Epoch 3/1000
…
Epoch 68/1000
5584/5584 - 2s - loss: 0.2587 - acc: 0.9257 - val_loss: 0.4847 - val_acc: 0.7728
INFO | Confusion matrix:
[[920 32]
[291 152]]
INFO | Confusion matrix normalized:
[[0.966 0.034]
[0.657 0.343]]
INFO | Saving model to file data/model.h5
INFO | Validation accuracy: 0.7727598547935486, loss: 0.48470408514836355
混淆矩陣 的輸出顯示,我們的訓練準確率相當高,但驗證準確率可以更高一些。我們現在可以開始超引數調優,看看是否能進一步最佳化模型的輸出。我將把這個實驗留給您,並提示您使用 ./main train --tune
標誌。
我們將模型 (data/model.h5
)、向量化器 (data/vectorizer.pickle
) 和特徵選擇器 (data/selector.pickle
) 儲存到磁碟,以便稍後用於預測目的,而無需額外的訓練步驟。
首次預測
我們現在可以透過從磁碟載入模型並預測一些輸入文字來測試模型
> ./main predict --test
INFO | Testing positive text:
Fix concurrent map access panic
Don't watch .mount cgroups to reduce number of inotify watches
Fix NVML initialization race condition
Fix brtfs disk metrics when using a subdirectory of a subvolume
INFO | Got prediction result: 0.9940581321716309
INFO | Matched expected positive prediction result
INFO | Testing negative text:
action required
1. Currently, if users were to explicitly specify CacheSize of 0 for
KMS provider, they would end-up with a provider that caches up to
1000 keys. This PR changes this behavior.
Post this PR, when users supply 0 for CacheSize this will result in
a validation error.
2. CacheSize type was changed from int32 to *int32. This allows
defaulting logic to differentiate between cases where users
explicitly supplied 0 vs. not supplied any value.
3. KMS Provider's endpoint (path to Unix socket) is now validated when
the EncryptionConfiguration files is loaded. This used to be handled
by the GRPCService.
INFO | Got prediction result: 0.1251964420080185
INFO | Matched expected negative prediction result
這兩個測試都是實際存在的真實世界示例。我們也可以嘗試一些完全不同的東西,比如我幾分鐘前發現的這條隨機推文
./main predict "My dudes, if you can understand SYN-ACK, you can understand consent"
INFO | Got prediction result: 0.1251964420080185
ERROR | Result is lower than selected threshold 0.6
看起來它沒有被歸類為釋出說明中的錯誤,這似乎是有效的。選擇一個好的閾值也不容易,但堅持使用 > 50% 應該只是最低限度。
一切自動化
下一步是找到一種自動化方法,用新資料持續更新模型。如果我在我的倉庫中更改任何原始碼,那麼我想獲得關於模型測試結果的反饋,而無需在我自己的機器上執行訓練。我希望利用 Kubernetes 叢集中的 GPU 來更快地訓練,並在 PR 合併後自動更新資料集。
藉助 Kubeflow 流水線,我們可以滿足大部分這些要求。我構建的流水線看起來像這樣
首先,我們簽出 PR 的原始碼,它將作為輸出工件傳遞給所有其他步驟。然後,我們增量更新 API 和內部資料,之後我們在始終是最新的資料集上執行訓練。預測測試在訓練後驗證我們沒有因為更改而對模型產生不良影響。
我們還在管道中構建了一個容器映象。這個容器映象 將先前構建的模型、向量化器和選擇器複製到容器中並執行 ./main serve
。這樣做時,我們啟動一個 kfserving Web 伺服器,該伺服器可用於預測。您想自己嘗試一下嗎?只需像這樣進行 JSON POST 請求,並針對端點執行預測
> curl https://kfserving.k8s.saschagrunert.de/v1/models/kubernetes-analysis:predict \
-d '{"text": "my test text"}'
{"result": 0.1251964420080185}
自定義 kfserving 實現非常直接,而部署則利用 Knative Serving 和 Istio ingress 閘道器在底層正確路由流量到叢集並提供正確的服務集。
commit-changes
和 rollout
步驟僅在流水線執行在 master
分支上時才會執行。這些步驟確保我們在 master 分支和 kfserving 部署中始終擁有最新的資料集。 金絲雀釋出步驟 首先建立一個新的金絲雀部署,它只接受 50% 的入站流量。在金絲雀成功部署後,它將被提升為服務的新主例項。這是一種很好的方式,可以確保部署按預期工作,並允許在金絲雀釋出後進行額外的測試。
但是如何在建立拉取請求時觸發 Kubeflow 流水線呢?Kubeflow 目前還沒有這個功能。這就是我決定使用 Prow,即 Kubernetes 用於 CI/CD 的測試基礎設施專案的原因。
首先,一個 24 小時週期性作業 確保我們倉庫中至少每天都有最新的資料可用。然後,如果我們建立一個拉取請求,Prow 將執行整個 Kubeflow 流水線,而不提交或回滾任何更改。如果我們透過 Prow 合併拉取請求,另一個作業將在 master 分支上執行並更新資料以及部署。這不是很棒嗎?
新 PR 的自動標記
預測 API 適用於測試,但現在我們需要一個真實的用例。Prow 支援可用於對任何 GitHub 事件採取行動的外部外掛。我編寫了一個外掛,它使用 kfserving API 根據新的拉取請求進行預測。這意味著如果我們現在在 kubernetes-analysis 倉庫中建立一個新的拉取請求,我們將看到以下內容
好吧,酷,現在讓我們根據現有資料集中的一個真實 bug 來修改釋出說明
機器人編輯了自己的評論,以大約 90% 的機率將其預測為 kind/bug
並自動添加了正確的標籤!現在,如果我們將它改回一些不同的——顯然是錯誤的——釋出說明
機器人為我們完成了工作,移除了標籤並告知我們它做了什麼!最後,如果我們將釋出說明更改為 None
機器人刪除了評論,這很好,減少了 PR 上的文字噪音。我演示的所有功能都在一個 Kubernetes 叢集中執行,這使得將 kfserving API 公開給公眾完全沒有必要。這引入了一種間接的 API 速率限制,因為只有透過 Prow 機器人使用者才能使用。
如果你想親自嘗試一下,請隨時在 kubernetes-analysis
中開啟一個 新的測試問題。這之所以有效,是因為我還為問題啟用了外掛,而不僅僅是為拉取請求。
那麼,我們有一個正在執行的 CI 機器人,它能夠根據機器學習模型對新的釋出說明進行分類。如果該機器人執行在官方的 Kubernetes 倉庫中,那麼我們就可以手動糾正錯誤的標籤預測。這樣,下一次訓練迭代就會採納這些糾正,從而隨著時間的推移不斷改進模型。一切都完全自動化!
總結
感謝您閱讀到這裡!這是我的 Kubernetes GitHub 倉庫資料科學之旅。還有很多其他可以最佳化的地方,例如引入更多類別(不僅僅是 kind/bug
或無類別)或者使用 Kubeflow 的 Katib 進行自動超引數調優。如果您有任何問題或建議,請隨時與我聯絡。再見!