Kubernetes 的本质是一组服务器集群,它可以在集群的每个节点上运行特定的程序,来对节点中的容器进行管理。它的目的就是实现资源管理的自动化,主要提供了如下的主要功能:

  • 自我修复:一旦某一个容器崩溃,能够在 1 秒左右迅速启动新的容器。
  • 弹性伸缩:可以根据需要,自动对集群中正在运行的容器数量进行调整。
  • 服务发现:服务可以通过自动发现的形式找到它所依赖的服务。
  • 负载均衡:如果一个服务启动了多个容器,能够自动实现请求的负载均衡。
  • 版本回退:如果发现新发布的程序版本有问题,可以立即回退到原来的版本。
  • 存储编排:可以根据容器自身的需求自动创建存储卷。
node

Kubernetes 组件

一个 Kubernetes 集群主要是由控制节点(master)、工作节点(node)构成,每个节点上都会安装不同的组件。

master:集群的控制平面,负责集群的决策(管理)

  • ApiServer:资源操作的唯一入口,接收用户输入的命令,提供认证、授权、API注册和发现等机制(控制入口)
  • Scheduler:负责集群资源调度,按照预定的调度策略将 Pod 调度到相应的 node 节点上
  • ControllerManager:负责维护集群的状态,比如程序部署安排、故障检测、自动扩展、滚动更新等
  • Etcd:负责存储集群中各种资源对象的信息

简单来说就是,用户操作命令会到达 ApiServer,然后由 Scheduler 计算操作应该发往哪个 node 上进行,计算完毕,由 ControllerManager 与 node 进行最后的交互。

而这个操作的最终结果会存储到 Etcd 里。

node:集群的数据平面,负责为容器提供运行环境(干活)

  • Kubelet:负责维护容器的生命周期,即通过控制 docker,来创建、更新、销毁容器
  • KubeProxy:负责提供集群内部的服务发现和负载均衡(访问入口)
  • Docker:负责节点上容器的各种操作

Kubelet 接收 master 的操作命令,然后控制 Docker 进行具体操作。

容器运行起来之后,由于 KubeProxy 提供集群内部的服务发现和负载均衡。

components

下面,以部署一个 nginx 服务来说明 kubernetes 系统各个组件调用关系:

  1. 首先要明确,一旦 kubernetes 环境启动之后,master 和 node 都会将自身的信息存储到 etcd 数据库中。
  2. 一个 nginx 服务的安装请求会首先被发送到 master 节点的 ApiServer 组件
  3. ApiServer 组件会调用 Scheduler 组件来决定到底应该把这个服务安装到哪个 node 节点上。在此时,它会从 etcd 中读取各个 node 节点的信息,然后 按照一定的算法进行选择,并将结果告知 ApiServer
  4. ApiServer 调用 ControllerManager 去调度 node 节点来安装 nginx 服务
  5. kubelet 接收到指令后,会通知 docker,然后由 docker 来启动一个 nginx 的 pod。pod 是 kubernetes 的最小操作单元,容器必须跑在 pod 中。
  6. 至此,一个 nginx 服务就运行了,如果需要访问 nginx,就需要通过 kube-proxy 来对 pod 产生访问的代理。这样,外界用户就可以访问集群中的 nginx 服务了。

Kubernetes 概念

  • Master:集群控制节点,每个集群需要至少一个 master 节点负责集群的管控。
  • Node:工作负载节点,由 master 分配容器到这些 node 工作节点上,然后 node 节点上的 docker 负责容器的运行。
  • Pod:kubernetes 的最小控制单元,容器都是运行在 pod 中的,一个 pod 可以有 1 个或者多个容器。
  • Controller:控制器,通过它来实现对 pod 的管理,比如启动 pod、停止 pod、伸缩 pod 的数量等等。
  • Service:pod 对外服务的统一入口,下面可以维护着同一类的多个 pod。
  • Label:标签,用于对 pod 进行分类,同一类 pod 会拥有相同的标签。
  • Namespace:命名空间,用来隔离 pod 的运行环境。
concept

