Kubernetes

Kubernetes in Action读书笔记第二章:迈进Docker和Kubernetes的第一步

本章主要内容:

  • 如何创建、运行、推送一个Docker image
  • 配置kubectl命令行工具的命令补齐和对象名自动补齐
  • 运行一个Kubernetes应用
  • 对Kubernetes应用进行扩容缩容操作

1如何创建、运行、推送一个Docker image

1.1安装Docker并运行1个container

参考Docker官方文档来安装:https://docs.docker.com/engine/install/ 具体的安装步骤和流程就不再在这里赘述了。

接下来我们运行一个镜像名为busybox的container,这个image在Docker hub,即Docker的官方仓库上。

这个image里包含了标准的Unix命令行工具,如echo/ls、gzip等。我们在master节点上执行:

结合上面的命令执行输出,我们可以得到这条命令底层的执行流程图如下:

从本地机器搜索看看是否存在名为busybox的image?

不存在,则从Docker image registry查找并下载该image的最新版到本地机器,默认不带tag,则表示busybox:latest使用标签。

然后,Docker 通过该image启动并运行一个container,并且在该container中执行一条命令echo “Hello World…”,命令执行之后,便退出container,进而该container也退出运行状态。

1.1.1 如何运行其它image
1.1.2 如何运行不同版本的image

1.2如何创建1个Docker image

1.2.1准备1个名为app.js的node.js文件

通过node.js来启动运行一个web server,监听在8080端口,接收客户端发起的请求,返回一个200的状态码,并打印输出该web server所在主机名给客户端。

关于该部分代码,引入两个node.js的内置module,HTTP和OS,前者可以用来创建1个web server,OS可以用来操作读取主机信息。相关参考:https://www.w3schools.com/nodejs/nodejs_modules.asp

在本机Mac上演示如何运行和访问该应用,前提条件是执行app.js的机器必需安装了node.js的环境。当然,将来我们把这个app.js应用程序打成Docker image之后,就把node.js的环境打到该image文件里了。只要有可以运行Docker的环境就可以运行该image,而不再需要额外安装配置node.js。这也是本实验的一个演示目的。

session1,通过node app.js来启动该应用:

session2,模拟客户端发起请求:

1.2.2准备Dockerfile

在和app.js的同级路径下,创建1个名为Dockerfile的文件,内容如下:

第1行命令:我们需要使用名为node tag为7的基础镜像文件,作为我们生成image的基础;因为我们的程序是1个node.js的程序,我们需要1个可以执行node程序的环境。当然,我们也可以选择任意其它的基础镜像,只要它包含了可以运行node.js程序的环境即可。

第2行命令:表示我们要把当前路径下的app.js文件,添加到基础镜像node:7的根路径下,并且依然命名为app.js,即我们修改了这个基础镜像,在它的基础上添加了我们自己的文件;

第3行命令:表示一旦运行这个被我们修改过的image,或者是我们生成的这个最终的image时,就会在container中执行node app.js程序,这个不就是我们希望的结果吗?

这里的FROM、ADD、ENTRYPOINT是Dockerfile的关键字,先保持这样写。后面会再介绍说明。

1.2.3生成Docker image

有了我们自己的可执行程序app.js和Dockerfile之后,就可以构建生成1个Docker image了。

本地Docker环境:

创建image:

成功之后,看到生成了我们需要的image文件kubia。上述命令解释:我们告诉Docker基于当前路径下的所有文件内容,生成1个名为kubia的image。这个当前路径是命令行上最后的那个”.”所表示的含义。-t表示完整的意思是打tag,这里kubia的kubia等价于kubia:latest,默认表示是最新版的tag。当然可以任意取一个其它我们想要的image名,不一定非要叫做kubia。

docker首先在指定的路径下(这里用点.来指定当前路径)找到Dockerfile,然后根据Dockerfile里的内容,再去构建我们需要的image。

下图是上面命令的执行流程:

docker client(执行上述命令的工具)把需要用到的文件(这里就是当前路径下的Dockerfile和app.js文件)upload给docker daemon,由docker daemon根据Dockerfile里的执行,进行下载需要的基础镜像到本地(如果本地没有node:7 image的话),然后再构建我们需要的image文件。生成最终的image文件是由Docker daemon完成的,不是执行docker build -t xx_name yy_path命令的客户端完成的。

1.2.4再谈Docker image分层

