# SUMMARY

* [SUMMARY](/)

## Docker

* [理论概述](/docker/chapter1)
* [安装入门](/docker/chapter2)
* [配置说明](/docker/chapter3)
* [基础命令](/docker/chapter4)
* [镜像构建](/docker/chapter5)
* 镜像存储
  * [OverlayFS 存储驱动](/docker/jing-xiang-cun-chu/docker-overlayfs)
  * [Habor 安装和升级标注](/docker/jing-xiang-cun-chu/harbor)
* Compose
  * [Compose 概览](/docker/compose/docker-compose-overview)
  * [Compose 安装](/docker/compose/docker-compose-install)
  * [Compose 入门](/docker/compose/docker-compose-getting-started)
  * [Compose 环境变量](/docker/compose/docker-compose-envs)
  * [Compose 服务扩展](/docker/compose/docker-compose-extends)
  * [Compose 网络](/docker/compose/docker-compose-network)
  * [Compose 生产实践](/docker/compose/docker-compose-production)
  * [Compose 启动顺序控制](/docker/compose/docker-compose-startup-order)

## Kubernetes

* [架构概览](/kubernetes/arch)
* [基础术语](/kubernetes/concepts)
* [集群构建](/kubernetes/install)
* [工作负载](/kubernetes/workload)
  * [Deployments](/kubernetes/workload/concepts-deployments)
  * [StatefulSets](/kubernetes/workload/concepts-statefulsets)
  * [Volumes](/kubernetes/workload/concepts-volumes)
  * [Persistent Volumes](/kubernetes/workload/concepts-pv)
* 集群调度
  * [亲和性和反亲和性](/kubernetes/ji-qun-tiao-du/assigning-pods-to-nodes)
  * [污点和容忍机制](/kubernetes/ji-qun-tiao-du/taint-and-toleration)
* 集群组件
  * [Kubelet](/kubernetes/ji-qun-zu-jian/kubelet)
* 网络方案
  * [网络策略](/kubernetes/wang-luo-fang-an/network-policies)
  * [~~Calico BGP 网络（v2.6.x）~~](/kubernetes/wang-luo-fang-an/calico)
  * [Kubelet CNI 源码解析](/kubernetes/wang-luo-fang-an/src-kubelet-cni)
* client-go
  * [client-go 背后机制](/kubernetes/client-go/controller-client-go)
* [Helm](/kubernetes/helm)
  * [Helm 架构](/kubernetes/helm/helm-arch)
  * [Helm 快速上手](/kubernetes/helm/helm-quickstart)
  * [Helm 使用](/kubernetes/helm/helm-using)
  * [Helm 命令](/kubernetes/helm/helm-command)
* [Google 大规模集群管理器 Borg](/kubernetes/borg)


# 理论概述

可能现在称 Docker 为 `Moby` 比较合适，2017 年 4 月 Github [docker](https://github.com/docker/docker) 项目已经正式改名为 [moby](https://github.com/moby/moby)。至于个中缘由，可以通过 [对于 Docker 改名 Moby ，大家怎么看？](https://www.zhihu.com/question/58805021) 做进一步了解，此处不过多说明，本文继续沿用 Docker。

说起 Docker 不得不提到传统的 VM 虚拟化。传统虚拟机实现资源隔离的方法是利用独立的 OS，并利用 `Hypervisor` 虚拟化 CPU、内存、IO 设备等。Docker 基于容器技术的轻量级虚拟化，相对于传统的虚拟化技术，省去了 `Hypervisor` 层的开销，而且其虚拟化技术是基于内核的 `Cgroup` 和 `Namespace` 技术，处理逻辑与内核深度融合。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvkbIdpDmmw3NGa5V%2Fdocker-vs-vm.png?generation=1567863282896373\&alt=media)

容器本身不是一个新技术，早期的 Linux 容器是基于 LXC 去管理的，而 Docker 让容器变得更易用。对于 Docker，可以认为它是一个开源的容器引擎，可以方便的对容器进行管理，并且通过镜像交付的方式，达到更简单的环境构建，理念就是 “Build, Ship, and Run Any App, Anywhere”。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvkbKUxKZOwYBrzmV%2Fdocker-build-ship.png?generation=1567863288515970\&alt=media)

## Docker 名词介绍

### Docker 镜像

Docker 镜像是 Docker 容器运行时的只读模板，每一个镜像由一系列的层 (layers) 组成。按照官方说明，镜像是一个轻量级，独立的，可执行的，包括软件运行一切所需，囊括了代码，运行态，lib 库以及环境变量和配置文件的包。通俗的理解，可以理解为一个封装好环境的集装箱。

### Docker 容器

容器是通过 Docker 镜像创建的一个运行态的实例，可以针对 Docker 容器执行运行、开始、停止、移动和删除等操作。

### Docker Registry

`Registry` 用来存放 Docker 镜像，如果把 Docker 镜像比作集装箱的话，那么 `Registry` 可比喻成装载集装箱的大货轮。`Registry` 有公有和私有的概念，Docker 官方 `Registry` 为 [Docker Hub](https://hub.docker.com)，国内的如阿里云、网易蜂巢、时速云等也均有相关仓库。Docker 镜像仓库起到了一个集中存储和分发 Docker 镜像的作用。

三者之间的关系可以参考下图（摘自《DevOps Kubernetes》）：

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvkbMIuahetZNcdLZ%2Fdocker-container-image-repo.png?generation=1567863289278064\&alt=media)

### 拓展

* [Visualizing Docker Containers and Images](http://merrigrove.blogspot.com/2015/10/visualizing-docker-containers-and-images.html)

## Docker 原理

关于 Docker 的原理需要结合 Linux 底层的 `Cgroup` 和 `Namespace` 去理解。Docker 通过 `Cgroup` 实现针对每个容器的资源管理，如 CPU、Memory、IO 等，而通过 `Namespace` 让每个容器都拥有自己的命名空间，包括 PID、USER、UTS、MNT、NET、IPC 等。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-M5LthqKPdp2-as5eyEM%2F-M5LtiRtuQTDb3_2yrlR%2FLinux-Arch.png?generation=1587374648619958\&alt=media)

> 图自 <https://msdnshared.blob.core.windows.net/media/2017/01/Linux-Arch.png>

如果需要深入理解相关知识，可以通过以下文章进一步学习：

