Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Kubernetes学习笔记

11
Jun
2016

Kubernetes学习笔记

By Alex
/ in PaaS
/ tags Docker, K8S
0 Comments
简介
什么是K8S

Kubernates(发音 / kubə'neitis /,简称K8S)是一个容器编排工具,使用它可以自动化容器的部署、扩容、管理。使用K8S可以将应用程序封装为容易管理、发现的逻辑单元。使用K8S你可以打造完全以容器为中心的开发环境。

K8S的特性包括:

  1. 自动装箱:根据容器的资源需求和其他约束条件,自动部署容器到合适的位置,与此同时,不影响可用性。K8S可以混合管理关键负载、非关键负载并尽可能的有效利用资源
  2. 自我修复:当容器宕掉后自动重启它,当节点宕掉后重新调度容器。关闭不能正确响应自定义健康检查的容器,在容器准备好提供服务之前,不将他们暴露给客户端
  3. 水平扩容:手工(UI、命令行)或自动(根据CPU负载)进行自动的扩容/缩容
  4. 服务发现/负载均衡:不需要修改应用程序来使用第三方的服务发现机制,K8S为容器提供专有IP,同时为一组容器(类似Swarm中的Service)提供单一的DNS名称,应用程序可以基于DNS名称发现服务。K8S还内置的负载均衡支持
  5. 无缝滚动更新/回滚:支持逐步的将更新应用到程序或配置,与此同时监控程序的健康状况,避免同时杀死程序的所有实例。如果出现问题,K8S能够自动回滚更新
  6. 密码和配置管理:部署、更新应用程序的密码、配置信息时,不需要重新构建镜像,不需要在配置信息中暴露密码信息
  7. 存储编排:自动从本地磁盘、公有云、网络存储系统挂载存储系统
  8. 资源监控、访问并处理日志、调试应用、提供认证和授权功能……

K8S提供了PaaS的简单性、IaaS的灵活性,支持在各基础设施提供商之间迁移。

尽管K8S提供了部署、扩容、负载均衡、日志、监控等服务,但是它并不是传统的PaaS平台:

  1. 它不限制能支持的应用程序类型,不限制编程语言、SDK。只要应用能在容器中运行,就可以在K8S下运行
  2.  不内置中间件(例如消息总线)、数据处理框架(例如Spark)、数据库、或者存储系统
  3. 不提供服务市场来下载服务
  4. 允许用户选择日志、监控、报警系统

同时,很多PaaS平台可以运行在K8S之上,例如OpenShift、Deis,你可以在K8S上部署自己的PaaS平台,集成自己喜欢的CI环境,或者仅仅是部署容器镜像。

什么是云原生

以Kubernetes为核心的技术生态圈,已经成为构建云原生架构的基石。

云原生架构没有权威的定义,但是基于这种架构的应用具有一系列的模式:

  1. 代码库:每个可部署的应用程序,都有独立的代码库,可以在不同环境部署多个实例
  2. 明确的依赖:应用程序的依赖应该基于适当的工具(例如Maven、Bazel)明确的声明,不对部署环境有任何依赖
  3. 配置注入:和发布环境(dev/stage/pdt)变化的配置信息,应该以操作系统级的环境变量注入
  4. 后端服务:数据库、消息代理应视为附加资源,并在所有环境中同等看待
  5. 编译、发布、运行:构建一个可部署的应用程序并将它与配置绑定,根据这个组件/配置的组合来启动一个或者多个进程,这两个阶段是严格分离的
  6. 进程:应用程序运行一个或多个无状态进程,不共享任何东西 —— 任何状态存储于后端服务
  7. 端口绑定:应用程序是独立的,并通过端口绑定(包括HTTP)导出任何服务
  8. 并发:并发通常通过水平扩展应用程序进程来实现
  9. 可任意处置:通过迅速启动和优雅的终止进程,可以最大程度上的实现鲁棒性。这些方面允许快速弹性缩放、部署更改和从崩溃中恢复
  10. 开发/生产平等:通过保持开发、预发布和生产环境尽可能的一致来实现持续交付和部署
  11. 日志:不管理日志文件,将日志视为事件流,允许执行环境通过集中式服务收集、聚合、索引和分析事件
  12. 管理任务:管理性的任务,例如数据库迁移,应该在应用程序运行的那个环境下,一次性的执行

关键字:环境解耦、无状态、微服务。

微服务架构选型
K8S VS Spring Cloud

两者之间存在高度的功能重叠,例如服务发现、负载均衡。

K8S VS Swarm

Swarm的功能比K8S要弱的多,可以实现简单的负载均衡、HA等特性,小规模部署可以使用。

架构图
整体架构

k8s-arch-1

Master架构

k8s-arch-2

Node架构

kube-arch-3

 

网络架构

K8S网络包括CNI、Service网络、Ingress、DNS这几个方面的内容:

  1. CNI负责Pod到Pod的网络连接
  2. Service网络负责Pod到Service的连接
  3. Ingress负责外部到集群的访问
  4. DNS负责解析集群内部域名

取决于具体实现,节点可能可以直接访问CNI、Service的IP地址。

Pod到集群外部的访问,基于SNAT实现。

Pod在节点内部的连接,经典方案是veth pair + bridge,也就是说多个Pod会连接到同一个网桥上,实现互联。

Pod在节点之间的连接,经典方案是bridge、overlay,Calico等插件则基于虚拟路由。

Kubernetes容器网络由Kubenet或CNI插件负责,前者未来会被废弃。

Kubenet

Kubenet是K8S早期的原生网络驱动,提供简单的单机容器网络。需要用Kubelet参数 --network-plugin=kubenet启用。

Kubenet本身不实现跨节点网络连接、网络策略,它通常和云提供商一起使用来实现路由规则配置、跨主机通信。Kubenet也用到一些CNI的功能,例如它基于CNI bridge插件创建名为cbr0的Linux Bridge,为每个容器创建一对veth pair并连接到cbr0网桥。

Kubenet在CNI插件的基础上拓展了很多功能:

  1. 基于host-local IP地址管理插件,为Pod分配IP地址并定期释放分配而未使用的IP
  2. 设置sysctl的net.bridge.bridge-nf-call-iptables=1
  3. 为Pod的IP设置SNAT(MASQUERADE),以允许Pod 访问外部网络
  4. 开启Linux Bridge的hairpin、混杂模式,允许Pod访问自己所在的Service IP
  5. HostPort管理、设置端口映射
  6. 带宽控制。可以通过kubernetes.io/ingress-bandwith、kubernetes.io/egress-bandwith来配置Pod网络带宽限制

Kubenet会使用bridge、lo、host-local等几个CNI插件,默认在/opt/cni/bin中搜索这些插件的二进制文件。你可以通过--network-plugin-dir参数定制搜索位置。此外Kubenet来回去/etc/cni/net.d中搜索CNI配置文件。

支持通过Kubelet参数--network-plugin-mtu定制MTU,支持限制带宽。这两个特性是Kubenet不能被CNI完全代替的原因。

CNI

CNI是容器网络的标准,试图通过一段JSON来描述容器网络配置。CNI是K8S和底层网络插件之间的抽象层。

CNI包含以下接口:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type CNI interface {
    AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
    CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
    DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
    GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
    GetNetworkListCachedConfig(net *NetworkConfigList, rt *RuntimeConf) ([]byte, *RuntimeConf, error)
 
    AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
    CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
    DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
    GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
    GetNetworkCachedConfig(net *NetworkConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error)
 
    ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error)
    ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error)
}
 
type RuntimeConf struct {
    ContainerID string
    NetNS       string
    IfName      string
    Args        [][2]string
    // A dictionary of capability-specific data passed by the runtime
    // to plugins as top-level keys in the 'runtimeConfig' dictionary
    // of the plugin's stdin data.  libcni will ensure that only keys
    // in this map which match the capabilities of the plugin are passed
    // to the plugin
    CapabilityArgs map[string]interface{}
 
    // DEPRECATED. Will be removed in a future release.
    CacheDir string
}
 
type NetworkConfig struct {
    Network *types.NetConf
    Bytes   []byte
}

AddNetwork负责在创建容器时,进行网络接口的配置;DelNetwork则在删除容器时,清理掉网络接口。参数NetworkConfig是网络配置信息,RuntimeConf则是容器运行时传入的网络命名空间信息。

CNI配置编写在如下形式的JSON文件中:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  name: "mynet",
  // 使用bridge CNI插件
  type: "bridge",
  bridge: "cni0",
  ipam: {
    // 基于host-local插件进行IP地址管理
    type: "host-local",
    subnet: "10.0.0.0/16",
    routes: [
      { "dst": "0.0.0.0/0" }
    ]
  }
}

上述配置文件,默认需要存在/etc/cni/net.d目录下,并且命名为*.conf,对应的插件二进制文件默认需要存放在/opt/cni/bin下。 

在K8S中启用基于CNI的网络插件,需要配置Kubelet参数 --network-plugin=cni。CNI配置文件位置通过参数 --cni-conf-dir配置,如果目录中存在多个配置文件则仅仅取第一个。CNI插件二进制文件位置通过 --cni-bin-dir配置。

K8S对象

K8S对象是指运行在K8S系统中的持久化实体,K8S使用这些实体来表示你的应用集群的状态。例如:

  1. 哪些容器化应用程序在执行,在何处(Node)执行
  2. 上述应用程序可用的资源情况
  3. 和应用程序行为有关的策略,例如重启策略、升级策略、容错策略

通过创建一系列对象,你可以告诉K8S,你的集群的负载是什么样的,所谓集群的期望状态(desired state)。

要创建/修改/删除K8S对象,可以调用Kubernetes API。提供命令行kubectl可以间接的调用此API,你也可以在程序中调用这些API,目前支持Go语言。

K8S对象包括:

  1. Pod、Service、Volume、Namespace等基本对象
  2. ReplicaSet、Deployment、StatefulSet、DaemonSet、Job等高级对象,这些高级对象在上述基本对象之上构建,并且提供了额外功能、便利化功能

本章不会一一解释这些不同类型的对象。

规格和状态

任何K8S对象都有两个内嵌的字段:规格、状态。前者由你提供,声明对象期望状态(所谓对象的期望状态即集群的期望状态)。后者则表示某一时刻对象的实际状态,此状态由K8S更新,K8S控制平面会积极的管理对象的状态,使其尽可能满足规格。

例如,Kubernetes Deployment是描述运行在集群中的一个应用程序的K8S对象。你创建一个Deployment时,可能在规格中声明你需要3个应用程序的Replica。K8S会读取规格并启动3个应用实例,如果一段时间后宕掉1个实例,则K8S会检测到Spec和Status之间的不同,进而启动一个新的实例代替宕掉的那个。

对象规格

调用K8S API创建对象时,你需要提供一个JSON格式的规格说明,其中包含期望状态和一些基本信息(例如应用的名称)。

通过kubectl创建对象时,你需要提供一个YML文件,kubectl会自动将其转换为JSON格式。

下面是对象规格(YML)的一个示例:

nginx-deployment.yaml
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 如果K8S版本小于1.8则使用 apps/v1beta1
apiVersion: apps/v1beta2
 
# 对象类型
kind: Deployment
 
# 对象元数据,唯一的识别对象,字段包括name、UID、namespace
metadata:
  # 名字通常由客户端提供,每一个对象类型内部,名字不得重复。在资源URL中名字用于引用对象,例如/api/v1/pods/some-name
  # 名字应该仅仅包含小写字母、数字、-、.这几类字符
  name: nginx-deployment
  # UID由K8S自动生成,全局唯一
  # namespace 指定对象所属的名字空间
 
# 对象规格
spec:
  replicas: 3
  # 标签选择器
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
          # 端口名称,可以被Service引用
          name: web
          protocol: TCP
 
# 标签
labels: ...

基于上述规格创建对象的命令示例:

Shell
1
kubectl create -f https://blog.gmem.cc/nginx-deployment.yaml --record
名字空间

K8S支持在一个物理集群上,创建多个虚拟集群,这些虚拟集群称为名字空间。名字空间为资源名称限定了作用域,同一名字空间内部名字不能重复,但是跨越名字空间则不受限制。

可以考虑使用名字空间的场景:

  1. 当跨越多个团队/项目的人员共享一套集群时,可以考虑使用名字空间机制。如果使用集群的人员仅仅在数十个级别,不需要使用名字空间
  2. 希望利用K8S的资源配额机制,在名字空间之间划分集群资源
  3. 在未来版本的K8S中,同一名字空间中的对象将具有一致的默认范围控制策略

如果两个资源仅仅有些许的不同,例如应用的两个版本,则不需要利用名字空间隔离。考虑使用标签(Label)来在名字空间内部区分这些资源。

大部分K8S资源(Pod、Service、Replication Controller...)都位于名字空间中。但是名字空间本身(也是资源)则不位于任何名字空间中。事件(Event)则可能位于、也可能不位于名字空间中,取决于事件的源对象。

查看名字空间

执行下面的命令可以查看集群中现有的名字空间:

Shell
1
2
3
4
5
kubectl get namespaces
# NAME          STATUS    AGE
# default       Active    1d
# kube-system   Active    1d
# kube-public   Active    1d 

上述命令输出了3个名字空间:

名字空间 说明
default 默认名字空间,没有显式指定名字空间的对象位于此空间
kube-system 由K8S系统创建的对象
kube-public 自动创建、可以被任何用户(包括未进行身份验证的)访问的名字空间。此名字空间基本上是保留供集群使用的,因为某些资源需要公开的、跨越整个集群的访问
指定名字空间

要在单个命令请求中,指定名字空间,可以:

Shell
1
2
kubectl --namespace=ns-dev run nginx --image=nginx
kubectl --namespace=ns-dev get pods

要永久的为某个上下文切换默认名字空间,可以:

Shell
1
2
kubectl config set-context $(kubectl config current-context) --namespace=ns-pdt
kubectl config view | grep namespace   # 验证设置
名字空间和DNS 

当你创建一个服务时,相应的DNS条目自动创建。此条目的默认格式是: ..svc.cluster.local。如果容器仅仅使用来引用服务,则解析到当前名字空间。这种特性可以方便的在多个名字空间(开发、测试、生产)中使用完全相同的配置信息。如果需要跨名字空间引用服务,则必须使用FQDN。

标签和选择器

所谓标签(Label)就是用户自定义的、关联到对象的键值对,这些标签和K8S的核心系统没有意义。你可以使用标签来管理、选择对象的子集。

你可以在创建对象的时候设置标签,也可以后续添加、修改标签。对于单个对象来说,标签的键必须是唯一的。

K8S会对标签进行索引/反向索引,以便对标签进行高效的查询、监控,在UI、CLI中对输出进行分组、排序。

动机

标签提供一种松散耦合的风格,让用户自由的映射他们的组织结构到K8S对象,而不需要客户端记住这些映射关系。

服务部署、批处理流水线,常常都是多维的实体(多分区/部署、多个发行条线、多层、每层多个微服务)。对这些实体进行管理,常需要横切性的操作——打破严格的层次性封装,这些层次可能由基础设施死板的规定。

样例标签集:

  1. 区分发行条线:{ "release" : "stable", "release" : "canary" }
  2. 区分运行环境:{ "environment" : "dev", "environment" : "qa", "environment" : "production" }
  3. 区分架构层次:{ "tier" : "frontend", "tier" : "backend", "tier" : "cache" }
  4. 区分分区: { "partition" : "customerA", "partition" : "customerB" }
  5. 区分Track: { "track" : "daily", "track" : "weekly" }
标签格式

标签的键,可以由两段组成:可选的前缀和名称,它们用 / 分隔:

  1. 名称部分必须63-字符,支持大小写字母、数字、下划线、短横线、点号
  2. 前缀如果存在,则必须是DNS子域名 —— 一系列以点号分隔的DNS标签,最后加上一个  / 
  3. 如果前缀被省略,则暗示标签是用户私有的
  4. 前缀 kubernetes.io/ 为K8S核心组件保留
标签选择器

一系列对象常常具有相同的标签。利用标签选择器,客户端可以识别一组对象。标签选择器是K8S提供的核心分组原语。

目前K8S API提供两种标签选择器:

  1. equality-based。操作符=、==(和=同义)、!=。示例:environment = production,tier != frontend
  2. set-based。操作符in、notin、exists。示例:environment in (production, qa),tier notin (frontend, backend)

如果有多个选择器需要匹配,则使用逗号(作用类似于&&)分隔。

NULL选择器匹配空集,空白选择器则匹配集合中所有对象。

标签相关API

LIST、WATCH之类的操作可以指定标签选择器,以过滤对象。

基于CLI的例子:

Shell
1
2
3
kubectl get pods -l environment=production,tier=frontend
kubectl get pods -l 'environment in (production),tier in (frontend)'
kubectl get pods -l 'environment,environment notin (frontend)'

某些K8S对象(Service、复制控制器)使用标签选择器指定关联的对象(例如Pod)。例如,作为服务目标的Pods是通过标签选择器指定的。replicationcontroller需要管理的pod生产,也是通过标签指定的。对象配置片断示例:

YAML
1
2
selector:
    component: redis

ReplicaSet、Deployment、DaemonSet、Job等对象,支持set-based风格的标签选择器,来指定其需求:

YAML
1
2
3
4
5
6
selector:
  matchLabels:
    component: redis
  matchExpressions:
    - {key: tier, operator: In, values: [cache]}
    - {key: environment, operator: NotIn, values: [dev]}
注解 

除了标签以外,你还可以使用注解(Annotations)来为K8S对象附加非识别性(non-identifying)元数据。客户端可以通过API获得这些元数据。

注意注解和标签不一样,后者可以用来识别、选择对象,前者则不能。此外注解的值大小没有限制。

注解也是键值对形式:

JavaScript
1
2
3
4
"annotations": {
  "key1" : "value1",
  "key2" : "value2"
}
节点

节点是K8S中的Worker机器,之前被叫做minion。节点可以是物理机器,也可以是VM。节点上运行着一些服务,运行Pod需要这些服务,这些服务被Master组件管理。运行在节点上的服务包括Docker、kubelet、kube-proxy等。

节点应该是x86_64(amd64)架构的Linux系统。其它架构或系统有可能支持K8S。

节点状态

以下几类信息用于描述节点的状态:

字段类别 说明
Addresses

这些字段的用法取决于你使用的云服务,或者裸金属的配置:

HostName:由节点的内核报告,可以由kubelet的--hostname-override参数覆盖
ExternalIP:可以被集群外部访问的节点IP
InternalIP:仅仅能在集群内部访问的节点IP

Condition

这些字段描述运行中节点的状态信息:

OutOfDisk:如果为True则意味着节点上没有足够空间供新Pod使用
Ready:如果为True则意味着节点状态健康,准备好接受Pods。如果为False则意味着节点不监控并且不接受Pods。如果为Unknown则意味着节点控制器已经40s没有听到节点的心跳了
MemoryPressure:如果为True则节点内存方面存在压力
DiskPressure:如果True则节点磁盘方面存在压力,也就是说磁盘空间不足
NetworkUnavailable:如果为True则意味着节点的网络配置不正确

节点的状态以JSON形式表示,例如:

JavaScript
1
2
3
4
5
6
"conditions": [
  {
    "kind": "Ready",
    "status": "True"
  }
]

如果节点的Ready状态为False/Unknown,并且持续时间大于pod-eviction-timeout(默认5分钟),则kube-controller-manager会接收到一个argument,该节点上所有Pods将被节点控制器调度以删除 

某些情况下节点不可达,APIServer无法与其上运行的kubelet通信。这样Pods删除信息无法传递到被分区(partitioned,网络被隔离的)节点,因而知道网络通信恢复前,其上的Pods会继续运行

在1.5-版本中,节点控制器会强制的从APIServer中删除不可达Pods,1.5+版本后只有在确认这些Pods已经从集群中停止运行后才进行删除。这些运行在不可达节点上的Pod的状态可能为Terminating或者Unknown

