分类目录归档:容器服务

定时自动重启Pod服务


2019年11月09日 03:25:44   6,689 次浏览

方法1:滚动重启

从1.15版开始,Kubernetes允许您滚动重启部署。作为Kubernetes的新增功能,这是最快的重启方法。

kubectl rollout restart deployment [deployment_name]

上面提到的命令将逐步执行关闭操作,并重新启动deployment中的每个pod容器。在重启过程中应用仍然可用,因为大多数容器仍在运行。

方法2:使用环境变量

另一种方法是设置或更改环境变量,以强制Pod重新启动并与您所做的更改同步。

例如,可以更改容器部署日期:

kubectl set env deployment [deployment_name] DEPLOY_DATE="$(date)"

在上面的示例中,该命令set env设置环境变量的更改,deployment [deployment_name]选择您的部署,并DEPLOY_DATE="$(date)"更改部署日期。

方法3:缩放副本数

我们可以使用该scale命令来更改deployment的副本数。将此数量设置为0实际上会关闭容器:

kubectl scale deployment [deployment_name] --replicas=0

要重新启动Pod,请使用相同的命令将副本数设置为大于零的任何值:

kubectl scale deployment [deployment_name] --replicas=1

将副本数设置为0时,Kubernetes会销毁副本。

将数字设置为大于0后,Kubernetes将创建新副本。新副本的名称将与旧副本的名称不同。您可以使用该命令kubectl get pods检查pod的状态,并查看新名称。

上面我们是通过(kubectl rollout重新启动) ,部署创建新的副本,并等待它们启动,然后删除旧的pods,并重新路由流量。服务将不间断地继续。接下来使用cronjob命令配合rollout restart每天定时重启动pod。

在开始之前必须先设置RBAC,以便从集群内部运行的Kubernetes client具有对Kubernetes API执行所需调用的权限。

---
# Service account the client will use to reset the deployment,
# by default the pods running inside the cluster can do no such things.
kind: ServiceAccount
apiVersion: v1
metadata:
  name: deployment-restart
  namespace: <YOUR NAMESPACE>
---
# allow getting status and patching only the one deployment you want
# to restart
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployment-restart
  namespace: <YOUR NAMESPACE>
rules:
  - apiGroups: ["apps", "extensions"]
    resources: ["deployments"]
    resourceNames: ["<YOUR DEPLOYMENT NAME>"]
    verbs: ["get", "patch", "list", "watch"] # "list" and "watch" are only needed
                                             # if you want to use `rollout status`
---
# bind the role to the service account
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: deployment-restart
  namespace: <YOUR NAMESPACE>
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: deployment-restart
subjects:
  - kind: ServiceAccount
    name: deployment-restart
    namespace: <YOUR NAMESPACE>

cronjob配置:

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: deployment-restart
  namespace: <YOUR NAMESPACE>
spec:
  concurrencyPolicy: Forbid
  schedule: '0 8 * * *' # cron spec of time, here, 8 o'clock
  jobTemplate:
    spec:
      backoffLimit: 2 # this has very low chance of failing, as all this does
                      # is prompt kubernetes to schedule new replica set for
                      # the deployment
      activeDeadlineSeconds: 600 # timeout, makes most sense with 
                                 # "waiting for rollout" variant specified below
      template:
        spec:
          serviceAccountName: deployment-restart # name of the service
                                                 # account configured above
          restartPolicy: Never
          containers:
            - name: kubectl
              image: bitnami/kubectl # probably any kubectl image will do,
                                     # optionaly specify version, but this
                                     # should not be necessary, as long the
                                     # version of kubectl is new enough to
                                     # have `rollout restart`
              command:
                - 'kubectl'
                - 'rollout'
                - 'restart'
                - 'deployment/<YOUR DEPLOYMENT NAME>'

如果希望cronjob等待部署完成,可以将cronjob命令更改为:

command:
 - bash
 - -c
 - >-
   kubectl rollout restart deployment/<YOUR DEPLOYMENT NAME> &&
   kubectl rollout status deployment/<YOUR DEPLOYMENT NAME>

Kubernetes Pod扩容与缩容


2019年11月07日 14:06:42   1,606 次浏览

针对不同时期流量的大小我们可以给Pod扩缩容,Kubernetes支持通过kubectl命令手动扩缩容,也支持通过HPA自动横向扩缩容。

手动扩缩容