在前面的第一章的3.3章节,我们提到docker image的分层除了可以实现image分发时的快速传递和多个container共享image layer之外,这里也顺便再次补充一点儿。我们在本地第1次执行docker build -t kubia .命令时,docker会默认在本地查找是否存在node:7的image,如果没有,则会从docker hub上下载该image到本地,这可能需要一些时间,但是,如果下次我们构建另外一个image,如果还需要用到该node:7作为基础镜像,则docker不会再次从docker hub上下载,因为本地已经存在。

另外,每一个docker image文件包含了若干layer,而不是仅仅只有1个layer。如,我们的Dockerfile中包含了3个命令,这里也需要注意的是,其实是docker从docker hub上下载了node:7基础镜像之后,当执行ADD app.js /app.js这条命令时,docker就在基础镜像上添加了1个layer,最后执行ENTRYPOINT [“node”,”app.js”]时,又添加了1个layer,只是这一层里添加的命令就是当通过该image来运行一个container时,会默认在container里运行这个命令:node app.js。

1.3如何运行我们创建的docker image

我们通过docker run –name kubia-container -p 8080:8080 -d kubia这条命令,就把我们的app.js这个程序运行在container里了,并且通过curl localhost:8080命令访问了运行在这个container中的应用了。

其中:

docker run是关键字指令;

–name kubia-container,表示指定我们的container的名字;

-p 8080:8080表示指定我们本地机器(运行这个container的机器)上的8080端口映射到container里的8080端口,也就是意味着,当我们访问本机的8080端口时,其实就是相当于访问container的8080端口,而我们container里的8080端口上的服务是由我们的app.js程序提供的。

后面的-d 表示我们的container是以detach方式,即后台的方式运行的;

最后的kubia指定的是image名字,其实这里相当于是kubia:latest;如果需要其它版本或者说其它tag来区分的话,就必须得显示指定出来;

整个命令输出的字符串:6fdb99edf0d69fe3a47b7f7ef889367d38f982b9c82e6aa277cabb29ac8725f1表示的是这个container的id;

docker ps输出的结果中container id列印证了上述的输出,同时command列中的内容表示的在这个container中正在执行的命令,node app.js;

curl localhost:8080返回的结果中,不再是本机名,而是我们的container的主机名,也是container的id;

1.4查看container的详细信息

1.5进入container一探究竟

通过执行docker exec -it kubia-container bash命令,其中的

exec是关键字(execute),表示要进入container执行命令;

-it分别表示interactive,和tty,即已交互式的方式,开启一个pseudo terminal;

kubia-container,表示要进入哪个container;

bash,意味着进入container之后,执行1个bash;

强调:这里执行的ps aux输出结果中,看到1号进程node app.js和13号进程bash,它们其实就是进入container之后,被完全隔离的2个进程,同时它们的namespace完全一致。

容器里运行的node app.js进程就被完全隔离了,此时它的pid=1,它认为它就是它所在那个容器的”全部”,此时它是看不到主机上任何其他进程的。但是,我们的主机上其实也运行着一个进程node app.js,只是主机上的该进程的pid完全不同于container中的pid。

如果我们的container是运行在Linux平台上的话,我们可以直接在Linux主机上执行ps aux|grep app.js来查看。如果我们的container是运行在Mac、Windows平台上的话,我们不能直接在主机上看到这个进程。需要进入到Docker给我们启动的一个VM里去,才能看的到。Docker另外运行了1个虚拟机,为什么这样呢?我们在前面提到,容器技术的本质是依赖于Linux的内核来实现的,Mac、Windows系统哪儿来的Linux内核呢。所以,Docker通过在这2个平台上启动一个VM来实现。

我这里的演示环境是Mac,所以,我可以通过下述命令,进入到我的Mac平台上的Docker的VM里:

如下:

通过docker run -it –rm –privileged –pid=host justincormack/nsenter1进入到Mac平台上的Docker VM之后,我们可以看到执行的node app.js进程号为2830,且前面我们执行的另外一个命令的bash进程号为2879。且这2个进程由于所在的container相同,所以他们的namespace也完全相同。

Mac上进入Docker VM的命令,docker run -it –rm –privileged –pid=host justincormack/nsenter1可以参考:https://gist.github.com/BretFisher/5e1a0c7bcca4c735e716abf62afad389

