吐核|Core Dump

笔记 随想 吐槽


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

解决rsyslogd在容器中无法正常使用的问题

发表于 2019-07-20

尝试在容器中运行rsyslog解决没有/var/log/secure日志的问题

1
2
3
4
# docker run -it --rm centos /bin/bash
# yum -y install rsyslog
# /usr/sbin/rsyslogd
# logger “this is a test”

会发现/var/log/message 中没有任何信息

因为在centos7中日志是被systemd托管的,所以默认配置都是systemd相关的

1
2
3
4
5
# The imjournal module bellow is nowused as a message source instead of imuxsock.
$ModLoad imuxsock # provides supportfor local system logging (e.g. via logger command)
$ModLoad imjournal # provides accessto the systemd journal
#$ModLoad imklog # reads kernelmessages (the same are read from journald)
#$ModLoad immark # provides --MARK-- message capability

默认启用的是imjournal这行,而我们用不上,所以把imjournal这行注释掉,同时保持imuxsock留着

与此同时配置中下面还有几个相关的配置需要修改

1
2
$OmitLocalLogging 改成off
$IMJournalStateFile imjournal.state #这行需要注释掉

启动rsyslogd 再看下还是不行,netstat -anlp 看并没有/dev/log的sock链接接出来
找了半天发现/etc/rsyslog.conf/下还有个listen配置一看还是和journal相关的,注释掉,再启动,ok了

K8S中的QOS有什么用?

发表于 2019-04-03 | 更新于 2019-07-20

按网上的介绍: “Kubernetes做为目前主流的容器集群管理平台,需要整体统筹平台资源使用情况、公平合理的将资源分配给相关pod容器使用,并且要保证容器生命周期内有足够的资源来保证其运行。 与此同时,由于资源发放的独占性,即资源已经分配给了某容器,同样的资源不会在分配给其他容器,对于资源利用率相对较低的容器来说,占用资源却没有实际使用(比如CPU、内存)造成了严重的资源浪费,Kubernetes需从优先级与公平性等角度提高资源的利用率。为了实现资源被有效调度和分配的同时提高资源利用率,Kubernetes针对不同服务质量的预期,通过QoS(Quality of Service)来对pod进行服务质量管理,提供了个采用requests和limits两种类型对资源进行分配和使用限制。对于一个pod来说,服务质量体现在两个为2个具体的指标: CPU与内存。实际过程中,当NODE节点上内存资源紧张时,kubernetes会根据预先设置的不同QoS类别进行相应处理”

再加上Qos这个词,看起来是k8s用来做服务质量保证的一个玩意,那我们具体看下QOS实际是怎么实现的。

Qos是Pod上的概念,有3种:

  1. Guaranteed: request=limit, Pod中所有容器都必须统一设置limits,并且设置参数都一致,如果有一个容器要设置requests,那么所有容器都要设置,并设置参数同limits一致,那么这个pod的QoS就是Guaranteed级别。注:如果一个容器只指明limit而未设定request,则request的值等于limit值
  2. Burstable: requset≠limit, Pod中只要有一个容器的requests和limits的设置不相同,该Pod的QoS即为Burstable
  3. Best-Effort: request & limit not set, 如果对于全部的resources来说requests与limits均未设置,该Pod的QoS即为Best-Effort

回顾下背景知识
Cgroup中CPU资源的限制原理:

  1. 软限制:cgroup目录下cpu.shares 文件中的值 1024表示1个核心 4096则是4个核心,在所有容器都跑满的情况下,按cpu.shares 的比例分CPU时间片,如果没有用满,则可以用超设置的核心;
  2. 硬限制:
     #cpu.cfs_quota_us 就是在这期间内可使用的 cpu 时间,默认 -1,即无限制
     #cpu.cfs_period_us 时间周期,默认为 100000,即百毫秒
           cpu.cfs_quota_us =  800000;
           cpu.cfs_period_us = 100000;  
    以上表示8核,在设置了cfs_quota_us的情况下,容器中的进程至多只能用到8核
    

Pod的Request和Limit:

  1. Request: 对应cpu.shares的值(做相应单位转换)
  2. Limit: 对应cpu.cfs_quota_us的值(做相应单位转换)

不过这个本来就是Pod上的设置,跟Qos本身没有太大的关系,这里唯一的体现是在cgroup目录下的文件路径不一样,/sys/fs/cgroup/cpu/kubepods/ 目录下会有besteffort,burstable,guaranteed 3个目录里面放相应Pod的Cgroup文件。