现有如下deployment配置(nginx-deployment.yml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      name: nginx
  replicas: 3
  template:
    metadata:
      labels:
        name: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.7.9
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80

创建该Deployment:

有3个nginx实例,现在用下面这条命令将nginx实例扩充到5个:

kubectl scale deployment nginx-deployment --replicas 5

缩容:

kubectl scale deployment nginx-deployment --replicas 1

HPA

HPA能够根据特定指标完成目标Pod的自动扩缩容。创建一个与上面nginx-deployment相对应的HPA(nginx-deployment-hpa.yml):

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: nginx-deployment-hpa-v1
spec:
  maxReplicas: 6
  minReplicas: 2
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nginx-deployment
  targetCPUUtilizationPercentage: 50

主要参数如下:

  1. scaleTargetRef:目标作用对象,可以是Deployment、ReplicationController或ReplicaSet。
  2. targetCPUUtilizationPercentage:期望每个Pod的CPU使用率都为50%,该使用率基于Pod设置的CPU Request值进行计算,例如该值为200m,那么系统将维持Pod的实际CPU使用值为100m。
  3. minReplicas和maxReplicas:Pod副本数量的最小值和最大值,系统将在这个范围内进行自动扩缩容操作,并维持每个Pod的CPU使用率为50%。

使用HPA,需要预先安装Metrics Server,用于采集Pod的CPU使用率。

Kubernetes Service基础


2019年11月07日 13:29:07   1,419 次浏览

Kubernetes可以为一组具有相同功能的Pod提供一个统一的入口地址,并且将请求均衡的转发到各个对应的Pod上。本节主要记录Service的一些基本用法。

基本用法

创建一个Tomcat RC(tomcat-rc.yml):

apiVersion: v1
kind: ReplicationController
metadata:
  name: tomcat-rc
spec:
  replicas: 2
  selector:
    name: tomcat
  template:
    metadata:
      name: tomcat
      labels:
        name: tomcat
    spec:
      containers:
        - name: tomcat
          image: tomcat
          ports:
            - containerPort: 8080

创建该RC:

我们可以通过Node IP + Container Port在Kubernetes集群中访问Tomcat:

由于Pod是Kubernetes集群范围内的虚拟概念,集群外的客户端无法通过Pod的IP和端口访问,我们可以将Pod的端口号映射到宿主机,以使客户端应用能够通过物理机访问容器应用,修改刚刚的tomcat-rc.yml,添加hostPort:

apiVersion: v1
kind: ReplicationController
metadata:
  name: tomcat-rc
spec:
  replicas: 2
  selector:
    name: tomcat
  template:
    metadata:
      name: tomcat
      labels:
        name: tomcat
    spec:
      containers:
        - name: tomcat
          image: tomcat
          ports:
            - containerPort: 8080
              hostPort: 8081 # 新增

更新该RC:

可以看到tomcat pod被分配到了node1和node2上,所以我们可以在宿主机外使用或者访问tomcat:

但是我们知道,Pod的IP地址是不可靠的,例如当Pod所在的Node发生故障时,Pod将被Kubernetes重新调度到另一个Node,Pod的IP地址将发生变化。所以我们可以定义一个tomcat pod的统一访问入口,这就是Service的作用。

创建Service有两种方式:

1.kubectl expose命令来创建Service

kubectl expose rc tomcat-rc

现在我们就可以通过Service的clusterIP + Port来访问了:

Service地址10.1.187.222:8080均衡的负载到了两个tomcat pod上(10.244.1.19:8080和10.244.4.13:8080)

2.通过配置文件创建

定义一个tomcat-service.yml:

apiVersion: v1
kind: Service
metadata:
  name: tomcat-service
spec:
  ports:
    - port: 8081 # service端口
      targetPort: 8080 # 目标端口
  selector:
    name: tomcat # 选择器,选择name=tomcat的pod

创建该Service:

同样,Service默认是不能外部访问的,如果想让外部能够访问到tomcat service,我们也需要将Service的端口映射到物理机,修改上面的tomcat-service.yml:

apiVersion: v1
kind: Service
metadata:
  name: tomcat-service
spec:
  type: NodePort # 新增
  ports:
    - port: 8081
      targetPort: 8080
      nodePort: 30000 # 新增
  selector:
    name: tomcat

上面配置通过设置nodePort(范围30000-32767)映射到物理机,同时设置Service的类型为NodePort,创建该Service:

现在我们就可以通过宿主机的IP+30000访问tomcat了:

负载均衡策略

Kubernetes Service提供了两种负载分发策略:RoundRobin和SessionAffinity:

  1. RoundRobin:轮询模式(默认),即轮询将请求转发到后端的各个Pod上。
  2. SessionAffinity:基于客户端IP地址进行会话保持的模式,即第1次将某个客户端发起的请求转发到后端的某个Pod上,之后从相同的客户端发起的请求都将被转发到后端相同的Pod上。可以通过设置service.spec.sessionAffinity=ClientIP来启用SessionAffinity策略:
apiVersion: v1
kind: Service
metadata:
  name: tomcat-service
spec:
  type: NodePort
  sessionAffinity: ClientIP # 采用SessionAffinity策略
  ports:
    - port: 8081
      targetPort: 8080
      nodePort: 30000
  selector:
    name: tomcat

Headless Service

Headless Service不提供ClusterIP,仅通过Label Selector将后端的Pod列表返回给调用的客户端。

创建tomcat-headless-service.yml:

apiVersion: v1
kind: Service
metadata:
  name: tomcat-headless-service
spec:
  ports:
    - port: 8080
  clusterIP: None # 设置clusterIP为None,表示headless service
  selector:
    name: tomcat

创建该headless service:

Kubernetes Pod升级与回滚


2019年11月05日 14:00:55   1,425 次浏览

当集群中的某个服务需要升级时,我们需要停止目前与该服务相关的所有Pod,然后下载新版本镜像并创建新的Pod。如果集群规模比较大,则这个工作变成了一个挑战,而且先全部停止然后逐步升级的方式会导致较长时间的服务不可用。Kubernetes提供了滚动升级功能来解决上述问题。如果在更新过程中发生了错误,则还可以通过回滚操作恢复Pod的版本。

Deployment升级

现有如下deployment定义(nginx-deployment.yml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      name: nginx
  replicas: 3
  template:
    metadata:
      labels:
        name: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.7.9
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80

创建该deployment:

通过kubectl命令将nginx的版本更新到1.9.1:

kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1

查看滚动升级的过程:

查看Pod,会发现名称已经改变了:

上述滚动升级的过程可以用下图表示:

查看Deployment nginx-deployment的详细事件信息可以证明这一点:

kubectl describe deployments/nginx-deployment

查看rs:

默认的升级策略为RollingUpdate(滚动更新),Kubernetes还支持Recreate(重建)策略:

  1. Recreate:先杀掉所有正在运行的Pod,然后创建新的Pod。
  2. RollingUpdate:以滚动更新的方式来逐个更新Pod。同时,可以通过设置spec.strategy.rollingUpdate下的两个参数(maxUnavailable和maxSurge)来控制滚动更新的过程。

RollingUpdate的两个参数含义如下:

  1. maxUnavailable:用于指定Deployment在更新过程中不可用状态的Pod数量的上限。该maxUnavailable的数值可以是绝对值(例如1)或Pod期望的副本数的百分比(例如10%)。
  2. maxSurge:用于指定在Deployment更新Pod的过程中Pod总数超过Pod期望副本数部分的最大值。该maxSurge的数值可以是绝对值(例如5)或Pod期望副本数的百分比(例如10%)。

此外,除了使用kubectl命令升级外,我们也可以直接通过修改deployment.yml的方式来完成。

Deployment回滚

如果升级后效果不满意的话,我们也可以将Deployment回滚到升级之前,使用下面这条命令查看滚动升级的历史:

kubectl rollout history deployment/nginx-deployment

REVISION为升级的历史版本,我们只完成了nginx从1.7.9升级到1.9.1的过程,所以REVISION 1表示nginx为1.7.9的版本,REVISION 2表示nginx为1.9.1的版本。因为我们在创建deployment的时候没有加上--record=true参数,所以CHANGE-CACSE列是空的。

需要查看特定版本的详细信息,则可以加上–revision=参数:

要将nginx回退到1.7.9版本,我们可以将REVISION指定为1:

kubectl rollout undo deployment/nginx-deployment --to-revision=1

暂停与恢复

因为Deployment配置一旦修改就会触发升级操作,所以当修改的地方较多的时候就会频繁触发升级。我们可以先暂停升级操作,当所有配置都修改好后再恢复升级。

暂停:

kubectl rollout pause deployment/nginx-deployment

暂停后,对nginx-deployment的修改不会触发升级。修改好后,恢复升级:

kubectl rollout resume deployment/nginx-deployment

其他Pod管理对象升级策略

DaemonSet

DaemonSet的升级策略包括两种:OnDelete和RollingUpdate:

  1. OnDelete(默认):在创建好新的DaemonSet配置之后,新的Pod并不会被自动创建,直到用户手动删除旧版本的Pod,才触发新建操作。
  2. RollingUpdate:和前面介绍的一致,不过不支持rollout,只能通过将配置改回去来实现。

StatefulSet

支持Recreate、OnDelete和RollingUpdate。

Kubernetes Pod管理对象与调度策略


2019年11月04日 13:04:40   1,466 次浏览

在前面的学习中我们了解到,在Kubernetes中,Pod管理对象主要有RC(RS)、Deployment、StatefulSet、DaemonSet和Job(CronJob)等。其中RC(RS)和Deployment的用法已经大致了解,这里主要记录下StatefulSet、DaemonSet和Job(CronJob)的用法。默认情况下,Pod管理对象在创建Pod的时候是根据系统自动调度算法来完成部署的,我们可以设置调度策略来实现Pod的精准调度。

Pod调度策略

NodeSelector

我们可以该某个节点Node设置标签,然后通过NodeSelector让Pod调度到该节点上。

前面搭建的Kubernetes集群有两个worker节点node1和node2,我们给node2打个标签:

kubectl label nodes node2 tier=frontend

接着定义一个RC配置(node-select-pod.yml):

apiVersion: v1
kind: ReplicationController
metadata:
  name: test-rc
spec:
  replicas: 1
  selector:
    name: nginx
  template:
    metadata:
      labels:
        name: nginx
    spec:
      containers:
        - name: nginx
          image: nginx
          ports:
            - containerPort: 80
      nodeSelector:
        tier: frontend

创建该RC,观察Pod最终调度的节点:

可以看到nginx pod已经成功调度到了node2节点上。

Kubernetes会给每个node贴上一些默认的标签,通过kubectl get node node1 -o yaml可以看到这些默认的标签:

beta.kubernetes.io/arch: amd64
beta.kubernetes.io/os: linux
kubernetes.io/arch: amd64
kubernetes.io/hostname: node1
kubernetes.io/os: linux

这些默认的标签在下面这些调度策略中也是蛮常用的。

NodeAffinity

Affinity[əˈfɪnəti]:亲和力,喜好。NodeAffinity为Node亲和力调度,主要有两个规则(名称有点长,快18cm了吧):

  1. RequiredDuringSchedulingIgnoredDuringExecution:必须满足指定的规则才可以调度Pod,硬规则。
  2. PreferredDuringSchedulingIgnoredDuringExecution:软规则,最好满足所列出的条件才调度Pod。

IgnoredDuringExecution的意思是,如果一个Pod已经调度到某个节点上了,然后这个节点的标签发生了改变,那么不影响已经调度好的Pod,ignore。

定义一个配置文件(node-affinity.yml),用于演示NodeAffinity:

apiVersion: v1
kind: ReplicationController
metadata:
  name: test-rc
spec:
  replicas: 1
  selector:
    name: nginx
  template:
    metadata:
      labels:
        name: nginx
    spec:
      containers:
        - name: nginx
          image: nginx
          ports:
            - containerPort: 80
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: beta.kubernetes.io/arch
                    operator: In
                    values:
                      - amd64
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 1
              preference:
                matchExpressions:
                  - key: disk-type
                    operator: In
                    values:
                      - ssd

上面配置使得RC在调度nginx pod的时候需要满足:节点具有beta.kubernetes.io/arch=amd64标签,如果具有disk-type=ssd标签就更好了,换句话说就是希望nginx pod调度在cpu架构为amd,磁盘为ssd的节点上。

上面配置中,操作符operator除了可以使用In外,还可以使用NotIn、Exists、DoesNotExist、Gt、Lt。

NodeAffinity规则设置需要注意的几个点:

  1. 如果nodeAffinity指定了多个nodeSelectorTerms,那么其中一个能够匹配成功即可;
  2. 如果在nodeSelectorTerms中有多个matchExpressions,则一个节点必须满足所有matchExpressions才能运行该Pod。

运行上面这个配置:

可以看到,因为node1和node2都具有beta.kubernetes.io/arch=amd64标签,而没有disk-type=ssd标签,所以nginx pod有可能被调度到node1也有可能被调度到node2。

我们给node1添加disk-type=ssd标签,重新运行上面的配置文件:

可以看到,nginx pod被调度到了node1上。

PodAffinity

PodAffinity指的是Pod亲和力调度股则,比如某些节点已经存在一些Pod了,新的Pod在调度的时候希望和指定Pod在一台节点上,亦或有意避开和指定Pod在一台节点上,这时候就可以用PodAffinity实现。

NodeAffinity规则设置也是通过requiredDuringSchedulingIgnoredDuringExecutionPreferredDuringSchedulingIgnoredDuringExecution实现的。此外,NodeAffinity还需要设置topology(拓扑规则),意为表达节点所属的topology范围:

  1. kubernetes.io/hostname(节点所处的服务器)
  2. failure-domain.beta.kubernetes.io/zone(节点所处的服务器云盘分区)
  3. failure-domain.beta.kubernetes.io/region(节点所处的服务器云盘所在的地区)

要演示PodAffinity的使用,需要先创建一个参照Pod,创建一个参照Pod的配置文件(flag-pod.yml):

apiVersion: v1
kind: Pod
metadata:
  name: flag-pod
  labels:
    tier: frontend
    app: flag-nginx
spec:
  containers:
    - name: flag-nginx
      image: nginx
      ports:
        - containerPort: 8081

这个Pod具有tier=frontendapp=flag-nginx标签。

创建这个参照Pod:

参照pod运行在了node1节点上。

创建好后,接着定义一个新的配置类(pod-affinity.yml):

apiVersion: v1
kind: Pod
metadata:
  name: pod-affinity
spec:
  containers:
    - name: nginx
      image: nginx
      ports:
        - containerPort: 80
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - key: tier
                operator: In
                values:
                  - frontend
          topologyKey: kubernetes.io/hostname

上述配置要求,nginx pod需要和标签包含tier=frontend的Pod分配在同一个Node上(topologyKey: kubernetes.io/hostname),创建该Pod,观察它被调度到哪个节点上了:

podAntiAffinity和podAffinity刚好相反,举个例子,创建一个新的配置类(pod-affinity-two.yml):

apiVersion: v1
kind: Pod
metadata:
  name: pod-affinity-two
spec:
  containers:
    - name: nginx
      image: nginx
      ports:
        - containerPort: 80
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchExpressions:
              - key: app
                operator: In
                values:
                  - flag-nginx
          topologyKey: kubernetes.io/hostname

上面配置希望nginx pod不和app=flag-nginx的Pod在同一个节点。

创建该Pod,观察它被调度到哪个节点上了:

可以看到,它和flag-nginx处于不同的节点,位于node2。

Taints & Tolerations

Taints用于给节点添加污点,而Tolerations用于定义Pod对节点污点的容忍度。在Node上设置一个或多个Taint之后,除非Pod明确声明能够容忍这些污点,否则无法在这些Node上运行。Toleration是Pod的属性,让Pod能够(注意,只是能够,而非必须)运行在标注了Taint的Node上。

给节点设置污点的语法为:

kubectl taint nodes [nodeName] key=value:rule

其中rule的取值有:

  1. NoScheudle:不调度;
  2. PreferNoSchedule:最好不要调度;
  3. NoExecute:不运行;
  4. PreferNoExecute:最好不运行。
  • NoSchedule不调度,如果是在调度后设置的污点,并且Pod没有容忍该污点,也能继续执行
  • NoExecute不执行,如果是在调度后设置的污点,并且Pod没有容忍该污点,则会被驱逐。可以设置驱逐时间tolerationSeconds: xx

NoSchedule和NoExecute区别:

我们给node1节点设置一个污点:

kubectl taint nodes node1 aa=bb:NoSchedule

然后新建一个Pod配置类(pod-taint-test.yml):

apiVersion: v1
kind: Pod
metadata:
  name: pod-taint
spec:
  containers:
    - name: nginx
      image: nginx
      ports:
        - containerPort: 80
  tolerations:
    - key: "aa"
      operator: "Equal"
      value: "bb"
      effect: "NoSchedule"

Pod的Toleration声明中的key和effect需要与Taint的设置保持一致,并且满足以下条件之一:

  1. operator的值是Exists(无须指定value),如:
......
  tolerations:
    - key: "aa"
      operator: "Exists"
      effect: "NoSchedule"
  1. operator的值是Equal并且value相等。如果不指定operator,则默认值为Equal。

另外,有如下两个特例:

  1. 空的key配合Exists操作符能够匹配所有的键和值;
  2. 空的effect匹配所有的effect。

回到pod-taint-test.yml,该配置文件定义nginx pod可以容忍aa=bb:NoSchedule这个污点,所以它有可能会被调度到node1上。创建该Pod,观察:

这时候在nginx pod所运行的节点node1上新增一个污点:

kubectl taint nodes node1 cc=dd:NoExecute

观察nginx pod情况:

可以看到它被驱逐了,已经没有正在运行的pod了。

Pod Priority Preemption

我们可以给Pod指定优先级,优先级可以通过PriorityClass对象创建,比如创建一个优先级为10000的PriorityClass:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: hign-priority
value: 10000
globalDefault: false
description: "10000优先级"

上述文件定义了一个名为high-priority的优先级类别,优先级为10000,数字越大,优先越高。超过一亿的数字被系统保留,用于指派给系统组件。

优先级创建后,可以在Pod定义中引用该优先级:

...
spec:
  containers:
    - name: nginx
      image: nginx
      ports:
        - containerPort: 80
  priorityClassName: hign-priority

Pod管理对象

Job

Job是一种特殊的Pod管理对象,是一种一次性Pod运行任务,任务结束后,Pod的生命周期也就结束了。

定义一个Job配置类(job.yml):

apiVersion: batch/v1
kind: Job
metadata:
  name: job-ex
spec:
  template:
    spec:
      containers:
        - name: job
          image: busybox
          command: ["echo", "hello job"]
      restartPolicy: Never

Job的重启策略restartPolicy只支持Never和OnFailure。

创建这个Job:

查看Job状态:

COMPLETIONS 1/1表示总共需要执行1个任务,共执行完1个任务。

查看对应Pod的状态:

状态为Completed。

查看Job日志:

Job还可以设置并发数量和总Job数,修改上面的job.yml:

apiVersion: batch/v1
kind: Job
metadata:
  name: job-ex
spec:
  parallelism: 2 # 并发数2
  completions: 6 # 总的Job数量
  template:
    spec:
      containers:
        - name: job
          image: busybox
          command: ["echo", "hello job"]
      restartPolicy: Never

运行该Job:

查看Pod:

CronJob

CronJob顾名思义就是支持Cron表达式的Job,不过Kubernetes的Cron表达式和传统的Cron表达式不太一样,不支持到秒级。具体规则如下:

Minutes Hours DayofMonth Month DayofWeek Year
  1. Minutes:可出现, - * / 这4个字符,有效范围为0~59的整数。
  2. Hours:可出现, - * /这4个字符, 有效范围为0~23的整数。
  3. DayofMonth:可出现, - * / ? L W C 这8个字符,有效范围为0~31的整数。
  4. Month:可出现, - * /这4个字符,有效范围为1~12的整数或JAN~DEC。
  5. DayofWeek:可出现, - * / ? L C 这8个字符,有效范围为1~7的整数或SUN~SAT。1表示星期天,2表示星期一,以此类推。

上面特殊字符的含义如下:

  1. *:表示匹配该域的任意值,假如在Minutes域使用*,则表示每分钟都会触发事件。
  2. /:表示从起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域设置为5/20,则意味着第1次触发在第5min时,接下来每20min触发一次,将在第25min、第45min等时刻分别触发。

定义一个CronJob的配置类(cron-job.yml):

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: cron-job-ex
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: hello
              image: busybox
              command: ["sh", "-c", "date;echo hello cronJob"]
          restartPolicy: Never

上面的定时任务每分钟执行一次。

创建该CronJob:

过个两三分钟查看运行情况:

DaemonSet

DaemonSet适用于在每个Node都需要运行一个Pod的时候使用,比如:每一个Node上运行一个日志采集、性能监控的Pod。

比如我们现在需要在每个节点上都部署一个node-exporter来采集节点信息,可以通过DaemonSet来实现。

定义一个DaemonSet配置文件(daemonset.yml):

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-exporter-d
spec:
  selector:
    matchLabels:
      name: node-exporter
  template:
    metadata:
      labels:
        name: node-exporter
    spec:
      containers:
        - name: node-exporter
          image: prom/node-exporter
          ports:
            - containerPort: 9100

创建该DaemonSet之前先删除上面在node1节点上创建的污点:

kubectl taint nodes node1 aa:NoSchedule-
kubectl taint nodes node1 cc:NoExecute-

创建该DaemonSet,然后观察Pod信息,看是否每个节点都部署了一个实例:

Kubernetes Pod基础


2019年11月03日 18:09:07   1,450 次浏览

Pod是Kubernetes的最小调度单位,包含一个或者多个容器(比如Docker容器),容器间共享网络和存储。这节主要记录什么是静态Pod,Pod容器如何共享存储,如何使用ConfigMap管理Pod配置,如何使用Downward API获取Pod信息等。

静态Pod

静态Pod是由kubelet创建并管理的特殊的Pod,无法和Pod管理对象关联,并且不能通过API Server关联。创建静态Pod有配置文件方式和HTTP方式:

配置文件方式

在搭建Kubernetes集群的时候,从启动Master节点的日志可以看出,静态Pod的目录位于/etc/kubernetes/manifests

在该目录下创建静态Pod文件:

cd /etc/kubernetes/manifests
vim static-pod.yaml

内容如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: static-pod
  labels:
    name: static-pod
spec:
  containers:
    - name: static-nginx
      image: nginx
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 80

过了一会查看Pod:

于静态Pod无法通过API Server直接管理,所以在Master上尝试删除这个Pod时,会使其变成Pending状态,且不会被删除。

删除该Pod的操作只能是到其所在Node上将其定义文件static-pod.yaml从/etc/kubernetes/manifests目录下删除:

HTTP方式

过设置kubelet的启动参数--manifest-url,kubelet将会定期从该URL地址下载Pod的定义文件,并以.yaml或.json文件的格式进行解析,然后创建Pod。

Pod容器共享Volume

同一个Pod的多个容器间可以共享Pod级别的Volume,举个例子:

vim pod-volume.yml

内容如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: pod-volume
spec:
  containers:
    - name: tomcat
      image: tomcat
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 8080
      volumeMounts:
        - mountPath: /usr/local/tomcat/logs
          name: logs
    - name: busybox
      image: busybox
      imagePullPolicy: IfNotPresent
      command: ["sh", "-c", "tail -f /logs/catalina*.log"]
      volumeMounts:
        - mountPath: /logs
          name: logs
  volumes:
    - name: logs
      emptyDir: {}

上面Pod定义中,创建了一个Pod级别的Volume,名称为logs,类型为emptyDir。这个Volume同时挂载到了tomcat的/usr/local/tomcat/logs目录下,也挂载到了busybox的/logs目录下。

创建该Pod:

kubectl create -f pod-volume.yml

这里的tomcat镜像比较大,大概有500MB左右,所以在创建之前,最好在Kubernetes集群的每个节点中配置Docker镜像加速地址。

当pod-volume状态为ready后,查看busybox的日志:

该日志为tomcat的启动日志,说明上面挂载的Volume生效了,可以通过查看tomcat/usr/local/tomcat/logs目录下和busybox/logs目录下的内容来证明这一点:

ConfigMap

ConfigMap以一个或多个key:value的形式保存在Kubernetes系统中供应用使用,既可以用于表示一个变量的值(例如version=v1),也可以用于表示一个完整配置文件的内容(例如server.xml=<?xml...>...)。

创建ConfigMap

创建ConfigMap主要有两种方式:

1.通过yml文件创建

创建simple-cm.yml文件,内容如下所示:

apiVersion: v1
kind: ConfigMap
metadata:
  name: simple-cm
data:
  version: v1
  release: stable

该ConfigMap仅包含两个简单的值version和releases。

创建该ConfigMap:

kubectl create -f simple-cm.yml

查看该ConfigMap:

在定义ConfigMap的时候,value除了可以使用简单的值外,还可以是整个配置文件的内容。

创建file-cm.yml,内容如下所示:

apiVersion: v1
kind: ConfigMap
metadata:
  name: file-cm
data:
  serverXml: |
    <?xml version="1.0" encoding="UTF-8"?>
    <Server port="8005" shutdown="SHUTDOWN">
      <Listener className="org.apache.catalina.startup.VersionLoggerListener"/>
      <Listener SSLEngine="on" className="org.apache.catalina.core.AprLifecycleListener"/>
      <!-- Prevent memory leaks due to use of particular java/javax APIs-->
      <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
      <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
      <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>
      <GlobalNamingResources>
        <Resource auth="Container" description="User database that can be updated and saved" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" name="UserDatabase" pathname="conf/tomcat-users.xml" type="org.apache.catalina.UserDatabase"/>
      </GlobalNamingResources>
      <Service name="Catalina">
        <Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
        <Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
        <Engine defaultHost="localhost" name="Catalina">
          <Realm className="org.apache.catalina.realm.LockOutRealm">
            <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
          </Realm>

          <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true">
            <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" pattern="%h %l %u %t &quot;%r&quot; %s %b" prefix="localhost_access_log" suffix=".txt"/>
          <Context docBase="D:\Code\apache-tomcat-8.5.32\webapps\tj_certification" path="/tj_certification" reloadable="true" source="org.eclipse.jst.jee.server:tj_certification"/></Host>
        </Engine>
      </Service>
    </Server>
  serverProperties: "1catalina.org.apache.juli.AsyncFileHandler.level = FINE
                     1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
                     1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.

                     2localhost.org.apache.juli.AsyncFileHandler.level = FINE
                     2localhost.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
                     2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost.

                     3manager.org.apache.juli.AsyncFileHandler.level = FINE
                     3manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
                     3manager.org.apache.juli.AsyncFileHandler.prefix = manager.

                     4host-manager.org.apache.juli.AsyncFileHandler.level = FINE
                     4host-manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
                     4host-manager.org.apache.juli.AsyncFileHandler.prefix = host-manager.

                     java.util.logging.ConsoleHandler.level = FINE
                     java.util.logging.ConsoleHandler.formatter = org.apache.juli.OneLineFormatter

                     org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO
                     org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.AsyncFileHandler

                     org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].level = INFO
                     org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].handlers = 3manager.org.apache.juli.AsyncFileHandler

                     org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].level = INFO
                     org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].handlers = 4host-manager.org.apache.juli.AsyncFileHandler"

