Golang在Kubernetes语境下的编程范式

阅读 1074
收藏 16
2017-10-01
原文链接:mp.weixin.qq.com

前言

本文根据Gopher Meetup杭州站嘉宾张磊的演讲进行整理,演讲主题为《Kubernetes语境下基于Golang的编程范式》,本文将从如下几个方面介绍:

1、Kubernetes项目选择Golang的原因

2、Kubernetes的设计模式

3、Controller Demo 演示

4、Kubernetes项目的编程范式

5、总结

Kubernetes项目为什么选择Golang?

第一部分首先什么是Kubernetes。Kubernetes的定位是非常明确和简单,就是容器的编排与调度管理的框架,它是由谷歌发起的,也是谷歌Borg/Omega项目的开源衍生项目。它是目前世界上开源项目中最流行的项目之一(Linux排第一,Kubernetes排第四),这也就意味着它的社区活跃度以及项目的发展速度是相当高。像这样一个快速迭代的平台级项目正是用Golang实现的,而且相比其他的项目,它更依赖Golang,这一方面是因为Golang和谷歌之间的紧密关系,另一方面是因为项目在写和开发的过程中,谷歌工程师引入了很多独有的设计模式在里面,这也是我们要谈的内容。

    和其他的项目不一样,很多项目说采用Golang的原因大多都是Golang的开发很方便,性能很好,或者并发编程很优秀。但对Kubernetes这样的项目来说,这些都不算是主要的理由。毕竟谷歌Borg项目是c++写的,如果现在要把这样类似的一个东西作为开源项目放出来,为什么不用C++呢?最起码内部的库可以共用,而且C++写一个平台级别的项目也是天经地义。但是,Kubernetes项目最先考虑的不是这些,而是在座的各位。什么意思?Kubernetes需要Golang的开发者们来支持这个项目,这是开源项目当时想到的最核心的一个点。可能大家觉得有点诧异,我们都知道谷歌这个公司开源方面做得其实不是很好,在Kubernetes之前,谷歌开源的东西靠谱程度不高,但是在Kubernetes之后,包括后面的TensorFlow都是非常巨大的成功,为什么发生这样一个变化?正是因为谷歌第一次看到了社区对于这些项目的成功所扮演的角色,这也是Golang这样一个处于成长期的编程语言能够带来的一个非常巨大优势。我这里还有一个例子,就是LMCTFY容器项目。这个是Google在紧随着Docker开源的一个内部的容器实现,C++写的, 我相信没有多少人用过。然而这项目本身比同时期的Docker要强大很多,功能非常完善,尤其是它的资源管理部分,毕竟因为也大家知道,cgroup本身就是谷歌的东西。可是这样的项目都没来得及和Docker发生过PK就直接死掉了,最后大家总结,甩锅到了C++头上。什么意思?大家知道Docker之所以会成功,是因为Docker可以给我们带来一个友好简洁的,可以极其方便地去操作的Operation System Container 的UI或者说是API。而LMCTFY失败在哪里呢?它把User Friendly这个东西给抛弃掉了,你可以去试一下,它的编译和安装都特别麻烦,有时候还需要给kernal打pache,这是第一个问题,它本身C++这样的项目跟编译,跟debug都是一个系统工程。第二个原因也是最重要的一个原因,contributor的门槛太高了,Docker之所以成功就在与它背后的1400个Controlbuter,但是对于LMCTFY这个项目就免谈了,给它做贡献实在是太麻烦。这就是为什么做Kubernetes这个项目的时候,大家根本没有选择C++。大家可以想一下,Google用c++去做了开源容器,大家甚至知道,这个开源容器就是Google自己在用的那个容器,就这样的东西拿出来用户却不能handle,如果再做一个基于C++的容器的管理项目,庞大如Kubernetes这样的项目,用户可以hold住吗?就像有人埋怨说Google你怎么这么抠,不开源Borg给我们?假设真开源了Borg, Google拿出来给你扔在这,几百万行C++写这么个东西放在这里,在座诸位有几个人有信心把这个东西弄起来?

这就是为什么Kubernetes为什么会非常紧密的Golang、Golang社区走在一起的最主要原因。

鉴于有些人不是Kubernetes的用户,所以单独介绍一下Kubernetes这个项目。Kubernetes项目所关注的核心就是Container。我现在有一个容器,我有了这个容器之后,Kubernetes提出的第一个理念就是:不应该管单个的容器,而应该管容器组。这个概念类似我们操作系统里面的进程和进程组的关系,就是我写一个操作系统,写一个high level的操作系统,可能并不是管一个单一进程,我是希望管一个进程组,这样的概念在Kubernetes里面称之为co-scheduling,即几个容器之间是有相互关系的。Kubernetes里认为是几个容器之间有相互的关系,称之为Pod。pod是在Kubernetes里面原子的调度的单位,也是它管理作业所使用的最基础模型,所以它不是直接管Container,当然我一个Pod可以只有一个Container。

第二个概念,Pod可以有很多副本,这样我可以处理更多流量,弹性伸缩,这样的需求称之为scale,这样的操作会在Kubernetes会创造出一系列同样的pod,这样的一组Pod在Kubernetes中怎样去描述,我们称之为Deployment。Deployment描述的是,第一,Pod本身怎么定义,第二,我这个Pod有几个副本。