再实际去看下Qos在k8s中代码相关的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func GetContainerOOMScoreAdjust(pod *v1.Pod, container *v1.Container, memoryCapacity int64) int {
switch GetPodQOS(pod) {
case v1.PodQOSGuaranteed:
return guaranteedOOMScoreAdj
case v1.PodQOSBestEffort:
return besteffortOOMScoreAdj
}
memoryRequest := container.Resources.Requests.Memory().Value()
oomScoreAdjust := 1000 - (1000*memoryRequest)/memoryCapacity

if int(oomScoreAdjust) < (1000 + guaranteedOOMScoreAdj) {
return (1000 + guaranteedOOMScoreAdj)
}
if int(oomScoreAdjust) == besteffortOOMScoreAdj {
return int(oomScoreAdjust - 1)
}
return int(oomScoreAdjust)
}

只有这里用于计算每个Pod设置的OOMScoreAdj值:
对于Guaranteed级别的pod,OOM_ADJ参数设置成了-998,对于BestEffort级别的pod,OOM_ADJ参数设置成了1000,对于Burstable级别的POD,OOM_ADJ参数取值从2到999(根据内存申请量占总内存的比例算的一个值)。对于kuberntes保留资源,比如kubelet,docker,OOM_ADJ参数设置成了-999,表示不会被OOM kill掉。(OOM_ADJ参数设置的越大,通过OOM_ADJ参数计算出来OOM分数越高,当出现资源竞争时会越容易被系统kill掉。)

不过这里设置的这个值,只有在整个Node用完了全部内存时才会触发,但是通常情况下容器里面用超都会被自己的Cgroup的内存限制住,OOM掉。所以感觉也没什么明显的用处。

另外一个是kubelet上的preempt逻辑,这里比较诡,是kubelet重启后,发现有个Pod是自己的,但处于pending状态,然后node上会走调度器的逻辑把这个Pod GeneralPredicates下,但也是失败(正常这种调度就过不来,猜测可能是多调度器导致的),并且发现pending的原因是资源不满足类的,就看下有没有别的pod可以踢掉换成这个Pod,好诡异的逻辑。没碰到过。需要再细看下。而且这种踢pod的逻辑正常需要关掉比较好。

还有个东西在高版本的k8s代码中和Qos相关,高版本中如果开启了cpuManager功能,会针对Guaranteed的Pod进行绑核的操作。

总结下 QOS的功能 1. oom_score_adj 2. cpumanager绑定核心 3.kubelet上preempt 删pod排序(不是调度器里面那个)

容器的环境变量问题

发表于 2019-04-02 | 更新于 2019-07-20

今天mei问容器里面怎么拿环境变量,让我想起了之前帮广告部门查的一个ssh后环境变量丢失的问题,那就顺道总结下。

首先我们的目标是:

容器里的环境变量,对于跑在容器内的程序按理,应该直接就可以获得,即直接从各种语言的读取环境变量的方式就可以了。
docker帮我们实现了:启动的时候entrypoint拿到的是正确的环境变量,并且docker exec进入的时候环境变量是正确的,可获取到的。
但是用户有2种方式可能导致环境变量丢失:
可能用su - username的方式切换了账户导致环境变量丢失,注意这里虽然su - 的语义就是丢弃环境变量,但是我们认为k8s pod上设置的环境变量是全局可用的,我们也保证这样切换后设置的环境变量可用,当然你也可以不这么干
使用ssh的方式进入,这里又有两种 ssh进去进入login bash,或者就是ssh $ip xxxcommand 执行一个命令,进入一个nointeractive nologinbash的方式
这里2种其实都和bash对于环境变量文件的加载问题有关,所以看下bash这里的逻辑

背景知识:

bash有2种大的类型或者区别(这里不知道怎么描述)

  1. login:就是你登陆进去后获得的(第一个!)bash
  2. interactive:就是有终端提示符,可以敲命令那种。

相应的也有取非的情况,两两排列组合,bash一共有4种模式

  1. login+interactive 就是你ssh 登陆进去后那个
  2. login+nointeractive 不常见,主要是bash -l script.sh 有时候我们在某些系统的运行脚本设置里面会这么写(比如监控的自定义命令监控)
  3. interactive+nologin 就是你开的各种终端模拟器开的那些 比如gnome-terminal那些标签页
  4. nointeractive+nologin 这种其实容易被遗忘,但这就是上次查问题所在 ssh $ip xxxxcommand 的情况

