本文已超過一年。較舊的文章可能包含過時的內容。請檢查頁面中的資訊自發布以來是否已變得不正確。
我對 Kubernetes 歷史的精彩旅程
編者註:Sascha 是 SIG Release 的成員,並且正在研究許多其他不同的容器執行階段相關主題。歡迎在 Twitter 上與他聯繫 @saschagrunert。
一個關於使用 Kubeflow、TensorFlow、Prow 和全自動 CI/CD 管道進行 90,000 個 GitHub issue 和 pull request 的資料科學故事。
簡介
在資料科學領域工作時,選擇正確的步驟絕非易事。大多數資料科學家可能都有其自訂的工作流程,這些流程的自動化程度可能或多或少,取決於他們的工作領域。當嘗試大規模自動化工作流程時,使用 Kubernetes 可能會帶來巨大的提升。在這篇部落格文章中,我想帶您踏上我的資料科學之旅,同時將整體工作流程整合到 Kubernetes 中。
我在過去幾個月所做的研究目標是找出關於我們在 Kubernetes 儲存庫中擁有的成千上萬個 GitHub issue 和 pull request (PR) 的任何有用資訊。我最終得到的是一個全自動化、在 Kubernetes 中運行的持續整合 (CI) 和部署 (CD) 資料科學工作流程,由 Kubeflow 和 Prow 提供支援。您可能不認識它們,但我們會說明它們的詳細作用。我的工作原始碼可以在 kubernetes-analysis GitHub 儲存庫中找到,其中包含所有與原始碼相關的內容以及原始資料。但是,如何檢索我所說的這些資料呢?嗯,這就是故事的開始。
取得資料
我的實驗基礎是純 JSON 格式的原始 GitHub API 資料。必要的資料可以透過 GitHub issue 端點檢索,該端點以 REST API 傳回所有 pull request 以及常規 issue。在第一次迭代中,我匯出了大約 91000 個 issue 和 pull request 到一個巨大的 650 MiB 資料 Blob 中。這花了我大約 8 小時的資料檢索時間,因為可以肯定的是,GitHub API 是 速率受限 的。為了能夠將這些資料放入 GitHub 儲存庫中,我選擇透過 xz(1)
壓縮它。結果是一個大約 25 MiB 大小的 tarball,這非常適合放入儲存庫中。
我必須找到一種定期更新資料集的方法,因為 Kubernetes issue 和 pull request 會隨著時間的推移而被使用者更新,並且也會建立新的 issue 和 pull request。為了實現持續更新,而無需一遍又一遍地等待 8 小時,我現在擷取 上次更新 和目前時間之間的 delta GitHub API 資料。這樣一來,持續整合作業可以定期更新資料,而我可以繼續使用最新的可用資料集進行研究。
從工具的角度來看,我編寫了一個 all-in-one 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 個 issue 和 pull request 在短短 5 分鐘內獲得更新!
有趣的是,Joe Beda,Kubernetes 的創始人之一,建立了第一個 GitHub issue 提及單元測試覆蓋率太低。該 issue 除了標題外沒有其他描述,也沒有應用增強的標籤,就像我們從最近的 issue 和 pull request 中了解到的那樣。但現在我們必須更深入地探索匯出的資料,以便對其進行一些有用的操作。
探索資料
在我們可以開始建立機器學習模型並訓練它們之前,我們必須了解我們的資料結構以及我們總體上想要實現什麼目標。
為了更好地了解資料量,讓我們看看在 Kubernetes 儲存庫中隨著時間的推移創建了多少 issue 和 pull request
> ./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 年中的成長情況有所了解。為了更好地了解專案的開發速度,我們可以查看建立與關閉指標。這表示在我們的時間軸上,如果 issue 或 pull request 被建立,我們將 y 軸加一,如果被關閉則減一。現在圖表看起來像這樣
> ./main analyze --created-vs-closed
在 2018 年初,Kubernetes 專案透過光榮的 fejta-bot 引入了一些更增強的生命週期管理。這會在 issue 和 pull request 在較長時間內過時後自動關閉它們。這導致大量 issue 被關閉,但 pull request 的關閉量沒有那麼多。例如,如果我們只查看 pull request 的建立與關閉指標。
> ./main analyze --created-vs-closed --pull-requests
總體影響不是那麼明顯。我們可以觀察到的是,PR 圖表中的峰值數量不斷增加,表明專案隨著時間的推移而發展得更快。通常,蠟燭圖會是顯示此類與波動性相關資訊的更好選擇。我也想強調的是,看起來該專案的開發在 2020 年初有所放緩。
在每次分析迭代中解析原始 JSON 不是在 Python 中執行的最快方法。這表示我決定將更重要的資訊 (例如內容、標題和建立時間) 解析為專用的 issue 和 PR 類別。這些資料也將被 pickle 序列化到儲存庫中,這允許總體上更快的啟動,而與 JSON Blob 無關。
在我的分析中,pull request 或多或少與 issue 相同,只是它包含發行說明。
Kubernetes 中的發行說明寫在 PR 描述中的單獨 release-note
區塊中,如下所示
```release-note
I changed something extremely important and you should note that.
```
這些發行說明由 專用的發行工程工具 (如 krel
) 在發行建立過程中解析,並將成為各種 CHANGELOG.md 檔案和 發行說明網站 的一部分。這看起來像是很多魔法,但最終,整體發行說明的品質要高得多,因為它們易於編輯,並且 PR 審閱者可以確保我們僅記錄真實的使用者導向變更,而沒有其他任何內容。
當進行資料科學時,輸入資料的品質是一個關鍵方面。我決定專注於發行說明,因為與 issue 和 PR 中的純描述相比,它們似乎具有最高的整體品質。除此之外,它們易於解析,而且我們不需要剝離 各種 issue 和 PR 範本 文字雜訊。
標籤、標籤、標籤
Kubernetes 中的 issue 和 pull request 在其生命週期中會應用不同的標籤。它們通常透過單個斜線 (/
) 分組。例如,我們有 kind/bug
和 kind/api-change
或 sig/node
和 sig/network
。了解存在哪些標籤組以及它們在儲存庫中的分佈情況的一種簡單方法是將它們繪製成條形圖
> ./main analyze --labels-by-group
看起來 sig/
、kind/
和 area/
標籤非常常見。像 size/
這樣的標籤現在可以忽略,因為這些標籤是根據 pull request 的程式碼變更量自動應用的。我們說我們希望專注於發行說明作為輸入資料,這表示我們必須檢查 PR 的標籤分佈情況。這表示 pull request 上的前 25 個標籤是
> ./main analyze --labels-by-name --pull-requests
同樣,我們可以忽略像 lgtm
(看起來不錯) 這樣的標籤,因為現在每個應該合併的 PR 都必須看起來不錯。包含發行說明的 pull request 會自動應用 release-note
標籤,這使得進一步篩選變得更容易。這並不表示每個包含該標籤的 PR 也包含發行說明區塊。該標籤可能是手動應用的,並且發行說明區塊的解析從專案開始時就不存在。這表示一方面我們可能會遺失相當數量的輸入資料。另一方面,我們可以專注於盡可能高的資料品質,因為正確應用標籤需要專案及其貢獻者具有更高的成熟度。
從標籤組的角度來看,我已選擇專注於 kind/
標籤。這些標籤是必須由 PR 作者手動應用的內容,它們在很大一部分 pull request 上可用,並且它們也與使用者導向的變更相關。除此之外,必須為每個 pull request 完成 kind/
選擇,因為它是 PR 範本的一部分。
好的,當僅專注於具有發行說明的 pull request 時,這些標籤的分佈情況如何?
> ./main analyze --release-notes-stats
有趣的是,我們大約有 7,000 個包含發行說明的 pull request,但只有約 5,000 個應用了 kind/
標籤。標籤的分佈不均勻,其中三分之一被標記為 kind/bug
。這將我帶到資料科學旅程中的下一個決定:我將建立一個二元分類器,為了簡單起見,它只能區分錯誤 (透過 kind/bug
) 和非錯誤 (未應用標籤)。
現在的主要目標是能夠根據我們已經從社群獲得的歷史資料,對新收到的發行說明進行分類,判斷它們是否與錯誤相關。
在執行此操作之前,我建議您也使用 ./main analyze -h
子命令來探索最新的資料集。您也可以查看我在分析儲存庫中提供的所有 持續更新的資產。例如,這些是 Kubernetes 儲存庫內的前 25 名 PR 建立者
建置機器學習模型
現在我們對資料集有了大致了解,我們可以開始建立第一個機器學習模型。在實際建立模型之前,我們必須預先處理從 PR 中提取的所有發行說明。否則,模型將無法理解我們的輸入。
進行一些初步的自然語言處理 (NLP)
首先,我們必須定義我們要訓練的詞彙表。我決定從 Python scikit-learn 機器學習庫中選擇 TfidfVectorizer。此向量化器能夠取得我們的輸入文字並從中建立一個巨大的詞彙表。這是我們所謂的 詞袋模型,它具有選定的 n-gram 範圍 (1, 2)
(unigram 和 bigram)。實際上,這表示我們始終將第一個單字和下一個單字用作單個詞彙表條目 (bigram)。我們也將單個單字用作詞彙表條目 (unigram)。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 的原始碼,它將作為輸出 Artifact 傳遞給所有其他步驟。然後,我們以增量方式更新 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 輸入閘道,以正確地將流量路由到叢集中,並提供正確的服務集。
commit-changes
和 rollout
步驟僅在管道在 master
分支上運行時才會運行。這些步驟確保我們始終在 master 分支以及 kfserving 部署中擁有最新的資料集。rollout 步驟 建立一個新的 Canary 部署,該部署首先僅接受 50% 的傳入流量。在 Canary 成功部署後,它將被提升為服務的新主要實例。這是確保部署按預期工作的好方法,並允許在推出 Canary 後進行額外的測試。
但是如何在建立 pull request 時觸發 Kubeflow 管道呢?Kubeflow 目前沒有此功能。這就是我決定使用 Prow (Kubernetes 測試基礎架構專案,用於 CI/CD 目的) 的原因。
首先,24 小時定期作業 可確保我們至少每天在儲存庫中擁有最新的資料。然後,如果我們建立 pull request,Prow 將運行整個 Kubeflow 管道,而不會提交或推出任何變更。如果我們透過 Prow 合併 pull request,則另一個作業會在 master 上運行,並更新資料以及部署。這不是很棒嗎?
自動標記新的 PR
預測 API 非常適合測試,但現在我們需要一個真實世界的用例。Prow 支援外部外掛程式,可用於對任何 GitHub 事件採取行動。我編寫了 一個外掛程式,它使用 kfserving API 根據新的 pull request 進行預測。這表示如果我們現在在 kubernetes-analysis 儲存庫中建立新的 pull request,我們將看到以下內容
好的,太棒了,現在讓我們根據已經存在資料集中的真實錯誤來變更發行說明
機器人編輯自己的評論,預測它大約有 90% 的機率為 kind/bug
,並自動新增正確的標籤!現在,如果我們將其變更回一些不同的 - 顯然是錯誤的 - 發行說明
機器人為我們完成工作,移除標籤並通知我們它做了什麼!最後,如果我們將發行說明變更為 None
機器人移除了評論,這很棒,並減少了 PR 上的文字雜訊。我示範的所有內容都在單個 Kubernetes 叢集中運行,這使得完全沒有必要向公眾公開 kfserving API。這引入了間接的 API 速率限制,因為唯一的用途將透過 Prow 機器人使用者實現。
如果您想自己試試看,請隨時在 kubernetes-analysis
中開啟一個 新的測試 issue。這是有效的,因為我也為 issue 而不是僅為 pull request 啟用了外掛程式。
因此,我們有一個正在運行的 CI 機器人,它能夠根據機器學習模型對新的發行說明進行分類。如果機器人在官方 Kubernetes 儲存庫中運行,那麼我們可以手動更正錯誤的標籤預測。這樣一來,下一次訓練迭代將會接受更正,並隨著時間的推移產生不斷改進的模型。一切完全自動化!
摘要
感謝您閱讀到這裡!這是我在 Kubernetes GitHub 儲存庫中的小小資料科學之旅。還有許多其他事項需要最佳化,例如引入更多類別 (而不僅僅是 kind/bug
或沒有) 或使用 Kubeflow 的 Katib 進行自動超參數調整。如果您有任何問題或建議,請隨時與我聯繫。再見!