应用部署方式演变

  • 传统部署:互联网早期,会直接将应用程序部署在物理机上。
    • 优点:简单,不需要其他技术的参与。
    • 缺点:不能为应用程序定义资源的使用边界,很难合理地分配计算资源,而且应用程序之间容易产生影响。
  • 虚拟化部署:可以在一台物理机上运行多个虚拟机,每个虚拟机都是独立的一个环境。
    • 优点:程序环境不会相互产生影响,提供了一定程度的安全性。
    • 缺点:增加了操作系统,浪费了部分资源。
  • 容器化部署:与虚拟化类似,但是共享了操作系统。
    • 优点:
      • 可以保证每个容器拥有自己的文件系统、CPU、内存、进程空间等。
      • 运行应用程序所需要的资源都被容器包装,并和底层基础架构解耦。
      • 容器化的应用程序可以跨云服务商、跨 Linux 操作系统发行版进行部署。
deployment

容器化部署的问题

  • 一个容器故障了,怎么样让另外一个容器立刻启动去替补停机的容器。
  • 当并发访问量变大的时候,怎么样做到横向扩展容器数量。

这些容器管理的问题统称为容器编排问题,为了解决这些容器编排问题,就产生了一些容器编排的软件:

  • Swarm:Docker 自己的容器编排工具。
  • Mesos:Apache 的一个统一管控工具,需要和 Marathon 结合使用
  • Kubernetes:Google 开源的容器编排工具。

禁止 ip 1.2.3.4 访问服务器

1
2
firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=1.2.3.4 drop"
firewall-cmd --reload

最后一个 drop 的效果,不返回结果给请求地址,所以客户端会等待直至超时。

reject/drop 说明

移除 ip 禁止访问规则

1
2
firewall-cmd --permanent --remove-rich-rule 'rule family="ipv4" source address="1.2.3.4" drop'
firewall-cmd --reload

列出所有规则

1
firewall-cmd --list-all

reject 和 drop 的区别

在firewalld中,DROP很简单就是直接丢弃数据,并不反馈任何回应。需要Client等待超时,Client容易发现自己被防火墙所阻挡。而REJECT则会更为礼貌的返回一个拒绝(终止)数据包(TCP FIN或UDP-ICMP-PORT-UNREACHABLE),明确的拒绝对方的连接动作。连接马上断开,Client会认为访问的主机不存在。

至于使用DROP还是REJECT更合适请大家根据自己的使用场景进行选择, REJECT是一种更符合规范的处理方式,并且在可控的网络环境中,更易于诊断和调试网络/防火墙所产生的问题;而DROP则提供了更高的防火墙安全性和少许的效率提高,但是由于DROP不很规范(不很符合TCP连接规范)的处理方式,可能会对你的网络造成一些不可预期或难以诊断的问题。因为DROP虽然单方面的中断了连接,但是并不返回任何拒绝信息,因此连接客户端将被动的等到tcp session超时才能判断连接是否成功。

在部署防火墙时,如果是面向企业内部(或部分可信任网络),那么最好使用规范的REJECT方法,对于需要经常变更或调试规则的网络也是如此;而对于面向危险的Internet/Extranet的防火墙,则有必要使用更为粗暴但是安全的DROP方法,可以在一定程度上延缓攻击的进度或难度。

在上周一次跟同事聊天的时候,他提到自己最近在尝试搭建一个 gerrit 服务器(一个代码审查平台),但是尝试多次未果,而且还将 ubuntu 虚拟机多次搞崩重装。

(这让我想起了自己大学时候折腾 ubuntu 时也是搞崩了无数次,而且不是虚拟机,有一段时间里,每天就重复着安装、崩溃、重装的过程,说实话有点枯燥无味,不过这个过程中熟悉了不少 linux 常用命令的使用。下图是以前折腾出来的一个 ubuntu 的 3D 效果,em... 当初搞这个纯粹是觉得酷炫,实际上没什么用。)

ubuntu-3d.jpg

然后我表示为什么不用 Docker,比虚拟机方便多了,然后才得知,原来他觉得虚拟机也很方便,但实际上很多场景下跑个 Docker 容器比启动一个虚拟机方便多了。最后应对方要求,写一篇关于 Docker 入门的指南,也就有了本文。

本文旨在帮助大家建立起对 Docker 的一个整体印象,至于具体如何使用更多是需要自己去实践。毕竟 Docker 的内容太多了,但我们实际用到的内容其实只是它最核心的一部分。

先通过这个实际的例子来比较一下传统虚拟机跟 Docker 之间在使用上存在的时间差别吧。

