数据科学为啥青睐Kubernetes?一场有趣的数据科学K8s之旅

微信扫一扫,分享到朋友圈

数据科学为啥青睐Kubernetes?一场有趣的数据科学K8s之旅

这是一个关于数据科学的故事,通过使用Kubeflow、TensorFlow、Prow和一个完全自动化的CI/CD pipeline,研究了90000个GitHub issues和pull请求。

简介

在数据科学领域中选择正确的步骤并没有什么灵丹妙药。大多数数据科学家都有自己的自定义工作流,根据工作领域的不同,工作流可能或多或少是自动化的。当试图大规模自动化工作流时,使用Kubernetes可以说是个巨大的增强。在本文中,我将带您踏上我的数据科学之旅,同时将整个工作流程集成到Kubernetes中。

在过去的几个月里,我的主要目标是找到所有关于我们在Kubernetes库中所拥有的数千个GitHub问题和pull请求(PRs)的有用信息。最终得到的是一个完全自动化的Kubernetes,它在Kubeflow和Prow支持下运行CI/CD数据科学工作流。我工作的源代码可以在kubernets -analysis GitHub知识库中找到,它包含了所有与源代码相关的内容以及原始数据。但是如何检索这些数据呢?这就是故事的开始。

获取数据

我实验的基础是纯JSON格式的原始GitHubAPI数据。必要的数据可以通过GitHub issues endpoint获取,它返回REST API中的所有请求和常规问题。我导出了大约91000个问题,并在第一次迭代中将请求拉入650 MiB data blob(数据块)。这花费了大约8小时的数据检索时间,因为可以肯定的是,GitHub API的速率是受限的。为了将数据放到GitHub存储库中,我通过xz(1)进行压缩。压缩到大约25mib大小的tarball,非常适合存储库。

必须找到一种定期更新数据集的方法,因为Kubernetes issues和pull请求会随着时间的推移而更新,而且还会创建新的请求。为了实现连续更新,而不必总是等待8小时,我现在获取上次更新和当前时间之间的delta 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个issues和pull请求在5分钟内得到更新!

有趣的是,Kubernetes的创始人之一Joe Beda创建了第一个GitHub问题,提到单元测试覆盖率太低。这个问题除了标题没有进一步的描述,也没有应用增强的标签,就像我们从最近的issues和pull请求中了解到的那样。现在必须更深入地研究导出的数据,以便对其进行有用的处理。

探索数据

在开始创建机器学习模型并进行训练之前,我们必须先了解数据是如何构建的,以及希望达成的总体目标。

为了更好地了解数据量,让我们看看在Kubernetes存储库中随着时间的推移创建了多少issues和pull请求:

> ./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的度量。这意味着在时间轴上,如果创建了一个issue或pull请求,我们将在y轴上添加一个,如果关闭,则减去一个。现在图表如下:

> ./main analyze --created-vs-closed

2018年初,Kubernetes项目通过fejta-bot引入了一些更强的生命周期管理。这将在问题长时间失效后自动关闭问题并拉取请求。这解决了大量问题,但不适用于相同数量的pull请求。例如,如果我们只查看pull请求的created vs closed度量。

> ./main analyze --created-vs-closed --pull-requests

总体影响并不明显。PR图中不断增加的峰值数量表明,随着时间的推移,项目进展得越来越快。通常,烛台图是显示此类波动信息的更好选择。我还想强调的是,这个项目的发展在2020年初似乎有所放缓。

在每次分析迭代中解析原始JSON并不是Python中最快的方法。我决定将更重要的信息,比如内容、标题和创建时间,解析为专门的issues和PR classes。这些数据也将被pickle序列化到存储库中,这样就可以独立于JSON blob更快地启动。

pull request和我分析的issues差不多,只是它包含了一个发布说明。

Kubernetes中的发行说明在PRs描述中写入一个单独的发行说明块,如下所示:

``release-note
I changed something extremely important and you should note that.
```

这些发行说明在发行版创建过程中由专门的发行版工程工具(如krel)解析,并将成为各种变更日志的一部分,.md文件和发行说明网站。这看起来很神奇,但最终,整个发布说明的质量要高得多,因为它们易于编辑,PR review人员可以确保我们只记录真实的面向用户的更改,而没有其他内容。

在进行数据科学研究时,输入数据的质量是一个关键。我决定把重点放在发行说明上,因为与issues和PRs中的简单描述相比,它们似乎具有最高的总体质量。除此之外,很容易解析,不需要去除各种issues和PR模板文本干扰。

标签,标签,标签

Kubernetes中的issues和pull请求会在其生命周期中应用不同的标签。通常通过单斜线(/)进行分组。例如,我们有kind/bug和kind/api-更改或sig/node和sig/network。要了解存在哪些标签组以及如何分布在存储库中,一种简单的方法是将它们绘制成条形图:

> ./main analyze --labels-by-group

看起来sig/、kind/和area/标签很常见。现在可以忽略size/之类的内容,因为这些标签是根据pull请求的代码更改量自动应用的。我们希望将发布说明作为输入数据,这意味着必须检查PRs标签的分发情况。pull请求的前25个标签是:

> ./main analyze --labels-by-name --pull-requests

同样,我们可以忽略像lgtm这样的标签,现在应该合并的每个PR都必须看起来不错。包含发布注释的Pull请求会自动应用发布注释标签。这并不意味着每个包含该标签的PR也包含release notes block。标签可以手动应用,并且release notesblock的解析从项目开始就不存在。这意味着一方面可能会丢失相当数量的输入数据。另一方面,可以关注尽可能高的数据质量,因为正确应用标签需要增强项目及贡献者的成熟度。

从标签组的角度来看,我选择关注kind/label。这些标签必须由PR的作者手动应用,它们在大量的pull请求中可用,并且也与用户面对的变更相关。除此之外,每个pull请求都必须进行类型/选择,因为它是PR模板的一部分。

好了,当只关注有发布说明的pull请求时,这些标签的分布是怎样的呢?

> ./main analyze --release-notes-stats

有趣的是,我们有大约7000个包含发行说明的请求,但是只有大约5000个应用了种类/标签。标签的分布不均匀,其中三分之一被标记为kind/bug。这让我想到了数据科学之旅中的下一个决定:构建一个二进制分类器,为了简单起见,它只能区分bug(通过kind/bug)和非bug(不应用标签的地方)。

现在的主要目标是,根据我们已经从社区获得的历史数据,对新发布的注释进行分类,看它们是否与bug有关。

在此之前,建议您使用./main analyze-h子命令来研究最新的数据集。您还可以查看我在分析存储库中提供的所有持续更新的资产。例如,这些是Kubernetes存储库中排名前25位的PR创建者:

建立机器学习模型

现在我们知道了数据集,可以开始构建第一个机器学习模型。在实际构建之前,我们必须预先处理从PRs中提取的所有发行说明。否则,模型将无法理解我们的输入。

第一自然语言处理(NLP)

开始时,必须定义一个想要训练的词汇。我决定从Pythonscikit-learn机器学习库中选择TfidfVectorizer。这个矢量化器能够从输入的文本中创建一个庞大的词汇表。这就是所谓的单词包,它选择的n元范围为(1,2)(单字和双字)。实际上,这意味着我们总是使用第一个单词和下一个单词作为单个词汇条目(双字母组合图)。我们也使用单个单词作为词汇条目(unigram)。TfidfVectorizer能够跳过出现多次的单词(max_df),并且需要最小数量(min_df)才能将单词添加到词汇表中。我决定不去改变这些值,因为我有一种直觉,即发行说明对于一个项目来说是特有的。

像min_df、max_df和n-gram范围的参数可以看作一些超参数。在机器学习模型建立后,这些参数必须专门进行优化。这一步叫做超参数调优,就是用不同的参数进行多次训练,并比较模型的精度。然后选择出最准确的参数。

在训练期间,矢量器将产生数据/特征。包含整个词汇表的json。这让我们很好地理解了这样一个词汇表的样子:

[
…
"hostname",
"hostname address",
"hostname and",
"hostname as",
"hostname being",
"hostname bug",
…
]

这就产生了大约50000个词条。以前对不同数据集的分析表明,根本不需要考虑这么多特性。一些通用数据集表明,20000的总词汇量就足够了,并且更高的词汇量不再影响准确性。为此,我们可以使用SelectKBest特性选择器来删除词汇表,只选择最重要的特性。不过,我还是决定选择前5万名,以免对模型的准确性产生负面影响。我们的数据量相对较低(约7000个样本)和每个样本的单词数很低(大约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

自动化一切

下一步是找到某种自动化的方法,用新数据不断更新模型。如果更改了存储库中的任何源代码,我希望获得关于模型测试结果的反馈,而不需要在机器上运行训练。我想利用Kubernetes集群中的gpu来更快地训练,并在PR被合并时自动更新数据集。

在Kubeflow pipelines的帮助下,可以满足这些要求中的大部分。pipeline是这样的:

首先,检查PR源代码,它作为outputartifact传递给所有其他步骤。然后,在一个持续更新的数据集上运行训练之前,逐步更新API和内部数据。训练后的预测试验证明,我们的改变并没有对模型造成严重影响。

我们还在pipeline中构建了一个容器映像。此映像将先前构建的模型、矢量器和选择器复制到容器中,并运行./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服务和under the hood的Istio ingress网关,来正确地将流量路由到集群并提供正确的服务集。

只有当pipeline在主分支上运行时,commit-changes和rollout步骤才会运行。确保在主分支和kfservice部署中始终有可用的最新数据集。Rollout Step创建了一个新的canary部署,它只接受50%的传入流量。金丝雀被成功部署后,它将被提升为该服务的新主实例。这是一种很好的方式,可以确保部署按预期进行,并允许在金丝雀之后进行额外的测试。

但是如何在创建一个pull请求时触发Kubeflowpipeline? Kubeflow目前还没有这方面的功能。这就是为什么我决定使用Prow ,Kubernetes测试基础设施项目用于CI/CD。

首先,24小时的定期作业确保存储库中至少每天都有最新的数据可用。然后,如果创建一个pull request, Prow将运行整个Kubeflow pipeline,而无需提交或任何更改。如果我们通过Prow merge pull请求,另一个作业在主服务器上运行并更新数据和部署。很简洁,不是吗?

自动标记新的PRs

Prediction API很适合测试,但我们需要一个真实的用例。Prow支持外部插件,可用于对任何GitHub事件执行操作。我编写了一个插件,它使用kfserving API根据新的pull请求进行预测。这意味着,如果我们现在在kubernetes分析存储库中创建一个新的pull请求,将看到以下内容:

Prediction API非常适合测试,但是现在我们需要一个真实的用例。Prow支持外部插件,可以用来对任何GitHub事件采取行动。我编写了一个插件,它使用kfserve API根据新的pull请求进行预测。这意味着,如果我们现在在kubernetes分析存储库中创建一个新的pull请求,我们将看到以下内容:

现在让我们根据现有数据集中的一个真正的bug来修改发布说明:

机器人编辑自己的评论,预测大约90%的kind/bug,并自动添加正确的标签!现在,如果我们改变它回到一些不同的-显然是错误的-发布说明:

机器人为我们工作,移除标签并告诉我们它做了什么!最后,如果将发布说明改为“无”:

bot删除了评论,减少了PR上的文本干扰。我演示的所有内容都在一个Kubernetes集群中运行,因此完全没有必要向公众公开kfserving API。这引入了indirect API速率限制,因为只有通过Prow bot用户才能使用。

如果你想尝试一下,可以在kubernetes-analysis中打开一个新的测试问题。这之所以有效,是因为启用这个插件是为了解决问题,而不仅仅是为了pull请求。

这样,我们就有了一个运行的CI bot,它能够根据机器学习模型对新发布的注释进行分类。如果机器人运行在官方的Kubernetes存储库中,那么我们可以手动纠正错误的标签预测。这样,下次训练迭代将会得到修正,并随着时间的推移产生持续改进的模型。完全自动化!

总结

感谢你读到这里!这是我在KubernetesGitHub repository中的数据科学之旅。还有很多其他的东西需要优化,例如引入更多的类(而不仅仅是kind/bug),或者使用KubeflowsKatib自动超参数调优。如果您有任何问题或建议,欢迎随时与我联系。

原文链接:

https://kubernetes.io/blog/2020/05/my-exciting-journey-into-kubernetes-history/

作者: Sascha Grunert

相关阅读:


视频公开课!从小白到专家,Istio技术实践之监控与跟踪


Istio实践避坑指南:10 大常见异常、最佳实践及解决方案


以业务为核心的云原生体系建设:不谋全局者,不足以谋一域(最详实篇)


从1到2000个微服务,2万+字长文详述云原生实践25步,史上最落地!

东方福利网推出个性化企业员工体检定制方案

上一篇

心随音动—NinaKiss糖果盒蓝牙耳机

下一篇

你也可能喜欢

数据科学为啥青睐Kubernetes?一场有趣的数据科学K8s之旅

长按储存图像,分享给朋友