然后,也可以看到运行在container中的进程它的文件系统也是独立的,如上的node app.js它能看到的文件系统就是它所在的container中的文件系统:

看到里面的除了我们引用的那个基础的node:7镜像的文件系统,同时还包含了我们手工添加进去的app.js文件。也看到我们之所以可以在container中执行node app.js,是因为container中有了我们需要的执行node.js的node环境。

1.6删除和停止container

1.7推送image到Docker hub

Docker hub的地址为:https://hub.docker.com/ 在推送之前,我们需要注册一个用户,然后把我们的image打成Docker hub需要的固定格式:docker_hub_username/image_name:tag ,你的用户名/镜像名:tag版本

我们通过docker tag命令来把我们本地的名为kubia的image打成我们需要的格式:renguzi/kubia:mytag 其中的renguzi是我的Docker hub注册用户名,kubia是image名,mytag是我打的标签。

docker tag source_image target_image;这是该命令的使用方式。

推送到远端Docker hub:

经过一段时间之后,可以看到推送成功。首次推送的时候,可能需要通过docker login来登录到远程仓库。

1.8在其它机器运行该image

如下在一台可以运行docker容器,但是不能执行node.js程序的Linux机器上,我们可以直接通过docker来运行前面我们提交到docker hub中的renguzi/kubia:mytag这个image:

终于告一段落,我们原本需要自己安装配置node.js环境才可以执行我们的app.js,现在我们通过把该程序打成docker image,然后可以使之运行在docker container中,到最后我们把这个image push到docker hub。进而使得任意一台机器,只要它可以运行docker,可以访问docker hub,就可以pull该image到本地,并最终通过运行该image,进而启动一个隔离的container环境。

2 配置kubectl命令行工具的命令补齐和对象名自动补齐

我们在前面的搭建Kubernetes集群环境章节里,已经安装部署了3个节点的Kubernetes cluster。我们可以通过登录到master节点查看集群状态:

这里,根据作者在书中提到,我们可以通过在主机上安装bash-completion软件包来实现kubectl命令补齐和对象名自动补全,其功能类似于bash shell的tab补全和命令补齐。

然后,在root的~/.bash_profile里添加:

自此,当我们在命令上上执行kubectl 命令,以及查看和处理Kubernetes的各种对象,比如pod,service等,我们就可以通过tab键来自动补全我们的命令,或者对象名。这就是模拟了bash shell的命令自动补齐和文件名补全的功能。

3 运行一个Kubernetes应用

前面我们的应用是跑在docker容器里的,接下来我们想要把应用运行在Kubernetes中去。常规的操作是我们需要准备一份程序清单(1个yaml或者json格式的文件)给到Kubernetes的API server,但是,鉴于我们是第一次运行Kubernetes的应用,所以我们直接通过命令来操作,待逐渐熟悉Kubernetes之后,我们再采用传统的方式在Kubernetes上部署应用。

3.1在Kubernetes上运行1个ReplicationController

作者在原书P74使用的命令,在他当时使用的低版本的Kubernetes上可以正常使用。但是,在我这边的Kubernetes v1.23上,已经不能用了:

参考链接:https://community.kodekloud.com/t/unknown-tag-generator/25444

于是,我们还是采用最正宗的yaml方式来部署一个ReplicationController(这里没有一上来就部署Deployment资源,是有原因的,Deployment更高级别的抽象对象,而ReplicationController则比较偏底层,我们先从底层了解开始):kubia-rc.yaml源于源码第4章路径下(kubernetes-in-action/Chapter04/kubia-rc.yaml ),把replicas从3改为1了

通过下述命令来查看部署的ReplicationController和pods(加上-owide,表示显示对象的更多信息):

也许我们刚从Docker的世界里赶过来,想要看看我们刚刚通过Kubernetes创建和运行的是哪个container?但是,不要,我们要记住,在Kubernetes的世界里,我们管理和维护的基本单元是一个被称作Pod的玩意儿。我们可以这么简单理解,pod就是对container的封装,一个pod可以包含1个或者多个container。pod可以被理解为一个逻辑的机器,该机器可以有自己的独立IP地址,主机名。运行在同一个pod里的container拥有相同的namespace,运行在不同pod里的container的namespace不同,即使这些不同的pod运行在同一个worker node上。