Docker 启动一个 gerrit 有多简单

对于大部分开发者来说,可能需要做的只是使用,比如,gerrit,为了达到快速体验一项技术的目的,我们可以直接忽略掉运行环境的复杂性(当然,想卷起来也可以去自己搞懂整个部署流程,甚至去看看源代码什么的)。

而 Docker 就为我们提供了这一种便利,通过 Docker,我们通过一两个命令就能启动一些我们想要的软件,比如下面两个命令就可以直接启动一个 gerrit 服务器:

1
2
docker pull gerritcodereview/gerrit
docker run -ti -p 8080:8080 -p 29418:29418 gerritcodereview/gerrit
gerrit.png

虚拟机的方式有多麻烦

至此,我们两条命令就可以把一个 gerrit 服务器运行起来了,不需要先安装一个虚拟机软件,再在虚拟机里面装一个系统,然后再安装 gerrit 需要的依赖,然后再部署 gerrit。

我们可以通过下图对比一下:

comparison.png

左边的第 5 步和右边的第 2 步是一样的。具体启动时间取决于应用本身。

我们可以明显看到通过虚拟机的方式我们需要做的步骤明显多了,而且其中每一步需要的时间都是一个未知数,如果对 linux 本身也不太熟悉,那这个过程可能就更长了。

相比之下,使用 Docker 的方式,我们只需要简单的两步就可以达到同样的目的。如果本身就不熟悉 linux,然后又要在真实的 linux 环境下部署一个,花的这些部署时间可能足够熟悉 gerrit 本身了。

Docker 是什么

我们可以从两个角度简单理解:

  1. 我们尽可以简单地把 Docker 看作是类似传统虚拟机一样地东西,本质上也是一种虚拟化的技术。但相比传统虚拟机,它更加轻量、使用更加便捷。

最直观的,我们可以直接从现有的镜像开始,构建我们的应用程序。而不用像传统虚拟机那样,自己先在虚拟机上安装一个操作系统,再去安装依赖,再安装我们真正需要的软件。

开始下文之前,让我们再来重新审视一下 Docker 的 logo 本身:

docker-logo.png

这个 logo 看上去像是一艘载着货物的船,Docker 本身就是这么运作的,通过启动一个 Docker 引擎,然后不同的容器可以运行在 Docker 引擎之上。

另外,我们开发的时候可以基于 Docker 容器提供的环境开发,我们开发完的应用可以打包成 Docker 镜像,然后传输到不同服务器上,而往往我们的应用是由多个依赖组成的(这些依赖也可以构建出一个个的镜像),然后再在服务器上通过我们传输的这一系列镜像启动不同的容器。

同时,Docker 引擎本身是不同平台都可用的,所以我们的应用并不依赖于实际的平台,只依赖于 Docker 引擎本身(实际上我们所需要的依赖都在不同的镜像里面包含了,并不是只有 Docker 引擎就够了)。

所以,我们可以有另外一个关于 Docker 是什么的描述:

  1. Docker 是一个用于开发、传送和运行应用程序的开放平台。Docker 使您能够将应用程序与基础设施分开,以便您可以快速交付软件。使用 Docker,您可以像管理应用程序一样管理基础设施。

Docker 跟虚拟机对比

docker-vs-vm.png

我们可以看到,传统虚拟机中,可能需要虚拟出多个不同的操作系统环境,才能实现在不同操作系统环境运行我们需要的软件。比如,我们一个应用依赖于 CentOS 环境,另外一个应用依赖于 Ubuntu 环境,我们就需要安装两个操作系统对应的虚拟机,这一步是非常耗时而麻烦的,而且,这个虚拟出来的操作系统环境也是很占资源的。

而在 Docker 里面,Docker 的容器引擎提供了我们容器所需的环境,而且所有的容器都是使用唯一一个容器引擎,相比较之下,传统虚拟机的方式显得有点笨重。

总的来说,Docker 相比传统虚拟机有以下优势:

  1. 使用 Docker 的情况下不需要为每一个容器虚拟出一个操作系统环境,而传统虚拟机的方式如果需要不同的操作系统环境支持,则必须要构建出不同的操作系统环境(就算我们这个虚拟机不运行其他任何东西,这个虚拟出来的操作系统就已经占用很多资源了)。
  2. Docker 启动一个容器所需要的资源非常小,比如一个 nginx 启动起来的容器可能就需要占用几 M 的内存。
  3. Docker 启动一个容器所需要的时间非常短,启动容器的过程可能只需要一瞬间,相比传统虚拟机启动一个虚拟操作系统快很多倍。