与此同时,系统有一堆文件,bash会去读,具体就不一个个说明了,总结如下:
bash的每种模式会读取其所在列的内容,首先执行A,然后是B,C。而B1,B2和B3表示只会执行第一个存在的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

+----------------+--------+-----------+---------------+
| | login |interactive|non-interactive|
| | |non-login |non-login |
+----------------+--------+-----------+---------------+
|/etc/profile | A | | |
+----------------+--------+-----------+---------------+
|/etc/bash.bashrc| | A | |
+----------------+--------+-----------+---------------+
|~/.bashrc | | B | |
+----------------+--------+-----------+---------------+
|~/.bash_profile | B1 | | |
+----------------+--------+-----------+---------------+
|~/.bash_login | B2 | | |
+----------------+--------+-----------+---------------+
|~/.profile | B3 | | |
+----------------+--------+-----------+---------------+
|BASH_ENV | | | A |
+----------------+--------+-----------+---------------+

另外通常情况下~/.bash_profile 里面都会加载~/.bashrc ,~/.bashrc里面又会加载/etc/bashrc 所以你平常加的时候,看起来到处加了都生效,但其实是上面的顺序。
除此之外login bash中B1 B2 B3 是有一个就不执行另外一个,所以通常不建议使用.bash_login 然后其实.profile 的目的是兼容非bash的,比如csh之类的,优雅点,可以.bash_profile里面写bash相关的,然后在.bash_profile里面自己主动加载下.profile(考虑到这个时候bash看到有.bash_profile是不会去加载.profile的)

回到正题:

对于切换账户的情况,我们不可能一个个去改每个账户下的.bash_profile 所以我们统一在/etc/profile 里面处理。
处理方式2种:

  1. 如这里所说 cat /proc/1/environ | tr ‘\0’ ‘\n’ 然后再逐行加载的方式(我不推荐,我从不认为在有系统命令的情况下,自己去解析一个文件是个好方案)
  2. 系统启动的时候执行的脚本中导出下环境变量到另外一个文件中 然后/etc/profile写上去加载这个文件

对于ssh 直接执行命令的情况:
可以看到表中只认BASH_ENV指定的文件,意思是你ssh前先export BASH_ENV=/etc/profile 然后执行ssh $ip xxcommand bash会去加载这个文件,但是又有2个问题,一个是用户操作麻烦,另外一个是如果用户脚本的shebang没写bash又不认这个玩意。。。所以我们用pam_env.so的方案来解决。具体看https://linux.die.net/man/8/pam_env

1
2
#需要修改 /etc/pam.d/password-auth 成
auth required pam_env.so envfile=/etc/environment

这个时候会发现有个系统提供好的路径/etc/environment 来放环境变量,那么对于切账户的问题中的环境变量也刚好用这个文件,多么的优雅。

再留个问题,对于crontab 又是个什么情况呢? 欢迎评论

给k8s的deployment实现上线顺序和暂停点功能

发表于 2019-03-29

k8s的deployment是通过调整2个rs的副本数来实现容器的升级和更新的:删一个rs里面的pod,扩容新的spec的rs里面的pod。
那么上线的顺序可以转化成删除rs的顺序。