创建该ConfigMap:

2.直接通过Kubectl命令创建

通过kubectl命令创建ConfigMap主要有以下三种用法:

  1. 通过–from-file参数从文件中进行创建,可以指定key的名称,也可以在一个命令行中创建包含多个key的ConfigMap,语法为:
kubectl cerate configmap [NAME] --from-file=[key=]source --from-file=[key=]source

通过–from-file参数从目录中进行创建,该目录下的每个配置文件名都被设置为key,文件的内容被设置为value,语法为:

kubectl cerate configmap [NAME] --from-file=config-file-dir

使用–from-literal时会从文本中进行创建,直接将指定的key#=value#创建为ConfigMap的内容,语法为:

kubectl cerate configmap [NAME] --from-literal=key1=value1 --from-literal=key2=value2

比如使用kubectl命令创建一个和simple-cm效果一样的ConfigMap:

使用kubectl命令创建一个和file-cm效果一样的ConfigMap:

首先在当前目录下准备好两个配置文件server.xml和server.properties:

然后使用kubectl命令创建:

Pod容器使用ConfigMap

Pod的容器要使用ConfigMap主要有两种方式:

  1. 通过环境变量获取ConfigMap中的内容;
  2. 通过Volume挂载的方式将ConfigMap中的内容挂载为容器内部的文件或目录。

