docker 时间轴

docker 是容器技术中的一种. 最早是由 dotcloud 公司开源的一款产品. dotcloud 公司是2010年新成立的一家公司, 主要基于 PaaS 平台为开发者提供服务. 在 PaaS 平台下, 所有的服务环境已经预先配置好了, 开发者只需要选择服务类型, 上传代码后即可对外提供服务, 不需要花费大量的时间来搭建服务和配置环境.

2013年, dotcloud 的 CEO 决定把 dotcloud 内部使用的 container 容器技术单独拿出来开源. 2013年3月发布 docker 的 V0.1版本, 并且保持每月一个版本的迭代速度. 到同年8月, docker 技术已经足够火爆. 同年10月, dotcloud 公司更名为 Docker 股份有限公司.

2014年6月9日, Docker 发布 V1.0版本, 并举办了首届 DockerCon 大会, 大会上来自 Google, IBM, RedHat, Rackspace 等公司的核心人物均发表了主题演讲🗣并纷纷加入 Docker 的阵营.

2014年12月, Docker 发布了集群管理工具 Machine 和 Swarm.

2016年1月, Docker 官方计划全面支持自身的 Alpine Linux, 使用它构建的基础镜像最小只有5M

什么是 Docker

按照官方的说法, Docker 是一个开源的应用容器引擎.

拿 Java 来说, 在 Java 之前的编程语言, 像 C/C++, 是严重依赖平台的, 在不同的平台下, 需要重新编译才能运行. Java 的一个非常重要的特性就是与平台无关性, Java 虚拟机(JVM)是实现这一特性的关键. Java 虚拟机屏蔽了与具体平台的相关性, 使得 Java 语言编译程序只需生成可以在 Java 虚拟机上运行的目标代码, 就可以在多种平台上不加修改的运行. Java 虚拟机在执行字节码的时候, 把字节码解释成具体平台上的机器指令执行.

Java 的理念是 “Write Once, Run Anywhere”, 而 Docker 则提出了”Build Once, Run Anywhere, Configure Once, Run Anything”

在 Java 的 JVM 中, 屏蔽了系统平台的干扰; 在 Docker 使用的容器技术中, 则屏蔽了除内核外的所有干扰. 运行在Docker 内的容器, 只对内核敏感. (相对于宿主机而言)

如果说 Java 是帮助我们把应用打包, 可以运行在任何装有 JVM 的服务器上; 那么使用 Docker 就是帮我们实现应用与环境的统一打包, 可以在任何装有 Docker 的服务器上运行.

在 Ubuntu 下直接使用useradd username添加用户可能会导致用户没有家目录或 tab 无法自动补齐的问题, 需要单独制定这些进行配置

1
useradd -r -m -s /bin/bash username

-r 创建一个系统用户
-m 声明为该用户创建默认的家目录(可以单独指定)
-s 指定该用户登录时使用的终端类型

在 docker 的使用中, 常常需要使用普通用户去操作 docker 命令, 简单的办法就是把需要操作 docker 的普通用户加入到 docker 组中即可, 但是需要注意不要覆盖掉用户原来所属的组

1
usermod -a -G docker username
  • -a: append 使用追加的方式添加到组, 不必离开现有的用户组
  • -G: 需要加入的目标组

默认情况下不管是 centos 系列还是 ubuntu 系列, 默认的 history 命令都只是显示了命令编号和具体命令这两列信息, 在实际生产环境中, 添加时间的显示十分实用和重要

编辑/etc/profile文件, 添加入下一行即可

1
export HISTTIMEFORMAT="`whoami` %F %T "
  • %F: 日期
  • %T: 时间

导出数据库的时候报如下错误 mysqldump: Error 2013: Lost connection to MySQL server during query when dumping tablemailat row: 2637433

查询资料

大概说是因为mysqldump来不及接受mysql server端发送过来的数据,Server端的数据就会积压在内存中等待发送,这个等待不是无限期的,当Server的等待时间超过net_write_timeout(默认是60秒)时它就失去了耐心,mysqldump的连接会被断开,同时抛出错误Got error: 2013: Lost connection。