我们看k8s的代码,可以看到rs副本数的调整是以下函数控制的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func (rsc *ReplicaSetController) manageReplicas(filteredPods []*v1.Pod, rs *apps.ReplicaSet) error {
if diff > 0 { //diff 是当前pod数和rs上的replica数的差,大于0为缩容
if diff > rsc.burstReplicas {
diff = rsc.burstReplicas
}
klog.V(2).Infof("Too many replicas for %v %s/%s, need %d, deleting %d", rsc.Kind, rs.Namespace, rs.Name, *(rs.Spec.Replicas), diff)
​
// Choose which Pods to delete, preferring those in earlier phases of startup.
podsToDelete := getPodsToDelete(filteredPods, diff) //删pod的顺序在这个里面
​
rsc.expectations.ExpectDeletions(rsKey, getPodKeys(podsToDelete))
​
errCh := make(chan error, diff)
var wg sync.WaitGroup
wg.Add(diff)
for _, pod := range podsToDelete {
go func(targetPod *v1.Pod) {
defer wg.Done()
if err := rsc.podControl.DeletePod(rs.Namespace, targetPod.Name, rs); err != nil {
// Decrement the expected number of deletes because the informer won't observe this deletion
podKey := controller.PodKey(targetPod)
klog.V(2).Infof("Failed to delete %v, decrementing expectations for %v %s/%s", podKey, rsc.Kind, rs.Namespace, rs.Name)
rsc.expectations.DeletionObserved(rsKey, podKey)
errCh <- err
}
}(pod)
}
wg.Wait()

具体看getPodsToDelete的逻辑

1
2
3
4
5
6
7
8
9
10
11
func getPodsToDelete(filteredPods []*v1.Pod, diff int) []*v1.Pod {
// No need to sort pods if we are about to delete all of them.
// diff will always be <= len(filteredPods), so not need to handle > case.
if diff < len(filteredPods) {
// Sort the pods in the order such that not-ready < ready, unscheduled
// < scheduled, and pending < running. This ensures that we delete pods
// in the earlier stages whenever possible.
sort.Sort(controller.ActivePods(filteredPods))
}
return filteredPods[:diff]
}

可以看到是找到需要删除的diff数量的pod,具体的顺序是ActivePods sort完的熟悉,我们看下这个类型的自定义排序逻辑
并不是随机的,而是有一定的顺序。
顺序为unassigned < Pending< Unknown < Not Ready < higher restart counts < new start pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func (s ActivePods) Less(i, j int) bool {

// 1. Unassigned < assigned
// If only one of the pods is unassigned, the unassigned one is smaller
if s[i].Spec.NodeName != s[j].Spec.NodeName && (len(s[i].Spec.NodeName) == 0 || len(s[j].Spec.NodeName) == 0) {
return len(s[i].Spec.NodeName) == 0
}
// 2. PodPending < PodUnknown < PodRunning
m := map[v1.PodPhase]int{v1.PodPending: 0, v1.PodUnknown: 1, v1.PodRunning: 2}
if m[s[i].Status.Phase] != m[s[j].Status.Phase] {
return m[s[i].Status.Phase] < m[s[j].Status.Phase]
}
// 3. Not ready < ready
// If only one of the pods is not ready, the not ready one is smaller
if podutil.IsPodReady(s[i]) != podutil.IsPodReady(s[j]) {
return !podutil.IsPodReady(s[i])
}
// TODO: take availability into account when we push minReadySeconds information from deployment into pods,
// see https://github.com/kubernetes/kubernetes/issues/22065
// 4. Been ready for empty time < less time < more time
// If both pods are ready, the latest ready one is smaller
if podutil.IsPodReady(s[i]) && podutil.IsPodReady(s[j]) && !podReadyTime(s[i]).Equal(podReadyTime(s[j])) {
return afterOrZero(podReadyTime(s[i]), podReadyTime(s[j]))
}
// 5. Pods with containers with higher restart counts < lower restart counts
if maxContainerRestarts(s[i]) != maxContainerRestarts(s[j]) {
return maxContainerRestarts(s[i]) > maxContainerRestarts(s[j])
}
// 6. Empty creation time pods < newer pods < older pods
if !s[i].CreationTimestamp.Equal(&s[j].CreationTimestamp) {
return afterOrZero(&s[i].CreationTimestamp, &s[j].CreationTimestamp)
}
return false
}

我们加个自定义的顺序逻辑,放到最前面 ,这样我们只要给pod打上不同数值的annotation,就可以控制rs删除pod的顺序也就控制了deployment更新pod的顺序,核心代码如下,具体见提交的github pr

1
2
3
4
5
6
7
8
9
//0. controller.alpha.kubernetes.io/delete-priority=1 < controller.alpha.kubernetes.io/delete-priority=0
//If pod has annotation controller.alpha.kubernetes.io/delete-priority the larger is small
if s[i].Annotations[DeletePriorityPodAnnotationKey] != s[j].Annotations[DeletePriorityPodAnnotationKey] {
s1, err1 := strconv.Atoi(s[i].Annotations[DeletePriorityPodAnnotationKey])
s2, err2 := strconv.Atoi(s[j].Annotations[DeletePriorityPodAnnotationKey])
if err1 == nil && err2 == nil {
return s1 > s2
}
}

至于暂停点的功能:
目前k8s是通过kubectl rollout pause deployment.v1.apps/nginx-deployment实现的,十分依赖人工操作的时机。我们也可以仿照上面的方法,加个annotation打到pod上,rs删到这个pod的时候调用下上面这个命令的内部调用,暂停下。然后业务层,再提供kubectl rollout resume deployment.v1.apps/nginx-deployment的入口,允许用户继续。

具体实现我还没写,现招人实现提交给github上,看这篇文章的同学有没有兴趣研究下,没错说的就是你。

GPU Volunteer Computing

发表于 2019-03-27 | 更新于 2019-03-29

首先这是个没有实现的东西,只是我的一个设想,其次虽然不想浪费今天发送的机会,但是今天有点心情不好,按倩姐的说法就是有点丧,所以就简单的描述下。

这个Idea来源于bvc (百度第一个最高奖,利用空闲的cpu资源来做计算的服务),我们把这个方案搬到GPU上

  1. 需要先实现GPU的共享机制(按之前的文章,包括业界方案已经可以实现)

  2. 需要有个分布式存储服务用户存储tensorflow的checkpoint文件,用于中断后恢复(也是一大把)

方案:

  1. 对于训练服务,我们可以将训练的Pod混布到Serving服务的GPU卡上,这里可能需要实现特殊的调度策略,假设Serving的容器是request 是1个GPU的情况下(但其实资源使用很低),我们需要能把混布的Pod调度过去;

  2. 需要用Daemonset之类启动个服务监视混布的Pod占用的资源情况,占用值超过设定的值,就把这个pod踢掉,让调度器重新调度

  3. 只对于算子Worker进行以上设计,PS角色还是尽量保证其稳定性(通常PS也不需要GPU),并且worker存日志到分布式存储上,不然重启就没了

阅读全文 »

Kubernetes v1.14中Pid Limit功能实现分析

发表于 2019-03-26 | 更新于 2019-03-29

今天看到kubernetes发布了v1.14版本,发布说明中有个PID限制功能,引起了我的兴趣,2方面原因1:之前matrix有这个功能,但是k8s没有 2:有实际场景需求。那么来看下具体是怎么实现的

找到官方的release note链接,看到

Pid Limiting is Graduating to Beta (#757)

Prevents a pod from starving pid resource

Ability to isolate pid resources pod-to-pod and node-to-pod kubernetes/kubernetes: #73651kubernetes/enhancements: #757 [kep]

#757 是 https://github.com/kubernetes/enhancements/issues/757 是个需求描述,说要增加这个功能,不是实现。

#73651 https://github.com/kubernetes/kubernetes/pull/73651/files 实现了node层面的pid限制,提供了类似之前预留cpu的system-reserved功能,可以预留pid

还有个release note没写,需要自己找下#57973

https://github.com/kubernetes/kubernetes/pull/57973 这个实现了pod层面的pid限制

1
2
3
4
5
6
7
8
9
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.SupportPodPidsLimit) {
supportedSubsystems = append(supportedSubsystems, &cgroupfs.PidsGroup{})
}
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.SupportPodPidsLimit) && cgroupConfig.ResourceParameters.PodPidsLimit != nil {
libcontainerCgroupConfig.PidsLimit = *cgroupConfig.ResourceParameters.PodPidsLimit
}
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.SupportPodPidsLimit) && cgroupConfig.ResourceParameters.PodPidsLimit != nil {
libcontainerCgroupConfig.PidsLimit = *cgroupConfig.ResourceParameters.PodPidsLimit
}