通过环境变量的方式

创建一个Pod配置(simple-cm-pod.yml):

apiVersion: v1
kind: Pod
metadata:
  name: simple-cm-pod
spec:
  containers:
    - name: busybox
      image: busybox
      command: ["sh", "-c", "env | grep ENV"] # 打印名称包含ENV的环境变量
      env:
        - name: ENVVERSION     # 定义环境变量名称
          valueFrom:           # 值来自...
            configMapKeyRef:   # ConfigMap键的引用
              key: version     # ConfigMap的key
              name: simple-cm  # ConfigMap的名称
        - name: ENVRELEASE
          valueFrom:
            configMapKeyRef:
              key: release
              name: simple-cm

创建该Pod,并查看日志:

可以看到,值已经成功从ConfigMap里去到了。

如果要引用某个ConfigMap的所有内容,可以使用下面这种方式。定义一个Pod配置(simple-cm-pod-all.uyml):

apiVersion: v1
kind: Pod
metadata:
  name: simple-cm-pod-all
spec:
  containers:
    - name: busybox
      image: busybox
      command: ["sh", "-c", "env"]
      envFrom:
        - configMapRef:
            name: simple-cm

创建该Pod,并查看日志:

通过Volume挂载的方式

创建一个Pod配置(file-cm-pod.yml):