解决方案一

增加net_write_timeout可以解决上述的问题的。在实践中发现,在增大 net_write_timeout后,Server端会消耗更多的内存,有时甚至会导致swap的使用(并不确定是不是修改 net_write_timeout所至)。建议在mysqldump之前修改net_write_timeout为一个较大的值(如1800),在 mysqldump结束后,在将这个值修改到默认的60。

在sql命令行里面设置临时全局生效用类似如下命令:
SET GLOBAL net_write_timeout=1800;

修改了这个参数后再备份,不再报错
注意,这个参数不是mysqldump选项,而是mysql的一个配置参数

解决方案二

在执行 mysqldump 的时候可以通过添加 --quick 的参数来避免出现这样的问题

1
2
3
 --quick,-q

该选项用于转储大的表。它强制mysqldump从服务器一次一行地检索表中的行而不是检索所有行并在输出前将它缓存到内存中。

参考文档

http://www.linuxyw.com/linux/yunweiguzhang/20130609/566.html

http://www.cnblogs.com/haven/archive/2012/10/27/2742141.html

查看全局变量

1
2
mysql> show global variables;
mysql> show global variables like '%timeout';

设置全局变量

1
mysql> set global innodb_ft_min_token_size=1;

在 docker 的概念中, 有两套导入导出的概念. 分别是export 和 import还有save 和 load 百度搜索🔍它们的区别, 有一篇被引用了很多次的文章虽然讲的很详细, 还加入了一些实验验证, 但是试验中为了实现同一个效果而使用了 commit 命令更改了镜像内容. 理解和使用起来可能会有些偏差, 本文将以最简单明了的方式介绍 docker 中的两套导入导出功能

export&import save&load
操作对象 容器 镜像
导出对象 tar 文件 tar 文件
导入对象 镜像 镜像
镜像层数 一层 多层

下面我们来一行一行的解释

操作对象

操作的对象不同, export 和 import 是用来导入导出容器用的, 而 save 和 load 是用来导入导出镜像用的.

例如: 你需要把 A 机器上的 容器迁移到 B 机器, 且 容器中有重要的数据需要随之一起迁移的, 就可以使用 export 和 import 参数来导入和导出

例如: 你的服务器不能 pull 下来某个镜像, 但是你有一台海外的机器, 你可以在海外的那台机器上把需要的镜像 pull 下来, 然后把该镜像通过 save 命令导出为一个 tar 包, 再将 tar 包拉回到本地导入

所以, 从功能定位上, 你就记住 save 和 load参数是用来迁移镜像的这个场景就行了, 另外的 export 和 import当然就是用来迁容器的

导出对象

在导出对象这一点, exportsave 导出的文件形式都是一样的, 两者都是导出为一个 tar 包

导入对象

在导入上, importload 通过 tar 包导入的都是一个镜像

导入的镜像层数

最大的区别就在这里, 通过export 和 import导出的容器形成镜像时, 该镜像只有一层

通过saveload 导出的镜像保留了原镜像所有的层次结构, 导出时原镜像有几层, 导入的时候就还是有几层

想导出容器, 但是还想保留层次结构怎么办?

导出容器, 很快就想到唯一一个可以导出容器的工具 export

但是又想保留底层镜像的层次结构, 那么 export 就不符合需求了

想想导出带层次结构的工具就只有镜像导出工具 save 了, 但是容器在镜像层之上还有一层新的数据怎么一起导出去呢?

这个时候就需要引入一个新的参数 commit, 用来保存容器现有的状态为一个新的镜像

比如在 A 机器上运行的 容器是基于 甲方乙方 这个镜像跑起来的, 那么我就可以通过 commit 参数, 将 容器的所有内容保存为一个新的镜像, 名字叫 私人订制 (内含一梗哦😆) 最后我再通过镜像导出工具 save 就可以完整的将 私人订制镜像(也就是 甲容器 )导出为一个 tar 包了

而且包含了 X+1 层镜像, X 层是原镜像 甲方乙方 的所有镜像层数, 1是容器 多的那一层可写层的镜像

总结