第三个问题现在要访问这一堆Pod,我怎么去统一访问他们,我去做一个带自动发现功能的LB,这个Pod Replicas的LB在Kubernetes称之为Service。再继续深入一步,在这个Service有了还不行,我想把它暴露在外网上,这个东西在Kubernetes里称之为Ingress。Ingerss的实现可以是Nginx,可以是HAproxy,甚至是F5都可以,关键是它可以提供L7的load balance,对外暴露出来。

接下来有用户说这个Pod比较特殊,我要跑成一个Daemon,要在每一个节点上跑一个副本,不能多不能少,这样叫Daemon,这样的模型在Kubernetes怎样去操作,我们称之为DaemonSet。在Kubernetes里它会保证这样的Pod永远只有一个实力在每个机器上,哪怕机器宕机了仍然会给你去scheduel。现在要求是我的Pod,我的容器是有State的,比如是部署一个Cassandra, 他的编号1和编号0的两个节点是不一样的,你这个怎么去处理,我们叫这个叫Topology State,拓扑结构的状态,第二种情况,我要部署MySQL,我的两个Replicas,我是要做sharding,数据不一样,要求你的Pod部署起来以后,要求你里面的存储数据能和instance绑定,这也是一种状态,叫做Storage State,这个事情怎么handle?在Kubernetes里面统一有一个模型,叫StatefulSet,去处理这样的事情。‘

还有的Pod更特殊,只跑一次,一跑完就退出了,比如我算1加1等于2,返回2就退出,这样的一个Job其实是一个run once的模型,这个我们在Kubernetes里面称之为Job。再特殊一点,这个Job不仅能跑一次,还要定时跑,这个称之为CronJob,遵守的是正常的Unix的CronJob格式,比如某天某时某分。

另外说一个用户数说我的这个Pod要使用,比如要访问MySQL,需要MySQL的User Name和Password,这种信息我往哪里写,在Kubernetes可以把这样的信息统一写到Etcd里加密存起来,这样的对象称之为Secret。比如用户又说了,我这个对象是需要加载配置文件的,我配置文件往哪里放,需要给我解决掉,Kubernetes也把这些东西统一放到Etcd存起来。所以如此的种种,大家可以看到Kubernetes是一个很平的项目。它用来处理作业类型是依靠定义好的特殊的Model,你使用它的模型去解决要解决的问题。这是Kubernetes设计的非常重要的一个点。

现在Pod创建好了,我现在要求它能Auto Scalling,可能是根据CPU使用量,也可能根据我的QPS这样的资源变化,这怎么办呢?像这样的东西,在Kubernetes也是通过一个Model处理,只不过说这个Model呢,叫Pod Auto Scaller,本身就像一个小的Daemon,来帮你去做这个scale,这个也是一种Model,跟其他一样都是平级的,都是一种Model,大家看过这样一个图就知道,Kubernetes原来是这样一个项目。

那么它是怎么去用呢?我这边随便列几条,比如说,你可以kubectl run ,和docker run是一样的,指定我的Replicas 等于3,就是说我的Nginx要跑三份,这也可以。但是我本人比较喜欢第二种,写个yaml文件,把这些参数都配进去,比如说我的Image是Nginx,Replicas我要三个,我比较喜欢这种,因为我的配置文件我可以统一管起来,然后我直接kubectl create-f,用这个文件去弄就完了。再复杂的比如说我刚讲的那个HPB,Auto-Scaler也可以通过这个配置文件去写,我需要去Scaler Target,就是我刚刚前面提到Nginx,我一匹配,我scale最多10个最少是1个,我的Target Resource是什么,比如我这边定的CPU,同样可以改成任何一个Resource Metrics所支持的Metadata格式,甚至说Resource Metrics格式不支持,也可以通过定义一个Resource的一个Model进去来描述他,比如说QPS,或者说我的Host Machine的数据都可以。然后完成后就是 create-f,从这个文件启动。

Kubernetes的设计模式

回到我们的议题上来,Kubernetes它工作的整个核心都是围绕这些Etcd里面存的对象来工作的,这就是我们称这之为面向API Object的编程模型,简单来说就是Container里的“面向对象”,这样的模型有两个操作:一个是我在Etcd里面创建一个对象,接下来是Kubernetes要做的事情,它会通过一种Control Loop的编程范式,来帮你去Reconcile调协所有的API Object,这个Kubernetes的工作,什么叫做调协,比如说我现在创建一个Pod,那么接下来Pod的启动,更新,删除都是通过我这个Controller来获取我这个Etcd里面的Pod资源变化来决定下一步,比如说,我多了一个Pod,会触发我一个On-Add的操作,On-Add了之后到底要干嘛,是交给Kubernetes还是scheduller,要把这个逻辑写进去,通过一个Control Loop触发这种的方式,看一个例子大家就知道了。