好嘛,其实就是利用了pids这个cgroup https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt做了下限制,这个功能怎么这么久才加。。。

再回到#73651

1
2
3
4
5
6
7
8
9

// Process IDs are not a node allocatable, so we have to do this ad hoc
pidlimits, err := pidlimit.Stats()
if err == nil && pidlimits != nil && pidlimits.MaxPID != nil {
allocatablePIDs = resource.NewQuantity(int64(*pidlimits.MaxPID), resource.DecimalSI)
allocatablePIDs.Sub(resource.MustParse(pids))
}
return allocatableCPU, allocatableMemory, allocatablePIDs
} }

拿了下机器上总的数减去配置的数量,限制了所有pod总的pid数量,这样给kubelet一类的程序留下了配置的数量。

总结下有的时候弄久了有些事情反而忘记了,cgroup其实有个pids subsystem的。我还以为是类似于tcp_throttle一样是狼厂自研的呢。

另外提示个点:这个pids系统限制的是线程+进程数,可以理解成pstree -pl看到的数量。

与此同时又想到个点,记得之前matrix还做了容器内可以bind端口的限制,不过好像这个在现在的场景是用不到的。另外还有个利用subtreequota 什么的实现的disk space子系统来控制用量,不过这个现在大部分用我之前提过的方案解决了。