apiVersion: v1
kind: Pod
metadata:
  name: file-cm-pod
spec:
  containers:
    - name: tomcat
      image: tomcat
      ports:
        - containerPort: 8080
      volumeMounts:
        - mountPath: /configs # 容器挂载目录为/configs
          name: config-vm # 引用下面定义的这个Volume
  volumes:
    - name: config-vm   # volume名称为config-vm
      configMap:        # 通过ConfigMap获取
        name: file-cm   # 引用名称为file-cm的ConfigMap
        items:
          - key: serverXml # ConfigMap里配置的key
            path: server.xml # 值使用server.xml文件进行挂载
          - key: serverProperties
            path: server.properties

创建该Pod,并进入到容器内部观察/configs目录下文件内容:

可以看到名称为file-cm的ConfigMap内容已经成功挂载到了tomcat容器内部。

如果在引用ConfigMap时不指定items,则使用volumeMount方式在容器内的目录下为每个item都生成一个文件名为key的文件。

在Pod对ConfigMap进行挂载(volumeMount)操作时,在容器内部只能挂载为“目录”,无法挂载为“文件”。在挂载到容器内部后,在目录下将包含ConfigMap定义的每个item,如果在该目录下原来还有其他文件,则容器内的该目录将被挂载的ConfigMap覆盖

Downward API

Downward API用于将Pod相关信息注入到容器内部,主要有环境变量Volume挂载两种方式。

环境变量

创建一个Pod配置(dapi-pod.yml):

apiVersion: v1
kind: Pod
metadata:
  name: dapi-pod
spec:
  containers:
    - name: busybox
      image: busybox
      command: ["sh", "-c", "env | grep MY_POD"]
      env:
        - name: MY_POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name # 通过downward api获取当前pod名称
        - name: MY_POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace  # 通过downward api获取当前pod namespace
        - name: MY_POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP  # 通过downward api获取当前pod IP

创建该Pod,并查看日志:

通过环境变量的方式还可以将容器的requests和limits信息注入到容器的环境变量中,创建一个Pod配置(dapi-pod-container-vars.yml):

apiVersion: v1
kind: Pod
metadata:
  name: dapi-pod-container-vars
spec:
  containers:
    - name: busybox
      image: busybox
      command: ["sh", "-c"]
      args:
        - while true;do
            echo -en '\n';
            printenv MY_CPU_REQUEST MY_CPU_LIMIT;
            printenv MY_MEM_REQUEST MY_MEM_LIMIT;
            sleep 3600;
          done;
      resources:
        requests:
          memory: "32Mi"
          cpu: "125m"
        limits:
          memory: "64Mi"
          cpu: "250m"
      env:
        - name: MY_CPU_REQUEST
          valueFrom:
            resourceFieldRef:
              resource: requests.cpu
              containerName: busybox
        - name: MY_CPU_LIMIT
          valueFrom:
            resourceFieldRef:
              resource: limits.cpu
              containerName: busybox
        - name: MY_MEM_REQUEST
          valueFrom:
            resourceFieldRef:
              resource: request.memory
              containerName: busybox
        - name: MY_MEM_LIMIT
          valueFrom:
            resourceFieldRef:
              resource: limits.memory
              containerName: busybox

创建该Pod并观察日志:

通过Volume挂载

我们可以通过Downward API将Pod的Label,Annotation等信息挂载到容器内部文件中,新建一个Pod配置(dapi-pod-volumes.yml):

apiVersion: v1
kind: Pod
metadata:
  name: dapi-pod-volume
  labels:
    tier: frontend
    release: canary
    environment: dev
  annotations:
    builder: mrbird
    blog: https://mrbird.cc
spec:
  containers:
    - name: busybox
      image: busybox
      command: ["sh", "-c", "sleep 36000"]
      volumeMounts:
        - mountPath: /podinfo # 挂载路径
          name: pod-info
  volumes:
    - name: pod-info
      downwardAPI: # 通过downward api获取pod labels和annations信息
        items:
          - path: "labels" # 挂载文件名称
            fieldRef:
              fieldPath: metadata.labels # 挂载内容
          - path: "annotation"
            fieldRef:
              fieldPath: metadata.annotations

创建该Pod,并进入到容器/podinfo目录观察结果:

Pod生命周期

阶段描述
PendingPod 已被 Kubernetes 接受,但尚未创建一个或多个容器镜像。这包括被调度之前的时间以及通过网络下载镜像所花费的时间,执行需要一段时间。
RunningPod 已经被绑定到了一个节点,所有容器已被创建。至少一个容器正在运行,或者正在启动或重新启动。
Succeeded所有容器成功终止,也不会重启。
Failed所有容器终止,至少有一个容器以失败方式终止。也就是说,这个容器要么已非 0 状态退出,要么被系统终止。
Unknown由于一些原因,Pod 的状态无法获取,通常是与 Pod 通信时出错导致的。

三种重启策略:

  1. Always:当容器失效时,由kubelet自动重启该容器;
  2. OnFailure:当容器终止运行且退出码不为0时,由kubelet自动重启该容器;
  3. Never:不论容器运行状态如何,kubelet都不会重启该容器。

结合Pod的状态和重启策略,以下为一些常见的状态转换场景:

Pod包含的容器数Pod当前的状态发生事件Pod的结果状态
RestarPolicy=AlwaysRestartPolicy=OnFailureRestartPolicy=Never
包含1个容器Running容器成功退出RunningSucceededSucceeded
包含1个容器Running容器失败退出RunningRunningFailed
包含两个容器Running1个容器失败退出RunningRunningRunning
包含两个容器Running容器被OOM杀掉RunningRunningFailed

使用docker-compose部署LNMP环境


2018年12月29日 15:04:00   1,563 次浏览

1、创建相关compose存放目录

< 5 docker-test - [root]: > mkdir -p /apps/compose_lnmp/nginx
< 5 docker-test - [root]: >  cd !$
< 5  docker-test - [root]: /apps/compose_lnmp/nginx >

2、下载nginx软件包,创建dockerfile

< 6  docker-test - [root]: /apps/compose_lnmp/nginx > # wget http://nginx.org/download/nginx-1.14.2.tar.gz

< 7  docker-test - [root]: /apps/compose_lnmp/nginx > # vim Dockerfile

FROM centos:7
MAINTAINER gujiwork
RUN yum install -y gcc gcc-c++ make openssl-devel pcre-devel
ADD nginx-1.14.2.tar.gz /tmp
RUN cd /tmp/nginx-1.14.2 && ./configure --prefix=/usr/local/nginx && make -j 4 && make install
COPY nginx.conf  /usr/local/nginx/conf
EXPOSE 80
WORKDIR /usr/local/nginx
CMD ["./sbin/nginx", "-g", "daemon off;"]

3、这里面用到了nginx.conf,放到compose_lnmp/nginx目录下,将 fastcgi_pass 127.0.0.1:9000改成php容器名, fastcgi_pass php-cgi:9000;

