深入Kubernetes微服务平台
Kubernetes的概念与功能
架构师普遍有这样的愿景:在系统中有ServiceA、ServiceB、ServiceC这3种服务,其中ServiceA需要部署3个实例,ServiceB与ServiceC各自需要部署5个实例,希望有一个平台(或工具)自动完成上述13个实例的分布式部署,并且持续监控它们。当发现某个服务器宕机或者某个服务实例发生故障时,平台能够自我修复,从而确保在任何时间点正在运行的服务实例的数量都符合预期。这样一来,团队只需关注服务开发本身,无须再为基础设施和运维监控的事情头疼了。
在 Kubernetes出现之前,没有一个平台公开声称实现了上面的愿景。Kubernetes是业界第一个将服务这个概念真正提升到第一位的平台。在Kubernetes的世界里,所有概念与组件都是围绕Service运转的。正是这种突破性的设计,使Kubernetes真正解决了多年来困扰我们的分布式系统里的众多难题,让团队有更多的时间去关注与业务需求和业务相关的代码本身,从而在很大程度上提高整个软件团队的工作效率与投入产出比。
Kubernetes里的Service其实就是微服务架构中微服务的概念,它有以下明显特点。
- 每个Service都分配了一个固定不变的虚拟IP地址——Cluster IP。
- 每个Service都以TCP/UDP方式在一个或多个端口 (Service Port)上提供服务。
- 客户端访问一个 Service时,就好像访问一个远程的TCP/UDP服务,只要与Cluster IP建立连接即可,目标端口就是某个Service Port。
Service既然有了IP地址,就可以顺理成章地采用DNS域名的方式来避免IP地址的变动了。Kubernetes 的 DNS组件自动为每个Service都建立了一个域名与IP的映射表,其中的域名就是Service的Name,IP就是对应的Cluster IP,并且在Kubernetes的每个Pod(类似于Docker'容器)里都设置了DNS Server为 Kubernetes 的 DNS Server,这样一来,微服务架构中的服务发现这个基本问题得以巧妙解决,不但不用复杂的服务发现API供客户端调用,还使所有以TCP/IP方式通信的分布式系统都能方便地迁移到Kubernetes平台上,仅从这个设计来看,Kubernetes就远胜过其他产品。
我们知道,在每个微服务的背后都有多个进程实例来提供服务,在Kubernetes平台上,这些进程实例被封装在Pod中,Pod基本上等同于Docker容器,稍有不同的是,Pod其实是一组密切捆绑在一起并且“同生共死”的 Docker 容器,这组容器共享同一个网络栈与文件系统,相互之间没有隔离,可以直接在进程间通信。最典型的例子是Kubenetes Sky DNS Pod,在这个Pod里有4个Docker '容器。
那么,Kubernetes里的 Service 与 Pod 是如何对应的呢?我们怎么知道哪些Pod 为某个Service提供具体的服务?下图给出了答案——“贴标签”。
每个Pod都可以贴一个或多个不同的标签(Label),而每个Service都有一个“标签选择器”(Label Selector),标签选择器确定了要选择拥有哪些标签的对象。下面这段YAML格式的内容定义了一个被称为ku8-redis-master的Service,它的标签选择器的内容为“app: ku8-redis-master",表明拥有“app= ku8-redis-master”这个标签的Pod都是为它服务的:
apiversion: v1
kind: Service
metadata:
name: ku8-redis-masterspec:
ports:
- port: 6379selector:
app: ku8-redis-master
下面是 ku8-redis-master这个 Pod 的定义,它的 labels属性的内容刚好匹配Service 的标签选择器的内容:
apiversion: v1kind: Pod
metadata:
name: ku8-redis-masterlabels:
app: ku8-redis-master
spec:
containers:
name: serverimage: redisports:
-containerPort:6379
restartPolicy: Never
如果我们需要一个Service在任意时刻都有N个Pod实例来提供服务,并且在其中1个Pod实例发生故障后,及时发现并且自动产生一个新的Pod实例以弥补空缺,那么我们要怎么做呢?答案就是采用 Deployment/RC,它的作用是告诉Kubernetes,拥有某个特定标签的 Pod需要在Kubernetes集群中创建几个副本实例。Deployment/RC的定义包括如下两部分内容。
●目标Pod的副本数量(replicas)。
●目标Pod的创建模板(Template)。
下面这个例子定义了一个RC,目标是确保在集群中任意时刻都有两个 Pod,其标签为“ app:ku8-redis-slave”,对应的容器镜像为redis slave,这两个 Pod 与ku8-redis-master构成了Redis主从集群(一主二从):
apiversion :v1
kind: ReplicationControllermetadata:
name: ku8-redis-slavespec:
replicas: 2template:
metadata:
labels:
app: ku8-redis-slavespec:
containers:
name: server
image: devopsbq/redis-slave
env:
name: MASTER ADDR
value: ku8-redis-masterports:
-containerPort:6379
至此,上述YAML文件创建了一个一主二从的Redis集群,其中Redis Master被定义为一个微服务,可以被其他Pod或 Service访问,如下图所示。
注意上图在 ku8-reids-slave的容器中有MASTER_ADDR的环境变量,这是Redis Master 的地址,这里填写的是“ku8-redis-master”,它是Redis Master Service 的名称,之前说过:Service的名称就是它的DNS域名,所以Redis Slave容器可以通过这个DNS与Redis Master Service进行通信,以实现Redis 主从同步功能。
Kubernetes 的核心概念就是Service、Pod 及 RC/Deployment。围绕着这三个核心概念,Kubernetes实现了有史以来最强大的基于容器技术的微服务架构平台。比如,在上述Redis集群中,如果我们希望组成一主三从的集群,则只要将控制Redis Slave的 ReplicationController中的replicas改为3,或者用kubectrl scale命令行功能实现扩容即可。命令如下,我们发现,服务的水平扩容变得如此方便:
kubectl scale --replicas=3 rc/ku8-redis-slave
不仅如此,Kubernetes还实现了水平自动扩容的高级特性——HPA ( Horizontal PodAutoscaling ),其原理是基于Pod 的性能度量参数(CPU utilization和 custom metrics)对RC/Deployment管理的Pod进行自动伸缩。举个例子,假如我们认为上述Redis Slave集群对应的Pod也对外提供查询服务,服务期间Pod的 CPU利用率会不断变化,在这些Pod 的CPU平均利用率超过80%后,就会自动扩容,直到CPU利用率下降到80%以下或者最多达到5个副本位置,而在请求的压力减小后,Pod的副本数减少为1个,用下面的HPA命令即可实现这一目标:
kubectl autoscale rc ku8-redis-slave --min=1 --max=5 --cpu-percent=80
除了很方便地实现微服务的水平扩容功能,Kubernetes还提供了使用简单、功能强大的微服务滚动升级功能(rolling update),只要一个简单的命令即可快速完成任务。举个例子,假如我们要将上述Redis Slave服务的镜像版本从devopsbq/redis-slave升级为leader/redis-slave,则只要执行下面这条命令即可:
kubectl rolling-update ku8-redis-slave --image=leader/redis-slave
滚动升级的原理如下图所示,Kubernetes在执行滚动升级的过程中,会创建一个新的RC,这个新的RC使用了新的Pod镜像,然后Kubernetes每隔一段时间就将旧RC的replicas数减少一个,导致旧版本的Pod副本数减少一个,然后将新RC的replicas数增加一个,于是多出一个新版本的Pod副本,在升级的过程中 Pod副本数基本保持不变,直到最后所有的副本都变成新的版本,升级才结束。
Kubernetes的组成与原理
Kubernetes集群本身作为一个分布式系统,也采用了经典的Master-Slave架构,如下图所示,集群中有一个节点是Master节点,在其上部署了3个主要的控制程序:API Sever、ControllerManager 及 Scheduler,还部署了Etcd进程,用来持久化存储Kubernetes管理的资源对象(如Service、Pod、RC/Deployment)等。
集群中的其他节点被称为Node节点,属于工人(Worker 节点),它们都由Master 节点领导,主要负责照顾各自节点上分配的Pod副本。下面这张图更加清晰地表明了Kubernetes各个进程之间的交互关系。
从上图可以看到,位于中心地位的进程是API Server,所有其他进程都与它直接交互,其他进程之间并不存在直接的交互关系。那么,APl Server的作用是什么呢?它其实是Kubernetes的数据网关,即所有进入Kubernetes 的数据都是通过这个网关保存到Etcd数据库中的,同时通过API Server将Eted里变化的数据实时发给其他相关的Kubernetes进程。API Server 以REST方式对外提供接口,这些接口基本上分为以下两类。
- 所有资源对象的CRUD API:资源对象会被保存到Etcd中存储并提供Query接口,比如针对Pod、Service及RC等的操作。
- 资源对象的 Watch API:客户端用此API来及时得到资源变化的相关通知,比如某个Service 相关的Pod实例被创建成功,或者某个Pod 状态发生变化等通知,Watch API主要用于Kubernetes 中的高效自动控制逻辑。
下面是上图中其他Kubernetes进程的主要功能。
- controller manager:负责所有自动化控制事物,比如RC/Deployment的自动控制、HPA自动水平扩容的控制、磁盘定期清理等各种事务。
- scheduler:负责Pod 的调度算法,在一个新的Pod被创建后,Scheduler根据算法找到最佳 Node节点,这个过程也被称为Pod Binding。
- kubelet:负责本Node节点上Pod实例的创建、监控、重启、删除、状态更新、性能采集并定期上报 Pod 及本机 Node节点的信息给Master节点,由于Pod实例最终体现为Docker'容器,所以Kubelet还会与Docker交互。
- kube-proxy:为 Service的负载均衡器,负责建立Service Cluster IP 到对应的Pod实例之间的NAT转发规则,这是通过Linux iptables实现的。
在理解了Kubernetes各个进程的功能后,我们来看看一个RC 从YAML定义到最终被部署成多个Pod 及容器背后所发生的事情。为了很清晰地说明这个复杂的流程,这里给出一张示意图。
首先,在我们通过kubectrl create命令创建一个RC(资源对象)时,kubectrl通过Create RC这个REST接口将数据提交到APl Server,随后API Server将数据写入Etcd里持久保存。与此同时,Controller Manager监听(Watch)所有RC,一旦有RC被写入Etcd中,Controller Manager就得到了通知,它会读取RC的定义,然后比较在RC中所控制的Pod 的实际副本数与期待值的差异,然后采取对应的行动。此刻,Controller Manager 发现在集群中还没有对应的Pod实例,就根据RC里的Pod模板(Template)定义,创建一个Pod并通过API Server保存到Etcd中。类似地,Scheduler进程监听所有 Pod,一旦发现系统产生了一个新生的Pod,就开始执行调度逻辑,为该Pod 安排一个新家(Node),如果一切顺利,该Pod就被安排到某个Node节点上,即Binding to a Node。接下来,Scheduler进程就把这个信息及 Pod状态更新到Etcd里,最后,目标Node节点上的Kubelet监听到有新的Pod被安排到自己这里来了,就按照Pod里的定义,拉取容器的镜像并且创建对应的容器。在容器成功创建后,Kubelet进程再把 Pod的状态更新为Running 并通过API Server更新到 Etcd 中。如果此 Pod还有对应的Service,每个Node上的Kube-proxy进程就会监听所有Service及这些Service对应的Pod实例的变化,一旦发现有变化,就会在所在 Node节点上的 iptables 里增加或者删除对应的NAT转发规则,最终实现了Service的智能负载均衡功能,这一切都是自动完成的,无须人工干预。
那么,如果某个Node'宕机,则会发生什么事情呢?假如某个Node宕机一段时间,则因为在此节点上没有Kubelet进程定时汇报这些Pod 的状态,因此这个Node 上的所有Pod'实例都会被判定为失败状态,此时Controller Manager会将这些Pod删除并产生新的Pod实例,于是这些Pod被调度到其他 Node 上创建出来,系统自动恢复。
本节最后说说Kube-proxy的演变,如下图所示。
Kube-proxy一开始是一个类似于HAProxy的代理服务器,实现了基于软件的负载均衡功能,将Client 发起的请求代理到后端的某个Pod 上,可以将其理解为Kubernetes Service的负载均衡器。Kube-proxy最初的实现机制是操控 iptables规则,将访问Cluster IP 的流量通过NAT方式重定向到本机的Kube-proxy,在这个过程中涉及网络报文从内核态到用户态的多次复制,因此效率不高。Kube-proxy 之后的版本改变了实现方式,在生成 iptables规则时,直接NAT 到目标Pod地址,不再通过Kube-proxy进行转发,因此效率更高、速度更快,采用这种方式比采用客户端负载均衡方式效率稍差一点,但编程简单,而且与具体的通信协议无关,适用范围更广。此时,我们可以认为Kubernetes Service基于 iptables机制来实现路由和负载均衡机制,从此以后,Kube-proxy已不再是一个真正的“proxy"”,仅仅是路由规则配置的一个工具类“代理”。
基于iptables 实现的路由和负载均衡机制虽然在性能方面比普通Proxy提升了很多,但也存在自身的固有缺陷,因为每个Service都会产生一定数量的 iptables 规则。在Service数量比较多的情况下,iptables 的规则数量会激增,对iptables的转发效率及对Linux内核的稳定性都造成一定的冲击。因此很多人都在尝试将IPVS(IP虚拟服务器)代替iptables。Kubernetes 从 1.8版本开始,新增了Kube-proxy对IPVS的支持,在1.11版本中正式纳入 GA。与 iptables 不同, IPVS本身就被定位为Linux官方标准中TCP/UDP服务的负载均衡器解决方案,因此非常适合代替iptables来实现 Service的路由和负载均衡。
此外,也有一些机制来代替 Kube-proxy,比如Service Mesh 中的 SideCar 完全代替了Kube-proxy的功能。在 Service 都基于HTTP接口的情况下,我们会有更多的选择方式,比如Ingress、Nginx 等。
基于Kubernetes 的 PaaS平台
PaaS其实是一个重量级但不怎么成功的产品,受限于多语言支持和开发模式的僵硬,但近期又随着容器技术及云计算的发展,重新引发了人们的关注,这是因为容器技术彻底解决了应用打包部署和自动化的难题。基于容器技术重新设计和实现的PaaS平台,既提升了平台的技术含量,又很好地弥补了之前PaaS平台难用、复杂、自动化水平低等缺点。
OpenShift是由 RedHat公司于2011年推出的PaaS云计算平台,在Kubernetes推出之前,OpenShift 就已经演变为两个版本(v1与v2),但在 Kubernetes推出之后,OpenShift的第3个版本v3放弃了自己的容器引擎与容器编排模块,转而全面拥抱Kubernetes。
Kubernetes 拥有如下特性。
- Pod(容器)可以让开发者将一个或多个容器整体作为一个“原子单元”进行部署。
- 采用固定的Cluster IP及内嵌的DNS这种独特设计思路的服务发现机制,让不同的Service很容易相互关联(Link)。
- RC可以保证我们关注的Pod副本的实例数量始终符合我们的预期。
- 非常强大的网络模型,让不同主机上的Pod能够相互通信。
- 支持有状态服务与无状态服务,能够将持久化存储也编排到容器中以支持有状态服务。
- 简单易用的编排模型,让用户很容易编排一个复杂的应用。
国内外已经有很多公司采用了Kubernetes作为它们的PaaS平台的内核,所以本节讲解如何基于Kubernetes 设计和实现一个强大的 PaaS平台。
一个 PaaS平台应该具备如下关键特性。
- 多租户支持:这里的租户可以是开发厂商或者应用本身。
- 应用的全生命周期管理:比如对应用的定义、部署、升级、下架等环节的支持。
- 具有完备的基础服务设施:比如单点登录服务、基于角色的用户权限服务、应用配置服务、日志服务等,同时PaaS平台集成了很多常见的中间件以方便应用调用,这些常见的中间件有消息队列、分布式文件系统、缓存中间件等。
- 多语言支持:一个好的PaaS平台可以支持多种常见的开发语言,例如Java、Node.js、PHP、Python、C++等。
接下来,我们看看基于Kubernetes 设计和实现的PaaS平台是如何支持上述关键特性的。
如何实现多租户
Kubernetes通过Namespace特性来支持多租户功能。
我们可以创建多个不同的Namespace资源对象,每个租户都有一个Namespace,在不同的Namespace下创建的Pod、Service 与RC等资源对象是无法在另外一个Namespace下看到的,于是形成了逻辑上的多租户隔离特性。但单纯的Namespace隔离并不能阻止不同Namespace下的网络隔离,如果知道其他Namespace中的某个 Pod的IP地址,则我们还是可以发起访问的,如下图所示。
针对多租户的网络隔离问题,Kubernetes增加了Network Policy这一特性,我们简单地将它类比为网络防火墙,通过定义Network Policy资源对象,我们可以控制一个Namespace(租户)下的Pod被哪些Namespace访问。假如我们有两个Namespace,分别为tenant2、tenant3,各自拥有一些Pod,如下图所示。
假如我们需要实现这些网络隔离目标: tenant3里拥有role:db标签的Pod只能被tenant3(本Namespace中)里拥有role:frontend标签的Pod访问,或者被tenent2里的任意Pod访问,则我们可以定义如下图所示的一个Network Policy资源对象,并通过kubectrl工具发布到Kubernetes集群中生效即可。
需要注意的是,Kubernetes Network Policy需要配合特定的CNI网络插件才能真正生效,目前支持Network Policy 的CNI 插件主要有以下几种。
- Calico:基于三层路由实现的容器网络方案。
- Weave Net:基于报文封装的二层容器解决方案。
- Romana:类似于Calico的容器网络方案。
Network Policy目前也才刚刚起步,还有很多问题需要去研究和解决,比如如何定义Service的访问策略?如果Service访问策略与Pod访问策略冲突又该如何解决﹖此外,外部服务的访问策略又该如何定义?总之,在容器领域,相对于计算虚拟化、存储虚拟化来说,网络虚拟化中的很多技术才刚刚起步。
Kubernetes 的 Namespace是从逻辑上隔离不同租户的程序,但多个租户的程序还是可能被调度到同一个物理机(Node)上的,如果我们希望不同租户的应用被调度到不同的Node 上,从而做到物理上的隔离,则可以通过集群分区的方式来实现。具体做法是我们先按照租户将整个集群划分为不同的分区(Partition),如下图所示,对每个分区里的所有 Node 都打上同样的标签,比如租户 a(tanenta)的标签为partition=tenant,租户 b( tanentb)的标签为partition= tenantb,我们在调度Pod 的时候可以使用nodeSelector属性来指定目标Node的标签,比如下面的写法表示Pod需要被调度到租户 a的分区节点上:
nodeSelector:
partition: tenanta
Kubernetes 分区与租户可以有多种对应的设计,上面所说的一个分区一个租户的设计是一种典型的设计,也可以将租户分为大客户与普通客户,每个大客户都有一个单独的资源分区,而普通客户可以以N个为一组,共享同一个分区的资源。
PaaS 平台的领域模型设计
我们知道,微服务架构下的一个应用通常是由多个微服务所组成的,而我们的Kubernetes通常会部署多个独立的应用,因此,如果用 Kubernetes建模微服务应用,则我们需要在 PaaS平台的领域模型中设计出 Application这个领域对象,一个Application包括多个微服务,并且最终在发布(部署)时会生成对应的Pod、Deployment 及 Service对象,如下图所示。
如下所示是有更多细节的领域模型图,Kubernetes中的 Node、Namespace分别被建模为K8sNode与TanentNS,分区则被建模为ResPartition对象,每个分区都可以包括1到N个TanentNS,即一个或多个租户(Tanent〉使用。每个租户都包括一些用户账号(User),用来定义和维护本租户的应用(Application)。为了分离权限,可以使用用户组(User Group)的方式,同时可以增加标准的基于角色的权限模型。
上图中的Service领域对象并不是Kubernetes Service,而是包括了Kubernetes Service及相关RC/Deployment的一个“复合结构”。在Service领域对象中只包括了必要的全部属性,在部署应用时会生成对应的Kubernetes Service和RC/Deployment实例。下图给出了Service的定义界面(原型)。
我们在界面上完成对一个Application的定义后,就可以发布应用了。在发布应用的过程中,先要选择某个分区,然后程序调用Kubernetes的 API接口,创建此 Application相关的所有Kubernetes资源对象,然后查询Pod的状态即可判断是否发布成功及失败的具体原因。下面给出了Application从定义到发布的关键模块的设计示意图。
我们知道Kubernetes是基于容器技术的微服务架构平台,每个微服务的二进制文件都被打包成标准的Docker镜像,因此应用的全生命周期管理过程的第一步,就是从源码到Docker镜像的打包,而这个过程很容易实现自动化,我们既可以通过编程方式实现,也可以通过成熟的第三方开源项目实现,这里推荐使用Jenkins。下图是Jenkins实现镜像打包流程的示意图,考虑到Jenkins的强大和用户群广泛,很多PaaS平台都集成了Jenkins 以实现应用的全生命周期管理功能。
PaaS 平台的基础中间件
一个完备的PaaS平台必须集成和提供一些常见的中间件,以方便应用开发和托管运行。首先,第1类重要的基础中间件是 ZooKeeper,ZooKeeper非常容易被部署到Kubernetes集群中,在Kubernetes 的 GitHub上有一个YAML参考文件。ZooKeeper除了给应用使用,也可以作为PaaS平台面向应用提供的“集中配置服务”的基础组成部分,如下图所示。
此外,考虑到很多开源分布式系统都采用了ZooKeeper来管理集群,所以我们也可以部署一个标准命名的ZooKeeper Service,以供这些集群共享使用。
第2类重要的中间件就是缓存中间件了,比如我们之前提到的Redis 及 Memcache,它们也很容易被部署到Kubernetes集群中,作为基础服务提供给第三方应用使用。在Kubernetes的入门案例中有一个GuestBook例子,演示了在PHP页面中访问Redis主从集群的方法,即使是复杂的Codis集群,也可以被成功部署到Kubernetes集群中。此外,RedHat 的J2EE内存缓存中间件Infinispan也有Kubernetes集群部署的案例。
第3类重要的中间件是消息队列中间件,不管是经典的ActiveMQ、RabbitMQ还是新一代的Kafka,这些消息中间件也很容易被部署到Kubernetes集群中提供服务。下图是一个3节点的RabbitMQ集群在Kubernetes平台上的建模示意图。为了组成RabbitMQ集群,我们定义了3个Pod,每个Pod都对应一个Kubernetes Service,从而映射到3个RabbitMQ Server 实例,此外,我们定义了一个单独的Service,名为 ku8-rabbit-mq-server,此 Service对外提供服务,并且对应到上述3个Pod 上,于是每个Pod都有两个标签。
第4类重要的中间件是分布式存储中间件,目前在Kubernetes集群上可以使用Ceph集群提供的块存储服务及GlusterFS提供的分布式文件存储服务,其中 GlusterFS被RedHat的OpenShift平台建议为文件存储的标配存储系统,下面是这种方案的示意图。
在 RedHat的方案中,GlusterFS集群被部署在独立的服务器集群上,这适用于较大的集群规模及对性能和存储要求高的场景。在机器有限的情况下,我们也可以把Kubernetes集群的每个Node节点都当作一个GlusterFS的存储节点,并采用DaemonSet的调度方式将GlusterFS部署到Kubernetes集群上,具体的部署方式在Kubernetes 的 GitHub网站中有详细的说明文档,以Pod方式部署GlusterFS集群也使得GlusterFS 的部署和运维都变得非常简单。
提供全文检索能力的ElasticSearch集群也很容易被部署到Kubernetes中,前面提到的日志集中收集与查询分析的三件套ELK目前基本上全部以Pod的方式部署,以实现Kubernetes集群日志与用户应用日志的统一收集、查询、分析等功能。
在当前热门的大数据领域中,很多系统也都能以容器化方式部署到Kubernetes集群中,比如Hadoop、HBase、Spark 及 Storm等重量级集群。下一节将给出 Storm On Kubernetes 的建模方案,并且将其部署到Kubernetes集群中,最后提交第6章的WordCountTopology 作业并且观察运行结果。
Storm On Kubernetes 实战
通过第6章的学习,我们知道一个 Storm集群是由ZooKeeper、Nimbus (Master)及一些Supervisor (Slave)节点组成的,集群的配置文件默认保存在 conf/storm.yaml中,最关键的配置参数如下。
- storm.zookeeper.servers: ZooKeeper集群的节点IP地址列表。
- nimbus.seeds:Nimbus的IP地址。
- supervisor.slots.ports:Supervisor 中的Worker 监听端口列表。
从上述关键配置信息及Storm集群的工作原理来看,我们首先需要将ZooKeeper建模为Kubernetes Service,以便提供一个固定的域名地址,使得Nimbus 与Supervisor能够访问它。下面是ZooKeeper 的建模过程(为了简单起见,我们只建模一个ZooKeeper节点)。
首先,定义ZooKeeper对应的Service,Service名称为ku8-zookeeper,关联的标签为app=ku8-zookeeper 的Pod:
apiVersion: v1kind: Servicemetadata:
name: ku8-zookeeperspec:
ports:
name: clientport: 2181selector:
app: ku8-zookeeper
其次,定义ZooKeeper对应的RC:
apiversion: v1
kind: Replicationcontrollermetadata:
name: ku8-zookeeper-lspec:
replicas: 1template:
metadata:
labels:
app: ku8-zookeeperspec:
containers:
name: server
image: jplock/ zookeeper
imagePu1lPolicy: IfNotPresentports:
-containerPort: 2181
接下来,我们需要将Nimbus也建模为Kubernetes Service,因为Storm客户端需要直接访问Nimbus服务以提交拓扑任务,所以在conf/storm.yaml中存在nimbus.sceds参数。由于Nimbus在6627端口上提供了基于Thrift的 RPC服务,因此对Nimbus服务的定义如下:
apiversion: v1kind: Servicemetadata:
name: nimbusspec:
selector:
app: storm-nimbusports:
-name: nimbus-rpc
port: 6627
targetPort:6627
考虑到在storm.yaml配置文件中有很多参数,所以为了实现任意参数的可配置性,我们可以用Kubernetes的Config Map资源对象来保存storm.yaml,并映射到Nimbus(以及 Supervisor)节点对应的Pod实例上。下面是在本案例中使用的storm.yaml 文件(storm-conf.yaml)的内容:
storm.zookeeper.servers: [ku8-zookeeper]
nimbus.seeds: [nimbus]storm.log.dir: "log"
storm. local.dir: "storm-data"supervisor.slots.ports:
-6700
670167026703
将上述配置文件创建为对应的ConfigMap ( storm-config),可以执行下面的命令:
kubelet create configmap storm-config --from-file=storm-conf.yaml
然后,storm-config 就可以被任意Pod 以 Volume方式挂载到容器内的任意指定路径上了。接下来,我们可以继续建模 Nimbus服务对应的Pod。在从Docker Hub上搜寻相关 Storm镜像并进行分析后,我们选择了Docker 官方提供的镜像storm:1.0。相对于其他Storm镜像来说,官方维护的这个镜像有以下优点。
- Storm版本新。
- Storm整体只有一个镜像,通过容器的command 命令参数来决定启动的是哪种类型的节点,比如Nimbus主节点、Nimbus-ui管理器或者Supervisor 从节点。
- 标准化的Storm进程启动方式,可以将conf/storm.yaml配置文件映射到容器外,因此可以采用Kubernetes 的 ConfigMap特性。
采用storm:1.0镜像定义Nimbus Pod的YAML文件如下:
apiversion: v1kind: Pod
metadata:
name: nimbuslabels:
app: storm-nimbusspec:
volumes:
name: config-volumeconfigMap:
name: storm-configitems:
一key:storm-conf.yaml
path:storm.yaml
containers:
- name: nimbus
image: storm: 1.0
imagePullPolicy: IfNotPresentports:
-containerPort: 6627
command:【"storm" ,"nimbus" ]volumeMounts:
- name: config-volumemountPath: /conf
restartPolicy: Always
这里我们需要关注两个细节:第1个细节是ConfigMap 的使用方法,首先要把之前定义的ConfigMap ——storm-config映射为Pod的一个Volume,然后在容器中将此Volume挂接到某个具体的路径上;第2个细节是容器的参数 command,上面的command: [ "storm" , "nimbus"]表示此容器启动的是nimus进程。
类似地,我们定义storm-ui服务,这是一个Web管理程序,提供了图形化的Storm管理功能,因为需要在Kubernetes集群之外访问它,所以我们通过NodePort方式映射8080端口到主机上的30010。storm-ui服务的YAML定义文件如下:
apiversion: v1kind: Servicemetadata:
name: storm-uispec:
type: NodePortselector:
app:storm-uiports:
-name: web
port: 8080
targetPort: 8080nodePort:30010
最后,我们来建模Supervisor。Supervisor看似不需要被建模为Service,因为Supervisor 不会被主动调用,但实际上Supervisor节点之间会相互发起通信,因此Supervisor节点注册到ZooKeeper 上的地址必须能被相互访问,在 Kubernetes平台上有两种方式解决此问题。
第1种方式,Supervisor节点注册到ZooKeeper上时,不用主机名(Pod名称),而是采用Pod的P地址。
第2种方式,用Headless Service模式,每个Supervisor节点都被建模为一个HeadlessService,并且确保Supervisor节点的容器名称(主机名)与Headless Service的名称一样,此时Supervisor节点注册到ZooKeeper 上的地址就跟Headless Service名称一样了,Supervisor节点之间都能用对方的Headless Service的域名进行通信。
其中,第1种方式需要修改Supervisor的启动脚本和对应的参数才能进行,实现起来比较麻烦,第②种方式无须修改镜像就能实现,所以我们采用了第﹖种方式建模。下面是某个Supervisor节点的Service定义,注意 clusterIP: None的特殊写法:
apiversion: v1
kind: Servicemetadata:
name:storm-supervisorspec:
clusterIP:Noneselector:
app:storm-supervisorports:
- port: 8000
storm-supervisor 这个节点对应的 Pod定义如下,需要注意Pod 的名称为storm-supervisor,并且command的值为[ "storm", "supervisor"]:
apiversion: v1kind: Pod
metadata:
name: storm-supervisorlabels:
app: storm-supervisorspec:
volumes:
name: config-volumeconfigMap:
name: storm-configitems:
一key:storm-conf.yaml
path: storm.yaml
containers:
name: storm-supervisorimage: storm: 1.0
imagePullPolicy: IfNotPresent
command:["storm", "supervisor" ]volumeMounts:
-name: config-volumemountPath: /conf
restartPolicy:Always
我们可以定义多个Supervisor 节点,比如在本案例中定义了两个Supervisor节点。在成功部署到Kubernetes集群后,我们通过Storm UI的30010端口进入Storm的管理界面,可以看到如下界面。
下面这个截图验证了两个Supervisor 节点也可以被成功注册在集群中,我们看到每个节点都有4个Slot,这符合我们在storm.yaml中的配置。
至此,Storm集群在Kubernetes 上的建模和部署已经顺利完成了。接下来我们看看如何在Storm集群中提交之前学习过的WordCountTopology作业并且观察它的运行情况。
首先,我们可以去 https:/ljar-download.com/下载编译好的 WordCountTopology 作业的JAR文件
storm-starter-topologies-1.0.3.jar,然后通过Storm Client工具将该Topology作业提交到Storm集群中,提交作业的命令如下:
storm jar/userlib/storm-starter-topologies-1.0.3.jar org.apache.storm.starter.ordcountTopology topology
由于在storm:1.0镜像中已经包括了Storm Client 工具,所以最简便的方式是定义一个Pod,然后把下载下来的
storm-starter-topologies-1.0.3.jar作为Volume映射到Pod里的/userlib/目录下。将容器的启动命令设置为上述提交作业的命令即可实现,下面是此Pod 的YAML定义:
apiversion: v1
kind: Podmetadata:
name: storm-topo-examplespec:
volumes:
name:user-libhostPath:
path: /root/stormname: config-volumeconfigMap:
name:storm-configitems:
-key: storm-conf.yaml
path: storm. yaml
containers:
name: storm-topo-exampleimage: storm: 1.0
imagePullPolicy: IfNotPresent
command: [ "storm","jar", "/userlib/storm-starter-topologies-1.0.3.jar",
"org.apache.storm.starter.WordCountTopology", "topology" ]
volumeMounts:
- name: config-volumemountPath: /conf
name:user-lib
mountPath: /userlib
restartPolicy: Never
上述定义有如下关键点。
- 将storm-starter-topologies-1.0.3.jar 放在主机的/root/storm目录中。
- 容器的启动命令是storm client,提交Topology 作业。
- Pod重启策略为Never,因为只要提交完Topology 作业即可。
创建上述 Pod以后,我们查看该Pod 的日志,如果看到下面这段输出,则表明WordCountTopology的拓扑作业已经被成功提交到Storm集群中了。
接下来,我们进入Storm UI去看看作业的执行情况。下图是WordCountTopology的汇总信息,状态为Active,运行了8分钟,占用了3个Worker进程,总共运行了28个Task。
在成功提交到Storm集群后,我们可以进入Supervisor节点(Pod)查看拓扑作业的日志输出,作业的日志输出在目录/log/workers-artifacts下,每个拓扑作业都有一个单独的文件夹存放日志,我们搜索WordCountTopology 的最后一个 Bolt——统计发送Tuple的日志,可以看到如下结果,即每个Word(字)都被统计输出了。
下面这个界面给出了WordCountTopology 的详细信息,分别显示了拓扑里所有Spout的相关信息,例如生成了几个Task、总共发送了多少个Tuple、失败了多少个,以及所有 Bolt 的相关信息,例如处理了多少个 Tuple、处理的延时等统计信息,有助于我们分析Topology 作业的性能瓶颈和改进的可能性。
除了上面的列表信息,Storm UI还提供了展示Stream运行情况的拓扑图,如下图所示,我们看到数据流从spout节点发出,经过 split 节点处理时用了3.13ms,然后抵达count节点,count节点的处理耗时为0.06ms。
Storm 的 Topology 作业一旦运行起来就不会停止,所以你会看到下面界面中的Tuple 的统计数字在不断增加,因为WordCountTopology的 Spout 节点在不断生成Tuple,所以如果我们需要停止作业,则可以单击图中的 Deactvate按钮挂起作业,或者终止作业。