<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; Docker</title>
	<atom:link href="https://blog.gmem.cc/tag/docker/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Fri, 03 Apr 2026 04:13:36 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>从镜像中抽取文件</title>
		<link>https://blog.gmem.cc/extract-files-from-image</link>
		<comments>https://blog.gmem.cc/extract-files-from-image#comments</comments>
		<pubDate>Sat, 18 Apr 2020 09:09:23 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[Docker]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=32389</guid>
		<description><![CDATA[<p>动机 在某个应用场景中，我们需要在每个K8S节点上运行一个Agent，此Agent能够执行运维人员动态配置的Python脚本，来检查节点是否出现故障。 Python脚本能够调用的库，需要按需不断更新，受限于运行环境，我们不便搭建和维护PyPI私服。因此，我们考虑将库封装在Docker镜像中进行分发。这里会面临两个问题： 如何尽量高效的分发Python库镜像 如何在Agent中正确的安装Python库 我们的想法是，利用镜像分层这一特性，将Python库的更新存放在单独的层中，这样Agent就可以仅仅拉取需要的层。此外，Agent需要分析层对文件系统做了哪些变更，把这些变更在自己的文件系统中回放一遍。 镜像仓库接口 包括Harbor在内的镜像仓库软件，底层是Registry，它是OCI Distribution Specification的参考实现，描述了客户端和镜像仓库服务器的交互接口。 OCI Distribution Specification是Docker Registry HTTP API V2的标准化（两者大体相同）。V2相对于V1，主要改进包括：镜像定义的简化、安全方面的增强、带宽占用的减少。 接口概览 新特性 镜像校验：客户端可以先下载manifest，校验清单的签名，然后再下载镜像层。这可以确保来源可靠且未被篡改。在每下载一个层后，客户端都可以计算其摘要，和清单中的值进行比对，确保一致 可恢复Push/Pull：推送、拉取镜像都支持断点续传 层去重，两个相同的层仅仅会被上传或存储一份： 如果上传层时发现它已经存在，则不上传 <a class="read-more" href="https://blog.gmem.cc/extract-files-from-image">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/extract-files-from-image">从镜像中抽取文件</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">动机</span></div>
<p>在某个应用场景中，我们需要在每个K8S节点上运行一个Agent，此Agent能够执行运维人员动态配置的Python脚本，来检查节点是否出现故障。</p>
<p>Python脚本能够调用的库，需要按需不断更新，受限于运行环境，我们不便搭建和维护PyPI私服。因此，我们考虑将库封装在Docker镜像中进行分发。这里会面临两个问题：</p>
<ol>
<li>如何尽量高效的分发Python库镜像</li>
<li>如何在Agent中正确的安装Python库</li>
</ol>
<p>我们的想法是，利用镜像分层这一特性，将Python库的更新存放在单独的层中，这样Agent就可以<span style="background-color: #c0c0c0;">仅仅拉取需要的层</span>。此外，Agent需要分析层对文件系统做了哪些变更，<span style="background-color: #c0c0c0;">把这些变更在自己的文件系统中回放一遍</span>。</p>
<div class="blog_h1"><span class="graybg">镜像仓库接口</span></div>
<p>包括Harbor在内的镜像仓库软件，底层是<a href="https://github.com/docker/distribution">Registry</a>，它是<a href="https://github.com/opencontainers/distribution-spec/blob/master/spec.md">OCI Distribution Specification</a>的参考实现，描述了客户端和镜像仓库服务器的交互接口。</p>
<p>OCI Distribution Specification是Docker Registry HTTP API V2的标准化（两者大体相同）。V2相对于V1，主要改进包括：镜像定义的简化、安全方面的增强、带宽占用的减少。</p>
<div class="blog_h2"><span class="graybg">接口概览</span></div>
<div class="blog_h3"><span class="graybg">新特性</span></div>
<ol>
<li>镜像校验：客户端可以先下载manifest，校验清单的签名，然后再下载镜像层。这可以确保来源可靠且未被篡改。在每下载一个层后，客户端都可以计算其摘要，和清单中的值进行比对，确保一致</li>
<li>可恢复Push/Pull：推送、拉取镜像都支持断点续传</li>
<li>层去重，两个相同的层仅仅会被上传或存储一份：
<ol>
<li>如果上传层时发现它已经存在，则不上传</li>
<li>如果两个进程同时上传一个层，则仅仅保存先上传的那个</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">URL前缀</span></div>
<p>所有接口端点都以API版本和仓库名为前缀，例如仓库library/ubuntu的URI前缀是<pre class="crayon-plain-tag">/v2/library/ubuntu/</pre>，这种URI布局便于实现身份验证和访问控制。</p>
<p>仓库名需要满足以下限制：</p>
<ol>
<li>仓库名可以包含多个路径分量（/分隔开的片断），每个分量匹配正则式<pre class="crayon-plain-tag">[a-z0-9]+(?:[._-][a-z0-9]+)*</pre></li>
<li>分量的数量没有限制</li>
<li>仓库名中长度（包括/）不超过256字符</li>
</ol>
<div class="blog_h3"><span class="graybg">错误报文</span></div>
<p>如果处理请求出现错误，返回4xx响应，并且附带下面格式的响应体：</p>
<pre class="crayon-plain-tag">{
    // 1-N个错误
    "errors:" [{
            // 全大写唯一标识符
            "code": &lt;error identifier&gt;,
            // 可读文本
            "message": &lt;message describing condition&gt;,
            // 任意结构化消息
            "detail": &lt;unstructured&gt;
        },
        ...
    ]
}</pre>
<p>示例：<pre class="crayon-plain-tag">{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":null}]}</pre>  </p>
<div class="blog_h3"><span class="graybg">内容摘要</span></div>
<p>镜像仓库接口非常依赖于<a href="https://en.wikipedia.org/wiki/Content-addressable_storage">内容可寻址性</a>，这种技术利用防冲撞哈希（collision-resistant hash）算法获取信息内容的摘要，此摘要作为信息的唯一标识符，既可用于寻址，也可用于信息完整性校验。</p>
<p>摘要的形式为sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b，前面是算法，后面是HEX形式的哈希值。</p>
<div class="blog_h3"><span class="graybg">摘要头</span></div>
<p>为了支持内容校验，所有接口响应都可以提供<pre class="crayon-plain-tag">Docker-Content-Digest</pre>头，其中包含响应中实体的摘要信息，对于：</p>
<ol>
<li>Blob，实体就是整个响应体</li>
<li>Manifest，实体是清单体，不包括签名部分</li>
</ol>
<p>出于安全性的考虑，客户端应该基于摘要来验证响应实体。</p>
<div class="blog_h2"><span class="graybg">镜像拉取</span></div>
<p>镜像是<span style="background-color: #c0c0c0;">一个JSON清单 + 一系列独立的层文件</span>，要获取完整镜像，需要对镜像仓库发起多次HTTP调用。</p>
<div class="blog_h3"><span class="graybg">清单拉取</span></div>
<p>镜像拉取的第一步是获取清单，API格式：</p>
<pre class="crayon-plain-tag">// reference可以包含一个Tag或摘要
// GET /v2/&lt;name&gt;/manifests/&lt;reference&gt;

// 示例
// curl -H 'Authorization: Basic &lt;credentials&gt;' 
//      -H 'Accept: application/vnd.docker.distribution.manifst.v2+json'
//      https://docker.gmem.cc/v2/alpine/manifests/3.11
// 默认内容类型 application/vnd.docker.distribution.manifest.v1+prettyjws
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
   "config": {
      ...
   },
   "layers": [
      {
         "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
         "size": 2802957,
         "digest": "sha256:c9b1b535fdd91a9855fb7f82348177e5f019329a58c53c47272962dd60f71fc9"
      }
   ]
}</pre>
<p>可以看到，镜像包含的所有层的信息，都可以从清单上找到。下一步就是基于层的标识符（摘要）发起层拉取请求。</p>
<div class="blog_h3"><span class="graybg">层拉取</span></div>
<p>拉取层或者其它Blob数据，使用的是同一接口。你需要在URL中传递Blob数据的摘要，包括算法前缀：</p>
<pre class="crayon-plain-tag">// name为仓库名，digest为层的摘要
// GET /v2/&lt;name&gt;/blobs/&lt;digest&gt;

// 示例
// https://docker.gmem.cc/v2/alpine/blobs/sha256:86235187a3abfce6fa63b526aaea608392a9f629fd827ad75867b906362f9dd0</pre>
<p><span style="background-color: #c0c0c0;">镜像仓库可以发送307（对于HTTP1.1以下版本则是302）响应，客户端应当正确处理响应，从其它地方下载层</span>。</p>
<p>镜像仓库会设置适当的头，以支持层的缓存。它还会设置Range头以支持断点续传。</p>
<div class="blog_h2"><span class="graybg">小结</span></div>
<p>了解镜像仓库的API后，我们可以确定，要下载镜像的某个层是很容易的。</p>
<p>对于本文开头的Python库镜像，我们可以检查并下载新增的层，剩下需要解决的问题就是，如何从层中读取对文件系统做的变更了。</p>
<div class="blog_h1"><span class="graybg">镜像格式</span></div>
<p>要从层中抽取文件，必须理解镜像、层的结构。 </p>
<p><a href="https://github.com/docker/distribution/blob/5cb406d511b7b9163bff9b6439072e4892e5ae3b/docs/spec/manifest-v2-1.md">Image Manifest Version 2, Schema 1</a>（已经弃用）、<a href="https://github.com/docker/distribution/blob/5cb406d511b7b9163bff9b6439072e4892e5ae3b/docs/spec/manifest-v2-2.md">Image Manifest Version 2, Schema 2</a> 是Docker关于镜像清单文件的规范。</p>
<p><a href="https://github.com/moby/moby/blob/master/image/spec/v1.2.md#docker-image-specification-v120">Docker Image Specification v1.2.0</a> 是Docker关于镜像的规范，包含层的格式说明。</p>
<p><a href="https://github.com/opencontainers/image-spec/blob/master/spec.md">OCI Image Format Specification</a> 是关于镜像格式的规范，包括Docker在内的主流容器引擎都支持支持此规范。</p>
<p>本章主要基于Docker相关的最新标准进行分析，OCI在很大程度上是兼容的。</p>
<div class="blog_h2"><span class="graybg">格式概览</span></div>
<p>对于Docker镜像，其清单格式大致如下：</p>
<pre class="crayon-plain-tag">{
    // 清单版本
    "schemaVersion": 2,
    // MIME类型
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    // 引用配置对象，配置存放在一个JSON blob中，容器运行时使用此配置来启动容器
    "config": {
        "mediaType": "application/vnd.docker.container.image.v1+json",
        "size": 7023,
        // 需要使用blobs接口下载配置对象
        "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
    },
    // 层列表，以Base镜像开始（顺序和Schema1相反）
    "layers": [
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 32654,
            // 需要使用blobs接口下载层
            "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
        }
    ]
}</pre>
<p>可以看到，清单给出了每个层的格式（mediaType），Schema 2支持的MIME类型包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">MIME</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>application/vnd.docker.distribution.manifest.v1+json</td>
<td>遗留的Schema 1清单格式</td>
</tr>
<tr>
<td>application/vnd.docker.distribution.manifest.v2+json</td>
<td>Schema 2清单格式</td>
</tr>
<tr>
<td>application/vnd.docker.distribution.manifest.list.v2+json</td>
<td>清单列表（所谓fat manifest）</td>
</tr>
<tr>
<td>application/vnd.docker.container.image.v1+json</td>
<td>容器配置对象格式</td>
</tr>
<tr>
<td>application/vnd.docker.image.rootfs.diff.tar.gzip</td>
<td>层，tar.gz格式</td>
</tr>
<tr>
<td>application/vnd.docker.image.rootfs.foreign.diff.tar.gzip</td>
<td>永远不应该被Push的层</td>
</tr>
<tr>
<td>application/vnd.docker.plugin.v1+json</td>
<td>插件配置JSON</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">镜像文件系统变更集</span></div>
<p>从MIME类型的说明上可以看到，镜像的层是gzip压缩处理的tar包。 </p>
<p>尝试解压缩，粗略看起来就是一棵Linux目录层次树。那么，如何识别出哪些文件是新增、哪些是修改，哪些又是删除掉的呢？</p>
<p>在相关规范中，层也叫镜像文件系统变更集（Image Filesystem Changeset ），不管是OCI还是Docker，都遵循：</p>
<ol>
<li>新增、修改的目录或文件，按照它的原始路径存储在变更集中</li>
<li>删除的目录或文件，为文件的basename前缀以<pre class="crayon-plain-tag">.wh.</pre>，存储位置保持不变。例如在某个变更集中<pre class="crayon-plain-tag">./etc/hosts</pre>被删除，则会创建一个空白的<pre class="crayon-plain-tag">./etc/.wh.hosts</pre></li>
</ol>
<div class="blog_h2"><span class="graybg">小结</span></div>
<p>可以看到，要解开存放在镜像层中的Python库更新，也很简单：</p>
<ol>
<li>根据MIME类型，选取适当的工具解压缩</li>
<li>遍历得到的目录树，遇到<pre class="crayon-plain-tag">.wh.</pre>开头的节点，则从Agent文件系统中删除，对于其它节点，覆盖即可</li>
</ol>
<div class="blog_h1"><span class="graybg">相关库</span></div>
<p>尽管利用Go语言标准库就可以完成镜像层下载、解压缩、应用到Agent文件系统，我们还是在这里介绍一些可以简化操作的库。 </p>
<div class="blog_h2"><span class="graybg"> opencontainers/image-tools</span></div>
<p>该项目提供了创建、校验、解包OCI镜像的工具，鉴于Docker和OCI规范具有很多兼容的地方，这个项目中的代码参考。</p>
<div class="blog_h2"><span class="graybg">docker/docker</span></div>
<p>github.com/docker/docker提供了Docker客户端库，比较不方便的地方是，它必须连接到Docker守护程序才能工作。</p>
<div class="blog_h3"><span class="graybg">创建客户端</span></div>
<pre class="crayon-plain-tag">var dockerClient *client.Client

var clientOpts []client.Opt
clientOpts = append(clientOpts, client.FromEnv)
clientOpts = append(clientOpts, client.WithAPIVersionNegotiation())

dockerClient, err = client.NewClientWithOpts(clientOpts...)

// 获取镜像基本信息
dockerClient.ImageInspectWithRaw(ctx, "nginx:latest")

// 从Docker守护程序获取一个或多个镜像，返回Reader
readCloser, err := dockerClient.ImageSave(ctx, []string{id})</pre>
<div class="blog_h3"><span class="graybg">镜像接口</span></div>
<p>Docker客户端实现了接口：</p>
<pre class="crayon-plain-tag">type ImageAPIClient interface {
	ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error)
	BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error)
	BuildCancel(ctx context.Context, id string) error
	ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
	ImageHistory(ctx context.Context, image string) ([]image.HistoryResponseItem, error)
	ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
	ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error)
	ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error)
	ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error)
	ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error)
	ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error)
	ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error)
	ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error)
	ImageSave(ctx context.Context, images []string) (io.ReadCloser, error)
	ImageTag(ctx context.Context, image, ref string) error
	ImagesPrune(ctx context.Context, pruneFilter filters.Args) (types.ImagesPruneReport, error)
}</pre>
<p>可以看到，它没有提供针对层的操控函数，因此本文的场景下没有什么用处。</p>
<div class="blog_h2"><span class="graybg">google/go-containerregistry</span></div>
<p>这个项目提供了镜像仓库接口的Go语言封装。提供读写镜像、层、ImageIndex等功能。 </p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/extract-files-from-image">从镜像中抽取文件</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/extract-files-from-image/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Kubernetes学习笔记</title>
		<link>https://blog.gmem.cc/kubernetes-study-note</link>
		<comments>https://blog.gmem.cc/kubernetes-study-note#comments</comments>
		<pubDate>Sat, 11 Jun 2016 02:12:08 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[PaaS]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[K8S]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=16626</guid>
		<description><![CDATA[<p>简介 什么是K8S Kubernates（发音 / kubə'neitis /，简称K8S）是一个容器编排工具，使用它可以自动化容器的部署、扩容、管理。使用K8S可以将应用程序封装为容易管理、发现的逻辑单元。使用K8S你可以打造完全以容器为中心的开发环境。 K8S的特性包括： 自动装箱：根据容器的资源需求和其他约束条件，自动部署容器到合适的位置，与此同时，不影响可用性。K8S可以混合管理关键负载、非关键负载并尽可能的有效利用资源 自我修复：当容器宕掉后自动重启它，当节点宕掉后重新调度容器。关闭不能正确响应自定义健康检查的容器，在容器准备好提供服务之前，不将他们暴露给客户端 水平扩容：手工（UI、命令行）或自动（根据CPU负载）进行自动的扩容/缩容 服务发现/负载均衡：不需要修改应用程序来使用第三方的服务发现机制，K8S为容器提供专有IP，同时为一组容器（类似Swarm中的Service）提供单一的DNS名称，应用程序可以基于DNS名称发现服务。K8S还内置的负载均衡支持 无缝滚动更新/回滚：支持逐步的将更新应用到程序或配置，与此同时监控程序的健康状况，避免同时杀死程序的所有实例。如果出现问题，K8S能够自动回滚更新 密码和配置管理：部署、更新应用程序的密码、配置信息时，不需要重新构建镜像，不需要在配置信息中暴露密码信息 存储编排：自动从本地磁盘、公有云、网络存储系统挂载存储系统 资源监控、访问并处理日志、调试应用、提供认证和授权功能…… K8S提供了PaaS的简单性、IaaS的灵活性，支持在各基础设施提供商之间迁移。 尽管K8S提供了部署、扩容、负载均衡、日志、监控等服务，但是它并不是传统的PaaS平台： 它不限制能支持的应用程序类型，不限制编程语言、SDK。只要应用能在容器中运行，就可以在K8S下运行  不内置中间件（例如消息总线）、数据处理框架（例如Spark）、数据库、或者存储系统 不提供服务市场来下载服务 允许用户选择日志、监控、报警系统 同时，很多PaaS平台可以运行在K8S之上，例如OpenShift、Deis，你可以在K8S上部署自己的PaaS平台，集成自己喜欢的CI环境，或者仅仅是部署容器镜像。 什么是云原生 <a class="read-more" href="https://blog.gmem.cc/kubernetes-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/kubernetes-study-note">Kubernetes学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<div class="blog_h2"><span class="graybg">什么是K8S</span></div>
<p>Kubernates（发音 / kubə'neitis /，简称K8S）是一个容器编排工具，使用它可以自动化容器的部署、扩容、管理。使用K8S可以将应用程序封装为容易管理、发现的逻辑单元。使用K8S你可以打造完全以容器为中心的开发环境。</p>
<p>K8S的特性包括：</p>
<ol>
<li>自动装箱：根据容器的资源需求和其他约束条件，自动部署容器到合适的位置，与此同时，不影响可用性。K8S可以混合管理关键负载、非关键负载并尽可能的有效利用资源</li>
<li>自我修复：当容器宕掉后自动重启它，当节点宕掉后重新调度容器。关闭不能正确响应自定义健康检查的容器，在容器准备好提供服务之前，不将他们暴露给客户端</li>
<li>水平扩容：手工（UI、命令行）或自动（根据CPU负载）进行自动的扩容/缩容</li>
<li>服务发现/负载均衡：不需要修改应用程序来使用第三方的服务发现机制，K8S为容器提供专有IP，同时为一组容器（类似Swarm中的Service）提供单一的DNS名称，应用程序可以基于DNS名称发现服务。K8S还内置的负载均衡支持</li>
<li>无缝滚动更新/回滚：支持逐步的将更新应用到程序或配置，与此同时监控程序的健康状况，避免同时杀死程序的所有实例。如果出现问题，K8S能够自动回滚更新</li>
<li>密码和配置管理：部署、更新应用程序的密码、配置信息时，不需要重新构建镜像，不需要在配置信息中暴露密码信息</li>
<li>存储编排：自动从本地磁盘、公有云、网络存储系统挂载存储系统</li>
<li>资源监控、访问并处理日志、调试应用、提供认证和授权功能……</li>
</ol>
<p>K8S提供了PaaS的简单性、IaaS的灵活性，支持在各基础设施提供商之间迁移。</p>
<p>尽管K8S提供了部署、扩容、负载均衡、日志、监控等服务，但是它并不是传统的PaaS平台：</p>
<ol>
<li>它不限制能支持的应用程序类型，不限制编程语言、SDK。只要应用能在容器中运行，就可以在K8S下运行</li>
<li> 不内置中间件（例如消息总线）、数据处理框架（例如Spark）、数据库、或者存储系统</li>
<li>不提供服务市场来下载服务</li>
<li>允许用户选择日志、监控、报警系统</li>
</ol>
<p>同时，很多PaaS平台可以运行在K8S之上，例如OpenShift、Deis，你可以在K8S上部署自己的PaaS平台，集成自己喜欢的CI环境，或者仅仅是部署容器镜像。</p>
<div class="blog_h2"><span class="graybg">什么是云原生</span></div>
<p>以Kubernetes为核心的技术生态圈，已经成为构建云原生架构的基石。</p>
<p>云原生架构没有权威的定义，但是基于这种架构的应用具有一系列的模式：</p>
<ol>
<li>代码库：每个可部署的应用程序，都有<span style="background-color: #c0c0c0;">独立的代码库</span>，可以在不同环境部署多个实例</li>
<li>明确的依赖：应用程序的依赖应该基于适当的工具（例如Maven、Bazel）明确的声明，<span style="background-color: #c0c0c0;">不对部署环境有任何依赖</span></li>
<li>配置注入：和发布环境（dev/stage/pdt）变化的<span style="background-color: #c0c0c0;">配置信息</span>，应该以操作系统级的环境变量<span style="background-color: #c0c0c0;">注入</span></li>
<li>后端服务：数据库、消息代理应视为附加资源，并在所有环境中同等看待</li>
<li>编译、发布、运行：构建一个可部署的<span style="background-color: #c0c0c0;">应用程序并将它与配置绑定</span>，根据这个组件/配置的组合来启动一个或者多个进程，这<span style="background-color: #c0c0c0;">两个阶段是严格分离</span>的</li>
<li>进程：应用程序运行一个或多个无状态进程，不共享任何东西 —— <span style="background-color: #c0c0c0;">任何状态存储于后端服务</span></li>
<li>端口绑定：应用程序是独立的，并通过<span style="background-color: #c0c0c0;">端口绑定（包括HTTP）导出任何服务</span></li>
<li>并发：并发通常通过<span style="background-color: #c0c0c0;">水平扩展</span>应用程序进程来实现</li>
<li>可任意处置：通过<span style="background-color: #c0c0c0;">迅速启动和优雅的终止进程</span>，可以最大程度上的实现鲁棒性。这些方面允许<span style="background-color: #c0c0c0;">快速弹性缩放、部署更改和从崩溃中恢复</span></li>
<li>开发/生产平等：通过保持开发、预发布和生产环境<span style="background-color: #c0c0c0;">尽可能的一致来实现持续交付和部署</span></li>
<li>日志：不管理<span style="background-color: #c0c0c0;">日志文件，将日志视为事件流</span>，允许执行环境通过<span style="background-color: #c0c0c0;">集中式服务收集、聚合、索引和分析事件</span></li>
<li>管理任务：管理性的任务，例如数据库迁移，应该在应用程序运行的<span style="background-color: #c0c0c0;">那个环境下，一次性的执行</span></li>
</ol>
<p>关键字：环境解耦、无状态、微服务。</p>
<div class="blog_h2"><span class="graybg">微服务架构选型</span></div>
<div class="blog_h3"><span class="graybg">K8S VS Spring Cloud</span></div>
<p>两者之间存在高度的功能重叠，例如服务发现、负载均衡。</p>
<div class="blog_h3"><span class="graybg">K8S VS Swarm</span></div>
<p>Swarm的功能比K8S要弱的多，可以实现简单的负载均衡、HA等特性，小规模部署可以使用。</p>
<div class="blog_h2"><span class="graybg">架构图</span></div>
<div class="blog_h3"><span class="graybg">整体架构</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2016/06/k8s-arch-1.png"><img class="aligncenter  wp-image-21073" src="https://blog.gmem.cc/wp-content/uploads/2016/06/k8s-arch-1.png" alt="k8s-arch-1" width="823" height="474" /></a></p>
<div class="blog_h3"><span class="graybg">Master架构</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2016/06/k8s-arch-2.png"><img class="aligncenter  wp-image-21077" src="https://blog.gmem.cc/wp-content/uploads/2016/06/k8s-arch-2.png" alt="k8s-arch-2" width="828" height="433" /></a></p>
<div class="blog_h3"><span class="graybg">Node架构</span></div>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2016/06/kube-arch-3.png"><img class="aligncenter  wp-image-21079" src="https://blog.gmem.cc/wp-content/uploads/2016/06/kube-arch-3.png" alt="kube-arch-3" width="823" height="457" /></a></p>
<p>&nbsp;</p>
<div class="blog_h2"><span class="graybg">网络架构</span></div>
<p>K8S网络包括CNI、Service网络、Ingress、DNS这几个方面的内容：</p>
<ol>
<li>CNI负责Pod到Pod的网络连接</li>
<li>Service网络负责Pod到Service的连接</li>
<li>Ingress负责外部到集群的访问</li>
<li>DNS负责解析集群内部域名</li>
</ol>
<p>取决于具体实现，节点可能可以直接访问CNI、Service的IP地址。</p>
<p>Pod<span style="background-color: #c0c0c0;">到集群外部的访问，基于SNAT实现</span>。</p>
<p>Pod在节点内部的连接，经典方案是veth pair + bridge，也就是说多个Pod会连接到同一个网桥上，实现互联。</p>
<p>Pod在节点之间的连接，经典方案是bridge、overlay，Calico等插件则基于虚拟路由。</p>
<p>Kubernetes容器网络由Kubenet或CNI插件负责，前者未来会被废弃。</p>
<div class="blog_h3"><span class="graybg">Kubenet</span></div>
<p>Kubenet是K8S早期的原生网络驱动，提供简单的单机容器网络。需要用Kubelet参数<pre class="crayon-plain-tag">--network-plugin=kubenet</pre>启用。</p>
<p>Kubenet本身不实现跨节点网络连接、网络策略，它通常和云提供商一起使用来实现路由规则配置、跨主机通信。Kubenet也用到一些CNI的功能，例如它基于CNI bridge插件创建<span style="background-color: #c0c0c0;">名为cbr0的Linux Bridge，为每个容器创建一对veth pair并连接到cbr0网桥</span>。</p>
<p>Kubenet在CNI插件的基础上拓展了很多功能：</p>
<ol>
<li>基于host-local IP地址管理插件，为Pod分配IP地址并定期释放分配而未使用的IP</li>
<li>设置sysctl的net.bridge.bridge-nf-call-iptables=1</li>
<li>为Pod的IP设置SNAT（MASQUERADE），以允许Pod 访问外部网络</li>
<li>开启Linux Bridge的hairpin、混杂模式，允许Pod访问自己所在的Service IP</li>
<li>HostPort管理、设置端口映射</li>
<li>带宽控制。可以通过kubernetes.io/ingress-bandwith、kubernetes.io/egress-bandwith来配置Pod网络带宽限制</li>
</ol>
<p>Kubenet会使用bridge、lo、host-local等几个CNI插件，默认在/opt/cni/bin中搜索这些插件的二进制文件。你可以通过--network-plugin-dir参数定制搜索位置。此外Kubenet来回去/etc/cni/net.d中搜索CNI配置文件。</p>
<p>支持通过Kubelet参数--network-plugin-mtu<span style="background-color: #c0c0c0;">定制MTU，支持限制带宽</span>。这两个特性是Kubenet<span style="background-color: #c0c0c0;">不能被CNI完全代替</span>的原因。</p>
<div class="blog_h3"><span class="graybg">CNI</span></div>
<p>CNI是容器网络的标准，试图通过一段JSON来描述容器网络配置。CNI是K8S和底层网络插件之间的抽象层。</p>
<p>CNI包含以下接口：</p>
<pre class="crayon-plain-tag">type CNI interface {
	AddNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
	CheckNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
	DelNetworkList(ctx context.Context, net *NetworkConfigList, rt *RuntimeConf) error
	GetNetworkListCachedResult(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
	GetNetworkListCachedConfig(net *NetworkConfigList, rt *RuntimeConf) ([]byte, *RuntimeConf, error)

	AddNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
	CheckNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
	DelNetwork(ctx context.Context, net *NetworkConfig, rt *RuntimeConf) error
	GetNetworkCachedResult(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
	GetNetworkCachedConfig(net *NetworkConfig, rt *RuntimeConf) ([]byte, *RuntimeConf, error)

	ValidateNetworkList(ctx context.Context, net *NetworkConfigList) ([]string, error)
	ValidateNetwork(ctx context.Context, net *NetworkConfig) ([]string, error)
}

type RuntimeConf struct {
	ContainerID string
	NetNS       string
	IfName      string
	Args        [][2]string
	// A dictionary of capability-specific data passed by the runtime
	// to plugins as top-level keys in the 'runtimeConfig' dictionary
	// of the plugin's stdin data.  libcni will ensure that only keys
	// in this map which match the capabilities of the plugin are passed
	// to the plugin
	CapabilityArgs map[string]interface{}

	// DEPRECATED. Will be removed in a future release.
	CacheDir string 
}

type NetworkConfig struct {
	Network *types.NetConf
	Bytes   []byte
}</pre>
<p>AddNetwork负责在创建容器时，进行网络接口的配置；DelNetwork则在删除容器时，清理掉网络接口。参数NetworkConfig是网络配置信息，RuntimeConf则是容器运行时传入的网络命名空间信息。</p>
<p>CNI配置编写在如下形式的JSON文件中：</p>
<pre class="crayon-plain-tag">{
  name: "mynet",
  // 使用bridge CNI插件
  type: "bridge",
  bridge: "cni0",
  ipam: {
    // 基于host-local插件进行IP地址管理
    type: "host-local",
    subnet: "10.0.0.0/16",
    routes: [
      { "dst": "0.0.0.0/0" }
    ]
  }
}</pre>
<p>上述配置文件，默认需要存在/etc/cni/net.d目录下，并且命名为*.conf，对应的插件二进制文件默认需要存放在/opt/cni/bin下。 </p>
<p>在K8S中启用基于CNI的网络插件，需要配置Kubelet参数<pre class="crayon-plain-tag">--network-plugin=cni</pre>。CNI配置文件位置通过参数<pre class="crayon-plain-tag">--cni-conf-dir</pre>配置，如果目录中存在多个配置文件则仅仅取第一个。CNI插件二进制文件位置通过<pre class="crayon-plain-tag">--cni-bin-dir</pre>配置。</p>
<div class="blog_h1"><span class="graybg">K8S对象</span></div>
<p>K8S对象是指运行在K8S系统中的持久化实体，K8S使用这些实体来表示你的应用集群的状态。例如：</p>
<ol>
<li>哪些容器化应用程序在执行，在何处（Node）执行</li>
<li>上述应用程序可用的资源情况</li>
<li>和应用程序行为有关的策略，例如重启策略、升级策略、容错策略</li>
</ol>
<p>通过创建一系列对象，你可以告诉K8S，你的集群的负载是什么样的，所谓集群的期望状态（desired state）。</p>
<p>要创建/修改/删除K8S对象，可以调用Kubernetes API。提供命令行kubectl可以间接的调用此API，你也可以在程序中调用这些API，目前支持Go语言。</p>
<p>K8S对象包括：</p>
<ol>
<li>Pod、Service、Volume、Namespace等基本对象</li>
<li>ReplicaSet、Deployment、StatefulSet、DaemonSet、Job等高级对象，这些高级对象在上述基本对象之上构建，并且提供了额外功能、便利化功能</li>
</ol>
<p>本章不会一一解释这些不同类型的对象。</p>
<div class="blog_h2"><span class="graybg">规格和状态</span></div>
<p>任何K8S对象都有两个内嵌的字段：规格、状态。前者由你提供，声明对象期望状态（所谓对象的期望状态即集群的期望状态）。后者则表示某一时刻对象的实际状态，此状态由K8S更新，K8S控制平面会积极的管理对象的状态，使其尽可能满足规格。</p>
<p>例如，Kubernetes Deployment是描述运行在集群中的一个应用程序的K8S对象。你创建一个Deployment时，可能在规格中声明你需要3个应用程序的Replica。K8S会读取规格并启动3个应用实例，如果一段时间后宕掉1个实例，则K8S会检测到Spec和Status之间的不同，进而启动一个新的实例代替宕掉的那个。</p>
<div class="blog_h2"><span class="graybg">对象规格</span></div>
<p>调用K8S API创建对象时，你需要提供一个JSON格式的规格说明，其中包含期望状态和一些基本信息（例如应用的名称）。</p>
<p>通过kubectl创建对象时，你需要提供一个YML文件，kubectl会自动将其转换为JSON格式。</p>
<p>下面是对象规格（YML）的一个示例：</p>
<pre class="crayon-plain-tag"># 如果K8S版本小于1.8则使用 apps/v1beta1
apiVersion: apps/v1beta2

# 对象类型
kind: Deployment

# 对象元数据，唯一的识别对象，字段包括name、UID、namespace
metadata:
  # 名字通常由客户端提供，每一个对象类型内部，名字不得重复。在资源URL中名字用于引用对象，例如/api/v1/pods/some-name
  # 名字应该仅仅包含小写字母、数字、-、.这几类字符
  name: nginx-deployment
  # UID由K8S自动生成，全局唯一
  # namespace 指定对象所属的名字空间

# 对象规格
spec:
  replicas: 3
  # 标签选择器
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
          # 端口名称，可以被Service引用
          name: web
          protocol: TCP

# 标签
labels: ...</pre>
<p>基于上述规格创建对象的命令示例：</p>
<pre class="crayon-plain-tag">kubectl create -f https://blog.gmem.cc/nginx-deployment.yaml --record</pre>
<div class="blog_h2"><span class="graybg">名字空间</span></div>
<p>K8S支持在一个物理集群上，创建多个虚拟集群，这些虚拟集群称为名字空间。名字空间为资源名称限定了作用域，同一名字空间内部名字不能重复，但是跨越名字空间则不受限制。</p>
<p>可以考虑使用名字空间的场景：</p>
<ol>
<li>当跨越多个团队/项目的人员共享一套集群时，可以考虑使用名字空间机制。如果使用集群的人员仅仅在数十个级别，不需要使用名字空间</li>
<li>希望利用K8S的资源配额机制，在名字空间之间划分集群资源</li>
<li>在未来版本的K8S中，同一名字空间中的对象将具有一致的默认范围控制策略</li>
</ol>
<p>如果两个资源仅仅有些许的不同，例如应用的两个版本，则不需要利用名字空间隔离。考虑使用标签（Label）来在名字空间内部区分这些资源。</p>
<p>大部分K8S资源（Pod、Service、Replication Controller...）都位于名字空间中。但是名字空间本身（也是资源）则不位于任何名字空间中。事件（Event）则可能位于、也可能不位于名字空间中，取决于事件的源对象。</p>
<div class="blog_h3"><span class="graybg">查看名字空间</span></div>
<p>执行下面的命令可以查看集群中现有的名字空间：</p>
<pre class="crayon-plain-tag">kubectl get namespaces
# NAME          STATUS    AGE
# default       Active    1d
# kube-system   Active    1d
# kube-public   Active    1d </pre>
<p>上述命令输出了3个名字空间：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">名字空间</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>default</td>
<td>默认名字空间，没有显式指定名字空间的对象位于此空间</td>
</tr>
<tr>
<td>kube-system</td>
<td>由K8S系统创建的对象</td>
</tr>
<tr>
<td>kube-public</td>
<td>自动创建、可以被任何用户（包括未进行身份验证的）访问的名字空间。此名字空间基本上是保留供集群使用的，因为某些资源需要公开的、跨越整个集群的访问</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">指定名字空间</span></div>
<p>要在单个命令请求中，指定名字空间，可以：</p>
<pre class="crayon-plain-tag">kubectl --namespace=ns-dev run nginx --image=nginx
kubectl --namespace=ns-dev get pods</pre>
<p>要永久的为某个上下文切换默认名字空间，可以：</p>
<pre class="crayon-plain-tag">kubectl config set-context $(kubectl config current-context) --namespace=ns-pdt
kubectl config view | grep namespace   # 验证设置</pre>
<div class="blog_h3"><span class="graybg">名字空间和DNS </span></div>
<p>当你创建一个服务时，相应的DNS条目自动创建。此条目的默认格式是：<pre class="crayon-plain-tag">..svc.cluster.local</pre>。如果容器仅仅使用来引用服务，则解析到当前名字空间。这种特性可以方便的在多个名字空间（开发、测试、生产）中使用完全相同的配置信息。如果需要跨名字空间引用服务，则必须使用FQDN。</p>
<div class="blog_h2"><span class="graybg">标签和选择器</span></div>
<p>所谓标签（Label）就是用户自定义的、关联到对象的键值对，这些标签和K8S的核心系统没有意义。你可以使用标签来管理、选择对象的子集。</p>
<p>你可以在创建对象的时候设置标签，也可以后续添加、修改标签。对于单个对象来说，标签的键必须是唯一的。</p>
<p>K8S会对标签进行索引/反向索引，以便对标签进行高效的查询、监控，在UI、CLI中对输出进行分组、排序。</p>
<div class="blog_h3"><span class="graybg">动机</span></div>
<p>标签提供一种松散耦合的风格，让用户自由的映射他们的组织结构到K8S对象，而不需要客户端记住这些映射关系。</p>
<p>服务部署、批处理流水线，常常都是多维的实体（多分区/部署、多个发行条线、多层、每层多个微服务）。对这些实体进行管理，常需要横切性的操作——打破严格的层次性封装，这些层次可能由基础设施死板的规定。</p>
<p>样例标签集：</p>
<ol>
<li>区分发行条线：{ "release" : "stable", "release" : "canary" }</li>
<li>区分运行环境：{ "environment" : "dev", "environment" : "qa", "environment" : "production" }</li>
<li>区分架构层次：{ "tier" : "frontend", "tier" : "backend", "tier" : "cache" }</li>
<li>区分分区： { "partition" : "customerA", "partition" : "customerB" }</li>
<li>区分Track： { "track" : "daily", "track" : "weekly" }</li>
</ol>
<div class="blog_h3"><span class="graybg">标签格式</span></div>
<p>标签的键，可以由两段组成：可选的前缀和名称，它们用 / 分隔：</p>
<ol>
<li>名称部分必须63-字符，支持大小写字母、数字、下划线、短横线、点号</li>
<li>前缀如果存在，则必须是DNS子域名 —— 一系列以点号分隔的DNS标签，最后加上一个  / </li>
<li>如果前缀被省略，则暗示标签是用户私有的</li>
<li>前缀 kubernetes.io/ 为K8S核心组件保留</li>
</ol>
<div class="blog_h3"><span class="graybg">标签选择器</span></div>
<p>一系列对象常常具有相同的标签。利用标签选择器，客户端可以识别一组对象。标签选择器是K8S提供的核心分组原语。</p>
<p>目前K8S API提供两种标签选择器：</p>
<ol>
<li>equality-based。操作符=、==（和=同义）、!=。示例：environment = production,tier != frontend</li>
<li>set-based。操作符in、notin、exists。示例：environment in (production, qa),tier notin (frontend, backend)</li>
</ol>
<p>如果有多个选择器需要匹配，则使用逗号（作用类似于&amp;&amp;）分隔。</p>
<p>NULL选择器匹配空集，空白选择器则匹配集合中所有对象。</p>
<div class="blog_h3"><span class="graybg">标签相关API</span></div>
<p>LIST、WATCH之类的操作可以指定标签选择器，以过滤对象。</p>
<p>基于CLI的例子：</p>
<pre class="crayon-plain-tag">kubectl get pods -l environment=production,tier=frontend
kubectl get pods -l 'environment in (production),tier in (frontend)'
kubectl get pods -l 'environment,environment notin (frontend)'</pre>
<p>某些K8S对象（Service、复制控制器）使用标签选择器指定关联的对象（例如Pod）。例如，作为服务目标的Pods是通过标签选择器指定的。replicationcontroller需要管理的pod生产，也是通过标签指定的。对象配置片断示例：</p>
<pre class="crayon-plain-tag">selector:
    component: redis</pre>
<p>ReplicaSet、Deployment、DaemonSet、Job等对象，支持set-based风格的标签选择器，来指定其需求：</p>
<pre class="crayon-plain-tag">selector:
  matchLabels:
    component: redis
  matchExpressions:
    - {key: tier, operator: In, values: [cache]}
    - {key: environment, operator: NotIn, values: [dev]}</pre>
<div class="blog_h2"><span class="graybg">注解 </span></div>
<p>除了标签以外，你还可以使用注解（Annotations）来为K8S对象附加非识别性（non-identifying）元数据。客户端可以通过API获得这些元数据。</p>
<p>注意注解和标签不一样，后者可以用来识别、选择对象，前者则不能。此外注解的值大小没有限制。</p>
<p>注解也是键值对形式：</p>
<pre class="crayon-plain-tag">"annotations": {
  "key1" : "value1",
  "key2" : "value2"
}</pre>
<div class="blog_h1"><span class="graybg">节点</span></div>
<p>节点是K8S中的Worker机器，之前被叫做minion。节点可以是物理机器，也可以是VM。节点上运行着一些服务，运行Pod需要这些服务，这些服务被Master组件管理。运行在节点上的服务包括Docker、kubelet、kube-proxy等。</p>
<p>节点应该是x86_64（amd64）架构的Linux系统。其它架构或系统有可能支持K8S。</p>
<div class="blog_h2"><span class="graybg">节点状态</span></div>
<p>以下几类信息用于描述节点的状态：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段类别</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Addresses</td>
<td>
<p>这些字段的用法取决于你使用的云服务，或者裸金属的配置：</p>
<p>HostName：由节点的内核报告，可以由kubelet的--hostname-override参数覆盖<br />ExternalIP：可以被集群外部访问的节点IP<br />InternalIP：仅仅能在集群内部访问的节点IP</p>
</td>
</tr>
<tr>
<td>Condition</td>
<td>
<p>这些字段描述运行中节点的状态信息：</p>
<p>OutOfDisk：如果为True则意味着节点上没有足够空间供新Pod使用<br />Ready：如果为True则意味着节点状态健康，准备好接受Pods。如果为False则意味着节点不监控并且不接受Pods。如果为Unknown则意味着节点控制器已经40s没有听到节点的心跳了<br />MemoryPressure：如果为True则节点内存方面存在压力<br />DiskPressure：如果True则节点磁盘方面存在压力，也就是说磁盘空间不足<br />NetworkUnavailable：如果为True则意味着节点的网络配置不正确</p>
<p>节点的状态以JSON形式表示，例如：</p>
<pre class="crayon-plain-tag">"conditions": [
  {
    "kind": "Ready",
    "status": "True"
  }
]</pre>
<p>如果节点的Ready状态为False/Unknown，并且持续时间大于pod-eviction-timeout（默认5分钟），则kube-controller-manager会接收到一个argument，该节点上所有Pods将被节点控制器调度以删除 </p>
<p>某些情况下节点不可达，APIServer无法与其上运行的kubelet通信。这样Pods删除信息无法传递到被分区（partitioned，网络被隔离的）节点，因而知道网络通信恢复前，其上的Pods会继续运行</p>
<p>在1.5-版本中，节点控制器会强制的从APIServer中删除不可达Pods，1.5+版本后只有在确认这些Pods已经从集群中停止运行后才进行删除。这些运行在不可达节点上的Pod的状态可能为Terminating或者Unknown</p>
<p>某些情况下，K8S无法在基础设施层推断节点是否永久的离开了集群，管理员可能需要手工进行节点移除</p>
<p>移除节点将导致其上的所有Pod对象从APIServer上删除</p>
<p>&nbsp;</p>
<p>从1.8开始K8S引入了一个Alpha特性，可以自动创建代表Condition的taints。要启用此特性，为APIServer、控制器管理器、调度器提供参数：</p>
<p style="padding-left: 60px;"><em>--feature-gates=...,TaintNodesByCondition=true</em></p>
<p>当TaintNodesByCondition启用后，调度器会忽略节点的Conditions，代之以查看节点的taints、Pod的toleration。你可以选择旧有的调度模型，或者使用新的更加灵活的调度模型：</p>
<ol>
<li>没有tolerations（容忍）的Pod基于旧模型调度</li>
<li>能够容忍特定节点taints（污染）的Pod，则可以在污染节点上调度</li>
</ol>
</td>
</tr>
<tr>
<td>Capacity</td>
<td>描述节点上可用的资源，包括CPU、内存、最多可以在其上调度的Pod数量 </td>
</tr>
<tr>
<td>Info</td>
<td>一般性信息，例如内核版本、K8S版本（kubelet/kube-proxy）、Docker版本</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">节点管理</span></div>
<p>与Node/Service不同，节点不是K8S创建的，它常常由外部云服务（例如Google Compute Engine）创建，或者存在于你的物理机/虚拟机池子中。K8S中创建一个节点，仅仅是创建代表此节点的对象。在创建节点对象后，K8S会验证其有效性，例如下面的节点配置信息：</p>
<pre class="crayon-plain-tag">{
  "kind": "Node",
  "apiVersion": "v1",
  "metadata": {
    "name": "10.240.79.157",
    "labels": {
      "name": "my-first-k8s-node"
    }
  }
}</pre>
<p>执行创建后，K8S会基于metadata.name字段来检查节点的健康状态。如果节点是有效的（所有必须的服务在其上运行）则它有资格运行Pod，否则它会被任何集群活动排除在外，知道它变为有效状态。 </p>
<p>除非显式删除，K8S会一致维护你创建的节点对象，并且周期性的检查其健康状态。</p>
<p>目前和节点接口交互的组件有三个：节点控制器、Kubelet、Kubectl。</p>
<div class="blog_h2"><span class="graybg">节点控制器</span></div>
<p>节点控制器属于K8S管理组件，可以管理节点的方方面面：</p>
<ol>
<li>当节点注册时，给节点分配一个CIDR块（如果CIDR分配打开）</li>
<li>在内部维护一个最新的节点列表。在云环境下，节点控制器会调用云提供商的接口，判断不健康节点的VM是否仍然可用，如果答案是否则则节点控制器从子集的节点列表中删除不健康节点</li>
<li>监控节点的健康状态。当节点不可达（节点由于某种原因不再响应心跳，例如宕机，默认40s）时，更新NodeStatus的NodeReady状态为ConditionUnknown。如果节点持续处于不可达状态（默认5m），则优雅的清除（Terminate）节点上所有Pods。控制器每隔--node-monitor-period秒检查节点状态</li>
<li>从1.6开始，节点控制器还负责清除运行在具有NoExecute taints节点上的Pods（如果Pod不容忍NoExecutes）</li>
</ol>
<p>从1.4开始，K8S更新了节点控制器的逻辑，当Master节点本身网络出现问题导致大量节点不可达的场景被优化处理。在决定清除一个Pods时，节点控制器会查看集群中所有节点的状态。</p>
<p>大部分情况下，节点控制器限制了节点清除的速度。参数--node-eviction-rate默认为0.1，也就是每10秒最多从单个节点清除一个Pod。</p>
<p>当给定可用性区域（availability zone，集群可能跨越云服务的多个可用性区域，例如北美、亚太）中节点变得不健康时，节点清除行为会有改变。 节点控制器会计算可用性区域中不健康（NodeReady=ConditionUnknown or ConditionFalse）节点的百分比：</p>
<ol>
<li>如果此比值不小于--unhealthy-zone-threshold（默认0.55）则清除速率降低。降低到--secondary-node-eviction-rate（默认0.01）</li>
<li>如果集群规模较小（小于--large-cluster-size-threshold，默认50）则清除行为停止</li>
</ol>
<p>之所以按可用性区域来决定上述行为，是因为某些区域可能和Master之间形成网络分区，另外一些这和Master之间保持连通。</p>
<p>跨越可用性区域分布节点的关键原因是，当整个区域不可用时，工作负载可以迁移到健康的区域中。</p>
<div class="blog_h2"><span class="graybg">节点自动注册</span></div>
<p>当为Kubelet提供参数--register-node=true（默认）时，Kubelet会尝试自动的到API服务器上注册自己。自动注册相关的参数：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--kubeconfig</td>
<td>用于包含自己的身份验证信息的文件路径</td>
</tr>
<tr>
<td>--cloud-provider</td>
<td>指定云服务提供商信息，用于读取关于节点本身的元数据</td>
</tr>
<tr>
<td>--register-node</td>
<td>自动注册自己</td>
</tr>
<tr>
<td>--register-with-taints</td>
<td>使用指定的taints注册自己，多个taints用逗号分隔：=:,=:</td>
</tr>
<tr>
<td>--node-ip</td>
<td>节点的IP地址</td>
</tr>
<tr>
<td>--node-labels</td>
<td>注册时附加到节点对象的标签集</td>
</tr>
<tr>
<td>--node-status-update-frequency</td>
<td>节点向Master提交自身状态的间隔</td>
</tr>
</tbody>
</table>
<p>当前，Kubelet有权创建/修改任何节点资源，但是通常它仅应该修改自己的。</p>
<div class="blog_h2"><span class="graybg">手工节点管理</span></div>
<p>作为集群管理员，你可以随时创建、修改节点对象。你可以设置kubelet标记--register-node=false，仅仅允许手工的管理。</p>
<p>你可以管理节点的资源，例如设置标签、标记其为unschedulable：</p>
<pre class="crayon-plain-tag"># 设置节点为unschedulable
kubectl cordon $NODENAME</pre>
<p>标记节点为unschedulable可以禁止新的Pods被分配到节点上，但是对节点上现有的Pod则没有影响。</p>
<p>注意DaemonSet控制器创建的Pod跳过了K8S的调度器，因而unschedulable标记为其无意义。</p>
<div class="blog_h2"><span class="graybg">节点容量</span></div>
<p>节点的容量（Capacity，CPU数量、内存量）属于节点对象属性的一部分。通常节点自我注册时会提供容量信息。如果你手工的注册节点，则必须提供容量信息。</p>
<p>K8S调度器会确保节点拥有足够的资源来运行分配给它的所有Pods，它会检查请求在节点上启动的容器（仅限kubelet启动的）所需总资源不大于节点的容量。</p>
<p>你可以显式的为非Pod进程保留节点资源：</p>
<pre class="crayon-plain-tag"># 作为占位符的Pod
apiVersion: v1
kind: Pod
metadata:
  name: resource-reserver
spec:
  containers:
  - name: sleep-forever
    image: gcr.io/google_containers/pause:0.8.0
    resources:
      # 请求保留的资源数量
      requests:
        cpu: 100m
        memory: 100Mi</pre>
<div class="blog_h2"><span class="graybg">节点通信</span></div>
<p>本节讨论Master（准确的说是APIServer）和K8S集群之间的通信。 </p>
<div class="blog_h3"><span class="graybg">集群 → Master</span></div>
<p>集群到Master的通信路径，总是在APIServer处终结，因为其它的Master组件均不暴露远程接口。</p>
<p>在典型配置上，API服务器基于HTTPS协议监听443端口，并且启用1-N种客户端验证、授权机制。</p>
<p>节点应该被预先分配集群的根证书，以便能够使用有效的客户端证书链接到APIServer。</p>
<p>利用服务账户（Service Account），Pod可以和APIServer安全的通信，根据服务账户的配置，K8S会自动把根证书、不记名令牌（bearer token ）注入到Pod中。所有名字空间中的K8S服务都配备了一个虚拟IP，通过kube-proxy重定向到APIServer上的HTTPS端点。</p>
<p>Master组件和APIServer基于非安全端口通信，此端口通常仅仅暴露在Master机器的localhost接口上。</p>
<div class="blog_h3"><span class="graybg">Master → 集群</span></div>
<p>从Master（APIServer）到集群的通信路径主要包括两条：</p>
<ol>
<li>从APIServer到运行在各节点上的kubelet进程。这些通信路径用于：
<ol>
<li>抓取Pod的日志</li>
<li>Attach到（利用kubectl）到运行中的Pod</li>
<li>提供Kubelet的端口转发功能</li>
</ol>
</li>
<li>从APIServer到任何节点/Pod/Service，基于APIServer的代理功能</li>
</ol>
<p>对于第1类通信路径，APIServer默认不会校验kubelet的服务器端证书，因而存在中间人攻击的可能性。使用--kubelet-certificate-authority标记可以为APIServer提供一个根证书，用于验证kubelet的服务器证书。</p>
<p>对于第2类通信路径，默认使用HTTP连接，不验证身份或加密。</p>
<div class="blog_h1"><span class="graybg">Master组件</span></div>
<p>这类组件构成了K8S集群的控制平面（Plane），负责集群的全局性管理，例如调度、检测/响应集群事件。</p>
<p>Master组件可以运行在集群的任何节点上，你可以创建多Master的集群以获得高可用性。</p>
<p>Master组件主要包括kube-apiserver、kube-controller-manager、kube-scheduler</p>
<div class="blog_h2"><span class="graybg">kube-apiserver</span></div>
<p>暴露K8S的API，作为控制平面的前端。此服务器本身支持水平扩容。</p>
<div class="blog_h2"><span class="graybg">etcd</span></div>
<p>一个分布式的键值存储，K8S集群的信息全部存放在其中。要注意做好etcd的备份。</p>
<div class="blog_h2"><span class="graybg">kube-controller-manager</span></div>
<p>运行控制器。控制器是一系列执行集群常规任务的后台线程，控制器包括：</p>
<ol>
<li>节点控制器：检测节点宕机并予以响应</li>
<li>复制控制器：为系统中每个复制控制器对象维护正确数量的Pod</li>
<li>端点控制器：产生端点对象</li>
<li>服务账号/令牌控制器：为新的名字空间创建默认账户、API访问令牌</li>
</ol>
<div class="blog_h2"><span class="graybg">cloud-controller-manager</span></div>
<p>运行和云服务提供商交互的控制器，1.6引入的试验特性。CCM和其它Master组件在一起运行，它也可以作为Addon启动（运行在K8S的上层）。</p>
<p>CCM的设计初衷是，让云服务商相关的代码和K8S核心解耦，相互独立演进。不使用CCM时的K8S集群架构如下：</p>
<p><img class="aligncenter  wp-image-17295" src="https://blog.gmem.cc/wp-content/uploads/2016/06/pre-ccm-arch.png" alt="pre-ccm-arch" width="787" height="322" /></p>
<p>上述架构中，云服务和Kubelet、APIServer、KCM进行交互，实现集成，交互复杂，不符合最少知识原则。</p>
<p>使用CCM后，K8S集群架构变为：</p>
<p><img class="aligncenter size-large wp-image-17297" src="https://blog.gmem.cc/wp-content/uploads/2016/06/post-ccm-arch-1024x443.png" alt="post-ccm-arch" width="710" height="307" /></p>
<p>新的架构中，CCM作为一个Facade，统一负责和云服务的交互。CCM分离了部分KCM的功能，在独立进程中运行它们，分离的原因是这些功能是依赖于具体云服务的：节点控制器、 卷控制器、路由控制器、服务控制器。在1.8版本中CCM运行前述控制器中的：</p>
<ol>
<li>节点控制器：通过从云服务获取运行在集群中的节点信息，来初始化节点对象。添加云服务特定的Zone/Regin标签、云服务特定的节点实例细节，获取节点网络地址和主机名，在节点不可用时调用云服务确认节点是否被删除（如果是则级联删除节点对象）</li>
<li>路由控制器：在云服务中配置路由，确保不同节点中运行的容器能够相互通信，仅用于GCE集群</li>
<li>服务控制器：监听服务的创建/更新/删除事件。它会根据K8S中当前服务的状态来配置云服务的负载均衡器</li>
</ol>
<p>CCM还运行一个PersistentVolumeLabels控制器，用于在PersistentVolumes（GCP/AWS提供）上设置Zone/Regin标签。卷控制器没有作为CCM的一部分是刻意的设计，主要原因是K8S已经在剥离云服务相关的卷逻辑上做了很多工作。</p>
<p>CCM还包含了云服务特定的Kubelet功能。在引入CCM之前，Kubelet负责利用云服务特定的信息（例如IP、Regin/Zone标签、节点实例类型）初始化节点，引入CCM之后这些职责被转移。在新架构中，Kubelet初始化节点时不知晓云服务特定的信息，但是它会给节点添加一个taint，从而将节点标记为不可调度的。直到CCM初始化了云服务特定的信息，taint才被移除，节点可以被调度。</p>
<p>CCM基于Go开发，暴露了一系列的接口（CloudProvider），允许任何云服务实现这些接口</p>
<div class="blog_h3"><span class="graybg">CloudProvider</span></div>
<p>此接口定义在pkg/cloudprovider/cloud.go，包含的功能有：</p>
<ol>
<li>管理第三层（TCP）负载均衡器</li>
<li>管理节点实例（云服务提供的）</li>
<li>管理网络路由</li>
</ol>
<p>不是所有功能都需要实现，取决于K8S组件的标记如何设置。运行K8S也不一定需要此接口的实现，比如在裸金属上运行时。</p>
<div class="blog_h2"><span class="graybg">kube-scheduler</span></div>
<p>监控新创建的、没有分配到Node的Pod，然后选择适当的Node供其运行。</p>
<div class="blog_h2"><span class="graybg">addons</span></div>
<p>加载项是实现了集群特性的Pod和服务，这些Pod可以被Deployments、ReplicationControllers等管理。限定了名字空间的加载项在名字空间kube-system中创建。加载项管理器创建、维护加载项资源。常见加载项如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">加载项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>DNS</td>
<td>
<p>所有集群都需要此加载项，以提供集群DNS服务。此加载项为K8S服务提供DNS记录</p>
<p>K8S启动的容器自动使用此DNS服务</p>
</td>
</tr>
<tr>
<td>Dashboard</td>
<td>此加载项是一个一般用途的Web客户端，用户可以基于此加载项来管理集群、管理或者针对K8S集群中运行的应用程序</td>
</tr>
<tr>
<td>CRM</td>
<td>容器资源监控（Container Resource Monitoring）在中心数据库中记录容器的度量信息，并提供查看这些信息的UI</td>
</tr>
<tr>
<td>CIL</td>
<td>集群级别日志（Cluster-level Logging）收集容器日志，集中保存，并提供搜索/查询接口</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">节点组件</span></div>
<p>这些组件可以运行在集群中的任何节点上，它们维护运行中的Pods、提供K8S运行时环境。主要包括kubelet、kube-proxy。</p>
<div class="blog_h2"><span class="graybg">kubelet</span></div>
<p>主要的节点代理程序，监控分配到（通过APIServer或本地配置文件）当前节点的Pod，并且：</p>
<ol>
<li>挂载Pod所需的卷</li>
<li>下载Pod的Secrets</li>
<li>通过Docker或rkt运行Pod的容器</li>
<li>周期性执行任何请求的容器存活探针</li>
<li>将Pod状态反馈到系统的其他部分</li>
<li>将节点状态反馈到系统的其它部分</li>
</ol>
<div class="blog_h2"><span class="graybg">kube-proxy</span></div>
<p>在每个节点上映射K8S的网络服务的代理。提供在宿主机上维护网络规则、执行连接转发，来实现K8S服务抽象。</p>
<p>最早的版本完全在用户空间实现，性能较差。从1.1开始，K8S实现了基于Iptable代理模式的kube-proxy，从1.8开始，添加基于IPVS实现的kube-proxy。</p>
<div class="blog_h2"><span class="graybg">docker/rkt</span></div>
<p>用于运行容器。</p>
<div class="blog_h2"><span class="graybg">supervisord</span></div>
<p>轻量级的监控系统，用于确保kubelet、docker持续运行（宕机重启之）。</p>
<div class="blog_h2"><span class="graybg">fluentd</span></div>
<p>用于配合实现集群级别日志（CIL）。</p>
<div class="blog_h1"><span class="graybg">容器</span></div>
<p>在Pod中引用Docker镜像之前，你必须将其Push到Registry中。</p>
<p>container对象的image属性可以包含Registry前缀和Tag，这个命名规则和Docker是一致的。</p>
<div class="blog_h2"><span class="graybg">镜像更新</span></div>
<p>默认的镜像拉取策略是IfNotPresent，如果镜像已经存在于本地，则Kubelet不会重复抓取。如果希望总是取抓取镜像，可以使用以下三种方法之一：</p>
<ol>
<li>设置imagePullPolicy=Always</li>
<li>使用镜像的:latest标签</li>
<li>启用AlwaysPullImages这一admission controller</li>
</ol>
<p>当不指定镜像tag时，默认即使用:latest，因而会导致每次都抓取最新镜像。:latest是应该尽可能避免使用的。</p>
<div class="blog_h2"><span class="graybg">使用私服</span></div>
<p>Docker将访问私服所需要的密钥信息存放在$HOME/.dockercfg或者$HOME/.docker/config.json文件中。如果在Kubelet的root的$home目录下存在这两个文件K8S会使用之。</p>
<p>可以使用如下Pod能验证私服能否正常访问：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: private-image-test-1
spec:
  containers:
    - name: uses-private-image
      image: $PRIVATE_IMAGE_NAME
      imagePullPolicy: Always
      command: [ "echo", "SUCCESS" ]

# 创建Pod对象
kubectl create -f /tmp/private-image-test-1.yaml
# 查看运行结果， 期望输出SUCCESS
kubectl logs private-image-test-1</pre>
<p>注意：</p>
<ol>
<li>所有节点必须具有相同的.docker/config.json，否则可能出现Pod仅能在部分节点上运行的情况。例如，如果你使用节点自动扩容，则每个实例模板需要包含.docker/config.json文件或者挂载包含此文件的卷</li>
<li>只要私服的访问密钥被添加到config.json中，则任何Pod都有权访问这些私服中的镜像</li>
</ol>
<div class="blog_h2"><span class="graybg">容器环境</span></div>
<p>K8S的容器环境，为容器提供了很多重要的资源：</p>
<ol>
<li>一个文件系统，由镜像 + 1-N个卷构成</li>
<li>关于容器本身的信息：
<ol>
<li>容器的hostname就是容器所在的Pod的name，此名称可以通过hostname命令/gethostname函数获得</li>
<li>容器所属的Pod name和namespace还可以通过<a href="https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/">downward API</a>获得</li>
<li>用户在Pod定义中声明的环境变量，对于容器都是可用的</li>
</ol>
</li>
<li>关于集群中其它对象的信息：
<ol>
<li>容器创建时所有服务的列表，可以通过环境变量得到。例如名为foo的服务对应环境变量如下：<br />
<pre class="crayon-plain-tag"># 在容器中可以访问环境变量：
FOO_SERVICE_HOST=
FOO_SERVICE_PORT= </pre>
</li>
<li>服务拥有专享的IP地址，容器可以基于DNS名称访问它（如果DNS Addon启用的话）</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">生命周期钩子</span></div>
<p>利用容器生命周期钩子框架，你可以在Kubelet管理的容器启动、关闭时执行特定的代码。可用的钩子（事件）包括：PostStart、PreStop。</p>
<div class="blog_h2"><span class="graybg">容器配置参数</span></div>
<div class="blog_h3"><span class="graybg">command/args</span></div>
<p>如果：</p>
<ol>
<li>既不提供command和args，则使用Docker镜像中定义的默认值</li>
<li>如果仅提供了command，则Docker镜像中的CMD/ENTRYPOINT被忽略</li>
<li>如果仅提供了args，则使用Docker镜像中的ENTRYPOINT + args</li>
<li>如果同时提供了command和args，则Docker镜像中的CMD/ENTRYPOINT被忽略 </li>
</ol>
<div class="blog_h1"><span class="graybg">Pod</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>Pod（本义：豆荚，箱子）是K8S对象模型中，最小的部署、复制、扩容单元 —— K8S中单个应用程序实例。</p>
<p>Pod封装了以下内容：</p>
<ol>
<li>应用程序容器</li>
<li>存储资源</li>
<li>唯一的网络IP</li>
<li>管理容器运行方式的选项</li>
</ol>
<p>Pod最常用的容器运行时是Docker，尽管其它容器运行时也被支持。</p>
<p>Pod的两种使用方式：</p>
<ol>
<li>运行单个容器：容器和Pod呈现一对一关系，这是最常见的用法。你可以将Pod看作是容器的简单包装器，K8S管理Pod而不直接管理容器</li>
<li>运行多个容器：Pod封装了由多个较小的、紧耦合的、共享资源的容器相互协作而组成的应用，以及和这些容器相关的存储资源</li>
</ol>
<p>Pod本身只是一套环境，因此容器可以重启，Pod则没有这一概念。</p>
<p>每个Pod对应一个应用程序实例，如果你需要水平扩容，则需要使用多个Pod。这种水平扩容在K8S中一般叫做复制（Replication）。复制的Pod由控制器创建、管理。</p>
<p>如果Pod包含多个容器，则这些容器一般被调度、运行在单个节点上。这些容器可以共享资源、依赖，相互通信，并且协调如何关闭（例如谁先关闭）。容器之间共享的资源主要有：</p>
<ol>
<li>网络：每个Pod具有唯一性的IP地址，Pod中的每个容器共享网络名字空间，包括IP地址和端口。Pod内的容器之间可以通过localhost + 端口相互通信。当与外部实体通信时，这些容器必须协调如何共享网络资源（例如端口）</li>
<li>存储：Pod可以指定一系列的共享存储卷，所有容器可以访问这些卷。如果Pod中部分容器重启，这些卷仍然保持可用</li>
<li>容器之间也可以基于IPC机制进行通信，例如SystemV信号量、POSIX共享内存</li>
</ol>
<p>在基于Docker时，Pod中的容器是共享名字空间、共享卷的Docker容器。</p>
<p>Pod可以和其它物理机器、其它Pod进行网络通信。</p>
<div class="blog_h2"><span class="graybg">Pod创建过程</span></div>
<ol>
<li>用户向API Server发送创建Pod的请求</li>
<li>K8S Scheduler选取一个节点，将Pod分配到节点上</li>
<li>节点上的Kubelet负责Pod的创建：
<ol>
<li>调用CNI实现（dockershim、containerd等）创建Pod内的容器</li>
<li>第一个创建的容器是<span style="background-color: #c0c0c0;">pause</span>，它会允许一个简单的程序，进行永久的阻塞。该容器的<span style="background-color: #c0c0c0;">作用是维持命名空间</span>，因为Linux的命名空间需要其中至少包含一个进程才能存活</li>
<li>其他容器会加入到pause容器所在的命名空间</li>
<li>初始化容器网络接口，由kubenet（即将废弃）获CNI插件负责。CNI会在pause容器内创建eth0接口并分配IP地址</li>
<li>容器和主机的网络协议栈，通过veth pair连接</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">pause容器</span></div>
<p>在Pod中，pause容器是所有其他容器的“父”容器，它：</p>
<ol>
<li>是各容器共享的命名空间的基础</li>
<li>可以启用PID命名空间共享，为每个Pod提供init进程</li>
</ol>
<p>pause容器的逻辑非常简单，它启动后就通过pause()系统调用暂停自己。当子进程变为孤儿进程（父进程提前出错退出）时，它会调用wait()防止僵尸进程的出现。</p>
<p>在1.8版本之前，默认启用PID命名空间共享，也就是说Pod的所有容器共享一个PID命名空间，之后的版本则默认禁用共享，可以配置<pre class="crayon-plain-tag">spec.shareProcessNamespace</pre>强制启用。不共享PID命名空间时，每个容器都有PID 1进程。</p>
<div class="blog_h2"><span class="graybg">使用Pod</span></div>
<p>你很少需要直接创建单个Pod，甚至是单例（不复制）的Pod，这是因为Pod被设计为是短命的、可丢弃的实体。当Pod被（你直接、或控制器）创建时，它被调度到集群中的某个节点。直到进程退出，Pod会一直存在于节点上。如果缺少资源、节点失败，则Pod会被清除、Pod对象也被从APIServer中删除。</p>
<p>Pod本身没有自愈能力，如果Pod被调度到一个失败的节点，或者调度操作本身失败，则Pod会被删除。类似的，当资源不足或者节点维护时，Pod也会被删除。K8S使用控制器这一高层抽象来管理Pod实例。通常你都是通过控制器来间接使用Pod。</p>
<div class="blog_h2"><span class="graybg">控制器</span></div>
<p>控制器能够创建、管理多个Pod，处理复制/回滚，并提供自愈（在集群级别）功能 —— 例如当节点失败时控制器会在其它节点上创建等价的Pod。一般来说，控制器使用你提供的Pod模板来创建Pod。</p>
<p>控制器主要有三类：</p>
<ol>
<li>Job：用于控制那些期望会终结的Pod，在批处理计算场景下用到。Job必须和restartPolicy为OnFailure/Never的Pod联用</li>
<li>ReplicationController, ReplicaSet,Deployment：用于控制不期望终结的Pod，例如Web服务。这些控制器必须和restartPolicy=Always联用</li>
<li>DaemonSet：用于某个机器上仅运行一个实例的Pod，用于提供机器特定的系统服务</li>
</ol>
<p>这三类控制器都包含了对应的Pod模板。</p>
<div class="blog_h2"><span class="graybg">Pod模板</span></div>
<p>Pod模板是包含在其它对象（复制控制器、Jobs、DaemonSets...）中的Pod的规格说明。控制器使用Pod模板来创建实际的Pod，模板示例如下：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp-container
    image: busybox
    command: ['sh', '-c', 'echo Hello Kubernetes! &amp;&amp; sleep 3600']</pre>
<p>Pod模板发生改变不会影响已经创建的Pod。 </p>
<div class="blog_h2"><span class="graybg">Pod的终结</span></div>
<p>当用户请求删除Pod时，K8S首先发送TERM信号给每个容器的主进程，当超过时限（完整宽限期，Full Grace Period）后，则发送KILL信号，之后删除Pod对象。</p>
<p>如果在等待容器进程退出期间Kubelet、Docker守护进程重启，则重启后重新进行完整宽限期内的TERM，以及必要的KILL。</p>
<p>强制删除：这种删除操作会导致Pod对象立即从APIServer中移除，这样它的名字可以立即被重用。实际的Pod会在较短的宽限期内被清除。</p>
<div class="blog_h2"><span class="graybg">Pod容器的特权模式</span></div>
<p>在容器规格的SecurityContext字段中指定privileged标记，则容器进入特权模式。如果容器需要操作网络栈、访问设备，则需要特权模式。</p>
<div class="blog_h2"><span class="graybg">Pod生命周期</span></div>
<p>Pod的status字段是一个PodStatus对象，后者具有一个phase字段。该字段是Pod所处生命周期阶段的概要说明：</p>
<ol>
<li>Pending：Pod已经被K8S系统接受，单是一或多个容器镜像尚未创建。此时Pod尚未调度到节点上，或者镜像尚未下载完毕</li>
<li>Running：Pod已经被调度到某个节点，且所有容器已被创建，至少有一个容器处于运行中/重启中/启动中</li>
<li>Succeeded：Pod的所有容器被正常终结，且不会再重启</li>
<li>Failed：Pod的所有容器被终结，且至少一个被异常终结——要么退出码非0要么被强杀</li>
<li>Unknown：Pod状态未知，通常由于和目标节点的通信中断导致</li>
</ol>
<p>PodStatus具有一个 PodCondition数组，数组的每个元素具有type、status字段。type可选值是PodScheduled、Ready、Initialized、Unschedulable，status字段的可选值是True、False、Unknown。</p>
<p>通常情况下，重复你或者控制器销毁Pod，它不会小时。唯一的例外是，phase值为Succeeded/Failed，并持续一定时间（具体由Master确定），则Pod会因为过期而自动销毁。</p>
<p>如果节点和集群断开，则K8S会把该节点上所有的Pod的phase更新为Failed</p>
<div class="blog_h2"><span class="graybg">容器探针</span></div>
<p>Probe由Kubelet周期性的针对容器调用，以执行健康检查。Kubelet会调用容器实现的Handler，Handler包括三类：</p>
<ol>
<li>ExecAction：在容器内部执行特定的命令，如果命令为0则意味着诊断成功</li>
<li>TCPSocketAction：针对容器的IP/端口进行TCP检查，如果端口打开则诊断成功</li>
<li>HTTPGetAction：针对容器的IP/端口进行HTTP检查，如果响应码为[200,400)之间则诊断成功</li>
</ol>
<p>每个探针的诊断结果可以是Success、Failure、Unknown</p>
<p>Kubelet可以在运行中的容器上执行两个可选探针，并作出反应：</p>
<ol>
<li>livenessProbe：探测容器是否还在运行。如果<span style="background-color: #c0c0c0;">探测失败则杀死容器</span>，并根据其重启策略决定如何反应。如果容器没有提供此探针，则总是返回Success</li>
<li>readinessProbe：探测容器是否能提供服务。如果探测失败，端点控制器会<span style="background-color: #c0c0c0;">从所有匹配Pod的服务的端点中移除Pod的IP地址</span>。在Initial delay之前默认值Failure，如果容器没有提供此探针，则Initial delay之后默认Success。<span style="background-color: #c0c0c0;">仅仅用于在Pod启动初期</span>、延迟的把它加入到服务集群中，不能用于将Pod从服务集群中移除</li>
</ol>
<p>注意，这些探针在Kubelet的网络名字空间中运行。</p>
<p>如果你的容器在出现问题时会自己崩溃，则不需要使用探针，只需要设置好restartPolicy即可。</p>
<div class="blog_h3"><span class="graybg">livenessProbe</span></div>
<p>基于HTTP的探针示例：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-http
spec:
  containers:
  - args:
    - /server
    image: gcr.io/google_containers/liveness
    # 指定探针，在容器级别上指定
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080  # 也可以使用命名端口（ContainerPort）
        httpHeaders:
        - name: X-Custom-Header
          value: Awesome
      # 第一次探针延迟多久执行
      initialDelaySeconds: 15
      # 每隔多久执行一次探针
      periodSeconds: 5
      # 探针执行超时，默认1秒
      timeoutSeconds: 1
      # 被判定为失败后，连续多少次探测没问题，才被重新认为是成功
      successThreshold: 1
      # 连续多少次探测失败，则认为无法恢复，自动重启
      failureThreshold: 3
    name: liveness</pre>
<p>基于命令的探针示例：</p>
<pre class="crayon-plain-tag">spec:
  containers:
  - name: runtime
    livenessProbe:
      exec:
        command:
        - cat
        - /app/liveness
      initialDelaySeconds: 5
      periodSeconds: 5</pre>
<p>要注意：命令的标准输出被收集，并在kubectl describe pod命令中显示为Unhealthy事件的原因，因此<span style="background-color: #c0c0c0;">探针的标准输出应该简洁明了</span>。</p>
<div class="blog_h3"><span class="graybg">readinessProbe</span></div>
<p>某些情况下，容器临时的不适合处理请求，例如其正在启动、正在加载大量数据。此时可以使用readiness探针。readiness探测失败的Pod不会接收到K8S Service转发来的请求。</p>
<p>readinessProbe的配置和livenessProbe没有区别。两个探针可以被同时执行。</p>
<div class="blog_h2"><span class="graybg">重启策略</span></div>
<p>PodSpec包含一个restartPolicy字段，可选值为：</p>
<ol>
<li>Always，默认值，应用到Pod的所有容器</li>
<li>OnFailure</li>
<li>Never</li>
</ol>
<p>重启时如果再次失败，重启延迟呈指数增长（10s，20s，40s）但是最大5分钟。启动成功后，10m后清除重启延迟。</p>
<div class="blog_h2"><span class="graybg">初始化容器</span></div>
<p>在PodSpec的containers字段之后，你可以声明一个初始化容器。这类容器在应用容器之前运行，可能包含应用镜像中没有的实用工具、安装脚本。</p>
<p>初始化容器可以有多个，K8S会按照声明顺序逐个执行它们，<span style="background-color: #c0c0c0;">只有前一个初始化容器成功完成，后面的初始化容器才会被调用</span>。如果某个初始化容器运行失败，K8S会反复重启它，直到成功，除非你设置其restartPolicy=Never。</p>
<div class="blog_h3"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  # 普通容器
  containers:
  - name: myapp-container
    image: busybox
    command: ['sh', '-c', 'echo The app is running! &amp;&amp; sleep 3600']
  # 初始化容器
  initContainers:
  - name: init-myservice
    image: busybox
    command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
  - name: init-mydb
    image: busybox
    command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 2; done;'] </pre>
<div class="blog_h2"><span class="graybg">Pod预设</span></div>
<p>PodPreset用于在创建Pod时，向Pod注入额外的运行时需求信息。你可以使用标签选择器来指定预设要应用到的Pod。当Pod创建请求出现时，系统中发生以下事件：</p>
<ol>
<li>取得所有可用的PodPreset</li>
<li>检查是否存在某个PodPreset，其标签选择器匹配准备创建的Pod</li>
<li>尝试合并PodPreset中的各种资源到准备创建的Pod中
<ol>
<li>如果出错，触发一个事件说明合并出错。在不注入任何资源的情况下创建Pod</li>
<li>如果成功，标注被修改的PodSpec为已被PodPreset修改 </li>
</ol>
</li>
</ol>
<p>要在集群中使用Pod预设功能，你需要：</p>
<ol>
<li>确保API类型settings.k8s.io/v1alpha1/podpreset启用。可以设置APIServer的--runtime-config选项，包含settings.k8s.io/v1alpha1=true</li>
<li>确保PodPreset这一Admission controller已经启用。可以设置APIServer的--admission-control选项，包含PodPreset</li>
<li>在你需要使用的名字空间中，定义一个PodPreset对象</li>
</ol>
<p>如果你在使用Kubeadm，则修改配置文件/etc/kubernetes/manifests/kube-apiserver.yaml即可。</p>
<div class="blog_h3"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag">apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
  name: allow-database
spec:
  selector:
    matchLabels:
      role: frontend
  # 支持的Pod配置项是有限的，仅仅支持：
  # 预设容器的环境变量
  env:
    - name: DB_PORT
      value: "6379"
    - name: expansion
      value: $(REPLACE_ME)
  # 预设容器的环境变量，从ConfigMap读取变量
  envFrom:
    - configMapRef:
        name: etcd-env-config
  # 预设容器的卷挂载
  volumeMounts:
    - mountPath: /cache
      name: cache-volume
    - mountPath: /etc/app/config.json
      readOnly: true
      name: secret-volume
  # 预设Pod的卷定义
  volumes:
    - name: cache-volume
      emptyDir: {}
    - name: secret-volume
      secret:
         secretName: config-details</pre>
<div class="blog_h2"><span class="graybg">高可用</span></div>
<p>要构建基于K8S的高可用应用，就要明白Pod什么时候会被中断（Disrpution）。</p>
<p>Pod不会凭空消失，除非你或控制器销毁了它们，或者出现不可避免的软硬件错误。这些不可避免的错误被称为非自愿中断（Involuntary Disruptions），例如：</p>
<ol>
<li>作为节点支撑的物理机器，出现硬件错误</li>
<li>集群管理员错误的删除了VM</li>
<li>云服务或者Hypervisor的错误导致VM消失</li>
<li>内核错误</li>
<li>网络分区导致节点从集群分离</li>
<li>节点资源不足导致Pod被清除</li>
</ol>
<p>其它情况则称为自愿中断，中断操作可能由应用程序所有者、集群管理员触发。应用程序所有者的操作包括：</p>
<ol>
<li>删除部署或者其它管理Pod的控制器</li>
<li>更新部署的Pod模板导致重启</li>
<li>直接意外的删除了Pod</li>
</ol>
<p>集群管理员的操作包括：</p>
<ol>
<li>Drain了某个节点，进行硬件维护或软件升级</li>
<li>Drain了某个节点，进行缩容</li>
<li>从节点移除Pod，流下资源供其他人使用</li>
</ol>
<p>缓和非自愿中断造成的影响，手段包括：</p>
<ol>
<li>确保Pod请求了其需要的资源</li>
<li>如果需要HA，启用（无状态/有状态）应用程序复制</li>
<li>如果需要进一步HA，启用跨机柜复制（anti-affinity）甚至跨区域复制（multi-zone cluster）</li>
</ol>
<p>自愿中断发生的频率根据集群用法的不同，有很大差异。对于某些基本的集群，根本不会出现自愿中断。</p>
<p>集群管理员/云服务提供商可能运行额外的服务，进而导致自愿中断。例如，滚动进行节点的软件更新就可能导致这种中断。某些节点自动扩容的实现，可能进行节点的取碎片化，从而导致自愿中断。</p>
<p>K8S提供了中断预算（Disruption Budgets）机制，帮助在频繁的自愿中断的情况下，实现HA。</p>
<div class="blog_h2"><span class="graybg">优先级和抢占</span></div>
<p>这是从1.8引入的特性，目前处于Alpha状态。该特性允许Pod具有优先级，并且能够在无法调度Pod时，从节点驱除低优先级的Pod。</p>
<p>从1.9开始，优先级影响Pod的调度顺序、资源不足时Pod的驱逐顺序。高优先级的Pod更早被调度，低优先级的Pod更早被驱除。</p>
<div class="blog_h3"><span class="graybg">启用特性</span></div>
<p>你需要为APIServer、scheduler、kubelet启用下面的特性开关：</p>
<pre class="crayon-plain-tag">--feature-gates=PodPriority=true,...</pre>
<p>同时在APIServer中启用scheduling.k8s.io/v1alpha1这个API以及Priority这个准许控制器：</p>
<pre class="crayon-plain-tag">--runtime-config=scheduling.k8s.io/v1alpha1=true --admission-control=Controller-Foo,Controller-Bar,...,Priority</pre>
<div class="blog_h3"><span class="graybg">PriorityClass</span></div>
<p>这是一类非名字空间化的对象，定义了一个优先级类的名称，以及一个整数的优先级值。最大取值为10亿，更大的值保留。规格示例：</p>
<pre class="crayon-plain-tag">apiVersion: scheduling.k8s.io/v1alpha1
kind: PriorityClass
metadata:
  # 名
  name: high-priority
# 值
value: 1000000
# 是否全局默认值，如果是，则没有定义PriorityClassName属性的Pod，其优先级均为此PriorityClass.value
globalDefault: false
# 供用户阅读的描述
description: "..."</pre>
<div class="blog_h3"><span class="graybg">抢占</span></div>
<p>当Pod被创建后，它会列队等待调度。调度器会选取队列中的一个Pod并尝试调度到某个节点上，如果没有节点能满足Pod的需求，则抢占逻辑被激活：</p>
<ol>
<li>尝试寻找这样的节点：其上具有更低优先级的Pod，并且将这些Pod驱除后，能满足正被调度的Pod的需求</li>
<li>如果找到匹配的节点，则对其上的低优先级Pod执行驱除，并调度当前Pod到节点</li>
</ol>
<div class="blog_h2"><span class="graybg">共享卷</span></div>
<p>同一个Pod内的多个容器，可以通过共享卷（Shared Volume）进行交互。</p>
<p>卷天然是Pod内共享的，因为<span style="background-color: #c0c0c0;">卷只能在Pod级别定义，而后任何一个容器都可以挂载它到自己的某个路径下</span>：</p>
<pre class="crayon-plain-tag">kind: Pod
spec:
  volumes:
  - name: shared-data
    # Pod被调度到某个节点上后，创建一个空目录
    emptyDir: {}
  containers:
  - name: nginx-container
    image: nginx
    volumeMounts:
    - name: shared-data
      mountPath: /usr/share/nginx/html

  - name: debian-container
    image: debian
    volumeMounts:
    - name: shared-data
      mountPath: /pod-data
    command: ["/bin/sh"]
    # 修改共享卷的内容
    args: ["-c", "echo Hello from the debian container &gt; /pod-data/index.html"] </pre>
<p>注意，K8S的共享卷和Docker的--volumes-from不同。</p>
<div class="blog_h2"><span class="graybg">容器组合模式</span></div>
<div class="blog_h3"><span class="graybg">Sidecar</span></div>
<p>跨斗，本指三轮摩托旁边的那个座位。</p>
<p>Sidecar用于辅助主要容器，让其更好的工作。例如Pod中的主容器是Nginx，它提供HTTP服务，Sidecar中运行一个Git，周期性的将最新的代码拉取过来，通过共享文件系统推送给Nginx。</p>
<div class="blog_h3"><span class="graybg">Ambassador</span></div>
<p>使者模式，辅助容器作为一个代理服务器，主容器直接通过localhost（因为Pod内所有容器共享网络名字空间）访问外部的服务。</p>
<p>好处是，主容器可以和开发环境完全一致，因为在开发时常常所有东西都在localhost上。</p>
<div class="blog_h3"><span class="graybg">Adaptor</span></div>
<p>不同容器输出的监控指标信息格式不一致，可以由一个配套的辅助容器对这些格式进行适配。</p>
<div class="blog_h2"><span class="graybg"><a id="label-mgr"></a>标签管理</span></div>
<p>建议统一规划、配置标签：</p>
<ol>
<li>tier，标识应用所属的层次，取值：
<ol>
<li>control-plane：K8S控制平面</li>
<li>infrastructure：提供网络、存储等基础设施</li>
<li>devops：开发、运维相关的工具</li>
<li>middleware：中间件、数据库等</li>
<li>application：业务域应用程序</li>
</ol>
</li>
<li>app，标识应用程序的类别</li>
<li>comp，标识应用程序组件</li>
<li>version，标识应用程序的版本</li>
<li>release，标识Helm Release的名称，或者手工部署的应用程序的实例名</li>
</ol>
<div class="blog_h2"><span class="graybg">hostNetwork</span></div>
<p>将此配置项设置为true，则Pod直接使用宿主机的网络。Pod直接在宿主机的网络接口上监听。</p>
<div class="blog_h2"><span class="graybg">Pod配置</span></div>
<div class="blog_h3"><span class="graybg">环境变量</span></div>
<p>将Pod的Spec/Status字段注入为环境变量：</p>
<pre class="crayon-plain-tag">env:
  - name: NODE_NAME
    valueFrom:
      fieldRef:
        fieldPath: spec.nodeName
  - name: POD_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.name
  - name: POD_NAMESPACE
    valueFrom:
      fieldRef:
        fieldPath: metadata.namespace
  - name: POD_IP
    valueFrom:
      fieldRef:
        fieldPath: status.podIP
  - name: POD_SERVICE_ACCOUNT
    valueFrom:
      fieldRef:
        fieldPath: spec.serviceAccountName</pre>
<div class="blog_h1"><span class="graybg">分配Pod到节点</span></div>
<p>你可以指定Pod和Node的对应关系，让Pod只能（或优选）在某些节点上运行。具体实现方式有几种，都是基于标签选择器的。</p>
<p>大部分情况下你不需要强制指定对应关系，因为K8S会进行合理的调度。你需要这种细致的控制机制的场景包括：</p>
<ol>
<li>确保Pod在具有SSD的机器上运行</li>
<li>让两个位于不同Service中的、频繁协作的Pod，能在同一个Zone内部运行</li>
</ol>
<div class="blog_h2"><span class="graybg">nodeSelector</span></div>
<p>这是Pod规格的一个字段，规定有资格运行Pod的节点，所具有的标签集。</p>
<p>你需要首先为节点添加标签：</p>
<pre class="crayon-plain-tag"># 获取集群的节点列表
kubectl get nodes
# 为某个节点添加标签
kubectl label nodes  disktype=ssd

# 显示节点标签
kubectl get nodes --show-labels</pre>
<p>然后，需要为Pod规格添加一个nodeSelector字段： </p>
<pre class="crayon-plain-tag">spec:
  nodeSelector:
    disktype: ssd</pre>
<p>执行命令：<pre class="crayon-plain-tag">kubectl get pods -o wide</pre>可以看到Pod被分配到的节点。 </p>
<div class="blog_h3"><span class="graybg">内置节点标签</span></div>
<p>截止1.4，K8S内置的节点标签有：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">节点标签</td>
</tr>
</thead>
<tbody>
<tr>
<td>kubernetes.io/hostname</td>
</tr>
<tr>
<td>failure-domain.beta.kubernetes.io/zone</td>
</tr>
<tr>
<td>failure-domain.beta.kubernetes.io/region</td>
</tr>
<tr>
<td>beta.kubernetes.io/instance-type</td>
</tr>
<tr>
<td>beta.kubernetes.io/os</td>
</tr>
<tr>
<td>beta.kubernetes.io/arch</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Affinity</span></div>
<p>Beta特性affinity，也可以用于将Pod分配到节点。但是这一特性更加强大：</p>
<ol>
<li>表达能力更强，不是简单的限制于nodeSelector的那种  k == v &amp;&amp; k == v</li>
<li>可以标注为“软规则”而非强制要求，如果调度器无法满足Pod的需求，Pod仍然会被调度</li>
<li>可以基于其它Pod上的标签进行约束，也就是说，可以让一系列的Pod同地协作（ co-located）</li>
</ol>
<p>Affinity分为两种类型：</p>
<ol>
<li>Node affinity：类似于nodeSelector，但是表达能力更强、可以启用软规则（对应上面的第1、2条）</li>
<li>Inter-pod affinity：基于Pod标签而非Node标签进行约束（对应上面第3条）</li>
</ol>
<div class="blog_h3"><span class="graybg">Node affinity</span></div>
<p>包含两种子类型：requiredDuringSchedulingIgnoredDuringExecution（硬限制）、preferredDuringSchedulingIgnoredDuringExecution（软限制）。</p>
<p>“IgnoredDuringExecution“的含义是，如果在Pod调度到Node并运行后，Node的标签发生改变，则Pod会继续运行，nodeSelector的行为也是这样的。未来可能支持后缀“RequiredDuringExecution“，也就说当运行是Node的标签发生改变，导致不满足规则后，Pod会被驱除。</p>
<p>示例：</p>
<pre class="crayon-plain-tag">spec:
  affinity:
    nodeAffinity:
      # 硬限制
      requiredDuringSchedulingIgnoredDuringExecution:
        # 加强版的节点选择器，值为数组。如果数组元素有多个，目标节点只需要匹配其中一个即可
        nodeSelectorTerms:
        # 每个节点选择器可以包含多个表达式，如果表达式有多个，目标节点必须匹配全部表达式
        - matchExpressions:
          # 必须在AZ（Availability Zone）e2e-az1或者e2e-az2中运行
          - key: kubernetes.io/e2e-az-name
            # 支持的操作符包括 In, NotIn, Exists, DoesNotExist, Gt, Lt
            # 其中 NotIn,DoesNotExist 用于实现anti-affinity
            operator: In
            values:
            - e2e-az1
            - e2e-az2
      # 软限制
      preferredDuringSchedulingIgnoredDuringExecution:
      # 权重，值越大越需要优先满足
      - weight: 1
        preference:
          # 节点选择器
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value</pre>
<div class="blog_h3"><span class="graybg">Inter-pod affinity</span></div>
<p>基于已经运行在节点上的Pod，而不是节点本身的标签进行匹配 —— 当前Pod应该/不应该运行在，已经运行了匹配规则R的Pod(s)的X上。其中：</p>
<ol>
<li>R表现为关联了一组名字空间的标签选择器</li>
<li>X是一个拓扑域（Topology Domain），可以是Node、Rack、Zone、Region等等</li>
</ol>
<p>和节点不同，Pod是限定名字空间的，因此它的标签也是有名字空间的。针对Pod的标签选择器必须声明其针对的名字空间。</p>
<p>Inter-pod affinity要求较大的计算量，因此可能拖累调度性能，不建议在大型集群（K+节点）上使用。</p>
<p>示例：</p>
<pre class="crayon-plain-tag">spec:
  affinity:
    # Pod亲和性
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      # 如果Zone（由topologyKey指定）上已经运行了具有标签security=S1的Pod，则当前Pod也必须调度到该Zone
      - labelSelector:
          matchExpressions:
          - key: security
            # 支持的操作符In, NotIn, Exists, DoesNotExist
            operator: In
            values:
            - S1
        topologyKey: failure-domain.beta.kubernetes.io/zone
    # Pod反亲和性
    podAntiAffinity:
      # 强制
      requiredDuringSchedulingIgnoredDuringExecution:
      # 尽可能
      preferredDuringSchedulingIgnoredDuringExecution:
      # 如果Host（节点）上已经运行了具有标签security=S2的Pod，则当前Pod不得调度到该Host
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: kubernetes.io/hostname</pre>
<p>当需要保证高级对象（ReplicaSets, Statefulsets, Deployments）在同一拓扑域内运行时， podAffinity非常有用。</p>
<p>下面是一个示例，3实例的Redis集群 + 3实例的Nginx前端，每个Redis不在同一节点运行，每个Nginx也不在同一节点运行，每个Nginx必须在本地有个Redis：</p>
<pre class="crayon-plain-tag">kind: Deployment
metadata:
  name: redis-cache
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: store
    # Pod模板
    spec:
      affinity:
        # 当前节点上不得存在Pod的标签是app=store，也就是说，复制集的每个实例都占据单独的节点
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: redis-server
        image: redis:3.2-alpine</pre><br />
<pre class="crayon-plain-tag">kind: Deployment
metadata:
  name: web-server
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: web-store
    spec:
      affinity:
        podAntiAffinity:
          # 当前节点上不得存在Pod具有标签app=web-store
          # 也就是说当前复制集的实例，都不会在同一节点上运行
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web-store
            topologyKey: "kubernetes.io/hostname"
        podAffinity:
          # 当前节点上的某个Pod必须具有app=store标签
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: web-app
        image: nginx:1.12-alpine</pre>
<div class="blog_h2"><span class="graybg">Taints/Tolerations </span></div>
<p>前文讨论的Node affinity，可以把Node和Pod吸引到一起。Taints则做相反的事情，让Pod拒绝某些Node。</p>
<div class="blog_h3"><span class="graybg">添加Taint</span></div>
<p>使用kubectl可以为节点添加一个Taint，例如：</p>
<pre class="crayon-plain-tag"># taint的键为key，值为value
# taint effect为 NoSchedule，意味着Pod不能被调度到该节点，除非它具有匹配的toleration
kubectl taint nodes node1 key=value:NoSchedule

# 恢复Master节点的禁止调度
kubectl taint nodes master node-role.kubernetes.io/master=:NoSchedule</pre>
<div class="blog_h3"><span class="graybg">删除Taint</span></div>
<pre class="crayon-plain-tag"># 删除Taint
kubectl taint nodes --all node-role.kubernetes.io/master-</pre>
<div class="blog_h3"><span class="graybg">容忍</span></div>
<p>下面的Pod都能容忍上述Taint：</p>
<pre class="crayon-plain-tag">tolerations:
# 容忍 键、值、效果组合
- key: "node-role.kubernetes.io/master"
  # 操作符，不指定默认为Equal
  operator: "Equal"
  value: "value"
  effect: "NoSchedule"

# 容忍 键、效果组合
tolerations:
- key: "node-role.kubernetes.io/master"
  operator: "Exists"
  effect: "NoSchedule"

# 容忍一切
tolerations:
- operator: "Exists"

# 容忍键和一切效果
tolerations:
- key: "node-role.kubernetes.io/master"
  operator: "Exists"

# 容忍某种效果
tolerations:
- effect: NoSchedule
  operator: Exists</pre>
<div class="blog_h3"><span class="graybg">effect </span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>NoSchedule</td>
<td>不能容忍Taint的Pod绝不会调度到节点</td>
</tr>
<tr>
<td>PreferNoSchedule</td>
<td>不能容忍Taint的Pod尽量不被调度到节点</td>
</tr>
<tr>
<td>NoExecute</td>
<td>
<p>如果Pod不能容忍此Effect且正在节点上运行，它会被从节点上被驱除：</p>
<pre class="crayon-plain-tag"># Pod配置
tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExecute"
  # 如果所在节点添加了key1=value1:NoExecute，则1小时后Pod被驱除
  # 如果1小时内Taint被移除，则Pod不会被驱除
  tolerationSeconds: 3600</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">应用场景</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">场景</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>专用节点</td>
<td>
<p>如果希望某些节点被特定Pod集专用，你可以为节点添加Taint、并为目标Pod添加匹配的Toleration。这样，目标Pod可以在专用节点上运行
<p>如果要让目标Pod仅能在专用节点，可以配合nodeSelector</p>
</td>
</tr>
<tr>
<td>特殊硬件节点</td>
<td>集群中可能存在一小部分具有特殊硬件（例如GPU）的节点，Taint可以让不需要特殊硬件的Pod和这些节点隔离</td>
</tr>
<tr>
<td>定制化Pod驱除</td>
<td>可以在节点出现问题时，为每个Pod定制驱除策略，这是一个Alpha特性 </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">基于Taint的驱除</span></div>
<p>使用NoExecute可以导致已经运行在节点上的Pod被驱除，Pod配置tolerationSeconds可以指定驱除的Buffer时间。</p>
<p>从1.6开始，节点控制器会在节点状态变化时，自动添加Taint，包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 45%; text-align: center;">Taint</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>node.kubernetes.io/not-ready</td>
<td>节点没有准备好，对应NodeCondition.Ready=False</td>
</tr>
<tr>
<td>node.alpha.kubernetes.io/unreachable</td>
<td>节点控制器无法连接到节点，对应NodeCondition.Ready=Unknown</td>
</tr>
<tr>
<td>node.kubernetes.io/out-of-disk</td>
<td>节点磁盘空间不足</td>
</tr>
<tr>
<td>node.kubernetes.io/memory-pressure</td>
<td>节点面临内存压力</td>
</tr>
<tr>
<td>node.kubernetes.io/disk-pressure</td>
<td>节点面临磁盘压力</td>
</tr>
<tr>
<td>node.kubernetes.io/network-unavailable</td>
<td>节点的网络不可用</td>
</tr>
<tr>
<td>node.cloudprovider.kubernetes.io/uninitialized</td>
<td>当Kubelet通过“外部”云服务启动时，它会设置此Taint，当CCM初始化了节点后，移除此Taint</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">PDB</span></div>
<p>PDB（PodDisruptionBudget）可以用来构建高可用的应用程序。</p>
<div class="blog_h2"><span class="graybg">自愿/非自愿中断</span></div>
<p>除非被人工或控制器删除，或者出现不可避免的软硬件错误，Pod不会消失。那些不可避免软硬件错误导致的Pod删除，称为非自愿中断（involuntary disruptions ），具体包括：</p>
<ol>
<li>硬件故障</li>
<li>虚拟机故障，例如被误删除</li>
<li>内核崩溃</li>
<li>集群网络分区导致节点丢失</li>
<li>由于资源不足，Pod被kubelet驱除</li>
</ol>
<p>要避免非自愿中断影响应用程序的可用性，可以考虑：</p>
<ol>
<li>合理的配置资源请求</li>
<li>使用Deployment/StatefulSet</li>
</ol>
<p>要避免资源中断影响应用程序的可用性，可以使用PDB。</p>
<div class="blog_h2"><span class="graybg">PDB</span></div>
<p>PDB可以限制复制集中，同时<span style="background-color: #c0c0c0;">由于自愿中断而宕掉</span>的Pod的最大数量。示例：</p>
<pre class="crayon-plain-tag">apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: zookeeper
spec:
  # 最小保证2实例处于运行中，可以指定百分比，例如 25%
  minAvailable: 2
  # 也可以指定最大宕机数量，可以指定百分比，例如 25%
  maxUnavailable:1
  # 控制的目标应用程序
  selector:
    matchLabels:
      app: zookeeper</pre>
<div class="blog_h1"><span class="graybg">ConfigMap</span></div>
<p>ConfigMap用于把配置信息和容器的镜像解耦，保证容器化应用程序的可移植性。</p>
<p>ConfigMap中文叫配置字典，下一章的Secrets中文叫保密字典，两者很类似，只是后者的内容是编码存储的。</p>
<div class="blog_h2"><span class="graybg">创建ConfigMap</span></div>
<p>命令格式：</p>
<pre class="crayon-plain-tag"># map-name是ConfigMap的名称
# data-source 目录、文件，或者硬编码的字面值
kubectl create configmap  
# 在ConfigMap对象中，data-source表现为键值对：
# key：文件名，或者通过命令行提供的key
# value：文件内容，或者通过命令行提供的字面值</pre>
<div class="blog_h3"><span class="graybg">--from-file</span></div>
<p>下面是把目录作为数据源的例子：</p>
<pre class="crayon-plain-tag">kubectl create configmap game-config --from-file=path-to-dir
# ls path-to-dir
# game.properties
# ui.properties

kubectl describe configmaps game-config
# Name:           game-config
# Namespace:      default
# Labels:         
# Annotations:    

# Data
# ====
# game.properties:        158 bytes
# ui.properties:          83 bytes</pre>
<p>目录中文件的内容，会读取到ConfigMap的data段中：</p>
<pre class="crayon-plain-tag">apiVersion: v1
data:
  # 文件名为键，文件内容为值（字符串）
  # 如果要自定义键，可以 --from-file==
  game.properties: |
    enemies=aliens
    lives=3
    enemies.cheat=true
    enemies.cheat.level=noGoodRotten
    secret.code.passphrase=UUDDLRLRBABAS
    secret.code.allowed=true
    secret.code.lives=30
  ui.properties: |
    color.good=purple
    color.bad=yellow
    allow.textmode=true
    how.nice.to.look=fairlyNice
kind: ConfigMap
metadata:
  creationTimestamp: 2016-02-18T18:52:05Z
  name: game-config
  namespace: default
  resourceVersion: "516"
  selfLink: /api/v1/namespaces/default/configmaps/game-config
  uid: b4952dc3-d670-11e5-8cd0-68f728db1985</pre>
<p>如果将文件作为数据源，可以指定--from-file多次，引用多个文件。</p>
<div class="blog_h3"><span class="graybg">--from-env-file</span></div>
<p>使用该选项可以从Env-File创建ConfigMap，例如文件：</p>
<pre class="crayon-plain-tag">enemies=aliens
lives=3
allowed="true"</pre>
<p>可以创建ConfigMap：</p>
<pre class="crayon-plain-tag">kubectl create configmap game-config-env-file --from-env-file=game-env-file.properties</pre>
<p>结果ConfigMap为：</p>
<pre class="crayon-plain-tag">apiVersion: v1
data:
  # 注意是一个个键值对，而不是字符串
  allowed: '"true"'
  enemies: aliens
  lives: "3"
kind: ConfigMap</pre>
<div class="blog_h3"><span class="graybg">--from-literal</span></div>
<p>使用该选项，直接通过命令行给出ConfigMap的数据：</p>
<pre class="crayon-plain-tag">kubectl create configmap special-config 
    --from-literal=special.how=very --from-literal=special.type=charm</pre>
<div class="blog_h2"><span class="graybg">使用ConfigMap</span></div>
<div class="blog_h3"><span class="graybg">定义环境变量</span></div>
<p>定义单个环境变量：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: test
spec:
  containers:
    - name: test
      env:
        # 定义一个环境变量
        - name: SPECIAL_LEVEL_KEY
          valueFrom:
            configMapKeyRef:
              # 环境变量的值，从名为special-config的ConfigMap中获取，使用其中的special.how的值
              name: special-config
              key: special.how</pre>
<p>你可以把ConfigMap中的所有键值对导出为Pod的环境变量：</p>
<pre class="crayon-plain-tag">spec:
  containers:
    - name: test-container
      envFrom:
      - configMapRef:
          name: special-config</pre>
<div class="blog_h3"><span class="graybg">在Pod命令中使用环境变量</span></div>
<p>使用特殊语法<pre class="crayon-plain-tag">$(VAR_NAME)</pre>，例如：?</p>
<pre class="crayon-plain-tag">command: [ "/bin/sh", "-c", "echo $(SPECIAL_LEVEL_KEY) $(SPECIAL_TYPE_KEY)" ]</pre>
<div class="blog_h3"><span class="graybg">作为卷使用</span></div>
<pre class="crayon-plain-tag">spec:
  containers:
    - name: test-container
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: special-config</pre>
<p>这样，Pod启动后，/etc/config目录下会出现若干文件。这些文件的名字是configMap的键，内容是键对应的值。</p>
<div class="blog_h3"><span class="graybg">映射到卷的特定路径</span></div>
<p>你可以使用path属性指定某个ConfigMap键映射到的文件路径：</p>
<pre class="crayon-plain-tag">spec:
  containers:
    - volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        name: special-config
        items:
          # ConfigMap中条目的key
        - key: special.level
          # 映射到mountPath下的什么文件，默认为key
          path: special.lv</pre>
<p>special.level的内容会映射到容器路径：/etc/config/special.lv</p>
<div class="blog_h2"><span class="graybg">自动刷新</span></div>
<p>加载为卷的ConfigMap被修改后，K8S会自动的监测到，并更新Pod的卷的内容。<span style="background-color: #c0c0c0;">延迟取决于Kubelet的同步周期</span>。</p>
<p>加载为环境变量的ConfigMap修改后不会刷新到已经运行的Pod。</p>
<div class="blog_h2"><span class="graybg"><a id="configmap-subpath"></a>SubPath</span></div>
<p>挂载ConfigMap时，如果ConfigMap中包含多个配置文件，可以指定每个文件映射到容器的什么路径：</p>
<pre class="crayon-plain-tag">volumeMounts:
    # 卷名，必须是ConfigMap或者Secret卷
  - name: dangconf
    # 挂载到容器路径
    mountPath: /etc/dangdangconfig/digital/api.config
    # ConfigMap或Secret中的文件名
    subPath: api-config.properties</pre>
<p>SubPath挂载的内容不会随ConfigMap变更，这可能是Kubernetes的Bug。</p>
<div class="blog_h1"><span class="graybg">Secret</span></div>
<p>Secret是一种K8S对象，用于保存敏感信息，例如密码、OAuth令牌、SSH私钥。将这些信息存放在Secret（而不是Pod规格、Docker镜像）中更加安全、灵活。 </p>
<p>你可以在Pod的规格配置中引用Secret。</p>
<div class="blog_h2"><span class="graybg">内置Secret </span></div>
<p>K8S会自动生成一些Secret，其中包含访问API所需的凭证，并自动修改Pod以使用这些Secret。</p>
<div class="blog_h2"><span class="graybg">创建Secret</span></div>
<p>你可以通过kubectl来创建Secret：</p>
<pre class="crayon-plain-tag"># 创建一个名为db-user-pass的一般性Secret
kubectl create secret generic db-user-pass --from-file=./username.txt --from-file=./password.txt

# 创建用作Ingress TLS证书的Secret
kubectl create secret tls tls-secret --cert=/home/alex/Documents/puTTY/k8s.gmem.cc/tls.crt --key=/home/alex/Documents/puTTY/k8s.gmem.cc/tls.key

# 直接提供字面值
kubectl -n kube-system create secret generic chartmuseum --from-literal=BASIC_AUTH_USER=alex --from-literal=BASIC_AUTH_PASS=alex

kubectl get secrets
# NAME                  TYPE                                  DATA      AGE
# db-user-pass          Opaque                                2         51s

kubectl describe secrets/db-user-pass
# ...
# Type:            Opaque

# Data
# ====
# password.txt:    12 bytes
# username.txt:    5 bytes</pre>
<p>你也可以手工的指定Secret规格：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  # 每个数据必须Base64处理
  # echo -n "admin" | base64
  username: YWRtaW4=
  password: MWYyZDFlMmU2N2Rm</pre>
<div class="blog_h2"><span class="graybg">解码Secret</span></div>
<p>使用如下命令可以获得Secret的所有属性，并输出为YML格式： </p>
<pre class="crayon-plain-tag">kubectl get secret mysecret -o yaml</pre>
<p>然后你可以复制Base格式的数据并解码：</p>
<pre class="crayon-plain-tag">echo "MWYyZDFlMmU2N2Rm" | base64 --decode</pre>
<div class="blog_h2"><span class="graybg">使用Secret</span></div>
<div class="blog_h3"><span class="graybg">挂载为文件</span></div>
<p>Secret可以作为数据卷挂载，示例：</p>
<pre class="crayon-plain-tag">spec:
  containers:
  - name: mypod
    image: redis
    volumeMounts:
    # 把foo卷只读挂载到文件系统路径下
    - name: foo
      mountPath: "/etc/foo"
      readOnly: true
  volumes:
  # 定义一个名为foo的卷，其内容为mysecret这个Secret
  - name: foo
    secret:
      secretName: mysecret</pre>
<p>默认的，Secret中的每个Data的key，都作为mountPath下的一个文件名。你可以定制Data到容器文件系统的映射关系：</p>
<pre class="crayon-plain-tag">spec:
  containers:
  - name: mypod
    image: redis
    volumeMounts:
    - name: foo
      mountPath: "/etc/foo"
      readOnly: true
  volumes:
  - name: foo
    secret:
      secretName: mysecret
      items:
      - key: username
        # 在容器中，访问路径为/etc/foo/my-group/my-username
        path: my-group/my-username
        # 文件模式为0400，这里必须转为十进制
        defaultMode: 256</pre>
<p>在容器中读取Data映射文件时，获得的是明文。 </p>
<div class="blog_h3"><span class="graybg">映射环境变量</span></div>
<p>Secret还可以映射为容器的环境变量：</p>
<pre class="crayon-plain-tag">spec:
  containers:
  - name: mycontainer
    image: redis
    env:
      - name: SECRET_USERNAME
        valueFrom:
          secretKeyRef:
            name: mysecret
            key: username</pre>
<div class="blog_h1"><span class="graybg">ServiceAccount</span></div>
<p>当你通过kubectl访问集群时，APIServer基于特定的用户账号（默认admin）对你进行身份验证。</p>
<p>服务账号（ServiceAccount）为运行在Pod中的进程提供身份（Identity）信息，这样当这些进程联系APIServer时，也能够通过身份验证。对服务账号的授权由<a href="https://kubernetes.io/docs/admin/authorization/#a-quick-note-on-service-accounts">授权插件和策略</a>负责。</p>
<div class="blog_h2"><span class="graybg">自动化</span></div>
<p>三个组件进行协作，完成和SA相关的自动化：</p>
<ol>
<li>服务账户准入控制器（Service account admission controller），是API Server的一部分，当Pod创建或更新时：
<ol>
<li>如果该 pod 没有 ServiceAccount 设置，将其 ServiceAccount 设为 default</li>
<li>如果设置的ServiceAccount不存在则拒绝Pod</li>
<li>如果Pod不包含ImagePullSecrets设置，则将SA中的ImagePullSecrets拷贝进来</li>
<li>将一个用于访问API Server的Token作为卷挂载到Pod</li>
<li>将/var/run/secrets/kubernetes.io/serviceaccount下的VolumeSource添加到每个容器</li>
</ol>
</li>
<li>Token 控制器（Token controller），是controller-manager的一部分，异步方式来：
<ol>
<li>当SA创建后，创建对应的Secret，用于支持API 访问</li>
<li>当SA删除后，删除对应的Token Secret</li>
<li>当Token Secret删除后，在SA中去除对应的数组元素</li>
<li>当通过annotation引用了SA的ServiceAccountToken类型的Secret创建后，自动生成Token并更新Secret字段：<br />
<pre class="crayon-plain-tag">apiVersion: v1
kind: Secret
metadata:
  name: build-robot-secret
  annotations:
    # 提示此Secret为了哪个SA
    kubernetes.io/service-account.name: build-robot
type: kubernetes.io/service-account-token</pre>
</li>
</ol>
</li>
<li>服务账户控制器（Service account controller）
<ol>
<li>确保每个命名空间具有default帐户</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">Token管理器</span></div>
<p>你需要通过<pre class="crayon-plain-tag">--service-account-private-key-file</pre>传递一个私钥给controller-manager，用于对SA的Token进行签名。</p>
<p>你需要通过<pre class="crayon-plain-tag">--service-account-key-file</pre>传递一个公钥给kube-api-server，以便API Server对Token进行校验。</p>
<div class="blog_h3"><span class="graybg">Token卷影射</span></div>
<p>v1.12引入的Beta特性。要启用此特性，需要给API Server传递：</p>
<ol>
<li>--service-account-issuer：SA Token颁发者的标识符，颁发者会断言颁发的Token的iss claim中具有此参数的值，字符串或URI，例如kubernetes.default.svc</li>
<li>--service-account-signing-key-file：包含SA Token颁发者私钥的路径，需要打开特性TokenRequest</li>
<li>--service-account-api-audiences：API的标识符</li>
</ol>
<p>Kubelet能够把SA Token影射到Pod中，你需要提供必须的属性，包括audience、有效期：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx
    volumeMounts:
    - mountPath: /var/run/secrets/tokens
      name: vault-token
  serviceAccountName: build-robot
  volumes:
  - name: vault-token
    projected:
      sources:
      - serviceAccountToken:
          path: vault-token
          expirationSeconds: 7200
          audience: vault</pre>
<p>对于上述配置，Kubelet会代表Pod来请求Token，并存储到对应位置，并且<span style="background-color: #c0c0c0;">在80%有效期过去后，自动刷新Token</span>。应用程序需要自己检测Token已经刷新。</p>
<div class="blog_h2"><span class="graybg">默认账号</span></div>
<p>当你创建Pod时，如果显式提供服务账号，则K8S在相同名字空间内为Pod分配一个默认账号（名为default），并自动设置到spec.serviceAccountName字段。</p>
<p>你可以在Pod内部调用K8S API，基于自动挂载的服务账号凭证（令牌）。从1.6开始，令牌的自动挂载可以禁用：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
# 在服务账号上禁用
automountServiceAccountToken: false

---

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  serviceAccountName: default
  # 针对单个Pod禁用
  automountServiceAccountToken: false</pre>
<div class="blog_h2"><span class="graybg">使用多账号</span></div>
<p>每个名字空间都有一个名为default的默认服务账号，你可以自己创建新的账号。</p>
<p>执行下面的命令列出所有账号：</p>
<pre class="crayon-plain-tag">kubectl get serviceAccounts
# NAME      SECRETS    AGE
# default   1          1d</pre>
<p>ServiceAccount的规格很简单，上一节有示例。创建账号后，你可以利用授权插件来<a href="https://kubernetes.io/docs/admin/authorization/#a-quick-note-on-service-accounts">设置账号的权限</a>。 </p>
<p>要让Pod使用非默认账号，配置spec.serviceAccountName字段即可。</p>
<div class="blog_h2"><span class="graybg">手工创建令牌</span></div>
<p>如果禁用了服务账号的自动凭证生成功能，你需要手工的创建令牌。令牌是一个Secret对象：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Secret
metadata:
  name: build-robot-secret
  annotations:
    kubernetes.io/service-account.name: build-robot
type: kubernetes.io/service-account-token</pre>
<div class="blog_h2"><span class="graybg">ImagePullSecrets</span></div>
<p>ImagePullSecrets是用于访问镜像私服的Secret，假设你<a href="https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod">已经创建好</a>一个名为gmemregsecret的Secret， 则可以使用下面的命令，为服务账号添加ImagePullSecrets：</p>
<pre class="crayon-plain-tag">kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "gmemregsecret"}]}'</pre>
<div class="blog_h3"><span class="graybg">实例</span></div>
<p>创建docker.gmem.cc的Secret：</p>
<pre class="crayon-plain-tag"># 创建名为gmemregsecret类型为docker-registry的secret
kubectl create secret docker-registry gmemregsecret \
    --docker-server=docker.gmem.cc --docker-username=alex  \
    --docker-password=pswd --docker-email=k8s@gmem.cc</pre>
<p>查看刚刚创建的Secret：</p>
<pre class="crayon-plain-tag">kubectl get secret gmemregsecret --output=yaml

# apiVersion: v1
# data:
#   .dockerconfigjson: eyJhdXRocyI6eyJkb2NrZXIuZ21lbS5jYzo1MDAwIjp7InVzZXJuYW1lIjoiYWxleCIsInBhc3N3b3JkIjoibGF2ZW5kZXIiLCJlbWFpbCI6Ims4c0BnbWVtLmNjIiwiYXV0aCI6IllXeGxlRHBzWVhabGJtUmxjZz09In19fQ==
# kind: Secret
# metadata:
#   creationTimestamp: 2018-02-11T14:04:52Z
#   name: gmemregsecret
#   namespace: default
#   resourceVersion: "1023033"
#  selfLink: /api/v1/namespaces/default/secrets/gmemregsecret
#  uid: 87db6e2e-0f34-11e8-92db-deadbeef00a0
# type: kubernetes.io/dockerconfigjson</pre>
<p>其中data字段为BASE64编码的密码数据，可以查看一下它的明文：</p>
<pre class="crayon-plain-tag">echo eyJhdXRocyI6eyJkb2NrZXIuZ21lbS5jYzo1MDAwIjp7InVzZXJuYW1lIjoiYWxleCIsInBhc3N3b3JkIjoibGF2ZW5kZXIiLCJlbWFpbCI6Ims4c0BnbWVtLmNjIiwiYXV0aCI6IllXeGxlRHBzWVhabGJtUmxjZz09In19fQ== | base64 -d
# {"auths":{"docker.gmem.cc":{"username":"alex","password":"lavender","email":"k8s@gmem.cc","auth":"YWxleDpsYXZlbmRlcg=="}}}alex@Zircon:~$</pre>
<p>现在你可以创建使用此Secret来拉取镜像的Pod了：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: digitalsrv-1
  labels:
    tier: rpc
    app: digitalsrv
spec:
  containers:
  - name: digitalsrv-1
    image: docker.gmem.cc/digitalsrv:1.0
  imagePullSecrets:
  - name: gmemregsecret</pre>
<div class="blog_h3"><span class="graybg">为默认账号分配</span></div>
<p>执行<pre class="crayon-plain-tag">kubectl edit serviceaccounts default</pre>，修改为如下内容：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: 2018-02-12T07:53:30Z
  name: default
  namespace: default
  resourceVersion: "1843"
  selfLink: /api/v1/namespaces/default/serviceaccounts/default
  uid: d0e062a3-0fc9-11e8-b942-deadbeef00a0
secrets:
- name: default-token-j9zdx
imagePullSecrets:
- name: gmemregsecret</pre>
<p>或者，执行命令：</p>
<pre class="crayon-plain-tag">kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": "gmemregsecret"}]}'</pre>
<div class="blog_h1"><span class="graybg">控制器</span></div>
<div class="blog_h2"><span class="graybg">ReplicaSet</span></div>
<p>下一代复制控制器，与Replication Controller仅仅的不同是对选择器的支持， ReplicaSet支持指定set-based的选择器（来匹配Pod），后者仅支持equality-based的选择器。</p>
<p>尽管ReplicaSet可以被独立使用，但是实际上它主要通过Deployment间接使用，作为编排Pod创建、更新、删除的工具。使用Deployment时你无需关心其自动创建的ReplicaSet对象。</p>
<p>ReplicaSet用于确保，在任何时刻，指定数量的Pod实例同时在集群中运行。示例：</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: frontend
  labels:
    app: guestbook
    tier: frontend
spec:
  # 复制实例的数量，默认1
  replicas: 3
  # 选择器，用于匹配被控制的Pod
  # 只要匹配，即使Pod不是ReplicaSet自己创建的，也被管理 —— 这允许替换ReplicaSet而不影响已存在的Pod的控制
  selector:
    matchLabels:
      tier: frontend
    matchExpressions:
      - {key: tier, operator: In, values: [frontend]}
  # template是spec段唯一强制要求的元素，提供一个Pod模板
  # 此Pod模板的结构和Pod对象完全一样，只是没有apiVersion/kind字段
  template:
    metadata:
      # ReplicaSet中的Pod模板必须指定标签、以及适当的重启策略
      labels:
        app: guestbook
        tier: frontend
    spec:
      containers:
      - name: php-redis
        image: gcr.io/google_samples/gb-frontend:v3
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        env:
        - name: GET_HOSTS_FROM
          value: dns
          # 如果集群没有配备DNS Addon，则可以从环境变量获取service的主机名
          # value: env
        ports:
        - containerPort: 80</pre>
<div class="blog_h3"><span class="graybg">删除</span></div>
<p>要删除ReplicaSet及其Pod，可以使用<pre class="crayon-plain-tag">kubectl delete</pre>命令。Kubectl会将ReplicaSet缩容为0，并在删除ReplicaSet之前等待其所有Pod的删除，使用REST API或者Go客户端库时，你需要手工缩容、等待Pod删除、并删除ReplicaSet。</p>
<p>要仅删除ReplicaSet，可以使用<pre class="crayon-plain-tag">kubectl delete --cascade=false</pre>命令。 使用REST API/Go客户端库时，简单删除ReplicaSet对象即可。</p>
<p>在删除ReplicaSet之后，你可以创建.spec.selector字段与之一样的新的ReplicaSet，这个新的RS会管理原先的Pod。但是现有Pod不会匹配新RS的Pod 模板，你可以通过滚动更新（Rolling Update）实现现有Pod的更新。</p>
<div class="blog_h3"><span class="graybg">隔离Pod</span></div>
<p>要从RS中隔离一个Pod，可以修改Pod的标签。注意，此Pod会很快被代替，以满足ReplicaSet的复制数量要求。</p>
<div class="blog_h3"><span class="graybg">扩容</span></div>
<p>要对RS进行扩容/缩容，你仅仅需要更新.spec.replicas字段。</p>
<div class="blog_h3"><span class="graybg">配合HPA</span></div>
<p>RS可以作为Pod水平自动扩容器（Horizontal Pod Autoscaler）的目标。HPA的示例：</p>
<pre class="crayon-plain-tag">apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: frontend-scaler
spec:
  # 扩容控制的目标
  scaleTargetRef:
    kind: ReplicaSet
    name: frontend
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 50</pre>
<p>v2版本的HPA示例：</p>
<pre class="crayon-plain-tag">apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: podinfo
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: podinfo
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 50 </pre>
<p>针对上述配置文件调用kubectl create -f即可创建HPA。你也可以直接调用</p>
<pre class="crayon-plain-tag">kubectl autoscale rs frontend</pre>
<p> 更加简便。</p>
<div class="blog_h2"><span class="graybg">ReplicationController</span></div>
<p>类似于ReplicaSet。ReplicaSet是下一代的复制控制器。</p>
<p>大部分支持ReplicationController的kubectl命令，同时也支持ReplicaSet，一个例外是rolling-update命令。此命令专门用于RC的滚动更新  —— 每次更新一个Pod。</p>
<div class="blog_h2"><span class="graybg">Deployment</span></div>
<p>此控制器为Pod或ReplicaSet提供声明式的更新支持。在Deployment中，你可以指定一个期望状态，Deployment控制器会以一定的控制速率来修改实际状态，让它和期望状态匹配。通过定义Deployment你可以创建新的ReplicaSet，或者移除现有的Deployment并接收其全部资源。</p>
<p>Deployment的典型应用场景：</p>
<ol>
<li>创建一个Deployment，来rollout一个ReplicaSet。ReplicaSet会在后台创建Pod。检查rollout的状态来确认是否成功</li>
<li>更新Deployment的PodTemplateSpec部分，来声明Pods的新状态。一个新的复制集会被创建，Pod会已移动的速率，从老的复制集中移动到新的复制集</li>
<li>回滚到旧的部署版本，如果当前版本的部署不稳定，则可以回滚到旧的Deployment版本</li>
<li>扩容以满足负载需要</li>
<li>暂停部署，对PodTemplateSpec进行更新，然后恢复部署，进行rollout</li>
<li>将Deployment的状态作为rollout卡死的提示器</li>
<li>清除你不再需要的复制集</li>
</ol>
<div class="blog_h3"><span class="graybg">创建</span></div>
<p>下面的例子创建了三个运行Nginx的Pod构成的复制集：</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  # 复制集容量
  replicas: 3
  # 可选此控制器如何找到需要管理的Pod，必须匹配.spec.template.metadata.labels
  selector:
    matchLabels:
      app: nginx
  # 如何替换旧Pod的策略，Recreate或RollingUpdate，默认RollingUpdate
  # Recreate 当新的Pod创建之前，所有旧Pod被删除
  # RollingUpdate 滚动更新（逐步替换）
  strategy.type: RollingUpdate
  # 更新期间，处于不可用状态的Pod的数量，可以指定绝对值或者百分比。默认25
  strategy.rollingUpdate.maxUnavailable: 0
  # 更新期间，同时存在的Pod可以超过期望数量的多少，可以指定绝对值或者百分比。默认25
  strategy.rollingUpdate.maxSurge: 1 
  # 在报告failed progressing之前等待更新完成的最长时间
  progressDeadlineSeconds: 60
  # 新创建的Pod，最少在启动多久后，才被认为是Ready。默认0
  minReadySeconds: 0
  # 保留rollout历史的数量，默认无限
  revisionHistoryLimit: 
  # Pod的模板
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        # 打开端口80，供Pod使用
        - containerPort: 80</pre>
<p>根据上述规格创建Deployment：</p>
<pre class="crayon-plain-tag">kubectl create -f nginx-deployment.yaml


# 获取对象信息
kubectl get deployments
# 部署名称            期望实例数  当前运行实体数  到达期望状态实例数 对用户可用实例数   应用运行时长
# UP-TO-DATE 意味着使用最新的Pod模板
# AVAILABLE的Pod至少进入Ready状态.spec.minReadySeconds秒
# NAME               DESIRED   CURRENT       UP-TO-DATE      AVAILABLE       AGE
# nginx-deployment   3         0             0               0               1s

# 要查看部署的rollout状态，可以执行：
kubectl rollout status deployment/nginx-deployment
# Waiting for rollout to finish: 2 out of 3 new replicas have been updated...
# deployment "nginx-deployment" successfully rolled out

# 一段时间后，再次获取对象信息
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3         3         3            3           18s

# 要查看Deployment自动创建的复制集，执行
kubectl get rs
# NAME                          DESIRED   CURRENT   READY   AGE
# nginx-deployment-2035384211   3         3         3       18s
# 复制集名称的格式为 [DEPLOYMENT-NAME]-[POD-TEMPLATE-HASH-VALUE]

# 要查看为Pod自动创建的标签，执行
kubectl get pods --show-labels
# NAME                                READY     STATUS    RESTARTS   AGE       LABELS
# nginx-deployment-2035384211-7ci7o   1/1       Running   0          18s       app=nginx,pod-template-hash=2035384211
# nginx-deployment-2035384211-kzszj   1/1       Running   0          18s       app=nginx,pod-template-hash=2035384211
# nginx-deployment-2035384211-qqcnn   1/1       Running   0          18s       app=nginx,pod-template-hash=2035384211</pre>
<div class="blog_h3"><span class="graybg">更新</span></div>
<p>部署的rollout仅仅在其Pod模板发生变化时才会触发。其它情况，例如进行扩容，不会触发rollout。</p>
<p>修改Pod使用的镜像，示例命令：</p>
<pre class="crayon-plain-tag"># 方式一
kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1
# 方式二
kubectl edit deployment/nginx-deployment</pre>
<p>Deployment能够控制在更新时：</p>
<ol>
<li>仅一定数量的Pod可以处于宕机状态。默认值是，最多运行期望Pod数量 - 1个处于宕机状态</li>
<li>存在超过期望数量的Pod存在。默认值是，最多同时存在 期望Pod数量 + 1 个Pod。K8S会先创建新Pod，然后删除旧Pod，这会导致同时存在的Pod数量超过期望Pod数</li>
</ol>
<p>Deployment允许多重同时进行中的更新（multiple updates in-flight），所谓Rollover。每当部署控制器监控到新的Deployment对象时，旧有的控制标签匹配.spec.selector而模板不匹配.spec.template的Pod复制集会缩容为0，新的复制集则会扩容到期望Pod数量。如果你更新Deployment时，它正在进行rollout，则新的复制集被创建（每次更新对应一个）并扩容，并且roll over 前一次更新创建的复制集 —— 将其加入到旧复制集列表，并进行缩容。举例来说：</p>
<ol>
<li>某个部署创建了5实例的nginx:1.7.9</li>
<li>随后更新部署为5实例的nginx:1.9.1，此时nginx:1.7.9的3个实例已经被创建</li>
<li>这时，部署控制器会立刻杀死nginx:1.7.9的3个实例，随后开始创建nginx:1.9.1实例。而不是等待nginx:1.7.9的5个实例都创建完毕，再对其缩容</li>
</ol>
<p>你也可以进行标签选择器的更新，但是这并不推荐，你应该预先规划好标签。</p>
<div class="blog_h3"><span class="graybg">回滚</span></div>
<p>某些情况下你需要回滚一次部署，这通常是因为新版本存在问题，例如无限循环崩溃。</p>
<p>默认情况下，Deployment所有的rollout历史（revision）都会保存在系统中，方便你随时进行回滚。注意revision仅仅在Deployment的rollout被触发时才生成，也就是仅仅在Deployment的Pod模板变更时才生成。执行回滚后，仅仅Pod模板部分被回滚，扩容、标签部分不受影响。</p>
<p>相关命令：</p>
<pre class="crayon-plain-tag"># 列出rollout历史
kubectl rollout history deployment/nginx-deployment
# deployments "nginx-deployment"
# REVISION    CHANGE-CAUSE
# 1           kubectl create -f docs/user-guide/nginx-deployment.yaml --record
# 2           kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1
# 3           kubectl set image deployment/nginx-deployment nginx=nginx:1.91

# 查看某次rollout的详细信息
kubectl rollout history deployment/nginx-deployment --revision=2

# 回滚到上一个版本
kubectl rollout undo deployment/nginx-deployment

# 回滚到指定的版本
kubectl rollout undo deployment/nginx-deployment --to-revision=2</pre>
<div class="blog_h3"><span class="graybg">扩容</span></div>
<p>设置Pod实例数量：</p>
<pre class="crayon-plain-tag">kubectl scale deployment nginx-deployment --replicas=10</pre>
<p>如果集群启用了Pod自动水平扩容（horizontal pod autoscaling），你可以为Deployment设置autoscaler：</p>
<pre class="crayon-plain-tag"># 实例数量在10到15之间，尽可能让这些实例的CPU占用靠近80%
kubectl autoscale deployment nginx-deployment --min=10 --max=15 --cpu-percent=80</pre>
<div class="blog_h3"><span class="graybg">暂停/恢复</span></div>
<p>在触发一个或多个更新之前，你可以暂停Deployment，避免不必要的rollout：</p>
<pre class="crayon-plain-tag"># 暂停rollout
kubectl rollout pause deployment/nginx-deployment

# 更新Deployment
kubectl set image deploy/nginx-deployment nginx=nginx:1.9.1
kubectl set resources deployment nginx-deployment -c=nginx --limits=cpu=200m,memory=512Mi

# 恢复rollout
kubectl rollout resume deploy/nginx-deployment</pre>
<div class="blog_h3"><span class="graybg">查看状态</span></div>
<p>Deployment在其生命周期中，会进入若干不同的状态：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">状态</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>progressing</td>
<td>
<p>执行以下任务之一时，K8S将Deployment标记为此状态：</p>
<ol>
<li>Deployment创建了新的复制集</li>
<li>Deployment正在扩容到最新的复制集</li>
<li>Deployment正在缩容到较老的复制集</li>
<li>新的Pod变为ready或available</li>
</ol>
</td>
</tr>
<tr>
<td>complete</td>
<td>
<p>如果具有以下特征，K8S将Deployment标记为此状态：</p>
<ol>
<li>所有Pod实例都更新为最新的版本</li>
<li>所有Pod实例均可用</li>
<li>不存在旧版本的Pod实例在运行</li>
</ol>
</td>
</tr>
<tr>
<td>fail to progress</td>
<td>
<p>无法部署Deployment的最新复制集，可能原因是：</p>
<ol>
<li>配额不足</li>
<li>Readiness探针失败</li>
<li>Image拉取出错</li>
<li>权限不足</li>
<li>应用程序的运行时配置存在问题</li>
</ol>
<p>具体多久没有完成部署，会让Deployment变为此状态，受spec.progressDeadlineSeconds控制</p>
<p>一旦超过上述Deadline，则部署控制器为Deployment.status.conditions添加一个元素，其属性为</p>
<p style="padding-left: 60px;">Type=Progressing,Status=False,Reason=ProgressDeadlineExceeded</p>
<p>注意，暂停中的Deployment不会超过Deadline</p>
</td>
</tr>
</tbody>
</table>
<p>执行下面的命令可以查看Deployment的状态：</p>
<pre class="crayon-plain-tag">kubectl rollout status</pre>
<div class="blog_h3"><span class="graybg">更新历史清理</span></div>
<p>设置Deployment的.spec.revisionHistoryLimit字段，可以控制更新历史（也就是多少旧的ReplicaSet）被保留，默认所有历史都保留。</p>
<div class="blog_h2"><span class="graybg">StatefulSet</span></div>
<p>原先叫做PetSet。该控制器用于管理部署、扩容Pod集。并提供一个保证：确保Pod的有序性、唯一性。</p>
<p>和Deployment类似，SS也能管理（基于相同的容器规格的一组）Pods，但是SS还能够维护每个Pod的粘性身份（Sticky Identity ）。尽管这些Pod的规格完全一样，但是不能相互替换（有状态），每个Pod都有自己的持久化的唯一标识，即使发生重新调度，也不会改变。</p>
<p>SS的行为模式和其它控制器类似，你需要定义一个StatefulSet对象，说明期望的状态。StatefulSet控制器会执行必要的更新以达到此状态。</p>
<p>对于有以下需求的应用程序，考虑使用SS：</p>
<ol>
<li>稳定（Pod重调度后不变）的、唯一的网络标识符（network identity）</li>
<li>稳定的、持久的存储</li>
<li>有序的、优雅的部署和扩容</li>
<li>有序的、优雅的删除和终结</li>
<li>有序的、自动化的滚动更新</li>
</ol>
<p>如果不满足上述需求之一，你应该考虑提供无状态复制集的控制器，例如Deployment、ReplicaSet。</p>
<p>使用SS时，要注意：</p>
<ol>
<li>在1.9之前处于Beta状态，1.5-版本完全不可用</li>
<li>和其它所有Alpha/Beta资源一样，SS可以通过APIServer参数--runtime-config禁用</li>
<li>Pod所需的存储资源，要么由 PersistentVolume Provisioner 提供，要么由管理员预先提供</li>
<li>删除/缩容SS时，和SS关联的卷不会自动删除</li>
<li>SS目前依赖Headless Service，后者负责维护Pod的网络标识符，此Service需要你来创建</li>
</ol>
<p>Headless Service的规格示例：</p>
<pre class="crayon-plain-tag"># 用于控制网络域（Network Domain）
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx </pre>
<p>StatefulSet的规格示例：</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  # 1.8-，如果spec.selector不设置，K8S使用默认值
  # 1.8+，不设置和.spec.template.metadata.labels匹配的值会在创建SS时出现验证错误
  selector:
    matchLabels:
      app: nginx # has to match .spec.template.metadata.labels
  # 管理此SS的Headless Service的名称
  serviceName: "nginx"
  # 包含三个有标识的Pod中启动的Nginx容器
  replicas: 3    
  template:
    metadata:
      labels:
        app: nginx # has to match .spec.selector.matchLabels
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: gcr.io/google_containers/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        # 将卷www挂载到目录树
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  # 使用PersistentVolumeProvisioner提供的PersistentVolume，作为持久化的存储
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: my-storage-class
      resources:
        requests:
          storage: 1Gi</pre>
<div class="blog_h3"><span class="graybg">Pod身份</span></div>
<p>SS创建的Pod具有唯一性的身份，此身份对应了稳定的网络标识符、稳定的存储。 此身份总是关联到Pod，不管Pod被重新调度到哪个节点上。</p>
<p>序号索引：对于副本份数为N的SS，每个Pod被分配一个整数序号，值范围 [ 0, N)</p>
<p>稳定网络标识符：每个Pod的hostname的形式为<pre class="crayon-plain-tag">${SS名称}-${序号索引}</pre>。例如上面的例子中，会创建web-0、web-1、web-2三个Pod。</p>
<p>SS可以利用Headless Service来控制其Pod的域名，HS的域名格式为<pre class="crayon-plain-tag">$(service name).$(namespace).svc.cluster.local</pre>，其中cluster.local是集群的域名，上例中的HS域名为nginx.default.svc.cluster.local。</p>
<p>在HS的管理下，Pod的域名格式为<pre class="crayon-plain-tag">$(podname).$(governing service domain)</pre>，因此web-0的域名为web-0.nginx.default.svc.cluster.local</p>
<div class="blog_h3"><span class="graybg">稳定存储</span></div>
<p>对于每个VolumeClaimTemplate，K8S会创建对应的PersistentVolume。在上面的例子中，每个Pod会被赋予StorageClass为my-storage-class的单个PersistentVolume，以及1GB的存储空间。如果不指定StorageClass使用默认值。</p>
<p>当Pod被重新调度时，volumeMounts指定的挂载规则会重新挂载PersistentVolume。此外，即使SS或Pod被删除，PersistentVolume也不会被自动删除，你必须手工的删除它。</p>
<div class="blog_h3"><span class="graybg">Pod标签</span></div>
<p>SS创建一个新Pod时，会为其添加一个标签：statefulset.kubernetes.io/pod-name。通过此标签，你可以为某个Pod实例Attach一个服务。</p>
<div class="blog_h3"><span class="graybg">部署/扩容保证</span></div>
<ol>
<li>当SS部署Pod时，会顺序的依次创建，从序号0开始逐个的</li>
<li>当SS删除Pod时，会逆序的依次删除，从序号N-1开始逐个的</li>
<li>水平扩容时，新序号之前的所有Pod必须已经Running &amp; Ready</li>
<li>在Pod被终结时，其后面的所有Pod必须已经被完全关闭</li>
</ol>
<p>SS不应该指定pod.Spec.TerminationGracePeriodSeconds=0。</p>
<div class="blog_h2"><span class="graybg">DaemonSet</span></div>
<p>该控制器能确保所有（或部分）节点运行Pod的单个副本。每当节点加入到集群时，Pod就被添加到其上；每当节点离开集群时，其上的Pod就被回收。删除DS会导致所有Pod被删除。</p>
<p>DS的典型应用场景包括：</p>
<ol>
<li>运行集群级别的存储守护程序，例如glusterd、ceph这些程序每个节点仅需要一个</li>
<li>在每个节点运行日志收集程序，例如fluentd、logstash</li>
<li>在每个节点上运行监控程序，例如collectd</li>
</ol>
<p>DS的规格示例：</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-elasticsearch
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd-elasticsearch
  template:
    metadata:
      labels:
        name: fluentd-elasticsearch
    spec:
      # 用于仅仅在部分节点上运行Pod
      nodeSelector: 
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd-elasticsearch
        image: gcr.io/google-containers/fluentd-elasticsearch:1.20
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers</pre>
<div class="blog_h3"><span class="graybg">和DS的Pod通信</span></div>
<p>要和DaemonSet中的Pod通信，有以下几种手段：</p>
<ol>
<li>Push：DS中的Pod可能配置为，向其它服务发送更新，例如向统计数据库</li>
<li>节点IP + 已知端口：Pod可能使用hostPort来暴露端口</li>
<li>DNS：使用和DS相同的Pod选择器，创建一个Headless Service。然后你可以通过endpoints资源来发现DS</li>
</ol>
<div class="blog_h3"><span class="graybg">hostPort</span></div>
<p>可以为DaemonSet的Pod声明hostPort，这样，宿主机的端口会通过iptables的NAT转发给Pod：</p>
<pre class="crayon-plain-tag">ports:
  - name: http
    containerPort: 80
    protocol: TCP
    hostPort: 80 </pre>
<div class="blog_h2"><span class="graybg">Job</span></div>
<p>Job可以创建一或多个Pod，并且确保一定数量的Pod成功的完成。Job会跟踪Pod们的执行状态，并判断它们是否成功执行，当指定数量的Pod成功了，则Job本身的状态变为成功。删除Job会清理掉它创建的Pod。</p>
<p>Job的一个简单用例是，确保Pod成功执行完成。如果第一个Pod失败/被删除，则Job会启动第二个实例。</p>
<p>Job也支持并行的运行多个Pod。</p>
<p>Job的规格示例：</p>
<pre class="crayon-plain-tag">apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  template:
    metadata:
      name: pi
    spec:
      containers:
      - name: pi
        image: perl
        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
      restartPolicy: Never
  backoffLimit: 4</pre>
<div class="blog_h2"><span class="graybg">CronJob</span></div>
<p>定期调度执行的Job，规格示例：</p>
<pre class="crayon-plain-tag">apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello
spec:
  # Cron表达式，每分钟执行
  schedule: "*/1 * * * *"
  # 如果由于某些原因，错过了调度时间，那么在什么时间差异之内，允许启动Job。默认没有deadline，总是允许启动
  startingDeadlineSeconds: 5
  # 并发控制策略，此CronJob创建的多个Job如何并发运行
  # Allow，默认，允许并发运行多个Job
  # Forbid，禁止并发运行，如果尝试调度时发现先前的Job仍然在运行，跳过本次调度
  # Replace，禁止并发运行，如果尝试调度时发现先前的Job仍然在运行，替换掉先前的Job
  concurrencyPolicy: Allow
  # 如果设置为true，则暂停后续调度，已经存在的Job不受影响
  suspend: false
  # 保留的成功、失败的Job的数量.超过限制则删除对应的K8S资源
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 0
  jobTemplate:
    # 下面的配置同Job
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date
          restartPolicy: OnFailure</pre>
<div class="blog_h2"><span class="graybg">垃圾回收</span></div>
<p>Kubernetes garbage collector是控制器的一种，它的职责是<span style="background-color: #c0c0c0;">删除</span>哪些<span style="background-color: #c0c0c0;">曾经拥有</span>，但是<span style="background-color: #c0c0c0;">现在已经没有Owner的对象</span>。</p>
<div class="blog_h3"><span class="graybg">对象所有权</span></div>
<p>某些对象是其它对象的所有者（Owner），例如ReplicaSet是一系列Pod的所有者。被所有者管理的对象称为依赖者（Dependent ）。任何依赖者都具有字段 <span style="background-color: #c0c0c0;">metadata.ownerReferences</span>，指向其所有者。</p>
<p>某些情况下，K8S会自动设置ownerReference。例如创建ReplicaSet时，对应Pod的ownerReference即自动设置。常作为所有者的内置资源类型包括ReplicationController, ReplicaSet, StatefulSet, DaemonSet, Deployment, Job和CronJob。</p>
<p>你也可以手工配置ownerReference字段，以建立所有者-依赖者关系。</p>
<div class="blog_h3"><span class="graybg">依赖者的删除</span></div>
<p>在删除对象时，可以指定是否级联删除（Cascading deletion）其依赖者（依赖被删除对象的哪些对象）。级联删除有两种执行模式：前台删除、后台删除。</p>
<div class="blog_h3"><span class="graybg">前台删除</span></div>
<p>在这种模式下，根对象（被显式删除的顶级Owner）首先进入删除中“（deletion in progress）”状态。此时：</p>
<ol>
<li>对象仍然可以通过REST API看到</li>
<li>对象的deletionTimestamp字段不再为空</li>
<li>对象的<pre class="crayon-plain-tag">metadata.finalizers</pre>数组包含元素“foregroundDeletion“</li>
</ol>
<p>一旦进入“删除中”状态，垃圾回收器就开始删除对象的依赖者。一旦所有<span style="background-color: #c0c0c0;">阻塞依赖者</span>（ownerReference.blockOwnerDeletion=true）全被删除，根对象就被删除。</p>
<p>当某种控制器设置了ownerReferences，它也会自动设置blockOwnerDeletion，不需要人工干预。</p>
<div class="blog_h3"><span class="graybg">后台删除</span></div>
<p>根对象立即被删除。垃圾回收器异步的在后台删除依赖者。</p>
<div class="blog_h3"><span class="graybg">设置删除模式</span></div>
<p>通过设置删除请求的DeleteOptions.propagationPolicy，可以修改级联删除模式：</p>
<ol>
<li>Orphan，孤儿化，解除ownerReferences而不删除</li>
<li>Foreground，前台级联删除</li>
<li>Background，后台级联删除</li>
</ol>
<div class="blog_h1"><span class="graybg">准许控制器</span></div>
<p>准许控制器（Admission Controller）是一小片的代码，被编译到kube-apiserver的二进制文件中。它能够拦截针对APIServer的请求，具体拦截时机是：操控对象被持久化之前、请求通过身份验证之后。很多K8S的高级特性需要准许控制器的介入。</p>
<p>准许控制器可以具有两个行为：</p>
<ol>
<li>validating：不能修改其admit的对象</li>
<li>mutating：能够修改其admit的对象</li>
</ol>
<p>准许控制的流程分为两个阶段，首先运行mutating类控制器，然后运行validating类控制器，任何一个控制器在任何阶段拒绝请求，则客户端收到一个错误。需要注意某些控制器同时有mutating、validating行为。</p>
<p>控制器的执行顺序，等同于它们在--admission-control参数中声明的顺序。</p>
<div class="blog_h2"><span class="graybg">推荐的准许控制器</span></div>
<p>对于1.9版本，建议按序开启以下准许控制器：</p>
<pre class="crayon-plain-tag">--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,
                    DefaultStorageClass,ValidatingAdmissionWebhook,ResourceQuota,
                    DefaultTolerationSeconds,MutatingAdmissionWebhook</pre>
<div class="blog_h2"><span class="graybg"><a id="webhooks"></a>动态准许控制</span></div>
<p>（静态）准许控制器本身不够灵活：</p>
<ol>
<li>需要被编译到kube-apiserver镜像中</li>
<li>仅仅当API Server启动后才可以被配置</li>
</ol>
<p>Admission Webhooks可以解决此问题，用它可以来开发代码独立（Out-of-tree）、支持运行时配置的准许控制器。</p>
<p>和静态的准许控制器一样，Webhook也分为validating、mutating两类。</p>
<p>Webhook实际上是由ValidatingAdmissionWebhook、MutatingAdmissionWebhook这两个静态准许控制器适配（到API Server）的，因此这两个遵顼控制器必须启用。</p>
<div class="blog_h3"><span class="graybg">开发Webhook服务器</span></div>
<p>这种服务需要处理admissionReview请求，并将其准许决定封装在admissionResponse中。</p>
<p>admissionReview可以是版本化的，Webhook可以使用admissionReviewVersions字段声明它能处理的Review的版本列表。调用Webhook时API Server会选择此列表中、它支持的第一个版本，如果找不到匹配版本，则失败。</p>
<p>下面是Kubernetes官方提供的样例Webhook服务器代码：</p>
<pre class="crayon-plain-tag">package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"

	"k8s.io/api/admission/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/klog"
)

// 一个助手函数，创建内嵌error的AdmissionResponse
func toAdmissionResponse(err error) *v1beta1.AdmissionResponse {
	return &amp;v1beta1.AdmissionResponse{
		Result: &amp;metav1.Status{
			Message: err.Error(),
		},
	}
}

// 所有validators和mutators都由admitFunc类型的函数负责实现
type admitFunc func(v1beta1.AdmissionReview) *v1beta1.AdmissionResponse

// 核心逻辑，API Server使用HTTP协议调用Webhook Server
func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) {
	var body []byte
	if r.Body != nil {
		if data, err := ioutil.ReadAll(r.Body); err == nil {
			body = data
		}
	}

	// 请求体校验
	contentType := r.Header.Get("Content-Type")
	if contentType != "application/json" {
		klog.Errorf("contentType=%s, expect application/json", contentType)
		return
	}

	klog.V(2).Info(fmt.Sprintf("handling request: %s", body))

	// 请求Review
	requestedAdmissionReview := v1beta1.AdmissionReview{}

	// 响应Review
	responseAdmissionReview := v1beta1.AdmissionReview{}

	deserializer := codecs.UniversalDeserializer()
	// 反串行化响应体为requestedAdmissionReview
	if _, _, err := deserializer.Decode(body, nil, &amp;requestedAdmissionReview); err != nil {
		klog.Error(err)
		responseAdmissionReview.Response = toAdmissionResponse(err)
	} else {
		// 如果没有错误，则执行准许逻辑
		responseAdmissionReview.Response = admit(requestedAdmissionReview)
	}

	// 返回相同的UID
	responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID

	klog.V(2).Info(fmt.Sprintf("sending response: %v", responseAdmissionReview.Response))

	respBytes, err := json.Marshal(responseAdmissionReview)
	if err != nil {
		klog.Error(err)
	}
	if _, err := w.Write(respBytes); err != nil {
		klog.Error(err)
	}
}

/* 各种validators和mutators */
func serveAlwaysDeny(w http.ResponseWriter, r *http.Request) {
	serve(w, r, alwaysDeny)
}

func serveAddLabel(w http.ResponseWriter, r *http.Request) {
	serve(w, r, addLabel)
}

func servePods(w http.ResponseWriter, r *http.Request) {
	serve(w, r, admitPods)
}

func serveAttachingPods(w http.ResponseWriter, r *http.Request) {
	serve(w, r, denySpecificAttachment)
}

func serveMutatePods(w http.ResponseWriter, r *http.Request) {
	serve(w, r, mutatePods)
}

func serveConfigmaps(w http.ResponseWriter, r *http.Request) {
	serve(w, r, admitConfigMaps)
}

func serveMutateConfigmaps(w http.ResponseWriter, r *http.Request) {
	serve(w, r, mutateConfigmaps)
}

func serveCustomResource(w http.ResponseWriter, r *http.Request) {
	serve(w, r, admitCustomResource)
}

func serveMutateCustomResource(w http.ResponseWriter, r *http.Request) {
	serve(w, r, mutateCustomResource)
}

func serveCRD(w http.ResponseWriter, r *http.Request) {
	serve(w, r, admitCRD)
}

type Config struct {
	CertFile string
	KeyFile  string
}

func (c *Config) addFlags() {
	// 包含用于HTTPS的x509服务器证书，如果包含CA证书，则连接在服务器证书后面
	flag.StringVar(&amp;c.CertFile, "tls-cert-file", c.CertFile, "...")
	// 匹配上述服务器证书的私钥
	flag.StringVar(&amp;c.KeyFile, "tls-private-key-file", c.KeyFile, "...")
}

func configTLS(config Config) *tls.Config {
	sCert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
	if err != nil {
		klog.Fatal(err)
	}
	return &amp;tls.Config{
		Certificates: []tls.Certificate{sCert},
		// 下面一行用于启用mTLS，也就是对客户端（API Server）进行身份验证
		// ClientAuth:   tls.RequireAndVerifyClientCert,
	}
}

func main() {
	var config Config
	config.addFlags()
	flag.Parse()

        // 不同的URL路径，对应不同的Webhook，处理函数也不同
	http.HandleFunc("/always-deny", serveAlwaysDeny)
	http.HandleFunc("/add-label", serveAddLabel)
	http.HandleFunc("/pods", servePods)
	http.HandleFunc("/pods/attach", serveAttachingPods)
	http.HandleFunc("/mutating-pods", serveMutatePods)
	http.HandleFunc("/configmaps", serveConfigmaps)
	http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps)
	http.HandleFunc("/custom-resource", serveCustomResource)
	http.HandleFunc("/mutating-custom-resource", serveMutateCustomResource)
	http.HandleFunc("/crd", serveCRD)

	server := &amp;http.Server{
		Addr:      ":443",
		TLSConfig: configTLS(config),
	}
	server.ListenAndServeTLS("", "")
}</pre>
<p>此样例中包含<a href="https://github.com/kubernetes/kubernetes/tree/v1.13.0/test/images/webhook">很多Webhook的样板代码</a>。下面选取几个分析。</p>
<p>addlabel.go是一个mutator，它为对象添加{"added-label": "yes"}标签：</p>
<pre class="crayon-plain-tag">package main

import (
	"encoding/json"

	"k8s.io/api/admission/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/klog"
)

const (
	addFirstLabelPatch string = `[
         { "op": "add", "path": "/metadata/labels", "value": {"added-label": "yes"}}
     ]`
	addAdditionalLabelPatch string = `[
         { "op": "add", "path": "/metadata/labels/added-label", "value": "yes" }
     ]`
)

func addLabel(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
	klog.V(2).Info("calling add-label")
	obj := struct {
		metav1.ObjectMeta
		Data map[string]string
	}{}
	// 从Review中提取原始对象（被拦截的API Server请求中的对象）
	raw := ar.Request.Object.Raw
	err := json.Unmarshal(raw, &amp;obj)
	if err != nil {
		klog.Error(err)
		return toAdmissionResponse(err)
	}
	// 响应
	reviewResponse := v1beta1.AdmissionResponse{}
	// 准许通过
	reviewResponse.Allowed = true
	if len(obj.ObjectMeta.Labels) == 0 {
		// Patch原始对象
		reviewResponse.Patch = []byte(addFirstLabelPatch)
	} else {
		reviewResponse.Patch = []byte(addAdditionalLabelPatch)
	}
	pt := v1beta1.PatchTypeJSONPatch
	reviewResponse.PatchType = &amp;pt
	return &amp;reviewResponse
}</pre>
<p>pods.go提供了三个Webhook：</p>
<pre class="crayon-plain-tag">package main

import (
	"fmt"
	"strings"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"k8s.io/api/admission/v1beta1"
	"k8s.io/klog"
)

const (
	podsInitContainerPatch string = `[
		 {"op":"add","path":"/spec/initContainers","value":[{"image":"webhook-added-image","name":"webhook-added-init-container","resources":{}}]}
	]`
)

// 校验Pod的规格
func admitPods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
	klog.V(2).Info("admitting pods")
	// 校验Review中的GVK
	podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
	if ar.Request.Resource != podResource {
		err := fmt.Errorf("expect resource to be %s", podResource)
		klog.Error(err)
		return toAdmissionResponse(err)
	}
	// 将原始对象转换为Pod
	raw := ar.Request.Object.Raw
	pod := corev1.Pod{}
	deserializer := codecs.UniversalDeserializer()
	if _, _, err := deserializer.Decode(raw, nil, &amp;pod); err != nil {
		klog.Error(err)
		return toAdmissionResponse(err)
	}
	reviewResponse := v1beta1.AdmissionResponse{}
	// 默认准许通过
	reviewResponse.Allowed = true

	var msg string
	// 演示各种不准许的情形
	if v, ok := pod.Labels["webhook-e2e-test"]; ok {
		if v == "webhook-disallow" {
			reviewResponse.Allowed = false
			msg = msg + "the pod contains unwanted label; "
		}
	}
	for _, container := range pod.Spec.Containers {
		if strings.Contains(container.Name, "webhook-disallow") {
			reviewResponse.Allowed = false
			msg = msg + "the pod contains unwanted container name; "
		}
	}
	if !reviewResponse.Allowed {
		reviewResponse.Result = &amp;metav1.Status{Message: strings.TrimSpace(msg)}
	}
	return &amp;reviewResponse
}

// 修改Pod，Patch一个Init容器进去
func mutatePods(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
	klog.V(2).Info("mutating pods")
	podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
	if ar.Request.Resource != podResource {
		klog.Errorf("expect resource to be %s", podResource)
		return nil
	}

	raw := ar.Request.Object.Raw
	pod := corev1.Pod{}
	deserializer := codecs.UniversalDeserializer()
	if _, _, err := deserializer.Decode(raw, nil, &amp;pod); err != nil {
		klog.Error(err)
		return toAdmissionResponse(err)
	}
	reviewResponse := v1beta1.AdmissionResponse{}
	reviewResponse.Allowed = true
	if pod.Name == "webhook-to-be-mutated" {
		reviewResponse.Patch = []byte(podsInitContainerPatch)
		pt := v1beta1.PatchTypeJSONPatch
		reviewResponse.PatchType = &amp;pt
	}
	return &amp;reviewResponse
}

// 禁止kubectl attach to-be-attached-pod -i -c=container1请求
func denySpecificAttachment(ar v1beta1.AdmissionReview) *v1beta1.AdmissionResponse {
	klog.V(2).Info("handling attaching pods")
	if ar.Request.Name != "to-be-attached-pod" {
		return &amp;v1beta1.AdmissionResponse{Allowed: true}
	}
	// 期望接收到的是Pod资源，attach子资源
	podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
	if e, a := podResource, ar.Request.Resource; e != a {
		err := fmt.Errorf("expect resource to be %s, got %s", e, a)
		klog.Error(err)
		return toAdmissionResponse(err)
	}
	if e, a := "attach", ar.Request.SubResource; e != a {
		err := fmt.Errorf("expect subresource to be %s, got %s", e, a)
		klog.Error(err)
		return toAdmissionResponse(err)
	}

	raw := ar.Request.Object.Raw
	podAttachOptions := corev1.PodAttachOptions{}
	deserializer := codecs.UniversalDeserializer()
	if _, _, err := deserializer.Decode(raw, nil, &amp;podAttachOptions); err != nil {
		klog.Error(err)
		return toAdmissionResponse(err)
	}
	klog.V(2).Info(fmt.Sprintf("podAttachOptions=%#v\n", podAttachOptions))
	// 如果不使用Stdin，或者容器不是container1则允许，否则不允许
	if !podAttachOptions.Stdin || podAttachOptions.Container != "container1" {
		return &amp;v1beta1.AdmissionResponse{Allowed: true}
	}
	return &amp;v1beta1.AdmissionResponse{
		Allowed: false,
		Result: &amp;metav1.Status{
			Message: "attaching to pod 'to-be-attached-pod' is not allowed",
		},
	}
}</pre>
<div class="blog_h3"><span class="graybg">部署Webhook服务</span></div>
<p>可以直接作为Deployment部署在K8S集群内部，也可以部署在外面，需要对应的配置来配合。</p>
<div class="blog_h3"><span class="graybg">配置Webhook</span></div>
<p>Webhook的配置通过ValidatingWebhookConfiguration、MutatingWebhookConfiguration这两类K8S资源进行：</p>
<pre class="crayon-plain-tag">apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
  name: sidecar-injectors
webhooks:
# Webhoooks列表
- name: init-inject
  # 匹配任意一个规则，则API Server会发送AdmissionReview
  # 给clientConfig所指定的Webhook服务器
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    resources:
    - pods
    scope: "Namespaced"
  clientConfig:
    # Webhook服务
    service:
      namespace: kube-system
      name: sidecar-injector
    # 签名了服务器端证书的CA，PEM格式
    # 有时候会设置为 caBundle: Cg== （\n），这是一个占位符，防止该CR被API Server拒绝。证书后续由程序自动更新
    caBundle: pem encoded ca cert that signs the server cert used by the webhook
  admissionReviewVersions:
  - v1beta1
  timeoutSeconds: 1</pre>
<div class="blog_h3"><span class="graybg">身份验证</span></div>
<p>如果Webhook需要对API Server进行身份验证，则需要进行以下步骤：</p>
<ol>
<li>为API Server提供<pre class="crayon-plain-tag">--admission-control-config-file</pre>参数，指定准许控制配置文件</li>
<li>在准许控制配置文件中，声明MutatingAdmissionWebhook、ValidatingAdmissionWebhook从何处读取凭证信息：<br />
<pre class="crayon-plain-tag">apiVersion: apiserver.k8s.io/v1alpha1
kind: AdmissionConfiguration
plugins:
- name: ValidatingAdmissionWebhook
  configuration:
    apiVersion: apiserver.config.k8s.io/v1alpha1
    kind: WebhookAdmission
    kubeConfigFile: /path/to/kubeconfig
- name: MutatingAdmissionWebhook
  configuration:
    apiVersion: apiserver.config.k8s.io/v1alpha1
    kind: WebhookAdmission
    kubeConfigFile: /path/to/kubeconfig</pre>
</li>
<li><span style="background-color: #c0c0c0;">凭证信息必须存放在KubeConfig文件中</span>，示例：<br />
<pre class="crayon-plain-tag">apiVersion: v1
kind: Config
users:

# 基于数字证书的身份验证
  # Webhook服务器的DNS名称
- name: 'webhook1.ns1.svc'  
  user:
    client-certificate-data: pem encoded certificate
    client-key-data: pem encoded key
# HTTP基本验证
  # 可以使用通配符
- name: '*.webhook-company.org'
  user:
    password: password
    username: name
# 基于令牌的身份验证
# 默认凭证
- name: '*'
  user:
    token: &lt;token&gt;</pre>
</li>
</ol>
<div class="blog_h2"><span class="graybg">准许控制器列表</span></div>
<div class="blog_h3"><span class="graybg">AlwaysAdmit</span></div>
<p>总是允许所有请求通过。</p>
<div class="blog_h3"><span class="graybg">AlwaysDeny</span></div>
<p>总是拒绝所有请求通过，仅测试用。 </p>
<div class="blog_h3"><span class="graybg">AlwaysPullImages</span></div>
<p>修改所有新的Pod，将它们的镜像拉取策略改为Always。在多团队共享集群上有用，可以确保仅具有合法凭证的用户才能拉取镜像。</p>
<p>如果没有该AC，则一旦镜像被拉取到Node上，任何用户、Pod都可以不提供凭证的使用之。</p>
<div class="blog_h3"><span class="graybg">DefaultStorageClass</span></div>
<p>监控PersistentVolumeClaim对象的创建过程，如果PVC没有要求任何存储类，则为其自动添加。</p>
<div class="blog_h3"><span class="graybg">DefaultTolerationSeconds</span></div>
<p>设置当Taint和Toleration不匹配时，默认能够容忍的最长时间。</p>
<div class="blog_h3"><span class="graybg">DenyEscalatingExec</span></div>
<p>禁止对以特权级别（privileged Pod、能访问宿主机IPC名字空间的Pod、能访问宿主机PID名字空间的Pod）运行的Pod执行exec/attach命令。</p>
<div class="blog_h3"><span class="graybg">EventRateLimit</span></div>
<p>Alpha/1.9，可以缓和APIServer被事件请求泛洪的问题。</p>
<div class="blog_h3"><span class="graybg">ExtendedResourceToleration</span></div>
<p>1.9引入的插件，辅助带有扩展资源（GPU/FPGA...）的节点的创建，这类节点应该以扩展资源名为键配置Taints。</p>
<p>该控制器自动为请求扩展资源的Pod添加Tolerations，用户不需要手工添加。</p>
<div class="blog_h3"><span class="graybg">ImagePolicyWebhook</span></div>
<p>允许后端的webhook给出准许决策。</p>
<div class="blog_h3"><span class="graybg">Initializers</span></div>
<p>Alpha/1.7，根据现有的InitializerConfiguration来确定资源的初始化器。</p>
<div class="blog_h3"><span class="graybg">InitialResources</span></div>
<p>监控Pod创建请求，如果容器没有提供资源用量请求/限制，该AC会根据基于相同镜像的容器的历史运行状况，自动为容器添加资源用量请求/限制。</p>
<div class="blog_h3"><span class="graybg">LimitPodHardAntiAffinity</span></div>
<p>拒绝任何这样的Pod，它的AntiAffinity（requiredDuringSchedulingRequiredDuringExecution）拓扑键是kubernetes.io/hostname之外的值。</p>
<div class="blog_h3"><span class="graybg">LimitRanger</span></div>
<p>监控入站请求，确保Namespace中LimitRange对象列出的任何约束条件不被违反。如果在部署中使用LimitRange，你必须启用该AC。</p>
<div class="blog_h3"><span class="graybg">NamespaceAutoProvision</span></div>
<p>检查针对所有名字空间化的资源的请求，如果所引用的名字空间不存在，则自动创建名字空间。</p>
<div class="blog_h3"><span class="graybg">NamespaceExists</span></div>
<p>检查针对所有名字空间化的资源的请求，如果所引用的名字空间不存在，则返回错误。</p>
<div class="blog_h3"><span class="graybg">NamespaceLifecycle</span></div>
<p>具有以下功能：</p>
<ol>
<li>确保正在删除的名字空间中，不会由新的对象被创建</li>
<li>确保针对不存在名字空间的资源创建请求被拒绝</li>
<li>禁止删除三个系统保留名字空间：default, kube-system, kube-public</li>
</ol>
<p>Namespace删除操作会级联删除其内部的Pod、Service等对象，为了确保删除过程的完整性，应当使用此AC。</p>
<div class="blog_h3"><span class="graybg">NodeRestriction</span></div>
<p>限制Kubelet能够修改的Pod、Node对象。</p>
<div class="blog_h3"><span class="graybg">PersistentVolumeLabel</span></div>
<p>自动将云服务商定义的Region/Zone标签附加到PV。可以用于保证PV和Pod位于相同的Region/Zone。</p>
<div class="blog_h3"><span class="graybg">PodNodeSelector</span></div>
<p>读取Namespace上的注解和全局配置，限制某个命名空间中的Pod能够运行在什么节点（通过nodeSelector）。</p>
<p>要启用此控制器，需要修改APIServer的配置<span style="color: #242729;">/etc/kubernetes/manifests/kube-apiserver.yaml，在--enable-admission-plugins=后添加PodNodeSelector。</span></p>
<p>使用下面的注解，可以为命名空间中任何Pod添加默认nodeSelector：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Namespace
metadata
 name: devops
 annotations:
   scheduler.alpha.kubernetes.io/node-selector: k8s.gmem.cc/dedicated-to=devops</pre>
<p>这样，此命名空间中创建的新Pod，都会具有nodeSelector：</p>
<pre class="crayon-plain-tag">nodeSelector
  k8s.gmem.cc/dedicated-to:devops</pre>
<div class="blog_h3"><span class="graybg">PodTolerationRestriction</span></div>
<p>此控制器首先校验Pod的容忍和<span style="background-color: #c0c0c0;">命名空间的容忍</span>，如果存在冲突则拒绝Pod创建。然后，它将<span style="background-color: #c0c0c0;">命名空间的容忍合并到Pod的容忍</span>中，合并后的结果进行<span style="background-color: #c0c0c0;">命名空间容忍白名单检查</span>，如果检查不通过则Pod被拒绝创建。</p>
<p>命名空间的容忍，使用注解scheduler.alpha.kubernetes.io/defaultTolerations配置，示例：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Namespace
metadata:
  annotations:
    scheduler.alpha.kubernetes.io/defaultTolerations: '[{"key":"k8s.gmem.cc/dedicated-to","operator":"Equal","value":"devops","effect":"NoSchedule"}]'</pre>
<p>命名空间的容忍白名单，使用注解scheduler.alpha.kubernetes.io/tolerationsWhitelist配置。 此注解的值类似上面的注解。</p>
<div class="blog_h3"><span class="graybg">PersistentVolumeClaimResize</span></div>
<p>打开特性开关ExpandPersistentVolumes后，你应该启用该AC。</p>
<p>此AC默认即用所有PVC的Resizing请求，除非PVC的存储类明确的启用（allowVolumeExpansion=true）</p>
<div class="blog_h3"><span class="graybg">PodPreset</span></div>
<p>注入匹配的PodPreset中的规格字段。</p>
<div class="blog_h3"><span class="graybg">Priority</span></div>
<p>使用priorityClassName字段并为Pod产生一个整数类型的优先级。如果目标priorityClass不存在则Pod被拒绝。</p>
<div class="blog_h3"><span class="graybg">ResourceQuota</span></div>
<p>监控入站请求，确保没有违反Namespace中ResourceQuota对象所列出的资源配额限制。</p>
<p>如果你使用ResourceQuota对象，则必须启用此AC。</p>
<div class="blog_h3"><span class="graybg">ServiceAccount</span></div>
<p>实现ServiceAccount的自动化。当使用ServiceAccount对象是应当启用此AC。</p>
<div class="blog_h3"><span class="graybg">SecurityContextDeny</span></div>
<p>禁止那些尝试通过SecurityContext中某些字段实现权限提升的Pod。如果没有启用PodSecurityPolicy，应当考虑启用该控制器。</p>
<div class="blog_h1"><span class="graybg">Service</span></div>
<p>K8S中的Pod是有生命周期的，当Pod死掉后它无法复活。某些控制器会动态的创建、删除Pod。尽管Pod会获得一个IP地址，但是随着时间的推进，IP也不能作为联系它的可靠手段，因为IP会改变。</p>
<p>那么，如果某个Pod（后端）集向其它Pod（前端）集提供功能，那么前端如何找到、跟踪后端集中的Pod呢？Service能帮助你实现这一需求。</p>
<p>所谓Service，是一个抽象，它定义了：</p>
<ol>
<li>一个逻辑的Pod集合。常常通过标签选择器来锚定</li>
<li>访问这些Pod的策略</li>
</ol>
<p>Service有时候也被称为微服务（micro-service）。</p>
<p>举个例子，假设有一个图像处理后端，包括三个实例。这些实例是无状态的，因而前端不关心它调用的是哪一个。组成后端的Pod实例可能发生变化（宕机、扩容），如果前端跟踪其调用的Pod显然会造成耦合。Service这个抽象层可以实现解耦。</p>
<p>对于K8S-Native应用程序，K8S提供了Endpoints API。每当Service包含的Pod集发生变化后，即更新端点。对于非K8S-Native的应用程序，K8S为Service提供了一个基于虚拟IP的桥，用于将请求重定向到后端的Pod。</p>
<div class="blog_h2"><span class="graybg">定义服务</span></div>
<p>假设你有一组Pod，其标签为app=MyApp，并且都暴露了9376端口。你可以为它们定义如下服务：</p>
<pre class="crayon-plain-tag">kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  # 被服务管理的Pod
  selector:
    app: MyApp
  ports:
  - protocol: TCP
    # 服务的集群IP上的端口
    port: 80
    # Pod的端口，可以指定为Pod中定义的端口名
    targetPort: 9376</pre>
<p>Service本身也被分配一个IP（称为集群IP），此IP会被服务代理使用。</p>
<p>Service的selector会被不断的估算，其匹配结果会被发送给与Service同名的Endpoints对象。</p>
<p>Service能够进行端口映射。针对服务的请求可以被映射到Pod的端口。支持的网络协议包括TCP、UDP两种。</p>
<div class="blog_h3"><span class="graybg"><a id="service-without-selector"></a>不使用选择器的Service</span></div>
<p>尽管Service通常用来抽象对Pod的访问，它也可以用来抽象其它类型的后端，例如：</p>
<ol>
<li>在生产环境下你希望使用外部数据库集群，但是在开发/测试时你希望使用自己的数据库</li>
<li>你希望把服务指向其它名字空间中的服务，甚至其它集群中的服务</li>
<li>你在把一部分工作负载迁移到K8S，而让另一部分在外部运行</li>
</ol>
<p>以上情况下，你都可以定义一个没有选择器的Service。由于没有选择器，Endpoints对象不会被创建。你可能需要手工映射服务到端点：</p>
<pre class="crayon-plain-tag">kind: Endpoints
apiVersion: v1
metadata:
  name: my-service
# 端点列表，这个例子中流量被路由给1.2.3.4:9376
subsets:
  - addresses:
      - ip: 1.2.3.4
    ports:
      - port: 9376</pre>
<div class="blog_h3"><span class="graybg">ExternalName</span></div>
<p>这是一种特殊的Service，它即不选择端点，也不定义端口。它只用于提供集群外部的服务的别名，集群内的组件可以基于别名访问外部服务：</p>
<pre class="crayon-plain-tag">kind: Service
apiVersion: v1
metadata:
  name: database
  namespace: prod
spec:
  type: ExternalName
  # IP地址也支持
  externalName: db.gmem.cc</pre>
<p>当集群内组件查找database.prod.svc.${CLUSTER_DOMAIN}时，集群的DNS服务会返回一个CNAME记录，其值为db.gmem.cc。 </p>
<p>注意：经过试验，externalName填写IP地址也是可以的，这种情况下，集群DNS服务应该是做了一个A记录。</p>
<div class="blog_h2"><span class="graybg">虚拟IP和服务代理</span></div>
<p>K8S集群中的每个节点都运行着kube-proxy组件。该组件负责为服务（除了ExternalName）提供虚拟IP。</p>
<p>在1.0版本，Service属于第四层（TCP/UDP over IP）构造，kube-proxy完全工作在用户空间。从1.1开始引入了Ingress API，用于实现第七层（HTTP）Service。基于iptables的代理也被添加，并从1.2开始成为默认操作模式。从1.9开始，ipvs代理被添加。</p>
<div class="blog_h3"><span class="graybg">userspace模式</span></div>
<p>此模式下，kube-proxy监控Master，关注Service、Endpoints对象的添加和删除。</p>
<p>对于每个Service，KP会在本地节点随机打开一个代理端口，任何发向此端口的的请求会被转发给服务的某个后端Pod（从Endpoints对象中获取）。到底使用哪个Pod，取决于Service的SessionAffinity设置，默认选取后端的算法是round robin。KP会把转发规则写入到iptables中，捕获Service的集群IP（虚拟的）的流量。</p>
<p>这种模式下，需要在内核空间和用户空间传递流量。</p>
<div class="blog_h3"><span class="graybg">iptables模式</span></div>
<p>此模式下，KP会直接安装iptables规则，捕获Service的集群IP + Service 端口的流量，转发给某个Service后端Pod，选择Pod的算法是随机。</p>
<p>这种模式下，不需要在内核空间和用户空间之间切换，通常比userspace模式更快、更可靠。但是，该模式不支持当第一次选择的Pod不响应后重新选择另外一个Pod，因此需要配合Readness探针使用。</p>
<div class="blog_h3"><span class="graybg">ipvs模式</span></div>
<p>关于IPVS的知识参考：<a href="/ipvs-and-keepalived">IPVS和Keepalived</a></p>
<p>在1.9中处于Beta状态。</p>
<p>此模式下，KP会调用netlink网络接口，创建和K8S的Service/Endpoint对应的ipvs规则，并且周期性的将K/E和ipvs规则同步。当访问Service时，流量被转发给某个Pod。</p>
<p>类似于iptables，ipvs基于netfilter钩子函数，但是它使用哈希表作为底层数据结构，且运行在内核态。这意味着ipvs的流量转发速度会快的多，且同步代理规则时性能更好。</p>
<p>在未来的版本中，ipvs会提供更多的负载均衡算法，包括：</p>
<ol>
<li>rr：循环选择</li>
<li>lc：最少连接（least connection）</li>
<li>dh：目的地哈希（destination hashing）</li>
<li>sh：源哈希（destination hashing）</li>
<li>sed：最短预期延迟（shortest expected delay）</li>
<li>nq：绝不排队（never queue）</li>
</ol>
<p>注意，该模式要求节点上安装了IPVS内核模块。如果该模块没有安装，会自动fallback为iptables模式。</p>
<p>基于IPVS的K8S Service，都是使用NAT模式，原因是，只有NAT模式才能进行端口映射，让Sevice端口和Pod端口不一致。</p>
<p>IPVS模式下，客户端角色是当前节点（中的Pod，或者节点本身），Direct角色是当前节点，Real Server是当前或其它节点上的Pod。不同客户端，对应的网络路径不一样。<span style="background-color: #c0c0c0;">IPVS模式仍然需要iptables规则进行一些配合</span>。</p>
<p>考虑Service 10.96.54.11:8080，Pod 10.244.1.2:8080，基于Flannel构建容器网络，当前flannel.1的IP地址为10.244.0.0：</p>
<ol>
<li>当前节点<span style="background-color: #c0c0c0;">出现一个虚拟网络接</span>口<pre class="crayon-plain-tag">kube-ipvs0</pre>，所有Service IP都绑定在上面，这是IPVS所要求的，Direct必须具有VIP。这也<span style="background-color: #c0c0c0;">导致了IPVS模式下，VIP可Ping</span>（iptables模式下不可以）</li>
<li>OUTPUT链被添加如下规则：<br />
<pre class="crayon-plain-tag">-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES

# 如果源IP不是Pod IP，且目的IP是K8S Service（所有Service IP放在IPSet中，大大减少iptables规则条目数量）
-A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment 
  --comment "Kubernetes service cluster ip + port for masquerade purpose" 
#                                               则打上标记
  -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ

-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000</pre></p>
<p>不是Pod发出的对Service IP的请求，<span style="background-color: #c0c0c0;">打标记0x4000/0x4000的目的是，后续进行SNAT</span>。因为节点自身（而非其中的Pod）对VIP进行访问时，因为VIP就在本机网卡，所以它<span style="background-color: #c0c0c0;">自动设置IP封包的源地址、目的地址均为10.96.54.11，这样的封包不做SNAT，仅仅DNAT到Pod上，回程报文是发不回来的</span>（因为每个节点都具有所有VIP）</p>
</li>
<li>POSTROUTING链被添加如下规则：<br />
<pre class="crayon-plain-tag">-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING

-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" 
    -m mark --mark 0x4000/0x4000 -j MASQUERADE</pre></p>
<p>如第2条所属，需要作SNAT， MASQUERADE意味着源地址取决于封包出口网卡</p>
</li>
</ol>
<p>为了更深入的理解，可以看看地址映射的过程，在节点上访问curl http://10.96.54.11:8080时：</p>
<ol>
<li>产生如下封包： 10.96.54.11:xxxxx -&gt; 10.96.54.11:8080。原因是目标地址在kube-ipvs0这个本机网卡上</li>
<li>经过IPVS模块，选择一个RIP，进行DNAT：10.96.54.11:xxxxx -&gt; 10.244.2.2:8080</li>
<li>经过OUTPUT、POSTROUTING链，进行SNAT：10.244.0.0:xxxxx -&gt; 10.244.2.2:8080 。10.244.0.0是出口网卡flannel.1的地址，封包发给此网卡后，经过内核包装为vxlan UDP包，再由物理网卡发出去</li>
</ol>
<div class="blog_h3"><span class="graybg">IP和VIP</span></div>
<p>Pod具有普通的IP地址，因为针对此IP的请求被路由到固定的位置。</p>
<p>Service的IP则是虚拟的 —— 它不会由某台固定的主机来应答。K8S使用Linux的iptables来定义透明、按需进行重定向的VIP。当客户端连接到VIP时，流量会透明的传输给适当的端点。Service相关的环境变量、DNS条目使用的都是Service的VIP和端口。</p>
<div class="blog_h3"><span class="graybg">小结</span></div>
<p>不管使用哪种代理模式，均保证：</p>
<ol>
<li>任何从Service的集群IP:Port进入的流量，都被转发给适当的后端Pod。客户端不会感知到K8S、Service、Pod这些内部组件的存在</li>
<li>要实现基于客户端IP的会话绑定（session affinity ），可以设置service.spec.sessionAffinity = ClientIP （默认None）。你可以进一步设置service.spec.sessionAffinityConfig.clientIP.timeoutSeconds来指定会话绑定的最长时间（默认10800）</li>
</ol>
<div class="blog_h2"><span class="graybg">IPVS</span></div>
<p>由于Iptables底层数据结构是链表，而IPVS则是哈希，IPVS比起iptables模式显著提高的性能，几乎不受服务规模增大的影响。现在的集群都使用IPVS模式的服务代理。</p>
<p>IPVS支持<a href="/keepalived-faq#ipvs-mode">三种工作模式</a>：DR（直接路由）、Tunneling（ipip）、NAT（Masq）。其中只有NAT模式支持端口映射（ClusterIP端口和Pod端口不一样），因此K8S使用NAT模式。此外，K8S还在NAT的DNAT（将对服务的请求的目的地址改为Pod IP）的<span style="background-color: #c0c0c0;">基础上，进行SNAT（将对服务的请求的源地址改为入站网络接口的地址，否则Pod直接发送源地址为Pod IP的回程报文，客户端不接受）</span>，尽管fullNAT这种IPVS扩展可以支持DNAT+SNAT。</p>
<div class="blog_h3"><span class="graybg">实现原理</span></div>
<p>IPVS是<span style="background-color: #c0c0c0;">在INPUT链上挂了钩子，运行复杂的负载均衡算法</span>，然后执行DNAT后从FORWARD链离开本机网络栈。</p>
<p>让网络包进入INPUT链有两种方式：</p>
<ol>
<li>将虚IP写到内核路由表中</li>
<li>创建一个Dummy网卡，将续IP绑定到此网卡</li>
</ol>
<p>Kube Proxy使用的是第二种方式。一旦Service对象创建，Kube proxy就会：</p>
<ol>
<li>确保Dummy网卡kube-ipvs0存在</li>
<li>将服务的虚IP绑定给kube-ipvs0</li>
<li>通过socket创建IPVS的virtual server。virtual server和service是N:1关系，原因是Service可能有多个虚拟IP，例如LoadBalancer IP + Cluster IP</li>
</ol>
<p>当Endpoint对象创建后，Kube Proxy会：</p>
<ol>
<li>创建IPVS的real server。real server和endpoint是1:1关系</li>
</ol>
<div class="blog_h3"><span class="graybg">关于iptables</span></div>
<p>即使使用IPVS模式，只能解决流量转发的问题。Kube Proxy的其它问题仍然依靠 Iptables 解决，它在以下情况下依赖Iptables：</p>
<ol>
<li>如果配置启动参数<pre class="crayon-plain-tag">--masquerade--all=true</pre>，也就是所有经过kube-proxy的包都进行一次SNAT</li>
<li>启动参数指定了集群IP地址范围</li>
<li>对于LoadBalancer类型的服务，需要Iptables配置白名单</li>
<li>对于NodePort类型的服务，用于在封包从入站节点发往其它节点的Pod时进行MASQUERADE</li>
</ol>
<div class="blog_h3"><span class="graybg">关于ipset</span></div>
<p>Kube Proxy使用ipset来减少需要创建的iptables规则，IPVS模式下iptables规则的数量不超过5个 </p>
<div class="blog_h2"><span class="graybg">多端口服务</span></div>
<p>很多服务需要暴露多个端口，K8S支持在Service的规格中指定多个端口：</p>
<pre class="crayon-plain-tag">kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  # 端口1
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  # 端口2
  - name: https
    protocol: TCP
    port: 443
    targetPort: 9377</pre>
<div class="blog_h2"><span class="graybg">指定IP地址</span></div>
<p>在创建Service时，你可以指定你想要的集群IP，设置spec.clusterIP字段即可。</p>
<p>你选择的IP地址必须在service-cluster-ip-range（APIServer参数）范围内，如果IP地址不合法，APIServer会返回响应码422。</p>
<div class="blog_h2"><span class="graybg">发现服务</span></div>
<p>K8S提供两种途径，来发现一个服务：环境变量、DNS。</p>
<div class="blog_h3"><span class="graybg">基于环境变量</span></div>
<p>当Pod运行时，Kubelet会以环境变量的形式注入每个活动的服务的信息。环境变量名称格式为{SVCNAME}_SERVICE_HOST、{SVCNAME}_SERVICE_PORT。其中SVCNAME是转换为大写、-转换为_的服务名。</p>
<p>例如服务redis-master，暴露TCP端口6379，获得集群IP 10.0.0.11。它会产生以下环境变量：</p>
<pre class="crayon-plain-tag">REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11</pre>
<p>需要注意：服务必须在Pod之前就创建好，否则Pod的环境变量无法更新。 </p>
<div class="blog_h3"><span class="graybg">DNS</span></div>
<p>K8S提供了DNS Addon。此DNS服务器会监控新创建的Service，并为其提供DNS条目。集群中所有Pod都可以使用该DNS服务。</p>
<p>位于名字空间my-ns中的服务my-svc，创建的DNS条目是my-svc.my-ns。位于名字空间my-ns中的Pod可以直接通过my-svc访问服务，其它名字空间中的Pod则需要使用my-svc.my-ns访问服务。DNS名称解析的结果是服务的Cluster IP。</p>
<p>要访问ExternalName类型的Service，DNS是唯一的途径。</p>
<div class="blog_h2"><span class="graybg">Headless Service</span></div>
<p>某些情况下，你不需要Service提供的负载均衡功能，也不需要单个Service IP。这种情况下你可以创建Headless Service，其实就是设置spec.clusterIP=None。</p>
<p>使用HS，开发者可以减少和K8S的耦合，自行实现服务发现机制。</p>
<p>HS没有Cluster IP，kube-proxy也不会处理这类服务，K8S不会为这类服务提供负载均衡机制。</p>
<p>DNS记录如何创建，取决于HS有没有配置选择器。</p>
<div class="blog_h3"><span class="graybg">有选择器</span></div>
<p>如果定义了选择器，端点控制器为HS创建Endpoints对象。并且修改DNS配置，返回A记录（地址），直接指向HS选择的Pod。</p>
<div class="blog_h3"><span class="graybg">无选择器</span></div>
<p>端点控制器不会为这类Service创建Endpoints对象，但是DNS系统会配置：</p>
<ol>
<li>为ExternalName类型的Service配置CNAME记录</li>
<li>对于其它类型的Service，为和Service共享名称的任何Endpoints创建一条DNS记录</li>
</ol>
<p>对于无选择器的HS，不会自动创建关联的Endpoints，因此我们有机会手工的关联Endpoints，从而实现：</p>
<ol>
<li>指向一个集群外部的数据库</li>
<li>指向其它namespace或集群的服务</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">kind: Endpoints
apiVersion: v1
metadata:
  name: my-service
subsets:
- addresses:
  - ip: 1.2.3.4
  ports:
  - port: 1234</pre>
<p>Endpoint的地址不能是Loopback、link-local、link-local多播地址。 </p>
<div class="blog_h2"><span class="graybg">发布服务</span></div>
<p>对于应用程序中的某些部分（前端），你通常需要在外部（集群外部）IP上暴露一个服务。</p>
<p>K8S允许你指定为服务指定类型（ServiceTypes）：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ClusterIP</td>
<td>
<p>在集群内部的IP地址上暴露服务，如果你仅希望在集群内部能访问到服务，则使用该类型</p>
<p>这是默认的类型</p>
</td>
</tr>
<tr>
<td>NodePort</td>
<td>
<p>在所有节点的IP上的一个静态端口上暴露服务。K8S会自动创建一个ClusterIP类型的服务，用于把NodePort类型的服务路由到适当的Pod</p>
<p>外部可以将任何Node作为入口，访问此类型的服务</p>
<p>K8S会自动分配一个范围内的端口（默认30000-32767），每个节点都会转发到此端口的请求。此端口表现在spec.ports[*].nodePort字段中</p>
<p>如果你需要手工指定端口，设置nodePort的值即可。手工指定的值必须在允许的端口范围之内，你还需要注意可能存在的端口冲突。<span style="background-color: #c0c0c0;">建议自动分配端口</span></p>
<p>服务可以通过以下两种方式访问到：</p>
<ol>
<li>:spec.ports[*].nodePort</li>
<li>spec.clusterIP:spec.ports[*].port</li>
</ol>
<p>NodePort是丐版的LoadBalancer，可供外部访问且成本低廉。但是在大规模集群上</p>
</td>
</tr>
<tr>
<td>LoadBalancer</td>
<td>
<p>向外部暴露服务，并使用云服务提供的负载均衡器。外部负载均衡器所需要的NodePort，以及ClusterIP会自动创建</p>
<p>服务的EXTERNAL-IP字段为云提供商的负载均衡器的IP</p>
<p>某些云服务（GCE、AWS等）提供负载均衡器，K8S支持和这种外部LB集成：</p>
<pre class="crayon-plain-tag">kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - protocol: TCP
    port: 80 
    targetPort: 9376
  clusterIP: 10.0.171.239
  # 某些云服务允许手工指定LB的IP地址。如果不指定则随机选择，如果指定了但是云服务不支持则忽略该字段
  loadBalancerIP: 78.11.24.19
  type: LoadBalancer
  loadBalancerSourceRanges:
  # 允许哪些地址访问此服务，此字段可以随时更新
  - 1.1.1.1/32
status:
  # 负载均衡器会异步的创建，其信息会发布到如下字段：
  loadBalancer:
    ingress:
    - ip: 146.148.47.155</pre>
</td>
</tr>
<tr>
<td>ExternalName</td>
<td>将外部服务映射为别名</td>
</tr>
</tbody>
</table>
<p>注：修改服务的类型，不会导致ClusterIP重新分配。
<div class="blog_h2"><span class="graybg">外部IP</span></div>
<p>如果你拥有一个或多个外部（例如公网）<span style="background-color: #c0c0c0;">IP路由到集群的某些节点</span>，<span style="background-color: #c0c0c0;">则Service可以在这些外部IP上暴露</span>。基于这些IP流入的流量，如果其端口就是Service的端口，会被K8S路由到服务的某个端点（Pod ...）上。</p>
<p>例如下面这个服务：</p>
<pre class="crayon-plain-tag">kind: Service
apiVersion: v1
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 9376
  externalIPs:
  - 80.11.12.10</pre>
<p>外部客户端可以通过80.11.12.10:80访问服务，流量会被转发给app=MyApp的Pod的9376端口。 </p>
<div class="blog_h2"><span class="graybg">局部性访问</span></div>
<p>当访问NodePort、LoadBalancer类型的Service时，流量到达节点后，可能再被转发到其它节点，从而增加了一跳。</p>
<p>要避免这种情况，可以设置：</p>
<pre class="crayon-plain-tag">kind: service
spec:
  #                默认  Cluster
  externalTrafficPolicy: Local</pre>
<p>这样，K8S会强制将流量发往具有Service后端Pod的端点。注意ClusterIP类型的服务不支持这种局部性访问配置。</p>
<div class="blog_h2"><span class="graybg">Endpoints</span></div>
<p>服务的端点，通常情况下对应到Pod的一个端口。带有选择器的Service通常自动管理Endpoint对象，你也可以<a href="#service-without-selector">定义不使用选择器的服务</a>，这种情况下需要手工管理端点，Endpoints对象的名字和Service的名字要一致，位于相同命名空间。</p>
<div class="blog_h2"><span class="graybg">EndpointSlice</span></div>
<p>所有网络端点都保存在同一个 Endpoints 资源中，这类资源可能变得非常巨大，进而影响控制平面组件性能，EndpointSlice可以<span style="background-color: #c0c0c0;">缓解这一（大量端点）问题</span>。此外涉及如何<span style="background-color: #c0c0c0;">路由内部流量时，EndpointSlice 可以充当 kube-proxy 的决策依据</span>。 </p>
<p>默认情况下，单个端点切片最多包含100个端点，可以通过控制器管理器的标记 --max-endpoints-per-slice来定制。</p>
<p>下面是端点切片对象的例子：</p>
<pre class="crayon-plain-tag">apiVersion: discovery.k8s.io/v1beta1
kind: EndpointSlice
metadata:
  name: example-abc
  labels:
    kubernetes.io/service-name: example
# 端点的地址类型：支持IPv4  IPv6  FQDN
addressType: IPv4
# 端口列表，适用于切片中所有端点
ports:
  - name: http
    protocol: TCP
    port: 80
endpoints:
  - addresses:
    - "10.1.2.3"
    # EndpointSlice存储了可能对使用者有用的、有关端点的状况。 这三个状况分别是
    #   ready        对应Pod的Ready状态，运行中的Pod的Ready为True
    #   serving      如果Pod处于终止过程中则Ready不为True，此时应该查看serving获知Pod是否就绪
    #                例外是spec.publishNotReadyAddresses的服务，这种服务的端点的Ready永远为True
    #   terminating
    conditions:
      ready: true
    hostname: pod-1
    # 拓扑感知，1.20废弃
    topology:
      kubernetes.io/hostname: node-1   # 未来使用nodeName字段代替
      topology.kubernetes.io/zone: us-west2-a</pre>
<div class="blog_h3"><span class="graybg">管理</span></div>
<p>通常情况下，由端点切片控制器自动创建、管理 EndpointSlice 对象。EndpointSlice 对象还有一些其他使用场景， 例如作为服务网格的实现。这些场景都会<span style="background-color: #c0c0c0;">导致有其他实体 或者控制器负责管理额外的 EndpointSlice 集合</span>。</p>
<p>为了确保多个实体可以管理 EndpointSlice 而且不会相互产生干扰，Kubernetes 定义了 标签 <span style="background-color: #c0c0c0;">endpointslice.kubernetes.io/managed-by</span>，用来标明哪个实体在管理某个 EndpointSlice。端点切片控制器会在自己所管理的所有 EndpointSlice 上将该标签值设置 为 endpointslice-controller.k8s.io。 管理 EndpointSlice 的其他实体也应该为此标签设置一个唯一值。</p>
<div class="blog_h3"><span class="graybg">所属服务</span></div>
<p>在大多数场合下，EndpointSlice 都由某个 Service 所有，这一属主关系是通过：</p>
<ol>
<li>为每个 EndpointSlice 设置一个 owner引用</li>
<li>设置 kubernetes.io/service-name 标签来标明的，目的是方便查找隶属于某服务的所有 EndpointSlice</li>
</ol>
<div class="blog_h3"><span class="graybg">切片分布</span></div>
<p>每个 EndpointSlice 都有一组端口值，适用于资源内的所有端点。 当为服务使用命名端口时，<span style="background-color: #c0c0c0;">Pod 可能会就同一命名端口获得不同的端口号，因而需要 不同的 EndpointSlice</span>。</p>
<p>控制面尝试尽量将 EndpointSlice 填满，不过不会主动地在若干 EndpointSlice 之间 执行再平衡操作。这里的逻辑也是相对直接的：</p>
<ol>
<li>列举所有现有的 EndpointSlices，移除那些<span style="background-color: #c0c0c0;">不再需要的端点并更新那些已经变化的端点</span></li>
<li>列举所有在第一步中<span style="background-color: #c0c0c0;">被更改过的 EndpointSlices，用新增加的端点将其填满</span></li>
<li>如果还有新的端点未被添加进去，尝试<span style="background-color: #c0c0c0;">将这些端点添加到之前未更改的切片中， 或者创建新切片</span></li>
</ol>
<p>这里比较重要的是，与在 EndpointSlice 之间完成最佳的分布相比，第3步中更看重<span style="background-color: #c0c0c0;">限制 EndpointSlice 更新的操作次数</span>。例如，如果有 10 个端点待添加，有两个 EndpointSlice 中各有 5 个空位，上述方法会创建一个新的 EndpointSlice 而不是 将现有的两个 EndpointSlice 都填满。换言之，与执行多个 EndpointSlice 更新操作 相比较，方法会优先考虑执行一个 EndpointSlice 创建操作。</p>
<p>由于 kube-proxy 在每个节点上运行并监视 EndpointSlice 状态，<span style="background-color: #c0c0c0;">EndpointSlice 的 每次变更都变得相对代价较高，因为这些状态变化要传递到集群中每个节点上</span>。 这一方法尝试限制要发送到所有节点上的变更消息个数，即使这样做可能会导致有 多个 EndpointSlice 没有被填满。</p>
<p>在实践中，上面这种并非最理想的分布是很少出现的。大多数被 EndpointSlice 控制器 处理的变更都是足够小的，可以添加到某已有 EndpointSlice 中去的。并且，假使无法添加到已有的切片中，不管怎样都会快就会需要一个新的 EndpointSlice 对象。 Deployment 的滚动更新为 EndpointSlice重新打包提供了一个自然的机会，所有 Pod 及其对应的端点在这一期间都会被替换掉。</p>
<div class="blog_h3"><span class="graybg">重复的端点</span></div>
<p>由于 EndpointSlice 变化的自身特点，端点可能会同时出现在不止一个 EndpointSlice 中。鉴于不同的 EndpointSlice 对象在不同时刻到达 Kubernetes 的监视/缓存中， 这种情况的出现是很自然的。 使用 EndpointSlice 的实现必须能够处理端点出现在多个切片中的状况。 关于如何执行端点去重（deduplication）的参考实现，你可以在 kube-proxy 的 EndpointSlice 实现中找到。</p>
<div class="blog_h1"><span class="graybg">DNS支持</span></div>
<p>K8S支持在集群中调度DNS Service/Pod，并且让Kubelet告知每个容器，DNS服务器的IP地址。</p>
<p>集群中的每个服务，包括DNS服务本身，都被分配一个DNS名称。</p>
<p>默认的，Pod的DNS搜索列表会包含Pod自己的名字空间，以及集群的默认Domain。如果一个服务foo运行在名字空间bar中，则位于bar中的Pod可以通过foo引用服务，位于其它名字空间中的Pod则需要通过foo.bar引用服务。</p>
<div class="blog_h2"><span class="graybg">Service</span></div>
<div class="blog_h3"><span class="graybg">A记录</span></div>
<p>普通服务被分配以A记录，格式：my-svc.my-namespace.svc.cluster.local，此记录解析到服务的Cluster IP。</p>
<p>Headless服务（无Cluster IP）也被分配如上的A记录，但是记录解析到服务选择的Pod的IP集（如果没有选择器则解析到什么IP取决于你给出的Endpoints配置），客户端应当使用此IP集或者使用Round-Robin方式从IP集中选取IP。</p>
<div class="blog_h3"><span class="graybg">SRV记录</span></div>
<p>这类记录为服务的知名端口分配，SRV记录格式：_my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local。</p>
<p>对于普通服务，SRV记录解析为端口号 + CNAME（my-svc.my-namespace.svc.cluster.local）。</p>
<p>对于Headless服务，SVR记录解析到多个答复，每个对应服务选择的Pod：端口号 + Pod的CNAME（auto-generated-name.my-svc.my-namespace.svc.cluster.local）。</p>
<div class="blog_h2"><span class="graybg">Pods</span></div>
<div class="blog_h3"><span class="graybg">A记录</span></div>
<p>当启用后，Pod被分配一个DNS A记录：pod-ip-address.my-namespace.pod.cluster.local。例如：</p>
<pre class="crayon-plain-tag"># 一个名为influx的Pod，它位于dev名字空间中，IP地址为172.27.0.20
172-27-0-20.dev.pod.k8s.gmem.cc</pre>
<div class="blog_h3"><span class="graybg">主机名/子域</span></div>
<p>Pod被创建后，其hostname为metadata.name字段的值。</p>
<p>在Pod的Spec中你可以：</p>
<ol>
<li>指定可选的hostname字段，可以手工指定hostname，比metadata.name的优先级高</li>
<li>指定可选可选的subdomain字段，说明指定Pod的子域。例如一个Pod的hostname为influxdb，subdomain为pods，名字空间为default，则Pod的全限定名为influxdb.default.dev.svc.k8s.gmem.cc</li>
</ol>
<p>如果在Pod所在名字空间中，存在一个Headless服务，其名称与Pod的subdomain字段一致。则KubeDNS服务器会为Pod的全限定名返回一个A记录，指向Pod的IP地址。</p>
<div class="blog_h3"><span class="graybg">DNS策略</span></div>
<p>你可以为每个Pod定义DNS策略（在规格中使用dnsPolicy字段）。目前K8S支持以下策略：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">策略</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Default</td>
<td>从Pod所在节点继承DNS解析设置</td>
</tr>
<tr>
<td>ClusterFirst</td>
<td>任何不匹配集群DNS后缀（可以设置）的查询，转发给上游（从节点继承）DNS处理</td>
</tr>
<tr>
<td>ClusterFirstWithHostNet</td>
<td>运行在hostNetwork中的Pod应当明确的设置为该值</td>
</tr>
<tr>
<td>None</td>
<td>1.9 Alpha，忽略来自K8S环境的DNS设置。所有<span style="background-color: #c0c0c0;">DNS设置从Pod规格的dnsConfig字段获取</span></td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">DNS配置</span></div>
<p>1.9 Alpha。用户可以对Pod的DNS设置进行细粒度控制。要启用该特性，集群管理员需要为APIServer、kubelet启用特性开关（Feature Gate）CustomPodDNS，示例：--feature-gates=CustomPodDNS=true,...  从1.14开始默认开启。</p>
<p>当启用上述特性后，用户可以设置Pod规格的dnsPolicy=None，并添加dnsConfig字段：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  namespace: ns1
  name: dns-example
spec:
  containers:
    - name: test
      image: nginx
  dnsPolicy: "None"
  dnsConfig:
    # 此Pod使用的DNS服务器列表
    nameservers:
      - 1.2.3.4
    # DNS搜索后缀列表
    searches:
      - ns1.svc.cluster.local
      - my.dns.search.suffix
    options:
      # 最大dot数量。最大值15
      - name: ndots
        value: "1"
      # 重试另外一个DNS服务器之前，等待应答的最大时间，单位秒。最大值30
      - name: timeout
        value: "5"
      # 放弃并返回错误给调用者之前，最大尝进行DNS查询的次数。最大值5
      - name: attempts
        value: "2"
      # 使用round-robin风格选择不同的DNS服务器，向其发送查询
      - name: rotate
      # 支持RFC 2671描述的DNS扩展
      - name: edns0
      # 串行发送A/AAAA请求
      - name: single-request
      # 发送第二个请求时重新打开套接字
      - name: single-request-reopen
      # 使用TCP协议发送DNS请求
      - name: use-vc
      # 禁止自动重新载入修改后的配置文件，需要glibc 2.26+
      - name: no-reload</pre>
<div class="blog_h1"><span class="graybg">网络</span></div>
<div class="blog_h2"><span class="graybg">连接应用和服务</span></div>
<p>本节以Nginx服务为例，说明如何在K8S集群中连接应用程序和服务。</p>
<div class="blog_h3"><span class="graybg">K8S网络模型</span></div>
<p>在讨论K8S的通信机制之前，我们先看一下普通的Docker网络如何运作。</p>
<p>默认情况下，Docker使用Host-private的网络，也就是说Docker容器仅能和同一台宿主机上的其它容器通信。为了让容器跨节点通信，它们必须在宿主机上暴露端口。这意味着，开发者需要仔细规划，让多个容器协调暴露宿主机的不同端口。</p>
<p>跨越多名开发者进行端口协调很麻烦，特别是进行扩容的时候，这种协调工作应该由集群负责，而不应该暴露给用户。</p>
<p>K8S假设所有Pod都是需要相互通信的，不管它们是不是位于同一台宿主机上。K8S为每个Pod分配集群私有IP地址，你不需要向Docker那样，为容器之间创建连接，或者暴露端口到宿主机的网络接口。</p>
<p>Pod内部的多个容器，可以利用localhost进行相互通信。</p>
<p>集群内所有Pod中的容器，都可以相互看到，不需要NAT的介入。</p>
<div class="blog_h3"><span class="graybg">暴露Pod到集群</span></div>
<p>考虑下面的Pod规格：</p>
<pre class="crayon-plain-tag">apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80</pre>
<p>你可以从集群的任何节点访问此Pod，执行下面的命令获得Pod的IP地址：</p>
<pre class="crayon-plain-tag">kubectl get pods -l run=my-nginx -o wide
# NAME                        READY     STATUS    RESTARTS   AGE       IP            NODE
# my-nginx-3800858182-jr4a2   1/1       Running   0          13s       10.244.3.4    kubernetes-minion-905m
# my-nginx-3800858182-kna2y   1/1       Running   0          13s       10.244.2.5    kubernetes-minion-ljyd</pre>
<p>登陆到任何集群节点后，你都可以访问上面的两个IP地址。需要注意，Pod不会使用节点的80端口，也不会使用任何NAT机制来把流量路由到Pod。这意味着你可以在单个节点上运行containerPort相同的多个Pod。</p>
<p>你可以像Docker那样，把容器端口映射到节点端口，单由于K8S网络模型的存在，这种映射往往没有必要。</p>
<div class="blog_h3"><span class="graybg">创建服务</span></div>
<p>现在，我们有了两个Pod，它们运行着Nginx，IP位于一个扁平的、集群范围的地址空间中。 理论上说，应用程序可以和Pod直接通信，但是如何Pod所在节点宕机了会怎么样？Deployment会自动在另外一个节点上调度一个Pod，这个新的Pod的IP地址会改变。如果直接和Pod 通信，你就要处理这种IP地址可能改变的情形。</p>
<p>服务可以避免上述处理逻辑，它是Pod之上的抽象层，它定义了一个逻辑的Pod集，这些Pod提供一模一样的功能。当创建服务时，它被分配以唯一性的IP地址（Cluster IP），在服务的整个生命周期中，该IP都不会变化。</p>
<p>应用程序可以配置和服务（而非Pod）通信，服务会进行负载均衡处理，把请求转发给某个Pod处理。</p>
<p>针对上述Deployment的Service规格：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
    # 服务暴露的端口
  - port: 80
    # 对接到Pod的端口
    targetPort: 80
    protocol: TCP
  selector:
    run: my-nginx</pre>
<p>创建服务后，可以查询其状态：</p>
<pre class="crayon-plain-tag">kubectl get svc my-nginx
# NAME       CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
# my-nginx   10.0.162.149           80/TCP    21s</pre>
<p>服务的背后是一个Pod集，服务的选择器会不断的估算，来选择匹配的Pods。匹配的Pod会被发送到一个Endpoints对象中，该对象的名字也叫my-nginx（和服务同名）。当Pod死去时它会自动从Endpoints中移除，类似的当新Pod出生后也被加入到Endpoints中。 </p>
<p>下面的命令可以获得端点的信息：</p>
<pre class="crayon-plain-tag"># 获得服务关联的端点
kubectl describe svc my-nginx
# Name:                my-nginx
# Namespace:           default
# Labels:              run=my-nginx
# Annotations:         
# Selector:            run=my-nginx
# Type:                ClusterIP
# IP:                  10.0.162.149
# Port:                 80/TCP
# Endpoints:           10.244.2.5:80,10.244.3.4:80
# Session Affinity:    None
# Events:              

# 获得端点信息
kubectl get ep my-nginx
# NAME       ENDPOINTS                     AGE
# my-nginx   10.244.2.5:80,10.244.3.4:80   1m</pre>
<p>注意：端点的IP/Port就是Pod的IP/Port。</p>
<p>现在，你可以在集群中任何节点上 wget :，再次强调，集群IP是纯粹的虚拟IP，它不会对应到哪一根网线。 </p>
<div class="blog_h3"><span class="graybg">访问服务 </span></div>
<p>K8S提供了两种服务访问方式：环境变量、DNS。</p>
<p>变量变量方式是开箱即用的，当Pod启动时，Kubelet会为其注入一系列的环境变量。这意味着在Pod创建之后才创建的服务，无法注入为环境变量。</p>
<p>K8S有一个DNS Addon，负责提供集群内的域名服务，执行下面的命令了解此服务是否启用：</p>
<pre class="crayon-plain-tag"># 服务名为kube-dns，运行在kube-system名字空间
kubectl get services kube-dns --namespace=kube-system
# NAME       CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
# kube-dns   10.0.0.10            53/UDP,53/TCP   8m</pre>
<p>如果没有运行DNS服务，你需要<a href="http://releases.k8s.io/master/cluster/addons/dns/README.md#how-do-i-configure-it">启用它</a>。 </p>
<p>DNS服务运行后，会为每个服务都分配一个域名，映射到它的集群IP。</p>
<div class="blog_h3"><span class="graybg">服务安全</span></div>
<p>现在，在集群内部的任何节点，都可以正常访问服务，而且可以享受K8S提供的负载均衡、HA了。</p>
<p>在把服务暴露到因特网之前，你需要确保信道的安全性。例如：</p>
<ol>
<li>提供数字证书，要么购买要么自签名</li>
<li>配置Nginx的HTTPS</li>
<li>配置一个Secret，让Pod都能访问证书 </li>
</ol>
<p>创建密钥和自签名证书，可以参考：</p>
<pre class="crayon-plain-tag">openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /d/tmp/nginx.key -out /d/tmp/nginx.crt -subj "/CN=nginxsvc/O=nginxsvc" </pre>
<p>K8S提供了一些examples，可以简化第三步。执行下面的命令下载样例：</p>
<pre class="crayon-plain-tag">git clone https://github.com/kubernetes/examples.git</pre>
<p>下面使用examples中的工具，根据上面生成的密钥，产生一个Secret的规格文件：</p>
<pre class="crayon-plain-tag"># make和go必须已经安装
# 实际通过调用 make_secret.go 产生Secret，后者则是调用K8S的API
make keys secret KEY=/tmp/nginx.key CERT=/tmp/nginx.crt SECRET=/tmp/secret.json</pre>
<p>使用规格文件，在K8S上创建一个Secret对象：</p>
<pre class="crayon-plain-tag">kubectl create -f /tmp/secret.json
# secret "nginxsecret" created</pre>
<p>查看Secret对象列表：</p>
<pre class="crayon-plain-tag">kubectl get secrets
# NAME                  TYPE                                  DATA      AGE
# default-token-il9rc   kubernetes.io/service-account-token   1         1d
# nginxsecret           Opaque                                2         1m</pre>
<p>下一步，修改Deployment、Service的规格：</p>
<pre class="crayon-plain-tag"># Deployment规格
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: my-nginx
spec:
  replicas: 1
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      # 声明一个卷，引用Secret
      volumes:
      - name: secret-volume
        secret:
          secretName: nginxsecret
      containers:
      - name: nginxhttps
        image: bprashanth/nginxhttps:1.0
        ports:
        - containerPort: 443
        - containerPort: 80
        # 挂载Secret卷到对应位置
        volumeMounts:
        - mountPath: /etc/nginx/ssl
          name: secret-volume

# Service规格
apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  type: NodePort
  ports:
  - port: 8080
    targetPort: 80
    protocol: TCP
    name: http
  - port: 443
    protocol: TCP
    name: https
  selector:
    run: my-nginx</pre>
<div class="blog_h3"><span class="graybg">暴露服务</span></div>
<p>要把服务暴露到集群外部的IP地址，可以使用NodePorts或者LoadBalancers。 </p>
<div class="blog_h2"><span class="graybg">Ingress</span></div>
<p>这是一个API对象，可以管理到集群内部服务（通常是HTTP）的访问。Ingress能够提供：负载均衡、SSL Termination、基于名称的虚拟主机服务。</p>
<div class="blog_h3"><span class="graybg">何为Ingress</span></div>
<p>典型情况下，Service/Pod的IP地址仅仅能在集群内部路由到，所有流量到达边缘路由器（Edge Router，为集群强制应用了防火墙策略的路由器，可能是云服务管理的网关，也可能是物理硬件）时要么被丢弃，要么被转发到别的地方。</p>
<p>Ingress（入口）是一个规则集，允许进入集群的连接到达对应的集群服务。</p>
<p>要定义Ingress对象，同样需要经过APIServer。一个Ingress控制器负责处理Ingress。</p>
<div class="blog_h3"><span class="graybg">前提条件</span></div>
<p>Ingress从1.1+开始可用，目前处于Beta版本。</p>
<p>要使用Ingress，你需要配备Ingress控制器。你可以在Pod中部署任意数量的自定义<a href="https://github.com/kubernetes/ingress-nginx/blob/master/README.md">Ingress控制器</a>，你需要为每个控制器标注适当的class。</p>
<div class="blog_h3"><span class="graybg">Ingress规格</span></div>
<pre class="crayon-plain-tag">apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    ingress.kubernetes.io/rewrite-target: /
# 规格中包含了创建LB或代理服务器所需的全部信息
spec:
  # 匹配入站请求的规则列表
  rules:
  # 目前仅支持HTTP规则
  - http:
      # 将来自任何域名的/testpat这个URL下的请求，都转发给test服务处理
      host: *
      paths:
      - path: /testpath
        backend:
          serviceName: test
          servicePort: 80</pre>
<div class="blog_h3"><span class="graybg">Ingress控制器</span></div>
<p>注意：<span style="background-color: #c0c0c0;">Ingress控制器可以响应不同名字空间中的多个Ingress</span>。</p>
<p>为了让Ingress对象能生效，集群中必须存在Ingress控制器。Ingress实际上<span style="background-color: #c0c0c0;">仅仅声明了转发规则</span>，没有能执行转发的程序。</p>
<p>大部分控制器都是kube-controller-manager这个可执行程序的一部分，并且随着集群的创建而自动启动。但是Ingress控制器不一样，它作为Pod运行，你需要根据集群的需要来选择一个Ingress控制器实现，或者自己实现一个。</p>
<p>Ingress Controller的实现非常多，大多以Nginx或Envoy为基础。可以参考文章：<a href="https://medium.com/flant-com/comparing-ingress-controllers-for-kubernetes-9b397483b46b">Comparing Ingress controllers for Kubernetes</a>。</p>
<p>K8S目前实现并维护<a href="https://git.k8s.io/ingress-gce/README.md">GCE</a>和<a href="nginx">nginx</a>两个Ingress控制器。下面是基于Nginx的示例：</p>
<pre class="crayon-plain-tag"># 默认后端，如果Nginx无处理请求，此后端负责兜底
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: nginx-ingress
    chart: nginx-ingress-0.14.1
    component: default-backend
  name: nginx-ingress-default-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-ingress
      component: default-backend
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx-ingress
        component: default-backend
    spec:
      containers:
      - image: k8s.gcr.io/defaultbackend:1.3
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 30
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 5
        name: nginx-ingress-default-backend
        ports:
        - containerPort: 8080
          protocol: TCP
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 60

---

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: nginx-ingress
    component: default-backend
  name: nginx-ingress-default-backend
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: nginx-ingress
    component: default-backend
  sessionAffinity: None
  type: ClusterIP

---

apiVersion: v1
data:
  enable-vts-status: "false"
kind: ConfigMap
metadata:
  labels:
    app: nginx-ingress
    component: controller
  name: nginx-ingress-controller

---
# 基于Nginx的Ingress Controller
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: nginx-ingress
    component: controller
  name: nginx-ingress-controller
spec:
  selector:
    matchLabels:
      app: nginx-ingress
      component: controller
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx-ingress
        component: controller
    spec:
      containers:
      - args:
        - /nginx-ingress-controller
        - --default-backend-service=kube-system/nginx-ingress-default-backend
        - --election-id=ingress-controller-leader
        - --ingress-class=nginx
        - --configmap=kube-system/nginx-ingress-controller
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
        image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.12.0
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 10254
            scheme: HTTP
          initialDelaySeconds: 10
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        name: nginx-ingress-controller
        ports:
        - containerPort: 80
          name: http
          protocol: TCP
        - containerPort: 443
          name: https
          protocol: TCP
        readinessProbe:
          failureThreshold: 3
          httpGet:
            path: /healthz
            port: 10254
            scheme: HTTP
          initialDelaySeconds: 10
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      terminationGracePeriodSeconds: 60

---
# 在裸金属集群上使用NodePort服务，将所有节点的80/443暴露出去，任何节点都可以作为Ingress
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: nginx-ingress
    component: controller
  name: nginx-ingress-controller
spec:
  externalTrafficPolicy: Cluster
  ports:
  - name: http
    # 此服务在所有节点上暴露的端口
    nodePort: 80
    # 此服务在集群IP上暴露的端口
    port: 80
    protocol: TCP
    # 上述两套80端口，转发给Pod的什么端口
    targetPort: 80
  - name: https
    nodePort: 443
    port: 443
    protocol: TCP
    targetPort: 443
  selector:
    app: nginx-ingress
    component: controller
  sessionAffinity: None
  type: NodePort</pre>
<p>此控制器配合的Ingress如下：</p>
<pre class="crayon-plain-tag"># kubectl create secret generic gmemk8scert --from-file=./k8s.gmem.cc

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    # 如果后端服务本身就是以HTTPS方式暴露，则需要添加下面这行：
    nginx.ingress.kubernetes.io/secure-backends: "true"
spec:
  tls:
  - hosts:
    - media-api.k8s.gmem.cc
    secretName: gmemk8scert
  rules:
  - host: media-api.k8s.gmem.cc
    http:
      paths:
      - backend:
          serviceName: media-api
          servicePort: 8800</pre>
<div class="blog_h3"><span class="graybg">运行多个控制器</span></div>
<p>如果Ingress资源上没有注解，则任何Ingress控制器都会处理此Ingress。</p>
<p>如果你希望仅仅让Nginx/Nginx Plus控制器来处理Ingress，则为后者添加Annotation：</p>
<pre class="crayon-plain-tag">kubernetes.io/ingress.class: "nginx"</pre>
<p> 这样，其它种类的Ingress Controller自动忽视此Ingress。</p>
<div class="blog_h3"><span class="graybg">ng控制器命令行</span></div>
<p>--configmap 指定配置的namespace/name</p>
<p>--default-ssl-certificate 指定包含默认数字整数的secret</p>
<p>--enable-dynamic-configuration 通过使用Lua，尽可能的避免Nginx Reload操作的必要，某些特性必须要求启用或禁用此特性</p>
<p>--enable-ssl-passthrough 是否启用SSL穿透的特性，默认禁用</p>
<p>--status-port Nginx状态信息在什么端口暴露，默认18080</p>
<p>--watch-namespace  监控哪些命名空间的Ingress资源，默认所有命名空间</p>
<p>--v 更多日志：</p>
<p style="padding-left: 30px;">2 显示配置文件的变动diff</p>
<p style="padding-left: 30px;">3 显示服务、Ingress规则、endpoint的变化细节，并输出为JSON</p>
<p style="padding-left: 30px;">5 让Nginx在Debug模式下运行</p>
<div class="blog_h3"><span class="graybg">ng控制器配置</span></div>
<p>配置文件通过参数--configmap=kube-system/ngress-controller参数注入。ConfigMap示意：</p>
<pre class="crayon-plain-tag">apiVersion: v1
data:
  # 返回客户端时添加响应头
  add-headers: "false"
  # 指定返回客户端时需要隐藏的头
  hide-headers: ""
  # 请求的缓冲和超时
  client-header-buffer-size: ""
  client-header-timeout: ""
  client-body-buffer-size: ""
  client-body-timeout: ""
  enable-vts-status: "false"
  ssl-redirect: "false"
  # 启用压缩
  use-gzip
  gzip-types: ""
  # 负载均衡算法
  # 可选：
  # least_conn 最少连接，不得联合--enable-dynamic-configuration
  # ip_hash 不得联合--enable-dynamic-configuration，可以用nginx.ingress.kubernetes.io/upstream-hash-by代替
  # ewma 峰值指数加权移动平均值算法，联合--enable-dynamic-configuration
  load-balance: "round_robin"
kind: ConfigMap</pre>
<p>可用配置项非常多，可查看<a href="https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/configmap.md">官方文档</a>。 这些配置最终会填充到nginx配置文件模板中，<a href="https://github.com/kubernetes/ingress-nginx/blob/master/internal/ingress/controller/config/config.go">模板变量和配置项名称并不对应</a>，需要注意。</p>
<div class="blog_h3"><span class="graybg">ng控制器的ingress注解</span></div>
<p>注解列表（默认前缀都是nginx.ingress.kubernetes.io/），这些注解可以附加在使用Nginx作为控制器的Ingress规则上：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">注解</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>affinity</td>
<td>
<p>启用针对上游服务的会话绑定：</p>
<pre class="crayon-plain-tag"># 基于什么绑定，仅支持cookie
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "INGRESSCOOKIE"
# Cookie值的哈希算法，sha1/md5/index
nginx.ingress.kubernetes.io/session-cookie-hash: "sha1" </pre>
</td>
</tr>
<tr>
<td>auth-tls-pass-certificate-to-upstream</td>
<td>是否把客户端证书转发给上游服务，默认false</td>
</tr>
<tr>
<td>default-backend</td>
<td>指定默认后端，这个后端处理Ingress规则没有匹配的任何请求</td>
</tr>
<tr>
<td>configuration-snippet</td>
<td>
<p>指定任意的Nginx配置片断，示例：
<pre class="crayon-plain-tag">kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/configuration-snippet: |
      location /avatar {
        rewrite  ^(.*)$ /public/$1.png;
      }
spec:
  rules:
    - host: grafana.k8s.gmem.cc</pre>
<p>对于上述Ingress，控制器会在自己的Nginx配置文件中生成：</p>
<pre class="crayon-plain-tag">http {
#...
    ## start server grafana.k8s.gmem.cc                                                                                                                                                  
    server {                                                                                                                                                                             
        server_name grafana.k8s.gmem.cc ;
        location / {
          # ...
          # 你定义的片断在这里
             location /avatar {
                 rewrite  ^(.*)$ /public/$1.png;
             }
        }
    }
}</pre>
<p>如果配置片断有错误，你会在控制器日志中发现：</p>
<p style="padding-left: 30px;">nginx: configuration file /tmp/nginx-*** test failed </p>
</td>
</tr>
<tr>
<td>enable-cors</td>
<td>
<p>是否启用跨站资源共享（CORS）：</p>
<pre class="crayon-plain-tag">nginx.ingress.kubernetes.io/enable-cors: "true"
# 允许哪些HTTP方法
nginx.ingress.kubernetes.io/cors-allow-methods: "PUT, GET, POST, OPTIONS"
# 允许哪些头
nginx.ingress.kubernetes.io/cors-allow-headers: "X-Forwarded-For, X-app123-XPTO"
# 允许哪些源
nginx.ingress.kubernetes.io/cors-allow-origin: "https://gmem.cc:5443"
# 是否允许凭证信息被跨站传递
nginx.ingress.kubernetes.io/cors-allow-credentials: "true" </pre>
</td>
</tr>
<tr>
<td>
<p>force-ssl-redirect
<p>ssl-redirect</p>
</td>
<td>
<p>如果某个Ingress启用了TLS，则默认会发送308永久重定向，将HTTP请求重定向到HTTPS，如果需要禁用此行为，可以在Nginx ConfigMap中设置：</p>
<pre class="crayon-plain-tag"># kubectl -n kube-system edit configmap nginx-ingress-controller
data:
  enable-vts-status: "false"
  ssl-redirect: "false"</pre>
<p>如果要针对某个Ingress禁用SSL重定向，可以为此Ingress资源配置：</p>
<pre class="crayon-plain-tag">nginx.ingress.kubernetes.io/ssl-redirect: "false"</pre>
<p>默认值true</p>
</td>
</tr>
<tr>
<td>ssl-passthrough</td>
<td>
<p>SSL穿透，允许在Pod上配置SSL服务端证书，而不是在Nginx上</p>
</td>
</tr>
<tr>
<td>limit-connections<br />limit-rps<br />limit-rpm </td>
<td>
<p>限制单个Client IP能够使用的资源量，用于缓和DDoS攻击：</p>
<p style="padding-left: 30px;">limit-connections 单IP最大连接<br />limit-rps 单IP没秒最多被接受的连接<br />limit-rpm 单IP每分最多被接受的连接</p>
</td>
</tr>
<tr>
<td>rewrite-target</td>
<td>
<p>某些情况下后端服务暴露的URL，和Ingress规则中的不同。如果不进行请求重写会导致404。示例：</p>
<pre class="crayon-plain-tag">kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
  name: monocular
spec:
  rules:
  - host: monocular.jx.k8s.gmem.cc
    http:
      paths:
      - backend:
          serviceName: jenkins-x-monocular-api
          servicePort: 80
        # monocular.jx.k8s.gmem.cc/api被重写为jenkins-x-monocular-api/
        path: /api/
      - backend:
          serviceName: jenkins-x-monocular-ui
          servicePort: 80
        path: /</pre>
</td>
</tr>
<tr>
<td>secure-backends</td>
<td>
<p>默认情况下Nginx使用HTTP协议访问后端服务，如果后端服务通过HTTPS暴露，需要在Ingress规则中设置：
<pre class="crayon-plain-tag">nginx.ingress.kubernetes.io/secure-backends: "true"</pre>
</td>
</tr>
<tr>
<td>service-upstream</td>
<td>
<p>默认情况下，在Nginx配置中使用Pod的ip:port作为upstream服务器
<p>设置此注解为true，则改用Service的集群IP，这可以避免反复Reload Nginx配置，防止Pod反复宕机导致可用性下降。但是需要注意：</p>
<ol>
<li>不能支持会话绑定</li>
</ol>
</td>
</tr>
<tr>
<td>load-balance</td>
<td>使用何种负载均衡算法，最新版本的balancer.lua脚本中引用，可以选择chash、round_robin、sticky、ewma</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Ingress类型</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>单服务</td>
<td>
<p>现有的Service类型可以向集群外部暴露单个服务。但是你也可以利用Ingress实现这种暴露：</p>
<pre class="crayon-plain-tag">apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
spec:
  # 指定没有任何规则的后端即可
  backend:
    serviceName: testsvc
    servicePort: 80</pre>
<p>创建Ingress对象后，执行下面的命令可以查看其状态：</p>
<pre class="crayon-plain-tag">kubectl get ing
# NAME                RULE          BACKEND        ADDRESS
# test-ingress        -             testsvc:80     107.178.254.228</pre>
<p>其中107.178.254.228是Ingress控制器为此Ingress分配的IP地址。RULE为 - 表示，所有发送到107.178.254.228的流量都被重定向给testsvc服务 </p>
</td>
</tr>
<tr>
<td>Fanout</td>
<td>
<p>前面提到过，Pod仅拥有集群内可用的IP，因此我们需要在集群的边界有那么一个组件，能够总揽所有入站请求，并转发给适当的端点，该组件常常是一个HA的负载均衡器。Ingress可以让你需要的负载均衡器数量尽可能小，例如下面这个流向示意：</p>
<pre class="crayon-plain-tag">foo.bar.com -&gt; 178.91.123.132 -&gt; / foo    s1:80
                                 / bar    s2:80</pre>
<p>对应的Ingress规格：</p>
<pre class="crayon-plain-tag">apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test
  annotations:
    ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  # 对于指向foo.bar.com的请求
  - host: foo.bar.com
    http:
      paths:
      # 把路径/foo下的HTTP请求转发给服务s1处理
      - path: /foo
        backend:
          serviceName: s1
          servicePort: 80
      # 把路径/bar下的HTTP请求转发给服务s2处理
      - path: /bar
        backend:
          serviceName: s2
          servicePort: 80</pre>
<p>创建上述Ingress后，查看其状态：</p>
<pre class="crayon-plain-tag">kubectl get ing
# NAME      RULE          BACKEND   ADDRESS
# test      -
#           foo.bar.com
#           /foo          s1:80
#           /bar          s2:80</pre>
<p>Ingress控制器会提供一个负载均衡器（依赖于实现），以满足Ingress的规定。 ADDRESS列存放的是负载均衡器的地址</p>
</td>
</tr>
<tr>
<td>虚拟主机</td>
<td>
<p>根据URL中主机名字段的不同，将请求转发给不同的服务：</p>
<pre class="crayon-plain-tag">apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test
spec:
  rules:
  # 虚拟主机1
  - host: foo.bar.com
    http:
      paths:
      - backend:
          serviceName: s1
          servicePort: 80
  # 虚拟主机2
  - host: bar.foo.com
    http:
      paths:
      - backend:
          serviceName: s2
          servicePort: 80</pre>
<p>默认后端（Default Backend）：所有不匹配任何规则的流量都被发送给默认后端，你可以使用默认后端来提供404页面</p>
</td>
</tr>
<tr>
<td>TLS</td>
<td>
<p>通过提供一个包含TLS私钥、证书的Secret，你可以让Ingress支持HTTPS。当前Ingress仅仅支持单个TLS端口（443）</p>
<p>Secret对象规格示例：</p>
<pre class="crayon-plain-tag">apiVersion: v1
data:
  tls.crt: base64 encoded cert
  tls.key: base64 encoded key
kind: Secret
metadata:
  name: testsecret
  namespace: default
type: Opaque</pre>
<p>启用TLS的Ingress的示例：</p>
<pre class="crayon-plain-tag">apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: no-rules-map
spec:
  tls:
  - secretName: testsecret
  backend:
    serviceName: s1
    servicePort: 80 </pre>
</td>
</tr>
<tr>
<td>负载均衡</td>
<td>
<p>Ingress控制器自带某些负载均衡设置，会应用到所有Ingress，包括：负载均衡算法、后端权重规则（backend weight scheme）。更高级的负载均衡，例如持久会话、动态权重目前不支持，要使用这些特性可参考<a href="https://github.com/kubernetes/contrib/tree/master/service-loadbalancer">service-loadbalancer</a>
<p>Ingress不直接支持健康检查，但是K8S提供了类似的特性，例如Readiness探针</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg"><a id="network-policy"></a>网络策略</span></div>
<p>NetworkPolicy对象指定了：</p>
<ol>
<li>一组Pod是否允许和另外一组Pod通信</li>
<li>一组Pod是否允许和其它网络端点通信</li>
</ol>
<p>网络策略由网络插件实现，也就是说你需要一个支持NetworkPolicy的CNI插件。</p>
<div class="blog_h3"><span class="graybg">隔离/非隔离Pod</span></div>
<p>默认情况下Pod是非隔离的，任何来源的流量都可以访问它。</p>
<p>当某个NetworkPolicy选择（匹配）了Pod则它变成隔离的（isolated），这样，不被任何网络策略允许的流量，会被该Pod拒绝。</p>
<p>NetworkPolicy是叠加的，如果Pod被多个策略匹配，则所有这些策略的ingress/egress规则的union决定了Pod的连接性。</p>
<div class="blog_h3"><span class="graybg">NetworkPolicy资源</span></div>
<p>字段说明如下：</p>
<pre class="crayon-plain-tag">apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  # 选择/匹配的Pod
  podSelector:
    matchLabels:
      role: db
  # 此策略应用到匹配Pod的出站，还是入站，或者both流量
  policyTypes:
  - Ingress
  - Egress
  # 允许哪些入站流量，每个元素是一个规则，每个规则由from+ports组成
  ingress:
  - from:
    # 允许172.17.0.0/16中的实体，但是其中172.17.1.0/24不允许
    - ipBlock:
        cidr: 172.17.0.0/16
        except:
        - 172.17.1.0/24
    # 或者具有如下命名空间、标签的Pod
    - namespaceSelector:
        matchLabels:
          project: myproject
    - podSelector:
        matchLabels:
          role: frontend
    # 允许TCP 6379端口
    ports:
    - protocol: TCP
      port: 6379
  # 类似ingress，每个规则由to+ports组成
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
    ports:
    - protocol: TCP
      port: 5978
      # 如果有下面这个字段，则限定端口范围而非单个端口
      # 需要特性开关： --feature-gates=NetworkPolicyEndPort=true,...
      endPort: 32768</pre>
<p>下面是一些示例：</p>
<pre class="crayon-plain-tag"># 默认禁止所有入站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}
  policyTypes:
  - Ingress

# 默认允许所有入站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all-ingress
spec:
  podSelector: {}
  ingress:
  - {}
  policyTypes:
  - Ingress

# 默认禁止所有出站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
spec:
  podSelector: {}
  policyTypes:
  - Egress

# 默认允许所有出站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all-egress
spec:
  podSelector: {}
  egress:
  - {}
  policyTypes:
  - Egress</pre>
<div class="blog_h3"><span class="graybg">不能做什么</span></div>
<p>到1.21为止，NetworkPolicy不支持：</p>
<ol>
<li>强制集群内部流量通过公共网关，可以考虑使用Service Mesh</li>
<li>任何TLS相关需求，可以考虑使用Service Mesh</li>
<li>特定于某个节点的策略</li>
<li>特定于某个服务（根据名称）的策略</li>
<li>应用到任何命名空间/Pod的默认策略</li>
<li>记录安全日志，也就是那些连接被禁止/接受</li>
<li>禁止loopback或者Pod所在节点（宿主命名空间）发来的流量</li>
</ol>
<div class="blog_h2"><span class="graybg">HostAliases</span></div>
<p>你可以在Pod规格中定义将会注入到/etc/hosts文件的条目：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: hostaliases-pod
spec:
  restartPolicy: Never
  # 条目列表
  hostAliases:
  - ip: "127.0.0.1"
    hostnames:
    - "foo.local"
    - "bar.local"
  - ip: "10.1.2.3"
    hostnames:
    - "foo.remote"
    - "bar.remote"
  containers:
  - name: cat-hosts
    image: busybox
    # 启动后打印hosts文件内容
    command:
    - cat
    args:
    - "/etc/hosts"</pre>
<div class="blog_h1"><span class="graybg">卷</span></div>
<p>容器本身的文件系统是临时的，如果容器崩溃，文件立刻丢失。使用Docker时，你可以挂载外部卷，解决此问题。</p>
<p>K8S在存储方面面临两个问题：</p>
<ol>
<li>容器重新调度后文件丢失</li>
<li>Pod内的多个容器常需要共享文件 </li>
</ol>
<p>K8S引入和Docker类似的概念：卷（Volume）解决上述问题。</p>
<div class="blog_h2"><span class="graybg">背景</span></div>
<p>Docker已经包含了卷的概念，但是这些卷是松散、缺乏管理的。Docker中的卷仅仅是磁盘中的一个目录，或者其它容器中的目录。最近版本Docker引入了卷驱动的概念，但是使用场景很受限。</p>
<p>K8S的卷，提供了明确的生命周期，就好像使用它的Pod那样。卷的寿命比引用它的容器更长，容器重启后数据不会丢失。当Pod停止存在时则卷也被删除。</p>
<p>K8S支持多种类型的卷，每个Pod可以同时使用其中的多种卷。</p>
<p>卷的核心就是一个目录，可能其中有一些数据，这个目录可以被容器访问。至于这目录从哪来，何种介质存储它，取决于具体的卷类型。</p>
<p>要使用一个卷，在Pod规格中定义spec.volumes字段，并且在容器spec.containers.volumeMounts字段中声明在何处挂载卷。</p>
<div class="blog_h2"><span class="graybg">卷类型</span></div>
<p>全部卷类型参考<a href="https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes">K8S官网</a>。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>cephfs</td>
<td>允许一个既有的CephFS卷被挂载到Pod中。对CephFS卷的修改会永久保留</td>
</tr>
<tr>
<td>emptyDir</td>
<td>
<p>这种卷在Pod第一次被调度到某个节点上时，在节点上创建。只要Pod还在节点上运行，这个卷就继续存在，当节点被迁移走后，卷被删除掉，容器崩溃不会导致卷被删除</p>
<p>emptyDir初始是一个空目录</p>
<p>示例：</p>
<pre class="crayon-plain-tag">spec:
  containers:
  - image: gcr.io/google_containers/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}</pre>
<p>默认情况下，emptyDir将数据存储在节点所在的媒体上（可能是SDD、网络存储）。你可以设置emptyDir.medium=Memory来挂载一个内存文件系统</p>
</td>
</tr>
<tr>
<td>flocker</td>
<td>
<p>Flocker是一个开源的，集群化的容器数据卷管理器。它能管理、编排基于多种存储后端的数据卷</p>
<p>这类卷可以将Flocker数据集挂载到Pod，如果数据集不存在，你需要使用Flocker API或者CLI创建之 </p>
</td>
</tr>
<tr>
<td>glusterfs</td>
<td>Glusterfs是一个开源网络文件系统，这类卷能够把Glusterfs卷挂载到Pod</td>
</tr>
<tr>
<td>hostPath</td>
<td>
<p>从Node宿主机文件系统挂载一个文件/目录到Pod。示例：</p>
<pre class="crayon-plain-tag">spec:
  containers:
  - image: gcr.io/google_containers/test-webserver
    name: test-container
    volumeMounts:
    # 在容器内的挂载点
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # 宿主机路径
      path: /data
      # 类型：
      # DirectoryOrCreate 目录，如果不存在则创建，设置权限0755，组、所有者和Kubelet相同
      # Directory 已经存在的目录
      # FileOrCreate 文件，如果不存在则创建，设置权限0644，组、所有者和Kubelet相同
      # File 已经存在的文件
      # Socket UNIX套接字
      # CharDevice 一个字符设备
      # BlockDevice 一个块设备
      type: Directory</pre>
</td>
</tr>
<tr>
<td>nfs</td>
<td>
<p>挂载现有的NFS到Pod
<p>容器中挂载点的File mode取决于NFS的对应目录</p>
</td>
</tr>
<tr>
<td>persistentVolumeClaim</td>
<td>
<p>用于挂载PersistentVolume到Pod，PersistentVolume是一种声明（claim）持久性（调度后不变）存储（例如GCE PersistentDisk、iSCSI卷）的方式，这种方式不需要知道特定云环境的细节</p>
</td>
</tr>
<tr>
<td>projected</td>
<td>能够影射多个现有的卷资源到同一目录</td>
</tr>
<tr>
<td>secret</td>
<td>
<p>这种卷用于传递敏感信息（例如密码）到Pod，你可以通过K8S API创建Sercret，再通过卷的方式挂载到Pod</p>
<p>这类卷总是由内存后备（tmpfs）</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">卷的挂载</span></div>
<p>卷在Pod级别上定义，而在容器级别上挂载。容器将卷挂载到某个路径后，<span style="background-color: #c0c0c0;">此路径上原有的文件全部不可见</span>，这个特性和docker --volume是一致的。</p>
<div class="blog_h2"><span class="graybg">可选卷</span></div>
<p>你可以声明一个卷是“可选的”：</p>
<pre class="crayon-plain-tag">volumes:                                                                                                                        
  - name: ingressgateway-certs
    secret:
      defaultMode: 420
      optional: true  # 可选
      secretName: istio-ingressgateway-certs</pre>
<p>这样，即使目标ConfigMap、Secret、PVC不存在，Pod仍然可以启动。</p>
<p>注意：当目标ConfigMap、Secret在Pod<span style="background-color: #c0c0c0;">启动后创建，仍然会自动、立即挂载到Pod相应路径</span>。</p>
<div class="blog_h2"><span class="graybg">影射卷</span></div>
<p>用于将多个卷源影射为一个合并卷，当前，支持影射的卷类型包括：Secret、downwardAPI、ConfigMap、ServiceAccountToken。</p>
<p>所有需要影射的卷，必须和Pod在同一个命名空间。</p>
<p>示例：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: volume-test
spec:
  containers:
  - name: container-test
    image: busybox
    volumeMounts:
    - name: all-in-one
      mountPath: "/projected-volume"
      readOnly: true
  volumes:
  - name: all-in-one
    projected:
      sources:
      - secret:
          name: mysecret
          items:
            - key: username
              path: my-group/my-username
      - secret:
          name: mysecret2
          items:
            - key: password
              path: my-group/my-password
              mode: 511 </pre>
<div class="blog_h1"><span class="graybg">持久卷</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>持久卷子系统提供了一套API，将存储如何提供的细节分离出去，存储的消费者不需要关心这一细节。为实现这种分离K8S引入两类对象PersistentVolume、PersistentVolumeClaim。</p>
<p>PersistentVolume（PV）是集群中，以及由管理员提供好的一个存储资源。它是一种集群资源，就好像节点是一种集群资源一样。持久卷和上节的卷类似，都属于一种卷插件（Volume Plugin），但是持久卷的生命周期和任何使用它的Pod都没有关联，不会因为这些Pod被删除而删除（不管是删除对象还是存储的数据）。该对象封装了存储的实现细节（NFS、iSCSI、云服务特定存储...）。</p>
<p>PersistentVolumeClaim（PVC）是用户发起的存储请求，它和Pod类似：</p>
<ol>
<li>Pod消耗节点资源而PVC消耗PV资源</li>
<li>Pod可以请求特定的资源级别（CPU和内存），PVC则可以请求特定的存储尺寸、访问模式（读/写）</li>
</ol>
<p>注意：PV和PVC是一一绑定的关系，<span style="background-color: #c0c0c0;">一个PV同时仅仅能绑定到一个PVC，但是一个PVC可以被多个Pod使用</span>。一旦PVC被删除，其对应的PV会变为RELEASED状态。</p>
<p>尽管利用PersistentVolumeClaim，用户可以消费抽象的存储资源。但是要求具有一定特质（例如性能）的PersistentVolume是很常见的。集群管理员需要提供多种多样（不仅仅是尺寸、访问模式不同）的PersistentVolume，同时不向用户暴露PersistentVolume的实现细节。StorageClass对象提供了更多相关的内容。</p>
<div class="blog_h2"><span class="graybg">持久卷类型</span></div>
<p>PersistentVolume类型被实现为插件，目前K8S支持的插件包括：GCEPersistentDisk、AWSElasticBlockStore、Flocker、NFS、iSCSI、RBD、CephFS、Glusterfs等。</p>
<div class="blog_h3"><span class="graybg">local</span></div>
<p>本地持久卷代表挂载到Pod运行节点的<span style="background-color: #c0c0c0;">磁盘、分区或者目录</span>。本地持久卷<span style="background-color: #c0c0c0;">不支持动态Provisioning</span>。</p>
<p>和HostPath相比，local卷<span style="background-color: #c0c0c0;">具有数据持久性、可移植性</span>。PV Controller和Scheduler会对Local PV做特殊的逻辑处理，确保Pod使用本地存储时，在发生reschedule的情况下能<span style="background-color: #c0c0c0;">再次调度到Local PV所在的节点</span>。如果节点宕机，则Local PV可能无法访问，应用程序需要容忍这种可用性降低的场景。</p>
<p>使用local卷时，你必须设置PV对象的nodeAffinity，以保证使用它的Pod被调度到恰当的节点：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 100Gi
  # 可以设置为Block，将原始块设备暴露出来
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  # 必须声明SC，且该SC声明延迟绑定
  storageClassName: local-storage
  local:
    # 卷在节点上的路径
    path: /mnt/disks/ssd1
  # 节点亲和性
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - example-node</pre>
<p>尽管本地持久卷不支持动态Provisioning，我们仍然需要为它创建StorageClass，用于延迟卷绑定（直到Pod调度完成） ：</p>
<pre class="crayon-plain-tag">kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
# 延迟卷的创建和绑定到（使用它的）Pod调度完成之后，这样可以保证Kubernetes为PVC作出绑定决策的时候，
# 能够同时评估使用它的Pod的约束（例如节点资源需求、Pod亲和性、节点亲和性、节点选择器）
volumeBindingMode: WaitForFirstConsumer</pre>
<p>PV必须手工创建，工作负载通过声明PVC来匹配到这些已经存在的PV。如果你的集群大量使用Local PV，并且需要为不同工作负载提供不同性能的存储，你可能需要声明多个SC。</p>
<p>驱动<a href="https://github.com/kubernetes-sigs/sig-storage-local-static-provisioner">Local Persistence Volume Static Provisioner</a>可以改善Local PV的生命周期管理。它能够检测宿主机上的磁盘，并为它们创建PV，在PV被释放之后，清理磁盘。</p>
<div class="blog_h2"><span class="graybg">生命周期 </span></div>
<div class="blog_h3"><span class="graybg">Provisioning</span></div>
<p>Provisioning的含义是提供。PV是集群中的资源，PVC则是对资源的请求。它们之间的交互遵循以下生命周期。</p>
<p>K8S支持以两种方式提供PV：</p>
<ol>
<li>静态提供：集群管理员创建一系列PV，这些PV映射到了底层的实际存储，这些PV存在于K8S API中，可以被消费</li>
<li>动态提供：当没有满足PVC要求的PV时，集群可以尝试动态的提供一个卷给PVC，如何提供取决于StorageClass：
<ol>
<li>PVC需要请求一个StorageClass，而管理员必须预先创建、配置好StorageClass对象，以便满足动态提供的需要</li>
<li>PVC如果设置StorageClass为""，则相当于禁止动态提供</li>
</ol>
</li>
</ol>
<p>要启用基于StorageClass的动态提供，集群管理员需要在APIServer启用准许控制器（Admission Controller）DefaultStorageClass。具体来说，就是在APIServer的选项--admission-control中附加DefaultStorageClass，注意该选项的值是逗号分隔的字符串列表。</p>
<div class="blog_h3"><span class="graybg">Binding</span></div>
<p>用户创建声明了一定容量、访问模式的PVC后，Master上的控制循环会监控到PVC，找到一个匹配的PV，然后将PV和PVC进行绑定。PV和PVC是One-To-One关系，也就是说<span style="background-color: #c0c0c0;">PVC对PV是独占使用</span>的。</p>
<p><span style="background-color: #c0c0c0;">如果没有满足的PV，则PVC会一直处于未绑定状态</span>。例如，集群提供了很多50G的PV，现在一个PVC要100G的，则PVC仅仅在集群中添加了100G+的PV后才能成功绑定。</p>
<p><span style="background-color: #c0c0c0;">PVC对象被删除后，其对应的PV对象可能被级联删除</span>。</p>
<div class="blog_h3"><span class="graybg">Using</span></div>
<p>Pod将PVC作为卷使用，集群会找到PVC绑定的PV，并挂载到Pod中容器的指定目录下。</p>
<div class="blog_h3"><span class="graybg">Reclaiming</span></div>
<p>当不再需要时，用户可以通过K8S API删除PVC对象，让K8S回收资源。</p>
<p>当PVC被删除时，也就释放了和PV之间的绑定。PV的回收策略（persistentVolumeReclaimPolicy字段）指示了如何回收PV。</p>
<div class="blog_h3"><span class="graybg">Expanding</span></div>
<p>1.8引入的特性，Alpha状态。在1.9版本中以下类型的卷支持扩容：gcePersistentDisk、awsElasticBlockStore、Cinder、glusterfs、rbd。</p>
<div class="blog_h2"><span class="graybg">回收策略</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">回收策略</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Retain</td>
<td>
<p>PV保持存在，被标记为released，先前用户的数据仍然存在其中。管理员需要手工回收此PV，它才能被其它PVC使用。手工回收步骤：</p>
<ol>
<li>删除PersistentVolume对象。删除后底层的存储资源（Storage Asset）仍然存在</li>
<li>从底层存储资源手工清除数据</li>
<li>手工删除存储资源。如果想重用此存储资源，使用其存储资源定义创建一个新的PV</li>
</ol>
</td>
</tr>
<tr>
<td>Recycle</td>
<td>
<p>如果存储插件支持，则执行类似rm -rf的操作清除卷中的数据，并允许其被重新绑定</p>
<p>你也可以通过命令行参数为kube-controller-manager自定义<a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/#recycling">回收器Pod模板</a>，自定义清除数据的方式</p>
</td>
</tr>
<tr>
<td>Delete</td>
<td>
<p>如果存储插件支持，则删除PersistentVolume对象，同时删除关联的底层存储资源</p>
<p>动态提供的卷会从其StorageClass继承默认回收策略，此策略也是Delete</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">访问模式</span></div>
<p>每个卷在同一时刻只能使用一种访问模式，即使它可以支持多种模式。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">模式</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ReadWriteOnce</td>
<td>可以被单节点挂载为读写，所有卷插件都支持</td>
</tr>
<tr>
<td>ReadOnlyMany</td>
<td>可以被多节点挂载为读 CephFS、Glusterfs、iSCSI、NFS、RBD等支持</td>
</tr>
<tr>
<td>ReadWriteMany</td>
<td>CephFS、Glusterfs、NFS等支持</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">PersistentVolume</span></div>
<p>每个PV都包含一个规格、一个状态。下面是一个示例规格：</p>
<pre class="crayon-plain-tag">apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv0003
  spec:
    # 卷的容量
    capacity:
      storage: 5Gi
    # 卷模式，1.9之前都会在卷上创建文件系统，1.9支持设置为Block，即使用裸块
    volumeMode: Filesystem
    accessModes:
      - ReadWriteOnce
    # 回收策略
    persistentVolumeReclaimPolicy: Recycle
    # 类别
    storageClassName: slow
    # 挂载选项
    mountOptions:
      - hard
      - nfsvers=4.1
    nfs:
      path: /tmp
      server: 172.17.0.2</pre>
<div class="blog_h3"><span class="graybg">StorageClass</span></div>
<p>PV可以有一个类型字段，其值为 StorageClass对象的名称。具有给定 StorageClass的PV，只能和请求相同 StorageClass的PVC进行绑定；没有指定 StorageClass的PV只能和没有请求 StorageClass的PVC绑定。</p>
<p>在过去，注解volume.beta.kubernetes.io/storage-class用于代替storageClassName属性，未来该注解可能被废弃。</p>
<div class="blog_h3"><span class="graybg">挂载选项</span></div>
<p>某些持久卷类型（NFS、iSCSI、RBD、CephFS、Glusterfs等）支持挂载选项，当卷被挂载到节点时会应用这些选项。 </p>
<p>在过去，注解volume.beta.kubernetes.io/mount-options用于代替mountOptions属性，未来该注解可能被废弃。</p>
<div class="blog_h3"><span class="graybg">Phase</span></div>
<p>一个卷可以处于以下阶段：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">Phase</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Available</td>
<td>游离的资源，没有绑定到PVC</td>
</tr>
<tr>
<td>Bound</td>
<td>绑定到了一个PVC</td>
</tr>
<tr>
<td>Released</td>
<td>PVC被删除了，但是PV的资源尚未被集群回收</td>
</tr>
<tr>
<td>Failed</td>
<td>卷的自动回收失败了</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">PersistentVolumeClaims</span></div>
<p>和PV一样，每个PVC也都有一个规格、一个状态。下面是一个示例规格：</p>
<pre class="crayon-plain-tag">kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: myclaim
spec:
  # 我需要什么样的访问模式
  accessModes:
    - ReadWriteOnce
  # 我需要什么样的卷模式（文件系统还是裸设备）
  volumeMode: Filesystem
  # 我需要多少资源
  resources:
    requests:
      storage: 8Gi
  # 我需要的存储类
  storageClassName: slow
  # 使用选择器对备选卷进行过滤
  selector:
    matchLabels:
      release: "stable"
    matchExpressions:
      - {key: environment, operator: In, values: [dev]}</pre>
<p>其中storageClassName 如果指定为""则仅仅存储类为""的PV才能绑定。如果不指定该属性，则系统的行为取决于是否启用了准许控制器DefaultStorageClass：</p>
<ol>
<li>如果启用，则集群管理员可以指定一个默认的StorageClass。这样所有不指定存储类的PVC都只能绑定到具有此默认存储类的PV
<ol>
<li>设置默认存储类的方法：为作为默认的StorageClass的storageclass.kubernetes.io/is-default-class注解设置为true</li>
<li>如果集群管理员没有设置默认StorageClass，创建PVC时你收到的提示和没有启用准许控制器DefaultStorageClass一样</li>
<li>如果将多个StorageClass注解为“默认”则准许控制器阻止任何PVC的创建</li>
</ol>
</li>
<li>如果准许控制器没有启用，则不存在默认StorageClass这一概念。不指定storageClassName和指定为""效果一样</li>
</ol>
<p>取决于集群安装方式，默认StorageClass可能被集群的Addon管理器在安装阶段部署。</p>
<p>如果PVC同时指定selector + storageClassName，则目标PV必须同时符合两个条件。注意：目前不支持对带有selector的PVC进行动态提供。</p>
<div class="blog_h2"><span class="graybg">挂载PVC为卷</span></div>
<p>Pod可以将PVC作为卷来挂载，供容器使用。PVC必须和Pod位于同一个名字空间中，集群会找到绑定到PVC的PV，并将PV挂载给Pod。</p>
<p>声明使用PVC的Pod的规格示例：</p>
<pre class="crayon-plain-tag">kind: Pod
apiVersion: v1
metadata:
  name: mypod
spec:
  containers:
    - name: myfrontend
      image: dockerfile/nginx
      volumeMounts:
      - mountPath: "/var/www/html"
        name: mypd
  volumes:
    - name: mypd
      persistentVolumeClaim:
        claimName: myclaim</pre>
<div class="blog_h2"><span class="graybg">裸卷的支持</span></div>
<p>在1.9引入的Alpha特性是对裸块卷（Raw Block Volume）的静态提供的支持，目前仅仅光纤通道（Fibre Channel，FC）是唯一支持此特性的插件。</p>
<p>PV规格示例：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: PersistentVolume
metadata:
  name: block-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  volumeMode: Block
  persistentVolumeReclaimPolicy: Retain
  fc:
    targetWWNs: ["50060e801049cfd1"]
    lun: 0
    readOnly: false</pre>
<p>PVC规格示例：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: block-pvc
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Block
  resources:
    requests:
      storage: 10Gi</pre>
<div class="blog_h2"><span class="graybg">可移植配置 </span></div>
<p>如果你在编写能够在很大范围的集群上使用的配置模板/样例，且需要使用持久化存储，可以参考如下建议：</p>
<ol>
<li>在Deployments/ConfigMap等配置中包含PersistentVolumeClaim声明</li>
<li> 不要在配置中包含PersistentVolume对象，因为实例化配置的用户可能没有创建PV的权限</li>
<li>允许用户实例化配置模板时，提供storageClassName</li>
<li>监控PVC的绑定情况，如果一段时间后没有绑定成功，应当通知用户。绑定失败意味着集群可能没有动态存储支持（用户需要手工创建PV）</li>
</ol>
<div class="blog_h2"><span class="graybg">动态提供</span></div>
<p>这一特性可以实现按需创建存储卷。如果没有动态提供，集群管理员必须手工调用云/存储提供商的接口，来创建新的存储卷，然后通过K8S API注册和存储卷对应的PV对象。</p>
<p>动态提供是基于API组storage.k8s.io中的StorageClass对象实现的。集群管理员可以定义任意数量的StorageClass，每个SC需要指定一个卷插件（也叫provisioner）和一组参数。卷插件基于这些参数来按需创建卷。对于一个存储系统，你可以定义多个SC，以暴露不同风味（例如速度快或慢）的存储资源。</p>
<div class="blog_h3"><span class="graybg">启用动态提供</span></div>
<p>要启用动态提供，一系列StorageClass需要被预先创建。下面的两个规格，分别创建了基于HDD的慢速SC和SDD的快速SC：</p>
<pre class="crayon-plain-tag">apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: slow
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-standard

---

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-ssd</pre>
<p>很多类型的卷没有内置的Provisioner，而动态提供需要由Provisioner来完成。你可能需要借助<a href="https://github.com/kubernetes-incubator/external-storage">external-storage</a>项目。</p>
<div class="blog_h3"><span class="graybg">使用动态提供</span></div>
<p>前面已经提到过，要使用动态提供，你需要为PVC声明storageClassName字段，也就是声明需要哪种风格的存储资源。 </p>
<div class="blog_h2"><span class="graybg">静态提供</span></div>
<p>你也可以手工创建持久卷对象，不依赖于内置或外置的Provisioner。任何K8S支持的<span style="background-color: #c0c0c0;">存储资源，并非必须依赖于Provisioner或者StorageClass才能使用</span>。</p>
<div class="blog_h3"><span class="graybg"><a id="static-provisioning-ceph"></a>ceph</span></div>
<p>下面是基于Ceph，创建静态持久卷的例子。持久卷定义如下：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: PersistentVolume
metadata:
  name: maven-repository
  labels:
    name: maven-repository
spec:
  accessModes:
  - ReadWriteMany
  capacity:
    storage: 16Gi
  cephfs:
    # Ceph MON节点列表
    monitors:
    - xenial-100
    - Carbon
    - Radon
    # 存储在默认CephFS的什么路径下
    path: /maven-repository
    # 访问密钥
    secretRef:
      name: pvc-ceph-key
    # 访问用户
    user: admin</pre>
<p>Pod不能直接引用持久卷，因此需要创建一个PVC，关联到上面的PV：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: maven-repository
spec:
  accessModes:
    # 访问模式，这里是允许多写
    - ReadWriteMany
  storageClassName: ""
  resources:
    requests:
      storage: 16Gi
  selector:
    # 使用标签来匹配目标卷
    matchLabels:
      name: maven-repository</pre>
<p>使用上面PV的Pod的例子：</p>
<pre class="crayon-plain-tag">kind: Pod
apiVersion: v1
metadata:
  name: maven-repository-0
spec:
  containers:
  - name: maven-repository
    image: docker.gmem.cc/busybox
    command:
    - "/bin/sh"
    args:
    - "-c"
    - "sleep 365d"
    volumeMounts:
    - name: data
      mountPath: "/data"
  restartPolicy: "Never"
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: maven-repository</pre>
<div class="blog_h3"><span class="graybg">nfs</span></div>
<p>持久卷示例：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: PersistentVolume
metadata:
  name: datastore
spec:
  capacity:
    storage: 1Ti
  accessModes:
    - ReadWriteMany
  nfs:
    server: 10.255.223.141
    path: "/datastore"</pre>
<div class="blog_h1"><span class="graybg">存储类</span></div>
<p>StorageClass（存储类）为集群管理员提供了一种方法，用于描述集群提供的存储的“类型”。不同的存储类，可能映射到了不同的QoS级别、备份策略，等等。K8S本身不对存储类表示什么进行解释，存储类的类似其它存储系统中的Profile。</p>
<div class="blog_h2"><span class="graybg">StorageClass对象</span></div>
<p>每个SC对象都包含provisioner、parameters、reclaimPolicy字段，当属于此存储类的PersistentVolume需要被提供出去时，这些参数会被K8S使用。</p>
<p>StorageClass的名称很关键，因为用户根据其名称来请求特定的存储类。SC的所有属性都在管理员初次创建时指定，后续不能更改。</p>
<p>在启用准许控制器DefaultStorageClass的情况下，管理员可以设置默认StorageClass。这样不关心SC是何物的PVC可以使用默认的SC。</p>
<p>SC对象的规格示例：</p>
<pre class="crayon-plain-tag">kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: standard
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2
reclaimPolicy: Retain
mountOptions:
  - debug</pre>
<div class="blog_h3"><span class="graybg">provisioner</span></div>
<p>该属性指定用于提供PV的存储插件，必须字段。K8S自带了一系列内部存储插件（以kubernetes.io/前缀开头） 。你也可以使用外部存储插件（服务K8S规范的独立程序）。K8S允许外部存储插件被非常自由的实现，一些可用的外部插件位于<a href="https://github.com/kubernetes-incubator/external-storage">K8S孵化器</a>中。</p>
<div class="blog_h3"><span class="graybg">reclaimPolicy</span></div>
<p>对于动态提供的PV，其回收策略从存储类继承。如果你没有给存储类指定该字段，默认为Delete。</p>
<div class="blog_h3"><span class="graybg">mountOptions</span></div>
<p>由存储类动态创建的PV会继承此挂载选项。</p>
<div class="blog_h2"><span class="graybg">配置参数</span></div>
<p>存储类有一个parameters字段，用于描述存储类创建的卷是什么样的。parameters包含哪些子字段取决于provisioner。</p>
<div class="blog_h3"><span class="graybg">AWS</span></div>
<pre class="crayon-plain-tag">kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: slow
provisioner: kubernetes.io/aws-ebs
parameters:
  # io1, gp2, sc1, st1，默认gp2
  type: io1
  # AWS区域，如果不指定，则在包含K8S节点的Zone上循环创建卷。zone指定单个值，zones指定逗号分隔的多个值
  zones: us-east-1d, us-east-1c
  # 每GB数据在每秒内支持的IO次数，近用于io1
  iopsPerGB: "10"</pre>
<div class="blog_h3"><span class="graybg">Glusterfs</span></div>
<pre class="crayon-plain-tag">apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: slow
provisioner: kubernetes.io/glusterfs
parameters:
  # Gluster的REST服务/Heketi服务的URL，存储类依赖于此URL来提供卷
  resturl: "http://127.0.0.1:8081"
  # Heketi用来提供卷的集群ID，可以是逗号分隔的多个值
  clusterid: "630372ccdc720a92c681fb928f27b53f"
  # Gluster REST服务是否需要身份验证，如果是，则需要restuser字段、restuserkey或secretNamespace+secretName字段
  restauthenabled: "true"
  restuser: "admin"
  secretNamespace: "default"
  secretName: "heketi-secret"
  # 存储类的GID范围
  gidMin: "40000"
  gidMax: "50000"
  # 卷类型及其参数
  volumetype: "replicate:3"</pre>
<div class="blog_h3"><span class="graybg">Ceph RBD </span></div>
<pre class="crayon-plain-tag">kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: fast
provisioner: kubernetes.io/rbd
parameters:
  # Ceph monitors，逗号分隔，必须
  monitors: 10.16.153.105:6789
  # 能够在池中创建image的 Ceph client ID
  adminId: kube
  # adminSecret的名字
  adminSecretName: ceph-secret
  # adminSecret的名字空间
  adminSecretNamespace: kube-system
  # Ceph RBD pool，默认rbd
  pool: kube
  # 用于映射RBD image的Ceph client ID，默认等于adminId
  userId: kube
  # user的Ceph Secret的名字必须和PVC位于同一名字空间。Secret的类型必须为kubernetes.io/rbd
  userSecretName: ceph-secret-user
  # 支持的文件类型
  fsType: ext4
  # Ceph RBD image 格式，1或2，默认1
  imageFormat: "2"
  imageFeatures: "layering" </pre>
<div class="blog_h2"><span class="graybg">默认StorageClass</span></div>
<p>要设置某个StorageClass为默认，执行：</p>
<pre class="crayon-plain-tag">kubectl patch storageclass ceph-rbd -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'</pre>
<p>这样，没有显示指定StorageClass的PVC，会自动使用ceph-rbd这个存储类。 </p>
<div class="blog_h1"><span class="graybg">使用API</span></div>
<div class="blog_h2"><span class="graybg">API概述</span></div>
<p>K8S APIServer暴露了REST风格的API，所有组件之间的通信都基于此API进行，包括kubectl或kubeadm等Shell工具。</p>
<p>你也可以直接进行REST调用，或者使用对应编程语言的<a href="https://kubernetes.io/docs/reference/client-libraries/">客户端库</a>发起API调用。</p>
<p>K8S支持版本化的API，不同版本包含的字段可以不一样。版本号示例：/api/v1、/apis/extensions/v1beta1。</p>
<p><span style="background-color: #c0c0c0;">人类用户、服务账号（SA）都可以访问API</span>，访问过程有三个阶段：</p>
<ol>
<li>身份验证</li>
<li>访问控制（授权）</li>
<li>Admission Control</li>
</ol>
<div class="blog_h2"><span class="graybg">传输层安全</span></div>
<p>典型情况下，APIServer使用端口443提供服务，它通常使用一个自签名的证书。客户端机器的$USER/.kube/config中通常包含了APIServer的根证书。</p>
<div class="blog_h2"><span class="graybg">身份验证</span></div>
<p>建立TLS连接后，API调用请求需要进行身份验证。自动创建集群的脚本，或者集群管理员，会为APIServer配置1-N个身份验证器（Authenticator）模块。身份验证器通常会检查客户端证书，或者HTTP请求头。</p>
<p>身份验证器模块包括：客户端证书、密码、明文Token、Bootstrap Token、JWT Token（SA使用）这几种。如果配置多个模块，会顺序尝试，直到成功。</p>
<p>如果身份验证失败，APIServer返回401错误。否则用户被验证为指定的Principal（用户名，username），某些身份验证器也提供了用户所属的组信息。K8S使用用户名进行后续的访问控制和日志记录。</p>
<div class="blog_h3"><span class="graybg">K8S的用户</span></div>
<p>集群的用户被分为两类：Service Account和普通用户，后者由外部系统管理，无法在K8S集群中进行登记。</p>
<p>Service Account由K8S API管理，绑定到特定的名字空间。API Server可能会自动创建SA，你也可以手工创建。SA关联到一系列保存了凭证的Secret中，例如，下面这个Secret就保存了Token这种凭证：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system describe secret default-token-vgvll
# token: ...</pre>
<div class="blog_h3"><span class="graybg">身份验证策略</span></div>
<p>K8S使用客户端证书、不记名令牌（bearer token）、 身份验证代理（authenticating proxy）、HTTP基本验证来对API请求者的身份进行验证。具体的验证工作由身份验证插件负责。</p>
<p>身份验证插件会尝试从请求中获取以下属性：</p>
<ol>
<li>用户名，例如kube-admin或者alex@gmem.cc</li>
<li>用户标识符</li>
<li>用户组，用户所属的多个分组</li>
<li>身份验证插件可能需要的其它额外字段</li>
</ol>
<p>你可以同时启用多种身份验证策略，并至少：</p>
<ol>
<li>为SA提供基于Token的身份验证</li>
<li>为普通用户提供某种认证策略</li>
</ol>
<p>当启用多种身份验证策略时，第一个成功的验证器获胜，K8S不保证验证器的执行顺序。</p>
<p>所有经过身份验证的用户都会归属于用户组：system:authenticated。</p>
<div class="blog_h3"><span class="graybg">客户端证书</span></div>
<p>为API Server提供选项 --client-ca-file=/path/to/cafile，即可启用基于客户端证书的身份验证。指定的文件必须包含1-N个CA证书，这些证书将用户客户端证书的校验。<span style="background-color: #c0c0c0;">如果客户端证书通过校验，则其Common name字段将作为用户名</span>，如果要将用户关联到多个组，你需要指定多个organization证书字段。CSR示例：</p>
<pre class="crayon-plain-tag">openssl req -new -key alex.key -out alex.csr -subj "/CN=alex/O=app1/O=app2"</pre>
<div class="blog_h3"><span class="graybg">静态Token文件</span></div>
<p>为API Server提供选项--token-auth-file=/path/to/token，则API Server从文件中读取静态Token列表，修改Token文件后必须重启API Server。</p>
<p>静态Token是一个CSV文件，包含至少三个字段：Token、用户名、UID，可以包含一个分组字段，示例：</p>
<pre class="crayon-plain-tag">token,user,uid,"group1,group2,group3"</pre>
<div class="blog_h3"><span class="graybg">在请求中包含Token</span></div>
<p>设置请求头即可：</p>
<pre class="crayon-plain-tag"># 必须以Bearer开始，后面跟着Token内容
Authorization: Bearer 31ada4fd-adec-460c-809a-9e56ceb75269</pre>
<div class="blog_h3"><span class="graybg">Bootstrap Token </span></div>
<p>为了方便的搭建新集群，K8S提供了一种动态管理的不记名Token —— Bootstrap Token这些Token存储为kube-system中的Secrets，可以被动态的创建和管理。</p>
<p>控制器管理器（Controller Manager）包含了一种控制器TokenCleaner，自动在过期后删除Bootstrap Token。</p>
<p>使用APIServer选项--experimental-bootstrap-token-auth来启用Bootstrap Token验证，使用 --controllers=***,tokencleaner来启用TokenCleaner。Kubeadm自动添加了这些选项。</p>
<p>基于Bootstrap Token认证的请求，其用户名设置为system:bootstrap:，分配到用户组system:bootstrappers。</p>
<div class="blog_h3"><span class="graybg">静态密码文件</span></div>
<p>为API Server提供选项--basic-auth-file=/path/to/passwd，则启用HTTP基本验证。密码文件也是CSV格式，形式如下：</p>
<pre class="crayon-plain-tag">password,user,uid,"group1,group2,group3"</pre>
<p>你必须提供如下格式的请求头来使用基本验证：</p>
<pre class="crayon-plain-tag">Authorization: Basic BASE64ENCODED(USER:PASSWORD)</pre>
<div class="blog_h3"><span class="graybg">SA Token</span></div>
<p>Service Account验证器（验证插件，authenticator）总是自动启用。这种验证器基于签名过的Bearer Token来验证请求。该插件使用以参数：</p>
<ol>
<li>--service-account-key-file 用于签名Bearer Token的PEM编码的私钥</li>
<li>--service-account-lookup 被删除的Token自动吊销</li>
</ol>
<p>SA通常由APIServer自动创建，并关联到集群中的Pod，关联行为由准许控制器ServiceAccount负责。使用serviceAccountName字段你可以显式的关联某个SA到Pod。</p>
<p>执行命令<pre class="crayon-plain-tag">kubectl create serviceaccount</pre>，你可以手工创建一个SA。K8S会自动创建与此SA关联的Secret，该Secret中包含：</p>
<ol>
<li>ca.crt，APIServer的证书，Base64编码</li>
<li>token，签名后的JSON Web Token。你在请求中提供此Token则APIServer将你验证为对应的SA</li>
</ol>
<p>SA的用户名为system:serviceaccount:(NAMESPACE):(SERVICEACCOUNT)，分配到用户组system:serviceaccounts、system:serviceaccounts:(NAMESPACE)。</p>
<p>警告：用于SA的账户Token存放在Secret中，因此你向用户授予读Secret权限时要小心。任何获取Secret的人都可以伪装为SA。</p>
<div class="blog_h3"><span class="graybg">匿名用户</span></div>
<p>从1.6+开始，如果使用了除AlwaysAllow之外的验证模式 ，则默认允许匿名访问。你可以使用APIServer选项--anonymous-auth=false显式禁用匿名访问。</p>
<p>匿名用户被分配用户名system:anonymous，分配用户组system:unauthenticated。从1.6开始，ABAC/RBAC授权器要求对匿名用户/组进行显式授权。</p>
<div class="blog_h3"><span class="graybg">用户仿冒</span></div>
<p>如果被授权，一个用户可以仿冒为任何其它用户，需要提供以下请求头：</p>
<ol>
<li>Impersonate-User，仿冒的目标用户。使用kubectl时，你可以用--as来指定</li>
<li>Impersonate-Group， 仿冒的目标组。使用kubectl时，你可以用--as-group来指定</li>
<li>Impersonate-Extra-***，用户的***动态字段</li>
</ol>
<p>要仿冒其它用户，原用户必须被授权。对于启用RBAC的集群，可以定义如下ClusterRole：</p>
<pre class="crayon-plain-tag">apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: impersonator
rules:
- apiGroups: [""]
                                                    # 允许设置Impersonate-Extra-scopes头
  resources: ["users", "groups", "serviceaccounts", "userextras/scopes"]
  verbs: ["impersonate"]</pre>
<div class="blog_h2"><span class="graybg">访问控制</span></div>
<p>经过身份验证后，请求（的username）还必须确认是否具有访问API的权限。请求中除了包含<span style="background-color: #c0c0c0;">username</span>之外，还包括需要<span style="background-color: #c0c0c0;">执行的操作</span>，<span style="background-color: #c0c0c0;">操作针对的对象</span>。</p>
<p>K8S支持多种授权模块，包括ABAC（基于属性的访问控制）、RBAC（基于角色的访问控制）、Webhook。管理员创建集群时，会配置使用的授权模块。</p>
<p>如果请求没有访问权限，APIServer返回403错误。</p>
<div class="blog_h3"><span class="graybg">授权器</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">授权器</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Node</td>
<td>特殊用途的授权器，根据在节点上运行的Pod，来授予kubectl权限</td>
</tr>
<tr>
<td>ABAC</td>
<td>
<p>基于属性的访问控制（Attribute-based access control），通过文件来指定策略</p>
<p>策略（Policy）将访问权限分组到一起，用户被关联到策略，策略可以使用任何类型的属性——用户属性、资源属性、对象属性……</p>
<p>使用API Server选项--authorization-mode=ABAC启用</p>
</td>
</tr>
<tr>
<td>RBAC</td>
<td>
<p>基于角色的访问控制（Role-based access control），通过API来创建、存储策略kind: ConfigMap<br />apiVersion: v1<br />metadata:<br /> name: calico-config<br /> namespace: kube-system<br />data:<br /> # The location of your etcd cluster. This uses the Service clusterIP defined below.<br /> etcd_endpoints: "http://10.5.38.24:2379,http://10.5.38.39:2379,http://10.5.39.41:2379"</p>
<p>使用API Server选项--authorization-mode=RBAC启用该授权器</p>
</td>
</tr>
<tr>
<td>AlwaysAllow</td>
<td>
<p>允许所有访问，相当于禁止访问控制</p>
<p>使用API Server选项 --authorization-mode=AlwaysAllow启用该授权器</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">检查权限</span></div>
<p>使用kubectl可以快速检查用户有没有特定的权限：</p>
<pre class="crayon-plain-tag"># 是否具有在dev创建deployment的权限
kubectl auth can-i create deployments --namespace dev</pre>
<div class="blog_h2"><span class="graybg">RBAC</span></div>
<p>这是使用最广泛的授权器，Kubeadm默认启用Node和RBAC两种授权器。</p>
<div class="blog_h3"><span class="graybg">Role</span></div>
<p>Role代表针对某个特定名字空间的一系列的权限（permissions），示例：</p>
<pre class="crayon-plain-tag">kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  # 针对default名字空间
  namespace: default
  name: pod-reader
rules:
  # "" 表示核心API组
- apiGroups: [""]
  # 可以访问核心API组的pods资源
  resources: ["pods"]
  # 可以针对资源进行下面三类操作
  verbs: ["get", "watch", "list"]</pre>
<p>某些API会牵涉到“子资源”，例如访问Pod的日志，URL为：GET /api/v1/namespaces/{namespace}/pods/{name}/log。如果你需要允许用户访问Pod，及其日志，需要这样配置：</p>
<pre class="crayon-plain-tag">resources: ["pods", "pods/log"]</pre>
<div class="blog_h3"><span class="graybg">ClusterRole</span></div>
<p>ClusterRole代表针对整个集群的一系列权限，支持Role的任何权限，此外还可以授权对以下资源的访问：</p>
<ol>
<li>集群级别的资源，例如node</li>
<li>非资源端点，例如/healthz</li>
<li>跨越所有名字空间的namespaced资源，使用--all-namespace需要这种权限</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  # 没有名字空间，因为ClusterRole不是namespaced的资源
  name: secret-reader
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "watch", "list"]</pre>
<p>从1.9开始，K8S允许组合一个ClusterRole，产生新的ClusterRole：</p>
<pre class="crayon-plain-tag">kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: monitoring
# 根据标签匹配相关ClusterRole
aggregationRule:
  clusterRoleSelectors:
  - matchLabels:
      rbac.example.com/aggregate-to-monitoring: "true"
# 控制器管理器会自动填写下面的字段
rules: []</pre>
<div class="blog_h3"><span class="graybg">RoleBinding</span></div>
<p>将角色绑定到一个或者一组用户（User、Group、SA），示例：</p>
<pre class="crayon-plain-tag">kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: read-pods
  namespace: default
# 被授权的主体，可以是用户、组、或者Service Account
subjects:
- kind: User
  name: admin@gmem.cc
  apiGroup: rbac.authorization.k8s.io
# 授予的角色
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io</pre>
<p>注意：roleRef可以引用ClusterRole，但是仅仅授予会当前名字空间的访问权限。</p>
<div class="blog_h3"><span class="graybg">ClusterRoleBinding</span></div>
<p>将集群角色绑定到一个或者一组用户，示例：</p>
<pre class="crayon-plain-tag">kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: read-secrets-global
subjects:
- kind: Group
  name: system:authenticated
  apiGroup: rbac.authorization.k8s.io
- kind: ServiceAccount
  name: default
  namespace: kube-system
roleRef:
  kind: ClusterRole
  name: secret-reader
  apiGroup: rbac.authorization.k8s.io</pre>
<p>使用命令：</p>
<pre class="crayon-plain-tag">kubectl create clusterrolebinding cluster-admin-default --clusterrole=cluster-admin --serviceaccount=tke:default </pre>
<div class="blog_h3"><span class="graybg">默认角色 </span></div>
<p>API Server自动创建了一些ClusterRole、ClusterRoleBinding对象。大部分对象都具有system:前缀，提示这些对象供K8S基础设施使用，随意修改这些对象会导致集群不可用。所有这些对象都具有标签kubernetes.io/bootstrapping=rbac-defaults。</p>
<p>默认角色/绑定如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">角色</td>
<td style="width: 200px; text-align: center;">角色绑定（到组）</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>system:basic-user</td>
<td>system:authenticated<br />system:unauthenticated</td>
<td>允许用户只读访问自己的基本信息</td>
</tr>
<tr>
<td>cluster-admin</td>
<td>system:masters</td>
<td>
<p>超级用户角色：</p>
<ol>
<li>在ClusterRoleBinding中使用时，允许任意访问</li>
<li>在RoleBinding中使用时，允许针对目标名字空间的任意访问</li>
</ol>
</td>
</tr>
<tr>
<td>admin</td>
<td> </td>
<td>允许针对名字空间的绝大部分操作，包括创建角色/绑定</td>
</tr>
<tr>
<td>edit</td>
<td> </td>
<td>允许针对名字空间的绝大部分资源进行写操作</td>
</tr>
<tr>
<td>view</td>
<td> </td>
<td>允许针对名字空间的绝大部分资源进行读操作</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Admission Control</span></div>
<p><a href="https://kubernetes.io/docs/admin/admission-controllers/">准许控制器</a>是可以<span style="background-color: #c0c0c0;">修改或者拒绝请求</span>的软件模块。准许控制器能够访问被创建、修改、删除的对象的内容。</p>
<p>管理员可以配置多个准许控制器，这些控制器会按照顺序被调用。</p>
<p>如果存在任何准许控制器拒绝请求，则请求立即停止处理。</p>
<div class="blog_h1"><span class="graybg">安全</span></div>
<div class="blog_h2"><span class="graybg">安全上下文</span></div>
<p>安全上下文（Security Context）定义操作系统相关的、Pod的安全性配置，例如uid、gid、SELinux角色。可以规定Pod级别或容器级别的安全上下文。</p>
<div class="blog_h3"><span class="graybg">Pod级别</span></div>
<p>应用到所有容器：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: hello-world
spec:
  securityContext:
    # 运行容器入口点脚本的UID
    runAsUser: 1000
    # 提示容器必须以非超级用户身份执行，Kubelet会在运行时检查容器用户的PID，如果为0且此选项为true则拒绝启动容器
    runAsNonRoot: false
    # 为容器PID为1的进程添加的额外组信息
    supplementalGroups: []
    # 支持所有权管理的那些卷，其所有者GID设置为下面的值，且允许下面的GID进行读写
    # 如果读取挂载的PV时出现permission denied错误，可以考虑将该字段配置为运行Docker容器所使用的用户
    fsGroup: 1000
    supplementalGroups: [5678]
    # 支持SELinux标签的卷，允许别下面的标签访问
    seLinuxOptions:
      level: "s0:c123,c456"</pre>
<div class="blog_h3"><span class="graybg">容器级别</span></div>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: hello-world
spec:
  containers:
    - name: my-container
      securityContext:
        # 是否在特权模式下执行容器，如果为true，则容器中进程的权限实际上等同于宿主机的root
        # 特权容器可以访问宿主机上的所有设备，非特权容器不能访问任何宿主机设备
        # 如果容器需要操控网络栈、访问硬件，则需要特权模式
        privileged: false
        # 容器的根文件系统是否为只读
        readOnlyRootFilesystem: false
        # 容器以什么UID运行
        runAsUser: 1000
        # 容器以什么GID运行（primary group）
        runAsGroup: 1000
        # 添加supplemental groups
        supplementalGroups:
        runAsNonRoot: false
        seLinuxOptions:
          level: "s0:c123,c456" </pre>
<div class="blog_h2"><span class="graybg"><a id="psp"></a>PodSecurityPolicy</span></div>
<p>该对象用于细粒度的控制Pod安全，可以限制Pod的创建/更新，或者为某些字段提供默认值。下面是一些例子：</p>
<pre class="crayon-plain-tag"># 最少限制的Policy，等价于没有启用PodSecurityPolicy准入控制器

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: privileged
  annotations:
    # 允许使用任何安全Porfile
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
spec:
  # 可以运行特权容器
  privileged: true
  # 允许权限提升，直接决定是否向容器传递no_new_privs标记
  # no_new_privs标记可以防止setuid的可执行文件改变它的Effective UID
  # 设置该字段为false，则容器中发起的子进程不会得到比父进程更多的权限，即使它setuid
  allowPrivilegeEscalation: true
  # 允许使用任何Linux Caps
  allowedCapabilities:
  - '*'
  # 允许使用任何类型的卷
  volumes:
  - '*'
  # 允许使用宿主机网络命名空间
  hostNetwork: true
  hostPorts:
  - min: 0
    max: 65535
  hostIPC: true
  hostPID: true
  runAsUser:
    rule: 'RunAsAny'
  seLinux:
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'RunAsAny'
  fsGroup:
    rule: 'RunAsAny'

---

# 要求不得运行特权容器，禁止权限提升

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
  # 限制允许的Apparmor/Seccomp模板
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default,runtime/default'
    apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default'
    seccomp.security.alpha.kubernetes.io/defaultProfileName:  'runtime/default'
    apparmor.security.beta.kubernetes.io/defaultProfileName:  'runtime/default'
spec:
  # 禁止特权容器
  privileged: false
  # 禁止提升为root
  allowPrivilegeEscalation: false
  # 这一条和禁止特权容器+禁止提升的功效重复，提供额外一层保护
  requiredDropCapabilities:
    - ALL
  # 允许核心的卷类型
  volumes:
    - 'configMap'
    - 'emptyDir'
    - 'projected'
    - 'secret'
    - 'downwardAPI'
    - 'persistentVolumeClaim'
  # 禁止使用宿主机的命名空间
  hostNetwork: false
  hostIPC: false
  hostPID: false
  runAsUser:
    # 不得以root身份允许容器进程
    rule: 'MustRunAsNonRoot'
  seLinux:
    # 假设节点运行AppArmor而非SELinux
    rule: 'RunAsAny'
  supplementalGroups:
    rule: 'MustRunAs'
    ranges:
      # 禁止加入root组
      - min: 1
        max: 65535
  fsGroup:
    rule: 'MustRunAs'
    ranges:
      # 禁止加入root组
      - min: 1
        max: 65535
  readOnlyRootFilesystem: false</pre>
<p>Pod安全策略被实现为Admission Controller，启用该控制器，但是不配置任何PodSecurityPolicy，则你无法创建任何Pod。</p>
<div class="blog_h3"><span class="graybg">策略控制角度</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">角度</td>
<td style="text-align: center;">字段名称</td>
</tr>
</thead>
<tbody>
<tr>
<td>运行特权容器</td>
<td>privileged</td>
</tr>
<tr>
<td>使用宿主名字空间</td>
<td>hostPID、hostIPC</td>
</tr>
<tr>
<td>使用宿主的网络和端口</td>
<td>hostNetwork, hostPorts</td>
</tr>
<tr>
<td>控制卷类型的使用</td>
<td>volumes</td>
</tr>
<tr>
<td>使用宿主文件系统</td>
<td>allowedHostPaths</td>
</tr>
<tr>
<td>允许使用特定的 FlexVolume 驱动</td>
<td>allowedFlexVolumes</td>
</tr>
<tr>
<td>分配拥有 Pod 卷的 FSGroup 账号</td>
<td>fsGroup</td>
</tr>
<tr>
<td>以只读方式访问根文件系统</td>
<td>readOnlyRootFilesystem</td>
</tr>
<tr>
<td>器的用户和组 ID</td>
<td>runAsUser, runAsGroup, supplementalGroups</td>
</tr>
<tr>
<td>限制 root 账号特权级提升</td>
<td>allowPrivilegeEscalation, defaultAllowPrivilegeEscalation</td>
</tr>
<tr>
<td>Linux 权能字（Capabilities）</td>
<td>defaultAddCapabilities, requiredDropCapabilities, allowedCapabilities</td>
</tr>
<tr>
<td>设置容器的 SELinux 上下文</td>
<td>seLinux</td>
</tr>
<tr>
<td>指定容器可以挂载的 proc 类型</td>
<td>allowedProcMountTypes</td>
</tr>
<tr>
<td>指定容器使用的 AppArmor 模版</td>
<td>annotations</td>
</tr>
<tr>
<td>指定容器使用的 seccomp 模版</td>
<td>annotations</td>
</tr>
<tr>
<td>指定容器使用的 sysctl 模版</td>
<td>forbiddenSysctls,allowedUnsafeSysctls</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">通过RBAC将Policy授予用户</span></div>
<p>新创建的PodSecurityPolicy不起任何作用，发起请求的用户/Pod的SA必须<span style="background-color: #c0c0c0;">对该PodSecurityPolicy具有use权限</span>：</p>
<pre class="crayon-plain-tag">apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: &lt;role name&gt;
rules:
- apiGroups: ['policy']
  resources: ['podsecuritypolicies']
  verbs:     ['use']
  resourceNames:
  - &lt;list of policies to authorize&gt;

---

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: &lt;binding name&gt;
roleRef:
  kind: ClusterRole
  name: &lt;role name&gt;
  apiGroup: rbac.authorization.k8s.io
subjects:
# 授权给特定SA
- kind: ServiceAccount
  name: &lt;authorized service account name&gt;
  namespace: &lt;authorized pod namespace&gt;
# 授权给指定用户（不推荐）
- kind: User
  apiGroup: rbac.authorization.k8s.io
  name: &lt;authorized user name&gt;</pre>
<p>如果使用RoleBinding而非ClusterRoleBinding，则仅仅特定命名空间下创建的Pod才可能获得PSP授权：</p>
<pre class="crayon-plain-tag">---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: &lt;binding name&gt;
  namespace: default
roleRef:
  kind: ClusterRole
  name: &lt;role name&gt;
  apiGroup: rbac.authorization.k8s.io
subjects:
# 该命名空间下所有SA获得授权，这些SA创建的Pod都可以使用目标PodSecurityPolicy
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:serviceaccounts
# 该命名空间下所有认证过的用户获得授权（和上个元素等效）
- kind: Group
  apiGroup: rbac.authorization.k8s.io
  name: system:authenticated</pre>
<div class="blog_h3"><span class="graybg">Policy优先级 </span></div>
<p>如果创建Pod的用户具有多个Policy的use权限，那么选取Policy的规则如下：</p>
<ol>
<li>不需要设置默认值、改变Pod规格的Policy，被优先使用</li>
<li>如果Pod必须被设置默认值 、修改规格，则使用按名称排序的第一个Policy</li>
</ol>
<div class="blog_h3"><span class="graybg">启用PSP准入控制器</span></div>
<p>需要设置API Server的参数：<pre class="crayon-plain-tag">--enable-admission-plugins=PodSecurityPolicy,...</pre>来启用此准入控制器。启用后，如果没有任何策略定义，则无法创建Pod。</p>
<div class="blog_h3"><span class="graybg">给kubelet授权</span></div>
<p>为API Server启用准入控制器PodSecurityPolicy之后，你会发现API Server的Pod不见了，这是因为Kubelet没有被授予PSP。可以为它授予：</p>
<pre class="crayon-plain-tag">apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: privileged
  labels:
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
rules:
  - apiGroups:
      - policy
    resources:
      - podsecuritypolicies
    # 引用具有完整权限的PSP
    resourceNames:
      - privileged
    verbs:
      - use

---

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: psp-nodes
  namespace: kube-system
  annotations:
    kubernetes.io/description: 'Allow nodes to create privileged pods. Should
      be used in combination with the NodeRestriction admission plugin to limit
      nodes to mirror pods bound to themselves.'
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
    kubernetes.io/cluster-service: 'true'
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: psp-privileged
subjects:
  # 授权给Kubelet
  - kind: Group
    apiGroup: rbac.authorization.k8s.io
    name: system:nodes
  - kind: User
    apiGroup: rbac.authorization.k8s.io
    # Legacy node ID
    name: kubelet</pre>
<div class="blog_h3"><span class="graybg">为当前系统生成PSP</span></div>
<p>可以利用Kubectl插件kube-psp-advisor，它可以：</p>
<ol>
<li>连接到现有K8S环境，根据其中的工作负载来创建PSP</li>
<li>读取一个YAML，根据其中的工作负载清单来创建PSP</li>
</ol>
<p>执行命令：<pre class="crayon-plain-tag">kubectl krew install advise-psp</pre>安装此插件，并且将<pre class="crayon-plain-tag">$HOME/.krew/bin</pre>加入到$PATH环境变量。</p>
<p>连接到K8S并为工作负载生成PSP/RBAC：</p>
<pre class="crayon-plain-tag"># 打印匹配当前集群所有工作负载的一个PSP
kubectl advise-psp inspect

# 打印匹配当前工作负载的PSPs，附加RBAC规则
kubectl advise-psp inspect -g

# 报告集群为什么需要PSP
kubectl advise-psp inspect --report | jq .


# 打印匹配指定命名空间的PSP
kubectl advise-psp inspect --namespace=default</pre>
<p>读取包含Pod Spec的文件，并生成PSP： </p>
<pre class="crayon-plain-tag"># --podFile中可以包含Deployment/DaemonSet/StatefulSet/Pod等形式的工作负载定义
kubectl advise-psp convert --podFile=Documents/K8S/rook/toolbox.yaml --pspFile=/tmp/psp.yaml </pre>
<div class="blog_h2"><span class="graybg">审计</span></div>
<p>API Server支持审计功能，可以记录用户/K8S组件的活动，并可以指定<span style="background-color: #c0c0c0;">监控到某些活动后的处理逻辑</span>。</p>
<div class="blog_h3"><span class="graybg">审计阶段</span></div>
<p>在处理请求的不同阶段，都可以生成审计事件，这些阶段包括：</p>
<ol>
<li>RequestReceived 审计处理器（Handler，API Server具有由一系列处理器的链条组成）接收到请求后，委托给其它Handler处理之前</li>
<li>ResponseStarted 响应头发送之后，响应体发送之前。仅仅长时间运行的请求才有这个阶段，例如watch操作</li>
<li>ResponseComplete 响应完全发送完毕后</li>
<li>Panic 当发生panic时</li>
</ol>
<p>审计事件回根据特定的策略进行预处理，记录到后端。<span style="background-color: #c0c0c0;">支持的后端包括文件、Webhook</span>。</p>
<div class="blog_h3"><span class="graybg">审计策略</span></div>
<p>你可以定义审计策略，格式如下：</p>
<pre class="crayon-plain-tag">apiVersion: audit.k8s.io/v1
kind: Policy
# 哪些阶段不做审计
omitStages:
  - "RequestReceived"
rules:
  # 审计规则
  # 审计级别：
  #   None - 符合这条规则的日志将不会记录
  #   Metadata - 记录请求的元数据（请求的用户、时间戳、资源、动词等等）， 但是不记录请求或者响应的消息体
  #   Request - 记录事件的元数据和请求的消息体，但是不记录响应的消息体。 不适用于非资源类型的请求
  #   RequestResponse - 记录事件的元数据，请求和响应的消息体。不适用于非资源类型的请求
  - level: RequestResponse
  # 规则针对的资源
    resources:
      - group: ""
        resources: ["pods", "deployments"]
  - level: Metadata
    resources:
      - group: ""
        resources: ["pods/exec", "pods/portforward", "pods/proxy", "services/proxy"]
  - level: Metadata
    omitStages:
      - "RequestReceived"</pre>
<p>下面这个示例，为所有请求在Metadata级别生成日志： </p>
<pre class="crayon-plain-tag"># 在 Metadata 级别为所有请求生成日志
apiVersion: audit.k8s.io/v1beta1
kind: Policy
rules:
- level: Metadata</pre>
<div class="blog_h3"><span class="graybg">使用日志后端</span></div>
<p>需要为API Server指定以下参数：</p>
<p style="padding-left: 30px;">--audit-log-path 指定用来写入审计事件的日志文件路径。不指定此标志会禁用日志后端<br />--audit-policy-file 审计策略文件<br />--audit-log-maxage 定义了保留旧审计日志文件的最大天数<br />--audit-log-maxbackup 定义了要保留的审计日志文件的最大数量<br />--audit-log-maxsize 定义审计日志文件的最大大小（兆字节）</p>
<p>由于通常情况下，API Server以静态Pod的形式运行，因此，需要将宿主机上的策略文件、日志文件挂载为数据卷：</p>
<pre class="crayon-plain-tag">volumeMounts:
  - mountPath: /etc/kubernetes/audit-policy.yaml
    name: audit
    readOnly: true
  - mountPath: /var/log/audit.log
    name: audit-log
    readOnly: false


volumes:
- name: audit
  hostPath:
    path: /etc/kubernetes/audit-policy.yaml
    type: File

- name: audit-log
  hostPath:
    path: /var/log/audit.log
    type: FileOrCreate</pre>
<div class="blog_h3"><span class="graybg">使用Webhook后端</span></div>
<p>你需要开发一个Webhook服务器，它提供和API Server相同的API。</p>
<p>需要为API Server指定以下参数：</p>
<p style="padding-left: 30px;">--audit-webhook-config-file webhook 配置文件的路径。Webhook 配置文件实际上是一个 kubeconfig 文件<br /> --audit-webhook-initial-backoff 指定在第一次失败后重发请求等待的时间。随后的请求将以指数退避重试</p>
<div class="blog_h3"><span class="graybg">批处理模式</span></div>
<p>不管使用哪种后端，都可以支持批处理，也就是缓存一批审计日志，然后一起发送给后端。默认情况下Log后端禁用批处理，Webhook后端启用批处理。</p>
<p>如果使用Webhook后端，你可以通过下面的参数配置批处理模式：</p>
<p style="padding-left: 30px;">--audit-webhook-mode 定义缓存策略，可选值如下：</p>
<ol>
<ol>
<li>batch   缓存事件，异步发送给后端。这是默认值</li>
<li>blocking   在 API 服务器处理每个单独事件时，阻塞其响应</li>
<li>blocking-strict  与 blocking 相同，不过当审计日志在 RequestReceived 阶段失败时，整个 API 服务请求会失效</li>
</ol>
</ol>
<p>当上述mode设置为batch时，你可以使用下面的额外参数：</p>
<p style="padding-left: 30px;">--audit-webhook-batch-buffer-size 定义 batch 之前要缓存的事件数。 如果传入事件的速率溢出缓存区，则会丢弃事件<br /> --audit-webhook-batch-max-size 定义一个 batch 中的最大事件数<br /> --audit-webhook-batch-max-wait 无条件 batch 队列中的事件前等待的最大事件<br /> --audit-webhook-batch-throttle-qps 每秒生成的最大批次数<br /> --audit-webhook-batch-throttle-burst 在达到允许的 QPS 前，同一时刻允许存在的最大 batch 生成数</p>
<p>使用Log后端时，将上述参数的前缀都改为--audit-log。</p>
<div class="blog_h2"><span class="graybg"><a id="opa"></a>Open Policy Agent</span></div>
<p><a href="https://www.openpolicyagent.org/docs/latest">开放策略代理</a>（OPA，读音oh-pa）是一个开源的、一般用途（不依赖于K8S）的策略引擎。OPA提供接口：</p>
<ol>
<li>声明式的语言Rego（读音ray-go），用来编写策略</li>
<li>简单的API，用于从其它应用来发起策略决策（policy decision-making）</li>
</ol>
<p>当需要作出策略决策时，你需要查询OPA，以结构化数据作为输入。OPA根据输入、策略定义，来输出策略决策。OPA和Rego是业务无感知（domain-agnostic）的，你几乎用它来描述任何策略，例如：</p>
<ol>
<li>什么用户可以访问什么资源</li>
<li>出口流量可以访问什么子网</li>
<li>工作负载必须部署到哪个集群</li>
<li>允许从何处下载容器镜像</li>
<li>容器可以使用哪些操作系统权能（Capability）</li>
<li>在什么时间允许访问系统</li>
</ol>
<p>OPA和PSP之类的K8S内置安全机制比起来，功能更加广泛。</p>
<p>策略决策也不简单的是Yes/No，而可以是任何结构化的数据。</p>
<div class="blog_h3"><span class="graybg">决策输入</span></div>
<p>可以是任何结构化的数据。例如一个描述系统状态的JSON：</p>
<pre class="crayon-plain-tag">// 这个数据结构描述了一个网络拓扑
{
    "servers": [
        {"id": "app", "protocols": ["https", "ssh"], "ports": ["p1", "p2", "p3"]},
        {"id": "db", "protocols": ["mysql"], "ports": ["p3"]},
        {"id": "cache", "protocols": ["memcache"], "ports": ["p3"]},
        {"id": "ci", "protocols": ["http"], "ports": ["p1", "p2"]},
        {"id": "busybox", "protocols": ["telnet"], "ports": ["p1"]}
    ],
    "networks": [
        {"id": "net1", "public": false},
        {"id": "net2", "public": false},
        {"id": "net3", "public": true},
        {"id": "net4", "public": true}
    ],
    "ports": [
        {"id": "p1", "network": "net1"},
        {"id": "p2", "network": "net3"},
        {"id": "p3", "network": "net2"}
    ]
}</pre>
<div class="blog_h3"><span class="graybg">Rego语言语法</span></div>
<p>此语言用于针对复杂数据结构（输入）来描述策略。语法类似于Go： </p>
<pre class="crayon-plain-tag">// 访问接口输入的service数据结构
input.servers
// 访问数组元素
input.servers[0].protocols[0]

// 如果访问不存在的数据，则OPA返回undefined decision，表示OPA不能根据输入作出决策
input.deadbeef

// 如果需要作出策略决策，你需要编写表达式
input.servers[0].id == "app"

// Rego提供了一些内置函数
count(input.servers[0].protocols) &gt;= 1


// 多个表达式可以使用逻辑与操作符 ; 链接在一起
input.servers[0].id == "app"; input.servers[0].protocols[0] == "https"
// 等价于
input.servers[0].id == "app"
input.servers[0].protocols[0] == "https"
// 任何一个表达式结果不是true，或者是undefined，则决策结果是undefined decision


// 变量
s := input.servers[0]
s.id == "app"
p := s.protocols[0]
p == "https"


// 迭代数据集
// 获得满足条件的network索引集合
some i; input.networks[i].public == true
// 获得满足条件的server、protocol索引组合
some i, j; input.servers[i].protocols[j] == "http"
// 变量可以被赋值给其它变量
some i, j
id := input.ports[i].id
input.ports[i].network == input.networks[j].id
input.networks[j].public
// +---+------+---+
// | i |  id  | j |
// +---+------+---+
// | 1 | "p2" | 2 |
// +---+------+---+

// 如果后续你不需要引用变量名，可以用_代替：
input.servers[_].protocols[_] == "http"
# true

// 如果找不到什么匹配表达式的变量，则结果是undefined
some i; input.servers[i].protocols[i] == "ssh"
// undefined decision</pre>
<div class="blog_h3"><span class="graybg">完整规则</span></div>
<p>Rego支持将可复用逻辑封装为规则（Rule） 。</p>
<p>规则可以是完整的（Complete），这样的规则是一个if-then语句，并且分配单个值到变量：</p>
<pre class="crayon-plain-tag">package example.rules

//                    这里的true可以省略
any_public_networks = true {  # 变量any_public_networks是true，如果
    net := input.networks[_]  # 存在某个网络
    net.public                # 并且它是public的
}</pre>
<p>规则由头、体两部分组成。如果<span style="background-color: #c0c0c0;">规则体中的某种变量组合（例如上面的_取值为0）可以让规则体结果为true，那么规则头为true</span>。</p>
<p>要调用上面的规则，使用它的名字即可<pre class="crayon-plain-tag">any_public_networks</pre>。规则产生的值存放在全局变量data中：</p>
<pre class="crayon-plain-tag">//   包名          值
data.example.rules.any_public_networks
// true</pre>
<p>如果想定义一个常量，省略规则体即可： </p>
<pre class="crayon-plain-tag">package example.constants

pi = 3.14</pre>
<p>如果找不到任何变量组合，满足规则体的表达式，则规则是未定义的（undefined decision），</p>
<div class="blog_h3"><span class="graybg">部分规则</span></div>
<p>规则可以是部分（Partial）的，与完整规则生成一个值不同，部分规则生成一组值，并且将这组值分配给一个变量：</p>
<pre class="crayon-plain-tag">package example.rules

public_network[net.id] {      # net.id 作 public_network的元素，如果
    net := input.networks[_]  # 如果网络存在
    net.public                # 且它是public的
}</pre>
<p>你可以查询生成的数据集的值：</p>
<pre class="crayon-plain-tag">public_network

// [
//  "net3",
//  "net4"
// ]

// public_network["net3"]
// "net3"</pre>
<p>通过部分规则，除了定义上面这种Set，还可以定义键值对（对象）：</p>
<pre class="crayon-plain-tag">apps_by_hostname[hostname] = app {
    some i
    server := sites[_].servers[_]
    hostname := server.hostname
    apps[i].servers[_] = server.name
    app := apps[i].name
}

// apps_by_hostname["hello"]
// "world"</pre>
<div class="blog_h3"><span class="graybg">规则逻辑或</span></div>
<p>下面的语句，表示协议为telnet或者ssh， shell_accessible的值都是true：</p>
<pre class="crayon-plain-tag">package example.logical_or

// 如果所有具有此名字的规则的结果都是undefined，则设置默认值
default shell_accessible = false

shell_accessible = true {
    input.servers[_].protocols[_] == "telnet"
}

shell_accessible = true {
    input.servers[_].protocols[_] == "ssh"
}

// shell_accessible
// true</pre>
<p>针对部分规则进行逻辑或，则最终的Set，是参与逻辑或的规则的结果的并集： </p>
<pre class="crayon-plain-tag">package example.logical_or

shell_accessible[server.id] {
    server := input.servers[_]
    server.protocols[_] == "telnet"
}

shell_accessible[server.id] {
    server := input.servers[_]
    server.protocols[_] == "ssh"
}

// shell_accessible
// [
//   "busybox",
//   "db"
// ]</pre>
<div class="blog_h3"><span class="graybg">规则示例 </span></div>
<p>针对上面的数据集， 假设我们需要检查拓扑是否满足安全策略：</p>
<ol>
<li>所有可以通过Internet访问的服务器，不能暴露http协议</li>
<li>所有服务器不得暴露telnet协议</li>
</ol>
<p>可以这样编写Rego规则：</p>
<pre class="crayon-plain-tag">package example

default allow = false                               # 除非明确定义，否则任何不满足策略

allow = true {                                      # 如果违反集中没有成员，则满足策略
    count(violation) == 0                           
}

// 两个部分规则进行逻辑与
violation[server.id] {                              # 以下条件下服务器（的ID）加入违反集
    some server
    public_server[server]                           # 它存在于public_server集
    server.protocols[_] == "http"                   # 它暴露了http协议
}

violation[server.id] {                              # 以下条件下服务器（的ID）加入违反集
    server := input.servers[_]                      # 它属于输入的服务器集
    server.protocols[_] == "telnet"                 # 它暴露了telnet协议
}

// 下面的规则生成上面规则需要的public_server集
public_server[server] {                             # 以下条件下服务器加入public_server集
    some i, j
    server := input.servers[_]                      # 服务器位于输入的服务器集中
    server.ports[_] == input.ports[i].id            # 服务器的ports定义在输入端口集中
    input.ports[i].network == input.networks[j].id  # 端口的network定义在输入的网络集中
    input.networks[j].public                        # 网络是public的
}</pre>
<div class="blog_h2"><span class="graybg">Gatekeeper</span></div>
<p>Gatekeeper将OPA整合到Kubernetes，它通过Validation Webhook和API Server对接，允许Kubernetes 管理员可以定义策略来保证集群的合规性，并符合最佳实践的要求。</p>
<p>使用Gatekeeper时，你需要在自定义资源中声明OPA规则。Gatekeeper会同步规则到OPA引擎中，当API Server接收到各种资源创建请求后，会转发给Gatekeeper进行策略检查。</p>
<div class="blog_h3"><span class="graybg">ContstraintTemplate</span></div>
<p>GateKeeper使用此CR来存储OPA规则，下面的例子，禁止使用latest标签的镜像：</p>
<pre class="crayon-plain-tag">apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8simagetagvalid
spec:
  # 此模板生成的自定义资源
  crd:
    spec:
      names:
        kind: K8sImageTagValid
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8simagetagvalid

        # 输入是传递给Webhook的请求对象
        
        # 逻辑或构造违反集
        # 规则1
        violation[{"msg": msg, "details":{}}] {
          # 如果某个容器的镜像
          image := input.review.object.spec.template.spec.containers[_].image
          # 没有指定tag部分
          not count(split(image, ":")) == 2
          # 则加入违反集，且构造出违反集元素
          msg := sprintf("image '%v' doesn't specify a valid tag", [image])
        }
        # 规则2
        violation[{"msg": msg, "details":{}}] {
          # 如果某个容器的镜像名字以latest结尾
          image := input.review.object.spec.template.spec.containers[_].image
          endswith(image, "latest")
          msg := sprintf("image '%v' uses latest tag", [image])
        }

        # 隐含下面的规则
        # allow = true {
        #     count(violation) == 0 
        # }</pre>
<div class="blog_h3"><span class="graybg">Constraint</span></div>
<p>有了ContstraintTemplate，还不会对集群产生任何影响。你还需要定义对应的 Constraint —— 由ContstraintTemplate生成的自定义资源：</p>
<pre class="crayon-plain-tag">apiVersion: constraints.gatekeeper.sh/v1beta1
# 类型是ContstraintTemplate创建的CRD的名字
kind: K8sImageTagValid
metadata:
  name: valid-image-tag
spec:
  # 如果启用dryrun，不会阻止资源的创建，而是仅仅记录违反策略的情况
  enforcementAction: dryrun
  # 将策略应用到哪些类型的K8S资源
  match:
    kinds:
      - apiGroups: [ "apps" ]
        kinds: [ "Deployment", "StatefulSet" ]</pre>
<p>在dryrun模式下，可以describe 上述资源，查询策略违反情况。</p>
<div class="blog_h1"><span class="graybg">CNI</span></div>
<p>CNI即容器网络接口（Container Network Interface），比起Docker的CNI，对开发者以来更少，也不受限制于Docker。</p>
<p>在CNI中，网络插件是一个独立的可执行文件，它会被上层容器平台调用，并实现：</p>
<ol>
<li>将容器加入到网络</li>
<li>将容器从网络中删除</li>
</ol>
<p>调用时需要的信息，通过环境变量、或标准输入传递。</p>
<p>实现CNI很简单，只需要：</p>
<ol>
<li>一个配置文件，描述CNI插件的版本、名称、描述等信息</li>
<li>开发CNI的可执行文件：
<ol>
<li>从环境变量或命令行中读取需要执行的操作、目标网络命名空间、容器网卡必要信息</li>
<li>实现两个操作：ADD/DEL</li>
</ol>
</li>
</ol>
<p>Kubernetes调用CNI的流程如下：</p>
<ol>
<li>调用CRI创建pause容器，生成网络命名空间</li>
<li>调用网络驱动，即CNI驱动</li>
<li>CNI驱动根据配置调用CNI插件</li>
<li>CNI插件给pause容器配置正确的网络。Pod中其它容器都使用pause容器的网络栈</li>
</ol>
<p>CNI具体负责的事情包括：</p>
<ol>
<li>为容器分配IP地址</li>
<li>为荣庆网络接口绑定IP地址</li>
<li>实现多主机连接功能</li>
</ol>
<div class="blog_h2"><span class="graybg">常见CNI实现简介</span></div>
<div class="blog_h3"><span class="graybg">flannel</span></div>
<p>flannel由CoreOS开发，能让集群内所有容器使用一个网络，<span style="background-color: #c0c0c0;">每个主机从该网络中划分一个子网</span>（防止IP地址冲突），主机创建容器中从子网分配一个IP地址。</p>
<p>为了解决容器之间的通信（路由）问题，flanne支持多种网络模式：</p>
<ol>
<li>overlay：早期使用的模式。这种模式下容器发出的数据包被flannel再次包装。新包头里面包含了主机本身IP，确保封包可以在底层网络中正确传递。再次包装的方式有：
<ol>
<li>UDP：flannel自定义的一种协议。在用户态解包、封包</li>
<li>VXLAN：在内核态解包、封包，性能比UDP好很多</li>
</ol>
</li>
<li>Host-Gateway模式：由运行在各节点上的Agent容器，将容器网络的路由信息同步到所有节点。这种模式不需要修改封包，<span style="background-color: #c0c0c0;">性能甚至比Calico更好</span>。问题是，<span style="background-color: #c0c0c0;">Host-Gateway模式只能改节点的路由，底层网络必须是二层</span>的，不像Calico的BGP那样可以将路由同步到路由器。由于广播封包问题，这种网络通常规模不能太大。近年来，也出现一些专门的设备，用以支持大规模二层网络，即所谓”大二层“</li>
</ol>
<p>从架构上来说，flannel分为两部分：</p>
<ol>
<li>控制平面：主要包含一个Etcd，用于协调各节点上容器网段的划分。控制平面没有服务器</li>
<li>数据平面，运行在节点上的flanneld进程，它会读取Etcd获取空闲网段，也把自己申请到的网段信息回填到Etcd中</li>
</ol>
<p>对于”固定IP“这种需求，flannel天生无法支持，原因是节点和网络的绑定关系。</p>
<div class="blog_h3"><span class="graybg">Calico</span></div>
<p>该插件将所有节点看做虚拟路由器，使用BGP协议，可以跨越多个二层网络。</p>
<p>除了提供网络连接外，Calico还支持网络策略，可以对容器、虚拟机工作负载、宿主机各节点之间的网络通信进行细粒度、动态的控制。</p>
<p>Calico还支持和Istio集成，在Service Mesh层面实施网络策略。</p>
<p>Calico允许容器漂移，在不同主机之前迁移容器不受限制。</p>
<p>Calico也支持隧道模式（ipip），性能稍差，但是对底层网络没有要求。</p>
<div class="blog_h3"><span class="graybg">Cilium</span></div>
<p>Cilium是一个CNI插件，是一个<span style="background-color: #c0c0c0;">API感知</span>的网络、安全开源软件，独特之处是安全，而且能理解<span style="background-color: #c0c0c0;">RESTful、Kafka协议</span>这样的API，因此比那种基于IP、端口的防火墙软件厉害的多。</p>
<p>在微服务时代，iptables无法满足安全策略的需求，一个安全策略的数量很多，而且需要频繁的更新。解决办法是利用BPF（Berkeley Packet Filter），它是一个内核中的高性能沙箱虚拟机，将内核变为可编程的。BPF可以在不影响安全、性能的前提下扩展Linux内核，进行网络数据包的处理。</p>
<p>BPF是类似于iptables的框架，允许在内核的多个挂钩点添加逻辑。</p>
<p>BPF是一个虚拟机，你可以将封包过滤规则以BPF虚拟机指令的形式写入内核，BPF会对指令进行验证，并JIT编译成CPU指令。任何网络报文都不能绕开BPF。</p>
<p>Cilium原生的理解服务、容器、Pod标志，能够理解gRPC、HTTP、Kafka等协议。</p>
<p>在服务网络方面，Cilium和IPVS具有同样的O(1)复杂度，可以用来作为Kube Proxy的代替。</p>
<p>Cilium甚至可以用来加速Istio。Istio的Sidecar可能引入10倍的性能损耗，原因是Istio使用iptables在数据包级别执行重定向，这导致每个数据包需要多次通过完整的网络栈。Linux内核基于BPF进行了一项所谓<span style="background-color: #c0c0c0;">Sockmap</span>的改进，可以直接在套接字层安全的进行重定向，QPS比iptables、甚至loopback网卡更高。</p>
<div class="blog_h3"><span class="graybg">CNI-Genie</span></div>
<p>华为开源的CNI适配器，它允许你动态按需的切换底层CNI插件。它允许用户在同一个集群中<span style="background-color: #c0c0c0;">使用多个底层CNI</span>，为<span style="background-color: #c0c0c0;">容器创建多个网络接口</span>。</p>
<div class="blog_h1"><span class="graybg">管理配置</span></div>
<div class="blog_h2"><span class="graybg">管理任务概览</span></div>
<div class="blog_h3"><span class="graybg">集群规划</span></div>
<p>第一步是选择<a href="https://kubernetes.io/docs/setup/pick-right-solution/">适当的部署方案</a>。K8S支持单机、本地计算机集群、云服务、VM集群、裸金属等多种运行环境，你需要考虑以下几点：</p>
<ol>
<li>是想在本机上学习K8S，还是需要搭建高可用、多节点的集群</li>
<li>为了实现HA，可以考虑跨Zone集群</li>
<li>某些服务商提供开箱即用的K8S集群，例如GKE</li>
<li>集群是内部部署（on-premises），还是在云端（IaaS）</li>
<li>对于内部部署的集群，你需要考虑使用何种K8S的网络模型</li>
<li>是否希望在裸金属或者VM上运行K8S</li>
</ol>
<div class="blog_h3"><span class="graybg">集群管理</span></div>
<p>集群的日常管理任务包括：</p>
<ol>
<li>集群生命周期管理：创建集群、升级Master/Worker节点、执行节点维护（例如内核升级）、升级API版本</li>
<li>对于多个团队共享的集群，需要进行配额的管理</li>
</ol>
<div class="blog_h3"><span class="graybg">集群安全</span></div>
<p>安全性相关的管理主题包括：</p>
<ol>
<li>数字证书管理</li>
<li>K8S API的访问控制</li>
<li>身份验证和授权</li>
<li>准许控制器：能够在身份验证和授权后，拦截发送给K8S API服务器的请求</li>
<li>内核参数调整（sysctl）</li>
<li>审计，和K8S的审计日志交互</li>
</ol>
<p>和Kubelet安全性相关的管理主题：</p>
<ol>
<li>与Master节点的安全通信</li>
<li>TLS支持</li>
<li>Kubelet的身份验证/授权</li>
</ol>
<div class="blog_h2"><span class="graybg">数字证书管理</span></div>
<p>要基于数字证书来进行客户端身份验证，你可以选用现有的部署脚本，或者使用easyrsa、openssl、cfssl之类的工具来生成数字证书。</p>
<div class="blog_h3"><span class="graybg">脚本</span></div>
<p>现有部署脚本调用方式：</p>
<pre class="crayon-plain-tag"># subject-alternate-names格式为 IP: or DNS:的列表
cluster/saltbase/salt/generate-cert/make-ca-cert.sh  ip-of-apiserver subject-alternate-names</pre>
<p>上述脚本产生三个文件：ca.crt, server.crt, server.key。然后修改API Server的启动参数：</p>
<pre class="crayon-plain-tag">--client-ca-file=/srv/kubernetes/ca.crt
--tls-cert-file=/srv/kubernetes/server.crt
--tls-private-key-file=/srv/kubernetes/server.key</pre>
<div class="blog_h3"><span class="graybg">openssl</span></div>
<p>基于OpenSSL来手工生成证书的方法如下：</p>
<ol>
<li>生成CA的密钥对：<pre class="crayon-plain-tag">openssl genrsa -out ca.key 2048</pre></li>
<li>CA生成自签名证书：<pre class="crayon-plain-tag">openssl req -x509 -new -nodes -key ca.key -subj "/CN=${MASTER_IP}" -days 10000 -out ca.crt</pre></li>
<li>生成API Server的密钥对：<pre class="crayon-plain-tag">openssl genrsa -out server.key 2048</pre></li>
<li>创建一个配置文件，用于生成API Server的证书签名请求（CSR）：<br />
<pre class="crayon-plain-tag">[ req ]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
    
[ dn ]
C = 
ST = 
L = 
O = 
OU = 
CN = &lt;MASTER_IP&gt;
    
[ req_ext ]
subjectAltName = @alt_names

# 所有可能用于访问APIServer的域名或IP地址    
[ alt_names ]
DNS.1 = kubernetes
DNS.2 = kubernetes.default
# 假设你使用cluster.local作为域名（后缀），这个可配置
DNS.3 = kubernetes.default.svc
DNS.4 = kubernetes.default.svc.cluster
DNS.5 = kubernetes.default.svc.cluster.local
IP.1 = &lt;MASTER_IP&gt;
# 往往是Servic CIDR中（由APIServer、控制器管理全的--service-cluster-ip-range参数指定）的第一个地址
IP.2 = &lt;MASTER_CLUSTER_IP&gt;
    
[ v3_ext ]
authorityKeyIdentifier=keyid,issuer:always
basicConstraints=CA:FALSE
keyUsage=keyEncipherment,dataEncipherment
extendedKeyUsage=serverAuth,clientAuth
subjectAltName=@alt_names </pre>
</li>
<li>生成CSR：<pre class="crayon-plain-tag">openssl req -new -key server.key -out server.csr -config csr.conf</pre></li>
<li>根据CSR来签名，生成APIServer的服务器证书：<br />
<pre class="crayon-plain-tag">openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
    -CAcreateserial -out server.crt -days 10000 \
    -extensions v3_ext -extfile csr.conf</pre>
</li>
</ol>
<p>然后，用相同的方法，修改APIServer的启动参数。</p>
<div class="blog_h3"><span class="graybg">分发自签名CA证书</span></div>
<p>自签名证书可能不被客户端节点信任，你需要手工分发：</p>
<pre class="crayon-plain-tag"># 在每个客户端执行：
# 复制CA证书到本地证书目录
sudo cp ca.crt /usr/local/share/ca-certificates/kubernetes.crt
# 更新证书配置
sudo update-ca-certificates</pre>
<div class="blog_h3"><span class="graybg">证书API</span></div>
<p>你可以使用<a href="https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster">certificates.k8s.io</a>API来提供数字证书。 </p>
<div class="blog_h2"><span class="graybg">资源管理</span></div>
<div class="blog_h3"><span class="graybg">组织资源配置</span></div>
<p>很多应用程序需要多项K8S资源（对象），你可以在单个YAML中同时定义它们，只需要用 <pre class="crayon-plain-tag">---</pre> 分隔两项资源即可。 </p>
<p>K8S会按照YAML中的声明顺序来创建资源，因此你应该先定义Service、再定义Deployment。</p>
<p><pre class="crayon-plain-tag">kubectl create</pre>也支持多个<pre class="crayon-plain-tag">-f</pre>参数，你可以同时指定多个配置文件。你还可以为-f指定一个配置目录，其中的yaml、yml、json文件都会被读取。你也可以为-f 指定为一个URL</p>
<p>K8S建议：</p>
<ol>
<li>将和一个微服务/应用程序分层（Tier）相关的资源，放到同一配置文件中</li>
<li>将和你的整个应用程序相关的所有资源，放到同一目录中</li>
<li>如果你的应用程序各Tier通过DNS相互绑定，那么，你可以简单的一起部署所有组件</li>
</ol>
<p>使用<pre class="crayon-plain-tag">-R</pre>参数可以递归的读取子目录中的配置文件，来创建对象。</p>
<p>使用<pre class="crayon-plain-tag">-o</pre>参数可以输出被创建对象的名称：</p>
<pre class="crayon-plain-tag"># -o name 输出resources/name格式  
kubectl get $(kubectl create -f docs/user-guide/nginx/ -o name | grep service)
# NAME           CLUSTER-IP   EXTERNAL-IP   PORT(S)      AGE
# my-nginx-svc   10.0.0.208        80/TCP       0s</pre>
<div class="blog_h3"><span class="graybg">kubectl批量操作</span></div>
<p>除了创建资源，其它操作也支持批量式的操作。例如删除操作：</p>
<pre class="crayon-plain-tag"># 从指定目录中寻找资源配置，并删除对应的K8S对象
kubectl delete -f docs/user-guide/nginx/
# 删除多个对象，对象以 resources/name格式指定
kubectl delete deployments/my-nginx services/my-nginx-svc
# 删除多个对象，根据标签 + 类型
kubectl delete deployment,services -l app=nginx</pre>
<div class="blog_h2"><span class="graybg">集群网络管理 </span></div>
<p>集群的网络架构和Docker不同，它需要解决四个不同的网络问题：</p>
<ol>
<li>高耦合容器之间的通信，在Pod内部，使用localhost相互通信</li>
<li>Pod之间的通信，使用集群网络，是本节的主题</li>
<li>Pod与Service之间的通信，利用K8S的Service机制解决</li>
<li>外部与Service之间的通信，也是利用K8S的Service机制解决</li>
</ol>
<p>K8S假设所有Pod之间都有通信的需求，因此不去罗嗦的为容器之间指定link。K8S为每个Pod分配了IP地址，你几乎从不需要进行容器到宿主机的端口映射。</p>
<p>Pod更像是一个VM或者物理主机，不管从端口分配、命名、服务发现、负载均衡、应用程序配置、迁移等角度。</p>
<div class="blog_h3"><span class="graybg">Docker网络架构</span></div>
<p>容器使用宿主机私有网络，它会创建一个名为docker0的虚拟网桥，并分配一个子网。每个创建的容器被分配一个虚拟以太网设备（veth），这些以太网卡被连接到网桥。在容器中veth被映射为eth0这个网络接口（使用了Linux名字空间机制，因此不会和宿主机冲突）。容器中的eth0被分配以网桥地址范围中的一个IP。</p>
<p>结果是，同一宿主机上，基于同一网桥（docker network）的容器可以相互通信。但是跨宿主机则无法通信，需要使用端口映射机制（映射到宿主机），你要么需要小心的分配端口，要么使用自动端口映射。</p>
<div class="blog_h3"><span class="graybg">K8S网络架构</span></div>
<p>K8S强制要求了以下网络特性：</p>
<ol>
<li>所有容器之间的通信，不使用NAT（端口映射）</li>
<li>所有节点 - 容器之间的通信不使用NAT</li>
<li>容器看到自己的IP地址，其它角色也能看到</li>
</ol>
<p>K8S为每个Pod分配了一个IP地址，Pod中的容器共享此IP地址。在Docker中，实现方式是，利用一个容器持有网络名字空间，然后Pod中其它容器加入到此名字空间：<pre class="crayon-plain-tag">--net=container:</pre></p>
<p>可以利用多种技术来实现K8S的网络架构，例如Cilium、Contiv、Contrail、Flannel、GCE、Kube-router、Linux网桥、Multus、OpenVSwitch、OVN </p>
<div class="blog_h2"><span class="graybg">日志架构 </span></div>
<p>应用和系统日志有利于了解集群中发生了什么，在调试问题和监控集群性能时日志非常有效。大部分应用程序都实现了某种日志机制，大部分容器引擎都支持某种日志收集方式，最常用的时标准输出/错误。</p>
<p>应用、容器的日志机制不能作为K8S的日志解决方案，容器可能崩溃、Pod可能被清除、节点也可能死掉，即使这些异常情况出现，你仍然需要访问自己的应用程序日志。</p>
<p>K8S引入集群级别日志（cluster-level-logging）的概念。CLL要求独立的日志存储机制，以便分析和查询。</p>
<p>K8S没有提供原生的日志数据存储方案，但是你可以把现有的方案集成进来。CLL假设你已经做好了位于集群内部/外部的存储方案</p>
<div class="blog_h3"><span class="graybg">基本日志</span></div>
<p>对于容器输出到标准输出的日志，可以使用命令直接查看：</p>
<pre class="crayon-plain-tag">kubectl logs counter   # counter是Pod名
# 要查看先前的容器实例（已经崩溃）的日志，可以附加 --previous 参数
# 如果Pod中有多个容器，使用 -c CONTAINER 参数可以查看指定容器的日志</pre>
<p>节点本地的Kubelet负责响应kubectl logs的请求。</p>
<div class="blog_h3"><span class="graybg">节点级日志</span></div>
<p>所有容器化应用程序输出到stderr/stdout的信息都被容器引擎重定向到某个地方 。Docker将其重定向到一个日志驱动处，K8S配置了个日志驱动，将容器日志输出为JSON文件格式。</p>
<p>默认情况下，容器重启后，Kubelet保留被终结的容器及其日志。如果Pod被从节点清除，所有容器及其日志都被清除。</p>
<p>对于节点级日志，需要实现日志轮换，防止节点存储空间被耗尽。K8S本身目前不负责日志轮换，但是kube-up.sh脚本提供了logrotate工具，负责每小时处理日志轮换。你也可以自己启动容器来处理日志轮换（例如基于Docker的log-opt）。</p>
<div class="blog_h3"><span class="graybg">K8S系统组件日志 </span></div>
<p>K8S系统组件可以分为两类：</p>
<ol>
<li>运行在容器中的组件，例如K8S Scheduler、kube-proxy。这类组件的日志总是写入到/var/log目录</li>
<li>直接运行在节点上的组件，例如Docker、Kubelet。在启用systemd的机器上，这些组件的日志写入到journald，没有启用systemd则写入到/var/log目录</li>
</ol>
<p>查看日志的命令示例：</p>
<pre class="crayon-plain-tag"># Ubuntu 16.04
journalctl -u kubelet -f</pre>
<p>日志冗长级别说明：</p>
<pre class="crayon-plain-tag">--v=0	Generally useful for this to ALWAYS be visible to an operator.
--v=1	A reasonable default log level if you don’t want verbosity.
--v=2	Useful steady state information about the service and important log messages that may correlate to significant changes in the system. This is the recommended default log level for most systems.
--v=3	Extended information about changes.
--v=4	Debug level verbosity.
--v=6	Display requested resources.
--v=7	Display HTTP request headers.
--v=8	Display HTTP request contents.</pre>
<div class="blog_h3"><span class="graybg">集群级别日志 </span></div>
<p>要实现集群级别的日志，你可以参考一些通用的实现途径：</p>
<ol>
<li>在每个节点上，使用节点级别的日志代理</li>
<li>在Pod中包含一个专用的、负责日志的容器</li>
<li>从应用程序中直接推送日志到一个后端实现 </li>
</ol>
<div class="blog_h2"><span class="graybg">垃圾回收</span></div>
<p>Kubelet的垃圾回收功能可以适时的清理无用的镜像、容器。默认镜像清理5min一次，容器清理1min一次。</p>
<div class="blog_h3"><span class="graybg">镜像回收</span></div>
<p>镜像管理器（在cadvisor的协作下）负责管理image的生命周期。两个相关的因素为HighThresholdPercent、LowThresholdPercent。如果磁盘用量大于HighThresholdPercent则会触发镜像的回收，会按照LRU算法清理镜像，知道磁盘用量小于LowThresholdPercent。</p>
<p>要定制上述两个参数，修改Kubelet的启动选项：image-gc-high-threshold（默认90%）、image-gc-low-threshold（默认80%）</p>
<div class="blog_h3"><span class="graybg">容器回收</span></div>
<p>和容器回收的三个用户定义参数为：</p>
<ol>
<li>MinAge，容器的最小生命，超过此生命时间才能被回收。对应Kubelet启动选项minimum-container-ttl-duration</li>
<li>MaxPerPodContainer，单个容器拥有的死亡容器个数。对应Kubelet启动选项maximum-dead-containers-per-container</li>
<li>MaxContainers，总计最大死亡容器个数。对应Kubelet启动选项maximum-dead-containers</li>
</ol>
<p>设置MinAge为0，MaxPerPodContainer、MaxContainers为负数则禁用回收。</p>
<div class="blog_h2"><span class="graybg">集群联邦</span></div>
<p>某些应用场景下，你可能考虑使用多个K8S集群：</p>
<ol>
<li>降低延迟：在不同区域（Regions）部署集群，让用户访问离的近的入口点</li>
<li>故障隔离：使用多个较小的集群，可能比一个大的集群更加容易隔离故障。你可以考虑在一个云服务商的多个Zone分别创建集群</li>
<li>扩容：单个K8S集群不能无限扩容，尽管对于大部分用户来说，单个K8S集群足够使用</li>
<li>混合云：你需要在多个云服务商、内部部署数据中心上创建多个集群</li>
</ol>
<p>联邦（Federation）用于管理多个K8S集群，它的主要功能是：</p>
<ol>
<li>跨集群同步资源，保证资源（例如Deployment）在多个集群之间保持同步</li>
<li>跨集群发现，支持基于来自多个集群的后端，来配置DNS和负载均衡器</li>
<li>HA：防止整个集群的崩溃、断网导致的不可用</li>
<li>避免绑定到云服务商，因为可以方便的跨集群迁移应用程序</li>
</ol>
<p>使用联邦时，单个集群的Scope应该保持在单个Zone/Availability Zone内部，原因是：</p>
<ol>
<li>比起全局单一大集群，这样部署可以减少单点故障</li>
<li>可用性属性更加容易估算</li>
<li>开发者设计系统时，可以对网络延迟、带宽等进行有根据的假设</li>
</ol>
<p>在单个Zone/Availability Zone部署多个集群也是可以的，但是K8S建议越少越好，原因是：</p>
<ol>
<li>可以减少运维成本（但是随着运维工具、过程的成熟此优势会变小）</li>
<li>减少每集群的固定资源开销，例如APIServer VMs（但是对于大型集群来说，这点开销也不算什么）</li>
</ol>
<div class="blog_h2"><span class="graybg">内核调优</span></div>
<p>Linux中的sysctl接口允许管理员在运行时修改内核参数。这些参数存在于虚拟文件系统/proc/sys/中。这些参数牵涉到多个子系统，例如：</p>
<ol>
<li>内核（前缀kernel.）</li>
<li>网络（前缀net.）</li>
<li>虚拟内存（前缀vm.）</li>
<li>MDADM（前缀dev.）</li>
</ol>
<p>执行命令<pre class="crayon-plain-tag">sudo sysctl -a</pre>可以获得所有系统参数的取值。</p>
<div class="blog_h3"><span class="graybg">名字空间支持</span></div>
<p>很多sysctl支持名字空间，包括kernel.shm*、kernel.msg*、kernel.sem、fs.mqueue.*、net.*。也就是说，节点上的每个Node可以分别、独立的设置这些内核参数。</p>
<p>不支持名字空间的sysctl，叫做节点级sysctl，必须由管理员手工设定。</p>
<p>对于定制了sysctl的节点，最好标记为tainted，并且仅仅将需要这些特定的sysctl设置的Pod调度到其上。</p>
<div class="blog_h3"><span class="graybg">安全sysctl </span></div>
<p>Sysctls被划分为两组：安全、不安全。安全Sysctl必须能适当的在同节点上的Pod之间隔离：</p>
<ol>
<li>不能对其它Pod产生任何影响</li>
<li>不能危害节点的健康</li>
<li> 不能超过配额获取CPU、内存资源</li>
</ol>
<p>对于K8S 1.4来说，以下Sysctl属于安全的：kernel.shm_rmid_forced、net.ipv4.ip_local_port_range、net.ipv4.tcp_syncookies</p>
<p>非安全的Sysctl默认是被禁用的，集群管理员必须手工在目标节点上启用：</p>
<pre class="crayon-plain-tag"># 对于Kubelet
kubelet --experimental-allowed-unsafe-sysctls 'kernel.msg*,net.ipv4.route.min_pmtu'
# 对于 Minikube
minikube start --extra-config="kubelet.AllowedUnsafeSysctls=kernel.msg*,net.ipv4.route.min_pmtu"...</pre>
<div class="blog_h3"><span class="graybg">为Pod设置</span></div>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
metadata:
  name: sysctl-example
  # 使用注解实现针对Pod的sysctl调优
  annotations:
    security.alpha.kubernetes.io/sysctls: kernel.shm_rmid_forced=1
    # Pod无法被调度到没有启用以下两个Unsafe sysctl的节点上，除非使用Taints
    security.alpha.kubernetes.io/unsafe-sysctls: net.ipv4.route.min_pmtu=1000,kernel.msgmax=1 2 3
spec:
  ...</pre>
<div class="blog_h2"><span class="graybg">K8S中的代理</span></div>
<p>使用Kubernetes时，你会注意到多个不同的代理。</p>
<div class="blog_h3"><span class="graybg">kubectl proxy</span></div>
<ol>
<li>在用户的桌面或者Pod中运行</li>
<li>将localhost地址代理为K8S APIServer地址</li>
<li>客户端到代理使用HTTP连接，代理到APIServer使用HTTPS连接</li>
<li>负责定位APIServer</li>
<li>添加身份验证请求头</li>
</ol>
<div class="blog_h3"><span class="graybg">APIServer proxy</span></div>
<ol>
<li>构建在APIServer内部</li>
<li>将集群外部的用户连接到集群IP，没有这种机制集群外部是无法直接访问集群IP的</li>
<li>在APIServer进程内部运行</li>
<li>客户端到此代理使用HTTPS协议，你也可以修改APIServer，使用HTTP协议</li>
<li>用于连接到任何节点、Pod、服务</li>
<li>当用于连接到服务时，负责负载均衡</li>
</ol>
<div class="blog_h3"><span class="graybg">kube proxy</span></div>
<ol>
<li>在每个节点上运行</li>
<li>代理UDP、TCP流量</li>
<li>不理解HTTP</li>
<li>提供负载均衡功能</li>
<li>仅仅用于连接到服务</li>
</ol>
<div class="blog_h3"><span class="graybg">前置代理/LB</span></div>
<p>位于APIServer前面的代理或负载均衡器：</p>
<ol>
<li>存在有否，实现方式依具体集群而不同</li>
<li>位于所有客户端和APIServers之间</li>
<li>如果集群存在多个APIServers，作为负载均衡器使用</li>
</ol>
<div class="blog_h3"><span class="graybg">云LB</span></div>
<ol>
<li>由云服务提供商提供</li>
<li>定义LoadBalancer类型的服务时，自动创建</li>
<li>仅仅使用UDP/TCP</li>
<li>具体实现依赖于云服务商</li>
</ol>
<div class="blog_h2"><span class="graybg">配置最佳实践</span></div>
<div class="blog_h3"><span class="graybg">配置的一般性建议</span></div>
<ol>
<li>定义配置时，指定最新版本的API，当前是v1</li>
<li>配置文件应当采用版本控制</li>
<li>使用YAML而不是JSON来编写配置文件</li>
<li>紧密耦合的一组对象，放在单个YAML中。应用程序的所有对象，可以放在单个目录中</li>
<li>不要指定默认值，让文件尽可能简短</li>
<li>将对象描述放到注解中</li>
</ol>
<div class="blog_h3"><span class="graybg">服务的配置</span></div>
<ol>
<li>应当先创建Service，然后创建复制控制器（或者Deployment等）</li>
<li>尽可能避免映射到宿主机端口</li>
<li>尽可能避免使用hostNetwork</li>
<li>如果不需要kube-proxy负载均衡，可以使用Headless Service进行简单的服务发现</li>
</ol>
<div class="blog_h3"><span class="graybg">合理使用标签</span></div>
<ol>
<li>使用标签来识别应用、部署的语义属性，例如这些标签：{ app: myapp, tier: frontend, phase: test, deployment: v3 }。而不要简单粗暴的定义{service: myservice}</li>
</ol>
<div class="blog_h2"><span class="graybg">命名空间资源配额</span></div>
<p>当多个团队共享一个固定节点数的集群时，需要进行资源的配额管理。</p>
<p>ResourceQuota对象用于定义资源的配额，<span style="background-color: #c0c0c0;">为某个名字空间限定总计的最大资源消耗</span>。包括：</p>
<ol>
<li>每个K8S名字空间最多能创建多少个指定类型的对象</li>
<li>上述对象总计消耗的计算资源的总量</li>
</ol>
<p>资源配额的工作机制为：</p>
<ol>
<li>不同的团队使用不同的名字空间，因而可以依照名字空间来管理配额。目前名字空间是自愿加入的，未来K8S会对其施加ACL</li>
<li>管理员为每个名字空间创建一个或多个资源配额对象</li>
<li>用户会在名字空间中创建对象（Service、Pod等），配额系统会跟踪名字空间的资源用量，确保不超过限制</li>
<li>当创建、更新资源，导致违反配额约束时，请求会失败，并且获得HTTP响应码403</li>
<li>如果配额中限定了CPU、内存等计算资源的用量，那么你必须在请求中指定需要的用量，否则配额系统会拒绝Pod的创建</li>
</ol>
<div class="blog_h3"><span class="graybg">启用配额</span></div>
<p>对于很多K8S发行版来说，资源配额支持是默认启用的。你可以设置--admission-control，添加ResourceQuota。</p>
<p>当名字空间中有ResourceQuota对象时，则该名字空间就启用了配额管理，每个名字空间最多有一个ResourceQuota对象。</p>
<div class="blog_h2"><span class="graybg">Pod资源配额</span></div>
<p>在创建Pod时，你可以指定某个容器需要使用的CPU、内存资源数量。调度器会根据Pod的需求，将其调度到适当的节点上。</p>
<div class="blog_h3"><span class="graybg">资源类型</span></div>
<p>CPU、内存是主要的计算资源（compute resources，有时简称resources），计算资源和K8S API资源不是一个概念。计算资源可以被请求、分配、消耗。</p>
<div class="blog_h3"><span class="graybg">资源请求和限制</span></div>
<p>每个容器的规格中，都可以包含以下数据项：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">规格配置项</td>
</tr>
</thead>
<tbody>
<tr>
<td>
<p>spec.containers[].resources.limits.cpu  容器最多可以使用的CPU资源</p>
<p>以CPU核心数为单位，可以指定小数，也可以使用m作为单位 1000m = 1</p>
</td>
</tr>
<tr>
<td>
<p>spec.containers[].resources.limits.memory  容器最多可以使用的内存资源</p>
<p>内存用量单位可以是E, P, T, G, M, k或者Ei, Pi, Ti, Gi, Mi, Ki，后者以2的次方计算</p>
</td>
</tr>
<tr>
<td>spec.containers[].resources.requests.cpu 容器至少可以获得的CPU资源</td>
</tr>
<tr>
<td>spec.containers[].resources.requests.memory 容器至少可以获得的内存资源</td>
</tr>
</tbody>
</table>
<p>Pod占有的资源，即每个容器占用的资源的总和。</p>
<div class="blog_h3"><span class="graybg">监控资源使用</span></div>
<p>Pod的资源使用状况，被报告为Pod Status的一部分。</p>
<div class="blog_h2"><span class="graybg">资源预留</span></div>
<p>Describe节点的时候，可以看到其Capacity、Allocatable两个字段，它们表示节点总计的、可被K8S分配给Pod的资源量：</p>
<p style="padding-left: 30px;"><strong>Allocatable</strong> = <strong>Node Capacity</strong> - <strong>kube-reserved</strong> - <strong>system-reserved</strong>   - <strong>eviction-threshold</strong></p>
<p>资源当前主要支持CPU、临时存储、内存者三类。至于磁盘IOPS、网络流量之类目前是无法配额的。</p>
<p>默认情况下，Pod可以使用Capacity指定的全部资源。这可能导致问题，因为K8S、操作系统本身需要运行一些守护程序，不做限制会导致Pod和守护程序竞争资源，导致系统卡死。</p>
<p>要使用资源预留，必须启用kubelet选项--cgroups-per-qos（默认启用）。该选项会将所有用户的Pod放在共同的、被kubelet管理的cgroups下面。</p>
<p>kubelet依赖于Cgroup驱动来管理cgroups，默认--cgroup-driver=cgroupfs，可以选取systemd。</p>
<div class="blog_h3"><span class="graybg">Kube Reserved</span></div>
<p>为kubelet、容器运行时服务、节点问题检测器等守护程序预留资源。作为DaemonSet运行的守护程序，不在内。取值多少，<a href="https://kubernetes.io/blog/2016/11/visualize-kubelet-performance-with-node-dashboard">通常是节点Pod密度的函数</a>。</p>
<p>要启用其资源预留，指定kubelet参数--kube-reserved-cgroup，其值为现有的、作为父控制组的cgroups名称。建议指定顶级cgroups，例如基于systemd的系统上可以指定runtime.slice。</p>
<p>要指定预留资源量，指定kubelet参数：</p>
<pre class="crayon-plain-tag">--kube-reserved=[cpu=100m][,][memory=100Mi][,][ephemeral-storage=1Gi]
--kube-reserved-cgroup=/kube.service</pre>
<p>Cgroup目录一定要预先创建，否则kubelet启动失败。CentOS下示例：</p>
<pre class="crayon-plain-tag">ExecStartPre=/usr/bin/mkdir -p /sys/fs/cgroup/cpuset/kube.slice
ExecStartPre=/usr/bin/mkdir -p /sys/fs/cgroup/memory/kube.slice
ExecStartPre=/usr/bin/mkdir -p /sys/fs/cgroup/hugetlb/kube.slice </pre>
<div class="blog_h3"><span class="graybg">System Reserved</span></div>
<p>为OS 守护程序、系统内核预留资源：</p>
<pre class="crayon-plain-tag">--system-reserved=[cpu=100mi][,][memory=100Mi][,][ephemeral-storage=1Gi]
--system-reserved-cgroup=/system.slice</pre>
<p>警告，上述限制可能导致关键OS服务遭遇OOM。</p>
<div class="blog_h3"><span class="graybg">Enforce Node Allocatable</span></div>
<p>默认值为pods，表示为Pods进行Cgroups控制。要为kube组件和系统预留资源，需要设置为pods,kube-reserved,system-reserve。</p>
<pre class="crayon-plain-tag">--enforce-node-allocatable=pods[,][system-reserved][,][kube-reserved] </pre>
<div class="blog_h2"><span class="graybg"><a id="out-of-resource"></a>资源不足</span></div>
<p>当资源不足时，Kubelet会采取一些措施，例如驱除Pod，以保证系统稳定运行。</p>
<div class="blog_h3"><span class="graybg">驱除信号</span></div>
<p>Kubelet可以在资源耗尽前，主动的停止一或多个Pod。触发驱除的信号来自节点状态，包括：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">信号</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>memory.available</td>
<td>等于node.status.capacity[memory] - node.stats.memory.workingSet</td>
</tr>
<tr>
<td>nodefs.available</td>
<td>
<p>等于node.stats.fs.available</p>
<p>nodefs是Kubelet用作卷、守护进程日志的文件系统</p>
</td>
</tr>
<tr>
<td>nodefs.inodesFree</td>
<td>等于node.stats.fs.inodesFree</td>
</tr>
<tr>
<td>imagefs.available</td>
<td>
<p>等于node.stats.runtime.imagefs.available</p>
<p>容器运行时用作存储镜像、可写层的文件系统</p>
</td>
</tr>
<tr>
<td>imagefs.inodesFree</td>
<td>等于node.stats.runtime.imagefs.inodesFree</td>
</tr>
</tbody>
</table>
<p>上述信号，均可以指定绝对值或者百分比。</p>
<div class="blog_h3"><span class="graybg">驱除阈值</span></div>
<p>驱除行为分为两类，分别为<span style="background-color: #c0c0c0;">软驱除、硬驱除</span>。后者没有grace period。</p>
<p>驱除阈值是kubelet命令行参数，指定Pod驱除发生的规则。包括：<pre class="crayon-plain-tag">eviction-soft</pre>、<pre class="crayon-plain-tag">eviction-hard</pre>。阈值的语法为：<pre class="crayon-plain-tag">[eviction-signal][operator][quantity]</pre>，例如memory.available&lt;10%、memory.available&lt;1Gi。一个实际的例子：</p>
<pre class="crayon-plain-tag">--eviction-hard=memory.available&lt;500Mi,nodefs.available&lt;10%</pre>
<p>和软驱除相关的额外的命令行参数包括：</p>
<ol>
<li>eviction-soft-grace-period，优雅期长度，取值例如1m30s，阈值到达后，此优雅期到达前，不会触发清除</li>
<li>eviction-max-pod-grace-period，执行delete pod操作的优雅期，单位秒</li>
</ol>
<p>Kubelet默认的硬清除阈值是：内存小于100Mi、nodefs可用小于10%、imagefs可用小于15%、nodefs可用inode小于5%</p>
<p>计算取值阈值是否到达的时间间隔，由housekeeping-interval控制。</p>
<div class="blog_h3"><span class="graybg">NodeCondition</span></div>
<p>Kubelet将驱除信号映射为节点状态。当超过硬、软清除阈值时，会更新节点的Condition：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">Condition</td>
<td style="text-align: center;">Eviction Signal</td>
</tr>
</thead>
<tbody>
<tr>
<td>MemoryPressure</td>
<td>memory.available</td>
</tr>
<tr>
<td>DiskPressure</td>
<td>nodefs.available, nodefs.inodesFree, imagefs.available,  imagefs.inodesFree</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">处理振荡</span></div>
<p>节点可能反复高于、低于软驱除阈值，且其切换周期低于软驱除优雅期。这导致NodeCondition反复在true/false之间变动，也导致Kubelet不能做出应当的驱除动作。</p>
<p>参数eviction-pressure-transition-period指定MemoryPressure、DiskPressure从true变为false至少需要等待的时间，避免振荡问题。</p>
<div class="blog_h3"><span class="graybg">驱除行为</span></div>
<p>到达驱除阈值、并过了优雅期后，Kubelet会执行回收操作，直到低于阈值：</p>
<ol>
<li>回收节点级资源，优先于回收Pod资源：
<ol>
<li>nodefs信号处理：删除死掉的Pod及其容器</li>
<li>imagefs信号处理：删除所有没有使用的镜像</li>
</ol>
</li>
<li>驱除Pod：
<ol>
<li>驱除优先级排名规则：
<ol>
<li>第一级排序：那些使用资源大于requests量的Pod排前面</li>
<li>第二级排序：那些低优先级的Pod排前面</li>
<li>第三集排序：根据Pod的调度请求声明排序</li>
</ol>
</li>
<li>驱除行为：
<ol>
<li>BestEffort或Burstable的Pod，超量使用资源的，根据优先级，依次驱除</li>
<li>Guaranteed且Burstable，没有使用超量资源的，最后驱除</li>
<li>Guaranteed的且每个容器的requests=limits!=0的Pod，不会因为其它Pod的资源消耗而被驱除</li>
</ol>
</li>
</ol>
</li>
</ol>
<p>DaemonSet创建的Pod永远不应该被驱除，因为控制器会立即重新创建。但是Kubelet目前没办法识别Pod是否由DS创建，为避免被误驱除，DaemonSet永远不应该创建BestEffort的Pod。</p>
<div class="blog_h3"><span class="graybg">最低资源回收量</span></div>
<p>某些情况下，Kubelet虽然驱除了多个Pod，但是回收的资源量并不多。为了避免这种无效的大量驱除，可以设置参数：</p>
<pre class="crayon-plain-tag">--eviction-minimum-reclaim="memory.available=0Mi,nodefs.available=500Mi,imagefs.available=2Gi"</pre>
<p>这样，仅当Pod驱除后能回收500+内存，它才会被清除。 </p>
<div class="blog_h3"><span class="graybg">节点OOM行为</span></div>
<p>如果节点在kubelet回收内存之前经历了OOM事件，它将基于oom-killer进行响应。</p>
<div class="blog_h2"><span class="graybg">kubeconfig</span> </div>
<p>你可以使用kubeconfig文件来组织集群、用户、名字空间、身份验证机制，等信息。 </p>
<p>kubectl也使用kubeconfig来获得需要连接到的集群的信息。默认的，kubectl会寻找~/.kube/config文件。你可以使用环境变量KUBECONFIG或者--kubeconfig选项覆盖此默认行为。</p>
<p>执行命令<pre class="crayon-plain-tag">kubectl config view</pre>可以查看当前配置信息。</p>
<div class="blog_h3"><span class="graybg">Context</span></div>
<p>context元素用于在一个别名下组织集群、名字空间、用户这三类访问参数。默认的kubectl使用“当前Context”，要切换当前Context，可以执行：</p>
<pre class="crayon-plain-tag">kubectl config use-context </pre>
<div class="blog_h2"><span class="graybg">kubectl</span></div>
<p>命令行接口默认使用kubeconfig位于~/.kube/config。</p>
<p>添加Bash自动完成支持：</p>
<pre class="crayon-plain-tag">echo "source &lt;(kubectl completion bash)" &gt;&gt; ~/.bashrc</pre>
<p>子命令如下表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><em>基本命令</em></td>
</tr>
<tr>
<td class="blog_h3">create</td>
<td>
<p>根据文件或者标准输入来创建一个资源，配置信息可以是YML或JSON格式</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">kubectl create -f FILENAME [options]</pre></p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--dry-run=false 如果true则仅仅显示命令的后果，不实际执行<br />--edit=false  在创建前编辑</p>
</td>
</tr>
<tr>
<td class="blog_h3">expose</td>
<td>
<p>将复制控制器、服务、部署、Pod暴露为K8S服务</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">kubectl expose (-f FILENAME | TYPE NAME)  [options]</pre></p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--cluster-ip='' 分配给服务的集群IP，为空则自动分配<br />--external-ip='' 服务的外部IP，不受K8S管理。此外部IP如果路由到某个集群节点，则可用来访问服务<br />-l, --labels='' 服务的标签<br />--load-balancer-ip='' 分配给负载均衡器的 IP<br />--name='' 服务的名称<br />--port=''  服务的监听端口<br />--protocol='' 服务使用的网络协议，默认TCP<br />--selector='' 此服务使用的标签选择器<br />--target-port=''  被暴露的容器的端口，服务端口转发到此端口<br />--type='ClusterIP' 服务类型，可选值ClusterIP, NodePort, LoadBalancer, ExternalName</p>
</td>
</tr>
<tr>
<td class="blog_h3">run</td>
<td>
<p>在集群上运行特定的镜像，实际上是创建Deployment或Job</p>
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">kubectl run NAME --image=image 
        [--env="key=value"] [--port=port] [--replicas=replicas] [--dry-run=bool]
        [--overrides=inline-json] [--command] -- [COMMAND] [args...] [options] </pre>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 运行单个实例的Nginx
kubectl run nginx --image=nginx

# 运行单个实例的hazelcast并暴露容器端口5701
kubectl run hazelcast --image=hazelcast --port=5701
# 运行单个实例的hazelcast并设置容器的环境变量
kubectl run hazelcast --image=hazelcast --env="DNS_DOMAIN=cluster"
# 运行单个实例的hazelcast并设置标签
kubectl run hazelcast --image=nginx --labels="app=hazelcast,env=prod"

# 5实例的复制
kubectl run nginx --image=nginx --replicas=5

# 运行一个Job
kubectl run pi --schedule="0/5 * * * ?" --image=perl --restart=OnFailure -- perl</pre>
</td>
</tr>
<tr>
<td>debug</td>
<td>
<p>使用交互式的调试容器来调试集群资源
<p>该命令的行为可以是：</p>
<ol>
<li>拷贝一个Pod，修改它的某些属性，然后运行<br />
<pre class="crayon-plain-tag">kubectl run myapp --image=busybox --restart=Never -- sleep 1d
# 调试Pod myapp，将其拷贝为myapp-debug，添加一个ubuntu容器
#                                      允许ubuntu容器看到Pod中其它容器的进程
kubectl debug myapp -it --image=ubuntu --share-processes --copy-to=myapp-debug

#                                             修改容器myapp      的命令
kubectl debug myapp -it --copy-to=myapp-debug --container=myapp -- sh

#                                         修改所有容器的镜像为ubuntu
kubectl debug myapp --copy-to=myapp-debug --set-image=*=ubuntu</pre>
</li>
<li>添加一个临时的容器到运行中的Pod，而不重新启动容器<br />
<pre class="crayon-plain-tag">kubectl run ephemeral-demo --image=k8s.gcr.io/pause:3.1 --restart=Never

# 报错，此容器没有sh
kubectl exec -it ephemeral-demo -- sh

# 我们可以为Pod添加一个临时容器，在此容器里执行shell
kubectl debug -it ephemeral-demo --image=busybox --target=ephemeral-demo </pre>
</li>
<li>在节点上创建一个容器，运行在宿主机的命名空间，可以访问宿主机资源<br />
<pre class="crayon-plain-tag">kubectl debug node/mynode -it --image=ubuntu</pre>
</li>
</ol>
</td>
</tr>
<tr>
<td class="blog_h3">set</td>
<td>在对象上设置特性</td>
</tr>
<tr>
<td class="blog_h3">get</td>
<td>
<p>列表形式显示一个或多个资源</p>
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">kubectl get -o 
    json|yaml|wide|custom-columns=...|custom-columns-file=... |
    go-template=...|go-template-file=...|jsonpath=...|jsonpath-file=...]     
                                 
(TYPE [NAME | -l label] | TYPE/NAME ...) [flags] [options]</pre>
<p><span style="background-color: #c0c0c0;">资源类型：</span></p>
<p>支持的资源类型包括：</p>
<p style="padding-left: 30px;">all<br />certificatesigningrequests (简写 'csr')<br />clusterrolebindings<br />clusterroles<br />componentstatuses (简写 'cs')<br />configmaps (简写 'cm')<br />controllerrevisions<br />cronjobs<br />customresourcedefinition (简写 'crd')<br />daemonsets (简写 'ds')<br />deployments (简写 'deploy')<br />endpoints (简写 'ep')<br />events (简写 'ev')<br />horizontalpodautoscalers (简写 'hpa')<br />ingresses (简写 'ing')<br />jobs<br />limitranges (简写 'limits')<br />namespaces (简写 'ns')<br />networkpolicies (简写 'netpol')<br />nodes (简写 'no')<br />persistentvolumeclaims (简写 'pvc')<br />persistentvolumes (简写 'pv')<br />poddisruptionbudgets (简写 'pdb')<br />podpreset<br />pods (简写 'po')<br />podsecuritypolicies (简写 'psp')<br />podtemplates<br />replicasets (简写 'rs')<br />replicationcontrollers (简写 'rc')<br />resourcequotas (简写 'quota')<br />rolebindings<br />roles<br />secrets<br />serviceaccounts (简写 'sa')<br />services (简写 'svc')<br />statefulsets (简写 'sts')<br />storageclasses (简写 'sc')</p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--all-namespaces 获取所有名字空间的资源<br />--export=false  如果设置为true，将集群相关的信息移除，这样可以在其它地方部署它<br />--field-selector='k1=v1,k2!=v2,k3==v3' 根据字段查询<br />-f 指定资源的配置文件，作为查询依据<br />-L  资源必须具有的标签，逗号分隔<br />-l  指定标签选择器，支持操作符== !=和=<br />-o 输出格式，支持json|yaml|wide|name|custom-columns=...<br />-w 获取请求的资源后，监控其变化</p>
<p><span style="background-color: #c0c0c0;">举例：</span></p>
<pre class="crayon-plain-tag"># 列出默认名字空间的所有Pod。字段 NAME READY STATUS RESTARTS AGE
kubectl get pods
# 列出dev中的所有Pod。字段 NAME READY STATUS RESTARTS AGE IP NODE
kubectl get pods -o wide --namespace=dev
# 列出名为web的replicationcontroller
kubectl get replicationcontroller web
# 获取两种不同类型的资源
kubectl get rc,services
# 获取所有资源
kubectl get all

# 仅仅获取Phase字段
kubectl get --namespace=dev -o template pod/media-api-56b5db7c65-8nvpf --template={{.status.phase}}

# 仅仅获取IP地址
kubectl get --namespace=dev  pod/media-api-56b5db7c65-8nvpf -o jsonpath='{.status.podIP}'

# 遍历所有匹配的资源，获取它们的IP，后缀以8800，然后连接为单个字符串
kubectl get pod --namespace=dev -o jsonpath='{range.items[*]}{.status.podIP}:8800 '
# 输出 172.27.41.137:8800 172.27.97.74:8800 172.27.61.80:8800 172.27.121.73:8800 172.27.187.201:8800

# 多重循环获取，逐行打印
kubectl get workloadclusters -o jsonpath='{range .items[*]}{ range .status.cluster.masters[*]}{.ip}{"\n"}'

# 过滤：获取InternalIP
kubectl get node carbon -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}'

# 查询处于Running阶段的Pod
kubectl get pod --field-selector='status.phase=Running'

# 查询事件
# 查询指定Pod相关的事件
kubectl get event -n default --field-selector involvedObject.name=pod-name</pre>
</td>
</tr>
<tr>
<td class="blog_h3">explain</td>
<td>显示资源的文档</td>
</tr>
<tr>
<td class="blog_h3">edit</td>
<td>编辑一个资源</td>
</tr>
<tr>
<td class="blog_h3">delete</td>
<td>
<p>根据文件名、Stdin、名称、标签来删除资源
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">kubectl delete ([-f FILENAME] | TYPE [(NAME | -l label | --all)]) [options]</pre></p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 根据JSON中的type和name来删除资源
kubectl delete -f ./pod.json
# 删除名字为baz foo的pod、service
kubectl delete pod,service baz foo
# 删除具有标签name=myLabel的pod、service
kubectl delete pods,services -l name=myLabel
# 立即删除pod，最小化延迟
kubectl delete pod foo --now
# 强制删除pod，即使其所在节点宕机
kubectl delete pod foo --grace-period=0 --force
# 删除所有pod
kubectl delete pods --all</pre>
</td>
</tr>
<tr>
<td>cp</td>
<td>
<p>在当前目录和Pod容器目录之间进行文件拷贝
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">kubectl cp [options]</pre></p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 拷贝当前目录中的图片，到Pod nginx的/usr/share/nginx/html/grafana目录，重命名
kubectl cp dang.png kube-system/nginx:/usr/share/nginx/html/grafana/avatar.png

# 拷贝到指定容器
kubectl cp /tmp/foo :/tmp/bar -c</pre>
</td>
</tr>
<tr>
<td colspan="2"><em>部署命令</em></td>
</tr>
<tr>
<td class="blog_h3">rollout</td>
<td>
<p>管理资源的滚动更新，可以用于滚动更新复制集（Replica Set）
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">kubectl rollout SUBCOMMAND [options]</pre></p>
<p><span style="background-color: #c0c0c0;">子命令：</span><br />history  查看滚动更新历史<br /> pause 将资源标记为暂停<br /> resume 恢复被暂停的资源<br /> status 显示滚动更新状态<br /> undo 撤销之前的一个滚动更新</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag">kubectl rollout status deployment/nginx-deployment</pre>
</td>
</tr>
<tr>
<td class="blog_h3">rolling-update</td>
<td>
<p>针对指定的复制控制器（Replication Controller）进行滚动更新。将指定的的复制控制器替换为新的复制控制器，每次更新其中一个Pod（使用新Pod模板）
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">kubectl rolling-update OLD_CONTROLLER_NAME ([NEW_CONTROLLER_NAME] 
        --image=NEW_CONTAINER_IMAGE | -f NEW_CONTROLLER_SPEC) [options]</pre>
</td>
</tr>
<tr>
<td class="blog_h3">scale</td>
<td>
<p>对 Deployment, ReplicaSet, Replication Controller, Job进行扩容
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 将复制集foo扩容到3实例
kubectl scale --replicas=3 rs/foo
# 将foo.yaml所声明的资源扩容到3实例
kubectl scale --replicas=3 -f foo.yaml
# 如果部署mysql当前是2实例，则扩容到3实例
kubectl scale --current-replicas=2 --replicas=3 deployment/mysql
# 扩容多个复制控制器
kubectl scale --replicas=5 rc/foo rc/bar rc/baz
# 扩容JOB
kubectl scale --replicas=3 job/cron</pre>
</td>
</tr>
<tr>
<td class="blog_h3">autoscale</td>
<td>
<p>对Deployment, ReplicaSet, ReplicationController进行自动扩容
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">kubectl autoscale (-f FILENAME | TYPE NAME | TYPE/NAME) 
        [--min=MINPODS] --max=MAXPODS [--cpu-percent=CPU] [flags]</pre>
<p><span style="background-color: #c0c0c0;"> 示例：</span></p>
<pre class="crayon-plain-tag"># 自动扩容部署foo，实例数量在2-10之间
kubectl autoscale deployment foo --min=2 --max=10
# 自动扩容复制控制器foo，最大实例数量5，当CPU占用80%时执行扩容
kubectl autoscale rc foo --max=5 --cpu-percent=80</pre>
</td>
</tr>
<tr>
<td colspan="2"><em>集群管理命令</em></td>
</tr>
<tr>
<td class="blog_h3">certificate</td>
<td>修改证书资源</td>
</tr>
<tr>
<td class="blog_h3">cluster-info</td>
<td>显示集群信息</td>
</tr>
<tr>
<td class="blog_h3">top</td>
<td>显示资源（CPU、内存、存储）用量</td>
</tr>
<tr>
<td class="blog_h3">cordon</td>
<td>将节点标记为不可调度</td>
</tr>
<tr>
<td class="blog_h3">uncordon</td>
<td>将节点标记为可调度</td>
</tr>
<tr>
<td class="blog_h3">drain</td>
<td>
<p>抽干节点上部署的资源，准备进行节点维护。节点不再接受Pod调度
<p>要恢复，执行：</p>
<pre class="crayon-plain-tag">kubectl patch node node-name -p '{"spec":{"unschedulable":false}}'
# 或者
kubectl uncordon ...</pre>
</td>
</tr>
<tr>
<td class="blog_h3">taint</td>
<td>
<p>为一个或多个节点标记Taint，每个Taint可以包含三个部分：
<ol>
<li>键：253字符以内，可以包含域名前缀和/，例如gmem.cc/app</li>
<li>值：可选，63字符以内</li>
<li>效果：必须是NoSchedule、PreferNoSchedule、NoExecute之一</li>
</ol>
<p>目前仅仅支持在Node上添加Taint</p>
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">kubectl taint NODE 
    NAME KEY_1=VAL_1:TAINT_EFFECT_1 ... KEY_N=VAL_N:TAINT_EFFECT_N [options]</pre>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 为节点foo添加一个键为dedicated，值为special-user，效果为NoSchedule的taint
# 如果指定键、效果的taint已经存在，则替换其值
kubectl taint nodes foo dedicated=special-user:NoSchedule

# 移除节点foo上键为dedicated、效果为NoSchedule的taint
kubectl taint nodes foo dedicated:NoSchedule- </pre>
</td>
</tr>
<tr>
<td colspan="2"><em>调试诊断命令</em></td>
</tr>
<tr>
<td class="blog_h3">describe</td>
<td>描述一个资源的详细信息</td>
</tr>
<tr>
<td class="blog_h3">logs</td>
<td>查看容器日志</td>
</tr>
<tr>
<td class="blog_h3">attach</td>
<td>连接到容器的控制台</td>
</tr>
<tr>
<td class="blog_h3">exec</td>
<td>
<p>在容器上执行命令，示例：
<pre class="crayon-plain-tag"># 连接到Pod redis的容器sidecar，执行Bash
kubectl exec -n dev redis -c sidecar  -it bash 
# 要传递参数给容器中的命令，需要用 -- 做分隔符
kubectl exec -n dev redis -c redis it -- rediscli -c</pre>
</td>
</tr>
<tr>
<td class="blog_h3">port-forward</td>
<td>转发一个或多个本地的端口到Pod</td>
</tr>
<tr>
<td class="blog_h3">proxy</td>
<td>运行API Server的代理</td>
</tr>
<tr>
<td class="blog_h3">auth</td>
<td>查看授权信息</td>
</tr>
<tr>
<td colspan="2"><em>网络命令</em></td>
</tr>
<tr>
<td class="blog_h3">port-forward</td>
<td>
<p>端口转发，将指定的本地端口转发给K8S对象（Service、Pod等）
<p>格式：</p>
<pre class="crayon-plain-tag">kubectl port-forward TYPE/NAME [LOCAL_PORT:]REMOTE_PORT [...[LOCAL_PORT_N:]REMOTE_PORT_N] [options]</pre>
<p>示例： </p>
<pre class="crayon-plain-tag"># 转发本地端口5000、6000到容器mypod的相应端口
kubectl port-forward pod/mypod 5000 6000
# 类似，但是转发给Deployment，自动选择一个Pod
kubectl port-forward deployment/mydeployment 5000 6000
# 将本地端口8888转发给mypod的5000
kubectl port-forward pod/mypod 8888:5000
# 随机监听一个本地端口并转父爱给mypod的5000
kubectl port-forward pod/mypod :5000</pre>
</td>
</tr>
<tr>
<td colspan="2"><em>高级命令</em></td>
</tr>
<tr>
<td class="blog_h3">apply</td>
<td>
<p>根据指定的文件或者Stdin来给资源应用一个配置
<p>如果操控的资源尚不存在，则此命令会自动创建之</p>
</td>
</tr>
<tr>
<td class="blog_h3">patch</td>
<td>
<p>更新资源的某些字段</p>
<p>可以用一段YAML来打补丁：</p>
<pre class="crayon-plain-tag">kubectl -n kube-system patch service core-dns --patch "$(cat &lt;&lt; EOF
spec:
  topologyKeys:
  - kubernetes.io/hostname
  - '*'
EOF
)"</pre>
<p> 也可以用一段JSON来打补丁：</p>
<pre class="crayon-plain-tag">kubectl patch node k8s-node-1 -p '{"spec":{"unschedulable":true}}' </pre>
</td>
</tr>
<tr>
<td class="blog_h3">replace</td>
<td>根据指定的文件或者Stdin来替换掉资源</td>
</tr>
<tr>
<td class="blog_h3">convert</td>
<td>在不同API版本之间转换配置文件</td>
</tr>
<tr>
<td colspan="2"><em>设置命令</em></td>
</tr>
<tr>
<td class="blog_h3">label</td>
<td>
<p>为资源更新标签
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 为某个节点添加标签
kubectl label node master-nQxw ceph-osd=enabled
# 为某个节点添加标签，如果该标签已经存在，则覆盖其值
kubectl label --overwrite  node master-nQxw ceph-osd=enabled
# 移除所有节点的ceph-osd标签
kubectl label node --all ceph-osd-

# 查询所有节点的所有标签
kubectl get node -o go-template='{{range .items}}Labels of {{.metadata.name}}{{":\n"}}{{range $k, $v := .metadata.labels}}{{$k}}={{$v}}{{"\n"}}{{end}}{{"\n\n"}}{{end}}'</pre>
</td>
</tr>
<tr>
<td class="blog_h3">annotate</td>
<td>为资源更新注解</td>
</tr>
<tr>
<td colspan="2"><em>客户端设置</em></td>
</tr>
<tr>
<td>config</td>
<td>
<p>此命令用于修改kubeconfig，即默认位于~/.kube/confg中的配置信息
<p><span style="background-color: #c0c0c0;">格式：</span> <pre class="crayon-plain-tag">kubectl config SUBCOMMAND [options]</pre></p>
<p><span style="background-color: #c0c0c0;">子命令：</span></p>
<p>current-context 显示当前使用的<span style="background-color: #c0c0c0;">上下文（集群+命名空间+用户的组合）</span><br /> delete-cluster 从配置文件中删除集群信息<br /> delete-context 从配置文件中删除指定的上下文<br /> get-clusters 显示可用的集群<br /> get-contexts 描述一个或多个上下文<br /> rename-context 重命名一个上下文<br /> set 设置配置文件中的单个项<br /> set-cluster 设置集群<br /> set-context 设置上下文<br /> set-credentials 设置用户信息<br /> use-context 修改当前上下文</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 创建一个新的集群设置
kubectl config --kubeconfig=.kube/config set-cluster dang --server=https:/10.5.39.222:6443 \
  --certificate-authority=fake-ca-file

# 创建一个新的用户设置
kubectl config --kubeconfig=.kube/config set-credentials developer \
  --client-certificate=fake-cert-file --client-key=fake-key-seefile

# 创建一个新的Context
kubectl config --kubeconfig=.kube/config set-context dev-frontend \
  --cluster=development --namespace=frontend --user=developer

# 显示可用Context列表
# kubectl config get-contexts 
CURRENT   NAME      CLUSTER   AUTHINFO      NAMESPACE
          dang      dang      dang-admin    default
*         local     local     local-admin   default

# 切换到新的Context
kubectl config use-context dang</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">日志级别</span></div>
<p><pre class="crayon-plain-tag">--v=0   用于那些应该 始终 对运维人员可见的信息，因为这些信息一般很有用
--v=1   如果您不想要看到冗余信息，此值是一个合理的默认日志级别
--v=2   输出有关服务的稳定状态的信息以及重要的日志消息，这些信息可能与系统中的重大变化有关。这是建议大多数系统设置的默认日志级别
--v=3   包含有关系统状态变化的扩展信息
--v=4   包含调试级别的冗余信息
--v=6   显示所请求的资源
--v=7   显示 HTTP 请求头
--v=8   显示 HTTP 请求内容
--v=9   显示 HTTP 请求内容而且不截断内容 </pre>
<div class="blog_h1"><span class="graybg">集群搭建（Kubeadm）</span></div>
<p>Kubeadm是K8S提供的一个工具箱，用于快速安装、初始化多机器环境下的K8S集群。</p>
<div class="blog_h2"><span class="graybg">前提条件</span></div>
<ol>
<li>Ubuntu 16.04、Debian 9、Fedora 25等操作系统</li>
<li>2GB或者更多内存</li>
<li>2核心或者更多</li>
<li>所有节点之间的完整网络连接性</li>
<li>每个节点具有唯一的主机名、MAC地址、product_uuid。product_uuid可以利用命令<pre class="crayon-plain-tag">sudo cat /sys/class/dmi/id/product_uuid</pre>获得</li>
<li><a href="https://kubernetes.io/docs/setup/independent/install-kubeadm/#check-required-ports">必要的端口</a>已经打开。可以简单的禁用防火墙：<pre class="crayon-plain-tag">sudo ufw disable</pre></li>
<li>禁用交换分区/文件：<pre class="crayon-plain-tag">sudo swapoff -a</pre> ，这样kubelet才能正常工作</li>
</ol>
<div class="blog_h2"><span class="graybg">Docker安装</span></div>
<p>每个节点都需要安装Docker，推荐1.12，但是17.03、1.13、1.11等版本也可以使用。17.06+可能是可以的，但是没有经过K8S团队验证。安装方法：</p>
<pre class="crayon-plain-tag"># Ubuntu / Debian
apt-get update
apt-get install -y docker.io

# CentOS / RHEL / Fedora
yum install -y docker
systemctl enable docker &amp;&amp; systemctl start docker</pre>
<div class="blog_h2"><span class="graybg">Cgroup驱动</span></div>
<p>你需要确保Kubelet使用的Cgroup驱动和Docker的一致。要查看Docker当前的Cgroup驱动，执行：</p>
<pre class="crayon-plain-tag">docker info | grep Driver
# Cgroup Driver: cgroupfs</pre>
<p>可选值是cgroupfs、systemd，如果要修改Docker的驱动，可以：</p>
<pre class="crayon-plain-tag">{
    ...
    "exec-opts": ["native.cgroupdriver=systemd"]
}</pre>
<p>或者，修改Kubelet的启动选项 <pre class="crayon-plain-tag">--cgroup-driver=cgroupfs</pre>。 </p>
<div class="blog_h2"><span class="graybg"><a id="install-k8s-comps"></a>K8S组件安装</span></div>
<p>Kubeadm用于集群的自举，在所有节点安装。kubeadm不会帮你安装kubelet、kubectl，因此你需要自己确保后两者的版本和K8S控制平面（由kubeadm安装）兼容。</p>
<pre class="crayon-plain-tag">apt-get update &amp;&amp; apt-get install -y apt-transport-https

curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -

cat</pre>
<div class="blog_h2"><span class="graybg">创建集群</span></div>
<div class="blog_h3"><span class="graybg">初始化Master</span></div>
<p>选取一台机器作为Master，控制平面组件将在其上运行，包括etcd（集群数据库）和APIServer。执行命令：</p>
<pre class="crayon-plain-tag">kubeadm init --apiserver-advertise-address 10.28.191.166

# 会在下面这一步卡住很长时间，如果在国内，要正确配置docker pull的代理，否则位于私服gcr.io下的APIServer等镜像是无法下载的
# Kubelet的日志信息，在Ubuntu上可以到/var/log/syslog查看，但是因为APIServer镜像拉取非常慢，所以报6443无法连接的日志非常多
# This might take a minute or longer if the control plane images have to be pulled.</pre>
<p>执行完毕后，会提示：Your Kubernetes master has initialized successfully，并下载以下镜像：</p>
<pre class="crayon-plain-tag">docker images
# REPOSITORY                                               TAG                 IMAGE ID            CREATED             SIZE
# gcr.io/google_containers/kube-proxy-amd64                v1.9.0              f6f363e6e98e        12 days ago         109 MB
# gcr.io/google_containers/kube-apiserver-amd64            v1.9.0              7bff5aa286d7        12 days ago         210 MB
# gcr.io/google_containers/kube-controller-manager-amd64   v1.9.0              3bb172f9452c        12 days ago         138 MB
# gcr.io/google_containers/kube-scheduler-amd64            v1.9.0              5ceb21996307        12 days ago         62.7 MB
# gcr.io/google_containers/etcd-amd64                      3.1.10              1406502a6459        3 months ago        193 MB
# gcr.io/google_containers/pause-amd64                     3.0                 99e59f495ffa        20 months ago       747 kB</pre>
<p>如果网络实在太慢，或者无法访问gcr.io，则kubeadm init最终会time out。可以到阿里云的海外站建立一个按时收费的VM，在其上执行kubeadm init并把Docker 镜像拷贝、下载到本地使用。 默认情况下镜像文件存放在/var/lib/docker/aufs目录中，/var/lib/docker/image下则是镜像的元数据。</p>
<p>记住控制台上输出的信息：</p>
<pre class="crayon-plain-tag">kubeadm join --token 24ae55.951196a500d44f96 10.28.191.166:6443 --discovery-token-ca-cert-hash sha256:1b99aad2ed25b8f05e14a4a2c20c9aa3e116dfb027f3544a7ac6a926669b3ed7</pre>
<p>添加节点的时候需要使用。 </p>
<div class="blog_h3"><span class="graybg">检查状态</span></div>
<p>首先要确保当前用户的kubectl的配置文件正确。</p>
<p>如果以root身份运行：</p>
<pre class="crayon-plain-tag">export KUBECONFIG=/etc/kubernetes/admin.conf</pre>
<p>其它用户可以使用如下方式指定kubectl配置文件：</p>
<pre class="crayon-plain-tag">mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config</pre>
<p>然后，你可以执行命令查看组件状态：</p>
<pre class="crayon-plain-tag">kubectl get cs
# NAME                 STATUS    MESSAGE              ERROR
# controller-manager   Healthy   ok                   
# scheduler            Healthy   ok                   
# etcd-0               Healthy   {"health": "true"}</pre>
<div class="blog_h3"><span class="graybg">安装Pod网络支持 </span></div>
<p>必须安装一个Pod Network Addon，这样Pod才能相互通信。安装Addon的通用命令格式为：</p>
<pre class="crayon-plain-tag">kubectl apply -f add-on.yaml</pre>
<p>Pod Network Addon必须在任何应用程序之前部署， 完成之前，kube-dns（集群DNS服务）不会启动。</p>
<p>Kubeadm仅仅支持基于容器网络接口（Container Network Interface，CNI）的网络，不支持kubenet（单机环境下的网络插件）。因此kubelet必须使用选项--network-plugin=cni。</p>
<p>CNI由一系列规范、用于编写插件的库组成，这些插件用于配置Linux容器的网络接口，目前已经有一系列CNI插件。CNI仅仅关注容器的连接性、容器删除时的资源回收。</p>
<p>有多个开源项目为K8S提供CNI网络支持，其中一些支持<a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">网络策略</a>：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">Addon</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td><a href="http://docs.projectcalico.org/latest/getting-started/kubernetes/installation/hosted/">Calico </a></td>
<td>
<p>一个安全的第三层网络连接，提供网络策略支持。仅仅支持amd64</p>
<p>要求kubeadm init参数：<pre class="crayon-plain-tag">--pod-network-cidr=192.168.0.0/16</pre></p>
<p>安装此Addon：</p>
<pre class="crayon-plain-tag">https://docs.projectcalico.org/v3.0/getting-started/kubernetes/installation/hosted/kubeadm/1.7/calico.yaml</pre>
</td>
</tr>
<tr>
<td><a href="https://github.com/tigera/canal/tree/master/k8s-install">Canal</a></td>
<td>
<p>联合使用Flannel+Calico，提供网络连接和策略。仅仅支持amd64
<p>要求kubeadm init参数：<pre class="crayon-plain-tag">--pod-network-cidr=10.244.0.0/16</pre></p>
<p>安装此Addon：</p>
<pre class="crayon-plain-tag">kubectl apply -f https://raw.githubusercontent.com/projectcalico/canal/master/k8s-install/1.7/rbac.yaml
kubectl apply -f https://raw.githubusercontent.com/projectcalico/canal/master/k8s-install/1.7/canal.yaml</pre>
</td>
</tr>
<tr>
<td><a href="https://github.com/coreos/flannel/blob/master/Documentation/kube-flannel.yml">Flannel</a></td>
<td>
<p>一个覆盖网络实现，支持和K8S一起使用。支持amd64, arm, arm64, ppc64le，但是除了amd64之外需要手工的处理
<p>要求kubeadm init参数：<pre class="crayon-plain-tag">--pod-network-cidr=10.244.0.0/16</pre></p>
<p>你需要设置：<pre class="crayon-plain-tag">sysctl net.bridge.bridge-nf-call-iptables=1</pre>，将桥接的IPv4流量传递给iptables chain，确保某些CNI正常工作</p>
<p>安装此Addon：</p>
<pre class="crayon-plain-tag">kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/v0.9.1/Documentation/kube-flannel.yml</pre>
</td>
</tr>
<tr>
<td><a href="https://github.com/cloudnativelabs/kube-router/blob/master/Documentation/kubeadm.md">Kube-router</a></td>
<td>
<p>提供网络连接、策略、高性能的基于IPVS（IP虚拟服务器）/LVS（Linux虚拟服务器）的服务代理
<p>你需要设置：<pre class="crayon-plain-tag">sysctl net.bridge.bridge-nf-call-iptables=1</pre>，将桥接的IPv4流量传递给iptables chain，确保某些CNI正常工作</p>
<p>Kube-router依赖于控制器管理器来为节点分配Pod CIDR，你需要指定--pod-network-cidr标记</p>
</td>
</tr>
<tr>
<td><a href="https://www.weave.works/docs/net/latest/kube-addon/">Weave Net</a></td>
<td>
<p>支持amd64/arm/arm64且不需要额外的处理。默认设置hairpin mode，允许Pod通过它们的Service IP访问自己</p>
<p>你需要设置：<pre class="crayon-plain-tag">sysctl net.bridge.bridge-nf-call-iptables=1</pre>，将桥接的IPv4流量传递给iptables chain，确保某些CNI正常工作</p>
<p>安装此Addon：</p>
<pre class="crayon-plain-tag">export kubever=$(kubectl version | base64 | tr -d '\n')
kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$kubever"</pre>
</td>
</tr>
</tbody>
</table>
<p>安装网络支持后，K8S需要到gcr.io下载三个镜像，如果网络不好的话又需要很长时间。你可以检查kube-dns是否进入Running状态：
<pre class="crayon-plain-tag">kubectl get pods --all-namespaces | grep kube-dns</pre>
<p>如果是，则可以将节点加入到集群了。 否则，查看一下kube-dns这个Pod的详细状态：</p>
<pre class="crayon-plain-tag"># kube-system   kube-dns-6f4fd4bdf-h84mc                 0/3       Pending   0          1m

kubectl --namespace=kube-system describe pod  kube-dns-6f4fd4bdf-h84mc

# Controlled By:  ReplicaSet/kube-dns-6f4fd4bdf
# Containers:
#   kubedns:
#     Requests:
#       cpu:      100m
#       memory:   70Mi
#   dnsmasq:
#     Requests:
#       cpu:        150m
#       memory:     20Mi
# Events:
#  Type     Reason            Age               From               Message
#  ----     ------            ----              ----               -------
#  Warning  FailedScheduling  3m (x7 over 3m)   default-scheduler  0/1 nodes are available: 1 NodeNotReady.
#  Warning  FailedScheduling  2m (x2 over 2m)   default-scheduler  0/1 nodes are available: 1 Insufficient cpu, 1 NodeNotReady.
#  Warning  FailedScheduling  53s (x3 over 1m)  default-scheduler  0/1 nodes are available: 1 Insufficient cpu.</pre>
<p>可以发现CPU资源不足会导致kube-dns无法运行。 </p>
<div class="blog_h3"><span class="graybg">允许在Master上调度Pod </span></div>
<p>默认情况下，集群不会在Master节点上调度Pod，主要是安全方面的考虑。如果你希望改变这一行为（例如仅仅有单个节点，开发用），可以去除taint：</p>
<pre class="crayon-plain-tag"># 移除所有节点上的Taint: node-role.kubernetes.io/master，这样调度器可以在任何地方调度Pod
kubectl taint nodes --all node-role.kubernetes.io/master-</pre>
<div class="blog_h3"><span class="graybg">添加节点</span></div>
<p>执行kubeadm的最后，有一段用于加入节点的命令，执行执行它就可以加入到集群中。然后在Master节点上验证此新节点的加入：</p>
<pre class="crayon-plain-tag">kubectl get nodes
# NAME                      STATUS    ROLES     AGE       VERSION
# izj6c2ryo8ck3lt8mzx0ezz   Ready     master    25m       v1.9.0
# izj6cdwcrbnfjxwlznyjvnz   Ready         1m        v1.9.0</pre>
<p>信息信息 在普通节点上，你都可以对集群进行控制，只需要将kubectl的配置文件拷贝过来就可以了：</p>
<pre class="crayon-plain-tag">scp root@10.28.191.166:/etc/kubernetes/admin.conf .
kubectl --kubeconfig ./admin.conf get nodes</pre>
<p> 你也可以从集群外部的节点来控制集群：</p>
<pre class="crayon-plain-tag">scp root@10.28.191.166:/etc/kubernetes/admin.conf .
kubectl --kubeconfig ./admin.conf proxy
# 现在可以通过http://localhost:8001/api/v1访问APIServer了</pre>
<div class="blog_h2"><span class="graybg">删除集群</span></div>
<div class="blog_h3"><span class="graybg">删除节点</span></div>
<pre class="crayon-plain-tag"># 抽干节点，忽略DaemonSet控制器管理Pods
kubectl drain izj6cdwcrbnfjxwlznyjvnz --delete-local-data --force --ignore-daemonsets
# 从集群中删除节点
kubectl delete node izj6cdwcrbnfjxwlznyjvnz</pre>
<div class="blog_h3"><span class="graybg">重置集群 </span></div>
<pre class="crayon-plain-tag"># 已经下载的镜像不会被删除，但是创建的容器会全部删除
kubeadm reset </pre>
<div class="blog_h2"><span class="graybg">kubeadm init</span></div>
<div class="blog_h3"><span class="graybg">工作流程</span></div>
<p>初始化一个K8S集群的Master节点。该子命令的工作流程如下：</p>
<ol>
<li>执行一系列的pre-flight检查，验证系统状态。某些验证会导致警告，另一些则触发错误并终止流程</li>
<li>生成自签名CA（或者使用你提供的）来为每个集群组件创建identity。你可以把自己生成好的CA证书/密钥放入--cert-dir目录（默认/etc/kubernetes/pki），则此步骤可以跳过</li>
<li>为Kubelet、控制器管理器、调度器创建配置文件，放入/etc/kubernetes/目录。这些组件利用配置文件（自己的identity）连接到APIServer</li>
<li>创建一个名为admin.conf的kubeconfig</li>
<li>如果使用选项--feature-gates=DynamicKubeletConfig。则将Kubelet<a href="https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file.md">初始化配置</a>写入/var/lib/kubelet/config/init/kubelet。此特性未来可能默认开启</li>
<li>为APIServer、控制器管理器、调度器生成静态Pod清单（manifests）。如果没有提供外部的etcd，则额外创建etcd的静态Pod清单<br />清单文件位于/etc/kubernetes/manifests目录下，Kubelet会监控此目录，并在启动时创建需要的Pod</li>
<li>上一步会初始化、启动控制平面，一旦完成，流程继续</li>
<li>如果使用选项--feature-gates=DynamicKubeletConfig，则继续完成Kubelet的动态配置——创建ConfigMap、一些RBAC规则，更新节点将Node.spec.configSource指向刚刚创建的ConfigMap</li>
<li>为Master节点添加Label、Taints，不让工作负载在Master节点上运行</li>
<li>生成令牌，方便Worker节点注册，你可以通过--token手工提供令牌</li>
<li>进行必要的配置，允许节点通过<a href="https://kubernetes.io/docs/admin/bootstrap-tokens/">Bootstrap Tokens</a> 和 <a href="https://kubernetes.io/docs/admin/kubelet-tls-bootstrapping/">TLS Bootstrap</a>加入到集群：
<ol>
<li>创建一个ConfigMap，添加加入集群所需的信息，创建相关的RBAC访问规则</li>
<li>让Bootstrap令牌访问CSR签名API</li>
<li>配置CSR请求的自动准许</li>
</ol>
</li>
<li>为APIServer安装内部DNS、kube-proxy Addon组件。尽管DNS Pod被部署，但是CNI准备好之前它不会被调度</li>
<li>如果使用选项--feature-gates=SelfHosting=true。则基于静态Pod的控制平面被转换为<a href="https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-init/#self-hosting">自托管（self-hosted）控制平面</a></li>
</ol>
<div class="blog_h3"><span class="graybg">命令选项</span></div>
<p>命令行参数可以用配置文件代替，传入<pre class="crayon-plain-tag">--config</pre>选项即可。配置文件格式：</p>
<pre class="crayon-plain-tag"># --dry-run 仅仅输出会做什么
# --config 传入此配置文件
# --ignore-preflight-errors stringSlice  将错误看作警告，值IsPrivilegedUser,Swap,all
# --skip-token-print 不在控制台上打印kubeadm join的令牌

apiVersion: kubeadm.k8s.io/v1alpha1
kind: MasterConfiguration
api:
  # --apiserver-advertise-address string 
  # APIServer在其上监听的网络接口，设置0.0.0.0则使用默认网络接口
  advertiseAddress: &lt;address|string&gt;
  # --apiserver-bind-port int32
  # APIServer监听端口，默认6443
  bindPort: 
etcd:
  endpoints:
  - &lt;endpoint1|string&gt;
  - &lt;endpoint2|string&gt;
  caFile: &lt;path|string&gt;
  certFile: &lt;path|string&gt;
  keyFile: &lt;path|string&gt;
  dataDir: &lt;path|string&gt;
  extraArgs:
    : &lt;value|string&gt;
    : &lt;value|string&gt;
  # 使用定制的镜像，不从k8s.gcr.io拉取
  image: 
networking:
  # --service-dns-domain string
  # 服务的DNS域名后缀，默认cluster.local
  dnsDomain: 
  # --service-cidr string
  # 服务的虚拟IP地址范围，默认10.96.0.0/12
  serviceSubnet: 
  # --pod-network-cidr string
  # Pod网络的的IP地址范围
  podSubnet: 
# --kubernetes-version string
# 创建的控制平面版本
kubernetesVersion: 
cloudProvider: 
# --node-name string
# 设置节点名称
nodeName: 
authorizationModes:
- &lt;authorizationMode1|string&gt;
- &lt;authorizationMode2|string&gt;
token: 
# --token-ttl duration 
# Bootstrap令牌的寿命，默认24h
tokenTTL: &lt;time&gt;
selfHosted: 
# 用于覆盖、扩展APIServer的行为
# 示例：
# apiServerExtraArgs:
#   feature-gates: APIResponseCompression=true
apiServerExtraArgs:
  : &lt;value|string&gt;
  : &lt;value|string&gt;
# 用于覆盖、扩展控制器管理器的行为
controllerManagerExtraArgs:
  : &lt;value|string&gt;
  : &lt;value|string&gt;
# 用于覆盖、扩展调度器的行为
schedulerExtraArgs:
  : &lt;value|string&gt;
  : &lt;value|string&gt;
apiServerExtraVolumes:
- name: &lt;value|string&gt;
  hostPath: &lt;value|string&gt;
  mountPath: &lt;value|string&gt;
controllerManagerExtraVolumes:
- name: &lt;value|string&gt;
  hostPath: &lt;value|string&gt;
  mountPath: &lt;value|string&gt;
schedulerExtraVolumes:
- name: &lt;value|string&gt;
  hostPath: &lt;value|string&gt;
  mountPath: &lt;value|string&gt;
# --apiserver-cert-extra-sans stringSlice
# APIServer HTTPS证书的，额外的主体备选名字（Subject Alternative Names，SAN），可以指定IP和DNS名
apiServerCertSANs:
- &lt;name1|string&gt;
- &lt;name2|string&gt;
# --cert-dir string
# 数字证书存放目录，默认/etc/kubernetes/pki
certificatesDir: 
# 不从k8s.gcr.io拉取镜像，从自定义位置拉取
imageRepository: 
# 使用其它的镜像作为控制平面组件
unifiedControlPlaneImage: 
# --feature-gates string
# 特性开关，包括：
#   CoreDNS=true|false                         (ALPHA - default=false)
#   DynamicKubeletConfig=true|false            (ALPHA - default=false)
#   SelfHosting=true|false                     (ALPHA - default=false)
#   StoreCertsInSecrets=true|false             (ALPHA - default=false)
featureGates:</pre>
<div class="blog_h3"><span class="graybg">使用定制镜像</span></div>
<p>默认情况下，Kubeadm从k8s.gcr.io拉取镜像。如果使用的K8S是CI版本，则从gcr.io/kubernetes-ci-images拉取镜像。</p>
<p>你可以设置配置文件的imageRepository选项，指定从其它服务器拉取镜像。</p>
<div class="blog_h3"><span class="graybg">使用定制证书</span></div>
<p>默认情况下，Kubeadm为集群生成所有需要的证书。你只需要在证书目录放置自己生成的证书，即可改变此行为。</p>
<p>如果/etc/kubernetes/pki/ca.crt、/etc/kubernetes/pki/ca.key文件存在，则分别被用作CA证书和私钥。Kubeadm会使用它们对其它CSR进行签名。</p>
<div class="blog_h3"><span class="graybg">管理Kubelet配置</span></div>
<p>Kubeadm包附带了一些配置文件，这些文件影响Kubelet的运行方式：</p>
<pre class="crayon-plain-tag">[Service]
# --bootstrap-kubeconfig 一个kubeconfig文件，用于节点加入集群时得到客户端证书信息，此文件会被写入到--kubeconfig指定位置
# --kubeconfig  包含了APIServer的位置、Kubelet的凭证
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf 
                                     --kubeconfig=/etc/kubernetes/kubelet.conf"
# --pod-manifest-path 从何处读取静态Pod的规格文件，这些Pod是控制平面的组件
# --allow-privileged 是否允许Kubelet执行特权Pod
Environment="KUBELET_SYSTEM_PODS_ARGS=--pod-manifest-path=/etc/kubernetes/manifests 
                                      --allow-privileged=true"
# --network-plugin 网络插件类型，Kubeadm仅仅支持CNI
# --cni-conf-dir CNI规格文件位置
# --cni-bin-dir  CNI可执行文件位置
Environment="KUBELET_NETWORK_ARGS=--network-plugin=cni --cni-conf-dir=/etc/cni/net.d 
                                  --cni-bin-dir=/opt/cni/bin"
# --cluster-dns 集群内部DNS的地址，会写入到Pod的/etc/resolv.conf的nameserver条目
# --cluster-domain 集群内部域名，会写入到Pod的/etc/resolv.conf的search条目
Environment="KUBELET_DNS_ARGS=--cluster-dns=10.96.0.10 
                              --cluster-domain=cluster.local"
# --authorization-mode 授权模式。Webhook表示通过POST一个SubjectAccessReview到APIServer来验证客户端
# --client-ca-file 用于验证的CA文件
Environment="KUBELET_AUTHZ_ARGS=--authorization-mode=Webhook 
                                --client-ca-file=/etc/kubernetes/pki/ca.crt"
# --cadvisor-port，设置为0表示禁用cAdvisor
Environment="KUBELET_CADVISOR_ARGS=--cadvisor-port=0"
# --rotate-certificates 证书过期后，自动向APIServer重新申请
# --cert-dir TLS证书目录
Environment="KUBELET_CERTIFICATE_ARGS=--rotate-certificates=true 
                                      --cert-dir=/var/lib/kubelet/pki"
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_AUTHZ_ARGS $KUBELET_CADVISOR_ARGS $KUBELET_CERTIFICATE_ARGS $KUBELET_EXTRA_ARGS</pre>
<div class="blog_h3"><span class="graybg">使用内部网络</span></div>
<p>如果集群有多套网络，你需要明确选择其中一套时，可以：</p>
<ol>
<li>初始化集群时指定APIServer监听端口：<pre class="crayon-plain-tag">kubeadm init --apiserver-advertise-address=10.0.0.1</pre></li>
<li>在Worker节点上安装好软件后，修改/etc/systemd/system/kubelet.service.d/10-kubeadm.conf，添加<pre class="crayon-plain-tag">--node-ip=10.0.0.x</pre></li>
<li>调用kubeadm join时，确保其中的IP地址填写的是10.0.0.1</li>
</ol>
<div class="blog_h3"><span class="graybg">自托管控制平面</span></div>
<p>从1.8开始，K8S支持所谓自托管控制平面，当前处于Alpha状态。自托管控制平面中，APIServer、控制器管理器、调度器等关键组件将以 DaemonSet而不是静态Pod方式运行。</p>
<div class="blog_h2"><span class="graybg">kubeadm join</span></div>
<p>初始化一个K8S集群的Worker节点，并加入到集群中。</p>
<p>当加入基于Kubeadm初始化的集群时，需要双向的身份验证，这通过两部分达成：</p>
<ol>
<li>Discovery：让Worker信任Master</li>
<li>TLS bootstrap：让Master信任Worker</li>
</ol>
<p>Discovery主要有两种模式：</p>
<ol>
<li>使用共享令牌 + APIServer的地址</li>
<li>使用一个文件，其内容是标准kubeconfig的子集</li>
</ol>
<p>TLS bootstrap也是基于共享令牌驱动的，用于临时的向APIServer提交身份验证，以便发起CSR请求。默认情况下，Kubeadm会自动配置Master，以完成对CSR的准许和签名。此令牌以–tls-bootstrap-token参数传递。</p>
<div class="blog_h3"><span class="graybg">工作流程</span></div>
<p>该命令初始化Worker节点并加入到集群。主要步骤：</p>
<ol>
<li>从APIServer下载必要的集群信息，默认的，使用Bootstrap令牌 + CA键的哈希来对信息进行验证</li>
<li>如果使用选项--feature-gates=DynamicKubeletConfig。则首先从Master下载Kubelet初始化配置，写入到磁盘。当Kubelet启动后Kubeadm更新节点的Node.spec.configSource属性</li>
<li>一旦集群信息获得，Kubelet可以启动TLS bootstrapping：
<ol>
<li>使用共享密钥临时和APIServer通信，提交CSRcat /etc/yum.repos.d/ceph.repo </li>
<li>默认的，控制平面会自动签名CSR</li>
</ol>
</li>
<li>Kubelet以后使用确定的Identity和APIServer通信</li>
</ol>
<div class="blog_h3"><span class="graybg">命令选项</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><em>--config  string</em>  指定一个kubeadm配置文件</p>
</td>
</tr>
<tr>
<td>
<p><em>--cri-socket string</em>  连接到的CRI（容器运行时接口，例如Docker）套接字，默认/var/run/dockershim.sock</p>
</td>
</tr>
<tr>
<td><em>--discovery-file string</em>  用于加载集群信息的文件或者URL </td>
</tr>
<tr>
<td><em>--discovery-token string</em>  用于验证加载到的集群信息的令牌</td>
</tr>
<tr>
<td>
<p><em>--discovery-token-ca-cert-hash stringSlice</em>  对于基于共享令牌的发现，使用此哈希验证根CA的公钥</p>
</td>
</tr>
<tr>
<td><em>--discovery-token-unsafe-skip-ca-verification</em>   对于基于共享令牌的发现，此选项禁用根CA的验证</td>
</tr>
<tr>
<td>
<p><em>--feature-gates string</em>  特性开关</p>
</td>
</tr>
<tr>
<td><em>--ignore-preflight-errors stringSlice</em>  忽略预先检查中出现的某些错误</td>
</tr>
<tr>
<td><em>--node-name string</em>   指定当前节点的名称</td>
</tr>
<tr>
<td><em>--tls-bootstrap-token string</em>   用于TLS bootstrap的共享令牌</td>
</tr>
<tr>
<td><em>--token string</em>  同时用于发现、TLS自举的令牌</td>
</tr>
</tbody>
</table>
<p>等价的配置文件形式：</p>
<pre class="crayon-plain-tag"># 使用 --config选项传入此文件

apiVersion: kubeadm.k8s.io/v1alpha1
kind: NodeConfiguration
caCertPath: &lt;path|string&gt;
discoveryFile: &lt;path|string&gt;
discoveryToken: 
discoveryTokenAPIServers:
- &lt;address|string&gt;
- &lt;address|string&gt;
nodeName: 
tlsBootstrapToken: 
token: 
discoveryTokenCACertHashes:
- 
- 
discoveryTokenUnsafeSkipCAVerification:</pre>
<div class="blog_h3"><span class="graybg">Master发现</span></div>
<p>Kubeadm提供了多种发现机制：</p>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><em>基于令牌的发现 + CA pinning</em></p>
<p>1.8+的默认机制。Kubeadm下载包括根CA在内的集群配置信息。使用令牌来验证配置信息，使用哈希来验证根CA公钥</p>
<p>缺点是：直到Master准备好之前，CA哈希是不知道的</p>
</td>
</tr>
<tr>
<td>
<p><em>基于令牌的发现，不验证CA</em></p>
<p>需要kubeadm --discovery-token-unsafe-skip-ca-verification</p>
<p>如果攻击者获得Bootstrap令牌，则他能够假扮（面向其它Worker）Master节点</p>
</td>
</tr>
<tr>
<td>
<p><em>基于文件/HTTPS的发现</em></p>
<p>需要kubeadm --discovery-file  path-or-url</p>
<p>缺点是，需要有某种机制把发现信息从Master传输到正在Join的节点上</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">安全加固</span></div>
<table class="full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><em>关闭Worker的CSR自动许可/签名</em></p>
<p>默认情况下，只要准备加入的Kubelet提供了合法的Bootstrap令牌，Master会自动准许其CSR。要关闭此特性，执行：</p>
<pre class="crayon-plain-tag">kubectl delete clusterrole kubeadm:node-autoapprove-bootstrap</pre>
<p> 这样的话，除非你手工准许了CSR，工作节点的Join请求会阻塞。准许方法：</p>
<pre class="crayon-plain-tag"># 获得CSR列表
kubectl get csr
# NAME                                                   AGE       REQUESTOR                 CONDITION
# node-csr-c69HXe7aYcqkS1bKmH4faEnHAWxn6i2bHZ2mD04jZyQ   18s       system:bootstrap:878f07   Pending

# 准许CSR
kubectl certificate approve node-csr-c69HXe7aYcqkS1bKmH4faEnHAWxn6i2bHZ2mD04jZyQ
# certificatesigningrequest "node-csr-c69HXe7aYcqkS1bKmH4faEnHAWxn6i2bHZ2mD04jZyQ" approved</pre>
</td>
</tr>
<tr>
<td>
<p><em>关闭对集群信息ConfigMap的开放访问</em>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">kubeadm upgrade</span></div>
<p>升级一个K8S集群到更新的版本，它将复杂的升级逻辑包装到单条命令当中。</p>
<div class="blog_h2"><span class="graybg">kubeadm token</span></div>
<p>管理用于kubeadm join的令牌：</p>
<pre class="crayon-plain-tag"># 创建Bootstrap令牌
kubeadm token create [token]
# 删除Bootstrap令牌
kubeadm token delete [token-value]
# 产生一个Bootstrap令牌，打印到屏幕上，但是不创建令牌对象
kubeadm token generate
# 列出令牌
kubeadm token list</pre>
<div class="blog_h2"><span class="graybg">kubeadm reset</span></div>
<p>撤销init/join命令对宿主机做出的任何改变。</p>
<p>如果使用外部etcd，则需要手工清除其中的数据：</p>
<pre class="crayon-plain-tag">etcdctl del "" --prefix</pre>
<div class="blog_h2"><span class="graybg">集群搭建实例</span></div>
<p>本集群由4台KVM虚拟机构成，静态IP地址分别为10.0.0.100-105，其中10.0.0.100作为Master节点。</p>
<p>由于本地网络访问Google存在困难，因此Docker镜像已经在云端下载并覆盖到各虚拟机。</p>
<div class="blog_h3"><span class="graybg"><a id="with-calico"></a>基于Calico</span></div>
<p>集群初始化：</p>
<pre class="crayon-plain-tag"># 访问Google需要代理
export http_proxy="http://10.0.0.1:8088/"
export https_proxy="http://10.0.0.1:8088/"
# 如果要使用其它CIDR，修改calico.yaml中的CALICO_IPV4POOL_CIDR
kubeadm init --apiserver-advertise-address 10.0.0.100  --pod-network-cidr=192.168.0.0/16 \
    --service-dns-domain  k8s.gmem.cc --kubernetes-version 1.9.0

# 拷贝kubeconfig
cp -i /etc/kubernetes/admin.conf ~/.kube/config &amp;&amp; chown $(id -u):$(id -g) ~/.kube/config</pre>
<p>完毕后，创建基于Calico的CNI网络：</p>
<pre class="crayon-plain-tag">kubectl apply -f https://docs.projectcalico.org/v3.0/getting-started/kubernetes/installation/hosted/kubeadm/1.7/calico.yaml</pre>
<p>等待所有控制平面组件Ready：</p>
<pre class="crayon-plain-tag"># 根据下面的命令输出判断
kubectl get pod --all-namespaces</pre>
<p>准备加入Worker节点，如果忘记了令牌，可以 这样查看：</p>
<pre class="crayon-plain-tag">kubeadm token list</pre>
<p>下面到Worker节点上执行：</p>
<pre class="crayon-plain-tag">kubeadm join --token f467bb.0cd7f59a071919a8 10.0.0.15:6443  --discovery-token-unsafe-skip-ca-verification</pre>
<p>可以拷贝Master上的kubeconfig，以便在Worker上执行集群管理：</p>
<pre class="crayon-plain-tag">mkdir ~/.kube
scp root@10.0.0.15:/etc/kubernetes/admin.conf ~/.kube/config</pre>
<p>依次加入所有工作节点。然后验证所有节点均正常：</p>
<pre class="crayon-plain-tag">kubectl get nodes
# NAME        STATUS    ROLES     AGE       VERSION
# xenial-15   Ready     master    12h       v1.9.0
# xenial-16   Ready         28m       v1.9.0
# xenial-17   Ready         6m        v1.9.0
# xenial-18   Ready         5m        v1.9.0</pre>
<p>下面测试Pod联通性。先创建一个Pod：</p>
<pre class="crayon-plain-tag">kubectl create -f /home/alex/Vmware/k8s/pods/busybox/sleep-600.yaml</pre><br />
<pre class="crayon-plain-tag"># Pod规格如下：
apiVersion: v1
kind: Pod
metadata:
  name: sleep-600
  labels:
    app: sleep-600
spec:
  containers:
  - name: sleep-600-container
    image: busybox
    command: ['sh', '-c', 'sleep 600']</pre>
<p>获得此Pod的基本信息：</p>
<pre class="crayon-plain-tag">kubectl describe pod sleep-600 | grep Node:
# Node: xenial-17/10.0.0.17
kubectl describe pod sleep-600 | grep IP:
# IP:           192.168.227.1</pre>
<p>尝试在Master节点上Ping此Pod，成功。然后在创建另外一个Pod，Ping它：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: Podkind: ConfigMap
apiVersion: v1
metadata:
 name: calico-config
 namespace: kube-system
data:
 # The location of your etcd cluster. This uses the Service clusterIP defined below.
 etcd_endpoints: "http://10.5.38.24:2379,http://10.5.38.39:2379,http://10.5.39.41:2379"
metadata:
  name: ping
  labels:
    app: ping
spec:
  containers:
  - name: ping-container
    image: busybox
    command: ['sh', '-c', 'ping 192.168.227.1']</pre>
<p>然后查看日志确认两个Pod可以联通：<pre class="crayon-plain-tag">kubectl logs ping</pre></p>
<div class="blog_h3"><span class="graybg">提供根密钥对</span></div>
<pre class="crayon-plain-tag">cd ~/Documents/puTTY
# 将加密的私钥转换为明文，根据提示输入密码
openssl rsa -in ca.gmem.cc.key -out ca.key

# 到Master节点执行：
mkdir -p /etc/kubernetes/pki/
cd /etc/kubernetes/pki/
scp alex@zircon.gmem.cc:/home/alex/Documents/puTTY/ca.gmem.cc.crt ca.crt
scp alex@zircon.gmem.cc:/home/alex/Documents/puTTY/ca.key ca.key</pre>
<div class="blog_h3"><span class="graybg">拷贝配置到客户端</span></div>
<pre class="crayon-plain-tag"># 在客户端机器（不是Kubernetes集群成员，仅仅使用kubectl命令）上执行：
scp -i ~/Documents/puTTY/gmem.key root@xenial-100.gmem.cc:/etc/kubernetes/admin.conf ~/.kube/config</pre>
<div class="blog_h3"><span class="graybg">基于Flannel</span></div>
<pre class="crayon-plain-tag">export http_proxy="http://10.0.0.1:8088/"
export https_proxy="http://10.0.0.1:8088/"

kubeadm init --apiserver-advertise-address 10.0.0.100 --service-dns-domain  k8s.gmem.cc --pod-network-cidr=10.244.0.0/16 --kubernetes-version 1.9.0

cp -i /etc/kubernetes/admin.conf ~/.kube/config &amp;&amp; chown $(id -u):$(id -g) ~/.kube/config

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/v0.9.1/Documentation/kube-flannel.yml </pre>
<div class="blog_h2"><span class="graybg">Master高可用</span></div>
<p>K8S要实现Master高可用，需要解决两个问题：</p>
<ol>
<li>Etcd的高可用，Etcd用于所有K8S状态信息的存储。建议在物理机上部署Etcd集群</li>
<li>API Server的高可用/负载均衡。任何四层代理均可，例如Nginx、LVS</li>
</ol>
<p>我们的方案是：Etcd三节点物理机部署、API Server三节点通过LVS负载均衡，架构图如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2016/06/k8s-master-ha.png"><img class="aligncenter  wp-image-22661" src="https://blog.gmem.cc/wp-content/uploads/2016/06/k8s-master-ha.png" alt="k8s-master-ha" width="924" height="284" /></a> </p>
<div class="blog_h3"><span class="graybg">实施步骤</span></div>
<ol>
<li>在物理节点上安装、配置好Etcd集群</li>
<li>在一台Master节点上执行<pre class="crayon-plain-tag">kubeadm init</pre></li>
<li>将/etc/kubernetes目录拷贝到其它Master节点</li>
<li>在其它Master节点上执行<pre class="crayon-plain-tag">kubeadm init</pre></li>
<li>配置LVS和Keepalived，作为API Server的负载均衡器</li>
<li>安装CNI，然后加入节点</li>
</ol>
<p>Kubeadm配置文件样例：</p>
<pre class="crayon-plain-tag">apiVersion: kubeadm.k8s.io/v1alpha1
kind: MasterConfiguration
api:
  # 这里填写虚IP
  advertiseAddress: 10.0.10.1
etcd:
  # 填写Etcd集群端点
  endpoints:
  - http://10.0.1.1:2379
  - http://10.0.2.1:2379
  - http://10.0.3.1:2379
networking:
  podSubnet: 172.27.0.0/16
kubernetesVersion: 1.10.2
imageRepository: docker.gmem.cc/k8s
apiServerCertSANs:
- 10.0.10.1
- 10.0.1.1
- 10.0.2.1
- 10.0.3.1
apiServerExtraArgs:
  apiserver-count: "3"</pre>
<div class="blog_h1"><span class="graybg">集群搭建（手工） </span></div>
<p>本章记录在Fedora 24上从零开始搭建K8S集群的详细步骤。</p>
<div class="blog_h2"><span class="graybg">准备阶段</span></div>
<div class="blog_h3"><span class="graybg">云服务</span></div>
<p>使用云服务时，你可能需要获取Cloud Provider，以管理TCP负载均衡、节点、网络路由。如果在本地VM、裸机上部署则不需要。</p>
<div class="blog_h3"><span class="graybg">节点</span></div>
<p>所有集群节点均应该是amd64的Linux。APIServer+etcd需要在1核/1G内存（10节点集群）或者更好的机器上运行。</p>
<div class="blog_h3"><span class="graybg">网络</span></div>
<p>K8S使用了特殊的网络模型。它会为每个Pod分配IP，你需要指定一个IP地址范围供Pod使用。Pod之间的连接性有两种达成途径：</p>
<ol>
<li>基于Overlay网络：基于流量封装，对Pod网络隐藏底层网络结构</li>
<li>不使用Overlay网络：配置底层设备（例如交换机）让其知晓Pod地址，不需要Overlay的那种封装，性能会更好</li>
</ol>
<p>具体实现方式，包括：</p>
<ol>
<li>使用CNI网络插件，例如Calico、Flannel、Weave</li>
<li>将Cloud Provider模块的Routes接口实现，直接编译到K8S中</li>
<li>在外部配置网络路由</li>
</ol>
<div class="blog_h3"><span class="graybg">网络策略</span></div>
<p>用于细粒度单Pod间网络流量控制，并非所有的网络实现支持。</p>
<div class="blog_h3"><span class="graybg">集群命名</span></div>
<p>你需要为每个集群起一个唯一性的名字，其用途为：</p>
<ol>
<li>kubectl用它来区分需要连接到哪个集群</li>
<li>区分属于不同集群的CloudProvider资源</li>
</ol>
<div class="blog_h3"><span class="graybg">需要的软件</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">软件</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>etcd</td>
<td>存放集群数据的分布式存储</td>
</tr>
<tr>
<td>CRI</td>
<td>容器运行时，Docker或rkt</td>
</tr>
<tr>
<td>K8S</td>
<td>kubelet、kube-proxy、kube-apiserver、kube-controller-manager、kube-scheduler</td>
</tr>
</tbody>
</table>
<p>这些软件都可以通过<a href="https://github.com/kubernetes/kubernetes/releases/tag/v1.9.0">K8S二进制发行版</a>获得。</p>
<p>Docker、Kubelet、Kube-Proxy在容器外部运行。</p>
<div class="blog_h3"><span class="graybg">需要的镜像</span></div>
<p>etcd、kube-apiserver、kube-controller-manager、 kube-scheduler则推荐以容器的方式运行。</p>
<p>获取K8S组件相关镜像的途径包括：</p>
<ol>
<li>使用GCR（Google Container Registry）提供的镜像：
<ol>
<li>例如gcr.io/google-containers/hyperkube:$TAG，其中TAG要和Kubelet/Kube-Proxy版本一致</li>
<li>此外<a href="https://releases.k8s.io/master/cmd/hyperkube">hyperkube</a>提供了All in On的二进制文件，使用hyperkube apiserver可以运行APIServer</li>
</ol>
</li>
<li>构建自己的镜像，如果打算搭建私服，可以使用该方式。二进制发行版中的./kubernetes/server/bin/kube-apiserver.tar可以被转换为Docker镜像：<br />
<pre class="crayon-plain-tag">docker load -i kube-apiserver.tar</pre>
</li>
</ol>
<p>获取etcd镜像的途径包括：</p>
<ol>
<li>使用GCR提供的镜像，例如gcr.io/google-containers/etcd:2.2.1</li>
<li>使用DockerHub或者Quay提供镜像，例如quay.io/coreos/etcd:v2.2.1</li>
<li>构建自己的镜像：<br />
<pre class="crayon-plain-tag">cd kubernetes/cluster/images/etcd; make</pre></p>
<p> 推荐使用K8S二进制发行版中附带的etcd</p>
</li>
</ol>
<div class="blog_h3"><span class="graybg">选择安全模型</span></div>
<p>两种访问APIServer的方式：</p>
<ol>
<li>使用HTTP，需要利用防火墙保证安全</li>
<li>使用HTTPS ，推荐的方式，需要制作数字证书：
<ol>
<li>Master需要数字证书，因为它的APIServer暴露HTTPS服务</li>
<li>Kubelet需要（可选）数字证书，用于：
<ol>
<li>APIServer的双向认证</li>
<li>自己也暴露HTTPS服务</li>
</ol>
</li>
</ol>
</li>
</ol>
<p>除非你想花钱购买证书，否则都需要首先生成根CA密钥对并自签名，然后用根证书对Master、kubelet进行签名。</p>
<p>管理员（或其它用户）需要一个表明自己身份的令牌（密码），令牌仅仅是一个数字+字母的字符串。</p>
<div class="blog_h2"><span class="graybg">详细步骤记录</span></div>
<div class="blog_h3"><span class="graybg">准备软件</span></div>
<pre class="crayon-plain-tag">wget https://github.com/kubernetes/kubernetes/releases/download/v1.9.0/kubernetes.tar.gz
tar xzf kubernetes.tar.gz &amp;&amp; rm kubernetes.tar.gz
# 下载客户端、服务器二进制文件
./kubernetes/cluster/get-kube-binaries.sh
# 下载的服务器文件位于 .kubernetes/server/kubernetes-server-linux-amd64.tar.gz
# 下载的客户端文件位于 .kubernetes/client/kubernetes-client-linux-amd64.tar.gz</pre>
<p>安装Docker：</p>
<pre class="crayon-plain-tag">yum install -y docker
# 启动Docker
systemctl enable docker &amp;&amp; systemctl start docker</pre>
<p>安装镜像：</p>
<pre class="crayon-plain-tag">docker pull gcr.io/google_containers/kube-proxy-amd64:v1.9.0
docker pull quay.io/calico/node:v2.6.5
docker pull quay.io/calico/kube-controllers:v1.0.2      
docker pull quay.io/calico/cni:v1.11.2     
docker pull gcr.io/google_containers/kube-proxy-amd64:v1.9.0      
docker pull gcr.io/google_containers/kube-apiserver-amd64:v1.9.0      
docker pull gcr.io/google_containers/kube-controller-manager-amd64:v1.9.0      
docker pull gcr.io/google_containers/kube-scheduler-amd64:v1.9.0      
docker pull weaveworks/weave-npc:2.1.3       
docker pull weaveworks/weave-kube:2.1.3       
docker pull quay.io/coreos/flannel:v0.9.1      
docker pull quay.io/coreos/flannel:v0.9.1-amd64
docker pull gcr.io/google_containers/k8s-dns-sidecar-amd64:1.14.7      
docker pull gcr.io/google_containers/k8s-dns-kube-dns-amd64:1.14.7      
docker pull gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:1.14.7      
docker pull quay.io/calico/node:v2.6.2      
docker pull quay.io/calico/cni:v1.11.0     
docker pull gcr.io/google_containers/etcd-amd64:3.1.10      
docker pull quay.io/coreos/etcd:v3.1.10     
docker pull gcr.io/google_containers/pause-amd64:3.0</pre>
<p>考虑到网络速度问题，将上述镜像Tag为私服镜像。实际编写K8S对象规格时，都使用私服镜像：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">源镜像</td>
<td style="text-align: center;">本地私服镜像</td>
</tr>
</thead>
<tbody>
<tr>
<td>quay.io/calico/node:v2.6.5</td>
<td>docker.gmem.cc/calico/node</td>
</tr>
<tr>
<td>quay.io/calico/kube-controllers:v1.0.2</td>
<td>docker.gmem.cc/calico/kube-controllers</td>
</tr>
<tr>
<td>quay.io/calico/cni:v1.11.2</td>
<td>docker.gmem.cc/calico/cni</td>
</tr>
<tr>
<td>gcr.io/google_containers/kube-proxy-amd64:v1.9.0</td>
<td>docker.gmem.cc/kube-proxy-amd64</td>
</tr>
<tr>
<td>gcr.io/google_containers/kube-apiserver-amd64:v1.9.0</td>
<td>docker.gmem.cc/kube-apiserver-amd64</td>
</tr>
<tr>
<td>gcr.io/google_containers/kube-controller-manager-amd64:v1.9.0</td>
<td>docker.gmem.cc/kube-controller-manager-amd64</td>
</tr>
<tr>
<td>gcr.io/google_containers/kube-scheduler-amd64:v1.9.0</td>
<td>docker.gmem.cc/kube-scheduler-amd64</td>
</tr>
<tr>
<td>weaveworks/weave-npc:2.1.3</td>
<td>docker.gmem.cc/weave-npc</td>
</tr>
<tr>
<td>weaveworks/weave-kube:2.1.3</td>
<td>docker.gmem.cc/weave-kube</td>
</tr>
<tr>
<td>quay.io/coreos/flannel:v0.9.1-amd64</td>
<td>docker.gmem.cc/flannel</td>
</tr>
<tr>
<td>gcr.io/google_containers/k8s-dns-sidecar-amd64:1.14.7</td>
<td>docker.gmem.cc/k8s-dns-sidecar-amd64</td>
</tr>
<tr>
<td>gcr.io/google_containers/k8s-dns-kube-dns-amd64:1.14.7</td>
<td>docker.gmem.cc/k8s-dns-kube-dns-amd64</td>
</tr>
<tr>
<td>gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:1.14.7</td>
<td>docker.gmem.cc/k8s-dns-dnsmasq-nanny-amd64</td>
</tr>
<tr>
<td>gcr.io/google_containers/etcd-amd64:3.1.10</td>
<td>docker.gmem.cc/etcd-amd64</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">GUI工具</span></div>
<div class="blog_h2"><span class="graybg">Dashboard</span></div>
<p>Kubernetes Dashboard是一个一般性用途的基于Web的UI工具。使用该工具你可以管理K8S集群中的应用程序，以及集群本身。</p>
<p>执行下面的命令部署Dashboard：</p>
<pre class="crayon-plain-tag">kubectl apply -f https://nginx.gmem.cc/k8s//dashboard/kubernetes-dashboard.yaml</pre>
<p>然后，你可以：</p>
<ol>
<li>客户端运行<pre class="crayon-plain-tag">kubectl proxy</pre>，并通过浏览器访问http://localhost:8001/ui，使用仪表盘</li>
<li>或者，直接访问https://kubernetes-dashboard.kube-system.svc.k8s.gmem.cc</li>
</ol>
<div class="blog_h3"><span class="graybg">使用SSL证书</span></div>
<pre class="crayon-plain-tag">openssl genrsa -out kube-system.svc.k8s.gmem.cc.key
openssl req -new -sha256 -key kube-system.svc.k8s.gmem.cc.key -out kube-system.svc.k8s.gmem.cc.csr
openssl x509 -req -days 3650 -in  kube-system.svc.k8s.gmem.cc.csr -CA ../ca.crt -CAkey ../ca.key  -CAcreateserial -out kube-system.svc.k8s.gmem.cc.crt

cp kube-system.svc.k8s.gmem.cc.key /tmp/cert/dashboard.key
cp kube-system.svc.k8s.gmem.cc.crt /tmp/cert/dashboard.crt

kubectl delete secret kubernetes-dashboard-certs -n kube-system
kubectl create secret generic kubernetes-dashboard-certs --from-file=/tmp/cert -n kube-system</pre>
<div class="blog_h3"><span class="graybg">登陆仪表盘</span></div>
<p>建议使用Token。首先创建一个SA：</p>
<pre class="crayon-plain-tag">apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin
  namespace: default
imagePullSecrets:
- name: gmemregsecret

---

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: admin-role-binding
subjects:
- kind: ServiceAccount
  name: admin
  namespace: default
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io</pre>
<p>然后查找此admin用户的secret：</p>
<pre class="crayon-plain-tag">kubectl describe sa admin
# Mountable secrets:   admin-token-msnfp
# Tokens:              admin-token-msnfp

kubectl describe secret admin-token-msnfp
# token: ...</pre>
<p>将上面的token字段拷贝出来，即可登陆。 </p>
<div class="blog_h3"><span class="graybg">配置参数</span></div>
<p><a href="https://github.com/kubernetes/dashboard/wiki/Dashboard-arguments">仪表盘的配置参数</a>，可以通过修改Deployment对象设置：</p>
<p style="padding-left: 30px;">- --token-ttl=0  会话过期时间，默认15分钟，设置为0永不过期</p>
<div class="blog_h2"><span class="graybg">Kubernetic</span></div>
<p>Kubernetic是一个支持OS X/Linux/Windows的桌面应用，可以查看各类K8S资源的状态。</p>
<div class="blog_h1"><span class="graybg">监控</span></div>
<div class="blog_h2"><span class="graybg">Grafana</span></div>
<p>这是一个优秀的、美观的开源时间序列数据分析、展示平台。</p>
<p>请参考：<a href="https://blog.gmem.cc/time-series-data-renderering-with-grafana">使用Grafana展示时间序列数据</a></p>
<div class="blog_h1"><span class="graybg">操控Etcd</span></div>
<div class="blog_h2"><span class="graybg">连接到Etcd</span></div>
<p>如果是通过kubeadm安装的单节点Master，可以在Master节点上执行：</p>
<pre class="crayon-plain-tag">ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379  \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt --key=/etc/kubernetes/pki/etcd/peer.key \
  --cert=/etc/kubernetes/pki/etcd/peer.crt  ...</pre>
<div class="blog_h2"><span class="graybg">基础操作</span></div>
<div class="blog_h3"><span class="graybg">搜索键</span></div>
<pre class="crayon-plain-tag"># 全文搜索 functions.kubeless
... get /registry --prefix -w json  | jq -r '.kvs[] | [.key,.value] | .[] ' | base64 -d | grep -a "functions.kubeless"</pre>
<div class="blog_h3"><span class="graybg">删除键</span></div>
<pre class="crayon-plain-tag">... del /registry/apiextensions.k8s.io/customresourcedefinitions/functions.kubeless.io </pre>
<div class="blog_h1"><span class="graybg">命令行参数</span></div>
<div class="blog_h2"><span class="graybg">kube-apiserver</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--advertise-address</td>
<td>
<p>在什么地址上声明当前API Server是集群的成员之一。指定的IP地址必须可以被集群的所有节点访问到</p>
<p>如果为空，默认使用--bind-address</p>
</td>
</tr>
<tr>
<td>--cors-allowed-origins strings</td>
<td>允许CORS的源列表</td>
</tr>
<tr>
<td>--external-hostname</td>
<td>生成外部URL时（例如Swagger API文档）时，当前master的主机名</td>
</tr>
<tr>
<td>--feature-gates mapStringBool</td>
<td>用于开启或关闭特性</td>
</tr>
<tr>
<td>--max-mutating-requests-inflight int</td>
<td>等待处理的写请求最大数量，默认200，超过后API Server拒绝请求</td>
</tr>
<tr>
<td>--max-requests-inflight int</td>
<td>等待处理的读请求最大数量，默认400，超过后API Server拒绝请求</td>
</tr>
<tr>
<td>--min-request-timeout int</td>
<td>最小请求超时，API Server认定一个请求超时之前必须经过的时间，默认1800，此参数仅仅被watch请求使用，覆盖--request-timeout </td>
</tr>
<tr>
<td>--request-timeout duration</td>
<td>请求超时，默认1m0s</td>
</tr>
<tr>
<td>--target-ram-mb</td>
<td>API Server的内存用量限制</td>
</tr>
<tr>
<td colspan="2"><strong><em>Etcd相关</em></strong></td>
</tr>
<tr>
<td>--default-watch-cache-size int</td>
<td>默认Watch缓存的大小，设置为0则没有默认Watch size的资源的Watch缓存被禁用，默认100</td>
</tr>
<tr>
<td>--delete-collection-workers int</td>
<td>用于DeleteCollection操作的工作线程数量，用于加速命名空间清理，默认1</td>
</tr>
<tr>
<td>--deserialization-cache-size int</td>
<td>反串行化到内存的JSON对象数量，这些对象做缓存用途</td>
</tr>
<tr>
<td>--etcd-cafile string</td>
<td>SSL证书文件</td>
</tr>
<tr>
<td>--etcd-prefix string</td>
<td>所有资源存放的路径前缀，默认/registry</td>
</tr>
<tr>
<td>--etcd-compaction-interval duration</td>
<td>压缩请求的间隔，如果为0则禁用API Server发出压缩请求，默认5m0s</td>
</tr>
<tr>
<td>--etcd-servers strings</td>
<td>需要连接到的Etcd服务器地址列表，形式scheme://ip:port,scheme://ip:port...</td>
</tr>
<tr>
<td>--etcd-servers-overrides strings</td>
<td>
<p>可以为不同资源指定不同的Etcd服务器，形式group/resource#servers，servers为Etcd服务器地址，形式如上一行</p>
</td>
</tr>
<tr>
<td>--storage-backend</td>
<td>存储后端，可选etcd3、etcd2</td>
</tr>
<tr>
<td>--storage-media-type string</td>
<td>存储MIME类型，默认application/vnd.kubernetes.protobuf</td>
</tr>
<tr>
<td>--watch-cache</td>
<td>在API Server中启用Watch缓存，默认true</td>
</tr>
<tr>
<td>--watch-cache-sizes strings</td>
<td>不同资源的Watch缓存大小，形式 resource[.group]#size</td>
</tr>
<tr>
<td colspan="2"><strong><em>安全选项</em></strong></td>
</tr>
<tr>
<td>--bind-address ip</td>
<td>在什么IP地址上监听--secure-port端口，默认0.0.0.0</td>
</tr>
<tr>
<td>--secure-port int</td>
<td>HTTPS端口</td>
</tr>
<tr>
<td>--cert-dir string</td>
<td>TLS证书存放目录，如果指定参数--tls-cert-file、--tls-private-key-file则此选项忽略</td>
</tr>
<tr>
<td>--tls-cert-file string</td>
<td>TLS证书位置</td>
</tr>
<tr>
<td>--tls-private-key-file string</td>
<td>TLS私钥位置</td>
</tr>
<tr>
<td>--insecure-bind-address ip</td>
<td>HTTP监听地址</td>
</tr>
<tr>
<td>--insecure-port</td>
<td>HTTP监听端口</td>
</tr>
<tr>
<td colspan="2"><strong><em>审计选项</em></strong></td>
</tr>
<tr>
<td>--audit-log-*</td>
<td>审计日志相关</td>
</tr>
<tr>
<td>--audit-webhook-*</td>
<td>审计Webhook相关</td>
</tr>
<tr>
<td colspan="2"><strong><em>特性选项</em></strong></td>
</tr>
<tr>
<td>--contention-profiling</td>
<td>在Profiling启用的前提下，启用锁争用Profiling</td>
</tr>
<tr>
<td>--enable-swagger-ui</td>
<td>在/swagger-ui下暴露Swagger UI</td>
</tr>
<tr>
<td colspan="2"><strong><em>身份验证</em></strong></td>
</tr>
<tr>
<td>--anonymous-auth=true</td>
<td>
<p>允许对HTTPS端口的匿名请求，没有被另外一种身份验证机制拒绝的请求被看作匿名请求</p>
<p>匿名请求的用户名为system:anonymous，组为system:unauthenticated</p>
</td>
</tr>
<tr>
<td>--authentication-token-webhook-cache-ttl</td>
<td>缓存webhook token authenticator响应的时长，默认2m0s</td>
</tr>
<tr>
<td>--authentication-token-webhook-config-file</td>
<td>
<p>用于Token验证的Webhook配置文件路径，此文件必须为kubeconfig格式</p>
<p>API Server会查询远程服务，来决定对bearer token的身份验证</p>
</td>
</tr>
<tr>
<td>--basic-auth-file string</td>
<td>如果设置，该文件用于准许通过HTTP基本认证来访问API Server的请求</td>
</tr>
<tr>
<td>--client-ca-file string</td>
<td>教研客户端证书的CA证书，<span style="background-color: #c0c0c0;">客户端身份存放在CommonName字段</span>中</td>
</tr>
<tr>
<td>--oidc-*</td>
<td>和OpenID服务有关</td>
</tr>
<tr>
<td>-requestheader-allowed-names</td>
<td>哪些客户端（通过CommonName识别）可以在 --requestheader-username-headers中提供用户名信息，如果为空，任何通过CA认证的客户端均可</td>
</tr>
<tr>
<td>--requestheader-client-ca-file</td>
<td>在信任客户端请求头中声明的username之前，需要对客户端证书进行认证，该参数指定认证时使用的CA证书</td>
</tr>
<tr>
<td>--requestheader-extra-headers-prefix</td>
<td>请求头前缀，建议X-Remote-Extra-</td>
</tr>
<tr>
<td>--requestheader-group-headers</td>
<td>用户所属组存放在什么请求头，建议X-Remote-Group</td>
</tr>
<tr>
<td>--requestheader-username-headers</td>
<td>用户名存放在什么请求头，，建议X-Remote-User</td>
</tr>
<tr>
<td>--authorization-mode</td>
<td>在安全端口上运行的授权插件列表， AlwaysAllow,AlwaysDeny,ABAC,Webhook,RBAC,Node，默认AlwaysAllow</td>
</tr>
<tr>
<td>--authorization-policy-file</td>
<td>CSV格式的授权策略文件，和--authorization-mode=ABAC连用</td>
</tr>
<tr>
<td>--authorization-webhook-cache-authorized-ttl</td>
<td>缓存来自Webhook authorizer的授权成功（authorized）响应的时长，默认5m0s</td>
</tr>
<tr>
<td>--authorization-webhook-cache-unauthorized-ttl</td>
<td>缓存来自Webhook authorizer的授权失败（unauthorized）响应的时长，默认30s</td>
</tr>
<tr>
<td>--authorization-webhook-config-file</td>
<td>kubeconfig格式的Webhook配置文件路径</td>
</tr>
<tr>
<td colspan="2"><strong><em>API开关</em></strong></td>
</tr>
<tr>
<td>--runtime-config mapStringString</td>
<td>可以用户开启/关闭特定的API版本</td>
</tr>
<tr>
<td colspan="2"><strong><em>Admission参数</em></strong></td>
</tr>
<tr>
<td>--admission-control</td>
<td>启用的准许控制器列表，此选项废弃</td>
</tr>
<tr>
<td>--admission-control-config-file</td>
<td>包含准许控制配置的文件</td>
</tr>
<tr>
<td>--disable-admission-plugins</td>
<td>禁用的准许控制器列表，即使它们默认启用</td>
</tr>
<tr>
<td>--enable-admission-plugins</td>
<td>启用的准许控制器列表</td>
</tr>
<tr>
<td colspan="2"><strong><em>杂项</em></strong></td>
</tr>
<tr>
<td>--allow-privileged=false</td>
<td>允许运行特权模式容器</td>
</tr>
<tr>
<td>--apiserver-count=1</td>
<td>集群中API Server的数量</td>
</tr>
<tr>
<td>--enable-aggregator-routing</td>
<td>允许路由请求到Endpoint IP而非Cluster IP</td>
</tr>
<tr>
<td>--enable-logs-handler=true</td>
<td>如果为true，则开启/logs端点，用于处理API Server日志</td>
</tr>
<tr>
<td>
<p>--endpoint-reconciler-type=lease</p>
</td>
<td> </td>
</tr>
<tr>
<td>--event-ttl=1h0m0s</td>
<td>事件历史的保留时间</td>
</tr>
<tr>
<td>--kubelet-certificate-authority</td>
<td>访问Kubelet的CA证书的路径</td>
</tr>
<tr>
<td>--kubelet-client-certificate</td>
<td>访问Kubelet的客户端证书路径</td>
</tr>
<tr>
<td>--kubelet-client-key</td>
<td>访问Kubelet的客户端私钥路径</td>
</tr>
<tr>
<td>--kubelet-https=true</td>
<td>通过HTTPS来访问Kubelet</td>
</tr>
<tr>
<td>
<p>--kubelet-timeout=5s</p>
</td>
<td>访问Kubelet的超时</td>
</tr>
<tr>
<td>--max-connection-bytes-per-sec</td>
<td>限流，每个用户连接每秒可以发送的字节数，当前仅仅允许长期运行的请求</td>
</tr>
<tr>
<td>--service-account-signing-key-file</td>
<td>Service Account Token Issuer的私钥路径，Issuer使用此私钥来签名issued ID token</td>
</tr>
<tr>
<td>--service-cluster-ip-range</td>
<td>Cluster IP的地址范围，默认 10.0.0.0/24</td>
</tr>
<tr>
<td>--service-node-port-range</td>
<td>NodePort范围，默认30000-32767</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">kube-proxy</span></div>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 35%; text-align: center;"><strong>参数</strong></td>
<td style="text-align: center;"><b>说明</b></td>
</tr>
<tr>
<td>--alsologtostderr</td>
<td>设置true则日志输出到stderr，也输出到日志文件</td>
</tr>
<tr>
<td>--bind-address 0.0.0.0</td>
<td>监听主机IP地址，0.0.0.0监听主机所有主机接口 ，默认0.0.0.0</td>
</tr>
<tr>
<td>--cleanup</td>
<td>如果设置为true，则清除iptables和ipvs规则并退出</td>
</tr>
<tr>
<td>--cleanup-ipvs</td>
<td>如果设置为true，在运行前kube-proxy将清除ipvs规则，默认true</td>
</tr>
<tr>
<td>--cluster-cidr string</td>
<td>集群中 Pod 的CIDR范围。集群外的发送到服务集群IP的流量将被伪装，从pod发送到外部 LoadBalancer IP的流量将被定向到相应的集群IP</td>
</tr>
<tr>
<td>--config string</td>
<td>配置文件路径</td>
</tr>
<tr>
<td>--config-sync-period duration</td>
<td>从apiserver同步配置的时间间隔，默认15m0s</td>
</tr>
<tr>
<td>--conntrack-max-per-core int32</td>
<td>
<p>每个CPU核跟踪的最大NAT连接数</p>
<p>0按原来保留限制并忽略conntrack-min，默认32768</p>
</td>
</tr>
<tr>
<td>--conntrack-min int32</td>
<td>
<p>分配的最小conntrack条目，无视conntrack-max-per-core选项</p>
<p>设置conntrack-max-per-core=0保持原始限制，默认default 131072</p>
</td>
</tr>
<tr>
<td>--conntrack-tcp-timeout-close-wait duration</td>
<td>对于TCP连接处于CLOSE_WAIT阶段的NAT超时时间，默认1h0m0s</td>
</tr>
<tr>
<td>--conntrack-tcp-timeout-established duration</td>
<td>TCP连接的空闲超时，默认24h0m0s</td>
</tr>
<tr>
<td>--feature-gates mapStringBool</td>
<td>打开特性</td>
</tr>
<tr>
<td>--healthz-bind-address 0.0.0.0</td>
<td>
<p>健康检查服务器提供服务的IP地址及端口，默认0.0.0.0:10256</p>
</td>
</tr>
<tr>
<td>--healthz-port int32</td>
<td>配置健康检查服务的端口，0表示禁止，默认10256</td>
</tr>
<tr>
<td>--hostname-override string</td>
<td>使用该名字作为标识而不是实际的主机名</td>
</tr>
<tr>
<td>--iptables-masquerade-bit int32</td>
<td>对于纯iptables代理，则表示fwmark space的位数，用于标记需要SNAT的数据包。[0,31]之间，default 14</td>
</tr>
<tr>
<td>--iptables-min-sync-period duration</td>
<td>当endpoints和service变化，刷新iptables规则的最小时间间隔 </td>
</tr>
<tr>
<td>--iptables-sync-period duration</td>
<td>iptables刷新的最大时间间隔，默认30s</td>
</tr>
<tr>
<td> --ipvs-exclude-cidrs strings</td>
<td>ipvs proxier清理IPVS规则时不触及的CIDR以逗号分隔的列表</td>
</tr>
<tr>
<td>--ipvs-min-sync-period duration</td>
<td>当endpoints和service变化，刷新ipvs规则的最小时间间隔 </td>
</tr>
<tr>
<td>--ipvs-scheduler string</td>
<td>当proxy模式设置为ipvs，ipvs调度的类型</td>
</tr>
<tr>
<td>--ipvs-sync-period duration</td>
<td>ipvs刷新的最大时间间隔，默认30s</td>
</tr>
<tr>
<td>--kube-api-burst int32</td>
<td>发送到kube-apiserver每秒请求量，默认10</td>
</tr>
<tr>
<td>--kube-api-content-type string</td>
<td>发送到kube-apiserver请求的MIME类型，默认application/vnd.kubernetes.protobuf</td>
</tr>
<tr>
<td>--kube-api-qps float32</td>
<td>与kube-apiserver通信的qps，默认default 5</td>
</tr>
<tr>
<td>--kubeconfig string</td>
<td>kubeconfig文件的路径</td>
</tr>
<tr>
<td>--log-backtrace-at traceLocation file:N</td>
<td>
<p>when logging hits line file:N, emit a stack trace (default :0)</p>
<p>当日志发生在什么代码位置时打印调用栈</p>
</td>
</tr>
<tr>
<td>--log-dir string</td>
<td>日志文件的存储位置</td>
</tr>
<tr>
<td>--log-flush-frequency duration</td>
<td>日志刷出的最大间隔，默认5s</td>
</tr>
<tr>
<td>--logtostderr</td>
<td>日志输出到标准错误而非文件</td>
</tr>
<tr>
<td>--masquerade-all</td>
<td>纯 iptables 代理，对所有通过集群 service IP发送的流量进行 SNAT（通常不配置）</td>
</tr>
<tr>
<td>--master string</td>
<td>Kubernetes API server地址，覆盖kubeconfig的配置</td>
</tr>
<tr>
<td>--metrics-bind-address 0.0.0.0</td>
<td>metrics服务地址和端口，默认127.0.0.1:10249</td>
</tr>
<tr>
<td>--nodeport-addresses strings</td>
<td>
<p>NodePort使用哪些IP地址，示例1.2.3.0/24, 1.2.3.4/32，默认情况下所有本地地址都使用</p>
</td>
</tr>
<tr>
<td>--oom-score-adj int32</td>
<td>kube-proxy进程的oom-score-adj值，合法值范围[-1000, 1000] ，默认-999</td>
</tr>
<tr>
<td>--profiling</td>
<td>设置为true，通过web接口/debug/pprof查看性能分析</td>
</tr>
<tr>
<td>--proxy-mode ProxyMode</td>
<td>代理模式，可选值userspace / iptables / ipvs，默认iptables</td>
</tr>
<tr>
<td>--proxy-port-range port-range</td>
<td>
<p>可以用于代理K8S Service流量的苏主机端口范围</p>
</td>
</tr>
<tr>
<td>--stderrthreshold severity</td>
<td>日志输出的最低级别，默认2</td>
</tr>
<tr>
<td>--udp-timeout duration</td>
<td>
<p>空闲UDP连接保持打开的时长，默认250ms</p>
</td>
</tr>
<tr>
<td>-v, --v Level</td>
<td>日志冗余级别</td>
</tr>
<tr>
<td>--version version[=true]</td>
<td>打印版本信息并退出</td>
</tr>
<tr>
<td>--vmodule moduleSpec</td>
<td>逗号分隔的模式=N的列表文件，用以筛选日志记录</td>
</tr>
<tr>
<td>--write-config-to string</td>
<td>输出默认配置到文件并退出</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">kube-scheduler</span></div>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 35%; text-align: center;"><strong>参数</strong></td>
<td style="text-align: center;"><strong>说明</strong></td>
</tr>
<tr>
<td>--address string</td>
<td>监听主机IP地址，0.0.0.0监听主机所有主机接口</td>
</tr>
<tr>
<td style="width: 263px;">--algorithm-provider string</td>
<td style="width: 585px;">设置调度算法，ClusterAutoscalerProvider或DefaultProvider，默认为DefaultProvider</td>
</tr>
<tr>
<td style="width: 263px;">--alsologtostderr</td>
<td style="width: 585px;">设置true则日志输出到stderr，也输出到日志文件</td>
</tr>
<tr>
<td style="width: 263px;">--config string</td>
<td style="width: 585px;">配置文件的路径</td>
</tr>
<tr>
<td style="width: 263px;">--kube-api-burst int32</td>
<td style="width: 585px;">发送到kube-apiserver每秒请求量 ，默认100</td>
</tr>
<tr>
<td style="width: 263px;">--kube-api-content-type string</td>
<td style="width: 585px;">发送到kube-apiserver请求内容类型，默认application/vnd.kubernetes.protobuf</td>
</tr>
<tr>
<td style="width: 263px;">--kube-api-qps float32</td>
<td style="width: 585px;">与kube-apiserver通信的qps，默认50</td>
</tr>
<tr>
<td style="width: 263px;">--kubeconfig string</td>
<td style="width: 585px;">kubeconfig配置文件路径</td>
</tr>
<tr>
<td style="width: 263px;">--leader-elect</td>
<td style="width: 585px;">多个master情况设置为true保证高可用，进行leader选举</td>
</tr>
<tr>
<td style="width: 263px;">--leader-elect-lease-duration duration</td>
<td style="width: 585px;">当leader-elect设置为true生效，选举过程中非leader候选等待选举的时间间隔，默认15s</td>
</tr>
<tr>
<td style="width: 263px;">--leader-elect-renew-deadline duration</td>
<td style="width: 585px;">leader选举过程中在停止leading，再次renew时间间隔，小于或者等于leader-elect-lease-duration duration，也是leader-elect设置为true生效，默认10s</td>
</tr>
<tr>
<td style="width: 263px;">--leader-elect-retry-period duration</td>
<td style="width: 585px;">当leader-elect设置为true生效，获取leader或者重新选举的等待间隔，默认2s</td>
</tr>
<tr>
<td style="width: 263px;">--lock-object-name string</td>
<td style="width: 585px;">定义lock对象名字，默认kube-scheduler</td>
</tr>
<tr>
<td style="width: 263px;">--lock-object-namespace string</td>
<td style="width: 585px;">定义lock对象的namespace，默认kube-system</td>
</tr>
<tr>
<td style="width: 263px;">--log-backtrace-at traceLocation</td>
<td style="width: 585px;">记录日志到file:行号时打印一次stack trace，默认0</td>
</tr>
<tr>
<td style="width: 263px;">--log-dir string</td>
<td style="width: 585px;">记录log的目录</td>
</tr>
<tr>
<td style="width: 263px;">--log-flush-frequency duration</td>
<td style="width: 585px;">flush log的时间间隔，默认5s</td>
</tr>
<tr>
<td style="width: 263px;">--logtostderr</td>
<td style="width: 585px;">写log到stderr，默认true</td>
</tr>
<tr>
<td style="width: 263px;">--master string</td>
<td style="width: 585px;">master的地址，会覆盖kubeconfig中的</td>
</tr>
<tr>
<td style="width: 263px;">--port int</td>
<td style="width: 585px;">没有认证鉴权的不安全端口，默认10251</td>
</tr>
<tr>
<td style="width: 263px;">--profiling</td>
<td style="width: 585px;">开启性能分析，通过host:port/debug/pprof/查看</td>
</tr>
<tr>
<td style="width: 263px;">--scheduler-name string</td>
<td style="width: 585px;">调度器名，由于哪些pod被调度器进行处理，根据pod的spec.schedulerName，默认default-scheduler</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">kubelet</span></div>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td style="width: 35%; text-align: center;"><strong>参数</strong></td>
<td style="text-align: center;"><strong>说明</strong></td>
</tr>
<tr>
<td>--allow-privileged=true</td>
<td>允许容器请求特权模式</td>
</tr>
<tr>
<td style="width: 419px;">--anonymous-auth=false</td>
<td style="width: 428px;">
<p>允许匿名请求到 kubelet 服务。未被另一个身份验证方法拒绝的请求被视为匿名请求。匿名请求包含系统的用户名: anonymous ，以及系统的组名: unauthenticated，默认 true </p>
</td>
</tr>
<tr>
<td style="width: 419px;">--application-metrics-count-limit int</td>
<td style="width: 428px;">每一个容器store最大application metrics，默认100</td>
</tr>
<tr>
<td style="width: 419px;">--authentication-token-webhook-cache-ttl</td>
<td style="width: 428px;">
<p>webhook 令牌身份验证缓存响应时间，默认2m0s</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--authorization-mode string</td>
<td style="width: 428px;">
<p>授权模式（AlwaysAllow/ Webhook），Webhook 模式使用 SubjectAccessReview API 来确定授权</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--authorization-webhook-cache-authorized-ttl</td>
<td style="width: 428px;">
<p>webhook 模式认证响应缓存时间，默认5m0s</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--authorization-webhook-cache-unauthorized-ttl</td>
<td style="width: 428px;">webhook 模式认证未响应缓存时间，默认30s</td>
</tr>
<tr>
<td style="width: 419px;">--authentication-token-webhook</td>
<td style="width: 428px;">使用 TokenReview API 来确定不记名令牌的身份验证</td>
</tr>
<tr>
<td style="width: 419px;">--bootstrap-kubeconfig</td>
<td style="width: 428px;">
<p>kubelet 客户端证书的kubeconfig 文件路径</p>
<p>如果指定的文件不存在，将使用 bootstrap kubeconfig 从 API 服务器请求一个客户端证书，成功后生成证书文件和密钥的 kubeconfig 将被写入指定的文件，客户端证书和密钥将被保存在 --cert-dir 指定的目录</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--cadvisor-port=0</td>
<td style="width: 428px;">cAdvisor 端口，默认 4194</td>
</tr>
<tr>
<td style="width: 419px;"> --cert-dir string</td>
<td style="width: 428px;">客户端证书和密钥保存到的目录</td>
</tr>
<tr>
<td style="width: 419px;">--cgroup-driver=cgroupfs</td>
<td style="width: 428px;">可选值有cgroupfs和systemd，与docker驱动一致。默认cgroupfs</td>
</tr>
<tr>
<td style="width: 419px;">--cgroup-root string</td>
<td style="width: 428px;">
<p>Pod的根cgroup</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--cgroups-per-qos</td>
<td style="width: 428px;">
<p>是否创建 QoS cgroup 层级，true 意味着创建顶级 QoS 和pod cgroups ，默认true</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--client-ca-file</td>
<td style="width: 428px;">集群CA证书，默认/etc/kubernetes/ssl/ca.pem</td>
</tr>
<tr>
<td style="width: 419px;">--cluster-dns</td>
<td style="width: 428px;">DNS 服务器的IP列表，逗号分隔</td>
</tr>
<tr>
<td style="width: 419px;">--cluster-domain</td>
<td style="width: 428px;">集群域名, kubelet 将配置所有容器除了主机搜索域还将搜索当前域</td>
</tr>
<tr>
<td style="width: 419px;">--cni-bin-dir</td>
<td style="width: 428px;">CNI插件二进制文件路径，默认/opt/cni/bin</td>
</tr>
<tr>
<td style="width: 419px;">--cni-conf-dir</td>
<td style="width: 428px;">CNI插件配置文件的完整路径，默认/etc/cni/net.d</td>
</tr>
<tr>
<td style="width: 419px;">--container-runtime</td>
<td style="width: 428px;">
<p>指定容器运行时引擎（CRI）</p>
</td>
</tr>
<tr>
<td style="width: 419px;"> --cpu-cfs-quota</td>
<td style="width: 428px;">
<p>开启cpu cfs配额来对容器指定cpu限制，默认true</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--cpu-cfs-quota-period duration</td>
<td style="width: 428px;">设置cpu cfs配置周期值，cpu.cfs_period_us，默认100ms</td>
</tr>
<tr>
<td style="width: 419px;">
<p>--enable-controller-attach-detach</p>
</td>
<td style="width: 428px;">开启attach/detach控制器来管理调度到该节点上的volume</td>
</tr>
<tr>
<td style="width: 419px;">--enforce-node-allocatable strings </td>
<td style="width: 428px;">
<p>默认pods</p>
<p>如果为kube组件和System进程预留资源，则需要设置为pods,kube-reserved,system-reserve</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--event-burst int32</td>
<td style="width: 428px;">
<p>突发事件记录的最大值</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--eviction-hard</td>
<td style="width: 428px;">
<p>清理阈值的集合，达到该阈值将触发一次容器清理，示例：</p>
<pre class="crayon-plain-tag">memory.available&lt;1000Mi,nodefs.available&lt;10%,nodefs.inodesFree&lt;10% </pre>
</td>
</tr>
<tr>
<td style="width: 419px;">--eviction-minimum-reclaim</td>
<td style="width: 428px;">
<p>资源回收最小值的集合，即 kubelet 压力较大时 ，执行 pod 清理回收的资源最小值，示例：
<pre class="crayon-plain-tag">memory.available=0Mi,nodefs.available=500Mi,imagefs.available=2Gi</pre>
</td>
</tr>
<tr>
<td style="width: 419px;">--eviction-soft</td>
<td style="width: 428px;">
<p>清理阈值的集合，如果达到一个清理周期将触发一次容器清理，示例：
<pre class="crayon-plain-tag">memory.available&lt;1.5Gi</pre>
</td>
</tr>
<tr>
<td style="width: 419px;">--eviction-soft-grace-period</td>
<td style="width: 428px;">
<p>清理周期的集合，在触发一个容器清理之前一个软清理阈值需要保持多久：
<pre class="crayon-plain-tag">memory.available=300s,nodefs.available=300s,nodefs.inodesFree=300s</pre>
</td>
</tr>
<tr>
<td style="width: 419px;">--fail-swap-on</td>
<td style="width: 428px;">开启了SWAP的前提下，如果设置为true则启动kubelet失败。默认true</td>
</tr>
<tr>
<td style="width: 419px;">
<p>--hairpin-mode string
</td>
<td style="width: 428px;">
<p>可选值promiscuous-bridge / hairpin-veth，NAT回环模式，默认promiscuous-bridge</p>
</td>
</tr>
<tr>
<td style="width: 419px;">
<p>--healthz-bind-address</p>
</td>
<td style="width: 428px;">
<p>健康检查服务的IP地址，默认127.0.0.1 </p>
</td>
</tr>
<tr>
<td style="width: 419px;">--healthz-port</td>
<td style="width: 428px;">
<p>本地健康检查服务的端口号，默认10248</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--host-ipc-sources</td>
<td style="width: 428px;">
<p>允许 pod 使用宿主机 ipc 命名空间列表，默认[*]</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--host-network-sources</td>
<td style="width: 428px;">允许 pod 使用宿主机 net 命名空间列表，默认[*]</td>
</tr>
<tr>
<td style="width: 419px;">--host-pid-sources</td>
<td style="width: 428px;">允许 pod 使用宿主机 pid 命名空间列表，默认[*]</td>
</tr>
<tr>
<td style="width: 419px;">--hostname-override</td>
<td style="width: 428px;">在K8S集群中的当前node name</td>
</tr>
<tr>
<td style="width: 419px;">
<p>--http-check-frequency</p>
</td>
<td style="width: 428px;">
<p>通过 http 检查新数据的周期，默认20s</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--image-gc-high-threshold</td>
<td style="width: 428px;">
<p>磁盘使用占比最大值，超过此值将执行镜像垃圾回收，默认85</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--image-gc-low-threshold</td>
<td style="width: 428px;">磁盘使用率最大值，低于此值将停止镜像垃圾回收，默认80</td>
</tr>
<tr>
<td style="width: 419px;">--image-pull-progress-deadline</td>
<td style="width: 428px;">镜像拉取进度最大时间，如果在这段时间拉取镜像没有任何进展，将取消拉取，默认1m0s</td>
</tr>
<tr>
<td style="width: 419px;"> --iptables-drop-bit</td>
<td style="width: 428px;">
<p>用于标记丢弃数据包的 fwmark  bit，取值范围[0，31]，默认15</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--iptables-masquerade-bit</td>
<td style="width: 428px;">
<p>标记 SNAT 数据包的 fwmark bit，取值范围[0，31]，此参数与 kube-proxy 中的相应参数匹配，默认14</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--kube-api-burst</td>
<td style="width: 428px;">与 kubernetes apiserver 会话时的并发数，默认 10</td>
</tr>
<tr>
<td style="width: 419px;">--kube-api-qps</td>
<td style="width: 428px;">与 kubernetes apiserver 会话时的 QPS，默认15</td>
</tr>
<tr>
<td style="width: 419px;">--kube-reserved</td>
<td style="width: 428px;">
<p>资源预留量，针对kubernetes 系统组件 ，示例：</p>
<pre class="crayon-plain-tag">cpu=200m,memory=500Mi, storage=1Gi</pre>
</td>
</tr>
<tr>
<td style="width: 419px;">--kubeconfig</td>
<td style="width: 428px;">用来指定如何连接到 API Server，默认/etc/kubernetes/kubelet.kubeconfig</td>
</tr>
<tr>
<td style="width: 419px;">--log-dir</td>
<td style="width: 428px;">日志文件路径，默认/var/log/kubernetes</td>
</tr>
<tr>
<td style="width: 419px;">--logtostderr</td>
<td style="width: 428px;">标准输出</td>
</tr>
<tr>
<td style="width: 419px;">--max-pods</td>
<td style="width: 428px;">可以运行的容器组数目，默认 110</td>
</tr>
<tr>
<td style="width: 419px;">--network-plugin</td>
<td style="width: 428px;">使用CNI插件</td>
</tr>
<tr>
<td style="width: 419px;">--node-ip</td>
<td style="width: 428px;">节点的IP地址，kubelet 将使用这个地址作为节点ip地址，主要针对多网卡环境</td>
</tr>
<tr>
<td style="width: 419px;">--node-labels</td>
<td style="width: 428px;">加入集群时为节点打标签，示例env=test</td>
</tr>
<tr>
<td style="width: 419px;">--pod-infra-container-image</td>
<td style="width: 428px;">每个 pod 中的 network/ipc 命名空间容器将使用的pause镜像，默认k8s.gcr.io/pause:3.1</td>
</tr>
<tr>
<td style="width: 419px;">--pod-manifest-path</td>
<td style="width: 428px;">静态启动的容器组（主要针对控制平面）的路径，默认etc/kubernetes/manifests</td>
</tr>
<tr>
<td style="width: 419px;">--register-with-taints</td>
<td style="width: 428px;">加入集群时自带的taint，示例：env=test:NoSchedule</td>
</tr>
<tr>
<td style="width: 419px;">--root-dir</td>
<td style="width: 428px;">kubelet 的工作目录</td>
</tr>
<tr>
<td style="width: 419px;">--registry-burst=10</td>
<td style="width: 428px;">拉取镜像的最大并发数，允许同时拉取的镜像数，不能超过 registry-qps ，仅当 --registry-qps 大于 0 时使用，默认 10</td>
</tr>
<tr>
<td style="width: 419px;">--serialize-image-pulls</td>
<td style="width: 428px;">是否禁用串行化（一个个）拉取镜像模式</td>
</tr>
<tr>
<td style="width: 419px;">--stderrthreshold</td>
<td style="width: 428px;">日志输出阈值 </td>
</tr>
<tr>
<td style="width: 419px;">--system-reserved</td>
<td style="width: 428px;">给系统预留资源，示例cpu=4,memory=5Gi</td>
</tr>
<tr>
<td style="width: 419px;">--tls-cert-file</td>
<td style="width: 428px;">
<p>用于 https 服务的 x509 证书的文件。如果没有提供 --tls-cert-file 和 --tls-private-key-file ， 将会生产一个自签名的证书及密钥给公开地址使用，并将其保存在 --cert-dir 指定的目录
<p>默认/etc/kubernetes/pki/kubelet.crt</p>
</td>
</tr>
<tr>
<td style="width: 419px;">--tls-private-key-file</td>
<td style="width: 428px;">包含 x509 私钥匹配的文件，默认/etc/kubernetes/pki/kubelet.key</td>
</tr>
<tr>
<td style="width: 419px;">--v</td>
<td style="width: 428px;">日志级别</td>
</tr>
<tr>
<td style="width: 419px;">
<p>--address</p>
</td>
<td style="width: 428px;">
<p>服务监听的IP地址，默认 0.0.0.0</p>
</td>
</tr>
</tbody>
</table>
<p>推荐参数：</p>
<pre class="crayon-plain-tag">--serialize-image-pulls=false
--image-pull-progress-deadline 5m0s
--image-gc-high-threshold=75
--image-gc-low-threshold=60 </pre>
<div class="blog_h2"><span class="graybg">kube-controller-manager</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">参数</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><strong><em>调试参数</em></strong></td>
</tr>
<tr>
<td>--contention-profiling</td>
<td>如果启用了 profiling，则启用锁争用性分析</td>
</tr>
<tr>
<td>--profiling</td>
<td>开启profilling，通过web接口host:port/debug/pprof/分析性能</td>
</tr>
<tr>
<td colspan="2"><strong><em>一般参数</em></strong></td>
</tr>
<tr>
<td>--allocate-node-cidrs</td>
<td>是否应在云提供商上分配和设置Pod的CIDR</td>
</tr>
<tr>
<td>--cidr-allocator-type string</td>
<td>CIDR分配器的类型，默认RangeAllocator</td>
</tr>
<tr>
<td>--cloud-config string</td>
<td>云提供商配置文件路径，空代表没有配置文件</td>
</tr>
<tr>
<td>--cloud-provider string</td>
<td>云提供商，空代表没有云提供商</td>
</tr>
<tr>
<td>--configure-cloud-routes</td>
<td>是否在云提供商上配置allocate-node-cidrs分配的CIDR，默认true</td>
</tr>
<tr>
<td>--cluster-cidr string</td>
<td>集群中Pod的CIDR范围，要求--allocate-node-cidrs为true</td>
</tr>
<tr>
<td>--cluster-name string</td>
<td>集群的实例前缀，默认kubernetes</td>
</tr>
<tr>
<td>--controller-start-interval</td>
<td>启动控制器管理器的间隔时间</td>
</tr>
<tr>
<td>--controllers</td>
<td>
<p>需要开启的控制器列表，默认*表示全部开启，foo表示开启foo，-foo表示禁用foo</p>
<p>可用控制器列表：</p>
<p>attachdetach, bootstrapsigner, clusterrole-aggregation,cronjob, csrapproving, csrcleaner, csrsigning, daemonset,deployment, disruption, endpoint, garbagecollector,horizontalpodautoscaling, job, namespace, nodeipam, nodelifecycle,persistentvolume-binder, persistentvolume-expander, podgc, pv-protection,pvc-protection, replicaset, replicationcontroller,resourcequota, route, service, serviceaccount, serviceaccount-token,statefulset, tokencleaner, ttl,ttl-after-finished</p>
</td>
</tr>
<tr>
<td>--feature-gates</td>
<td>开启特性</td>
</tr>
<tr>
<td>--kube-api-burst</td>
<td>发送到kube-apiserver每秒请求爆发量，默认100</td>
</tr>
<tr>
<td>--kube-api-content-type</td>
<td>发送到kube-apiserver的MIME类型，默认application/vnd.kubernetes.protobuf</td>
</tr>
<tr>
<td>--kube-api-qps</td>
<td>
<p>与kube-apiserver通信的qps，默认50</p>
</td>
</tr>
<tr>
<td>--leader-elect</td>
<td>多个master情况设置为true保证高可用，进行leader选举</td>
</tr>
<tr>
<td>--leader-elect-lease-duration</td>
<td>当leader-elect设置为true生效，选举过程中非leader候选等待选举的时间间隔，默认15s</td>
</tr>
<tr>
<td>--leader-elect-renew-deadline</td>
<td>当leader-elect设置为true生效，leader选举过程中在停止leading，再次renew的时间间隔，小于或者等于leader-elect-lease-duration，默认10s</td>
</tr>
<tr>
<td>--leader-elect-retry-period</td>
<td>当leader-elect设置为true生效，leader或者重新选举的等待间隔，默认2s</td>
</tr>
<tr>
<td>--min-resync-period</td>
<td>获取K8S资源的重新同步周期，默认12h0m0s</td>
</tr>
<tr>
<td>--route-reconciliation-period</td>
<td>协调由云提供商为节点创建的路由的时间间隔，默认10s</td>
</tr>
<tr>
<td>--use-service-account-credentials</td>
<td>
<p>是否为每个控制器设置独立的SA。如果设置为true，则Kubernetes为控制器管理器中的每个控制器创建SA：</p>
<p style="padding-left: 30px;">attachdetach-controller<br />calico-kube-controller<br />certificate-controller<br />clusterrole-aggregation-controller<br />cronjob-controller<br />daemon-set-controller<br />deployment-controller<br />disruption-controller<br />endpoint-controller<br />expand-controller<br />job-controller<br />namespace-controller<br />node-controller<br />pv-protection-controller<br />pvc-protection-controller<br />replicaset-controller<br />replication-controller<br />resourcequota-controller<br />service-account-controller<br />service-controller<br />statefulset-controller<br />ttl-controller</p>
</td>
</tr>
<tr>
<td colspan="2"><strong><em>安全参数</em></strong></td>
</tr>
<tr>
<td>--bind-address</td>
<td>监听--secure-port端口的IP地</td>
</tr>
<tr>
<td>-cert-dir</td>
<td>TLS证书所在的目录。如果提供了--tls-cert-file和--tls-private-key-file，则将忽略此标志</td>
</tr>
<tr>
<td>--http2-max-streams-per-connection</td>
<td>API Server提供给 client 的HTTP / 2最大 stream 连接数</td>
</tr>
<tr>
<td>--secure-port</td>
<td>使用身份验证和授权提供服务的HTTPS端口</td>
</tr>
<tr>
<td>--tls-cert-file</td>
<td>文件包含HTTPS的默认x509证书的文件。如果启用了HTTPS服务，但是 --tls-cert-file和--tls-private-key-file 未设置，则会为公共地址生成自签名证书和密钥，并将其保存到--cert-dir的目录中</td>
</tr>
<tr>
<td>--tls-cipher-suites</td>
<td>逗号分隔的cipher suites列表</td>
</tr>
<tr>
<td>--tls-min-version</td>
<td>支持最低TLS版本，取值VersionTLS10，VersionTLS11，VersionTLS12</td>
</tr>
<tr>
<td>--tls-private-key-file</td>
<td>文件包括与 --tls-cert-file 匹配的默认x509私钥</td>
</tr>
<tr>
<td>--tls-sni-cert-key</td>
<td>x509证书和私钥对的文件路径，例如example.crt,example.key</td>
</tr>
<tr>
<td colspan="2"><strong><em>身份验证</em></strong></td>
</tr>
<tr>
<td>--authentication-kubeconfig</td>
<td>有权创建tokenaccessreviews.authentication.k8s.io的kubeconfig，如果不设置所有Token请求视为匿名</td>
</tr>
<tr>
<td>--authentication-skip-lookup</td>
<td>如果设置false，authentication-kubeconfig用来在集群中查找缺失的authentication配置</td>
</tr>
<tr>
<td>--authentication-token-webhook-cache-ttl</td>
<td>来自webhook token authenticator的响应的缓存时间，默认10s</td>
</tr>
<tr>
<td>--client-ca-file</td>
<td>如果设置，此参数指定的CA列表中任何CA签名的客户端证书，均身份验证成功，并且其身份标识从CommanName读取</td>
</tr>
<tr>
<td colspan="2"><strong><em>授权</em></strong></td>
</tr>
<tr>
<td>--authorization-always-allow-paths</td>
<td>无需授权即可访问的URL列表，默认/healthz</td>
</tr>
<tr>
<td>--authorization-kubeconfig</td>
<td>有权创建subjectaccessreviews.authorization.k8s.io的kubeconfig</td>
</tr>
<tr>
<td>--authorization-webhook-cache-authorized-ttl</td>
<td>来自webhook authorizer的针对授权URL的响应的缓存时间，默认10s</td>
</tr>
<tr>
<td>--authorization-webhook-cache-unauthorized-ttl</td>
<td>来自webhook authorizer的针对非授权URL的响应的缓存时间，默认10s </td>
</tr>
<tr>
<td colspan="2"><strong><em>节点控制器</em></strong></td>
</tr>
<tr>
<td>--node-monitor-period </td>
<td>NodeController同步NodeStatus的时间间隔，默认5s</td>
</tr>
<tr>
<td colspan="2"><strong><em>服务控制器</em></strong></td>
</tr>
<tr>
<td>--concurrent-service-syncs</td>
<td>允许同时同步的 service 数量。 数字越大服务管理响应越快，但消耗更多 CPU 和网络资源</td>
</tr>
<tr>
<td colspan="2"><strong><em>Attachdetach控制器</em></strong></td>
</tr>
<tr>
<td>--attach-detach-reconcile-sync-period </td>
<td>卷attach、detach之间，Reconciler同步的等待时间，必须大于1m</td>
</tr>
<tr>
<td> --disable-attach-detach-reconcile-sync</td>
<td>禁用卷attach/detach的Reconciler同步，禁用可能导致卷和Pod不匹配，小心 </td>
</tr>
<tr>
<td colspan="2"><strong><em>证书请求（CSR）签名控制器</em></strong></td>
</tr>
<tr>
<td>--cluster-signing-cert-file</td>
<td>PEM编码的X509 CA证书，用于签发集群范围的证书，默认/etc/kubernetes/ca/ca.pem</td>
</tr>
<tr>
<td>--cluster-signing-key-file</td>
<td>上述CA证书的私钥路径，默认/etc/kubernetes/ca/ca.key</td>
</tr>
<tr>
<td colspan="2"><strong><em>Deployment控制器</em></strong></td>
</tr>
<tr>
<td>--concurrent-deployment-syncs</td>
<td>允许并行Sync的Deployment对象数量，默认5</td>
</tr>
<tr>
<td>--deployment-controller-sync-period</td>
<td>同步K8S资源的间隔，默认30s</td>
</tr>
<tr>
<td colspan="2"><strong><em>Endpoint控制器</em></strong></td>
</tr>
<tr>
<td>--concurrent-endpoint-syncs</td>
<td>允许并行Sync的Endpoint对象数量，默认5</td>
</tr>
<tr>
<td colspan="2"><strong><em>Garbagecollector控制器</em></strong></td>
</tr>
<tr>
<td>--concurrent-gc-syncs</td>
<td>运行并行Sync的垃圾回收器Worker的数量，默认20</td>
</tr>
<tr>
<td>--enable-garbage-collector</td>
<td>是否启用垃圾回收器，必须和APIServer的参数匹配</td>
</tr>
<tr>
<td colspan="2"><strong><em>HPA控制器</em></strong></td>
</tr>
<tr>
<td>--horizontal-pod-autoscaler-cpu-initialization-period</td>
<td>Pod启动后，多长时间内不进行CPU采样，默认5m0s</td>
</tr>
<tr>
<td>--horizontal-pod-autoscaler-downscale-stabilization</td>
<td>扩容后，多长时间内不会所容，默认5m0s</td>
</tr>
<tr>
<td>--horizontal-pod-autoscaler-initial-readiness-delay</td>
<td>Pod启动后，多长时间内readiness的变更被看作最初的readiness（也就是Pod可以提供服务了），默认30s</td>
</tr>
<tr>
<td>--horizontal-pod-autoscaler-sync-period</td>
<td>多长时间来同步Pod的数量信息，默认15s</td>
</tr>
<tr>
<td>--horizontal-pod-autoscaler-tolerance</td>
<td>
<p>引发HPA考虑应当扩容的，监控指标的变化最小量，默认0.1</p>
</td>
</tr>
<tr>
<td colspan="2"><strong><em>Namespace控制器</em></strong></td>
</tr>
<tr>
<td>--concurrent-namespace-syncs</td>
<td>允许并行Sync的命名空间对象的数量，默认10</td>
</tr>
<tr>
<td>--namespace-sync-period</td>
<td>命名空间Sync间隔</td>
</tr>
<tr>
<td colspan="2"><strong><em>Nodeipam控制器</em></strong></td>
</tr>
<tr>
<td>--node-cidr-mask-size</td>
<td>Node的CIDR的掩码大小，默认24</td>
</tr>
<tr>
<td>--service-cluster-ip-range</td>
<td>K8S服务的CIDR范围</td>
</tr>
<tr>
<td colspan="2"><strong><em>Nodelifecycle控制器</em></strong></td>
</tr>
<tr>
<td>--enable-taint-manager</td>
<td>设置为true，则启用NoExecute Taint，并且驱除所有不能容忍Taint的Pod，默认true</td>
</tr>
<tr>
<td>--large-cluster-size-threshold</td>
<td>多少节点的集群被认为是大集群，默认50。此控制器的逻辑会改变</td>
</tr>
<tr>
<td>--node-eviction-rate</td>
<td>当可用区（Zone）是健康的前提下，节点失败后需要驱除其上的Pod，那么每秒由多少节点上的Pod被删除，默认0.1</td>
</tr>
<tr>
<td>--secondary-node-eviction-rate</td>
<td>类似上面，但是集群被认为是小集群时自动置0</td>
</tr>
<tr>
<td>--node-monitor-grace-period</td>
<td>节点不响应心跳后，至少等待多久才能将其标记为不健康。必须是Kubelet的nodeStatusUpdateFrequency参数的N倍，默认40s</td>
</tr>
<tr>
<td>--node-startup-grace-period</td>
<td>对于一个启动中的节点，在此时间之前不得标记为不健康，默认1m0s</td>
</tr>
<tr>
<td>--pod-eviction-timeout</td>
<td>在失败节点上删除Pod的等待期，默认5m0s</td>
</tr>
<tr>
<td>--unhealthy-zone-threshold</td>
<td>什么比例的Node处于NotReady状态，才认为整个Zone是不健康的</td>
</tr>
<tr>
<td colspan="2"><strong><em>Persistentvolume-binder控制器</em></strong></td>
</tr>
<tr>
<td>--enable-dynamic-provisioning</td>
<td>启用卷的动态提供，默认true</td>
</tr>
<tr>
<td>--enable-hostpath-provisioner</td>
<td>启用HostPath PV的动态提供，主要用在非云环境，用于开发、测试目的</td>
</tr>
<tr>
<td>--flex-volume-plugin-dir</td>
<td>
<p>从什么目录来搜索额外的第三方Flex卷插件</p>
<p>默认/usr/libexec/kubernetes/kubelet-plugins/volume/exec/</p>
</td>
</tr>
<tr>
<td>--pv-recycler-increment-timeout-nfs</td>
<td>每Gi容量为NFS Scrubber Pod的ActiveDeadlineSeconds增加多少时间，默认30</td>
</tr>
<tr>
<td>--pv-recycler-minimum-timeout-hostpath </td>
<td>HostPath Recycler Pod的最小ActiveDeadlineSeconds </td>
</tr>
<tr>
<td>--pv-recycler-minimum-timeout-nfs </td>
<td>NFS Recycler Pod的最小ActiveDeadlineSeconds</td>
</tr>
<tr>
<td>--pv-recycler-pod-template-filepath-nfs</td>
<td>NFS Recycler Pod的模板 </td>
</tr>
<tr>
<td> --pvclaimbinder-sync-period</td>
<td>同步PV/PVC的间隔，默认15s </td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">其它</span></div>
<div class="blog_h2"><span class="graybg">JSONPath支持</span></div>
<p>除了<a href="/javascript-faq#jsonpath">标准的JSONPath</a>之外，Kubernetes还额外支持：</p>
<ol>
<li>可选的<pre class="crayon-plain-tag">$</pre>运算符，默认表达式从根对象开始</li>
<li>支持通过<pre class="crayon-plain-tag">""</pre>引用JSONPath表达式中的文本</li>
<li>支持通过<pre class="crayon-plain-tag">range</pre>迭代列表</li>
</ol>
<div class="blog_h3"><span class="graybg">函数列表</span></div>
<p>对于输入：</p>
<pre class="crayon-plain-tag">{
  "kind": "List",
  "items":[
    {
      "kind":"None",
      "metadata":{"name":"127.0.0.1"},
      "status":{
        "capacity":{"cpu":"4"},
        "addresses":[{"type": "LegacyHostIP", "address":"127.0.0.1"}]
      }
    },
    {
      "kind":"None",
      "metadata":{"name":"127.0.0.2"},
      "status":{
        "capacity":{"cpu":"8"},
        "addresses":[
          {"type": "LegacyHostIP", "address":"127.0.0.2"},
          {"type": "another", "address":"127.0.0.3"}
        ]
      }
    }
  ],
  "users":[
    {
      "name": "myself",
      "user": {}
    },
    {
      "name": "e2e",
      "user": {"username": "admin", "password": "secret"}
    }
  ]
}</pre>
<p> 可用的函数，以及示例如下：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 120px; text-align: center;">函数</td>
<td style="width: 200px; text-align: center;">说明</td>
<td style="text-align: center;">示例</td>
</tr>
</thead>
<tbody>
<tr>
<td>text</td>
<td>原始文本</td>
<td><pre class="crayon-plain-tag">kind is {.kind}</pre>，结果：kind is List</td>
</tr>
<tr>
<td>@</td>
<td>当前对象</td>
<td><pre class="crayon-plain-tag">{@}</pre>输出和输入一致</td>
</tr>
<tr>
<td>. 或 []</td>
<td>取子对象</td>
<td><pre class="crayon-plain-tag">{.kind}</pre>, <pre class="crayon-plain-tag">{['kind']}</pre>，<pre class="crayon-plain-tag">{['name\.type']}</pre></td>
</tr>
<tr>
<td>..</td>
<td>递归子对象</td>
<td><pre class="crayon-plain-tag">{..name}</pre>，结果：127.0.0.1 127.0.0.2 myself e2e</td>
</tr>
<tr>
<td>*</td>
<td>通配，获取所有对象</td>
<td><pre class="crayon-plain-tag">{.items[*].metadata.name}</pre>，结果： [127.0.0.1 127.0.0.2]</td>
</tr>
<tr>
<td>[start:end :step]</td>
<td>切片</td>
<td><pre class="crayon-plain-tag">{.users[0].name}</pre>，结果：myself</td>
</tr>
<tr>
<td>[,]</td>
<td>联合</td>
<td><pre class="crayon-plain-tag">{.items[*]['metadata.name', 'status.capacity']}</pre>，结果：127.0.0.1 127.0.0.2 map[cpu:4] map[cpu:8]</td>
</tr>
<tr>
<td>?()</td>
<td>过滤</td>
<td><pre class="crayon-plain-tag">{.users[?(@.name=="e2e")].user.password}</pre>，结果：secret</td>
</tr>
<tr>
<td>range, end</td>
<td>迭代列表</td>
<td><pre class="crayon-plain-tag">{range .items[*]}[{.metadata.name}, {.status.capacity}] {end}</pre>，结果：[127.0.0.1, map[cpu:4]] [127.0.0.2, map[cpu:8]]</td>
</tr>
<tr>
<td>''</td>
<td>因为转义字符</td>
<td><pre class="crayon-plain-tag">{range .items[*]}{.metadata.name}{'\t'}{end}</pre>，结果：127.0.0.1 127.0.0.2</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">大规模集群调优</span></div>
<div class="blog_h2"><span class="graybg">APIServer</span></div>
<div class="blog_h3"><span class="graybg">监控</span></div>
<p>在大规模集群中，对APIServer的监控需要调优，避免过于频繁的数据采集。特别是APIServer副本数很多的情况下，这会对Etcd造成巨大的读压力。</p>
<div class="blog_h3"><span class="graybg">NodeLease</span></div>
<p>在大规模集群下开启NodeLease来降低API Server处理心跳的成本，可以降低50% CPU开销。</p>
<div class="blog_h2"><span class="graybg">Etcd</span></div>
<div class="blog_h3"><span class="graybg">基础设施调优</span></div>
<p>Etcd是Kubernetes集群正常运作的基石，但是多节点、强一致性的需求导致Etcd很容易出现性能瓶颈。</p>
<p>在云环境中，即使是规模仅有<span style="background-color: #c0c0c0;"><strong>500节点</strong></span>的集群中，使用<strong><span style="background-color: #c0c0c0;">基于SSD的存储 + 万兆网络</span></strong>，也可能随机出现Etcd响应延迟过大的情况，<span style="background-color: #c0c0c0;"><strong>延迟可达500ms</strong></span>。此问题的根本原因是Etcd存在大量<strong><span style="background-color: #c0c0c0;">顺序IO</span></strong>，对<strong><span style="background-color: #c0c0c0;">磁盘延迟敏感，而不是IOPS</span></strong>。使用本地SSD而非网络存储，可以将Etcd的<span style="background-color: #c0c0c0;"><strong>写延迟降低10倍以上</strong></span>。</p>
<div class="blog_h3"><span class="graybg">参数调优</span></div>
<ol>
<li>通过参数<pre class="crayon-plain-tag">-quota-backend-bytes</pre>调整Etcd最大存储大小，默认2GB</li>
<li>根据网络延迟来调整心跳间隔、选举超时</li>
<li>提升Etcd的网络、磁盘IO优先级</li>
</ol>
<div class="blog_h3"><span class="graybg">Etcd垂直分片</span></div>
<p>将相对次要，且产生数据量大的资源类型，存储到单独的Etcd集群中，例如Events：</p>
<pre class="crayon-plain-tag">--etcd-servers-overrides=/events#https://0.example.com:2381;https://1.example.com:2381;https://2.example.com:2381</pre>
<div class="blog_h3"><span class="graybg">Boltdb优化</span></div>
<p>优化Etcd底层存储Bolt的B+树的空闲列表分配/释放算法的时间复杂度为O(1)，解决超大数据量（100GB级别）下Etcd的性能瓶颈，操作延迟可以降低10倍。</p>
<div class="blog_h2"><span class="graybg">Scheduler</span></div>
<div class="blog_h3"><span class="graybg">策略调优</span></div>
<ol>
<li>根据应用场景调整：默认策略倾向于均摊负载到所有节点。对于批处理应用场景，可以修改调度策略，让负载集中，并释放空闲节点以节约资源</li>
<li>扩展调度器，支持基于负载IO需求、节点IO能力的调度</li>
<li>预选阶段优化：
<ol>
<li>根据集群规模调整参与可调度评分的节点比例，对于大规模集群（1000+），调低参数可以提升40%以上的调度性能：<br />
<pre class="crayon-plain-tag">apiVersion: componentconfig/v1alpha1
kind: KubeSchedulerConfiguration
algorithmSource:
  provider: DefaultProvider
percentageOfNodesToScore: 50</pre>
</li>
<li>预选短路，优先执行低成本的断言，并且一个断言失败即排除节点</li>
</ol>
</li>
</ol>
<div class="blog_h2"><span class="graybg">DNS优化</span></div>
<div class="blog_h3"><span class="graybg">KubeDNS</span></div>
<p>每个节点上包含多个KubeDNS Pod没有意义，可以设置反亲和：</p>
<pre class="crayon-plain-tag">affinity:
 podAntiAffinity:
   requiredDuringSchedulingIgnoredDuringExecution:
   - weight: 100
     labelSelector:
       matchExpressions:
       - key: k8s-app
         operator: In
         values:
         - kube-dns
     topologyKey: kubernetes.io/hostname</pre>
<div class="blog_h3"><span class="graybg">ARP缓存</span></div>
<p>在高性能计算集群中，可能需要增大节点的ARP缓存：</p>
<pre class="crayon-plain-tag">net.ipv4.neigh.default.gc_thresh1 = 80000
net.ipv4.neigh.default.gc_thresh2 = 90000
net.ipv4.neigh.default.gc_thresh3 = 100000</pre>
<p>否则，在内核日志中可能看到neighbor table overflow!错误。 </p>
<div class="blog_h3"><span class="graybg">CoreDNS优化</span></div>
<p>引入autopath插件，可以将针对外部DNS名称的查询QPS提升5倍。</p>
<div class="blog_h2"><span class="graybg">镜像拉取</span></div>
<div class="blog_h3"><span class="graybg">并行拉取</span></div>
<p>设置Kubelet参数<pre class="crayon-plain-tag">--serialize-image-pulls=false</pre>， 注意需要配合设置Docker的存储驱动为Overlay2，提高<pre class="crayon-plain-tag">--max-concurrent-downloads</pre>的值。</p>
<p>最好将Docker数据目录挂载到本地磁盘。</p>
<div class="blog_h3"><span class="graybg">拉取超时</span></div>
<p>设置Kubelet参数<pre class="crayon-plain-tag">--image-pull-progress-deadline=30m</pre>，来支持超大镜像的拉取，避免rpc error: code = 2 desc = net/http: request canceled错误</p>
<div class="blog_h1"><span class="graybg">关键新特性</span></div>
<div class="blog_h2"><span class="graybg">1.13</span></div>
<ol>
<li>kubeadm达到GA</li>
<li>CSI达到GA</li>
<li>CoreDNS作为默认DNS实现</li>
<li>支持第三方设备监控插件，Alpha</li>
<li>设备插件注册，达到稳定状态。此特性实现了一个通用的kubelet插件发现模型，用于各种节点级别的插件，例如设备插件、CSI、CNI和kubelet的交互</li>
<li><span style="background-color: #c0c0c0;">拓扑感知的卷调度</span>，达到稳定状态</li>
<li>APIServer DryRun达到Beta状态</li>
<li>Kubectl Diff达到Beta状态，用于比较本地资源清单和K8S中对象的区别</li>
</ol>
<div class="blog_h2"><span class="graybg">1.14</span></div>
<ol>
<li>Windows支持达到生产可用，现在可以支持Windows工作节点，并向其调度Windows容器</li>
<li>Kubectl插件机制达到稳定状态</li>
<li>继承Kustomize到Kubectl</li>
<li>本地PV达到生产可用</li>
<li><span style="background-color: #c0c0c0;">Pod Ready++生产可用</span>，此特性改进Readiness/Liveness探针的不足—— 在一些情况下，往往只是新的 Pod 完成自身初始化，系统尚未完成 Endpoint、负载均衡器等外部可达的访问信息刷新，老的 Pod 就立即被删除，最终造成服务不可用，允许用户通过ReadinessGates自定义Pod就绪条件</li>
<li><span style="background-color: #c0c0c0;">Pod优先级和抢占式调度达到生产可用</span></li>
<li>PID数量限制特性，达到Beta，使用kubelet参数<pre class="crayon-plain-tag">pod-max-pids</pre>开启</li>
<li>Kubeadm支持在HA集群中自动执行控制平面的证书复制</li>
</ol>
<div class="blog_h2"><span class="graybg">1.15</span></div>
<ol>
<li>CRD进入GA，支持基于Structural Schema校验自定义资源的每个字段</li>
<li>支持在运行时进行CR不同版本之间的转换，通过Webhook</li>
<li>CRD默认值，Alpha</li>
<li>CRD未知字段裁剪，通过CRD的<pre class="crayon-plain-tag">spec.preserveUnknownFields: false</pre>启用</li>
<li>Admission Webhook重新调用，如果某个Webhook修改了资源，可以重新调用Webhook进行校验</li>
<li>通过<span style="background-color: #c0c0c0;">Device Monitoring Agent进行异构硬件监控</span>，不需要修改Kubelet</li>
<li><span style="background-color: #c0c0c0;">Scheduler Framework</span>，Alpha， 加强调度器扩展定制能力。在原有的Priority/Predicates 接口的基础上增加了QueueSort, Prebind, Postbind, Reserve, Unreserve 和 Permit接口。Scheduler Framework 仍以 Pod 为单位进行调度，对于需要<span style="background-color: #c0c0c0;">成组Pod一起调度的离线类（计算）业务，考虑使用Volcano之类的社区框架</span></li>
<li>Kubeadm HA达到GA</li>
</ol>
<div class="blog_h2"><span class="graybg">1.16</span></div>
<ol>
<li>Admission Webhook达到GA</li>
<li>存储方面的改进，<span style="background-color: #c0c0c0;">CSI的卷Resize</span>的API达到Beta</li>
<li>Kubeadm支持添加Windows工作节点，Alpha</li>
<li>Windows节点的CSI插件支持，Alpha</li>
<li>能够支持更加强大的集群扩容、更灵活的网络地址处理的网络端点切片（Endpoint Slices）特性。EndpointSlice可以作为Endpoint的替代</li>
<li><span style="background-color: #c0c0c0;">Ephemeral containers</span>：在一个运行中的Pod中，临时运行一个容器，用于诊断目的</li>
</ol>
<div class="blog_h2"><span class="graybg">1.17</span></div>
<ol>
<li>Pod共享PID命名空间达到GA，可用于容器之间需要相互发信号的场景</li>
<li><span style="background-color: #c0c0c0;">拓扑感知服务路由</span>，可以避免跨物理位置访问Service的Endpoint，Alpha状态</li>
<li>Endpoints对象存放了服务的所有端点，1.17的EndpointSlice代替之，可扩容性更好。<span style="background-color: #c0c0c0;">拓扑感知路由也依赖于EndpointSlice</span></li>
<li>可以在一个集群中同时支持IPv4和IPv6</li>
<li>CSI：<span style="background-color: #c0c0c0;">卷快照API到达Beta状态，支持拓扑感知</span></li>
</ol>
<div class="blog_h2"><span class="graybg">1.18</span></div>
<ol>
<li>为Service Account Token Issuer提供OpenID Connect (OIDC)发现。SA可以基于Token（JWT）来请求K8S API Server，进行身份验证。目前K8S API Server是唯一能验证Token的服务，但是它不能从外部网络访问，这意味着某些工作负载需要使用单独的身份验证系统。OIDC发现可以让K8S的Token验证在集群外可用，实现方式是<span style="background-color: #c0c0c0;">API Server提供OIDC发现文档（其中包含Token公钥），OIDC Authenticator可以基于此OIDC发现文档校验Token</span></li>
<li>支持定制单个HPA的扩容速度，以前的版本仅仅支持全局设置。为HPA添加以下设置：<br />
<pre class="crayon-plain-tag">behavior:
   scaleUp:
     policies:
     - type: Percent
       value: 100
       periodSeconds: 15
   scaleDown:
     policies:
     - type: Pods
       value: 4
       periodSeconds: 60
# 15秒内扩容到4个，60秒内缩容到2个</pre>
</li>
<li>CertificateSigningRequest接口，支持由K8S对证书请求进行签名</li>
<li>kubectl debug，用于在运行中的Pod上进行调试，它可以创建一个Debug容器，或者使用新配置重新部署Pod</li>
<li>Windows增强：
<ol>
<li>实现RuntimeClass</li>
<li>支持CRI-ContainerD</li>
</ol>
</li>
<li>调度相关
<ol>
<li>支持多个调度Profile。不同工作负载的调度需求不同，对于Web服务，倾向于分散到不同节点上，对于某些延迟敏感的服务，则倾向于调度在同一节点上。在以前，可以配置多个调度器来满足差异化调度，但是这会引入竞态条件，因为每个调度器在同一时刻看到不同的集群视图。调度Profile允许同一调度器以多种配置运行，每个配置有自己的schedulerName，Pod仍然使用schedulerName来指定使用哪个Profile</li>
<li>Beta：跨越多个故障域平均分布Pod，使用topologySpreadConstraints，可以定义如何在跨越区域（Zone）的大规模集群上平均调度Pod</li>
<li>Alpha：跨越多个故障域平均分布Pod，支持配置默认规则</li>
</ol>
</li>
<li>节点相关
<ol>
<li>Hugepage支持，硬件优化会让大块内存访问更加快速，对于在内存中处理大量数据集的应用（例如数据库）、以及内存延迟敏感应用很有价值。现在Pod可以请求不同尺寸的Hugepage：<br />
<pre class="crayon-plain-tag">apiVersion: v1
kind: Pod
…
spec:
  containers:
…
    resources:
      requests:
        hugepages-2Mi: 1Gi
        hugepages-1Gi: 2Gi </pre>
</li>
<li>统计Pod沙盒消耗的（而不是关联到特定容器）资源。启用PodOverhead特性门，则调度时考虑Pod沙盒的资源消耗，资源消耗在Admission期间计算并固化，其值和RuntimeClass有关</li>
<li>节点拓扑管理器（Node Topology Manager）是Kubelet的组件，负责专门协调硬件资源的分配。机器学习、科学计算、财务服务等都是计算密集型、低延迟要求的应用，这类应用锁定到特定的核心上会得到更好的性能，NTM能够执行这种锁定</li>
<li>延迟liveness探针执行：<br />
<pre class="crayon-plain-tag">startupProbe:
  httpGet:
    path: /healthz
    port: liveness-port
  failureThreshold: 30
  periodSeconds: 10</pre></p>
<p>仅当此探针成功，才认为Pod启动完毕，然后才会执行其它探针，例如liveness</p>
</li>
</ol>
</li>
<li>网络
<ol>
<li> NodeLocal DNSCache到达GA，此特性增强集群DNS性能，避免DNAT和conntrack导致的和UDP相关的竞态条件</li>
</ol>
</li>
<li>存储
<ol>
<li>跳过卷所有权变更，在bind挂载卷到容器之前，它的文件权限会被改为fsGroup设置的值。如果卷很大，这个处理过程会很慢，而且这种修改会导致某些权限敏感的应用出问题，例如数据库。新增的FSGroupChangePolicy字段可以控制此行为，如果设置为Always行为依旧，如果设置为OnRootMismatch，则仅当卷的根目录的权限不匹配fsGroup时才设置权限</li>
<li>不可变ConfigMap和Secret：<br />
<pre class="crayon-plain-tag">apiVersion: v1
kind: Secret
metadata:
  …
data:
  …
immutable: true</pre>
</li>
<li>
<p>跳过non-attachable卷的Attach行为。这是个内部优化，简化了 不需要Attach/Detach操作的CSI驱动（例如NFS）的VolumeAttachment对象的创建</p>
</li>
<li>支持<span style="background-color: #c0c0c0;">访问原始（Raw）块存储</span>，BlockVolume到达GA。要访问原始卷（跳过文件系统，提升IO性能降低延迟），将volumeMode设置为block</li>
</ol>
</li>
</ol>
<div class="blog_h1"><span class="graybg">Kubernetes开发</span></div>
<div class="blog_h2"><span class="graybg">构建</span></div>
<div class="blog_h3"><span class="graybg">本地构建</span></div>
<pre class="crayon-plain-tag"># 交叉编译，需要先安装交叉编译器
sudo apt-get install gcc-5-aarch64-linux-gnu
sudo apt-get install gcc-aarch64-linux-gnu


# 签出代码
mkdir -p $GOPATH/src/k8s.io
cd $GOPATH/src/k8s.io
git clone https://github.com/kubernetes/kubernetes
cd kubernetes


# 构建二进制文件
#    构建kubelet      设置版本号为v1.18.3       去除版本号的dirty
make WHAT=cmd/kubelet KUBE_GIT_VERSION=v1.18.3 KUBE_GIT_TREE_STATE=clean
     # 为ARM平台构建                   设置GOFLAGS
     KUBE_BUILD_PLATFORMS=linux/arm64 GOFLAGS="-tags=nokmem"


#                   打印帮助
make release-images PRINT_HELP=y


# 跨平台编译
make cross

# 校验，确保代码格式化完毕、Bazel依赖更新完毕
make verify

# 执行所有更新脚本
make update

# 执行单元测试
make test
make test WHAT=./pkg/api/helper GOFLAGS=-v

# 执行集成测试
make test-integration



# release-skip-tests quick-release 只是默认参数不同

# 生成镜像                不跳过测试                 镜像前缀                                   
make release-skip-tests  KUBE_RELEASE_RUN_TESTS=y KUBE_DOCKER_REGISTRY=docker.gmem.cc/tcnp
    # Base镜像前缀
    KUBE_BASE_IMAGE_REGISTRY=registry.aliyuncs.com/google_containers
    # 不要总是尝试拉取镜像，某些镜像在国内拉取不到   打印更多的日志信息
    KUBE_BUILD_PULL_LATEST_IMAGES=n              KUBE_VERBOSE=5

# 生成镜像           交叉编译其它平台
make quick-release  KUBE_FASTBUILD=false</pre>
<div class="blog_h3"><span class="graybg">在容器中构建</span></div>
<pre class="crayon-plain-tag"># 仅编译linux二进制程序
build/run.sh make 
# 编译跨平台二进制程序
build/run.sh make cross
# 指定组件与平台编译
build/run.sh make kubectl KUBE_BUILD_PLATFORMS=darwin/amd64
# 运行所有单元测试用例
build/run.sh make test 
# 运行集成测试用例
build/run.sh make test-integration 
# 运行命令行测试
build/run.sh make test-cmd 
# 把docker中的 _output/dockerized/bin 拷贝到本地的 _output/dockerized/bin
build/copy-output.sh
# 清理_output目录
build/make-clean.sh 
# 进入bash shell中进行编译
build/shell.sh </pre>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">多网卡机器上报告错误节点IP</span></div>
<p>现象：<pre class="crayon-plain-tag">kubectl get node -o yaml | grep InternalIP -B 1</pre> 返回不期望的IP地址。</p>
<p>原因：具有多个可以相互连接的网络接口。</p>
<p>解决办法：</p>
<pre class="crayon-plain-tag">KUBELET_EXTRA_ARGS="--node-ip=10.0.4.1"</pre>
<p>然后重启kubelet即可。 </p>
<div class="blog_h3"><span class="graybg">NodePort不能使用80</span></div>
<p>报错信息：is invalid: spec.ports[0].nodePort: Invalid value: 80: provided port is not in the valid range. The range of valid ports is 30000-32767 </p>
<p>解决方案：修改Master节点的/etc/kubernetes/manifests/kube-apiserver.yaml，添加参数：</p>
<p style="padding-left: 60px;">--service-node-port-range=80-32767</p>
<div class="blog_h3"><span class="graybg">僵尸Pod问题</span></div>
<p>如果Node宕机或者网络分区，Pod可能进入Unknown状态，因为1.5+版本仅仅当Master确认Pod不在集群中运行时才会重新调度之。</p>
<p>这种情况下，如果作为管理员的你知道Node是宕机了，可以：</p>
<ol>
<li>强制删除<pre class="crayon-plain-tag">kubectl delete pod pod-name --grace-period=0 --force</pre>后重新创建之</li>
</ol>
<div class="blog_h3"><span class="graybg">设置Pod时区</span></div>
<pre class="crayon-plain-tag">volumeMounts:
  - name: tz-config
    # 常见的Linux发行版都从下面的文件中读取时区
    mountPath: /etc/localtime

volumes:
  - name: tz-config
    hostPath:
      # Ubuntu、CentOS等宿主机上都有下面的文件
      path: /usr/share/zoneinfo/Asia/Shanghai</pre>
<div class="blog_h3"><span class="graybg">设置资源预留后无法启动kubelet</span></div>
<p>报错信息：Failed to enforce System Reserved Cgroup Limits on "/system.slice": failed to set supported cgroup subsystems for cgroup /system.slice: Failed to set config for supported subsystems : failed to write 536870912 to memory.limit_in_bytes: write /sys/fs/cgroup/memory/system.slice/memory.limit_in_bytes: device or resource busy</p>
<p>报错原因：设置的限额，比当前system.slice组已经使用的不可压缩资源量（内存）更小，无法执行</p>
<div class="blog_h3"><span class="graybg">使用SubPath的Configmap导致容器重启失败</span></div>
<p>报错信息：</p>
<p>Error: failed to start container "dubbo":<span style="background-color: #c0c0c0;"> Error response from daemon: OCI runtime create failed</span>: container_linux.go:348:<br /> starting container process caused "process_linux.go:402: container init caused \"rootfs_linux.go:58: <span style="background-color: #c0c0c0;">mounting</span> \\\"/var/lib/kubelet/pods/1df7c1e1-d8ef-11e8-8029-3863bb2bccdc/volume-subp<br />aths/tomcatconf/dubbo/3\\\" <span style="background-color: #c0c0c0;">to rootfs</span> \\\"/var/lib/docker/overlay2/2fb619c8c8911576b936b460711d229f85489c82f222918ce79c90013b95e640/merged\\\" at \\\"/var/lib/docker/overlay2/2fb619c8c8<br />911576b936b460711d229f85489c82f222918ce79c90013b95e640/merged/tomcat/conf/server.xml\\\" <span style="background-color: #c0c0c0;">caused</span> \\\"<span style="background-color: #c0c0c0;">no such file or directory</span>\\\"\"": <span style="background-color: #c0c0c0;">unknown </span></p>
<p>原因：<a href="https://github.com/kubernetes/kubernetes/issues/68211">已知的缺陷</a>，当挂载ConfigMap/Secret，使用subPath时，如果ConfigMap被修改（我们的问题场景下貌似没有修改），则容器（而不是Pod）重启（可能由于健康检查失败导致）会失败。</p>
<div class="blog_h3"><span class="graybg">ConfigMap格式混乱</span></div>
<p>现象：换行符被替换为\n，无法阅读</p>
<p>原因：1.12版本中，只有任意一行以空格结尾，就会出现这种情况。</p>
<div class="blog_h3"><span class="graybg">如何重启Pod</span></div>
<p>K8S没有提供重启Pod的功能，Pod只能删除。但是Pod删除后，容器的本地文件系统状态就丢失了。</p>
<p>可以通过杀死进程的方式，让Pod（的容器）重启：</p>
<pre class="crayon-plain-tag">kubectl exec -it ubuntu kill --  1</pre>
<p>注意不要kill -9，原因是PID 1是init进程，即使在PID命名空间中也是这样。<span style="background-color: #c0c0c0;">PID 1不定义KILL的信号处理器，因此无法被SIGKILL杀死</span>。 </p>
<div class="blog_h3"><span class="graybg">因Docker版本无法加入集群</span></div>
<p>报错：unsupported docker version: 18.09.0</p>
<p>解决办法：为kubeadm添加参数<pre class="crayon-plain-tag">--ignore-preflight-errors=SystemVerification</pre></p>
<div class="blog_h2"><span class="graybg">Kubeadm相关</span></div>
<div class="blog_h3"><span class="graybg">为证书添加SAN</span></div>
<p>删除旧证书：</p>
<pre class="crayon-plain-tag">/etc/kubernetes/pki/apiserver.*</pre>
<p>修改/etc/kubernetes/kubeadm-config.yaml，添加额外的SAN。</p>
<p>执行下面的命令生成新的API Server证书：</p>
<pre class="crayon-plain-tag">kubeadm init phase certs apiserver --config /etc/kubernetes/kubeadm-config.yaml</pre>
<div class="blog_h3"><span class="graybg">证书过期处理</span></div>
<p>通过Kubeadm安装的K8S集群，其控制平面证书有效期默认为1年，到期后你会遇到x509: certificate has expired or is not yet valid错误。</p>
<p>解决办法，通过kubeadm命令重新生成证书：</p>
<pre class="crayon-plain-tag">cd /etc/kubernetes/

rm -rf pki/apiserver.* pki/apiserver-kubelet-.* pki/front-proxy-*

# apiserver API Server服务器证书 
# apiserver-etcd-client API Server访问Kubelet使用的证书
# apiserver-kubelet-client 用于API Server连接到kubelet的证书
# ca 根证书
# sa 用于签名Service Account Token的证书
# front-proxy-ca 前置代理的自签名CA证书
# front-proxy-client 前置代理客户端证书
kubeadm alpha phase certs apiserver \
  --apiserver-cert-extra-sans=k8s.gmem.cc,localhost,10.0.2.1,10.0.3.1,127.0.0.1,radon,neon
kubeadm alpha phase certs apiserver-kubelet-clientkubec
kubeadm alpha phase certs sa
kubeadm alpha phase certs front-proxy-ca
kubeadm alpha phase certs front-proxy-client

# 配置文件也需要重新生成
rm -rf *.conf
kubeadm alpha phase kubeconfig all --apiserver-advertise-address=10.0.2.1
cp admin.conf $HOME/.kube/config</pre>
<p>生成的文件中，除了front-proxy-client.crt之外的所有crt文件，都是基于/etc/kubernetes/pki/ca.*进行签名。</p>
<p>用于Service Account Token的签名和校验的sa.pub则仅仅是公钥，这也意味着，<span style="background-color: #c0c0c0;">所有API Server必须共享一致的sa.key/sa.pub</span>。</p>
<p>证书更新后，kube-proxy可能无法访问API Server，报错 Unauthorized，原因是重新生成了sa.*，签名用的密钥变了。其实<span style="background-color: #c0c0c0;">sa.*不需要重新生成，它不牵涉证书过期的问题</span>。清空所有Token，导致其重新自动生成即可解决此问题：</p>
<pre class="crayon-plain-tag">kubectl get secrets  --all-namespaces  --no-headers  | grep service-account-token | awk '{system("kubectl -n "$1" delete secrets "$2)}'</pre>
<p>所有依赖于Service Account Token的Pod（基本上所有控制平面Pod都需要访问API Server）都需要重启：</p>
<pre class="crayon-plain-tag"># 否则，这些Pod访问API Server后，API Server会报错
#     invalid bearer token, [invalid bearer token, square/go-jose: error in cryptographic primitive
kubectl -n kube-system delete pod --all</pre>
<div class="blog_h2"><span class="graybg">Kubelet相关</span></div>
<div class="blog_h3"><span class="graybg">the server could not find the requested resource ( pods/log ...</span></div>
<p>命令：<pre class="crayon-plain-tag">kubectl -n kube-system logs tiller-deploy-79988f9658-874gs -v 9</pre>可以获得更加详细的错误。</p>
<p>logs和exec子资源最终调用kubelet，因此获取其它K8S资源时没有问题，可以考虑是否节点存在问题。</p>
<p>查看节点Kubelet日志，发现：Failed to set some node status fields: failed to validate nodeIP: node IP: "172.17.0.4" not found in the host's network interfaces。原来是Kind集群重启后，IP地址发生变化导致。</p>
<p>Kind的节点IP静态的写在 /var/lib/kubelet/kubeadm-flags.env文件中，可以修改之。</p>
<div class="blog_h3"><span class="graybg">Orphaned pod "***" found, but volume paths are still present on disk</span></div>
<p>到目标节点上，进入/var/lib/kubelet/pods/***目录，查看etc-hosts文件内容，即可得知Pod的名字。</p>
<p>如果Pod已经不存在，可以删除***目录。防止继续出现此报错。</p>
<div class="blog_h3"><span class="graybg">Failed to start cAdvisor inotify_add_watch /sys/fs/cgroup/cpu,cpuacct/system.slice/run-u11867.scope: no space left on device</span></div>
<p>可能原因是内核参数取值太小：<pre class="crayon-plain-tag">sudo sysctl fs.inotify.max_user_watches=1048576</pre></p>
<div class="blog_h2"><span class="graybg">集群管理</span></div>
<div class="blog_h3"><span class="graybg">无法Ping通ClusterIP</span></div>
<p>是的，默认情况下你会无法Ping通集群IP，但是访问服务开放的端口是没问题的。 </p>
<div class="blog_h3"><span class="graybg">DNS无法解析非FQDN</span></div>
<p>使用kubeadm时，自定义集群域名时，出现此现象。</p>
<p>解决办法，使用kubelet参数：--cluster-domain=k8s.gmem.cc</p>
<div class="blog_h2"><span class="graybg">宕机快检</span></div>
<p>默认配置下，对节点宕机的响应速度较慢。可以进行以下调整。</p>
<div class="blog_h3"><span class="graybg">Kubelet</span></div>
<p>默认每10秒上报当前节点的健康状态，可以调低：--node-status-update-frequency=3s</p>
<div class="blog_h3"><span class="graybg">控制器管理器</span></div>
<p>默认每5秒在Master上检测其它节点状态，可以调低：--node-monitor-period=3s</p>
<p>如果发现其它节点不响应了，默认需要过40秒（10 *4）才能认为节点处于Unhealthy状态。可以调低 --node-monitor-grace-period=12s ，取值必须是--node-status-update-frequency的整数倍。</p>
<p>如果节点处于Unhealthy状态，则经过5分钟后控制器管理器才开始清除其上的Pod，可以调低 --pod-eviction-timeout=36s</p>
<p>控制器管理器的配置文件位于/etc/kubernetes/manifests/kube-controller-manager.yaml。</p>
<div class="blog_h2"><span class="graybg">查看日志</span></div>
<div class="blog_h3"><span class="graybg">控制器管理器</span></div>
<pre class="crayon-plain-tag"># 如果以静态Pod方式运行
PODNAME=$(kubectl -n kube-system get pod -l component=kube-controller-manager -o jsonpath='{.items[0].metadata.name}')
kubectl -n kube-system logs $PODNAME --tail 100 -f</pre>
<div class="blog_h3"><span class="graybg">调度器</span></div>
<pre class="crayon-plain-tag"># 如果以静态Pod方式执行
PODNAME=$(kubectl -n kube-system get pod -l component=kube-scheduler -o jsonpath='{.items[0].metadata.name}')
kubectl -n kube-system logs $PODNAME --tail 100 -f</pre>
<div class="blog_h3"><span class="graybg">Kubelet</span></div>
<pre class="crayon-plain-tag">journalctl -u kubelet -f  -p 0..7</pre>
<div class="blog_h3"><span class="graybg">Kubeproxy </span></div>
<pre class="crayon-plain-tag">kubectl -n kube-system get pod  | grep kube-proxy</pre>
<div class="blog_h3"><span class="graybg">容器日志</span></div>
<pre class="crayon-plain-tag">kubectl  -n ns podname containername</pre>
<p>如果只有一个容器，不需要指定containername，否则不指定containername会报错：</p>
<p>Error from server (BadRequest): a container name must be specified for pod maven-cddc1, choose one of: [** **]</p>
<div class="blog_h2"><span class="graybg">Ingress相关</span></div>
<div class="blog_h3"><span class="graybg">直接提供HTTPS的服务</span></div>
<p>Ingress必须添加注解：<pre class="crayon-plain-tag">nginx.ingress.kubernetes.io/secure-backends: "true"</pre></p>
<p>否则：</p>
<ol>
<li>Ingress Controller报错：upstream sent no valid HTTP/1.0 header while reading response header from upstream</li>
<li>Chrome浏览器报错：ERR_SPDY_PROTOCOL_ERROR</li>
</ol>
<div class="blog_h3"><span class="graybg">如何暴露gRPC服务</span></div>
<p>Ingress必须添加注解：<pre class="crayon-plain-tag">nginx.ingress.kubernetes.io/backend-protocol: "GRPC"</pre></p>
<div class="blog_h2"><span class="graybg">构建相关</span></div>
<div class="blog_h3"><span class="graybg">cmd/go: -tags space-separated list contains comma</span></div>
<p>这个问题和Goland有关，去除环境变量：</p>
<pre class="crayon-plain-tag">unset GOFLAGS</pre>
<p>即可解决。</p>
<div class="blog_h2"><span class="graybg">CentOS相关</span></div>
<div class="blog_h3"><span class="graybg">Bitnami镜像无法运行</span></div>
<p>报错：Error executing 'postInstallation': EEXIST: file already exists, symlink '/opt/bitnami/redis/conf' -&gt; '/opt/bitnami/redis/etc'，但是实际上文件根本不存在。</p>
<p>原因：CentOS使用的内核存在缺陷，升级到最新版本内核即可。</p>
<div class="blog_h2"><span class="graybg">Minikube相关</span></div>
<div class="blog_h3"><span class="graybg">Mac下基于VMware Fusion时报SSH错</span></div>
<p>报错信息：handshake failed: ssh: unable to authenticate, no supported methods remain</p>
<p>报错原因： 和VMware Fusion存在兼容性问题，Minikube的docker用户所需要的密钥没有正确拷贝</p>
<p>解决办法：</p>
<ol>
<li>启动Minikube：<br />
<pre class="crayon-plain-tag">minikube start --vm-driver=vmwarefusion -v=10 --kubernetes-version v1.15.0

# 使用代理
export PROXY=http://192.168.72.77:8087
https_proxy=$PROXY minikube start --docker-env HTTP_PROXY=$PROXY \
    --docker-env HTTPS_PROXY=$PROXY --docker-env NO_PROXY=192.168.0.0/16 \
    --vm-driver=vmwarefusion -v=10 --kubernetes-version v1.15.0</pre>
</li>
<li>在另外一个命令行窗口执行：<br />
<pre class="crayon-plain-tag">'/Applications/VMware Fusion.app/Contents/Library/vmrun' -gu docker -gp tcuser \
  createDirectoryInGuest ~/.minikube/machines/minikube/minikube.vmx \
  /home/docker/.ssh

'/Applications/VMware Fusion.app/Contents/Library/vmrun' -gu docker -gp tcuser \
  CopyFileFromHostToGuest ~/.minikube/machines/minikube/minikube.vmx \
  ~/.minikube/machines/minikube/id_rsa.pub /home/docker/.ssh/authorized_keys</pre>
</li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/kubernetes-study-note">Kubernetes学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/kubernetes-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Docker学习笔记</title>
		<link>https://blog.gmem.cc/docker-study-note</link>
		<comments>https://blog.gmem.cc/docker-study-note#comments</comments>
		<pubDate>Tue, 20 Oct 2015 07:10:08 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Virtualization]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[学习笔记]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=13593</guid>
		<description><![CDATA[<p>基础知识 简介 Docker是一个容器化软件，所谓容器化即操作系统级别的虚拟化（Operating-system-level virtualization）。比起硬件虚拟化： 容器更加轻量，它不需要运行独立操作系统，因而减少了磁盘（操作系统文件占用GB+空间）、CPU（进程调度、硬件模拟等额外消耗）等基础资源的消耗，可扩容性更强 容器性能更高，虚拟硬件导致的低性能问题不复存在 容器启动非常迅速（小于1秒），普通VM启动时间可能需要1分钟 更加适合部署松耦合、分布式、弹性的微服务 容器化软件允许在同一个操作系统内核下存在多个相互隔离的用户空间实例，这些实例即被称为容器（Container）。从这些容器的所有者/用户的角度来看，它们就像是一个独立的服务器一样。除了隔离机制之外，容器化软件通常提供资源管理功能，限制一个容器的活动对其它容器的影响。 内核中用于支持容器化的特性： 名字空间机制，用于实现容器的隔离。名字空间包括： pid名字空间，不同空间中的PID可以重复 net名字空间，管理多个网络协议栈的实例 ipc名字空间，管理和访问IPC资源 mnt名字空间，管理文件系统的挂载点 控制组（Cgroups），用于控制容器的资源用量 UnionFS，联合文件系统 构建（Build）、分发（Ship）、运行（Run）是Docker提出的宣传口号，它的目标就是高效的完成这三件事，提高开发、测试、运维的效率： Docker将应用程序和它的运行环境（例如依赖、库）打包到一起，屏蔽不同运行环境的差异，实现可移植部署 Docker让应用程序在一个被隔离的容器中运行，多个应用程序可以依赖相互冲突的库却不相互干扰（尽管它们运行在单个内核中） 下图阐述了Docker和硬件虚拟化的区别： 使用流程 <a class="read-more" href="https://blog.gmem.cc/docker-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/docker-study-note">Docker学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">基础知识</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>Docker是一个容器化软件，所谓容器化即操作系统级别的虚拟化（Operating-system-level virtualization）。比起硬件虚拟化：</p>
<ol>
<li>容器更加轻量，它不需要运行独立操作系统，因而减少了磁盘（操作系统文件占用GB+空间）、CPU（进程调度、硬件模拟等额外消耗）等基础资源的消耗，可扩容性更强</li>
<li>容器性能更高，虚拟硬件导致的低性能问题不复存在</li>
<li>容器启动非常迅速（小于1秒），普通VM启动时间可能需要1分钟</li>
<li>更加适合部署松耦合、分布式、弹性的微服务</li>
</ol>
<p>容器化软件允许在<span style="background-color: #c0c0c0;">同一个操作系统内核下存在多个相互隔离的用户空间实例</span>，这些实例即被称为容器（Container）。从这些容器的所有者/用户的角度来看，它们就像是一个独立的服务器一样。除了隔离机制之外，容器化软件通常提供资源管理功能，限制一个容器的活动对其它容器的影响。</p>
<p>内核中用于支持容器化的特性：</p>
<ol>
<li>名字空间机制，用于实现容器的隔离。名字空间包括：
<ol>
<li>pid名字空间，不同空间中的PID可以重复</li>
<li>net名字空间，管理多个网络协议栈的实例</li>
<li>ipc名字空间，管理和访问IPC资源</li>
<li>mnt名字空间，管理文件系统的挂载点</li>
</ol>
</li>
<li>控制组（Cgroups），用于控制容器的资源用量</li>
<li>UnionFS，联合文件系统</li>
</ol>
<p><span style="background-color: #c0c0c0;">构建（Build）、分发（Ship）、运行（Run）</span>是Docker提出的宣传口号，它的目标就是高效的完成这三件事，提高开发、测试、运维的效率：</p>
<ol>
<li>Docker将应用程序和它的运行环境（例如依赖、库）打包到一起，屏蔽不同运行环境的差异，实现可移植部署</li>
<li>Docker让应用程序在一个被隔离的容器中运行，多个应用程序可以依赖相互冲突的库却不相互干扰（尽管它们运行在单个内核中）</li>
</ol>
<p>下图阐述了Docker和硬件虚拟化的区别：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2015/10/docker-vm-diff.png"><img class="aligncenter  wp-image-21067" src="https://blog.gmem.cc/wp-content/uploads/2015/10/docker-vm-diff.png" alt="docker-vm-diff" width="801" height="405" /></a></p>
<div class="blog_h2"><span class="graybg">使用流程</span></div>
<p>从用户的角度来看，使用Docker的典型工作流如下：</p>
<ol>
<li>将应用程序代码及其依赖纳入到Docker容器中
<ol>
<li>编写一个Dockerfile，描述执行环境，并拉取代码</li>
<li>如果应用程序依赖于外部应用（例如MySQL、Redis），你需要在某个仓库（例如Docker Hub）中找到它们。某些收费的外部应用可以在<a href="https://store.docker.com/">Docker Store</a>中找到</li>
<li>在Docker Compose file中引用应用程序，以及上面的那些外部应用，让他们能够同时运行</li>
<li>利用<a href="https://docs.docker.com/machine/overview/">Docker Machine</a>，在一个虚拟主机上构建、运行你的容器</li>
</ol>
</li>
<li>如果需要，为你的解决方案配置网络、存储</li>
<li>可选的，上传你的构建结果到仓库（私有、Docker官方），与团队成员协作</li>
<li>如果出现扩容（scale）需求，考虑使用<a href="https://docs.docker.com/engine/swarm/">Swarm</a>集群，通过<a href="https://docs.docker.com/ucp/overview/">Universal Control Plane</a>你可以方便的管理Swarm集群</li>
<li>最终，利用<a href="https://docs.docker.com/docker-cloud/overview/">Docker Cloud</a>将容器镜像部署到自有服务器或者云上</li>
</ol>
<div class="blog_h2"><span class="graybg">核心概念</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">概念</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Dockerfile</td>
<td>一段文本，Docker读取其中的指令以便自动化的构建Docker镜像。你可以在Dockerfile中声明任何命令</td>
</tr>
<tr>
<td>Docker Compose</td>
<td>一个工具，用来定义多容器（Multi-container）的Docker应用程序。你可以编写Composefile，来配置你的应用程序所依赖的服务，并通过单个命令来启动所有这些服务</td>
</tr>
<tr>
<td>Docker Engine</td>
<td>
<p>Docker的核心组件，它负责创建Docker镜像、运行Docker容器<br />从1.12版本开始，Docker引擎支持Swarm mode</p>
<p>包括三个组件：</p>
<ol>
<li>Docker守护程序，即dockerd</li>
<li>REST风格的接口，dockerd暴露的API</li>
<li>Docker命令行，通过REST API和dockerd通信，对其进行控制</li>
</ol>
</td>
</tr>
<tr>
<td>Docker Image</td>
<td>
<p>Docker镜像是一个文件系统 + 参数集，供Docker运行容器时使用。镜像本身不包含状态</p>
<p>任何人可以通过Docker镜像的方式来创建、分享软件</p>
<p>首次使用镜像时，会从仓库下载，之后，除非镜像的源码改变，不会再次下载</p>
</td>
</tr>
<tr>
<td>Layer</td>
<td>
<p>层，或者叫镜像层（Image layer），是指对<span style="background-color: #c0c0c0;">镜像的一个变更，或者指一个中间镜像</span>（intermediate image）。之所以叫层，和UFS（联合文件系统）有关，UFS允许：其包含的文件和目录可以分布在多个其它文件系统中，这些文件/目录（称为Branches）可以被叠加（overlay）形成单个新的文件系统</p>
<p>在Dockerfile中指定的指令，例如FROM/RUN/COPY，会导致先前的镜像发生变化，因而导致创建新的层</p>
<p>层有利于缩短构建的时间，Dockerfile发生变化后，变化之前的中间镜像不需要重新构建，可以作为缓存使用</p>
</td>
</tr>
<tr>
<td>Docker Container</td>
<td>
<p>Docker容器是Docker镜像的运行时实例</p>
<p>容器的行为取决于镜像如何被配置，可能是简单的执行一条命令，也可能是启动数据库这样的复杂服务</p>
</td>
</tr>
<tr>
<td>Docker Hub</td>
<td>
<p>一种服务，用于构建、管理镜像。其角色类似于Maven仓库或者PyPI</p>
<p>在Docker Hub上你可以很轻松下载到大量已经容器化的应用镜像，即拉即用。这些镜像中，有些是Docker官方维护的，更多的是众多开发者自发上传分享的</p>
<p>你可以将Github账号绑定到Docker Hub账号，并配置自动生成镜像的功能。这样，当Github中代码更新时，Docker镜像会自动更新</p>
</td>
</tr>
<tr>
<td>Docker Trusted Registry</td>
<td>DTR，企业级的Docker镜像存储方案，其角色类似于Docker Hub的私服</td>
</tr>
<tr>
<td>Docker Cloud</td>
<td>一种服务，能够构建、测试、部署镜像到你的主机上</td>
</tr>
<tr>
<td>Docker Universal Control Plane</td>
<td>UCP，管理Docker宿主机的集群，让它们整体上表现的像是单台机器</td>
</tr>
<tr>
<td>Docker Machine</td>
<td>一个工具，使用它你可以：
<ol>
<li>在Windows或者Mac平台上安装、运行Docker</li>
<li>基于命令docker-machine准备、管理多台Docker宿主机。管理行为包括：启动/停止/重启宿主机、升级Docker客户端和Daemon、配置Docker客户端，等等</li>
<li>准备Swarm集群</li>
</ol>
<p>当前，只有通过Docker Machine，你才能在Mac或者Windows上运行Docker。同时它也是管理<span style="background-color: #c0c0c0;">大量</span>基于各种Linux变体的宿主机的便捷方法</p>
</td>
</tr>
<tr>
<td>libcontainer</td>
<td>封装了名字空间、控制组、UnionFS的库，提供容器运行时基础功能</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">安装配置</span></div>
<p>本章主要介绍Ubuntu 14.04 LTS下安装、配置Docker的步骤。</p>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>你需要安装64bit的操作系统，内核的最低版本是3.10。为了支持aufs存储驱动，最好安装额外的内核包：</p>
<pre class="crayon-plain-tag">sudo apt-get update
sudo apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual</pre>
<p>添加Docker项目的APT源：</p>
<pre class="crayon-plain-tag">sudo echo  "deb https://apt.dockerproject.org/repo ubuntu-trusty main" &gt; /etc/apt/sources.list.d/docker.list
# 添加GPG Key
sudo apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
sudo apt-get update
# 可以查看当前系统支持的Docker引擎版本
apt-cache policy docker-engine</pre>
<p>安装Docker引擎并启动Docker守护程序：</p>
<pre class="crayon-plain-tag">sudo apt-get install docker-engine
sudo service docker start</pre>
<p>验证安装是否成功：</p>
<pre class="crayon-plain-tag"># 通知Docker引擎，将hello-world镜像载入到新建的容器中
docker run hello-world

# 执行下面的命令显示系统中容器的列表
docker ps -a
# 执行下面的命令显示系统中镜像的列表
docker images</pre>
<p>上述命令将下载一个测试目的的Docker镜像，并在一个容器中运行。该镜像会打印一条消息，然后退出。 </p>
<div class="blog_h2"><span class="graybg">升级与删除</span></div>
<p>执行下面的命令，来升级或者删除Docker：</p>
<pre class="crayon-plain-tag"># 升级
sudo apt-get upgrade docker-engine

# 删除
sudo apt-get purge docker-engine
sudo apt-get autoremove --purge docker-engine
# 上面的命令不会删除镜像、容器、卷或者用户创建的配置文件，你需要手工删除：
rm -rf /var/lib/docker</pre>
<div class="blog_h2"><span class="graybg">Windows和Mac</span></div>
<p>Docker最初是为Linux开发的，在Windows/Mac系统中，你可以借助Docker Machine来运行Docker。</p>
<p>从1.13开始，Windows和Mac具有基于原生虚拟化机制的Docker实现，不再依赖于Docker Machine。在Windows下，Docker基于Microsoft Hyper-V；在Mac下则基于HyperKit。</p>
<div class="blog_h3"><span class="graybg">Mac下命令自动完成</span></div>
<p>为Bash添加自动完成支持：</p>
<pre class="crayon-plain-tag">brew install bash-completion
brew tap homebrew/completions</pre>
<p>在.bash_profile中添加：</p>
<pre class="crayon-plain-tag">if [ -f $(brew --prefix)/etc/bash_completion ]; then  
    . $(brew --prefix)/etc/bash_completion
fi</pre>
<p>添加Docker的自动完成脚本：</p>
<pre class="crayon-plain-tag">pushd /usr/local/etc/bash_completion.d  
ln -s /Applications/Docker.app/Contents/Resources/etc/docker.bash-completion  
ln -s /Applications/Docker.app/Contents/Resources/etc/docker-machine.bash-completion  
ln -s /Applications/Docker.app/Contents/Resources/etc/docker-compose.bash-completion  </pre>
<div class="blog_h2"><span class="graybg">配置</span></div>
<p>执行一些必要的配置，可以让Ubuntu和Docker更好的一起工作。Docker配置文件默认为/etc/default/docker。</p>
<div class="blog_h3"><span class="graybg">创建Docker组</span></div>
<p>Docker的守护程序绑定到Unix套接字（而不是TCP/IP套接字）。默认的，该套接字的所有者是root，其它用户要访问它必须sudo。为此，Docker守护程序总是以root身份运行。</p>
<p>为了避免在调用<pre class="crayon-plain-tag">docker</pre> 命令时必须sudo，可以创建一个名为docker的UNIX组，并把你的用户添加到该组中。Docker守护程序启动时会为该组赋予读写权限</p>
<p>执行以下命令：</p>
<pre class="crayon-plain-tag">sudo groupadd docker
sudo usermod -aG docker $USER
# 用户$USER需要重新登录</pre>
<div class="blog_h3"><span class="graybg">调整内存和swap审计</span></div>
<p>当运行Docker时，你可能会看到类似下面的消息：</p>
<pre class="crayon-plain-tag">WARNING: Your kernel does not support cgroup swap limit.
WARNING: Yourkernel does not support swap limit capabilities. Limitation discarded.</pre>
<p>在主机上启用内存和swap审计，可以避免该消息：</p>
<ol>
<li>修改<pre class="crayon-plain-tag">/etc/default/grub</pre> ，设置内核选项：<br />
<pre class="crayon-plain-tag">GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"</pre>
</li>
<li>更新GRUB：<pre class="crayon-plain-tag">sudo update-grub</pre> </li>
<li>重新启动系统</li>
</ol>
<p>注意：启用后，即使不使用Docker，也会消耗额外1%左右的内存、降低10%左右的性能。</p>
<div class="blog_h3"><span class="graybg">启用UFW转发</span></div>
<p>如果在Docker的宿主机上使用<a href="/ubuntu-faq#ufw">UFW</a>，你需要额外的配置——由于UFW的默认行为是丢弃所有转发（路由）包，这会导致Docker无法正常工作。执行以下修改：</p>
<ol>
<li>修改配置文件/etc/default/ufw，设置<pre class="crayon-plain-tag">DEFAULT_FORWARD_POLICY="ACCEPT"</pre> </li>
<li>重新加载配置文件：<pre class="crayon-plain-tag">sudo ufw reload</pre> </li>
<li>允许针对Docker的入站连接：<pre class="crayon-plain-tag">sudo ufw allow 2375/tcp</pre> </li>
</ol>
<div class="blog_h3"><span class="graybg">配置DNS</span></div>
<p>Ubuntu及其衍生的桌面版Linux，通常会自动设置/etc/resolv.conf，将127.0.0.1作为默认DNS，同时网络管理器启用dnsmasq，将DNS请求代理给真实的DNS服务器</p>
<p>在这样的配置下，启动Docker容器会导致如下警告：</p>
<pre class="crayon-plain-tag">WARNING: Local (127.0.0.1) DNS resolver found in resolv.conf and containers
can't use it. Using default external servers : [8.8.8.8 8.8.4.4]</pre>
<p>出现此错误是因为Docker容器不能使用本地DNS服务器127.0.0.1，因此它默认使用了谷歌的DNS服务器</p>
<p>要避免此警告，依次执行：</p>
<ol>
<li>修改<pre class="crayon-plain-tag">/etc/default/docker</pre> 设置<pre class="crayon-plain-tag">DOCKER_OPTS="--dns 10.0.0.1"</pre> ，其中10.0.0.1替换为你的内网DNS服务器。注意<pre class="crayon-plain-tag">--dns</pre> 选项可以指定多次，对应多个备选DNS服务器</li>
<li>重新启动Docker守护程序：<pre class="crayon-plain-tag">sudo service docker restart</pre> </li>
</ol>
<p>你也可以修改网络管理器配置，/etc/NetworkManager/NetworkManager.conf，禁用dnsmasq：</p>
<pre class="crayon-plain-tag"># 注释掉：
dns=dnsmasq</pre>
<div class="blog_h3"><span class="graybg">设置自启动</span></div>
<p>14.10以下的版本，已经自动通过upstart配置Docker为自启动。15.04版本开始，Ubuntu使用systemd作为启动/服务管理器，你需要执行：</p>
<pre class="crayon-plain-tag">sudo systemctl enable docker </pre>
<div class="blog_h3"><span class="graybg">改变存储目录</span></div>
<p>默认情况下，Docker将/var/lib/docker作为基目录，并在其子目录下存储下载的镜像，以及运行过的容器，可以为Docker序指定选项-g以改变基目录：</p>
<pre class="crayon-plain-tag">DOCKER_OPTS=" -g /home/alex/Vmware/docker" </pre>
<p>你也可以使用符号链接来改变镜像的位置。</p>
<div class="blog_h3"><span class="graybg">为Docker Hub指定Mirror</span></div>
<p>国内访问Docker Hub比较缓慢，可以使用<a href="https://dashboard.daocloud.io/">DaoCloud</a>提供的Mirror（需要1.3.2+）。注册DaoCloud的账号后，进入仪表盘页面，点击“加速器”链接，在新的页面上会显示类似下面的命令：</p>
<pre class="crayon-plain-tag">curl -sSL https://get.daocloud.io/daotools/set_mirror.sh | sh -s http://0aa2e1e9.m.daocloud.io</pre>
<p>以root身份执行上面的命令后，文件/etc/docker/daemon.json被修改，添加以下内容：</p>
<pre class="crayon-plain-tag">{"registry-mirrors": ["http://0aa2e1e9.m.daocloud.io"]}</pre>
<p>你也可以手工修改/etc/default/docker文件，设置DOCKER_OPTS，追加：</p>
<pre class="crayon-plain-tag">DOCKER_OPTS= "  --registry-mirror http://0aa2e1e9.m.daocloud.io"</pre>
<p>此外，也可以<a href="https://link.jianshu.com/?t=https%3A%2F%2Fcr.console.aliyun.com%2F%23%2Faccelerator">注册阿里云开发者账号</a>，获得自己的Mirror URL。</p>
<div class="blog_h3"><span class="graybg">独立于守护进程运行</span></div>
<p>默认的，当Docker守护程序退出时，所有正在运行的容器被自动关闭。自1.12开始，你可以配置守护程序，让容器在守护程序退出后保持运行状态。</p>
<p>要启用该特性，可以使用选项<pre class="crayon-plain-tag">sudo dockerd --live-restore</pre> ，或者在/etc/docker/daemon.json中配置：</p>
<pre class="crayon-plain-tag">{
    "live-restore": true
}</pre>
<p>该特性于Swarm模式不兼容。 </p>
<div class="blog_h1"><span class="graybg">Docker入门</span></div>
<div class="blog_h2"><span class="graybg">构建镜像</span></div>
<p>whalesay是Docker官方提供的一个学习用镜像，运行此镜像，可以在屏幕上显示一头鲸和一句话：</p>
<pre class="crayon-plain-tag"># docker/whalesay为镜像名
# cowsay为执行的命令，boo-boo为命令行参数，即鲸说的那句话
docker run docker/whalesay cowsay boo-boo</pre>
<p>本节我们以whalesay镜像为基础，学习创建自己的Docker镜像</p>
<div class="blog_h3"><span class="graybg">编写Dockerfile</span></div>
<p>在前面的章节我们提到过，Dockerfile用于描述镜像如何被构建。创建镜像的第一步就是编写Dockerfile。</p>
<p>新建一个目录作为构建镜像的上下文目录，所谓上下文目录，意味着构建过程所需的全部文件都位于其中：</p>
<pre class="crayon-plain-tag">mkdir mywhalesay
cd mywhalesay

touch Dockerfile</pre>
<p>编辑Dockerfile文件，添加以下内容： </p>
<pre class="crayon-plain-tag"># FROM关键字说明当前镜像基于哪个镜像来构建
FROM docker/whalesay:latest

# RUN关键字用于在构建时执行任意命令，这里安装fortunes，能够随机的输出名人名言
# 能够正常使用apt-get是因为docker/whalesay FROM ubuntu:14.04
RUN apt-get -y update &amp;&amp; apt-get install -y fortunes

# CMD关键字用来指定镜像被加载后执行的命令，现在这头鲸会自顾自的引用名言了
CMD /usr/games/fortune -a | cowsay</pre>
<div class="blog_h3"><span class="graybg">执行构建</span></div>
<p>在上下文执行下面的命令以构建镜像：</p>
<pre class="crayon-plain-tag">docker build -t mywhalesay .</pre>
<p> 构建过程中控制台会打印详细过程，说明如下：</p>
<pre class="crayon-plain-tag"># 发送构建上下文给守护程序检查，确保构建需要的文件完备
# Sending build context to Docker daemon 2.048 kB
# 第一步，加载镜像docker/whalesay到临时容器，后续RUN关键字就是在修改此容器
Step 1 : FROM docker/whalesay:latest
 ---&gt; 6b362a9f73eb   # 这里显示的是镜像ID
# 第二步，在临时容器中运行命令，安装新软件，这导致容器的文件系统发生改变，与其镜像变得不同
Step 2 : RUN apt-get -y update &amp;&amp; apt-get install -y fortunes
 ---&gt; Running in f8c9dec97efc  # 这里显示的是容器ID
Processing triggers for libc-bin (2.19-0ubuntu6.6) ...
 ---&gt; 17c653933644
# 移除前面的临时容器
Removing intermediate container f8c9dec97efc
# 第三步，固化前面临时容器对docker/whalesay的改变，作为新的镜像fd35a325caf4
# 创建新的临时容器，尝试运行CMD关键字指定的命令
Step 3 : CMD /usr/games/fortune -a | cowsay
 ---&gt; Running in 8ae48258f958  
 ---&gt; fd35a325caf4  # 新的镜像
Removing intermediate container 8ae48258f958
# 构建完毕
Successfully built fd35a325caf4</pre>
<div class="blog_h3"><span class="graybg">运行新镜像</span></div>
<p>构建过程不会对上下文目录进行修改，构建好的镜像自动存放到/var/lib/docker目录下。可以运行命令查看：</p>
<pre class="crayon-plain-tag">docker images
# REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
# mywhalesay          latest              c779eaff986e        2 minutes ago       275 MB
# docker/whalesay     latest              6b362a9f73eb        17 months ago       247 MB</pre>
<p> 好了，看看这头智慧鲸都说了些什么吧：</p>
<pre class="crayon-plain-tag">docker run mywhalesay</pre>
<div class="blog_h2"><span class="graybg">使用Docker Hub</span></div>
<p>首先你需要<a href="https://hub.docker.com">注册</a>一个账号， 然后创建一个仓库（Repository），这里的仓库类似于Git仓库，它以你的Docker Hub用户名作为默认的名字空间，例如gmemcc/mywhalesay。</p>
<div class="blog_h3"><span class="graybg">打标签</span></div>
<p>标签（tag）是用于<span style="background-color: #c0c0c0;">区分镜像变体</span>的一种方法。</p>
<p>利用tag命令，你可以把一个本地镜像关联到Docker Hub仓库。首先，查询镜像的ID：</p>
<pre class="crayon-plain-tag">docker images 
# REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
# mywhalesay          latest              baa64efc0f1d        6 minutes ago       275 MB</pre>
<p>为之前构建的本地镜像mywhalesay打标签：</p>
<pre class="crayon-plain-tag">docker tag baa64efc0f1d gmemcc/mywhalesay:latest
# 可以删除本地镜像
docker rmi -f baa64efc0f1d</pre>
<p>输入Docker Hub账号密码以登录：</p>
<pre class="crayon-plain-tag">docker login</pre>
<div class="blog_h3"><span class="graybg">PUSH操作</span></div>
<p>使用push子命令，可以把本地镜像推送到Docker Hub上的仓库中： </p>
<pre class="crayon-plain-tag">docker push gmemcc/mywhalesay</pre>
<p>推送完毕后，可以到仓库主页去看看Tags选项卡，会出现一个lastest标签。</p>
<div class="blog_h3"><span class="graybg">PULL操作</span></div>
<p>在运行容器、构建镜像时，只要所需镜像在本地不存在，都会执行pull子命令拉取镜像。你也可以手工的拉取镜像：</p>
<pre class="crayon-plain-tag">docker pull gmemcc/mywhalesay </pre>
<div class="blog_h2"><span class="graybg">搭建Docker私服</span></div>
<p>为了方便企业内部Docker镜像的管理，你可以搭建Docker私服（registry server）。最简单的私服例子如下：</p>
<pre class="crayon-plain-tag">docker run -d -p 5000:5000 --restart=always -h registry --network local --dns 172.21.0.1  --ip 172.21.0.4 --name registry registry:2
# 如果宿主机是ARM平台，考虑使用镜像 budry/registry-arm
# 否则你会收到错误 docker: no supported platform found in manifest list.</pre>
<p>现在，你可以从Docker Hub上拉取镜像，并tag到私服中：</p>
<pre class="crayon-plain-tag">docker pull ubuntu
docker tag ubuntu localhost:5000/ubuntu</pre>
<p>然后，推送镜像到私服：</p>
<pre class="crayon-plain-tag">docker push localhost:5000/ubuntu</pre>
<p>之后，你可以再把镜像拉取下来：</p>
<pre class="crayon-plain-tag">docker pull localhost:5000/ubuntu</pre>
<div class="blog_h3"><span class="graybg">存储</span></div>
<p>默认情况下，私服中的数据存放在Docker卷中，此卷位于宿主机文件系统中。要改变存放位置，可以指定选项：</p>
<pre class="crayon-plain-tag">--volume /path/on/host:/var/lib/registry</pre>
<div class="blog_h3"><span class="graybg">外部访问</span></div>
<p>要想其它宿主机能访问私服，最好启用TLS，其配置类似于Web服务器的SSL配置：</p>
<pre class="crayon-plain-tag"># 创建密钥对
openssl genrsa -out zircon.local.key 4096
openssl req -new -x509 -days 3650 -text -key zircon.local.key -out zircon.local.crt

# 把证书复制到特定目录
mkdir -p /etc/docker/certs.d/docker.gmem.cc
sudo cp zircon.local.crt /etc/docker/certs.d/zircon.local\:5000/domain.crt
# 非本机：
scp alex@zircon.local:~/Vmware/docker/registry/certs/zircon.local.crt /etc/docker/certs.d/zircon.local\:5000/domain.crt

# 可选：全局启用证书
cp zircon.local.crt /usr/share/ca-certificates/
# 启用zircon.local的证书
sudo dpkg-reconfigure ca-certificates
# 需要重启服务
sudo service docker restart

# 重新运行私服
docker run -d -p 5000:5000 --restart=always --name registry \
 -v /home/alex/Vmware/docker/registry/certs:/certs \
 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/zircon.local.crt \
 -e REGISTRY_HTTP_TLS_KEY=/certs/zircon.local.key \
 registry:2</pre>
<p>这样，在网络中的机器都可以使用域名zircon.local代替localhost来访问私服了。</p>
<p>Mac版本的Docker目前无法访问自签名的私服，需要配置：</p>
<pre class="crayon-plain-tag">"insecure-registries":["zircon.local"]</pre>
<div class="blog_h3"><span class="graybg">使用letsencrypt证书</span></div>
<p>只需要注意把cert.pem、chain.pem合并到一起，作为证书即可：</p>
<pre class="crayon-plain-tag">cd /etc/letsencrypt/live/docker.gmem.cc/
cp privkey.pem domain.key
cat cert.pem chain.pem &gt; domain.crt
chmod 777 domain.crt
chmod 777 domain.key </pre>
<div class="blog_h3"><span class="graybg">访问控制</span></div>
<p>除了启用TLS，你可能还需要进行用户身份验证。首先，在宿主机上建立一个目录，并生成密码文件：</p>
<pre class="crayon-plain-tag">cd /home/alex/Vmware/docker/registry/
mkdir auth
# 生成密码，输出到宿主机文件
docker run --entrypoint htpasswd registry:2 -Bbn user passwd &gt; auth/htpasswd</pre>
<p>然后，运行私服： </p>
<pre class="crayon-plain-tag">docker run -d -p 5000:5000 --restart=always --name registry \
  -v /home/alex/Vmware/docker/registry/auth:/auth \
  -e "REGISTRY_AUTH=htpasswd" \
  -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  -v /home/alex/Vmware/docker/registry/certs:/certs \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/zircon.local.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/zircon.local.key \
  registry:2</pre>
<p>现在你可以登录到私服了： </p>
<pre class="crayon-plain-tag">docker login docker.gmem.cc </pre>
<div class="blog_h2"><span class="graybg">管理容器</span></div>
<p>要运行容器，首先要获得相应的镜像。运行容器时，如果镜像在本地不存在，Docker会自动到仓库下载。默认的仓库是Docker Hub。执行docker run命令可以启动一个容器：</p>
<pre class="crayon-plain-tag"># 不指定tag，默认使用lastest
docker run ubuntu
# 指定tag
docker run ubuntu:lastest
docker run ubuntu:14.04 </pre>
<div class="blog_h3"><span class="graybg">基本用法</span></div>
<p>将镜像加载到容器中后，相当于得到一个预装了某些软件的操作系统。例如对于ubuntu镜像，你可以： </p>
<pre class="crayon-plain-tag"># 像使用普通虚拟机一样使用容器：

# 调用echo命令
docker run ubuntu /bin/echo 'Hello world'

# 交互式的命令行
# -t 在容器内分配Terminal或者伪TTY
# -i 启动一个交互式的连接
docker run -t -i ubuntu /bin/bash

# -d 在后台运行容器（daemonize）。启动后，Docker会输出当前容器的ID
docker run -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done"
# 216dd35d275a6c6e1e548232bdd9db7cab8a6b2f9078367a3c842762dc458c1a

# 查看正在运行的容器，由于容器ID太长，难以记忆，因此Docker会为每一个容器生成一个名字，例如gloomy_bohr。这个名字可以修改
docker ps
# CONTAINER ID IMAGE     COMMAND                  CREATED          STATUS         PORTS    NAMES
# 216dd35d275a  ubuntu   "/bin/sh -c 'while tr"   9 seconds ago    Up 8 seconds            gloomy_bohr

# 查看所有已创建的容器，不管容器是否正在运行
docker ps --all

# 指定容器名称，容器名称必须具有唯一性
docker run --name webserver training/webapp python app.py 

# 查看容器运行日志，打印标准输出
docker logs gloomy_bohr

# 在任何时候，你可以附到一个运行中的容器，grab其标准输入/输出
docker attach gloomy_bohr

# 停止容器。超时容器未停止，则发送SIGKILL信号
docker stop gloomy_bohr

# 启动容器
docker start gloomy_bohr

# 在一个正在运行的容器中执行命令
docker exec gloomy_bohr ping 8.8.8.8
# 连接到容器，交互式的执行命令
docker exec -it ubuntu-16.04

# 查看容器内运行的进程
docker top gloomy_bohr
# 以JSON格式打印容器的详细配置信息
docker inspect gloomy_bohr

# 删除容器
# -f 即使正在运行，也将容器删除
docker rm -f gloomy_bohr</pre>
<p>上面的每个docker命令调用，实际上都在使用<span style="background-color: #c0c0c0;">Docker客户端</span>。Docker是基于Go语言开发的，执行<pre class="crayon-plain-tag">version</pre> 子命令可以查看客户端、服务器的版本。</p>
<div class="blog_h3"><span class="graybg">运行网络应用</span></div>
<p>Docker的主要应用场景在服务器端，而后者运行的程序大多是网络应用。这里我们使用training/webapp这个镜像来说明如何在容器中运行Web服务器：</p>
<pre class="crayon-plain-tag"># -P 自动将容器中的监听端口映射到宿主机的某个端口
docker run -d -P --name web training/webapp python app.py
# 执行下面的命令查看端口如何映射
docker ps -l
# PORTS  端口映射情况位于该列
# 0.0.0.0:32769-&gt;5000/tcp   宿主机的32769端口映射到容器的5000端口

# -p 手工指定端口映射，宿主机的80端口映射的哦容器的5000端口
docker run -d -p 80:5000 training/webapp python app.py

# 查询端口映射
docker port hungry_payne 5000

# 查看Web应用日志
# -f的效果类似于tail -f
docker logs -f hungry_payne</pre>
<div class="blog_h2"><span class="graybg">管理网络</span></div>
<p>利用网络驱动（network drivers） ，Docker为容器提供了网络支持。网络驱动主要有两类：bridge、overlay。默认随着Docker引擎一起安装了三个网络：</p>
<pre class="crayon-plain-tag">docker network ls
# NETWORK ID          NAME                DRIVER              SCOPE
# f160b7cc8b94        bridge              bridge              local               
# fc47bb582730        host                host                local               
# 5ac20f3697f3        none                null                local</pre>
<p>除非明确指定，新启动的容器总是使用<span style="background-color: #c0c0c0;">bridge</span>这个网络。 </p>
<p>执行network inspect子命令可以查看一个网络的详细信息，包括它为各连接到的容器分配的IP地址：</p>
<p>执行：<pre class="crayon-plain-tag">docker network inspect bridge</pre> ，结果如下：</p>
<pre class="crayon-plain-tag">[
    {
        "Name": "bridge",
        "Id": "f160b7cc8b94b782e5fed8b2e50e72f14ad205917540b1dc464f509f7eb11dec",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        /* 连接到此网络的容器的集合 */
        "Containers": {
            "87afe74d233dcd2e6caf2d569c785b0457123d4ad25f1c1bd9850576ba3afd1c": {
                "Name": "web",
                "EndpointID": "e8a0db5f066fd13bc2cb432f375582cb92e445a982a0ed667d26d92adb19550f",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]</pre>
<p>要把一个容器从网络中移除，可以执行：</p>
<pre class="crayon-plain-tag"># 把web从网络bridge中移除
docker network disconnect bridge web</pre>
<div class="blog_h3"><span class="graybg">创建网络</span></div>
<p>要创建一个简单的网络，可以执行：</p>
<pre class="crayon-plain-tag">docker network create -d bridge newbridge</pre>
<p>类似的，再不需要时，可以删除自定义网络：</p>
<pre class="crayon-plain-tag">docker network rm newbridge</pre>
<p>注意：名为bridge的默认网络不能被删除。</p>
<div class="blog_h3"><span class="graybg">添加容器到网络</span></div>
<p>如果不希望容器使用默认网络，可以在运行容器的时候指定--network参数：</p>
<pre class="crayon-plain-tag">docker run -d --network=newbridge --name db training/postgres
# 获得容器的IP地址
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' db</pre>
<p>Docker允许在运行时将容器添加到任意多个网络：</p>
<pre class="crayon-plain-tag">docker network connect bridge db</pre>
<div class="blog_h2"><span class="graybg">管理数据卷</span></div>
<p>所谓数据卷（Data volumes），是一个或者多个容器中<span style="background-color: #c0c0c0;">特定的目录</span>，这些目录<span style="background-color: #c0c0c0;">绕过容器的联合文件系统</span>（UFS，可以将不同物理位置合并mount到Linux目录树的同一位置） ，数据卷的一系列特性有利于数据的持久化、共享：</p>
<ol>
<li>数据卷在容器被创建时初始化，如果容器使用的基础镜像在数据卷的挂载点上包含数据，则这些数据被拷贝到新初始化的数据卷中。注意：当挂载宿主机目录作为数据卷时，拷贝行为不发生</li>
<li>数据卷可以被多个容器共享，或者被重用</li>
<li>对数据卷的更改是直接进行的</li>
<li>当你升级基础镜像时，对数据卷的修改不被包含其中</li>
<li>即使容器本身被删除，数据卷依然存在，数据卷独立于容器的生命周期</li>
</ol>
<p>注意：<span style="background-color: #c0c0c0;">作为挂载点的容器目录，其原有的文件全部不可见</span>，不会进行Overlay。</p>
<div class="blog_h3"><span class="graybg">添加数据卷</span></div>
<p>要添加匿名数据卷，可以在启动容器时指定-v参数：</p>
<pre class="crayon-plain-tag"># 在容器的/webapp位置创建一个匿名数据卷
docker run -d -P --name web -v /webapp training/webapp python app.py</pre>
<p>在制作镜像时，你可以用VOLUME指令声明一个或者多个数据卷，任何基于此镜像的容器，自动添加这些数据卷。</p>
<div class="blog_h3"><span class="graybg">定位数据卷</span></div>
<p>匿名数据卷在宿主机上以目录的形式存在，要定位此目录，可以执行inspect子命令：</p>
<pre class="crayon-plain-tag">/* docker inspect web的部分输出 */
"Mounts": [
    {
        "Name": "356707e2ddc02715db78b958b623707c2475f66258c14b68de48f0e29188bc0f",
        /* 在宿主机上的存放位置 */
        "Source": "/mnt/c3d88ac1-b4d5-4cdd-86b4-4255aba9ddb1/docker/volumes/35670b68de.../_data",
        "Destination": "/webapp",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
]</pre>
<div class="blog_h3"><span class="graybg">挂载宿主机目录</span></div>
<p>你也可以把任意宿主机目录挂载为数据卷，映射到容器的目录树中，仍然使用-v参数： </p>
<pre class="crayon-plain-tag"># 宿主机目录/src/webapp被挂载到容器的/webapp
# 容器中的挂载点必须总是指定绝对路径
# 宿主机目录可以指定绝对路径，也可以指定一个名称，Docker引擎依据此名称创建/引用命名卷
docker run -d -P --name web -v /src/webapp:/webapp training/webapp python app.py</pre>
<p>上面的例子中，如果容器的基础镜像已经包含了/webapp目录，则它会被宿主机的/src/webapp覆盖，但是容器从基础镜像得到的/webapp中的内容不会被删除，一旦数据卷被卸载，则/webapp中的内容恢复原样。</p>
<div class="blog_h3"><span class="graybg">挂载宿主机文件</span></div>
<p>挂载单个宿主机文件也被支持：<pre class="crayon-plain-tag">-v ~/.bash_history:/root/.bash_history</pre> </p>
<div class="blog_h3"><span class="graybg">挂载共享存储</span></div>
<p>除了挂载宿主机的本地目录，你也可以挂载共享存储为数据卷，Docker通过Volume plugins来支持iSCSI、NFS、FC等共享存储。使用共享存储的好处是它们是不依赖于主机的。</p>
<p>你可以在启动容器时，即时的在共享存储上创建<span style="background-color: #c0c0c0;">命名卷（named volume）</span>：</p>
<pre class="crayon-plain-tag"># --volume-driver 指定卷驱动，可选
# my-named-volume为新创建的卷的名字
docker run -d -P  --volume-driver=flocker   -v my-named-volume:/webapp  
    --name web training/webapp python app.py</pre>
<p>或者，先手工创建命名卷，然后引用命名卷：</p>
<pre class="crayon-plain-tag">docker volume create -d flocker -o size=20GB my-named-volume
docker run -d -P -v my-named-volume:/webapp</pre>
<p>例子中的flocker是一个专为Docker设计的数据卷管理工具。flocker管理的数据卷可以位于集群中的任何主机上，而不是绑定到某台宿主机。</p>
<p>注意：</p>
<ol>
<li>命名卷对应/var/lib/docker/volumes/my-named-volume/_data目录。该目录会自动创建</li>
<li>容器A、B共享一个命名卷，则A的写入B可以看到</li>
<li>A使用 -v my-named-volume:/data，则data中原有（镜像自带）的文件可以在my-named-volume中看到</li>
</ol>
<div class="blog_h3"><span class="graybg">挂载为只读</span></div>
<p>数据卷可以挂载为只读：<pre class="crayon-plain-tag">-v /src/webapp:/webapp:ro #添加:ro后缀</pre> </p>
<div class="blog_h3"><span class="graybg">备份/恢复</span></div>
<p>可以参考下面的命令，来备份一个数据卷：</p>
<pre class="crayon-plain-tag"># --volumes-from  引用来自dbstore的数据卷/dbdata
# -v 挂载宿主机当前目录到/backup
# 容器启动后，使用tar命令，将/dbdata的内容压缩，存放到/backup，亦即宿主机的当前目录
# tar完成后，容器停止，备份完毕
docker run --rm --volumes-from dbstore -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata</pre>
<p>使用类似的方法，可以首先数据卷的恢复。 跨宿主机联用备份/恢复，可以实现数据卷迁移。</p>
<div class="blog_h3"><span class="graybg">删除数据卷</span></div>
<p>默认情况下，容器删除后，其挂载的数据卷会被保留。一般的，你可以指定：当容器被删除后，自动清理匿名卷：</p>
<pre class="crayon-plain-tag"># --rm示意自动清理匿名卷/foo
# 挂载到/bar的命名卷awesone不会被清理
docker run --rm -v /foo -v awesome:/bar busybox top</pre>
<p>数据卷可以通过--volumes-from选项被其它容器引用，这种情况下，创建/引用数据卷的全部容器被删除之前，数据卷是无法删除的。要在删除容器时，同时删除数据卷，可以指定-v选项，例如</p>
<pre class="crayon-plain-tag"># 删除db3的同时，删除其使用的数据卷。注意，db3必须是最后一个使用它引用的所有数据卷的容器
docker rm -v db3</pre>
<div class="blog_h3"><span class="graybg">数据卷容器</span></div>
<p>如果需要在多个容器之间共享持久化数据，或者期望在非持久化容器中使用持久化数据，最好的方法是创建命名数据卷容器——仅仅为了提供共享数据卷的容器，并在其中挂载数据卷供其它容器引用。</p>
<p>首先创建一个命名的容器，但是不需要它执行任何命令：</p>
<pre class="crayon-plain-tag"># 创建一个名为dbstore的容器，其包含一个数据卷/dbdata
docker create -v /dbdata --name dbstore training/postgres /bin/true
# 注意 -v 非常重要，如果不声明-v，则此容器的目录不会暴露出去
docker run -v /jdk --name jdk docker.gmem.cc/jdk:7u80</pre>
<p>然后，你可以利用选项--volumes-from，在其它容器中挂载dbstore的/dbdata目录 ：</p>
<pre class="crayon-plain-tag">docker run -d --volumes-from dbstore --name db1 training/postgres
# 再创建一个容器，与db1共享来dbstore的/dbdata。两个容器的共享卷的挂载路径一致
docker run -d --volumes-from dbstore --name db2 training/postgres</pre>
<p>这样启动db1、db2后，如果postgres镜像包含/dbdata目录，它将被来自dbstore的数据卷mask掉，仅dbstore的/dbdata对于db1、db2可见。</p>
<p>选项--volumes-from 可以指定多次，这样你可以联合使用来自多个容器的数据卷。</p>
<p>选项--volumes-from可以通过链式的结构扩展——链条上的后面的容器可以使用前面任意容器声明的数据卷：</p>
<pre class="crayon-plain-tag"># 由于db1使用了dbstore的数据卷/dbdata
# 因此db3也可以使用
docker run -d --name db3 --volumes-from db1 training/postgres</pre>
<p>注意，<span style="background-color: #c0c0c0;">提供数据卷的容器不需要处于运行状态</span>。</p>
<div class="blog_h3"><span class="graybg">共享存储陷阱</span></div>
<p>多个容器共享一个数据卷时，可能导致数据破坏，这是由并发的写操作导致的。并发写操作可能来自于容器或者宿主机。 </p>
<div class="blog_h1"><span class="graybg">镜像</span></div>
<div class="blog_h2"><span class="graybg">基本镜像</span></div>
<p>所谓基本镜像（Base image），一般是指没有父镜像的镜像。此类镜像打包一个空白的操作系统。制作基本镜像的步骤依赖于你想打包的Linux发行版。</p>
<div class="blog_h3"><span class="graybg">使用Tar创建完整镜像</span></div>
<p>一般情况下，你可以在这样的Linux操作系统下完成基本镜像的创建——该操作系统就是你希望打包的Linux发行版。</p>
<p>某些工具可以简化镜像的创建，例如<a href="https://wiki.debian.org/Debootstrap">Debootstrap</a>可以安装一个Debian/Ubuntu的基本操作系统到一个目录中。使用该工具的示例：</p>
<pre class="crayon-plain-tag"># 下载Ubuntu 16.04（xenial）基本操作系统到xenial目录
sudo debootstrap xenial xenial

# tar -C切换工作目录 -c 创建压缩文件
# 压缩结果通过管道传递给docker import
# import子命令创建一个空白文件系统镜像，然后把Tar的内容导入进去
# ubuntu:16.04 创建一个名为ubuntu的仓库，Tag为16.04
sudo tar -C xenial -c . | docker import - ubuntu:16.04</pre>
<p>其它创建基本镜像的脚本，可以参考<a href="https://docs.docker.com/engine/userguide/eng-image/baseimages/">Create a base image </a></p>
<div class="blog_h3"><span class="graybg">扩展scratch镜像</span></div>
<p>scratch是Docker保留的名称，它不是一个镜像，你也不能把任何仓库命名为scratch。scratch可以作为构建基本镜像的起点：</p>
<pre class="crayon-plain-tag">FROM scratch
ADD hello /
CMD ["/hello"]</pre>
<div class="blog_h2"><span class="graybg">多体系结构支持</span></div>
<p>该主题包含以下关注点：</p>
<ol>
<li>如何透明的依据体系结构，自动拉取镜像。这个需求可以通过镜像清单（Manifest）满足，清单包含同一功能的镜像（例如Alpine:3.10）对应到多个体系结构的版本。拉取镜像时，不管是什么平台，都使用alpine:3.10这个名字，Docker会自动拉取匹配体系结构的镜像</li>
<li>如何去构建其它体系结构的镜像，这个将是本节的主题</li>
</ol>
<div class="blog_h3"><span class="graybg">binfmt_misc</span></div>
<p>如果你使用了桌面版本的Docker，或者使用高于4.8+内核的Linux，你可以直接在x86_64的机器上，运行各种其它体系结构的镜像。</p>
<p>在Linux上启用Docker与QEMU集成的方式：</p>
<pre class="crayon-plain-tag">docker run --rm --privileged linuxkit/binfmt:v0.8

# 正常情况下，不会报错，并且
ls -1 /proc/sys/fs/binfmt_misc/qemu-*
# 出现：
# /proc/sys/fs/binfmt_misc/qemu-aarch64
# /proc/sys/fs/binfmt_misc/qemu-arm
# /proc/sys/fs/binfmt_misc/qemu-ppc64le
# /proc/sys/fs/binfmt_misc/qemu-riscv64
# /proc/sys/fs/binfmt_misc/qemu-s390x</pre>
<p>重启Docker守护进程后，可以看到默认Builder支持多种平台：</p>
<pre class="crayon-plain-tag">docker buildx ls

# BEFORE:
# NAME/NODE DRIVER/ENDPOINT STATUS  PLATFORMS
# default * docker                  
#   default default         running linux/amd64, linux/386


# AFTER:
NAME/NODE DRIVER/ENDPOINT STATUS  PLATFORMS
default * docker                  
  default default         running linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6</pre>
<div class="blog_h3"><span class="graybg">buildx</span></div>
<p>这是一个Docker CLI插件，可以通过BuildKit进行Docker镜像的构建。使用buildx，你可以为不同体系结构构建镜像，并合并在一个镜像清单中，不需要作Dockerfile或源代码上的变动。</p>
<p>buildx把工作委托给builders：</p>
<pre class="crayon-plain-tag"># 列出现有的builders
docker buildx ls

# 创建本地builder:
docker buildx create --use --name local default
# 注意，docker驱动使用守护进程中的代理设置，但是docker-container不使用。此外由于builder容器
# 不是CLI创建的，因此~/.docker下设置的代理也不会变为容器的环境变量
# 目前支持创建builder时指定代理设置
docker buildx create 
                     # 使用哪种驱动
                     #   docker  使用编译到Docker守护进程中的BuildKit库，体验和原先的docker build相似
                     #   docker-container 在容器中启动BuildKit
                     #   kubernetes
                     --driver docker-container \
                     # 这些代理貌似也没什么价值，sh到builder容器中代理生效。但是Dockerfile中的命令，例如
                     # apk却不会使用这些代理，因此对构建过程没有价值
                     --driver-opt env.HTTP_PROXY=http://10.0.0.1:8088 \
                     --driver-opt env.HTTPS_PROXY=http://10.0.0.1:8088 \
                     --driver-opt network=host   \
                     --driver-opt '"env.NO_PROXY='$NO_PROXY'"'   \
  --use --name proxied default


# 添加远程builder：
docker context create arm --docker "host=tcp://10.0.0.90:2376"
docker context use arm
docker buildx use arm

# 切换使用的builder：
docker buildx use default</pre>
<p>要使用buildx发起镜像构建，执行命令：</p>
<pre class="crayon-plain-tag">docker buildx build .</pre>
<p>buildx将使用BuildKit引擎进行构建，不需要设置<pre class="crayon-plain-tag">DOCKER_BUILDKIT=1</pre>环境变量。 </p>
<p>buildx支持docker build的所有特性，包括19.03引入的输出配置、内联build缓存、指定目标platform。此外，buildx还支持manifest、分布式缓存、导出为OCI格式等docker build不支持的特性。</p>
<p>buildx可以<span style="background-color: #c0c0c0;">在不同配置下运行，每个配置称为driver</span>。默认使用编译到Docker守护进程中的BuildKit库，此驱动称为docker，该驱动使用你本地的Docker守护进程，并提供和docker build相似的体验。此外，你还可以使用docker-container驱动，它在容器中启动BuildKit。</p>
<p>驱动docker的输出，自动在docker images列表中可见。对于其它驱动，输出到何处需要<pre class="crayon-plain-tag">--output</pre>来指定。</p>
<p>通过创建新的builder实例，可以得到隔离的构建环境（不改变共享的Docker守护进程的状态），在CI中较为有用。 你甚至可以在远程Docker守护进程上创建多个builder，形成builder farm，并随意在这些builder之间切换。</p>
<p>Docker 19.03引入了类似Kubectl的context特性，可以为一个远程Docker守护进程的API端点提供一个名称。对于每个context，buildx会生成一个默认的builder实例。</p>
<div class="blog_h3"><span class="graybg">多体系结构镜像构建</span></div>
<pre class="crayon-plain-tag">docker buildx build --platform linux/amd64,linux/arm64 .</pre>
<div class="blog_h3"><span class="graybg">将buildx作为默认builder</span></div>
<p>执行命令：<pre class="crayon-plain-tag">docker buildx install</pre>，则docker build命令变为docker buildx的别名，这就意味着docker build自动使用docker buildx进行构建。 </p>
<p>执行<pre class="crayon-plain-tag">docker buildx uninstall</pre>移除别名。 </p>
<div class="blog_h1"><span class="graybg">Dockerfile</span></div>
<p>Docker读取Dockerfile中的指令以构建新的镜像，Dockerfile中的指令说明了镜像应该如何一步步的被构建。</p>
<div class="blog_h2"><span class="graybg">Dockerfile与镜像构建</span></div>
<p>执行<pre class="crayon-plain-tag">docker build</pre> 命令，可以从一个Dockerfile、一个上下文构建镜像。构建过程是由Docker守护程序（而不是客户端）执行的，客户端（docker build命令）做的第一件事情就是把整个上下文发送给守护程序。</p>
<p>所谓上下文，是指定PATH（宿主机本地目录）或者URL（Git存储库）中的文件集合。上下文会被递归的处理，即子目录被包含在上下文中。在大部分情况下，最好将空白目录作为上下文，其中仅包含一个Dockerfile和构建过程必备的文件。</p>
<p>要使用上下文中的文件，你必须编写特定的指令，例如COPY。你可以在上下文目录中添加一个<pre class="crayon-plain-tag">.dockerignore</pre> 来排除某些文件、目录以提高构建性能。</p>
<p>按照约定，在上下文根目录中的名为Dockerfile的文件被作为“Dockerfile”，但是你可以指定任意文件作为“Dockerfile”：</p>
<pre class="crayon-plain-tag">docker build -f /path/to/a/Dockerfile .</pre>
<p>你可以为正在构建的镜像指定仓库（Repository）:标签（tag），如果构建成功，镜像将被存放到相应的仓库:标签：</p>
<pre class="crayon-plain-tag">docker build -t gmemcc/myapp .
# 指定多个仓库/标签也是允许的
docker build -t gmemcc/myapp:1.0.2 -t gmemcc/myapp:latest .</pre>
<p>Docker守护程序会逐条的执行Dockerfile中的指令，如果必要，将指令的执行结果提交到正在构建的那个新的镜像中去。在最终输出镜像ID之前，守护程序会自动清理客户端发送的上下文。</p>
<p>需要注意的是，每条指令都是<span style="background-color: #c0c0c0;">独立的执行</span>的，因此<pre class="crayon-plain-tag">RUN cd /tmp</pre> 这样的指令不会对下一条指令的“工作目录”产生影响。</p>
<p>为了提高构建的性能，Docker会重用中间的（intermediate ）镜像（所谓缓存）。当使用缓存时，Docker会在控制台打印<pre class="crayon-plain-tag">Using cache</pre> 字样。</p>
<div class="blog_h2"><span class="graybg">Dockerfile格式</span></div>
<p>Dockerfile中只有注释、指令两类元素：</p>
<pre class="crayon-plain-tag"># Comment
INSTRUCTION arguments</pre>
<p>指令名称大小写不敏感，通常使用全大写。<span style="background-color: #c0c0c0;">第一条指令必须是FROM</span>，指定基础镜像。 以#开头的行通常被作为注释看待，除非它是合法的解析器指令（parser directive）。</p>
<div class="blog_h2"><span class="graybg">解析器指令</span></div>
<p>解析器指令（directive）影响Dockerfile中其它行的处理方式，该指令不会增加额外的层，也不会显示为构建步骤。</p>
<p>解析器指令的语法类似于一种特殊的注释：<pre class="crayon-plain-tag"># directive=value</pre> ，单个指令仅能被使用一次。一旦处理过任何注释、空行、指令（instruction），Docker就不再尝试分析任何解析器指令，因此<span style="background-color: #c0c0c0;">解析器指令必须位于Dockerfile的最开始处</span>。</p>
<p>解析器指令同样是大小写不敏感的，但是通常都使用全小写。在解析器指令之后，通常留有空白行。编写解析器指令时不得使用行连接符（\）</p>
<p>目前支持的解析器指令包括：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">解析器指令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>escape</td>
<td>
<p>设置Dockerfile中用来转义的前导字符，默认\。示例：<pre class="crayon-plain-tag"># escape=`</pre> </p>
<p>转义前导字符可以用于行内转义，也可以转义换行符。这允许一个指令（instruction）跨越多行</p>
<p>注意：对于<pre class="crayon-plain-tag">RUN</pre> 指令，转义仅仅会在行尾发生，即仅转义换行符</p>
<p>一般在<span style="background-color: #c0c0c0;">Windows上会将转义字符设置为`</span>，因为反斜杠是Windows的路径分隔符</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">变量替换</span></div>
<p>使用<pre class="crayon-plain-tag">ENV</pre> 指令可以声明环境变量，在Dockerfile中你可以声明Bash风格的变量替换：<pre class="crayon-plain-tag">$variable_name</pre> 或者<pre class="crayon-plain-tag">${variable_name}</pre> 。除了这两种基本格式以外，还可以使用某些Bash修饰符：</p>
<ol>
<li><pre class="crayon-plain-tag">${variable:-word}</pre> ，如果设置了环境变量variable，表达式的结果是$variable，否则是word</li>
<li><pre class="crayon-plain-tag">${variable:+word}</pre> ，如果设置了环境变量variable，表达式的结果是word，否则是空串</li>
</ol>
<p>以下指令<span style="background-color: #c0c0c0;">支持变量替换：ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL</span>。在1.4+，当ONBUILD与前述指令一起使用时，也支持环境变量。</p>
<p>注意：环境变量的值在同一个指令中保持不变，考虑下面的片断：</p>
<pre class="crayon-plain-tag">ENV abc=hello
ENV abc=bye def=$abc   # def值为hello而不是bye
ENV ghi=$abc           # ghi值为bye</pre>
<div class="blog_h2"><span class="graybg">.dockerignore</span></div>
<p>Docker客户端发送上下文到守护程序之前，会读取上下文根目录下名为.dockerignore的文件。如果此文件存在，则客户端会依据其中声明的规则来排除掉上下文中的部分文件或者目录。使用.dockerignore可以避免过多的、敏感的文件到守护程序，并被ADD/COPY命令复制到镜像中。</p>
<p>此文件中的每行是一个UNIX glob风格的匹配Pattern，上下文根目录被作为此Pattern的根目录，下面是一些示例：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">Pattern</td>
<td style="text-align: center;">排除</td>
</tr>
</thead>
<tbody>
<tr>
<td># comment</td>
<td>注释，被忽略</td>
</tr>
<tr>
<td>*/temp*</td>
<td>根目录的任意直接子目录中，任何以temp开头的目录或者文件</td>
</tr>
<tr>
<td>*/*/temp*</td>
<td>根目录的任意孙子目录中，任何以temp开头的目录或者文件</td>
</tr>
<tr>
<td>temp?</td>
<td>根目录值中任意以temp开头，后面附加一个任意字符的目录或者文件</td>
</tr>
<tr>
<td>**/*.go</td>
<td>任意以.go结尾的文件。**匹配0-N个目录</td>
</tr>
<tr>
<td>*.md<br /> !README.md</td>
<td>根目录中任何.md文件，除了README.md，!用于为排除指定例外</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">FROM指令</span></div>
<p>指令格式：</p>
<pre class="crayon-plain-tag">FROM &lt;image&gt;
FROM &lt;image&gt;:&lt;tag&gt;
FROM &lt;image&gt;@&lt;digest&gt;

# --platform  如果image是多平台的镜像，则此参数选择该镜像的特定平台版本
#             默认自动选择匹配构建目标平台的版本

# AS 为该build stage命名，名字可以被后续的stage引用
FROM [--platform=&lt;platform&gt;] &lt;image&gt; [AS &lt;name&gt;]</pre>
<p>用于指定正在构建镜像的Base镜像：</p>
<ol>
<li>该指令必须是Dockerfile中第一个非注释指令</li>
<li>该指令可以在一个Dockerfile中出现多次，用于构建多个镜像</li>
<li>tag、digest可选， 如果不指定，自动使用latest </li>
</ol>
<div class="blog_h3"><span class="graybg">多Stage构建</span></div>
<p>一个Dockerfile中可以包含多个FROM指令，这叫多Stage构建。下面是一个例子：</p>
<pre class="crayon-plain-tag">ARG ARCH

# 第一个stage，构建出二进制文件
FROM golang:1.13 as builder
WORKDIR /workspace
RUN CGO_ENABLED=0 GOOS=linux GOARCH=$ARCH GO111MODULE=on go build -a -o pause pause.go

# 第二个stage，构建出镜像
FROM docker.gmem.cc/tcnp/alpine-${ARCH}:3.11
WORKDIR /
#    用名字来引用前面的stage，复制出其中的文件
COPY --from=builder /workspace/pause .
ENTRYPOINT ["/pause"]</pre>
<div class="blog_h3"><span class="graybg">使用变量</span></div>
<p>如上面的例子所示，FROM中可以使用在<span style="background-color: #c0c0c0;"><strong>第一个FROM指令之前</strong>出现的ARG中定义的</span>构建时变量。</p>
<div class="blog_h2"><span class="graybg">MAINTAINER指令</span></div>
<p>指令格式：<pre class="crayon-plain-tag">MAINTAINER &lt;name&gt;</pre> 。用于指定生成的镜像的Author字段</p>
<div class="blog_h2"><span class="graybg">RUN指令</span></div>
<p>该指令具有两种格式：</p>
<pre class="crayon-plain-tag"># shell格式，command在Shell中运行，默认/bin/sh -c或者 cmd /S /C
RUN &lt;command&gt;
RUN ["executable", "param1", "param2"]

# 示例：
RUN ["/bin/bash", "-c", "echo hello"]</pre>
<p>该指令在当前intermediate镜像之上新建一层，并在其中执行任意命令，然后提交结果。提交结果后形成的新intermediate镜像被下一个指令使用。</p>
<p>运行RUN指令时触发分层（Layering）符合Docker的核心理念——提交（Commit）操作想对廉价，并且可以从镜像历史的任意位置（任意一层）创建容器。分层也是构建过程中镜像缓存的基础。</p>
<p>使用exec格式可以避免Shell字符串相关的陷阱，也可以在Base镜像中没有/bin/sh程序的情况下执行命令。使用exec格式时需要注意：</p>
<ol>
<li>由于exec格式不调用Shell，因此也不会发生变量替换：<br />
<pre class="crayon-plain-tag"># $HOME不会被替换
RUN [ "echo", "$HOME" ]
# $HOME会被替换，因为虽然是exec格式，但是它调用了sh
RUN [ "sh", "-c", "echo $HOME" ] </pre>
</li>
<li>exec指令的参数必须是规范化的JSON数组，所以：
<ol>
<li>字符串必须使用双引号包围</li>
<li>字符串中的反斜杠必须JavaScript语法转义，即<pre class="crayon-plain-tag">\\</pre>  </li>
</ol>
</li>
</ol>
<p>使用shell格式时，你可以使用<pre class="crayon-plain-tag">\</pre> 让命令跨越多行。</p>
<p>RUN指令导致的镜像缓存不会自动失效，要禁用缓存，可以在构建时指定<pre class="crayon-plain-tag">--no-cache</pre>选项。ADD指令可以导致RUN的缓存失效。</p>
<p>可以用SHELL指令，设置shell格式的RUN指令所使用的Shell程序，其它几个具有exec格式/shell格式区分的指令，也受到SHELL指令的影响。</p>
<div class="blog_h2"><span class="graybg">CMD指令</span></div>
<p>该指令具有三种格式：</p>
<pre class="crayon-plain-tag"># exec格式，推荐的格式
CMD ["executable","param1","param2"]
# 用作ENTRYPOINT的默认参数
CMD ["param1","param2"]
# shell格式
CMD command param1 param2</pre>
<p>Dockerfile中<span style="background-color: #c0c0c0;">仅能包含一个CMD指令</span>， 如果指定了多个CMD指令则仅最后一个有效。该指令的主要意图是为执行容器（executing container）提供默认值（defaults），这些默认值可以包含一个可执行文件，与ENTRYPOINT指令联用时，则仅仅包含执行选项。</p>
<p>使用CMD指令时要注意：</p>
<ol>
<li>CMD不会在构建阶段做任何事情</li>
<li>如果用CMD为ENTRYPOINT提供默认参数，则这两个指令的参数均为规范化的JSON数组</li>
<li>exec、shell格式调用的注意点，参考RUN指令</li>
</ol>
<p>如果使用了CMD指令，并且运行容器时没有指定需要执行的命令，则CMD中的命令会被执行。要<span style="background-color: #c0c0c0;">让容器每次都运行同一个程序</span>，应当联合使用ENTRYPOINT、CMD。</p>
<div class="blog_h2"><span class="graybg">LABEL指令</span></div>
<p>指令格式：<pre class="crayon-plain-tag">LABEL &lt;key&gt;=&lt;value&gt; &lt;key&gt;=&lt;value&gt; &lt;key&gt;=&lt;value&gt; ...</pre> </p>
<p>该指令为镜像添加元数据（metadata），每个标签是一个键值对。如果需要为镜像添加多个标签，最好在单个LABEL指令中完成。下面是一些示例：</p>
<pre class="crayon-plain-tag"># 如果键/值中包含空格，可以用引号包围
LABEL "Author Name"="Alex Wong"
# 可以使用反斜杠跨行
LABEL description="Hello \
There !"</pre>
<p>镜像会继承来自FROM镜像的标签，如果当前镜像的某个标签的键与FROM镜像冲突，则当前的<span style="background-color: #c0c0c0;">覆盖FROM</span>的。 </p>
<p>使用<pre class="crayon-plain-tag">docker inspect</pre> 命令可以查看一个镜像的所有标签。</p>
<div class="blog_h2"><span class="graybg">EXPOSE指令</span></div>
<p>指令格式：<pre class="crayon-plain-tag">EXPOSE &lt;port&gt; [&lt;port&gt;...]</pre> </p>
<p>该指令声明容器在运行时侦听的端口。该指令并不会让容器和宿主机之间发生端口映射，要真正映射（发布）端口，必须在创建容器时使用选项：</p>
<ol>
<li>使用-p参数运行镜像，指定发布的端口范围</li>
<li>使用-P参数，发布所有EXPOSE声明的端口</li>
</ol>
<div class="blog_h2"><span class="graybg">ENV指令</span></div>
<p>该指令支持两种格式：</p>
<pre class="crayon-plain-tag"># 为单个变量（key）设置单个值（value）。第一个空格之后的全部内容被作为值看待，即使其中包含空格、引号
ENV &lt;key&gt; &lt;value&gt;
# 允许设置多个环境变量
ENV &lt;key&gt;=&lt;value&gt; ...</pre>
<p>设置<span style="background-color: #c0c0c0;">构建过程、运行期间均可见的环境变量</span>，这些环境变量对任何后代Dockerfile都可用。</p>
<p>部分Dockerfile指令或者指令的某种形式，支持引用环境变量，语法和Bash一致。</p>
<p>在运行期间，可以使用docker inspect查看环境变量。在命令行中，可以使用<pre class="crayon-plain-tag">--env &lt;key&gt;=&lt;value&gt;</pre>覆盖环境变量设置。</p>
<div class="blog_h2"><span class="graybg">ADD指令</span></div>
<p> 该指令支持两种格式：</p>
<pre class="crayon-plain-tag"># src是上下文中的目录或者远程URI；dest为镜像中的目录
ADD &lt;src&gt;... &lt;dest&gt;
# 用于路径中包含空格的情况
ADD ["&lt;src&gt;",... "&lt;dest&gt;"]</pre>
<p>拷贝本地目录、文件或者远程文件URI，将它们加入到容器文件系统的dest位置。注意以下几点：</p>
<ol>
<li>可以指定多个src，src中可以包含*、?等<span style="background-color: #c0c0c0;">通配符</span></li>
<li>如果src是目录、文件，必须指定<span style="background-color: #c0c0c0;">相对于上下文根的相对路径</span>，src目录/文件必须位于上下文内部。/something、../something均非法</li>
<li>当src是目录时，其内部所有内容，包括文件系统元数据都被拷贝。但是目录本身不被拷贝</li>
<li>当src是一个本地压缩文件时，它被解压为一个目录，然后拷贝到dest</li>
<li>当src是本地目录（包括解压后的压缩文件）时，覆盖到dest的行为类似于tar -x</li>
<li>当src是远程URI时：
<ol>
<li>如果dest以/结尾，则文件名从URI中推定，保存为&lt;dest&gt;/&lt;filename&gt;</li>
<li>如果dest不以/结尾，则文件保存为dest</li>
</ol>
</li>
<li>dest指定容器中的绝对路径。如果使用相对路径，则相对于WORKDIR</li>
<li>当dest以/结尾时，被看作目录，否则看作一般文件。如果dest不存在，会被自动创建</li>
<li>容器中所有的新文件，以UID=GID=0创建</li>
<li>如果src为远程URI，则dest的模式被设置为600。如果通过HTTP协议获取远程文件，则Last-Modified头用于设置dest中文件的mtime</li>
<li>mtime不会影响缓存判断</li>
<li>当通过stdin读取Dockerfile并构建时：<pre class="crayon-plain-tag">docker build - &lt; somefile</pre> ，由于没有构建上下文，因此ADD指令必须使用基于URI的src。如果somefile是一个压缩包（tar.gz），则压缩包中根目录被作为构建上下文，而根目录中须有Dockerfile文件</li>
<li>由于ADD不支持身份认证，因此远程URI需要身份验证时，你必须使用RUN wget/curl代替ADD</li>
<li>如果src的内容发生变化，ADD会导致所有后续指令对应的Layer缓存失效</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag"># 拷贝上下文根目录中所有hom开头的文件到镜像的/mydir/目录下
ADD hom* /mydir/
# 拷贝上下文目录中home.txt之类的文件，e可以是任何单字符
ADD hom?.txt /mydir/

# 拷贝到相对路径 $WORKDIR/relativeDir/下
ADD test relativeDir/</pre>
<div class="blog_h2"><span class="graybg">COPY指令</span></div>
<p>该指令支持两种格式：</p>
<pre class="crayon-plain-tag">COPY &lt;src&gt;... &lt;dest&gt;
# 用于路径中包含空格的情况
COPY ["&lt;src&gt;",... "&lt;dest&gt;"]</pre>
<p>拷贝文件或者目录到容器文件系统中。 注意点类似于ADD，但是COPY不理解远程URI也不处理压缩文件。</p>
<div class="blog_h2"><span class="graybg">ENTRYPOINT指令</span></div>
<p>该指令支持两种格式：</p>
<pre class="crayon-plain-tag"># 推荐的exec格式，参数以JSON数组传递。该方式不会调用Shell命令，因此不会发生参数替换等行为
ENTRYPOINT ["executable", "param1", "param2"]
# shell格式，阻止传参。可以使用环境变量。容器运行时调用/bin/sh -c "command param1 param2"
ENTRYPOINT command param1 param2</pre>
<p>使用入口点，可以如同使用可执行文件那样运行一个镜像。docker run时，镜像名后面的所有参数，都<span style="background-color: #c0c0c0;">作为参数传递给ENTRYPOINT指定的命令/可执行文件</span>，并且覆盖CMD中指定的默认参数。当镜像名和可执行文件名一致的时候，dock run看起来就好像在执行一个文件，而不是运行镜像。</p>
<p><span style="background-color: #c0c0c0;">shell格式的调用，阻止CMD指定的默认参数和dock run指定的参数</span>。但是这种格式下可执行文件将作为/bin/sh -c的子命令运行，这导致可执行文件的PID不是1并且不能接收UNIX信号。你的可执行文件将无法接收docker stop发送来的SIGTERM信号。</p>
<p>如果该指令使用多次，则仅仅最后一个被使用。</p>
<p>你可以使用<pre class="crayon-plain-tag">--entrypoint</pre>选项覆盖ENTRYPOINT，但是只能指定执行的程序：</p>
<pre class="crayon-plain-tag"># 以命令bash作为入口点，覆盖默认的入口点，将--version作为bash的参数
docker run -it --rm  --entrypoint bash docker.gmem.cc/maven:3.5.2 --version</pre>
<div class="blog_h3"><span class="graybg">exec格式入口点示例</span></div>
<p>使用该格式，可以方便的设置容器执行的<span style="background-color: #c0c0c0;">默认命令</span>及其<span style="background-color: #c0c0c0;">稳定的默认参数</span>：<pre class="crayon-plain-tag">ENTRYPOINT ["top", "-b"]</pre> </p>
<p>然后，可以联用CMD指令，设置可<span style="background-color: #c0c0c0;">覆盖的默认参数</span>：<pre class="crayon-plain-tag">CMD ["-c"]</pre> </p>
<p>下面的例子，在前台（PID为1）运行Apache服务器：</p>
<pre class="crayon-plain-tag">FROM debian:stable
RUN apt-get update &amp;&amp; apt-get install -y --force-yes apache2
EXPOSE 80 443
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"]
# -D FOREGROUND 不去fork出子进程
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]</pre>
<p>你可以使用任何脚本作为入口点可执行程序的启动脚本，为了确保底层可执行文件能够接收UNIX信号，必须使用exec命令替换进程：</p>
<pre class="crayon-plain-tag">ENTRYPOINT ["/usr/bin/start-postgres.sh"]</pre><br />
<pre class="crayon-plain-tag">#!/bin/bash
set -e
# 如果第一个参数是postgress，则启动postgress服务
if [ "$1" = 'postgres' ]; then
    # 初始化，改变目录所有权
    chown -R postgres "$PGDATA"
    # 初始化数据库
    if [ -z "$(ls -A "$PGDATA")" ]; then
        gosu postgres initdb
    fi
    # 使用exec、管理员权限运行postgres。exec确保postgres进程PID=1，能够接收UNIX信号
    exec gosu postgres "$@"
fi
# 否则，运行参数指定的程序
exec "$@"</pre>
<p>如果需要进行清理工作，可以在入口点脚本中编写trap语句：</p>
<pre class="crayon-plain-tag"># 捕获多种信号，然后执行引号中的脚本，停止Apache服务
trap "stopping apache /usr/sbin/apachectl stop" HUP INT QUIT TERM
# 在后台启动Apache服务，这种服务无法接收信号
usr/sbin/apachectl start
echo "Press ENTER to exit"
read
# 回车后，也可以停止Apache服务
echo "stopping apache"
/usr/sbin/apachectl stop</pre>
<div class="blog_h3"><span class="graybg">shell格式入口点示例</span></div>
<p>这种方式指定的入口点，将在Shell中（默认 /bin/sh -c） 执行指定的程序，能够进行变量替换。该方式会<span style="background-color: #c0c0c0;">忽略CMD指令和docker run image 后面的参数</span>。</p>
<p>为了确保长时间运行的入口点程序能够正确接收docker stop发来的UNIX信号，应当使用exec来执行目标程序，例如：<pre class="crayon-plain-tag">ENTRYPOINT exec top -b</pre>  </p>
<div class="blog_h2"><span class="graybg">VOLUME指令</span></div>
<p>指令格式：</p>
<pre class="crayon-plain-tag">VOLUME ["/mount/point"]
VOLUME  /mount/point1 /mount/point2</pre>
<p>在镜像中创建一个挂载点，并将其标记为“承载来自宿主机、其它容器的外部卷”。</p>
<p>在构建阶段，你可以往挂载点写入数据，<span style="background-color: #c0c0c0;">然后再声明</span>VOLUME指令，这样数据会持久化在镜像中：</p>
<pre class="crayon-plain-tag">RUN echo "Hello Docker!" &gt; /mount/point/greetings
# 后声明VOLUME
VOLUME /mount/point
# 再此之后对/mount/point进行任何改动，都会被丢弃</pre>
<p>在docker run时， 容器会在/mount/point挂载新的卷，并且把greetings文件拷贝到此卷中。</p>
<div class="blog_h2"><span class="graybg">USER指令</span></div>
<p>指令格式：<pre class="crayon-plain-tag">USER username-or-uid</pre> ，指定RUN,CMD,ENTRYPOINT指令以什么身份运行。</p>
<p>在我机器上尝试MySQL镜像时，设置USER为mysql，生效的ID为999，而mysql用户的真实ID是118。直接设置USER为118，没有问题。</p>
<div class="blog_h2"><span class="graybg">WORKDIR指令</span></div>
<p>指令格式：<pre class="crayon-plain-tag">WORKDIR /path/to/workdir</pre> </p>
<p>指定RUN, CMD, ENTRYPOINT, COPY,ADD指令的工作目录，你可以多次使用该指令，并且可以使用相对（于上一个WORKDIR）路径。示例：</p>
<pre class="crayon-plain-tag">ENV PREFIX /usr/local
# 可以使用环境变量
WORKDIR $PREFIX/bin
# 下面的指令导致工作目录变为/usr/local/bin/python
WORKDIR python</pre>
<div class="blog_h2"><span class="graybg">ARG指令</span></div>
<p>指令格式：<pre class="crayon-plain-tag">ARG &lt;name&gt;[=&lt;default value&gt;] #该指令可以声明多次，也可以指定一个默认值</pre>  </p>
<p>该指令定义一个<span style="background-color: #c0c0c0;">构建时变量</span>。变量的值可以通过<pre class="crayon-plain-tag">docker build --build-arg &lt;varname&gt;=&lt;value&gt;</pre> 指定、覆盖。如果用户通过build-arg传递没有通过ARG声明的变量，构建会失败。</p>
<p>ARG定义的变量，对Dockerfile<span style="background-color: #c0c0c0;">当前行之后的指令</span>生效。ARG变量<span style="background-color: #c0c0c0;">不会持久化</span>到镜像中。</p>
<p>使用ENV、ARG都可以向RUN指令传递变量，如果这两个指令声明<span style="background-color: #c0c0c0;">同名的变量，则ENV总是覆盖ARG</span>。</p>
<p>以下ARG是Docker预定义的，不需要声明即可使用：http_proxy、https_proxy、ftp_proxy、no_proxy以及这4个变量的全大写版本。</p>
<div class="blog_h3"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag">ARG JAR_FILE
ADD target/$JAR_FILE /app.jar </pre>
<div class="blog_h3"><span class="graybg">对缓存的影响</span></div>
<p>如果ARG的值在下一次构建时发生了修改，则第一次使用它定义的变量的那个指令对应的Layer缓存失效。</p>
<div class="blog_h2"><span class="graybg">ONBUILD指令</span></div>
<p>指令格式：<pre class="crayon-plain-tag">ONBUILD [INSTRUCTION]</pre> </p>
<p>为镜像添加一个触发器指令（ trigger instruction），该指令会在<span style="background-color: #c0c0c0;">当前镜像被作为其它构建的Base镜像时执行</span>。ONBUILD指定的<span style="background-color: #99cc00;">指令</span>会在子镜像构建时、<span style="background-color: #c0c0c0;">在子镜像的构建上下文中</span>立即执行，就好像把该<span style="background-color: #99cc00;">指令</span>直接插入到FROM指令后面一样。</p>
<p>所有构建指令都可以通过ONBUILD注册为触发器。</p>
<p>当你制作一个专用于被扩展的Base镜像ONBUILD很有用。举例来说，假设你构建了一个Pyhton的应用编译环境（Base镜像），它要求被构建的Python源码被放置在特定的目录，并在之后调用编译脚本。你无法在构建Base镜像的时候使用ADD、RUN完成前述的工作，因为Base镜像的构建者并不知道源码在哪——每个具体应用程序的源码位置都不同。这时，ONBUILD可以帮助你：</p>
<ol>
<li>构建Base镜像时，当遇到ONBUILD时，Docker在镜像元数据中添加一条触发器数据。ONBUILD中的指令不会影响Base镜像的构建</li>
<li>在Base镜像构建完毕后，所有触发器的列表（对应多个ONBUILD）被存放到镜像的元数据清单（manifest）中，以OnBuild为Key。你可以对Base镜像执行docker inspect查看触发器列表</li>
<li>构建子镜像时，执行FROM指令时，ONBUILD触发器会按照它们在Base镜像中的声明顺序，依次执行，指有它们全部执行成功，子镜像才会继续构建，否则在FROM指令处失败</li>
<li>子镜像构建完毕后，ONBUILD触发器从子镜像的元数据中移除，这意味着<span style="background-color: #c0c0c0;">孙子镜像不会感知到ONBUILD</span>触发器</li>
</ol>
<p>下面是一个具体的例子：</p>
<pre class="crayon-plain-tag"># 将子镜像的构建上下文拷贝到临时容器的特定位置
ONBUILD ADD . /app/src
# 构建Python应用程序，结果的结果固化在子镜像中
ONBUILD RUN /usr/local/bin/python-build --dir /app/src</pre>
<p>构建子镜像时，只需要把Python程序源码放在上下文目录中，构建完毕后，子镜像的容器就直接可以使用与<span style="background-color: #c0c0c0;">子镜像同时构建的Python程序</span>了。 </p>
<p>使用ONBUILD时需要注意：</p>
<ol>
<li>不支持ONBUILD ONBUILD...</li>
<li>不支持ONBUILD FROM或者ONBUILD MAINTAINER</li>
</ol>
<div class="blog_h2"><span class="graybg">STOPSIGNAL指令</span></div>
<p>指令格式：<pre class="crayon-plain-tag">STOPSIGNAL signal</pre> ，指定为了让容器退出时，向其发送的信号。支持信号名称或者数字，例如SIGKILL、9。</p>
<div class="blog_h2"><span class="graybg">HEALTHCHECK指令</span></div>
<p>该指令支持两种格式：</p>
<pre class="crayon-plain-tag"># 在容器内运行一个命令以检测容器的健康状况
# command 要么是一个Shell命令字符串，要么是一个JSON数组
HEALTHCHECK [OPTIONS] CMD command
# 在CMD之前，可以指定以下选项：
--interval=DURATION   # 默认30s，第一次运行健康检测，于容器启动后DURATION秒后，以后每隔DURATION秒检测一次
--timeout=DURATION    # 默认30s，如果健康检测命令超过DURATION秒没有完成，则认为执行失败
--retries=N           # 默认3， 健康检测命令连续失败的最大次数，超过此次数，认定容器处于unhealthy状态

# 禁用任何从Base镜像继承来的健康检测指令
HEALTHCHECK NONE</pre>
<p>该指令告知Docker，如何确认容器仍然在正常工作。正常工作不仅仅要求进程在运行，还要求特定于具体应用的检测可以通过（例如对于Web应用，能够正常处理HTTP登录请求）。</p>
<p>当指定了该指令后，在normal status之外，还多了一个health status。该状态的初始值为starting，当健康检测通过后，变为healthy；当检测不通过、或者若干次检测失败后，变为unhealthy。</p>
<p>command的退出状态用于判断容器是否健康：0 表示健康；1表示不健康；2为暂不使用的保留值。command的输出到stdout/stderr的信息，可以通过docker inspect命令查看到。</p>
<p>每个Dockerfile仅支持一个HEALTHCHECK指令，如果指定了多个，只有最后一个生效。</p>
<div class="blog_h2"><span class="graybg">SHELL指令</span></div>
<p>指令格式：<pre class="crayon-plain-tag">SHELL ["executable", "parameters"]</pre> </p>
<p>用于覆盖那些使用shell格式的指令所使用的默认Shell：</p>
<ol>
<li>Linux下默认Shell为<pre class="crayon-plain-tag">["/bin/sh", "-c"]</pre> </li>
<li>Windows下默认Shell为<pre class="crayon-plain-tag">["cmd", "/S", "/C"]</pre> </li>
</ol>
<p>在Windows平台上该指令很有用，因为Windows提供了两个常用却完全不同的Shell：cmd和powershell。</p>
<p>SHELL指令可以出现多次，每次均覆盖之前的取值。</p>
<div class="blog_h2"><span class="graybg">多阶段构建</span></div>
<p>你可以在Dockerfile中使用多个FROM语句，每个FROM触发一个新的构建阶段（Stage）。你可以选择性的把某些构建从一个阶段拷贝到另外一个，而把任何不需要引入最终镜像的文件丢弃 —— 例如构建工具、依赖包。</p>
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag">FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# --from表示从前面的stage拷贝
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]</pre>
<p>这个例子的第一阶段使用Go SDK构建源码，第二阶段则将第一阶段中的文件拷贝过来。</p>
<p>除了使用序号，你还可以通过名称来引用Stage：</p>
<div class="blog_h3"><span class="graybg">命名Stage</span></div>
<pre class="crayon-plain-tag">FROM golang:1.7.3 as builder
...
FROM alpine:latest 
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .</pre>
<div class="blog_h3"><span class="graybg">中止构建</span></div>
<p>你可以仅仅执行一部分Stage，然后就停止构建：</p>
<pre class="crayon-plain-tag"># 执行到builder这个stage即停止
docker build --target builder -t alexellis2/href-counter:latest .</pre>
<div class="blog_h3"><span class="graybg">外部镜像作为Stage</span></div>
<p>任何一个镜像都可以作为Stage使用，从中拷贝文件：</p>
<pre class="crayon-plain-tag">COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf</pre>
<div class="blog_h2"><span class="graybg">最佳实践</span></div>
<p>为了编写易用、有效的Dockerfile，Docker官方发布了若干条最佳实践。如果你要构建官方镜像，则必须遵守这些实践。</p>
<div class="blog_h3"><span class="graybg">朝生暮死的容器</span></div>
<p>容器的生命周期应该尽可能的短暂，创建、启动、停止、删除应当消耗最小化的时间。这意味着你应该尽可能的通过Dockerfile把内容放在镜像内。</p>
<div class="blog_h3"><span class="graybg">使用.dockerignore文件</span></div>
<p>大部分情况下，可以把Dockerfile放置在空白的目录中，后续仅在此目录中存放构建镜像时所必须的文件。 </p>
<p>如果目录中包含一些你需要排除出构建过程的目录/文件，可以编写一个.dockerignore文件，其格式类似于 .gitignore。</p>
<div class="blog_h3"><span class="graybg">避免安装不必要的包</span></div>
<p>尽可能少的安装Linux软件包，这样可以降低镜像的大小、构建过程的耗时</p>
<div class="blog_h3"><span class="graybg">一个容器一个进程</span></div>
<p>大部分情况下，你应该在单个容器中运行仅单个程序（例如Web服务、DB服务）。将应用程序解耦到不同容器中可以更好的实现容器重用、水平扩展。</p>
<p>当一个程序依赖于另外一个时，可以使用容器链接（container linking）。</p>
<div class="blog_h3"><span class="graybg">最小化层数</span></div>
<p>应当十分谨慎的考虑Dockerfile使用的层数（Number of layers），在Dockerfile可读性（可维护性）和最小化层数之间寻求平衡。</p>
<div class="blog_h3"><span class="graybg">排序命令行参数</span></div>
<p>尽可能的按照字典序对参数（特别是apt安装的软件包参数）进行排序，这样可以避免后续维护者添加重复的安装包。</p>
<div class="blog_h3"><span class="graybg">构建缓存</span></div>
<p>在构建镜像时，Docker会遍历你的Dockerfile的指令，检查完这些指令后，它会到缓存中寻找已经构建的镜像，而不是构建“重复”镜像。要避免使用缓存，可以在docker build时指定<pre class="crayon-plain-tag">--no-cache=true</pre> 参数。</p>
<p>如果使用缓存，你必须明白它是如何寻找“重复”镜像的：</p>
<ol>
<li>选取与当前Dockfile使用同一Base镜像的缓存镜像列表，遍历此列表，如果某个条目的构建指令与当前Dockerfile完全一致，则该条目可能是“重复镜像”</li>
<li>对于ADD、COPY指令，目标文件集合被检查并计算Checksum（不包含修改时间、最后访问时间信息），如果某个缓存镜像条目对应的ADD、COPY指令操控的文件集合的Checksum与前面的Checksum一致，则该条目匹配“重复”镜像</li>
</ol>
<p>当找不到匹配的镜像时，Docker将从头开始，构建一个新镜像。</p>
<div class="blog_h3"><span class="graybg">指令相关的建议</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 10%; text-align: center;">指令</td>
<td style="text-align: center;">最佳实践</td>
</tr>
</thead>
<tbody>
<tr>
<td>FROM</td>
<td>如果可能，使用Docker官方仓库作为基本镜像的来源，推荐使用debian:latest，因为此镜像作为一个完整发行版，一直并很好的控制并保持最小化（目前150MB）</td>
</tr>
<tr>
<td>LABEL</td>
<td>
<p>可以为镜像添加标签，以便更好的按照项目来组织镜像、记录License信息、辅助自动化。对于每个标签，以LABEL指令开头，后面跟随一个或者多个键值对信息。下面是一些示例：</p>
<pre class="crayon-plain-tag"># 在同一行中声明多个标签
LABEL cc.gmem.version="0.0.1-beta" cc.gmem.release-date="2015-02-12"
# 除了使用引号包围含有空格的字符串以外，还可以使用转义的方式：
LABEL vendor=Gmem\ Studio</pre>
</td>
</tr>
<tr>
<td>RUN</td>
<td>
<p>为了可读性、可维护性，应该把复杂的RUN语句编写在多行中，并用反斜杠分隔
<p><em><strong>apt-get</strong></em></p>
<p>大部分RUN指令都是调用apt-get来安装软件包的。注意避免使用upgrade、dist-upgrade子命令，因为某些基本的软件包不能在非特权容器中升级。</p>
<p>应当总是同时使用update子命令和install子命令，例如：</p>
<pre class="crayon-plain-tag">RUN apt-get update &amp;&amp; apt-get install pkg-bar</pre>
<p>因为单独使用RUN apt-get update指令，会导致软件包元信息驻留Docker缓存，以后的构建可能不更新软件包元信息。当然，你也可以指定软件包版本，这也可能避免上述缓存问题</p>
<p>一个RUN指令规范例子如下：</p>
<p><pre class="crayon-plain-tag"># 在同一个指令中完成update和install
RUN apt-get update &amp;&amp; apt-get install -y \
    # 按字典序排列软件包，便于维护
    aufs-tools \
    build-essential \
    curl \
    dpkg-sig \
    ruby1.9.1 \
    ruby1.9.1-dev \
    s3cmd=1.1.* \ # 指定软件包版本
 &amp;&amp; rm -rf /var/lib/apt/lists/*  
    # 清理apt缓存，减少镜像尺寸。
    # 注：Docker官方提供的Debian/Ubuntu镜像自动运行apt-get clean，因此不需要这里的rm命令</pre>
</td>
</tr>
<tr>
<td>CMD</td>
<td>
<p>该指令用于运行镜像中包含的软件，调用格式一般为：<pre class="crayon-plain-tag">CMD ["executable", "param1", "param2"…]</pre> </p>
<p>对于那些作为服务的软件，应该让它在前端执行（而不是在fork出的子进程中执行），例如运行Apache时应该使用指令<pre class="crayon-plain-tag">CMD ["apache2","-DFOREGROUND"</pre> </p>
<p>对于大部分其它软件，CMD指令应该给出一个交互式的Shell，例如<pre class="crayon-plain-tag">CMD ["perl", "-de0"]</pre> 、<pre class="crayon-plain-tag">CMD ["python"]</pre> </p>
</td>
</tr>
<tr>
<td>EXPOSE</td>
<td>
<p>该指令声明容器用来监听外部连接的端口，你应该使用约定俗成的端口：</p>
<p><pre class="crayon-plain-tag"># 暴露一个端口范围
EXPOSE 7000-8000
# 暴露一个端口
EXPOSE 8080 </pre>
</td>
</tr>
<tr>
<td>ENV</td>
<td>
<p>可以使用该指令更新PATH环境变量的值，以简化软件的调用。例如：<pre class="crayon-plain-tag">ENV PATH /usr/local/nginx/bin:$PATH</pre> 可以让Nginx通过简单的指令<pre class="crayon-plain-tag">CMD [“nginx”]</pre> 运行。</p>
<p>某些软件需要环境变量的设置，亦可通过该指令完成</p>
</td>
</tr>
<tr>
<td>ADD<br />COPY</td>
<td>
<p>两者功能类似，COPY应该更优先使用，因为它比ADD简单直观。COPY仅支持将本地文件拷贝到容器内这样的基本操作，而ADD则支持一些高级特性：</p>
<ol>
<li>解压本地Tar到容器中</li>
<li>远程URL支持</li>
</ol>
<p>最常见的需要使用ADD的场景是解压文件到镜像中：<pre class="crayon-plain-tag">ADD rootfs.tar.xz /.</pre> </p>
<p>如果在Dockerfile中存在多个步骤需要复制文件，不要把它们合并到单个COPY指令中。这样做的原因还是缓存，分开COPY可以减少不必要的缓存失效</p>
</td>
</tr>
<tr>
<td>ENTRYPOINT</td>
<td>
<p>该指令最佳应用场景是设置镜像的main命令，让容器像一个可执行文件那样运行。你可以配合使用CMD指令，来指定main命令的默认选项：</p>
<pre class="crayon-plain-tag">ENTRYPOINT ["s3cmd"]
CMD ["--help"]</pre>
<p>这样，你就可以运行镜像，而不指定需要执行的命令：</p>
<pre class="crayon-plain-tag"># 这里的s3cmd是镜像名，而不是s3cmd命令
docker run s3cmd
# 效果上等同于docker run s3cmd  s3cmd --help

# 你也可以传递任何其它选项
docker run s3cmd ls s3://gmem</pre>
<p>由于镜像名、命令名相同， 省略了其中之一的效果，就好像docker run在直接执行一个命令</p>
<p>也可以将ENTRYPOINT设置为一个脚本文件的路径：<pre class="crayon-plain-tag">ENTRYPOINT ["/docker-entrypoint.sh"]</pre> ，此脚本中的<pre class="crayon-plain-tag">$@</pre> 对应docker run imgname后面的整个参数数组</p>
</td>
</tr>
<tr>
<td>VOLUME</td>
<td>使用该指令暴露所有数据库的存储区域、配置文件的存储、以及容器创建的文件/目录，总之，所有容易变化的文件系统部分，都应该使用该指令暴露</td>
</tr>
<tr>
<td>USER</td>
<td>
<p>如果容器运行的服务不需要特权，则应该使用该指令指定一个非root用户，但是注意要在Dockerfile中预先添加这样的用户，例如：</p>
<pre class="crayon-plain-tag">RUN groupadd -r postgres &amp;&amp; useradd -r -g postgres postgres</pre>
<p>注意：自动分配的UID/GID在多次的镜像构建之间并不保持唯一 ，因此你最好手工指定UID/GID</p>
<p>避免安装/使用sudo，因为其不确定的TTY、信号转发行为会导致一些问题。如果必须使用类似于sudo的功能，可以安装<a href="https://github.com/tianon/gosu">gosu</a></p>
</td>
</tr>
<tr>
<td>WORKDIR</td>
<td>为了简洁、明确，使用WORKDIR指令时，应该总是使用绝对路径。应该使用WORKDIR来代替任何<pre class="crayon-plain-tag">RUN cd … &amp;&amp; do-sth</pre> 风格的指令</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">存储驱动</span></div>
<p>要有效利用Docker的存储机制，首先需要搞清楚它是如何构建、存储镜像的，然后要理解镜像是如何被容器使用的。</p>
<div class="blog_h2"><span class="graybg">镜像与层</span></div>
<p>一个镜像文件由一系列层层叠加的、只读的层（Layers）构成，这些层共同组成了镜像的根文件系统。Docker的存储驱动负责管理这些层，对外提供单一的文件系统视图。</p>
<p>当你创建一个容器时，Docker会在镜像的层叠之上，添加一个薄的、可读写的<span style="background-color: #c0c0c0;">容器层</span>（container layer）。容器运行过程中对文件系统的任何更改（增加、修改、删除文件），都持久化到容器层中。</p>
<p>所有镜像、容器的层都位于本地文件系统中，由存储驱动管理。在Linux下，默认存储目录为/var/lib/docker/。</p>
<p>下图是基于Ubuntu:15.04的容器的文件系统层叠示意图：</p>
<p><img class="aligncenter size-full wp-image-13721" src="https://blog.gmem.cc/wp-content/uploads/2015/10/container-layers.jpg" alt="container-layers" width="675" height="469" /></p>
<div class="blog_h3"><span class="graybg">内容寻址存储</span></div>
<p>Docker从1.10开始，引入了一个新的内容寻址存储（Content addressable storage，CAS）模型。该模型提供了在磁盘上寻址镜像、层数据的全新方法。在此之前，镜像、层数据使用随机的UUID来引用和存储，CAS则使用内容哈希值（content hash）来引用镜像、层数据。</p>
<p>CAS增强了安全性，内置了防止ID冲突的机制，并且能在pull、push、load、save操作后保证数据完整性。它也提供了更好的层共享机制——允许很多镜像自由的共享层，即时这些层来自不同的构建。</p>
<p>使用CAS后，所有层的ID均变成基于其内容的哈希值。但是，容器的ID仍然是随机的UUID。</p>
<p>这一变化导致，从老版本的Docker升级到1.10后：</p>
<ol>
<li>既有镜像需要迁移：由于层ID的生成规则发生变化，老版本Docker从仓库Pull下来的镜像的ID必须重新生成。新版本的Docker守护程序在初次运行时会自动执行这一迁移操作。迁移操作完毕后，所有镜像、标签具有新的secure IDs。如果本地镜像非常多，迁移操作可能消耗很长的时间才能完成，这期间Docker守护程序无法响应外部请求</li>
</ol>
<div class="blog_h2"><span class="graybg">容器与层</span></div>
<p>从存储角度来看，镜像与容器的主要区别就在于后者的Layer栈顶部包含一个薄的可读写层。当容器创建后，该读写层一同创建；当容器删除后，该读写层也被删除；容器运行时，所有对文件系统的变更都落到这个读写层中。</p>
<p>由于一个镜像的所有容器具有自己的可读写层，因而它们能够安全的共享底层的镜像。Docker的存储驱动负责管理所有的层（不管是镜像还是容器的），如何管理取决于具体的驱动，但是两个关键技术是通用的：1、可层叠镜像层（stackable image layers）；2、copy-on-write</p>
<div class="blog_h2"><span class="graybg">copy-on-write策略</span></div>
<p>这个策略在软件系统中很常见。例如在操作系统中，两个使用相同数据块的进程，可以安全共享数据块的单份拷贝。只有当其中一个进程需要对共享数据进行修改（而另外一个进程不进行修改）时，才需要共享数据的额外拷贝。</p>
<p>Docker同时对镜像、容器使用此CoW策略，以便节约存储空间并缩短容器启动时间。CoW依赖于存储驱动的支持，不是所有驱动支持CoW。</p>
<div class="blog_h3"><span class="graybg">共享：更小的镜像</span></div>
<p>执行pull/push子命令时，Docker客户端会报告其操作的Layer：</p>
<pre class="crayon-plain-tag">docker pull ubuntu:15.04
# 9502adfba7f1: Pull complete 
# 4332ffb06e4b: Pull complete 
# 2f937cc07b5f: Pull complete 
# a3ed95caeb02: Pull complete 
# Digest: sha256:2fb27e433b3ecccea2a14e794875b086711f5d49953ef173d8a03e8707f1510f
# Status: Downloaded newer image for ubuntu:15.04</pre>
<p>可以看到，拉取Ubuntu镜像时，实际上下载了4个Layer，它们共同组成了完整的系统镜像。</p>
<p>如果使用AUFS驱动，在1.10版本之前，Layer以其ID为目录名，存放在本地存储的子目录/var/lib/docker/aufs/layers中。</p>
<p>不管Docker引擎的版本是多少，相同的Layer都会被共享，而不是重新拉取。</p>
<div class="blog_h3"><span class="graybg">拷贝：高效的容器</span></div>
<p>前面提到，对容器文件系统的所有修改都写在一个薄的顶部层中，来自镜像的层都是只读的，这意味着多个容器可以共享一个镜像。</p>
<p>当容器修改了一个文件后，Docker利用存储引擎来执行CoW策略，具体细节取决于引擎。对于AUFS、OverlayFS这两种存储引擎来说，CoW的大概步骤如下：</p>
<ol>
<li>从顶层向下，逐层寻找，定位到被修改的文件</li>
<li>将找到的文件拷贝（Copy-up）到顶部的可写层</li>
<li>修改位于可写层中的文件副本</li>
</ol>
<p>Copy-up操作可能带来重大的性能影响，影响程度取决于存储驱动。但是<span style="background-color: #c0c0c0;">过大的文件、过多的层、过深的目录树</span>都会加重影响程度。不过Copy-up操作对一个文件至多发生一次。</p>
<p>CoW让容器共享镜像，这一方面让容器本身的尺寸很小，另一方面则让容器高效的创建、执行，因为不需要牵涉到太多的I/O操作。</p>
<div class="blog_h2"><span class="graybg">数据卷与存储驱动</span></div>
<p>当容器被删除后，所有没有存储在数据卷（Data volume）中的数据都会被删除。数据卷是宿主机上的一个目录或者文件，它被直接挂载到容器的目录树中。</p>
<p>多个容器可以共享数据卷，但是要注意并发修改的问题。</p>
<p>数据卷不受存储驱动的控制，对其进行的I/O操作绕过存储驱动，直接由宿主机执行，其速度和宿主机上普通I/O操作一样快。</p>
<div class="blog_h2"><span class="graybg">存储驱动选型</span></div>
<div class="blog_h3"><span class="graybg">可拔插存储驱动架构</span></div>
<p>为了基于实际运行环境选择最好的驱动，Docker设计了可拔插的存储驱动架构。存储驱动基于一个Linux文件系统或者卷管理器，它们可以自由的实现镜像/容器Layer的管理。</p>
<p>在决定使用哪种驱动后，你需要设置Docker守护程序的启动参数。修改/etc/default/docker中的DOCKER_OPTS，添加<pre class="crayon-plain-tag">--storage-driver=&lt;sd_name&gt;</pre> 选项。</p>
<p>注意一个<span style="background-color: #c0c0c0;">Docker守护程序同时只能使用一种存储驱动</span>。执行命令：</p>
<pre class="crayon-plain-tag">docker info | grep "Storage Driver"
# Storage Driver: aufs</pre>
<p>可以看到当前使用的存储驱动。上例中的aufs为存储驱动的名称，存储技术与存储驱动名称对照表如下： </p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">存储技术</td>
<td style="width: 15%; text-align: center;">存储驱动名称</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>OverlayFS</td>
<td>overlay<br />overlay2</td>
<td>
<p>内核的一部分，速度最快的UnionFS。支持页共享缓存，多个容器访问同一个文件时，可共享相同的页面缓存，因而其<span style="background-color: #c0c0c0;">内存利用效率很高</span></p>
<p>overlay2是overlay的升级版，在4.0内核之后，添加额外的特性，防止过多的索引节点（inode）消耗</p>
</td>
</tr>
<tr>
<td>AUFS</td>
<td>aufs</td>
<td>
<p>Ubuntu/Debian系统默认的存储驱动，历史悠久、稳定</p>
<p>允许容器之间共享公共库的内存，因此如果有<span style="background-color: #c0c0c0;">千百个相同的容器，其内存效率比较高</span></p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<td>Btrfs</td>
<td>btrfs</td>
<td> </td>
</tr>
<tr>
<td>Device Mapper</td>
<td>devicemapper</td>
<td>
<p>Redhat/Fedora系统默认的存储驱动</p>
<p>Device Mapper是内核中支持LVM的通用设备映射机制，它为块设备驱动提供了一个模块化的内核架构</p>
<p>基于块设备而非文件系统，内置配额支持，其它驱动则不支持</p>
</td>
</tr>
<tr>
<td>VFS</td>
<td>vfs</td>
<td> </td>
</tr>
<tr>
<td>ZFS</td>
<td>zfs</td>
<td> </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">共享存储与存储驱动</span></div>
<p>许多企业使用SAN、NAS之类的共享存储技术以提高性能和可靠性，或者实现Thin Provisioning（避免预分配过多的容量）、数据复制、压缩等高级特性。</p>
<p>Docker存储驱动、数据卷均可以在这些共享存储系统之上工作，但是Docker不能直接与底层的共享存储技术集成。你需要选择与共享存储技术匹配的存储驱动。</p>
<div class="blog_h3"><span class="graybg">存储驱动对比</span></div>
<p><img class="aligncenter size-large wp-image-13750" src="https://blog.gmem.cc/wp-content/uploads/2015/10/driver-pros-cons-1024x464.png" alt="driver-pros-cons" width="710" height="321" /></p>
<div class="blog_h3"><span class="graybg">Overlay与Overlay2</span></div>
<p>OverlayFS是目前使用比较广泛的层次文件系统，实现简单，<span style="background-color: #c0c0c0;">读写性能较好</span>，并且稳定。</p>
<p>与OverlayFS相关的存储驱动有两个：overlay、overlay2。前者的缺陷包括inode耗尽和commit performance，后者就是为了解决这两个问题而生，但是需要4.0或者更高版本的Linux内核。</p>
<div class="blog_h2"><span class="graybg">AUFS驱动</span></div>
<p>AUFS是第一个出现的Docker驱动，它非常稳定，在生产环境下有很多部署案例，并且社区支持很好。AUFS的优势包括：</p>
<ol>
<li>容器启动时间短</li>
<li>存储利用效率高</li>
<li>内存利用效率高</li>
</ol>
<p>对于PaaS或者其它需要<span style="background-color: #c0c0c0;">高密度容器实例</span>的应用场景，AUFS是很好的选择。但是由于CoW策略，第一次写文件操作可能带来很大的开销。</p>
<p>由于AUFS不在Linux内核主线中，因此某些发行版不会自带AUFS，需要手工下载和安装。</p>
<div class="blog_h3"><span class="graybg">安装与配置</span></div>
<p>要验证当前系统是否支持AUFS，可以执行命令：</p>
<pre class="crayon-plain-tag">cat /proc/filesystems | grep aufs</pre>
<p>如果上述命令没有输出，先确保你的内核版本高于3.13，并参考如下命令安装：</p>
<pre class="crayon-plain-tag">sudo apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual</pre>
<p>安装完毕后，修改Docker守护程序的参数：</p>
<pre class="crayon-plain-tag">sudo dockerd --storage-driver=aufs</pre>
<p>上面的参数修改不是持久化的，要永久的修改，需要打开Docker配置文件，添加：</p>
<pre class="crayon-plain-tag">DOCKER_OPTS="--storage-driver=aufs" </pre>
<div class="blog_h3"><span class="graybg">镜像分层与共享</span></div>
<p>AUFS是一种联合文件系统（UFS），它管理<span style="background-color: #c0c0c0;">单个Linux宿主机</span>上的多个目录，并将其叠加在一起，在单个挂载点形成统一的视图。与UnionFS、OverlayFS类似，AUFS是一种union mounting实现。AUFS管理的每个Linux宿主机目录称为联合挂载点（union mount point）或者分支（branch）。</p>
<p>AUFS的思想与Docker的Layer机制天然吻合——每个branch对应一个镜像/容器的Layer。Copy-up操作实质上就是在宿主机文件系统的不同目录之间复制文件。</p>
<div class="blog_h3"><span class="graybg">容器读写</span></div>
<p>由于AUFS在文件级别上操作，这意味着，即使在仅仅修改文件一小部分的情况下，CoW操作也需要复制整个文件。因此，当写入一个体积很大的文件时，AUFS会遭遇一次性的性能问题。</p>
<p>当删除文件时，AUFS在顶层Layer添加一个所谓without文件，该文件命名为<pre class="crayon-plain-tag">.wh.filename</pre> ，用来标记目标文件在容器中被删除。</p>
<div class="blog_h3"><span class="graybg">本地存储</span></div>
<p>使用AUFS时，镜像、容器的文件默认被存放到dockerd所在机器的/var/lib/docker/aufs/目录下 。</p>
<div class="blog_h1"><span class="graybg">网络配置</span></div>
<div class="blog_h2"><span class="graybg">默认网络</span></div>
<p>安装Docker之后，会自动创建bridge、none、host这三个网络，可以通过命令<pre class="crayon-plain-tag">docker network ls</pre> 查看。如果启动容器时不指定<pre class="crayon-plain-tag">--network</pre> 参数，默认使用bridge网络，bridge在宿主机网络栈中映射为docker0。none表示容器不连接到任何网络，其本地网络设备仅仅lo这个环回网卡。host网络则把容器添加到宿主机网络栈中，在容器中执行ifconfig你会看到输出与宿主机一致。</p>
<p>在三个默认网络中，通常你只会和bridge做交互。这些网络不能被删除，因为Docker本身需要使用。</p>
<p>你可以创建其它自定义网络，并在不需要的时候删除它们。</p>
<div class="blog_h2"><span class="graybg">自定义网络</span></div>
<p>为了更好的隔离容器，可以创建自定义网络。利用Docker提供的网络驱动，你可以创建桥接（Bridged）、重叠（Overlay）、MACVLAN等类型的网络。</p>
<p>自定义网络可以创建多个，每个容器也可以加入到多个网络中。容器仅仅能在网络内部而不能跨网络通信。当容器连接到多个网络时，其外网连接性由第一个（词法序）具有外部连接能力的网络提供。</p>
<div class="blog_h2"><span class="graybg">桥接网络</span></div>
<p>Linux内核支持虚拟网桥，可以连接多个网络接口，功能类似于交换机。</p>
<p>在自定义网络中，桥接是最简单的一种。添加到桥接网络的容器，必须位于<span style="background-color: #c0c0c0;">同一台宿主机上</span>。网络中的所有容器可以相互通信，但是这些容器不能访问外部网络。</p>
<p>与Docker0不同，自定义桥接网络不支持--link。但是你可以暴露/发布容器的端口，这样桥接网络的一部分可以被外部访问。<span style="background-color: #c0c0c0;">自定义桥接网络嵌入了DNS服务器</span>，容器之间可以通过名字访问。</p>
<p>当你需要基于单宿主机建立一个简单的网络时，可以考虑自定义桥接网络。</p>
<p>桥接网络的示例：</p>
<pre class="crayon-plain-tag">docker network create -d bridge --subnet=172.21.0.0/16 --gateway=172.21.0.99 local </pre>
<div class="blog_h3"><span class="graybg">docker_gwbridge</span></div>
<p>这个特殊的本地桥接网络，在以下两种情况下，由Docker自动创建：</p>
<ol>
<li>当你初始化或者加入到一个swarm时，Docker创建docker_gwbridge，以便跨主机的进行swarm节点之间的通信</li>
<li>如果容器所加入的任何网络都不具有外部连接性，Docker把容器连接到docker_gwbridge</li>
</ol>
<p>你可以提前手工创建docker_gwbridge网络，进行定制化配置。</p>
<p>当使用overlay网络时，docker_gwbridge总是存在。</p>
<div class="blog_h2"><span class="graybg">覆盖网络</span></div>
<p>利用Overlay网络可以跨越多台宿主机组网。</p>
<div class="blog_h3"><span class="graybg">基于swarm</span></div>
<p>在swarm模式下运行Docker引擎时，你可以在管理节点上创建overlay网络，这种网络不需要外部的key-value存储。</p>
<p>swarm可以让overlay网络仅仅对需要它以提供一个服务的swarm节点可用。当你创建一个使用overlay网络的服务时，管理节点会自动扩展overlay网络以覆盖运行服务的节点。</p>
<p>下面的命令示例如何在swarm管理节点创建overlay网络，并在服务中使用它：</p>
<pre class="crayon-plain-tag"># 创建一个overlay网络
docker network create --driver overlay --subnet 10.0.9.0/24 my-multi-host-network
# 创建一个Nginx服务，并把刚刚创建的网络扩展到运行Nginx服务的那些节点
docker service create --replicas 2 --network my-multi-host-network --name my-web nginx</pre>
<p>注意：这种overlay网络对docker run启动的容器是不可用的，目标容器必须是swarm模式服务的一部分。</p>
<p>基于swarm模式的overlay网络默认具有安全保证。swarm节点使用gossip协议来交换overlay网络的信息，并且使用GCM模式的AES算法加密gossip协议，管理节点默认每12小时更换密钥。</p>
<p>如果要加密容器通过overlay网络交换的信息，需要使用选项：</p>
<pre class="crayon-plain-tag">docker network create --opt encrypted --driver overlay  nw</pre>
<p>上述命令将自动为参与到overlay网络的节点创建IPSEC通道，这些通道也使用GCM模式的AES算法，每12小时更换密钥。 </p>
<div class="blog_h3"><span class="graybg">基于外部KV存储</span></div>
<p>此方式的overlay网络与swarm模式的overlay网络不兼容。主要用于需要考虑兼容性的场景。</p>
<p>当不在swarm模式下使用Docker引擎时，启用overlay网络依赖于外部key-value存储的支持，这些存储包括Consul、Etcd、ZooKeeper。你需要手工安装这些存储，并确保它们可以和Docker宿主机自由通信。</p>
<p>宿主机必须开启4789、7946端口。如果启用加密的overlay网络（--opt encrypted）则需要允许protocol50(ESP)流量。此外KV服务也需要暴露端口。</p>
<p>各宿主机上的Docker引擎守护程序，需要配置dockerd选项以支持overlay网络：</p>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--cluster-store=PROVIDER://URL</td>
<td>指名KV服务的位置</td>
</tr>
<tr>
<td>--cluster-advertise=HOST_IP|HOST_IFACE:PORT</td>
<td>指定用于集群的宿主机网络接口</td>
</tr>
<tr>
<td>--cluster-store-opt=KEY-VALUE OPTIONS</td>
<td>指定KV服务的配置项</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Macvlan网络</span></div>
<p>这种网络不同于overlay，可以把容器直接暴露到物理网络中。</p>
<p>首先，创建网络：</p>
<pre class="crayon-plain-tag"># 设置目标网卡为混杂模式
sudo ip link set virbr0 promisc on
# parent指定桥接到的宿主机网卡，以及子网信息
docker network create -d macvlan --subnet=10.0.0.0/8 --gateway=10.0.0.5  -o parent=virbr0 virbr0</pre>
<p>然后，运行容器： </p>
<pre class="crayon-plain-tag">docker run -it --rm --network=virbr0 --ip=10.0.0.100 ubuntu:14.04</pre>
<p>注意，如果不指定IP，则容器的IP会从当前网段的192.168.0.2开始分配，不管是否被占用。 </p>
<div class="blog_h2"><span class="graybg">网络别名</span></div>
<div class="blog_h3"><span class="graybg">--link与出站解析</span></div>
<p>连接到默认桥接网络，且以--link选项创建容器时：</p>
<ol>
<li>可以解析其它容器的名字为IP</li>
<li>可以为其它容器指定任意别名：<pre class="crayon-plain-tag">--link=CONTAINER-NAME:ALIAS</pre> </li>
<li>安全容器连接，即--icc=false</li>
<li>环境变量注入</li>
</ol>
<p>当使用自定义桥接网络时，上述特性默认启用，不需要额外的选项。并且你可以：</p>
<ol>
<li>基于网络内嵌的DNS服务器进行容器名到IP的解析</li>
<li>使用--link（仅用来）指定别名</li>
</ol>
<p>link选项的示例：</p>
<pre class="crayon-plain-tag"># 创建容器，连接到isolated_nw，并且当前容器访问container5时可以使用别名c5
docker run --network=isolated_nw -itd --name=container4 --link container5:c5 busybox
# 在连接到某个网络时，也可以指定link选项
docker network connect --link container5:foo local_alias container4</pre>
<div class="blog_h3"><span class="graybg">--network-alias</span></div>
<p>与--link不同 ，该选项可以指定当前容器在网络中的别名，网络中的其它容器都可以使用该别名：</p>
<pre class="crayon-plain-tag">docker run --network=isolated_nw -itd --name=container6 --network-alias app busybox</pre>
<p>多个容器可以声明相同的别名，这种情况下，其中一个容器（随机）会响应DNS解析。当该容器宕掉后，其它同名容器自动响应解析。这个特性可以用来实现简单的高可用性。</p>
<div class="blog_h2"><span class="graybg">自定义网络中的DNS</span></div>
<p>自定义网络中的容器的DNS查找行为与默认bridge网络不同，处于向后兼容的目的，后者的行为与旧版本保持一致。</p>
<p>自1.10版本开始，Docker守护程序嵌入了DNS服务，此服务支持基于容器link、name、net-alias配置的DNS查找。容器还可以通过--dns等选项来指定外部DNS服务器，当内嵌DNS服务器无法解析某个主机名时，会转给外部DNS处理。</p>
<div class="blog_h1"><span class="graybg">资源限制</span></div>
<p>默认情况下，容器不被施加资源限制，可以使用宿主机内核调度器允许的最大资源。Docker提供了控制内存、CPU、块I/O用量限制的方法。</p>
<div class="blog_h2"><span class="graybg">内存</span></div>
<p>你可以为容器施加硬性内存限制，确保容器不占用超过特定值的用户/系统内存。你也可以施加软性限制，允许容器按需使用内存，但当特定条件（例如内核检测到宿主机内存过低）发生时，限制容器内存占用。</p>
<p>内存限制通过docker run命令的选项给出，这些选项的值都是正整数，后面可以跟着b/k/m/g等单位：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-m    --memory</td>
<td>限制容器能够使用的最大内存量，最小值4m</td>
</tr>
<tr>
<td>--memory-swap</td>
<td>该容器可以被交换到磁盘的内存的量 </td>
</tr>
<tr>
<td>--memory-swappiness</td>
<td>0-100之间， 允许宿主机交换出容器使用的匿名页的百分比</td>
</tr>
<tr>
<td>--memory-reservation</td>
<td>指定一个小与-m的软限制，当Docker检测到宿主机存在内存争用、内存低下情况时，限制容器使用不超过此数值的内存 </td>
</tr>
<tr>
<td>--kernel-memory</td>
<td>限制容器能够使用的最大内核内存，最小值4m。注意内核内存不能被换出 </td>
</tr>
<tr>
<td>--oom-kill-disable</td>
<td>默认情况下内存溢出错误发生后，Docker会杀死容器中的进程，此选项禁用该默认行为 </td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">CPU</span></div>
<p>默认的，容器可以无限制的使用CPU时钟周期。Docker提供了若干选项，对容器的CPU使用进行限制。这些选项支持CFS调度器，自1.13开始实时调度器也支持某些选项。</p>
<div class="blog_h3"><span class="graybg">CFS相关选项</span></div>
<p>CFS是用于大部分Linux进程的调度器，Docker提供以下容器选项：</p>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 25%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--cpus</td>
<td>
<p>指定容器可以使用多少CPU资源。如果宿主机具有2个CPU，你可以设置该选项为1.5表示容器最多使用1个半CPU的运算能力</p>
<p>--cpus=1.5和配置--cpu-period="100000" --cpu-quota="150000"等价</p>
</td>
</tr>
<tr>
<td>--cpu-period</td>
<td>指定CFS调度周期数</td>
</tr>
<tr>
<td>--cpu-quota</td>
<td>指定CFS调度配额数</td>
</tr>
<tr>
<td>--cpuset-cpus</td>
<td>限制容器能够使用的CPU核心，0-3表示可以使用第1-4个CPU核心，1,3表示可以使用第二个、第四个</td>
</tr>
<tr>
<td>--cpu-shares</td>
<td>
<p>默认值1024，设置更大或者更小的值，可以调整容器可以访问的CPU时钟周期的相对权重</p>
<p>如果足够的空闲CPU周期存在，此选项不会限制容器的CPU使用</p>
<p>在Swarm模式下，该选项不会限制容器被调度</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">实时调度器相关选项</span></div>
<p>自1.13版本开始，Docker支持配置容器使用实时调度器。作为前提条件，宿主机内核选项CONFIG_RT_GROUP_SCHED必须开启。</p>
<p>要使用实时调度器运行容器，需要设置dockerd选项<pre class="crayon-plain-tag">--cpu-rt-runtime</pre> 来指定在某个运行周期内，为实时任务保留的毫秒数。对于默认的10000微秒周期，设置--cpu-rt-runtime=95000意味着至少保留5000微秒给非实时任务使用。</p>
<p>相关配置选项：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--cap-add</td>
<td>授予容器CAP_SYS_NICE特性，这样容器可以提升进程的nice值、设置实时调度策略、设置CPU关联性（affinity）</td>
</tr>
<tr>
<td>--cpu-rt-runtime</td>
<td>在Docker守护程序的实时调度周期内，容器最多可以使用的微秒数</td>
</tr>
<tr>
<td>--ulimit</td>
<td>容器的最大实时优先级</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">多进程容器</span></div>
<p>典型的容器在启动时，仅仅会发动一个进程——例如Apache或者SSH守护进程。要在容器中运行多个服务，你可以编写脚本，或者使用进程管理工具。</p>
<p>Supervisor是一个流行的进程管理工具，使用它可以更加方便的控制、管理、重启容器中的进程。本章以一个例子来说明如何使用Supervisor来管理容器中的多个服务。</p>
<div class="blog_h2"><span class="graybg">Dockerfile</span></div>
<pre class="crayon-plain-tag"># 安装Supervisor和两个服务
RUN apt-get update &amp;&amp; apt-get install -y openssh-server apache2 supervisor
# 守护进程需要的目录
RUN mkdir -p /var/lock/apache2 /var/run/apache2 /var/run/sshd /var/log/supervisor
# 复制Supervisor配置文件到容器上下文
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# 暴露端口
EXPOSE 22 80
# 容器启动时执行supervisord
CMD ["/usr/bin/supervisord"]</pre>
<div class="blog_h2"><span class="graybg">Supervisor配置文件</span></div>
<p>该配置文件内容如下：</p>
<pre class="crayon-plain-tag">; 第一段，配置Supervisor本身
; nodaemon提示Supervisor以交互式运行，而不是守护式。这样可以确保它可以正常接收信号
[supervisord]
nodaemon=true

; 以下的每个段，分别定义一个需要被控制的服务
[program:sshd]
command=/usr/sbin/sshd -D

[program:apache2]
command=/bin/bash -c "source /etc/apache2/envvars &amp;&amp; exec /usr/sbin/apache2 -DFOREGROUND"</pre>
<div class="blog_h1"><span class="graybg">运行时度量</span></div>
<div class="blog_h2"><span class="graybg"> docker stats</span></div>
<p>该命令可以实时监控容器的CPU、内存、网络、块I/O资源的占用情况。</p>
<div class="blog_h2"><span class="graybg">控制组Cgroup</span></div>
<p>Linux容器所依赖的内核机制——控制组（Cgroups），不仅仅支持跟踪进程组，还暴露了度量CPU、内存、块I/O的接口。</p>
<p>控制组通过一个伪文件系统<pre class="crayon-plain-tag">/sys/fs/cgroup</pre> 暴露，每一个子目录对应了一种Cgroup层次（子系统）。某些老的系统挂载位置可能有不同，你可以执行<pre class="crayon-plain-tag">grep cgroup /proc/mounts</pre> 命令以查看。</p>
<p>在每个子系统内部，可以存在多级子目录。这些目录的最深处，会包含1-N个伪文件，其中包含了统计信息。</p>
<p>查看文件/proc/cgroups可以查看系统中已经支持的Cgroup层次。输出中包含Cgroup子系统名称、包含的组数量等信息。</p>
<p>查看文件/proc/$PID/cgroup可以查看某个进程所属的Cgroup。输出 / 表示没有划分到特定的Cgroup，/lxc/pumpkin可能意味着进程属于容器pumpkin的成员。</p>
<div class="blog_h3"><span class="graybg">内存度量</span></div>
<p>子系统memory提供内存的统计信息。由于memory控制组会增加一定的overhead，因此某些发行版默认情况下禁用了它。你可能需要添加内核参数：</p>
<pre class="crayon-plain-tag">cgroup_enable=memory swapaccount=1</pre>
<p>该子系统对应的伪文件是memory.stat。不包含total_前缀的数据项，与当前Cgroup中的进程有关，包含total_d前缀的数据项，则与当前Cgroup、所有子代Cgroup中的进程有关。</p>
<p>常用数据项列表如下：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">度量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>cache</td>
<td>使用的系统缓存量，这些缓存代表映射到块文件系统中的内存页。当你读写文件（open/write/read系统调用）、进行内存映射（mmap系统调用）、挂载tmpfs时，该数据项值增加</td>
</tr>
<tr>
<td>rss</td>
<td>没有映射到块文件系统的内存页，包括栈、堆、匿名内存映射</td>
</tr>
<tr>
<td>mapped_file</td>
<td>Cgroup中进程映射的内存的量</td>
</tr>
<tr>
<td>pgfault, pgmajfault</td>
<td>
<p>Cgroup中进程触发页错误（page fault）、页major fault的次数</p>
<p>当进程访问不存在（例如访问无效地址，这可能导致进程接收附带Segmentation fault消息的SIGSEGV信号进而被杀死）、或者被保护（进程读取当前已经被换出的页）的虚拟内存空间时，会触发页错误</p>
<p>Major错误在内核真实的从磁盘读取页时发生</p>
</td>
</tr>
<tr>
<td>swap</td>
<td>Cgroup中进程当前使用的交换文件大小</td>
</tr>
<tr>
<td>active_anon, inactive_anon</td>
<td>
<p>被内核识别为活动、非活动的匿名内存的量。所谓匿名内存是指没有和磁盘页关联的内存</p>
<p>页一开始的状态是活动的，内核会定期扫描内存，并把某些页标记为非活动。一旦这些页再次被访问，立即重新标记为活动。当内存不足需要交换出磁盘时，非活动页被交换出</p>
<p>rss = active_anon + inactive_anon - tmpfs </p>
</td>
</tr>
<tr>
<td>active_file, inactive_file</td>
<td>
<p>被内核识别为活动、非活动的非内存的量</p>
<p>cache = active_file + inactive_file + tmpfs</p>
</td>
</tr>
<tr>
<td>unevictable</td>
<td>不可交换出磁盘的内存用量。某些敏感信息，例如密钥，会被mlock保护，防止被交换到磁盘上</td>
</tr>
<tr>
<td>memory_limit, memsw_limit</td>
<td>不是真正的度量信息，而是应用到Cgroup的资源限制。前者为物理内存用量限制，后者为物理内存+Swap</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">CPU度量</span></div>
<p>该子系统对应的伪文件是cpuacct.stat，包含容器中进程累计的CPU使用信息。user、system分别表示用户空间、系统空间中代码消耗的时间。时间的单位为jiffies，在X86上通常为10ms。</p>
<div class="blog_h3"><span class="graybg">块I/O度量</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 25%; text-align: center;">度量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>blkio.sectors</td>
<td>Cgroup中进程读取/写入的512字节的扇区总数</td>
</tr>
<tr>
<td>blkio.io_service_bytes</td>
<td>Cgroup中进程读取/写入的字节数，每个设备包含4个数据，分别对应同步/异步的读/写</td>
</tr>
<tr>
<td>blkio.io_serviced</td>
<td>Cgroup中进程读取/写入操作次数，每个设备包含4个数据，分别对应同步/异步的读/写</td>
</tr>
<tr>
<td>blkio.io_queued</td>
<td>
<p>Cgroup中进程发起的、正在排队的I/O操作数量。如果Cgroup没有执行任何I/O操作，则计数为0，如果Cgroup正在执行IO操作，计数可能为0——例如正在空闲设备上执行纯同步操作</p>
<p>注意此计数是一个相对值。可以用来判断哪个容器在给IO系统施加压力</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">网络度量</span></div>
<p>Cgroup没有直接暴露网络度量信息。尽管内核可以计算出一个Cgroup中进程收发的网络包数量，但是意义不大。你可能需要基于网络接口的度量，因为lo接口上的流量没有太大价值。</p>
<p>单个Cgroup中的进程可以属于多个网络名字空间（ network namespace），多个网络名字空间意味着多个lo甚至eth0，这导致难以收集Cgroup的网络流量。</p>
<p>你可以基于iptables规则设置计数器，然后使用命令获取度量。</p>
<p>获取网络接口级别的度量信息是可能的，因为每个容器都关联了宿主机上的一个虚拟以太网接口。但是难以知道这些接口和容器的对应关系。</p>
<p>命令<pre class="crayon-plain-tag">ip netns exec</pre> 允许你在宿主机上，进入任意网络名字空间，执行任意命令。这意味着宿主机可以进入容器的网络名字空间。参考如下命令：</p>
<pre class="crayon-plain-tag"># $CID为容器ID，TASKS为容器下进程的PID列表
TASKS=/sys/fs/cgroup/devices/docker/$CID*/tasks
PID=$(head -n 1 $TASKS)
# 创建后面命令需要的符号连接
mkdir -p /var/run/netns
ln -sf /proc/$PID/ns/net /var/run/netns/$CID
# 在容器的网络名字空间下执行netstat命令
ip netns exec $CID netstat -i</pre>
<div class="blog_h2"><span class="graybg">容器所属的Cgroup</span></div>
<p>对于每一个容器，在每一个Cgroup子系统中均会创建一个与之对应的组。</p>
<p>如果使用最近版本的LXC工具，Cgroup名字为lxc/$container_name。</p>
<p>对于使用Cgroup的Docker容器，Cgroup路径为/sys/fs/cgroup/$subsystem/docker/$longid/，其中longid为容器完整的ID。</p>
<div class="blog_h1"><span class="graybg">Swarm</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>要使用Swarm模式，你可以安装1.12版本以上的Docker。Swarm模式用于管理Docker引擎的集群。你可以使用Docker CLI来创建Swarm、部署应用服务到Swarm、管理Swarm的行为。</p>
<div class="blog_h3"><span class="graybg">特性列表</span></div>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong>集成到Docker引擎的集群管理功能</strong></p>
<p>你可以直接使用Docker CLI来管理Swarm，不需要额外的软件或者组件</p>
</td>
</tr>
<tr>
<td>
<p><strong>去中心化设计</strong></p>
<p>Docker引擎在运行时来处理节点角色的特殊化——Manager还是Worker，而不是在部署期间。你可以用单个镜像来部署整个Swarm</p>
</td>
</tr>
<tr>
<td>
<p><strong>声明式服务模型</strong></p>
<p>Docker引擎基于一种声明式的途径来定义你的应用栈中各种服务的期望状态。例如你可以声明式的描述由三个组件构成的应用：前端Web服务、消息队列服务、数据库</p>
</td>
</tr>
<tr>
<td>
<p><strong>扩容（Scaling）</strong></p>
<p>对于每一个服务，你可以声明你期望运行的任务数（Tasks）。当Scale up/down时Docker引擎会自动add/remove任务，以维持期望的状态</p>
</td>
</tr>
<tr>
<td>
<p><strong>期望状态协调（Desired state reconciliation）</strong></p>
<p>Swam管理节点会持续监控集群的状态，并且尽可能消除当前状态与期望状态之间的差别。例如，假设你定义一个运行容器10个实例的服务，而一台运行2个实例的宿主机宕机了，此时管理节点会自动创建两个实例代替之，并把实例分配给可用的Worker节点</p>
</td>
</tr>
<tr>
<td>
<p><strong>跨主机网络支持</strong></p>
<p>你可以为服务创建Overlay网络，管理节点会在初始化、更新应用程序时，自动为容器分配对应Overlay网络上的IP地址</p>
</td>
</tr>
<tr>
<td>
<p><strong>服务发现</strong></p>
<p>管理节点为Swarm中的每个服务分配唯一的DNS名称，你可以通过Swarm中内嵌的DNS进行服务查找</p>
</td>
</tr>
<tr>
<td>
<p><strong>负载均衡</strong></p>
<p>你可以选择暴露服务的端口给外部的负载均衡器。在内部，Swarm允许你指定如何在节点之间分发服务</p>
</td>
</tr>
<tr>
<td>
<p><strong>安全性</strong></p>
<p>Swarm中节点之间的通信基于TLS认证和加密。你可以使用自签名根证书</p>
</td>
</tr>
<tr>
<td>
<p><strong>滚动更新（Rolling updates）</strong></p>
<p>你可以增量的更新节点上的服务，Swarm允许控制不同节点集上服务部署的延迟。如果出现异常情况，你可以把一个Task回滚到上一个版本</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">关键概念</span></div>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong>Swarm</strong></p>
<p>基于SwarmKit构建的、内嵌在Docker引擎中的集群管理和编排机制。参与到Swarm集群中的Docker引擎运行在Swarm模式。要切换到Swarm模式，你可以新建一个Swarm、或者加入一个既有的Swarm</p>
<p>Swarm也可以指代基于上述机制的Docker引擎（或者叫节点）集群，你在Swarm中部署服务。CLI和Docker API包含管理节点、部署和编排服务的命令</p>
<p>在普通模式下，你执行容器命令；在Swarm模式下，你编排服务。在同一个Docker引擎下，你可以同时运行独立容器、Swarm服务</p>
</td>
</tr>
<tr>
<td>
<p><strong>Node</strong></p>
<p>节点即参与到Swarm中的Docker引擎。你可以在单台物理机器上运行一个或者多个节点。通常生产环境下Swarm由跨越多台物理机器的节点组成</p>
<p>要部署应用到Swarm，你需要把服务定义（service definition）到管理节点。管理节点负责分发称为任务（Task）的工作单元给Worker节点</p>
<p>管理节点也负责执行服务编排、集群管理功能，以维持集群处于期望的状态。管理节点们会推举一个Leader节点来主导编排工作</p>
<p>Worker节点接收、执行管理节点派发的任务，默认情况下管理节点也像Worker节点一样运行服务，但是你可以将其配置为Manager-only节点。Worker节点上运行着一个代理（Agent），此代理负责报告分配给Worker的Task的状态到Manager，这样Manager就可以维持期望状态</p>
</td>
</tr>
<tr>
<td>
<p><strong>Service &amp; Task</strong></p>
<p>所谓Service，是关于需要在Worker节点上执行的Tasks的定义。服务是Swarm系统的核心结构，也是用户和Swarm交互的主要切入点</p>
<p>当定义服务时，你可以指定使用什么镜像，以及当运行容器时需要执行什么命令</p>
<p>在复制服务（ replicated services）模型下，Manager节点会基于你在期望状态中设置的Scale，分发一定数量的复制Task</p>
<p>对于全局服务（global services），Swarm在集群中每个可用节点上，运行此服务的单个Task</p>
<p>Task使用一个容器，并在其中执行特定的命令。Task是Swarm的原子调度单元。前面提到过，Swarm根据Service的Scale设定决定Task的数量并分发。分发的Task只能在某个节点上运行或者失败，而不能转移到其它节点</p>
</td>
</tr>
<tr>
<td>
<p><strong>负载均衡</strong></p>
<p>Swarm基于入口负载均衡（ingress load balancing ）暴露对外服务。你可以为服务配置PublishedPort，如果不指定Swarm可以自动分配30000-32767之间的端口</p>
<p>诸如云负载均衡器之类的外部组件，可以访问Swarm集群中<span style="background-color: #c0c0c0;">任意节点的PublishedPort</span>以使用服务，不管节点是否运行服务的Task。所有节点都会自动把入口连接路由到运行了Task的节点</p>
<p>Swarm基于内部负载均衡（internal load balancing）将请求分配给服务的实例，其依据是服务的DNS名称。Swarm内置的DNS组件会自动的给所有Service分配DNS条目</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">起步</span></div>
<div class="blog_h3"><span class="graybg">硬件和网络</span></div>
<p>准备三台宿主机，一台用作Manager，其它的用作Worker。Swarm中所有节点都必须能够访问Manager的IP地址。以下端口必须开启：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 60px; text-align: center;">端口</td>
<td style="width: 100px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>2377</td>
<td>TCP</td>
<td>此端口用于集群管理通信</td>
</tr>
<tr>
<td>7946</td>
<td>TCP/UDP</td>
<td>用于Overlay网络的流量传输</td>
</tr>
<tr>
<td>4789</td>
<td>UDP</td>
<td>用于容器入口路由网</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">创建Swarm</span></div>
<p>默认的，Swarm模式是禁用的。你可以创建新Swarm、加入既有Swarm，以使当前节点进入Swarm模式。</p>
<p>确保Docker引擎守护程序正在运行，然后登录到Manager节点，执行：</p>
<pre class="crayon-plain-tag">docker swarm init --advertise-addr 10.0.0.1</pre>
<p>这样当前节点就称为新建Swarm集群的管理节点了。管理节点使用通知地址（advertise address）来允许集群中其它节点访问Swarmkit API以及Overlay网络，因此IP地址10.0.0.1必须可以被所有其它节点访问到。如果宿主机具有单个IP地址，你可以不指定--advertise-addr，反之则必须指定。</p>
<p>执行docker info，可以看到当前Swarm的基本信息；执行docker node ls则可以看到集群中的节点列表。</p>
<div class="blog_h3"><span class="graybg">添加节点</span></div>
<p>首先在Manager节点上执行：</p>
<pre class="crayon-plain-tag"># 获取Worker的加入令牌
docker swarm join-token worker --quiet
# 输出  SWMTKN-1-5evgfnqh3xw67rtqucyq3cctxb929sqwfczv8s1gtz0cptpe5m-84evs0quy124fql5zcyxp7ppe

# 获取Manager的加入令牌
docker swarm join-token manager --quiet </pre>
<p>登录到用作Worker节点的宿主机，执行：</p>
<pre class="crayon-plain-tag">docker swarm join --token SWMTKN-1-...-... 10.0.0.1:2377</pre>
<p>作为工作节点加入时，swarm join子命令会执行以下操作：</p>
<ol>
<li>把目标节点的Docker引擎切换到Swarm模式</li>
<li>向管理节点请求一个TLS证书</li>
<li>基于宿主机名来命名节点</li>
<li>通过管理节点的通知地址、基于令牌加入目标节点到集群中</li>
<li>设置目标节点的可用性为Active，使之能够接收Task</li>
<li>扩展ingress overlay网络，使之覆盖当前节点</li>
</ol>
<p>作为管理节点加入时，执行的操作与上面类似。新的管理节点状态为Reachable，但是Swarm的Leader不变。</p>
<p>所谓令牌，是加入Swarm时需要的一个字符串，作为管理节点/工作节点加入时的令牌是不一样的。管理节点的令牌要特别注意保护。当发生以下情况下，考虑使用join-token子命令更改（rotate）令牌：</p>
<ol>
<li>令牌被意外泄漏，例如签入到版本控制系统</li>
<li>如果怀疑某个节点被入侵</li>
<li>如果期望禁止任何可能的新节点加入到Swarm</li>
<li>建议最多每6个月更换令牌</li>
</ol>
<p>下面的命令示例如何更换工作节点令牌：</p>
<pre class="crayon-plain-tag">docker swarm join-token  --rotate worker </pre>
<div class="blog_h3"><span class="graybg">查看节点</span></div>
<p>在管理节点上运行<pre class="crayon-plain-tag">docker node ls</pre> 可以查看Swarm中所有节点的基本信息。</p>
<p>输出的AVAILABILITY列表示节点的可用性：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">可用性</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Active</td>
<td>调度器可以分配任务给此节点</td>
</tr>
<tr>
<td>Pause</td>
<td>调度去不会分配新任务给此节点，但是既有的任务会保持运行</td>
</tr>
<tr>
<td>Drain</td>
<td>调度去不会分配新任务给此节点，并且节点上正在运行的任务会被停止，重新分配到Active节点上运行</td>
</tr>
</tbody>
</table>
<p>输出的MANAGER STATUS表示节点参与Raft consensus的情况：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">管理节点状态</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Leader</td>
<td>此节点是主管理节点，负责所有Swarm管理工作、编排决定</td>
</tr>
<tr>
<td>Reachable</td>
<td>此节点参与Raft consensus，如果当前的主管理节点宕机，此节点有望晋升</td>
</tr>
<tr>
<td>Unavailable</td>
<td>此节点无法和其它管理节点通信。这种情况下你要么重新添加一个管理节点，要么提升一个工作节点为管理节点</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">部署服务</span></div>
<p>所谓服务就是在特定镜像上执行的命令，而任务即服务的实例，任务的数量即服务的replicas个数。</p>
<p>在管理节点上运行：</p>
<pre class="crayon-plain-tag">docker service create --replicas 4 --name ping ubuntu:14.04 ping 10.0.0.1</pre>
<p>即可创建在Ubuntu 14.04上运行ping命令的服务，这个服务有4个实例（Task） 。</p>
<p>执行下面的命令可以查看任务执行的概要信息：</p>
<pre class="crayon-plain-tag">docker service ls

# ID            NAME  REPLICAS  IMAGE         COMMAND
# 48qbjfwd8u8r  ping  1/4       ubuntu:14.04  ping 10.0.0.1</pre>
<p>如果想让服务对Swarm外部可见，你需要暴露特定的端口。</p>
<p>你可以设置服务的环境变量、工作目录、运行身份：</p>
<pre class="crayon-plain-tag">docker service create --name ping --env MYVAR=myvalue --workdir /tmp  --user my_user  ...</pre>
<p>使用--secret选项，可以授予服务对Docker管理的Secret的访问权。</p>
<div class="blog_h3"><span class="graybg">查看服务</span></div>
<p>可以执行以下命令查看服务执行的详细信息：</p>
<pre class="crayon-plain-tag">docker service inspect --pretty  ping
docker service ps  ping</pre>
<div class="blog_h3"><span class="graybg">扩容服务</span></div>
<p>在管理节点上执行下面的命令，可以动态的修改服务的任务数量：</p>
<pre class="crayon-plain-tag">docker service scale ping=1</pre>
<div class="blog_h3"><span class="graybg">删除服务</span></div>
<p>在管理节点上执行下面的命令可以删除服务：</p>
<pre class="crayon-plain-tag">docker service rm ping</pre>
<p>注意，尽管服务被删除，执行Task的容器可能还需要一段时间执行清理工作</p>
<div class="blog_h3"><span class="graybg">滚动更新</span></div>
<p>所谓滚动更新，是指逐步的更新服务的每个实例。下面的例子演示如何从Redis 3.0.6滚动更新到Redis 3.0.7。</p>
<p>首先创建服务：</p>
<pre class="crayon-plain-tag">docker service create  --replicas 3   --name redis   --update-delay 10s  --update-parallelism 1 redis:3.0.6</pre>
<p>选项--update-delay定义了更新一个/一组任务的延迟时间，你可以使用10m30s这样的形式。 默认情况下是一个接着一个的更新，要每次更新多个Task可以使用选项--update-parallelism。</p>
<p>默认情况下，当正在更新的任务状态变为RUNNING后，调度器会调度下一个任务的更新，此步骤一直执行直到所有任务都被更新，如果更新过程中任何一个任务返回FAILED状态，则调度器暂停更新。选项--update-failure-action可以改变此行为。</p>
<p>执行下面的命令把Redis版本更改为3.0.7：</p>
<pre class="crayon-plain-tag">docker service update --image redis:3.0.7 redis

# 更新步骤：
# 1、停止第1个任务
# 2、调度此任务的更新操作
# 3、启动被更新过的任务
# 4、如果新任务返回RUNNING，继续更新下一个任务
# 5、如果新任务返回FAILED，暂停更新</pre>
<p>要重启被暂停的更新，可以执行：</p>
<pre class="crayon-plain-tag">docker service update redis</pre>
<div class="blog_h3"><span class="graybg">回滚服务</span></div>
<p>如果更新后的服务工作不正常，你可以回滚到前一个版本：</p>
<pre class="crayon-plain-tag"># --rollback 回滚服务
# --update-delay 0s 立即回滚
docker service update --rollback --update-delay 0s my_web</pre>
<div class="blog_h3"><span class="graybg">Overlay网络</span></div>
<p>当Swarm中的几个服务需要相互通信时，可以使用Overlay网络。</p>
<p>首先，在Swarm模式下的管理节点上创建Overlay网络：</p>
<pre class="crayon-plain-tag">docker network create --driver overlay my-network</pre>
<p>这样，所有管理节点都可以访问该Overlay网络了。创建服务：</p>
<pre class="crayon-plain-tag">docker service create  --replicas 3 --network my-network --name my-web nginx</pre>
<p>这样，所有运行my-web服务的Task的节点，都被Overlay网络覆盖。</p>
<div class="blog_h3"><span class="graybg">Drain节点</span></div>
<p>前面例子中的Worker节点，其可用性（availability）都是ACTIVE。Manager节点可以向ACTIVE派发任务。</p>
<p>某些时候（例如需要维护节点硬件），需要把可用性设置为DRAIN。DRAIN阻止管理节点派发新的任务，并且，停止DRAIN节点上正在运行的任务，在可用的ACTIVE节点上启动对应数量的任务副本。</p>
<p>执行下面的命令可以查看节点可用性：</p>
<pre class="crayon-plain-tag">docker node ls

# ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
# 5imye5fakqgdd7jg6erkize8a    coreos    Ready   Active        
# ap7gwtam1jjqvp3h4arco6vdz *  Zircon    Ready   Active        Leader
# bfini24kw83rzrh2e5cb33g8f    Jade      Ready   Active</pre>
<p>执行下面的命令可以设置DRAIN：</p>
<pre class="crayon-plain-tag"># 可以使用HOSTNAME或者ID
docker node update --availability drain  coreos</pre>
<p>维护完成后，重新设置为ACTIVE：</p>
<pre class="crayon-plain-tag">docker node update --availability active coreos</pre>
<div class="blog_h3"><span class="graybg">入口路由网</span></div>
<p>为了让外部资源能够轻松的访问Swarm中的服务，Docker引擎提供了方便的端口暴露机制。所有Swarm节点均参与到一个入口路由网（Ingress Routing Mesh），此路由网允许Swarm中的任意节点接收某个服务暴露端口的请求——甚至在节点没有运行此服务的任务的情况下。路由网负责把对暴露端口的请求路由到活动的服务容器中。</p>
<p>要正常使用入口路由网，需要确保TCP/UDP端口7946、UDP端口4789开放。当然，Swarm服务暴露的端口也需要被外部资源（例如负载均衡器）正常访问。</p>
<div class="blog_h3"><span class="graybg">暴露端口</span></div>
<p>要暴露端口，可以在创建服务时使用<pre class="crayon-plain-tag">--publish PUBLISHED-PORT:TARGET-PORT</pre> 选项。其中TARGET-PORT是运行服务实例（Task）的容器监听的端口，PUBLISHED-PORT则是Swarm对外暴露的端口。示例：</p>
<pre class="crayon-plain-tag"># 暴露端口8080，此端口自动转发给my-web服务运行节点上的80端口
docker service create --name my-web  --publish 8080:80 --replicas 2  nginx</pre>
<p>对于已经存在的服务，可以在更新时指定<pre class="crayon-plain-tag">--publish-add PUBLISHED-PORT:TARGET-PORT</pre> 选项来新增暴露端口。</p>
<p>暴露的端口，默认是TCP端口，可以使用以下格式指定TCP或者UDP端口：</p>
<pre class="crayon-plain-tag"># TCP
--publish 53:53
--publish 53:53/tcp
# TCP和UDP
--publish 53:53/tcp -p 53:53/udp 
# UDP
--publish 53:53/udp</pre>
<div class="blog_h3"><span class="graybg">直接暴露端口</span></div>
<p>使用入口路由网的端口暴露机制，可能不满足应用需求。你可能需要根据应用程序状态来决定如何路由请求，或者你需要对路由处理过程进行完全的控制。</p>
<p>要直接暴露服务所在运行的节点上的端口，可以使用<pre class="crayon-plain-tag">--publish mode=host</pre> 选项。如果不和<pre class="crayon-plain-tag">--mode=global</pre> 联用该选项，将难以知晓哪些节点运行了服务。</p>
<div class="blog_h3"><span class="graybg">更新镜像</span></div>
<p>在1.13版本之后，在服务创建之后你可以使用<pre class="crayon-plain-tag">service update --image</pre> 更改服务基于的镜像。较老的版本则只能重新创建服务。</p>
<p>每个镜像Tag对应了一个摘要（Digest），就像Git的Hash一样。某些标签，例如latest，其指向的摘要会改变。当你运行service update --image时，管理节点根据Tag到Docker Hub或者本地私服查询。如果：</p>
<ol>
<li>管理节点能够把Tag解析为Digest，则指示工作节点使用Digest对应的镜像来重新部署任务
<ol>
<li>如果工作节点已经缓存了此Digest对应的镜像，则使用之</li>
<li>否则，从Docker Hub或者私服拉取镜像
<ol>
<li>如果拉取成功，基于新镜像部署任务</li>
<li>否则，服务在工作节点上部署失败。Docker会尝试重新部署任务（可能在其它节点）</li>
</ol>
</li>
</ol>
</li>
<li>管理节点不能够正常解析Tag，则指示工作节点使用Tag对应的镜像重新部署任务
<ol>
<li>如果工作节点已经缓存了此Tag对应的镜像，则使用之</li>
<li>否则，从Docker Hub或者私服拉取镜像
<ol>
<li>如果拉取成功，基于新镜像部署任务</li>
<li>否则，服务在工作节点上部署失败。Docker会尝试重新部署任务（可能在其它节点）</li>
</ol>
</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">切换服务模式和Scale</span></div>
<p>可以使用--replicas选项设置服务的需要的任务数：</p>
<pre class="crayon-plain-tag">docker service create --name my_web --replicas 3 nginx</pre>
<p>可以使用--mode选项设置服务是全局模式还是复制模式：</p>
<pre class="crayon-plain-tag">docker service create --name myservice --mode global alpine top</pre>
<div class="blog_h3"><span class="graybg">为服务预留内存和CPU</span></div>
<p>选项<pre class="crayon-plain-tag">--reserve-memory</pre> 和<pre class="crayon-plain-tag">--reserve-cpu</pre> 用于声明服务要求的空闲内存、CPU数量。如果节点不满足条件，则不会被分配任务。</p>
<div class="blog_h3"><span class="graybg">挂载配置</span></div>
<p>你可以为Swarm服务创建两种类型的挂载：volume、bind。要创建挂载，指定--mount选项，如果不指定--type，默认类型为volume。</p>
<p>卷挂载（volume）是当运行任务的容器被移除后，仍然存在的存储。要利用既有的卷时，通常使用此类型的挂载：</p>
<pre class="crayon-plain-tag">docker service create  --mount src=VOLUME-NAME,dst=CONTAINER-PATH --name myservice IMAGE

# 在部署期间（调度器分发任务后、启动容器前）创建一个卷挂载：
docker service create --mount type=volume,src=VOLUME-NAME,dst=CONTAINER-PATH,volume-driver=DRIVER,\
    volume-opt=KEY0=VALUE0,volume-opt=KEY1=VALUE1  --name myservice IMAGE</pre>
<p>绑定挂载（Bind） 映射到运行服务容器的宿主机。在Swarm初始化容器前，宿主机路径必须存在。示例：</p>
<pre class="crayon-plain-tag"># 挂载为读写
docker service create --mount type=bind,src=HOST-PATH,dst=CONTAINER-PATH --name myservice IMAGE
# 挂载为只读
docker service create --mount type=bind,src=HOST-PATH,dst=CONTAINER-PATH,readonly --name myservice IMAGE</pre>
<p>绑定挂载很有用，但是也可能很危险：</p>
<ol>
<li>由于任务可能被分配到任何满足条件的节点上，而绑定挂载要求路径预先存在</li>
<li>调度器可能在任何时候重新分配某个任务 </li>
</ol>
<div class="blog_h3"><span class="graybg">管理节点</span></div>
<p>管理节点负责以下集群管理工作：</p>
<ol>
<li>维护集群状态</li>
<li>调度服务</li>
<li>提供Swarm模式的HTTP API端点服务</li>
</ol>
<p>基于Raft（一种算法），管理节点维护整个Swarm、所有运行中服务的一致性内部状态。</p>
<p>在测试环境下，你可以使用单管理节点，但是，一点此节点宕机，你需要重新创建Swarm才能恢复。</p>
<p>为了利用Swarm的容错特性，最好建立奇数节点数的管理节点。如果有多个管理节点，Swarm可以自动从管理节点宕机中恢复，没有downtime。当总计3个管理节点时，可以容忍1个宕机；当5个管理节点时，可以容忍2个宕机；当N个管理节点时，可以容忍(N-1)/2个管理节点宕机。Docker推荐每个Swarm中有7个管理节点。</p>
<div class="blog_h3"><span class="graybg">工作节点 </span></div>
<p>工作节点的唯一任务就是执行容器。默认的，管理节点同时也是工作节点。</p>
<p>要阻止派发任务给管理节点，可以将后者的可用性设置为Drain，调度器会优雅的停止Drain上运行的任务并且在其它节点上重新调度。</p>
<div class="blog_h2"><span class="graybg">服务如何工作</span></div>
<p>要在Swarm模式下部署应用程序，你需要创建一个“服务”。通常情况下，服务是某个较大应用程序的上下文中的某个“微服务“， 例如HTTP服务、数据库服务、或者任何形式的可执行程序。</p>
<p>当创建服务时，你需要指定使用什么镜像，以及在基于此镜像的容器中运行什么命令。你可以同时指定：</p>
<ol>
<li>Swarm暴露的端口，这使得服务对外可用</li>
<li>让服务可以与Swarm中其它服务进行通信的Overlay网络</li>
<li>CPU和内存限额、预留</li>
<li>滚动更新策略</li>
<li>服务的复制（replicas）份数</li>
</ol>
<div class="blog_h3"><span class="graybg">服务/任务/容器</span></div>
<p>当你在Swarm中部署服务时，Swarm接受你给出的服务定义（service definition），作为目标服务的期望状态（desired state ）。之后，Swarm调度服务，形成在节点上运行的一个或N个任务。每个任务独立于集群中其它节点运行。</p>
<p>下面是具有三个Replica的HTTP服务的例子：</p>
<p><img class="size-full wp-image-14678 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2015/10/services-diagram.png" alt="services-diagram" width="95%" /></p>
<p>容器是一个被隔离的进程，在Swarm模式的模型中，每个任务调用仅一个容器。任务就好像是一个插槽，调度器将容器插入其中。一旦容器开始运行，调度器将任务设置为RUNNING状态；如果容器未通过健康检查、停止运行，则任务也被终结。</p>
<div class="blog_h3"><span class="graybg">任务和调度</span></div>
<p>任务是Swarm调度的原子单元。当你通过创建/更新服务来声明服务的期望状态时，编排器（orchestrator）识别出被调度任务的期望状态——当你声明保持3个HTTP服务一直运行，编排器就会创建三个任务。</p>
<p>任务是一个插槽，调度器产生容器进程并填充到插槽。容器是任务的实例。任务是一种单向的机制，它从：已分配（assigned）、已准备（prepared）、正在运行（running）等一系列状态单向的前进。如果某个任务未通过健康检查或者终结，任务及其容器被编排器移除，新的副本任务以及对应的容器被创建，以满足期望状态。</p>
<p>Swarm模式底层组件包括了一般性用途的调度器、编排器。服务、任务的抽象实现，不理解容器这个概念。理论上你可以实现在非容器中运行的服务。</p>
<p>下图说明Swarm模式如何接受用户创建服务的请求，如何调度服务：</p>
<p><img class="alignnone size-full wp-image-14680" src="https://blog.gmem.cc/wp-content/uploads/2015/10/service-lifecycle.png" alt="service-lifecycle" width="95%" /></p>
<div class="blog_h3"><span class="graybg">悬挂服务</span></div>
<p>可以配置服务未悬挂的（pending），这样Swarm中没有节点可以运行该服务的任务。如果你仅仅需要防止服务被部署，只需要Scale到0，而不是尝试让服务进入pending状态。</p>
<p>下列情况下，服务会变为pending：</p>
<ol>
<li>如果所有节点被paused或者drained，那么新创建的服务会保持pending状态，直到某个节点可用。在实际情况下，第一个可用的节点会获得所有任务</li>
<li>你可以为任务保留一定量的内存，如果Swarm中所有节点都没有足够的内存，则服务保持pending状态</li>
</ol>
<div class="blog_h3"><span class="graybg">复制/全局服务</span></div>
<p>从部署份数的角度来看，服务可以分为复制（replicated）、全局（global）两种。</p>
<p>复制服务，由指定数量的任务构成。全局服务则在每个节点运行单个任务。</p>
<div class="blog_h2"><span class="graybg">管理敏感数据</span></div>
<p>Swarm模式下，我们可以使用Docker secrets管理敏感数据。所谓Secret是指一块数据，存放密码、SSH私钥、SSL证书或者其它不应该通过网络传递、明文存放在Dockerfile中的信息。</p>
<div class="blog_h1"><span class="graybg">Docker Compose</span></div>
<p>Compose是用于定义、运行多容器Docker应用程序的工具。通过编辑一个Compose文件，你可以配置应用程序的服务组件，然后，只需要单条命令，你就可以创建、启动所有需要的服务。</p>
<p>使用Compose通常包括以下三大步骤：</p>
<ol>
<li>使用Dockerfile定义应用程序的环境，以便可以在任何地方重现</li>
<li>在文件docker-compose.yml中定义构成应用的服务组件，以便它们可以在一个隔离的环境下运行</li>
<li>最后，执行<pre class="crayon-plain-tag">docker-compose up</pre> 启动整个应用</li>
</ol>
<div class="blog_h2"><span class="graybg">特性列表</span></div>
<table class=" fixed-word-wrap full-width">
<tbody>
<tr>
<td>
<p><strong>单个宿主机上的多个隔离环境</strong></p>
<p>Compose使用工程名称（project name）来隔离环境，你可以在几个上下文中使用工程名称：</p>
<ol>
<li>在开发机上，创建单个环境的多个副本</li>
<li>在持续集成（CI）服务器上，为了防止构建之间的相互干扰，你可以把构建号设置为工程名称</li>
<li>在共享主机/开发主机上，用于防止可能使用相同服务名的不同项目的相互干扰</li>
</ol>
<p>工程名称默认为工程的目录名，你可以使用-p选项或者 COMPOSE_PROJECT_NAME 环境变量设置工程名称</p>
</td>
</tr>
<tr>
<td>
<p><strong>在创建容器时保留卷数据</strong></p>
<p>Compose会保留你的服务使用的所有卷。当执行docker-compose up时，如果发现某个容器之前运行过，则将其卷复制给新容器实例，确保你在卷中的数据不丢失</p>
</td>
</tr>
<tr>
<td>
<p><strong>仅重建变化了的容器</strong></p>
<p>Compose会缓存用于创建容器的配置信息，当你重启一个没有变化的服务时，它的容器会被重用，而不是重新创建</p>
</td>
</tr>
<tr>
<td>
<p><strong>支持变量</strong></p>
<p>你可以在Compose文件中设置变量，以便为不同的运行环境定义服务组合</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">应用场景</span></div>
<p>Compose的典型应用场景包括：</p>
<table class=" full-width fixed-word-wrap">
<tbody>
<tr>
<td>
<p><strong>开发环境</strong></p>
<p>在开发软件时，能够在隔离环境中运行应用程序，并与之交互很关键。Compose很适合创建这样的环境</p>
<p>Compose文件能够配置应用程序的所有依赖（数据库、消息队列、缓存、WebService，等等）。通过一条命令，你可以为每个依赖启动一个或者多个容器</p>
</td>
</tr>
<tr>
<td>
<p><strong>自动化测试环境</strong></p>
<p>自动化测试套件是CI的重要组成部分。自动化的段对端测试要求一个运行环境，Compose可以方便的创建、销毁隔离的测试环境</p>
</td>
</tr>
<tr>
<td>
<p><strong>部署环境</strong></p>
<p>Compose典型的应用场景在开发、测试工作流中。但是，你也可以用Compose部署应用到远程Docker——包括单个Docker引擎或者整个Swarm集群</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">安装</span></div>
<p>Compose可以在macOS、Windows或者64位Linux上运行。在Linux上的安装步骤可以参考：</p>
<pre class="crayon-plain-tag">sudo curl -L "https://github.com/docker/compose/releases/download/1.11.2/docker-compose-$(uname -s)-$(uname -m)" \
    -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# 查看版本
docker-compose --version</pre>
<div class="blog_h1"><span class="graybg">Docker Machine</span></div>
<p>使用Docker Machine，你可以：</p>
<ol>
<li>在Windows或者Mac上安装、运行Docker。在1.12之前，DM是唯一在Windows/Mac上运行Docker的途径，之后这两个平台有了Native的Docker实现</li>
<li>划分/管理多台远程Docker宿主机。可以自动在宿主机上划分虚拟机，并在虚拟机上安装Docker</li>
<li>划分/管理Docker Swarm集群</li>
</ol>
<p>Docker Machine是一套工具，它允许你在虚拟主机上安装Docker引擎，以及通过docker-machine命令来管理宿主机（Machine，通常是虚拟机，这些虚拟机通常由DM创建）。你可以使用DM在非Linux系统、公司网络、数据中心、云端来创建Docker宿主机。</p>
<p>使用docker-machine命令，你可以启动、查看、停止受管理的宿主机，升级Docker客户端/守护程序。</p>
<div class="blog_h2"><span class="graybg">安装</span></div>
<div class="blog_h3"><span class="graybg">Linux</span></div>
<p>执行以下命令下载并安装：</p>
<pre class="crayon-plain-tag">curl -L https://github.com/docker/machine/releases/download/v0.10.0/docker-machine-`uname -s`-`uname -m` &gt;/tmp/docker-machine
chmod +x /tmp/docker-machine
sudo cp /tmp/docker-machine /usr/local/bin/docker-machine

# 执行命令查看版本信息
docker-machine version</pre>
<div class="blog_h2"><span class="graybg">起步</span></div>
<div class="blog_h3"><span class="graybg">使用DM运行容器</span></div>
<p>步骤通常为：</p>
<ol>
<li>创建一个新的（或者启动既有的）宿主机</li>
<li> 切换环境到宿主机</li>
<li>使用Docker客户端创建、载入、管理容器</li>
</ol>
<div class="blog_h3"><span class="graybg">创建宿主机</span></div>
<p>使用下面的命令创建新的宿主机：</p>
<pre class="crayon-plain-tag"># 在不支持Hyper-V的老Windows下或者在Mac下，使用virtualbox驱动
# 在支持Hyper-V的Windows下，使用hyperv驱动
docker-machine create --driver virtualbox default</pre>
<p>注意，在Linux下需要VirtualBox预先被安装。</p>
<p>使用命令<pre class="crayon-plain-tag">docker-machine ls</pre> 可以列出现有的虚拟机。</p>
<p>使用下面的命令，可以切换环境变量，指向新创建的宿主机：</p>
<pre class="crayon-plain-tag"># 这个命令用于获取环境变量，这些环境变量导致当前Shell的连接目标改变
docker-machine env default
# 输出
# export DOCKER_TLS_VERIFY="1"
# export DOCKER_HOST="tcp://172.16.62.130:2376"
# export DOCKER_CERT_PATH="/home/alex/.docker/machine/machines/default"
# export DOCKER_MACHINE_NAME="default"

# 执行下面的命令，切换到default这台宿主机
eval "$(docker-machine env default)"</pre>
<p>现在，你可以针对宿主机进行操作了。要停止或者启动宿主机，可以：</p>
<pre class="crayon-plain-tag">docker-machine stop default
docker-machine start default</pre>
<div class="blog_h3"><span class="graybg">连接到宿主机</span></div>
<p>你也可以连接到既有的宿主机，好处是，管理这台宿主机是，不需要每次提供URL：</p>
<pre class="crayon-plain-tag">docker-machine create --driver none --url=tcp://10.0.0.1:2376 fedora-10</pre>
<p>宿主机必须预先配置，支持基于TLS的连接。 </p>
<div class="blog_h1"><span class="graybg">安全性</span></div>
<div class="blog_h2"><span class="graybg">Docker安全性</span></div>
<p>检查Docker安全性时，有4个主要方面需要考虑：</p>
<ol>
<li>内核本身的安全性：名字空间的支持、Cgroups</li>
<li>Docker守护程序本身的攻击面（attack surface ）</li>
<li>容器配置的漏洞，这些漏洞可能是默认就附带的，或者用户定制而引入的</li>
<li>内核中固化的安全特性，这些特性如何与容器交互</li>
</ol>
<div class="blog_h3"><span class="graybg">内核名字空间</span></div>
<p>Docker容器与LXC容器很类似，它们具有相似的安全特性。当你启动容器时，Docker会为容器创建一系列的名字空间和控制组。</p>
<p>名字空间提供第一级的、最直接的隔离性——容器中运行的进程看不到，甚至不能影响到其它容器、宿主机中运行的进程。内核名字空间机制从2.6.15 - 2.6.26版本开始引入，目前已经非常稳定。</p>
<p>每个容器具有自身的网络栈（network stack），这意味着容器不具有其它容器套接字、网络接口的访问权限。当然，如果宿主机正确的配置，容器之间可以基于各自的网络接口进行交互，就像与外部主机一样。</p>
<div class="blog_h3"><span class="graybg">控制组</span></div>
<p>Cgroups是Linux容器的另一个关键组件，它实现了资源审计和限额，并提供很多有价值的度量信息。利用控制组，可以让容器获得公平的CPU、内存、磁盘I/O等资源。Cgroups能够有效的防范某些DoS攻击。Cgroups于2.6.24被合并到内核。</p>
<div class="blog_h3"><span class="graybg">守护进程攻击面</span></div>
<p>通过Docker运行容器，意味着需要运行Docker守护程序，后者需要Root权限。只有受信任用户才应该被允许控制守护程序。</p>
<p>由于Docker允许在宿主机、容器之间共享目录，这意味着容器可能任意的修改宿主机文件系统。</p>
<p>当在服务器上运行Docker时，推荐宿主机仅仅运行Docker，而把所有其它服务（除了SSH服务器这类管理工具）都放在容器中运行。</p>
<div class="blog_h3"><span class="graybg">内核能力</span></div>
<p>默认的，Docker以一组受限的能力（capabilities）来启动容器。能力把root/非root划分为细粒度的访问控制系统。需要绑定到1024-端口的进程不再需要root权限，而仅需要被授予net_bind_service能力。</p>
<p>容器也不需要被授予真正的root权限。事实上，容器中的root用户缺陷受到很大的限制，例如：</p>
<ol>
<li>禁止任何mount操作</li>
<li>禁止访问原始套接字（避免包嗅探）</li>
<li>禁止某些文件系统操作，例如创建新设备节点、修改文件所有权、修改属性</li>
<li>禁止模块加载操作</li>
</ol>
<p>由于这些限制的存在，即使攻击者获得容器的root权限，也难以进行严重的破坏。</p>
<p>你可以增加、删除容器的能力，以提升功能或安全性。</p>
<div class="blog_h3"><span class="graybg">其它内核特性</span></div>
<p>能力（capabilities）仅仅是现代Linux内核提供的众多安全特性之一。其它已知的著名安全系统包括：TOMOYO、AppArmor、SELinux、GRSEC等，它们都可以和Docker协作。</p>
<p>考虑以下建议：</p>
<ol>
<li>可以基于GRSEC、PAX来运行内核。这样会增加额外的安全检查（编译时、运行时）并防御很多缺陷。不需要针对Docker的配置，因为这些安全特性是系统级的</li>
<li>如果你使用的发行版提供了针对Docker容器的安全模型模板（security model templates），你可以使用它们</li>
<li>你可以使用某种访问控制系统，定义自己的安全策略</li>
</ol>
<p>从1.10开始，用户名字空间被Docker直接支持。这一特性允许容器中的root用户直接映射到容器外部的非0 UID的任何用户，进而减少安全风险。这一特性默认没有开启。</p>
<div class="blog_h2"><span class="graybg">保护守护进程套接字</span></div>
<p>默认的，Docker通过非网络化的Unix套接字运行，你也可以基于HTTP套接字与之通信。</p>
<p>如果你期望通过网络安全的访问Docker，应当启用TLS。这样，在守护程序端，仅仅通过CA认证的客户端才允许连接；在客户端，则仅仅允许向通过CA认知的服务器发起连接。</p>
<p>在守护程序上启用TLS的示例：</p>
<pre class="crayon-plain-tag">dockerd --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem -H=0.0.0.0:2376</pre>
<p>在客户端上启用TLS的示例：</p>
<pre class="crayon-plain-tag">docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem  -H=127.0.0.1:2376 version</pre>
<div class="blog_h2"><span class="graybg">使用受信任镜像</span></div>
<p>在使用Docker的过程中，我们常常需要从/到Docker Hub或者私服pull/push镜像。内容信任（Content trust）机制允许验证数据的完整性、镜像的发布者，不论你是从什么渠道获得镜像。</p>
<div class="blog_h3"><span class="graybg">理解Docker中的信任</span></div>
<p>内容信任机制可以强制客户端在与远程服务（Hub或私服，registry）交互时，进行客户端签名和镜像Tag验证。该机制默认情况下是禁用的，要启用，可以设置环境变量<pre class="crayon-plain-tag">DOCKER_CONTENT_TRUST</pre> 为1。</p>
<p>一旦内容信任被启用，镜像发布者就可以对自己的镜像进行签名。镜像的消费者则可以确保镜像是来自发布者，未经篡改。</p>
<p>每一个镜像记录由以下字段唯一的标识：<pre class="crayon-plain-tag">[REGISTRY_HOST[:REGISTRY_PORT]/]REPOSITORY[:TAG]</pre> 。一个镜像仓库（REPOSITORY）可以具有多个标签，镜像构建者可以使用仓库+标签的组合多次构建并更新镜像。</p>
<p>内容信任与TAG部分关联，每个REPOSITORY具有一组供发布者签名镜像TAG的密钥。单个REPOSITORY中可以包含签名、未签名的TAG。对于启用内容信任的消费者，未签名的TAG是不可见的。</p>
<p>子命令push、build、create、pull、run与内容信任机制交互。例如，当你执行docker pull someimage:latest时，仅当someimage:latest被正确签名时，命令才会成功。除了指定TAG，你也可以直接指定签名Hash：</p>
<pre class="crayon-plain-tag">docker pull someimage@sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a</pre>
<p>与镜像标签信任管理相关的是一系列的签名密钥。当第一次使用到内容信任功能时，密钥被创建。这些密钥包括：</p>
<ol>
<li>作为内容信任根的离线密钥，该密钥属于发布镜像的组织或个人，必须被客户端妥善的秘密保存，丢弃此密钥将非常难以恢复</li>
<li>签名仓库、标签的密钥，该密钥与一个镜像仓库关联，使用该密钥可以pull/push模板仓库的任何标签，保存在客户端</li>
<li>服务器管理的密钥，例如时间戳密钥。保存在服务器端</li>
</ol>
<div class="blog_h3"><span class="graybg">典型内容信任操作</span></div>
<p>除了通过环境变量来全局性的启用内容信任之外，你还可以在调用docker命令时指定<pre class="crayon-plain-tag">--disable-content-trust</pre> 来临时的禁用内容信任：</p>
<pre class="crayon-plain-tag">docker build --disable-content-trust -t gmemcc/nottrusttest:latest
docker pull --disable-content-trust gmemcc/nottrusttest:latest
docker push --disable-content-trust gmemcc/nottrusttest:latest</pre>
<div class="blog_h3"><span class="graybg">推送签名内容</span></div>
<p>当你第一次推送受信任内容时，Docker会提示你：</p>
<ol>
<li>警告你，新的root密钥将被创建 </li>
<li>提示输入root密钥的密码</li>
<li>在~/.docker/trust目录生成root密钥</li>
<li>提示输入仓库密钥的密码</li>
<li>在~/.docker/trust目录生成仓库密钥</li>
</ol>
<div class="blog_h3"><span class="graybg">拉取签名内容</span></div>
<p>如果启用内容信任机制，没有被签名的镜像是无法拉取的。</p>
<div class="blog_h3"><span class="graybg">自动化构建</span></div>
<p>要使用自动化脚本来执行镜像TAG签名，你需要设置环境变量：</p>
<ol>
<li>DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE  根密钥的密码</li>
<li>DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE  仓库密钥的密码</li>
</ol>
<div class="blog_h1"><span class="graybg">常用命令</span></div>
<p>参考<a href="https://docs.docker.com/engine/reference/commandline/docker/">Engine(docker) CLI</a>。</p>
<div class="blog_h2"><span class="graybg">docker</span></div>
<p>本质上此命令行通过REST  API和守护程序通信，和守护程序一样。该命令支持HTTP_PROXY、HTTPS_PROXY，并且优先使用后者。</p>
<table class=" fixed-word-wrap full-width">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">build</td>
<td>
<p>基于Dockerfile构建镜像</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker build [OPTIONS] PATH | URL | -</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--build-arg value  设置构建时（容器运行时看不到）变量，例如环境变量<br /> --cgroup-parent string 容器的父Cgroup组<br /> --cpu-period int  限制CFS周期<br /> --cpu-quota int 限制CFS配额<br /> -c, --cpu-shares int  限制CPU权重<br /> --cpuset-cpus string 限制允许在哪些CPU上执行<br /> --cpuset-mems string  限制允许使用哪些内存条<br /> --disable-content-trust  禁止镜像验证，默认true<br /> -f, --file string  可选的Dockerfile名称<br /> --force-rm  总是移除中间容器<br /> --isolation string  容器隔离计数<br /> --label value  在镜像上设置元标签<br /> -m, --memory string  设置内存限制<br /> --memory-swap string   内存和交换文件限制<br /> --no-cache   在构建镜像时不使用缓存<br /> --pull   总是尝试拉取更新版本的镜像<br /> -q, --quiet  禁止输出内容，仅在成功时打印镜像ID<br /> --rm   在成功构建后，移除中间容器，默认true<br /> --shm-size string  /dev/shm的大小，默认64MB<br /> -t, --tag value  为镜像设置标签<br /> --ulimit value  Ulimit选项</p>
</td>
</tr>
<tr>
<td class="blog_h3">info</td>
<td>查看Docker守护程序的基本信息</td>
</tr>
<tr>
<td class="blog_h3">images</td>
<td>
<p>列出本地可用的镜像</p>
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker images [OPTIONS] [REPOSITORY[:TAG]]</pre>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-a, --all  显示所有镜像，默认情况下中间镜像被隐藏<br /> --digests 显示摘要<br /> -f, --filter value  根据条件过滤输出<br /> --format string  依据Go语言模板指定输出的格式化方式<br /> --no-trunc  不截断输出<br /> -q, --quiet  安静模式，仅显示容器ID，不显示表头</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag">docker images             # 列出所有镜像
docker images java        # 列出所有Repository为java的镜像
docker images java:8      # 同时限定Repository和tag</pre>
</td>
</tr>
<tr>
<td class="blog_h3">rmi</td>
<td>
<p><span style="background-color: #c0c0c0;">示例：</span>
<pre class="crayon-plain-tag"># 删除所有镜像
docker rmi `docker images -q` 

# 删除无标签镜像
docker rmi $(docker images | grep "^&lt;none&gt;" | awk "{print $3}")</pre>
</td>
</tr>
<tr>
<td class="blog_h3">commit</td>
<td>
<p>把容器的变更提交到一个新的镜像中
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-a, --author string  镜像作者信息，例如Alex &lt;alex@gmem.cc&gt;<br /> -c, --change value 为新镜像应用Dockerfile指令<br /> -m, --message string  提交注释<br /> -p, --pause 在提交期间暂停容器</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 提交为镜像，并修改环境变量、入口点、暴露端口
docker commit --change "ENV DEBUG true"  CONTAINER_ID REGISTRY/REPO:TAG
              -c 'CMD ["apachectl", "-DFOREGROUND"]'
              -c 'EXPOSE 80' </pre>
</td>
</tr>
<tr>
<td class="blog_h3">run</td>
<td>
<p>从一个镜像创建容器并运行指定的命令
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker run [OPTIONS] IMAGE [COMMAND] [ARG...] </pre>
<p><span style="background-color: #c0c0c0;">选项：</span><br /> --add-host value  添加自定义的主机:IP地址映射<br /> -a, --attach  关联STDIN、STDOUT或者STDERR<br /> --blkio-weight value  块I/O相对权重，10-1000之间<br /> --blkio-weight-device value  块I/O设备相对权重<br /> --cap-add value  添加Linux特性（capabilities）<br /> --cap-drop value  去除Linux特性（capabilities）<br /> --cgroup-parent string  为容器指定父cgroup<br /> --cidfile string  输出容器ID到目标文件<br /> --cpu-percent int  CPU占用百分比，仅Windows<br /> --cpu-period int  限制容器使用的CFS周期<br /> --cpu-quota int  限制容器使用的CFS配额<br /> -c, --cpu-shares int  限制容器使用的CPU相对权重<br /> --cpuset-cpus string  限制容器可以使用的CPU的序号，例如0-3, 0,1<br /> --cpuset-mems string  限制容器可以使用的内存的变号，例如0-3, 0,1<br /> -d, --detach    在后端运行容器，并打印容器的ID<br /> --detach-keys    设置解除终端与容器关联的快捷键<br /> --device value    添加一个宿主机设备给容器<br /> --device-read-bps value  以字节/秒为单位限制容器读取一个设备的速度<br /> --device-read-iops value 以次数/秒为单位限制容器读取一个设备的速度<br /> --device-write-bps value 以字节/秒为单位限制容器写入一个设备的速度<br /> --device-write-iops value 以次数/秒为单位限制容器写入一个设备的速度<br /> --disable-content-trust  跳过镜像安全性验证<br /> --dns value  指定容器使用的DNS服务器<br /> --dns-opt value  设置容器的DNS选项<br /> --dns-search  设置自定义的DNS查找域<br /> --entrypoint  覆盖镜像默认的入口点（ENTRYPOINT）配置<br /> -e, --env value  设置环境变量，示例：<pre class="crayon-plain-tag">-e "LIMIT=10"</pre><br /> --env-file value  从文件中读取环境变量<br /> --expose value  暴露一个端口，作用类似于Dockfile中的expose命令。从1.5开始，支持暴露一组端口，例如<pre class="crayon-plain-tag">--expose=7000-8000</pre><br /> --group-add value   指定额外需要加入的组<br /> --health-cmd string   指定容器健康状态检查的命令<br /> --health-interval duration  容器健康状态检查间隔<br /> --health-retries int  报告为不健康之前重新检查的次数<br /> --health-timeout duration  一次健康检查最多执行的时间<br /> -h, --hostname string  容器的主机名<br /> -i, --interactive    即使没有关联到终端，也保持容器的标准输入打开<br /> --io-maxbandwidth string   系统驱动器最大IO带宽限制，仅Windows<br /> --io-maxiops uint  系统驱动器最大IOPS限制，仅Windows <br /> --ip string  设置容器的IPv4地址<br /> --ip6 string  设置容器的IPv6地址<br /> --ipc string  容器使用的IPC名字空间，设置为host则与宿主机共享IPC名字空间<br /> --isolation string  容器隔离技术<br /> --kernel-memory string  内核内存限制<br /> -l, --label value  为容器指定标签（元数据）<br /> --label-file value  从逗号分隔符文件中读取标签<br /> --link value  链接到其它容器，可以允许docker0网络中两个容器通信<br /> --link-local-ip value 设置容器的IPv4/IPv6 link-local地址<br /> --log-driver string  容器的日志驱动（Logging driver）。可选None不显示日志；Json-file默认值，以JSO格式记录日志；Syslog把日志输出到系统日志文件；journald、fluentd、splunk、gelf、awslogs<br /> --log-opt value  容器日志驱动的选项<br /> --mac-address string  设置容器的MAC地址<br /> -m, --memory string  设置容器的内存用量限额<br /> --memory-reservation string 设置容器的内存用量软限制<br /> --memory-swap string  设置交换文件限额，-1表示不限制<br /> --memory-swappiness int  微调容器的swappiness，0-100之间<br /> --name string   为容器分配名称<br /> --network string  连接容器到指定的网络<br /> --network-alias value  为容器指定目标网络范围内的别名<br /> --no-healthcheck  禁止容器特定的健康状态检查<br /> --oom-kill-disable   禁止内存溢出Killer<br /> --oom-score-adj int  微调宿主机的OOM参数，-1000到1000之间<br /> --pid string PID  使用的名字空间类型，取值host则与宿主机共享PID名字空间<br /> --pids-limit int  微调容器的PIDs限制<br /> --privileged  为容器授予扩展的权限<br /> -p, --publish value  发布容器暴露的端口到宿主机，value格式 <span style="background-color: #c0c0c0;">宿主机端口:容器端口</span><br /> -P, --publish-all Publish  随机的发布容器暴露的端口到宿主机，通过--expose指定或者在Dockerfile中以EXPOSE指定的任意端口都会被发布。宿主机的端口范围由/proc/sys/net/ipv4/ip_local_port_range这个内核参数确定，默认32768-61000之间<br /> --read-only  以指定方式挂载容器的根文件系统<br /> --restart string  设置容器退出时的重启/宿主机开机后的启动策略，默认no。On-failure当容器命令返回非0时重启，Always 自动重启，并且总是随着守护程序启动，Unless-stopped类似于Always，但是不随着守护程序启动<br /> --rm  在容器退出时自动删除它<br /> --runtime string  设置容器使用的运行时间<br /> --security-opt value  设置安全选项<br /> --shm-size string Size  设置/dev/shm的大小，默认64MB<br /> --sig-proxy  代理接收到的信号给容器进程<br /> --stop-signal string  用于退出容器的信号，默认SIGTERM<br /> --storage-opt value  容器的存储驱动选项，devicemapper、overlay2等支持<br /> --sysctl value  容器的Sysctl选项<br /> --tmpfs value  挂载一个tmpfs目录<br /> -t, --tty  为容器分配一个伪终端<br /> --ulimit value  设置Ulimit选项<br /> -u, --user string  设置容器执行身份，格式&lt;name|uid&gt;[:&lt;group|gid&gt;]<br /> --userns string  设置使用的用户名字空间<br /> --uts string  设置使用的UTS名字空间，设置为host则与宿主机使用相同的hostname和domain<br /> -v, --volume value  挂载一个卷<br /> --volume-driver string  可选的卷驱动<br /> --volumes-from value  从指定的容器挂载卷<br /> -w, --workdir string  设置容器的工作目录</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 交互式的运行一个容器，分配伪终端，在退出时删除容器
docker run  -i -t --rm --name=temp ubuntu:14.04
# 指定存储驱动选项
docker run --storage-opt size=120G ubuntu
# 挂载一个虚拟内存文件系统，并指定选项
docker run --tmpfs /run:rw,noexec,nosuid,size=65536K
# 指定环境变量（命令行、文件）
docker run -e NAME=VAL --env-file=path-to-file
# 挂载其它容器定义的卷，指定ro/rw
docker run --volumes-from CONTAINER_ID:rw
# 修改容器hosts文件
docker run --add-host=zircon:10.0.0.1
# 限制容器的资源用量 type=softlimit[:hardlimit]
docker run --ulimit nofile=1024:1024</pre>
</td>
</tr>
<tr>
<td class="blog_h3">create</td>
<td>
<p>创建，但不启动容器
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker create [OPTIONS] IMAGE [COMMAND] [ARG...]</pre> </p>
</td>
</tr>
<tr>
<td class="blog_h3">start</td>
<td>
<p>启动一个现有的容器</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker start [OPTIONS] CONTAINER [CONTAINER...]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span><br />-a, --attach  关联容器的标准输入/输出到当前终端，并转发信号<br />-i, --interactive  关联容器的标准输入到当前终端<br />--detach-keys string  覆盖解除终端与容器关联的快捷键序列</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 交互式启动容器，并关联当前终端到该容器
docker start -i -a ubuntu</pre>
</td>
</tr>
<tr>
<td class="blog_h3">stop</td>
<td>
<p>停止一个运行中的容器
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker stop [OPTIONS] CONTAINER [CONTAINER...]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-t, --time int  强制杀死前，等待的秒数，默认10</p>
</td>
</tr>
<tr>
<td class="blog_h3">kill</td>
<td>
<p>杀死一个或者多个容器</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker kill [OPTIONS] CONTAINER [CONTAINER...]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-s, --signal string  用于杀死容器的信号，默认KILL</p>
</td>
</tr>
<tr>
<td class="blog_h3">attach</td>
<td>
<p>关联当前终端到运行中的容器，之后你可以按Ctrl+P , Ctrl+Q解除关联（保持容器运行）</p>
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker attach [OPTIONS] CONTAINER</pre>
<p><span style="background-color: #c0c0c0;">选项：</span><br />--detach-keys string 覆盖解除终端与容器关联的快捷键序列<br />--sig-proxy 代理接收到的信号给容器进程<br />--no-stdin   不关联标准输入</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 执行此命令后，容器进入前台运行
docker attach ubuntu</pre>
</td>
</tr>
<tr>
<td class="blog_h3">exec</td>
<td>
<p>在运行中的容器里执行命令
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--detach,-d 后台模式运行<br />--env,-e 设置环境变量<br />-i 交互模式，保持标准输入打开<br />-t 分配伪终端</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag">docker exec -d touch /etc/config</pre>
</td>
</tr>
<tr>
<td class="blog_h3">rm</td>
<td>
<p>删除容器
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker rm [OPTIONS] CONTAINER [CONTAINER...]</pre>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-f, --force  强制删除，如果目标容器正在运行则发送SIGKILL信号<br /> -l, --link  移除指定的链接<br /> -v, --volumes  移除容器关联的卷</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 删除所有容器
docker rm `docker ps --no-trunc -aq` 
# 删除所有停止的容器
docker ps --filter "status=exited" --quiet |  xargs --no-run-if-empty docker rm</pre>
</td>
</tr>
<tr>
<td class="blog_h3">ps</td>
<td>
<p>列出现有的容器
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker ps [OPTIONS]</pre>
<p><span style="background-color: #c0c0c0;">选项：</span><br />-a, --all  显示所有容器，包括已经停止的<br />-f, --filter value  过滤输出<br />--format string  依据Go语言模板指定输出的格式化方式<br />-n, --last int   显示最后int个创建的容器<br />-l, --latest   显示最后一个创建的容器<br />-q, --quiet   安静模式，仅显示容器ID，不显示表头<br />-s, --size  打印总的空间占用</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 显示已经退出的所有容器的ID
docker ps --filter "status=exited" --quiet</pre>
</td>
</tr>
<tr>
<td class="blog_h3">logs</td>
<td>
<p>抓取一个容器的日志
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker logs [OPTIONS] CONTAINER</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--details  显示额外信息<br /> -f, --follow  跟随日志输出<br /> --since string 显示指定时间戳之后的日志<br /> --tail string 显示末尾N行日志<br /> -t, --timestamps 显示时间戳</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 跟踪日志输出
docker logs -f ubuntu
# 要删除容器的日志，参考如下脚本： Linux only
rm $(docker inspect $1 | grep -G '"LogPath": "*"' | sed -e 's/.*"LogPath": "//g' | sed -e 's/",//g');</pre>
</td>
</tr>
<tr>
<td class="blog_h3">cp</td>
<td>
<p>从容器中复制文件到宿主机，或者从宿主机拷贝文件到容器
<p>格式：</p>
<pre class="crayon-plain-tag">docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH</pre>
<p>示例：</p>
<pre class="crayon-plain-tag">docker cp apache2:/usr/lib/php5/20131226 /home/alex/Docker/projects/apache2/usr/lib/php5/ </pre>
</td>
</tr>
<tr>
<td class="blog_h3"> stat</td>
<td>
<p>显示容器的资源使用情况
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker stats [OPTIONS] [CONTAINER...]</pre>
<p><span style="background-color: #c0c0c0;">选项：</span><br />-a, --all显示所有容器<br />--no-stream 仅打印一次统计信息，然后立即退出而不是刷新</p>
</td>
</tr>
<tr>
<td class="blog_h3">network</td>
<td>
<p>网络相关子命令</p>
<p><span style="background-color: #c0c0c0;">通用网络选项：</span><br />--internal 禁止通过网络进行外部访问<br />--ipv6  启用IPv6支持</p>
<p><span style="background-color: #c0c0c0;">桥接网络选项：</span></p>
<p>com.docker.network.bridge.name  Linux网桥的名称<br />com.docker.network.bridge.enable_ip_masquerade，--ip-masq  启用IP遮掩<br />com.docker.network.bridge.enable_icc，--icc  启用或禁止跨容器连接性（Inter Container Connectivity）<br />com.docker.network.bridge.host_binding_ipv4 ，--ip  绑定容器暴露的端口时，使用的宿主机IP<br />com.docker.network.driver.mtu，--mtu  设置最大传输单元</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 列出可用的网络
docker network ls

# 查看网络bridge的详细信息，包括连接到网络的容器信息
docker network inspect bridge

# 将容器连接到已经存在的网络，连接后容器立刻可以和网络中的其它容器通信
docker network connect isolated_nw container2
# 断开容器到网络的连接
# -f表示强制断开，可以解决stale endpoints问题
docker network disconnect -f isolated_nw container2

# 创建一个名为isolated_nw的桥接网络，如果不指定dirver默认使用bridge
docker network create --driver bridge isolated_nw
# 创建网络并提供参数
docker network create --subnet 172.30.0.0/16  \
 --opt com.docker.network.bridge.name=docker_gwbridge \
 --opt com.docker.network.bridge.enable_icc=false \
 docker_gwbridge

# Bridge网络仅支持单个子网，overlay则支持多个
# 强烈建议显式的指定子网

docker network create -d overlay \
 --subnet=192.168.0.0/16 \
 --subnet=192.170.0.0/16 \
 --gateway=192.168.0.100 \
 --gateway=192.170.0.100 \
 --ip-range=192.168.1.0/24 \
 --aux-address="my-router=192.168.1.5" --aux-address="my-switch=192.168.1.6" \
 --aux-address="my-printer=192.170.1.5" --aux-address="my-nas=192.170.1.6" \
 my-multihost-network

# 使用 -o来指定选项
docker network create  \
    -o "com.docker.network.bridge.host_binding_ipv4"="172.23.0.1" my-network

# 移除网络，仅当没有容器连接到网络时，才能移除
docker network rm isolated_nw</pre>
</td>
</tr>
<tr>
<td class="blog_h3">load</td>
<td>
<p>从TAR加载镜像，格式：
<pre class="crayon-plain-tag"># 选项
# -i 指定输入TAR的路径，如果不指定从STDIN读取
docker load [OPTIONS]</pre>
<p>示例：</p>
<pre class="crayon-plain-tag"># 解压并从标准输入加载
gunzip -c busybox.tar.gz  | docker load</pre>
</td>
</tr>
<tr>
<td class="blog_h3">save</td>
<td>
<p>保存一个或者多个镜像到TAR归档文件，镜像的层次历史保留
<p>格式：</p>
<pre class="crayon-plain-tag"># 选项 -o 指定输出文件路径，不指定则输出到STDOUT
docker save [OPTIONS] IMAGE [IMAGE...]</pre>
<p> 示例：</p>
<pre class="crayon-plain-tag"># 保存到标准输出，然后压缩
docker save busybox:1.30.1 | gzip -c &gt; busybox.tar.gz</pre>
</td>
</tr>
<tr>
<td class="blog_h3">import</td>
<td>
<p>从TAR归档文件导入内容，创建一个扁平的文件系统镜像
<p>格式：</p>
<pre class="crayon-plain-tag"># 选项：
# -c 应用Dockerfile指令到新创建的镜像
docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]</pre>
</td>
</tr>
<tr>
<td class="blog_h3">export</td>
<td>
<p>导出一个容器的文件系统为TAR归档文件，镜像层次历史丢失
<p>格式：</p>
<pre class="crayon-plain-tag"># 选项 -o 指定输出文件路径，不指定则输出到STDOUT
docker export [OPTIONS] CONTAINER</pre>
</td>
</tr>
<tr>
<td class="blog_h3">swarm init</td>
<td>
<p>初始化Swarm集群
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker swarm init [OPTIONS]</pre>
<p><span style="background-color: #c0c0c0;">选项：</span><br />--advertise-addr  通知地址，格式ip[:port]<br /> --cert-expiry duration 证书有效期，默认2160h0m0s<br /> --dispatcher-heartbeat duration 分发器心跳时间，默认5s<br /> --external-ca value 指定一个或者多个证书签名端点<br /> --force-new-cluster 强制从当前状态创建新集群<br /> --listen-addr value 监听地址，默认0.0.0.0:2377<br /> --task-history-limit int  任务历史存留限制，默认5</p>
</td>
</tr>
<tr>
<td class="blog_h3">swarm join</td>
<td>
<p>以Worker或/和Manager身份加入到集群</p>
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker swarm join [OPTIONS] HOST:PORT</pre>
<p><span style="background-color: #c0c0c0;">选项：<br /></span>--advertise-addr  通知地址，格式ip[:port]<br />--listen-addr value 监听地址，默认0.0.0.0:2377<br />--token string   用于进入集群的令牌</p>
</td>
</tr>
<tr>
<td class="blog_h3">swarm join-token</td>
<td>
<p>管理加入集群使用的令牌</p>
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker swarm join-token [-q] [--rotate] (worker|manager)</pre>
<p> -q, --quiet 仅仅显示令牌<br /> --rotate 轮换令牌</p>
</td>
</tr>
<tr>
<td class="blog_h3">swarm update</td>
<td>
<p>更新Swarm集群</p>
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker swarm update [OPTIONS]</pre>
<p><span style="background-color: #c0c0c0;">选项：</span><br />--cert-expiry duration 证书有效期，默认2160h0m0s<br />--dispatcher-heartbeat duration 分发器心跳时间，默认5s<br />--external-ca value 指定一个或者多个证书签名端点<br />--task-history-limit int 任务历史存留限制，默认5</p>
</td>
</tr>
<tr>
<td class="blog_h3">swarm leave</td>
<td>
<p>离开一个Swarm集群，仅用于Worker</p>
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker swarm leave [OPTIONS]</pre>
<p><span style="background-color: #c0c0c0;">选项：</span><br />--force  强制离开集群，忽略警告 </p>
</td>
</tr>
<tr>
<td class="blog_h3">node demote</td>
<td>
<p>把一个或者多个节点降级为Worker</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker node demote NODE [NODE...]</pre> </p>
</td>
</tr>
<tr>
<td class="blog_h3">node inspect</td>
<td>
<p>查看一个或者多个节点的详细信息</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker node inspect [OPTIONS] self|NODE [NODE...]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-f, --format string  基于给定的模板进行格式化<br />--pretty  优化输出（不以JSON格式显示）</p>
</td>
</tr>
<tr>
<td class="blog_h3">node ls</td>
<td>
<p>列出集群中的节点</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker node ls [OPTIONS]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-f, --filter value 基于给定的选项过滤输出<br />-q, --quiet  仅仅显示ID</p>
</td>
</tr>
<tr>
<td class="blog_h3">node promote</td>
<td>
<p>把一个或者多个节点提升为Manager</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker node promote NODE [NODE...]</pre> </p>
</td>
</tr>
<tr>
<td class="blog_h3">node rm</td>
<td>
<p>从Swarm中移除一个或者多个节点</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker node rm [OPTIONS] NODE [NODE...]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--force  强制移除活动节点</p>
</td>
</tr>
<tr>
<td class="blog_h3">node ps</td>
<td>
<p>列出节点上运行的Tasks</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker node ps [OPTIONS] self|NODE</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-f, --filter value 基于给定的选项过滤输出<br /> --no-resolve 不把ID映射为名称</p>
</td>
</tr>
<tr>
<td class="blog_h3">node update</td>
<td>
<p>更新一个节点</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker node update [OPTIONS] NODE</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--availability string  节点可用性：active/pause/drain<br /> --label-add value 以key=value的形式添加节点标签<br /> --label-rm value  移除一个节点标签（如果存在）<br /> --role string  设置节点的角色：worker/manager</p>
</td>
</tr>
<tr>
<td class="blog_h3">service create</td>
<td>
<p>创建一个新服务</p>
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker service create [OPTIONS] IMAGE [COMMAND] [ARG...]</pre>
<p><span style="background-color: #c0c0c0;">选项：<br /></span></p>
<p>--constraint value 设置约束<br /> --container-label value 设置容器标签<br /> --endpoint-mode string 端点模式dnsrr或者vip<br /> -e, --env value 设置环境变量<br /> -l, --label value 设置服务标签<br /> --limit-cpu value 限制CPU，默认0.000<br /> --limit-memory value 限制内存，默认 0 B<br /> --log-driver string  服务的日志驱动<br /> --log-opt value  日志驱动选项<br /> --mode string  服务模式，replicated（默认）或者 global<br /> --mount value 附加一个挂载到服务<br /> --name string 服务的名称<br /> --network value 加入的网络<br /> -p, --publish value 暴露一个端口，pubPort:targetPort格式<br /> --replicas value   任务的数量，默认0<br /> --reserve-cpu value 保留CPU，默认0.000<br /> --reserve-memory value 保留内存，默认 0 B<br /> --restart-condition string  何时自动重启，none, on-failure, any<br /> --restart-delay value  重启尝试的延迟<br /> --restart-max-attempts value  放弃前重试的重启次数<br /> --restart-window value  用于评估重启策略的窗口<br /> --stop-grace-period value  在杀死容器前，等待的时间<br /> --update-delay duration  更新之间的延迟时间<br /> --update-failure-action string  更新失败后的动作，pause或continue<br /> --update-parallelism uint 同时更新的Task的最大数量，默认1，0表示全部同时更新<br /> -u, --user string   用户名或者UID<br /> --with-registry-auth 发送registry认证详细信息给Swarm代理<br /> -w, --workdir string  设置容器工作目录</p>
</td>
</tr>
<tr>
<td class="blog_h3">service inspect</td>
<td>
<p>查看一个或者多个服务的详细信息</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker service inspect [OPTIONS] SERVICE [SERVICE...]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-f, --format string  基于给定的模板进行格式化<br />--pretty  优化输出（不以JSON格式显示）</p>
</td>
</tr>
<tr>
<td class="blog_h3">service ps</td>
<td>
<p>列出服务包含的任务</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker service ps [OPTIONS] SERVICE</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-f, --filter value 基于给定的选项过滤输出<br />--no-resolve 不把ID映射为名称</p>
</td>
</tr>
<tr>
<td class="blog_h3">service ls</td>
<td>
<p>列出服务</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker service ls [OPTIONS]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-f, --filter value 基于给定的选项过滤输出<br />--no-resolve 不把ID映射为名称</p>
</td>
</tr>
<tr>
<td class="blog_h3">service rm</td>
<td>
<p>移除一个或者多个服务</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker service rm [OPTIONS] SERVICE [SERVICE...]</pre> </p>
</td>
</tr>
<tr>
<td class="blog_h3">service scale</td>
<td>
<p>扩容一个或者多个服务</p>
<p><span style="background-color: #c0c0c0;">格式：</span></p>
<pre class="crayon-plain-tag">docker service scale SERVICE=REPLICAS [SERVICE=REPLICAS...] </pre>
</td>
</tr>
<tr>
<td class="blog_h3">service update</td>
<td>
<p>更新一个服务
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker service update [OPTIONS] SERVICE</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>参考service create子命令</p>
</td>
</tr>
<tr>
<td>system df</td>
<td>查看Docker的磁盘占用情况</td>
</tr>
<tr>
<td>system prune</td>
<td>
<p>删除停止的容器、无人使用的网络、dangling镜像、构建缓存</p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>-a  同时删除没有容器使用的镜像</p>
</td>
</tr>
<tr>
<td class="blog_h3">volume create</td>
<td>
<p>创建一个卷，卷可以被容器消费，并存储数据到其中</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker volume create [OPTIONS] [VOLUME]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--driver, -d  指定卷驱动名称，默认local<br />--label  为卷添加元数据<br />--name  设置卷的名称，不指定则随机名称。在不同驱动之间，名称不能重复。使用同一种驱动、同一个名称，被认为是卷重用，不会报错<br />--opt, -o  驱动特定的选项</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 创建一个名为hello的卷
docker volume create hello
# 创建一个100MB的tmpfs卷
docker volume create --driver local --opt type=tmpfs \
    --opt device=tmpfs --opt o=size=100m,uid=1000  foo
# 创建一个映射到NFS服务192.168.1.1的/path/to/dir目录的卷
docker volume create --driver local --opt type=nfs \
    --opt o=addr=192.168.1.1,rw--opt device=:/path/to/dir foo

# 挂载卷到容器的/world目录，注意挂载点必须是绝对路径
docker run -d -v hello:/world busybox ls /world</pre>
</td>
</tr>
<tr>
<td class="blog_h3">volume inspect</td>
<td>
<p>显示一个或者多个卷的详细信息
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker volume inspect [OPTIONS] VOLUME [VOLUME...]</pre> </p>
</td>
</tr>
<tr>
<td class="blog_h3">volume ls</td>
<td>
<p>列出可用的卷</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker volume ls [OPTIONS]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--filter, -f  过滤条件<br />--format 使用Go语言模板优化输出<br />--quiet, -q  仅显示卷名称</p>
</td>
</tr>
<tr>
<td class="blog_h3">volume prune</td>
<td>
<p>移除所有不被使用的卷</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker volume prune [OPTIONS]</pre> </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--force, -f  强制删除不提示</p>
</td>
</tr>
<tr>
<td class="blog_h3">volume rm</td>
<td>
<p>删除一个或多个卷</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker volume rm [OPTIONS] VOLUME [VOLUME...]</pre>  </p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p>--force, -f 强制删除不提示</p>
</td>
</tr>
<tr>
<td class="blog_h3">manifest</td>
<td>
<p>当前是Docker客户端的试验特性。用于管理清单（manifest）或清单列表</p>
<p>所谓清单，是关于镜像的信息，包括层、大小、摘要值。docker manifest命令可以在清单中增加镜像所针对的操作系统、体系结构的信息</p>
<p>清单列表则是通过指定1或多个镜像名称来创建的层列表。典型情况下，清单列表是基于一组功能相同、针对不同os_arch的镜像</p>
<p>当拉取镜像时，Docker会检查返回的清单信息，如果发现是支持multi arch的清单列表对象，则在列表中<span style="background-color: #c0c0c0;">自动寻找匹配当前体系结构的镜像</span></p>
<p>注意：</p>
<ol>
<li>客户端，需要在~/.docker/config.json中设置 <pre class="crayon-plain-tag">"experimental": "enabled"</pre></li>
</ol>
</td>
</tr>
<tr>
<td class="blog_h3">manifest inspect</td>
<td>
<p>查看镜像清单、或者清单列表的信息</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker manifest inspect [OPTIONS] [MANIFEST_LIST] MANIFEST</pre></p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag">// 查看镜像的清单，获取os_arch信息
// docker manifest inspect hello-world --verbose
{
        "Ref": "docker.io/library/hello-world:latest",
        "Digest": "sha256:f3b3b28a45160805bb16542c9531888519430e9e6d6ffc09d72261b0d26ff74f",
        "SchemaV2Manifest": {
                "schemaVersion": 2,
                "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
                "config": {
                        "mediaType": "application/vnd.docker.container.image.v1+json",
                        "size": 1520,
                        "digest": "sha256:1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57"
                },
                "layers": [
                        {
                                "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
                                "size": 972,
                                "digest": "sha256:b04784fba78d739b526e27edc02a5a8cd07b1052e9283f5fc155828f4b614c28"
                        }
                ]
        },
        "Platform": {
                "architecture": "amd64",
                "os": "linux"
        }
} </pre>
</td>
</tr>
<tr>
<td class="blog_h3">manifest create</td>
<td>
<p>创建一个<strong><span style="background-color: #c0c0c0;">本地</span></strong>清单列表，用于后续的标注、推送。清单的存放位置为：
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">${HOME}/.docker/manifests/$(REGISTRY_DOMAIN)_$(REGISTRY_PREFIX)_$(IMAGE)-$(VERSION)</pre></p>
<p>要删除清单，删除上述文件即可。此外你也可以在push的时候指定--purge来删除本地清单</p>
<p>清单必须和镜像一同推送到镜像仓库，否则拉取镜像时会提示找不到manifest</p>
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker manifest create MANIFEST_LIST MANIFEST [MANIFEST...]</pre></p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p style="padding-left: 30px;">-a, --amend  修改现有清单列表<br />--insecure 支持和不安全仓库通信</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 你需要指定一组成员镜像，来创建清单列表
#                      清单列表名字，任何平台的客户端都使用该名字拉取镜像
docker manifest create docker.gmem.cc/coolapp:v1 \
    docker.gmem.cc/coolapp-ppc64le-linux:v1 \
    docker.gmem.cc/coolapp-arm-linux:v1 \
    docker.gmem.cc/coolapp-amd64-linux:v1 \
    docker.gmem.cc/coolapp-amd64-windows:v1 </pre>
</td>
</tr>
<tr>
<td class="blog_h3">manifest annotate</td>
<td>
<p>为本地镜像清单标注额外的信息，设置清单列表中的某个清单对应的体系结构、操作系统
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker manifest annotate [OPTIONS] MANIFEST_LIST MANIFEST</pre></p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p style="padding-left: 30px;">--arch 标注架构<br />--variant 标注架构变体<br />--os 标注操作系统<br />--os-features 标注操作系统特性</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag"># 将清单列表中的某个镜像的体系结构标注为arm
docker manifest annotate docker.gmem.cc/coolapp:v1 \
  docker.gmem.cc/coolapp-arm-linux --arch arm</pre>
</td>
</tr>
<tr>
<td class="blog_h3">manifest push</td>
<td>
<p>将镜像清单列表推送到仓库
<p><span style="background-color: #c0c0c0;">格式：</span><pre class="crayon-plain-tag">docker manifest push [OPTIONS] MANIFEST_LIST</pre></p>
<p><span style="background-color: #c0c0c0;">选项：</span></p>
<p style="padding-left: 30px;">-p, --purge 推送后删除本地清单<br />--insecure 支持和不安全仓库通信</p>
<p><span style="background-color: #c0c0c0;">示例：</span></p>
<pre class="crayon-plain-tag">docker manifest push docker.gmem.cc/coolapp:v1 </pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">docker-machine</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">active</td>
<td>显示哪些宿主机是活动的</td>
</tr>
<tr>
<td class="blog_h3">config</td>
<td>打印一个宿主机的连接配置</td>
</tr>
<tr>
<td class="blog_h3">create</td>
<td>创建一个宿主机</td>
</tr>
<tr>
<td class="blog_h3">env</td>
<td>打印用于准备连接到目标宿主机的Shell命令</td>
</tr>
<tr>
<td class="blog_h3">inspect</td>
<td>显示一个宿主机的详细信息</td>
</tr>
<tr>
<td class="blog_h3">ip</td>
<td>获取一个宿主机的IP地址</td>
</tr>
<tr>
<td class="blog_h3">kill</td>
<td>杀死一个宿主机</td>
</tr>
<tr>
<td class="blog_h3">provision</td>
<td>重新provision现有的宿主机</td>
</tr>
<tr>
<td class="blog_h3">regenerate-certs</td>
<td>为一个宿主机准备TLS证书</td>
</tr>
<tr>
<td class="blog_h3">restart</td>
<td>重启一个宿主机</td>
</tr>
<tr>
<td class="blog_h3">rm</td>
<td>移除一个宿主机</td>
</tr>
<tr>
<td class="blog_h3">ssh</td>
<td>通过SSH登录宿主机或者执行命令</td>
</tr>
<tr>
<td class="blog_h3">scp</td>
<td>在宿主机之间复制文件</td>
</tr>
<tr>
<td class="blog_h3">start</td>
<td>启动一个宿主机</td>
</tr>
<tr>
<td class="blog_h3">status</td>
<td>获取宿主机状态</td>
</tr>
<tr>
<td class="blog_h3">stop</td>
<td>停止一个宿主机</td>
</tr>
<tr>
<td class="blog_h3">upgrade</td>
<td>升级宿主机的Docker到最新版本</td>
</tr>
<tr>
<td class="blog_h3">url</td>
<td>获取一个宿主机的URL</td>
</tr>
<tr>
<td class="blog_h3">version</td>
<td>查看DM的版本信息</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">docker-compose</span></div>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">build</td>
<td>构建或者重新构建服务</td>
</tr>
<tr>
<td class="blog_h3">bundle</td>
<td>从Compose创建一个Docker分布式应用程序束（Distributed Application Bundle，ADB）</td>
</tr>
<tr>
<td class="blog_h3">config</td>
<td>验证并查看Compose文件</td>
</tr>
<tr>
<td class="blog_h3">create</td>
<td>创建服务</td>
</tr>
<tr>
<td class="blog_h3">down</td>
<td>停止并移除容器、网络、镜像、卷</td>
</tr>
<tr>
<td class="blog_h3">events</td>
<td>从容器接收实时事件</td>
</tr>
<tr>
<td class="blog_h3">images</td>
<td>列出镜像</td>
</tr>
<tr>
<td class="blog_h3">kill</td>
<td>杀死容器</td>
</tr>
<tr>
<td class="blog_h3">logs</td>
<td>查看容器的日志</td>
</tr>
<tr>
<td class="blog_h3">pause</td>
<td>暂停服务</td>
</tr>
<tr>
<td class="blog_h3">port</td>
<td>打印一个端口绑定的公共端口</td>
</tr>
<tr>
<td class="blog_h3">ps</td>
<td>列出容器</td>
</tr>
<tr>
<td class="blog_h3">pull</td>
<td>拉取服务镜像</td>
</tr>
<tr>
<td class="blog_h3">push</td>
<td>推送服务镜像</td>
</tr>
<tr>
<td class="blog_h3">restart</td>
<td>重启服务</td>
</tr>
<tr>
<td class="blog_h3">rm</td>
<td>移除停止的容器</td>
</tr>
<tr>
<td class="blog_h3">run</td>
<td>运行一次性命令</td>
</tr>
<tr>
<td class="blog_h3">scale</td>
<td>扩充或减小一个服务的容器数量</td>
</tr>
<tr>
<td class="blog_h3">stop</td>
<td>停止服务</td>
</tr>
<tr>
<td class="blog_h3">top</td>
<td>显示运行中的进程</td>
</tr>
<tr>
<td class="blog_h3">unpause</td>
<td>从暂停中恢复服务</td>
</tr>
<tr>
<td class="blog_h3">up</td>
<td>创建并启动容器</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">dockerd</span></div>
<div class="blog_h3"><span class="graybg">建议参数</span></div>
<p><pre class="crayon-plain-tag">{
  "live-restore": true,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "5"
  }
}</pre>
<div class="blog_h1"><span class="graybg">容器网络</span></div>
<div class="blog_h2"><span class="graybg">容器网络模型CNM</span></div>
<p>Docker通过libnetwork实现了此模型，CNM的架构如下：</p>
<p><img class="aligncenter  wp-image-27163" src="https://blog.gmem.cc/wp-content/uploads/2015/10/cnm.png" alt="cnm" width="905" height="416" /></p>
<p>其中：</p>
<ol>
<li>网络沙盒：提供容器的网络栈，包括网络接口、路由表、DNS配置、Iptables等。实现技术包括Linux网络命名空间、FreeBSD Jail等</li>
<li>每个沙盒中可以包含来自多个网络的端点（Endpoint），端点是连接网络沙盒和后端网络的桥梁，实现技术包括Veth对、tap/tun设备、OVS内部端口等</li>
<li>容器网络（Backend Network）：一组可以相互通信的端点的集合。对应的实现技术可以是Linux Bridge、VLAN等</li>
</ol>
<p>此外，CNM还依赖于另外两个对象来完成Docker网络管理：</p>
<ol>
<li>Network Controller：对外提供分配、管理网络的APIs</li>
<li>Drivers：负责一种网络的管理，包括IPAM</li>
</ol>
<p>CNM的原生实现是Libnetwork，是Docker团队从Docker核心中分离出来的网络相关功能。</p>
<p>默认情况下，Docker使用Veth对的方案：</p>
<ol>
<li>创建一个docker0网桥，如果没有容器运行，则此网桥是down状态</li>
<li>创建一个Veth对，一端接到网桥docker0，另外一端放在容器网络命名空间中，作为eth0</li>
<li>属于同一网络的容器，都会连接到docker0，因此可以相互通信</li>
</ol>
<div class="blog_h2"><span class="graybg">组网方案</span></div>
<div class="blog_h3"><span class="graybg">隧道方案</span></div>
<p>隧道网络也叫overlay网络，在IaaS网络中就被大量使用。</p>
<p>优势：几乎不依赖基础网络架构，只要3层互联即可。</p>
<p>劣势：</p>
<ol>
<li>随着节点规模的增加，复杂度随之升高</li>
<li>封包二次包装，网络问题定位麻烦，而且影响性能</li>
</ol>
<p>基于覆盖网络的插件包括：</p>
<ol>
<li>Weave，基于VXLAN</li>
<li>Open vSwitch（<span style="background-color: #c0c0c0;">OVS</span>）：基于VXLAN + GRE，<span style="background-color: #c0c0c0;">性能方面损失较为严重</span></li>
<li>flannel：支持自研UDP封包（性能较差，性能损失50%+），以及Linux内核的VXLAN（性能损失20-30%）</li>
</ol>
<div class="blog_h3"><span class="graybg">路由方案</span></div>
<p>隧道方案解决的问题是，主机之间无法直接传递容器IP的封包。如果解决路由的问题，就可以避免二次包装的性能损失。</p>
<p>基于路由的方案包括：</p>
<ol>
<li>Calico，基于BGP，支持细致的ACL控制，混合云友好</li>
<li>Macvlan，隔离性好，性能最优，需要二层网络，大多数云服务商不支持，难以实现混合云</li>
<li>Metaswitch，容器内部分配一个路由只想宿主机地址，性能接近原生</li>
</ol>
<div class="blog_h1"><span class="graybg">常见问题</span></div>
<div class="blog_h2"><span class="graybg">零散问题</span></div>
<div class="blog_h3"><span class="graybg">如何获取docker容器的runc配置文件</span></div>
<p>位置在：/run/containerd/io.containerd.runtime.v1.linux/moby/$CONTAINER_ID/config.json</p>
<div class="blog_h3"><span class="graybg">如果进入容器的/tmp目录</span></div>
<p>如果容器正在运行，可以通过nsenter进入。</p>
<p>如果容器已经死掉，可以通过命令：</p>
<pre class="crayon-plain-tag">docker inspect 3b7227ffdb88 | grep UpperDir</pre>
<p>得到UpperDir，然后进入UpperDir，即可在tmp子目录中看到容器修改后的/tmp目录的内容。 </p>
<div class="blog_h3"><span class="graybg">dockerd日志在哪</span></div>
<p>老版本Ubuntu：<pre class="crayon-plain-tag">/var/log/upstart/docker.log</pre></p>
<p>使用Systemd的：<pre class="crayon-plain-tag">sudo journalctl -fu docker.service</pre></p>
<p>CentOS：<pre class="crayon-plain-tag">/var/log/daemon.log | grep docker</pre></p>
<div class="blog_h3"><span class="graybg">同时监听TCP/UDS</span></div>
<pre class="crayon-plain-tag">DOCKER_OPTS=" -H 10.0.0.1:2376 -H unix:///var/run/docker.sock"
# 测试 docker -H 10.0.0.1:2376 ps</pre>
<div class="blog_h3"><span class="graybg">使用代理-容器</span></div>
<p>要让启动的容器自动设置代理环境变量，可以：</p>
<pre class="crayon-plain-tag">{
    "proxies": {
        "default": {
                "httpProxy": "http://10.0.0.1:8087",
                "httpsProxy": "http://10.0.0.1:8087",
                "noProxy": "localhost,127.0.0.1,172.21.*,172.17.*,172.27.*,192.168.*,10.0.*"
            }
        }
    }
}</pre>
<p>然后启动容器，你就可以看到对应的环境变量了.</p>
<div class="blog_h3"><span class="graybg">使用代理-守护进程</span></div>
<p>docker pull等操作是发生在守护进程中的，需要按此节的说明设置</p>
<p>必须配置Docker的配置文件，命令行直接设置代理无效：</p>
<pre class="crayon-plain-tag"># Ubuntu适用
export http_proxy="http://10.0.0.1:8088"
export https_proxy="http://10.0.0.1:8088"</pre>
<p>如果使用Fedora，则：</p>
<pre class="crayon-plain-tag">mkdir -p /etc/systemd/system/docker.service.d</pre><br />
<pre class="crayon-plain-tag">[Service]
Environment="HTTP_PROXY=http://10.0.0.1:8088/"</pre>
<p> 刷出变更并重启服务：</p>
<pre class="crayon-plain-tag">systemctl daemon-reload
systemctl restart docker</pre>
<div class="blog_h3"><span class="graybg">报image not found</span></div>
<p>可能原因是没有登陆到私服</p>
<div class="blog_h3"><span class="graybg">报证书过期</span></div>
<p>报错信息：certificate has expired or is not yet valid docker</p>
<p>如果测试wget/curl等命令没问题，可能是由于Docker使用的代理导致。</p>
<div class="blog_h3"><span class="graybg">docker-proxy占用CPU高</span></div>
<p>由于NAT（Docker端口映射）导致。默认情况下Docker使用用于空间代理docker-proxy来处理NAT，性能较低。可以设置dockerd参数禁用：</p>
<pre class="crayon-plain-tag">dockerd ... --userland-proxy=false</pre>
<div class="blog_h3"><span class="graybg">挂载卷性能差</span></div>
<p>Mac OS下可能存在此问题，解决方案：</p>
<pre class="crayon-plain-tag">docker run -v /Users/alex/project:/project:cached alpine command</pre>
<div class="blog_h3"><span class="graybg">x509: certificate signed by unknown authority</span></div>
<p>进行docker login时出现此错误，可以把目标仓库的证书拷贝到<pre class="crayon-plain-tag">/etc/docker/certs.d/</pre>下，以仓库域名为子目录。</p>
<p>如果使用Let's Encrypt签名的证书，务必使用完整证书链。</p>
<div class="blog_h3"><span class="graybg">iptables: No chain/target/match by that name docker</span></div>
<p>在CentOS下，启动firewalld后，新创建Docker容器并进行端口映射会出现此错误。重启Docker即可。</p>
<div class="blog_h2"><span class="graybg">网络命名空间</span></div>
<div class="blog_h3"><span class="graybg">无法在宿主机看到</span></div>
<p>如果无法在宿主机上通过<pre class="crayon-plain-tag">ip netns list</pre>看到容器的网络命名空间，可以调用命令：</p>
<pre class="crayon-plain-tag">ln -s /var/run/docker/netns  /var/run/netns  </pre>
<div class="blog_h2"><span class="graybg">内核参数调优</span></div>
<div class="blog_h3"><span class="graybg">--sysctl</span></div>
<p>从1.12开始支持，该参数可以直接传递sysctl变量：</p>
<pre class="crayon-plain-tag">docker run --sysctl net.ipv4.ip_forward=1</pre>
<p>用于配置命名空间化的内核参数（namespaced kernel parameters）。 </p>
<p>注意，仅仅被<span style="background-color: #c0c0c0;">Docker加入白名单</span>的内核参数可以调整，否则你会收到错误：sysctl '***' is not whitelisted。白名单包括：</p>
<ol>
<li>kernel.sem、kernel.shmall、kernel.shmmax、kernel.shmmni、kernel.shm_rmid_forced</li>
<li>fs.mqueue.***</li>
<li>net.***</li>
</ol>
<p>此外，被调整的内核参数还必须是：</p>
<ol>
<li>在容器中可见，例如net.core.rmem_max</li>
<li>支持命名空间化，有些配置是全局的，不支持命名空间化</li>
</ol>
<div class="blog_h3"><span class="graybg">网络参数调优</span></div>
<p>如果使用--net=host，则使用宿主机的网络栈，直接使用宿主机的内核参数，不需要特殊操作。</p>
<div class="blog_h2"><span class="graybg">无法启动守护进程</span></div>
<div class="blog_h3"><span class="graybg">msg="[graphdriver] prior storage driver aufs failed: driver not supported"</span></div>
<pre class="crayon-plain-tag">sudo rm /var/lib/docker/aufs -rf</pre>
<div class="blog_h2"><span class="graybg">无法访问外部网络</span></div>
<p>原因未知，可能和Docker的NAT存在问题，重启Docker后解决。</p>
<div class="blog_h2"><span class="graybg"><a id="lvs-related-rst"></a>LVS相关的RST</span></div>
<div class="blog_h3"><span class="graybg">症状</span></div>
<p>访问某通过LVS暴露的接口，20%几率出现Connection Reset By Peer。进一步检查发现：</p>
<ol>
<li>此问题和Docker本身无关，但凡客户端通过NAT访问的，都有几率出现</li>
<li>curl测试，有几率出现卡死，但是目标接口速度很快</li>
</ol>
<div class="blog_h3"><span class="graybg">分析</span></div>
<p>当卡死时，最终提示：curl: (56) Recv failure: Connection timed out，Docker宿主机抓包如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2015/10/rst-timeout.png"><img class="aligncenter  wp-image-22571" src="https://blog.gmem.cc/wp-content/uploads/2015/10/rst-timeout.png" alt="rst-timeout" width="923" height="183" /></a></p>
<p>可以看到发送HTTP请求后，出现重传，随后即RST。LVS发来的TCP Dup ACK中的SLE、SRE（选择性ACK时，已经Ack的字节范围，提示对方不再传输这些范围的数据）很不正常。在客户端禁用SACK后，问题消失：</p>
<pre class="crayon-plain-tag">net.ipv4.tcp_sack = 0
net.ipv4.tcp_dsack = 0
net.ipv4.tcp_fack = 0 </pre>
<p>值得注意的是：在NATed的客户端来看，是服务器主动RST，在NAT设备来看，是它主动RST，并谎报给NATed客户端说服务器进行了重置。</p>
<div class="blog_h2"><span class="graybg">构建镜像时的问题</span></div>
<div class="blog_h3"><span class="graybg">构建镜像时Dockerfile中的apt-get命令时报错：<span style="color: #242729;">Could not resolve 'archive.ubuntu.com'</span></span></div>
<p>可能是DNS配置错误导致的，执行以下步骤修复： </p>
<ol>
<li>修改/etc/default/docker，设置正确的Docker守护程序选项，例如：<pre class="crayon-plain-tag">DOCKER_OPTS="--dns 178.79.131.110 --dns 8.8.8.8"</pre> </li>
<li>重启Docker守护程序：<pre class="crayon-plain-tag">sudo service docker restart</pre> </li>
<li>重新构建，指定no-cache选项，强制Docker镜像重新获取DNS：<pre class="crayon-plain-tag">docker build --no-cache=true ...</pre>  </li>
</ol>
<div class="blog_h3"><span class="graybg">时区如何设置</span></div>
<pre class="crayon-plain-tag">RUN rm /etc/localtime &amp;&amp; ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime</pre>
<p>部分基础镜像没有/usr/share/zoneinfo目录，需要手工拷贝/etc/localtime文件</p>
<div class="blog_h3"><span class="graybg">如何编写入口点脚本</span></div>
<p>两个简单原则：</p>
<ol>
<li>能够处理挂断信号。需要注意，docker stop / docker kill只会向PID为1的容器进程发送信号</li>
<li>能够阻止脚本退出</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">#!/usr/bin/env bash

# 捕获挂断信号，执行清理
sighdl() {
    service myservice stop
    TERM_FLAG=1
}
trap sighdl HUP INT PIPE QUIT TERM

# 启动服务
service myservice start

# 防止脚本退出的命令
while [ "$TERM_FLAG" != "1" ] ; do :; done</pre>
<p>如果防止脚本退出的命令调用了其它程序，例如：<pre class="crayon-plain-tag">tail -f /var/log/myservice.log</pre>，会出现一个子进程，虽然上述脚本的PID为1，但是子进程不会收到信号，因而容器仍然卡在那不会退出。</p>
<p>一个生产环境中入口点脚本的例子：<a href="/webrtc-server-basedon-kurento#entrypoint">Kurento的入口点脚本</a></p>
<div class="blog_h3"><span class="graybg">如何扁平化镜像</span></div>
<p>层次过多的文件系统，不但访问效率较低，而且占用更大的磁盘空间。可以export容器并import为镜像，这样产生的镜像只有一个层次：</p>
<pre class="crayon-plain-tag">docker export container-name | docker import - image:tag </pre>
<div class="blog_h2"><span class="graybg">Swarm模式相关问题</span></div>
<div class="blog_h3"><span class="graybg">节点运行服务报错：No such image</span></div>
<p>在管理节点上预先登录到Docker Hub或者私服，然后：</p>
<pre class="crayon-plain-tag">docker service create --with-registry-auth   # 添加该选项
  --replicas 10 --network overlay --name ping docker.gmem.cc/ubuntu:14.04 ping 10.0.0.1</pre>
<div class="blog_h2"><span class="graybg">运行容器时的问题</span></div>
<div class="blog_h3"><span class="graybg">如何进入无法启动的容器</span></div>
<p>有些情况下，因为配置错误，容器启动后立即退出。如果想通过Shell交互式的查问题，可以：</p>
<pre class="crayon-plain-tag"># 提交为临时镜像
docker commit mongo-c6 tempimg

# 使用Bash作为入口点进入
docker run --entrypoint=bash -it --rm  tempimg

# 解决完问题后，删除临时镜像
docker rmi tempimg</pre>
<div class="blog_h3"><span class="graybg">如何删除孤儿卷</span></div>
<pre class="crayon-plain-tag"># 列出孤儿卷
docker volume ls -qf dangling=true

# 删除所有孤儿卷
docker volume rm $(docker volume ls -qf dangling=true)</pre>
<div class="blog_h3"><span class="graybg">挂载卷的权限问题</span></div>
<p>使用<pre class="crayon-plain-tag">docker -v</pre>命令把宿主机上不存在的目录挂载到容器指定位置时：</p>
<ol>
<li>自动以root身份创建不存在的目录</li>
<li>容器中目录的所有者为root，即使目录本来存在（而被覆盖）且所有者不是root</li>
</ol>
<p>这种行为会导致潜在的无权访问的问题。</p>
<p>解决办法：容器中的用户，和宿主机的用户，以ID对应。因此，你可以预先创建宿主机目录，并chown给容器中需要使用此目录的用户的ID。 </p>
<div class="blog_h3"><span class="graybg">无法捕获信号</span></div>
<p>最好确保你的应用程序的PID为1，如果Entrypoint Spawn的子进程中运行应用程序，则Entrypoint必须能够捕获信号并执行适当的命令来停止应用程序进程。</p>
<p>无法捕获信号的常见原因：</p>
<ol>
<li>使用了Shell风格的Entrypoint，这种情况下，入口点变为/bin/sh的子进程，无法收到信号。你需要：<br />
<pre class="crayon-plain-tag"># 不要用这种形式
ENTRYPOINT "/entrypoint.sh" arg1 arg2
# 用下面的形式
ENTRYPOINT ["/entrypoint.sh", "arg1", "arg2"]</pre>
</li>
<li>入口点调用了应用程序，但是没有替换当前进程。你需要以exec方式调用目标应用程序，例如：<br />
<pre class="crayon-plain-tag">exec /jdk/bin/java $JAVA_OPTS -cp app.jar $JAVA_MAIN_CLASS $JAVA_MAIN_ARGS</pre>
</li>
<li>
<p>虽然使用exec替换进程，但是不是替换入口点进程。例如引入了管道：</p>
<pre class="crayon-plain-tag"># 这会导致在子Shell中进行
exec java | grep ... </pre>
</li>
<li>可能没有捕获信号并正确处理</li>
</ol>
<div class="blog_h2"><span class="graybg">Ubuntu相关</span></div>
<div class="blog_h3"><span class="graybg">升级到CE 18后无法启动容器</span></div>
<p>报错：OCI runtime create failed: container_linux.go:348: starting container process caused "process_linux.go:297: copying bootstrap data to pipe caused \"write init-p: broken pipe\"": unknown
<p>解决办法：</p>
<pre class="crayon-plain-tag">sudo apt-get install docker-ce=18.06.1~ce~3-0~ubuntu</pre>
<div class="blog_h2"><span class="graybg">Alpine相关</span></div>
<div class="blog_h3"><span class="graybg"> exec user process caused no such file or directory</span></div>
<p>以Alpine作为基础镜像，运行动态链接的Go应用程序，出现此报错。</p>
<p>原因是，应用程序动态链接到了 GNU libc。Alpine的musl libc库提供了对GNU libc的部分兼容性，可用于尝试解决此问题：</p>
<pre class="crayon-plain-tag">apk add libc6-compat</pre>
<div class="blog_h2"><span class="graybg">CentOS相关</span></div>
<div class="blog_h3"><span class="graybg">配置文件和命令</span></div>
<p>Docker配置信息可能位于：</p>
<ol>
<li>/usr/lib/systemd/system/docker.service</li>
</ol>
<p>Docker服务相关命令：</p>
<pre class="crayon-plain-tag">systemctl daemon-reload
systemctl restart docker </pre>
<div class="blog_h2"><span class="graybg">软件包缺失的问题</span></div>
<div class="blog_h3"><span class="graybg">add-apt-repository</span></div>
<pre class="crayon-plain-tag">apt-get install software-properties-common</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/docker-study-note">Docker学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/docker-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