总结来说我也希望大家使用 docker 能遵循 docker 的设计初衷, 运行一个无状态的容器. 如果是无状态的容器, 就不会产生 想导出容器, 但是还想保留层次结构怎么办? 这样的奇葩问题了.

所以最后还是要再次强调:

  • export 和 import 是用来导出导入容器用的, 导出的是容器的文件系统, 导出后的镜像只有一层
  • save 和 load 是用来导出导入镜像用的, 导出后保持着原镜像的层次结构

这里我没有详细介绍这四个参数的具体用法, 因为网上已经有很详细的文章介绍啦, 一下就是百度经常能搜到的关于 docker 导入导出的文章

https://my.oschina.net/zjzhai/blog/225112

具体用法可以参考以上文章, 但是概念理解, 我还是觉得自己写的比较清楚😆, 因为我没有故意构造出导出容器和导出镜像结果相同的场景


参考文档

docker export 官方文档https://docs.docker.com/engine/reference/commandline/export/

docker save 官方文档https://docs.docker.com/engine/reference/commandline/save/

docker commit 官方文档https://docs.docker.com/engine/reference/commandline/commit/

在 docker 原生支持的众多日志引擎中, 结合实际生产环境的需求, 最终选择了众多linux 发行版都默认支持的 journald 日志系统, journald 日志系统是 systemd 自带的服务, 在实际使用过程中, 上手很快, 而且很符合线上查看 docker 日志的习惯与需求

docker原生支持的日志引擎

  • none 关闭 docker 的回显日志, docker logs 看不到任何输出
  • json-file 把每个 container 的回显日志打到每个 container 的内部, 形式为json 文件
  • syslog 把所有 container 的回显日志打到系统的 syslog 中
  • journald 把所有 container 的回显日志打到系统的 journald 服务中
  • fluentd 把所有 container 的回显日志打到 fluentd 服务中
  • gelf 把所有 container 的回显日志打到支持 GELF(Graylog Extended Log Format) 格式的服务中, 比如 Graylog 或 Logstash

以上原生的日志引擎是最常用的6种,其中 json-file 是 docker 默认使用的日志系统

为什么选择了journald

  • 首先排除了 none 的日志引擎, 因为它抛弃了宿主机中所有 container 的回显日志
  • 之所有排除 json-file 是因为在实际使用中, 有的 container 在启动后有大量的回显日志, 尤其在程序内部报错时打出的日志信息尤其巨大, 而该 json 文件会放到磁盘对应该 container 的文件夹中越来越大, 常常因为某几个 container 的 json 日志而撑爆整个宿主机的磁盘
  • syslog 日志收集的方式我在使用 harbor 私有镜像服务的时候见过, harbor 官方提供的部署方式中, 所有的 container 的日志都集中收集在某一个 container 中, 执行 docker logs 的时候会提示你 "logs" command is supported only for "json-file" and "journald" logging drivers (got: syslog)
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
# syslog 在 harbor 中的应用解析

#harbor 服务由多了 container 组成, 其中有一个 container 的名字叫 "harbor-log" 这个就是整个 harbor 服务日志的大本营

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
55917fe77a5d nginx:1.11.5 "nginx -g 'daemon off" 5 weeks ago Up 5 weeks 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp nginx
66a5f3fd3c41 vmware/harbor-jobservice:0.5.0 "/harbor/harbor_jobse" 5 weeks ago Up 5 weeks harbor-jobservice
732e69978f61 vmware/harbor-ui:0.5.0 "/harbor/harbor_ui" 5 weeks ago Up 5 weeks harbor-ui
61678dddbc21 library/registry:2.5.0 "/entrypoint.sh serve" 5 weeks ago Up 5 weeks 5000/tcp registry
18a49a051913 vmware/harbor-db:0.5.0 "docker-entrypoint.sh" 5 weeks ago Up 5 weeks 3306/tcp harbor-db
aac08df72abb vmware/harbor-log:0.5.0 "/bin/sh -c 'crond &&" 5 weeks ago Up 5 weeks 0.0.0.0:1514->514/tcp harbor-log