比如说我要创建一个Pod,很自然这个请求被交给Api-Server存到Etcd里面去,那么接下来会发生什么,首先Etcd里面会多这样一个对象,叫做Pod,然后关键来了,Kubernetes里面的所有组件,Scheduller,Kubelet等等,他们全部维护了自己的一个Control Loop,相当于在不断地循环,比如说像Scheduller,它就watch Ectd里面新增加Pod,当发现新增加Pod之后,他就去跑一遍调度算法,给你找到一个合适的node匹配起来,这个时候schedule做完了,做完之后它会把Pod的这个Node字段给你填成一个目标的Node,这个Scheduller也就完事了,不会做别的事情,它继续跑它的Control Loop,与此同时呢,Etcd里面这个Pod的Node字段变了,而kubelet也是一个单独的Control Loop,它获取的Pod Staters Node字段的变化,当他发现我的这个Pod Node字段被新添了或更改了,然后再一匹配发现这是Node的名字是我,他就会把这个数据拿下来,在我这个机器上去驱动。

大家可以看到整个Kubernetes里面的所有组件,他们之间的协同都是通过来watch Etcd里面它所关心的Object的变化,然后再决定这个变化之后我要做什么。像这样的一种设计模式我们称之为Controller  ,但是注意的是它并不是事件驱动,它是Level Driven,而不是Edge Driven(事件驱动),什么叫做事件驱动?我这边举一个例子,我一个设备的CPU说,我现在ready了,你可以来找我给我写数据,他会怎么办,可以发一个脉冲,CPU发现了这个脉冲知道你已经ready,这个是事件,因为你需要确认一个Event,然后CPU需要有这个能力read这个Event,需要Capture这个Event,这是一种。像Openstack和CloudFoundry都是这样,需要内置一个message queue来去处理这个Event,但是在Kubernetes不是这样,为什么我们叫它Level Driven?直接把我这个重设置为一个新值,从一个低电频之间变为高电频,不会发生第二个变化,直到你处理完或者是你状态发生一个新的变化,除此之外,这个Level是不变的,所以对于CPU来说,它不需要去关心Event,不需要关心特定事件,它只需要watch我的状态是否发生了变化,如果要去比的话会发现各有优劣,但对Kubernetes希望这么做的原因,是希望简化编程模式。

这样的Controller就是Kubernetes设计得最核心的点,无论是Kubernetes还是Scheduller,还是Controller Manager,它们可以全部简单看或简单或复杂的Controller。他们每天在那里跑循环是干嘛呢?首先从Etcd里拿到所关心的Object的Design状态,然后再想办法拿到我这个集群里当前object的实际状态,比如它实际跑了9个,这时候说明需要创建一个新的Pod,所以根据我的Dive出来的change来决定我下一步要干什么,这是所有Controller工作的样子。Kubernetes里提供这样的Controller的机制给你,一方面能够简化它的架构,另一方面是希望你自己去写Controller来扩展Kubernetes的能力。这才是我们今天要聊的最核心的东西,可能有人会之一,说我的用户为什么要去扩展Kubernetes的能力呢?原因还在这个图上面,既然 Kubernetes通过Etcd的Object 去管理所有的东西,很自然有一个需求我的Object不在Kubernetes的定义里怎么办法?

典型的例子,我们可以定义一个对象叫Network,这个Kubernetes原生是没有的,因为这个Network有不同自己的实现,我没有办法定义Network应该怎么去设置。像这样的Network对象,要让Kubernetes管理,到Pod能去用,就需要把这个Object放到Kubernetes里管理,这样当一个Network出现之后,可能需要把这个Network和Pod绑定,然后做什么事情,才有接下来做这个事情的一个理由。

Controller Demo 演示

接下来我们写一个小Demo,就是写一个Controller,Controller写什么对象进去,我想了半天,决定把“Asta Xie”写进去。也就是把谢大邀请进Kubernetes的社区里面,邀请谢大到Kubernetes的api里面。然后希望写一个Controller,我对“Asta Xie”这个对象的增删改,希望能够操作,这就是Kubernetes Controller最basic的Model。我写完这个之后,希望直接能够通过kubectl get AstaXie,反馈Asta Xie是什么状态、什么属性。

我先解释一下写这个东西的模式。首先不是有了Etcd,就把它当成数据库,我要把谢大写进去,Kubernetes不建议你直接操作Etcd这样做。所有对Etcd的操作需要通过Kube-apiserver,它相当于做了一个DAO层帮你翻译这个操作到Etcd里,这是第一步。第二步,首先我需要让Etcd知道Asta Xie的对象到底是怎么定义的,第一个叫types。第二个需要把这样的定义注册写到Etcd里去,这怎么写,这个叫Register。第三个既然能把谢大放进去,接下来怎么控制Asta Xie,Controller怎么写,这个一会儿会提到。然后Controller前面说过,它是通过watch API和Etcd交互,这个部分,这个watch怎么实现,需不需要操作Etcd获取的API,一会儿可以看得到。把这些都做完了之后,比如说现在用户要创建一个Asta Xie对象,直接kubectl create一个Asta Xie对象,这个的对象进来之后会触发on-Add的操作,如果去更新这样的对象,会触发on- Update操作,如果把对象删除,会触发On-delet操作。这些Handler分别怎么定义到Kubernetes上,所以整个Demo是做这个事。看起来有点复杂,实际上非常简单,因为全是套路。Kubernetes早就给你定义好了controller的套路模板,大家一起看一下这个模板怎么填。首先第一步Types,就是定义谢大或者Asta Xie到底是一个什么情况,一个对象在Kubernetes里需要内制Mate Data,其实这些都是一些名字,Namespace,这些操作不用管它。直接开始写谢大的NameSpace,就是谢大到底是怎么描述的,因为是API这部分的操作,API是需要做encoding和decoding,这个需要帮助Kubernetes里先定义一下。