某些情况下,K8S无法在基础设施层推断节点是否永久的离开了集群,管理员可能需要手工进行节点移除

移除节点将导致其上的所有Pod对象从APIServer上删除

 

从1.8开始K8S引入了一个Alpha特性,可以自动创建代表Condition的taints。要启用此特性,为APIServer、控制器管理器、调度器提供参数:

--feature-gates=...,TaintNodesByCondition=true

当TaintNodesByCondition启用后,调度器会忽略节点的Conditions,代之以查看节点的taints、Pod的toleration。你可以选择旧有的调度模型,或者使用新的更加灵活的调度模型:

  1. 没有tolerations(容忍)的Pod基于旧模型调度
  2. 能够容忍特定节点taints(污染)的Pod,则可以在污染节点上调度
Capacity 描述节点上可用的资源,包括CPU、内存、最多可以在其上调度的Pod数量 
Info 一般性信息,例如内核版本、K8S版本(kubelet/kube-proxy)、Docker版本
节点管理

与Node/Service不同,节点不是K8S创建的,它常常由外部云服务(例如Google Compute Engine)创建,或者存在于你的物理机/虚拟机池子中。K8S中创建一个节点,仅仅是创建代表此节点的对象。在创建节点对象后,K8S会验证其有效性,例如下面的节点配置信息:

JavaScript
1
2
3
4
5
6
7
8
9
10
{
  "kind": "Node",
  "apiVersion": "v1",
  "metadata": {
    "name": "10.240.79.157",
    "labels": {
      "name": "my-first-k8s-node"
    }
  }
}

执行创建后,K8S会基于metadata.name字段来检查节点的健康状态。如果节点是有效的(所有必须的服务在其上运行)则它有资格运行Pod,否则它会被任何集群活动排除在外,知道它变为有效状态。 

除非显式删除,K8S会一致维护你创建的节点对象,并且周期性的检查其健康状态。

目前和节点接口交互的组件有三个:节点控制器、Kubelet、Kubectl。

节点控制器

节点控制器属于K8S管理组件,可以管理节点的方方面面:

  1. 当节点注册时,给节点分配一个CIDR块(如果CIDR分配打开)
  2. 在内部维护一个最新的节点列表。在云环境下,节点控制器会调用云提供商的接口,判断不健康节点的VM是否仍然可用,如果答案是否则则节点控制器从子集的节点列表中删除不健康节点
  3. 监控节点的健康状态。当节点不可达(节点由于某种原因不再响应心跳,例如宕机,默认40s)时,更新NodeStatus的NodeReady状态为ConditionUnknown。如果节点持续处于不可达状态(默认5m),则优雅的清除(Terminate)节点上所有Pods。控制器每隔--node-monitor-period秒检查节点状态
  4. 从1.6开始,节点控制器还负责清除运行在具有NoExecute taints节点上的Pods(如果Pod不容忍NoExecutes)

从1.4开始,K8S更新了节点控制器的逻辑,当Master节点本身网络出现问题导致大量节点不可达的场景被优化处理。在决定清除一个Pods时,节点控制器会查看集群中所有节点的状态。

大部分情况下,节点控制器限制了节点清除的速度。参数--node-eviction-rate默认为0.1,也就是每10秒最多从单个节点清除一个Pod。

当给定可用性区域(availability zone,集群可能跨越云服务的多个可用性区域,例如北美、亚太)中节点变得不健康时,节点清除行为会有改变。 节点控制器会计算可用性区域中不健康(NodeReady=ConditionUnknown or ConditionFalse)节点的百分比:

  1. 如果此比值不小于--unhealthy-zone-threshold(默认0.55)则清除速率降低。降低到--secondary-node-eviction-rate(默认0.01)
  2. 如果集群规模较小(小于--large-cluster-size-threshold,默认50)则清除行为停止

之所以按可用性区域来决定上述行为,是因为某些区域可能和Master之间形成网络分区,另外一些这和Master之间保持连通。

跨越可用性区域分布节点的关键原因是,当整个区域不可用时,工作负载可以迁移到健康的区域中。

节点自动注册

当为Kubelet提供参数--register-node=true(默认)时,Kubelet会尝试自动的到API服务器上注册自己。自动注册相关的参数:

参数 说明
--kubeconfig 用于包含自己的身份验证信息的文件路径
--cloud-provider 指定云服务提供商信息,用于读取关于节点本身的元数据
--register-node 自动注册自己
--register-with-taints 使用指定的taints注册自己,多个taints用逗号分隔:=:,=:
--node-ip 节点的IP地址
--node-labels 注册时附加到节点对象的标签集
--node-status-update-frequency 节点向Master提交自身状态的间隔

当前,Kubelet有权创建/修改任何节点资源,但是通常它仅应该修改自己的。

手工节点管理

作为集群管理员,你可以随时创建、修改节点对象。你可以设置kubelet标记--register-node=false,仅仅允许手工的管理。

你可以管理节点的资源,例如设置标签、标记其为unschedulable:

Shell
1
2
# 设置节点为unschedulable
kubectl cordon $NODENAME

标记节点为unschedulable可以禁止新的Pods被分配到节点上,但是对节点上现有的Pod则没有影响。

注意DaemonSet控制器创建的Pod跳过了K8S的调度器,因而unschedulable标记为其无意义。

节点容量

节点的容量(Capacity,CPU数量、内存量)属于节点对象属性的一部分。通常节点自我注册时会提供容量信息。如果你手工的注册节点,则必须提供容量信息。

K8S调度器会确保节点拥有足够的资源来运行分配给它的所有Pods,它会检查请求在节点上启动的容器(仅限kubelet启动的)所需总资源不大于节点的容量。

你可以显式的为非Pod进程保留节点资源:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 作为占位符的Pod
apiVersion: v1
kind: Pod
metadata:
  name: resource-reserver
spec:
  containers:
  - name: sleep-forever
    image: gcr.io/google_containers/pause:0.8.0
    resources:
      # 请求保留的资源数量
      requests:
        cpu: 100m
        memory: 100Mi
节点通信

本节讨论Master(准确的说是APIServer)和K8S集群之间的通信。 

集群 → Master

集群到Master的通信路径,总是在APIServer处终结,因为其它的Master组件均不暴露远程接口。

在典型配置上,API服务器基于HTTPS协议监听443端口,并且启用1-N种客户端验证、授权机制。

节点应该被预先分配集群的根证书,以便能够使用有效的客户端证书链接到APIServer。

利用服务账户(Service Account),Pod可以和APIServer安全的通信,根据服务账户的配置,K8S会自动把根证书、不记名令牌(bearer token )注入到Pod中。所有名字空间中的K8S服务都配备了一个虚拟IP,通过kube-proxy重定向到APIServer上的HTTPS端点。

Master组件和APIServer基于非安全端口通信,此端口通常仅仅暴露在Master机器的localhost接口上。

Master → 集群

从Master(APIServer)到集群的通信路径主要包括两条:

  1. 从APIServer到运行在各节点上的kubelet进程。这些通信路径用于:
    1. 抓取Pod的日志
    2. Attach到(利用kubectl)到运行中的Pod
    3. 提供Kubelet的端口转发功能
  2. 从APIServer到任何节点/Pod/Service,基于APIServer的代理功能

对于第1类通信路径,APIServer默认不会校验kubelet的服务器端证书,因而存在中间人攻击的可能性。使用--kubelet-certificate-authority标记可以为APIServer提供一个根证书,用于验证kubelet的服务器证书。

对于第2类通信路径,默认使用HTTP连接,不验证身份或加密。

Master组件

这类组件构成了K8S集群的控制平面(Plane),负责集群的全局性管理,例如调度、检测/响应集群事件。

Master组件可以运行在集群的任何节点上,你可以创建多Master的集群以获得高可用性。

Master组件主要包括kube-apiserver、kube-controller-manager、kube-scheduler

kube-apiserver

暴露K8S的API,作为控制平面的前端。此服务器本身支持水平扩容。

etcd

一个分布式的键值存储,K8S集群的信息全部存放在其中。要注意做好etcd的备份。

kube-controller-manager

运行控制器。控制器是一系列执行集群常规任务的后台线程,控制器包括:

  1. 节点控制器:检测节点宕机并予以响应
  2. 复制控制器:为系统中每个复制控制器对象维护正确数量的Pod
  3. 端点控制器:产生端点对象
  4. 服务账号/令牌控制器:为新的名字空间创建默认账户、API访问令牌
cloud-controller-manager

运行和云服务提供商交互的控制器,1.6引入的试验特性。CCM和其它Master组件在一起运行,它也可以作为Addon启动(运行在K8S的上层)。

CCM的设计初衷是,让云服务商相关的代码和K8S核心解耦,相互独立演进。不使用CCM时的K8S集群架构如下:

pre-ccm-arch

上述架构中,云服务和Kubelet、APIServer、KCM进行交互,实现集成,交互复杂,不符合最少知识原则。

使用CCM后,K8S集群架构变为:

post-ccm-arch

新的架构中,CCM作为一个Facade,统一负责和云服务的交互。CCM分离了部分KCM的功能,在独立进程中运行它们,分离的原因是这些功能是依赖于具体云服务的:节点控制器、 卷控制器、路由控制器、服务控制器。在1.8版本中CCM运行前述控制器中的:

  1. 节点控制器:通过从云服务获取运行在集群中的节点信息,来初始化节点对象。添加云服务特定的Zone/Regin标签、云服务特定的节点实例细节,获取节点网络地址和主机名,在节点不可用时调用云服务确认节点是否被删除(如果是则级联删除节点对象)
  2. 路由控制器:在云服务中配置路由,确保不同节点中运行的容器能够相互通信,仅用于GCE集群
  3. 服务控制器:监听服务的创建/更新/删除事件。它会根据K8S中当前服务的状态来配置云服务的负载均衡器

CCM还运行一个PersistentVolumeLabels控制器,用于在PersistentVolumes(GCP/AWS提供)上设置Zone/Regin标签。卷控制器没有作为CCM的一部分是刻意的设计,主要原因是K8S已经在剥离云服务相关的卷逻辑上做了很多工作。

CCM还包含了云服务特定的Kubelet功能。在引入CCM之前,Kubelet负责利用云服务特定的信息(例如IP、Regin/Zone标签、节点实例类型)初始化节点,引入CCM之后这些职责被转移。在新架构中,Kubelet初始化节点时不知晓云服务特定的信息,但是它会给节点添加一个taint,从而将节点标记为不可调度的。直到CCM初始化了云服务特定的信息,taint才被移除,节点可以被调度。

CCM基于Go开发,暴露了一系列的接口(CloudProvider),允许任何云服务实现这些接口

CloudProvider

此接口定义在pkg/cloudprovider/cloud.go,包含的功能有:

  1. 管理第三层(TCP)负载均衡器
  2. 管理节点实例(云服务提供的)
  3. 管理网络路由

不是所有功能都需要实现,取决于K8S组件的标记如何设置。运行K8S也不一定需要此接口的实现,比如在裸金属上运行时。

kube-scheduler

监控新创建的、没有分配到Node的Pod,然后选择适当的Node供其运行。

addons

加载项是实现了集群特性的Pod和服务,这些Pod可以被Deployments、ReplicationControllers等管理。限定了名字空间的加载项在名字空间kube-system中创建。加载项管理器创建、维护加载项资源。常见加载项如下:

加载项 说明
DNS

所有集群都需要此加载项,以提供集群DNS服务。此加载项为K8S服务提供DNS记录

K8S启动的容器自动使用此DNS服务

Dashboard 此加载项是一个一般用途的Web客户端,用户可以基于此加载项来管理集群、管理或者针对K8S集群中运行的应用程序
CRM 容器资源监控(Container Resource Monitoring)在中心数据库中记录容器的度量信息,并提供查看这些信息的UI
CIL 集群级别日志(Cluster-level Logging)收集容器日志,集中保存,并提供搜索/查询接口
节点组件

这些组件可以运行在集群中的任何节点上,它们维护运行中的Pods、提供K8S运行时环境。主要包括kubelet、kube-proxy。

kubelet

主要的节点代理程序,监控分配到(通过APIServer或本地配置文件)当前节点的Pod,并且:

  1. 挂载Pod所需的卷
  2. 下载Pod的Secrets
  3. 通过Docker或rkt运行Pod的容器
  4. 周期性执行任何请求的容器存活探针
  5. 将Pod状态反馈到系统的其他部分
  6. 将节点状态反馈到系统的其它部分
kube-proxy

在每个节点上映射K8S的网络服务的代理。提供在宿主机上维护网络规则、执行连接转发,来实现K8S服务抽象。

最早的版本完全在用户空间实现,性能较差。从1.1开始,K8S实现了基于Iptable代理模式的kube-proxy,从1.8开始,添加基于IPVS实现的kube-proxy。

docker/rkt

用于运行容器。

supervisord

轻量级的监控系统,用于确保kubelet、docker持续运行(宕机重启之)。

fluentd

用于配合实现集群级别日志(CIL)。

容器

在Pod中引用Docker镜像之前,你必须将其Push到Registry中。

container对象的image属性可以包含Registry前缀和Tag,这个命名规则和Docker是一致的。

镜像更新

默认的镜像拉取策略是IfNotPresent,如果镜像已经存在于本地,则Kubelet不会重复抓取。如果希望总是取抓取镜像,可以使用以下三种方法之一:

  1. 设置imagePullPolicy=Always
  2. 使用镜像的:latest标签
  3. 启用AlwaysPullImages这一admission controller

当不指定镜像tag时,默认即使用:latest,因而会导致每次都抓取最新镜像。:latest是应该尽可能避免使用的。

使用私服

Docker将访问私服所需要的密钥信息存放在$HOME/.dockercfg或者$HOME/.docker/config.json文件中。如果在Kubelet的root的$home目录下存在这两个文件K8S会使用之。

可以使用如下Pod能验证私服能否正常访问:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
  name: private-image-test-1
spec:
  containers:
    - name: uses-private-image
      image: $PRIVATE_IMAGE_NAME
      imagePullPolicy: Always
      command: [ "echo", "SUCCESS" ]
 
# 创建Pod对象
kubectl create -f /tmp/private-image-test-1.yaml
# 查看运行结果, 期望输出SUCCESS
kubectl logs private-image-test-1

注意:

  1. 所有节点必须具有相同的.docker/config.json,否则可能出现Pod仅能在部分节点上运行的情况。例如,如果你使用节点自动扩容,则每个实例模板需要包含.docker/config.json文件或者挂载包含此文件的卷
  2. 只要私服的访问密钥被添加到config.json中,则任何Pod都有权访问这些私服中的镜像
容器环境

K8S的容器环境,为容器提供了很多重要的资源:

  1. 一个文件系统,由镜像 + 1-N个卷构成
  2. 关于容器本身的信息:
    1. 容器的hostname就是容器所在的Pod的name,此名称可以通过hostname命令/gethostname函数获得
    2. 容器所属的Pod name和namespace还可以通过downward API获得
    3. 用户在Pod定义中声明的环境变量,对于容器都是可用的
  3. 关于集群中其它对象的信息:
    1. 容器创建时所有服务的列表,可以通过环境变量得到。例如名为foo的服务对应环境变量如下:
      Shell
      1
      2
      3
      # 在容器中可以访问环境变量:
      FOO_SERVICE_HOST=
      FOO_SERVICE_PORT= 
    2. 服务拥有专享的IP地址,容器可以基于DNS名称访问它(如果DNS Addon启用的话)
生命周期钩子

利用容器生命周期钩子框架,你可以在Kubelet管理的容器启动、关闭时执行特定的代码。可用的钩子(事件)包括:PostStart、PreStop。

容器配置参数
command/args

如果:

  1. 既不提供command和args,则使用Docker镜像中定义的默认值
  2. 如果仅提供了command,则Docker镜像中的CMD/ENTRYPOINT被忽略
  3. 如果仅提供了args,则使用Docker镜像中的ENTRYPOINT + args
  4. 如果同时提供了command和args,则Docker镜像中的CMD/ENTRYPOINT被忽略 
Pod
简介

Pod(本义:豆荚,箱子)是K8S对象模型中,最小的部署、复制、扩容单元 —— K8S中单个应用程序实例。

Pod封装了以下内容:

  1. 应用程序容器
  2. 存储资源
  3. 唯一的网络IP
  4. 管理容器运行方式的选项

Pod最常用的容器运行时是Docker,尽管其它容器运行时也被支持。

Pod的两种使用方式:

  1. 运行单个容器:容器和Pod呈现一对一关系,这是最常见的用法。你可以将Pod看作是容器的简单包装器,K8S管理Pod而不直接管理容器
  2. 运行多个容器:Pod封装了由多个较小的、紧耦合的、共享资源的容器相互协作而组成的应用,以及和这些容器相关的存储资源

Pod本身只是一套环境,因此容器可以重启,Pod则没有这一概念。

每个Pod对应一个应用程序实例,如果你需要水平扩容,则需要使用多个Pod。这种水平扩容在K8S中一般叫做复制(Replication)。复制的Pod由控制器创建、管理。

如果Pod包含多个容器,则这些容器一般被调度、运行在单个节点上。这些容器可以共享资源、依赖,相互通信,并且协调如何关闭(例如谁先关闭)。容器之间共享的资源主要有:

  1. 网络:每个Pod具有唯一性的IP地址,Pod中的每个容器共享网络名字空间,包括IP地址和端口。Pod内的容器之间可以通过localhost + 端口相互通信。当与外部实体通信时,这些容器必须协调如何共享网络资源(例如端口)
  2. 存储:Pod可以指定一系列的共享存储卷,所有容器可以访问这些卷。如果Pod中部分容器重启,这些卷仍然保持可用
  3. 容器之间也可以基于IPC机制进行通信,例如SystemV信号量、POSIX共享内存

在基于Docker时,Pod中的容器是共享名字空间、共享卷的Docker容器。

Pod可以和其它物理机器、其它Pod进行网络通信。

Pod创建过程
  1. 用户向API Server发送创建Pod的请求
  2. K8S Scheduler选取一个节点,将Pod分配到节点上
  3. 节点上的Kubelet负责Pod的创建:
    1. 调用CNI实现(dockershim、containerd等)创建Pod内的容器
    2. 第一个创建的容器是pause,它会允许一个简单的程序,进行永久的阻塞。该容器的作用是维持命名空间,因为Linux的命名空间需要其中至少包含一个进程才能存活
    3. 其他容器会加入到pause容器所在的命名空间
    4. 初始化容器网络接口,由kubenet(即将废弃)获CNI插件负责。CNI会在pause容器内创建eth0接口并分配IP地址
    5. 容器和主机的网络协议栈,通过veth pair连接
pause容器

在Pod中,pause容器是所有其他容器的“父”容器,它:

  1. 是各容器共享的命名空间的基础
  2. 可以启用PID命名空间共享,为每个Pod提供init进程

pause容器的逻辑非常简单,它启动后就通过pause()系统调用暂停自己。当子进程变为孤儿进程(父进程提前出错退出)时,它会调用wait()防止僵尸进程的出现。

在1.8版本之前,默认启用PID命名空间共享,也就是说Pod的所有容器共享一个PID命名空间,之后的版本则默认禁用共享,可以配置 spec.shareProcessNamespace强制启用。不共享PID命名空间时,每个容器都有PID 1进程。

使用Pod

你很少需要直接创建单个Pod,甚至是单例(不复制)的Pod,这是因为Pod被设计为是短命的、可丢弃的实体。当Pod被(你直接、或控制器)创建时,它被调度到集群中的某个节点。直到进程退出,Pod会一直存在于节点上。如果缺少资源、节点失败,则Pod会被清除、Pod对象也被从APIServer中删除。