# 除了 "harbor-log" 这个容器之外, 所有容器的日志类型都是如下指定的

"LogConfig": {
"Type": "syslog",
"Config": {
"syslog-address": "tcp://127.0.0.1:1514",
"tag": "proxy"
}
},

# 所有容器的回显日志都发送到了 "127.0.0.1:1514" 这个地址, 而这个地址就是 "harbor-log" 容器对外映射的端口

# 可以看出 harbor 的用法是单独创建一个容器用来收集所有相关容器的日志

这个模式在我们生产环境中并不适用, 因为无法通过 docker logs 直接查看日志, 而是需要到指定的容器中去找, 这对于研发查看日志拍错来说无疑是个巨大的麻烦.所以这个日志引擎也不适用于我们的生产环境

  • fluentd 和 gelf 这两个日志服务类型一般是配合 elasticsearch/kibana 来一起使用, 因为回显日志是程序回显, 不是业务日志回显, 所以没有进入到 elasticsearch 分析的需求, 业务日志一部分在 nginx 那一层就分析了, 还有的是通过把日志目录挂载出来, 让宿主机的 Log 服务去处理的. 因为规模比较小, 所以暂时用不到这个重的日志服务. 如果你需要分析很多 container 的日志, 可以考虑使用 fluentd 或 logstash

小结: 因为量级不够大, 用不到 fluentd 和 logstash; 因为研发有需求通过 docker logs 快速查看日志, 所以不能用 syslog; 因为 json-file 有让日志文件无止境变大的问题, 而清掉这个日志只能通过停止⏹docker 服务后才能echo > filename被重定向掉, 否则会引起 docker 服务的崩溃; 最终选择了现在大多数 Linux 发行版默认支持的 journald 日志服务.

更改 docker 的默认存储引擎

1
2
3
4
> vim /etc/docker/daemon.js
{
"log-driver": "journald"
}

只需在 json 配置文件中加入一行日志引擎的配置即可, 最后重启 docker 服务

可以通过 docker info 查看更改后的日志引擎

指定 container 启动时的存储引擎

1
2
3
docker run \
--log-driver=journald \
alpine ash

更换为 journald 日志引擎后, 所有使用 journald 引擎的 container 依然可以通过 docker logs containerName 的方式查看回显日志, 也可以在宿主机中使用 journalctl 命令来查看日志的集合,以下是 journalctl 的常用配置及用法简介

journald 配置文件

journald 的配置文件存放在 /etc/systemd/journald.conf

默认的内容如下:

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
[Journal]
#Storage=auto 存储为自动模式, 其他模式有无日志模式, 纯内存模式和纯磁盘模式
#Compress=yes 默认启动压缩功能
#Seal=yes 默认加密, 日志文件为二进制文件, 无法 vim或tail 查看
#SplitMode=uid
#SyncIntervalSec=5m
#RateLimitIntervalSec=30s
#RateLimitBurst=1000
#SystemMaxUse= 精确设置日志可以占用的最大磁盘空间
#SystemKeepFree= 保证系统剩余的空间大小
#SystemMaxFileSize=
#SystemMaxFiles=100
#RuntimeMaxUse=
#RuntimeKeepFree=
#RuntimeMaxFileSize=
#RuntimeMaxFiles=100
#MaxRetentionSec=
#MaxFileSec=1month
#ForwardToSyslog=no
#ForwardToKMsg=no
#ForwardToConsole=no
#ForwardToWall=yes
#TTYPath=/dev/console
#MaxLevelStore=debug
#MaxLevelSyslog=debug
#MaxLevelKMsg=notice
#MaxLevelConsole=info
#MaxLevelWall=emerg

journald 默认配置下, 默认日志最大限制为所在文件系统容量的 10%

默认配置下, journald 的日志存放在 /var/log/journal 下, 如果该目录在根分区下, 且根分区空间大小为10G, 那么 journald 存放日志最大的大小为1G, 超出1G 后将删除最早的日志

也可以精确指定占用的空间大小

1
SystemMaxUse=50M

其他 journald 的配置也没有细研究过, 主要还是没有需求~ 😆