第一个属性定义是Spec,Spec用来描述谢大,就是描述Asta Xie到底有哪些属性。第二个是这个对象的状态,Asta Xie states,同样需要定义json,两个了。一个是Spec,一个是States,然后我们来写Spec。Spec怎么描述谢大,首先谢大会写代码,会需要掌握一种编程语言。然后谢大有什么特点,帅,有人能质疑吗?(没有)。把这两个定义为一个json。这样的话就定义好了一个完整的Asta Xie的格式。大家可以看到,没什么任何难度,如果你想定义一个Network,这部分写你的IP Range,写的Getway IP地址都可以,今天咱们就全操作Asta Xie,希望Asta Xie可以给力。

另外,States可以定义一个Omit Empty,就是说可以不填,这个没问题。第一步完成了,Types,需要注意一点,所有的对象上有一个Annotation,叫Kubernetes Deep Copy,这是什么意思?因为这边需要做很多深拷贝的操作,在API这里。这样操作在Golang里面会用Deep Copy直接操作,但这个效率相对不是很高,所以在Kubernetes里面,谷歌这帮人直接写了一个库,帮助开发者给任何一个对象生成Deep Copy的方法,这是一个小的trick。

定义完谢大这个Type之后,我们给谢大定义两个状态,第一我们邀请谢大叫Invitation,叫Invited,第二个叫谢大接受邀请,称之为Accepted,这个状态需要来回改。最后一个需要定义一个谢大的list,毕竟好多个谢大怎么办,需要定义一个list,这个list也是一个死的模板,直接ctl+c,ctl+v就完了,同样给他定义一个Deep Copy。OK,这个Types完成了,Kubernetes已经知道谢大这个对象怎么去描述。第二步怎么把这个Object告诉Kubernetes,说可以写进去,需要遵循Kubernetes里的东西。首先要给谢大指定一个复数,很多个谢大,加一个S。第二个需要指定一个AIP的group,是属于哪一API组的,然后给他这个group制定一个version,是V1还是V2,这都是模板,ctl+c,ctl+v就可以了。然后需要告诉Kubernetes要注册的是什么样的对象,就往这里写,叫Asta Xie。还能够写一组AstaXie list,其他都不要管,因为只要把这两个Types注册进去,那Kubernetes就知道怎么去处理、怎么去读写Asta Xie Object。第二步Register也完事了,希望我没写错。然后第三步,现在已经有了Object,怎么用Kubernetes的client去操作Asta Xie,需要定义Client,将来写代码的时候可以直接调用Client,而不是每次去拼restful的请求。这个Client怎么写,Kubernetes也已经全写好了,整个代码不用改,一行都不用改了。需要改的是什么呢?就是定义一些细节。什么细节,就是将来要创建的这个对象,在Kubernetes里统一称为Customers Resource Defination,这个CRD将来要处理的什么对象,就是谢大,所以就给谢大起个名字。这个名字也是固定的,一定是复数加上group,其他也不用改,主要是这里写清楚,这个CRD是要操作Asta Xie这个Object,所以全是ctl+c和ctl+v。所以说写这个代码,最常见的操作是全选、替换的操作。好了,Client就有了。截止到现在,就已经可以把Kubernetes里通过写的client,去增删该谢大这个对象了。接下来开始写Controller了。Controller乍一听特别复杂,还要watch,watch不算什么,watch的性能还要去handle、去优化,幸好这都是Kubernetes里帮你做的。所以,真正在Kubernetes里要写Controller,不会超过十行,从38到63行,还加了很多注释,去掉注释可能就六七行,就能写一个Controller。

Controller怎么定义呢?第一,它要能操控一些对象,所以需要注意AstaXie Client,作用于这个Source,这个watch的源,把这个Source往Controller里去写。第二,这种Controller在Kubernetes里,统一被定义为一个Informer,为什么叫Informer,其实它就是Etcd的watch写了一个Wapper,包起来,同做了一些性能优化。比如说这边的Informer叫Shared Informer,就是如果它和其他的Informer一起,share一些Connection,以及一些Data cache,这个东西不用care,就直接复制粘贴。只需要告诉Controller,要处理的是Asta Xie对象,只要改变一个地方,就是only one place to change,就是这个地方。然后接下来定义On-Update ,On-Delete,又是模板。到这里全部是复制粘贴,唯一需要写代码的地方,就是这些On-Add, On-Delete ,On-Update 的操作。因为时间关系,我写一个On-Add,你可能觉得On-Add这边可以写代码了,其实还不用,因为Kubernetes又定义好这些东西,首先要做一个copy,做一个Deep copy,因为这边引用,然后把这个copy一个对象自己做。拿到AstaXie copy之后,去做什么?想做什么做什么,比如把AstaXie的状态更改一下。因为一开始是Invited,我们要求谢大把这个邀请接受一下,所以,我说copy的States重置一下。重置成什么呢?重置为一个新的状态,把他更改为Asta Xie Accepted,谢大接受我们的邀请。第二个让谢大说句话,说句什么呢?让谢大说:AstaXie accepted Harry’s  invitation,好,谢大说“很荣幸的接受了你的邀请,加入了Kubernetes”。