Pod本身没有自愈能力,如果Pod被调度到一个失败的节点,或者调度操作本身失败,则Pod会被删除。类似的,当资源不足或者节点维护时,Pod也会被删除。K8S使用控制器这一高层抽象来管理Pod实例。通常你都是通过控制器来间接使用Pod。

控制器

控制器能够创建、管理多个Pod,处理复制/回滚,并提供自愈(在集群级别)功能 —— 例如当节点失败时控制器会在其它节点上创建等价的Pod。一般来说,控制器使用你提供的Pod模板来创建Pod。

控制器主要有三类:

  1. Job:用于控制那些期望会终结的Pod,在批处理计算场景下用到。Job必须和restartPolicy为OnFailure/Never的Pod联用
  2. ReplicationController, ReplicaSet,Deployment:用于控制不期望终结的Pod,例如Web服务。这些控制器必须和restartPolicy=Always联用
  3. DaemonSet:用于某个机器上仅运行一个实例的Pod,用于提供机器特定的系统服务

这三类控制器都包含了对应的Pod模板。

Pod模板

Pod模板是包含在其它对象(复制控制器、Jobs、DaemonSets...)中的Pod的规格说明。控制器使用Pod模板来创建实际的Pod,模板示例如下:

YAML
1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container
    image: busybox
    command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']

Pod模板发生改变不会影响已经创建的Pod。 

Pod的终结

当用户请求删除Pod时,K8S首先发送TERM信号给每个容器的主进程,当超过时限(完整宽限期,Full Grace Period)后,则发送KILL信号,之后删除Pod对象。

如果在等待容器进程退出期间Kubelet、Docker守护进程重启,则重启后重新进行完整宽限期内的TERM,以及必要的KILL。

强制删除:这种删除操作会导致Pod对象立即从APIServer中移除,这样它的名字可以立即被重用。实际的Pod会在较短的宽限期内被清除。

Pod容器的特权模式

在容器规格的SecurityContext字段中指定privileged标记,则容器进入特权模式。如果容器需要操作网络栈、访问设备,则需要特权模式。

Pod生命周期

Pod的status字段是一个PodStatus对象,后者具有一个phase字段。该字段是Pod所处生命周期阶段的概要说明:

  1. Pending:Pod已经被K8S系统接受,单是一或多个容器镜像尚未创建。此时Pod尚未调度到节点上,或者镜像尚未下载完毕
  2. Running:Pod已经被调度到某个节点,且所有容器已被创建,至少有一个容器处于运行中/重启中/启动中
  3. Succeeded:Pod的所有容器被正常终结,且不会再重启
  4. Failed:Pod的所有容器被终结,且至少一个被异常终结——要么退出码非0要么被强杀
  5. Unknown:Pod状态未知,通常由于和目标节点的通信中断导致

PodStatus具有一个 PodCondition数组,数组的每个元素具有type、status字段。type可选值是PodScheduled、Ready、Initialized、Unschedulable,status字段的可选值是True、False、Unknown。

通常情况下,重复你或者控制器销毁Pod,它不会小时。唯一的例外是,phase值为Succeeded/Failed,并持续一定时间(具体由Master确定),则Pod会因为过期而自动销毁。

如果节点和集群断开,则K8S会把该节点上所有的Pod的phase更新为Failed

容器探针

Probe由Kubelet周期性的针对容器调用,以执行健康检查。Kubelet会调用容器实现的Handler,Handler包括三类:

  1. ExecAction:在容器内部执行特定的命令,如果命令为0则意味着诊断成功
  2. TCPSocketAction:针对容器的IP/端口进行TCP检查,如果端口打开则诊断成功
  3. HTTPGetAction:针对容器的IP/端口进行HTTP检查,如果响应码为[200,400)之间则诊断成功

每个探针的诊断结果可以是Success、Failure、Unknown

Kubelet可以在运行中的容器上执行两个可选探针,并作出反应:

  1. livenessProbe:探测容器是否还在运行。如果探测失败则杀死容器,并根据其重启策略决定如何反应。如果容器没有提供此探针,则总是返回Success
  2. readinessProbe:探测容器是否能提供服务。如果探测失败,端点控制器会从所有匹配Pod的服务的端点中移除Pod的IP地址。在Initial delay之前默认值Failure,如果容器没有提供此探针,则Initial delay之后默认Success。仅仅用于在Pod启动初期、延迟的把它加入到服务集群中,不能用于将Pod从服务集群中移除

注意,这些探针在Kubelet的网络名字空间中运行。

如果你的容器在出现问题时会自己崩溃,则不需要使用探针,只需要设置好restartPolicy即可。

livenessProbe

基于HTTP的探针示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-http
spec:
  containers:
  - args:
    - /server
    image: gcr.io/google_containers/liveness
    # 指定探针,在容器级别上指定
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080  # 也可以使用命名端口(ContainerPort)
        httpHeaders:
        - name: X-Custom-Header
          value: Awesome
      # 第一次探针延迟多久执行
      initialDelaySeconds: 15
      # 每隔多久执行一次探针
      periodSeconds: 5
      # 探针执行超时,默认1秒
      timeoutSeconds: 1
      # 被判定为失败后,连续多少次探测没问题,才被重新认为是成功
      successThreshold: 1
      # 连续多少次探测失败,则认为无法恢复,自动重启
      failureThreshold: 3
    name: liveness

基于命令的探针示例:

YAML
1
2
3
4
5
6
7
8
9
10
spec:
  containers:
  - name: runtime
    livenessProbe:
      exec:
        command:
        - cat
        - /app/liveness
      initialDelaySeconds: 5
      periodSeconds: 5

要注意:命令的标准输出被收集,并在kubectl describe pod命令中显示为Unhealthy事件的原因,因此探针的标准输出应该简洁明了。

readinessProbe

某些情况下,容器临时的不适合处理请求,例如其正在启动、正在加载大量数据。此时可以使用readiness探针。readiness探测失败的Pod不会接收到K8S Service转发来的请求。

readinessProbe的配置和livenessProbe没有区别。两个探针可以被同时执行。

重启策略

PodSpec包含一个restartPolicy字段,可选值为:

  1. Always,默认值,应用到Pod的所有容器
  2. OnFailure
  3. Never

重启时如果再次失败,重启延迟呈指数增长(10s,20s,40s)但是最大5分钟。启动成功后,10m后清除重启延迟。

初始化容器

在PodSpec的containers字段之后,你可以声明一个初始化容器。这类容器在应用容器之前运行,可能包含应用镜像中没有的实用工具、安装脚本。

初始化容器可以有多个,K8S会按照声明顺序逐个执行它们,只有前一个初始化容器成功完成,后面的初始化容器才会被调用。如果某个初始化容器运行失败,K8S会反复重启它,直到成功,除非你设置其restartPolicy=Never。

示例
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  # 普通容器
  containers:
  - name: myapp-container
    image: busybox
    command: ['sh', '-c', 'echo The app is running! && sleep 3600']
  # 初始化容器
  initContainers:
  - name: init-myservice
    image: busybox
    command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
  - name: init-mydb
    image: busybox
    command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 2; done;'] 
Pod预设

PodPreset用于在创建Pod时,向Pod注入额外的运行时需求信息。你可以使用标签选择器来指定预设要应用到的Pod。当Pod创建请求出现时,系统中发生以下事件:

  1. 取得所有可用的PodPreset
  2. 检查是否存在某个PodPreset,其标签选择器匹配准备创建的Pod
  3. 尝试合并PodPreset中的各种资源到准备创建的Pod中
    1. 如果出错,触发一个事件说明合并出错。在不注入任何资源的情况下创建Pod
    2. 如果成功,标注被修改的PodSpec为已被PodPreset修改 

要在集群中使用Pod预设功能,你需要:

  1. 确保API类型settings.k8s.io/v1alpha1/podpreset启用。可以设置APIServer的--runtime-config选项,包含settings.k8s.io/v1alpha1=true
  2. 确保PodPreset这一Admission controller已经启用。可以设置APIServer的--admission-control选项,包含PodPreset
  3. 在你需要使用的名字空间中,定义一个PodPreset对象

如果你在使用Kubeadm,则修改配置文件/etc/kubernetes/manifests/kube-apiserver.yaml即可。

示例
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
  name: allow-database
spec:
  selector:
    matchLabels:
      role: frontend
  # 支持的Pod配置项是有限的,仅仅支持:
  # 预设容器的环境变量
  env:
    - name: DB_PORT
      value: "6379"
    - name: expansion
      value: $(REPLACE_ME)
  # 预设容器的环境变量,从ConfigMap读取变量
  envFrom:
    - configMapRef:
        name: etcd-env-config
  # 预设容器的卷挂载
  volumeMounts:
    - mountPath: /cache
      name: cache-volume
    - mountPath: /etc/app/config.json
      readOnly: true
      name: secret-volume
  # 预设Pod的卷定义
  volumes:
    - name: cache-volume
      emptyDir: {}
    - name: secret-volume
      secret:
         secretName: config-details
高可用

要构建基于K8S的高可用应用,就要明白Pod什么时候会被中断(Disrpution)。

Pod不会凭空消失,除非你或控制器销毁了它们,或者出现不可避免的软硬件错误。这些不可避免的错误被称为非自愿中断(Involuntary Disruptions),例如:

  1. 作为节点支撑的物理机器,出现硬件错误
  2. 集群管理员错误的删除了VM
  3. 云服务或者Hypervisor的错误导致VM消失
  4. 内核错误
  5. 网络分区导致节点从集群分离
  6. 节点资源不足导致Pod被清除

其它情况则称为自愿中断,中断操作可能由应用程序所有者、集群管理员触发。应用程序所有者的操作包括:

  1. 删除部署或者其它管理Pod的控制器
  2. 更新部署的Pod模板导致重启
  3. 直接意外的删除了Pod

集群管理员的操作包括:

  1. Drain了某个节点,进行硬件维护或软件升级
  2. Drain了某个节点,进行缩容
  3. 从节点移除Pod,流下资源供其他人使用

缓和非自愿中断造成的影响,手段包括:

  1. 确保Pod请求了其需要的资源
  2. 如果需要HA,启用(无状态/有状态)应用程序复制
  3. 如果需要进一步HA,启用跨机柜复制(anti-affinity)甚至跨区域复制(multi-zone cluster)

自愿中断发生的频率根据集群用法的不同,有很大差异。对于某些基本的集群,根本不会出现自愿中断。

集群管理员/云服务提供商可能运行额外的服务,进而导致自愿中断。例如,滚动进行节点的软件更新就可能导致这种中断。某些节点自动扩容的实现,可能进行节点的取碎片化,从而导致自愿中断。

K8S提供了中断预算(Disruption Budgets)机制,帮助在频繁的自愿中断的情况下,实现HA。

优先级和抢占

这是从1.8引入的特性,目前处于Alpha状态。该特性允许Pod具有优先级,并且能够在无法调度Pod时,从节点驱除低优先级的Pod。

从1.9开始,优先级影响Pod的调度顺序、资源不足时Pod的驱逐顺序。高优先级的Pod更早被调度,低优先级的Pod更早被驱除。

启用特性

你需要为APIServer、scheduler、kubelet启用下面的特性开关:

Shell
1
--feature-gates=PodPriority=true,...

同时在APIServer中启用scheduling.k8s.io/v1alpha1这个API以及Priority这个准许控制器:

Shell
1
--runtime-config=scheduling.k8s.io/v1alpha1=true --admission-control=Controller-Foo,Controller-Bar,...,Priority
PriorityClass

这是一类非名字空间化的对象,定义了一个优先级类的名称,以及一个整数的优先级值。最大取值为10亿,更大的值保留。规格示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
apiVersion: scheduling.k8s.io/v1alpha1
kind: PriorityClass
metadata:
  # 名
  name: high-priority
# 值
value: 1000000
# 是否全局默认值,如果是,则没有定义PriorityClassName属性的Pod,其优先级均为此PriorityClass.value
globalDefault: false
# 供用户阅读的描述
description: "..."
抢占

当Pod被创建后,它会列队等待调度。调度器会选取队列中的一个Pod并尝试调度到某个节点上,如果没有节点能满足Pod的需求,则抢占逻辑被激活:

  1. 尝试寻找这样的节点:其上具有更低优先级的Pod,并且将这些Pod驱除后,能满足正被调度的Pod的需求
  2. 如果找到匹配的节点,则对其上的低优先级Pod执行驱除,并调度当前Pod到节点
共享卷

同一个Pod内的多个容器,可以通过共享卷(Shared Volume)进行交互。

卷天然是Pod内共享的,因为卷只能在Pod级别定义,而后任何一个容器都可以挂载它到自己的某个路径下:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kind: Pod
spec:
  volumes:
  - name: shared-data
    # Pod被调度到某个节点上后,创建一个空目录
    emptyDir: {}
  containers:
  - name: nginx-container
    image: nginx
    volumeMounts:
    - name: shared-data
      mountPath: /usr/share/nginx/html
 
  - name: debian-container
    image: debian
    volumeMounts:
    - name: shared-data
      mountPath: /pod-data
    command: ["/bin/sh"]
    # 修改共享卷的内容
    args: ["-c", "echo Hello from the debian container > /pod-data/index.html"] 

注意,K8S的共享卷和Docker的--volumes-from不同。

容器组合模式
Sidecar

跨斗,本指三轮摩托旁边的那个座位。

Sidecar用于辅助主要容器,让其更好的工作。例如Pod中的主容器是Nginx,它提供HTTP服务,Sidecar中运行一个Git,周期性的将最新的代码拉取过来,通过共享文件系统推送给Nginx。

Ambassador

使者模式,辅助容器作为一个代理服务器,主容器直接通过localhost(因为Pod内所有容器共享网络名字空间)访问外部的服务。

好处是,主容器可以和开发环境完全一致,因为在开发时常常所有东西都在localhost上。

Adaptor

不同容器输出的监控指标信息格式不一致,可以由一个配套的辅助容器对这些格式进行适配。

标签管理

建议统一规划、配置标签:

  1. tier,标识应用所属的层次,取值:
    1. control-plane:K8S控制平面
    2. infrastructure:提供网络、存储等基础设施
    3. devops:开发、运维相关的工具
    4. middleware:中间件、数据库等
    5. application:业务域应用程序
  2. app,标识应用程序的类别
  3. comp,标识应用程序组件
  4. version,标识应用程序的版本
  5. release,标识Helm Release的名称,或者手工部署的应用程序的实例名
hostNetwork

将此配置项设置为true,则Pod直接使用宿主机的网络。Pod直接在宿主机的网络接口上监听。

Pod配置
环境变量

将Pod的Spec/Status字段注入为环境变量:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
env:
  - name: NODE_NAME
    valueFrom:
      fieldRef:
        fieldPath: spec.nodeName
  - name: POD_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.name
  - name: POD_NAMESPACE
    valueFrom:
      fieldRef:
        fieldPath: metadata.namespace
  - name: POD_IP
    valueFrom:
      fieldRef:
        fieldPath: status.podIP
  - name: POD_SERVICE_ACCOUNT
    valueFrom:
      fieldRef:
        fieldPath: spec.serviceAccountName
分配Pod到节点

你可以指定Pod和Node的对应关系,让Pod只能(或优选)在某些节点上运行。具体实现方式有几种,都是基于标签选择器的。

大部分情况下你不需要强制指定对应关系,因为K8S会进行合理的调度。你需要这种细致的控制机制的场景包括:

  1. 确保Pod在具有SSD的机器上运行
  2. 让两个位于不同Service中的、频繁协作的Pod,能在同一个Zone内部运行
nodeSelector

这是Pod规格的一个字段,规定有资格运行Pod的节点,所具有的标签集。

你需要首先为节点添加标签:

Shell
1
2
3
4
5
6
7
# 获取集群的节点列表
kubectl get nodes
# 为某个节点添加标签
kubectl label nodes  disktype=ssd
 
# 显示节点标签
kubectl get nodes --show-labels

然后,需要为Pod规格添加一个nodeSelector字段: 

YAML
1
2
3
spec:
  nodeSelector:
    disktype: ssd

执行命令: kubectl get pods -o wide可以看到Pod被分配到的节点。 

内置节点标签

截止1.4,K8S内置的节点标签有:

节点标签
kubernetes.io/hostname
failure-domain.beta.kubernetes.io/zone
failure-domain.beta.kubernetes.io/region
beta.kubernetes.io/instance-type
beta.kubernetes.io/os
beta.kubernetes.io/arch
Affinity

Beta特性affinity,也可以用于将Pod分配到节点。但是这一特性更加强大:

  1. 表达能力更强,不是简单的限制于nodeSelector的那种  k == v && k == v
  2. 可以标注为“软规则”而非强制要求,如果调度器无法满足Pod的需求,Pod仍然会被调度
  3. 可以基于其它Pod上的标签进行约束,也就是说,可以让一系列的Pod同地协作( co-located)

Affinity分为两种类型:

  1. Node affinity:类似于nodeSelector,但是表达能力更强、可以启用软规则(对应上面的第1、2条)
  2. Inter-pod affinity:基于Pod标签而非Node标签进行约束(对应上面第3条)
Node affinity

包含两种子类型:requiredDuringSchedulingIgnoredDuringExecution(硬限制)、preferredDuringSchedulingIgnoredDuringExecution(软限制)。

“IgnoredDuringExecution“的含义是,如果在Pod调度到Node并运行后,Node的标签发生改变,则Pod会继续运行,nodeSelector的行为也是这样的。未来可能支持后缀“RequiredDuringExecution“,也就说当运行是Node的标签发生改变,导致不满足规则后,Pod会被驱除。

示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
spec:
  affinity:
    nodeAffinity:
      # 硬限制
      requiredDuringSchedulingIgnoredDuringExecution:
        # 加强版的节点选择器,值为数组。如果数组元素有多个,目标节点只需要匹配其中一个即可
        nodeSelectorTerms:
        # 每个节点选择器可以包含多个表达式,如果表达式有多个,目标节点必须匹配全部表达式
        - matchExpressions:
          # 必须在AZ(Availability Zone)e2e-az1或者e2e-az2中运行
          - key: kubernetes.io/e2e-az-name
            # 支持的操作符包括 In, NotIn, Exists, DoesNotExist, Gt, Lt
            # 其中 NotIn,DoesNotExist 用于实现anti-affinity
            operator: In
            values:
            - e2e-az1
            - e2e-az2
      # 软限制
      preferredDuringSchedulingIgnoredDuringExecution:
      # 权重,值越大越需要优先满足
      - weight: 1
        preference:
          # 节点选择器
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value
Inter-pod affinity

基于已经运行在节点上的Pod,而不是节点本身的标签进行匹配 —— 当前Pod应该/不应该运行在,已经运行了匹配规则R的Pod(s)的X上。其中:

  1. R表现为关联了一组名字空间的标签选择器
  2. X是一个拓扑域(Topology Domain),可以是Node、Rack、Zone、Region等等

和节点不同,Pod是限定名字空间的,因此它的标签也是有名字空间的。针对Pod的标签选择器必须声明其针对的名字空间。

Inter-pod affinity要求较大的计算量,因此可能拖累调度性能,不建议在大型集群(K+节点)上使用。

示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
spec:
  affinity:
    # Pod亲和性
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      # 如果Zone(由topologyKey指定)上已经运行了具有标签security=S1的Pod,则当前Pod也必须调度到该Zone
      - labelSelector:
          matchExpressions:
          - key: security
            # 支持的操作符In, NotIn, Exists, DoesNotExist
            operator: In
            values:
            - S1
        topologyKey: failure-domain.beta.kubernetes.io/zone
    # Pod反亲和性
    podAntiAffinity:
      # 强制
      requiredDuringSchedulingIgnoredDuringExecution:
      # 尽可能
      preferredDuringSchedulingIgnoredDuringExecution:
      # 如果Host(节点)上已经运行了具有标签security=S2的Pod,则当前Pod不得调度到该Host
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: kubernetes.io/hostname