journalctl 的使用

  • journalctl 从头查看所有日志
  • journalctl -b 查看本次开机后的所有系统引导日志 journalctl -b -1 显示上次开机后的系统引导日志, 以此类推
  • journalctl -f 类似于 tail -ftailf
  • journalctl /usr/bin/dockerd 后加程序的绝对路径,可以显示该程序的所有日志信息
  • journalctl -xe systemd 启动程序失败时会提示你使用这个命令查看错误信息, 其中x 表示在日志输出中增加一些解释性的短文本, e表示立即跳转至日志的尾部

journald 使用注意事项

在 journald 日志引擎的实际使用中, 我们发现了另一个问题, 就是 journald 的默认配置文件中的一个问题. 在 journald 默认的配置文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
root@larry-ali:~# cat /etc/systemd/journald.conf
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Entries in this file show the compile time defaults.
# You can change settings by editing this file.
# Defaults can be restored by simply deleting this file.
#
# See journald.conf(5) for details.

[Journal]
#Storage=auto
#Compress=yes
#Seal=yes
#SplitMode=uid
#SyncIntervalSec=5m
#RateLimitInterval=30s
#RateLimitBurst=1000
#SystemMaxUse=
#SystemKeepFree=
#SystemMaxFileSize=
#SystemMaxFiles=100
#RuntimeMaxUse=
#RuntimeKeepFree=
#RuntimeMaxFileSize=
#RuntimeMaxFiles=100
#MaxRetentionSec=
#MaxFileSec=1month
#ForwardToSyslog=yes
#ForwardToKMsg=no
#ForwardToConsole=no
#ForwardToWall=yes
#TTYPath=/dev/console
#MaxLevelStore=debug
#MaxLevelSyslog=debug
#MaxLevelKMsg=notice
#MaxLevelConsole=info
#MaxLevelWall=emerg

有以下几个非常重要的优化项目

  • ForwardToSyslog=yes
  • ForwardToWall=yes

以上两个参数默认都是 yes 意味着 journald 收集日志后还会转发到 syslog 中

由此造成的影响是: journald 虽然可以乖乖的按照上面的配置进行日志的清理, 但是 syslog 却不在 journald 的控制范围之内

在日志量很大的情况下, 我们发现 syslog 中的日志量也变得巨大, 而且里面都是容器的日志, 所以我们需要把上面两个参数的值改为no

禁止🚫journald 收到日志后转发给 syslog

更改完配置文件后, 重启 journald 服务 systemctl restart systemd-journald.service


参考文档

docker 日志引擎官方文档: https://docs.docker.com/engine/admin/logging/overview/

journald 配置文件官方文档: https://www.freedesktop.org/software/systemd/man/journald.conf.html#

journalctl 命令官方文档: https://www.freedesktop.org/software/systemd/man/journalctl.html#

journalctl 中文man手册:http://www.jinbuguo.com/systemd/journalctl.html

journald.conf 中文手册:http://www.jinbuguo.com/systemd/journald.conf.html

journald 初级指南:http://os.51cto.com/art/201405/440886.htm

在 docker 1.13版本发布后, 在众多的存储引擎中, overlayfs2 脱颖而出, 即将用作为未来主要的存储引擎. 本篇文章介绍如何更改 docker 的存储引擎为 overlayfs2

require

  • kernel 4.0+

overlayfs 的使用需要保证内核版本不低于3.8, 但是如果使用 overlayfs2 则需要保证内核版本不低于 4.0

overlayfs2 相比较于 overlayfs 有大量的性能优化和新的特性, 建议使用 overlayfs2

  • Ubuntu 16.04 LTS 中的内核版本为 4.4+
  • Fedora 25 server 中的内核版本为 4.8+
  • CentOS 7+ 中的内核版本为 3.10+ 可以使用 yum 安装长期支持版的内核 4.4+ (亲测可用, 但是线上环境慎用)

指定存储引擎

Ubuntu 安装好 docker 后, docker 默认还是使用了 aufs 存储引擎.