用户创建了一个谢大,一旦他创建的对象触发了On-Add的Handler,我们在这个Handler里面,就把这个对象的状态改为Accepted,是不是特别简单?当你把这个改完成之后,很自然的用Client,用一个Put的操作,put的方法,把新的谢大对象写回到Etcd里去做更新,这一步没有问题。然后再多做一步操作,把所有的AstaXie对象list出来看一下,更新有没有成功, On-Add结束了,code写完了,然后像On-Delete 和 On-Update就不改了,直接打一个log,Controller结束了。

到目前为止,描写的 code就已经结束了,我们写main函数操作这些东西,main函数特别简单,需要定义一个什么东西是如何把Asta Xie Client给new出来。唯一需要传一个参数的是什么?是Kubernetes的一个配置文件,Kubernetes每次装好之后,会生成一个配置文件,定义了Kubernetes API地址、证书,就用这个,不需要其他参数,非常简单。有了这个Client之后,拿这个Client把Controller初始化出来,Controller就有了,接下来无脑的把这个Controller run一下,启动,它就停在这里做死循环,等待你去create谢大对象,执行你定义好的On-Add, On -Delete,On-Update的操作,这个代码结束了,把它推到开发机上面去试试吧。大家看看就改变这么多东西,没有什么特殊的。这个是Demo Asta Xie,OK,push一下。上去了,拿到我开发机里面来,把这个拿下来,好了,拿下来之后可以run了。main函数写好了,走一个。跑的时候,一边看一下怎么定义一下Asta Xie这个yaml。OK,这边看的Asta Xie,Kubernetes没有这样的东西,这是我们自己定义的。api version 是我们刚才自己拼的一个字符串,这个随便写,首先我们对Asta Xie对象起个名字,一号谢大。谢大写什么语言呢,Golang,然后handsome=true,初始状态是Invited,我们对谢大说句话,说我们要Invite Asta Xie join Kubernetes。好了,States定义好了,谢大的名字也起好了,就这样的一个对象可以用了。看一下Controller启动了没有,它说Controller start sucess,没编译错。挺好的。然后,可以看Kubernetes是不是已经知道了谢大这样的对象,用CRD,Kubernetes已经有一个称之为元数据,或者原Object,叫Asta Xie。刚才定义的是AstaXie的复数,Asta Xie‘s,意味着我们可以操作这个对象了。对象已经在yaml里了,直接kubectl create-f,然后yaml写这儿了,走一个。

前面说了我们可以kubectl create get AstaXie才对,有了谢大一。到底发生了什么呢?看log,在Controller里经历了这样的事情。第一,增加了谢大的对象,会有触发On-Add的这个handeller,打这个新增添的东西Asta Xie,Language是Golang,handsome是true,States是也invited,这是初始状态。紧接着我们在On-Add里自己写的代码也做了什么事?我们把Asta Xie的状态做更新,更新成为Accepted,然后还要谢大说一句话,叫“我接受了希望Harry的邀请”,这个是update下的操作。这个Uptate的操作一旦成功之后,它会触发On-Uptate的Handeller,打两个日志。最后list一下Asta Xie,返回结果看到,状态已经被验证了Accepted,可以了,这就是Controller的整个life cycle,除了这个life cycle,还差一个,还可以delete把这个Asta Xie给踢了,Asta Xie谢大一。

我这边想做一些清理工作,因为我刚刚提到Network的话,可能把这边删掉,需要同步去删掉一些Network namespace,清一些设备,都在这里做。这时候我们再回头看出Asta Xie,应该就已经不见了,这就是一个完整的Controller的设计和实现的过程,这是非常简单的。没有特别复杂的,因为他把很底层,像watch,像informer这种写法,通知的写法,基于Etcd的协作编程方式全都包装起来,这其中像CoreOS李响他们,做了很多工作。还有RedHat做的,他们直接有一个team,专门做share informer的工作,把Kubernetes的性能提高了很多。总之,站在这些巨人的肩膀上,再做Controller的事情,确实比单挑 Etcd要容易很多。

前面可能觉得这个demo比较简单,还有一个真实的例子。这是我们自己maintain的一个OpenStack Foundation的项目,叫Stackube。这个项目就是我有一个Kubernetes,希望用OpenStack的网络和存储,这其中就需要处理Network,怎么处理呢?我需要跟Neutron打交道。我希望Neutron能够根据租户,租户就是Kubernetes里的Namespace,根据这个Namespace创建一个同名的Network给我,一个二层隔离网络给我,接下来写一个CNI Neutron的插件,这个Pod的容器就可以Neutron 网络了,就这么简单,这个思想很直接。这其中就需要写Controller,用Controller干嘛呢?首先,我希望把Network的对象切进去,这个Network的对象就定义了网段、Getway、网络名字、网络对象的talent ID等信息。第二个,需要添加一个网络给Kubernetes之后,在On-Add里,同步给Call Neutron API,帮我创建这样一个的Network给我,我后面还要用。接下来还需要做的一件事,需要在On Add 的时候,创建一套Kube DNS,就是Kube DNS Server给我,因为一旦用了Neutron的网络,它是一个二层隔离的网络,以前Kubernetes那种一个Kube  DNS server 管所有的pod的模型不work,因为这时候的Pod是被隔离在不同的二层网里,我需要在每个网络都起一Kube DNS。这个想法也很主流,所以我在On-Add里做了这样两件事。然后我又处理了On-Delete,起到前面说的一个事实,就是在把这个网络删掉之后,能够把数据机上创建的乱七八糟的网络Namespace删掉。OK,Update主要是用来更新这个Network状态,因为对应到Neutron那边,它的Network有一套完整的状态,我需要把它mantain起来。就这样的事情,这个事情因为时间关系就不对大家讲了。如果把这个代码直接打开,如果打开就可以看到,听起来更复杂,但和刚才的模型一模一样,Controller还是这么短,然后On-Add也不会长,我们这边还是做copy。copy之后就读里面的信息,最主要我做一个操作是Add Network to Driver,其实这就是调了一下OpenStack 的API就完事了。