当需要保证高级对象(ReplicaSets, Statefulsets, Deployments)在同一拓扑域内运行时, podAffinity非常有用。

下面是一个示例,3实例的Redis集群 + 3实例的Nginx前端,每个Redis不在同一节点运行,每个Nginx也不在同一节点运行,每个Nginx必须在本地有个Redis:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
kind: Deployment
metadata:
  name: redis-cache
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: store
    # Pod模板
    spec:
      affinity:
        # 当前节点上不得存在Pod的标签是app=store,也就是说,复制集的每个实例都占据单独的节点
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: redis-server
        image: redis:3.2-alpine

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
kind: Deployment
metadata:
  name: web-server
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: web-store
    spec:
      affinity:
        podAntiAffinity:
          # 当前节点上不得存在Pod具有标签app=web-store
          # 也就是说当前复制集的实例,都不会在同一节点上运行
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web-store
            topologyKey: "kubernetes.io/hostname"
        podAffinity:
          # 当前节点上的某个Pod必须具有app=store标签
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: web-app
        image: nginx:1.12-alpine
Taints/Tolerations 

前文讨论的Node affinity,可以把Node和Pod吸引到一起。Taints则做相反的事情,让Pod拒绝某些Node。

添加Taint

使用kubectl可以为节点添加一个Taint,例如:

Shell
1
2
3
4
5
6
# taint的键为key,值为value
# taint effect为 NoSchedule,意味着Pod不能被调度到该节点,除非它具有匹配的toleration
kubectl taint nodes node1 key=value:NoSchedule
 
# 恢复Master节点的禁止调度
kubectl taint nodes master node-role.kubernetes.io/master=:NoSchedule
删除Taint
Shell
1
2
# 删除Taint
kubectl taint nodes --all node-role.kubernetes.io/master-
容忍

下面的Pod都能容忍上述Taint:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
tolerations:
# 容忍 键、值、效果组合
- key: "node-role.kubernetes.io/master"
  # 操作符,不指定默认为Equal
  operator: "Equal"
  value: "value"
  effect: "NoSchedule"
 
# 容忍 键、效果组合
tolerations:
- key: "node-role.kubernetes.io/master"
  operator: "Exists"
  effect: "NoSchedule"
 
# 容忍一切
tolerations:
- operator: "Exists"
 
# 容忍键和一切效果
tolerations:
- key: "node-role.kubernetes.io/master"
  operator: "Exists"
 
# 容忍某种效果
tolerations:
- effect: NoSchedule
  operator: Exists
effect 
取值 说明
NoSchedule 不能容忍Taint的Pod绝不会调度到节点
PreferNoSchedule 不能容忍Taint的Pod尽量不被调度到节点
NoExecute

如果Pod不能容忍此Effect且正在节点上运行,它会被从节点上被驱除:

YAML
1
2
3
4
5
6
7
8
9
# Pod配置
tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExecute"
  # 如果所在节点添加了key1=value1:NoExecute,则1小时后Pod被驱除
  # 如果1小时内Taint被移除,则Pod不会被驱除
  tolerationSeconds: 3600
应用场景
场景 说明
专用节点

如果希望某些节点被特定Pod集专用,你可以为节点添加Taint、并为目标Pod添加匹配的Toleration。这样,目标Pod可以在专用节点上运行

如果要让目标Pod仅能在专用节点,可以配合nodeSelector

特殊硬件节点 集群中可能存在一小部分具有特殊硬件(例如GPU)的节点,Taint可以让不需要特殊硬件的Pod和这些节点隔离
定制化Pod驱除 可以在节点出现问题时,为每个Pod定制驱除策略,这是一个Alpha特性 
基于Taint的驱除

使用NoExecute可以导致已经运行在节点上的Pod被驱除,Pod配置tolerationSeconds可以指定驱除的Buffer时间。

从1.6开始,节点控制器会在节点状态变化时,自动添加Taint,包括:

Taint 说明
node.kubernetes.io/not-ready 节点没有准备好,对应NodeCondition.Ready=False
node.alpha.kubernetes.io/unreachable 节点控制器无法连接到节点,对应NodeCondition.Ready=Unknown
node.kubernetes.io/out-of-disk 节点磁盘空间不足
node.kubernetes.io/memory-pressure 节点面临内存压力
node.kubernetes.io/disk-pressure 节点面临磁盘压力
node.kubernetes.io/network-unavailable 节点的网络不可用
node.cloudprovider.kubernetes.io/uninitialized 当Kubelet通过“外部”云服务启动时,它会设置此Taint,当CCM初始化了节点后,移除此Taint
PDB

PDB(PodDisruptionBudget)可以用来构建高可用的应用程序。

自愿/非自愿中断

除非被人工或控制器删除,或者出现不可避免的软硬件错误,Pod不会消失。那些不可避免软硬件错误导致的Pod删除,称为非自愿中断(involuntary disruptions ),具体包括:

  1. 硬件故障
  2. 虚拟机故障,例如被误删除
  3. 内核崩溃
  4. 集群网络分区导致节点丢失
  5. 由于资源不足,Pod被kubelet驱除

要避免非自愿中断影响应用程序的可用性,可以考虑:

  1. 合理的配置资源请求
  2. 使用Deployment/StatefulSet

要避免资源中断影响应用程序的可用性,可以使用PDB。

PDB

PDB可以限制复制集中,同时由于自愿中断而宕掉的Pod的最大数量。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: zookeeper
spec:
  # 最小保证2实例处于运行中,可以指定百分比,例如 25%
  minAvailable: 2
  # 也可以指定最大宕机数量,可以指定百分比,例如 25%
  maxUnavailable:1
  # 控制的目标应用程序
  selector:
    matchLabels:
      app: zookeeper
ConfigMap

ConfigMap用于把配置信息和容器的镜像解耦,保证容器化应用程序的可移植性。

ConfigMap中文叫配置字典,下一章的Secrets中文叫保密字典,两者很类似,只是后者的内容是编码存储的。

创建ConfigMap

命令格式:

Shell
1
2
3
4
5
6
# map-name是ConfigMap的名称
# data-source 目录、文件,或者硬编码的字面值
kubectl create configmap  
# 在ConfigMap对象中,data-source表现为键值对:
# key:文件名,或者通过命令行提供的key
# value:文件内容,或者通过命令行提供的字面值
--from-file

下面是把目录作为数据源的例子:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kubectl create configmap game-config --from-file=path-to-dir
# ls path-to-dir
# game.properties
# ui.properties
 
kubectl describe configmaps game-config
# Name:           game-config
# Namespace:      default
# Labels:        
# Annotations:    
 
# Data
# ====
# game.properties:        158 bytes
# ui.properties:          83 bytes

目录中文件的内容,会读取到ConfigMap的data段中:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: v1
data:
  # 文件名为键,文件内容为值(字符串)
  # 如果要自定义键,可以 --from-file==
  game.properties: |
    enemies=aliens
    lives=3
    enemies.cheat=true
    enemies.cheat.level=noGoodRotten
    secret.code.passphrase=UUDDLRLRBABAS
    secret.code.allowed=true
    secret.code.lives=30
  ui.properties: |
    color.good=purple
    color.bad=yellow
    allow.textmode=true
    how.nice.to.look=fairlyNice
kind: ConfigMap
metadata:
  creationTimestamp: 2016-02-18T18:52:05Z
  name: game-config
  namespace: default
  resourceVersion: "516"
  selfLink: /api/v1/namespaces/default/configmaps/game-config
  uid: b4952dc3-d670-11e5-8cd0-68f728db1985

如果将文件作为数据源,可以指定--from-file多次,引用多个文件。

--from-env-file

使用该选项可以从Env-File创建ConfigMap,例如文件:

game-env-file.properties
1
2
3
enemies=aliens
lives=3
allowed="true"

可以创建ConfigMap:

Shell
1
kubectl create configmap game-config-env-file --from-env-file=game-env-file.properties

结果ConfigMap为:

YAML
1
2
3
4
5
6
7
apiVersion: v1
data:
  # 注意是一个个键值对,而不是字符串
  allowed: '"true"'
  enemies: aliens
  lives: "3"
kind: ConfigMap
--from-literal

使用该选项,直接通过命令行给出ConfigMap的数据:

Shell
1
2
kubectl create configmap special-config
    --from-literal=special.how=very --from-literal=special.type=charm
使用ConfigMap
定义环境变量

定义单个环境变量:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
  name: test
spec:
  containers:
    - name: test
      env:
        # 定义一个环境变量
        - name: SPECIAL_LEVEL_KEY
          valueFrom:
            configMapKeyRef:
              # 环境变量的值,从名为special-config的ConfigMap中获取,使用其中的special.how的值
              name: special-config
              key: special.how

你可以把ConfigMap中的所有键值对导出为Pod的环境变量:

YAML
1
2
3
4
5
6
spec:
  containers:
    - name: test-container
      envFrom:
      - configMapRef:
          name: special-config
在Pod命令中使用环境变量

使用特殊语法 $(VAR_NAME),例如:?

YAML
1
command: [ "/bin/sh", "-c", "echo $(SPECIAL_LEVEL_KEY) $(SPECIAL_TYPE_KEY)" ]
作为卷使用
YAML
1
2
3
4
5
6
7
8
9
10
spec:
  containers:
    - name: test-container
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: special-config

这样,Pod启动后,/etc/config目录下会出现若干文件。这些文件的名字是configMap的键,内容是键对应的值。

映射到卷的特定路径

你可以使用path属性指定某个ConfigMap键映射到的文件路径:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spec:
  containers:
    - volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: special-config
        items:
          # ConfigMap中条目的key
        - key: special.level
          # 映射到mountPath下的什么文件,默认为key
          path: special.lv

special.level的内容会映射到容器路径:/etc/config/special.lv

自动刷新

加载为卷的ConfigMap被修改后,K8S会自动的监测到,并更新Pod的卷的内容。延迟取决于Kubelet的同步周期。

加载为环境变量的ConfigMap修改后不会刷新到已经运行的Pod。

SubPath

挂载ConfigMap时,如果ConfigMap中包含多个配置文件,可以指定每个文件映射到容器的什么路径:

YAML
1
2
3
4
5
6
7
volumeMounts:
    # 卷名,必须是ConfigMap或者Secret卷
  - name: dangconf
    # 挂载到容器路径
    mountPath: /etc/dangdangconfig/digital/api.config
    # ConfigMap或Secret中的文件名
    subPath: api-config.properties

SubPath挂载的内容不会随ConfigMap变更,这可能是Kubernetes的Bug。

Secret

Secret是一种K8S对象,用于保存敏感信息,例如密码、OAuth令牌、SSH私钥。将这些信息存放在Secret(而不是Pod规格、Docker镜像)中更加安全、灵活。 

你可以在Pod的规格配置中引用Secret。

内置Secret 

K8S会自动生成一些Secret,其中包含访问API所需的凭证,并自动修改Pod以使用这些Secret。

创建Secret

你可以通过kubectl来创建Secret:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 创建一个名为db-user-pass的一般性Secret
kubectl create secret generic db-user-pass --from-file=./username.txt --from-file=./password.txt
 
# 创建用作Ingress TLS证书的Secret
kubectl create secret tls tls-secret --cert=/home/alex/Documents/puTTY/k8s.gmem.cc/tls.crt --key=/home/alex/Documents/puTTY/k8s.gmem.cc/tls.key
 
# 直接提供字面值
kubectl -n kube-system create secret generic chartmuseum --from-literal=BASIC_AUTH_USER=alex --from-literal=BASIC_AUTH_PASS=alex
 
kubectl get secrets
# NAME                  TYPE                                  DATA      AGE
# db-user-pass          Opaque                                2         51s
 
kubectl describe secrets/db-user-pass
# ...
# Type:            Opaque
 
# Data
# ====
# password.txt:    12 bytes
# username.txt:    5 bytes

你也可以手工的指定Secret规格:

YAML
1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  # 每个数据必须Base64处理
  # echo -n "admin" | base64
  username: YWRtaW4=
  password: MWYyZDFlMmU2N2Rm
解码Secret

使用如下命令可以获得Secret的所有属性,并输出为YML格式: 

Shell
1
kubectl get secret mysecret -o yaml

然后你可以复制Base格式的数据并解码:

Shell
1
echo "MWYyZDFlMmU2N2Rm" | base64 --decode
使用Secret
挂载为文件

Secret可以作为数据卷挂载,示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spec:
  containers:
  - name: mypod
    image: redis
    volumeMounts:
    # 把foo卷只读挂载到文件系统路径下
    - name: foo
      mountPath: "/etc/foo"
      readOnly: true
  volumes:
  # 定义一个名为foo的卷,其内容为mysecret这个Secret
  - name: foo
    secret:
      secretName: mysecret

默认的,Secret中的每个Data的key,都作为mountPath下的一个文件名。你可以定制Data到容器文件系统的映射关系:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spec:
  containers:
  - name: mypod
    image: redis
    volumeMounts:
    - name: foo
      mountPath: "/etc/foo"
      readOnly: true
  volumes:
  - name: foo
    secret:
      secretName: mysecret
      items:
      - key: username
        # 在容器中,访问路径为/etc/foo/my-group/my-username
        path: my-group/my-username
        # 文件模式为0400,这里必须转为十进制
        defaultMode: 256

在容器中读取Data映射文件时,获得的是明文。 

映射环境变量

Secret还可以映射为容器的环境变量:

YAML
1
2
3
4
5
6
7
8
9
10
spec:
  containers:
  - name: mycontainer
    image: redis
    env:
      - name: SECRET_USERNAME
        valueFrom:
          secretKeyRef:
            name: mysecret
            key: username
ServiceAccount

当你通过kubectl访问集群时,APIServer基于特定的用户账号(默认admin)对你进行身份验证。

服务账号(ServiceAccount)为运行在Pod中的进程提供身份(Identity)信息,这样当这些进程联系APIServer时,也能够通过身份验证。对服务账号的授权由授权插件和策略负责。

自动化

三个组件进行协作,完成和SA相关的自动化:

  1. 服务账户准入控制器(Service account admission controller),是API Server的一部分,当Pod创建或更新时:
    1. 如果该 pod 没有 ServiceAccount 设置,将其 ServiceAccount 设为 default
    2. 如果设置的ServiceAccount不存在则拒绝Pod
    3. 如果Pod不包含ImagePullSecrets设置,则将SA中的ImagePullSecrets拷贝进来
    4. 将一个用于访问API Server的Token作为卷挂载到Pod
    5. 将/var/run/secrets/kubernetes.io/serviceaccount下的VolumeSource添加到每个容器
  2. Token 控制器(Token controller),是controller-manager的一部分,异步方式来:
    1. 当SA创建后,创建对应的Secret,用于支持API 访问
    2. 当SA删除后,删除对应的Token Secret
    3. 当Token Secret删除后,在SA中去除对应的数组元素
    4. 当通过annotation引用了SA的ServiceAccountToken类型的Secret创建后,自动生成Token并更新Secret字段:
      YAML
      1
      2
      3
      4
      5
      6
      7
      8
      apiVersion: v1
      kind: Secret
      metadata:
        name: build-robot-secret
        annotations:
          # 提示此Secret为了哪个SA
          kubernetes.io/service-account.name: build-robot
      type: kubernetes.io/service-account-token
  3. 服务账户控制器(Service account controller)
    1. 确保每个命名空间具有default帐户
Token管理器

你需要通过 --service-account-private-key-file传递一个私钥给controller-manager,用于对SA的Token进行签名。

你需要通过 --service-account-key-file传递一个公钥给kube-api-server,以便API Server对Token进行校验。

Token卷影射

v1.12引入的Beta特性。要启用此特性,需要给API Server传递:

  1. --service-account-issuer:SA Token颁发者的标识符,颁发者会断言颁发的Token的iss claim中具有此参数的值,字符串或URI,例如kubernetes.default.svc
  2. --service-account-signing-key-file:包含SA Token颁发者私钥的路径,需要打开特性TokenRequest
  3. --service-account-api-audiences:API的标识符

Kubelet能够把SA Token影射到Pod中,你需要提供必须的属性,包括audience、有效期:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx
    volumeMounts:
    - mountPath: /var/run/secrets/tokens
      name: vault-token
  serviceAccountName: build-robot
  volumes:
  - name: vault-token
    projected:
      sources:
      - serviceAccountToken:
          path: vault-token
          expirationSeconds: 7200
          audience: vault

对于上述配置,Kubelet会代表Pod来请求Token,并存储到对应位置,并且在80%有效期过去后,自动刷新Token。应用程序需要自己检测Token已经刷新。

默认账号

当你创建Pod时,如果显式提供服务账号,则K8S在相同名字空间内为Pod分配一个默认账号(名为default),并自动设置到spec.serviceAccountName字段。

你可以在Pod内部调用K8S API,基于自动挂载的服务账号凭证(令牌)。从1.6开始,令牌的自动挂载可以禁用:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
# 在服务账号上禁用
automountServiceAccountToken: false
 
---
 
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  serviceAccountName: default
  # 针对单个Pod禁用
  automountServiceAccountToken: false
使用多账号

每个名字空间都有一个名为default的默认服务账号,你可以自己创建新的账号。

执行下面的命令列出所有账号:

Shell
1
2
3
kubectl get serviceAccounts
# NAME      SECRETS    AGE
# default   1          1d

ServiceAccount的规格很简单,上一节有示例。创建账号后,你可以利用授权插件来设置账号的权限。 

要让Pod使用非默认账号,配置spec.serviceAccountName字段即可。

手工创建令牌

如果禁用了服务账号的自动凭证生成功能,你需要手工的创建令牌。令牌是一个Secret对象:

YAML
1
2
3
4
5
6
7
apiVersion: v1
kind: Secret
metadata:
  name: build-robot-secret
  annotations:
    kubernetes.io/service-account.name: build-robot
type: kubernetes.io/service-account-token
ImagePullSecrets

ImagePullSecrets是用于访问镜像私服的Secret,假设你已经创建好一个名为gmemregsecret的Secret, 则可以使用下面的命令,为服务账号添加ImagePullSecrets:

Shell
1
kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "gmemregsecret"}]}'
实例

创建docker.gmem.cc的Secret:

Shell
1
2
3
4
# 创建名为gmemregsecret类型为docker-registry的secret
kubectl create secret docker-registry gmemregsecret \
    --docker-server=docker.gmem.cc --docker-username=alex  \
    --docker-password=pswd --docker-email=k8s@gmem.cc

查看刚刚创建的Secret:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
kubectl get secret gmemregsecret --output=yaml
 
# apiVersion: v1
# data:
#   .dockerconfigjson: eyJhdXRocyI6eyJkb2NrZXIuZ21lbS5jYzo1MDAwIjp7InVzZXJuYW1lIjoiYWxleCIsInBhc3N3b3JkIjoibGF2ZW5kZXIiLCJlbWFpbCI6Ims4c0BnbWVtLmNjIiwiYXV0aCI6IllXeGxlRHBzWVhabGJtUmxjZz09In19fQ==
# kind: Secret
# metadata:
#   creationTimestamp: 2018-02-11T14:04:52Z
#   name: gmemregsecret
#   namespace: default
#   resourceVersion: "1023033"
#  selfLink: /api/v1/namespaces/default/secrets/gmemregsecret
#  uid: 87db6e2e-0f34-11e8-92db-deadbeef00a0
# type: kubernetes.io/dockerconfigjson

其中data字段为BASE64编码的密码数据,可以查看一下它的明文:

Shell
1
2
echo eyJhdXRocyI6eyJkb2NrZXIuZ21lbS5jYzo1MDAwIjp7InVzZXJuYW1lIjoiYWxleCIsInBhc3N3b3JkIjoibGF2ZW5kZXIiLCJlbWFpbCI6Ims4c0BnbWVtLmNjIiwiYXV0aCI6IllXeGxlRHBzWVhabGJtUmxjZz09In19fQ== | base64 -d
# {"auths":{"docker.gmem.cc":{"username":"alex","password":"lavender","email":"k8s@gmem.cc","auth":"YWxleDpsYXZlbmRlcg=="}}}alex@Zircon:~$

现在你可以创建使用此Secret来拉取镜像的Pod了:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Pod
metadata:
  name: digitalsrv-1
  labels:
    tier: rpc
    app: digitalsrv
spec:
  containers:
  - name: digitalsrv-1
    image: docker.gmem.cc/digitalsrv:1.0
  imagePullSecrets:
  - name: gmemregsecret
为默认账号分配

执行 kubectl edit serviceaccounts default,修改为如下内容:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: 2018-02-12T07:53:30Z
  name: default
  namespace: default
  resourceVersion: "1843"
  selfLink: /api/v1/namespaces/default/serviceaccounts/default
  uid: d0e062a3-0fc9-11e8-b942-deadbeef00a0
secrets:
- name: default-token-j9zdx
imagePullSecrets:
- name: gmemregsecret

或者,执行命令:

Shell
1
kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "gmemregsecret"}]}'
控制器
ReplicaSet

下一代复制控制器,与Replication Controller仅仅的不同是对选择器的支持, ReplicaSet支持指定set-based的选择器(来匹配Pod),后者仅支持equality-based的选择器。

尽管ReplicaSet可以被独立使用,但是实际上它主要通过Deployment间接使用,作为编排Pod创建、更新、删除的工具。使用Deployment时你无需关心其自动创建的ReplicaSet对象。

ReplicaSet用于确保,在任何时刻,指定数量的Pod实例同时在集群中运行。示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: frontend
  labels:
    app: guestbook
    tier: frontend
spec:
  # 复制实例的数量,默认1
  replicas: 3
  # 选择器,用于匹配被控制的Pod
  # 只要匹配,即使Pod不是ReplicaSet自己创建的,也被管理 —— 这允许替换ReplicaSet而不影响已存在的Pod的控制
  selector:
    matchLabels:
      tier: frontend
    matchExpressions:
      - {key: tier, operator: In, values: [frontend]}
  # template是spec段唯一强制要求的元素,提供一个Pod模板
  # 此Pod模板的结构和Pod对象完全一样,只是没有apiVersion/kind字段
  template:
    metadata:
      # ReplicaSet中的Pod模板必须指定标签、以及适当的重启策略
      labels:
        app: guestbook
        tier: frontend
    spec:
      containers:
      - name: php-redis
        image: gcr.io/google_samples/gb-frontend:v3
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        env:
        - name: GET_HOSTS_FROM
          value: dns
          # 如果集群没有配备DNS Addon,则可以从环境变量获取service的主机名
          # value: env
        ports:
        - containerPort: 80
删除

要删除ReplicaSet及其Pod,可以使用 kubectl delete命令。Kubectl会将ReplicaSet缩容为0,并在删除ReplicaSet之前等待其所有Pod的删除,使用REST API或者Go客户端库时,你需要手工缩容、等待Pod删除、并删除ReplicaSet。

要仅删除ReplicaSet,可以使用 kubectl delete --cascade=false命令。 使用REST API/Go客户端库时,简单删除ReplicaSet对象即可。

在删除ReplicaSet之后,你可以创建.spec.selector字段与之一样的新的ReplicaSet,这个新的RS会管理原先的Pod。但是现有Pod不会匹配新RS的Pod 模板,你可以通过滚动更新(Rolling Update)实现现有Pod的更新。

隔离Pod

要从RS中隔离一个Pod,可以修改Pod的标签。注意,此Pod会很快被代替,以满足ReplicaSet的复制数量要求。

扩容

要对RS进行扩容/缩容,你仅仅需要更新.spec.replicas字段。

配合HPA

RS可以作为Pod水平自动扩容器(Horizontal Pod Autoscaler)的目标。HPA的示例:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: frontend-scaler
spec:
  # 扩容控制的目标
  scaleTargetRef:
    kind: ReplicaSet
    name: frontend
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 50

v2版本的HPA示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: podinfo
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: podinfo
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 50 

针对上述配置文件调用kubectl create -f即可创建HPA。你也可以直接调用

Shell
1
kubectl autoscale rs frontend

 更加简便。

ReplicationController

类似于ReplicaSet。ReplicaSet是下一代的复制控制器。

大部分支持ReplicationController的kubectl命令,同时也支持ReplicaSet,一个例外是rolling-update命令。此命令专门用于RC的滚动更新  —— 每次更新一个Pod。

Deployment

此控制器为Pod或ReplicaSet提供声明式的更新支持。在Deployment中,你可以指定一个期望状态,Deployment控制器会以一定的控制速率来修改实际状态,让它和期望状态匹配。通过定义Deployment你可以创建新的ReplicaSet,或者移除现有的Deployment并接收其全部资源。

Deployment的典型应用场景:

  1. 创建一个Deployment,来rollout一个ReplicaSet。ReplicaSet会在后台创建Pod。检查rollout的状态来确认是否成功
  2. 更新Deployment的PodTemplateSpec部分,来声明Pods的新状态。一个新的复制集会被创建,Pod会已移动的速率,从老的复制集中移动到新的复制集
  3. 回滚到旧的部署版本,如果当前版本的部署不稳定,则可以回滚到旧的Deployment版本
  4. 扩容以满足负载需要
  5. 暂停部署,对PodTemplateSpec进行更新,然后恢复部署,进行rollout
  6. 将Deployment的状态作为rollout卡死的提示器
  7. 清除你不再需要的复制集
创建

下面的例子创建了三个运行Nginx的Pod构成的复制集:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  # 复制集容量
  replicas: 3
  # 可选此控制器如何找到需要管理的Pod,必须匹配.spec.template.metadata.labels
  selector:
    matchLabels:
      app: nginx
  # 如何替换旧Pod的策略,Recreate或RollingUpdate,默认RollingUpdate
  # Recreate 当新的Pod创建之前,所有旧Pod被删除
  # RollingUpdate 滚动更新(逐步替换)
  strategy.type: RollingUpdate
  # 更新期间,处于不可用状态的Pod的数量,可以指定绝对值或者百分比。默认25
  strategy.rollingUpdate.maxUnavailable: 0
  # 更新期间,同时存在的Pod可以超过期望数量的多少,可以指定绝对值或者百分比。默认25
  strategy.rollingUpdate.maxSurge: 1
  # 在报告failed progressing之前等待更新完成的最长时间
  progressDeadlineSeconds: 60
  # 新创建的Pod,最少在启动多久后,才被认为是Ready。默认0
  minReadySeconds: 0
  # 保留rollout历史的数量,默认无限
  revisionHistoryLimit:
  # Pod的模板
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        # 打开端口80,供Pod使用
        - containerPort: 80

根据上述规格创建Deployment:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
kubectl create -f nginx-deployment.yaml
 
 
# 获取对象信息
kubectl get deployments
# 部署名称            期望实例数  当前运行实体数  到达期望状态实例数 对用户可用实例数   应用运行时长
# UP-TO-DATE 意味着使用最新的Pod模板
# AVAILABLE的Pod至少进入Ready状态.spec.minReadySeconds秒
# NAME               DESIRED   CURRENT       UP-TO-DATE      AVAILABLE       AGE
# nginx-deployment   3         0             0               0               1s
 
# 要查看部署的rollout状态,可以执行:
kubectl rollout status deployment/nginx-deployment
# Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
# deployment "nginx-deployment" successfully rolled out
 
# 一段时间后,再次获取对象信息
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3         3         3            3           18s
 
# 要查看Deployment自动创建的复制集,执行
kubectl get rs
# NAME                          DESIRED   CURRENT   READY   AGE
# nginx-deployment-2035384211   3         3         3       18s
# 复制集名称的格式为 [DEPLOYMENT-NAME]-[POD-TEMPLATE-HASH-VALUE]
 
# 要查看为Pod自动创建的标签,执行
kubectl get pods --show-labels
# NAME                                READY     STATUS    RESTARTS   AGE       LABELS
# nginx-deployment-2035384211-7ci7o   1/1       Running   0          18s       app=nginx,pod-template-hash=2035384211
# nginx-deployment-2035384211-kzszj   1/1       Running   0          18s       app=nginx,pod-template-hash=2035384211
# nginx-deployment-2035384211-qqcnn   1/1       Running   0          18s       app=nginx,pod-template-hash=2035384211
更新

部署的rollout仅仅在其Pod模板发生变化时才会触发。其它情况,例如进行扩容,不会触发rollout。

修改Pod使用的镜像,示例命令:

Shell
1
2
3
4
# 方式一
kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1
# 方式二
kubectl edit deployment/nginx-deployment

Deployment能够控制在更新时:

  1. 仅一定数量的Pod可以处于宕机状态。默认值是,最多运行期望Pod数量 - 1个处于宕机状态
  2. 存在超过期望数量的Pod存在。默认值是,最多同时存在 期望Pod数量 + 1 个Pod。K8S会先创建新Pod,然后删除旧Pod,这会导致同时存在的Pod数量超过期望Pod数

Deployment允许多重同时进行中的更新(multiple updates in-flight),所谓Rollover。每当部署控制器监控到新的Deployment对象时,旧有的控制标签匹配.spec.selector而模板不匹配.spec.template的Pod复制集会缩容为0,新的复制集则会扩容到期望Pod数量。如果你更新Deployment时,它正在进行rollout,则新的复制集被创建(每次更新对应一个)并扩容,并且roll over 前一次更新创建的复制集 —— 将其加入到旧复制集列表,并进行缩容。举例来说:

  1. 某个部署创建了5实例的nginx:1.7.9
  2. 随后更新部署为5实例的nginx:1.9.1,此时nginx:1.7.9的3个实例已经被创建
  3. 这时,部署控制器会立刻杀死nginx:1.7.9的3个实例,随后开始创建nginx:1.9.1实例。而不是等待nginx:1.7.9的5个实例都创建完毕,再对其缩容

你也可以进行标签选择器的更新,但是这并不推荐,你应该预先规划好标签。

回滚

某些情况下你需要回滚一次部署,这通常是因为新版本存在问题,例如无限循环崩溃。

默认情况下,Deployment所有的rollout历史(revision)都会保存在系统中,方便你随时进行回滚。注意revision仅仅在Deployment的rollout被触发时才生成,也就是仅仅在Deployment的Pod模板变更时才生成。执行回滚后,仅仅Pod模板部分被回滚,扩容、标签部分不受影响。

相关命令:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 列出rollout历史
kubectl rollout history deployment/nginx-deployment
# deployments "nginx-deployment"
# REVISION    CHANGE-CAUSE
# 1           kubectl create -f docs/user-guide/nginx-deployment.yaml --record
# 2           kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1
# 3           kubectl set image deployment/nginx-deployment nginx=nginx:1.91
 
# 查看某次rollout的详细信息
kubectl rollout history deployment/nginx-deployment --revision=2
 
# 回滚到上一个版本
kubectl rollout undo deployment/nginx-deployment
 
# 回滚到指定的版本
kubectl rollout undo deployment/nginx-deployment --to-revision=2
扩容

设置Pod实例数量:

Shell
1
kubectl scale deployment nginx-deployment --replicas=10

如果集群启用了Pod自动水平扩容(horizontal pod autoscaling),你可以为Deployment设置autoscaler:

Shell
1
2
# 实例数量在10到15之间,尽可能让这些实例的CPU占用靠近80%
kubectl autoscale deployment nginx-deployment --min=10 --max=15 --cpu-percent=80
暂停/恢复

在触发一个或多个更新之前,你可以暂停Deployment,避免不必要的rollout:

Shell
1
2
3
4
5
6
7
8
9
# 暂停rollout
kubectl rollout pause deployment/nginx-deployment
 
# 更新Deployment
kubectl set image deploy/nginx-deployment nginx=nginx:1.9.1
kubectl set resources deployment nginx-deployment -c=nginx --limits=cpu=200m,memory=512Mi
 
# 恢复rollout
kubectl rollout resume deploy/nginx-deployment
查看状态

Deployment在其生命周期中,会进入若干不同的状态:

状态 说明
progressing

执行以下任务之一时,K8S将Deployment标记为此状态:

  1. Deployment创建了新的复制集
  2. Deployment正在扩容到最新的复制集
  3. Deployment正在缩容到较老的复制集
  4. 新的Pod变为ready或available
complete

如果具有以下特征,K8S将Deployment标记为此状态:

  1. 所有Pod实例都更新为最新的版本
  2. 所有Pod实例均可用
  3. 不存在旧版本的Pod实例在运行
fail to progress

无法部署Deployment的最新复制集,可能原因是:

  1. 配额不足
  2. Readiness探针失败
  3. Image拉取出错
  4. 权限不足
  5. 应用程序的运行时配置存在问题

具体多久没有完成部署,会让Deployment变为此状态,受spec.progressDeadlineSeconds控制

一旦超过上述Deadline,则部署控制器为Deployment.status.conditions添加一个元素,其属性为

Type=Progressing,Status=False,Reason=ProgressDeadlineExceeded

注意,暂停中的Deployment不会超过Deadline

执行下面的命令可以查看Deployment的状态:

Shell
1
kubectl rollout status
更新历史清理

设置Deployment的.spec.revisionHistoryLimit字段,可以控制更新历史(也就是多少旧的ReplicaSet)被保留,默认所有历史都保留。

StatefulSet

原先叫做PetSet。该控制器用于管理部署、扩容Pod集。并提供一个保证:确保Pod的有序性、唯一性。

和Deployment类似,SS也能管理(基于相同的容器规格的一组)Pods,但是SS还能够维护每个Pod的粘性身份(Sticky Identity )。尽管这些Pod的规格完全一样,但是不能相互替换(有状态),每个Pod都有自己的持久化的唯一标识,即使发生重新调度,也不会改变。

SS的行为模式和其它控制器类似,你需要定义一个StatefulSet对象,说明期望的状态。StatefulSet控制器会执行必要的更新以达到此状态。

对于有以下需求的应用程序,考虑使用SS:

  1. 稳定(Pod重调度后不变)的、唯一的网络标识符(network identity)
  2. 稳定的、持久的存储
  3. 有序的、优雅的部署和扩容
  4. 有序的、优雅的删除和终结
  5. 有序的、自动化的滚动更新

如果不满足上述需求之一,你应该考虑提供无状态复制集的控制器,例如Deployment、ReplicaSet。

使用SS时,要注意:

  1. 在1.9之前处于Beta状态,1.5-版本完全不可用
  2. 和其它所有Alpha/Beta资源一样,SS可以通过APIServer参数--runtime-config禁用
  3. Pod所需的存储资源,要么由 PersistentVolume Provisioner 提供,要么由管理员预先提供
  4. 删除/缩容SS时,和SS关联的卷不会自动删除
  5. SS目前依赖Headless Service,后者负责维护Pod的网络标识符,此Service需要你来创建

Headless Service的规格示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 用于控制网络域(Network Domain)
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx 

StatefulSet的规格示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  # 1.8-,如果spec.selector不设置,K8S使用默认值
  # 1.8+,不设置和.spec.template.metadata.labels匹配的值会在创建SS时出现验证错误
  selector:
    matchLabels:
      app: nginx # has to match .spec.template.metadata.labels
  # 管理此SS的Headless Service的名称
  serviceName: "nginx"
  # 包含三个有标识的Pod中启动的Nginx容器
  replicas: 3    
  template:
    metadata:
      labels:
        app: nginx # has to match .spec.selector.matchLabels
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: gcr.io/google_containers/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        # 将卷www挂载到目录树
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  # 使用PersistentVolumeProvisioner提供的PersistentVolume,作为持久化的存储
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: my-storage-class
      resources:
        requests:
          storage: 1Gi
Pod身份

SS创建的Pod具有唯一性的身份,此身份对应了稳定的网络标识符、稳定的存储。 此身份总是关联到Pod,不管Pod被重新调度到哪个节点上。

序号索引:对于副本份数为N的SS,每个Pod被分配一个整数序号,值范围 [ 0, N)

稳定网络标识符:每个Pod的hostname的形式为 ${SS名称}-${序号索引}。例如上面的例子中,会创建web-0、web-1、web-2三个Pod。

SS可以利用Headless Service来控制其Pod的域名,HS的域名格式为 $(service name).$(namespace).svc.cluster.local,其中cluster.local是集群的域名,上例中的HS域名为nginx.default.svc.cluster.local。

在HS的管理下,Pod的域名格式为 $(podname).$(governing service domain),因此web-0的域名为web-0.nginx.default.svc.cluster.local

稳定存储

对于每个VolumeClaimTemplate,K8S会创建对应的PersistentVolume。在上面的例子中,每个Pod会被赋予StorageClass为my-storage-class的单个PersistentVolume,以及1GB的存储空间。如果不指定StorageClass使用默认值。

当Pod被重新调度时,volumeMounts指定的挂载规则会重新挂载PersistentVolume。此外,即使SS或Pod被删除,PersistentVolume也不会被自动删除,你必须手工的删除它。

Pod标签

SS创建一个新Pod时,会为其添加一个标签:statefulset.kubernetes.io/pod-name。通过此标签,你可以为某个Pod实例Attach一个服务。

部署/扩容保证
  1. 当SS部署Pod时,会顺序的依次创建,从序号0开始逐个的
  2. 当SS删除Pod时,会逆序的依次删除,从序号N-1开始逐个的
  3. 水平扩容时,新序号之前的所有Pod必须已经Running & Ready
  4. 在Pod被终结时,其后面的所有Pod必须已经被完全关闭

SS不应该指定pod.Spec.TerminationGracePeriodSeconds=0。

DaemonSet

该控制器能确保所有(或部分)节点运行Pod的单个副本。每当节点加入到集群时,Pod就被添加到其上;每当节点离开集群时,其上的Pod就被回收。删除DS会导致所有Pod被删除。

DS的典型应用场景包括:

  1. 运行集群级别的存储守护程序,例如glusterd、ceph这些程序每个节点仅需要一个
  2. 在每个节点运行日志收集程序,例如fluentd、logstash
  3. 在每个节点上运行监控程序,例如collectd

DS的规格示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-elasticsearch
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd-elasticsearch
  template:
    metadata:
      labels:
        name: fluentd-elasticsearch
    spec:
      # 用于仅仅在部分节点上运行Pod
      nodeSelector:
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd-elasticsearch
        image: gcr.io/google-containers/fluentd-elasticsearch:1.20
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
和DS的Pod通信

要和DaemonSet中的Pod通信,有以下几种手段:

  1. Push:DS中的Pod可能配置为,向其它服务发送更新,例如向统计数据库
  2. 节点IP + 已知端口:Pod可能使用hostPort来暴露端口
  3. DNS:使用和DS相同的Pod选择器,创建一个Headless Service。然后你可以通过endpoints资源来发现DS
hostPort

可以为DaemonSet的Pod声明hostPort,这样,宿主机的端口会通过iptables的NAT转发给Pod:

YAML
1
2
3
4
5
ports:
  - name: http
    containerPort: 80
    protocol: TCP
    hostPort: 80 
Job

Job可以创建一或多个Pod,并且确保一定数量的Pod成功的完成。Job会跟踪Pod们的执行状态,并判断它们是否成功执行,当指定数量的Pod成功了,则Job本身的状态变为成功。删除Job会清理掉它创建的Pod。

Job的一个简单用例是,确保Pod成功执行完成。如果第一个Pod失败/被删除,则Job会启动第二个实例。

Job也支持并行的运行多个Pod。

Job的规格示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  template:
    metadata:
      name: pi
    spec:
      containers:
      - name: pi
        image: perl
        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
      restartPolicy: Never
  backoffLimit: 4