顺便补充一句:在Kubernetes的世界里,其实我们不应该也不需要直接去管理和维护pod,而应该管理和维护的是在pod之上抽象出来的对象,如:service,Deployment,ReplicaSet等。随着学习的深入,我们才能理解这句话的意思。先学着吧,边学边看边理解边吸收。

A pod is a group of one or more tightly related containers that will always run together on the same worker node and in the same Linux namespace(s). Each pod is like a separate logical machine with its own IP, hostname, processes, and so on, running a single application. The application can be a single process, running in a single container, or it can be a main application process and additional supporting processes, each running in its own container. All the containers in a pod will appear to be running on the same logical machine, whereas containers in other pods, even if they’re running on the same worker node, will appear to be running on a different one.

P75

我们再来回过头看一下上面的kubectl get rc -owide命令的输出结果:我们创建了1个名为kubia的ReplicationController,我们DESIRED希望的|要求的个数是1个,当前CURRENT正好有1个,而且READY的也是1个,一切刚刚好。AGE字段描述的是这个ReplicationController截止到当前已经生存了33秒,Container指的是它底层的容器的名字;IMAGES是它的container所依赖的image名字,SELECTOR是它的选择器,这个我们先了解,后面会再详细涉及到。

kubectl get pods -owide的结果中的字段:STATUS 指的pod的当前状态,RESTARTS,pod重启的次数,我们知道Kubernetes会自动帮我们维护和管理pod,在某些场景下会reschedule到其它node,IP是当前pod的IP地址10.244.2.25,需要注意的是,pod的IP是有可能发生变化的,NODE表示当前pod运行在Kubernetes cluster中的哪个节点上,这里是运行在node-2上。这里,我们可以到node-2上去验证:

找到container id,然后通过docker exec -it 58ef90f9f8a6 /bin/bash 进入container内部,执行hostname,ps aux,ip a分别查看container的主机名,以及它内部所运行的进程信息,和network interface信息。看到的主机名为kubia-lzmxz,以及IP:10.244.2.24和我们从container外部看到的信息一样。

接下来,我们可以访问我们的pod:

接下来,再让我们把目光转向那个yaml文件,kubia-rc.yaml:

一个完整的yaml文件,通常第一行都是版本号:通常为v1,然后就是对象的资源类型。接下来包含3个部分:

  • 元数据:对象名,所在namespace,资源类型,标签信息等;
  • 描述信息:该类型的对象具体包含的内容,container,volume等信息;
  • 状态信息:当前对象的状态信息。

关于yaml:Yet Another Markup Language,也叫做YAML Arenot Markup Language。它是JSON的superset,任何一份合法的json文件都可以转为yaml。我们在Kubernetes里,建议选用yaml格式的文件来作为我们的manifest,毕竟相比于json,它的可阅读性更好一些。随着,我们对于Kubernetes中不同类型对象的yaml文件的使用和编辑,我们会慢慢的习惯于这种格式的声明文件。当然,我也会抽出时间和精力来另外写一些关于yaml的教程内容分享出来。

ReplicationController解释:它是封装在pod之上的用于统一管理调度pod的Kubernetes对象,我们创建ReplicationController对象,ReplicationController帮我们创建pod,我们不直接创建和管理pod。ReplicationController和pod通过label selector关联在一起。如果由ReplicationController创建的pod出现意外故障而宕掉,ReplicationController会自动帮我们重新拉起1个新的pod,用于取代那个故障的pod。而如果pod是我们手工创建的,它出现故障的话,Kubernetes不会干预和管理这个故障的pod。通过ReplicationController可以轻松容易的对它所管理的pod进行扩容缩容操作。ReplicationController对象渐渐的被ReplicaSet取代。关于它的进一步深入学习将在第4章。

3.2Kubernetes上运行ReplicationController的流程

好了,再一次让我们总览一下,在Kubernetes上部署一个应用的总体流程:

上图描述了,一个3节点的Kubernetes环境,部署一个应用的流程图,大概有8个步骤。

3.3通过service把ReplicationController暴露出去

在我们深入了解学习pod之前,我们先记住,我们的应用是跑在pod里的container里的,我们应该怎么访问这些应用呢?我们可以通过pod的IP访问,如上例:我们可以通过curl 10.244.2.25:8080进行服务的访问。但是,但是我们不应该也不需要通过pod的IP去访问服务。而是应该通过service去访问,这里的service是Kubernetes里的基于pod之上抽象出来的一种对象。后面的第五章,我们会详细深入学习。