Docker 里面镜像跟容器是什么?

上面有两个反复提到的词,"镜像"、"容器",这两个是 Docker 里面核心的几个概念之一了,所以很有必要先解释一下。

我们理解镜像跟容器,可以对比一下程序和进程:

  1. 什么是程序,一个可以直接运行的二进制文件我们称它为程序,它本质上是一个文件。比如,redis 二进制文件(redis 安装目录里面 bin/redis 这个文件),这个就是一个程序。
  2. 什么是进程,我们运行程序的时候,操作系统会创建一个进程,这个进程里面做的事情就是我们预先在程序里面定义好的。比如,运行 bin/redis 的时候,我们的计算机上就多了一个 redis 的进程。

有了程序跟进程的大概印象,再来理解一下镜像跟容器:

  1. 什么是镜像,镜像实际上也是一个静态的概念,实现上可能表现为磁盘上的一个文件或者多个文件的集合。
  2. 什么是容器,容器就是我们通过 Docker 引擎运行的一个镜像的实例。

我们可以从一个程序启动多个进程,同样的,我们也可以从一个镜像启动多个容器。所以,我们可以将镜像视作 Docker 里面的程序,将容器视作 Docker 里面的进程。

实际上,Docker 容器本质上就是一个进程,这个进程跟传统进程不一样的是,它依赖的东西由镜像提供,而不是操作系统提供(比如动态链接库这些)。所以 Docker 镜像的作用是为我们程序运行提供所需要的依赖。

Docker 架构

这里引用一下官网文档里面的架构图:

docker-arch.png

理解了这个图,我们实际上就已经掌握了 Docker 的核心。

我们可以通过上面的例子来讲述使用 Docker 的实际步骤:

  1. 安装配置 Docker,启动 Docker。这一步没啥好说的

  2. 拉取 gerrit 镜像。

这一步我们会从 register 获取 gerrit 镜像文件,拉取的过程就是,我们在命令行上运行 docker pull gerritcodereview/gerrit 的过程,这个过程会做以下事情:我们的命令会发送给 Docker 引擎(也就是图上中间的 Docker daemon),Docker 引擎会从 register 下载 gerritcodereview/gerrit 这个镜像文件到本地,最后退出

  1. 启动 gerrit 容器。(这一步我们会从 Docker 的 gerrit 镜像启动一个 gerrit 容器)

这一步我们通过运行 docker run -ti -p 8080:8080 -p 29418:29418 gerritcodereview/gerrit 实现,这个过程,Docker 引擎会启动一个新的进程,这个进程里面运行的就是我们镜像指定的程序(这个下面细说,现在需要记住,一个容器本质上就是一个进程)。

好了,现在我们对于 Docker 的运行机制有了一个大概的印象了,接下来可以详细说说这个图:

Client

  1. 这个是指我们运行命令的工具,最常见的就是 shell,比如:
1
docker pull redis

这里,我们的终端(windows 下是 cmd 窗口,macos 是 terminal 窗口)就是一个 client。

  1. 当然也可以是其他自己实现或者第三方实现的,因为 Docker 还可以通过 rest api 的方式来操作。所以只要可以向 Docker 引擎发起 HTTP 请求,都可以视作是一个 Client。

DOCKER_HOST

这是 Docker 的核心部分,我们的 Docker 引擎位于这一部分,这里面也细分为三个主要部分:Docker daemon、Containers、Images。

Docker daemon

这个在实际使用中就是一个 dockerd 进程,就是一个 Docker 的守护进程。它的作用是,监听客户端的请求(比如我们运行 docker run 的时候就是向 Docker daemon 发起了一个请求),同时管理 Docker 里面的所有其他东西,比如容器、镜像、网络、卷等。

Containers

这一块就是一堆的容器,每一个容器本质上就是一个进程。(当然实际使用中,一个容器可能是有多个关联的进程的,比如它的子进程)

Images

这一块就是一堆的镜像,本质上是一堆的静态文件的集合。

