Kubernetes 安全机制解读

在 Kubernetes 中,所有资源的访问和变更都是围绕 APIServer 展开的。比如说 kubectl 命令、客户端 HTTP RESTFUL 请求,都是去 call APIServer 的 API 进行的,本文就重点解读 k8s 为了集群安全,都做了些什么。

首先,Kubernetes 官方文档给出了上面这张图。描述了用户在访问或变更资源的之前,需要经过 APIServer 的认证机制、授权机制以及准入控制机制。这三个机制可以这样理解,先检查是否合法用户,再检查该请求的行为是否有权限,最后做进一步的验证或添加默认参数。

用户

Kubernetes 中有两种用户,一种是内置“用户” ServiceAccount,另一种我称之为自然人。

所谓自然人就是指区别于 pod 等资源概念的“人”,可以理解成实际操作 "kubectl" 命令的人。admin 可以分发私钥,但自然人可以储存类似 KeyStone 甚至包含账号密码的文件,所以 k8s 中没有对自然人以 API 对象描述之。

在典型的 Kubernetes 集群中,API 通常服务在 443 端口,APIServer 提供自签名证书。当你使用 kube-up.sh 创建集群用户时,证书会自动在 $USER/.kube/config 中创建出来,而后续用 kubectl 命令访问 APIServer 时,都是用这个证书。

与之相反,k8s 中以 API 对象的形式描述和管理 ServiceAccount。它们被绑定在某个具体的 namespace 中,可以由 APIServer 自动创建出来或手动 call k8s API。

认证机制(Authentication)

k8s 中的认证机制,是在用户访问 APIServer 的第一步。通常是一个完整的 HTTP 请求打过来,但是这一步往往只检测请求头或客户端证书。

认证机制目前有客户端证书、bearer tokens、authenticating proxy、HTTP basic auth 这几种模式。使用方式通常有以下几种:

  1. X509 Client Certs: 客户端证书模式需要在 kubectl 命令中加入 --client-ca-file=<SOMEFILE> 参数,指明证书所在位置。

  2. Static Token File: --token-auth-file=<SOMEFILE> 参数指明 bearer tokens 所在位置。

  3. bearer tokens: 在 HTTP 请求头中加入 Authorization: Bearer <TOKEN>

  4. Bootstrap Tokens: 与 bearer tokens 一致,但 TOKEN 格式为 [a-z0-9]{6}.[a-z0-9]{16}。该方式称为 dynamically-managed Bearer token,以 secret 的方式保存在 kube-system namespace 中,可以被动态的创建和管理。同时,启用这种方式还需要在 APIServer 中打开 --enable-bootstrap-token-auth ,这种方式还处于 alpha 阶段。

  5. Static Password File: 以参数 --basic-auth-file=<SOMEFILE> 指明 basic auth file 的位置。这个 basic auth file 以 csv 文件的形式存在,里面至少包含三个信息:password、username、user id,同时该模式在使用时需要在请求头中加入 Authorization: Basic BASE64ENCODED(USER:PASSWORD)

  6. Service Account Tokens: 该方式通常被 pod 所使用,在 PodSpec 中指明 ServiceAccount 来访问 ApiServer。

除了以上列出来的几种方式外,还有一些比较特殊的访问方式,这里不再详细解读。

授权机制(Authorization)

当用户通过认证后,k8s 的授权机制将对用户的行为等进行授权检查。换句话说,就是对这个请求本身,是否对某资源、某 namespace、某操作有权限限制。

授权机制目前有 4 种模式:RBAC、ABAC、Node、Webhook。下面对这 4 种模式分别做分析。

RBAC

Role-based access control (RBAC) 是基于角色的权限访问控制,通常是对于“内置用户”而言的。该模式是在 k8s v1.6 开发出来的。若要开启该模式,需要在 APIServer 启动时,设置参数 --authorization-mode=RBAC

RBAC 所使用的 API Group 是 rbac.authorization.k8s.io/v1beta1,直到 Kubernetes v1.8 后,RBAC 模块达到稳定水平,所使用的 API Group 为 rbac.authorization.k8s.io/v1

所谓基于角色的权限访问控制,就是对某个用户赋予某个角色,而这个角色通常决定了对哪些资源拥有怎样的权限。

ServiceAccount

首先来看看这个 “内置用户”,在大多时候我们都不使用 “自然人” 这个功能,而是使用 ServiceAccount,再对其他资源授予某个 ServiceAccount,就使得其能够以 “内置用户” 的身份去访问 APIServer。

创建一个 ServiceAccount 很简单,只需要指定其所在 namespace 和 name 即可。举个例子:

apiVersion: v1
kind: ServiceAccount
metadata:
  namespace: hdls
  name: hdls-sa

Role & Rolebinding

RBAC 中最重要的概念就是 RoleRoleBindingRole 定义了一组对 Kubernetes API 对象的操作权限,而 RoleBinding 则定义的是具体的 ServiceAccount 和 Role 的对应关系。

举个 Role 的例子如下:

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
    namespace: hdls
    name: hdls-role
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]

其中:
namespace: 在这里仅限于逻辑上的“隔离”,并不会提供任何实际的隔离或者多租户能力;
rules:定义的是权限规则,允许“被作用者”,对 hdls 下面的 Pod 对象,进行 GET 和 LIST 操作;
apiGroups:为 "" 代表 core API Group;
resources:指的是资源类型,对此还可以进行详细的划分,指定可以操作的资源的名字,比如:

rules:
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["my-config"]
  verbs: ["get"]

verbs: 指的是具体的操作,当前 Kubernetes(v1.11)里能够对 API 对象进行的所有操作有 "get", "list", "watch", "create", "update", "patch", "delete"。

再看 RoleBinding 的例子:

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
    name: hdls-rolebinding
    namespace: hdls
subjects:
- kind: ServiceAccount
    name: hdls-sa
    apiGroup: rbac.authorization.k8s.io
roleRef:
    kind: Role
    name: hdls-role
    apiGroup: rbac.authorization.k8s.io

可以看到,这个 RoleBinding 对象里定义了一个 subjects 字段,即“被作用者”。它的类型是 ServiceAccount,就是上面创建的 sa。这个 subjects 还可以是 User 和 Group,User 是指 k8s 里的用户,而 Group 是指 ServiceAccounts。

roleRef 字段是用来直接通过名字,引用我们前面定义的 Role 对象(hdls-role),从而定义了 Subject 和 Role 之间的绑定关系。

此时,我们再用 kubectl get sa -n hdls -o yaml 命令查看之前的 ServiceAccount,就可以看到 ServiceAccount.secret,这是因为 k8s 会为一个 ServiceAccount 自动创建并分配一个 Secret 对象,而这个 Secret 就是用来跟 APIServer 进行交互的授权文件: TokenToken 文件的内容一般是证书或者密码,以一个 Secret 对象的方式保存在 etcd 当中。

这个时候,我们在我们的 Pod 的 YAML 文件中定义字段 .spec.serviceAccountName 为上面的 ServiceAccount name 即可声明使用。

如果一个 Pod 没有声明 serviceAccountName,Kubernetes 会自动在它的 Namespace 下创建一个名叫 default 的默认 ServiceAccount,然后分配给这个 Pod。然而这个默认 ServiceAccount 并没有关联任何 Role。也就是说,此时它有访问 APIServer 的绝大多数权限。

ClusterRole & ClusterRoleBinding

需要注意的是 Role 和 RoleBinding 对象都是 Namespaced 对象,它们只对自己的 Namespace 内的资源有效。