#user  nginx;
worker_processes  2;
worker_cpu_affinity 0101 1010;
#error_log  logs/error.log;
error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
#pid        logs/nginx.pid;
events {
   worker_connections  10240;
}
http {
   include       mime.types;
   default_type  application/octet-stream;
   server_tokens off;  
   #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
   #                  '$status $body_bytes_sent "$http_referer" '
   #                  '"$http_user_agent" "$http_x_forwarded_for"';
   #access_log  logs/access.log  main;
   sendfile        on;
   tcp_nopush on;
   tcp_nodelay on;
   server_names_hash_bucket_size 128;
   server_names_hash_max_size 512;
   client_header_timeout 15s;
   client_body_timeout 15s;
   send_timeout 60s;
   #tcp_nopush     on;
   #keepalive_timeout  0;
   keepalive_timeout  65;
   #gzip  on;
   server {
       listen       80;
       server_name  localhost;
       #charset koi8-r;
       #access_log  logs/host.access.log  main;
       location / {
           root   html;
           index  index.html index.htm;
       }
       #error_page  404              /404.html;
       # redirect server error pages to the static page /50x.html
       #
       error_page   500 502 503 504  /50x.html;
       location = /50x.html {
           root   html;
       }
       # proxy the PHP scripts to Apache listening on 127.0.0.1:80
       #
       #location ~ \.php$ {
       #    proxy_pass   http://127.0.0.1;
       #}
       # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
       #
       location ~ \.php$ {
           root           html;
           fastcgi_pass   php-cgi:9000;
           fastcgi_index  index.php;
           fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
           include        fastcgi_params;
       }
       # deny access to .htaccess files, if Apache's document root
       # concurs with nginx's one
       #
       #location ~ /\.ht {
       #    deny  all;
       #}
   }
   # another virtual host using mix of IP-, name-, and port-based configuration
   #
   #server {
   #    listen       8000;
   #    listen       somename:8080;
   #    server_name  somename  alias  another.alias;
   #    location / {
   #        root   html;
   #        index  index.html index.htm;
   #    }
   #}
   # HTTPS server
   #
   #server {
   #    listen       443 ssl;
   #    server_name  localhost;
   #    ssl_certificate      cert.pem;
   #    ssl_certificate_key  cert.key;
   #    ssl_session_cache    shared:SSL:1m;
   #    ssl_session_timeout  5m;
   #    ssl_ciphers  HIGH:!aNULL:!MD5;
   #    ssl_prefer_server_ciphers  on;
   #    location / {
   #        root   html;
   #        index  index.html index.htm;
   #    }
   #}
include vhost/*.conf;
}

4、创建mysql目录,数据和配置文件做持久化,这里我们使用mysql官方镜像,因此不需要写dockerfile

< 39 docker-test - [root]: /apps/compose_lnmp > # mkdir mysql/{conf,data}

< 39  docker-test - [root]: /apps/compose_lnmp > # tree  mysql/

mysql/

|-- conf

|   `-- my.cnf

`-- data

< 40  docker-test - [root]: /apps/compose_lnmp > # cat mysql/conf/my.cnf

[mysqld]
user=mysql
port=3306
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.socket
pid-file=/var/run/mysql/mysql.pid
log_error=/var/log/mysql/error.log
character_set_server = utf8
max_connections=3600

5、创建php目录及dockerfile,php.ini主要修改了时区为shanghai

< 49  docker-test - [root]: /apps/compose_lnmp > # tree  php/

php/

|-- Dockfile
|-- php-5.6.31.tar.gz
`-- php.ini

< 50  docker-test - [root]: /apps/compose_lnmp > # cat php/Dockfile

FROM centos:7
MAINTAINER gujiwork
RUN yum install -y gcc gcc-c++ gd-devel libxml2-devel libcurl-devel libjpeg-devel libpng-devel openssl-devel make
ADD php-5.6.31.tar.gz /tmp/
RUN cd /tmp/php-5.6.31 && \
   ./configure --prefix=/usr/local/php \
   --with-config-file-path=/usr/local/php/etc \
   --with-mysql --with-mysqli \
   --with-openssl --with-zlib --with-curl --with-gd \
   --with-jpeg-dir --with-png-dir --with-iconv \
   --enable-fpm --enable-zip --enable-mbstring && \
   make -j 4 && make install && \
   cp /usr/local/php/etc/php-fpm.conf.default /usr/local/php/etc/php-fpm.conf && \
   sed -i "s/127.0.0.1/0.0.0.0/" /usr/local/php/etc/php-fpm.conf && \
   cp ./sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm && \
   chmod +x /etc/init.d/php-fpm

COPY php.ini /usr/local/php/etc
EXPOSE 9000
CMD /etc/init.d/php-fpm start && tail -F /var/log/messages

6、创建docker-compose维护文件

< 53  docker-test - [root]: /apps/compose_lnmp > # cat docker-compose.yml

version: '3'
services:
 nginx:
   hostname: nginx
   build:
     context: ./nginx
     dockerfile: Dockerfile

   ports:
     - 80:80
   links:
     - php:php-cgi
   volumes:
     - ./wwwroot:/usr/local/nginx/html


 php:
   hostname: php
   build: ./php
   links:
     - mysql:mysql-db
   volumes:
     - ./wwwroot:/usr/local/nginx/html

 mysql:
   hostname: mysql
   image: mysql:5.6
   ports:
     - 3306:3306
   volumes:
     - ./mysql/conf:/etc/mysql/conf.d
     - ./mysql/data:/var/lib/mysql

   #command: --character-set-server=utf8

   environment:
     MYSQL_ROOT_PASSWORD: 123456
     MYSQL_DATABASE: wordpress
     MYSQL_USER: user
     MYSQL_PASSWORD: user123

7、创建web挂载目录,整体目录结构如下

< 56  docker-test - [root]: /apps/compose_lnmp > # tree .
.
|-- docker-compose.yml
|-- mysql
|   |-- conf
|   |   `-- my.cnf
|   `-- data
|-- nginx
|   |-- Dockerfile
|   |-- nginx-1.14.2.tar.gz
|   `-- nginx.conf
|-- php
|   |-- Dockfile
|   |-- php-5.6.31.tar.gz
|   `-- php.ini
`-- wwwroot


6 directories, 8 files

8、执行build构建镜像

< 81  docker-test - [root]: /apps/compose_lnmp > # docker-compose  build

9、出现success表示构建成功

make[1]: Leaving directory `/tmp/nginx-1.14.2'
Removing intermediate container b7fb79c4671e
---> b756345aac5b
Step 6/9 : COPY nginx.conf /usr/local/nginx/conf
---> cb180351db65
Step 7/9 : EXPOSE 80
---> Running in 20805a3b58a5
Removing intermediate container 20805a3b58a5
---> 9f75c3834969
Step 8/9 : WORKDIR /usr/local/nginx
---> Running in 6abf5341ee7b
Removing intermediate container 6abf5341ee7b
---> 0cb88354c8b8
Step 9/9 : CMD ["./sbin/nginx", "-g", "daemon off;"]
---> Running in a2db489f2b5b
Removing intermediate container a2db489f2b5b
---> 76ae0759f3b1
Successfully built 76ae0759f3b1
Successfully tagged compose_lnmp_nginx:latest

10、启动服务测试

< 82  docker-test - [root]: /apps/compose_lnmp > # docker-compose  up -d

Creating network "compose_lnmp_default" with the default driver
Pulling mysql (mysql:5.6)...
5.6: Pulling from library/mysql
177e7ef0df69: Pull complete
cac25352c4c8: Pull complete
8585afabb40a: Pull complete
1e4af4996053: Pull complete
c326522894da: Pull complete
50ec9776c6b3: Pull complete
b81a89945365: Pull complete
80f5ab6567ca: Pull complete
5caf5e4c5eb0: Pull complete
9295ceea71e2: Pull complete
fb029976ca26: Pull complete
Creating compose_lnmp_mysql_1_32333e982f89 ... done
Creating compose_lnmp_php_1_856b9f701287   ... done
Creating compose_lnmp_nginx_1_c5360c9dc627 ... done



< 144  docker-test - [root]: /apps/compose_lnmp/wwwroot > # echo "111" >index.html


#测试

< 144  docker-test - [root]: /apps/compose_lnmp/wwwroot > # curl localhost/

111

 

Docker主机数据挂载到容器


2018年12月19日 22:11:00   1,731 次浏览

Docker数据管理

Docker提供三种不同的方式将数据从宿主机挂载到容器中:volumes,bind mounts 和tmpfs。

olumes

docker管理宿主机文件系统的一部分(/var/lib/docker/volumes)。

管理卷

[root@docker-master ~]# docker volume create nginx-vo1
nginx-vo1

[root@docker-master ~]# docker volume ls

DRIVER              VOLUME NAME
local               144c5839b36278cea8b3bc7bf3d10d051c11e7acc5610d420f25314f240ef574
local               a0c117c96d84eeaeebd548eda827577f17926a595c85ea69a9f4ea9f756c1557
local               f0e495702096c6845110ae74e0ed24455059e1265831acca1bb303458b4400ab
local               nginx-vo1

[root@docker-master ~]# docker volume inspect nginx-vo1
[
   {
       "CreatedAt": "2018-12-12T01:05:59+08:00",
       "Driver": "local",
       "Labels": {},
       "Mountpoint": "/var/lib/docker/volumes/nginx-vo1/_data",
       "Name": "nginx-vo1",
       "Options": {},
       "Scope": "local"
   }
]

用卷创建一个容器

docker run -itd --name=nginx-test --mount src=nginx-vo1,dst=/usr/share/nginx/html nginx

进入容器内部查看nginx web目录

[root@docker-master ~]# docker exec -it nginx-test bash

root@23821377e2f7:/# ls /usr/share/nginx/html/
50x.html  index.html

在宿主机docker volumes目录可以发现有相同的两个html文件,无论是在容器内新创建文件还是在宿主机创建文件,内容都不会同步映射到对应目录

[root@docker-master ~]# ll /var/lib/docker/volumes/nginx-vo1/_data/

total 8
-rw-r--r-- 1 root root 494 Nov 27 20:31 50x.html
-rw-r--r-- 1 root root 612 Nov 27 20:31 index.html

清理

docker container stop nginx-test
docker container rm nginx-test
docker volume rm nginx-vo1

注意:

1.如果没有指定卷,自动创建

2.建议使用–mount,更通用

bind mounts

可以存储在宿主机系统的任意位置。将宿主机的目录映射到容器中做持久卷,删除容器后数据存储在宿主机上不会丢失

用卷创建一个容器

[root@docker-master ~]# mkdir /app/wwwroot -p

[root@docker-master ~]# docker run -itd --name=nginx-test --mount type=bind,src=/app/wwwroot,dst=/usr/share/nginx/html nginx
453be5dc46e410c3daa5c34f219ed5d195c134a538b2e5f85d0c1d333fd7e7ff

[root@docker-master ~]# docker exec -it nginx-test bash

root@453be5dc46e4:/# mount

/dev/mapper/docker-8:2-528671-5a0afb1cab58d9e41d9ec2ecfc57b7045452918f77a15ede38aa9acb69d7bdac on / type xfs (rw,relatime,nouuid,attr2,inode64,logbsize=64k,sunit=128,swidth=128,noquota)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)
sysfs on /sys type sysfs (ro,nosuid,nodev,noexec,relatime)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,relatime,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (ro,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (ro,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/cpuset type cgroup (ro,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/net_cls type cgroup (ro,nosuid,nodev,noexec,relatime,net_cls)
cgroup on /sys/fs/cgroup/blkio type cgroup (ro,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/devices type cgroup (ro,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (ro,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (ro,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/perf_event type cgroup (ro,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/memory type cgroup (ro,nosuid,nodev,noexec,relatime,memory)
mqueue on /dev/mqueue type mqueue (rw,nosuid,nodev,noexec,relatime)
/dev/sda2 on /etc/resolv.conf type ext4 (rw,relatime,data=ordered)
/dev/sda2 on /etc/hostname type ext4 (rw,relatime,data=ordered)
/dev/sda2 on /etc/hosts type ext4 (rw,relatime,data=ordered)
shm on /dev/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=65536k)
/dev/sda2 on /usr/share/nginx/html type ext4 (rw,relatime,data=ordered)/dev/sda2 on /usr/share/nginx/html type ext4 (rw,relatime,data=ordered)

进入到容器内使用mount命令可以发现/dev/sda2 on /usr/share/nginx/html type ext4 (rw,relatime,data=ordered)已经挂载了

docker run -itd --name=nginx-test -v /app/wwwroot:/usr/share/nginx/html nginx

验证绑定

docker inspect nginx-test

清理

docker container stop nginx-test
docker container rm nginx-test

注意:

1.如果源文件/目录没有存在,不会自动创建,会抛出一个错误。

docker: Error response from daemon: invalid mount config for type “bind”: bind source path does not exist.
See ‘docker run –help’.

2.如果挂载目标在容器中非空目录,则该目录现有内容将被隐藏。

tmpfs

挂载存储在宿主机系统的内存中,而不会写入宿主机的文件系统。

Docker容器监控系统 cAdvisor+InfluxDB+Grafana


2018年12月19日 14:54:00   2,028 次浏览

cAdvisor:Google开源的工具,用于监控Docker主机和容器系统资源,通过图形页面实时显示数据,但不存储;它通过宿主机/proc、/sys、/var/lib/docker等目录下文件获取宿主机和容器运行信息

InfluxDB:是一个分布式的时间序列数据库,用来存储cAdvisor收集的系统资源数据。

Grafana:可视化展示平台,可做仪表盘,并图表页面操作很方便,数据源支持zabbix、Graphite、InfluxDB、OpenTSDB、Elasticsearch等

它们之间关系:

cAdvisor容器数据采集—–> InfluxDB容器数据存储—–>Grafana可视化展示

部署InfluxDB

[root@docker-master ~]# docker run -itd  -p8083:8083  --name influxdb  tutum/influxdb
Unable to find image 'tutum/influxdb:latest' locally
latest: Pulling from tutum/influxdb
a3ed95caeb02: Pull complete
23efb549476f: Pull complete
aa2f8df21433: Pull complete
ef072d3c9b41: Pull complete
c9f371853f28: Pull complete
a248b0871c3c: Pull complete
749db6d368d0: Pull complete
db2492acfcc3: Pull complete
b7e7d2e12d53: Pull complete
4272a53eef10: Pull complete
9b2fefdb5321: Pull complete
Digest: sha256:2772d80e80284b801c6ef255f7e185dd5290757f0f031d77762390dd4df2a9a3
Status: Downloaded newer image for tutum/influxdb:latest
docker: Error response from daemon: mkdir /var/lib/docker/overlay/6d0d06de343d70cdd581480780933d4c05904bce9cf381efc1ac630f28acd083-init/merged/dev/shm: invalid argument.

在docker run的时候报了一个错, mkdir /var/lib/docker/overlay/6d0d06de343d70cdd581480780933d4c05904bce9cf381efc1ac630f28acd083-init/merged/dev/shm: invalid argument

解决方法:

[root@docker-master ~]# vim /etc/docker/daemon.json
{
"storage-driver": "devicemapper"
}


[root@docker-master ~]# docker run -idt -p 8083:8083 --name influxdb tutum/influxdb
Unable to find image 'tutum/influxdb:latest' locally
latest: Pulling from tutum/influxdb
a3ed95caeb02: Pull complete
23efb549476f: Pull complete
aa2f8df21433: Pull complete
ef072d3c9b41: Pull complete
c9f371853f28: Pull complete
a248b0871c3c: Pull complete
749db6d368d0: Pull complete
db2492acfcc3: Pull complete
b7e7d2e12d53: Pull complete
4272a53eef10: Pull complete
9b2fefdb5321: Pull complete
Digest: sha256:2772d80e80284b801c6ef255f7e185dd5290757f0f031d77762390dd4df2a9a3

创建数据库用来存放cadvisor采集的数据,再创建数据库用户供grafana连接

create database "cadvisor"

查看数据库可以发现数据库已经创建成功了。

# 创建用户

create user "cadvisor" with password 'cadvisor'

OK,数据库用户也已经创建成功。

show users

 

部署cAdvisor

docker run -d \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:rw \
--volume=/sys:/sys:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--link influxdb:influxdb \
-p 8081:8080 \
--name=cadvisor \
google/cadvisor:latest \
-storage_driver=influxdb \
-storage_driver_db=cadvisor \
-storage_driver_host=influxdb:8086



Unable to find image 'google/cadvisor:latest' locally
latest: Pulling from google/cadvisor
ab7e51e37a18: Pull complete
a2dc2f1bce51: Pull complete
3b017de60d4f: Pull complete
Digest: sha256:9e347affc725efd3bfe95aa69362cf833aa810f84e6cb9eed1cb65c35216632a
Status: Downloaded newer image for google/cadvisor:latest
c4b2aa505f02e01f61a704dfeac9574ca9d36897896bda9be5c8d688d9ed441f

部署Granfana

docker run -d \
-p 3000:3000 \
-e INFLUXDB_HOST=localhost \
-e INFLUXDB_PORT=8086 \
-e INFLUXDB_NAME=cadvisor \
-e INFLUXDB_USER=cadvisor \
-e INFLUXDB_PASS=cadvisor \
--link influxdb:influxsrv \
--name grafana \
grafana/grafana



Unable to find image 'grafana/grafana:latest' locally
latest: Pulling from grafana/grafana
a5a6f2f73cd8: Pull complete
08e6195c0f29: Pull complete
b7bd3a2a524c: Pull complete
d3421658103b: Pull complete
cd7c84229877: Pull complete
49917e11f039: Pull complete
Digest: sha256:b9a31857e86e9cf43552605bd7f3c990c123f8792ab6bea8f499db1a1bdb7d53
Status: Downloaded newer image for grafana/grafana:latest
65e7390c90d8595495a776a66a03df77e474a4fa69d9d248f71236c5b62a580b

访问http://IP:3000 默认用户:admin 密码:admin

添加数据源Add Type里面选择InfluxDB ,最后选择Save&Test 保存测试。

URL填写:http://influxdb:8086  数据库用户及密码填写:cadvisor

在grafana上绘制图表即可展现相关数据

Kubernetes优雅下线微服务应用


2018年10月31日 10:40:22   1,846 次浏览

对于微服务来说,服务的优雅上下线是必要的。就上线来说,如果组件或者容器没有启动成功,就不应该对外暴露服务,对于下线来说,如果机器已经停机了,就应该保证服务已下线,如此可避免上游流量进入不健康的机器。

优雅下线

基础下线(Spring/SpringBoot/内置容器)

首先JVM本身是支持通过shutdownHook的方式优雅停机的。

Runtime.getRuntime().addShutdownHook(new Thread() {
    @Override
    public void run() {
        close();
    }
});

此方式支持在以下几种场景优雅停机:

  1. 程序正常退出
  2. 使用System.exit()
  3. 终端使用Ctrl+C
  4. 使用Kill pid干掉进程
    那么如果你偏偏要kill -9 程序肯定是不知所措的。
    而在Springboot中,其实已经帮你实现好了一个shutdownHook,支持响应Ctrl+c或者kill -15 TERM信号。
    随便启动一个应用,然后Ctrl+c一下,观察日志就可知, 它在AnnotationConfigEmbeddedWebApplicationContext这个类中打印出了疑似Closing…的日志,真正的实现逻辑在其父类
    AbstractApplicationContext中(这个其实是spring中的类,意味着什么呢,在spring中就支持了对优雅停机的扩展)。
public void registerShutdownHook() {
    if (this.shutdownHook == null) {
        this.shutdownHook = new Thread() {
            public void run() {
                synchronized(AbstractApplicationContext.this.startupShutdownMonitor) {
                    AbstractApplicationContext.this.doClose();
                }
            }
        };
        Runtime.getRuntime().addShutdownHook(this.shutdownHook);
    }
 
}
 
public void destroy() {
    this.close();
}
 
public void close() {
    Object var1 = this.startupShutdownMonitor;
    synchronized(this.startupShutdownMonitor) {
        this.doClose();
        if (this.shutdownHook != null) {
            try {
                Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
            } catch (IllegalStateException var4) {
                ;
            }
        }
 
    }
}
 
protected void doClose() {
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Closing " + this);
        }
 
        LiveBeansView.unregisterApplicationContext(this);
 
        try {
            this.publishEvent((ApplicationEvent)(new ContextClosedEvent(this)));
        } catch (Throwable var3) {
            this.logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", var3);
        }
 
        if (this.lifecycleProcessor != null) {
            try {
                this.lifecycleProcessor.onClose();
            } catch (Throwable var2) {
                this.logger.warn("Exception thrown from LifecycleProcessor on context close", var2);
            }
        }
 
        this.destroyBeans();
        this.closeBeanFactory();
        this.onClose();
        this.active.set(false);
    }
 
}

我们能对它做些什么呢,其实很明显,在doClose方法中它发布了一个ContextClosedEvent的方法,不就是给我们扩展用的么。于是我们可以写个监听器监听ContextClosedEvent,在发生事件的时候做下线逻辑,对微服务来说即是从注册中心中注销掉服务。

@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {
    
    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent){
       //注销逻辑
       zookeeperRegistry.unregister(mCurrentServiceURL);
       ...
    }
}

可能会有疑问的是,微服务中一般来说,注销服务往往是优雅下线的第一步,接着才会执行停机操作,那么这个时候流量进来怎么办呢?

个人会建议是,在注销服务之后就可开启请求挡板拒绝流量了,通过微服务框架本身的故障转移功能去处理被拒绝的流量即可。

Docker中的下线

好有人说了,我用docker部署服务,支不支持优雅下线。

那来看看docker的一些停止命令都会干些啥:

一般来说,正常人可能会用docker stop或者docker kill 命令去关闭容器(当然如果上一步注册了USR2自定义信息,可能会通过docker exec kill -12去关闭)。

对于docker stop来说,它会发一个SIGTERM(kill -15 term信息)给容器的PID1进程,并且默认会等待10s,再发送一个SIGKILL(kill -9 信息)给PID1。

那么很明显,docker stop允许程序有个默认10s的反应时间去做一下优雅停机的操作,程序只要能对kill -15 信号做些反应就好了,如上一步描述。那么这是比较良好的方式。

当然如果shutdownHook方法执行了个50s,那肯定不优雅了。可以通过docker stop -t 加上等待时间。

外置容器的shutdown脚本(Jetty)

如果非要用外置容器方式部署(个人认为浪费资源并提升复杂度)。那么能不能优雅停机呢。

可以当然也是可以的,这里有两种方式:

首先RPC框架本身提供优雅上下线接口,以供调用来结束整个应用的生命周期,并且提供扩展点供开发者自定义服务下线自身的停机逻辑。同时调用该接口的操作会封装成一个preStop操作固化在jetty或者其他容器的shutdown脚本中,保证在容器停止之前先调用下线接口结束掉整个应用的生命周期。shutdown脚本中执行类发起下线服务 -> 关闭端口 -> 检查下线服务直至完成 -> 关闭容器的流程。

而更简单的另一种方法是直接在脚本中加入kill -15命令。

优雅上线

优雅上线相对来说可能会更加困难一些,因为没有什么默认的实现方式,但是总之呢,一个原则就是确保端口存在之后才上线服务。

springboot内置容器优雅上线

这个就很简单了,并且业界在应用层面的优雅上线均是在内置容器的前提下实现的,并且还可以配合一些列健康检查做文章。

参看sofa-boot的健康检查的源码,它会在程序启动的时候先对springboot的组件做一些健康检查,然后再对它自己搞得sofa的一些中间件做健康检查,整个健康检查的流程完毕之后(sofaboot 目前是没法对自身应用层面做健康检查的,它有写相关接口,但是写死了port is ready…)才会暴露服务或者说优雅上线,那么它健康检查的时机是什么时候呢:

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
    healthCheckerProcessor.init();
    healthIndicatorProcessor.init();
    afterHealthCheckCallbackProcessor.init();
    publishBeforeHealthCheckEvent();
    readinessHealthCheck();
}

可以看到它是监听了ContextRefreshedEvent这个事件。在内置容器模式中,内置容器模式的start方法是在refreshContext方法中,方法执行完成之后发布一个ContextRefreshedEvent事件,也就是说在监听到这个事件的时候,内置容器必然是启动成功了的。

但ContextRefreshedEvent这个事件,在一些特定场景中由于种种原因,ContextRefreshedEvent会被监听到多次,没有办法保证当前是最后一次event,从而正确执行优雅上线逻辑。

在springboot中还有一个更加靠后的事件,叫做ApplicationReadyEvent,它的发布藏在了afterRefresh还要后面的那一句listeners.finished(context, null)中,完完全全可以保证内置容器 端口已经存在了,所以我们可以监听这个事件去做优雅上线的逻辑,甚至可以把中间件相关的健康检查集成在这里。

@Component
public class GracefulStartupListener implements ApplicationListener<ApplicationReadyEvent> {    
    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent){
       //注册逻辑 优雅上线
       apiRegister.register(urls);
       ...
    }
}

外置容器(Jetty)优雅上线

目前大多数应用的部署模式不管是jetty部署模式还是docker部署模式(同样使用jetty镜像),本质上用的都是外置容器。那么这个情况就比较困难了,至少在应用层面无法观察到外部容器的运行状态,并且容器本身没有提供什么hook给你实现。

那么和优雅上线一样,需要RPC框架提供优雅上线接口来初始化整个应用的生命周期,并且提供扩展点给开发者供执行自定义的上线逻辑(上报版本探测信息等)。同样将调用这个接口封装成一个postStart操作,固化在jetty等外置容器的startup脚本中,保证应用在容器启动之后在上线。容器执行类似启动容器 -> 健康检查 -> 上线服务逻辑 -> 健康上线服务直至完成 的流程。