这里,由于我们的Kubernetes cluster环境是部署在本地,无法提供基于loadbalancer类型(该类型服务通常由云服务厂商提供支持,具体后面第5章讲解)的service。当然,如果创建服务时指定了服务类型为LoadBalancer,如果我们的Kubernetes cluster不支持LoadBalancer的话,则会自动将该服务降级为NodePort类型的服务。这里就是这种情形,服务自动变为NodePort类型的service。关于Service的类型,我们先了解即可。

这里,我们创建了1个类型为NodePort(其实是由Loadbalancer降级来的)的service,它的名字是kubia-http,它是封装在名为kubia的ReplicationController之上的对象。从输出结果中的30928我们可以知道,这时候,我们可以通过这个Kubernetes集群的任意节点+这个端口号就可以访问我们的服务了。如下,我在我的Mac上,执行:

这也就意味着,我们的应用程序,跑在pod里的container里的那个node.js的app.js程序提供的服务,可以通过我们的3个节点进行访问了。而我们不需要关注这个container是跑在哪个pod上,这个pod的IP是什么,这个pod是运行在哪个node上。由于pod可能会从一个node调度到另外一个新的node上,且pod的IP地址会发生变化,我们不能保证通过一个固定的IP去访问服务。这就是为什么Kubernetes要在pod之上再封装1层service对象的根本原因了。

补充:作者在原书中使用的service类型是Loadbalancer,而这需要1个公网IP,如果你的Kubernetes运行在云服务商,如Google Kubernetes Engine或者AWS以及其他厂商的话,可以完成该类型service的创建。

Kubernetes的service类型,通常有4类:ClusterIP,NodePort,LoadBalancer,ExternalName。具体的内容,我们谈到service章节时,再深入学习了解。

3.4小结pod、container、ReplicationController、service的关系

我们的应用程序是运行在container中的,container跑在pod里,pod被ReplicationController管控,用户通过service来访问我们的服务。当service对象收到client请求时,它把请求转发到在它之下的ReplicationController管控的任意1个pod上,最后到达运行在pod的container里的应用上了。

这里更精髓的描述,可以翻看原书pdf的P80页(页面数是48)。作者描述的是真精髓。

4扩缩容Kubernetes应用

4.1扩容Kubernetes应用

前面,我们了解到在Kubernetes里我们不应该直接去创建和管理pod,而应该通过创建它上层的对象(ReplicationController,replicaSet、Deployment),然后通过上层的对象去管理和维护pod。也知道了,由ReplicationController管理的pod可以轻松快速进行扩缩容操作。

我们通过命令kubectl scale replicationcontroller kubia –replicas=3对名为kubia的ReplicationController进行扩容,要求它帮我们创建3个pod出来,–replicas=3实现的。

过一会儿之后,我们再次查看,发现多2个pod,且replicationcontrollers 的状态发生变化:

前面,我们不是讲到了通过service访问应用,而不应该通过pod访问的好处吗?也提到,当我们的请求到达service时,service把请求转发到由它管理的底层的任意一个pod上。我们现在知道,当前的service对象kubia-http通过NodePort类型的服务暴露出去,我们再次访问它验证一下:

从返回的结果,我们可以看到,服务请求确实到了底层的不同的pod上了。

4.2 缩容Kubernetes应用

接下来,演示一下如何缩容Kubernetes应用。我们再次把这里的kubia这个ReplicationController管理的pod的副本数从3个改为最开始的1个。

修改之前,我们通过kubectl get replicationcontrollers kubia查看其状态:

kubectl get replicationcontrollers kubia -oyaml以输出yaml格式到终端上,我们看到其副本数是3个。

通过kubectl edit replicationcontrollers kubia 对其进行修改:

修改之后:

过一会儿之后,我们会发现ReplicationController和pod的状态都发生了变化,pod从3个变成了1个,其中有2个状态为Terminating。

5 本章小结

学完本章,我们掌握了:

  • 如何创建、运行一个Docker container;
  • 如何把我们的应用打成Docker image,以及底层原理流程;
  • 如何把我们的Docker image push到Docker hub;
  • 在Kubernetes上运行我们自己的Docker image及底层原理流程;
  • 设置了kubectl命令行工具的命令补齐和对象名补全;
  • 了解了pod、container、ReplicationController、service的关系;
  • 快速水平扩、缩容我们的Kubernetes应用;

留言