CronJob

定期调度执行的Job,规格示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello
spec:
  # Cron表达式,每分钟执行
  schedule: "*/1 * * * *"
  # 如果由于某些原因,错过了调度时间,那么在什么时间差异之内,允许启动Job。默认没有deadline,总是允许启动
  startingDeadlineSeconds: 5
  # 并发控制策略,此CronJob创建的多个Job如何并发运行
  # Allow,默认,允许并发运行多个Job
  # Forbid,禁止并发运行,如果尝试调度时发现先前的Job仍然在运行,跳过本次调度
  # Replace,禁止并发运行,如果尝试调度时发现先前的Job仍然在运行,替换掉先前的Job
  concurrencyPolicy: Allow
  # 如果设置为true,则暂停后续调度,已经存在的Job不受影响
  suspend: false
  # 保留的成功、失败的Job的数量.超过限制则删除对应的K8S资源
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 0
  jobTemplate:
    # 下面的配置同Job
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date
          restartPolicy: OnFailure
垃圾回收

Kubernetes garbage collector是控制器的一种,它的职责是删除哪些曾经拥有,但是现在已经没有Owner的对象。

对象所有权

某些对象是其它对象的所有者(Owner),例如ReplicaSet是一系列Pod的所有者。被所有者管理的对象称为依赖者(Dependent )。任何依赖者都具有字段 metadata.ownerReferences,指向其所有者。

某些情况下,K8S会自动设置ownerReference。例如创建ReplicaSet时,对应Pod的ownerReference即自动设置。常作为所有者的内置资源类型包括ReplicationController, ReplicaSet, StatefulSet, DaemonSet, Deployment, Job和CronJob。

你也可以手工配置ownerReference字段,以建立所有者-依赖者关系。

依赖者的删除

在删除对象时,可以指定是否级联删除(Cascading deletion)其依赖者(依赖被删除对象的哪些对象)。级联删除有两种执行模式:前台删除、后台删除。

前台删除

在这种模式下,根对象(被显式删除的顶级Owner)首先进入删除中“(deletion in progress)”状态。此时:

  1. 对象仍然可以通过REST API看到
  2. 对象的deletionTimestamp字段不再为空
  3. 对象的 metadata.finalizers数组包含元素“foregroundDeletion“

一旦进入“删除中”状态,垃圾回收器就开始删除对象的依赖者。一旦所有阻塞依赖者(ownerReference.blockOwnerDeletion=true)全被删除,根对象就被删除。

当某种控制器设置了ownerReferences,它也会自动设置blockOwnerDeletion,不需要人工干预。

后台删除

根对象立即被删除。垃圾回收器异步的在后台删除依赖者。

设置删除模式

通过设置删除请求的DeleteOptions.propagationPolicy,可以修改级联删除模式:

  1. Orphan,孤儿化,解除ownerReferences而不删除
  2. Foreground,前台级联删除
  3. Background,后台级联删除
准许控制器

准许控制器(Admission Controller)是一小片的代码,被编译到kube-apiserver的二进制文件中。它能够拦截针对APIServer的请求,具体拦截时机是:操控对象被持久化之前、请求通过身份验证之后。很多K8S的高级特性需要准许控制器的介入。

准许控制器可以具有两个行为:

  1. validating:不能修改其admit的对象
  2. mutating:能够修改其admit的对象

准许控制的流程分为两个阶段,首先运行mutating类控制器,然后运行validating类控制器,任何一个控制器在任何阶段拒绝请求,则客户端收到一个错误。需要注意某些控制器同时有mutating、validating行为。

控制器的执行顺序,等同于它们在--admission-control参数中声明的顺序。

推荐的准许控制器

对于1.9版本,建议按序开启以下准许控制器:

Shell
1
2
3
--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,
                    DefaultStorageClass,ValidatingAdmissionWebhook,ResourceQuota,
                    DefaultTolerationSeconds,MutatingAdmissionWebhook
动态准许控制

(静态)准许控制器本身不够灵活:

  1. 需要被编译到kube-apiserver镜像中
  2. 仅仅当API Server启动后才可以被配置

Admission Webhooks可以解决此问题,用它可以来开发代码独立(Out-of-tree)、支持运行时配置的准许控制器。

和静态的准许控制器一样,Webhook也分为validating、mutating两类。

Webhook实际上是由ValidatingAdmissionWebhook、MutatingAdmissionWebhook这两个静态准许控制器适配(到API Server)的,因此这两个遵顼控制器必须启用。

开发Webhook服务器

这种服务需要处理admissionReview请求,并将其准许决定封装在admissionResponse中。

admissionReview可以是版本化的,Webhook可以使用admissionReviewVersions字段声明它能处理的Review的版本列表。调用Webhook时API Server会选择此列表中、它支持的第一个版本,如果找不到匹配版本,则失败。

下面是Kubernetes官方提供的样例Webhook服务器代码:

kubernetes/v1.13.0/test/images/webhook/main.go
Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package main
 
import (
    "encoding/json"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
 
    "k8s.io/api/admission/v1beta1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/klog"
)
 
// 一个助手函数,创建内嵌error的AdmissionResponse
func toAdmissionResponse(err error) *v1beta1.AdmissionResponse {
    return &v1beta1.AdmissionResponse{
        Result: &metav1.Status{
            Message: err.Error(),
        },
    }
}
 
// 所有validators和mutators都由admitFunc类型的函数负责实现
type admitFunc func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse
 
// 核心逻辑,API Server使用HTTP协议调用Webhook Server
func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) {
    var body []byte
    if r.Body != nil {
        if data, err := ioutil.ReadAll(r.Body); err == nil {
            body = data
        }
    }
 
    // 请求体校验
    contentType := r.Header.Get("Content-Type")
    if contentType != "application/json" {
        klog.Errorf("contentType=%s, expect application/json", contentType)
        return
    }
 
    klog.V(2).Info(fmt.Sprintf("handling request: %s", body))
 
    // 请求Review
    requestedAdmissionReview := v1beta1.AdmissionReview{}
 
    // 响应Review
    responseAdmissionReview := v1beta1.AdmissionReview{}
 
    deserializer := codecs.UniversalDeserializer()
    // 反串行化响应体为requestedAdmissionReview
    if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil {
        klog.Error(err)
        responseAdmissionReview.Response = toAdmissionResponse(err)
    } else {
        // 如果没有错误,则执行准许逻辑
        responseAdmissionReview.Response = admit(requestedAdmissionReview)
    }
 
    // 返回相同的UID
    responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
 
    klog.V(2).Info(fmt.Sprintf("sending response: %v", responseAdmissionReview.Response))
 
    respBytes, err := json.Marshal(responseAdmissionReview)
    if err != nil {
        klog.Error(err)
    }
    if _, err := w.Write(respBytes); err != nil {
        klog.Error(err)
    }
}
 
/* 各种validators和mutators */
func serveAlwaysDeny(w http.ResponseWriter, r *http.Request) {
    serve(w, r, alwaysDeny)
}
 
func serveAddLabel(w http.ResponseWriter, r *http.Request) {
    serve(w, r, addLabel)
}
 
func servePods(w http.ResponseWriter, r *http.Request) {
    serve(w, r, admitPods)
}
 
func serveAttachingPods(w http.ResponseWriter, r *http.Request) {
    serve(w, r, denySpecificAttachment)
}
 
func serveMutatePods(w http.ResponseWriter, r *http.Request) {
    serve(w, r, mutatePods)
}
 
func serveConfigmaps(w http.ResponseWriter, r *http.Request) {
    serve(w, r, admitConfigMaps)
}
 
func serveMutateConfigmaps(w http.ResponseWriter, r *http.Request) {
    serve(w, r, mutateConfigmaps)
}
 
func serveCustomResource(w http.ResponseWriter, r *http.Request) {
    serve(w, r, admitCustomResource)
}
 
func serveMutateCustomResource(w http.ResponseWriter, r *http.Request) {
    serve(w, r, mutateCustomResource)
}
 
func serveCRD(w http.ResponseWriter, r *http.Request) {
    serve(w, r, admitCRD)
}
 
type Config struct {
    CertFile string
    KeyFile  string
}
 
func (c *Config) addFlags() {
    // 包含用于HTTPS的x509服务器证书,如果包含CA证书,则连接在服务器证书后面
    flag.StringVar(&c.CertFile, "tls-cert-file", c.CertFile, "...")
    // 匹配上述服务器证书的私钥
    flag.StringVar(&c.KeyFile, "tls-private-key-file", c.KeyFile, "...")
}
 
func configTLS(config Config) *tls.Config {
    sCert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
    if err != nil {
        klog.Fatal(err)
    }
    return &tls.Config{
        Certificates: []tls.Certificate{sCert},
        // 下面一行用于启用mTLS,也就是对客户端(API Server)进行身份验证
        // ClientAuth:   tls.RequireAndVerifyClientCert,
    }
}
 
func main() {
    var config Config
    config.addFlags()
    flag.Parse()
 
        // 不同的URL路径,对应不同的Webhook,处理函数也不同
    http.HandleFunc("/always-deny", serveAlwaysDeny)
    http.HandleFunc("/add-label", serveAddLabel)
    http.HandleFunc("/pods", servePods)
    http.HandleFunc("/pods/attach", serveAttachingPods)
    http.HandleFunc("/mutating-pods", serveMutatePods)
    http.HandleFunc("/configmaps", serveConfigmaps)
    http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps)
    http.HandleFunc("/custom-resource", serveCustomResource)
    http.HandleFunc("/mutating-custom-resource", serveMutateCustomResource)
    http.HandleFunc("/crd", serveCRD)
 
    server := &http.Server{
        Addr:      ":443",
        TLSConfig: configTLS(config),
    }
    server.ListenAndServeTLS("", "")
}

此样例中包含很多Webhook的样板代码。下面选取几个分析。

addlabel.go是一个mutator,它为对象添加{"added-label": "yes"}标签:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main
 
import (
    "encoding/json"
 
    "k8s.io/api/admission/v1beta1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/klog"
)
 
const (
    addFirstLabelPatch string = `[
         { "op": "add", "path": "/metadata/labels", "value": {"added-label": "yes"}}
     ]`
    addAdditionalLabelPatch string = `[
         { "op": "add", "path": "/metadata/labels/added-label", "value": "yes" }
     ]`
)
 
func addLabel(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
    klog.V(2).Info("calling add-label")
    obj := struct {
        metav1.ObjectMeta
        Data map[string]string
    }{}
    // 从Review中提取原始对象(被拦截的API Server请求中的对象)
    raw := ar.Request.Object.Raw
    err := json.Unmarshal(raw, &obj)
    if err != nil {
        klog.Error(err)
        return toAdmissionResponse(err)
    }
    // 响应
    reviewResponse := v1beta1.AdmissionResponse{}
    // 准许通过
    reviewResponse.Allowed = true
    if len(obj.ObjectMeta.Labels) == 0 {
        // Patch原始对象
        reviewResponse.Patch = []byte(addFirstLabelPatch)
    } else {
        reviewResponse.Patch = []byte(addAdditionalLabelPatch)
    }
    pt := v1beta1.PatchTypeJSONPatch
    reviewResponse.PatchType = &pt
    return &reviewResponse
}

pods.go提供了三个Webhook:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package main
 
import (
    "fmt"
    "strings"
 
    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
    "k8s.io/api/admission/v1beta1"
    "k8s.io/klog"
)
 
const (
    podsInitContainerPatch string = `[
         {"op":"add","path":"/spec/initContainers","value":[{"image":"webhook-added-image","name":"webhook-added-init-container","resources":{}}]}
    ]`
)
 
// 校验Pod的规格
func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
    klog.V(2).Info("admitting pods")
    // 校验Review中的GVK
    podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
    if ar.Request.Resource != podResource {
        err := fmt.Errorf("expect resource to be %s", podResource)
        klog.Error(err)
        return toAdmissionResponse(err)
    }
    // 将原始对象转换为Pod
    raw := ar.Request.Object.Raw
    pod := corev1.Pod{}
    deserializer := codecs.UniversalDeserializer()
    if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil {
        klog.Error(err)
        return toAdmissionResponse(err)
    }
    reviewResponse := v1beta1.AdmissionResponse{}
    // 默认准许通过
    reviewResponse.Allowed = true
 
    var msg string
    // 演示各种不准许的情形
    if v, ok := pod.Labels["webhook-e2e-test"]; ok {
        if v == "webhook-disallow" {
            reviewResponse.Allowed = false
            msg = msg + "the pod contains unwanted label; "
        }
    }
    for _, container := range pod.Spec.Containers {
        if strings.Contains(container.Name, "webhook-disallow") {
            reviewResponse.Allowed = false
            msg = msg + "the pod contains unwanted container name; "
        }
    }
    if !reviewResponse.Allowed {
        reviewResponse.Result = &metav1.Status{Message: strings.TrimSpace(msg)}
    }
    return &reviewResponse
}
 
// 修改Pod,Patch一个Init容器进去
func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
    klog.V(2).Info("mutating pods")
    podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
    if ar.Request.Resource != podResource {
        klog.Errorf("expect resource to be %s", podResource)
        return nil
    }
 
    raw := ar.Request.Object.Raw
    pod := corev1.Pod{}
    deserializer := codecs.UniversalDeserializer()
    if _, _, err := deserializer.Decode(raw, nil, &pod); err != nil {
        klog.Error(err)
        return toAdmissionResponse(err)
    }
    reviewResponse := v1beta1.AdmissionResponse{}
    reviewResponse.Allowed = true
    if pod.Name == "webhook-to-be-mutated" {
        reviewResponse.Patch = []byte(podsInitContainerPatch)
        pt := v1beta1.PatchTypeJSONPatch
        reviewResponse.PatchType = &pt
    }
    return &reviewResponse
}
 
// 禁止kubectl attach to-be-attached-pod -i -c=container1请求
func denySpecificAttachment(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
    klog.V(2).Info("handling attaching pods")
    if ar.Request.Name != "to-be-attached-pod" {
        return &v1beta1.AdmissionResponse{Allowed: true}
    }
    // 期望接收到的是Pod资源,attach子资源
    podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
    if e, a := podResource, ar.Request.Resource; e != a {
        err := fmt.Errorf("expect resource to be %s, got %s", e, a)
        klog.Error(err)
        return toAdmissionResponse(err)
    }
    if e, a := "attach", ar.Request.SubResource; e != a {
        err := fmt.Errorf("expect subresource to be %s, got %s", e, a)
        klog.Error(err)
        return toAdmissionResponse(err)
    }
 
    raw := ar.Request.Object.Raw
    podAttachOptions := corev1.PodAttachOptions{}
    deserializer := codecs.UniversalDeserializer()
    if _, _, err := deserializer.Decode(raw, nil, &podAttachOptions); err != nil {
        klog.Error(err)
        return toAdmissionResponse(err)
    }
    klog.V(2).Info(fmt.Sprintf("podAttachOptions=%#v\n", podAttachOptions))
    // 如果不使用Stdin,或者容器不是container1则允许,否则不允许
    if !podAttachOptions.Stdin || podAttachOptions.Container != "container1" {
        return &v1beta1.AdmissionResponse{Allowed: true}
    }
    return &v1beta1.AdmissionResponse{
        Allowed: false,
        Result: &metav1.Status{
            Message: "attaching to pod 'to-be-attached-pod' is not allowed",
        },
    }
}
部署Webhook服务

可以直接作为Deployment部署在K8S集群内部,也可以部署在外面,需要对应的配置来配合。

配置Webhook

Webhook的配置通过ValidatingWebhookConfiguration、MutatingWebhookConfiguration这两类K8S资源进行:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
  name: sidecar-injectors
webhooks:
# Webhoooks列表
- name: init-inject
  # 匹配任意一个规则,则API Server会发送AdmissionReview
  # 给clientConfig所指定的Webhook服务器
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods
    scope: "Namespaced"
  clientConfig:
    # Webhook服务
    service:
      namespace: kube-system
      name: sidecar-injector
    # 签名了服务器端证书的CA,PEM格式
    # 有时候会设置为 caBundle: Cg== (\n),这是一个占位符,防止该CR被API Server拒绝。证书后续由程序自动更新
    caBundle: pem encoded ca cert that signs the server cert used by the webhook
  admissionReviewVersions:
  - v1beta1
  timeoutSeconds: 1
身份验证

如果Webhook需要对API Server进行身份验证,则需要进行以下步骤:

  1. 为API Server提供 --admission-control-config-file参数,指定准许控制配置文件
  2. 在准许控制配置文件中,声明MutatingAdmissionWebhook、ValidatingAdmissionWebhook从何处读取凭证信息:
    YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    apiVersion: apiserver.k8s.io/v1alpha1
    kind: AdmissionConfiguration
    plugins:
    - name: ValidatingAdmissionWebhook
      configuration:
        apiVersion: apiserver.config.k8s.io/v1alpha1
        kind: WebhookAdmission
        kubeConfigFile: /path/to/kubeconfig
    - name: MutatingAdmissionWebhook
      configuration:
        apiVersion: apiserver.config.k8s.io/v1alpha1
        kind: WebhookAdmission
        kubeConfigFile: /path/to/kubeconfig
  3. 凭证信息必须存放在KubeConfig文件中,示例:
    YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    apiVersion: v1
    kind: Config
    users:
     
    # 基于数字证书的身份验证
      # Webhook服务器的DNS名称
    - name: 'webhook1.ns1.svc'  
      user:
        client-certificate-data: pem encoded certificate
        client-key-data: pem encoded key
    # HTTP基本验证
      # 可以使用通配符
    - name: '*.webhook-company.org'
      user:
        password: password
        username: name
    # 基于令牌的身份验证
    # 默认凭证
    - name: '*'
      user:
        token: <token>
准许控制器列表
AlwaysAdmit

总是允许所有请求通过。

AlwaysDeny

总是拒绝所有请求通过,仅测试用。 

AlwaysPullImages

修改所有新的Pod,将它们的镜像拉取策略改为Always。在多团队共享集群上有用,可以确保仅具有合法凭证的用户才能拉取镜像。

如果没有该AC,则一旦镜像被拉取到Node上,任何用户、Pod都可以不提供凭证的使用之。

DefaultStorageClass

监控PersistentVolumeClaim对象的创建过程,如果PVC没有要求任何存储类,则为其自动添加。

DefaultTolerationSeconds

设置当Taint和Toleration不匹配时,默认能够容忍的最长时间。

DenyEscalatingExec

禁止对以特权级别(privileged Pod、能访问宿主机IPC名字空间的Pod、能访问宿主机PID名字空间的Pod)运行的Pod执行exec/attach命令。

EventRateLimit

Alpha/1.9,可以缓和APIServer被事件请求泛洪的问题。

ExtendedResourceToleration

1.9引入的插件,辅助带有扩展资源(GPU/FPGA...)的节点的创建,这类节点应该以扩展资源名为键配置Taints。

该控制器自动为请求扩展资源的Pod添加Tolerations,用户不需要手工添加。

ImagePolicyWebhook

允许后端的webhook给出准许决策。

Initializers

Alpha/1.7,根据现有的InitializerConfiguration来确定资源的初始化器。

InitialResources

监控Pod创建请求,如果容器没有提供资源用量请求/限制,该AC会根据基于相同镜像的容器的历史运行状况,自动为容器添加资源用量请求/限制。

LimitPodHardAntiAffinity

拒绝任何这样的Pod,它的AntiAffinity(requiredDuringSchedulingRequiredDuringExecution)拓扑键是kubernetes.io/hostname之外的值。

LimitRanger

监控入站请求,确保Namespace中LimitRange对象列出的任何约束条件不被违反。如果在部署中使用LimitRange,你必须启用该AC。

NamespaceAutoProvision

检查针对所有名字空间化的资源的请求,如果所引用的名字空间不存在,则自动创建名字空间。