Registry

Registry 就是存储 Docker 镜像的地方。类似 github,只不过上面保存的是 Docker 镜像,同样我们也可以上传自己的镜像上去。

一般来说我们都是使用 Docker 官方的 Registry,我们也可以搭建自己私有的 registry,用来存储一些不想对外公开的 Docker 镜像。

Docker 工作流程

我们来看看一个带了序号标记的 Docker 架构图:

docker-arch-1.png

上面也说了,理解了 Docker 的架构图,也就掌握了 Docker 的核心。所以这里详细说一下它的核心工作流程:

  • 1+4:运行 docker build 的时候,Docker daemon 会根据我们定义的 Dockerfile 文件来生成一个镜像文件,这个镜像文件保存到本机(4)。

这一步的作用是构建我们自己的镜像,最常见的应用场景是,软件官方提供的镜像不能够满足我们的需要,我们需要在官方提供的镜像的基础上,做一些自己的改动,比如添加一些我们所需要的依赖(比如,往 php 镜像里面加几个扩展)。

  • 2+7+5:运行 docker pull 的时候,Docker daemon 会根据我们在后面传递的参数,从 Registry 上面下载对应的镜像文件,7 是向 Registry 服务器发起请求,5 是 Registry 服务器响应请求,最终的结果就是,我们本地的镜像里面多了一个镜像。

这一步的作用是拉取我们所需要的镜像,不管是不是我们自己构建的镜像,都会发生这一步。如果我们是从别人提供的镜像上构建一个新的镜像,我们运行 docker build 构建属于我们自己的镜像的时候,就会从 Registry 拉取基础镜像。

如,docker pull redis,这个命令会从 Registry 下载 redis 最新的镜像文件,然后保存到本地。这个命令也可以指定版本,如 docker pull redis:5 这个命令下载 tag 为 5 的 redis 镜像。至于有哪些 tag 可以用,我们可以在官网的 Registry 上面搜索一下。

  • 3+8+6(可能会发生 7+5 这两步操作):运行 docker run 的时候,Docker daemon 会根据参数从本地的镜像列表获取对应的镜像(7),然后从这个镜像启动一个容器(6)。如果本地没有这个镜像,会从 Docker Registry 先拉取这个镜像(跟上一点说到的流程一致)。

这一步的作用就是我们实际所需要的,它会从镜像启动一个新的容器。

如,docker run redis,这个命令会看本地有没有 redis:latest 这个镜像(tag 省略的话,默认是 latest),有的话,直接从这个镜像启动一个容器,如果本地没有这个镜像,则会先从 Registry 上下载这个镜像,然后再启动容器。

再捋一捋 Docker 里面的几个核心概念

  • Docker daemon

这个是 Docker 的守护进程,如果我们想使用 Docker 的话,首先得把 Docker 启动起来是吧,我们启动起来的那个进程就是 Docker daemon。

  • 镜像(Image)

官方描述:一个 Docker 镜像就是创建 Docker 容器的只读模板。通俗地说,镜像就是一个我们程序的运行环境,当然你也可以将自己的程序也放进镜像里面。

  • 容器(Container)

官方描述:一个 Docker 容器就是一个 Docker 镜像的实例。本质上就是,一个进程,而这个进程所需要的依赖由镜像提供。

  • Registry

保存镜像的地方。我们一般是使用官方提供的,也可以搭建自己的 Registry 用来保存一些私有镜像,或者为了达到加速拉取镜像的目的。

容器本质上是一个进程

上一小节说了,镜像就一个我们程序的运行环境。虽然我们用了程序跟进程的概念来做比较,但本质上,一个 Docker 镜像只是为我们程序运行提供环境。而我们的容器,实际上是我们在这个容器里面执行某个程序产生的进程。

比如,我现在有一段 nodejs 文件:

test.js,位于目录 /Users/ruby

1
2
3
4
5
6
7
8
9
const fs = require('fs')

try {
const data = fs.readFileSync('/test.js', 'utf8')
console.log('data:')
console.log(data)
} catch (err) {
console.error(err)
}

但是我本机没有安装 nodejs,现在想通过 Docker 来执行我们这段代码,具体需要怎么做呢?

  1. 拉取 node 镜像
1
docker pull node
  1. 从 node 镜像创建一个容器来运行我们这个文件