而某个 Role 需要对于非 Namespaced 对象(比如:Node),或者想要作用于所有的 Namespace 的时候,我们需要使用 ClusterRole 和 ClusterRoleBinding 去做授权。

这两个 API 对象的用法跟 Role 和 RoleBinding 完全一样。只不过,它们的定义里,没有了 Namespace 字段。

值得一提的是,Kubernetes 已经内置了很多个为系统保留的 ClusterRole,它们的名字都以 system: 开头。一般来说,这些系统级别的 ClusterRole,是绑定给 Kubernetes 系统组件对应的 ServiceAccount 使用的。

除此之外,Kubernetes 还提供了四个内置的 ClusterRole 来供用户直接使用:

cluster-admin:整个集群的最高权限。如果在 ClusterRoleBinding 中使用,意味着在这个集群中的所有 namespace 中的所有资源都拥有最高权限,为所欲为;如果在 RoleBinding 中使用,即在某个 namespace 中为所欲为。

admin:管理员权限。如果在 RoleBinding 中使用,意味着在某个 namespace 中,对大部分资源拥有读写权限,包括创建 Role 和 RoleBinding 的权限,但没有对资源 quota 和 namespace 本身的写权限。

edit:写权限。在某个 namespace 中,拥有对大部分资源的读写权限,但没有对 Role 和 RoleBinding 的读写权限。

view:读权限。在某个 namespace 中,仅拥有对大部分资源的读权限,没有对 Role 和 RoleBinding 的读权限,也没有对 seccrets 的读权限。

Aggregated ClusterRoles

在 Kubernetes v1.9 之后,ClusterRole 有一种新的定义方法,就是使用 aggregationRule 将多个 ClusterRole 合成一个新的 ClusterRole

首先看个 k8s 官网的例子:

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: monitoring
aggregationRule:
  clusterRoleSelectors:
  - matchLabels:
      rbac.example.com/aggregate-to-monitoring: "true"
rules: []

其中 rules 字段不必定义,会被 controller manager 自动填充。

可以看出 aggregationRule 就是将所有满足 label 条件的 ClusterRole 的合成一个 ClusterRole,而这个新的 ClusterRole 权限为其他总和。

Group

相对于 User 而言,k8s 还拥有“用户组”(Group)的概念,也就是一组“用户”的意思。而对于“内置用户” ServiceAccount 来说,“用户组”的概念也同样适用。

实际上,一个 ServiceAccount,在 Kubernetes 里对应的“用户”的名字是: system:serviceaccount:<ServiceAccount 名字 > ;而它对应的内置“用户组”的名字,就是 system:serviceaccounts:<Namespace 名字 >

对于 Group 的运用,我们举个例子,在 RoleBinding 里这样定义 subjects:

subjects:
- kind: Group
    name: system:serviceaccounts:hdls
    apiGroup: rbac.authorization.k8s.io

这就意味着这个 Role 的权限规则,作用于 hdls 里的所有 ServiceAccount。

而如果 Group 不指定 Namespace,即直接定义为 system:serviceaccounts,意味着作用于整个系统里的所有 ServiceAccount。

ABAC

Attribute-based access control (ABAC) 是基于属性的权限访问控制。若要开启该模式,需要在 APIServer 启动时,开启 --authorization-policy-file=<SOME_FILENAME>--authorization-mode=ABAC 两个参数。

其 policy 文件用来指定权限规则,必须满足每行都是一个 json 对象的格式。可以指定 user 或 group 为某个特定的对象,并描述其拥有的权限。

与 Yaml 文件一致,必须描述的属性有 apiVersion、kind、spec,而 spec 里描述了具体的用户、资源和行为。看个例子:

{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "bob", "namespace": "projectCaribou", "resource": "pods", "readonly": true}}

这就描述了用户 bob 只有在 namespace projectCaribou 下对 pod 的读权限。类似的,这个 User 可以是某个人,也可以是 kubelet 或者某个 ServiceAccount,这里 ServiceAccount 需要写全,比如:system:serviceaccount:kube-system:default

如果是描述某个 namespace 下的所有人,需要用到 group,比如:

{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"group": "system:serviceaccounts:default", "readonly": true, "resource": "pods"}}

Node

Node 授权机制是一种特殊的模式,是 kubelet 发起的请求授权。开启该模式,需要开启参数 --authorization-mode=Node

通过启动 --enable-admission-plugins=...,NodeRestriction,...,来限制 kubelet 访问 node,endpoint、pod、service以及secret、configmap、PV 和 PVC 等相关的资源。

Webhook

Webhook 模式是一种 HTTP 回调模式,是一种通过 HTTP POST 方式实现的简单事件通知。该模式需要 APIServer 配置参数 –authorization-webhook-config-file=<SOME_FILENAME>,HTTP 配置文件的格式跟 kubeconfig 的格式类似。

# Kubernetes API version
apiVersion: v1
# kind of the API object
kind: Config
# clusters refers to the remote service.
clusters:
  - name: name-of-remote-authz-service
    cluster:
      # CA for verifying the remote service.
      certificate-authority: /path/to/ca.pem
      # URL of remote service to query. Must use 'https'. May not include parameters.
      server: https://authz.example.com/authorize

# users refers to the API Server's webhook configuration.
users:
  - name: name-of-api-server
    user:
      client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
      client-key: /path/to/key.pem          # key matching the cert

# kubeconfig files require a context. Provide one for the API Server.
current-context: webhook
contexts:
- context:
    cluster: name-of-remote-authz-service
    user: name-of-api-server
  name: webhook

其中,Cluster 指需要回调的地方的客户端,指定其访问证书和 URL;user 指回调处访问的身份,指明其所需证书和 key;contexts 指回调的内容。

准入控制(Admission Controllers)

在一个请求通过了认证机制和授权认证后,需要经过最后一层筛查,即准入控制。这个准入控制模块的代码通常在 APIServer 中,并被编译到二进制文件中被执行。这一层安全检查的意义在于,检查该请求是否达到系统的门槛,即是否满足系统的默认设置,并添加默认参数。

准入控制以插件的形式存在,开启的方式为:
kube-apiserver --enable-admission-plugins=NamespaceLifecycle,LimitRanger ...

关闭的方式为:
kube-apiserver --disable-admission-plugins=PodNodeSelector,AlwaysDeny ...

常用的准入控制插件有:

  • AlwaysAdmit:允许所有请求通过,被官方反对,因为没有实际意义;
  • AlwaysPullImages:将每个 pod 的 image pull policy 改为 always,在多租户的集群被使用;
  • AlwaysDeny:禁止所有请求通过,被官方反对,因为没有实际意义;
  • DefaultStorageClass:为每个 PersistentVolumeClaim 创建默认的 PV;
  • DefaultTolerationSeconds:如果 pod 对污点 node.kubernetes.io/not-ready:NoExecutenode.alpha.kubernetes.io/unreachable:NoExecute 没有容忍,为其创建默认的 5 分钟容忍 notready:NoExecuteunreachable:NoExecute
  • LimitRanger:确保每个请求都没有超过其 namespace 下的 LimitRange,如果在 Deployment 中使用了 LimitRange 对象,该准入控制插件必须开启;
  • NamespaceAutoProvision:检查请求中对应的 namespace 是否存在,若不存在自动创建;
  • NamespaceExists:检查请求中对应的 namespace 是否存在,若不存在拒绝该请求;
  • NamespaceLifecycle:保证被删除的 namespace 中不会创建新的资源;
  • NodeRestriction:不允许 kubelet 修改 Node 和 Pod 对象;
  • PodNodeSelector:通过读取 namespace 的注解和全局配置,来控制某 namespace 下哪些 label 选择器可被使用;
  • PodPreset:满足预先设置的标准的 pod 不允许被创建;
  • Priority:通过 priorityClassName 来决定优先级;
  • ResourceQuota:保证 namespace 下的资源配额;
  • ServiceAccount:保证 ServiceAccount 的自动创建,如果用到 ServiceAccount,建议开启;