NamespaceExists

检查针对所有名字空间化的资源的请求,如果所引用的名字空间不存在,则返回错误。

NamespaceLifecycle

具有以下功能:

  1. 确保正在删除的名字空间中,不会由新的对象被创建
  2. 确保针对不存在名字空间的资源创建请求被拒绝
  3. 禁止删除三个系统保留名字空间:default, kube-system, kube-public

Namespace删除操作会级联删除其内部的Pod、Service等对象,为了确保删除过程的完整性,应当使用此AC。

NodeRestriction

限制Kubelet能够修改的Pod、Node对象。

PersistentVolumeLabel

自动将云服务商定义的Region/Zone标签附加到PV。可以用于保证PV和Pod位于相同的Region/Zone。

PodNodeSelector

读取Namespace上的注解和全局配置,限制某个命名空间中的Pod能够运行在什么节点(通过nodeSelector)。

要启用此控制器,需要修改APIServer的配置/etc/kubernetes/manifests/kube-apiserver.yaml,在--enable-admission-plugins=后添加PodNodeSelector。

使用下面的注解,可以为命名空间中任何Pod添加默认nodeSelector:

YAML
1
2
3
4
5
6
apiVersion: v1
kind: Namespace
metadata
name: devops
annotations:
   scheduler.alpha.kubernetes.io/node-selector: k8s.gmem.cc/dedicated-to=devops

这样,此命名空间中创建的新Pod,都会具有nodeSelector:

YAML
1
2
nodeSelector
  k8s.gmem.cc/dedicated-to:devops
PodTolerationRestriction

此控制器首先校验Pod的容忍和命名空间的容忍,如果存在冲突则拒绝Pod创建。然后,它将命名空间的容忍合并到Pod的容忍中,合并后的结果进行命名空间容忍白名单检查,如果检查不通过则Pod被拒绝创建。

命名空间的容忍,使用注解scheduler.alpha.kubernetes.io/defaultTolerations配置,示例:

YAML
1
2
3
4
5
apiVersion: v1
kind: Namespace
metadata:
  annotations:
    scheduler.alpha.kubernetes.io/defaultTolerations: '[{"key":"k8s.gmem.cc/dedicated-to","operator":"Equal","value":"devops","effect":"NoSchedule"}]'

命名空间的容忍白名单,使用注解scheduler.alpha.kubernetes.io/tolerationsWhitelist配置。 此注解的值类似上面的注解。

PersistentVolumeClaimResize

打开特性开关ExpandPersistentVolumes后,你应该启用该AC。

此AC默认即用所有PVC的Resizing请求,除非PVC的存储类明确的启用(allowVolumeExpansion=true)

PodPreset

注入匹配的PodPreset中的规格字段。

Priority

使用priorityClassName字段并为Pod产生一个整数类型的优先级。如果目标priorityClass不存在则Pod被拒绝。

ResourceQuota

监控入站请求,确保没有违反Namespace中ResourceQuota对象所列出的资源配额限制。

如果你使用ResourceQuota对象,则必须启用此AC。

ServiceAccount

实现ServiceAccount的自动化。当使用ServiceAccount对象是应当启用此AC。

SecurityContextDeny

禁止那些尝试通过SecurityContext中某些字段实现权限提升的Pod。如果没有启用PodSecurityPolicy,应当考虑启用该控制器。

Service

K8S中的Pod是有生命周期的,当Pod死掉后它无法复活。某些控制器会动态的创建、删除Pod。尽管Pod会获得一个IP地址,但是随着时间的推进,IP也不能作为联系它的可靠手段,因为IP会改变。

那么,如果某个Pod(后端)集向其它Pod(前端)集提供功能,那么前端如何找到、跟踪后端集中的Pod呢?Service能帮助你实现这一需求。

所谓Service,是一个抽象,它定义了:

  1. 一个逻辑的Pod集合。常常通过标签选择器来锚定
  2. 访问这些Pod的策略

Service有时候也被称为微服务(micro-service)。

举个例子,假设有一个图像处理后端,包括三个实例。这些实例是无状态的,因而前端不关心它调用的是哪一个。组成后端的Pod实例可能发生变化(宕机、扩容),如果前端跟踪其调用的Pod显然会造成耦合。Service这个抽象层可以实现解耦。

对于K8S-Native应用程序,K8S提供了Endpoints API。每当Service包含的Pod集发生变化后,即更新端点。对于非K8S-Native的应用程序,K8S为Service提供了一个基于虚拟IP的桥,用于将请求重定向到后端的Pod。

定义服务

假设你有一组Pod,其标签为app=MyApp,并且都暴露了9376端口。你可以为它们定义如下服务:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  # 被服务管理的Pod
  selector:
    app: MyApp
  ports:
  - protocol: TCP
    # 服务的集群IP上的端口
    port: 80
    # Pod的端口,可以指定为Pod中定义的端口名
    targetPort: 9376

Service本身也被分配一个IP(称为集群IP),此IP会被服务代理使用。

Service的selector会被不断的估算,其匹配结果会被发送给与Service同名的Endpoints对象。

Service能够进行端口映射。针对服务的请求可以被映射到Pod的端口。支持的网络协议包括TCP、UDP两种。

不使用选择器的Service

尽管Service通常用来抽象对Pod的访问,它也可以用来抽象其它类型的后端,例如:

  1. 在生产环境下你希望使用外部数据库集群,但是在开发/测试时你希望使用自己的数据库
  2. 你希望把服务指向其它名字空间中的服务,甚至其它集群中的服务
  3. 你在把一部分工作负载迁移到K8S,而让另一部分在外部运行

以上情况下,你都可以定义一个没有选择器的Service。由于没有选择器,Endpoints对象不会被创建。你可能需要手工映射服务到端点:

YAML
1
2
3
4
5
6
7
8
9
10
kind: Endpoints
apiVersion: v1
metadata:
  name: my-service
# 端点列表,这个例子中流量被路由给1.2.3.4:9376
subsets:
  - addresses:
      - ip: 1.2.3.4
    ports:
      - port: 9376
ExternalName

这是一种特殊的Service,它即不选择端点,也不定义端口。它只用于提供集群外部的服务的别名,集群内的组件可以基于别名访问外部服务:

YAML
1
2
3
4
5
6
7
8
9
kind: Service
apiVersion: v1
metadata:
  name: database
  namespace: prod
spec:
  type: ExternalName
  # IP地址也支持
  externalName: db.gmem.cc

当集群内组件查找database.prod.svc.${CLUSTER_DOMAIN}时,集群的DNS服务会返回一个CNAME记录,其值为db.gmem.cc。 

注意:经过试验,externalName填写IP地址也是可以的,这种情况下,集群DNS服务应该是做了一个A记录。

虚拟IP和服务代理

K8S集群中的每个节点都运行着kube-proxy组件。该组件负责为服务(除了ExternalName)提供虚拟IP。

在1.0版本,Service属于第四层(TCP/UDP over IP)构造,kube-proxy完全工作在用户空间。从1.1开始引入了Ingress API,用于实现第七层(HTTP)Service。基于iptables的代理也被添加,并从1.2开始成为默认操作模式。从1.9开始,ipvs代理被添加。

userspace模式

此模式下,kube-proxy监控Master,关注Service、Endpoints对象的添加和删除。

对于每个Service,KP会在本地节点随机打开一个代理端口,任何发向此端口的的请求会被转发给服务的某个后端Pod(从Endpoints对象中获取)。到底使用哪个Pod,取决于Service的SessionAffinity设置,默认选取后端的算法是round robin。KP会把转发规则写入到iptables中,捕获Service的集群IP(虚拟的)的流量。

这种模式下,需要在内核空间和用户空间传递流量。

iptables模式

此模式下,KP会直接安装iptables规则,捕获Service的集群IP + Service 端口的流量,转发给某个Service后端Pod,选择Pod的算法是随机。

这种模式下,不需要在内核空间和用户空间之间切换,通常比userspace模式更快、更可靠。但是,该模式不支持当第一次选择的Pod不响应后重新选择另外一个Pod,因此需要配合Readness探针使用。

ipvs模式

关于IPVS的知识参考:IPVS和Keepalived

在1.9中处于Beta状态。

此模式下,KP会调用netlink网络接口,创建和K8S的Service/Endpoint对应的ipvs规则,并且周期性的将K/E和ipvs规则同步。当访问Service时,流量被转发给某个Pod。

类似于iptables,ipvs基于netfilter钩子函数,但是它使用哈希表作为底层数据结构,且运行在内核态。这意味着ipvs的流量转发速度会快的多,且同步代理规则时性能更好。

在未来的版本中,ipvs会提供更多的负载均衡算法,包括:

  1. rr:循环选择
  2. lc:最少连接(least connection)
  3. dh:目的地哈希(destination hashing)
  4. sh:源哈希(destination hashing)
  5. sed:最短预期延迟(shortest expected delay)
  6. nq:绝不排队(never queue)

注意,该模式要求节点上安装了IPVS内核模块。如果该模块没有安装,会自动fallback为iptables模式。

基于IPVS的K8S Service,都是使用NAT模式,原因是,只有NAT模式才能进行端口映射,让Sevice端口和Pod端口不一致。

IPVS模式下,客户端角色是当前节点(中的Pod,或者节点本身),Direct角色是当前节点,Real Server是当前或其它节点上的Pod。不同客户端,对应的网络路径不一样。IPVS模式仍然需要iptables规则进行一些配合。

考虑Service 10.96.54.11:8080,Pod 10.244.1.2:8080,基于Flannel构建容器网络,当前flannel.1的IP地址为10.244.0.0:

  1. 当前节点出现一个虚拟网络接口 kube-ipvs0,所有Service IP都绑定在上面,这是IPVS所要求的,Direct必须具有VIP。这也导致了IPVS模式下,VIP可Ping(iptables模式下不可以)
  2. OUTPUT链被添加如下规则:
    Shell
    1
    2
    3
    4
    5
    6
    7
    8
    9
    -A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
     
    # 如果源IP不是Pod IP,且目的IP是K8S Service(所有Service IP放在IPSet中,大大减少iptables规则条目数量)
    -A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment
      --comment "Kubernetes service cluster ip + port for masquerade purpose"
    #                                               则打上标记
      -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ
     
    -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000

    不是Pod发出的对Service IP的请求,打标记0x4000/0x4000的目的是,后续进行SNAT。因为节点自身(而非其中的Pod)对VIP进行访问时,因为VIP就在本机网卡,所以它自动设置IP封包的源地址、目的地址均为10.96.54.11,这样的封包不做SNAT,仅仅DNAT到Pod上,回程报文是发不回来的(因为每个节点都具有所有VIP)

  3. POSTROUTING链被添加如下规则:
    Shell
    1
    2
    3
    4
    -A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
     
    -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT"
        -m mark --mark 0x4000/0x4000 -j MASQUERADE

    如第2条所属,需要作SNAT, MASQUERADE意味着源地址取决于封包出口网卡

为了更深入的理解,可以看看地址映射的过程,在节点上访问curl http://10.96.54.11:8080时:

  1. 产生如下封包: 10.96.54.11:xxxxx -> 10.96.54.11:8080。原因是目标地址在kube-ipvs0这个本机网卡上
  2. 经过IPVS模块,选择一个RIP,进行DNAT:10.96.54.11:xxxxx -> 10.244.2.2:8080
  3. 经过OUTPUT、POSTROUTING链,进行SNAT:10.244.0.0:xxxxx -> 10.244.2.2:8080 。10.244.0.0是出口网卡flannel.1的地址,封包发给此网卡后,经过内核包装为vxlan UDP包,再由物理网卡发出去
IP和VIP

Pod具有普通的IP地址,因为针对此IP的请求被路由到固定的位置。

Service的IP则是虚拟的 —— 它不会由某台固定的主机来应答。K8S使用Linux的iptables来定义透明、按需进行重定向的VIP。当客户端连接到VIP时,流量会透明的传输给适当的端点。Service相关的环境变量、DNS条目使用的都是Service的VIP和端口。

小结

不管使用哪种代理模式,均保证:

  1. 任何从Service的集群IP:Port进入的流量,都被转发给适当的后端Pod。客户端不会感知到K8S、Service、Pod这些内部组件的存在
  2. 要实现基于客户端IP的会话绑定(session affinity ),可以设置service.spec.sessionAffinity = ClientIP (默认None)。你可以进一步设置service.spec.sessionAffinityConfig.clientIP.timeoutSeconds来指定会话绑定的最长时间(默认10800)
IPVS

由于Iptables底层数据结构是链表,而IPVS则是哈希,IPVS比起iptables模式显著提高的性能,几乎不受服务规模增大的影响。现在的集群都使用IPVS模式的服务代理。

IPVS支持三种工作模式:DR(直接路由)、Tunneling(ipip)、NAT(Masq)。其中只有NAT模式支持端口映射(ClusterIP端口和Pod端口不一样),因此K8S使用NAT模式。此外,K8S还在NAT的DNAT(将对服务的请求的目的地址改为Pod IP)的基础上,进行SNAT(将对服务的请求的源地址改为入站网络接口的地址,否则Pod直接发送源地址为Pod IP的回程报文,客户端不接受),尽管fullNAT这种IPVS扩展可以支持DNAT+SNAT。

实现原理

IPVS是在INPUT链上挂了钩子,运行复杂的负载均衡算法,然后执行DNAT后从FORWARD链离开本机网络栈。

让网络包进入INPUT链有两种方式:

  1. 将虚IP写到内核路由表中
  2. 创建一个Dummy网卡,将续IP绑定到此网卡

Kube Proxy使用的是第二种方式。一旦Service对象创建,Kube proxy就会:

  1. 确保Dummy网卡kube-ipvs0存在
  2. 将服务的虚IP绑定给kube-ipvs0
  3. 通过socket创建IPVS的virtual server。virtual server和service是N:1关系,原因是Service可能有多个虚拟IP,例如LoadBalancer IP + Cluster IP

当Endpoint对象创建后,Kube Proxy会:

  1. 创建IPVS的real server。real server和endpoint是1:1关系
关于iptables

即使使用IPVS模式,只能解决流量转发的问题。Kube Proxy的其它问题仍然依靠 Iptables 解决,它在以下情况下依赖Iptables:

  1. 如果配置启动参数 --masquerade--all=true,也就是所有经过kube-proxy的包都进行一次SNAT
  2. 启动参数指定了集群IP地址范围
  3. 对于LoadBalancer类型的服务,需要Iptables配置白名单
  4. 对于NodePort类型的服务,用于在封包从入站节点发往其它节点的Pod时进行MASQUERADE
关于ipset

Kube Proxy使用ipset来减少需要创建的iptables规则,IPVS模式下iptables规则的数量不超过5个 

多端口服务

很多服务需要暴露多个端口,K8S支持在Service的规格中指定多个端口:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  # 端口1
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  # 端口2
  - name: https
    protocol: TCP
    port: 443
    targetPort: 9377
指定IP地址

在创建Service时,你可以指定你想要的集群IP,设置spec.clusterIP字段即可。

你选择的IP地址必须在service-cluster-ip-range(APIServer参数)范围内,如果IP地址不合法,APIServer会返回响应码422。

发现服务

K8S提供两种途径,来发现一个服务:环境变量、DNS。

基于环境变量

当Pod运行时,Kubelet会以环境变量的形式注入每个活动的服务的信息。环境变量名称格式为{SVCNAME}_SERVICE_HOST、{SVCNAME}_SERVICE_PORT。其中SVCNAME是转换为大写、-转换为_的服务名。

例如服务redis-master,暴露TCP端口6379,获得集群IP 10.0.0.11。它会产生以下环境变量:

Shell
1
2
3
4
5
6
7
REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11

需要注意:服务必须在Pod之前就创建好,否则Pod的环境变量无法更新。 

DNS

K8S提供了DNS Addon。此DNS服务器会监控新创建的Service,并为其提供DNS条目。集群中所有Pod都可以使用该DNS服务。

位于名字空间my-ns中的服务my-svc,创建的DNS条目是my-svc.my-ns。位于名字空间my-ns中的Pod可以直接通过my-svc访问服务,其它名字空间中的Pod则需要使用my-svc.my-ns访问服务。DNS名称解析的结果是服务的Cluster IP。

要访问ExternalName类型的Service,DNS是唯一的途径。

Headless Service

某些情况下,你不需要Service提供的负载均衡功能,也不需要单个Service IP。这种情况下你可以创建Headless Service,其实就是设置spec.clusterIP=None。

使用HS,开发者可以减少和K8S的耦合,自行实现服务发现机制。

HS没有Cluster IP,kube-proxy也不会处理这类服务,K8S不会为这类服务提供负载均衡机制。

DNS记录如何创建,取决于HS有没有配置选择器。

有选择器

如果定义了选择器,端点控制器为HS创建Endpoints对象。并且修改DNS配置,返回A记录(地址),直接指向HS选择的Pod。

无选择器

端点控制器不会为这类Service创建Endpoints对象,但是DNS系统会配置:

  1. 为ExternalName类型的Service配置CNAME记录
  2. 对于其它类型的Service,为和Service共享名称的任何Endpoints创建一条DNS记录

对于无选择器的HS,不会自动创建关联的Endpoints,因此我们有机会手工的关联Endpoints,从而实现:

  1. 指向一个集群外部的数据库
  2. 指向其它namespace或集群的服务

示例:

YAML
1
2
3
4
5
6
7
8
9
kind: Endpoints
apiVersion: v1
metadata:
  name: my-service
subsets:
- addresses:
  - ip: 1.2.3.4
  ports:
  - port: 1234

Endpoint的地址不能是Loopback、link-local、link-local多播地址。 

发布服务

对于应用程序中的某些部分(前端),你通常需要在外部(集群外部)IP上暴露一个服务。

K8S允许你指定为服务指定类型(ServiceTypes):

类型 说明
ClusterIP

在集群内部的IP地址上暴露服务,如果你仅希望在集群内部能访问到服务,则使用该类型

这是默认的类型

NodePort

在所有节点的IP上的一个静态端口上暴露服务。K8S会自动创建一个ClusterIP类型的服务,用于把NodePort类型的服务路由到适当的Pod

外部可以将任何Node作为入口,访问此类型的服务

K8S会自动分配一个范围内的端口(默认30000-32767),每个节点都会转发到此端口的请求。此端口表现在spec.ports[*].nodePort字段中

如果你需要手工指定端口,设置nodePort的值即可。手工指定的值必须在允许的端口范围之内,你还需要注意可能存在的端口冲突。建议自动分配端口

服务可以通过以下两种方式访问到:

  1. :spec.ports[*].nodePort
  2. spec.clusterIP:spec.ports[*].port

NodePort是丐版的LoadBalancer,可供外部访问且成本低廉。但是在大规模集群上

LoadBalancer

向外部暴露服务,并使用云服务提供的负载均衡器。外部负载均衡器所需要的NodePort,以及ClusterIP会自动创建

服务的EXTERNAL-IP字段为云提供商的负载均衡器的IP

某些云服务(GCE、AWS等)提供负载均衡器,K8S支持和这种外部LB集成:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 9376
  clusterIP: 10.0.171.239
  # 某些云服务允许手工指定LB的IP地址。如果不指定则随机选择,如果指定了但是云服务不支持则忽略该字段
  loadBalancerIP: 78.11.24.19
  type: LoadBalancer
  loadBalancerSourceRanges:
  # 允许哪些地址访问此服务,此字段可以随时更新
  - 1.1.1.1/32
status:
  # 负载均衡器会异步的创建,其信息会发布到如下字段:
  loadBalancer:
    ingress:
    - ip: 146.148.47.155
ExternalName 将外部服务映射为别名

注:修改服务的类型,不会导致ClusterIP重新分配。

外部IP