1
docker run -v /Users/ruby/test.js:/test.js node node /test.js

这个命令怎么理解

这个命令很长,所以画了个图来更直观地阐释一下:

docker-run.png

这个图里面的命令有两种颜色区分开两种不同含义的内容:

第一部分表示是 Docker 相关的命令以及参数,而第二部分就是我们的程序名称以及传递给我们程序的参数。

需要注意的地方是:

  • Docker 里面针对不同的内容有不同的二级命令,比如管理镜像是 docker image,管理 Docker 网络是 docker network
  • 图中的 3 是一系列的选项以及参数,当然这部分是可以省略的,如果我们的确不需要什么参数。
  • 图中的 4 是镜像的名称,表示我们基于哪个镜像提供的运行环境来运行 5 那个程序。比如,我们想用一个 12.0.0 的版本来运行我们的程序,我们就可以 docker run -v /Users/ruby/test.js:/test.js node:12.0.0 node /test.js。(需要特别注意的是,这里的 4 跟 5 看起来一样,但本质上是完全不同的)
  • 5 以及后面那部分,也是一个命令以及参数,只不过这个命令是运行在 4 这个镜像提供的运行环境上的,而不是在我们的宿主机上运行的。另外,第 6 那个 /test.js 以及后面接的所有其他内容,都是传递给容器里面的 node 程序的参数。

为什么说容器是一个进程

经过上面对于命令图解,我们也看到了,实际上到最后,我们运行的命令是 node /test.js,而我所说的 "容器是一个进程" 说的就是这个,我们启动容器最后的步骤其实就是运行我们指定的程序。

只不过相比较于我们直接在宿主机上运行 node /test.js,通过容器的方式我们使用的是 Docker 镜像提供的运行环境。如果我们想在宿主机上运行,我们就需要在宿主机上安装 node 的运行环境。

为什么执行命令之后就容器就退出了

我们执行了上面那个 docker run 命令之后,输出了一些东西然后就退出了,可能有很多初学者会觉得很奇怪,为什么退容器也出了。这也跟上面说的容器是一个进程本质上是同一个问题,进程都是做完它应该做的事情就退出了的,把容器看作进程就很容易理解了。

上面这个也一样,在容器里面执行完 node /test.js 的时候,node 的工作就已经完成了,所以进程就退出了。具体表现就是,我们的容器也停止了。

如何指定容器要执行的程序

  1. 如果是通过 Dockerfile 来构建的镜像,可以在 Dockerfile 里面最后一行通过 RUN 指令来指定。
  2. 如果使用 docker-compose,则可以在 docker-compose 里面通过 command 或者 entrypoint 来指定。
  3. 可以在 docker run 命令的镜像名称参数后面指定(上图的 5)。

启发

我们在使用 Docker 的过程中,尽管可以把容器理解为一个进程,我们需要什么样的依赖,就通过 Docker 来启动一个容器(进程)。比如,我们想使用 redis,就可以通过 docker run 来启动一个 redis 进程(redis 容器)。

这样看来,其实 Docker 也没有什么神秘的地方了,我们使用起来也不会有太多的心智负担。

如何开始使用 Docker

花了很大的篇幅来讲 Docker 的架构,现在基于它的架构回到最初的例子来解释一下如何开始使用 Docker。

目标:启动一个 gerrit 服务器。

详细步骤:

  1. 上面说了,镜像提供了我们所需运行的进程的环境。所以这一步我们需要先拉取 gerrit 对应的镜像。

所有我们能想到的常用的软件基本上都有现成的镜像,具体可以在 hub.docker.com 上面搜索一下,我们在上面搜索一下 "gerrit":

docker-search.png

我们这里需要注意一下,有一些名字一样的,但不是官方提供的,我们可以看一下右上角有多少下载就可以知道哪些是官方提供的了。当然这个图上面官方镜像说明那里也明说了,"Official Gerrit Code Review Docker image"。

我们点击进去看看,右边的 "Docker Pull Command" 就我们拉取这个镜像的命令,点击一下红框部分就可以复制这个命令了:

docker-image-detail.png

复制之后,在命令行运行这个命令:

1
docker pull gerritcodereview/gerrit

成功运行这个命令之后,我们本地就有了 gerritcodereview/gerrit 这个镜像了。我们可以通过 docker images 列出我们目前的镜像列表:

1
docker images

输出:

1
2
3
REPOSITORY                TAG            IMAGE ID       CREATED        SIZE
node latest 3b66eb585643 2 days ago 905MB
gerritcodereview/gerrit latest fa5925fec1e3 7 weeks ago 693MB
  1. 上面也说了,容器就是基于镜像启动的一个进程。所以这一步我们需要做的是,通过我们下载的 gerritcodereview/gerrit 镜像来启动一个容器。

这一步也很简单,docker run 然后再指定一个镜像名称就行了:

1
docker run -ti -p 8080:8080 -p 29418:29418 gerritcodereview/gerrit

不出以外的话,过一会应该可以看到如下输出:

1
2
3
4
5
[2021-10-25T07:21:58.234Z] [main] INFO  org.eclipse.jetty.server.handler.ContextHandler : Started o.e.j.s.ServletContextHandler@174e79f9{/,null,AVAILABLE}
[2021-10-25T07:21:58.271Z] [main] INFO org.eclipse.jetty.server.AbstractConnector : Started ServerConnector@6e8426aa{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
[2021-10-25T07:21:58.272Z] [main] INFO org.eclipse.jetty.server.Server : Started @9345ms
[2021-10-25T07:21:58.280Z] [main] INFO com.google.gerrit.pgm.Daemon : Gerrit Code Review 3.4.1 ready
[2021-10-25T07:23:11.551Z] [plugin-manager-preloader] INFO com.googlesource.gerrit.plugins.manager.OnStartStop : 65 plugins successfully pre-loaded

这里的日志可以看出我们的服务器正常启动了,再通过浏览器访问 http://localhost:8080 就可以访问 gerrit 了。

这里的 -it 可以在启动之后直接在前台运行,-p 指定端口映射,可以指定多个。

到此为止,我们的目的就已经达到了。重要的步骤就这两步了。

什么时候使用 Docker

  1. 想使用某个软件,但是又不想在自己的电脑上安装太多东西。(这也是很多人使用 Docker 的原因)
  2. 只是想体验一下某项技术(比如,PHP8 出来了,想体验一下语言的新特性),这种情况下,我们的目的很单纯,就是想用一用这项技术(如果只是为了验证一行代码而花了大量时间在环境搭建上就本末倒置了)。我们都知道在宿主机上安装一个 PHP 并且跑起来是一件非常费劲的事,有那个时间都够在容器里面把新特性跑个遍了。
  3. 团队统一环境。在以前,很多人会采用虚拟机的方式,但是 Docker 比传统虚拟机的方式不止方便一点点。
  4. 生产环境部署使用,Docker 相比以往的部署方式,直接屏蔽了不同平台的差异,只要 Docker 能安装,容器一般也就能跑,所以部署成本低了很多。同时也可以限制每个容器所能使用的资源,防止某个进程占用资源过多对整个服务器造成影响。另外,配合 k8s 也可以很方便对这些 Docker 容器进行管理。

总结

  1. Docker 本质上也是一种虚拟化技术,但是相比传统虚拟机,不需要每一个容器都创建一个操作系统环境,所有的容器都依赖于 Docker 引擎。
  2. Docker 镜像是我们程序的运行环境。
  3. Docker 容器本质上是在镜像提供的环境上运行的一个进程。
  4. Docker daemon 是 Docker 用以管理镜像、容器等东西的一个守护进程。
  5. Docker Registry 是我们拉取镜像的地方,当然我们也可以将我们自己构建的镜像上传到 Registry。(可以理解为镜像的托管平台,类似于 github 跟开源项目的关系。)
  6. Docker 容器执行的程序,在进程终止的时候,容器也会停止运行。

后记

很遗憾,本文不是一个速成的指南,也没有说到什么 Docker 命令的实际使用,也没有介绍什么 Docker 相关工具的使用(比如 docker-compose),毕竟说了也没什么用,大家也记不住,最终使用的时候还是会去搜索对应的命令是什么。不过相对完整地向读者展现了 Docker 的架构以及一个整体的运作流程,足以帮助读者建立起对 Docker 的基本印象了。

至于其他的东西,比如 docker run 后面有哪些选项、又或者怎么构建自己的镜像、怎么使用 docker-compose 等等这些其实开始使用的时候不需要了解太多,在真正需要用到的时候搜索一下就行了,我们的记忆力毕竟是有限的,而且记住太多东西也不一定会帮助我们去更好地使用。

反而,虽然我们只是花了少量时间去建立起了对 Docker 的一个整体印象,但是对于实际使用中遇到的百分之八九十的问题,我们都可以回到这个整体印象中来,去思考是哪个环节出的问题。也就是说,我们的思考有了一个抓手,就算问题出现了,我们也可以根据现象跟这个整体印象建立联系,从而找到解决问题的方向,而不是迷失在纷繁芜杂的细节中。

  1. UI 操作是通过以下的方式来操控具体的 app 的
1
2
3
4
5
6
7
tell application "Capture One 21"
tell application "System Events"
tell process "Capture One 21"
-- 这里是对具体应用的 UI 模拟操作
end tell
end tell
end tell
  1. 模拟按下键盘

使用键盘按键对应的 key code,示例:

1
2
3
tell application "System Events"
key code 49
end tell

所有 keycode:

keycode.png
  1. 模拟按下组合键

如:shift+command+G

1
key code 5 using {shift dow, command down}
  1. 查看所有可操作的 UI 元素可使用 UI elements,这一点非常有用,在 app 不支持 applescript 的时候,我们只能通过模拟人为去操作 UI 元素,而这个 UI elements 可以让我们具体元素的名称是什么。
1
2
3
4
5
6
7
tell application "Capture One 21"
tell application "System Events"
tell process "Capture One 21"
UI elements
end tell
end tell
end tell

输出:

1
{window "Capture One Catalog" of application process "Capture One 21" of application "System Events", menu bar 1 of application process "Capture One 21" of application "System Events"}

如果我们想再获取 UI 元素里面的子元素,可以使用 of,如:

1
2
3
4
5
6
7
tell application "Capture One 21"
tell application "System Events"
tell process "Capture One 21"
UI elements of window "Capture One Catalog" -- 这里的 window "Capture One Catalog" 是通过 UI elements 获取的元素
end tell
end tell
end tell

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{image 1 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
static text "1/90 s" of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
slider 1 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
button 1 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
static text "ISO 200" of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
static text "Alex_Benes.RAF" of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
UI element 7 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
static text "120" of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
static text "148" of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
static text "125" of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
static text "100" of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
button 2 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
button 3 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
button 4 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
group 1 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
static text "1/6" of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
button 5 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
button 6 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
button 7 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
button 8 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
group 2 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
radio group 1 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
toolbar 1 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
button 9 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
button 10 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
button 11 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
image "Capture One.cocatalogdb" of window "Capture One Catalog" of application process "Capture One 21" of application "System Events",
static text "Capture One Catalog" of window "Capture One Catalog" of application process "Capture One 21" of application "System Events"}

同理,如果我们再想获取下一级的子元素,同样可以使用 of,如:

1
2
3
4
5
6
7
tell application "Capture One 21"
tell application "System Events"
tell process "Capture One 21"
UI elements of group 1 of window "Capture One Catalog"
end tell
end tell
end tell

输出:

1
{scroll area 1 of group 1 of window "Capture One Catalog" of application process "Capture One 21" of application "System Events"}
  1. 我们可以使用 script editor 的 File->Dictionary 来查看某一个 app 具体有哪些命令或者类,这样我们就可以在 applescript 中使用它们,这个可能不太准确。

准确的层级结构可以 script debugger 来查看,在 script debugger 里面输入如下一行即可查看:

1
tell application "Capture One 21" to set appObjectModel to it
  1. 可以使用 xcode 里面的 Accessibility Inspector 来查看大概有哪些 UI 元素

  2. 查看应用里面的属性(在 apple script 里面,一个应用实际上会映射为 apple script 里面的一个对象,就是 application xx 这是一个 apple script 对象)

1
2
3
4
5
6
tell application "Capture One 20" to set a to it

tell application "Capture One 20"
get properties -- 获取 "Capture One 20" 的属性
get properties of viewer of a -- 获取 "Capture One 20" 里面的 viewer 里面的属性
end tell

我们可以通过这种方式获取到一个应用的对象结构。

参考链接:

  1. applescript key code
  2. A Strategy for UI Scripting in AppleScript
  3. UserInterfaceScripting
0%