在K8S中,Service是分布式集群架构的核心,一个Service对象拥有如下关键特征。
Service的服务进程目前都基于Socket通信方式对外提供服务。虽然一个Service通常由多个相关的服务进程提供服务,每个服务进程都有一个独立的Endpoint(IP+Port)访问点,但Kubernetes能够让我们通过Service(虚拟Cluster IP+Service Port)连接到指定的Service。这个Service本身一旦创建就不再变化。
Service提供服务的这组进程放入容器中进行隔离。为此,Kubernetes设计了Pod对象,将服务进程放入到Pod。因此,就产生了一个Service与Pod的对应关系问题,K8s首先给每个Pod贴上标签(Label),然后给相应的Service定义标签选择器(Label Selector),就解决了Service与Pod的关联问题。
Pod简单介绍,Pod运行在被称为Node的节点上,可以是虚机也可以是物理机,每个Node上可以运行上百个Pod。Pod中特备地具有一个叫Pause的容器,其他称为业务容器,业务容器共享Pause容器的网络栈和Volume挂载卷。设计时尽量把相关的服务放在一个Pod中,但不是每个Pod和它里面运行的容器都能被映射到一个Service,只有提供服务(无论对内还是对外)的那组Pod才会被映射为一个服务。
在集群管理方面,k8s将集群中的机器划分为一个Master和一些Node。在Master上运行着集群管理相关的一组进程,kube-apiserver,kube-controller-manager,kube-scheduler,自动化地进行集群管理。Node作为集群地工作节点,运行着真正的应用程序,在Node上K8S管理地最小单元是Pod。在Node上运行着K8S的kubelet、kube-proxy服务进程,对Pod进行管理。
在K8S集群中,需要扩容地Service关联地Pod创建一个RC(Replication Controller),服务扩容以至服务升级等令人头疼地问题都迎刃而解。在一个RC定义文件中包括以下3个关键信息:
K8S会通过RC中定义的Label筛选出Pod,监测其数量,如不达到,就复制Pod模板选择合适的Node进行启动。
K8S中的大部分概念如Node,Pod,Replication Controller,Servcie等都可以被看做一种资源对象,几乎所有资源对象都可以通过K8s提供的kubectl工具(或者API编程调用)执行增删改查等操作并将其保存在etcd中持久化存储。从这个角度,K8s其实是一个高度自动化的资源控制系统,它通过跟踪对比etcd库里保存的“资源期望状态”与当前环境中的“实际资源状态”的差异来实现自动控制和自动纠错的高级功能。
在声明一个Kubernetes资源对象的时候,需要注意一个关键属性:apiVersion。
k8s平台采用“核心+外围扩展”的设计思路。k8s大部分常见的核心资源对象都归属于都属于v1这个核心API,比如Node,Pod,Service,Endpoints,Namespace,RC,PersistentVolume等。还有测试的例如extensions/v1beat1,apps/v1beta1,apps/v1beta2等API组,而在1.9版本后引入apps/v1的正式API组,正式淘汰上述三个测试组。
可以用YAML或JSON格式声明(定义或创建)一个kubernetes资源对象,每个资源对象都有自己的特定语法格式(可以理解为数据库中一个特定的表)。在版本迭代中,为了引入新特性且不影响当前功能,有两种典型方法:
在每个表中,增加一个很长的备注字段,之后扩展的数据以某种格式(如XML,JSON,简单字符串拼接等)放入备注字段。程序改动范围小,但不美观。
annotations:
pod.beta.kubernetes.io/init-containers:'[
{
"name":"init-mydb",
"image":"busybox",
"command":[....]
}
]'
方法2,直接修改数据库表,增加一个或多个新的列,此时程序改动范围大,风险大,但不美观。
initContainers:
-name:init-mydb
image:busybox
command:
- xxx
Kubernetes为每个资源对象都增加了类似数据库表里的备注字段的通用属性Annotations。
Master是集群控制节点,在每个Kubernetes集群里都需要有一个Master来负责整个集群的管理和控制,基本上所有的控制命令都是发给它,它负责具体的执行过程。占据一个独立的服务器,高可用多个。
在Master上运行着以下关键的进程。
另外,在Master上通常还需要部署etcd服务,因为Kubernetes里的所有资源对象的数据都被保存在etcd中。
除了Master,其他节点称为Node,每个Node都会被Master分配一些工作负载(Docker容器),当Node宕机时,其上的工作负载会被Master自动化地转移到其他节点上。
在每个Node上都运行着以下关键进程:
Node可以在运行期间动态增加到Kubernetes集群中,前提是在这个节点上已经正确安装、配置和启动了上述关键进程,在默认情况下kubelet会向master注册自己,这也是kubernetes推荐地node管理方式。一旦Node被纳入集群管理的范围,kubelet就是定时向master汇报自己的操作系统、CPU和内存使用情况,Docker版本,以及当前有哪些Pod在运行等,这样Master就可以获知每个Node的资源使用情况,并实现高效均衡的资源调度策略。若Node超过指定时间没有上报信息给Master会被认为是失联,且被标记为不可用Not Ready,随后Master会触发“工作负载大转移”的自动流程。
查看集群中的Node的数量:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
k8s-node-1 Ready 350d v1.14.0
查看某个node的详细信息
kubectl describe node
详细信息中的解释:
Pause容器:gcr.io/google_containers/pause-amd64。其他为业务容器。
原因:
K8S为每个Pod分配唯一的IP地址,称之为Pod IP,一个Pod里有多个容器共享Pod IP地址。K8S允许集群内任意两个不同Pod间的容器通信,通过L2的Opne vSwitch或Flannel虚拟网络技术实现。
Pod分为:普通的Pod和静态的Pod(Static Pod)。Static Pod比较特殊,不放在集群中Master的etcd中,而被放在某个具体的Node上为一个具体文件,并且只在此Node上启动,运行。普通的Pod先创建放在etcd中,然后master会根据情况调度它到具体的node进行bending绑定,随后kubelet会将此pod实例化成一组相关的docker进行启动。当master发现某个Pod中的一个容器宕机,则master重启这个Pod(即其中所有容器),如果是Node宕机,则将此Node上的所有Pod转移到新的Node上。
kubernetes里所有的资源对象都可以用YAML或者JSON格式的文件定义或描述,
apiVersion:v1
kind:Pod
metadata:
name:myweb
labels:
name:myweb
spec:
containers:
-name:myweb
image:kubeguide/tomcat-app:v1
ports:
- containerPort:8080
env:
- name:MYSQL_SERVICE_HOST
value:'mysql'
- name:MYSQL_SERVICE_PORT
value:'3306'
kind声明一个Port定义。metadata里的name属性是Pod的名称,在metadata里还能定义资源对象的标签,这里声明myweb拥有一个name=myweb的标签。在Pod里所包含的容器组的定义则在spec一节中声明,这里定义了一个名为myweb、对应镜像为kubeguide/tomcat-app:v1的容器,该容器注入了名为MYSQL_SERVICE_HOST='mysql’和MYSQL_SERVICE_PORT='3306’的环境变量(env关键字),并且在8080端口(containerPort)启动容器进程。
Pod的IP加上这里的容器端口(containerPort)就组成了一个新的概念——Endpoint,它代表此Pod里的一个服务进程的对外通信地址。一个Pod也存在具有多个Endpoint的情况,比如当我们把Tomcat定义为一个Pod时,可以对外暴露管理端口与服务端口这两个Endpoint。
Pod Volume是Docker Volume的一些扩展,比如可以用分布式文件系统GlusterFS实现后端存储功能;Pod Volume是被定义在Pod上,然后被各个容器挂载到自己的文件系统中的。
Event是一个事件的记录,记录了事件的最早产生时间,最后重现时间,重复次数,发起者,类型,以及导致此事件的原因等众多信息。Event通常会被关联到某个具体的资源对象上,是排查故障的重要参考信息,Pod同样具有event。可以通过kubectl describe pod xxx
来查看它的描述信息,以定位问题的成因。
Pod可以被限制使用的CPU和内存大小。K8S里以千分之一的CPU配额为最小单位,用m来表示,通常一个容器的CPU配额被定义为100-300m,即占用0.1-0.3个CPU。Memory配额的单位是内存字节数,绝对值不是相对值。
计算资源配额限制的参数:
Requests:该资源的最小申请量,系统必须满足。
Limits:该资源最大允许使用的量,不能突破,如果容器试图超过这个量,会被K8S“杀掉”重启。
spec:
containers:
- name:db
image:mysql
resources:
requests:
memory:"64Mi"
cpu:"250m"
limits:
memory:"128Mi"
cpu:"500m"
Mysql容器申请最少0.25个CPU以及64MiB内存,在运行过程中Mysql最多使用0.5个CPU和128MB的内存。
一个Label是一个key=value的键值对,其中key与value由用户自己指定。一个资源对象可以定义任意数量的Label。Label可以在创建时添加,也可在创建后动态添加或删除。指定资源对象捆绑一个或多个Label来实现多维度的资源分组管理功能,以便灵活、方便地进行资源调度分配配置部署等管理工作。常用Label:
随后可以通过Label Selector(标签选择器)查询和筛选拥有某些Label的资源对象,K8S通过这种方式实现了类似SQL的简单又通用的对象查询机制。
Label Selector表达式有两种:基于等式的(Equality-based)和基于集合的(Set-based),前者采用等式类表达式匹配标签,下面给出具体例子。
后者则使用集合操作类表达式匹配标签,具体例子。
多个表达式之间用【,】进行分隔,几个条件是“AND”的关系。
name=redis-slave,env!=production
name not in (php-frontend),env!=production
Pod的Label定义在metadata。而管理对象RC和Service则通过Selector字段设置需要关联的Pod的Label。
其他管理对象如Deployment、ReplicaSet、DaemonSet和Job则可以在Selector中使用基于集合的筛选条件定义。例如:
selector:
matchLabels:
app:myweb
matchExpressions:
- {key:tier,operator:In, values:[frontend]}
- {key:enviroment, operator:NotIn, values:[dev]}
matchLabels用于定义一组Label,与直接写在Selector中的作用相同;matchExpressions用于定义一组基于集合的筛选条件,可用的条件运算符包括In、NotIn,Exists和DoesNotExists。如果同时设置了matchLabels和matchExpressions则两组为AND关系。
Label Selector在Kubernetes中的重要使用情景:
RC定义了一个期望的场景,定义如下:
通过RC,K8S实现了用户应用集群的高可用性。且在运行时,可以通过修改RC的副本数量,来实现Pod的动态缩放(Scaling),这可以通过执行kubectl scale命令来一键完成:
kubectl scale rc redis-slave --replicas=3
需要注意的是,删除RC并不会影响通过该RC已经创建号的Pod。为了删除所有的Pod,可以设置replicas的值为0,然后更新该RC。K8S还具有“滚动升级”(Rolling Update),升级版本用。
Replication Controller在K8S的v1.2中,升级为另外一个新概念——Replica Set,官方解释为“下一代的RC”。Replica Set与RC当前的唯一区别是,Repelica Set支持基于集合的Label Selector,而RC只支持基于等价的Label Selector。
kubectl命令行工具适用于RC的大部分命令也适用于RS,但RS很少单独使用,它主要被Deployment这个更高层的资源对象所使用,从而形成了一整套Pod创建、删除、更新的编排机制。我们在使用Deployment时,无需关心它是如何创建和维护Replica Set的,这一切都是自动发生的。
最后总结一下RC的一些特性与作用:
内部使用Replica Set来实现目的,无论从Deployment的作用与目的、YAML定义,还是从它的具体命令行操作来看,我们都可以把它看作RC的一次升级。最大升级在于我们随时可以知道当前Pod“部署”的进度。启动N个Pod副本的目标状态。
Deployment的典型使用场景:
除了Kind和API声明有所区别,Deployment的定义与Replica Set的定义很类似:
apiVersion:extensions/v1beta1 apiVersion:v1
kind:Deployment kind:ReplicaSet
metadata: metadata
name:nginx-deployment name:nginx-repset
查看Deployment的信息
kubectl get deployments
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
tomcat-deploy 1 1 1 1 4m
查看对应的Replica Set
kubectl get rs
NAME DESIRED CURRENT AGE
tomcat-deploy-1640611518 1 1 1m
查看Replica Set创建的对应的Pod
kubectl get pods
NAME READY STATUS RESTARTS AGE
tomcat-deploy-1640611518-zhrsc 1/1 Running 0 3m
可以发现Pod的命名以Deployment对应的Replica Set的名称为前缀,这种命名很清晰地表明了一个Replica Set创建了哪些Pod,对于Pod滚动升级这种复杂的过程来说,容易排查错误。
Pod的管理对象,除了RC和Deployment,还包括ReplicaSet、DaemonSet、StatefulSet、Job等,分别应用于不同的场景。
Pod横向自动扩容,HPA。在K8S 1.2升级为稳定版本(apiVersion:autoscaling/v1),但仍保留了旧版本(apiVersion:extensions/v1beta1)。K8S的 1.6版本后,增强了根据应用自定义的指标进行自动扩容和缩容的功能,API版本为autoscaling/v2alphal,并不断演进。
原理:针对指定RC控制的所有目标Pod的负载变化情况,来确定是否需要有针对性地调整目标Pod的副本数量。
HPA度量Pod负载的指标:
CPUUtilizationPercentage是1min内的平均值,通常通过查询Heapster监控子系统来得到,需要安装部署Heapster,这样便增加了系统的复杂度和实时HPA特性的复杂度。因此,从版本1.7开始,有了一个基础性能数据采集监控框架——k8s Monitoring Architecture ,从而更好地支持HPA和其他需要使用到基础性能数据的功能模块。在Kubernetes Monitoring Architecture 中,kubernetes定义了一套标准化的API接口Resource Metrics API,以方便客户端应用程序(如HPA)从Metrics Server中获取目标资源对象的性能数据。到K8S 1.8则Resource Metrics API被升级为metrics.k8s.io/v1beta1。
HPA的例子:
apiVersion:autoscaling/v1
kind:HorizontalPodAutoscaler
metadata:
name:php-apache
namespace:default
spec:
maxReplicas:10
minReplicas:1
scaleTargetRef:
kind:Deployment
name:php-apache
targetCPUUtilizationPercentage:90
HPA控制的目标对象为一个名为php-apache的Deployment里的Pod副本,当这些Pod副本的CPUUtilizationPercentage的值超过90%的时候,会触发自动动态扩容行为,在扩容或缩容时必须满足的一个约束条件是Pod的副本数为1-10之间。
除了可以直接通过创建YAML文件并且调用kubectl create的命令来创建一个HPA资源对象的方式,还可以通过以下命令创建等价的HPA对象:
kubectl autoscale deployment php-apache --cpu-percent=90 --min=1 --max=10
Pod的管理对象RC、Deployment、DaemonSet和Job都面向无状态服务。但现实中有很多服务是有状态的,特别是一些复杂的中间件集群。这些集群有4个共同点:
RC或Deployment产生Pod的名称是随机的,IP也是随机的不满足上述第一点,不是有状态服务。另外,为了能够在其他节点上恢复某个失败的节点,这种集群中的Pod需要挂接某种共享存储,为了解决这个问题,就有了StatefulSet。从本质上说StatefulSet是Deployment/RC的变种。有以下特性:
StatefulSet不仅需要PV卷存储状态数据,而且还需要Headless Service配合使用,即在每个StatefulSet定义中都要声明它属于哪个Headless Service。Headless Service与普通Service区别在于它没有Cluster IP,如果解析Headless Service返回的是该Service对应的全部Pod的Endpoint列表。StatefulSet在Headless Service的基础上又为每一个Pod创建了一个DNS域名,这个域名的格式:
$(podname).$(headless service name)
例如3节点的Kafka的StatefulSet集群,对应的Headless service名字为Kafka,StatefulSet名称为kafka,则StatefulSet里的3个Pod的DNS名称分别是kafka-0.kafka,kafka-1.kafka,kafka-2.kafka,这些名字都可以在配置文件中固定下来。
Service没有共用一个负载均衡器的IP地址,每个Service都被分配了一个全局唯一的虚拟IP地址,这个虚拟IP地址被称为Cluster IP。每个服务就变成了具备唯一IP地址的通信节点,服务调用就变成了最基础的TCP网络通信问题。
服务发现问题的解决:只要用Service的Name与Service的Cluster IP地址做一个DNS域名映射即可完美解决问题。
kubectl get endpoints
可以看见产生一个Pod的IP地址以及Pod内容器暴露的端口号。
kubectl get svc tomcat-service -o yaml
查看Cluster IP
spec:
clusterIP:169.169.65.227
ports:
- port:8080
protocol:TCP
targetPort:8080
在spec.ports的定义中,targetPort属性用来确定提供该服务的容器所暴露(EXPOSURE)端口号;port属性则定义了Service的虚端口号。前面定义Tomcat服务时没有指定targetPort名则默认targetPort与port相同。
Service的多端口问题
要求每个Endpoint都定义一个名称来区分
spec:
ports:
- port:8080
name:service-port
- port:8005
name:shutdown-port
早期使用Linux环境变量解决这个服务发现的问题,每个Service的IP地址及端口都有标准的命名规范,遵循这个命名规范,就可以通过代码访问系统环境变量来得到所需的信息,实现服务的调用。
后来,增加了Add-On增值包引入DNS系统,把服务名作为DNS域名,这样程序就可以直接使用服务名来建立通信连接。
采用NodePort解决外部访问问题。
apiVersion:v1
kind:Node
metadata:
name:tomcat-service
spec:
type:NodePort
ports:
- port:8080
nodePort:31002
selecor:
tier:frontend
nodePort:31002这个属性表明手动指定tomcat-service的NodePort为31002,否则k8s会自动分配一个可用的端口。
接下来可以在浏览器里访问http://:310002/就可以访问了。
NodePort的实现方式是在K8S集群里的每个NODE上都为需要外部访问的Service开启一个对应的TCP监听端口,外部系统只要用任意一个Node的IP地址+具体的NodePort端口号即可访问此服务,在任意Node上运行netstat命令,就可以看到NodePort端口被监听。
netstat -tlp | grep 31002
但NodePort还没有完全解决外部访问Service的所有问题,比如负载均衡问题。Load balancer组件独立于Kubernetes集群之外,通常是一个硬件的负载均衡器或以软件方式实现,例如HAProxy或者Nginx。于是,k8s提供了自动化的解决方案,如果我们的集群运行在谷歌的公有云GCE上,那么只要把Service的type改为type=LoadBalancer,k8s就会自动创建一个对应的负载均衡器实例并返回它的IP地址供外部客户端使用。
批处理任务通常并行(或者串行)启动多个计算进程去处理一批工作先(work item),在处理完成后,整个批处理任务结束。
Job 于RC的区别:
Volume(存储卷)是Pod中能够被多个容器访问的共享目录。k8s的Volume与Pod的生命周期关联,而不与Docker相关联,docker可重启终止,且不会丢失数据。k8s支持多种类型的Volume,例如GlusterFS、Ceph等先进的分布式文件系统。
使用:先在Pod里声明一个Volume,然后容器引用该Volumn并挂载到容器里的某个目录上。例如:
template:
metadata:
labels:
app:app-demo
tier:frontend
spec:
volumes:
- name: datavol
emptyDir:{}
containers:
- name:tomcat-demo
image:tomcat
volumeMounts:
- mountPath: /mydata-data
name:datavol
imagePullPolicy:IfNotPresent
除了可以让一个Pod里的多个容器共享文件,让容器的数据写到宿主机的磁盘上或者写文件到网络存储中,K8S的Volumn还扩展出了一种非常有实用价值的功能,即容器配置文件集中化定义与管理,这是通过ConfigMap这种新的资源对象来实现的。
k8s提供了丰富的Volume类型,例如:
目前,无法指定emptyDir的介质种类。
hostPath为在Pod上挂载宿主机上的文件和目录,用途:
使用此类型的Volume,需要注意:
例如:
volumes:
- name: "persistent-storage"
hostPath:
path:"/data"
使用谷歌云提供的永久磁盘(PD)存放Volume的数据,永久保存。
使用gcePersistentDisk有以下一些限制条件:
Node(运行kubelet的节点)需要的是GCE虚拟机
这些虚拟机需要与PD存在于相同的GCE项目和Zone中
通过一个gcloud命令即可创建一个PD
gcloud compute disks create --size=500GB --zone=us-centall-a my-data-disk
定义gcePersistentDisk类型的volumes的示例:
volumes:
- name: test-volume
# This GCE PDmust already exist.
gcePesistentDisk:
pdName: my-data-disk
fsType:ext4
需要创建一个EBS Volume才能使用。
限制:
Node需要是AWS EC2实例
这些AWS EC2实例需要与EBS Volume存在于相同的region和availability-zone中
EBS只支持单个EC2实例挂载一个Volume
aws ec2 create-volume --availability-zone eu-west-la --size 10 --volume-type gp2
定义awsElasticBlockStore类型的Volume的示例:
volumes:
name:test-volume
awsElasticBlockStore:
volumeID:aws:///
fsType:etx4
使用NFS网络文件系统提供的共享目录存储数据时,需要在系统中部署一个NFS Server。定义NFS类型的Volume的示例如下:
volumes:
name:test-volumes
nfs:
#改为你NFS服务器地址
server:nfs-server.localhost
path:"/"
网络存储是相对独立于计算资源而存在的一种实体资源,被称为PV和与之相关联的Persistent Volume Claim(PVC)。PV可以被理解为K8S集群中的某个网络存储对应的一块存储,它有Volume类似,但有以下区别:
例子:NFS类型PV的一个YAML定义文件,声明需要5G的存储空间
apiVersion:v1
kind:PersistentVolume
metadata:
name:pv0003
spec:
capacity:
storage:5Gi
accessModes:
- ReadWriteOnce
nfs:
path: /somepath
server:172.17.0.2
比较重要的PV的accessModes属性,有以下类型:
如果Pod想申请某种类型的PV,则首先需要定义一个PersistentVolumeClaim对象:
kind:PersistentVolumeClaim
apiVersion:v1
metadata:
name:myclaim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage:8Gi
在Pod的Volume定义中引用上述PVC即可:
volumes:
- name: mypd
persistentVolumeClaim:
claimName:myclaim
PV是有状态的对象:
用于多租户的资源隔离。Namespace通过将集群内部的资源对象“分配”到不同的Namespace中,形成逻辑上分组的不同项目,便于不同的分组在共享使用整个集群的资源的同时还能被分别管理。
查询namespace
kubectl get namespaces
NAME LABELS STATUS
default Active
如果不加声明,都会被定义到default的namespace
apiVersion:v1
kind:Namespace ##
metadata:
name:development
定义了一个名谓development的Namespace
一旦创建了Namespace,在创建资源对象时就可以指定这个资源对象属于哪个Namespace,例如:
apiVersion:v1
kind:Pod
metadata:
name:busybox
namespace:development ##
spec:
containers:
- image:busybox
command:
- sleep
- "3600"
name:busybox
如果不加参数使用kubectl get pods仅仅显示属于default命名空间的资源对象。
kubectl get pods --namespace=development
NAME READY STATUS RESTARTS AGE
busybox 1/1 Running 0 1m
当给每个租户创建一个Namespace来实现多租户的资源隔离时,还能结合Kubernetes的资源配额管理,限定不同租户能占用的资源。
注解,Annotation是用户任意定义的附加信息,以便于外部工具查找。通常,用Annotation记录的信息如下:
配置文件中的参数在运行期间如何修改的问题。
Docker提供了两种方式:
通常后一种方法适合我们的系统,但这也有个缺陷:我们必须在目标主机上先创建号对应的配置文件,然后才能映射到容器里。希望能集中管理系统的配置文件。
首先,把所有的配置项都当作key-value字符串,当然value可以来自某个文本文件,比如配置项passwd=123456,user=root,host=192.168.8.4用于表示俩你今儿FTP服务器的配置参数。这些配置项可以作为Map表中的一个项,整个Map的数据可以被持久化存储在K8S的etcd数据库中,然后提供API以方便K8S相关组件或客户应用CRUD操作这些数据,上述专门用来保存配置参数的Map就是K8S ConfigMap资源对象。
接下来,K8S提供一种内建机制,将存储在etcd中的ConfigMap通过Volume映射的方式变成目标Pod内的配置文件,不管目标Pod被调度到哪台服务器上,都会完成自动映射。进一步的,如果ConfigMap中的key-value数据被修改,则映射到Pod中的“配置文件”也会随之自动更新。于是,k8s ConfigMap就成了分布式系统中最为简单(使用简单,但背后实现比较复杂)且对应用无侵入的配置中心。