Kubernetes项目的编程范式

Kubernetes模型可以推到很复杂的事情、很复杂的状态,但所付出的代价相对而言少很多,这就是为什么要介绍编程范式的原因。同时这样的编程范式还可以拓展到自己,比如说不用Kubernetes,但自己去写这样的业界称之为Orchestrator,这样一个东西的时候,这个模型显然非常有帮助。实际上,像Docker的SwarmKit也是借鉴这样的模型,它自己也写一套这样的框架,流程几乎完全一样,就可以看到这个模型并不是谷歌闲的没事儿弄的,而是有它的好处在里面。

第一个范式讲完了Controller,第二个范式也是为什么Kubernetes这个项目对Golang的依赖还是很大,其中最重要的就是它至少有三分之一到一半的代码是通过Generator生成,它的Generator相当多。我这里没有列完,可能列了三分之二左右。比如说Client-gen,刚才大家看到了我写一个Asta Xie对象操作,需要定义一个Client,这样看显然它是有模式的,那几个字段都差不多,所以这样的东西完全从可以定义一个对象和Type,从这里开始,用一个命令行直接给Type生成一个Kubernetes restful client。这个在Kubernetes里就是这么做的。第二个Conversion-gen,这个是比如说Kubernetes要加一个feature,比如新做一个AstaXie这样的feature,这个feature一开始第一个release是α,第二个是β,它的API叫V1α1,第二个是V1β1这样的API,他们之间怎么做conversion也的操作,这样的代码显然也可以通过Generator生成出来,每一个加速的字段都是Kubernetes里一个小的binary,下来都可以找到。第三个就是刚才讲的Deepcopy-gen,这里列了一个例子,就是怎么用Deepcopy-gen,很简单,甚至带自己的项目就可以这么做了。直接go get把Kubernetes cmd装起来,就是把Deepcopy-gen的binary装起来,然后直接写Deepcopy-gen你的目录,它会把所有的Type生成一批代码出来。然后还有Default,就是刚才提到如果不给Asta Xie论设置它的属性,这个default是什么值,这个明显有这种模式可以遵循,所以在Kubernetes里所有的Default也是通过这个设置的方式设置的。

还有比较重要的就是go-go –Protobuf的东西,这个是Kubernetes里使用GRPC和大家不太一样的地方,他们没有使用标准GRPC的包,而是用go-go-protobuf这个东西,因为它的performance相对好一点,因为用这套东西,Kubernetes把这套命令行单独做封装,然后用这个conmand line可以生成Kubernetes格式的proto文件,这个和标准的stander的文件有一点点区别。

还有Informer-gen,刚才的Informer-gen一共有Controller 10行代码,也是死的,只要改Type就可以了,所以这个Informer也是可以一样,写一个Asta Xie对象,把这个binary执行一遍,替它生成一个Asta Xie Informer,就这样做的。还有OpenAPI键,这些东西全部是通过code-gen生成出来的。所以说,最后是什么效果呢?就是这个效果,就是在API里自己定义,不是自己定义,就是开发者的Kubernetes里面定义Type,整个流程下来基本上操作的Type的所需的所有代码都已经ready了,接下来就可以直接写业务逻辑了,就是On Add、On-Delete的东西了。

如果大家对这个感兴趣,可以看Kubernetes有一个库,叫 gengo,里面就是基于Golang的所有定义。另外还有go2idl,也是相关的binary,大家可以去研究它,可以把它import进来,vendor进来自己用没有问题。

第三个模式是Kubernetes如何使用GRPC解耦。大家可以想到Kubernetes在哪些地方需要用gRPC,最主要的点就是Kubernetes如何和Docker解耦这个世纪性难题。因为在1.3、1.4之前,Kubernetes和其他项目都一样,都是直接去vendor一个Docker Client,然后向Docker daemon发请求的,这就使得Kubernetes的Kubelet 这个部分特别难维护。