以上只列举了部分,详情请移步 Kubernetes 官方文档。

官方建议:

  • 版本 > v1.10:
--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota
  • v1.9
--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota
  • v1.6 - v1.8
--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds
  • v1.4 - v1.5
--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota
2018/11/26 posted in  Kubernetes

Kubernetes 入门之网络详解

Service 是 k8s 网络部分的核心概念,在 k8s 中,Service 主要担任了四层负载均衡的职责。本文从负载均衡、外网访问、DNS 服务的搭建及 Ingress 七层路由机制等方面,讲解 k8s 的网络相关原理。

Service 详解

Service 是主要用来实现应用程序对外提供服务的机制。

如上图所示,Service 是对 Pod 的一层抽象,主要通过 TCP/IP 机制及监听 IP 和端口号来对外提供服务。与 Pod 不同的是,Service 一旦创建,系统会为其分发一个 ClusterIP (也可以自己指定),且在其生命周期内不会发生变化。

Service 的创建

命令行快速创建

在创建好 RC 后,可以通过命令行 kubectl expose 来快速创建一个对应的 Service 。比如现已有一个名为 hdls 的 rc:

kubectl expose rc hdls

这种方式创建出来的 Service,其 ClusterIP 是系统自动为其分配的,而 Service 的端口号是从 Pod 中的 containerPort 复制而来。

通过 YAML 创建

apiVersion: v1
kind: Service
metadata:
  name: hdls
spec:
  ports:
  - port: 8080   # Service 的虚拟端口
    targetPort: 8000  # 指定后端 Pod 的端口号
  selector:  # Label 选择器
    app: hdls

定义好 YAML 文件后,通过命令 kubectl create -f <service.yml> 即可创建。Service 的定义需要指定以下几个关键字段:

  • ports
    • port: Service 的虚拟端口
    • targetPort: 后端 Pod 的端口号,若不填则默认与 Service 的端口一致
  • selector: Label 选择器,指定后端 Pod 所拥有的 Label

负载分发策略

k8s 提供了两种负载分发策略:

  • RoundRobin:轮询方式。即轮询将请求转发到后端的各个 Pod 上。
  • SessionAffinity:基于客户端 IP 地址进行会话保持模式。即相同 IP 的客户端发起的请求被转发到相同的 Pod 上。

在默认情况下,k8s 采用轮询模式进行路由选择,但我们也可以通过将 service.spec.SessionAffinity 设置为 “ClusterIP” 来启用 SessionAffinity 模式。

一些特殊情况

开发人员需要自己控制负载均衡策略的情况

在这种情况下,k8s 通过 Headless Service 的概念来实现,即不给 Service 设置 ClusterIP (无入口 IP),仅通过 Label Selector 将后端的 Pod 列表返回给调用的客户端。

apiVersion: v1
kind: Service
metadata:
  name: hdls
spec:
  ports:
  - port: 8080   
    targetPort: 8000 
  clusterIP: None
  selector: 
    app: hdls

该 Service 没有虚拟的 ClusterIP ,对其访问可以获得所有具有 app=hdls 的 Pod 列表,客户端需要实现自己的负责均衡策略,再确定具体访问哪一个 Pod。

需要将某些服务作为后端服务

一般来说,应用系统需要将外部数据库作为后端服务进行连接,或另一个集群或 namespace 中的服务作为后端服务。这些情况,可以通过建立一个无 Label Selector 的 Service 来实现:

apiVersion: v1
kind: Service
metadata:
  name: hdls
spec:
  ports:
  - port: 8080   
    targetPort: 8000 

该 Service 没有标签选择器,即无法选择后端 Pod。这时系统不会自动创建 Endpoint,需要手动创建一个与该 Service 同名的 Endpoint,用于指向实际的后端访问地址。

apiVersion: v1
kind: Endpoints
metadata:
  name: hdls  # 与 Service 同名
subsets:
  - addresss:
   - IP: 1.2.3.4   # 用户指定的 IP 
   ports:
   - port: 8000

此时,如上面的 YAML 创建出来 Endpoint,访问无 Label Selector 的 Service ,即可将请求路由到用户指定的 Endpoint 上。

多端口的 Service

在 service.spec.ports 中定义多个 port 即可,包括指定 port 的名字和协议。

apiVersion: v1
kind: Service
metadata:
  name: hdls
spec:
  ports:
  - name: dns
    port: 8080   
    protocol: TCP
  - name: dns-tcp
    port: 8080 
    protocol: UDP
  selector: 
    app: hdls

外网访问

Pod 和 Service 都是 k8s 集群内部的虚拟概念,所以集群外的客户无法访问。但在某些特殊条件下,我们需要外网可以访问 Pod 或 Service,这时我们需要将 Pod 或 Service 的端口号映射到宿主机,这样客户就可以通过物理机访问容器应用。

外网访问 Pod

将容器应用的端口号映射到物理机上。有两种方式,如下。

设置容器级别的 hostPort

这种是将容器应用的端口号映射到物理机。设置如下:

apiVersion: v1
kind: Pod
metadata:
  name: hdls-pod
spec:
  containers:
  - name: hdls-container
   image: ***
   ports:
   - containerPort: 8000
     hostPort: 8000

设置 Pod 级别的 hostNetwork=true

这种是将该 Pod 中所有容器端口号都直接映射到物理机上。此时需要注意的是,在容器的 ports 定义部分,若不指定 hostPort,默认 hostPort=containerPort,若设置了 hostPort,则 hostPort 必须等于 containerPort。设置如下:

apiVersion: v1
kind: Pod
metadata:
  name: hdls-pod
spec:
  hostNetwork: true
  containers:
  - name: hdls-container
   image: ***
   ports:
   - containerPort: 8000

外网访问 Service

也有两种方式。

设置 nodePort 映射到物理机

首先需要设置 nodePort 映射到物理机,同时需要设置 Service 的类型为 NodePort:

apiVersion: v1
kind: Service
metadata:
  name: hdls
spec:
  type: NodePort  # 指定类型为 NodePort
  ports:
  - port: 8080 
    targetPort: 8000 
    nodePort: 8000   # 指定 nodePort
  selector: 
    app: hdls

设置 LoadBalancer 映射到云服务商提供的 LoadBalancer 地址

这种用法仅用于在公有云服务提供商的云平台上设置 Service 的场景。需要将 service.status.loadBalancer.ingress.ip 设置为云服务商提供的负载均衡器的 IP。则对该 Service 的访问请求将会通过 LoadBalancer 转发到后端 Pod,且负载均衡的实现方式依赖于云服务商提供的 LoadBalancer 的实现机制。

DNS 搭建

为了能够实现通过服务名在集群内部进行服务的相互访问,需要创建一个虚拟的 DNS 服务来完成服务名到 ClusterIP 的解析。

k8s 提供的 DNS

k8s 提供的 DNS 服务名为 skydns,由下面四个组件组成:

  • etcd: DNS 存储;
  • kube2sky: 将 k8s Master 中的 Service 注册到 etcd ;
  • skyDNS: DNS 域名解析服务;
  • healthz: 对 skyDNS 的健康检查。

