因为中间省略了8个字母,这是硅谷那些程序员的一个惯例。
BusyBox是一个遵循GPL协议、以自由软件形式发行的应用程序。Busybox在单一的可执行文件中提供了精简的Unix工具集,可运行于多款POSIX环境的操作系统。在Kubernetes的使用中,BusyBox常作为Pod的init container执行一些预配置工作。
容器是运行在Linux上的一个隔离进行运行环境,使用Linux的namespace机制做隔离,cgroups做资源限制,rootfs做文件系统。运行在容器里的进程貌似自己单独运行在一个操作系统中。
容器和虚拟机的区别是,虚拟机真的模拟出了一个操作系统,而容器仅仅是环境隔离,运行在同一个宿主机上的容器是共享宿主机内核的。
容器镜像的概念来自于Docker。Docker是一个打包,分发和运行应用程序的平台。它将应用程序以及其依赖的环境,包括操作系统打包到一起,叫做镜像。通过镜像分发的应用具有完全相同的环境,分发以后,Docker还可以以容器的方式运行镜像。镜像也叫Docker Image,分发Docker Image的平台叫做镜像仓库,运行Docker Image的容器就叫做Docker Container。
Kubernetes是一个部署和管理容器化软件的平台。它将底层的基础设施都抽象化,让开发者部署一个集群就像部署一个节点那样简单。
Kubernetes由两部分组成,控制面板和工作节点。控制面板运行在主节点上,容器运行在工作节点上。控制面板组件提供了
工作节点上的组件有:
Pod是一组并置的容器,代表了Kubernetes中基本的调度单元。一个Pod可以运行多个容器,然而大部分时间只运行一个。在同一个Pod中的容器,总会被调度到同一个节点上。所以Pod管理的是容器组。
命名空间提供了另外一个维度的隔离,标签组织的Pod(或者其他资源)好比在数据库中的一张表上,可以通过不同的字段来选出不同Pod组,但是主键(Pod的名字)不能重复。而命名空间相当于另外一张表,Pod名字不仅可以重复,在一个命名空间中的kubectl命令操作也不会影响另外一个命名空间的资源。命名空间可以用来组织Kubernetes中的不同项目,或者不同开发,测试,线上的环境。
存活探针检查容器是否在正常运行。有时候容器并没有崩溃,但是已经不能正常工作了,比如进入死循环,比如死锁(最近发现一个例子是数据库连接池资源耗尽,程序缓慢的等待一个连接超时才能进行一个查询),这时候Kubelet并不能发现容易运行异常,需要使用外部手段探测,存活探针就是干这个的。
存活探针有3种:
如果一个容器崩溃了,节点上的kubelet会重启这个Pod,但是如果这个节点完蛋了,就没有人负责这个Pod了。ReplicationController和ReplicationSet是Kubernetes集群级别的资源,由它们可以通过标签选择负责一组Pod,在节点Fail的时候,会选择其他节点运行Pod,保证Pod的数目符合预期。使用ReplicationController或者ReplicationSet创建的Pod也叫托管Pod。托管Pod的扩容,缩容只需要简单指定期望个数即可,ReplicationController或者ReplicationSet会自动增加或者删除Pod。
ReplicationSet的行为和ReplicationController完全相同,但是Pod选择器的表达能力更强。
ReplicationController只允许包含某个标签的匹配,ReplicationSet还允许缺失某个标签的匹配,标签多个值的匹配等等。应该使用ReplicationSet完全替代ReplicationController。
DaemonSet在每一个节点上运行一个Pod,一般都是和系统相关的,比如Kubernetes的kube-proxy进程。DaemonSet也可以通过标签选择节点,比如,把所有的SSD节点打上标签,然后将一个ssd-monitor的进程托管给DaemonSet,选择有SSD标签的节点。
Job用于运行一次的任务(Pod)。因为一般的Pod如果退出,会被ReplicationSet重启,Job正常退出后不会被重启,但是如果是异常退出(崩溃或者返回错误的退出码),或者节点失败,仍会被调度到其他节点重新运行。
还有一种定时运行的Job叫做CronJob,可以在将来某一个时刻,或者周期性的时刻运行一个Pod。
Job和ReplicationSet, DaemoSet是相同级别的概念,都是运行Pod的一种方式,在YAML定义中都是先定义自己,然后定义Pod,所以有两个spec
因为Pod是不稳定的,可能由于扩容,缩容, 节点失败等原因被调度到其他地方,并且Pod的IP是调度的时候才赋予的,因此,通过静态的配置文件是无法获得稳定的Pod IP地址列表。服务相当于一个集群内的反向代理,使得提供相同服务的Pod可以通过一个单一稳定的ClusterIP来访问提供服务的Pod。
通过集群IP,客户端的请求每次只能转发到一个Pod上,有时候客户端需要连接所有Pod或者自己选择连接哪个Pod,这时候可以使用headless服务。将服务定义中的clusterIP设置为None,就会创建一个headless服务。这时候用过域名(FQDN)访问Kubernetes的DNS会返回所有Pod的IP地址。
通过nslookup查看服务的DNS记录,正常的服务返回的是服务的IP地址,headless服务返回的是Pods的IP列表。
分为三种类型,集群内服务发现,集群内发现集群外服务,集群外发现集群内服务。
集群内服务发现
通过环境变量发现。在一个Pod开始运行的时候,Kubernetes会将已有的服务写入到Pod的环境变量中。Pod上的进程可以根据环境变量获取服务的IP和端口号。但是在Pod创建之后创建的服务无法被这个Pod发现。
通过DNS发现服务。Kubernetes有一个Pod叫做kube-dns,是集群内部的DNS服务器,如果Pod使用这个内部的DNS服务(通过dnsPolicy属性指定),就可以通过服务的名字找到服务的IP。这种方式只能获得服务的IP,端口号如果是标准端口号(比如80,3306等)也没有问题。否则就需要程序预先配置或者从环境变量读取。
连接集群外部服务
先了解下什么是EndPoint?EndPoint也是Kubernetes的资源,是一组IP和端口的抽象。服务通过Pod的选择器得到一组EndPoint。当客户端连接到服务时,服务代理选择一组EndPoint,将连接重定向过去。
创建一个不包含Pod选择器的服务,这样这个服务就没有EndPoint,然后,手动创建EndPoint,将外部服务的IP和Port赋值给EndPoint,并制定这个EndPoint属于之前那个服务。这样内部到服务的连接就会被重定向到外部服务。
为外部服务创建别名。创建一个新服务,type属性为ExternalName,然后执行externalName属性为外部域名。然后内部Pod就可以使用内部服务域名来访问外部服务了。ExternalName服务仅在DNS级别实施,为服务创建了简单的CNAME DNS记录,连接到服务的客户端将直接连接到外部服务,完全绕过代理。
这个功能的用途是,可以让只少数集群节点具有外网访问功能,创建External服务的时候指定节点选择器,放在有外网服务的节点上,其他Pod都没有外网访问权限。
将服务暴露给外部
使用Ingress必须确保集群的Ingress控制器已经启用,可以通过addons查看。(基本上都会启用)
Pod启动后,会立即加入服务,接收到请求,这时候如果Pod中的服务进程还没有初始化完毕,这些请求就会失败,就绪探针就是解决这个问题的。就绪探针周期性检查容器,如果检查失败,则将Pod从服务中移除,然后继续检查,如果随后容器就绪,再重新加回服务。
就绪探针和存活探针一样,有三种类型,Exec,HTTP GET和TCP Socket。但是与存活探针不同的是,就绪探针检查失败,并不重启容器,而是将Pod移出服务。就绪探针确保服务中的Pod都是可用的。
因为每个容器都有自己独立的文件系统,默认是无法共享的。Kubernetes使用存储卷来管理文件系统。卷不是Kubernetes的顶级资源,而是Pod的一部分。Pod中的容器通过卷来共享数据。
卷有很多种,下边介绍几个简单的:
如果应用不支持动态更新配置,最好不要使用热更新配置的特性,因为已经存在的Pod不会更新配置,新创建的Pod使用新配置,造成容器的不一致性。除非这种不一致是预期的。
ConfigMap用于传递非敏感配置,数据明文存放的,Secret用于传递敏感配置,数据是base64编码的,并且只在内存中加载,不会写入磁盘
通过环境变量暴露的Secret可能在程序崩溃的时候被dump出来,另外子进程自动继承父进程的环境变量,这都是可能泄露的原因。推荐使用卷的范式传递Secret
Downward API 和 访问Kubernetes API Server
Downward API
在yaml文件中将Pod自身的元数据写入环境变量或者映射到Downward API卷中。这种方式只能获取自身的metadata,并且信息有限。
Kubernetes API Server
通过Kubernetes API Server的RESTFul API获取集群中的信息,这种方式可以获取集群中更多的资源信息。不仅限于Pod自身的运行环境。
因为Pod的Label和Annotation是可以动态改变的(不用重启Pod ),而环境变量一旦被加载,就不能改变了。
Pod如何与Kubernetes API Server 通信?
要解决两个问题,一个是Pod如何找到API Server的地址,第二个是Pod和API Server如何互相信任。
Pod如何找到API Server的地址?
Kubernetes API Server也是一个服务,它的地址和端口在Pod启动的时候就已经作为环境变量写入到每一个容器中了。
root@curl:/# env | grep KUBERNETES_SERVICE
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_HOST=10.0.0.1
KUBERNETES_SERVICE_PORT_HTTPS=443
如果API Server使用的是通用端口(443),即使不通过环境变量,也可以通过DNS直接访问API Server服务(https://kubernetes.)
Pod和API Server如何互相信任?
Kubernetes 会创建一个叫做default-token-xyz的Secret,这个Secret会被自动mount到每一个容器中。
root@curl:/# ls /var/run/secrets/kubernetes.io/serviceaccount/
ca.crt namespace token
Pod使用ca.crt文件来验证API Server的真实性。Pod通过传递token来证明自己的合法性。
root@curl:/# TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
由于Pod与API Server 的互相验证过程复杂且程序化,因此可以使用一个代理容易来做这件事,将验证过程抽出来镜像化,之后就可以复用。这种模式叫做ambassador模式。
另外也可以使用Kubernetes API客户端来做这件事
Deployment会创建另外一个ReplicaSet,启动新版本的应用Pod,待新版本的Pod加入服务并且Ready之后,原来的ReplicaSet会删除一个旧Pod,实现滚动更新,一次更新几个Pod是可以通过maxSurge和maxUnavailable配置的,默认是总Pod数的25%。还可以使用minReadySeconds减慢滚动更新速度,来确保滚动升级的安全。
因为就绪探针成功一次就会被认为就绪了,然后就加入到服务,之后会删除旧版本的Pod然后再启动一个新版本的Pod,造成更新不可控。使用minReadySecond可以多观察一会服务,稳定了再进行下一步。最好minReadySecond中要有几次就绪探测。
对于无状态的服务,应该使用RelicaSet,RelicaSet管理的Pod之间提供的服务没有实质的差别。对于有状态的服务,应该使用StatefulSet,StatefulSet管理的Pod都是独一无二的,如果一个Pod死掉了,StatefulSet保证新启动的Pod具有一样的名称,网络地址,存储。一些例子:显然,对于提供持久存储服务的Pod,应该使用StatefulSet管理,这样才能不丢失数据。另外对于一些分布式应用,不同的Pod有不同的角色,应用需要通过稳定的网络地址访问服务,即使这个角色的Pod挂掉了,新启动的Pod需要恢复这个角色的状态。
如果不使用headless服务,服务的DNS解析返回的是服务的虚拟IP地址,无法访问指定的Pod。headless服务解析的时候,会返回所有Pod的IP,StatefulSet会给这些IP配置一条DNS域名,类似a-0.foo.default.svc.cluster.local
。因为StatefulSet Pod名字是稳定不变的,因此就可以通过每一个Pod的域名来访问Pod,这在不同的Pod有不同的司职的情况下能提供方便。
否则需要将不同司职的Pod创建不同的服务
同时每个Pod有了域名之后,StatefulSet还会为headless服务创建一条SRV记录。
SRV记录是DNS服务器的数据库中支持的一种资源记录的类型,它记录了哪台计算机提供了哪个服务这么一个简单的信息。与常见的A记录、cname不同的是,SRV中除了记录服务器的地址,还记录了服务的端口,并且可以设置每个服务地址的优先级和权重。
SRV的记录格式为:_Service._Proto.Name TTL Class SRV Priority Weight Port Target