* [Docker 核心技术与实现原理](https://draveness.me/docker)
* [Docker基础技术：Linux CGroup](https://coolshell.cn/articles/17049.html)
* [Docker基础技术：Linux Namespace（上）](https://coolshell.cn/articles/17010.html)
* [Docker基础技术：Linux Namespace（下）](https://coolshell.cn/articles/17029.html)


# 安装入门

上文说的 `Moby` 在 Docker 官网称为社区版，支持的系统可以参见 [Install Docker](https://docs.docker.com/engine/installation/)。从 Docker `17.03` 开始，Docker 使用基于时间的版本发行机制。支持的系统除了常见的 Linux 发行版外，还支持 macOS、Windows 系统。本文只介绍基于 macOS 和 CentOS 这两个系统的 Docker 安装，关于更多系统的安装方式参见前面提到的官网安装文档。

{% hint style="info" %}
推荐安装最新的 `docker-ce` 版本
{% endhint %}

## macOS Docker 安装

关于 macOS Docker 的安装方式官方教程已经很详细了，[Install Docker for Mac](https://docs.docker.com/docker-for-mac/install/)。目前针对 Mac 系统，官方的 Docker 支持 `OS X El Capitan 10.11` 或者更新的 `macOS` 发行版，针对硬件也有限制，只支持 2010 或者更新的 Mac。

下载 [Get Docker for Mac \[stable\]](https://download.docker.com/mac/stable/Docker.dmg) dmg 文件，双击即可安装，安装之后点击运行 Docker。因为国内下载镜像比较慢的原因，所以需要额外配置一下国内的 Registry mirror 用以加速镜像下载：

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoB7AwuLWjzKEhAM6N2%2F-LoB7BeiEPtZnbtbTCEt%2Fmac-docker-config.png?generation=1567866540252029\&alt=media)

目前国内有很多家企业提供公共的镜像加速服务：

* 网易云镜像加速 <http://hub-mirror.c.163.com/>
* Azure 中国镜像加速 <https://dockerhub.azk8s.cn>
* ~~Docker 中国官方镜像加速~~[~~https://registry.docker-cn.com~~](https://registry.docker-cn.com)  已失效

除以上两个公开的加速器外，还有阿里云、Daocloud 等厂商也提供加速服务，不过需要通过注册帐号登录才可以获取专有的镜像加速服务地址。

{% hint style="info" %}
~~macOS 上运行 Docker，需要注意的是删除镜像占用空间也不会释放，所以如果你的 Mac 磁盘不是很大的话，还是得悠着点用，具体的详情可以参见这个帖子~~ [~~Docker.qcow2 never shrinks - disk space usage leak in docker for mac~~](https://github.com/docker/for-mac/issues/371) 这个问题新版本已解决
{% endhint %}

## CentOS 7 Docker 安装

关于 Docker 社区版在 CentOS 上的安装，官网提供了教程 [Get Docker CE for CentOS](https://docs.docker.com/engine/installation/linux/docker-ce/centos/)，最新版本的 Docker CE 本文暂时不做介绍，以 CentOS 源提供版本为主。

```bash
# cat /etc/centos-release
CentOS Linux release 7.3.1611 (Core)
```

Docker 已收录在 `CentOS-Extras` 软件库内，可以直接通过如下方式安装

```bash
yum install -y docker
```

当前通过 CentOS 源默认安装版本为 `1.12.6`。`1.12.6` 默认配置如下：

```bash
# grep -vE '^$|^#' /etc/sysconfig/docker
OPTIONS='--selinux-enabled --log-driver=journald --signature-verification=false'
if [ -z "${DOCKER_CERT_PATH}" ]; then
    DOCKER_CERT_PATH=/etc/docker
fi
```

默认源除了提供 `1.12.6` 以外，还提供一个 `docker-latest` 的版本，该版本为 `1.13.1`，可以通过以下方式安装：

```bash
yum install -y docker-latest
```

关于 `docker-latest` 更详细信息可以参考红帽官方介绍 [Introducing docker-latest for RHEL 7 and RHEL Atomic Hos](https://access.redhat.com/articles/2317361)，笔者不建议直接使用该软件版本。

如果要安装一个较新的版本，还可以通过加入以下软件库实现：

```bash
[virt7-container-common-candidate]
name=virt7-container-common-candidate
baseurl=https://cbs.centos.org/repos/virt7-container-common-candidate/x86_64/os/
enabled=1
gpgcheck=0
```

```bash
yum install oci-systemd-hook oci-register-machine -y
yum install -y docker --disablerepo=extras
systemctl start docker
```

> 关于 Docker `1.13.x` 和 `1.12.x` 版本的区别可以参见 [Docker 1.13.0 详细更新日志](http://dockone.io/article/1834)

### Reference

* [Installing Docker - CentOS-7](https://wiki.centos.org/Container/Tools)


# 配置说明

以下配置说明，统一以 `CentOS 7.3` 为系统环境，其它系统版本可能会有所不同。

## 相关配置文件

基本配置文件：

* `/etc/sysconfig/docker`
* `/etc/sysconfig/docker-storage-setup`
* `/etc/sysconfig/docker-network`
* `/etc/docker/daemon.json`

systemd 服务配置：

* `/usr/lib/systemd/system/docker.service`

Docker 从 `1.12` 开始支持通过 `/etc/docker/daemon.json` 文件管理 Docker daemon 的配置选项。

## 具体配置说明

默认配置内容如下：

```
# grep -vE '^#|^$' /etc/sysconfig/docker
OPTIONS='--selinux-enabled --log-driver=journald'
if [ -z "${DOCKER_CERT_PATH}" ]; then
    DOCKER_CERT_PATH=/etc/docker
fi
```

关于 docker daemon 配置选项，本文主要参考官方文档，最新说明以官方 [Daemon CLI reference(dockerd)](https://docs.docker.com/engine/reference/commandline/dockerd/) 为主。

### Daemon socket 选项

Docker daemon 可以三种不同类型的 Socket 监听 Docker API 请求：unix，tcp，fd。默认情况下，会创建一个名为 `/var/run/docker.sock` 的 unix Socket 文件，该文件的访问权限需要是 root 权限或者属于 docker 组。如果有远程访问需求，那么则需要开启 tcp Socket。正常开启 tcp Socket，是没有任何加密和安全认证的，可以通过 HTTPS 等方式加密 tcp Socket，默认不建议开启 tcp Socket。

```
# ls -l /var/run/docker.sock
srw-rw---- 1 root root 0 Sep 13 00:53 /var/run/docker.sock
```

{% hint style="info" %}
默认情况下，没有 `docker` 用户组，需要手动创建才会有。但是不建议授权非 root 用户到 docker 组，如此该用户就等于拥有 root 权限了（如直接 mount 宿主根目录到容器，即可变相获取 root 用户的权限）。
{% endhint %}

```
# groupadd docker
# systemctl restart docker
# ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Sep 13 00:59 /var/run/docker.sock    // 注意此时 docker.sock 文件已经属于 docker 用户组了
# usermod -G docker test                                        // 添加 test 用户到 docker 组
# su - test
$ docker ps
```

> 参考：[Enabling Non-root Users to Run Docker Commands](https://docs.oracle.com/cd/E37670_01/E75728/html/section_rdz_hmw_2q.html)

通过 `-H` 选项可以指定 Docker daemon 使用的 Socket 类型，默认 unix Socket 方式，通过添加 `-H tcp://0.0.0.0:2375` 达到使用 tcp Socket 的方式，`0.0.0.0` 表示监听当前主机所有网络接口。

```
# grep -vE '^#|^$' /etc/sysconfig/docker
OPTIONS='--selinux-enabled --log-driver=journald -H tcp://0.0.0.0:2375'
if [ -z "${DOCKER_CERT_PATH}" ]; then
    DOCKER_CERT_PATH=/etc/docker
fi
# systemctl restart docker
# netstat -tulnp | grep 2375
tcp6       0      0 :::2375                 :::*                    LISTEN      16288/dockerd-curre
# docker ps
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
// docker 客户端默认是以 unix socket 连接，因为指定了 tcp Socket，而没有指定 unix Socket，因此直接执行连接失败
# export DOCKER_HOST="tcp://0.0.0.0:2375"   // 设置环境变量，修改 docker 客户端默认连接
# docker ps
```

指定多种连接：

```
$ sudo dockerd -H unix:///var/run/docker.sock -H tcp://192.168.59.106 -H tcp://10.10.10.2
```

### Daemon storage-driver 选项

Docker daemon 当前支持以下几种镜像层存储驱动：

* aufs
* devicemapper
* btrfs
* zfs
* overlay
* overlay2

以上关于不同类型的存储驱动，后续会具体介绍，这一章节只介绍基本的存储驱动配置项，针对 CentOS 7 系统则选择使用 devicemapper、overlay、overlay2 居多。当前笔者通过 [Docker 安装](https://github.com/opskumu/wiki/tree/00a0e36edecba2ec87e9aafcfef51c23c16c16a3/moby/chapter2-1.md#centos7-docker-安装) 的默认存储驱动为 overlay2：

```
# docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 1.13.1
Storage Driver: overlay2
 Backing Filesystem: xfs
 Supports d_type: true
 Native Overlay Diff: false
... ...
```

用户可以通过添加 `--storage-driver` 选项设置运行存储驱动，不过更推荐使用 `/etc/docker/daemon.json` 配置文件配置。

通过添加 `--storage-driver` 选项指定存储驱动：

```
# grep -vE '^#|^$' /etc/sysconfig/docker
OPTIONS='--selinux-enabled --log-driver=journald --storage-driver=devicemapper'
if [ -z "${DOCKER_CERT_PATH}" ]; then
    DOCKER_CERT_PATH=/etc/docker
fi
# systemctl restart docker
# docker info
... ...
Storage Driver: devicemapper
 Pool Name: docker-253:0-67599031-pool
 Pool Blocksize: 65.54 kB
 Base Device Size: 10.74 GB
 Backing Filesystem: xfs
 Data file: /dev/loop0
 Metadata file: /dev/loop1
... ...
```

{% hint style="info" %}
考虑到 `daemon.json` 是跨平台的，并且为了和系统初始化脚本配置冲突的问题，所以 Docker 官方推荐使用 `daemon.json` 方式代替 `--storage-driver` 选项方式。
{% endhint %}

移除 `--storage-driver` 选项，并且在 `/etc/docker/daemon.json` 文件中添加配置，如果文件不存在则创建即可。

```
# cat /etc/docker/daemon.json
{
  "storage-driver": "devicemapper"
}
# systemctl restart docker
```

{% hint style="info" %}
以上指定存储驱动为 devicemapper，如果不添加其它选项，那么此时属于 `loop-lvm` 模式，这种模式下因为回环设备的原因，性能比较差，只适用于测试环境下使用。针对生产环境，则建议使用 `direct-lvm` 模式，后文会专门针对存储驱动做详细介绍。
{% endhint %}

### Docker runtime execution 选项

通过指定 `native.cgroupdriver` 选项，可以配置容器 cgroups 管理。

```
# docker info | grep 'Cgroup Driver'       // 可以看到 CentOS7 默认 cgroup 驱动为 systemd
Cgroup Driver: systemd
# cat /usr/lib/systemd/system/docker.service
// CentOS7 的运行时选项是直接写死在 docker.service 文件中的，如果要修改，则需要修改该文件。
... ...
ExecStart=/usr/bin/dockerd-current \
          --add-runtime oci=/usr/libexec/docker/docker-runc-current \
          --default-runtime=oci \
          --authorization-plugin=rhel-push-plugin \
          --containerd /run/containerd.sock \
          --exec-opt native.cgroupdriver=cgroupfs \
          --userland-proxy-path=/usr/libexec/docker/docker-proxy-current \
... ...
# systemctl daemon-reload
# systemctl restart docker
# docker info | grep 'Cgroup Driver'
Cgroup Driver: cgroupfs
```

{% hint style="info" %}
如无特别需求，Cgroup Driver 保持默认即可。
{% endhint %}

### Daemon DNS 选项

| 选项                       | 说明                 |
| ------------------------ | ------------------ |
| --dns 8.8.8.8            | 设置容器 DNS           |
| --dns-search example.com | 设置容器 search domain |

### Docker Registry 相关选项

#### insecure registries

Docker 认为一个私有仓库要么安全的，要么就是不安全的。以私有仓库 `myregistry:5000` 为例，一个安全的镜像仓库需要使用 TLS，并且需要拷贝 CA 证书到每台 Docker 主机 `/etc/docker/certs.d/myregistry:5000/ca.crt` 上。

通过选项 `--insecure-registry` 可以标识指定私有仓库为不安全的。 如 `--insecure-registry myregistry:5000` 标识为 `myregistry:5000` 私有仓库为不安全的，而 `--insecure-registry 10.1.0.0/16` 则告诉 Docker daemon 所有域名被解析到这个网段中的镜像仓库都被标识为不安全的。一个不安全的镜像只有被标识为不安全的时候，才可以正常的进行 docker pull、push、search 等操作。

#### lagacy registries

默认情况下，Registry V1 协议是被禁用的，Docker daemon 不会在执行 push、pull 以及 login 操作的时候去尝试通过 V1 协议去连接。可以通过 `--disable-legacy-registry=false` 启用该选项。需要注意的是，在 Docker 17.12 版本中该选项将会被移除，不再支持 Registry V1。

{% hint style="info" %}
Docker v17.12 之后 `disable-legacy-registry` 配置选项不再支持。
{% endhint %}

### Default ulimit settings

选项 `--default-ulimit` 可以设置所有容器的默认 ulimit 值，通过 `--default-ulimit nproc=10240:10240 --default-ulimit nofile=65535:65535` 设置容器的 nproc 和 nofile 值。如果该值没有设置，那么 ulimit 相关会继承宿主的设置。如果 docker run 设置 ulimit 相关，则会覆盖默认值，也就是说 docker run 优先级最高。

### Daemon configuration file

`--config-file` 选项用来指定 daemon 的 JSON 格式配置文件，默认 Linux 上 JSON 格式的配置文件为 `/etc/docker/daemon.json`。

以下为所有支持配置在 JSON 文件中的选项:

```
{
    "authorization-plugins": [],
    "data-root": "",
    "dns": [],
    "dns-opts": [],
    "dns-search": [],
    "exec-opts": [],
    "exec-root": "",
    "experimental": false,
    "storage-driver": "",
    "storage-opts": [],
    "labels": [],
    "live-restore": true,
    "log-driver": "",
    "log-opts": {},
    "mtu": 0,
    "pidfile": "",
    "cluster-store": "",
    "cluster-store-opts": {},
    "cluster-advertise": "",
    "max-concurrent-downloads": 3,
    "max-concurrent-uploads": 5,
    "default-shm-size": "64M",
    "shutdown-timeout": 15,
    "debug": true,
    "hosts": [],
    "log-level": "",
    "tls": true,
    "tlsverify": true,
    "tlscacert": "",
    "tlscert": "",
    "tlskey": "",
    "swarm-default-advertise-addr": "",
    "api-cors-header": "",
    "selinux-enabled": false,
    "userns-remap": "",
    "group": "",
    "cgroup-parent": "",
    "default-ulimits": {},
    "init": false,
    "init-path": "/usr/libexec/docker-init",
    "ipv6": false,
    "iptables": false,
    "ip-forward": false,
    "ip-masq": false,
    "userland-proxy": false,
    "userland-proxy-path": "/usr/libexec/docker-proxy",
    "ip": "0.0.0.0",
    "bridge": "",
    "bip": "",
    "fixed-cidr": "",
    "fixed-cidr-v6": "",
    "default-gateway": "",
    "default-gateway-v6": "",
    "icc": false,
    "raw-logs": false,
    "allow-nondistributable-artifacts": [],
    "registry-mirrors": [],
    "seccomp-profile": "",
    "insecure-registries": [],
    "disable-legacy-registry": false,
    "no-new-privileges": false,
    "default-runtime": "runc",
    "oom-score-adjust": -500,
    "runtimes": {
        "runc": {
            "path": "runc"
        },
        "custom": {
            "path": "/usr/local/bin/my-runc-replacement",
            "runtimeArgs": [
                "--debug"
            ]
        }
    }
}
```


# 基础命令

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvnqmnI1SzV0tvLyf%2Fdocker-command.png?generation=1567863299136931\&alt=media)

## Docker run 命令

> 详细内容见官方文档 [Docker run reference](https://docs.docker.com/engine/reference/run/)

```
# docker run --help

Usage:  docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

Run a command in a new container
```

### Detached vs foreground

容器的运行方式有前台和后台（detached）两种模式，默认为前台运行。

#### Detached (-d)

| 选项           | 说明              |
| ------------ | --------------- |
| -d, --detach | 后台运行容器，并输出容器 id |

使用 `-d` 选项或者 `-d=true` 使得容器后台运行：

```
# docker run -d busybox sleep 20
a69a80e9e16298255612c6ba73efc94b3d43d40d7ae30e4832c5e4b41de24356
# docker attach a69a80
```

使用 `docker attach` 命令重新连接后台容器。`attach` 可以理解为在当前终端，连接到后台运行的容器，等同前台操作运行容器。

#### Foreground

| 选项                | 说明                                              |
| ----------------- | ----------------------------------------------- |
| -a, --attach list | Attach to STDIN, STDOUT or STDERR (default \[]) |
| -t, --tty         | 分配一个伪终端                                         |
| --sig-proxy       | 转发所有的信号给进程 (默认为 true，仅在 non-tty 模式下生效)          |
| -i, --interactive | 保持 STDIN 打开即使在后台运行                              |

```
# docker run --rm -it busybox sh
/ # echo "This is a test"
This is a test
```

* `--rm`: 选项表示容器停止后，自动清理容器，方便调试情况下使用，不能和 `-d` 选项同时执行
* `-it`: `-i`、`-t` 选项一般同时执行，用于和容器交互操作，比较常用

#### 相关快捷键

* 退出：`Ctrl-D` or `exit`
* detach：`Ctrl-P` + `Ctrl-Q`
* attach: `docker attach <container_id>`

### 运行时资源限制

Docker 目前支持 MEM、CPU、IO 等资源的限制。

如果要进行压测，可以使用工具 [stress](http://people.seas.harvard.edu/~apw/stress/) 进行内存、CPU 的压测，通过如下 Dockerfile 构建简单的压测镜像：

```
# cat Dockerfile
FROM ubuntu:latest

RUN apt-get update && \
    apt-get install stress && \
    rm -rf /var/lib/apt/lists/*
# docker build -t ubuntu-stress:latest .
```

stress 工具常用命令：

```
stress --vm 1 --vm-bytes 1000M  # 占用 1000MB 内存
stress -c 1                     # 占用 1core CPU
stress --cpu 8 --io 4 --vm 2 --vm-bytes 128M --timeout 10s
```

#### 内存限制

| 选项                          | 说明                                    |
| --------------------------- | ------------------------------------- |
| --kernel-memory string      | 内核内存限制                                |
| -m, --memory string         | 内存限制，最小 4M                            |
| --memory-reservation string | 内存软限制                                 |
| --memory-swap string        | 内存总限制（memory + swap）， `-1` 表示不限制 swap |
| --memory-swappiness int     | 调整容器 swappiness (0 to 100) (默认 `-1`)  |
| --oom-kill-disable          | 禁用 OOM Killer                         |
| --oom-score-adj int         | 调整 OOM 优先级 (-1000 to 1000)            |

* `-m, --memory string`，`--memory-swap string`

关闭 swap 限制（宿主 swap 多少，则容器就能使用多少），并且设置内存限制为 300M：

```
# docker run -it -m 300M --memory-swap -1 ubuntu
```

```
# docker run -it -m 300M ubuntu
```

如果只设置 `--memory` 限制，默认情况下，总虚拟内存大小 (`--memory-swap`) 会设置成内存的两倍，即内存和 swap 之和 2\*300M。意味着这个容器可以使用 300M 的内存以及 300M 的 swap。

```
# docker run -it -m 300M --memory-swap 1G ubuntu
```

因 `--memory-swap` 是内存和 swap 之和，此例中容器内存限制为 300M，容器 swap 限制大小为 700M。

`docker stats <container_id>` 命令可以查看容器运行的资源占用状态，其中内存只显示 `--memory` 设定，swap 占用不会显示。

```
# docker stats 90eb76ad26d2
CONTAINER           CPU %               MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS
90eb76ad26d2        0.00%               900KiB / 300MiB     0.29%               1.95kB / 0B         0B / 0B             1
```

* `--memory-reservation string`

`--memory-reservation` 内存预留选项用于设置内存的软限制，在正常情况下，容器可以根据需要使用尽可能多的内存，并且仅受限于使用 `--memory` 选项设置的硬限制。Docker 会检测内存抢占或内存不足，在这种情况下，Docker 会强制容器将其消耗限制在预留限制内。`--memory-reservation` 设定值要低于 `--memory`，否则 `--memory` 优先。

* `--memory-swappiness int`

swappiness 可以认为是宿主 `/proc/sys/vm/swappiness` 设定：

{% hint style="info" %}
Swappiness is a Linux kernel parameter that controls the relative weight given to swapping out runtime memory, as opposed to dropping pages from the system page cache. Swappiness can be set to values between 0 and 100 inclusive. A low value causes the kernel to avoid swapping, a higher value causes the kernel to try to use swap space. [Swappiness](https://en.wikipedia.org/wiki/Swappiness)
{% endhint %}

`--memory-swappiness=0` 表示禁用容器 swap 功能。这点不同于宿主机，宿主机 swappiness 即使设置为 0 也不保证 swap 不会被使用。默认情况下，该值继承父进程设置。

{% hint style="info" %}
当然，在宿主机本身内存不足这种特殊情况下，容器依然会使用 swap 的。
{% endhint %}

```
# docker run -it --memory-swappiness=0 ubuntu
```

* `--oom-kill-disable`，`--oom-score-adj int`

OOM 相关命令选项一般不建议使用，特别针对以下这种没有对容器作任何资源限制的情况，添加 `--oom-kill-disable` 选项就比较 **危险** 了：

```
# docker run -it --oom-kill-disable ubuntu:14.04
```

因为此时容器内存没有限制，并且不会被 oom kill，此时系统为了释放内存，就可能会 kill 系统进程用于释放内存。

#### CPU 限制

| 选项                   | 说明                                               |
| -------------------- | ------------------------------------------------ |
| --cpu-period int     | Limit CPU CFS (Completely Fair Scheduler) period |
| --cpu-quota int      | Limit CPU CFS (Completely Fair Scheduler) quota  |
| --cpu-rt-period int  | Limit CPU real-time period in microseconds       |
| --cpu-rt-runtime int | Limit CPU real-time runtime in microseconds      |
| -c, --cpu-shares int | CPU shares (relative weight)                     |
| --cpus decimal       | Number of CPUs (default 0.000)                   |
| --cpuset-cpus string | CPUs in which to allow execution (0-3, 0,1)      |
| --cpuset-mems string | MEMs in which to allow execution (0-3, 0,1)      |

* CPU share constraint: `-c` or `--cpu-shares`

默认所有的容器对于 CPU 的利用占比都是一样的，`-c` 或者 `--cpu-shares` 可以设置 CPU 利用率权重，默认为 1024，可以设置权重为 2 或者更高(单个 CPU 为 1024，两个为 2048，以此类推)。如果设置选项为 0，则系统会忽略该选项并且使用默认值 1024。通过以上设置，只会在 CPU 密集(繁忙)型运行进程时体现出来。当一个 container 空闲时，其它容器都是可以占用 CPU 的。cpu-shares 值为一个相对值，实际 CPU 利用率则取决于系统上运行容器的数量。

假如一个 1core 的主机运行 3 个 container，其中一个 cpu-shares 设置为 1024，而其它 cpu-shares 被设置成 512。当 3 个容器中的进程尝试使用 100% CPU 的时候「尝试使用 100% CPU 很重要，此时才可以体现设置值」，则设置 1024 的容器会占用 50% 的 CPU 时间。如果又添加一个 cpu-shares 为 1024 的 container，那么两个设置为 1024 的容器 CPU 利用占比为 33%，而另外两个则为 16.5%。简单的算法就是，所有设置的值相加，每个容器的占比就是 CPU 的利用率，如果只有一个容器，那么此时它无论设置 512 或者 1024，CPU 利用率都将是 100%。当然，如果主机是 3core，运行 3 个容器，两个 cpu-shares 设置为 512，一个设置为 1024，则此时每个 container 都能占用其中一个 CPU 为 100%。

```
# docker run -it --rm --cpu-shares 512 ubuntu
```

* CPU period & quota constraint: `--cpu-period` & `--cpu-quota`

默认的 CPU CFS「Completely Fair Scheduler」period 是 100ms。我们可以通过 `--cpu-period` 值限制容器的 CPU 使用。一般 `--cpu-period` 配合 `--cpu-quota` 一起使用。

设置 cpu-period 为 50ms，cpu-quota 为 25ms。如果有一个 CPU，那么表示该 CPU 每 50ms 运行时段，容器可以占用该 CPU 50%。

```
# docker run -it --cpu-period=50000 --cpu-quota=25000 ubuntu
```

也可以通过 `--cpus=0.5` 来达到和以上相同的效果，即一个 CFS 时段容器最多占用 50%：

```
# docker run -it --cpus=0.5 ubuntu
```

* Cpuset constraint: `--cpuset-cpus`、`--cpuset-mems`

通过 `--cpuset-cpus` 可以设置容器绑定指定 CPU：

设置容器只能使用 CPU1 和 CPU3，即最多使用 2 个 固定的 CPU：

```
# docker run -it --cpuset-cpus="1,3" ubuntu
```

以下表示容器可以利用 CPU0、CPU1 和 CPU2：

```
# docker run -it --cpuset-cpus="0-2" ubuntu
```

`--cpuset-mems` 只应用于 NUMA 架构的 CPU 生效，关于这个选项这里不过多介绍。关于 NUMA 架构可以参考这篇文章 [NUMA 架构的 CPU -- 你真的用好了么？](http://cenalulu.github.io/linux/numa/)。

#### 磁盘 IO 限制

| 选项                                    | 说明                                                                           |
| ------------------------------------- | ---------------------------------------------------------------------------- |
| --blkio-weight uint16                 | Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0) |
| --blkio-weight-device weighted-device | Block IO weight (relative device weight) (default \[])                       |
| --device-read-bps throttled-device    | Limit read rate (bytes per second) from a device (default \[])               |
| --device-write-bps throttled-device   | Limit write rate (bytes per second) to a device (default \[])                |
| --device-read-iops throttled-device   | Limit read rate (IO per second) from a device (default \[])                  |
| --device-write-iops throttled-device  | Limit write rate (IO per second) to a device (default \[])                   |

* `--blkio-weight`、`--blkio-weight-device`

默认，所有的容器对于 IO 操作「block IO bandwidth -- blkio」都拥有相同比例，该比例为 500。可以通过 `--blkio-weight` 修改容器 blkio 权重。`--blkio-weight` 权重值在 10 \~ 1000 之间。值越大，优先级越高。

{% hint style="info" %}
The blkio weight setting is only available for direct IO. Buffered IO is not currently supported. 其实不仅仅是 blkio 权重，其它的限制也只能针对直写 IO 有效。
{% endhint %}

```
# docker run -it --name c1 --blkio-weight 300 ubuntu
# docker run -it --name c2 --blkio-weight 600 ubuntu
```

在运行的容器上同时执行如下命令，统计测试时间：

```
time dd if=/dev/zero of=test.out bs=1M count=1024 oflag=direct
```

官方文档介绍是因为比例权重的不同，时间也会不同。经实际测试使用 blkio weight 还需要注意 IO 的调度必须为 `CFQ`，否则基本没什么效果：

```
# cat /sys/block/sda/queue/scheduler
noop [deadline] cfq
# sudo sh -c "echo cfq > /sys/block/sda/queue/scheduler"
# cat /sys/block/sda/queue/scheduler
noop deadline [cfq]
```

`--blkio-weight-device="<DEVICE_NAME>:<WEIGHT>"` 可以指定某个设备的权重大小，如果同时指定 `--blkio-weight` 则以 `--blkio-weight` 为全局默认配置，针对指定设备以 `--blkio-weight-device` 指定设备值为主。

```
# docker run -it --rm --blkio-weight-device "/dev/sda:100" ubuntu
```

* `--device-read-bps`、`--device-write-bps`

通过以上两个选项限制容器读写磁盘速率 bps (bytes per second)

```
docker run -it --device-read-bps /dev/sda:1mb ubuntu
docker run -it --device-write-bps /dev/sda:1mb ubuntu
```

测试显示写限制生效：

```
root@34b4ef2fd6bf:/# dd if=/dev/zero of=test.out bs=1M count=100 oflag=direct
100+0 records in
100+0 records out
104857600 bytes (105 MB, 100 MiB) copied, 100.003 s, 1.0 MB/s
```

读限制也是通过 dd 来测试，可以发现也达到了同样的限速效果：

```
root@7df60a2ff701:/# dd if=/dev/zero of=test.out bs=1M count=10
10+0 records in
10+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0.0109524 s, 957 MB/s
root@7df60a2ff701:/# sync
root@7df60a2ff701:/# echo 3 > /proc/sys/vm/drop_caches
root@7df60a2ff701:/# dd if=test.out of=/dev/null bs=1M
10+0 records in
10+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 10.0227 s, 1.0 MB/s
```

{% hint style="info" %}
测试读性能的时候必须添加 `--privileged` 选项启用超级权限，否则无法操作 `/proc/sys/vm/drop_caches` 文件清空缓存。
{% endhint %}

* `--device-read-iops`、`--device-write-iops`

通过以上两个选项限制容器读写磁盘 IO iops (IO per second)

```
docker run -it --device-read-iops /dev/sda:1000 ubuntu
docker run -it --device-write-iops /dev/sda:1000 ubuntu
```

### 运行时权限和 Linux 功能

| 选项                 | 说明                                                                            |
| ------------------ | ----------------------------------------------------------------------------- |
| --cap-add          | Add Linux capabilities                                                        |
| --cap-drop         | Drop Linux capabilities                                                       |
| --privileged=false | Give extended privileges to this container                                    |
| --device=\[]       | Allows you to run devices inside the container without the --privileged flag. |

默认运行的容器是没有超级权限的，拥有超级权限的容器可以操作访问所有设备 [cgroups devices](https://www.kernel.org/doc/Documentation/cgroup-v1/devices.txt)。给容器 `privileged` 权限是很不安全的一种做法，所以 Docker 通过一些选项来实现操作访问相关设备而不需要赋予容器 `privileged`。

通过 `--device` 可以指定访问某个设备：

```
docker run --device=/dev/snd:/dev/snd ...
```

默认情况下，容器可以针对这些设备执行 `read`、`write`、`mknod` 操作，也可以自定义相关操作：

```
# docker run --device=/dev/sda:/dev/xvdc --rm -it ubuntu fdisk  /dev/xvdc

Command (m for help): q
# docker run --device=/dev/sda:/dev/xvdc:r --rm -it ubuntu fdisk  /dev/xvdc
You will not be able to write the partition table.

Command (m for help): q

# docker run --device=/dev/sda:/dev/xvdc:w --rm -it ubuntu fdisk  /dev/xvdc
    crash....

# docker run --device=/dev/sda:/dev/xvdc:m --rm -it ubuntu fdisk  /dev/xvdc
fdisk: unable to open /dev/xvdc: Operation not permitted
```

* `--cap-add`、`--cap-drop`

`--cap-add`、`--cap-drop` 选项可以在没有 `privileged` 情况下，做一些精细化的控制操作。具体的功能列表，可以参见官档 [Runtime privilege and Linux capabilities](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)。

```
# docker run -it --rm  ubuntu:14.04 ip link add dummy0 type dummy
RTNETLINK answers: Operation not permitted
# docker run -it --rm --cap-add=NET_ADMIN ubuntu:14.04 ip link add dummy0 type dummy
```

### 日志驱动

容器可以单独设置日志驱动，和 Docker daemon 的设置保持不同。通过 `docker run` 配合 `--log-driver=<VALUE>` 设置容器的日志驱动。当前支持以下日志驱动：

| 选项        | 说明                                                                                                                            |
| --------- | ----------------------------------------------------------------------------------------------------------------------------- |
| none      | 针对容器禁用任何日志，`docker logs` 在该驱动下不能使用。                                                                                           |
| json-file | 默认的 Docker 日志驱动，以 JSON 数据格式写入文件。                                                                                              |
| syslog    | Syslog 日志驱动，写入日志数据到 syslog。                                                                                                   |
| journald  | Journald 日志驱动，写入日志数据到 journald。                                                                                               |
| gelf      | Graylog Extended Log Format (GELF) logging driver for Docker. Writes log messages to a GELF endpoint likeGraylog or Logstash. |
| fluentd   | Fluentd 日志驱动，写入日志数据到 fluentd (forward input)。                                                                                 |
| awslogs   | Amazon CloudWatch Logs logging driver for Docker. Writes log messages to Amazon CloudWatch Logs                               |
| splunk    | Splunk logging driver for Docker. Writes log messages to splunk using Event Http Collector.                                   |

### 覆盖 Dockerfile 镜像默认值

可以在 `docker run` 的时候指定相关选项覆盖 Dockerfile 默认值或追加一些配置项，当前支持以下选项：

* CMD (默认命令或选项)

```
docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
```

如果镜像中同时也指定了 `ENTRYPOINT`，那么 `CMD` 或者 `COMMAND` 作为选项追加给 `ENTRYPOINT`。

* ENTRYPOINT (默认执行命令)

```
--entrypoint="": Overwrite the default entrypoint set by the image
```

通过 `--entrypoint` 选项可以覆盖镜像中原有的 `ENTRYPOINT` 选项。该选项会清除原有镜像中的任何命令集。

* EXPOSE (暴露端口)

```
--expose=[]: Expose a port or a range of ports inside the container.
             These are additional to those exposed by the `EXPOSE` instruction
-P         : Publish all exposed ports to the host interfaces
-p=[]      : Publish a container᾿s port or a range of ports to the host
               format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
               Both hostPort and containerPort can be specified as a
               range of ports. When specifying ranges for both, the
               number of container ports in the range must match the
               number of host ports in the range, for example:
                   -p 1234-1236:1234-1236/tcp

               When specifying a range for hostPort only, the
               containerPort must not be a range.  In this case the
               container port is published somewhere within the
               specified hostPort range. (e.g., `-p 1234-1236:1234/tcp`)

               (use 'docker port' to see the actual mapping)
```

通过 `--expose` 、`-P` 或者 `-p` 选项指定端口暴露。

* ENV (环境变量)

通过 `-e` 选项可以指定容器运行环境变量：

```
$ export today=Wednesday
$ docker run -e "deep=purple" -e today --rm alpine env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=d2219b854598
deep=purple
today=Wednesday
HOME=/root
```

* HEALTHCHECK （设置健康检测）

```
--health-cmd string                     Command to run to check health
--health-interval duration              Time between running the check (ns|us|ms|s|m|h) (default 0s)
--health-retries int                    Consecutive failures needed to report unhealthy
--health-timeout duration               Maximum time to allow one check to run (ns|us|ms|s|m|h) (default 0s)
--no-healthcheck                        Disable any container-specified HEALTHCHECK
```

* TMPFS（挂载 tpmfs 文件系统）

```
--tmpfs=[]: Create a tmpfs mount with: container-dir[:<options>],
            where the options are identical to the Linux
            'mount -t tmpfs -o' command.
```

* VOLUME （存储卷操作）

```
-v, --volume=[host-src:]container-dest[:<options>]: Bind mount a volume.
The comma-delimited `options` are [rw|ro], [z|Z],
[[r]shared|[r]slave|[r]private], and [nocopy].
The 'host-src' is an absolute path or a name value.

If neither 'rw' or 'ro' is specified then the volume is mounted in
read-write mode.

The `nocopy` modes is used to disable automatic copying requested volume
path in the container to the volume storage location.
For named volumes, `copy` is the default mode. Copy modes are not supported
for bind-mounted volumes.

--volumes-from="": Mount all volumes from the given container(s)
```

* USER （指定运行用户）

```
-u="", --user="": Sets the username or UID used and optionally the groupname or GID for the specified command.

The followings examples are all valid:
--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ]
```

* WORKDIR （指定工作目录）

```
-w="": Working directory inside the container
```


# 镜像构建

虽然可以通过 [Docker hub](https://hub.docker.com/) 获取到公共镜像，但是针对自己的应用 Docker 化的时候，我们必须要定制镜像了。镜像构建需要引入 `Dockerfile` 文件，`Dockerfile` 是一个包含创建镜像所有命令的文本文件，Docker 通过 `Dockerfile` 的内容来自动构建镜像。

## Dockerfile

### 用法

`docker build` 命令构建镜像需要一个 `Dockerfile` 和一个构建环境（context）。

{% hint style="info" %}
关于构建环境可以是文件系统的具体目录路径也可以是一个 URL，其中 URL 需要是一个 Git 仓库地址。
{% endhint %}

镜像构建环境是一个递归处理的过程，针对目录来说，则遍历目录下的所有子目录，而 URL 则囊括 Git 仓库本身和它的子模块。镜像构建是通过 Docker daemon 来实现的，而不是客户端。构建开始时，构建进程会把构建环境整个发送给 Docker daemon。假如你的环境是本地文件系统的一个目录，那么尽可能的只包括 `Dockerfile` 和镜像构建所需要的文件。

{% hint style="warning" %}
不要使用 root 目录 `/` 作为构建环境，否则会发送当前整个文件系统给 Docker daemon。
{% endhint %}

为了提升构建性能，可以通过在当前构建环境根目录下创建 `.dockerignore` 文件来排除一些不必要的文件和目录（类似 `.gitignore`）。

可以通过 `-f` 选项来指定 `Dockerfile`，如果不指定则 `docker build` 默认读取当前名为 Dockerfile 的文件。

```
$ docker build .                            # 默认读取当前目录下名为 Dockerfile 的文件
$ docker build -f /path/to/a/Dockerfile .   # 指定 Dockerfile
```

通过 `-t` 可以指定镜像仓库和标签:

```
$ docker build -t shykes/myapp .
```

`-t` 选项可以指定多次：

```
$ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest .
```

在构建过程中，Docker daemon 会逐个运行 `Dockerfile` 中的指令，在必要时将每条指令的结果提交成为一个新的镜像，并输出新的镜像 ID。Docker daemon 会自动清除发送过去的环境（context）。Docker 中每个指令都是独立的，一条指令创建一个镜像。因为镜像的分层机制，Docker 构建过程中会利用中间镜像（缓存），用来提升构建效率。

构建缓存只能用于拥有同一个本地父链（local parent chain）的镜像。意思就是说这些镜像由之前历史构建创建的或者整条镜像链都是由 docker 加载的。如果希望使用特定镜像的构建缓存，则可以使用 `--cache-from` 选项指定，`--cache-from` 不需要拥有一个父链并且可以从其它镜像仓库获取。

{% hint style="info" %}
这段描述的有些晦涩，另外 `--cache-from` 实际过程中应该使用的很少，笔者基本没有这样的应用场景。
{% endhint %}

### 格式

`Dockerfile` 的格式是：

```
# Comment 通过 # 号注释
INSTRUCTION arguments
```

{% hint style="info" %}
`Dockerfile` 指令并不区分大小写，但是为了区分，建议指令统一采用 `大写`
{% endhint %}

Docker 运行 `Dockerfile` 指令是顺序执行的，一个 `Dockerfile` 文件必须以 `FROM` 指令开始。`FROM` 指令指定了构建镜像的基础镜像。

### 环境变量替换

通过 `ENV` 可以在 Dockerfile 中声明一个变量，有些指令可以直接通过 `$variable_name` 或者 `${variable_name}` 获取变量（这种方式同 bash 中引用一样）。当然，`${variable_name}` 还支持标准的 `bash` 修饰符：

* `${variable:-word}` 表示如果 `variable` 有值则使用该值，否则为值 `word`
* `${variable:+word}` 表示如果 `variable` 有值则使用 `word`，否则为空值

还可以通过 `\` 转义环境变量：

```
FROM busybox
ENV foo /bar
WORKDIR ${foo}   # WORKDIR /bar
ADD . $foo       # ADD . /bar
COPY \$foo /quux # COPY $foo /quux 此处变量被转义
```

不是所有的 `Dockerfile` 指令支持环境变量，当前支持的有如下指令：

* `ADD`
* `COPY`
* `ENV`
* `EXPOSE`
* `FROM`
* `LABEL`
* `STOPSIGNAL`
* `USER`
* `VOLUME`
* `RUN`
* `WORKDIR`

### .dockerignore 文件

前面已经提到过 `.dockerignore`， 它的功能类似 `.gitignore`。它需要存放在构建环境根目录下才会起作用，通过 `.dockerignore` 定义匹配规则来排除文件和目录。通过 `.dockerignore` 可以避免不必要的大型或敏感文件和目录发送给 Docker daemon，从而避免 `ADD` 或者 `COPY` . 拷贝这些文件和目录。

简单的 `.dockerignore` 文件如下：

```
# comment
*/temp*
*/*/temp*
temp?
```

| 规则          | 解释                                                                               |
| ----------- | -------------------------------------------------------------------------------- |
| `# comment` | 注释，忽略                                                                            |
| `*/temp*`   | 排除根目录一级子目录下所有以 `temp` 开头的文件和目录。如 `/somedir/temp`、`/somedir/temporary.txt` 都将会被排除 |
| `*/*/temp*` | 排除根目录下二级子目录下所有以 `temp` 开头的文件和目录，如 `/somedir/subdir/temporary.txt` 会被排除           |
| `temp?`     | ? 号表示占用一个字符串，如 `/tempa`、`/tempb` 文件目录都会被排除                                       |

```
.
├── a               # 不匹配规则被保留
│   ├── b           # 不匹配规则被保留
│   │   └── tempb   # 匹配 */*/temp* 规则被排除
│   └── tempa       # 匹配 */temp* 规则被排除
├── temp            # 不匹配规则被保留
└── tempc           # 匹配规则 temp? 被排除
```

`.dockerignore` 的匹配规则遵循 Go 的 [filepath.Match](https://golang.org/pkg/path/filepath/#Match) 规则。除了该规则外，Docker 还支持了一些特殊的通配符，`**` 匹配任意层级的目录。例如，`**/*.go` 将排除构建环境根目录下所有以 `.go` 为后缀的文件。`!` 表示忽略排除，如下：

```
*.md
!README.md
```

表示排除根目录当前层级除了`README.md` 外所有以 `.md` 为后缀的文件。

```
.
├── README.md       # 匹配规则被保留
├── a.md            # 匹配规则被排除
└── temp            # 不匹配规则被保留
    └── t.md        # 不匹配规则被保留
```

{% hint style="info" %}
匹配是有顺序的，如果前后的规则有重叠或者冲突，则后面的规则生效。如果 `!README.md` 在 `*.md` 之前，则以 `*.md` 为规则，`README.md` 依然会被排除。
{% endhint %}

可以通过 `.dockerignore` 来排除 `Dockerfile` 和 `.dockerignore` 文件。但是这些文件依然会发送到 Docker daemon。不过，`ADD` 和 `COPY` 指令将不会拷贝它们。

### 指令

#### FROM

`FROM` 用来指定构建镜像的基础镜像，如果本地没有指定的镜像，在构建过程中会自动从相应镜像仓库 pull。如果 `FROM` 语句没有指定镜像标签，则默认使用 `latest` 标签。

```
FROM <image>[:<tag>]
```

#### RUN

`RUN` 有两种格式：

* `RUN <command>` （shell 格式，命令会在 shell 中执行，默认是 `/bin/sh -c`）
* `RUN ["executable", "param1", "param2"]` （exec 格式）

`RUN` 指令会在当前镜像的新层上执行命令并提交执结果，后续 `Dockerfile` 的指令操作则基于此最新提交的镜像。分层 `RUN` 指令提交方式是 Docker 的核心理念，首先提交的成本比较低，并且容器可以基于任何历史镜像点创建，好比源码版本控制（`git checkout`）。

{% hint style="info" %}
`exec` 格式会被解析成一个 JSON 数组，所以必须使用 **双引号** ，而非单引号。`exec` 格式执行命令不会调用 command shell，所以也不会继承环境变量。
{% endhint %}

```
RUN ["echo", "$HOME"]
```

这种方式不会输出 `HOME` 变量，正确在 `exec` 这种格式下集成环境变量可以使用如下方式：

```
RUN ["/bin/sh", "-c", "echo", "$HOME"]
```

`RUN` 指令操作缓存在下次构建时不会自动失效，如果不想利用缓存，则可以添加 `--no-cache` 选项禁用缓存，即 `docker build --no-cache`。

正常情况下，建议使用 `RUN <command>` shell 格式类型：

```
RUN yum install -y rsync && \
    yum clean all
```

简单来说，`RUN` 指令主要是在镜像构建过程中，执行一系列的 Linux 命令以达到定制镜像的目的。

#### CMD

`CMD` 有三种格式：

* `CMD ["executable","param1","param2"]`（exec 格式, 推荐使用这种格式）
* `CMD ["param1","param2"]` （作为 `ENTRYPOINT` 指令参数）
* `CMD command param1 param2` （shell 格式，默认 `/bin/sh -c`）

`Dockerfile` 只能有一个 `CMD` 指令，如果有多个，则只有最后一个 `CMD` 会生效。`CMD` 的主要作用是用于容器启动的默认执行命令或者作为 `ENTRYPOINT` 指令的参数。

{% hint style="info" %}
同 `RUN` 指令的 `exec` 格式，`CMD` 指令的 `exec` 格式也会被解析成一个 JSON 数组，所以必须使用 **双引号** ，而非单引号。同样 `exec` 格式执行命令不会调用 command shell，所以也不会继承环境变量。
{% endhint %}

简单来说，不同于 `RUN` 只会在构建就像时执行，`CMD` 是在容器启动时才会执行里面的命令，并且在 `Dockerfile` 中只能有一个 `CMD`。

#### LABEL

```
LABEL <key>=<value> \
      <key>=<value> \
      <key>=<value> ...
```

`LABEL` 指令主要用于添加镜像的元数据，是一个 key-value 键值对，使用示例如下：

```
LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."
```

通过 `docker inspect` 可以查看镜像相关的标签信息。

#### EXPOSE

```
EXPOSE <port> [<port>/<protocol>...]
```

`EXPOSE` 指令通知 Docker 在容器运行时对外暴露的监听端口。可以指定 `TCP` 或者 `UDP`，默认是 TCP。`EXPOSE` 指令并不会实际对外暴露指定端口，如果需要暴露，则还需要在 `docker run` 时添加 `-p` 或者 `-P` 选项，其中 `-p` 可以指定某个或某几个端口映射，而 `-P` 选择则把 `EXPOSE` 的所有端口映射到宿主。

#### ENV

```
ENV <key> <value>               # 这种格式只能定义一个环境变量
ENV <key>=<value> ...           # 这种格式可以定义对个环境变量
```

`ENV` 指令通过键值对定义环境变量。`Dockerfile` 中定义的环境变量，可以在执行 `docker run` 的时候通过 `-e` 选项替换值。

{% hint style="info" %}
如果需要针对一个单独的命令添加环境变量，则可以通过 `RUN <key>=<value> 设置`。
{% endhint %}

#### ADD

`ADD` 有两种格式：

* `ADD <src>... <dest>`
* `ADD ["<src>",... "<dest>"]` （这种格式一般在路径有空格的情况下使用）

`ADD` 指令复制本地主机文件、目录或者远程文件 URLS 从 `<src>` 添加到镜像中的路径 `<dest>` （其中如果远程 URL 需要认证，则只能通过 `RUN wegt` 或者 `RUN curl` 代理，不过一般也不用 `ADD` 添加远程文件）。`<src>` 支持正则匹配，基于 Go 的 [filepath.Match](http://golang.org/pkg/path/filepath#Match) 规则。例如：

```
ADD hom* /mydir/        # 添加所有以 hom 开头的文件
ADD hom?.txt /mydir/    # ? 用于代表单个字符，如 home.txt
```

{% hint style="info" %}
`<src>` 根目录不是以系统 `/` 开始的，而是当前构建环境的根目录，如构建环境目录为 `~/docker/app/`，则 `ADD` 拷贝本地文件目录只能局限于 `~/docker/app/` 下的子文件或者子目录。
{% endhint %}

`<dest>` 是一个绝对路径，或者基于 `WORKDIR` 的绝对路径：

```
ADD test relativeDir/          # 添加 test 到 `WORKDIR`/relativeDir/
ADD test /absoluteDir/         # 添加 test 到 /absoluteDir/
```

{% hint style="info" %}
通过 `ADD` 添加的文件和目录在镜像文件系统中 UID 和 GID 都是 0。如果添加的是一个目录，则只会把目录下的内容（包括文件系统元数据）传输到镜像 `<dest>` 下，目录本身不拷贝。如果 `<dest>` 中目录不存在，则会自动层级创建相应目录。
{% endhint %}

如果 `<src>` 是一个本地 tar 包（tar.gz、tar.xz、tar.bz 都行），添加到镜像中会自动解压成一个文件（解压同 `tar -x`），远程文件不支持。

{% hint style="info" %}
如果 `<src>` 有多个资源指定，那么 `<dest>` 必须以斜线 `/` 结尾。
{% endhint %}

#### COPY

`COPY` 有两种格式：

* `COPY <src>... <dest>`
* `COPY ["<src>",... "<dest>"]` （这种格式一般在路径有空格的情况下使用）

`COPY` 作用同 `ADD`，都是拷贝资源到镜像，不过 `COPY` 功能相对单一，不支持远程 URLs，也不支持自动解压 tar 文件。正常如果不是添加 tar 包的话，统一用 `COPY` 即可。

#### ENTRYPOINT

`ENTRYPOINT` 有两种格式：

* `ENTRYPOINT ["executable", "param1", "param2"]` （exec 格式，推荐优先使用这种格式）
* `ENTRYPOINT command param1 param2` （shell 格式）

`ENTRYPOINT` 和 `CMD` 指令有相同的作用，都可以用于容器启动执行命令。两者也可以结合使用，如：

```
ENTRYPOINT ["command"]        # ENTRYPOINT 作为命令
CMD ["param1", "param2"]      # CMD 作为命令选项
```

`CMD` 可以在 `docker run` 的时候轻易被覆盖，而如果要覆盖 `ENTRYPOINT`，则必须添加 `--entrypoint` 选项。同 `CMD`，一个 `Dockerfile` 中只能有一个 `ENTRYPOINT`，如果有多个则最后一个生效。

```
docker run -it --rm --entrypoint=bash nginx     # 运行 nginx 容器，并且以 bash 命令启动
```

{% hint style="info" %}
不推荐使用 shell 格式，因为通过 shell 格式之后，命令会以 `/bin/sh -c` 的一个子命令启动，并且不会传递任何信号。意思就是说，执行命令在容器中并不会以 `PID 1` 运行，并且不会接收 UNIX 信号，那么容器在 `docker stop` 时就不能接收到 `SIGTERM` 完成正常的退出。
{% endhint %}

如果你需要给一个执行程序写一个启动脚本，你必须确保最终执行程序能通过 `exec` 和 `gosu` 命令收到 Unix 信号，以完成程序优雅的退出：

```
#!/usr/bin/env bash
set -e

if [ "$1" = 'postgres' ]; then
    chown -R postgres "$PGDATA"

    if [ -z "$(ls -A "$PGDATA")" ]; then
        gosu postgres initdb
    fi

    exec gosu postgres "$@"
fi

exec "$@"
```

如果你在容器停止的时候做一些额外清理工作，或者容器中运行不止一个执行程序，你需要确保 `ENTRYPOINT` 脚本能收到 Unix 信号，并且正常传递，那么你可以通过如下方式实现：

```
#!/bin/sh
# Note: I've written this using sh so it works in the busybox container too

# USE the trap if you need to also do manual cleanup after the service is stopped,
#     or need to start multiple services in the one container
# 通过使用 trap 命令实现
trap "echo TRAPed signal" HUP INT QUIT TERM

# start service in background here
/usr/sbin/apachectl start

echo "[hit enter key to exit] or run 'docker stop <container>'"
read

# stop service and clean up here
echo "stopping apache"
/usr/sbin/apachectl stop

echo "exited $0"
```

{% hint style="info" %}
`ENTRYPOINT` 可以通过 `--entrypoint` 覆盖，不过只能是以 exec 格式。exec 格式会被解析成一个 JSON 数组，所以必须是 `双引号`。
{% endhint %}

`Dockerfile` 中至少要指定 `CMD` 或者 `ENTRYPOINT` 中的一个。关于 `CMD` 和 `ENTRYPOINT` 的更多，建议参考官方文档 [Understand how CMD and ENTRYPOINT interact](https://github.com/docker/docker-ce/blob/master/components/cli/docs/reference/builder.md#understand-how-cmd-and-entrypoint-interact)

#### VOLUME

```
VOLUME ["/data"]
```

`VOLUME` 指令创建一个指定名称的挂载点，并讲其标记为从本地主机或者其它容器外挂卷。该值可以为 JSON 数组，也可以是包含多个参数的普通字符串，如 `VOLUME /var/log` 或者 `VOLUME /var/log /var/db`。

#### USER

```
USER <user>[:<group>]
```

或者 `USER [:]`

`USER` 指令用来表示容器执行程序的用户（UID）和组（GID）。

#### WORKDIR

```
WORKDIR /path/to/workdir
```

`WORKDIR` 用于设置工作目录，`RUN`、`CMD`、`ENTRYPOINT`、`COPY` 和 `ADD` 指令将会遵从这一规则。

{% hint style="info" %}
如果设置的 `WORKDIR` 不存在，则会自动创建
{% endhint %}

`Dockerfile` 还有一些高级技巧和黑魔法，比如可以通过 `STOPSIGNAL signal` 设置 system call 信号用以传送给容器退出。这里不做过多的介绍，更多参见 [Dockerfile reference](https://github.com/docker/docker-ce/blob/master/components/cli/docs/reference/builder.md)

## Dockerfile 最佳实践

### 使用 `.dockerignore` 文件

使用`.dockerignore` 文件可以避免不必要的文件发送到 Docker daemon，以提升镜像构建效率，因此强烈建议使用 `.dockerignore` 文件。

### 避免安装不必要的软件包

为了降低复杂性、依赖性、文件大小以及构建时间，应该避免安装额外的或不必要的包。例如，不需要在一个数据库镜像中安装一个文本编辑器。

### 每个容器应该只包括一个 `concern`

将应用程序解耦为多个容器，可以让容器更便于横向扩展和复用。针对容器，你可能经常会听到 `一个容器一个进程` 的理念，这是一个好的经验法则，但并不是一条硬性规定，实际过程中保持容器尽可能干净和模块化即可。

### 最小化镜像层数

* Docker 1.10 或者更高版本开始，只有 `RUN`、`COPY` 和 `ADD` 指令会创建镜像层，其它指令创建临时中间镜像，不再直接增加构建的大小
* Docker 17.05 或者更高版本还支持多阶段构建（[multi-stage builds](https://docs.docker.com/engine/userguide/eng-image/multistage-build/)）

### 多行参数排序

如果可能，通过字母顺序来排序，这样可以避免安装包的重复并且更容易更新列表，另外可读性也会更强，添加一个空行使用 `\` 换行:

```
RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion
```

### 构建缓存

在镜像构建过程中，Docker 会按照 `Dockerfile` 中的顺序执行指令，Docker 会检测缓存中是否有可以复用的镜像，而不是直接创建新的镜像。如果不想使用缓存，可以通过 `--no-cache=true` 取消缓存读取。

* Docker 从缓存中的父镜像开始，将下一条指令和该基础镜像派生出的所有子镜像对比，查看是否使用了完全相同的构建指令，以确定缓存是否可复用。如果不相同，缓存失效。
* 大多数情况，只需要将 `Dockerfile` 中的指令与子镜像进行比较就够了。针对 `COPY` 和 `ADD` 指令则有些不同，除了比较指令是否相同，还需要校验比较镜像中的文件内容（忽略修改时间和访问时间）。如果文件中有任何内容改变，则缓存失效。
* `RUN apt-get -y update` 这类命令，则不会匹配缓存。

{% hint style="info" %}
为了有效地利用缓存，你需要保持你的 Dockerfile 一致，并且尽量在末尾修改。
{% endhint %}

### 指令最佳实践

#### FROM

如果有可能，尽量使用官方仓库的镜像作为基础镜像。（比如安全因素、干净性等）

#### RUN

保持 `Dockerfile` 可读性、可理解、可维护性，通过 `\` 分隔比较长或者复杂的 `RUN` 指令：

```
RUN apt-get update && apt-get install -y \
    aufs-tools \
    automake \
    build-essential \
    curl \
    dpkg-sig \
    libcap-dev \
    libsqlite3-dev \
    mercurial \
    reprepro \
    ruby1.9.1 \
    ruby1.9.1-dev \
    s3cmd=1.1.* \
 && rm -rf /var/lib/apt/lists/*
```

{% hint style="info" %}
`apt-get update` 要和 `apt-get install` 指令要同时使用，否则单独使用 `apt-get update` 会导致缓存问题（直接使用缓存而不执行该条命令）并且导致 `apt-get install 安装命令失败`。
{% endhint %}

**使用管道**

使用 `RUN` 运行命令的时候，可能一些命令依赖 shell 管道的的功能，如：

```
RUN wget -O - https://some.site | wc -l > /number
```

Docker 执行这些命令的时候使用的是 `/bin/sh -c`，最后执行的命令退出码决定整个命令是否执行成功。也就是说管道前的命令 `wget` 即使执行失败，只要 `wc -l` 能成功执行，就不会停止镜像构建。为了规避这个问题，可以加入 `set -o pipefail &&` 来保证镜像正常构建：

```
RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
```

{% hint style="info" %}
不是所有的 shell 都支持 `-o pipefail` 选项的（比如 dash）。Debian 基础类的镜像默认 shell 是 dash，可以通过如下方式来支持 `pipefail`：
{% endhint %}

> ```
> RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
> ```

#### CMD

`CMD` 应该以 `CMD ["executable", "param1", "param2"…]` 这种格式运行。不建议结合 `ENTRYPOINT` 使用，这样反而会带来一定的复杂性。

#### EXPOSE

`EXPOSE` 指令用来表面容器将监听连接的端口，建议使用标准的端口，如 Nginx Web 服务则是 `EXPOSE 80`，而 MongoDB 服务则是 `EXPOSE 27017`。至于外部映射的端口，用户则可以根据实际自己定义。

#### ENV

通过指定一些环境变量，可以使得 `Dockerfile` 更方便维护，如：

```
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
```

#### ADD or COPY

尽管 `ADD` 和 `COPY` 功能上很类似，一般建议优先使用 `COPY`。`COPY` 相对 `ADD` 更透明，就是提供本地文件的拷贝。`ADD` 最好的应用场景就是，拷贝 tar 包，自动解压。其它场景建议一律使用 `COPY`，针对远程文件的拷贝，则使用 `RUN` 结合 `wget` 或者 `curl` 命令代替：

```
RUN mkdir -p /usr/src/things \
    && curl -SL http://example.com/big.tar.xz \
    | tar -xJC /usr/src/things \
    && make -C /usr/src/things all
```

#### ENTRYPOINT

`ENTRYPOINT` 最好的就是用其设置镜像的主运行命令，方便镜像运行的时候直接指定命令参数（或者结合 `CMD` 设置默认参数）。

#### VOLUME

`VOLUME` 指令用来定义数据存储路径，强烈建议有存储相关的路径通过 `VOLUME` 设置卷。

#### USER

应该尽量避免安装或者使用 sudo，因为它具有不可预知的 TTY 和信号转发行为，可能会导致很多问题。如果需要 sudo 类似的功能（例如，以 root 初始化守护进程，但是以非 root 身份运行守护进程），则可以使用 `gosu`。

为了减少镜像层和降低复杂性，应该避免频繁的用户切换。

#### WORKDIR

为了清晰和可靠性，建议 `WORKDIR` 使用绝对路径。另外，建议通过 `WORKDIR` 来替换类似 `RUN cd … && do-something` 指定，以带来更好的可读性、故障定位等。

获取更多关于 `Dockerfile` 的最佳实践，详细的请阅读 Docker 官方文档 [Best practices for writing Dockerfiles](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices) 。


# 镜像存储


# OverlayFS 存储驱动

本文为 [Use the OverlayFS storage driver](https://docs.docker.com/storage/storagedriver/overlayfs-driver/) 译文，一直以来对 OverlayFS 工作机制不太理解，趁着间隙把 Docker 官方的文档看了一遍。虽然不涉及到底层的技术实现，但是基本的工作机制，通过这篇文章差不多可以了解个大概了。

OverlayFS 是类似 AUFS 的现代联合文件系统（union filesystem），但是速度更快，实现更简单。针对 OverlayFS 提供了两个存储驱动：最初的 `overlay`，以及更新更稳定的 `overlay2`。

> Note：如果你使用 OverlayFS，使用 `overlay2` 而不是 `overlay` 驱动，因为 `overlay2` 在 inode 利用率上更高效。要使用新的驱动，你需要系统内核版本 4.0 或者更高版本，除非你是使用 RHEL 或者 CentOS 用户，此时需要内核版本在 3.10.0-514 或更高版本。

## 先决条件

除了上述的系统内核版本，使用 OverlayFS 还需要以下条件：

* 因为 inode 以及后续的 Docker 版本兼容问题，不推荐使用 `overlay`，满足条件下优先使用 `overlay2`
* 以下文件系统支持：
  * ext4（只支持 RHEL 7.1）
  * xfs（RHEL 7.2 或更高版本），`d_type=true` 必须开启。使用 `xfs_info` 验证 `ftype` 选项是否为 `1`。
* 修改 Docker 存储驱动会使已存在的容器和镜像不可访问，可以提前使用 `docker save` 保存镜像或推送到 Docker Hub（也可以是内部私有镜像仓库），防止镜像丢失

```
mkfs -t xfs -n ftype=1 /PATH/TO/DEVICE  # 开启 d_type 选项
xfs_info /PATH/TO/DEVICE | grep ftype   # 验证是否已支持
```

## 配置 `overlay` 或 `overlay2` 驱动

满足使用 OverlayFS 的条件后，通过 `/etc/docker/daemon.json` 加入 `overlay2` 存储配置项，重启 docker daemon 即可生效。

```
{
  "storage-driver": "overlay2"
}
```

## `overlay2` 驱动是如何工作的

OverlayFS 层（layers） 在单个 Linux 主机上分为两个目录，并且将它们呈现为单个目录。这些目录统称为层（layers），统一过程称为联合挂载（union mount）。OverlayFS 把下层目录称为 `lowerdir`，上层目录称为 `upperdir`，统一视图通过称为 `merged` 自身目录暴露。

`overlay` 驱动仅适用单个 lower OverlayFS 层，因此需要通过硬链接来实现多层镜像，`overlay2` 驱动原生支持 128 个 lower OverlayFS 层。这个功能为与层相关的命令如 `docker build` 和 `docker commit` 提供了更好的性能，并且在后备文件系统上消耗更少的 inode。

### 磁盘上的镜像和容器层

当通过 `docker pull ubuntu` 下载一个五层镜像后，你可以在 `/var/lib/docker/overlay2` 目录下看到 6 个目录。

```
$ ls -l /var/lib/docker/overlay2

total 24
drwx------ 5 root root 4096 Jun 20 07:36 223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7
drwx------ 3 root root 4096 Jun 20 07:36 3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b
drwx------ 5 root root 4096 Jun 20 07:36 4e9fa83caff3e8f4cc83693fa407a4a9fac9573deaf481506c102d484dd1e6a1
drwx------ 5 root root 4096 Jun 20 07:36 e8876a226237217ec61c4baf238a32992291d059fdac95ed6303bdff3f59cff5
drwx------ 5 root root 4096 Jun 20 07:36 eca1e4e1694283e001f200a667bb3cb40853cf2d1b12c29feda7422fed78afed
drwx------ 2 root root 4096 Jun 20 07:36 l
```

新的 `l`（小写 `L`） 目录包含缩短的层标识符作为软链接，这些标识符用于避免 `mount` 命令参数页面大小限制。

```
$ ls -l /var/lib/docker/overlay2/l

total 20
lrwxrwxrwx 1 root root 72 Jun 20 07:36 6Y5IM2XC7TSNIJZZFLJCS6I4I4 -> ../3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 B3WWEFKBG3PLLV737KZFIASSW7 -> ../4e9fa83caff3e8f4cc83693fa407a4a9fac9573deaf481506c102d484dd1e6a1/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 JEYMODZYFCZFYSDABYXD5MF6YO -> ../eca1e4e1694283e001f200a667bb3cb40853cf2d1b12c29feda7422fed78afed/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 NFYKDW6APBCCUCTOUSYDH4DXAT -> ../223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/diff
lrwxrwxrwx 1 root root 72 Jun 20 07:36 UL2MW33MSE3Q5VYIKBRN4ZAGQP -> ../e8876a226237217ec61c4baf238a32992291d059fdac95ed6303bdff3f59cff5/diff
```

最底层包含一个名为 `link` 的文件，其中包含缩短标识符的名称，以及一个包含镜像内容的名为 `diff` 的目录。

```
$ ls /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/
diff  link
$ cat /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/link
6Y5IM2XC7TSNIJZZFLJCS6I4I4
$ ls  /var/lib/docker/overlay2/3a36935c9df35472229c57f4a27105a136f5e4dbef0f87905b2e506e494e348b/diff
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
```

第二下层，以及更高层，包含一个名为 `lower` 的文件，表示其父级，以及包含这层镜像内容的名为 `diff` 的目录。它包含一个 `merged` 目录，包括父层以及自身的统一内容，以及 OverlayFS 自身使用的 `work` 目录。

```
$ ls /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7
diff  link  lower  merged  work
$ cat /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/lower
l/6Y5IM2XC7TSNIJZZFLJCS6I4I4
$ ls /var/lib/docker/overlay2/223c2864175491657d238e2664251df13b63adb8d050924fd1bfcdb278b866f7/diff/
etc  sbin  usr  var
```

通过 `mount` 命令查看 Docker 使用 `overlay2` 存储驱动的挂载情况：

```
$ mount | grep overlay
overlay on /var/lib/docker/overlay2/9186877cdf386d0a3b016149cf30c208f326dca307529e646afce5b3f83f5304/merged
type overlay (rw,relatime,
lowerdir=l/DJA75GUWHWG7EWICFYX54FIOVT:l/B3WWEFKBG3PLLV737KZFIASSW7:l/JEYMODZYFCZFYSDABYXD5MF6YO:l/UL2MW33MSE3Q5VYIKBRN4ZAGQP:l/NFYKDW6APBCCUCTOUSYDH4DXAT:l/6Y5IM2XC7TSNIJZZFLJCS6I4I4,
upperdir=9186877cdf386d0a3b016149cf30c208f326dca307529e646afce5b3f83f5304/diff,
workdir=9186877cdf386d0a3b016149cf30c208f326dca307529e646afce5b3f83f5304/work)
```

`rw` 选项显示 `overlay` 是读写方式挂载的。

## `overlay` 驱动是如何工作的

OverlayFS 层（layers） 在单个 Linux 主机上分为两个目录，并且将它们呈现为单个目录。这些目录统称为层（layers），统一过程称为联合挂载（union mount）。OverlayFS 把下层目录称为 `lowerdir`，上层目录称为 `upperdir`，统一视图通过称为 `merged` 自身目录暴露。

下图展示了一个 Docker 镜像和一个 Docker 容器如何分层。镜像层术语 `lowerdir`，容器层术语 `upperdir`。统一视图通过名为 `merged` 的目录暴露。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2Fsync%2Ffffe07f5601263564cd0ed181991e30bf2c8ff20.jpg?generation=1589272858392798\&alt=media)

在镜像层和容器层都包含相同文件时，则容器层为主，并且掩盖镜像层同一个文件的存在。

`overlay` 驱动仅适用于两层，这意味着多层镜像不能实现多个 OverlayFS 层。取而代之，每个镜像层都在 `/var/lib/docker/overlay` 下实现自己的目录。通过硬链接引用与底层共享数据的方式来节省空间。从 Docker 1.10 开始，镜像层 IDs 不再对应于 `/var/lib/docker` 中的目录名。

为了创建一个容器，`overlay` 驱动组合顶层的目录以及容器的新目录。镜像的顶层是叠加层中的 `lowerdir`，并且是只读挂载的。容器的新目录是 `upperdir` 并且是可写的。

### 磁盘上的镜像和容器层

`docker pull` 命令暂时了一个 Docker 主机下载一个包含五层的 Docker 镜像。

```
$ docker pull ubuntu

Using default tag: latest
latest: Pulling from library/ubuntu

5ba4f30e5bea: Pull complete
9d7d19c9dc56: Pull complete
ac6ad7efd0f9: Pull complete
e7491a747824: Pull complete
a3ed95caeb02: Pull complete
Digest: sha256:46fb5d001b88ad904c5c732b086b596b92cfb4a4840a3abd0e35dbb6870585e4
Status: Downloaded newer image for ubuntu:latest
```

#### 镜像层

每个镜像层都在 `/var/lib/docker/overlay/` 目录下有自己的目录。

```
$ ls -l /var/lib/docker/overlay/

total 20
drwx------ 3 root root 4096 Jun 20 16:11 38f3ed2eac129654acef11c32670b534670c3a06e483fce313d72e3e0a15baa8
drwx------ 3 root root 4096 Jun 20 16:11 55f1e14c361b90570df46371b20ce6d480c434981cbda5fd68c6ff61aa0a5358
drwx------ 3 root root 4096 Jun 20 16:11 824c8a961a4f5e8fe4f4243dab57c5be798e7fd195f6d88ab06aea92ba931654
drwx------ 3 root root 4096 Jun 20 16:11 ad0fe55125ebf599da124da175174a4b8c1878afe6907bf7c78570341f308461
drwx------ 3 root root 4096 Jun 20 16:11 edab9b5e5bf73f2997524eebeac1de4cf9c8b904fa8ad3ec43b3504196aa3801
```

镜像层的目录包含该层唯一的文件以及与较低层共享数据的硬链接，以此来高效利用磁盘空间。

```
$ ls -i /var/lib/docker/overlay/38f3ed2eac129654acef11c32670b534670c3a06e483fce313d72e3e0a15baa8/root/bin/ls
19793696 /var/lib/docker/overlay/38f3ed2eac129654acef11c32670b534670c3a06e483fce313d72e3e0a15baa8/root/bin/ls
$ ls -i /var/lib/docker/overlay/55f1e14c361b90570df46371b20ce6d480c434981cbda5fd68c6ff61aa0a5358/root/bin/ls
19793696 /var/lib/docker/overlay/55f1e14c361b90570df46371b20ce6d480c434981cbda5fd68c6ff61aa0a5358/root/bin/ls
```

#### 容器层

容器层也是在 `/var/lib/docker/overlay/` 目录下。如果你使用 `ls -l` 命令列出运行容器的子目录，可以看到三个目录和一个文件存在：

```
$ ls -l /var/lib/docker/overlay/<directory-of-running-container>

total 16
-rw-r--r-- 1 root root   64 Jun 20 16:39 lower-id
drwxr-xr-x 1 root root 4096 Jun 20 16:39 merged
drwxr-xr-x 4 root root 4096 Jun 20 16:39 upper
drwx------ 3 root root 4096 Jun 20 16:39 work
```

`lower-id` 文件包含了容器所基于的镜像的顶层 ID，即 OverlayFS `lowerdir`。

```
$ cat /var/lib/docker/overlay/ec444863a55a9f1ca2df72223d459c5d940a721b2288ff86a3f27be28b53be6c/lower-id
55f1e14c361b90570df46371b20ce6d480c434981cbda5fd68c6ff61aa0a5358
```

`upper` 目录包含容器读写层的内容，对应 OverlayFS 中的 `upperdir`。

`merged` 目录是 `lowerdir` 和 `upperdir` 的联合挂载，包含正在运行的容器文件系统视图。

`work` 目录是 OverlayFS 内部目录。

通过 `mount` 命令查看 Docker 使用 `overlay` 存储驱动的挂载情况：

```
$ mount | grep overlay

overlay on /var/lib/docker/overlay/ec444863a55a.../merged
type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay/55f1e14c361b.../root,
upperdir=/var/lib/docker/overlay/ec444863a55a.../upper,
workdir=/var/lib/docker/overlay/ec444863a55a.../work)
```

`rw` 选项显示 `overlay` 是读写方式挂载的。

## 容器是如何通过 `overlay` 或 `overlay2` 读写的

### 读取文件

考虑三个容器通过 `overlay` 打开文件读取的场景。

* 容器层中不存在该文件：如果容器打开读取一个并不存在容器层（`upperdir`），则从镜像层（`lowerdir`）读取该文件。这会导致很少的性能开销。
* 文件仅存在于容器层：如果容器打开读取一个存在于容器层（`upperdir`）但不存在于镜像层（`lowerdir`）的文件，则直接从容器层中读取该文件。
* 该文件同时存在于容器层和镜像层：如果容器打开读取一个同时存在于容器层（`upperdir`）和镜像层（`lowerdir`）的文件，则容器层（`upperdir`）会覆盖镜像层（`lowerdir`） 相同名字的文件。

### 修改文件或目录

同样分三个场景来介绍修改：

* 第一次写入文件：容器第一次写入现有文件时，这个文件还不存在于容器层（`upperdir`）。`overlay`/`overlay2` 驱动程序从镜像层（`lowerdir`）执行一个 `copy_up` 操作到容器层（`upperdir`）。然后，容器将更改写入容器层中的文件的新副本。但是，OverlayFS 工作在文件级别而不是块级别，意味着所有 OverlayFS `copy_up` 操作都会复制整个文件，即使文件非常大，并且只修改了其中的一小部分。这就对容器写入性能产生显著的影响。不过，有两件事值得注意：
  * `copy_up` 操作仅在第一次写入文件时发生，对同一文件的后续写入操作只对已复制到容器的文件副本进行操作。
  * OverlayFS 仅适用于两层，意味着它性能应该是优于 AUFS 的，当搜索多个层的镜像文件时，AUFS 会出现明显的延迟。这个优势适用于 `overlay` 和 `overlay2`，`overlayfs2` 在初始读取时的性能略低于 `overlayfs`，因为它会查看更多层级，但是它会缓存结果。
* 删除文件或者目录：在容器中删除文件时，会在容器层（`upperdir`）中创建一个 whiteout 文件。镜像层（`lowerdir`）中文件的版本并不会被删除（因为 `lowerdir` 是只读的）。但是，whiteout 文件会阻止其在容器中可用。当在容器中删除目录时，会在容器层（`upperdir`）中创建一个 opaque 目录。它的工作机制同 whiteout，并且有效地防止目录被访问，即使它仍然存在于镜像层（`lowerdir`）。
* 重命名目录：仅当源路径和目标路径都在顶层时，才允许目录调用 `rename(2)`，否则会返回 `EXDEV` error（"cross-device link not permitted"）。

## OverlayFS 和 Docker 性能

`overlay2` 和 `overlay` 驱动比 `aufs` 和 `devicemapper` 拥有更好的性能。在某些情况下，`overlay2` 的性能表现可能比 `btrfs` 还要好。不过要注意以下几点：

* **Page Caching**： OverlayFS 支持页级别的缓存共享。多个容器访问同样的文件共享此文件的同一个页缓存。这个特性使得 `overlay` 和 `overlay2` 驱动高效利用内存以及高密度使用案例的优先选择如 PaaS。
* **copy\_up**： 同 AUFS 一样，容器第一次写入文件时，OverlayFS 会有一个 copy-up 的操作。这会增加写入操作的延迟，特别是大文件操作。不过，一旦文件已经被复制，后续文件的写操作都是发生在上层的，不再会有 copy-up 的操作。OverlayFS 的 `copy_up` 比 AUFS 同样的操作要更快，因为 AUFS 比 OverlayFS 拥有更多的层级，如果在多个 AUFS 层级搜索可能会造成大的延迟。`overlay2` 也支持多层，但通过缓存减轻了性能损失。
* **Inode limits**：使用 `overlay` 存储驱动会导致过多的 inode 损耗。特别是 Docker 主机上存在大量的镜像和容器时尤为明显。格式化文件系统增加可用的 inode 数量是唯一的解决方式。为了避免这个问题，因此建议尽可能的使用 `overlay2`。

### 性能最佳实践

以下通用性能最佳实践也适用于 OverlayFS。

* 使用更快的存储：使用 SSD
* 针对写频繁工作负载使用 volumes 功能：Volumes 为写入频繁的工作负载提供了最佳和最可预测的性能。这是因为它们绕过存储驱动，并且避免 thin provisioning 和写时复制的任何潜在开销。Volumes 还有其它好处，如允许容器间共享数据以及持久化数据存储等。

## OverlayFS 兼容性限制

* **open(2)**：OverlayFS 只实现了 POSIX 标准的子集。这可能导致某些 OverlayFS 操作破坏了 POSIX 标准。其中一个操作就是 copy-up 操作。假设你的应用调用 `fd1=open("foo", O_RDONLY)` 和 `fd2=open("foo", O_RDWR)`。在这个案例中，你的应用期望 `fd1` 和 `fd2` 引用同一个文件。但是，因为发生了 copy-up 操作导致第二次调用 `open(2)`，文件描述符指向的是不同的文件。`fd1` 继续引用镜像层（`lowerdir`）而 `fd2` 引用的文件在容器层（`upperdir`）。解决方式是 `touch` 这些文件，引发 copy-up 操作发生。所有的后续 `open(2)` 操作无论是读写访问模式都引用容器层(`upperdir`)的文件。
  * `yum` 是已知受影响的，除非 `yum-plugin-ovl` 已经安装了。如果 `yum-plugin-ovl` 包在你的发行版中不可用如 RHEL/CentOS 6.8 或 7.2，你可能需要在运行 `yum install` 前执行 `touch /var/lib/rpm/*`。`yum-plugin-ovl` 软件包实现了针对 `yum` 的 `touch` 变通方案。
* **rename(2)**：OverlayFS 不完全支持 `rename(2)` 系统调用。你的应用需要检测它的失败以及返回 "copy and unlink" 策略。


# Habor 安装和升级标注

> 安装统一下载在线安装包，离线安装包比较大，因为集成了离线镜像，意义不大 <https://github.com/goharbor/harbor/releases>

## 1.5.x

### 目录结构

```
# tree -L 1 harbor
harbor
├── common                          # 配置目录
├── docker-compose.clair.yml        # clair 编排文件
├── docker-compose.notary.yml       # notary 编排文件
├── docker-compose.yml              # 编排文件
├── ha                              # ha 配置目录
├── harbor.cfg                      # 配置文件
├── install.sh                      # 安装脚本
├── LICENSE
├── NOTICE
├── open_source_license
└── prepare                         # 环境初始化脚本

2 directories, 9 files
```

### 环境和配置初始化

`harbor.cfg` 为配置文件，根据实际需求修改。`common` 下为相关组件的模板文件，`prepare` 脚本会根据 `harbor.cfg` 和 `common` 下的模板文件生成实际的配置文件。`install.sh` 会调用 `prepare` 并启动 harbor。因此正常情况下我们只需要修改 `harbor.cfg` 并执行 `install.sh` 即可。不过实际生产环境使用过程中一般会有一些自定义的需求，比如一般会把 MySQL 单独抽离，使用现成的服务，还不是默认 compose 文件中启动的。 接下来会详细介绍一下，首先修改 `harbor.cfg`，然后执行 `prepare`，`harbor.cfg` 相对易懂，这里不展开讲：

```
# ./prepare
Generated and saved secret to file: /data/secretkey
Generated configuration file: ./common/config/nginx/nginx.conf
Generated configuration file: ./common/config/adminserver/env
Generated configuration file: ./common/config/ui/env
Generated configuration file: ./common/config/registry/config.yml
Generated configuration file: ./common/config/db/env
Generated configuration file: ./common/config/jobservice/env
Generated configuration file: ./common/config/jobservice/config.yml
Generated configuration file: ./common/config/log/logrotate.conf
Generated configuration file: ./common/config/jobservice/config.yml
Generated configuration file: ./common/config/ui/app.conf
Generated certificate, key file: ./common/config/ui/private_key.pem, cert file: ./common/config/registry/root.crt
The configuration files are ready, please use docker-compose to start the service.
```

可以从输出看出 `prepare` 脚本主要是用来生成证书、配置文件等。

> 如果开启 https，则需要提前创建相关证书，可参考 <https://github.com/goharbor/harbor/blob/v1.5.2/docs/configure_https.md>

`harbor.cfg` 定义了 `db_host`、`db_password`、`db_port` 以及 `db_user` 唯独没有定义库名，这里模板 `common/templates/adminserver/env` 中是固定死的，为 `MYSQL_DATABASE=registry`。如果你使用外部的数据库，那么你需要根据实际的库名修改此处。

使用外部的数据库需要提前导入相关的表，相关 SQL 文件安装包并没有提供，需要下载 `vmware/harbor-db:v1.5.2` 镜像，SQL 文件位置为 `/docker-entrypoint-initdb.d/registry.sql` 拷贝出来，导入数据库即可。在执行相关操作的时候，还需要额外修改 `docker-compose.yml` 文件，去除 harbor-db 的依赖，然后再执行 `docker-compose up -d` 启动 harbor 服务。当然，也可以直接执行 `install.sh` 一步到位，这里拆开来说是方便了解整个过程。

## 升级 1.5.x -> 1.6.x

因为 1.6.0 版本开始数据库从 MariaDB 变更到 Postgresql，1.5.x 的版本如果往上升级则需要先升级到 1.6.x 版本，在此基础上进行后续的升级。

关闭和备份旧版本

```
docker-compose down
mv harbor harbor_bak
```

备份数据和配置（更新到什么版本，下载具体 tag 的迁移镜像，如此处升级到 1.6.3 则迁移镜像为 `goharbor/harbor-migrator:v1.6.3`）

```
docker run -it --rm -e DB_USR=root -e DB_PWD=<数据库密码> -v <旧版本数据存储目录>:/var/lib/mysql -v <旧版本配置路径>:/harbor-migration/harbor-cfg/harbor.cfg -v <备份目录>:/harbor-migration/backup goharbor/harbor-migrator:[tag] backup
```

数据和配置升级，在 1.5.x 升级到 1.6.x 时候，因为涉及到 DB 的变更，这步操作会把原始数据目录的格式转为 PostgreSQL，此处要注意，每次升级前都要执行上面的备份操作。

```
docker run -it --rm -e DB_USR=root -e DB_PWD=<数据库密码> -v <旧版本数据存储目录>:/var/lib/mysql -v <旧版本配置路径>:/harbor-migration/harbor-cfg/harbor.cfg goharbor/harbor-migrator:[tag] up
```

把新版本解压到原始程序目录 harbor 中，然后使用上面的更新过的配置替换当前的配置，执行 `./install.sh` 即可启动新版本的服务，当然如果涉及到外部的数据库，操作同之前的。

> <https://github.com/goharbor/harbor/blob/v1.6.3/docs/migration_guide.md>

## 升级 1.6.x -> 1.8.x

因为版本限制，如果要升级到 1.10.x 需要先升级到 1.7.x，这里直接跳过升级到 1.8.x（当然升级到 1.7.x 再升级到 1.10.x 也是可以的）。后续的 Harbor 版本安装对 Docker 版本有要求了，所以建议升级 Docker 版本到最新版本。

```
docker-compose down
mv harbor harbor_bak
cp -r /data/database /my_backup_dir/
tar xf harbor-online-installer-v1.8.6.tgz
```

更新配置

```
docker run -it --rm -v <旧版本配置路径>:/harbor-migration/harbor-cfg/harbor.yml -v <新版本 harbor.yml 配置路径>:/harbor-migration/harbor-cfg-out/harbor.yml goharbor/harbor-migrator:[tag] --cfg up
```

安装启动

`./install.sh --with-chartmuseum` 执行安装指令，这里还额外支持 Helm Charts。

> <https://github.com/goharbor/harbor/blob/v1.8.6/docs/migration_guide.md>


# Compose


# Compose 概览

* [Overview of Docker Compose](https://docs.docker.com/compose/)

Compose 是一个用于定义和运行多容器 Docker 应用的工具。通过 Compose，你使用一个 YAML 文件来配置你的应用的服务。然后，通过一个命令，从你的配置中创建和启动所有的服务。为了学习更多 Compose 所有的特性，可以查看 [特性列表](https://docs.docker.com/compose/#features)。

Compose 可以在所有的环境中工作：生产，预发，开发，测试以及 CI 工作流。你可以从 [常用案例](https://docs.docker.com/compose/#common-use-cases) 中学到更多。

使用 Compose 基本三步走：

* 1、通过 `Dockerfile` 定义你的应用环境，以便在任何场景复用
* 2、在 `docker-compose.yml` 中定义组成应用程序的服务，以便他们能在隔离的环境中一同运行
* 3、运行 `docker-compose up`，Compose 启动和运行你整个应用

一个 `docker-compose.yml` 看起来像这样：

```
version: '2.0'
services:
  web:
    build: .
    ports:
    - "5000:5000"
    volumes:
    - .:/code
    - logvolume01:/var/log
    links:
    - redis
  redis:
    image: redis
volumes:
  logvolume01: {}
```

获取更多的 Compose 文件信息，可以见 [Compose 文件参考](https://docs.docker.com/compose/compose-file/)

Compose 有一系列命令管理应用的整个生命周期：

* 启动，停止和重建服务
* 查看运行服务的状态
* 查看服务的日志输出
* 在服务上运行一次性命令

## Compose 文档

* [安装 Compose](https://docs.docker.com/compose/install/)
* [Compose 入门](https://docs.docker.com/compose/gettingstarted/)
* [Django 服务 Compose 入门](https://docs.docker.com/compose/django/)
* [Rails 服务 Compose 入门](https://docs.docker.com/compose/rails/)
* [WordPress 服务 Compose 入门](https://docs.docker.com/compose/wordpress/)
* [常见问题](https://docs.docker.com/compose/faq/)
* [命令行参考](https://docs.docker.com/compose/reference/)
* [Compose 文件参考](https://docs.docker.com/compose/compose-file/)

## 特性

Compose 的这些特性让它更高效：

* [单个主机多环境隔离](https://docs.docker.com/compose/#multiple-isolated-environments-on-a-single-host)
* [创建容器时保留卷数据](https://docs.docker.com/compose/#preserve-volume-data-when-containers-are-created)
* [仅在变更时重新创建容器](https://docs.docker.com/compose/#only-recreate-containers-that-have-changed)
* [通过变量来控制不同环境](https://docs.docker.com/compose/#variables-and-moving-a-composition-between-environments)

### 单个主机多环境隔离

Compose 通过一个项目名来隔离隔离环境。你可以在若干不同的上下文中使用这个项目名：

* 在一个开发机上，创建单个环境的多个副本，例如当你想针对一个项目每个功能分支各运行一个稳定的副本
* 在一个 CI 服务器上，为了防止内部版本相互干扰，可以将项目名设置为唯一的版本号
* 在共享主机或者开发机上，以防止可能使用相同服务名称的不同项目相互干扰

默认的项目名是项目目录名。你可以通过 `-p` 命令选项或者 `COMPOSE_PROJECT_NAME` 环境变量自定义项目名。

### 创建容器时保留卷数据

Compose 保留服务用到的所有卷。当 `docker-compose up` 运行时，如果发现任何之前已经运行的容器，它会从旧的容器复制数据到新的容器。这一操作确保你在卷中创建的任何数据都不会丢失。如果你在 Windows 机器上使用 `docker-compose`，查看 [环境变量](https://docs.docker.com/compose/reference/envvars/) 并根据特定需求调整环境变量。

### 仅在变更时重新创建容器

Compose 缓存用于创建容器的配置。当你重启一个没有任何变更的服务时，Compose 会重新使用现有的容器。重复使用容器意味着你可以快速更改环境。

### 通过变量来控制不同环境

Compose 支持 Compose 文件中的变量。你可以使用这些变量来针对不同的环境或不同的用户自定义。具体见 [变量替换](https://docs.docker.com/compose/compose-file/#variable-substitution)。

你可以使用 `extends` 字段扩展 Compose 文件或者通过创建多个 Compose 文件。具体见 [扩展](https://docs.docker.com/compose/extends/)。

## 常见案例

Compose 可以用在不同的方式中。下面概述了一些常用的案例。

### 开发环境

当你开发一个软件时，隔离环境运行应用和交互是至关重要的。Compose 命令行工具可用于创建环境并与之交互。

[Compose 文件](https://docs.docker.com/compose/compose-file/) 提供了文档化和配置应用所有服务依赖项（数据库，队列，缓存，Web 服务 APIs 等等）的一种方法。使用 Compose 命令行工具，你可以使用单个命令（`docker-compose up`）为每个依赖创建和启动一个或多个容器。

这些功能为开发者提供了一种方便的方法来开始一个项目。Compose 可以将多页的 “开发者入门指南” 简化为单个机器可读的 Compose 文件和一些命令。

### 自动化测试环境

自动化测试套件是任何持续部署或者持续集成过程的重要组成部分。自动化端到端的测试需要一个环境来运行测试。Compose 提供了快捷的方式为测试套件创建和销毁隔离的测试环境。

通过在 [Compose 文件](https://docs.docker.com/compose/compose-file/) 中定义完整的环境，你只需要几个命令即可创建和销毁这些环境：

```
$ docker-compose up -d
$ ./run_tests
$ docker-compose down
```

### 单主机部署

Compose 一直专注于开发和测试工作流，但是每个版本都会在面向生产上有一些进展并提供了相应的功能。你可以使用 Compose 部署到远程 Docker Engine。Docker Engine 可以是配备 [Docker Machine](https://docs.docker.com/machine/overview/) 的单个实例或者整个 [Docker Swarm](https://docs.docker.com/engine/swarm/) 集群。

有关面向生产特性的详细信息，可以见文档 [compose in production](https://docs.docker.com/compose/production/)。

## 发行说明

要获取 Docker Compose 过去和现在发行版本的详细列表，见 [CHANGELOG](https://github.com/docker/compose/blob/master/CHANGELOG.md)。

## 获取帮助

Docker Compose 还在积极开发中。如果你需要帮助，想做出一些贡献，或者只是想和志趣相投的人谈论该项目，我们有许多开放的沟通渠道。

* 反馈 Bug 或者文件功能请求：使用 [issue tracker on Github](https://github.com/docker/compose/issues)
* 需要实时讨论该项目：Slack 加入 `#docker-compose` 频道
* 贡献代码或者文档变更：提交 [pull request on Github](https://github.com/docker/compose/pulls)


# Compose 安装

你可以在 macOS，Windows，以及 64 位的 Linux 上运行 Compose。Compose 本身就是一个独立的二进制程序，所以安装比较简单，具体见 [Install Docker Compose](https://docs.docker.com/compose/install/)，这里不过多介绍。


# Compose 入门

* [Get started with Docker Compose](https://docs.docker.com/compose/gettingstarted/)

本页你构建一个简单的 Python web 应用通过 Docker Compose 运行。应用使用 Flask 框架以及使用 Redis 缓存。虽然示例使用 Python，但即使你不熟悉这些技术栈，你应该也可以理解。

## 准备

确保你已经安装了 [Docker Engine](https://docs.docker.com/compose/install/) 和 [Docker Compose](https://docs.docker.com/compose/install/)。你不需要安装 Python 或者 Redis，所有的这些由 Docker 镜像提供。

## 步骤 1：Setup

定义应用的依赖。

* 1、创建一个项目目录

```
$ mkdir composetest
$ cd composetest
```

* 2、在项目目录中创建一个名为 `app.py` 的文件并复制以下内容进去：

```
import time

import redis
from flask import Flask

app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)


def get_hit_count():
    retries = 5
    while True:
        try:
            return cache.incr('hits')
        except redis.exceptions.ConnectionError as exc:
            if retries == 0:
                raise exc
            retries -= 1
            time.sleep(0.5)


@app.route('/')
def hello():
    count = get_hit_count()
    return 'Hello World! I have been seen {} times.\n'.format(count)
```

这个示例中，在应用网络中 redis 容器的主机名是 `redis`。我们使用 Redis 默认的端口，`6379`。

* 3、创建另外一个叫 `requirements.txt` 在你的项目目录并粘贴：

```
flask
redis
```

## 步骤 2：创建一个 Dockerfile

在这一步中，你需要写一个 Dockerfile 来构建一个 Docker 镜像。这个镜像包含 Python 应用的所有依赖项，包括 Python 本身。

在你的项目目录中，创建一个名为 `Dockerfile` 的文件并粘贴：

```
FROM python:3.7-alpine
WORKDIR /code
ENV FLASK_APP app.py
ENV FLASK_RUN_HOST 0.0.0.0
RUN apk add --no-cache gcc musl-dev linux-headers
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY . .
CMD ["flask", "run"]
```

这个告诉 Docker：

* 以 Python 3.7 镜像为基础构建镜像
* 设置工作目录为 `/code`
* 设置 `flask` 命令用到的环境变量
* 安装 gcc，以便诸如 MarkupSafe 和 SQLAlchemy 之类 Python 包的编译加速
* 拷贝 `requirement.txt` 并安装 Python 依赖
* 拷贝当前的目录 `.` 到镜像中的工作目录中
* 设置容器的默认启动命令为 `flask run`

关于更多编写 Dockerfile 的信息，详见 [Docker 用户指南](https://docs.docker.com/develop/) 和 [Dockerfile 参考](https://docs.docker.com/engine/reference/builder/)。

## 步骤 3：在 Compose 文件中定义服务

在项目目录下创建一个叫 `docker-compose.yml` 文件，并粘贴：

```
version: '3'
services:
  web:
    build: .
    ports:
      - "5000:5000"
  redis:
    image: "redis:alpine"
```

Compose 文件定义两个服务：`web` 和 `redis`。

### Web 服务

Web 服务使用了当前目录下的 `Dockerfile` 构建的镜像。然后把容器的端口 5000 映射到主机 5000 端口上。此示例使用的是 Flask web 服务器的默认端口，`5000`。

### Redis 服务

Redis 服务使用了 Docker Hub 仓库的公共 [Redis](https://registry.hub.docker.com/_/redis/) 镜像。

## 步骤 4：通过 Compose 构建和运行你的 app

* 1、从你的项目目录，通过运行 `docker-compose up` 启动你的应用。

```
$ docker-compose up
Creating network "composetest_default" with the default driver
Creating composetest_web_1 ...
Creating composetest_redis_1 ...
Creating composetest_web_1
Creating composetest_redis_1 ... done
Attaching to composetest_web_1, composetest_redis_1
web_1    |  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
redis_1  | 1:C 17 Aug 22:11:10.480 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis_1  | 1:C 17 Aug 22:11:10.480 # Redis version=4.0.1, bits=64, commit=00000000, modified=0, pid=1, just started
redis_1  | 1:C 17 Aug 22:11:10.480 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
web_1    |  * Restarting with stat
redis_1  | 1:M 17 Aug 22:11:10.483 * Running mode=standalone, port=6379.
redis_1  | 1:M 17 Aug 22:11:10.483 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
web_1    |  * Debugger is active!
redis_1  | 1:M 17 Aug 22:11:10.483 # Server initialized
redis_1  | 1:M 17 Aug 22:11:10.483 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
web_1    |  * Debugger PIN: 330-787-903
redis_1  | 1:M 17 Aug 22:11:10.483 * Ready to accept connections
```

Compose 拉取 Redis 镜像，从你的代码中构建一个镜像，并启动你定义的服务。这个示例中，代码会在构建时复制到镜像中。

* 2、浏览器访问 <http://localhost:5000/> 查看应用运行

如果你在本地运行 Docker，那么浏览器通过 <http://localhost:5000/> 访问可以看到 `Hello World` 的信息。如果不能解析，可以尝试 [http://127.0.0.1:5000。](http://127.0.0.1/:5000。)

如果你在 Mac 或者 Windows 上使用 Docker Machine，你可以使用 `docker-machine ip MACHINE_VM` 获取你 Docker 主机的 IP 信息。然后在浏览器打开 `http://MACHINE_VM_IP:5000`。

你会看到浏览器上的信息：

```
Hello World! I have been seen 1 times.
```

* 3、刷新页面

数字会递增

```
Hello World! I have been seen 2 times.
```

* 4、切换到另外一个终端窗口，执行 `docker images ls` 列出本地镜像。

```
$ docker image ls
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
composetest_web         latest              e2c21aa48cc1        4 minutes ago       93.8MB
python                  3.4-alpine          84e6077c7ab6        7 days ago          82.5MB
redis                   alpine              9d8fa9aa0e5b        3 weeks ago         27.5MB
```

你可以通过 `docker inspect <tag or id>` 检查镜像。

* 5、停止应用，在第二个打开的终端下，进入你的项目目录中执行 `docker-compose down`，或者直接在启动应用的原始终端执行 CTRL+C  终止应用。

## 步骤 5：编辑你的 Compose 文件并加入 mount 映射

在你的项目目录中编辑 `docker-compose.yml`，并给 `web` 服务添加一个 [bind mount](https://docs.docker.com/storage/bind-mounts/)：

```
version: '3'
services:
  web:
    build: .
    ports:
      - "5000:5000"
    volumes:
      - .:/code
    environment:
      FLASK_ENV: development
  redis:
    image: "redis:alpine"
```

这个新的 `volumes` 字段挂载主机的项目目录到容器中的 `/code` 下，允许你即时修改代码，而无需重新构建镜像。`environment` 字段设置 `FLASK_ENV` 环境变量，告诉 `flask run` 运行在开发模式，在代码变更时自动加载。这种模式仅能用于开发环境。

## 步骤 6：使用 Compose 重新构建和运行应用

从你的项目目录下，键入 `docker-compose up` 通过更新的 Compose 文件来构建应用，并运行。

```
$ docker-compose up
Creating network "composetest_default" with the default driver
Creating composetest_web_1 ...
Creating composetest_redis_1 ...
Creating composetest_web_1
Creating composetest_redis_1 ... done
Attaching to composetest_web_1, composetest_redis_1
web_1    |  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
...
```

在浏览器上检查 `Hello World`，并刷新查看数量递增。

## 步骤 7：更新应用

因为现在应用代码是使用卷挂载到容器中的，你可以更改代码并立即查看变化，而不用重新构建镜像。

* 1、修改 `app.py` 并保存。如，把 `Hello World!` 变为 `Hello from Docker!`：

```
return 'Hello from Docker! I have been seen {} times.\n'.format(count)
```

* 2、浏览器刷新应用 URL，欢迎词是更新的，并且数量还是递增的

## 步骤 8：试用其他命令

如果你想在后台运行应用，你可以传递 `-d` 选项到 `docker-compose up` 并使用 `docker-compose ps` 查看当前运行状况：

```
$ docker-compose up -d
Starting composetest_redis_1...
Starting composetest_web_1...

$ docker-compose ps
Name                 Command            State       Ports
-------------------------------------------------------------------
composetest_redis_1   /usr/local/bin/run         Up
composetest_web_1     /bin/sh -c python app.py   Up      5000->5000/tcp
```

`docker-compose run` 允许运行服务的一次性命令。如查看 `web` 服务当前的环境变量：

```
$ docker-compose run web env
```

通过 `docker-compose --help` 查看其他可用的命令。你也可以安装 bash 和 zsh [命令补全](https://docs.docker.com/compose/completion/) 来查看可用的指令。

如果你使用 `docker-compose up -d` 启动，可以使用如下命令停止：

```
$ docker-compose stop
```

你可以使用 `down` 命令关闭所有内容，完全删除容器。传递 `--volumes` 选项还可以移除 Redis 数据卷。

```
$ docker-compose down --volumes
```

至此，你已经基本了解了 Compose 工作机制。


# Compose 环境变量

* [Environment variables in Compose](https://docs.docker.com/compose/environment-variables/)

## 在 Compose 文件中替换环境变量

你可以在 shell 中使用环境变量来填充 Compose 文件中的值：

```
web:
  image: "webapp:${TAG}"
```

可以通过 Compose 文件参考 [变量替换](https://docs.docker.com/compose/compose-file/#variable-substitution) 章节获取更多信息。

## 在容器中设置环境变量

可以通过 ['environment' 键](https://docs.docker.com/compose/compose-file/#environment) 来设置服务中容器的环境变量，同 `docker run -e VARIABLE=VALUE ...`：

```
web:
  environment:
    - DEBUG=1
```

## 传递环境变量给容器

你可以使用 ['environment' 键](https://docs.docker.com/compose/compose-file/#environment) 从 shell 直接传递环境变量到服务中的容器，而不赋值，同 `docker run -e VARIABLE ...`：

```
web:
  environment:
    - DEBUG
```

容器中 `DEBUG` 变量的值取自 Compose 运行的 shell 同名环境变量的值。

## "env\_file" 配置项

你可以通过 ['env\_file' 选项](https://docs.docker.com/compose/compose-file/#env_file) 让一个服务的容器从外部文件中传递多个环境变量，类似 `docker run --env-file=FILE ...`：

```
web:
  env_file:
    - web-variables.env
```

## 通过 'docker-compose run' 设置环境变量

同 `docker run -e` 一样，你可以执行 `docker-compose run -e` 设置环境变量：

```
docker-compose run -e DEBUG=1 web python console.py
```

也可以传递一个没有值的环境变量，此时则会继承当前 shell 的环境变量值：

```
docker-compose run -e DEBUG web python console.py
```

## ".env" 文件

你可以在名为 '.env' 的环境变量文件中，为 Compose 中引用或者用于配置 Compose 的任何环境变量设置默认值：

```
$ cat .env
TAG=v1.5

$ cat docker-compose.yml
version: '3'
services:
  web:
    image: "webapp:${TAG}"
```

当你运行 `docker-compose up`，上面定义的 Web 服务使用镜像 `webapp:v1.5`。你可以使用 config 命令验证这一点，该命令会将你解析的应用程序配置输出到终端：

```
$ docker-compose config

version: '3'
services:
  web:
    image: 'webapp:v1.5'
```

Shell 中的值优先于 `.env` 文件中指定的值。如果在 Shell 上把 `TAG` 设置为其它值，则会被替换为该值：

```
$ export TAG=v2.0
$ docker-compose config

version: '3'
services:
  web:
    image: 'webapp:v2.0'
```

当你在多个文件中设置了相同的环境变量时，以下为 Compose 使用的优先级顺序：

* 1、Compose 文件
* 2、Shell 环境变量的值
* 3、环境变量文件（.env）
* 4、Dockerfile
* 5、环境变量未定义

在下面的例子中，我们在环境变量文件和 Compose 文件中设置了同样的环境变量：

```
$ cat ./Docker/api/api.env
NODE_ENV=test

$ cat docker-compose.yml
version: '3'
services:
  api:
    image: 'node:6-alpine'
    env_file:
     - ./Docker/api/api.env
    environment:
     - NODE_ENV=production
```

运行容器时，Compose 中定义的环境变量优先：

```
$ docker-compose exec api node

> process.env.NODE_ENV
'production'
```

仅当 Compose 文件中没有 'enviroment' 或者 'env\_file' 条目时，才会对 Dockerfile 中的 `ARG` 或 `ENV` 设置进行评估。

> 注意：针对 NodeJS 容器，如果有一个 `package.json` 条目 `script:start`，类似 `NODE_ENV=test node server.js`，那么这将会覆盖 `docker-compose.yml` 文件中的环境变量。（其实不仅仅是 NodeJS，所有容器启动脚本相关的操作都会覆盖系统自身的环境变量）

## 通过环境变量配置 Compose

这儿有一些环境变量来配置 Docker Compose 的命令行行为。他们以 `COMPOSE_` 或 `DOCKER_` 开头，具体可以见 [CLI Environment Variables](https://docs.docker.com/compose/reference/envvars/)。


# Compose 服务扩展

* [Extend services in Compose](https://docs.docker.com/compose/extends/)

Compose 支持两种共享通用配置的方法：

* 1、通过 [使用多个 Compose 文件](https://docs.docker.com/compose/extends/#multiple-compose-files)
* 2、使用 `extends` 字段扩展单个服务（3.x 已经不支持了，可以忽略该选项）

## 多个 Compose 文件

你可以使用多个 Compose 文件自定义 Compose 应用，以适配不同的环境或者工作流。

### 理解多个 Compose 文件

默认，Compose 读取两个文件，一个是 `docker-compose.yml`，以及另外一个可选的 `docker-compose.override.yml` 文件。按照约定，`docker-compose.yml` 包含了基本的配置。override 文件，顾名思义，包含的配置可以覆盖现有服务或者是全新服务配置。

如果一个服务定义多个文件中，Compose 会使用 [Adding and overriding configuration](https://docs.docker.com/compose/extends/#adding-and-overriding-configuration) 中的规则合并配置。

要使用多个覆盖文件，或者不同名称的覆盖文件，可以使用 `-f` 选项来指定文件列表。Compose 按照命令行指定配置的顺序来合并。具体见 [docker-compose 命令参考](https://docs.docker.com/compose/reference/overview/) 获取更多关于 `-f` 的信息。

```
$ docker-compose -f docker-compose.yml -f docker-compose.admin.yml run backup_db
```

## 添加和覆盖配置

将配置从原始服务复制到本地服务。如果原始服务和本地服务中都定义了配置选项，则本地值将替换或扩展原始值。

针对单值选项类似 `image`，`command` 或者 `mem_limit`，新值替换旧值。

```
# original service
command: python app.py

# local service
command: python otherapp.py

# result
command: python otherapp.py
```

对于多值选项类似 `ports`，`expose`，`external_links`，`dns`，`dns_search`，以及 `tmpfs`，Compose 会合并这些值：

```
# original service
expose:
  - "3000"

# local service
expose:
  - "4000"
  - "5000"

# result
expose:
  - "3000"
  - "4000"
  - "5000"
```

在 `environment`，`labels`，`volume` 和 `devices` 中，Compose 会以 local 优先的方式合并这些值：

```
# original service
environment:
  - FOO=original
  - BAR=original

# local service
environment:
  - BAR=local
  - BAZ=local

# result
environment:
  - FOO=original
  - BAR=local
  - BAZ=local
```

```
# original service
volumes:
  - ./original:/foo
  - ./original:/bar

# local service
volumes:
  - ./local:/bar
  - ./local:/baz

# result
volumes:
  - ./original:/foo
  - ./local:/bar
  - ./local:/baz
```


# Compose 网络

* [Networking in Compose](https://docs.docker.com/compose/networking/)

默认 Compose 会给应用设置一个单独的网络。每个服务中的容器加入到默认的网络中，互相可以访问，并通过和容器名一样的主机名来服务发现。

> 注意：应用网络命名依赖项目名，项目名称基于当前所在目录名。可以通过 `--project-name` 选项或者 `COMPOSE_PROJECT_NAME` 环境变量来覆盖。

例如，你的 app 所在目录为 `myapp`，你的 `docker-compose.yml` 内容如下：

```
version: "3"
services:
  web:
    build: .
    ports:
      - "8000:8000"
  db:
    image: postgres
    ports:
      - "8001:5432"
```

当你运行 `docker-compose up`，会发生如下情况：

* 1、一个命名为 `myapp_default` 的网络创建
* 2、一个使用 `web` 配置的容器创建。它以 `web` 为名加入到 `myapp_default` 网络
* 3、一个使用 `db` 配置的容器创建。它以 `db` 为名加入到 `myapp_default` 网络

现在每个容器都可以通过主机名 `web` 或 `db` 获取相应容器的 IP 地址。如 `web` 应用代码可以通过 `postgres://db:5432` 连接使用 Postgres 数据库。

## 更新容器

如果服务配置变更了，并且运行 `docker-compose up` 更新它，旧的容器会被移除，新的容器会以不同的 IP 地址但是相同的名字加入到网络。运行的容器可以使用名字连接到新的地址，但是旧的地址已经不再提供服务。

如果有任何容器与旧的容器连接，那么会被关闭。容器有责任检测这种情况，并再次查找名称重新连接。

## Links

Links 允许定义额外的别名，通过该别名可以从另外一个服务访问服务。默认情况下，任何服务都可以通过服务名访问，不需要额外启用。接下来的例子中，`db` 服务可以被 `web` 以 `db` 和 `database` 主机名访问到：

```
version: "3"
services:

  web:
    build: .
    links:
      - "db:database"
  db:
    image: postgres
```

## [指定自定义网络](https://docs.docker.com/compose/networking/#specify-custom-networks)

```
version: "3"
services:

  proxy:
    build: ./proxy
    networks:
      - frontend
  app:
    build: ./app
    networks:
      - frontend
      - backend
  db:
    image: postgres
    networks:
      - backend

networks:
  frontend:
    # Use a custom driver
    driver: custom-driver-1
  backend:
    # Use a custom driver which takes special options
    driver: custom-driver-2
    driver_opts:
      foo: "1"
      bar: "2"
```

## 配置默认网络

```
version: "3"
services:

  web:
    build: .
    ports:
      - "8000:8000"
  db:
    image: postgres

networks:
  default:
    # Use a custom driver
    driver: custom-driver-1
```

## 使用之前已存在的网络

```
networks:
  default:
    external:
      name: my-pre-existing-network
```


# Compose 生产实践

* [Use Compose in production](https://docs.docker.com/compose/production/)

部署应用程序最简单的方法是在单个服务器上运行它，类似运行开发环境的方式。如果要扩容应用程序，则可以在 Swarm 集群上运行 Compose 应用。

## 修改 Compose 文件以适配生产

你可能要修改你的应用配置以使它可以生产就绪的。这些变化包括：

* 移除程序代码绑定的卷，以使得代码运行在容器中并且不能被外部更改
* 在主机上绑定不同的端口
* 设置不同的环境变量，如调整日志级别降低输出，或者指定外部服务的设置如电子邮件服务器
* 指定重启策略，如 `restart: always` 以避免停机
* 添加额外的服务，如日志收集服务

基于这些原因，可以考虑定义一个附加的 Compose 文件，如 `production.yml`，指定生产适用的配置。这个配置只需要包含源 Compose 文件需要变更的部分，以覆盖源文件创建新的配置。

```
docker-compose -f docker-compose.yml -f production.yml up -d
```

## 部署变更

当你变更了你的应用代码，记得重新构建你的镜像并重新创建应用容器。重新部署名为 `web` 的服务，使用：

```
$ docker-compose build web
$ docker-compose up --no-deps -d web
```

这首先会创建 `web` 镜像，然后仅停止、销毁以及重新创建 `web` 服务。`--no-deps` 选项防止 Compose 重新创建 Web 依赖的任何服务。


# Compose 启动顺序控制

* [Control startup order](https://docs.docker.com/compose/startup-order/)

你可以通过 [depends\_on](https://docs.docker.com/compose/compose-file/#depends_on) 选项控制服务启动顺序。Compose 总是按照依赖的顺序启动和停止容器，依赖包括 `depends_on`，`links`，`volumes_from` 以及 `network_mode: "service:..."`。

Compose 启动不会等到容器中服务就绪，而只要容器启动即可。简单来说，Compose 不管这一层，官档的意思是应用程序应该考虑应对依赖的服务没有启动的情况。最好的方式就是代码中处理好这块逻辑，如果不行的话可以封装一些脚本曲线解决问题：

* 使用如 [wait-for-it](https://github.com/vishnubob/wait-for-it)，[dockerize](https://github.com/jwilder/dockerize) 或者 shell 兼容的 [wait-for](https://github.com/Eficode/wait-for) 这类的工具。这些小型的封装脚本，可以把这些加入到镜像中，以轮询给定的主机和端口，直到它接受 TCP 连接为止。如使用 `wait-for-it.sh` 或者 `wait-for` 封装你的服务命令：

```
version: "3"
services:
  web:
    build: .
    ports:
      - "80:8000"
    depends_on:
      - "db"
    command: ["./wait-for-it.sh", "db:5432", "--", "python", "app.py"]
  db:
    image: postgres
```

> 使用这类的工具好处使方便，坏处也有，比如服务端口监听并不表示服务已经可以对外提供服务了，这种细粒度的事情，这些脚本做不了，可以使用下面的解决方式。

* 或者编写自己的封装脚本以执行更特定于应用程序运行状态的检测，如等到 Postgres 准备好接受命令为止：

```
#!/bin/sh
# wait-for-postgres.sh

set -e

host="$1"
shift
cmd="$@"

until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$host" -U "postgres" -c '\q'; do
  >&2 echo "Postgres is unavailable - sleeping"
  sleep 1
done

>&2 echo "Postgres is up - executing command"
exec $cmd
```

定制好了之后通过如下方式设置：

```
command: ["./wait-for-postgres.sh", "db", "python", "app.py"]
```


# 架构概览

> `Kubernetes` (通常称为 K8s) 是用于自动部署、扩展和管理容器化（containerized）应用程序的开源系统。Google 设计并捐赠给 Cloud Native Computing Foundation（CNCF，今属 Linux 基金会）来使用的。它旨在提供 “跨主机集群的自动部署、扩展以及运行应用程序容器的平台”。它支持一系列容器工具, 包括 Docker 等。 -- 摘自维基百科 [Kubernetes](https://zh.wikipedia.org/wiki/Kubernetes) 词条

## Kubernetes 架构

![kubernetes-architecuture](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvkcKB3-iD9OHtdvF%2Farchitecture.png?generation=1567863282651830\&alt=media)

> [Kubernetes Design and Architecture](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/architecture.md#controller-manager-server)

### 集群控制平面（Cluster control plane）即 Master

Kubernetes 控制平面由一系列组件组成，可以运行在一个单独的主节点上，也可以分布部署以支持高可用集群，或者运行在 Kubernetes 之上。

#### API 服务（API Server）

* kube-apiserver

API server 提供 [Kubernetes API](https://kubernetes.io/docs/concepts/overview/kubernetes-api/)。API server 扮演集群网关的角色，它主要处理 REST 操作，验证并更新到 `etcd` 存储。

#### 集群状态存储（Cluster state store）

* etcd

`etcd` 是一个分布式 key-value 数据库，Kubernetes 用 `etcd` 作为后端数据存储。集群所有的持久性状态都存储在 `etcd` 实例中。`etcd` 提供了可靠配置数据存储。通过 `watch` 的支持，可以非常快速地通知协调组件变更。

#### 控制管理服务（Controller-Manager Server）

* kube-controller-manager

集群内部的管理控制中心，如 Node、Volume、Deployment 、Service 等资源管理，以及空间生命周期，Pod GC、节点 GC 等。

#### 调度器（Scheduler）

* kube-scheduler

执行 pod 的相关调度。调度程序监视未调度的 pod，并根据所请求资源的可用性，服务质量要求、亲和性和反亲和性设置以及其它约束，通过 `/binding` pod 子资源 API 绑定到相应节点。

### Kubernetes 节点（The Kubernetes Node）

#### Kubelet

Kubelet 是 Kubernetes 中最重要和突出的控制器，它是驱动容器执行层的 Pod 和 Node API 的主要实现者。没有这些 API，Kubernetes 只是一个后端由键值存储支持的面向 CRUD 的 REST 应用程序框架。

Kubelet 决定 Pod 是否可以运行在给定的节点上的最终决策者，不是调度器也或者 DaemonSets。此外，Kubelet 还集成了 [cAdvisor](https://github.com/google/cadvisor) 资源监控 agent。

#### 容器运行时（Container runtime）

每一个节点运行一个容器运行时，负责下载镜像和运行容器。Kubelet 不集成容器运行时。作为替代，定义了一个 [Container Runtime Interface](https://github.com/kubernetes/community/blob/master/contributors/devel/container-runtime-interface.md) 控制底层运行时并促进该层的可插拔性。当前支持的有 docker、[rkt](https://github.com/rkt/rkt)、[cri-o](https://github.com/kubernetes-incubator/cri-o)、[frakti](https://github.com/kubernetes/frakti)。

#### Kube Proxy

service 的抽象提供了一种在公共访问策略（如负载均衡）下对 pod 进行分组的方式。Service 通过创建 VIP，提供给客户端访问，再透明代理到 Service 中的 pods。每个节点都运行一个 kube-proxy 进程，该进程维护一套 iptables 规则，以捕获对服务 IPs 的访问，并重定向到正确的后端（1.12.x ipvs 正式 GA，性能相对 iptables 有很大的提升）。

附上华为关于 Service 性能这块的介绍，主要是对比 ipvs 和 iptables：

* [华为云在 K8S 大规模场景下的 Service 性能优化实践](https://zhuanlan.zhihu.com/p/37230013)

### 附加组件和依赖

* [DNS](https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/dns) 提供集群内部解析和服务发现
* [Ingress controller](https://github.com/kubernetes/ingress-nginx) 提供内部服务七层代理到外部
* [Kubernetes Metrics Server](https://github.com/kubernetes-incubator/metrics-server) 替换 Heapster 监控
* [Dashboard](https://github.com/kubernetes/dashboard/) Kubernetes GUI

以及包括 [kube-state-metrics](https://github.com/kubernetes/kube-state-metrics) 等其它 [add-ons ](https://github.com/kubernetes/kubernetes/tree/master/cluster/addons) 组件。

#### 一些基于 Kubernetes 的开源平台

Kubernetes GUI 从体验上来说还是相对比较差的，可以使用一些开源的替代版本：

* [Multi-Cluster Kubernetes Management -- Rancher](https://rancher.com/)
  * Rancher 团队是做的比较早的，现在已经发布 2.x 版本了，支持中文，具体可以参考文档 [Rancher 2.0-CN](https://www.cnrancher.com/docs/rancher/v2.x/cn/overview/)
* [Web UI for Kubernetes multi-clusters -- Wayne](https://github.com/Qihoo360/wayne)
  * 360 开源的 Kubernetes 多集群 Web UI


# 基础术语

## Namespaces

同一个 Kubernetes 物理集群支持多个虚拟集群，而这个虚拟集群的概念就是 namespaces。这是官方的介绍，官方的介绍多少有点让人不那么容易理解。简单来说，namespaces 可以认为是一个环境或者项目组的概念，namespaces 下创建操作相应的服务。每个 namespaces 都是逻辑隔离的，针对指定 namespaces 可以做相应的资源（CPU、Memory 等）限制以及用户权限控制（RBAC）。namespaces 名字是全局唯一的。

{% hint style="info" %}
针对同一软件的不同版本，官方是不建议启用多个 namespaces 的，而是推荐在同一个 namespaces 下使用 `labels` 去区分标识。不过，这还是得看情况，针对多环境不同版本测试来说，还是采用多个 namespaces 比较好，方便隔离。
{% endhint %}

Kubernetes 集群创建之后会看到三个初始化 namespaces：

* `default` 默认 namespace
* `kube-system` Kubernetes 系统 namespace
* `kube-public` 用于集群中所有用户都可读的 namespace，是个惯例做法，但是非必须的

## Pods

Pods 是 Kubernetes 中创建和管理的最小可部署计算单元，一个 pod 是由一个或者多个容器组成（如 Docker 容器），pod 中的容器共享存储、网络。

## ReplicaSet (RS) and ReplicationController (RC)

单个部署 pod，如果 pod 因为一些因素异常退出了，pod 本身是不会自动恢复的。RS 和 RC 则担任管理 pod 状态的角色，RS 和 RC 的机制保证通过它们管理的 pod 保持固定的副本数并持续运行。如果 pod 因异常原退出了，那么 RS 或 RC 会请求创建新的 pod。

{% hint style="info" %}
需要注意的是，ReplicationController 已经被 ReplicaSet 替代
{% endhint %}

## Deployments

Deployments 提供了 pod 和 ReplicaSets 的更新声明。一般情况下不需要单独创建 ReplicaSet，而是直接通过创建 Deployments，由 Deployments 创建管理 ReplicaSet。此外，Deployments 还提供了滚动更新、回滚、暂停、恢复等功能。

## StatefulSets

StatefulSets 同 Deployments/Replicas 类似，相较于 Deployments/ReplicaSets 对应无状态服务，StatefulSets 则针对有状态服务。StatefulSets 适用于以下特性的应用：

* 稳定唯一的网络标识
* 稳定持久性存储
* 有序优雅的部署和扩展
* 有序优雅的删除和销毁
* 有序自动更新

## DaemonSet

DeamonSet 确保所有或者部分节点运行同一个 Pod，当节点添加之后，Pod 会自动在所在节点创建，节点移除则 Pod 会被自动清理。删除 DaemonSet 将会清除它创建的所有 Pod，一般用于以下场景：

* 在每个节点运行集群存储 daemon，如 glusterfs、ceph 等
* 在每个节点运行日志收集 daemon，如 fluentd、filebeat 等
* 在每个节点运行节点监控 daemon，如 Prometheus Node Exporter、sysdig agent 等

## Jobs and CronJob

Jobs 用于一次性的部署任务，可以是一个或者多个 Pods。Pods 成功执行后，Jobs 本身也完成了。如运行单元测试、一次性的脚本运行等等都可以使用 Jobs 来做。CronJob 顾名思义，则是定时执行的一种 Job。

## Services

因为 K8s 中 pod 的 ip 是不固定的，那么应用之间就不能单纯的简单靠 ip 来访问，另外有些应用拥有多个副本。因此，K8s 引入了 Services 的抽象，Services 简单可以理解为一个负载均衡器，每个 Services 都拥有一个名字和 vip，通过 Label 来对应一个或者一组 pods，集群内部的应用通过 Services name 直接访问对应的应用。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvl-j0OLkPJup--8f%2Fservice.svg?generation=1567863290021426\&alt=media)

> 图引用自 [CoreOS Overview of a Service](https://coreos.com/kubernetes/docs/latest/services.html)

## Ingress

Services 用于内部集群应用间调用，Ingress 定义则为了集群内部服务暴露到外部访问。单纯创建 Ingress 还不够，需要结合 Ingress controller 才能真正实现服务的外部暴露。当前 Ingress controller 有 [ingress-nginx](https://github.com/kubernetes/ingress-nginx)、[Traefik](https://github.com/containous/traefik) 等。

## Configmap

Configmap 提供键值对存储，一般用于静态配置文件或者环境变量配置等。


# 集群构建

集群构建的方式有很多，官方提供`kubeadm` 可以很方便的构建，相关文档可以直接看官方提供的 [Using kubeadm to Create a Cluster](https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/)。

{% hint style="info" %}
`kubeadm` 已经 GA 了，生产环境用户可以选择这种方式部署。不过，还是建议手动部署一遍 Kubernetes 集群，加深对 Kubernetes 整个运维架构的理解，也方便自行定制。
{% endhint %}

本章节会介绍以下两种集群构建方式：

* [从头开始构建一个 Kubernetes 集群](broken://pages/-LoAvkA_3Dyg8WBUqpsg)
* [通过 Ansible 自动构建 Kubernetes 集群 kubespray](https://github.com/kubernetes-sigs/kubespray)
  * 此处 Ansible 自动构建 Kubernetes 集群，给的项目为 kubespray，已经非常成熟，可以借鉴该项目自行定制

另外，如果有本地运行 Kubernetes 的需求，可以直接使用 [Minikube](https://github.com/kubernetes/minikube) 达到快速构建的目的，具体可以参考官方介绍。


# 工作负载

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvky6aHnDurR7RmQn%2Fk8s-workload.png?generation=1567863290759632\&alt=media)

> 原图摘自 [《Kubernetes in Action》](https://book.douban.com/subject/26997846/)


# Deployments

Deployments 生产实践相关选项可以参考下图：

![](https://raw.githubusercontent.com/opskumu/Day/master/awesome-map/Deployments.png)


# StatefulSets

* [StatefulSets 官方说明](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#deployment-and-scaling-guarantees)

一个标准的 StatefulSet 由 `Pod template` 和 `Volume claim template` 组成：

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvl3t672Y9NFr6Dxk%2FStatefulSet.png?generation=1567863288433075\&alt=media)

> 图摘自 [Kubernetes in action](https://www.manning.com/books/kubernetes-in-action)

## 说明

* StatefulSet 在 K8s 1.9 版本正式 GA，1.9 之前属于 beta 版本，1.5 之前的版本则不可用
* Pod 所需的存储要么基于 [PersistentVolume Provisioner](https://github.com/kubernetes/examples/tree/master/staging/persistent-volume-provisioning/README.md) 请求的 `stogage class` 动态获取，要么通过管理员预先提供
* 删除或者缩容一个 StatefulSet 将不会删除绑定的存储卷。这么做是为了保证数据安全
* StatefulSet 依赖 [Headless Service](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services) 处理 Pods 的网络标识

## 部署和扩容缩容保障

* 如果一个 StatefulSet 有 N 个 replicas（副本），Pod 按照 {0..N-1} 的顺序部署
* 当 Pods 被删除时，则按照 {N-1..0} 的顺序终止
* 在扩容缩容操作应用到 Pod 时，之前的实例都是运行和准备就绪的
* 在 Pod 终止前，它的继任者都必须完全关闭状态

StatefulSet `pod.Spec.TerminationGracePeriodSeconds` 值不应该指定为 `0`。

## 组件

* Headless Service 用于域名注册
* volumeClaimTemplates 用于提供存储

### Pod 选择器

必须指定 `.spec.selector` 字段匹配 `.spec.template.metadate.labels`。在 Kubernetes 1.8 之前，`.spec.selector` 字段如果为空则取默认值。在 1.8 以及之后版本，不指定则报错。

### Pod 标识

StatefulSet Pod 具有唯一的标识，由序数、稳定网络标识和稳定存储组成，无论其被调度到哪个节点。

**序数索引**

对于拥有 N 副本的 StatefulSet，StatefulSet 的每个 Pod 将被分配一个整数序数，从 0 到 N-1，在副本集中是唯一的。

**稳定的网络 ID**

StatefulSet 中的每个 Pod 都从 StatefulSet 的名称和 Pod 序号派生出主机名。构造的主机名的模式是 $(statefulset name)-$(ordinal)。StatefulSet 可以通过 [Headless Service](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services) 管理 Pods 的域名，域名的格式为： $(service name).$(namespace).svc.cluster.local，其中 `cluster.local` 为集群域，以实际设置为主。作为每个创建的 Pod，它获取匹配的 DNS 子域，格式为：$(podname).$(governing service domain)，其中，governing service 通过 StatefulSet 的 `serviceName` 字段定义。以下为官方示例中对应关系：

| Cluster Domain | Service(ns/name) | StatefulSet(ns/name) | StatefulSet Domain              | Pod DNS                                      | Pod Hostname |
| -------------- | ---------------- | -------------------- | ------------------------------- | -------------------------------------------- | ------------ |
| cluster.local  | default/nginx    | default/web          | nginx.default.svc.cluster.local | web-{0..N-1}.nginx.default.svc.cluster.local | web-{0..N-1} |
| cluster.local  | foo/nginx        | foo/web              | nginx.foo.svc.cluster.local     | web-{0..N-1}.nginx.foo.svc.cluster.local     | web-{0..N-1} |
| kube.local     | foo/nginx        | foo/web              | nginx.foo.svc.kube.local        | web-{0..N-1}.nginx.foo.svc.kube.local        | web-{0..N-1} |

### Pod 管理策略

K8s 1.7 之后，StatefulSet 通过 `.spec.podManagementPolicy` 字段可以设置是否严格按照顺序部署和扩容缩容操作。

**`OrderedReady` Pod 管理**

`OrderedReady` pod 管理是默认的 StatefulSets 策略，保障了有序部署和扩容缩容。

**`Parallel` Pod 管理**

`Parallel` pod 管理指定 StatefulSet 控制器并行运行和终止 Pods，而不是等待 Pods 运行和准备就绪再运行或者终止完上一个 Pod 再终止另外一个。

## 更新策略

K8s 1.7 之后，StatefulSet 通过 `.spec.updateStrategy` 字段允许用户配置和禁用 Pods 自动滚动更新容器、标签、资源限制以及注释。

### `OnDelete`

`OnDelete` 更新策略实现了旧的（1.6 或之前的版本）更新方式，当一个 StatefulSet 的 `.spec.updateStrategy.type` 设置为 `OnDelete` 时，StatefulSet 控制器将不会自动更新 StatefulSet 中的 Pods。在修改 `.spec.template` 后，用户必须手动删除 Pods 以触发控制器创建新的 Pods。

### `RollingUpdate`

`RollingUpdate` 更新策略实现了在 StatefulSet 中自动、滚动更新 Pods。当 `.spec.updateStrategy` 没有定义的时候，默认就是 `RollingUpdate` 策略。当 StatefulSet `.spec.updateStrategy.type` 设置为 `RollingUpdate` 时，StatefulSet 控制器在有变更的时候会删除和重建 StatefulSet 中的每一个 Pod。它将以 Pod 终止（从最大序数到最小序数）的顺序进行，一次更新一个 Pod。在继续更新前会等待更新的 Pod 运行直接准备就绪。

**`Patition`**

`RollingUpdate` 更新策略可以分区操作，通过指定 `.spec.updateStrategy.rollingUpdate.partition` 选项。如果指定了分区，则更新 StatefulSet 的 `.spec.template` 时，将更新序数大于或者等于该分区的所有 Pods。序数小于分区的所有 Pods 都不会更新，即使被删除，也会以之前的版本重建。如果 StatefulSet 的 `.spec.updateStrategy.rollingUpdate.partition` 大于 `.spec.replicas`，则即使 `.spec.template` 更新了，Pods 也不会被更新。

{% hint style="info" %}
大多数情况下是不需要使用分区的，但是如果有金丝雀或者分阶段更新需求，那么分区将会很有用。
{% endhint %}


# Volumes

* [Volumes 官方说明](https://kubernetes.io/docs/concepts/storage/volumes/)

在 Kubernetes 中，容器运行后新增或修改的磁盘文件都是临时的，在容器 Crash 或者更新后，Kubelet 会重新启动新的容器，新的容器将是一个全新干净的环境，原有新增或修改的文件将会丢失。针对这种情况，对于不需要有数据存储的应用是不受影响的，但是如果有数据存储的需求就影响很大，这时候就需要引入 `Volumes` 的概念了，Kubernetes 提供 Volumes 的概念来满足有存储需求的应用。

我们知道 Docker 也有 [Volumes](https://docs.docker.com/storage/volumes/) 的概念，但是相对松散和缺乏管理的。在 Docker 中，卷只是磁盘上或者另外一个容器中的目录。生命周期也不受管理，直到最近才有本地磁盘支持的卷。Docker 现在提供了卷驱动程序，但是功能非常有限（例如，从 Docker 1.7 开始，每个 容器只允许一个卷驱动程序，并且无法将参数传递给卷）。

Kubernetes 卷具有明确的生命周期，和伴随它的 Pod 相同。因此，卷可以比 Pod 中运行的容器周期要长，并且可以在容器重新启动之间保留数据。当 Pod 不存在时，卷也就不复存在了（数据是否保留取决于设定规则）。另外，Kubernetes 支持多种类型的卷，Pod 可以同时使用任意数量的卷。

本质上来说，卷只是一个目录，可以被 Pod 中的容器访问。目录是如何形成的，取决于使用的卷类型。

要使用卷，需要同时指定 `.spec.volumes`（指定卷）字段和 `.spec.containers.volumeMounts`（容器中挂载路径） 字段。

## 支持卷类型

这里列出 Kubernetes 支持的常用卷类型：

### cephfs

### rbd

`cephfs`、`rbd` 是 Ceph 提供的功能，如果使用了相关卷，需要在宿主上安装好对应版本的 `ceph-common`，以支持 kubelet 挂载调用，否则会导致挂载失败。

### configMap

`configMap` 资源提供了一种将配置数据注入 Pod 的方法，存储在 `configMap` 对象中的数据可以在 `configMap` 类型的卷中引用，然后被 Pod 中运行的容器化应用程序使用。通过 `configMap` 可以动态的修改指定的容器中的配置，从而达到在不同环境使用同一镜像不同配置的目的，因此这是一个非常常用的一个卷类型。

{% hint style="info" %}
早期 `configMap` 只能以目录的形式覆盖对应容器中的配置目录，针对单个文件是无法覆盖的，Kubernetes [CHANGELOG-1.3](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.3.md) 开始支持 subPath 特性 [Add subPath to mount a child dir or file of a volumeMount](https://github.com/kubernetes/kubernetes/pull/22575)，通过 [subPath](https://kubernetes.io/docs/concepts/storage/volumes/#using-subpath) 可以挂载子目录或者文件，如此便可以在目标容器配置目录有多个文件的时候只覆盖指定的配置，而不是直接目录覆盖。 不过，要注意的是，subPath 挂载会导致 configMap 更新之后容器中的文件不会实时同步更新。
{% endhint %}

### emptyDir

`emptyDir` 卷在 Pod 关联到节点后初始化创建，Pod 运行在该节点多久就存在多久。一般用于存放临时文件。

{% hint style="info" %}
容器崩溃不会从节点中删除 Pod，因此 `emptyDir` 卷中的数据在容器崩溃中是安全的。
{% endhint %}

默认情况下，`emptyDir` 卷存储支持节点上的任何介质，可能是磁盘、SSD 或者网络存储，这取决于你的环境。不过你可以通过 `emptyDir.medium` 字段来设置为 `Memory`，用来告诉 Kubernetes 挂载 tmpfs。虽然 tmpfs 速度很快，不同于磁盘，tmpfs 会在节点重启后数据被清除，而且使用的内存受限于容器的内存限制。

### glusterfs

同 `cephfs`、`rbd`，如果要使用 `glusterfs`，那么宿主节点需要安装好对应版本的 `glusterfs-libs`、`glusterfs`，以支持 kubelet 挂载调用，否则导致挂载失败。

### hostPath

`hostPath` 卷用于将主机节点的文件或目录挂载到 Pod。`hostPath` 可以挂载主机的特定目录、文件到 Pod 中，但是要注意的一点是，如果是数据存储的需求使用 `hostPath` 需要通过 [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/) 来固定对应节点，否则下次调度到不同节点之后，数据就不存在了。

`hostPath` 有一个 `type` 属性字段，支持以下值：

| 值                 | 用途                                                 |
| ----------------- | -------------------------------------------------- |
|                   | 空值（默认）用于向后兼容，在挂载 hostPath 卷之前不会执行任何检查              |
| DirectoryOrCreate | 如果给定的路径不存在，则创建一个 0755 权限的空目录，和 Kubelet 拥有相同的属主和属组  |
| Directory         | 给定的目录路径必须存在                                        |
| FileOrCreate      | 如果给定的路径不存在，则会创建一个 0644 权限的空文件，和 Kubelet 拥有相同的属主和属组 |
| File              | 给定的文件路径必须存在                                        |
| Socket            | 给定的 UNIX socket 路径必须存在                             |
| CharDevice        | 给定的字符串设备路径必须存在                                     |
| BlockDevice       | 给定的块设备路径必须存在                                       |

默认在底层宿主上创建的文件或目录只能由 root 写入，因此需要以 root 权限运行进程或者在宿主上修改文件权限以支持写入 `hostPath` 卷。

{% hint style="info" %}
也可以通过 Pod `securityContext.fsGroup` 来修改卷的属组。
{% endhint %}

示例：

```
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # directory location on host
      path: /data
      # this field is optional
      type: Directory
```

### iscsi

### local（FEATURE STATE: Kubernetes v1.10 beta）

`local` 卷表示已挂载的本地存储设备，如磁盘、分区或目录。`local` 卷只能用于静态创建的 PersistentVolume，还不支持动态配置。相对 `hostPath` 卷，可以持久且可移植的方式使用本地卷，并无需像 `hostPath` 一样指定调度节点，系统通过查看 PersistentVolume 上的节点关联性来了解卷的节点约束（这一点来看，其实只是将原先在 Pod 层面的节点指定移到了卷上指定而已，并没有实质性变化）。

但是 `local` 卷仍然受限于节点，如果节点不健康，那么 `local` 卷也会变得不可访问，使用它的 Pod 也将无法运行。使用 `local` 卷的程序必须能够容忍这种降低可用性以及潜在数据丢失的可能性，这具体取决于底层磁盘的持久性特征。以下是使用 `local` 卷和 `nodeAffinity` 的示例 PersistentVolume 规范：

```
apiVersion:   v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 100Gi
  # volumeMode field requires BlockVolume Alpha feature gate to be enabled.
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/ssd1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - example-node
```

### nfs

### persistentVolumeClaim

### secret

以上列出的笔者觉得比较常用的，有些加了说明，另外还支持 azure、aws 等云厂商的存储，更详细的信息建议参见官方文档 [Types of Volumes](https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes)。


# Persistent Volumes

* [Persistent Volumes 官方说明](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)

## 介绍

为管理存储，K8s 引入了两个新的资源对象：`PersistentVolume` 和 `PersistentVolumeClaim`。

`PersistentVolume`(PV) 是集群中管理员分配的一块存储。它属于集群中的资源，如同节点是集群中的资源一样，它不属于任何 Namespace。PVs 是存储卷插件，它拥有生命周期，但是独立于那些使用 PV 的 Pod 生命周期。支持 NFS、iSCSI 或者云供应商存储系统。

`PersistentVolumeClaim`(PVC) 用于用户请求存储资源。它类似 Pod，Pod 消耗节点资源，而 PVC 消耗 PV 资源。Pods 可以请求特定级别的资源（CPU 和 内存），PVC 可以请求特定的存储大小和访问模式（如可以一次读写挂载或只读模式）。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvmmR9yCJqafe-HQh%2Fpv-pvc.png?generation=1567863296919589\&alt=media)

> 图摘自 [Kubernetes in action](https://www.manning.com/books/kubernetes-in-action)

### PersistentVolumes

#### 容量

当前，仅支持存储大小请求设置，未来可能包括 IOPS、吞吐量等。

#### 访问模式

* ReadWriteOnce(RWO) - 卷只能被一个节点读写挂载
* ReadOnlyMany(ROX) - 卷可以被多个节点以只读方式挂载
* ReadWriteMany(RWX) - 卷可以被多个节点读写挂载

#### Class

一个 PVC 可以通过 `storageClassName` 字段指定 [StorageClass](https://kubernetes.io/docs/concepts/storage/storage-classes/) 的名称来请求特定 class。只有所请求 class 的 PVs 才能绑定到请求指定相同 class 的 PVCs。

{% hint style="warning" %}
早期版本，使用 annotation `volume.beta.kubernetes.io/storage-class` 替代 `storageClassName`。当前 annotation 依然生效，不过在后续版本会被废弃。
{% endhint %}

#### 状态

* Available – 可用 PV，未绑定任何 PVC
* Bound – 已经绑定相关 PVC
* Released – PVC 被删除，资源还未被回收
* Failed – 动态回收失败

### PersistentVolumeClaims

#### Class

同 `PersistentVolume`，`PersistentVolumeClaim` 可以通过 `storageClassName` 指定 [StorageClass](https://kubernetes.io/docs/concepts/storage/storage-classes/) 的名字。只有同 PVC 相同 `storageClassName` 的 PVs，才可以绑定到此 PVC。

## 卷和声明的生命周期

PVs 是集群中的资源，PVCs 是对这些资源的请求，它们遵循以下生命周期：

### 供应

可以通过两种方式配置 PVs：静态或者动态方式。

**静态**

集群管理员创建一定数量的 PVs，它们包括可供集群用户使用实际存储的详细信息。

**动态**

当用户的 `PersistentVolumeClaim` 没有匹配到管理员创建的 PVs 时，集群可能会尝试为 PVC 专门配置动态卷。这种动态提供基于 `StorageClasses`：PVC 请求 [storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/) ，storage class 由管理员创建和配置以达到动态提供的目的。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvmmTMeYhLrMqg9Qz%2Fstorage-class.png?generation=1567863291220809\&alt=media)

> 图摘自 [Kubernetes in action](https://www.manning.com/books/kubernetes-in-action)

为了开启基于 storage class 的动态存储，集群管理员需要在 API server 上启用 `DefaultStorageClass` [admission controller](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#defaultstorageclass)。确认 `DefaultStorageClass` 在 `--enable-admission-plugins` 逗号分隔的参数列表中。

{% hint style="info" %}
在 1.10.x 及以上版本控制选项为 `--enable-admission-plugins`，而 1.9.x 及以下版本为 `--admission-control`。
{% endhint %}

### 绑定

用户在动态卷下创建或者已经创建了具有请求特定存储大小与访问模式的 `PersistentVolumeClaim` 情况下，主控制器中的控制回环监视新的 PVCs，并匹配 PV（如果能匹配到），然后绑定它们在一起。如果一个 PV 动态提供一个新的 PVC，那么该 PV 总是会绑定此 PVC。一旦绑定之后，无论是如何绑定的，`PersistentVolumeClaim` 绑定都是独占的，PVC 到 PV 绑定是一对一映射的。

如果匹配的卷不存在，PVC 将一直处于未绑定状态，直到匹配的卷可用。

### 使用

Pods 使用声明作为卷，集群通过检查声明关联卷并挂载卷到 pod。当一个用户拥有一个 PVC 并且已经处于绑定状态，那么绑定的 PV 只要用户需要，会一直属于他。用户通过他们 Pod volumes 块的 `persistentVolumeClaim` 调度和访问他们声明的 PVs。

### 回收

当用户使用完存储卷，他们可以通过 API 删除 PVC 对象允许资源回收。`PersistentVolume` 回收策略定义集群在 PVC 释放后如何处理，当前支持保留、回收或者删除。

**保留**

`Retain` 回收策略允许手动回收资源，当 `PersistentVolumeClaim` 删除后，`PersistentVolume` 仍然存在并处于 "Released" 状态。因为之前声明的数据仍然存在卷上，PV 依然不能被其它 PVC 绑定。管理员可以通过以下操作回收卷：

* 1、删除 `PersistentVolume`。删除后，关联的存储在外部基础设施（例如 AWS EBS，GCE PD，Azure Disk 或者 Cinder 卷）依然存在
* 2、手动清理相关存储数据
* 3、手动删除相关存储资产，或者如果需要重用，可以重新创建一个新的 `PersistentVolume`

**删除**

对于支持 `Delete` 回收策略的卷插件，删除会移除 `PersistentVolume`，以及外部关联的存储资源，如 AWS EBS、GCE PD、Azure Disk 或者 Cinder 卷。 `StorageClass` 默认的回收策略为 `Delete`，管理员应该根据用户的期望配置该选项，否则需要更新 PV 策略 [Change the Reclaim Policy of a PersistentVolume](https://kubernetes.io/docs/tasks/administer-cluster/change-pv-reclaim-policy/)。

**回收**

使用 `Recycle` 回收策略，会自动清理数据并使其可以被其它新的声明使用。（当前只有 NFS 和 HostPath 支持回收）

{% hint style="warning" %}
`Recycle` 回收策略已经被废弃，推荐使用动态供应代替（即 `StorageClass`）
{% endhint %}


# 集群调度


# 亲和性和反亲和性

* [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/)

通过 Kubernetes 你可以将一个 pod 限制或倾向于在某些特定节点运行。有几种方式可以达到这个目的，它们都通过 [label selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) 进行选择。通常情况下，这样的约束是不必要的，因为调度程序会自动进行合理的调度（如通过一系列的评分机制将 pods 合理分配到最优节点上，而不会将 pod 分配在没有足够资源的节点上等）。但是在某些情况下，可能需要更多的策略控制，例如，将 pod 调度到 SSD 的计算节点上，或者将两个通信比较频繁的不同服务 pod 调度到同一个可用域。

`labels` 在 K8s 中是一个很重要的概念，作为一个标识，Service、Deployments 和 Pods 之间的关联都是通过 `label` 来实现的。而每个节点也都拥有 `label`，通过设置 `label` 相关的策略可以使得 pods 关联到对应 `label` 的节点上。

## nodeSelector

`nodeSelector` 是最简单的约束方式。`nodeSelector` 是 PodSpec 的一个字段。

通过 `--show-labels` 可以查看当前 nodes 的 `labels`

```
$ kubectl get nodes --show-labels
NAME       STATUS    ROLES     AGE       VERSION   LABELS
minikube   Ready     <none>    1m        v1.10.0   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/
hostname=minikube
```

如果没有额外添加 nodes `labels`，那么看到的如上所示的默认标签。我们可以通过 `kubectl label node` 命令给指定 node 添加 `labels`：

```
$ kubectl label node minikube disktype=ssd
node/minikube labeled
$ kubectl get nodes --show-labels
NAME       STATUS    ROLES     AGE       VERSION   LABELS
minikube   Ready     <none>    5m        v1.10.0   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/hostname=minikube
```

> 当然，你也可以通过 `kubectl label node` 删除指定的 `labels`（标签 key 接 `-` 号即可）
>
> ```
> $ kubectl label node minikube disktype-
> node/minikube labeled
> $ kubectl get node --show-labels
> NAME       STATUS    ROLES     AGE       VERSION   LABELS
> minikube   Ready     <none>    23m       v1.10.0 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=minikube
> ```

创建测试 pod 并指定 `nodeSelector` 选项绑定节点：

```
$ cat nginx.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  nodeSelector:
    disktype: ssd
$ kubectl create -f nginx.yaml
pod/nginx created
```

查看 pod 调度的节点，即我们指定有 `disktype=ssd` label 的 minikube 节点：

```
$ kubectl get pods -o wide
NAME      READY     STATUS    RESTARTS   AGE       IP           NODE
nginx     1/1       Running   0          1m        172.18.0.4   minikube
```

`nodeSelector` 可以很方便的解决以上比较简单的需求，但是它还不够灵活。比如我想以机架为单位，部署的服务可以很好的分散在不同机架的服务器上，此时 `nodeSelector` 就并不是那么管用了。因此，Kubernetes 引入了亲和性和反亲和性概念。

## 亲和性和反亲和性（Affinity and anti-affinity）

affinity/anti-affinity 特性还处于 beta 状态，相比 `nodeSelector` 来说有几点优势：

* 1、提供更多的表示式（不仅仅是 and 匹配）
* 2、可以指定 “软” 规则限制而不仅仅是硬限制，因此即使调度器不能满足它的规则，也可以正常调度 pod
* 3、可以针对 node 上运行的 pods 指定 `labels`，而不仅仅局限于 node 本身

affinity 特性拥有两种类型，一种是 node affinity，一种是 pod affinity/anti-affinity。node affinity 类似 `nodeSelector`，但同时拥有上文提到的 1、2 两点优势，pod affinity/anti-affinity 针对 pods 指定 `labels`，同时拥有以上三点优势。

### Node affinity

K8s 在 1.2 的时候以 alpha 的特性引入 node affinity。node affinity 通过 node `labels` 约束 pod 调度节点。Node affinity 有两种类型：

* `requiredDuringSchedulingIgnoredDuringExecution`  （硬限制，同 `nodeSelector`）
* `preferredDuringSchedulingIgnoredDuringExecution` （软限制）

看以下例子：

```
apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/e2e-az-name
            operator: In
            values:
            - e2e-az1
            - e2e-az2
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value
  containers:
  - name: with-node-affinity
    image: k8s.gcr.io/pause:2.0
```

以上规则表达的意思是，该 Pod 只能被调度到拥有 `kubernetes.io/e2e-az-name=e2e-az1` 或者 `kubernetes.io/e2e-az-name=e2e-az2` 标签的节点上，其中在满足之前标签条件的同时更倾向于调度在拥有 `another-node-label-key=another-node-label-value` 标签的节点上。

新的 node affinity 支持 `In`、`NotIn`、`Exists`、`DoesNotExist`、`Gt`、`Lt` 操作符。可以使用 `NotIn`、`DoesNotExist` 来实现反亲和性，也可以通过 [node taints](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/) 来实现。

如果同时指定 `nodeSelector` 和 `nodeAffinity`，两者同时满足才会被调度。如果 `nodeAffinity` 中指定了多个 `nodeSelectorTerms`，只要满足其中一个 `nodeSelectorTerms` 匹配条件即可调度。如果 `nodeSelectorTerms` 中有多个 `matchExpressions`，那么自由满足所有的条件才会被调度。

> [Node affinity and NodeSelector 设计文档](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/scheduling/nodeaffinity.md)

### Pod affinity and anti-affinity

pod 亲和性和反亲和性在 K8s 1.4 版本引入，它基于运行在 node 上的 pod 标签来限制 pod 调度在哪个节点上，而不是节点的标签。

{% hint style="warning" %}
pod 亲和性和反亲和性需要大量的计算，会显著降低集群的调度速度，不建议在大于几百个节点的集群中使用。

pod 反亲和性要求集群中的所有节点必须具有 `topologyKey` 匹配的标签，否则可能会导致意外情况发生。
{% endhint %}

同 node affinity，pod 亲和性和反亲和性也有两种类型：

* `requiredDuringSchedulingIgnoredDuringExecution` （硬限制）
* `preferredDuringSchedulingIgnoredDuringExecution` （软限制）

不同的是，pod 通过 `podAntiAffinity` 设置反亲和性，如下例子：

```
apiVersion: v1
kind: Pod
metadata:
  name: with-pod-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: failure-domain.beta.kubernetes.io/zone
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: kubernetes.io/hostname
  containers:
  - name: with-pod-affinity
    image: k8s.gcr.io/pause:2.0
```

以上示例表示，pod 必须调度在至少运行一个 `security=S1` 标签的 pod 的节点上（更准确的说，这个 pod 可以运行在节点 N 上，如果该节点有标签 key 为 `failure-domain.beta.kubernetes.io/zone`，而且运行着标签为 `security=S1` 的实例）。另外，反亲和规则表明最好不要调度到运行有 `security=S2` 标签的 pod 的节点上（更准确的说，如果这个节点拥有标签 key 为 `failure-domain.beta.kubernetes.io/zone`，但运行有 `security=S2` 标签的 pod，那么这个节点就不会被优先选择调度）。

> [pod affinity and anti-affinity 设计文档](https://git.k8s.io/community/contributors/design-proposals/scheduling/podaffinity.md)

`podAffinity` 和 `podAntiAffinity` 支持 `In`、`NotIn`、`Exists`、`DoesNotExist` 四种表达式。

原则上，`topologyKey` 可以为任何合法的键值对。但是因为性能和安全的原因，有以下限制：

* 1、针对 `podAffinity` 和 `podAntiAffinity` 中的 `requiredDuringSchedulingIgnoredDuringExecution`， `topologyKey` 为空是不允许的
* 2、针对 `podAntiAffinity` 中的 `requiredDuringSchedulingIgnoredDuringExecution`，准入控制器选项 `LimitPodHardAntiAffinityTopology` 可以把 `topologyKey` 限制为 `kubernetes.io/hostname`，如果想自定义值，可以修改准入控制器或者直接禁用
* 3、针对 `podAntiAffinity` 中的 `preferredDuringSchedulingIgnoredDuringExecution`，`topologyKey` 为空，则代表所有拓扑（仅限于 `kubernetes.io/hostname`, `failure-domain.beta.kubernetes.io/zone` and `failure-domain.beta.kubernetes.io/region`）
* 4、除了以上情况，`topologyKey` 可以为任意合法的键值对

除了 `labelSelector` 和 `topologyKey`，还可以指定 namespace 的 `labelSelector` 作为匹配。`labelSelector` 和 `topologyKey` 属于同一级别，如果未定义或设置为空值，那么默认为定义 pod affinity 和 anti-affinity 所在的空间。

## 实践案例

### 始终调度在同一个节点

在一个三个节点的集群中，一个 web 应用程序依赖内存存储，如 redis，我们想 web 程序尽可能的和缓存调度在同一个节点上。redis 的 Deployment 配置如下：

```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
spec:
  selector:
    matchLabels:
      app: store
  replicas: 3
  template:
    metadata:
      labels:
        app: store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: redis-server
        image: redis:3.2-alpine
```

redis-cache 规则表达 pod 不被调度在同一个节点上。web-server 规则设置如下，表达 pod 不被调度在同一个节点上，并且必须调度在运行标签 `app=store` pod 的节点上。

```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-server
spec:
  selector:
    matchLabels:
      app: web-store
  replicas: 3
  template:
    metadata:
      labels:
        app: web-store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web-store
            topologyKey: "kubernetes.io/hostname"
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: web-app
        image: nginx:1.12-alpine
```

> 相关更多应用场景，建议阅读 [抽象优雅的 Affinity](http://wsfdl.com/kubernetes/2018/06/30/k8s-scheduler-1-affinity.html)，讲解的非常详细。


# 污点和容忍机制

* [Taints and Tolerations](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/)

节点亲和性（affinity），是 pods 的一种属性，可以将 pods 调度到一类节点上去（作为优先选择或者一个硬性要求）。污点（Taints）则相反，它们允许节点排斥一类 pods。

Taints 和 tolerations 一起工作以确保 pods 不调度到不适合的节点上去。一个或者多个 taints 规则应用于节点，这标记节点不会接受任何没有容忍这些 taints 规则的 pods。Tolerations 规则应用于 pods，并且允许（非强制）这些 pods 调度到匹配 taints 规则的节点上。

## 概念

可以通过 [kubectl taint](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#taint) 给节点添加一个 taint 规则。如：

```
kubectl taint nodes node1 key=value:NoSchedule
```

给节点 `node1` 标记了一个 taint。这个 taint 包含键 `key`，值 `value`，以及 taint effect `NoSchedule`。这意味着没有 pod 能够调度到 `node1` 上，除非它有匹配的 toleration。

如果要移除刚刚添加的 taint，可以运行：

```
kubectl taint nodes node1 key:NoSchedule-
```

你可以在 PodSpec 字段指定一个 toleration。以下两个 tolerations 都匹配上面通过 `kubectl taint` 创建的 taint，因此有任何一个 toleration 都可以调度到 `node1`：

```
tolerations:
- key: "key"
  operator: "Equal"
  value: "value"
  effect: "NoSchedule"
```

```
tolerations:
- key: "key"
  operator: "Exists"
  effect: "NoSchedule"
```

这儿有一个 pod 使用 tolerations 的例子：

```
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  tolerations:
  - key: "example-key"
    operator: "Exists"
    effect: "NoSchedule"
```

如果在 `key` 和 `effect` 相同的情况下，toleration 则匹配 taint，其中：

* `operator` 是 `Exists`（在这种情况下不应指定任何 `value`），或
* `operator` 是 `Equal` 或 `value` 相等

如果不指定 `operator`，默认为 `Equal`。

> **注意：** 这里有两个特殊的案例：
>
> * `key` 为空，`operator` 是 `Exists` 则匹配所有的键值和效果，意味着可以 tolerate 一切。
>
> ```
> tolerations:
> - operator: "Exists"
> ```
>
> * 一个空的 `effect` 匹配所有键为 `key` 的效果。
>
> ```
> tolerations:
> - key: "key"
>   operator: "Exists"
> ```

上面的例子使用了 `effect` 为 `NoSchedule`。或者，你可以使用 `effect` 为 `PreferNoSchedule`。这是 `NoSchedule` 的 “优先选项” 或者 “软” 版本 -- 系统会尝试避免调度一个没有容忍 taint 的 pod 到该节点上，但是这不是强制的。第三种类型的 `effect` 是 `NoExecute`，后面再描述。

你可以在同一个节点上设置多个 taints，也可以在同一个 pod 上设置多个 tolerations。Kubernetes 处理多个 taints 和 tolerations 的方式就像一个过滤器：遍历一个节点上的所有 taints，然后忽略 pod 上有匹配 toleration 的 taints。其它未忽略的 taints 会对 pod 产生作用。特别是：

* 如果至少有一个 `effect` 是 `NoSchedule` 的未忽略的 taint，那么 Kubernetes 将不会在该节点上调度这个 pod
* 如果没有 `effect` 是 `NoSchedule` 的未忽略 taint，但至少有一个 `effect` 是 `PreferNoSchedule` 的未忽略的 taint，那么 Kubernetes 会尝试不让该 pod 调度到此节点上
* 如果有至少一个 `effect` 是 `NoExecute` 的未忽略 taint，那么 pod 会从该节点上驱离（如果 pod 已经运行在该节点），并且不会被调度到该节点（如果 pod 没有在节点运行）。

举例来说，有个节点有如下 taint：

```
kubectl taint nodes node1 key1=value1:NoSchedule
kubectl taint nodes node1 key1=value1:NoExecute
kubectl taint nodes node1 key2=value2:NoSchedule
```

pod 有两个 tolerations：

```
tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoSchedule"
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExecute"
```

这个案例，pod 不会被调度到这个节点上，因为 pod 没有匹配第三个 taint。但是如果在节点添加 taint 的时 pod 已经运行了，那么 pod 会继续在该节点运行（简单说 `NoSchedule` 只在调度时生效）。

正常情况下，如果一个 `effect` 是 `NoExecute` 的 taint 添加到节点，那些没有容忍此 taint 的将会被直接驱逐，然后那些容忍这个 taint 的 pods 将永远不会被驱逐。另外，一个 `effect` 是 `NoExecute` 的 toleration 可以指定一个可选的 `tolerationSeconds` 字段，以指示当节点 taint 被添加之后 pod 运行在节点的时长，如：

```
tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExecute"
  tolerationSeconds: 3600
```

意思是 pod 在节点运行时，当一个匹配的 taint 被添加到该节点后，那么这个 pod 将继续在当前节点运行 3600s，之后会被驱逐。如果 taint 在时间到达之前被移除，那么 pod 不会被驱逐。

## 用例

Taints 和 tolerations 是一种灵活的方式，将 pod 从节点上移除或驱逐不应运行的 pods。

* **专用节点：** 如果你想将一组节点给一类特定的用户使用，你可以在这些节点上添加 taint（也就是说，`kubectl taint nodes nodename dedicated=groupName:NoSchedule` ）并且在他们的 pods 上添加 toleration（通过自定义[准入控制器](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/)会更容易做到）。这些拥有 tolerations 的 pods 将被允许使用 tainted 的节点以及集群中的其它节点。如果你想要专门使用这些节点并确保只会使用这些节点，那么还需要像给这组节点添加 taint 一样添加一个标签（如，`kubectl label nodes nodename edicated=groupName`），并且准入控制器还要添加一个节点 affinity，以要求 pods 只能调度到有 `dedicated=groupName` 标签的节点上。
* **拥有特定硬件的节点：** 在一小部分节点具有专用硬件（如 GPU）的集群中，最好将不需要专用硬件的 pods 排除在这些节点之外，从而为专用硬件的 pods 留出空间。这个可以通过给特定硬件的节点添加 taint（如 `kubectl taint nodes nodename special=true:NoSchedule` 或者 `kubectl taint nodes nodename special=true:PreferNoSchedule`）并在需要使用这些特定硬件的 pods 上添加相应的 toleration。像在专用节点用例中一样，使用自定义准入控制器来应用 tolerations 可能是最简单的。举例来说，推荐使用 Extended Resources 来表示特殊硬件，用扩展资源的名称 taint 你的特定硬件并且运行 [ExtendedResourceToleration](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#extendedresourcetoleration) 准入控制器。现在，因为节点已经被 tainted，没有相应 toleration 的 pods 不会被调度到这些节点，`ExtendedResourceToleration` 准入控制器会自动的向 pods 添加正确的 toleration，然后 pod 将会被调度在这些特定硬件的节点上。这样可以确保这些特定硬件的节点被请求此类硬件的 pods 使用，而你不需要手动向 pods 中添加 tolerations。
* **通过 taint 驱逐：** 节点出现问题时，可以按每个节点配置驱逐行为，这个后面详解。

## 通过 taint 驱逐

前面我们提到了 `NoExecute` taint `effect`，它会影响已经运行在节点的 pods：

* 没有相应 tolerations 的 pods 将会被直接驱逐
* 有相应 tolerations 并且未指定 `tolerationSeconds` 的 pods 永远保持运行
* 有相应 tolerations 且指定 `tolerationSeconds` 的 pods 会在时间到达之后被驱逐

此外，Kubernetes 1.6 时以 alpha 状态引入该功能来支持表示节点问题。换句话说，当满足特定条件时，节点控制器会自动对节点进行 taints。以下为内建的 taints：

* `node.kubernetes.io/not-ready`：节点没有就绪。对应 NodeCondition `Ready` 为 "False"
* `node.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.kubernetes.io/unschedulable`：节点不可调度
* `node.cloudprovider.kubernetes.io/uninitialized`：当使用外部 cloud provider 启动 kubelet 时，将节点设置 taint 以标记为不可用。来自 cloud-controller-manager 的控制器初始化此节点后，kubelet 删除此 taint。

如果要驱逐节点，则节点控制器或者 kubelet 会添加具有 `NoExecute` 的 `effect` 相关 taints。如果故障情况恢复正常，则 kubelet 或节点控制器会移除相关的 taints。

> **注意：** 为了维持由于节点问题导致驱逐的速率限制行为，系统实际以速率限制的方式添加 taints。 这样可以防止主节点和节点连接中断发生大规模的 pod 驱逐。

该功能和 `tolerationSeconds` 结合使用，允许一个 pod 指定当一个或者多个问题的时候在节点运行的时间。

例如，一个具有很多本地状态的应用在网络中断事件中像停留一段事件，以期望网络能够恢复，这样可以避免 pod 被驱逐。这个 pod 的 toleration 设置可以如下：

```
tolerations:
- key: "node.kubernetes.io/unreachable"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 6000
```

注意 Kubernetes 会自动添加一个 `node.kubernetes.io/not-ready` 且 `tolerationSeconds=300` 的 toleration，除非 pod 配置中已经设置 `node.kubernetes.io/not-ready` 的 toleration。同样的它会添加一个 `node.kubernetes.io/unreachable` 且 `tolerationSeconds=300` 的 toleration，除非 pod 配置中已经设置 `node.kubernetes.io/unreachable` 的 toleration。

这些自动添加的 tolerations 确保在检测到这些问题之后，pod 默认会在当前节点保留运行 5 分钟。这两个默认 tolerations 被 [DefaultTolerationSeconds admission controller](https://git.k8s.io/kubernetes/plugin/pkg/admission/defaulttolerationseconds) 添加。

DaemonSet 创建含有 `NoExecute` tolerations 的 pods 针对以下 taints 没有 `tolerationSeconds` 选项：

* `node.kubernetes.io/unreachable`
* `node.kubernetes.io/not-ready`

这样可以确保 DeamonSet pods 不会因为这些问题而被驱逐。

## 根据节点状态 taint

节点生命周期控制器自动会为对应节点状态的节点创建 `effect` 为 `NoSchedule` 的 taints。同样调度器不会检查节点状态，而是检查 taints。这样可以保证节点状态不会影响到已调度的 pods。用户可以通过添加适当的 Pod tolerations 以选择忽略某些节点问题。

从 Kubernetes 1.8 开始，DaemonSet 控制器自动给所有的 daemons 添加 `NoSchedule` tolerations，以阻止 DaemonSets 中断。

* `node.kubernetes.io/memory-pressure`
* `node.kubernetes.io/disk-pressure`
* `node.kubernetes.io/out-of-disk (only for critical pods)`
* `node.kubernetes.io/unschedulable (1.10 or later)`
* `node.kubernetes.io/network-unavailable (host network only)`

添加这些 tolerastions 确保向后兼容。你可以向 DaemonSets 中添加任意的 tolerations。

## 扩展阅读

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2Fsync%2Fdbf6ea5f8203a5fdf0e3ad182ba6cf270cf6bf76.gif?generation=1588918924066988\&alt=media)

* [Taints and tolerations, pod and node affinities demystified](https://banzaicloud.com/blog/k8s-taints-tolerations-affinities/)


# 集群组件


# Kubelet

## 配置项

```
[root@k8s bin]# ./kubelet --version
Kubernetes v1.7.3
[root@k8s bin]# ./kubelet --help
Usage of ./kubelet:
      --address ip        The IP address for the Kubelet to serve on (set to 0.0.0.0 for all interfaces) (default 0.0.0.0)
      --allow-privileged  If true, allow containers to request privileged mode.
      ... ...
```

### 配置说明

#### 基本配置项

| 选项                                 | 说明                                                                                                                          |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| --address ip                       | kubelet 监听地址，默认 `0.0.0.0`，表示监听在所有网络接口                                                                                       |
| --allow-privileged                 | 如果值为 `true` 则允许容器请求 `privileged` 选项，默认 `false`                                                                              |
| --cadvisor-port int32              | 指定 cAdvisor 端口， 默认 `4194`                                                                                                   |
| --cluster-dns stringSlice          | 指定集群 DNS 服务地址列表，通过逗号分隔，用于 Pod 设置项 `dnsPolicy=ClusterFirst` 的容器 DNS 服务器。                                                     |
| --cpu-cfs-quota                    | 启用 CPU CFS 配额用于容器 CPU 资源限制，默认 `true`                                                                                        |
| --kubeconfig string                | kubeconfig 文件路径, 指定如何连接 API server。除非 `--require-kubeconfig` 选项设置了，否则使用 `--api-servers`。 默认 `"/var/lib/kubelet/kubeconfig"` |
| --http-check-frequency duration    | http check 时间间隔，默认 `20s`                                                                                                    |
| --kube-api-burst int32             | Burst 用于 kubelet 与 apiserver 通信限制，默认 `10`                                                                                   |
| --kube-api-content-type string     | 指定发送请求给 apiserver 的通信内容类型，默认 `"application/vnd.kubernetes.protobuf"`                                                        |
| --kube-api-qps int32               | QPS 用于 kubelet 与 apiserver 通信限制，默认 `5`                                                                                      |
| --max-pods int32                   | 当前 Kubelet 节点上可以运行的最大 pod 数，默认 `110`                                                                                        |
| --max-open-files int               | kubelet 进程最大打开文件句柄数, 默认 `1000000`                                                                                           |
| --node-labels mapStringString      | <警告：Alpha 特性> 当启动时注册到 apiserver 的标签，标签必须 `key=value` 键值对，以逗号分隔                                                              |
| --pod-infra-container-image string | pod 中容器共享的 `network/ipc` 命名空间基础组件镜像，默认 `"gcr.io/google_containers/pause-amd64:3.0"`                                         |
| --port int32                       | kubelet 监听端口，默认 `10250`                                                                                                     |
| --require-kubeconfig               | 如果设置为 `true`，则配置不存在 Kubelet 进程会退出, 并且会忽略 `--api-servers` 选项                                                                 |
| -read-only-port int32              | kubelet 只读端口，一般用于 metrics 信息获取，设置为 0 表示禁用，默认 `10255`                                                                        |
| --resolv-conf string               | DNS 解析文件指定，默认为 `"/etc/resolv.conf"`                                                                                         |
| --root-dir string                  | 用于管理 kubelet 文件（volume mounts 等）目录路径，默认 `"/var/lib/kubelet"`                                                                |

#### 认证配置项

| 选项                            | 说明                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --anonymous-auth              | 启用对 kubelet 服务的匿名请求，匿名请求的用户名为 `system:anonymous`，组名为 `system:unauthenticated`，默认为 `true`                                                                                                                                                                                                                                                                                                                                                                       |
| --authorization-mode string   | kubelet 授权方式，可用选项包括 `AlwaysAllow` 和 `Webhook`，`Webhook` 通过 SubjectAccessReview API 确认授权，默认 `"AlwaysAllow"`                                                                                                                                                                                                                                                                                                                                                     |
| --bootstrap-kubeconfig string | Path to a kubeconfig file that will be used to get client certificate for kubelet. If the file specified by --kubeconfig does not exist, the bootstrap kubeconfig is used to request a client certificate from the API server. On success, a kubeconfig file referencing the generated client certificate and key is written to the path specified by --kubeconfig. The client certificate and key file will be stored in the directory pointed by --cert-dir. |
| --cert-dir string             | TLS certs 证书目录，如果同时指定 `--tls-cert-file` 和 `--tls-private-key-file` 则该参数会被忽略。 默认 `"/var/run/kubernetes"`                                                                                                                                                                                                                                                                                                                                                        |
| --client-ca-file string       | If set, any request presenting a client certificate signed by one of the authorities in the client-ca-file is authenticated with an identity corresponding to the CommonName of the client certificate.                                                                                                                                                                                                                                                        |
| --tls-cert-file string        | 包含 x509 证书的文件路径，用于提供 HTTPS 服务，如果未提供 `--tls-cert-file` 和 `--tls-private-key-file`，则会为公用地址生成自签名证书和密钥，并保存到 `--cert-dir` 目录                                                                                                                                                                                                                                                                                                                                        |
| --tls-private-key-file string | 包含 X509 匹配 `--tls-cert-file` 私钥的文件路径                                                                                                                                                                                                                                                                                                                                                                                                                           |

#### 日志配置项

| 选项                               | 说明                                                               |
| -------------------------------- | ---------------------------------------------------------------- |
| --alsologtostderr                | 日志输出到文件同时输出到 stderr                                              |
| --log-backtrace-at traceLocation | when logging hits line file:N, emit a stack trace (default `:0`) |
| --log-cadvisor-usage             | 记录 cadvisor 运行日志                                                 |
| --log-dir string                 | 如果非空，则输出日志到指定目录                                                  |
| --log-flush-frequency duration   | 日志刷新的最大间隔秒数，默认 `5s`                                              |
| --logtostderr                    | 日志输出到标准错误输出而不是文件，默认值为 `true`                                     |
| --stderrthreshold severity       | 超过此阈值的日志将转到 stderr，默认为 `2`                                       |
| -v, --v Level                    | log level for V logs                                             |

#### Docker 配置项

| 选项                           | 说明                                                                                                                              |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| --container-runtime string   | 选择容器运行类型，`docker` 和 `rkt` 值可供选择，默认 `"docker"`                                                                                   |
| --docker string              | docker endpoint 设置，默认为 `"unix:///var/run/docker.sock"`                                                                          |
| --docker-disable-shared-pid  | 当使用 Docker 1.13.1 或更高版本运行时，容器运行时接口（CRI）针对一个 pod 中的容器间默认为共享 PID 命名空间。通过设置此标志可以达到 pod 间容器 PID 命名空间互相隔离。此功能将在未来的 Kubernetes 版本中被删除 |
| --docker-endpoint string     | 同 `--docker`，默认为 `"unix:///var/run/docker.sock"`                                                                                |
| --docker-exec-handler string | 容器中执行命令 Handler 指定，有 `"native"` 和 `"nsenter"` 可选，默认 `"native"`                                                                  |
| --docker-only                | 除了根信息统计之外，只报告 docker 容器统计数据                                                                                                     |

#### 镜像配置项

| 选项                                      | 说明                                                                                             |
| --------------------------------------- | ---------------------------------------------------------------------------------------------- |
| --image-gc-high-threshold int32         | 当磁盘使用率达到该百分比后会一直运行镜像 GC 机制，默认 `85`                                                             |
| --image-gc-low-threshold int32          | 在磁盘使用率没有达到该百分比之前，不触发镜像 GC 机制，默认 `80`                                                           |
| --image-pull-progress-deadline duration | 如果指定时间 pull 镜像没有任何进度，则取消 pull，默认 `1m0s`                                                        |
| --minimum-image-ttl-duration duration   | 在镜像 GC 之前未使用的镜像最小时间值。 例如 `300ms`, `10s` 或者 `2h45m`，默认 `2m0s`                                   |
| --registry-burst int32                  | 最高 pull 数限制, 实际值依然受 `registry-qps`限制，不能超过该值，并且只有 `--registry-qps > 0` 才生效，默认 `10`              |
| --registry-qps int32                    | 如果 > 0, 限制 registry pull QPS 为指定值，如果为 0, 则不限制，默认 `5`                                           |
| --serialize-image-pulls                 | 一次只 pull 一个镜像。在 docker daemon 版本 < 1.9 或者使用 Aufs 存储驱动的时候不建议修改默认值。具体可以参见 Issue #10959，默认 `true` |

#### 网络配置项

| 选项                         | 说明                                                                  |
| -------------------------- | ------------------------------------------------------------------- |
| --cni-bin-dir string       | <警告: Alpha 特性> 指定搜索 CNI plugin binaries 的目录绝对路径，默认 `"/opt/cni/bin"` |
| --cni-conf-dir string      | <警告: Alpha 特性> 指定搜索 CNI 配置文件的目录绝对路径，默认 `"/etc/cni/net.d"`           |
| --network-plugin string    | <警告: Alpha 特性> 指定网络插件名称，如 `"--network-plugin=cni"` 指定使用 `cni` 插件    |
| --network-plugin-mtu int32 | <警告: Alpha 特性> 通过网络插件传值 MTU，覆盖系统默认值。 如果设置为 0 则默认使用 `1460` MTU.      |

#### Volume 卷配置项

| 选项                                 | 说明                                                                                                                         |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| --enable-controller-attach-detach  | 启用 `Attach/Detach` controller 管理调度到该节点的 volume 卷 `attachment/detachment` 操作，并且禁用 kubelet 执行任何 `attach/detach` 操作，默认 `true` |
| --keep-terminated-pod-volumes      | 在 pod 终止后，将终止的 pod 卷在节点保留，可用于调试卷相关的问题                                                                                      |
| --volume-plugin-dir string         | <警告: Alpha 特性> 指定搜索其他第三方卷插件的目录绝对路径 ，默认 `"/usr/libexec/kubernetes/kubelet-plugins/volume/exec/"`                            |
| --volume-stats-agg-period duration | 指定 kubelet 所有 pod 统计以及缓存卷磁盘使用率的时间间隔。如果要禁用，设置该值为 0 即可，默认 `1m0s`                                                             |

#### cgroup/namespace 配置项

| 选项                                 | 说明                                                                                                                                                                                              |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --cgroup-driver string             | kubelet 操作主机 cgroups 驱动选择，`cgroupfs` 和 `systemd` 值可供选择，默认 `"cgroupfs"`。**CentOS 7 设置为 `"systemd"`**                                                                                             |
| --cgroup-root string               | 针对 pod 可选项 root cgroup，默认 `''`，表示使用容器运行时的默认值                                                                                                                                                    |
| --cgroups-per-qos                  | Enable creation of QoS cgroup hierarchy, if true top level QoS and pod cgroups are created. 默认 `true`                                                                                           |
| --host-ipc-sources stringSlice     | 指定允许使用主机 ipc namespace pod 列表，默认 `[*]`，逗号分隔                                                                                                                                                     |
| --host-network-sources stringSlice | 指定允许使用 host network 的 pod，默认 `[*]`，逗号分隔                                                                                                                                                         |
| --host-pid-sources stringSlice     | 指定允许使用主机 pid namespace pod 列表，默认 `[*]`，逗号分隔                                                                                                                                                     |
| --kube-reserved-cgroup string      | Absolute name of the top level cgroup that is used to manage kubernetes components for which compute resources were reserved via '--kube-reserved' flag. Ex. '/kube-reserved'. 默认 `''`          |
| --kubelet-cgroups string           | Optional absolute name of cgroups to create and run the Kubelet in.                                                                                                                             |
| --runtime-cgroups string           | Optional absolute name of cgroups to create and run the runtime in.                                                                                                                             |
| --system-cgroups /                 | Optional absolute name of cgroups in which to place all non-kernel processes that are not already inside a cgroup under /. Empty for no container. Rolling back the flag requires a reboot.     |
| --system-reserved-cgroup string    | Absolute name of the top level cgroup that is used to manage non-kubernetes components for which compute resources were reserved via '--system-reserved' flag. Ex. '/system-reserved'.  默认 `''` |

{% hint style="info" %}
cgroup 相关选项有些笔者没有深入相关调研，建议使用默认值即可
{% endhint %}

#### event 配置项

| 选项                                 | 说明                                                                                                                                                                                                                                                                               |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --event-burst int32                | Maximum size of a bursty event records, temporarily allows event records to burst to this number, while still not exceeding event-qps. Only used if --event-qps > 0 (default 10)                                                                                                 |
| --event-qps int32                  | If > 0, limit event creations per second to this value. If 0, unlimited. (default 5)                                                                                                                                                                                             |
| --event-storage-age-limit string   | Max length of time for which to store events (per type). Value is a comma separated list of key values, where the keys are event types (e.g.: creation, oom) or "default" and the value is a duration. Default is applied to all non-specified event types (default "default=0") |
| --event-storage-event-limit string | Max number of events to store (per type). Value is a comma separated list of key values, where the keys are event types (e.g.: creation, oom) or "default" and the value is an integer. Default is applied to all non-specified event types (default "default=0")                |

{% hint style="info" %}
event 配置项笔者也没有进行相关设置，建议使用默认值即可
{% endhint %}

#### Pod `eviction` 配置项

| 选项                                             | 说明                                                                                                                                                                                                                                 |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --eviction-soft string                         | pod eviction 阈值软限制（例如 `"memory.available<1.5Gi"`），如果超过 grace period 则会触发 pod 驱逐机制                                                                                                                                                  |
| --eviction-soft-grace-period string            | 设置 eviction grace periods（例如 `"memory.available=1m30s"`），对应达到 pod eviction 软阈值时，触发 pod eviction 所需要等待的时间                                                                                                                           |
| --eviction-max-pod-grace-period int32          | 当达到 pod eviction 软阈值的时候，terminating pods 最长允许的 grace period (in seconds)。如果为负数, 则取决于 pod 实际指定的值。默认全局的一个 grace period 为 `30s` 详见 [Termination of Pods](https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods) |
| --eviction-hard string                         | pod eviction 阈值硬限制（例如  `"memory.available<1Gi"`），如果达到该值则会触发 `pod eviction`，默认为 `"memory.available<100Mi"`                                                                                                                          |
| --eviction-minimum-reclaim string              | 最小回收值设置，（例如 `"imagefs.available=2Gi"`），表示当 kubelet 资源处于压力状态下执行 pod eviction 时回收的最小资源量                                                                                                                                              |
| --eviction-pressure-transition-period duration | 在转移 eviction pressure 条件前，kubelet 需要等待的时间，默认 `5m0s`)                                                                                                                                                                               |

关于 kubelet eviction 策略可参考：

* [Eviction Policy](https://kubernetes.io/docs/tasks/administer-cluster/out-of-resource/#eviction-policy)
* [Learn how kubelet eviction policies impact cluster rebalancing](https://blog.kublr.com/learn-how-kubelet-eviction-policies-impact-cluster-rebalancing-2e976ebc53ea)

### 推荐配置项

**/etc/kubernetes/kubelet**

```
###
# kubernetes kubelet config

# The address for the info server to serve on (set to 0.0.0.0 or "" for all interfaces)
KUBELET_ADDRESS="--address=0.0.0.0"

# The port for the info server to serve on
KUBELET_PORT="--port=10250"

# You may leave this blank to use the actual hostname 根据实际需求填写，默认主机名
KUBELET_HOSTNAME="--hostname-override=<hostname>"

# location of the api-server 根据实际需求填写，后续该配置会被废弃，--kubeconfig 替换
KUBELET_API_SERVER="--api-servers=http://<apiserver>:8080"

# pod infrastructure container 建议把 pause 组件 push 到私有内部镜像
KUBELET_POD_INFRA_CONTAINER="--pod-infra-container-image=<private_registry>/google_containers/pause-amd64:3.0"

# Add your own! --cluster-dns 根据实际选项填写
KUBELET_ARGS="--cluster-dns=<kubedns-ip> --image-gc-high-threshold=85 --image-gc-low-threshold=70 --serialize-image-pulls=false --cgroup-driver=systemd --fail-swap-on=false --max-pods=50 --container-runtime=docker --cloud-provider="""
```


# 网络方案


# 网络策略

* [Network Policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/#isolated-and-non-isolated-pods)

如果你在 IP 地址或者端口级别（OSI 3 层或者 4 层）控制流量，那么你可以考虑对集群中的特定应用程序使用 Kubernetes 网络策略。

网络策略通过 [network plugin](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/) 实现，使用网络策略必须选用支持 `NetworkPolicy` 的网络解决方案，比如 Calico 方案是支持网络策略的。创建 `NetworkPolicy` 资源，但是没有相关的控制器实现，策略本身是不生效的。可以类比 `Ingress` 如果没有 ingress-controller，本身的规则也是无意义的。

默认，pods 之间所有的流量都放行的。网络策略不存在冲突，如果一个或者多个策略选择一个 Pod，那么该 Pod 受限于这些策略的 ingress/egress 规则允许的并集。评估顺序本身并不影响策略结果。

## NetworkPolicy 资源

```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - ipBlock:
        cidr: 172.17.0.0/16
        except:
        - 172.17.1.0/24
    - namespaceSelector:
        matchLabels:
          project: myproject
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 6379
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
    ports:
    - protocol: TCP
      port: 5978
```

* `podSelector`：每个 NetworkPolicy 包括一个 `podSelector` 用于选择策略所应用的 pods 分组。示例中策略选择器拥有 `role: db` 标签的 pods。空的 `podSelector` 匹配空间下的所有 pods。
* `policyTypes`：每个 NetworkPolicy 包括一个 `policyTypes` 列表，包括 `Ingress`，`Egress` 或两者都有。`policyTypes` 表示是否应用 ingress（入口） 流量到选定 pod 或者从选择的 pods 应用 egress（出口） 流量规则。 如果 NetworkPolicy 上未指定任何 `policyTypes`，则默认情况下始终设置 `Ingress`，如果 NetworkPolicy 具有任何出口规则，则设置 `Egress`。
* `ingress`：每个 NetworkPolicy 可以包括允许的 `ingress` 规则列表。每个规则允许匹配 `from` 和 `ports` 部分的流量。示例中包含一个规则，表示任何匹配源中都可以访问匹配的 Pod 的 6379 TCP 端口。
* `egress`：每个 NetworkPolicy 可以包括允许的 egress 规则列表。每个规则允许匹配到 `to` 和 `ports` 部分的流量。示例中包含一个规则，表示匹配的 Pod 可以访问任何在网段 10.0.0.0/24 中的 5978 TCP 端口。

示例规则综合起来表示，隔离 default 空间下标签为 "role=db" 的 pods 的 ingress 和 egress 流量。对于 ingress 流量，针对 default 空间下所有含有 "role=db" 的 pods 的 6379 端口访问，规则允许 "default" 空间下任何标签为 "role=frontend" 的 pod 访问，允许含有 "project=myproject" 标签的空间下的任何 pod 访问，还允许 172.17.0.0/16 网段中除了 172.17.1.0/24 段的 IP 访问。出口流量则允许 default 空间下含有 "role=db" 的 pod 访问网段为 10.0.0.0/24 并且端口为 5978 的服务。

## `to` 和 `from` 选择器

支持以下四种方式过滤选择：

* `podSelector`：在 NetworkPolicy 同一空间下选择 Pods，作为 ingress 源或者 egress 目的地
* `namespaceSelector`：选择特定空间下的所有 Pods，作为 ingress 源或者 egress 目的地
* `to` 或者 `from` 下的 `namespaceSelector` 和 `podSelector`： to/from 下的条目，同时指定 `namespaceSelector` 和 `podSelector` 来指定特定的 pods
* `ipBlock`：选择特定的 IP CIDR 范围来允许 ingress 源或者 egress 目的地。这些应该用于集群外的 IP，因为 Pod IP 并不是固定的

更多的示例可以参考 [Kubernetes Network Policy Recipes](https://github.com/ahmetb/kubernetes-network-policy-recipes)


# Calico BGP 网络（v2.6.x）

> 基于版本为 Calico v2.6.x「当前官方最新版本为 v3.0 [Calico Reference](https://docs.projectcalico.org/v3.0/reference/)」

## calicoctl

`calicoctl` 是 calico 网络命令行管理工具。

### 概览

```
# calicoctl --help
Usage:
  calicoctl [options] <command> [<args>...]

    create    Create a resource by filename or stdin.   
              // 从标准输入或者文件创建资源
    replace   Replace a resource by filename or stdin.  
              // 从标准输入或者文件更新资源
    apply     Apply a resource by filename or stdin.  This creates a resource
              if it does not exist, and replaces a resource if it does exists.
              // 从标准输入或者文件应用资源，如果资源存在则更新「Replace」，不在则创建 「Create」
    delete    Delete a resource identified by file, stdin or resource type and
              name.
              // 从文件、标准输出或者资源类型和名字删除资源
    get       Get a resource identified by file, stdin or resource type and
              name.
              // 通过文件、标准输入或者资源类型和名字获取定义的资源
    config    Manage system-wide and low-level node configuration options.
              // 管理系统层和较低级别的节点配置选项
    ipam      IP address management.
              // IP 地址管理
    node      Calico node management.
              // Calico 节点管理
    version   Display the version of calicoctl.
              // 显示 calicoctl 版本

Options:
  -h --help               Show this screen.
  -l --log-level=<level>  Set the log level (one of panic, fatal, error,
                          warn, info, debug) [default: panic]
                          // 设置日志级别
```

### create

```
# calicoctl create --help
Set the Calico datastore access information in the environment variables or
or supply details in a config file.

Usage:
  calicoctl create --filename=<FILENAME> [--skip-exists] [--config=<CONFIG>]

Examples:
  # Create a policy using the data in policy.yaml.
  # 通过 yaml 文件创建对应资源
  calicoctl create -f ./policy.yaml

  # Create a policy based on the JSON passed into stdin.
  # 通过传递 json 内容到标准输出创建对应资源
  cat policy.json | calicoctl create -f -

Options:
  -h --help                 Show this screen.
  -f --filename=<FILENAME>  Filename to use to create the resource.  If set to
                            "-" loads from stdin.
     --skip-exists          Skip over and treat as successful any attempts to
                            create an entry that already exists.
  -c --config=<CONFIG>      Path to the file containing connection
                            configuration in YAML or JSON format.
                            [default: /etc/calico/calicoctl.cfg]
... ...
Valid resource types are:

  * node
  * bgpPeer
  * hostEndpoint
  * workloadEndpoint
  * ipPool
  * policy
  * profile
... ...
```

> v3.0 中新增了 `-n --namespace=<NS>` 选项

### replace

使用选项同 `create`，`replace` 用于更新，如果资源对象不存在则抛错。

### apply

使用选项同 `create`，`apply` 执行时如果资源不存在则创建该资源对象，如果存在则更新。

### delete

```
# calicoctl delete --help
Set the Calico datastore access information in the environment variables or
or supply details in a config file.

Usage:
  calicoctl delete ([--scope=<SCOPE>] [--node=<NODE>] [--orchestrator=<ORCH>]
                    [--workload=<WORKLOAD>] (<KIND> [<NAME>]) |
                   --filename=<FILE>)
                   [--skip-not-exists] [--config=<CONFIG>]

Examples:
  # Delete a policy using the type and name specified in policy.yaml.
  calicoctl delete -f ./policy.yaml

  # Delete a policy based on the type and name in the YAML passed into stdin.
  cat policy.yaml | calicoctl delete -f -

  # Delete policy with name "foo"
  calicoctl delete policy foo

Options:
  -h --help                 Show this screen.
  -s --skip-not-exists      Skip over and treat as successful, resources that
                            don't exist.
  -f --filename=<FILENAME>  Filename to use to delete the resource.  If set to
                            "-" loads from stdin.
  -n --node=<NODE>          The node (this may be the hostname of the compute
                            server if your installation does not explicitly set
                            the names of each Calico node).
     --orchestrator=<ORCH>  The orchestrator (valid for workload endpoints).
     --workload=<WORKLOAD>  The workload (valid for workload endpoints).
     --scope=<SCOPE>        The scope of the resource type.  One of global,
                            node.  This is only valid for BGP peers and is used
                            to indicate whether the peer is a global peer or
                            node-specific.
  -c --config=<CONFIG>      Path to the file containing connection
                            configuration in YAML or JSON format.
                            [default: /etc/calico/calicoctl.cfg]
... ...
Valid resource types are:

  * node
  * bgpPeer
  * hostEndpoint
  * workloadEndpoint
  * ipPool
  * policy
  * profile
... ...
```

### `get`

```
# List all policy in default output format.
 calicoctl get policy

# List a specific policy in YAML format
calicoctl get -o yaml policy my-policy-1
```

```
-o --output=<OUTPUT FORMAT>  Output format.  One of: yaml, json, ps, wide,
                             custom-columns=..., go-template=...,
                             go-template-file=...   [Default: ps]
```

默认 `get` 命令输出格式为 `ps`

```
$ calicoctl get hostEndpoint
HOSTNAME   NAME        
host1      endpoint1   
myhost     eth0
```

`wide` 格式输出会更详细，会输出资源的一些附加列

```
$ calicoctl get hostEndpoint --output=wide
HOSTNAME   NAME        INTERFACE   IPS                PROFILES      
host1      endpoint1               1.2.3.4,0:bb::aa   prof1,prof2   
myhost     eth0                                       profile1
```

`custom-columns` 可以自定义输出列

```
$ calicoctl get hostEndpoint --output=custom-columns=NAME,IPS
NAME        IPS                
endpoint1   1.2.3.4,0:bb::aa   
eth0
```

`yaml`/`json` 以 `yaml` 或者 `json` 格式输出

```
$ calicoctl get hostEndpoint --output=yaml
- apiVersion: v1
  kind: hostEndpoint
  metadata:
    hostname: host1
    labels:
      type: database
    name: endpoint1
  spec:
    expectedIPs:
    - 1.2.3.4
    - 0:bb::aa
... ...
```

如果节点没有运行 etcd，那么需要通过 `ETCD_ENDPOINTS` 指定 etcd 地址，否则将无法操作：

```
ETCD_ENDPOINTS=http://172.16.0.10:2379 calicoctl get bgppeers
```

### `config`

```
# calicoctl config --help
Set the Calico datastore access information in the environment variables or
or supply details in a config file.

Usage:
  calicoctl config set <NAME> <VALUE> [--node=<NODE>]
                                      [--raw=(bgp|felix)]
                                      [--config=<CONFIG>]
  calicoctl config unset <NAME> [--node=<NODE>]
                                [--raw=(bgp|felix)]
                                [--config=<CONFIG>]
  calicoctl config get <NAME> [--node=<NODE>]
                              [--raw=(bgp|felix)]
                              [--config=<CONFIG>]

Examples:
  # Turn off the full BGP node-to-node mesh
  calicoctl config set nodeToNodeMesh off

  # Set global log level to warning
  calicoctl config set logLevel warning

  # Set log level to info for node "node1"
  calicoctl config set logLevel info --node=node1

  # Display the current setting for the nodeToNodeMesh
  calicoctl config get nodeToNodeMesh

Options:
  -n --node=<NODE>      The node name.
     --raw=(bgp|felix)  Apply raw configuration for the specified component.
                        This option should be used with care; the data is not
                        validated and it is possible to configure or remove
                        data that may prevent the component from working as
                        expected.
  -c --config=<CONFIG>  Path to the file containing connection configuration in
                        YAML or JSON format.
                        [default: /etc/calico/calicoctl.cfg]

... ...

 Name            | Scope       | Value                                  |
-----------------+-------------+----------------------------------------+
 logLevel        | global,node | none,debug,info,warning,error,critical |
 nodeToNodeMesh  | global      | on,off                                 |
 asNumber        | global      | 0-4294967295                           |
 ipip            | global      | on,off                                 |
```

目前 `calicoctl config` 只有 `logLevel` 可以单独设置节点，其它如 `nodeToNodeMesh`、`asNumber`、`ipip` 配置的都是全局选项。默认安装之后 `nodeToNodeMesh` 为开启状态，如果需要和内部交换机打通，需要通过如下命令关闭该选项：

```
# calicoctl config get nodeToNodeMesh       // 获取当前 nodeToNodeMesh 值，显示为 on
on
# calicoctl config set nodeToNodeMesh off   // 关闭 nodeToNodeMesh
```

### `ipam`

```
Usage:
  calicoctl ipam <command> [<args>...]

    release      Release a Calico assigned IP address.
    show         Show details of a Calico assigned IP address.

Options:
  -h --help      Show this screen.

Description:
  IP Address Management specific commands for calicoctl.

  See 'calicoctl ipam <command> --help' to read about a specific subcommand.
```

目前 `calicoctl ipam` 的地址管理相对 `v2.0` 以下的版本，功能还是比较弱的，有 `release` 和 `show` 两个命令。

`calico ipam release` 用于从 Calico 清除未被正常回收的地址

```
$ calicoctl ipam release --ip=192.168.1.2
```

`calico ipam show` 用于获取指定 ip 地址使用情况

```
# IP is not assigned to an endpoint
$ calicoctl ipam show --ip=192.168.1.2
IP 192.168.1.2 is not currently assigned

# Basic Docker container has the assigned IP
# 表明该 IP 地址已绑定 Docker 容器
$ calicoctl ipam show --ip=192.168.1.1
No attributes defined for 192.168.1.1
```

### `node`

```
Usage:
  calicoctl node <command> [<args>...]

    status       View the current status of a Calico node.
                 // 获取 Calico 节点当前状态
    diags        Gather a diagnostics bundle for a Calico node.
                 // 收集节点诊断信息
    checksystem  Verify the compute host is able to run a Calico node instance.
                 // 验证系统环境是否可以运行 Calico 节点实例

Options:
  -h --help      Show this screen.

Description:
  Node specific commands for calicoctl.  These commands must be run directly on
  the compute host running the Calico node instance.

  See 'calicoctl node <command> --help' to read about a specific subcommand.
```

```
# calicoctl node --help
Set the Calico datastore access information in the environment variables or
or supply details in a config file.

Usage:
  calicoctl node <command> [<args>...]

    run          Run the Calico node container image.
                // 运行节点容器镜像
    status       View the current status of a Calico node.
                // 获取 Calico 节点当前状态
    diags        Gather a diagnostics bundle for a Calico node.
                // 收集节点诊断信息
    checksystem  Verify the compute host is able to run a Calico node instance.
                // 验证系统环境是否可以运行 Calico 节点实例

Options:
  -h --help      Show this screen.

Description:
  Node specific commands for calicoctl.  These commands must be run directly on
  the compute host running the Calico node instance.

  See 'calicoctl node <command> --help' to read about a specific subcommand.
```

获取 Calico 节点状态信息：

```
$ sudo calicoctl node status
Calico process is running.

IPv4 BGP status
+--------------+-------------------+-------+----------+-------------+
| PEER ADDRESS |     PEER TYPE     | STATE |  SINCE   |    INFO     |
+--------------+-------------------+-------+----------+-------------+
| 172.17.8.102 | node-to-node mesh | up    | 23:30:04 | Established |
+--------------+-------------------+-------+----------+-------------+

IPv6 BGP status
No IPv6 peers found.
```

`calicoctl node run` calico 节点启动参数选项：

```
Usage:
  calicoctl node run [--ip=<IP>] [--ip6=<IP6>] [--as=<AS_NUM>]
                     [--name=<NAME>]
                     [--ip-autodetection-method=<IP_AUTODETECTION_METHOD>]
                     [--ip6-autodetection-method=<IP6_AUTODETECTION_METHOD>]
                     [--log-dir=<LOG_DIR>]
                     [--node-image=<DOCKER_IMAGE_NAME>]
                     [--backend=(bird|gobgp|none)]
                     [--config=<CONFIG>]
                     [--no-default-ippools]
                     [--dryrun]
                     [--init-system]
                     [--disable-docker-networking]
                     [--docker-networking-ifprefix=<IFPREFIX>]
                     [--use-docker-networking-container-labels]

Options:
  -h --help                Show this screen.
     --name=<NAME>         The name of the Calico node.  If this is not
                           supplied it defaults to the host name.
                           // 指定 Calico 节点名，如果没有指定则默认主机名
     --as=<AS_NUM>         Set the AS number for this node.  If omitted, it
                           will use the value configured on the node resource.
                           If there is no configured value and --as option is
                           omitted, the node will inherit the global AS number
                           (see 'calicoctl config' for details).
                           // 设置当前节点的 AS number，如果未指定，默认使用全局 As number
     --ip=<IP>             Set the local IPv4 routing address for this node.
                           If omitted, it will use the value configured on the
                           node resource.  If there is no configured value
                           and the --ip option is omitted, the node will
                           attempt to autodetect an IP address to use.  Use a
                           value of 'autodetect' to always force autodetection
                           of the IP each time the node starts.
                           // 设置当前节点本地 IPv4 路由地址，如果未指定，
                           // 则使用节点资源配置的值，如果也未配置，则自动探测使用地址
     --ip6=<IP6>           Set the local IPv6 routing address for this node.
                           If omitted, it will use the value configured on the
                           node resource.  If there is no configured value
                           and the --ip6 option is omitted, the node will not
                           route IPv6.
                           // 设置当前节点本地 IPv6 路由地址，如果未指定，
                           // 则使用节点资源配置的值，如果也未配置，则不会路由 IPv6
    ... ...
     --log-dir=<LOG_DIR>   The directory containing Calico logs.
                           [default: /var/log/calico]
                           // 指定 Calico 日志存储目录，默认为 /var/log/calico
     --node-image=<DOCKER_IMAGE_NAME>
                           Docker image to use for Calico's per-node container.
                           [default: calico/node:%s]
                           // 指定节点镜像
     --backend=(bird|gobgp|none)
                           Specify which networking backend to use.  When set
                           to "none", Calico node runs in policy only mode.
                           The option to run with gobgp is currently
                           experimental.
                           [default: bird]
                           // 指定网络存储类型，gobgp 当前处于实验性阶段，默认使用 bird
     --dryrun              Output the appropriate command, without starting the
                           container.
                           // 只输出执行命令信息，而不启动容器
     --init-system         Run the appropriate command to use with an init
                           system.
                           // 使用 init system 运行命令
     --no-default-ippools  Do not create default pools upon startup.
                           Default IP pools will be created if this is not set
                           and there are no pre-existing Calico IP pools.
                           // 启动不创建默认的 IP 池
     --disable-docker-networking
                           Disable Docker networking.
                           // 停用容器网络
     --docker-networking-ifprefix=<IFPREFIX>
                           Interface prefix to use for the network interface
                           within the Docker containers that have been networked
                           by the Calico driver.
                           [default: cali]
                           // docker 容器接口前缀，默认 cali
    ... ...
  -c --config=<CONFIG>     Path to the file containing connection
                           configuration in YAML or JSON format.
                           [default: /etc/calico/calicoctl.cfg]
                           // 配置文件路径，默认 /etc/calico/calicoctl.cfg
```

> 注：经测试，此处 `--name` 选项必须为主机名，否则和 bgppeer 的 `node` 字段匹配不上，bgppeer 的 `node` 字段必须为主机名，后续还需进一步测试。

### 资源类型

资源结构概览：

```
apiVersion: v1                      // API 版本号
kind: <type of resource>            // 资源类型
metadata:                           // 元数据
  # Identifying information
  name: <name of resource>
  ...
spec:                               // 资源配置信息
  # Specification of the resource
  ...
```

#### bgpPeer

* [Calico’s documentation on L3 Topologies](http://docs.projectcalico.org/v2.6/reference/private-cloud/l3-interconnect-fabric)

配置 Calico 集群节点，支持如下别名：`bgppeer`、`bgppeers`、`bgpp`、`bgpps`、`bp`、`bps`

```
apiVersion: v1
kind: bgpPeer
metadata:
  scope: node           // 范围：global/node
  node: rack1-host1     // 节点对应的主机名，如果是 scope 为 global，则此行必须省略
  peerIP: 192.168.1.1   // 当前 peer ip 地址
spec:
  asNumber: 63400       // 当前 peer As Number
```

> v3.0 `apiVersion` 已变更为 `projectcalico.org/v3`

| Field  | Description                                                                   | Accepted Values                                        | Schema |
| ------ | ----------------------------------------------------------------------------- | ------------------------------------------------------ | ------ |
| scope  | Determines the Calico nodes to which this peer applies.                       | global, node                                           | string |
| node   | Must be specified if scope is node, and must be omitted when scope is global. | The `hostname` of the node to which this peer applies. | string |
| peerIP | The IP address of this peer.                                                  | Valid IPv4 or IPv6 address.                            | string |

> 此处 node 官档标明为主机名，另外实际测试中如果此处指定 calico node 启动时指定的节点名「非主机名」时，跨容器路由会有问题，所以此处必须标注为主机名。

关于 BGP 的一些术语：

> 在 BGP 网络中，所有参与 BGP 进程的路由器都称为 BGP-speaking 路由器（BGP-speaking 可以看成是 BGP 会话的意思）
>
> 对于活动的 BGP-speaking设备，称为 peer设备，它与其他 BGP-speaking 设备之间有一个活动的 TCP 连接。BGP speaker是指本地 BGP 路由器，而 peer（对等，或者对端）是指任何其他 BGP-speaking 网络设备
>
> 当 BGP peer 路由器位于不同 AS 中时，它们之间互称对方为外部 peer，当它们位于同一个 AS 中时，则称为内部 peer
>
> 当在 peer 设备间（也就是相互直接连接的 BGP 路由器之间）建立了 TCP 连接，每个 BGP peer 就会立即与对端交换所有的路由表，也就是完整的 BGP 路由表

#### Host Endpoint Resource (hostEndpoint)

`Host Endpoint` 资源表示运行 Calico 主机关联接口，每个主机的 endpoint 包括针对该接口设置 labels 和 profiles，用以应用相关策略。

```
apiVersion: v1
kind: hostEndpoint
metadata:
  name: eth0
  node: myhost
  labels:
    type: production
spec:
  interfaceName: eth0
  expectedIPs:
  - 192.168.0.1
  - 192.168.0.2
  profiles:
  - profile1
  - profile2
```

#### IP Pool Resource(ipPool)

定义 Calico IP 地址资源池，除了 `ipPool` 还有以下别名：`ippool`、`ippools`、`ipp`、`ipps`、`pool`、`pools`

```
apiVersion: v1
kind: ipPool
metadata:
  cidr: 10.1.0.0/16
spec:
  ipip:
    enabled: false
  nat-outgoing: true
  disabled: false       # 标注为 true 表示不启用该地址池
```

`ipip`：ipip tunneling configuration for this pool. If not specified, ipip tunneling is disabled for this pool. 在公有云平台跨主机通信需要添加这一选项

`nat-outgoing`：When enabled, packets sent from calico networked containers in this pool to destinations outside of this pool will be masqueraded。简单说，使得容器可以访问外网

> 如果直接和内网交换机打通，则去除 `nat-outgoing` 选项，否则容器访问外部网络还是以 nat 方式出去的。
>
> 如果 ip 池启用了 `ipip`，建议同时也开启 `nat-outgoing`。否则当工作负载和运行 Calico 的主机之间没有 `nat-outgoing` 路由时启用 `ipip` 是不对称的，并且可能导致流量由于 RPF 检查失败而被过滤。

#### Node Resource (node)

定义节点资源，除了 `node`，还可以使用 `nodes`、`no`、`nos`

默认启动 Calico node 实例，会自动创建一个使用主机名的节点资源。

```
apiVersion: v1
kind: node
metadata:
  name: node-hostname
spec:
  bgp:
    asNumber: 64512
    ipv4Address: 10.244.0.1
    ipv6Address: 2001:db8:85a3::8a2e:370:7334
```

获取节点信息：

```
# calicoctl get node  -o wide
NAME                   ASN     IPV4            IPV6
host1                  64511   192.168.1.1
... ...
```

#### Policy Resource(policy) 和 Profile Resource(profile)

关于规则的设置，当前还没有实际使用，具体可参考官网内容 [Policy Resource (policy)](http://docs.projectcalico.org/v2.6/reference/calicoctl/resources/policy) 和 [Profile Resource(profile)](http://docs.projectcalico.org/v2.6/reference/calicoctl/resources/profile)。


# Kubelet CNI 源码解析

* `cmd/kubelet/kubelet.go`
* `pkg/kubelet/kubelet.go`

> **注：** 基于 [Kubernetes release-1.9](https://github.com/kubernetes/kubernetes/tree/release-1.9)

## 网络

Kubernetes 容器使用的网络规范为 `CNI`（容器网络接口），`CNI` 包括方法规范和参数规范。Kubernetes 并不实际去操作容器的网络，而是通过遵循 CNI 规范的各种网络插件去管理容器网络资源，如 `Calico`、`Flannel`、`Contiv netplugin` 网络插件等。

* [Container Network Interface Specification](https://github.com/containernetworking/cni/blob/master/SPEC.md)

### CNI 接口

* `github.com/containernetworking/cni/libcni/api.go`

CNI 接口只需要实现以下方法，实际就是两种，一个添加网络调用，一个删除调用：

```
type CNI interface {
    AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
    DelNetworkList(net *NetworkConfigList, rt *RuntimeConf) error

    AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
    DelNetwork(net *NetworkConfig, rt *RuntimeConf) error
}
```

### 网络初始化

Kubelet 启动过程中针对网络主要做以下步骤，分别是探针获取当前环境的网络插件以及初始化网络。

#### 步骤 1：探针获取当前环境的网络插件

* `cmd/kubelet/app/server.go`

```
func UnsecuredDependencies(s *options.KubeletServer) (*kubelet.Dependencies, error) {
... ...
                // 执行具体函数，获取当前环境的网络插件
                NetworkPlugins:      ProbeNetworkPlugins(s.CNIConfDir, s.CNIBinDir),
... ...
}
```

* `cmd/kubelet/app/plugins.go`

```
// ProbeNetworkPlugins collects all compiled-in plugins
func ProbeNetworkPlugins(cniConfDir, cniBinDir string) []network.NetworkPlugin {
    allPlugins := []network.NetworkPlugin{}

    // for each existing plugin, add to the list
    allPlugins = append(allPlugins, cni.ProbeNetworkPlugins(cniConfDir, cniBinDir)...)
    allPlugins = append(allPlugins, kubenet.NewPlugin(cniBinDir))

    return allPlugins
}
```

* `pkg/kubelet/network/plugins.go`

以下是 kubelet `NetworkPlugin` 接口，`pkg/kubelet/network/cni/cni.go` 中 `cniNetworkPlugin` 实现了这套接口：

```
// Plugin is an interface to network plugins for the kubelet
type NetworkPlugin interface {
    // Init initializes the plugin.  This will be called exactly once
    // before any other methods are called.
    Init(host Host, hairpinMode kubeletconfig.HairpinMode, nonMasqueradeCIDR string, mtu int) error

    // Called on various events like:
    // NET_PLUGIN_EVENT_POD_CIDR_CHANGE
    Event(name string, details map[string]interface{})

    // Name returns the plugin's name. This will be used when searching
    // for a plugin by name, e.g.
    Name() string

    // Returns a set of NET_PLUGIN_CAPABILITY_*
    Capabilities() utilsets.Int

    // SetUpPod is the method called after the infra container of
    // the pod has been created but before the other containers of the
    // pod are launched.
    SetUpPod(namespace string, name string, podSandboxID kubecontainer.ContainerID, annotations map[string]string) error

    // TearDownPod is the method called before a pod's infra container will be deleted
    TearDownPod(namespace string, name string, podSandboxID kubecontainer.ContainerID) error

    // GetPodNetworkStatus is the method called to obtain the ipv4 or ipv6 addresses of the container
    GetPodNetworkStatus(namespace string, name string, podSandboxID kubecontainer.ContainerID) (*PodNetworkStatus, error)

    // Status returns error if the network plugin is in error state
    Status() error
}
```

* `pkg/kubelet/network/cni/cni.go`

```
func probeNetworkPluginsWithVendorCNIDirPrefix(pluginDir, binDir, vendorCNIDirPrefix string) []network.NetworkPlugin {
    if binDir == "" {
        // DefaultCNIDir 默认值为 `/opt/cni/bin`
        binDir = DefaultCNIDir
    }
    plugin := &cniNetworkPlugin{
        defaultNetwork:     nil,
        // 默认会设置 loNetwork 用于添加 lo 设备，所以在 binDir 下，即 CNI 插件目录下必须需要 `loopback` 插件
        loNetwork:          getLoNetwork(binDir, vendorCNIDirPrefix),
        execer:             utilexec.New(),
        pluginDir:          pluginDir,
        binDir:             binDir,
        vendorCNIDirPrefix: vendorCNIDirPrefix,
    }

    // sync NetworkConfig in best effort during probing.
    // 探测网络，并同步网络配置，此处没有针对 err 处理，syncNetworkConfig 函数执行错误只会记录相关日志
    plugin.syncNetworkConfig()
    // 虽然是个列表，但运行时只会支持一种插件
    return []network.NetworkPlugin{plugin}
}

func ProbeNetworkPlugins(pluginDir, binDir string) []network.NetworkPlugin {
    return probeNetworkPluginsWithVendorCNIDirPrefix(pluginDir, binDir, "")
}

... ...

// 探测网络，并设置插件默认网络
func (plugin *cniNetworkPlugin) syncNetworkConfig() {
    network, err := getDefaultCNINetwork(plugin.pluginDir, plugin.binDir, plugin.vendorCNIDirPrefix)
    if err != nil {
        glog.Warningf("Unable to update cni config: %s", err)
        return
    }
    plugin.setDefaultNetwork(network)
}
```

```
func getDefaultCNINetwork(pluginDir, binDir, vendorCNIDirPrefix string) (*cniNetwork, error) {
    // 默认 pluginDir `/etc/cni/net.d`
    if pluginDir == "" {
        pluginDir = DefaultNetDir
    }
    files, err := libcni.ConfFiles(pluginDir, []string{".conf", ".conflist", ".json"})
    switch {
    case err != nil:
        return nil, err
    case len(files) == 0:
        return nil, fmt.Errorf("No networks found in %s", pluginDir)
    }

    sort.Strings(files)
    // 遍历所有的配置文件，只要匹配文件满足条件就返回，因此多个配置设置是无效的
    for _, confFile := range files {
        var confList *libcni.NetworkConfigList
        if strings.HasSuffix(confFile, ".conflist") {
            confList, err = libcni.ConfListFromFile(confFile)
            if err != nil {
                glog.Warningf("Error loading CNI config list file %s: %v", confFile, err)
                continue
            }
        } else {
            conf, err := libcni.ConfFromFile(confFile)
            if err != nil {
                glog.Warningf("Error loading CNI config file %s: %v", confFile, err)
                continue
            }
            // Ensure the config has a "type" so we know what plugin to run.
            // Also catches the case where somebody put a conflist into a conf file.
            if conf.Network.Type == "" {
                glog.Warningf("Error loading CNI config file %s: no 'type'; perhaps this is a .conflist?", confFile)
                continue
            }

            confList, err = libcni.ConfListFromConf(conf)
            if err != nil {
                glog.Warningf("Error converting CNI config file %s to list: %v", confFile, err)
                continue
            }
        }
        if len(confList.Plugins) == 0 {
            glog.Warningf("CNI config list %s has no networks, skipping", confFile)
            continue
        }
        confType := confList.Plugins[0].Network.Type

        // Search for vendor-specific plugins as well as default plugins in the CNI codebase.
        vendorDir := vendorCNIDir(vendorCNIDirPrefix, confType)
        cninet := &libcni.CNIConfig{
            Path: []string{vendorDir, binDir},
        }
        network := &cniNetwork{name: confList.Name, NetworkConfig: confList, CNIConfig: cninet}
        return network, nil
    }
    return nil, fmt.Errorf("No valid networks found in %s", pluginDir)
}
```

#### 步骤 2：初始化网络插件

* `pkg/kubelet/kubelet.go`

```
plug, err := network.InitNetworkPlugin(kubeDeps.NetworkPlugins, crOptions.NetworkPluginName, &criNetworkHost{&networkHost{klet}, &network.NoopPortMappingGetter{}}, hairpinMode, nonMasqueradeCIDR, int(crOptions.NetworkPluginMTU))
if err != nil {
        return nil, err
}
klet.networkPlugin = plug
```

* `pkg/kubelet/network/plugins.go`

```
// InitNetworkPlugin inits the plugin that matches networkPluginName. Plugins must have unique names.
func InitNetworkPlugin(plugins []NetworkPlugin, networkPluginName string, host Host, hairpinMode kubeletconfig.HairpinMode, nonMasqueradeCIDR string, mtu int) (NetworkPlugin, error) {
        // 如果未指定网络插件 `--network-plugin`，默认为 `noop` 插件，使用 CNI 网络，指定该插件为 `cni`，
        // 关于 `noop` 具体详见官方说明 https://kubernetes.io/docs/concepts/cluster-administration/network-plugins/
        if networkPluginName == "" {
                // default to the no_op plugin
                plug := &NoopNetworkPlugin{}
                plug.Sysctl = utilsysctl.New()
                // `noop` 网络初始化
                if err := plug.Init(host, hairpinMode, nonMasqueradeCIDR, mtu); err != nil {
                        return nil, err
                }
                return plug, nil
        }

        pluginMap := map[string]NetworkPlugin{}

        allErrs := []error{}
        for _, plugin := range plugins {
                name := plugin.Name()
                if errs := validation.IsQualifiedName(name); len(errs) != 0 {
                        allErrs = append(allErrs, fmt.Errorf("network plugin has invalid name: %q: %s", name, strings.Join(errs, ";")))
                        continue
                }

                if _, found := pluginMap[name]; found {
                        allErrs = append(allErrs, fmt.Errorf("network plugin %q was registered more than once", name))
                        continue
                }
                pluginMap[name] = plugin
        }

        // 确认是否和与指定的网络插件匹配，如果匹配则进行相关初始化
        chosenPlugin := pluginMap[networkPluginName]
        if chosenPlugin != nil {
                err := chosenPlugin.Init(host, hairpinMode, nonMasqueradeCIDR, mtu)
                if err != nil {
                        allErrs = append(allErrs, fmt.Errorf("Network plugin %q failed init: %v", networkPluginName, err))
                } else {
                        glog.V(1).Infof("Loaded network plugin %q", networkPluginName)
                }
        } else {
                allErrs = append(allErrs, fmt.Errorf("Network plugin %q not found.", networkPluginName))
        }

        return chosenPlugin, utilerrors.NewAggregate(allErrs)
}
```

{% hint style="info" %}
上文 hairpinMode 设置 haripin NAT 方式，使得服务后端 endpoints 访问服务自身时负载到本地，配置项为 `--hairpin-mode`，默认值 `promiscuous-bridge`
{% endhint %}

* `pkg/kubelet network/cni/cni.go`

```
func (plugin *cniNetworkPlugin) Init(host network.Host, hairpinMode kubeletconfig.HairpinMode, nonMasqueradeCIDR string, mtu int) error {
    // platformInit 用于确定主机是否有 `nsenter` 命令
    err := plugin.platformInit()
    if err != nil {
        return err
    }

    plugin.host = host

    plugin.syncNetworkConfig()
    return nil
}
```

### 网络操作

网络操作主要是 Pod 创建的网络添加以及删除的网络回收操作，上文中介绍了 `NetworkPlugin` 接口，其中包含了添加网络和删除网络的方法：

* `pkg/kubelet/network/plugins.go`

```
// Plugin is an interface to network plugins for the kubelet
type NetworkPlugin interface {
... ...

    // SetUpPod is the method called after the infra container of
    // the pod has been created but before the other containers of the
    // pod are launched.
    SetUpPod(namespace string, name string, podSandboxID kubecontainer.ContainerID, annotations map[string]string) error

    // TearDownPod is the method called before a pod's infra container will be deleted
    TearDownPod(namespace string, name string, podSandboxID kubecontainer.ContainerID) error
... ...
}
```

以下为 Kubelet 调用 CNI 网络的具体操作实现：

#### 添加网络

* `pkg/kubelet/network/cni/cni.go`

```
func (plugin *cniNetworkPlugin) SetUpPod(namespace string, name string, id kubecontainer.ContainerID, annotations map[string]string) error {
    if err := plugin.checkInitialized(); err != nil {
        return err
    }
    // 通过 GetNetNS() 获取指定容器 net 命名空间路径，格式为 `/proc/<pid>/net`
    // pkg/kubelet/dockershim/helpers_linux.go `getNetworkNamespace`
    netnsPath, err := plugin.host.GetNetNS(id.ID)
    if err != nil {
        return fmt.Errorf("CNI failed to retrieve network namespace path: %v", err)
    }

    // Windows doesn't have loNetwork. It comes only with Linux
    // 给容器生成 lo 网卡
    if plugin.loNetwork != nil {
        if _, err = plugin.addToNetwork(plugin.loNetwork, name, namespace, id, netnsPath); err != nil {
            glog.Errorf("Error while adding to cni lo network: %s", err)
            return err
        }
    }

    _, err = plugin.addToNetwork(plugin.getDefaultNetwork(), name, namespace, id, netnsPath)
    if err != nil {
        glog.Errorf("Error while adding to cni network: %s", err)
        return err
    }

    return err
}

... ...

func (plugin *cniNetworkPlugin) addToNetwork(network *cniNetwork, podName string, podNamespace string, podSandboxID kubecontainer.ContainerID, podNetnsPath string) (cnitypes.Result, error) {
    rt, err := plugin.buildCNIRuntimeConf(podName, podNamespace, podSandboxID, podNetnsPath)
    if err != nil {
        glog.Errorf("Error adding network when building cni runtime conf: %v", err)
        return nil, err
    }

    netConf, cniNet := network.NetworkConfig, network.CNIConfig
    glog.V(4).Infof("About to add CNI network %v (type=%v)", netConf.Name, netConf.Plugins[0].Network.Type)
    res, err := cniNet.AddNetworkList(netConf, rt)
    if err != nil {
        glog.Errorf("Error adding network: %v", err)
        return nil, err
    }

    return res, nil
}
```

* `github.com/containernetworking/cni/libcni/api.go`

```
// AddNetworkList executes a sequence of plugins with the ADD command
func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
    var prevResult types.Result
    for _, net := range list.Plugins {
        pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path)
        if err != nil {
            return nil, err
        }

        newConf, err := buildOneConfig(list, net, prevResult, rt)
        if err != nil {
            return nil, err
        }

        // 调用插件添加网络
        prevResult, err = invoke.ExecPluginWithResult(pluginPath, newConf.Bytes, c.args("ADD", rt))
        if err != nil {
            return nil, err
        }
    }

    return prevResult, nil
}
```

#### 删除网络

* `pkg/kubelet/network/cni/cni.go`

```
func (plugin *cniNetworkPlugin) TearDownPod(namespace string, name string, id kubecontainer.ContainerID) error {
    if err := plugin.checkInitialized(); err != nil {
        return err
    }

    // Lack of namespace should not be fatal on teardown
    netnsPath, err := plugin.host.GetNetNS(id.ID)
    if err != nil {
        glog.Warningf("CNI failed to retrieve network namespace path: %v", err)
    }

    return plugin.deleteFromNetwork(plugin.getDefaultNetwork(), name, namespace, id, netnsPath)
}
... ...

func (plugin *cniNetworkPlugin) deleteFromNetwork(network *cniNetwork, podName string, podNamespace string, podSandboxID kubecontainer.ContainerID, podNetnsPath string) error {
    rt, err := plugin.buildCNIRuntimeConf(podName, podNamespace, podSandboxID, podNetnsPath)
    if err != nil {
        glog.Errorf("Error deleting network when building cni runtime conf: %v", err)
        return err
    }

    netConf, cniNet := network.NetworkConfig, network.CNIConfig
    glog.V(4).Infof("About to del CNI network %v (type=%v)", netConf.Name, netConf.Plugins[0].Network.Type)
    err = cniNet.DelNetworkList(netConf, rt)
    if err != nil {
        glog.Errorf("Error deleting network: %v", err)
        return err
    }
    return nil
}
```

* `github.com/containernetworking/cni/libcni/api.go`

```
// DelNetworkList executes a sequence of plugins with the DEL command
func (c *CNIConfig) DelNetworkList(list *NetworkConfigList, rt *RuntimeConf) error {
    for i := len(list.Plugins) - 1; i >= 0; i-- {
        net := list.Plugins[i]

        pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path)
        if err != nil {
            return err
        }

        newConf, err := buildOneConfig(list, net, nil, rt)
        if err != nil {
            return err
        }

        // 调用插件删除网络
        if err := invoke.ExecPluginWithoutResult(pluginPath, newConf.Bytes, c.args("DEL", rt)); err != nil {
            return err
        }
    }

    return nil
}
```

### 参考

* [Kubernetes 网络](https://github.com/keontang/k8s-notes/blob/master/kubernetes-network.md)
* [Kubernetes网络插件CNI调研整理](https://yucs.github.io/2017/12/06/2017-12-6-CNI/)
* [kubernetes 容器网络接口(CNI)网络插件的设计与实现 ](http://dockone.io/article/2188)


# client-go


# client-go 背后机制

* 原文 [client-go under the hood](https://github.com/kubernetes/sample-controller/blob/master/docs/controller-client-go.md)

[client-go](https://github.com/kubernetes/client-go/) 库囊括了各种机制，你可以在开发自定义控制器的时候使用它们。这些机制定义在 [tools/cache](https://github.com/kubernetes/client-go/tree/master/tools/cache) 目录下。

下图展示了 client—go 库中各组件工作机制，以及和你编写的自定义控制器的交互点。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2Fsync%2F48134fa0a3115146f305121a124322fd3a4ff6bb.jpeg?generation=1591761645297086\&alt=media)

## client-go 组件

* Reflector：reflector 定义在 [type Reflector inside package cache](https://github.com/kubernetes/client-go/blob/master/tools/cache/reflector.go)，监视 Kubernetes API 中指定的资源类型（kind）。完成此功能的函数是 `ListAndWatch`。可以监视内置的资源，也可以监视自定义资源。当 reflector 通过 watch API 接收到新资源存在的通知时，它将使用相应的 listing API 获取新创建的对象，并将其存放到 `watchHandler` 函数中的 Delta FIFO 队列中。
* Informer：informer 定义在 [base controller inside package cache](https://github.com/kubernetes/client-go/blob/master/tools/cache/controller.go)，它会从 Delta FIFO 队列中弹出对象。完成此功能的函数是 `processLoop`。该基础控制器的任务是保存对象以备检索，并调用我们的控制器传递该对象。
* Indexer：indexer 提供了资源索引功能。它定义在 [type Indexer inside package cache](https://github.com/kubernetes/client-go/blob/master/tools/cache/controller.go)。一个典型的索引用例是基于对象的标签来创建索引。indexer 可以基于几个索引函数来维护索引。Indexer 使用了一个线程安全的数据存储来存放对象和它们的键。这里有一个名为 `MetaNamespaceKeyFunc` 的函数定义在 [type Store inside package cache](https://github.com/kubernetes/client-go/blob/master/tools/cache/store.go)，它会为对象生成一个 `<namespace>/<name>` 组合键。

## 自定义控制器组件

* Informer reference：这是对 Informer 实例的一个引用，该实例知道如何同你的自定义资源对象工作。你的自定义控制器代码需要创建合适的 Informer。
* Indexer reference：这是对 Indexer 实例的引用，该实例知道如何同自定义资源对象工作。你的自定义控制器代码需要创建这个。你会使用这个 reference 检索对象以备后用。

client-go 中的基础控制器提供了 `NewIndexerInformer` 函数来创建 Informer 和 Indexer。在你的代码中，你可以直接调用 [此函数](https://github.com/kubernetes/client-go/blob/master/examples/workqueue/main.go#L174)，或者使用 [工厂方法](https://github.com/kubernetes/sample-controller/blob/master/main.go#L61) 创建 informer。

* Resource Event Handlers：当需要传递一个对象给你的控制器时，Informer 会调用回调函数。典型的一个模式是编写这些函数获取调度对象的键并把键加入到工作队列以进一步处理。
* Work queue：这是你在控制器代码中创建的队列，用于将对象的交付和处理分离。编写 Resource event handler 函数是为了获取交付对象的键并将其添加到工作队列中。
* Process Item：这个函数是创建在你的代码中用来处理工作队列中的项目。这里可能有一个或多个其它函数来实际处理。这些函数通常使用 [Indexer reference](https://github.com/kubernetes/client-go/blob/master/examples/workqueue/main.go#L73)，或者 Listing wrapper 来检索键对应的对象。


# Helm

本系列文档是基于 Helm 官方文档翻译，[helm.sh/docs](https://helm.sh/docs/)，版本基于最新的 Helm3。

* [Helm 架构](/kubernetes/helm/helm-arch)
* [Helm 快速上手](/kubernetes/helm/helm-quickstart)
* [Helm 使用](/kubernetes/helm/helm-using)
* [Helm 命令](/kubernetes/helm/helm-command)

{% hint style="info" %}
Helm3 是一个命令行工具，如果有 HTTP 方式操作 Helm 的需求，可以考虑我结合 Helm Go SDK 封装的开源 HTTP Server [helm-wrapper](https://github.com/opskumu/helm-wrapper)。
{% endhint %}


# Helm 架构

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-M3pA_kxwcdeMNSUKKPy%2F-M3pAaLzpnBxrWjuy-Eb%2Fhelm3-arch.png?generation=1585735167386119\&alt=media)

> <https://developer.ibm.com/technologies/containers/blogs/kubernetes-helm-3/>

## HELM 的目的

Helm 是一个管理 Kubernetes 包 charts 的工具。Helm 可以做以下事情：

* 从 scratch 创建新的 charts
* 打包 charts 为归档（tgz）文件
* 与 chart 仓库进行交互
* 在已存在的 Kubernetes 集群中安装和卸载 charts
* 管理已安装 charts 的 release 生命周期

对于 Helm，这里有三个重要的概念：

* 1、chart 包含创建一个 Kubernetes 应用实例所必要的信息
* 2、config 包含可以合并到 chart 包创建可发布对象的配置信息
* 3、release 是 chart 的运行实例，包含指定的配置

## 组件

Helm 是一个由两个不同部分实现的可执行文件：

**The Helm Client** 是提供终端用户的命令行客户端。客户端负责以下功能：

* 本地 chart 开发
* 管理 repositories
* 管理 releases
* 对接 Helm library 仓库
  * 发送要安装的 charts
  * 请求升级或者卸载已存在的 releases

**The Helm Library** 提供了执行所有 Helm 操作的逻辑。它与 Kubernetes API 服务器交互并提供以下功能：

* 结合 chart 和配置以构建一个 release
* 在 Kubernetes 中安装 charts，并提供后续的 release 对象
* 通过与 Kubernetes 交互来升级或者卸载 charts

独立的 Helm library 封装了 Helm 逻辑，以便可以由不同的客户端使用。

## 实现

Helm client 和 library 通过 Go 语言编写。

library 使用 Kubernetes client library 连接 Kubernetes。当前，library 使用 RESET + JSON。它使用 Kubernetes 内部的 Secrets 来存储信息。它不需要自己的数据库

配置文件尽可能以 YAML 编写。


# Helm 快速上手

这个指南介绍如何快速上手使用 Helm。

## 前提条件

成功并正确安全的使用 Helm 需要具备如下几个条件：

* 1、一个 Kubernetes 集群
* 2、如果有，确定要应用于安装的安全配置
* 3、安装和配置 Helm

### 安装 Kubernetes 或者有一个可访问的集群

* 你必须有一个安装好的 Kubernetes。针对最新版本的 Helm，我们推荐最新稳定版的 Kubernetes，大多数情况下也是第二次新版本
* 你还需要一个本地 `kubectl` 的配置副本

> 注意：1.6 之前的 Kubernetes 版本对于基于角色的访问控制（RBAC）的支持是受限或者不支持的。

## 安装 Helm

下载 Helm 客户端的二进制版本。你可以通过类似 `homebrew`（macOS 下包管理工具） 的工具，或者[官方版本页](https://github.com/helm/helm/releases)查看。

更详细的信息，或者选项，参见 [安装指南](https://helm.sh/docs/intro/install/)。

> 本身 Helm 的客户端就是一个二进制，安装来说不存在任何难度，不同系统安装不同的二进制版本即可。

## 初始化一个 Helm chart repository

安装好 Helm 之后，你可以添加一个 chart repository。从官方 Helm 稳定 charts 是一个好的开始：

```
$ helm repo add stable https://kubernetes-charts.storage.googleapis.com/
```

当你安装之后，你可以列出你可以安装的 charts：

```
$ helm search repo stable
NAME                                    CHART VERSION   APP VERSION                     DESCRIPTION
stable/acs-engine-autoscaler            2.2.2           2.1.1                           DEPRECATED Scales worker nodes within agent pools
stable/aerospike                        0.2.8           v4.5.0.5                        A Helm chart for Aerospike in Kubernetes
stable/airflow                          4.1.0           1.10.4                          Airflow is a platform to programmatically autho...
stable/ambassador                       4.1.0           0.81.0                          A Helm chart for Datawire Ambassador
# ... and many more
```

## 安装示例 Chart

你可以运行 `helm install` 命令安装一个 chart。Helm 有几种方式发现和安装一个 chart，但是最简单的是使用官方稳定的 charts。

```
$ helm repo update              # Make sure we get the latest list of charts
$ helm install stable/mysql --generate-name
Released smiling-penguin
```

在上面的例子中，`stable/mysql` chart 发布了，新版本的名称是 `smiling-penguin`。

通过运行 `helm show chart stable/mysql` 可以简单的了解 MySQL chart 的功能。或者运行 `helm show all stable/mysql` 获取该 chart 更多的信息。

当你安装一个 chart，一个新的版本就被创建了。一个 chart 可以在相同的集群安装多次。每一个都是可以被独立管理和更新的。

`helm install` 是具备很多功能的强大命令。获取更多的帮助可以查看 [Using Helm Guide](https://helm.sh/docs/intro/using_helm/)。

## 了解有关发布（RELEASES）的信息

通过 Helm 很容易看到发布了什么：

```
$ helm ls
NAME             VERSION   UPDATED                   STATUS    CHART
smiling-penguin  1         Wed Sep 28 12:59:46 2016  DEPLOYED  mysql-0.1.0
```

`helm list` 函数展示所有部署的发布列表。

## 卸载一个 RELEASE

使用 `helm uninstall` 卸载一个 release：

```
$ helm uninstall smiling-penguin
Removed smiling-penguin
```

这将从 Kubernetes 中卸载 `smiling-penguin`，这将删除所有与这个发布相关的资源和历史记录。

如果 `--keep-history` 选项开启，release 历史将会保存。你可以获取这个 release 相关的信息：

```
$ helm status smiling-penguin
Status: UNINSTALLED
...
```

因为 Helm 可以跟踪你的发布，即使在删除之后，你可以审计集群的历史，甚至取消删除 release（通过 `helm rollback`）。

## 查看帮助文档

使用 `helm help` 或者相关命令和 `-h` 选项组合可以获取更多可用的 Helm 命令：

```
$ helm get -h
```


# Helm 使用

这份指南介绍在 Kubernetes 集群中使用 Helm 管理包的基础知识。假设你已经安装了 Helm client。

如果你仅对运行一些快捷命令感兴趣，那么可以从 [Quickstart Guide](https://helm.sh/docs/intro/quickstart/) 开始入手。这个章节覆盖了 Helm 命令的细节，并解释如何使用 Helm。

## 三大概念

`Chart` 是 Helm 的包。它包含了在 Kubernetes 集群中运行一个应用、工具或者服务的所有资源的必要定义。好比 Homebrew 的 formula，Apt 的 apkg ，或者 Yum 的 RPM 文件。

`Repository` 是存放收集和共享 charts 的地方。好比 Perl 的 [CPAN archive](https://www.cpan.org/) 或者 Fedora 的 [Package Database](https://admin.fedoraproject.org/pkgdb/)，只是它是针对 Kubernetes 的包。

`Release` 是 chart 运行在 Kubernetes 集群的对应实例。一个 chart 可以在一个相同的集群俺逐行多次。每次安装都创建一个新的 release。以 MySQL chart 为例，如果你想在集群中运行两个数据库，你可以安装这个 chart 两次。每次安装对应一个 release，每个 release 都有对应的名称。

伴随这几个概念，我们现在可以这样解释 Helm：

> Helm 安装 charts 到 Kubernetes 中，每次安装创建一个新的 release。如果要找新的 charts，你可以通过搜索 Helm chart repositories。

## 'HELM SEARCH': 搜索 CHARTS

Helm 拥有强大的搜索命名，它可以搜索两种不同类型的源：

* `helm search hub` 搜索 [the Helm Hub](https://hub.helm.sh/)，其中包括来自数十个不同 helm charts repositories
* `helm search repo` 搜索本地 helm client 添加过的 repositories。该搜索是通过本地数据库完成的，不需要访问公网连接。

你可以通过运行 `helm search hub` 发现公共可用的 charts：

```
$ helm search hub wordpress
URL                                                   CHART VERSION    APP VERSION    DESCRIPTION
https://hub.helm.sh/charts/bitnami/wordpress          7.6.7            5.2.4          Web publishing platform for building blogs and ...
https://hub.helm.sh/charts/presslabs/wordpress-...    v0.6.3           v0.6.3         Presslabs WordPress Operator Helm Chart
https://hub.helm.sh/charts/presslabs/wordpress-...    v0.7.1           v0.7.1         A Helm chart for deploying a WordPress site on ...
```

以上列出了在 Helm Hub 上所有 `wordpress` charts。

在没有过滤的情况下，`helm search hub` 会展示所有可用的 charts。

使用 `helm search repo`，你可以在已经添加过的 repositories 中找到 charts 的名称：

```
$ helm repo add brigade https://brigadecore.github.io/charts
"brigade" has been added to your repositories
$ helm search repo brigade
NAME                            CHART VERSION    APP VERSION    DESCRIPTION
brigade/brigade                 1.3.2            v1.2.1         Brigade provides event-driven scripting of Kube...
brigade/brigade-github-app      0.4.1            v0.2.1         The Brigade GitHub App, an advanced gateway for...
brigade/brigade-github-oauth    0.2.0            v0.20.0        The legacy OAuth GitHub Gateway for Brigade
brigade/brigade-k8s-gateway     0.1.0                           A Helm chart for Kubernetes
brigade/brigade-project         1.0.0            v1.0.0         Create a Brigade project
brigade/kashti                  0.4.0            v0.4.0         A Helm chart for Kubernetes
```

Helm 搜索使用模糊字匹配算法，因此你可以输入单词或者短语的一部分：

```
$ helm search repo kash
NAME              CHART VERSION    APP VERSION    DESCRIPTION
brigade/kashti    0.4.0            v0.4.0         A Helm chart for Kubernetes
```

搜索是一种发现可用包的好方法，当你寻找到想要安装的包后，你可以使用 `helm install` 来安装它。

## 'HELM INSTALL'：安装一个包

使用 `helm install` 命令来安装一个新包。简单来说，它包含两个参数：你选择的 release 名称和你需要安装 chart 的名称。

```
$ helm install happy-panda stable/mariadb
```

当前 `mariadb` chart 已经安装了。注意安装一个 chart 创建了一个新的 `release` object。release 名称是 `happy-panda`。（如果你想让 Helm 生成随机名称，删除自定义名并添加 `--generate-name` 选项。）

在安装过程中，`helm` 客户端会打印出有用的信息，包括什么资源被创建了，release 的状态信息，以及一些需要你介入的附加配置项。

Helm 不会等所有的资源都运行后才推出。许多 charts 需要超过 600M 大小的镜像，并且需要很长时间才能安装到集群。

为了跟踪 release 的状态，或者重新读取配置信息，你可以使用 `helm status`:

```
$ helm status happy-panda
Last Deployed: Wed Sep 28 12:32:28 2016
Namespace: default
Status: DEPLOYED
...
```

以上展示了你的 release 当前的状态。

### 安装前自定义 Chart

刚刚安装的方式，只是使用 chart 的默认选项。很多时候，你需要自定义 chart 为你的首选配置。

可以通过 `helm show values` 查看一个 chart 的配置项：

```
$ helm show values stable/mariadb
```

你可以覆盖 YAML 格式文件中的任意配置，在安装的时候传递到文件中。

```
$ echo '{mariadbUser: user0, mariadbDatabase: user0db}' > config.yaml
$ helm install -f config.yaml stable/mariadb --generate-name
```

上面将会创建一个默认的 MariaDB 用户 `user0`，并把该用户赋权给新创建的 `user0db` 数据库，其他项都是用 chart 的默认值。

这里有两个方式在安装时传递配置数据：

* `--values` （或者 `-f`）：指定替换的 YAML 文件。可多次指定选项，最右边的文件优先
* `--set`：命令行上指定替代

如果同时使用，`--set` 值会以更高的优先级合并到 `--values` 中。通过 `--set` 覆盖值将保存在 ConfigMap 中。给定 release `--set` 的值可以通过 `helm get values <release-name>` 查看。`--set` 设置的值可以通过运行 `helm upgrade` 指定 `--reset-values` 来清理。

### `--set` 格式和限制

`--set` 选项采用零个或多个 name/value 对。最简单的用法是： `--set name=value`。相当于 YAML：

```
name: value
```

多个值通过 `,` 分隔，因此 `--set a=b,c=d` 等价于：

```
a: b
c: d
```

复杂的表达式也支持。如，`--set outer.inner=value` 被翻译成：

```
outer:
  inner: value
```

可以通过 `{` 和 `}` 来表示列表。如，`--set name={a, b, c}` 翻译成：

```
name:
  - a
  - b
  - c
```

从 Helm 2.5.0 开始，可以使用数组索引语法访问列表项。如，`--set servers[0].port=80`：

```
servers:
  - port: 80
```

`--set servers[0].port=80,servers[0].host=example` 设置多个值：

```
servers:
  - port: 80
    host: example
```

有时候你需要在 `--set` 上使用特殊的字符。你可以通过 `\` 转义，`--set name=value1\,value2`：

```
name: "value1,value2"
```

同样，你可以转义点序列，这会给使用 `toYaml` 函数解析 annotations，labels 和 node selectors 时带来便利。`--set nodeSelector."kubernetes\.io/role"=master`：

```
nodeSelector:
  kubernetes.io/role: master
```

使用 `--set` 很难表达深层嵌套的数据结构。鼓励 Chart 设计人员在设计 `values.yaml` 文件的时候考虑 `--set` 用法。

### 更多安装的方法

`helm install` 命令可以从多个源安装：

* chart repository（和上面提到的一样）
* 本地 chart 归档（`helm install foo foo-0.1.1.tgz`）
* 解包的 chart 目录（`helm install foopath/to/foo`）
* 完整的 URL（`helm install foo https://example.com/charts/foo-1.2.3.tgz`）

## 'HELM UPGRADE' 和 'HELM ROLLBACK': 升级和失败恢复 RELEASE

当 chart 的新版本发布了，或者当你想修改你的 release 的时候，你可以使用 `helm upgrade` 命令。

升级针对当前存在的 release 通过你提供的信息升级。因为 Kubernetes charts 可能很大并且复杂，因此 Helm 尝试执行侵入性最小的升级。它将只更新自上一个 release 以来已经变更的内容。

```
$ helm upgrade -f panda.yaml happy-panda stable/mariadb
Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/helm.sh/helm/mariadb-0.3.0.tgz
happy-panda has been upgraded. Happy Helming!
Last Deployed: Wed Sep 28 12:47:54 2016
Namespace: default
Status: DEPLOYED
...
```

上面的例子，`happy-panda` release 通过新的 YAML 文件，使用同一个 chart 升级：

```
mariadbUser: user1
```

我们可以使用 `helm get values` 查看新的设置是否生效。

```
$ helm get values happy-panda
mariadbUser: user1
```

`helm get` 命令是集群中获取 release 的非常有用的工具。从上面我们可以看到，它展示了已经部署到集群的 `panda.yaml` 新的值。

现在，如果当一个 release 没有按照计划的方式运行，通过 `helm rollback [RELEASE] [REVISION]` 可以很容易回滚到之前的版本。

```
$ helm rollback happy-panda 1
```

上面回滚我们的 happy-panda 到它的第一个 release 版本。release 版本是一个递增的修订。每一次安装，升级或者回滚发生时，修订号递增 1。第一个修订号总是 1。我们可以通过 `helm history [RELEASE]` 查看某个 release 修订号

## INSTALL/UPGRADE/ROLLBACK 帮助项

这里有几个其他有用的选项，以便当使用 Helm 执行 install/upgrade/rollback 时自定义操作。请注意这不是一个完整的客户端参数。查看所有参数的描述，运行 `helm <command> --help`。

* `--timeout`：指定等待 Kubernetes 命令完成时间，默认 5m0s
* `--wait`：等待直到所有的 Pods 处于 Ready 状态，PVCs bound，Deployments 达到最低限度（Desired - maxUnavailable）的 Pods 处于 Ready 状态以及 Service 有一个 IP 地址（并且 Ingress 如果需要 `LoadBalancer`）时，才标记 release 成功。等待的时间受 `--timeout` 值限制。如果超时了，则 release 会被标记为 `FAILED`。注意：在 Deployment `replicas` 设置为 1，滚动更新策略 `maxUnavailable` 没有被设置为 0 时，`--wait` 会返回 ready，因为它已经满足 ready 条件中的最小 Pod 数。
* `--no-hooks`：跳过执行钩子

## 'HELM UNINSTALL'：卸载一个 RELEASE

通过 `helm uninstall` 命令从集群卸载 release：

```
$ helm uninstall happy-panda
```

这将从集群中移除 release，你可以通过 `helm list` 命令列出当前所有已经部署的 releases：

```
$ helm list
NAME            VERSION UPDATED                         STATUS          CHART
inky-cat        1       Wed Sep 28 12:59:46 2016
```

从上面的输出，你可以看到 `happy-panda` release 已经卸载了。

在之前的 Helm 版本中，当一个 release 已经被删除了，它的删除记录会被保留。Helm3 中，删除 release 也会删除其记录。如果你想保留删除 release 的记录，使用 `helm uninstall --keep-history`。使用 `helm list --uninstalled` 只展示卸载是使用 `--keep-history` 参数的 releases。

`helm list --all` 参数会展示 Helm 保留的所有 release 记录，包括失败的或者删除项（如果 `--keep-history` 指定了）：

```
$  helm list --all
NAME            VERSION UPDATED                         STATUS          CHART
happy-panda     2       Wed Sep 28 12:47:54 2016        UNINSTALLED     mariadb-0.3.0
inky-cat        1       Wed Sep 28 12:59:46 2016        DEPLOYED        alpine-0.1.0
kindred-angelf  2       Tue Sep 27 16:16:10 2016
```

注意因为当前默认 releases 删除了，针对已卸载的资源是不能再回滚的。

## 'HELM REPO'

Helm3 不再附带默认的 chart 仓库了。`helm repo` 命令集提供添加，列表和移除仓库。

你可以通过 `helm repo list` 查看配置了哪些仓库：

```
$ helm repo list
NAME            URL
stable          https://kubernetes-charts.storage.googleapis.com
mumoshu         https://mumoshu.github.io/charts
```

新的仓库可以通过 `helm repo add` 添加：

```
$ helm repo add dev https://example.com/dev-charts
```

因为 chart 仓库变化比较频繁，可以通过 `helm repo update` 确保 Helm 客户端更新到最新版本。

仓库可以通过 `helm repo remove` 移除。

## 创建你自己的 CHARTS

[Chart Development Guide](https://helm.sh/docs/topics/charts/) 说明了如何开发你自己的 charts。但是你可以通过 `helm create` 命令快速入门：

```
$ helm create deis-workflow
Creating deis-workflow
```

现在在 `./deis-workflow` 有一个 chart。你可以编辑和创建属于你自己的模板。

在编辑 chart 时，可以通过运行 `helm lint` 验证格式是否正确。

当需要打包 chart 以进行分发时，可以运行 `helm package` 命令：

```
$ helm package deis-workflow
deis-workflow-0.1.0.tgz
```

然后现在就可以通过 `helm install` 轻松安装了：

```
$ helm install deis-workflow ./deis-workflow-0.1.0.tgz
...
```

打包的 Charts 可以加载到 chart 仓库。具体参见你的 chart 仓库服务器以学习怎么上传。

注意：`stable` 仓库管理在 [Kubernetes Charts GitHub repository](https://github.com/helm/charts)。这个项目接受 chart 源码，并（审核后）为你打包。

## 结束

这个章节覆盖了 `helm` 客户端基本的使用方式，包括搜索，安装，升级和卸载。它还覆盖了类似 `helm status`，`helm get` 以及 `helm repo` 这样的实用命令。

获取更多的信息，可以通过 `helm help` 获取 Helm 内建的帮助。


# Helm 命令

## Helm Completion

helm 命令补全，类似 `kubectl completion`

```
source <(helm completion bash)
```

> 建议把以上命令根据实际的 shell 加入到对应的配置文件中永久生效，如 bash 为 `~/.bashrc`，zsh 则为 `~/.zshrc`

## Helm Create

根据给定的名字创建一个新的 chart

```
# helm create test
Creating test
# tree -aF test
test
├── charts/                         // 可选，用于存放当前 Chart 依赖的其它 Chart 的说明文件
├── Chart.yaml                      // 用于描述 Chart 的元数据信息
├── .helmignore                     // Helm charts 打包时要忽略的信息，类似 .gitignore 和 .dockerignore
├── templates/                      // 可选，模板文件目录
│   ├── deployment.yaml
│   ├── _helpers.tpl
│   ├── ingress.yaml
│   ├── NOTES.txt
│   ├── serviceaccount.yaml
│   ├── service.yaml
│   └── tests/                      // 测试文件
│       └── test-connection.yaml
└── values.yaml                     // 模板默认值

3 directories, 10 files
```

## Helm Dependency

管理 chart 依赖

例如，这个 Chart.yaml 声明了两个依赖：

```
# Chart.yaml
dependencies:
- name: nginx
  version: "1.2.3"
  repository: "https://example.com/charts"
- name: memcached
  version: "3.2.1"
  repository: "https://another.example.com/charts"
```

也可以通过 `file://` 方式指定本地地址

```
# Chart.yaml
dependencies:
- name: nginx
  version: "1.2.3"
  repository: "file://../dependency_chart/nginx"
```

### Helm Dependency build

基于 Chart.lock 文件重新构建 charts/ 目录，选择一个 chart 目录执行命令：

```
# helm dependency build
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "incubator" chart repository
...Successfully got an update from the "stable" chart repository
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 1 charts
Downloading nginx from repo https://charts.bitnami.com/bitnami
Deleting outdated charts
```

> 如果没有 Chart.lock 文件，该命令会同 `helm dependency update` 一样创建此文件

```
# cat  Chart.lock
dependencies:
- name: nginx
  repository: https://charts.bitnami.com/bitnami
  version: 5.1.7
digest: sha256:3c3b4389ddb5d3ff6ef489d49713a369ec9d8474d04a8591f4be9ee78a122bc9
generated: "2020-03-05T15:30:02.76679778+08:00"
```

> 如果 Chart.yaml 变更了，Chart.lock 文件没有更新，则 `helm dependency build` 命令会执行失败，需要先执行 update 操作

```
# helm dependency build
Error: the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml). Please update the dependencies
```

### Helm Dependency list

列出给定 chart 的依赖信息：

```
# helm dependency list
NAME    VERSION REPOSITORY                              STATUS
nginx   5.1.7   https://charts.bitnami.com/bitnami      ok
# rm -f charts/nginx-5.1.7.tgz
# helm dependency list
NAME    VERSION REPOSITORY                              STATUS
nginx   5.1.7   https://charts.bitnami.com/bitnami      missing
```

### Helm Dependency update

基于 Chart.yaml 内容更新 charts/

```
# helm dependency update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "incubator" chart repository
...Successfully got an update from the "stable" chart repository
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 2 charts
Downloading nginx from repo https://charts.bitnami.com/bitnami
Downloading mysql from repo https://charts.bitnami.com/bitnami
Deleting outdated charts
# cat Chart.lock                // update 命令会同步更新 Chart.lock
dependencies:
- name: nginx
  repository: https://charts.bitnami.com/bitnami
  version: 5.1.7
- name: mysql
  repository: https://charts.bitnami.com/bitnami
  version: 6.9.2
digest: sha256:8a9ccbc57ff8e49cd5d788b736a0daeba182afe714721d1e5d17f13384935a6a
generated: "2020-03-05T15:44:24.017928459+08:00"
```

## Helm Env

打印出 Helm 所有在使用的环境变量

```
# helm env
HELM_BIN="helm"
HELM_DEBUG="false"
HELM_KUBECONTEXT=""
HELM_NAMESPACE="default"
HELM_PLUGINS="/root/.local/share/helm/plugins"
HELM_REGISTRY_CONFIG="/root/.config/helm/registry.json"
HELM_REPOSITORY_CACHE="/root/.cache/helm/repository"
HELM_REPOSITORY_CONFIG="/root/.config/helm/repositories.yaml"
```

## Helm Get

获取 release 扩展信息

```
# helm get -h

This command consists of multiple subcommands which can be used to
get extended information about the release, including:

- The values used to generate the release
- The generated manifest file
- The notes provided by the chart of the release
- The hooks associated with the release

Usage:
  helm get [command]

Available Commands:
  all         download all information for a named release  // 所有的信息
  hooks       download all hooks for a named release        // hooks 相关
  manifest    download the manifest for a named release     // 主要是 K8s 资源信息，Deployment、ConfigMap 等等
  notes       download the notes for a named release        // 注解
  values      download the values file for a named release  // 变量内容
```

```
# helm get hooks helm-grafana
---
# Source: grafana/templates/tests/test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: helm-grafana-test
  labels:
    helm.sh/chart: grafana-5.0.4
    app.kubernetes.io/name: grafana
    app.kubernetes.io/instance: helm-grafana
    app.kubernetes.io/version: "6.6.2"
    app.kubernetes.io/managed-by: Helm
  annotations:
    "helm.sh/hook": test-success
  namespace: default
spec:
  serviceAccountName: helm-grafana-test
  containers:
    - name: helm-grafana-test
      image: "bats/bats:v1.1.0"
      command: ["/opt/bats/bin/bats", "-t", "/tests/run.sh"]
      volumeMounts:
        - mountPath: /tests
          name: tests
          readOnly: true
  volumes:
  - name: tests
    configMap:
      name: helm-grafana-test
  restartPolicy: Never
```

## Helm History

获取 release 历史

```
# helm history helm-grafana
REVISION        UPDATED                         STATUS          CHART           APP VERSION     DESCRIPTION
1               Tue Mar  3 15:12:14 2020        deployed        grafana-5.0.4   6.6.2           Install complete
```

## Helm Install

安装一个 chart，官方示例如下：

```
$ helm install -f myvalues.yaml myredis ./redis
$ helm install --set name=prod myredis ./redis
$ helm install --set-string long_int=1234567890 myredis ./redis
$ helm install -f myvalues.yaml -f override.yaml  myredis ./redis
$ helm install --set foo=bar --set foo=newbar  myredis ./redis
```

## Helm Lint

Helm chart lint 命令，验证 chart 格式是否正确。

```
# helm lint
==> Linting .
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed
```

## Helm List

releases 列表

默认只列出已经部署或者失败的 release，`--uninstalled` 和 `--all` 选项可以列出更多，还可以采用 `--uninstalled --failed` 组合模式。通过 `--filter` 还可以支持搜索正则。

```
$ helm list --filter 'ara[a-z]+'
NAME                UPDATED                     CHART
maudlin-arachnid    Mon May  9 16:07:08 2016    alpine-0.1.0
```

```
# helm list -h
...
Usage:
  helm list [flags]

Aliases:
  list, ls

Flags:
  -a, --all              show all releases without any filter applied
  -A, --all-namespaces   list releases across all namespaces
  -d, --date             sort by release date
      --deployed         show deployed releases. If no other is specified, this will be automatically enabled
      --failed           show failed releases
  -f, --filter string    a regular expression (Perl compatible). Any releases that match the expression will be included in the results
  -h, --help             help for list
  -m, --max int          maximum number of releases to fetch (default 256) // 设置 0 并不会显示所有的，会使用服务器的默认值，该值可能高于 256
      --offset int       next release name in the list, used to offset from start value
  -o, --output format    prints the output in the specified format. Allowed values: table, json, yaml (default table)
      --pending          show pending releases
  -r, --reverse          reverse the sort order
  -q, --short            output short (quiet) listing format
      --superseded       show superseded releases
      --uninstalled      show uninstalled releases (if 'helm uninstall --keep-history' was used)
      --uninstalling     show releases that are currently being uninstalled
...
```

## Helm Package

把一个 chart 目录归档

```
# helm package test/
Successfully packaged chart and saved it to: /root/shuihan/test-0.1.0.tgz
```

```
# helm package -h
...
Usage:
  helm package [CHART_PATH] [...] [flags]

Flags:
      --app-version string   set the appVersion on the chart to this version
  -u, --dependency-update    update dependencies from "Chart.yaml" to dir "charts/" before packaging
  -d, --destination string   location to write the chart. (default ".")
  -h, --help                 help for package
      --key string           name of the key to use when signing. Used if --sign is true
      --keyring string       location of a public keyring (default "/root/.gnupg/pubring.gpg")
      --sign                 use a PGP private key to sign this package
      --version string       set the version on the chart to this semver version
...
```

## Helm Plugin

安装、列表或者卸载 Helm plugins

```
# helm plugin -h

Manage client-side Helm plugins.

Usage:
  helm plugin [command]

Available Commands:
  install     install one or more Helm plugins
  list        list installed Helm plugins
  uninstall   uninstall one or more Helm plugins
  update      update one or more Helm plugins

Flags:
  -h, --help   help for plugin
...
```

## Helm Pull

从仓库下载一个 chart 并（可选）解包在本地目录下。

```
# helm pull bitnami/nginx
```

```
# helm pull -h
...
Usage:
  helm pull [chart URL | repo/chartname] [...] [flags]

Aliases:
  pull, fetch

Flags:
      --ca-file string       verify certificates of HTTPS-enabled servers using this CA bundle
      --cert-file string     identify HTTPS client using this SSL certificate file
  -d, --destination string   location to write the chart. If this and tardir are specified, tardir is appended to this (default ".")
      --devel                use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.
  -h, --help                 help for pull
      --key-file string      identify HTTPS client using this SSL key file
      --keyring string       location of public keys used for verification (default "/root/.gnupg/pubring.gpg")
      --password string      chart repository password where to locate the requested chart
      --prov                 fetch the provenance file, but don't perform verification
      --repo string          chart repository url where to locate the requested chart
      --untar                if set to true, will untar the chart after downloading it
      --untardir string      if untar is specified, this flag specifies the name of the directory into which the chart is expanded (default ".")
      --username string      chart repository username where to locate the requested chart
      --verify               verify the package before installing it
      --version string       specify the exact chart version to install. If this is not specified, the latest version is installed
...
```

## Helm Repo

添加、列表、移除、更新以及索引 chart 仓库

```
# helm repo -h

This command consists of multiple subcommands to interact with chart repositories.

It can be used to add, remove, list, and index chart repositories.

Usage:
  helm repo [command]

Available Commands:
  add         add a chart repository
  index       generate an index file given a directory containing packaged charts
  list        list chart repositories
  remove      remove a chart repository
  update      update information of available charts locally from chart repositories
...
```

```
# helm repo list
NAME            URL
incubator       http://storage.googleapis.com/kubernetes-charts-incubator
bitnami         https://charts.bitnami.com/bitnami
# helm repo remove bitnami
"bitnami" has been removed from your repositories
# helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories
# helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "stable" chart repository
...Successfully got an update from the "incubator" chart repository
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈ Happy Helming!⎈
```

`helm repo index` 用于给 chart 仓库目录生成 `index.yaml` 文件索引。

```
# helm repo index -h

Read the current directory and generate an index file based on the charts found.

This tool is used for creating an 'index.yaml' file for a chart repository. To
set an absolute URL to the charts, use '--url' flag.

To merge the generated index with an existing index file, use the '--merge'
flag. In this case, the charts found in the current directory will be merged
into the existing index, with local charts taking priority over existing charts.

Usage:
  helm repo index [DIR] [flags]

Flags:
  -h, --help           help for index
      --merge string   merge the generated index into the given index
      --url string     url of chart repository
```

## Helm Rollback

release 版本回滚

```
# helm rollback -h

This command rolls back a release to a previous revision.

The first argument of the rollback command is the name of a release, and the
second is a revision (version) number. If this argument is omitted, it will
roll back to the previous release.

To see revision numbers, run 'helm history RELEASE'.

Usage:
  helm rollback <RELEASE> [REVISION] [flags]

Flags:
      --cleanup-on-fail    allow deletion of new resources created in this rollback when rollback fails
      --dry-run            simulate a rollback
      --force              force resource update through delete/recreate if needed
  -h, --help               help for rollback
      --no-hooks           prevent hooks from running during rollback
      --recreate-pods      performs pods restart for the resource if applicable
      --timeout duration   time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s)
      --wait               if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout
```

## Helm Search

charts 搜索

```
# helm search

Search provides the ability to search for Helm charts in the various places
they can be stored including the Helm Hub and repositories you have added. Use
search subcommands to search different locations for charts.

Usage:
  helm search [command]

Available Commands:
  hub         search for charts in the Helm Hub or an instance of Monocular // https://hub.helm.sh/
  repo        search repositories for a keyword in charts
```

## Helm Show

展示 chart 信息

```
# helm show

This command consists of multiple subcommands to display information about a chart

Usage:
  helm show [command]

Aliases:
  show, inspect

Available Commands:
  all         shows all information of the chart
  chart       shows the chart's definition
  readme      shows the chart's README
  values      shows the chart's values
```

## Helm Status

显示 release 状态信息

```
# helm status helm-grafana
NAME: helm-grafana
LAST DEPLOYED: Tue Mar  3 15:12:14 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get your 'admin' user password by running:

   kubectl get secret --namespace default helm-grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo

2. The Grafana server can be accessed via port 80 on the following DNS name from within your cluster:

   helm-grafana.default.svc.cluster.local

   Get the Grafana URL to visit by running these commands in the same shell:

     export POD_NAME=$(kubectl get pods --namespace default -l "app=grafana,release=helm-grafana" -o jsonpath="{.items[0].metadata.name}")
     kubectl --namespace default port-forward $POD_NAME 3000

3. Login with the password from step 1 and the username: admin
#################################################################################
######   WARNING: Persistence is disabled!!! You will lose your data when   #####
######            the Grafana pod is terminated.                            #####
#################################################################################
```

## Helm Template

本地渲染模板

通过 helm 自定义选项并输出。

```
# helm template test bitnami/grafana --set-string image.tag=5.0.4       // 修改镜像 tag
```

## Helm Test

针对已部署的 release 运行测试，这些测试在已安装 chart 中定义好了。

## Helm Uninstall

卸载一个 release

```
# helm uninstall helm-grafana
release "helm-grafana" uninstalled
```

## Helm Upgrade

release 升级

```
$ helm upgrade -f myvalues.yaml -f override.yaml redis ./redis
$ helm upgrade --set foo=bar --set foo=newbar redis ./redis
```

## Helm Verify

验证指定的 chart 已签名并且有效

## Helm Version

查看 helm 版本信息

```
# helm version
version.BuildInfo{Version:"v3.1.1", GitCommit:"afe70585407b420d0097d07b21c47dc511525ac8", GitTreeState:"clean", GoVersion:"go1.13.8"}
```


# Google 大规模集群管理器 Borg

* 原文地址：[Large-scale cluster management at Google with Borg](https://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/43438.pdf)

## Abstract

Google Borg 系统是一个集群管理器，运行着数千个应用程序的数以十万计的作业，跨多个由数万台机器组成的集群。

Borg 通过超配、进程级别资源隔离等，实现高效的资源利用率。支持应用高可用，最大限度的减少故障时间，并且可以通过调度策略降低相关故障发生的可能性。Borg 还提供了声明性工作规范语言，名称服务集成，实时作业监控以及分析和模拟系统行为的工具，简化用户操作。

## 1. Introduction

集群管理系统的内部代号是 Borg，它全程管理、调度、启动、重启以及监控 Google 运行的应用程序。

Borg 提供三个好处：

* (1) 向用户隐藏资源管理和故障处理的细节，用户只需专注于应用程序开发
* (2) 高可靠性和高可用性的操作，同时支持应用程序相关特性
* (3) 有效的在数以万计的机器上运行工作负载

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm6exPvvH4ebBEZq%2Fborg-arch.png?generation=1567863290155913\&alt=media)

## 2. The user persoective

Borg 的面向用户为运行 Google 应用程序和服务的 Google 开发者和系统管理员（Google 内部称为网站高可用工程师或者简写 SRE）。用户向 Borg 以作业（`jobs`）的方式提交工作，每个 job 由包含着相同程序的一个或多个任务（`tasks`）组成。每个 job 运行在一个 Borg `cell` (一组机器集合管理单元) 上。

### 2.1 The workload

Borg cells 包括两种类型的 workload。第一种是那些长时间运行的服务，并且对请求延迟敏感（几微秒到几百毫秒之间）。这类服务一般是直接面向终端用户的产品，如 Gmail、Google Docs 和 Web 搜索以及内部基础设施服务（如 BigTable）。另外一种是那些运行几秒或者几天即可完成的批处理作业，这类服务对短期性能波动不敏感。

一个典型的 cell，一般分配 70% CPU 资源，实际使用为 60%，分配 55% 的内存资源，实际使用为 85%。

### 2.2 Clusters and cells

一个 cell 的机器都归属于单个集群，通过高性能的数据中心级别的光纤网络连接。一个集群部署在一个独立的数据中心建筑中，多个数据中心建筑构成一个 `site`。一个集群通常包括一个大规模的 cell 和许多小规模的测试或者特殊目的的 cells。尽量避免单点故障。

排除测试 cells，一个中等规模的 cell 一般由 10k 机器组成。一个 cell 中的机器规格是不同的，诸如配置（CPU、RAM、磁盘、网络），处理器型号，性能等方面。用户无需关心这些差异，Borg 确定在哪个 cell 上运行任务，分配资源，安装程序和依赖项，并监控应用运行状况以及在运行失败时重启。

### 2.3 Jobs and tasks

一个 Borg job 的属性包括名字、属主以及 tasks 数量。通过一些约束，可以强制 Job 的 tasks 在具有特定属性的机器上运行，例如处理器架构、操作系统版本，或者额外的 IP 地址。约束是分软限制和强限制。可以指定 job 运行顺序，如一个 job 在另外一个 job 运行之后再启动。一个 job 只能运行在一个 cell 上。

每个 task 映射成一组 Linux 进程运行在一台机器的一个容器中。大部分的 Borg workload 都不是运行在虚拟机中，不想在虚拟化上花费精力是一方面。另外，设计 Borg 的时候还没有出现硬件虚拟化。task 也有拥有属性，例如资源需求等。大多数的 task 属性同它们的 job 一样，不过也可以被覆盖。如提供 task 专用的命令行参数，以及 CPU、内存、磁盘空间、磁盘 IO 大小、TCP 端口等都可以分配设置。用户通过 RPC 与 Borg 交互来操作 job，大多数是通过命令行工具完成的，其它的则通过监控系统。大部分 job 描述文件是用声明式配置语言 BCL (GCL 变体) 编写的。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm6gwiFjJGU0GjKP%2Fborg-job-task-state.png?generation=1567863296038434\&alt=media)

用户可以修改一个运行中的 job 属性值并发布到 Borg，然后 Borg 按照新的 job 配置来更新 tasks。更新通常是以滚动方式完成，并且可以对更新导致的任务中断（重新调度或者抢占）的数量进行限制，任何导致更多中断的更改都会被跳过。

tasks 需要能够处理 Unix `SIGTERM` 信号， 以便在被强制发送 `SIGKILL` 之前，可以有时间进行清理，保存状态，完成当前执行请求，拒绝新的请求。在实践中，规定时间有 80% 的可以正常处理信号。

### 2.4 Allocs（allocation）

Borg `alloc` 是可以运行在一个或多个 tasks 的机器上的一组预留资源。无论资源是否使用，资源仍分配。Allocs 可以被用于将来的 tasks 资源使用，在停止和启动 task 之间保留资源，并且可以将不同 jobs 的 tasks 聚集到同一台机器上。一个 alloc 的资源和机器上资源类似的方式处理，多个 tasks 运行在 alloc 上是共享资源的，如果一个 alloc 必须重新分配到另外一台主机，它的 tasks 也会同它一起重新被调度。

一个 `alloc` 集合和 job 很像，它是一组分配在多台机器上的预留资源。一旦创建一个 alloc 集合，就可以提交一个或多个 jobs 运行在其中。为简洁起见，通常使用 "task" 引用 alloc 或者一个顶级的 task(alloc 之外的 task) 和 "job" 来引用一个 job 或者 alloc 集合。

### 2.5 Priority, quota, and adminssion control

优先级和配额用于防止运行的比实际能容纳多的这种负载情况。每个 job 都有一个 `priority` 优先级，一个小的正整数。高优先级的 task 可以在牺牲较低优先级的 task 来获取资源，甚至是以抢占方式。 Borg 为不同用途定义不同的优先级：监控、生产、批处理和 best effort。

针对生产级别的 jobs 是禁止 task 互相抢占的。优先级决定 jobs 在 cell 中处于运行还是等待状态。 `Quota` 配额被用于确定调度哪些 jobs。配额表示为一段时间内（通常为几个月）给定优先级的资源量（CPU、RAM、磁盘等）。这些值指定了用户的 job 在请求时间段内可以使用的最大资源量。配额检查是准入控制的一部分，配额不足情况下，job 会被拒绝调度。

高优先级的配额成本比低优先级要高。生产级别的配额仅限于 cell 中实际可用资源，因此用户提交满足生产级别 job 运行预期的资源配额。虽然不建议用户配置超买，但是很多用户都会比实际的需要配额要大，以防止后续用户增长可能造成的资源短缺。对于超买，应对方案就是超卖。

配额分配的使用在 Borg 之外进行处理，和物理容量设计密切相关，结果反映在不同数据中心的配额价格和可用性上。Borg 通过 capability 系统，给予某些用户特殊权限，如允许管理员删除或者修改任意 cell 中的 job，或者运行用户访问受限的内核功能或者 Borg 操作，如禁用其 jobs 预算。

### 2.6 Naming and monitoring

只是提供创建和运行是不够的，服务客户端和相关系统需要能够访问到对应的服务，即使被重新调度到新的机器上。因此，Borg 针对每个 task 创建一个稳定的 "Borg name service" (BNS)，包括 cell 名，job 名和 task 数量。Borg 用这个名字将 task 的主机名和端口写入到 Chubby 一致且高可用的文件中，该文件用于 RPC 系统查找 task 端。BNS 名也用于 task DNS 名构成基础，如用户 ubar 在 cell cc 上执行的 job jfoo 第 50 个 task，可以通过 50.jfoo.ubar.cc.borg.google.com 访问。Borg 还会在发生变化的时候把 job 大小和 task 健康信息写入到 Chubby，以使得负载均衡器可以获取到请求路由指向。

几乎所有运行在 Borg 上的 task 都包含一个内建的 HTTP server，用于发布 task 的健康信息和数千个性能指标（如 RPC 延迟）。Borg 监控健康检测 URL 并且在 tasks 无响应或者返回错误的 HTTP 码时重启。其它的数据会被监控工具追踪展示在 Dashboards 上并且在服务级别（SLO）问题时告警。

用户可以通过一个名叫 Sigma 提供的 Web 用户界面上，检查 jobs 的状态，查看特定的 cell，或者深入查看各个 jobs 和 tasks，检测它们的资源占用，详细的日志和执行历史，以及最终的宿命。应用程序会产生大量的日志，通过日志轮转避免磁盘空间不足，并且在任务退出后保留一段时间以协助进行调试。如果一项工作没有运行，Borg 会提供一个 “有待处理的” 注释，以及如何修改 job 资源请求用以更好的适配 cell。

Borg 记录所有 job 提交和 task 事件，详细到每个 task 资源使用信息记录在基础设施存储。这是一个可伸缩的只读数据存储，并且由 Dremel（Google 交互式数据分析系统）提供类 SQL 方式进行交互。数据被用于计费，调试 job 和系统故障以及长期的容量规划。它也提供 Google 集群工作负载跟踪数据。

所有的这些特性帮助用户理解和调试 Borg 以及他们的 jobs，并且帮助我们的 SREs 每人管理数万台主机。

## 3. Borg architecture

一个 Borg cell 由一组主机组成，一个名为 Borgmaster 的逻辑集中控制器，和一个名为 Borglet 的代理进程组成，Borglet 运行在 cell 中的每个主机上。所有的 Borg 组件都是通过 C++ 编写的。

### 3.1 Borgmaster

每个 cell 的 Borgmaster 包含两个进程：主 Borgmaster 进程和一个独立的调度器。主 Borgmaster 进程处理客户端 RPCs 请求，状态变化（如创建 job）或者提供数据的只读访问（如查找 job）。它还管理系统中所有对象的状态（如主机，tasks，allocs 等），和 Borglets 通信，并提供一个 Web UI 作为 Sigma 的备份。

Borgmaster 在逻辑上是一个单一的进程，但是实际上它有五个副本。每个副本都维护一大部分 cell 状态的内存副本，并且这个状态以高可用，分布式，基于 Paxos 存储保存在副本所在的本地磁盘上。每个 cell serves 选举出的 master，作为 Paxos 主导和状态 mutator，处理所有变更 cell 状态的操作，例如在某台主机上提交一个 job 或者终止一个 task。

master 在当 cell 启动和选举的 master 失效时选举（通过 Paxos）；它 获得一个 Chubby 锁，以便其它系统可以找到它。选择一个 master 并故障转移到新设备大概需要 10s，但是在大型 cell 中因为内存状态的重建，这个时间可能需要一分钟。当副本从中断中恢复时，它会动态的从其它最新的 Paxos 副本重新同步状态。

Borgmaster 在某个时间点的状态称为 checkpoint，采用定期快照以及保存更改日志在 Paxos 存储。Checkpoint 有多个用途，包括将 Borgmaster 的状态恢复到过去的任意点（例如，在接收触发 Borg 中的软件缺陷请求前调试）；在极端情况下需要手动维护；为将来的查询构建持久的事件日志和离线模拟。

一个名为 Fauxmaster 的高保真 Borgmaster 模拟器可以用于读取 checkpoint 文件，并包含生产 Borgmaster 代码的完整副本，以及 Borglets 的存根接口。它接收 RPCs 来进行状态机变更和执行操作，如 “调度所有待处理的任务”，我们使用它来调试失败，通过与它进行交互就好像它是一个实时 Borgmaster，利用 checkpoint 文件模拟 Borglets 重放真实交互。用户可以按步执行并观察过去实际发生的系统状态变化。Fauxmaster 还可以用于容量规划（“这种类型的新 jobs 多少适合？”），以及在变更 cell 配置前进行健全性检查（“这次变更是否会驱逐其它重要的 jobs？”）。

### 3.2 Scheduling

当提交一个 job 后，Borgmaster 会将它永久记录在 Paxos 存储中并将 job 的 tasks 加入到待处理队列。调度器异步遍历，如果有足够的可用资源满足 job 的需求，则分配 tasks 到主机。(调度器主要针对 tasks 操作，而不是 jobs。)遍历优先级从高到低，在优先级内通过轮询方案进行调制，确保用户之间的公平性，避免在阻塞大型 job 后。调度算法有两部分组成：可行性检查，通过选择一个可行性机器评分寻找 task 可运行的主机。在可行性检查中，调度器找到一组满足 task 需求的主机以及足够的可用资源 -- 其中包括分配给可以驱逐的优先级较低的任务资源。在评分中，调度器确保每个可行性主机的“良好性”。评分综合了用户指定的首选项，但主要由内置条件驱动，例如最小化抢占 tasks 的数量和优先级，挑选已拥有 task 副本包的主机，在 power 和故障域之间传播 task 以及 packing 质量，包括将高优先级和低优先级任务混布到一台机器，以允许高优先级任务在负载峰值扩展。

Borg 最初使用 E-PVM 的一种变体进行评分，它可以跨异构资源生成单一成本值，并在放置 task 时最小化成本变化。在实践中，E-PVM 最终会在所有的主机上分散负载，为负载峰值留下空间 - 但代价是增加了碎片化，特别针对需要大多数主机的大型 tasks；我们有时称之为“最糟糕的”。

对应的另外一端则是“最佳匹配”，尝试尽可能紧密的填充主机。这使得一些主机没有用户 jobs 运行（它们仍然运行存储服务器），因此放置大型 tasks 很简单，但是严密的 packing 会导致用户或者 Borg 对资源需求的误估。这会破坏具有突发负载的应用程序，对于指定低 CPU 请求的批处理 jobs 尤其糟糕。20% 的非生产 tasks 请求小于 0.1 核 CPU，因此它们可以轻松调度并尝试在未使用的资源中机会性运行。

我们当前的评分模型是一个混合模型，它试图减少搁浅资源的数量 - 因为机器上的另外一个资源被完全分配而无法使用。他提供的 packing 效率比最适合我们的 workload 的高 3-5%。

如果评分阶段选择的主机没有足够的可用资源来适用新 task，Borg 会抢占（kills）低优先级的 tasks，从低到高优先级，直到可行。我们将抢占的 tasks 添加到调度器的待处理队列中，而不是迁移或者休眠它们。

Task 启动延迟（时间从 job 提交到 task 运行）是已经接收并继续受重视的区域。它变数很大，中位数通常约为 25s，软件包安装占 80%：已知瓶颈之一是本地磁盘写入软件包的竞争。为了减少任务启动时间，调度器更倾向把任务分配到已安装必要软件包（程序和数据）的主机：大多数软件包是不可变的，因此可以共享和缓存。（这是 Borg 调度器支持的唯一数据方形式。）此外，Borg 使用树和类似 toreent 的协议将程序包并行分发到主机。

另外，调度器使用多种技术使其扩展到数万台主机的 cells。

### 3.3 Borglet

Borglet 是 cell 中每个主机上运行的本地 Borg agent。它负责启动和停止 tasks；如果失败了就重启它们；通过操作系统内核设置来管理本地资源；滚动调试日志；并且汇报所在主机状态给 Borgmaster 和其它监控系统。Borgmaster 每隔几秒轮询 Borglet 以检索主机的当前状态并向其发送未完成的请求。这使得 Borgmaster 可以控制通信速率，避免需要明确的流控机制，并防止恢复风暴。

被选举的 master 负责准备发送信息给 Borglets 并且通过它们的响应来更新 cell 的状态。为了提升性能，每个 Borgmaster 副本运行一个无状态的链接分片来处理与某些 Borglet 的通信；每当 Borgmaster 选举发生时，都会重新计算分区。为了弹性，Borglet 始终报告其完整状态，但链接分片通过报告的信息和机器状态的差异来聚合和压缩此信息，以减少选定 master 的更新负载。

如果 Borglet 多次没有响应轮询信息，则将其主机标记为关闭，并重新调度主机上的 tasks。如果恢复响应，Borgmaster 会通知 Borglet 杀死那些已经重新调度的 tasks，以避免重复。Borglet 即使失去和 Borgmaster 的联系也会继续正常运行，因此即使所有的 Borgmaster 副本失败，当前正在运行的任务和服务也会保持正常运行。

### 3.4 Scalability

我们很难确认 Borg 集中式架构可扩展性的限制在哪里；到目前为止，每次到瓶颈时，我们都设法消除它。一个 Borgmaster 可以在一个 cell 中管理数千台主机，并且几个 cells 每分钟可以处理 1000 个 tasks。一个繁忙的 Borgmaster 使用 10-14 个 CPU 和 50GiB 内存。我们使用多种技术来实现这种规模。

早期版本的 Borgmaster 有一个简单的同步循环，它接收请求，调度 tasks，并与 Borglets 通信。为了能处理更大的 cells，我们将调度程序拆分为一个独立的进程，以便它可以与其它 Borgmaster 功能并行运行，这些功能是为了容器而复制的。调度程序副本在 cell 状态的缓存副本上运行。它重复以下流程：从选定的 master 检索状态变化（包括分配和待处理的工作）；更新其本地副本；调度传递分配 tasks；并通知 master 这些任务。master 会接收并应用这些任务，除非它们不合适（如，基于过期状态），这会导致它们在下次流程中重新分配。这与 Omega 中使用的乐观并发控制非常相似，实际上我们最近在 Borg 中添加了为不同 workload 类型使用不同调度器的能力。

为了提升响应时间，我们添加了单独的线程和 Borglets 通信并响应只读的 RPCs。为了更好的性能，我们通过 5 个 Borgmaster 副本共享（分区）这些功能。同时，这些将 UI 的 99%ile 响应时间保持在 1s 以内，Borglet 轮询间隔的 95%ile 保持在 10s 以内。

有几件事让 Borg 调度器更具扩展性：

**Score cahing**（评分缓存）： 评估可行性和主机评分成本是昂贵的，因此 Borg 会缓存评分，知道主机或者任务的属性发生变更。例如，主机的任务终止，属性被更改，或者任务的需求变更。忽略资源数量的微小变化，减少缓存失效。

**Equivalence classes**（等价类）：一个 Borg job 中的 tasks 通常拥有相同的要求和约束，因此不是对每台机器上待处理的每个任务评分以确定可行性，而是对所有可行的机器进行评分，Borg 只对每个等价类（具有相同要求的一组任务）的任务进行可靠性和评分。

**Relaxed randomization**（轻松随机化）：计算大型 cell 所有机器的可用性和分数是极大浪费的，因此调度程序以随机顺序检查机器，直到找到 “足够” 可行的机器进行评分，然后在其中选择最佳组。这减少了 tasks 进入和离开系统时所需的评分和缓存失效量，并加快了将任务分配给主机的速度。轻松随机化有点类似 Sparrow 的批量抽样，同时还处理优先级、抢占、异质性和打包安装的成本。

在我们的实验中，从头开始调度一个 cell 的整个负载往往需要花费几百秒时间，但是在上述技术被禁用后超过 3 天后仍未完成。通常情况下，挂起队列的在线调度传递在半秒内完成。

## 4. Availability

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm6itGJ6R0z9cEgv%2Fborg-evictions-per-task-week.png?generation=1567863294028628\&alt=media)

失败是大规模系统的常态：上图提供了 15 个样本 cells 中任务驱逐原因的细分。运行在 Borg 上的任务使用诸如复制、存储持久化状态在分布式文件系统中，以及（如果适用）偶尔检查点等技术来处理此类事件。即便如此，我们仍试图减轻这些事件的影响。例如，Borg：

* 如果有必要，在新的主机上自动重新调度被驱逐的 tasks
* 通过在主机、机架和电源域等故障域中分散 job 的 tasks 来减少相关故障
* 限制 task 中断的允许速率以及在维护活动（如操作系统或主机升级）期间可同时停机的 job tasks 数量
* 使用声明性的期望状态表示以及幂等操作，以便失败的客户端可以无害的重新提交任何被遗忘的请求
* 速率限制从无法访问的机器中查找任务的新位置，因为它无法区分大规模故障和网络分区
* 避免重复 task::machine 配对导致任务或主机崩溃
* 通过反复重新运行 logsaver task 来恢复写入本地磁盘的关键中间数据，即使它附加的 alloc 已终止或移动到另外一台主机。用户可以设置系统持续尝试的时间，几天时间是很常见的

Borg 的一个关键设计功能是，即使 Borgmaster 或者运行 tasks 的 Borglet 出现故障，已经运行的 tasks 仍会继续运行。但是保持 master 运行仍然很重要，因为当它宕机时，无法提交新的作业或更新现有的作业，并且无法重新调度故障主机的 tasks。

Borgmaster 使用多种技术组合，使其在实践中实现 99.99% 的可用性：主机故障复制；准入控制避免过载；使用简单的低级工具部署实例，最大限度地减少外部依赖性。每个 cell 互相独立，最小化相关操作员错误和故障传播的可能性。这些目标，不是可扩展性限制，是针对更大 cells 的依据。

## 5. Utilization

Borg 的主要目标之一是有效利用 Google 的主机，这是一笔巨大的金融投资：提高几个百分点的利用率可以节省数百万美元。本节讨论并评估 Borg 用于执行此操作的一些策略和技术。

### 5.1 Evaluation methodology

我们的 jobs 有存放限制，需要处理罕见的工作负载峰值，我们的主机是异构的，我们在从回收服务 jobs 的资源中运行批处理作业。因此，为了评估我们的策略选择，我们需要一个比 “平均利用率” 更精密的指标。经过大量的实验，我们选择了 cell 压实：给定一个工作负载，通过移除 cell 中的主机直到不能满足工作负载运行为止，在 scratch 上反复重新打包工作负载，以确保我们没有挂起不幸的配置。这提供了清理终结的条件以及促进了自动比较而没有合成工作负载生成和建模的缺陷。

没有办法在生产环境 cells 上实验，但是我们使用 Fauxmaster 获取高保真模拟结果，使用来自实际生产 cells 和工作负载的数据，包括所有约束，实际限制，预留和使用数据。这些数据来自 Borg checkpoints 于周三 2014-10-01 14:00 PDT。（其它 checkpoints 也产生了类似的结果。）我们在消除特殊用途的前提下，选择了 15 个 Borg cells 进行报告，测试小型（小于 5000 主机）cells，然后对剩余种群进行取样，以在一定范围内实现大致均匀的分布。

为了保持压实 cell 中的机器异构性，我们随机选择要移除的主机。为了保持工作负载的异构性，我们保留了除特定机器（例如 Borglets） 相关的服务器和存储 tasks。对于大雨原始 cell 大小一般的 jobs，我们将硬约束更改为软约束，如果它们非常“挑剔”并且只能调度在少数主机上，则允许 0.2% 的 tasks 处于等待状态。大量实验表明，这以很小的方差产生了可重复的结果。如果我们需要比原来更大的 cell，则在压实前多克隆原始 cell 几次；如果需要更多的 cells，我们只从原始 cell 克隆。

针对具有不同随机数种子的每个 cells，每个实验重复 11 次。在图标中，我们使用误差条显示所需机器的最小值和最大值，并选择 90%ile 值作为结果 - 如果系统管理员想合理的确定工作负载是否合适，平均值和中位数不能反应应该怎么做。我们认为 cell 压实提供了一种公平一致的方式来比较调度策略，并且可以直接转换为成本/收益结果：更好的额策略需要更少的机器来运行相同的工作负载。

我们的实验侧重于从某个时间点调度（打包）工作负载，而不是重放长期工作负载跟踪。一部分原因是为了避免应对打开和关闭队列模型困难，一部分是因为传统的完成时间指标不适用于我们的环境以及长期运行的服务，一部分是为了压实提供一个干净的信号，一部分是我们不相信结果会有显著差异，以及一部分实际问题：我们发现自己在某一时刻因为我们的实验消耗了 200000 Borg CPU 内核 -- 即使在 Google 这个规模上，这也是一项非常大的投资。

在生产中，我们故意让工作负载增长，偶尔的 “黑天鹅” 事件，负载峰值，主机故障，硬件升级以及大规模局部故障（如电源总线管道）。图 4 展示了我们应用 cell 压实，真实世界的 cells 会变小多少。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm6k16nxCnON67g2%2Fborg-compacted-size.png?generation=1567863293500323\&alt=media)

### 5.2 Cell sharing

我们几乎所有的机器同时运行生产和非生产的 tasks：98% 的主机在共享 Borg cells 中，在 Borg 管理的整套机器中占 83%。（我们有一些特殊用途的专用 cells）

自从许多其他组织在独立的集群中运行面向用户和批处理 jobs，我们审查了如果我们做同样的事情会发生什么。图 5 显示，在中型的 cell 中，分离生产和非生产工作需要额外 20-30% 的主机来运行我们的工作负载。这是因为生产 jobs 通常会预留资源来处理罕见的工作负载峰值，但大多数时候都不使用这些资源。Borg 回收未使用的资源来运行大部分非生产的工作，因此我们可以需要更少的机器。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm6m9roVhZ8SI4Tx%2Fborg-prod-noprod-different-cells.png?generation=1567863293291515\&alt=media)

大多数 Borg cells 由数千个用户共享。图 6 展示了原因。对于这些测试，针对需要至少 10TiB 的内存（或 100TiB）的用户，我们将用户的工作负载分成新的 cell。我们现有的政策看起来很好：即使门槛较大，我们也需要 2-16 倍的 cells，以及 20-150% 的额外机器。再次强调，汇集资源可以显著降低成本。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm6oK1r_tUMYtj2n%2Fborg-user-different-cells.png?generation=1567863294022557\&alt=media)

但是，将不相关的用户和 job 类型打包到同一台主机会导致 CPU 竞争，因此我们需要更多的机器来补偿？为了评估这一点，我们研究了在具有相同时钟速度的相同机器类型下运行不同环境中的 CPI（每条指令的周期）如何变化。在这些条件下，CPI 值具有可比性，可用作性能干扰的一个代理，因为 CPI 加倍会使 CPU 绑定程序的运行时间加倍。数据是一周内从大约 12000 个随机选择的生产 tasks 中收集的，使用 \[83] 中描述的硬件配置基础设施计算 5 分钟间隔内的周期和指令，并对样本进行加权，以便每秒计算 CPU 时间。结果并不明确。

* （1）我们发现 CPI 与同一时间间隔内的两次测量正相关：机器上的总体 CPU 使用率，以及（很大程度上独立地）机器上的任务数量；向机器添加 task 会使其它 tasks 的 CPI 增加 0.3%（使用适合数据的线性模型）；将机器 CPU 使用率提高 10% 会使 CPI 增加不到 2%。但即使相关性具有显著性，它们也只解释了我们在 CPI 测量中看到的 5% 的方差；其它因素占主导地位，例如应用和特定干扰模式的固有差异 \[24,83]。
* （2）将我们从共享 cells 中采样的 CPI 与应用种类较少的专用 cells 比较，我们看到共享 cells 的平均 CPI 为 1.58（σ = 0.35），专用 cells 的平均 CPI 为 1.53（σ = 0.32）-- 即，共享 cells 中的 CPU 性能差大约 3%。
* （3）为了解决应用在不同 cells 中可能有不同的工作负载，或者甚至遭受选择性偏差（可能对干扰更敏感的程序被移入到专用 cells）的担忧，我们查看了 Borglet 的 CPI，它在两种类型的 cells 中的所有机器上运行。我们发现它在专用单元中的 CPI 为 1.20（σ = 0.29），在共享 cells 中的 CPI 为 1.43（σ = 0.45），这表明它在专用 cell 中的运行速度比在共享 cell 中快 1.19 倍，尽管这超过了轻负载机器的效果，但是结果稍微偏向于专用 cells。

这些实验证实，仓库规模的性能比较是棘手的，加强了 \[51] 中的观察，并且表明共享并不会大幅度增加程序运行的成本。

但即使我们假设我们的结果最不利，共享仍然是占优的：由于几种不同分区方案所需要的机器减少，CPU 减速不再受到影响，共享优势适用于所有资源，包括内存和磁盘，而不仅仅是 CPU。

### 5.3 Large cells

Google 构建了大型 cells，既允许运行大型计算，又可以减少资源碎片。我们通过在多个较小的 cells 中划分 cell 的工作负载来测试后者的影响 - 首先随机置换 jobs 并在分区间以循环方式分配它们。图 7 证实使用较小的 cells 需要更多的机器。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm6qX9ohG7kAIWu7%2Fborg-small-cells-more-machines.png?generation=1567863295160080\&alt=media)

### 5.4 Fine-grained resource requests

Borg 用户以 ms（毫秒）为单位请求 CPU，以 bytes（字节）为单位请求内存和磁盘空间。（核心是处理器超线程，针对机器类型的性能进行了标准化）图 8 显示了它们利用了这种粒度：请求的内存或 CPU 内核数量中几乎没有明显的 “甜点”，并且在这些资源间几乎没有明显的相关性。这些分布与\[68] 中的分布非常相似，除了我们看到 90% ile 以及以上的内存请求略大。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm6sPRzQ5FB_4zwp%2Fborg-cdf-cpu-memory.png?generation=1567863295339114\&alt=media)

提供一组固定大小的容器或者虚拟机，虽然在 IaaS（基础设施即服务）提供商中很常见，但并不能很好的满足我们的需求。为了证明这一点，我们通过将它们四舍五入到每个资源维度中的下一个最接近 2 的幂来 “分配” 生产作业和分配的 CPU 和内存资源限制，从 CPU 的 0.5core 到内存的 1GiB 开始。图 9 显示，这样做在中位数情况下需要额外 30%\~50% 的资源。上限来自在将原始 cell 翻两番之后压缩开始之前将整个机器分配给大型 tasks 并不适合；允许这些 tasks 进入 pending 的下限。（这比 \[37] 报告的要少大约 100%，因为我们支持 4 buckets 并允许 CPU 和内存容量独立扩展）

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm6u80XZq0uM8-6h%2Fborg-cdf-add-overheads.png?generation=1567863295351288\&alt=media)

### 5.5 Resource reclamation

一个 job 可以指定资源限制 - 应该配置每个 tasks 的资源上限。Borg 使用这个限制来确定用户是否有足够的配额来接纳这个 job，并确定特定主机是否有足够的可用资源来调度这个 task。正如有些用户购买超过他们需要的配额一样，有些用户请求的资源比他们的 tasks 要多，因为 Borg 通常会尝试杀死比其请求更多的内存或者磁盘空间任务，或使用的 CPU 限制在请求值以内。此外，某些 tasks 偶尔需要使用其所有的资源（例如，在一天的高峰时段或在应对拒绝式服务攻击时），但大多数情况下不会。

相比浪费当前未使用的已分配资源，我们估计一个 task 将使用多少资源，并回收可以容忍较低质量资源的工作，例如批处理 jobs。整个过程称为资源回收，这个预估被称为 task 的预留，并由 Borgmaster 每隔几秒使用 Borglet 捕获的细粒度使用（资源消耗）信息计算。初始预留设置等于资源请求（限制）；在 300秒之后，为了允许启动瞬态，它朝着实际使用加上安全余量缓慢衰减。如果使用量超过预留量，则保留会迅速增加。

Borg 调度程序使用限制来计算生产 tasks 可行性，因此它们从不依赖于回收的资源，也不会暴露于资源超额预订；对于非生产的 tasks，它使用现有 tasks 的预留，因此可以将新的 tasks 调度到回收资源。

如果预留（预测）错误，机器可能会在运行时耗尽资源 - 即使所有的 tasks 的使用都低于其限制。如果发生这种情况，我们会杀死或限制非生产 tasks，从来不针对生产 tasks。

图 10 显示了在没有资源回收的情况下需要更多的机器。大约 20% 的 workload 在中位数单元格中的回收资源中运行。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm6wa9GuvqSbkdFv%2Fborg-cdf-additional-machines.png?generation=1567863294831869\&alt=media)

我们可以从图 11 中看到更多的细节，其中显示了预留和使用与限制的比率。超出其内存限制的 task 将是第一个在需要资源时被抢占的 task，无论其优先级如何，因此在 tasks 超出其内存资源限制的情况下比较少见。另一方面，CPU 可以很容易地被限制，因此短期峰值可以相当无害地推动使用超出预留。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm6yKyws7xCc9RR0%2Fborg-cdf-ratio-cpu-mem.png?generation=1567863289902745\&alt=media)

图 12 显示了发生的事情。预留显然更接近于第二周的使用情况，而在第三周则有所不同，基线周（第 1 周和第 4 周）显示最大差距。正如预期的那样，内存 OOM 事件的发生率在第 2 周和第 3 周略有增加。在审查这些结果后，我们认为净增益超过了下行趋势，并将中等资源回收参数部署到其它 cells。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm7-mAFlmU4pjz6b%2Fborg-oom-events.png?generation=1567863292545491\&alt=media)

## 6. Isolation

我们 50% 的机器运行 9 个或更多 tasks；90%ile 机器大约运行 25 个 tasks 并将运行 4500 个线程 \[83]。尽管在应用程序之间共享机器可以提高利用率，但它还需要良好的机制来防止 tasks 之间互相干扰。这适用于安全性和性能。

### 6.1 Security isolation

我们使用 Linux chroot jail 作为同一台机器上多个 tasks 之间的主要安全隔离机制。为了允许远程调试，我们习惯于自动分发（和撤销）ssh 密钥，以便用户只有在未用户运行 tasks 才能访问机器。这已被 borgssh 命令所取代，该命令与 Borglet 合作构建一个 ssh 连接，该 shell 连接在 tasks 相同的 chroot 和 cgroup 中运行，更严格的锁定访问。

VM 和安全沙箱技术用于运行 Google 的 AppEngine（GAE）\[38] 和 Google Compute Engine（GCE）的外部软件。我们在运行每个托管 VM 的 KVM 进程 \[54] 中运行 Borg task。

### 6.2 Performance isolation

Borglet 的早期版本具有原始的资源隔离：对内存、磁盘空间和 CPU 周期性进行事后检查，结合终止使用过多内存或磁盘的 tasks 以及积极应用 Linux 的优先级来控制使用 CPU 太多的 tasks。但是，流氓 tasks 太容易影响机器上其它 tasks 的性能，因此一些用户夸大他们的资源请求，以减少 Borg 可以与他们共同调度的 tasks 数量，从而降低利用率。由于所涉及的安全边际，资源回收可能会收回部分盈余，但不是全部。在最极端的情况下，用户请求使用专用机器或 cell。

现在，所有的 Borg tasks 运行在一个 Linux cgroup 基础的资源容器中 \[17,58,62] 并且 Borglet 操作容器设置，因为操作系统内核处于循环中，所以控制得到了很大改善。即使如此，偶然的低级资源干扰（例如，存储器带宽或 L3 高速缓存污染）仍然发生，如 \[60,83]。

为了帮助过载和过度使用，Borg tasks 有一个应用程序类或 app 类。最重要的区别在于延迟敏感（LS）应用程序和其它应用程序，本文中称为批处理。LS 任务用于需要快速响应请求的面向用户的应用程序和共享基础结构服务。高优先级 LS 任务获得最佳处理，并且能够一次暂停几秒钟的批处理任务。

第二个分裂是在可压缩资源（如，CPU 周期，磁盘 I/O 带宽）之间，这些资源是基于速率的，并且可以通过降低服务质量而不会杀死 tasks 来调整；不可压缩的资源（例如，存储器，磁盘空间），其通常在不杀死 tasks 的情况下不能被调整。如果机器用完了不可压缩的资源，Borglet 会立即终止从最低优先级到最高优先级的 tasks，直到可以满足剩余的预留。如果机器耗尽了可压缩的资源，Borglet 会限制使用（支持 LS tasks），以便在不中断任何 tasks 的情况下处理短负载峰值。如果事情没有改善，Borgmaster 将从机器中删除一个或者多个 tasks。

Borglet 中的用户空间控制循环将内存分配给容器根据预测的未来使用（针对生产 tasks）或者内存压力（对于非生产 tasks）；处理内核中的 OOM 事件；并且当 tasks 尝试分配超过内存限制时，或者当一个过度提交的机器实际内存耗尽时杀死他们。由于需要精确的内存统计，Linux eager file-caching 会使实现变得非常复杂。

为了提高性能隔离，LS tasks 可以保留整个物理 CPU 核，从而阻止其它 LS tasks 使用它们。允许批处理 tasks 在任何核上运行，但是它们相对与 LS tasks 被赋予微小的调度程序共享。Borglet 动态调整贪婪 LS tasks 的资源上限，以确保它们不会在几分钟内使批处理 tasks 匮乏，在需要时选择性地应用 CFS 带宽控制 \[75]；分配不足，因为我们有多个优先级。

和 Leverich \[56] 一样，我们发现标准 Linux CPU 调度器（CFS）需要进行大量调整以支持低延迟和高利用率。为了减少调度延迟，我们的 CFS 版本使用扩展的每个 cgroup 加载历史记录 \[16]，允许通过 LS tasks 抢占批处理 tasks，并在多个 LS tasks 可在 CPU 上运行时减少调度量。幸运的是，我们的许多应用程序都使用 thread-per-request 模型，这可以减轻持续负载不平衡的影响。我们谨慎使用 cpusets 将 CPU 内核分配给具有特别严格的延迟要求的应用程序。这些努力的一些结果如图 13 所示。该领域的工作仍在继续，增加线程布局和 CPU 管理，即 NUMA，超线程和功耗感知（例如，\[81]），并提高控制保真度 Borglet。

![](https://2920767072-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LoAvJI9gBldK6-l44i7%2F-LoAvd-24y_bdbAVPeKH%2F-LoAvm71IN4AebsWdWx9%2Fborg-cpu-util.png?generation=1567863291269579\&alt=media)

允许 tasks 消耗资源达到其限制。它们中的大多数被允许超出 CPU 等可压缩资源的范围，以利用未使用的（slack）资源。只有 5% 的 LS tasks 会禁用此功能，可能是为了获得更好的可预测性；不到 1% 的批处理 tasks 这样做。默认情况下禁用使用 slack 内存，因为它会增加 tasks 被杀的可能性，但即便如此，10% 的 LS tasks 会这样做，79% 的批处理 tasks 会这样做，因为它是 MapReduce 框架的默认设置。这补充了 reclaimed 资源的结果。批处理 tasks 愿意机会性的利用未使用的以及 reclaimed 的内存：大多数时候这是有效的，尽管当 LS tasks 急需资源时偶尔会牺牲批处理 tasks。

## 7. Related work

资源调度已经研究了数十年，其中包括广域 HPC 超级计算网格，工作站网格和大规模服务器集群等各种环境。我们只关注大规模服务器集群环境中最相关的工作。

最近的几项研究分析了雅虎、谷歌和 Facebook 的集群痕迹 \[20, 52, 63, 68, 70, 80, 82]，并说明这些现代数据中心和工作负载所固有的规模和异构性的挑战。 \[69] 包含集群管理器体系结构的分类。

Apache Mesos \[45] 使用 offer-based 机制，在一个中心资源管理器（有点类似 Borgmaster 去除其调度器）和多个 “框架” 如 Hadoop \[41] 和 Spark \[73] 之间拆分资源管理和放置功能。Borg 主要使用基于请求的机制集中这些功能，这种机制可以很好的扩展。DRF \[29, 35, 36, 66] 最初是 Mesos 开发的；Borg 使用优先级和入场配额。Mesos 开发人员已宣布扩展 Mesos 的野心，包括资源分配和回收，以及解决 \[69] 中确定的一些问题。

YARN \[76] 是一个以 Hadoop 为中心的集群管理器。每个应用程序都有一个管理员，通过中央资源管理器协商所需要的资源；这与 Google MapReduce 工作从 2008 年左右用于从 Borg 获取资源的方案大致相同。YARN 的资源管理器最近才变得容错。一个相关的开源工作是 Hadoop Capacity Scheduler \[42]，它提供多租户支持，包括容量保证，分层队列，弹性共享和公平性。YARN 最近已经扩展到支持多种资源类型，优先级，抢占和高级准入控制 \[21]。俄罗斯方块研究原型 \[40] 支持完工意识的工作包装。

Facebook 的 Tupperware \[64] 是一个类似 Borg 的系统，用于集群上的 cgroup 容器；虽然它似乎提供了一种资源回收形式，但只披露了一些细节。Twitter 有开源的 Aurora \[5]，一个类似 Borg 的调度程序，用于运行在 Mesos 之上的长期运行服务，配置语言和状态机类似于 Borg。

Microsoft \[48] 的 Autopilot 系统提供 “自动化软件配置和部署；系统监控；执行修复操作以处理有故障的软件和硬件” 针对 Microsoft 集群。Borg 生态系统提供了类似的功能，Isaard \[48] 概述了我们所遵循的许多最佳实践。

Quincy \[49] 使用网络流模型为数百个节点集群上的数据处理 DAG 提供公平性和数据位置感知调度。Borg 使用配额和优先级来共享用户之间的资源，并扩展到数万台计算机。Quincy 直接处理执行图，而这是在 Borg 之上单独构建的。

Cosmos \[44] 专注于批处理，重点是确保用户能够公平的访问他们捐赠给集群的资源。它使用每个 per-job 管理来获取资源；公开的细节很少。

微软的 Apollo 系统 \[13] 使用 per-job 调度器进行短期批处理 jobs，以便在看起来与 Borg cells 大小相当的集群上实现高吞吐量。 Apollo 使用低优先级后台工作的机会性执行，以（有时）多天排队延迟为代价将利用率提高到高水平。Apollo 节点提供任务开始时间的预测矩阵，作为超过两个资源维度的大小的函数，调度器与启动成本和远程数据访问的估计相结合以进行放置决策，由随机延迟调制以减少冲突。Borg 使用中央调度程序根据先前分配的状态进行放置决策，可以处理更多的资源维度，并专注于高可用性，长期运行的应用程序的需求；Apollo 可能会处理更高的任务到达率。

Alibaba 的伏羲 \[84] 支持数据分析工作量；它自 2009 以来一直在运行。与 Borgmaster 一样，FuxiMaster（为故障容忍而复制）从节点收集资源可用性信息，接受来自应用程序的请求，并将其匹配。伏羲增量调度策略与 Borg 的等价类相反：伏羲不是将每个任务与一组合适的机器匹配，而是将新可用资源与待处理工作的积压相匹配。与 Mesos 一样，伏羲允许定义 “虚拟资源” 类型。只有合成工作负载结果可公开获得。

Omega \[69] 支持多并行，特定的 “垂直”大致相当于 Borgmaster 减去持久性存储和链接分片。Omega 调度程序使用乐观并发控制来操作共享表示所期望和观察到的 cell 状态存储找中央持久化存储，该存储通过单独的链接组件与 Borglet 同步。Omega 体系结构旨在支持多个不同的工作负载，这些工作负载具有自己特定于应用程序的 RPC 接口，状态机和调度策略（例如，长时间运行的服务，来自各种框架的批处理 jobs，类似集群存储系统的基础设施服务，来自 Google 云的虚拟机）。另一方面，Borg 提供了 “一刀切” 的 RPC 接口，状态机语义和调度策略，由于需要支持许多不同的工作负载，其规模和复杂性随着时间的推移而增长，并且可扩展性尚不成问题。

Google 开源 Kubernetes 系统 \[53] 将 Docker 容器 \[28] 中运行的应用放置在不同的节点。它既可以在裸机（如 Borg）上运行，也可以在各种云托管服务提供商上运行，例如 Google Computer Engine。许多构建 Borg 的工程师正在积极开发它。Google 提供了一个名未 Google Container Engine 的托管版本 \[39]。我们将在下一节讨论如何将 Borg 的经验教训应用于 Kubernetes 上。

高性能计算社区在该领域有着悠久的工作传统（例如，Maui，Moab， Platform LFS \[2,47,50]）；但是，规模、工作负载以及容错的要求与 Google 的 cells 不同。通常，这样的系统通过具有待处理工作的大量积压（队列）来实现高利用率。

VMware \[77] 等虚拟化提供商和数据中心解决方案公司如 HP 和 IBM \[46] 提供集群管理解决方案通常可扩展到 O（1000）主机。此外，一些研究小组已经制定了原型机系统，以某种方式来提高调度决策的质量（例如，\[25,40,72,74]）

最后，正如我们所指出的，管理大规模集群的另外一个重要组成部分是自动化和 “operator scaleout”。\[43] 描述如何规划故障，多租户，健康检查，准入控制和可重启性，是每个 operator 管理大量机器所必需的。Borg 的设计理念类似，允许我们每个 oprator（SRE）管理数万台主机。

## 8. Lessons and future work

在本节中，我们将重述 Borg 运行在生产环境十多年来的一些经验教训，并描述这些观察结果如何在设计 Kubernetes 时得到应用。

### 8.1 Lessons learned: the bad

我们从 Borg 的一些特征开始，作为警示故事，并在 Kubernetes 中提供有根据的替代设计。

**Job 作为唯一的 tasks 分组机制，是有限制性的。** Borg 没有一流的方式将整个 multi-job 服务作为单个实体进行管理，或者关联一个服务（例如，金丝雀和生产 tracks）。作为一个 hack 方式，用户在 job 名称中硬编码服务拓扑，并构建更高级别的管理工来解析这些名字。另一方面，不能引用 job 的任意子集，这回导致滚动更新和 job 大小调整的语义不灵活等问题。为了避免这种困难，Kubernetes 摒弃了 job 理念，而是通过标签来组织调度单元（pods）- 用户可以使用任意 键/值 对来关联任意系统中的对象。等价于 Borg 的一个 job，可以通过将 job:jobname 标签关联到一组 pods，也可以表示任何其它有用的分组，例如 service，层或者发布类型（如，生产，staging，测试）。Kubernetes 中的操作通过标签查询来识别其目标并应用对象。这种方式比单个固定的 job 组有更大的灵活性。

**每个主机一个 IP 地址带来复杂性。** 在 Borg，主机上的所有 task 都使用其主机的单个 IP 地址，从而共享主机的端口空间。这会导致很多麻烦：Borg 必须将端口作为资源，tasks 必须预先声明它们需要多少端口，并且声明在启动时使用哪些端口；Borglet 必须强制执行端口隔离；以及命名和 RPC 系统必须像对待 IP 地址一样处理端口。

由于 Linux 命名空间，VMs，IPv6，以及软件定义网络的出现，Kubernetes 可以采用对用户更加友好的方式，来消除这些复杂性：每个 pod 和 service 都有自己的 IP 地址，允许开发人员选择端口而不是要求他们的软件适配基础设施，并消除了端口管理的基础架构复杂性。

**Optimizing for power users at the expense of casual ones。** Borg 提供了一系列针对 “高级用户” 的功能，因此他们可以微调程序的运行方式（BCL 规范列出了大约 230 个参数）：最初的焦点是支持 Google 最大的资源消费者，他们效率的提高是最重要的。不幸的是，这种 API 的丰富性使得 “帮工” 用户更加困难，并限制了它的发展。我们的解决方案是构建在 Borg 之上运行的自动化工具和服务，并通过实验确定适当的设置。这些可以从容错应用提供的自由实验中获益：如果自动化出错，那就是麻烦，而不是灾难。

### 8.2 Lessons learned: the good

另外一方面，Borg 的一些设计异常优越，并经过了时间的考验。

**Allocs 非常有用。** Borg alloc 抽象产生了广泛使用的 logsaver 模式和另外一个通过简单的数据加载器 task 定期更新 web 服务器数据的流行模式。Allocs 和软件包允许由不同的团队开发这样的帮助程序服务。Kubernetes 等效于 alloc 是 pod，它是一个或多个容器的资源包，这些容器始终被调度到同一台主机上并可以共享资源。不同于 alloc 中的 tasks，Kubernetes 在同一个 pod 中使用 helper 容器，但是这个想法是一样的。

**集群管理不仅仅是 task 管理。** 虽然 Borg 的主要职责是管理 tasks 和主机的生命周期，但在 Borg 上运行的应用程序可以从许多其它集群服务中受益，包括命名和负载均衡。Kubernetes 通过 service 抽象支持命名和负载均衡：一个 service 有一个名字和一个通过标签选择器的动态 pod 集。集群中的任何一个容器都可以通过 service 名来访问服务。在这一层上，Kubernetes 会自动对和标签选择器匹配的 pod 之间的服务连接进行负载均衡，并跟踪由于故障而重新调度的 pod。

**自我检查是至关重要的。** 虽然 Borg 几乎总是 “正常工作”，但当出现问题时，找到根因可能是很具挑战性的。Borg 的一个重要设计决策是向所有用户显示调试信息而不是隐藏它：Borg 拥有数千名用户，因此 “自助” 必须是调试的第一步。尽管这使得我们更难以弃用功能并改变用户所依赖的内部策略，但是它仍然是一个胜利，我们发现实际没有选择的余地。为了处理庞大的数据，我们提供了多个级别的 UI 和调试工具，因此用户可以快速识别与其 jobs 相关的异常事件，然后深入查看其应用程序和基础架构本身的详细数据和错误日志。

Kubernetes 旨在复制 Borg 的许多自我检查技术。例如，它附带了用于资源监控的 cAdvisor \[15] 工具，以及基于 Elasticsearch/Kibana \[30] 和 Fluentd \[32] 的日志聚合。可以查询主服务器以获取其对象状态的快照。Kubernetes 有一个统一的机制，所有组件都可以用来记录事件（例如，正在调度的容器，失败的容器）以提供给客户。

**master 是分布式系统的核心。** Borgmaster 最初设计为一个单一的系统，但是随着时间的推移，它变得更像是一个内核，它位于管理用户 jobs 的服务生态系统的核心。例如，我们将调度程序和主 UI（Sigma）拆分为单独的进程，并添加针对准入控制，垂直和水平自动扩展，重新打包 tasks，定期 job 执行（cron），工作流管理和离线归档系统用作离线查询的服务。

Kubernetes 架构的未来走向：它的核心拥有一个 API server，它负责处理请求和操作底层的状态对象。集群管理逻辑可作为此 API server 客户端的小型可组合服务，例如复制控制器，它可以在出现故障时维护容器所需的副本数量，以及节点控制器，它管理机器生命周期。

### 8.3 Conclusion

实际上，Google 的所有集群工作负载在过去十年中都转向使用 Borg。我们会继续发展它，并将我们从中学到的经验应用到 Kubernetes。