skyDNS 服务由一个 RC 和一个 Service 组成。在 RC 的配置文件中,需要定义 etcd / kube2sky / skydns / healthz 四个容器,以保证 DNS 服务正常工作。需要注意的是:

  1. kube2sky 容器需要访问 k8s Master,所以需要在配置文件中为其配置 Master 所在物理主机的 IP 地址和端口;
  2. 需要将 kube2sky 和 skydns 容器的启动参数 --domain 设置为 k8s 集群中 Service 所属域名。容器启动后 kube2sky 会通过 API Server 监控集群中所有 service 的定义,生成相应的记录并保存到 etcd ;
  3. skydns 的启动参数 -addr=<IP:Port> 表示本机 TCP 和 UDP 的 Port 端口提供服务。

在 DNS Service 的配置文件中,skyDNS 的 ClusterIP 需要我们指定,每个 Node 的 kubelet 都会使用这个 IP 地址,不会通过系统自动分配;另外,这个 IP 需要在 kube-apiserver 启动参数 --service-cluster-ip-range 内。

在 skydns 容器创建之前,需要先修改每个 Node 上 kubelet 的启动参数:

  • --cluster_dns= ,dns_cluster_ip 为 DNS 服务的 ClusterIP ;
  • --cluster_domain= , dns_domain 为 DNS 服务中设置的域名。

DNS 工作原理

  1. 首先 kube2sky 容器应用通过调用 k8s Master 的 API 获得集群中所有 Service 信息,并持续监控新 Service 的生成,写入 etcd;
  2. 根据 kubelet 的启动参数的设置,kubelet 会在每个新创建的 Pod 中设置 DNS 域名解析配置文件 /etc/resolv.conf 中增加一条 nameserver 配置和 search 配置,通过 nameserver 访问的实际上就是 skydns 在对应端口上提供的 DNS 解析服务;
  3. 最后,应用程序就可以像访问网站域名一样,仅通过服务的名字就能访问服务了。

Ingress

Service 工作在 TCP/IP 层,而 Ingress 将不同的 URL 访问请求转发到后端不同的 Service ,实现 HTTP 层的业务路由机制。而在 k8s 中,需要结合 Ingress 和 Ingress Controller ,才能形成完整的 HTTP 负载均衡。

Ingress Controller

Ingress Controller 用来实现为所有后端 Service 提供一个统一的入口,需要实现基于不同 HTTP URL 向后转发的负载分发规则。Ingress Controller 以 Pod 的形式运行,需要实现的逻辑:

  • 监听 APIServer,获取所有 Ingress 定义;
  • 基于 Ingress 的定义,生成 Nginx 所需的配置文件 /etc/nginx/nginx.conf
  • 执行 nginx -s reload ,重新加载 nginx.conf 配置文件的内容。

定义 Ingress

k8s 中有一种单独的名为 Ingress 的资源,在其配置文件中可以设置到后端 Service 的转发规则。比如,为 hdls.me 定义一个 ingress.yml:

apiVersion: extensions/v1beta1
kind: Ingress
metadata: 
  name: hdls-ingress
spec:
  rules:
  - host: hdls.me
   http:
     paths:
     - path: /web
       backend:
         serviceName: hdls
         servicePort: 8000

最后采用 kubectl create -f ingress.yml 创建 Ingress。可以登录 nginx-ingress Pod 查看其自动生成的 nginx.conf 配置文件内容。

2018/9/23 posted in  Kubernetes

Python 协程

协程(Coroutine)又称微线程,即轻量级的线程。协程可以理解成与调用方协作,产出由调用方提供的值的过程。与线程相比,其优势在于上下文切换的成本更低,且由用户自己控制。

发展史

Python 中的协程主要经历了三个阶段。协程最开始是在 Python 2.5 中实现的,由生成器变形而来,以关键词 yield/send 等实现;引入 yield from,可以把复杂的生成器重构成小型的嵌套生成器;Python 3.5 中引入了 async/await 语法糖。

由于 yield from 已被移除 python 的语法,本文重点分析 yield/send 和 async/await 关键字是怎么实现协程的。

yield / send

协程的运行

在生成器中使用 yield 关键字,而后生成器的调用方使用 .send(value) 方法发送数据,该数据 value 就会成为生成器函数中 yield 表达式的值。换句话说,yield 是生成器中的一个暂停器,第一次调用时在 yield 处暂停,将 yield 右边的值 return 出去;下一次 send 进来的数据成为 yield 表达式的值。举个例子:

def count_num():
    r = 0
    print("Started.")
    while True:
        x = yield r
        print("Received x: {}".format(x))
        r = r + 1


if __name__ == "__main__":
    coroutine = count_num()
    next(coroutine)

    for i in "hello":
        t = coroutine.send(i)
        print("Coroutine times: {}".format(t))
    coroutine.close()

运行结果如下:

Started.
Received x: h
Coroutine times: 1
Received x: e
Coroutine times: 2
Received x: l
Coroutine times: 3
Received x: l
Coroutine times: 4
Received x: o
Coroutine times: 5

由此可以看出,局部变量 r 的值没有随协程的暂停而改变,可知协程中的局部变量保持在一个上下文中。这也是使用协程的一个好处,无需使用类对象的属性或闭包在多次调用中保持在上下文中。

另外,这里还需要注意的是,next(coroutine) 这一处的意思是先调用协程使其先运行到 yield 处进行第一次暂停,使协程处于暂停状态。之后协程再 send 时,才能生效,这一举动称为“预激”。

协程的状态总共有 4 种,分别为:
GEN_CREATED :等待开始执行状态
GEN_RUNNING :解释器正在执行
GEN_SUSPENDED :在 yield 表达式处暂停
GEN_CLOSED :执行结束

协程预激除了 next() 方法,还可以使用 .send(None) 方法,效果一样。如果将上述例子中预激的代码注释掉,运行后会报错:

Traceback (most recent call last):
  File "test.py", line 15, in <module>
    t = coroutine.send(i)
TypeError: can't send non-None value to a just-started generator

错误栈中表达的很清楚:在生成器还处于开始状态时,不可 send 不为 None 的值。

协程异常处理

协程中若出现未处理的异常,会向上传至 next 或 send 的调用方,且此时协程停止。而我们大多时候需要协程内部在出现异常时不退出,这时候通常的处理方法是 throw 方法。

throw 可以使协程抛出指定的异常,而不影响其运行的流程,协程依然在 yield 处暂停。在上述的例子中加入异常处理的功能:

class Error(Exception):
    pass


def count_num():
    r = 0
    print("Started.")
    while True:
        try:
            x = yield r
            print("Received x: {}".format(x))
        except Error:
            print("Coroutine error.")
        r = r + 1


if __name__ == "__main__":
    coroutine = count_num()
    next(coroutine)

    n = 0
    for i in "hello":
        n = n + 1
        if n % 2 == 0:
            coroutine.throw(Error)
        else:
            t = coroutine.send(i)
            print("Coroutine times: {}".format(t))
    coroutine.close()

运行结果:

Started.
Received x: h
Coroutine times: 1
Coroutine error.
Received x: l
Coroutine times: 3
Coroutine error.
Received x: o
Coroutine times: 5

协程处理异常除了用 throw 方法,还可以用 send 方法传入一个非法的值,比如常用的 None,这个也称为哨值。将上述代码中的 coroutine.throw(Error) 换成 coroutine.send(None) 也是一样的效果。

上述代码中最后调用了 close 方法,将协程的状态切换成 GEN_CLOSED。该方法的原理是在 yield 暂停处抛出 GeneratorExit 异常,若协程调用方没有处理这个异常或抛出了 StopIteration 异常,则不做处理,且将其状态切换成 GEN_CLOSED

async / await