为什么?一方面,你看vendor  Docker Client之后就需要和Docker的变化,而Docker这个项目特别不靠谱,变得到很快,隔三差五还改名,所以这就很麻烦,怎么去维护这样的Kubelet。第二个很重要的,CoreOS推了rkt,使得rkt成为了Kubernetes继docker之后,第二个内置的container Engine。这样intree的container runtime维护很麻烦,导致1.3之前的Kubernetes里这部分代码特别乱,报bug没法修,也没法调了,Docker runtime的问题调着调着调到rkt里,这怎么玩?所以在这个过程中,大家可能听过Kubernetes里华人的一个Leader Dawn Chen,她顶着很大的压力推了CRI,就是Container Runtime Interface ,就是一个完全基于GRPC的 Interface ,把Docker,rkt等runtime跟Kubelet隔离开。有人说有政治斗争在那儿,肯定有。这样的事情怎么在Kubernetes里面怎么做呢?我们来看一下CRI的design。这个想法很直接,就是现在Kubernetes不做这些操作,Docker操作Controller Runtime的事情,Kubelet就当一个Client,可以看一下Workflow。比如说Kubelet里要list Pod,那你的code是Runtime Server 的list Sandbox这个方法,对应到Interface里,就是Interface定义到底应该支持哪些操作,对应的就是list Pod的操作。这样的操作,Kubelet作为一个Client,把这个请求发出去,不去实现,也不管怎么实现,把这个交给谁呢是重点。就是任何的一种Runtime都需要写一个Shim。大家可以看到,你说rkt要不要写一个Shim,肯定要写。Docker也写一个这样的Shim,叫dockershim。然后把dockershim内制到Kubelet里,其他Shim悄悄拿出去,所以就实现了一个非常简单也很紧密,但也很高效的解耦。

Shim作为Interface的实现,同时也作为一个GRPC的Server,去response这个请求,我帮你实现这个list Pod Sandbox,我干嘛?我vendor Docker Client,去给Docker API发get请求,去拿Container,去组装,把Container组装成Pod返回去,这都是你来做,Kubelet不care。所以,这个在Kubernetes里怎么用CIA实现解耦的很标准的做法。所以一旦把这个写好之后,会发现kubelet和docker这两个项目之间,只需要共同维护这样的一个interface,有哪些方法,哪个方法有哪些字段,每个字段有哪些类型,返回值是什么,有几个。只需要盯这些就够了,不管DockerAPI将来变成什么了,只要还有这几个API就能用,这是理想的状态。所以,后来像Docker改成moby对Kubernetes没有任何影响,这是解耦的好处。

在这个过程中,GRPC细节就不讲了,一会儿谢大有一个topic,相信他比我讲得肯定好多了,我只是说Kubernetes在这里小的一些tips可以给大家借鉴。比如说刚才前面提到的go2idl部分。由于使用了go-go-protobutf,因为大家知道写GRPC的protocl文件还是比较烦。他写一个generator,只要给一个对象,给一个Type,生成这部分的protocl文件。

所以,把这部分的操作定义成一个脚本,放在Kubernetes的库里,将来是写dockershim,只需要把这个脚本跑一遍,proto文件,而且是对应版本的proto文件就有了,这个小tip可以学习一下这个过程。通过维护这样一个生成器,来维护一个大家可以写的proto的文件。

简单介绍一下CRI的方式。前面已经说了很多了,这里简单提一下,就是Kubernetes里灰色的部分就是它的managment的部分,负责响应用户的请求,并且把所有的Deployment、Pod等等,最后映射成为Kubelet来说,就是一个Pod的操作,是要增还是要删,还是要改,还是要查。可能前面创造了一个比较大的东西,最后映射下来还是一个Pod的操作。这样的pod操作会被交给一个Generic Runtime的部分去执行,并不是直接到Docker。什么叫Generic Runtime呢?它负责把Pod解耦成Container API,一般Pod还是一组,具体要操作某一个Container,这样的操作要通过Generic Runtime来解。解开了之后,把它分拆成CRI的方法。CRI的方法只有三种:第一个,是怎么去操作Pod Sandbox,就是怎么实现Pod Sandbox;第二个,怎么操作Container;第三个操Imge。什么叫Pod Sandbox?在Kubernetes有一个特殊的容器,叫infra container,它是要hold整个Pod的Network Namespace,这样Pod里所有的Container是share一个Namespace,这个东西直接就machine成Pod的Sandbox,这也提醒你不同的Runtime的实现,可以做自己的Pod Sandbox。比如rkt,就自己写了一个Pod。甚至没有infra container也可以,比如就拿虚拟机作为一个Sandbox,没问题,都可以,hyper就是这么干的。所以,这有很大的灵活性,让Kubernetes不用care这些Runtime的细节。接下来一些Container的操作就直接交给Container API,就可以直接找到dockershim,包括向Docker发请求。如果用的不是Docker,是rkt或者hyper怎么办?需要做这样的一个配置,非常简单。基于CRI,基于GRPC的特点就非常简单,简单到什么程度?听起来很复杂,让Kubernetes支持rkt,怎么办?只需要在Kubelet这边加入一个Container Runtime的remote。然后remote的end point是谁,什么什么sock就可以了。

这个过程做完之后你可以继续用Kubernetes标准东西,比如说Kubeadm Init、Kubeadm Join部署这个集群,全部都不会影响Runtime的配置,因为它就是对应到Kubelet一个配置文件的更改而已,不需要去维护什么东西,也不需要单独做一个项目管这些东西,都不需要,这是CRI。也是我讲的最后一个pattern。