1
2
3
4
> vim /etc/docker/daemon.js
{
"storage-driver": "overlay2"
}

在 docker 的 js 配置文件中, 添加以上一行配置即可, 如果已经有其他配置选项, 记得添加逗号~

1
2
3
4
5
6
{
"registry-mirrors": [
"http://172d594a.m.daocloud.io"
], # <--- 一定要注意这个逗号, json 的格式不用再多说了吧...
"storage-driver": "overlay2"
}

在 docker 的官方文档中, 介绍了三种访问 docker Remote API 的方式, 分别是 unix 套接字文件/ tcp 监听端口和 fd 文件描述符. 由于 docker 默认会为我们开启本地 socket 套接字(/var/run/docker.sock) 所以本篇文章主要介绍如何让 docker 监听 tcp端口

在 docker 配置文件中设置

docker 1.12 版本之后, 建议在 docker 的 js 配置文件中配置, 路径为 /etc/docker/daemon.js 默认没有这个文件, 可以手动创建此文件, docker 启动时默认会读取此配置文件

1
2
3
4
5
6
7
> vim /etc/docker/daemon.js
{
"hosts": [
"tcp://0.0.0.0:2375",
"unix:///var/run/docker.sock"
]
}

注意上面👆一定要写上本地的 socket 位置, 不然无法使用本地的 CLI 控制 docker

unix:///var/run/docker.sock

修改完js配置文件后, 重启 docker 服务, 出现了如下报错

1
2
> systemctl restart docker.service
Job for docker.service failed because the control process exited with error code. See "systemctl status docker.service" and "journalctl -xe" for details.

查看日志发现关键信息如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> systemctl status docker.service
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: failed (Result: exit-code) since Mon 2017-02-06 12:16:57 CST; 13s ago
Docs: https://docs.docker.com
Process: 21952 ExecStart=/usr/bin/dockerd -H fd:// (code=exited, status=1/FAILURE)
Main PID: 21952 (code=exited, status=1/FAILURE)

Feb 06 12:16:57 xsl systemd[1]: Starting Docker Application Container Engine...
Feb 06 12:16:57 xsl dockerd[21952]: unable to configure the Docker daemon with file /etc/docker/daemon.json: the following directives are specified both as a flag and in the configuration file: hosts: (from flag: [fd://], from file: [tcp://0.0.0.0:2375, unix:///var/run/docker.socket])
Feb 06 12:16:57 xsl systemd[1]: docker.service: Main process exited, code=exited, status=1/FAILURE
Feb 06 12:16:57 xsl systemd[1]: Failed to start Docker Application Container Engine.
Feb 06 12:16:57 xsl systemd[1]: docker.service: Unit entered failed state.
Feb 06 12:16:57 xsl systemd[1]: docker.service: Failed with result 'exit-code'.

从上面👆的报错信息中可以判断出, 是因为 docker 的 socket 配置出现了冲突, 接下来查看 docker 的启动入口文件

1
2
3
4
5
> vim /lib/systemd/system/docker.service # Ubuntu的路径; CentOS 的路径为: /usr/lib/systemd/system/docker.service
# ...省略
# 关键行
ExecStart=/usr/bin/dockerd -H fd://
# ...省略

从上面可以看出, 在 docker 的启动入口文件中配置了 host 相关的信息, 而在 docker 的配置文件中也配置了 host 的信息, 所以发生了冲突. 解决办法, 建议将 docker 启动入口文件中的 -H fd:// 删除, 再重启 docker 服务即可

1
2
# ExecStart=/usr/bin/dockerd -H fd://
ExecStart=/usr/bin/dockerd

在 docker 启动入口中设置

当然你也可以在启动入口配置监听的端口和本地 socket 信息

1
2
3
4
5
> vim /lib/systemd/system/docker.service # Ubuntu的路径; CentOS 的路径为: /usr/lib/systemd/system/docker.service

# ExecStart=/usr/bin/dockerd -H fd://
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock
# ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375

最下面两条配置是等价的, docker 默认的本地 socket 文件就在 /var/run/docker.sock 这个位置, 如果没有特殊需求, 不需要显示的进行配置