从 python3.5 开始,Python 新加了一种协程定义方法 async def。简单的讲,async 定义一个协程,await 用于挂起阻塞的异步调用接口;而协程的调用方法在 Python3.7 中做了些许改动,所以这一节以 Python 版本分成两部分来讲解。

python 3.5 - 3.6

阅读协程的官方文档就会知道:协程本身无法运行,只有将其置于事件循环(event_loop)中才能运行其代码。那么事件循环是什么?在源码中的定义为:

# A TLS for the running event loop, used by _get_running_loop.
class _RunningLoop(threading.local):
    loop_pid = (None, None)


_running_loop = _RunningLoop()

event_loop 继承了 threading.local,创建一个全局 ThreadLocal 对象。后续将协程推进这个 loop,只有 loop 是 running 的,协程才得以执行。

协程的执行

执行协程首先需要将协程包成 future 或 task 再推进 event_loop;然后执行 loop.run_until_complete ,运行 loop 中所有协程。

这里 future 指一种对象,表示异步执行的操作;task 指对协程进一步封装,其中包含任务的各种状态,其中 task 是 future 的子类。

有两种方式:asyncio.ensure_futureloop.create_task 。但二者本质都是一样:将协程包成 future。下面两种实现方式效果一样:

async def count_num(num):
    print("count num: {}".format(num))


if __name__ == "__main__":
    loop = asyncio.get_event_loop()

    future = asyncio.ensure_future(count_num(100))  
    loop.run_until_complete(future)
    loop.close()
async def count_num(num):
    print("count num: {}".format(num))


if __name__ == "__main__":
    loop = asyncio.get_event_loop()

    task = loop.create_task(count_num(100))
    loop.run_until_complete(task)
    loop.close()

需要注意的是:也可以直接执行 loop.run_until_complete(coroutine),但这里是其实是先将 coroutine 包进了 ensure_future 里的。

协程的并发与阻塞

既然协程是为了异步而生,那么其异步执行肯定是重点。asyncio 的调用方法 asyncio.gather() 就可以将多个协程推进同一个事件循环。看个例子:

import asyncio
import time


async def count_num(num):
    print("Started coroutine #{} at".format(num), time.strftime('%X'))
    for i in range(num):
        time.sleep(1)
        print("[coroutine #{}] count: {} at".format(num, i), time.strftime('%X'), "...")
    print("Finish coroutine #{} at".format(num), time.strftime('%X'))


if __name__ == "__main__":
    print("Start.")
    loop = asyncio.get_event_loop()

    loop.run_until_complete(asyncio.gather(
        count_num(3),
        count_num(4),
    ))
    loop.close()

    print("Finish at", time.strftime('%X'))

该例子中调用了两个协程,功能是数数,从 0 开始数到传进去的数结束,每数一个数都会 sleep 1s。为了直观,每一步都将当前的时间打印出来。执行结果:

Start.
Started coroutine #3 at 11:51:10
[coroutine #3] count: 0 at 11:51:11...
[coroutine #3] count: 1 at 11:51:12...
[coroutine #3] count: 2 at 11:51:13...
Finish coroutine #3 at 11:51:13
Started coroutine #4 at 11:51:13
[coroutine #4] count: 0 at 11:51:14...
[coroutine #4] count: 1 at 11:51:15...
[coroutine #4] count: 2 at 11:51:16...
[coroutine #4] count: 3 at 11:51:17...
Finish coroutine #4 at 11:51:17
Finish at 11:51:17

通过执行结果可以看出,#3 和 #4 是分别执行,没有我们想要的并发的效果。而这里就需要 await 关键词来发挥作用了,await 可以将阻塞的协程挂起,让事件循环执行别的协程,直到其他协程挂起或执行完毕。我们将上例中的 sleep 进行修改:

import asyncio
import time


async def count_num(num):
    print("Started coroutine #{} at".format(num), time.strftime('%X'))
    for i in range(num):
        await asyncio.sleep(1)
        print("[coroutine #{}] count: {} at".format(num, i), time.strftime('%X'), "...")
    print("Finish coroutine #{} at".format(num), time.strftime('%X'))


if __name__ == "__main__":
    print("Start.")
    loop = asyncio.get_event_loop()

    loop.run_until_complete(asyncio.gather(
        count_num(3),
        count_num(4),
    ))
    loop.close()

    print("Finish at", time.strftime('%X'))

执行结果:

Start.
Started coroutine #3 at 11:59:16
Started coroutine #4 at 11:59:16
[coroutine #3] count: 0 at 11:59:17...
[coroutine #4] count: 0 at 11:59:17...
[coroutine #3] count: 1 at 11:59:18...
[coroutine #4] count: 1 at 11:59:18...
[coroutine #3] count: 2 at 11:59:19...
Finish coroutine #3 at 11:59:19
[coroutine #4] count: 2 at 11:59:19...
[coroutine #4] count: 3 at 11:59:20...
Finish coroutine #4 at 11:59:20
Finish at 11:59:20

python3.7

Python3.7 在原来的基础上对协程的执行做了一层封装,使得这个功能更加亲人。我们只需要定义我们需要的协程,然后调用 .run() 即可;在多协程的情况下,我们做出一个统一入口即可,看个例子:

import asyncio
import time


async def count_num(num):
    print("Started coroutine #{} at ".format(num), time.strftime('%X'))
    for i in range(num):
        await asyncio.sleep(1)
        print("[coroutine #{}] count: {} at ".format(num, i),
              time.strftime('%X'), "...")
    print("Finish coroutine #{} at ".format(num), time.strftime('%X'))


async def main():
    await asyncio.gather(
        count_num(3),
        count_num(4),
    )


if __name__ == "__main__":
    print("Start.")
    asyncio.run(main())
    print("Finish at ", time.strftime('%X'))

这段代码的结果与上面例子的结果一毛一样,可以看到调用起来就简单很多,这是因为绝大部分的逻辑(包括事件循环)都在 .run() 方法中替你封装好了。拜读一下源码:

def run(main, *, debug=False):
    if events._get_running_loop() is not None:
        raise RuntimeError(
            "asyncio.run() cannot be called from a running event loop")

    if not coroutines.iscoroutine(main):
        raise ValueError("a coroutine was expected, got {!r}".format(main))

    loop = events.new_event_loop()
    try:
        events.set_event_loop(loop)
        loop.set_debug(debug)
        return loop.run_until_complete(main)
    finally:
        try:
            _cancel_all_tasks(loop)
            loop.run_until_complete(loop.shutdown_asyncgens())
        finally:
            events.set_event_loop(None)
            loop.close()

然后这里有一点需要注意的是,.run() 函数不能在同一个线程已有事件循环的情况下调用,它始终会新建一个事件循环,并且在执行完所有的协程后将其关闭。

2018/9/14 posted in  Python

Kubernetes 入门之 Pod 详解

Pod 作为 k8s 的基本调度单元,是 k8s 的关键所在。本文从 Pod 的使用、控制、调度、应用配置等方面入手,全方面讲解 k8s 如何发布和管理应用。

pod 基本用法

对长时间运行容器的要求是:其主程序需要一直在前台运行。kubelet 创建包含这个容器的 Pod 之后运行完命令,即认为 Pod 执行结束,接着立即销毁该 Pod ,根据 RS 中定义的 Pod 副本数量,会立即再生成一个新 Pod ,会进入无限循环。

属于一个 Pod 的多个容器应用之间仅需通过 localhost 通信,一组容器被绑定在了一个环境中。

在同一个 Pod 中的容器共享 Pod 级别的 Volume,而每个容器即可各自进行挂载,将 Volume 挂载为容器内部需要的目录。

静态 Pod

静态 pod 是指由 kubelet 管理的仅存在特定 Node 上的 Pod。它们不能通过 APIServer 管理,无法与 RS 等进行关联。

创建静态 Pod 的方式:

静态 pod 的创建方式有两种,分别为配置文件方式和 HTTP 方式。

配置文件方式

设置 kubelet 的启动参数 --config,指定 kubelet 需要监控的配置文件所在目录,kubelet 定期扫描该目录,根据目录中的 .yaml 或 .json 文件创建操作。由于无法通过 APIServer 对静态 pod 进行直接管理,在 Master 节点上尝试删除这个 Pod 将会将其变成 pending 状态,不会删除;若需要删除该 pod 需要在其所在 Node 上,将其定义文件从 /etc/kubelet.d 目录下删除。

HTTP 方式

设置 kubelet 的启动参数 --manifest-url ,kubelet 定期从该 URL 地址下载 Pod 的定义文件,并以 .yaml 或 .json 文件的格式解析,然后创建 Pod。实现方式与配置文件方式一致。

Pod 配置管理 —— ConfigMap

应用部署的一个最佳实践是将应用所需的配置信息与程序分离,好处是可以实现应用程序被更好的服用,通过不同的配置也能实现更灵活的功能。ConfigMap 是 k8s 统一的集群配置管理方案。

用法

生成为容器的环境变量,设置容器启动命令的启动参数,以 Volume 形式挂载为容器内部的文件或目录,以 key:value 的形式保存,既可以表示变量的值,也可以表示一个完整配置文件的内容。
blog.cdn.updev.cn

ConfigMap 的创建

通过 yaml 文件的方式:书写好 yaml 文件后, kubectl create -f ***.yaml 命令即可创建 ConfigMap。

直接使用 kubectl create configmap 命令行的方式:可以根据目录、文件或字面值创建 ConfigMap。

1) 可以通过 --from-file 参数从目录中创建

kubectl create configmap <map-name> --from-file=config-files-dir

其中, <map-name> 代表 ConfigMap 的名字,config-files-dir 代表目录。创建出来的 ConfigMap 的 key 即为文件名。

2) 可以通过 --from-file 参数从文件中创建,并自定义 key

kubectl create configmap <map-name> --from-file=<my-key-name>=<path-to-file>

其中, my-key-name 为自定义 key,path-to-file 代表文件。

3) 可以通过 --from-literal 参数从文件中创建,并指定字面值

kubectl create configmap <map-name> --from-literal=<key>=<value>

其中, <key>=<value> 代表指定的键值对。

ConfigMap 的使用方式

下面就是容器中的应用如何使用 ConfigMap 的方法,主要有环境变量方式和挂载 Volume 方式。

环境变量方式

在 Deployment 的 yaml 的 container 中定义 env ,格式如下:

env:
- name: HDLS_KEY
  valueFrom:
    configMapKeyRef:
      name: special-config
      key: hdls

意为:该容器中环境变量 HDLS_KEY 的值取自名为 special-config 的 ConfigMap 中,key 为 hdls

volumeMount 模式

需要在 Pod 的 yaml 的 container 中定义 volumeMounts (引用 volume 名和挂载到容器内的目录),且在 volumes 中定义需要挂载的 volume 名和 ConfigMap 等信息。如下:

apiVersion: v1
kind: Pod
metadata:
  name: hdls-pod
spec:
  containers:
  - name: hdls-container
    image: ...
    ports:
      - containerPort: 8080
    volumeMounts:   # 在 container 中定义 volumeMounts
    - name: hdls-server   # 引用的 volume 名
      mountPath: /configfiles   # 挂载到容器中的目录
  volumes:
  - name: hdls-server   # pod 中挂载的 Volume 名
    configMap:
      name: special-config   # 使用 ConfigMap “special-config”
  ...

ConfigMap 的限制条件

  1. 必须在 Pod 之前创建
  2. 只有处于相同 namespace 的 Pod 可以引用
  3. 没有配额管理
  4. 静态 Pod 无法使用 ConfigMap
  5. 在 Pod 对 ConfigMap 进行挂载时,容器内部只能挂载为目录,不能是文件

Pod 生命周期和重启策略

我们在调度、管理 Pod 时,需要熟悉 Pod 在整个生命周期的各个状态,而设置 Pod 的重启策略也是基于对 Pod 的各种状态的了解。

Pod 的状态

Pod 的所有状态总共有 5 种,分别如下:

Pending : APIServer 已经创建该 Pod ,但 Pod 内还有容器的镜像没有创建或正在下载;
Running : Pod 中所有的容器均已创建,且至少有一个容器处于运行、正在启动、正在重启状态;
Succeeded : Pod 中所有容器已成功退出,且不会重启;
Failed : Pod 中所求容器均已退出,但至少有一个容器处于失败状态;
Unknown : 由于某种原因无法获取该 Pod 的状态,可能由于网络不畅所致。

Pod 的重启策略

Pod 的重启策略有 3 种,默认值为 Always。

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

kubelet 重启失效容器的时间间隔以 sync-frequency 乘以 2n 来计算,最长延迟 5 分钟,并在成功重启后的 10 分钟重置该时间。

每种控制器对 Pod 的重启策略

Pod 的重启策略与控制方式有关,每种控制器对 Pod 的重启策略要求如下:

RS 和 DaemonSet:必须设置为 Always
Job:OnFailure 或 Never
kubelet (静态 Pod):Pod 失效时自动重启,且不进行健康检查

健康检查

Pod 的健康检查分别两种:存活检查和就绪检查,分别使用 LivenessProbe 探针和 ReadinessProbe 探针。

LivenessProbe (存活检查)

用于判断容器是否存活,一旦检测到容器不健康, kubelet 即杀掉该容器,并根据重启策略做相应处理,如果容器不包含 LivenessProbe 探针,kubelet 认为其返回值永远是 success。

ReadinessProbe (就绪检查)

用于判断容器是否启动完成,即是否 ready 状态,一旦检测到失败,则 Pod 的状态被改写,并将该 Pod 的 Endpoint 从 Service 的转发 Endpoint 中删除。

探针设置方式

ExecAction :在容器内执行一个命令,如果该命令的返回码为0,表明容器健康;
TCPSocketAction :通过容器的 IP 和端口号执行 TCP 检查,如果能建立 TCP 连接,表明容器健康;
HTTPGetAction :通过容器的 IP 地址、端口号及路径调用 HTTP Get 方法,如果返回码 >=200 ,且 <400,认为容器健康。

以上每种探测方式都需要设置的参数:
initialDelaySeconds:延迟检查的时间,单位为s
timeoutSeconds:健康检查发送后等待响应的超时时间,单位为s
periodSeconds:执行周期

Pod 调度

Pod 实为 k8s 中的最小调度单元,只是容器的载体,其本身无法完成自动调度的功能。 k8s 采用了 RS、Deployment、DaemonSet、Job 等方式实现 Pod 的调度和自动控制。

RS、Deployment 全自动调度

RS 的主要功能之一就是自动部署一个容器应用的多份副本,及持续监控副本的数量。而 RS/RC 在 k8s 中一般很少单独使用,都是在 Deployment 中使用,而 Deployment 是 k8s 引入的一个更好解决 Pod 编排问题的概念,通过 Deployment 我们可以随时知道 Pod 的调度情况。

在 Pod 的定义中,可以采用 NodeSelector 或 NodeAffinity 两种方式进行调度。

NodeSelector 定向调度

Pod 调度是通过 Master 上的 Scheduler 负责实现的,原理是通过 Node 标签和 Pod 的 nodeSelector 属性匹配。

NodeSelector 定向调度的过程为:通过 kubectl label 给目标 Node 打上标签;在 Pod 的定义上加上 nodeSelector 的设置。需要注意的是,一旦 Pod 指定了 nodeSelector ,若集群中不存在匹配的 Node ,即使有空闲的 Node,也不会调度。

通过 kubectl 进行打标签的方法:

kubectl label nodes <node-name> <label-key>=<label_value>

在 Pod 中指定 nodeSelector:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hdls
spec:
  template:
    metadata:
      name: hdls-pod
    spec:
      containers:
      - name: hdls-container
        image: ...
        ports:
        - containerPort: 8080
      nodeSelector:
        <label-key>: <label_value>

NodeAffinity 亲和性调度

NodeAffinity 意为 Node 亲和性的调度策略,是替换 NodeSelector 的下一代调度策略。在 NodeSelector 的基础上增加了 In/NotIn/Exists/DoesNotExsist/Gt/Lt 等操作。

设置亲和性:
requiredDuringSchedulingRequiredDuringExecution:类似于 NodeSelector ,在 Node 不满足条件时,系统从该 Node 上移除之前调度上的 Pod ;
requiredDuringSchedulingIgnoredDuringExecution:在 Node 不满足条件时,系统不一定从该 Node 上移除之前调度上的 Pod;
preferredDuringSchedulingIgnoredDuringExecution:指定在满足条件的 Node 中,哪些 Node 更优先地调度;同时,Node 不满足条件时,不一定从该 Node 上移除之前调度上的 Pod。

DaemonSet 特定场景调度

DaemonSet 是 Kubernetes 1.2 版本中新增的一种资源对象,用于确保全部(或指定的某些)Node 上运行一个 Pod 副本。其 Pod 调度策略与 RC 类似。

使用场景:

  1. 在每个 Node 上运行一个 GlusterFS 存储或者 Ceph 存储的 daemon 进程
  2. 在每个 Node 上运行一个日志采集程序,如 fluentd 、logstach
  3. 在每个 Node 上运行一个健康程序,采集该 Node 的运行性能数据

Job 批处理调度

Job 是 Kubernetes 1.2 版本中新增的支持批处理的资源对象。批处理任务通常并行或串行启动多个计算进程去处理一批工作项(work item),处理完后整个 Job 结束。

Job 的模式:

  1. Job Template Expansion 模式:一个 Job 对应一个 work item 。通常适合 work item 较少、每个 work item 要处理的数据量较大的场景;
  2. Queue with Pod Per Work Item 模式:采用一个任务队列存放 work item,Job 对象去消费这些 work item 。这种模式是一个 Pod 对应一个 work item,一个 Job 启动多个 Pod;
  3. Queue with Variable Pod Count 模式:与上面一种模式相同,唯一不同的是 Job 启动的 Pod 数量时可变的;
  4. Single Job with Static Work Assignment 模式:也是一个 Job 产生多个 Pod 的模式,但采用的是程序静态方式分配任务,而不是队列模式。

Job 的类型

考虑到批处理的并行问题,Job 被分为以下几种类型:

  1. Non-parallel Jobs :一个 Job 启动一个 Pod;
  2. Parallel Jobs with a fixed completion count :并行 Job 启动多个 Pod。需要设置 Job 的 .spec.completions 参数为一个正数,此为正常结束的 Pod 的上限值;.spec.parallelism 参数为并行数,即同时处理 work item 的 Pod 数;
  3. Parallel Jobs with a work queue :并行 Job 有个独立的 queue 来存放 work item ,此时不能设置 .spec.completions 参数。

Pod 扩容和缩容

在实际生产系统中,服务扩容是个不容忽视的场景。在 k8s 中,有两种方式来实现 Pod 的扩容和缩容,分别为 RC 的 Scale 机制和 HPA (Horizontal Pod Autoscaler)。

RC 的 Scale 机制

通过 kebectl 命令可设置 Pod 的副本数:

kubectl scale rc <rc-name> --replicas=3

通过 --replicas=<num> 参数将 Pod 的副本数手动调高,即可完成 Pod 的扩容;相应的,将该参数设置为较低的数,系统将 kill 掉一些运行中的 Pod ,以实现应用集群缩容。

HPA

HPA (Horizontal Pod Autoscaler) 是 Kubernetes v1.1 新增的控制器,用以实现基于 CPU 使用率进行自动 Pod 扩缩容的功能。HPA 基于 Master 的 kube-controller-manager 服务启动参数 --horizontal-pod-autoscaler-sync-period 定义的时长(默认为 30 秒),周期性的检测目标 Pod 的 CPU 使用率,并在满足条件时对 RC 或 Deployment 中的 Pod 副本数进行调整,以符合用户定义的平均 Pod CPU 使用率。PodCPU 使用率来源于 heapster 组件,所以需要预先安装好 heapster。

创建 HPA 时可以使用 kubectlautoscale 命令或使用 yaml 配置文件。在创建 HPA 之前,需要确保已经存在一个 RC 或 Deployment 对象,且该 RC 或 Deployment 中的 Pod 必须定义 resources.requests.cpu 的资源请求值。

滚动升级

在实际生产环境中,应用的升级也是一个很重要的场景。在集群规模较大时,先全部停止再逐步升级的方式会导致较长时间内服务不可用,升级工作就成了一个不小的挑战。 k8s 提供了滚动升级功能来解决这个问题。

滚动升级通过执行 kubectl rolling-update 命令一键完成。整个过程为:

  1. 创建一个新的 RC;
  2. 自动控制旧的 RC 中的 Pod 副本的数量逐渐减少到 0;
  3. 同时新 RC 中的 Pod 副本数从 0 逐步增加到到目标值,旧的 Pod 每减少 1,新的 Pod 就增加 1。

需要注意的是,新旧 RC 必须在同一 namespace 下。

采用 yaml 配置文件

若采用 yaml 配置文件来实现滚动升级,需要先手动创建一个新的 RC yaml,且 yaml 文件中需要注意的是:

  1. RC 的名字不可与旧 RC 名字相同;
  2. 在 selector 在应至少有一个 Label 与旧的 RC 的 Label 不同,以标识为新的 RC ;

再运行 kubectl 命令完成滚动升级:

kubectl rolling-update <RC-name> -f <new-RC-yaml>

不使用 yaml 配置文件

若不使用 yaml 配置文件,可直接用 kubectl rolling-update 命令,加上 --image 参数指定新版镜像名。

kubectl rolling-update <RC-name> --image=<image-name>

与使用配置文件的方式不同,该方法执行的结果是旧的 RC 被删除,新的 RC 仍使用旧的 RC 名,且完成升级后,新 RC 多一个 key 为 “deployment”(这个名字可通过 --deployment-label-key 参数进行修改) 的 Label ,值为 RC 的内容进行 Hash 计算后的值。

最后,若 Pod 需要回滚,可中断更新操作,执行 kubectl rolling-update-rollback 完成 Pod 版本的回滚。

2018/8/18 posted in  Kubernetes

Django 的信号机制

Django 将 signal 描述为“信号调度员”,主要以信号的形式,来触发多个应用程序。这篇文章将从源码分析的角度,讲解 Django 中 signal 的工作机制及使用方法。

Signal 类

blog.cdn.updev.cn
signal 最常用的场景是通知,例如你的博客有了评论,系统会有一个通知的机制将评论推送给你。用 signal 实现的话,只需要在评论发布的时候触发信号通知,以此来代替将通知的逻辑放在评论发布之后,大大降低了程序耦合度,更利于系统后期的维护。

Django 中实现了一个 Signal 类,这个类用以实现“信号调度员”的功能,其工作机制如下图所示,主要分为两部分,一是每个需要被调度的 callback 函数注册到 signal 上,二是事件触发 sender 发送信号。

receiver

在 signal 中维护了一个列表 receiver ,里面记录的是连接到 signal 的回调函数及其 id 。其中每个 receiver 必须是回调函数,且接受关键词参数 **kwarg , signal 的 connect 方法用来将回调函数连接到 signal 。

我们先来看看 connect 的源代码,如下。

class Signal:

    ...
    
    def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
            from django.conf import settings

        # If DEBUG is on, check that we got a good receiver
        if settings.configured and settings.DEBUG:
            assert callable(receiver), "Signal receivers must be callable."

            # Check for **kwargs
            if not func_accepts_kwargs(receiver):
                raise ValueError("Signal receivers must accept keyword arguments (**kwargs).")

        if dispatch_uid:
            lookup_key = (dispatch_uid, _make_id(sender))
        else:
            lookup_key = (_make_id(receiver), _make_id(sender))

        if weak:
            ref = weakref.ref
            receiver_object = receiver
            # Check for bound methods
            if hasattr(receiver, '__self__') and hasattr(receiver, '__func__'):
                ref = weakref.WeakMethod
                receiver_object = receiver.__self__
            receiver = ref(receiver)
            weakref.finalize(receiver_object, self._remove_receiver)

        with self.lock:
            self._clear_dead_receivers()
            for r_key, _ in self.receivers:
                if r_key == lookup_key:
                    break
            else:
                self.receivers.append((lookup_key, receiver))
            self.sender_receivers_cache.clear()

代码十分清真,分为四部分:检查参数、获取 receiver 的 ID、 receiver 弱引用、加锁。这里我们主要看看后面两部分的内容。

receiver 弱引用

预备知识

弱引用:Python 中对垃圾回收的处理采用的是标记引用的方式,而弱引用的作用在于避免循环引用导致内存泄漏。主要原理是在弱引用某对象时,不在引用标记中增加引用数,所以在该对象的强引用为 0 时,系统依然将其回收,此时弱引用失效。

method 和 function :Python 的函数与其他语言的一样,包含函数名和函数体,支持形参;与函数相比,方法多了一层类的关系,也就是说方法是定义在类里的函数。CPython 中对方法的定义是通过 PyMethod_New 函数,这个函数是通过 func 来一步步配置 method 的,看一段节选源代码:

PyObject *
PyMethod_New(PyObject *func, PyObject *self, PyObject *klass)
{
    ...
        
    im->im_weakreflist = NULL;
    Py_INCREF(func);
        
    im->im_func = func;
    Py_XINCREF(self);
    im->im_self = self;
    Py_XINCREF(klass);
    im->im_class = klass;
    _PyObject_GC_TRACK(im);
    return (PyObject *)im;
    }

method 中除了函数属性 im_func 以外,还有一个 im_self 属性表 self ,和 im_class 属性表 class

Bound Method 和 Unbound Method:方法又可以分为 bound 方法和 unbound 方法,区别在于 bound 方法多了一层实例绑定,也就是说, bound method 是通过实例调用方法,而 unbound method 是直接通过类调用方法。

signal 中的弱引用

熟悉 Python 垃圾回收的同学应该知道,当一个对象的引用计数为 0 时,Python 才对其进行垃圾回收。所以, signal 中所有对回调函数的引用默认均采用弱引用,以免造成内存泄漏。

首先, connect 的参数 weak 表示是否用弱引用,默认为 Truereceiver 可以是函数,也可以是方法,而 bound method 的引用是短暂的,与实例的生命周期一致,所以标准的弱引用不足以保持,需要采用 weakref.WeakMethod 来模拟 bound method 的弱引用;最后 weakref.finalize 方法返回一个可调用的终结器对象,当 receiver 被垃圾回收时调用,与普通弱引用不同的是,终结器在调用前将始终存活,被调用之后死亡,从而大大简化了生命周期管理。

加锁

锁的存在是为了实现线程安全,而线程安全是指在多个线程同时存在时,运行结果依然符合预期。显然,signal 中的 receiver 注册过程不是天生线程安全,signal 中实现线程安全的方法是加锁,来实现 connect 方法的原子操作。

锁在 signal 的 __init__ 方法中定义的,采用的是标准库中的 Lock

self.lock = threading.Lock()

signal 用线程锁将清理 receiver 列表中的弱引用对象、 receiver 列表中增加元素、清理全局缓存字典这三个操作封装成了原子操作,如下:

with self.lock:
    self._clear_dead_receivers()
    for r_key, _ in self.receivers:
        if r_key == lookup_key:
            break
    else:
        self.receivers.append((lookup_key, receiver))
    self.sender_receivers_cache.clear()

sender

准确的讲,signal 中的 sender 这是一个标识,用来记录是“谁”触发了 signal ,真正起作用的是 send 方法,这个方法就是在 event 中用来触发 signal 给所有 receiver “发送消息”的。以下是 send 的源代码。

class Signal:

    ...
    
    def send(self, sender, **named):
        if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
            return []

        return [
            (receiver, receiver(signal=self, sender=sender, **named))
            for receiver in self._live_receivers(sender)
        ]

不难看出,触发所有记录在案的回调函数,这个过程是同步的,所以 signal 不适合用来处理大批量任务,当然我们可以将其改写成异步任务。

signal 的使用方法

signal 的使用只需要配置两个地方,一个是回调函数的注册,一个是事件触发。

回调函数的注册有两种方式,一种是常规的 signal.connect() ;另外是 Django signal 提供了装饰器 receiver ,只需要传入是哪个 signal 即可完成装饰,也可以指定 sender ,如果不指定就接收所有的 sender 发送的信息。事件触发只需在可触发的地方调用 <your_signal>.send() 即可。下面给出一个 demo。

from django.dispatch import Signal, receiver

signal = Signal()


@receiver(signal, sender="main")
def my_receiver(sender, **kwargs):
    print("here is my receiver.")
    print("hello sender: {}".format(sender))


if __name__ == "__main__":
    print("begin...")
    signal.send(sender="main")

输出:

begin...
here is my receiver.
hello sender: main

Django 内置的 signal

Django 中内置了很多 个 signal ,方便我们直接使用。

模型相关

pre_init = ModelSignal(providing_args=["instance", "args", "kwargs"], use_caching=True)  # 对象初始化前
post_init = ModelSignal(providing_args=["instance"], use_caching=True)  #对象初始化后

pre_save = ModelSignal(providing_args=["instance", "raw", "using", "update_fields"],
                       use_caching=True)  # 对象保存修改前
post_save = ModelSignal(providing_args=["instance", "raw", "created", "using", "update_fields"], use_caching=True)  #对象保存修改后

pre_delete = ModelSignal(providing_args=["instance", "using"], use_caching=True)  #对象删除前
post_delete = ModelSignal(providing_args=["instance", "using"], use_caching=True)  #对象删除后

m2m_changed = ModelSignal(
    providing_args=["action", "instance", "reverse", "model", "pk_set", "using"],
    use_caching=True,
)  #ManyToManyField 字段更新后触发

pre_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using", "apps", "plan"])  #数据迁移前
post_migrate = Signal(providing_args=["app_config", "verbosity", "interactive", "using", "apps", "plan"])  #数据迁移后

请求相关

request_started = Signal(providing_args=["environ"])  #request 请求前
request_finished = Signal()  #request 请求后
got_request_exception = Signal(providing_args=["request"])  #request 请求出错后
setting_changed = Signal(providing_args=["setting", "value", "enter"])  #request 请求某些设置被修改后
2018/7/30 posted in  Django