如果你拥有一个或多个外部(例如公网)IP路由到集群的某些节点,则Service可以在这些外部IP上暴露。基于这些IP流入的流量,如果其端口就是Service的端口,会被K8S路由到服务的某个端点(Pod ...)上。

例如下面这个服务:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  externalIPs:
  - 80.11.12.10

外部客户端可以通过80.11.12.10:80访问服务,流量会被转发给app=MyApp的Pod的9376端口。 

局部性访问

当访问NodePort、LoadBalancer类型的Service时,流量到达节点后,可能再被转发到其它节点,从而增加了一跳。

要避免这种情况,可以设置:

YAML
1
2
3
4
kind: service
spec:
  #                默认  Cluster
  externalTrafficPolicy: Local

这样,K8S会强制将流量发往具有Service后端Pod的端点。注意ClusterIP类型的服务不支持这种局部性访问配置。

Endpoints

服务的端点,通常情况下对应到Pod的一个端口。带有选择器的Service通常自动管理Endpoint对象,你也可以定义不使用选择器的服务,这种情况下需要手工管理端点,Endpoints对象的名字和Service的名字要一致,位于相同命名空间。

EndpointSlice

所有网络端点都保存在同一个 Endpoints 资源中,这类资源可能变得非常巨大,进而影响控制平面组件性能,EndpointSlice可以缓解这一(大量端点)问题。此外涉及如何路由内部流量时,EndpointSlice 可以充当 kube-proxy 的决策依据。 

默认情况下,单个端点切片最多包含100个端点,可以通过控制器管理器的标记 --max-endpoints-per-slice来定制。

下面是端点切片对象的例子:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: discovery.k8s.io/v1beta1
kind: EndpointSlice
metadata:
  name: example-abc
  labels:
    kubernetes.io/service-name: example
# 端点的地址类型:支持IPv4  IPv6  FQDN
addressType: IPv4
# 端口列表,适用于切片中所有端点
ports:
  - name: http
    protocol: TCP
    port: 80
endpoints:
  - addresses:
    - "10.1.2.3"
    # EndpointSlice存储了可能对使用者有用的、有关端点的状况。 这三个状况分别是
    #   ready        对应Pod的Ready状态,运行中的Pod的Ready为True
    #   serving      如果Pod处于终止过程中则Ready不为True,此时应该查看serving获知Pod是否就绪
    #                例外是spec.publishNotReadyAddresses的服务,这种服务的端点的Ready永远为True
    #   terminating
    conditions:
      ready: true
    hostname: pod-1
    # 拓扑感知,1.20废弃
    topology:
      kubernetes.io/hostname: node-1   # 未来使用nodeName字段代替
      topology.kubernetes.io/zone: us-west2-a
管理

通常情况下,由端点切片控制器自动创建、管理 EndpointSlice 对象。EndpointSlice 对象还有一些其他使用场景, 例如作为服务网格的实现。这些场景都会导致有其他实体 或者控制器负责管理额外的 EndpointSlice 集合。

为了确保多个实体可以管理 EndpointSlice 而且不会相互产生干扰,Kubernetes 定义了 标签 endpointslice.kubernetes.io/managed-by,用来标明哪个实体在管理某个 EndpointSlice。端点切片控制器会在自己所管理的所有 EndpointSlice 上将该标签值设置 为 endpointslice-controller.k8s.io。 管理 EndpointSlice 的其他实体也应该为此标签设置一个唯一值。

所属服务

在大多数场合下,EndpointSlice 都由某个 Service 所有,这一属主关系是通过:

  1. 为每个 EndpointSlice 设置一个 owner引用
  2. 设置 kubernetes.io/service-name 标签来标明的,目的是方便查找隶属于某服务的所有 EndpointSlice
切片分布

每个 EndpointSlice 都有一组端口值,适用于资源内的所有端点。 当为服务使用命名端口时,Pod 可能会就同一命名端口获得不同的端口号,因而需要 不同的 EndpointSlice。

控制面尝试尽量将 EndpointSlice 填满,不过不会主动地在若干 EndpointSlice 之间 执行再平衡操作。这里的逻辑也是相对直接的:

  1. 列举所有现有的 EndpointSlices,移除那些不再需要的端点并更新那些已经变化的端点
  2. 列举所有在第一步中被更改过的 EndpointSlices,用新增加的端点将其填满
  3. 如果还有新的端点未被添加进去,尝试将这些端点添加到之前未更改的切片中, 或者创建新切片

这里比较重要的是,与在 EndpointSlice 之间完成最佳的分布相比,第3步中更看重限制 EndpointSlice 更新的操作次数。例如,如果有 10 个端点待添加,有两个 EndpointSlice 中各有 5 个空位,上述方法会创建一个新的 EndpointSlice 而不是 将现有的两个 EndpointSlice 都填满。换言之,与执行多个 EndpointSlice 更新操作 相比较,方法会优先考虑执行一个 EndpointSlice 创建操作。

由于 kube-proxy 在每个节点上运行并监视 EndpointSlice 状态,EndpointSlice 的 每次变更都变得相对代价较高,因为这些状态变化要传递到集群中每个节点上。 这一方法尝试限制要发送到所有节点上的变更消息个数,即使这样做可能会导致有 多个 EndpointSlice 没有被填满。

在实践中,上面这种并非最理想的分布是很少出现的。大多数被 EndpointSlice 控制器 处理的变更都是足够小的,可以添加到某已有 EndpointSlice 中去的。并且,假使无法添加到已有的切片中,不管怎样都会快就会需要一个新的 EndpointSlice 对象。 Deployment 的滚动更新为 EndpointSlice重新打包提供了一个自然的机会,所有 Pod 及其对应的端点在这一期间都会被替换掉。

重复的端点

由于 EndpointSlice 变化的自身特点,端点可能会同时出现在不止一个 EndpointSlice 中。鉴于不同的 EndpointSlice 对象在不同时刻到达 Kubernetes 的监视/缓存中, 这种情况的出现是很自然的。 使用 EndpointSlice 的实现必须能够处理端点出现在多个切片中的状况。 关于如何执行端点去重(deduplication)的参考实现,你可以在 kube-proxy 的 EndpointSlice 实现中找到。

DNS支持

K8S支持在集群中调度DNS Service/Pod,并且让Kubelet告知每个容器,DNS服务器的IP地址。

集群中的每个服务,包括DNS服务本身,都被分配一个DNS名称。

默认的,Pod的DNS搜索列表会包含Pod自己的名字空间,以及集群的默认Domain。如果一个服务foo运行在名字空间bar中,则位于bar中的Pod可以通过foo引用服务,位于其它名字空间中的Pod则需要通过foo.bar引用服务。

Service
A记录

普通服务被分配以A记录,格式:my-svc.my-namespace.svc.cluster.local,此记录解析到服务的Cluster IP。

Headless服务(无Cluster IP)也被分配如上的A记录,但是记录解析到服务选择的Pod的IP集(如果没有选择器则解析到什么IP取决于你给出的Endpoints配置),客户端应当使用此IP集或者使用Round-Robin方式从IP集中选取IP。

SRV记录

这类记录为服务的知名端口分配,SRV记录格式:_my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local。

对于普通服务,SRV记录解析为端口号 + CNAME(my-svc.my-namespace.svc.cluster.local)。

对于Headless服务,SVR记录解析到多个答复,每个对应服务选择的Pod:端口号 + Pod的CNAME(auto-generated-name.my-svc.my-namespace.svc.cluster.local)。

Pods
A记录

当启用后,Pod被分配一个DNS A记录:pod-ip-address.my-namespace.pod.cluster.local。例如:

Shell
1
2
# 一个名为influx的Pod,它位于dev名字空间中,IP地址为172.27.0.20
172-27-0-20.dev.pod.k8s.gmem.cc
主机名/子域

Pod被创建后,其hostname为metadata.name字段的值。

在Pod的Spec中你可以:

  1. 指定可选的hostname字段,可以手工指定hostname,比metadata.name的优先级高
  2. 指定可选可选的subdomain字段,说明指定Pod的子域。例如一个Pod的hostname为influxdb,subdomain为pods,名字空间为default,则Pod的全限定名为influxdb.default.dev.svc.k8s.gmem.cc

如果在Pod所在名字空间中,存在一个Headless服务,其名称与Pod的subdomain字段一致。则KubeDNS服务器会为Pod的全限定名返回一个A记录,指向Pod的IP地址。

DNS策略

你可以为每个Pod定义DNS策略(在规格中使用dnsPolicy字段)。目前K8S支持以下策略:

策略 说明
Default 从Pod所在节点继承DNS解析设置
ClusterFirst 任何不匹配集群DNS后缀(可以设置)的查询,转发给上游(从节点继承)DNS处理
ClusterFirstWithHostNet 运行在hostNetwork中的Pod应当明确的设置为该值
None 1.9 Alpha,忽略来自K8S环境的DNS设置。所有DNS设置从Pod规格的dnsConfig字段获取
DNS配置

1.9 Alpha。用户可以对Pod的DNS设置进行细粒度控制。要启用该特性,集群管理员需要为APIServer、kubelet启用特性开关(Feature Gate)CustomPodDNS,示例:--feature-gates=CustomPodDNS=true,...  从1.14开始默认开启。

当启用上述特性后,用户可以设置Pod规格的dnsPolicy=None,并添加dnsConfig字段:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: v1
kind: Pod
metadata:
  namespace: ns1
  name: dns-example
spec:
  containers:
    - name: test
      image: nginx
  dnsPolicy: "None"
  dnsConfig:
    # 此Pod使用的DNS服务器列表
    nameservers:
      - 1.2.3.4
    # DNS搜索后缀列表
    searches:
      - ns1.svc.cluster.local
      - my.dns.search.suffix
    options:
      # 最大dot数量。最大值15
      - name: ndots
        value: "1"
      # 重试另外一个DNS服务器之前,等待应答的最大时间,单位秒。最大值30
      - name: timeout
        value: "5"
      # 放弃并返回错误给调用者之前,最大尝进行DNS查询的次数。最大值5
      - name: attempts
        value: "2"
      # 使用round-robin风格选择不同的DNS服务器,向其发送查询
      - name: rotate
      # 支持RFC 2671描述的DNS扩展
      - name: edns0
      # 串行发送A/AAAA请求
      - name: single-request
      # 发送第二个请求时重新打开套接字
      - name: single-request-reopen
      # 使用TCP协议发送DNS请求
      - name: use-vc
      # 禁止自动重新载入修改后的配置文件,需要glibc 2.26+
      - name: no-reload
网络
连接应用和服务

本节以Nginx服务为例,说明如何在K8S集群中连接应用程序和服务。

K8S网络模型

在讨论K8S的通信机制之前,我们先看一下普通的Docker网络如何运作。

默认情况下,Docker使用Host-private的网络,也就是说Docker容器仅能和同一台宿主机上的其它容器通信。为了让容器跨节点通信,它们必须在宿主机上暴露端口。这意味着,开发者需要仔细规划,让多个容器协调暴露宿主机的不同端口。

跨越多名开发者进行端口协调很麻烦,特别是进行扩容的时候,这种协调工作应该由集群负责,而不应该暴露给用户。

K8S假设所有Pod都是需要相互通信的,不管它们是不是位于同一台宿主机上。K8S为每个Pod分配集群私有IP地址,你不需要向Docker那样,为容器之间创建连接,或者暴露端口到宿主机的网络接口。

Pod内部的多个容器,可以利用localhost进行相互通信。

集群内所有Pod中的容器,都可以相互看到,不需要NAT的介入。

暴露Pod到集群

考虑下面的Pod规格:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80

你可以从集群的任何节点访问此Pod,执行下面的命令获得Pod的IP地址:

Shell
1
2
3
4
kubectl get pods -l run=my-nginx -o wide
# NAME                        READY     STATUS    RESTARTS   AGE       IP            NODE
# my-nginx-3800858182-jr4a2   1/1       Running   0          13s       10.244.3.4    kubernetes-minion-905m
# my-nginx-3800858182-kna2y   1/1       Running   0          13s       10.244.2.5    kubernetes-minion-ljyd

登陆到任何集群节点后,你都可以访问上面的两个IP地址。需要注意,Pod不会使用节点的80端口,也不会使用任何NAT机制来把流量路由到Pod。这意味着你可以在单个节点上运行containerPort相同的多个Pod。

你可以像Docker那样,把容器端口映射到节点端口,单由于K8S网络模型的存在,这种映射往往没有必要。

创建服务

现在,我们有了两个Pod,它们运行着Nginx,IP位于一个扁平的、集群范围的地址空间中。 理论上说,应用程序可以和Pod直接通信,但是如何Pod所在节点宕机了会怎么样?Deployment会自动在另外一个节点上调度一个Pod,这个新的Pod的IP地址会改变。如果直接和Pod 通信,你就要处理这种IP地址可能改变的情形。

服务可以避免上述处理逻辑,它是Pod之上的抽象层,它定义了一个逻辑的Pod集,这些Pod提供一模一样的功能。当创建服务时,它被分配以唯一性的IP地址(Cluster IP),在服务的整个生命周期中,该IP都不会变化。

应用程序可以配置和服务(而非Pod)通信,服务会进行负载均衡处理,把请求转发给某个Pod处理。

针对上述Deployment的Service规格:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
    # 服务暴露的端口
  - port: 80
    # 对接到Pod的端口
    targetPort: 80
    protocol: TCP
  selector:
    run: my-nginx

创建服务后,可以查询其状态:

Shell
1
2
3
kubectl get svc my-nginx
# NAME       CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
# my-nginx   10.0.162.149           80/TCP    21s

服务的背后是一个Pod集,服务的选择器会不断的估算,来选择匹配的Pods。匹配的Pod会被发送到一个Endpoints对象中,该对象的名字也叫my-nginx(和服务同名)。当Pod死去时它会自动从Endpoints中移除,类似的当新Pod出生后也被加入到Endpoints中。 

下面的命令可以获得端点的信息:

Shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 获得服务关联的端点
kubectl describe svc my-nginx
# Name:                my-nginx
# Namespace:           default
# Labels:              run=my-nginx
# Annotations:        
# Selector:            run=my-nginx
# Type:                ClusterIP
# IP:                  10.0.162.149
# Port:                 80/TCP
# Endpoints:           10.244.2.5:80,10.244.3.4:80
# Session Affinity:    None
# Events:              
 
# 获得端点信息
kubectl get ep my-nginx
# NAME       ENDPOINTS                     AGE
# my-nginx   10.244.2.5:80,10.244.3.4:80   1m

注意:端点的IP/Port就是Pod的IP/Port。

现在,你可以在集群中任何节点上 wget :,再次强调,集群IP是纯粹的虚拟IP,它不会对应到哪一根网线。 

访问服务 

K8S提供了两种服务访问方式:环境变量、DNS。

变量变量方式是开箱即用的,当Pod启动时,Kubelet会为其注入一系列的环境变量。这意味着在Pod创建之后才创建的服务,无法注入为环境变量。

K8S有一个DNS Addon,负责提供集群内的域名服务,执行下面的命令了解此服务是否启用:

Shell
1
2
3
4
# 服务名为kube-dns,运行在kube-system名字空间
kubectl get services kube-dns --namespace=kube-system
# NAME       CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
# kube-dns   10.0.0.10            53/UDP,53/TCP   8m

如果没有运行DNS服务,你需要启用它。 

DNS服务运行后,会为每个服务都分配一个域名,映射到它的集群IP。

服务安全

现在,在集群内部的任何节点,都可以正常访问服务,而且可以享受K8S提供的负载均衡、HA了。

在把服务暴露到因特网之前,你需要确保信道的安全性。例如:

  1. 提供数字证书,要么购买要么自签名
  2. 配置Nginx的HTTPS
  3. 配置一个Secret,让Pod都能访问证书 

创建密钥和自签名证书,可以参考:

Shell
1
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /d/tmp/nginx.key -out /d/tmp/nginx.crt -subj "/CN=nginxsvc/O=nginxsvc" 

K8S提供了一些examples,可以简化第三步。执行下面的命令下载样例:

Shell
1
git clone https://github.com/kubernetes/examples.git

下面使用examples中的工具,根据上面生成的密钥,产生一个Secret的规格文件:

Shell
1
2
3
# make和go必须已经安装
# 实际通过调用 make_secret.go 产生Secret,后者则是调用K8S的API
make keys secret KEY=/tmp/nginx.key CERT=/tmp/nginx.crt SECRET=/tmp/secret.json

使用规格文件,在K8S上创建一个Secret对象:

Shell
1
2
kubectl create -f /tmp/secret.json
# secret "nginxsecret" created

查看Secret对象列表:

Shell
1
2
3
4
kubectl get secrets
# NAME                  TYPE                                  DATA      AGE
# default-token-il9rc   kubernetes.io/service-account-token   1         1d
# nginxsecret           Opaque                                2         1m

下一步,修改Deployment、Service的规格:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Deployment规格
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 1
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      # 声明一个卷,引用Secret
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      containers:
      - name: nginxhttps
        image: bprashanth/nginxhttps:1.0
        ports:
        - containerPort: 443
        - containerPort: 80
        # 挂载Secret卷到对应位置
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume
 
# Service规格
apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 80
    protocol: TCP
    name: http
  - port: 443
    protocol: TCP
    name: https
  selector:
    run: my-nginx
暴露服务

要把服务暴露到集群外部的IP地址,可以使用NodePorts或者LoadBalancers。 

Ingress

这是一个API对象,可以管理到集群内部服务(通常是HTTP)的访问。Ingress能够提供:负载均衡、SSL Termination、基于名称的虚拟主机服务。

何为Ingress

典型情况下,Service/Pod的IP地址仅仅能在集群内部路由到,所有流量到达边缘路由器(Edge Router,为集群强制应用了防火墙策略的路由器,可能是云服务管理的网关,也可能是物理硬件)时要么被丢弃,要么被转发到别的地方。

Ingress(入口)是一个规则集,允许进入集群的连接到达对应的集群服务。

要定义Ingress对象,同样需要经过APIServer。一个Ingress控制器负责处理Ingress。

前提条件

Ingress从1.1+开始可用,目前处于Beta版本。

要使用Ingress,你需要配备Ingress控制器。你可以在Pod中部署任意数量的自定义Ingress控制器,你需要为每个控制器标注适当的class。

Ingress规格
YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    ingress.kubernetes.io/rewrite-target: /
# 规格中包含了创建LB或代理服务器所需的全部信息
spec:
  # 匹配入站请求的规则列表
  rules:
  # 目前仅支持HTTP规则
  - http:
      # 将来自任何域名的/testpat这个URL下的请求,都转发给test服务处理
      host: *
      paths:
      - path: /testpath
        backend:
          serviceName: test
          servicePort: 80
Ingress控制器

注意:Ingress控制器可以响应不同名字空间中的多个Ingress。

为了让Ingress对象能生效,集群中必须存在Ingress控制器。Ingress实际上仅仅声明了转发规则,没有能执行转发的程序。

大部分控制器都是kube-controller-manager这个可执行程序的一部分,并且随着集群的创建而自动启动。但是Ingress控制器不一样,它作为Pod运行,你需要根据集群的需要来选择一个Ingress控制器实现,或者自己实现一个。

Ingress Controller的实现非常多,大多以Nginx或Envoy为基础。可以参考文章:Comparing Ingress controllers for Kubernetes。

K8S目前实现并维护GCE和nginx两个Ingress控制器。下面是基于Nginx的示例:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# 默认后端,如果Nginx无处理请求,此后端负责兜底
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: nginx-ingress
    chart: nginx-ingress-0.14.1
    component: default-backend
  name: nginx-ingress-default-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-ingress
      component: default-backend
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx-ingress
        component: default-backend
    spec:
      containers:
      - image: k8s.gcr.io/defaultbackend:1.3
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 30
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 5
        name: nginx-ingress-default-backend
        ports:
        - containerPort: 8080
          protocol: TCP