一种vGPU方案:控制GPU内存使用上限

发表于 2019-03-20 | 更新于 2019-03-22

在已经实现了k8s层面的GPU按小数申请支持的情况下(阿里是按内存数量支持),我们需要有个实际的资源限制的方案。
目前不太有好的办法控制gpu的算力(其实有,局限性比较大)。我们先控制下gpu显存的使用,具体实现就不方便贴了,讲下思路和效果

  1. 首先需要了解cuda api,所有框架如果细看代码都会发现底层是使用的cuda api去操作nvidia 的gpu,python只是个binding
  2. 需要了解hook 系统api的方法( 搜索关键词ring3 hook)

我们从tensorflow的运行日志去翻代码 找设备初始化信息的地方 Found device ,跟进去,可以发现实际是调用了cuMemGetInfo这个函数获取的信息。 (这个页面中mem相关的函数都建议看下)

那么我们只需要重新写个so,实现这个函数就可以了,这个函数根据我们自己的逻辑设置最大的内存上限,与此同时由于cuMemGetInfo ( size_t free, size_t total ) 这个函数还返回了free的情况,所以我们也需要记录free的值,可以hook掉cuMemAlloc ,申请的时候记录下各个容器的使用情况,也可以利用外围的监控程序获取信息

最终实现的效果,设置环境变量后tensorflow看到的内存值是我们设置的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@cnn2-1553057347-worker-0:~# export GPU_MEMORY=2147483648
root@cnn2-1553057347-worker-0:~# python
Python 3.5.2 (default, Nov 23 2017, 16:37:01)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import tensorflow as tf
>>> sess =tf.Session()
2019-03-22 14:57:33.394861: I tensorflow/core/platform/cpu_feature_guard.cc:140] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.1 SSE4.2 AVX AVX2 FMA
hacking gpu memory from GPU_MEMORY env by lovejoy. mem=<2147483648>
2019-03-22 14:57:33.554589: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1344] Found device 0 with properties:
name: Tesla K20c major: 3 minor: 5 memoryClockRate(GHz): 0.7055
pciBusID: 0000:02:00.0
totalMemory: 2.00GiB freeMemory: 1.00GiB
2019-03-22 14:57:33.554637: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1423] Adding visible gpu devices: 0
2019-03-22 14:57:33.843047: I tensorflow/core/common_runtime/gpu/gpu_device.cc:911] Device interconnect StreamExecutor with strength 1 edge matrix:
2019-03-22 14:57:33.843094: I tensorflow/core/common_runtime/gpu/gpu_device.cc:917] 0
2019-03-22 14:57:33.843106: I tensorflow/core/common_runtime/gpu/gpu_device.cc:930] 0: N

如何实现系列三:如何自己实现一个CNI插件

发表于 2019-03-12

其实现在cni插件挺多的,最出名的就是calico了,然后还有几个很有特色的https://github.com/cilium/cilium https://github.com/cni-genie/CNI-Genie

正常情况下我们直接用这些开源的实现就好,不过通常情况下,我观察了下外面的分享的,好多公司都有类似需求,需要保持IP不变,那么就需要我们对相应的实现进行改造了。所以这里干脆讲下如果自己直接实现一个CNI,该如何实现。

我们以阿里云的CNIterway举例,主要看cni.go这个文件中的代码。

首先CNI我们需要知道它被调用的环节
CNI
可以看到CNI被调用的时候netns已经创建好了,我们需要做的事情就是配置这个netns,需要实现https://github.com/containernetworking/cni/blob/master/SPEC.md 这个spec描述的内容,
可以简单的实现引入github.com/containernetworking/cni ,然后我们其实只需要实现2个接口