最后一部分希望给大家简单介绍一下Kubernetes这个项目的最佳实践,前面提到的是Kubernetes的开发者,这里是给在座的开发者提供怎样的编程范式。其实有点像现实写代码的设计模式,设计模式干嘛的?第一个是解耦,Kubernetes解耦的是容器与容器之间的关系,第二个是重用,就是一个镜像不需要来回打包很多次,塞很多东西进去,单纯一个binary,帮你做这样的事情。最后实现希望能够帮助实现设计得更好的基于容器部署的作业方式,也就是解答一个问题,怎么在Kubernetes上部署一个分布式的MySQL Service。

第一个例子sideCar,就可以理解为一种设计模式。也就是解决镜像重用的问题,很多人的Docker Image这么写的:第一个往里扔一个tomcat或者是binary应用程序。然后再扔一个Logging Agent进去,因为要把日志拿出来,要把日志导到Elastic Search或者MangoDB是集中存的方式。这样镜像耦合度是不是有点高?比如说将来更新应用程序,要重新写镜像,更麻烦的是要更新Logging Agent,镜像还要重新做。在Kubernetes里建议是Logging Agent是自己得一个镜像,很小,应用也是自己的镜像,然后起两个Container,各起一个Container,一共两个Container。然后把这个Container打包到这个Pod里,由于在Pod里,Volume和Network是共享的,所以他们两者之间share文件、互相连都非常自由,直接读本地文件就可以了,不需要维护一个Volume,也不需要做一些labol,保证他俩在一个机制上,不需要。也不需要频繁的更改镜像,这就是side car。所以可以把它推广到任何一种情况,反正有多个Container,他们之间有一种冲动老想想把他们打到一镜像,这种冲动首先要考虑side car的方式去解决。

第二个问题,大家可能已经想到了,假设这几个Pod,这几个side car之间,容器之间有先后关系,我是想把它写到一个镜像里,但要保证一个先后启动顺序,在Kubernetes里,这样的定义叫InitContainer。就是你要先启的这个容器,比如说这是一个MySQL,启动之前需要执行一些initialize的脚本,这很正常。可能正常写就直接在Docker里面写了,先要call 一个脚本。不需要,镜像永远就是这个应用,这个脚本什么时候执行,单独一个镜像,单独一个容器,并且把这个容器定义为InitContainer,它会先于所有的用户容器被执行到,并且保证执行完了之后再启动用户容器,而且可以定义多个,可能要进行好几步initialize都没有问题。这叫InitController。

再深入一步,前面是说几个容器要协作该怎么办,第二个是这几个协作是有顺序的怎么办,这是第二个patterm。

第三patterm更高级了,就是说现在是需要这么做,但发现每次都需要在一个Pod,一个Deployment yaml文件里,都需要重新定义一个Logging Agent的容器,好说歹说也这么长,写起来很费事。而且会发现所有的Pod,所有的Deployment的都一样,是不是自然而然想出一个需求能不能让Kubernetes自动把这部分写进去,能不能自动把这个Logging Agent注入进去,这有点像spring的切面编程。如果写一个代码,在不改代码的情况下,让spring注入方法进去,比如说保证每一个方法执行前打一个log,可以用spring可以做这样的事情,用Kubernetes同样可以做这样的事情。我们来看一下,这个应用程序的yaml就这么短,就一个镜像启动,然后再单独定义一个东西,叫Initializer,这也是Kubernetes里一个新东西,叫Initializer。把需要注入的东西,写入到Etcd里存起来。然后在需要备注的Deployment写一下需要被注入一个什么东西,这边叫Logging Agent,Logging Agent正是这一行到这一行之间的内容。把这个定义好之后,再执行kubectl create。创建出来的Pod里面本来应该只有一个容器,但它会被Kubernetes按照定义的这个格式,再注入一个Logging Agent容器。就这么简单,一个完全自动化的操作,这个叫Initializer。

总结

所以,你会看到Kubernetes里,到底在玩什么。玩的就是这种东西,一个希望帮助你更好的、更合理的部署应用的design pattern。今天这不多展开了,因为今天不是Kubernetes的分享。所以,来到topic的结尾,总结一下今天要讲的什么东西。

第一个,讲的是Kubernetes怎么用Golang,首先介绍了Kubernetes,然后重点介绍演示demo了Kubernetes用Golang的一个范式,我们称之为Controller,也是Kubernetes的设计核心。我们还写了一下如何把Asta Xie加入进去,写一个Controller,去控制它。

第二个,简单介绍了Kubernetes的Code gem,,每一个怎么实现,都可以在库里找到。

第三个,简单介绍了基于GRPC的Interface的设计。

第四个,这边可以给大家推荐一下Kubernetes的Util库。它里面有很多东西,可以像淘宝一样,可以淘到很多有意思的工具。比如说怎么生成一个真正唯一的UEID,大家可能都考虑过,Golang怎么去做,这个库里都是这些小东西。这些是Kubernetes开发者所需要了解的。

第二部分非常简短的向大家介绍了Kubernetes给开发者,给在座的developer提供了怎样的编程范式,如果大家有问题的话,可以线交流。今天就不多介绍了,但我需要提的是第二个问题怎么帮助开发者开发基于容器的应用问题,才是Kubernetes项目的核心。

评论