1
func cmdAdd(args *skel.CmdArgs) error {

和

1
func cmdDel(args *skel.CmdArgs) error {

先不管Del,先看阿里这个Add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
func cmdAdd(args *skel.CmdArgs) error {
versionDecoder := &cniversion.ConfigDecoder{}
confVersion, err := versionDecoder.Decode(args.StdinData)
if err != nil {
return err
}

cniNetns, err := ns.GetNS(args.Netns) //获得当前的NS
if err != nil {
return err
}
//...
allocResult, err := terwayBackendClient.AllocIP( //阿里自己的东西去拿ip,我们实现的话也是类似这样逻辑
timeoutContext,
&rpc.AllocIPRequest{
Netns: args.Netns,
K8SPodName: string(k8sConfig.K8S_POD_NAME),
K8SPodNamespace: string(k8sConfig.K8S_POD_NAMESPACE),
K8SPodInfraContainerId: string(k8sConfig.K8S_POD_INFRA_CONTAINER_ID),
IfName: args.IfName,
})
hostVethName := link.VethNameForPod(string(k8sConfig.K8S_POD_NAME), string(k8sConfig.K8S_POD_NAMESPACE), defaultVethPrefix)
var (
allocatedIPAddr net.IPNet
allocatedGatewayAddr net.IP
)

switch allocResult.IPType {
case rpc.IPType_TypeENIMultiIP:


err = networkDriver.Setup(hostVethName, args.IfName, subnet, gw, nil, int(deviceId), ingress, egress, cniNetns)
//阿里自己的网络驱动吧,其实也类似,如果我们有类似的东西的话需要去设置下
if err != nil {
return fmt.Errorf("setup network failed: %v", err)
}
allocatedIPAddr = *subnet
allocatedGatewayAddr = gw

case rpc.IPType_TypeVPCIP:
if allocResult.GetVpcIp() == nil || allocResult.GetVpcIp().GetPodConfig() == nil ||
allocResult.GetVpcIp().NodeCidr == "" {
return fmt.Errorf("vpc ip result is empty: %v", allocResult)
}
...
var r types.Result
//阿里这是调用ipam插件去设置网络
r, err = ipam.ExecAdd(delegateIpam, []byte(fmt.Sprintf(delegateConf, subnet.String())))
if err != nil {
return fmt.Errorf("error allocate ip from delegate ipam %v: %v", delegateIpam, err)
}
podIPAddr := ipamResult.IPs[0].Address
gateway := ipamResult.IPs[0].Gateway

ingress := allocResult.GetVpcIp().GetPodConfig().GetIngress()
egress := allocResult.GetVpcIp().GetPodConfig().GetEgress()

err = networkDriver.Setup(hostVethName, args.IfName, &podIPAddr, gateway, nil, 0, ingress, egress, cniNetns)
if err != nil {
return fmt.Errorf("setup network failed: %v", err)
}
allocatedIPAddr = podIPAddr
allocatedGatewayAddr = gateway
case rpc.IPType_TypeVPCENI:
if allocResult.GetVpcEni() == nil || allocResult.GetVpcEni().GetServiceCidr() == "" ||
allocResult.GetVpcEni().GetEniConfig() == nil {
return fmt.Errorf("vpcEni ip result is empty: %v", allocResult)
}
var srvSubnet *net.IPNet
_, srvSubnet, err = net.ParseCIDR(allocResult.GetVpcEni().GetServiceCidr())
if err != nil {
return fmt.Errorf("vpc eni return srv subnet is not vaild: %v", allocResult.GetVpcEni().GetServiceCidr())
}
...
ingress := allocResult.GetVpcEni().GetPodConfig().GetIngress()
egress := allocResult.GetVpcEni().GetPodConfig().GetEgress()
//这个看起来又是另外一个阿里的网络类型
err = nicDriver.Setup(hostVethName, args.IfName, eniAddrSubnet, gw, nil, int(deviceNumber), 0, 0, cniNetns)
if err != nil {
return fmt.Errorf("setup network for vpc eni failed: %v", err)
}
allocatedIPAddr = *eniAddrSubnet
allocatedGatewayAddr = gw
default:
return fmt.Errorf("not support this network type")
}
//这个是关键,其实我们正常简单实现就是在cmdAdd中加自己的逻辑,并且完成下面这个结构的拼装,其他就是和框架的事情了。
result := &current.Result{
IPs: []*current.IPConfig{{
Version: "4",
Address: allocatedIPAddr,
Gateway: allocatedGatewayAddr,
}},
}

return types.PrintResult(result, confVersion)
}

从上面看阿里的代码可以看到,其实大部分都是他们自身的逻辑,我们真正需要实现的其实就只有去获得IP的功能,然后我们可以在这个地方加入的自己逻辑,保持一个分组下容器的IP不变。
那么如果我们需要去实现一个标准库已经支持的网络(bridge ipvlan macvlan等)的CNI的时候就异常简单了
比如我们实现一个macvlan的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func cmdAdd(args *skel.CmdArgs) error {
types.LoadArgs()//加载参数,取你需要的东西
/*type CmdArgs struct {
ContainerID string
Netns string
IfName string
Args string //这里应该是k8s传过来,只看到有K8S_POD_NAMESPACE K8S_POD_NAME K8S_POD_INFRA_CONTAINER_ID
Path string
StdinData []byte
}*/

//写你自己的逻辑去拿下IP
//填写下面的结构
result := &current.Result{
IPs: []*current.IPConfig{{
Version: "4",
Address: allocatedIPAddr,
Gateway: allocatedGatewayAddr,
}},
}

return types.PrintResult(result, CONF_VERSION)
}

除此之外需要加个配置文件,符合下面的结构

1
2
3
4
5
6
7
8
9
10
11
12
// NetConf describes a network.
type NetConf struct {
CNIVersion string `json:"cniVersion,omitempty"`

Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
Capabilities map[string]bool `json:"capabilities,omitempty"`
IPAM struct {
Type string `json:"type,omitempty"`
} `json:"ipam,omitempty"`
DNS DNS `json:"dns"`
}

比如

1
2
3
4
5
6
7
8
{
"name": "macvlan",//随意
"type": "macvlan",//需要有这么一个二进制程序,编译官方的就可以了
"master": "eth1",
"ipam": {
"type": "ipam",
}
}

建议阅读:
http://dockone.io/article/2578
https://kubernetes.feisky.xyz/cha-jian-kuo-zhan/network/index
https://jimmysong.io/kubernetes-handbook/concepts/cni.html
https://www.lijiaocn.com/项目/2017/05/03/cni.html
https://yucs.github.io/2017/12/06/2017-12-6-CNI/
https://page.pikeszfish.me/2018/01/26/write-cni-plugin-with-shell/
https://blog.51cto.com/tryingstuff/2165805

这块内容还是有点问题,有空细看下CNI标准本身的实现,以及kubelet相关的实现,再写篇文章。

使用jsonpath自定义kubectl的输出格式

发表于 2019-03-11

原始需求其实是想知道查看pod用的GPU数量,找个jsonpath的选项可以很好的解决这个问题

jsonpath接少见jsonpath,还有个在线的解析器http://jsonpath.com/

查看POD所用的资源: kubectl get pod -n default -o=jsonpath=’{range .items[]}{.metadata.name}{“\t”}{.spec.containers[].resources.limits}{“\n”}{end}

查看podd用调度GPU数量: kubectl get pod -n default -o=jsonpath=’{range .items[]}{.metadata.name}{“\t”}{.spec.containers[].resources.limits.alpha.kubernetes.io\/nvidia-gpu}{“\n”}{end}’

其他更多的参考https://gist.github.com/so0k/42313dbb3b547a0f51a547bb968696ba

巧妙的在pod启动前将进程丢到pod的cgroup中

发表于 2019-03-11

给CFS实现FlexVolumePlugin的时候,有个问题,由于CFS的Client是运行在物理机上的,Client本身的资源是不受控制的。本意打算给所有Client套一个cgroup统一控制,但是后面仔细考虑了下,每个client是给不同的pod使用,并且client的资源使用和业务的写入读取速度以及数据量相关,所以考虑还是把client丢到业务pod的cgroup中,受业务pod的资源限制统一管理。

这里其实一个麻烦点是:volumemanager是在pause的container启动前就已经进行的,所以脚本执行的时候还没有cgroup目录生成,,然后FlexVolume的程序是要提前退出的,那么我们只能先启动这个进程,然后压后台等容器生成咯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
putPidToCgroup() {
cat > cgroup.sh <<"EOF"
#!/bin/bash
PID=$1
POD_UID=$2
for i in `seq 1 10`;do
PAUSE_ID=$(docker ps | grep $POD_UID | grep pause | awk '{print $1}')
if [ x"$PAUSE_ID" == x ];then
sleep 10
else
CGROUP_PARENT=$(docker inspect $PAUSE_ID 2>/dev/null | jq -r .[0].HostConfig.CgroupParent )
if [ X"${CGROUP_PARENT}" != X"" ];then
echo $PID >> "/sys/fs/cgroup/cpu"$CGROUP_PARENT/tasks
echo $PID >> "/sys/fs/cgroup/memory"$CGROUP_PARENT/tasks
break
fi
fi
done
EOF
chmod +x cgroup.sh
nohup ./cgroup.sh $1 $2 &>/dev/null &
}

12…6

lovejoy

今天不开心

56 日志
12 分类
13 标签
Github Twitter
© 2019 lovejoy
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Pisces v7.0.1