Docker问答录系列——Docker引擎相关问题(五)

http://mp.weixin.qq.com/s?__biz=MzI3MTI2NzkxMA==&mid=2247484800&idx=1&sn=56559de9deeb36b032ee420891481c40&chksm=eac524a9ddb2adbf5555dd59151ac03520dfaf4421f82409b1718e73b7740b19385608283a8d&scene=21#wechat_redirect


本系列文章总结了一些初学Docker时比较常见问题的解决方法,解决思路大多遵循Docker官方的最佳实践的原则而进行的解答。文中内容绝对干货,强烈学习Docker的读者一读!本次主要介绍Docker引擎使用和日志相关方面的一些问题。


  • 日志问题 


Docker 日志都在哪里?怎么收集? 


日志分两类,一类是 Docker 引擎日志;另一类是 容器日志。


  • Docker 引擎日志 


Docker 引擎日志 一般是交给了 Upstart(Ubuntu 14.04) 或者 systemd (CentOS 7, Ubuntu 16.04)。前者一般位于 /var/log/upstart/docker.log 下,后者一般通过 jounarlctl -u docker 来读取。不同系统的位置都不一样,SO上有人总结了一份列表,我修正了一下,可以参考:


Docker问答录系列——Docker引擎相关问题(五)_第1张图片


  • 容器日志 


容器的日志 则可以通过 docker logs 命令来访问,而且可以像 tail -f 一样,使用 docker logs -f 来实时查看。如果使用 Docker Compose,则可以通过 docker-compose logs <服务名> 来查看。


如果深究其日志位置,每个容器的日志默认都会以 json-file 的格式存储于 /var/lib/docker/containers/<容器id>/<容器id>-json.log 下,不过并不建议去这里直接读取内容,因为 Docker 提供了更完善地日志收集方式 - Docker 日志收集驱动。


关于日志收集,Docker 内置了很多日志驱动,可以通过类似于 fluentd, syslog 这类服务收集日志。无论是 Docker 引擎,还是容器,都可以使用日志驱动。比如,如果打算用  fluentd 收集某个容器日志,可以这样启动容器:


$ docker run -d \

    --log-driver=fluentd \

    --log-opt fluentd-address=10.2.3.4:24224 \

    --log-opt tag="docker.{{.Name}}" \

    nginx


其中 10.2.3.4:24224 是 fluentd 服务地址,实际环境中应该换成真实的地址。


具体使用 fluentd 的方法,请参考我写的一组 fluentd 日志收集的例子:


https://coding.net/u/twang2218/p/docker-example/git/tree/master/fluentd


不同容器的日志汇聚到 fluentd 后如何区分? 


有两种概念的区分,一种是区分开不同容器的日志,另一种是区分开来不同服务的日志。


区分不同容器的日志是很直观的想法。运行了几个不同的容器,日志都送向日志收集,那么显然不希望 nginx 容器的日志和 MySQL 容器的日志混杂在一起看。


但是在 Swarm 集群环境中,区分容器就已经不再是合理的做法了。因为同一个服务可能有许多副本,而又有很多个服务,如果一个个的容器区分去分析,很难看到一个整体上某个服务的服务状态是什么样子的。而且,容器是短生存周期的,在维护期间容器生存死亡是很常见的事情。如果是像传统虚拟机那样子以容器为单元去分析日志,其结果很难具有价值。因此更多的时候是对某一个服务的日志整体分析,无需区别日志具体来自于哪个容器,不需要关心容器是什么时间产生以及是否消亡,只需要以服务为单元去区分日志即可。


这两类的区分日志的办法,Docker 都可以做到,这里我们以 fluentd 为例说明。


version: '2'

services:

    web:

        image: nginx:1.11-alpine

        ports:

            - "3000:80"

        labels:

            section: frontend

            group: alpha

            service: web

            image: nginx

            base_os: alpine

        logging:

            driver: fluentd

            options:

                fluentd-address: "localhost:24224"

                tag: "frontend.web.nginx.{{.Name}}"

                labels: "section,group,service,image,base_os"


这里我们运行了一个 nginx:alpine 的容器,服务名为 web。容器的日志使用 fluentd 进行收集,并且附上标签 frontend.web.nginx.<容器名>。除此以外,我们还定义了一组 labels,并且在 logging 的 options 中的 labels 中指明希望哪些标签随日志记录。这些信息中很多一部分都会出现在所收集的日志里。


让我们来看一下 fluentd 收到的信息什么样子的。


{

  "frontend.web.nginx.service_web_1": {

    "image": "nginx",

    "base_os": "alpine",

    "container_id": "f7212f7108de033045ddc22858569d0ac50921b043b97a2c8bf83b1b1ee50e34",

    "section": "frontend",

    "service": "web",

    "log": "172.20.0.1 - - [09/Dec/2016:15:02:45 +0000] \"GET / HTTP/1.1\" 200 612 \"-\" \"curl/7.49.1\" \"-\"",

    "group": "alpha",

    "container_name": "/service_web_1",

    "source": "stdout",

    "remote": "172.20.0.1",

    "host": "-",

    "user": "-",

    "method": "GET",

    "path": "/",

    "code": "200",

    "size": "612",

    "referer": "-",

    "agent": "curl/7.49.1",

    "forward": "-"

  }

}


如果去除 nginx 正常的访问日志项目外,我们就可以更清晰的看到有哪些元数据信息可以利用了。


{

  "frontend.web.nginx.service_web_1": {

    "image": "nginx",

    "base_os": "alpine",

    "container_id": "f7212f7108de033045ddc22858569d0ac50921b043b97a2c8bf83b1b1ee50e34",

    "section": "frontend",

    "service": "web",

    "group": "alpha",

    "container_name": "/service_web_1",

    "source": "stdout",

  }

}


可以看到,我们在 logging 下所有指定的 labels 都在。我们完全可以对每个服务设定不同的标签,通过标签来区分服务。比如这里,我们对 web 服务指定了 service=web 的标签,我们同样可以对数据库的服务设定标签为 service=mysql,这样在汇总后,只需要对 service 标签分组过滤即可,分离聚合不同服务的日志。


此外,我们可以设置不止一个标签,比如上面的例子,我们设置了多组不同颗粒度的标签,在后期分组的时候,可以很灵活的进行组合,以满足不同需求。


此外,注意 frontend.web.nginx.service_web_1,这是我们之前利用 --log-opt tag=frontend.web.nginx.<容器名> 进行设定的,其中 <容器名> 我们使用的是 Go 模板表达式 {{.Name}}。Go 模板很强大,我们可以用它实现非常复杂的标签。在 fluentd 中, 项可以根据标签来进行筛选。


这里可以唯一表示容器的,有容器 ID container_id,而容器名 container_name 也从某种程度上可以用来区分不同容器。因此进行容器区分日志的时候,可以使用这两项。


还有一个 source,这表示了日志是从标准输出还是标准错误输出得到的,由此可以区分正常日志和错误日志。


现在我们可以知道,除了容器自身输出的信息外,Docker 还可以为每一个容器的日志添加很多元数据,以帮助后期的日志处理中应对不同需求的搜索和过滤。


在后期处理中,fluentd 中可以利用 或者 插件根据 tag 或者其它元数据进行分别处理。而日志到了 ElasticSearch 这类系统后,则可以用更丰富的查询语言进行过滤、聚合。


  • 使用问题 


如何在 Docker 容器内使用 docker 命令(比如在 Jenkins 容器中)? 


首先,不要在 Docker 容器中安装、运行 Docker 引擎,也就是所谓的 Docker In Docker (DIND),参考文章:


https://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/


为了让容器内可以构建镜像,应该使用 Docker Remote API 的客户端来直接调用宿主的 Docker Engine。可以是原生的 Docker CLI (docker 命令),也可以是其它语言的库。


为 Jenkins 添加 Docker 命令行 


下面以定制 jenkins 镜像为例,使用 Dockerfile 添加 docker 命令行可执行文件,并调整权限。


FROM jenkins:alpine

# 下载安装Docker CLI

USER root

RUN curl -O https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz \

    && tar zxvf docker-latest.tgz \

    && cp docker/docker /usr/local/bin/ \

    && rm -rf docker docker-latest.tgz

# 将 `jenkins` 用户的组 ID 改为宿主 `docker` 组的组ID,从而具有执行 `docker` 命令的权限。

ARG DOCKER_GID=999

USER jenkins:${DOCKER_GID}


在这个例子里,我们下载了静态编译的 docker 可执行文件,并提取命令行安装到系统目录下。然后调整了 jenkins 用户的组 ID,调整为宿主 docker 组ID,从而使其具有执行 docker 命令的权限。


组 ID 使用了 DOCKER_GID 参数来定义,以方便进一步定制。构建时可以通过 --build-arg 来改变 DOCKER_GID 的默认值,运行时也可以通过 --user jenkins:1234 来改变运行用户的身份。


这里的基础镜像使用的是 jenkins:alpine,换为非 alpine 的镜像 jenkins:latest 也是一样的。


用下面的命令来构建镜像(假设镜像名为 jenkins-docker):


$ docker build -t jenkins-docker .


如果需要构建时调整 docker 组 ID,可以使用 --build-arg 来覆盖参数默认值:


$ docker build -t jenkins-docker --build-arg DOCKER_GID=1234 .


在启动容器的时候,将宿主的 /var/run/docker.sock 文件挂载到容器内的同样位置,从而让容器内可以通过 unix socket 调用宿主的 Docker 引擎。


比如,可以用下面的命令启动 jenkins:


$ docker run --name jenkins \

    -d \

    -p 8080:8080 \

    -v /var/run/docker.sock:/var/run/docker.sock \

    jenkins-docker


在 jenkins 容器中,就已经可以执行 docker 命令了,可以通过 docker exec 来验证这个结果:


$ docker exec -it jenkins sh

/ $ id

uid=1000(jenkins) gid=999(ping) groups=999(ping)

/ $ docker version

Client:

 Version:      1.12.3

 API version:  1.24

 Go version:   go1.6.3

 Git commit:   6b644ec

 Built:        Wed Oct 26 23:26:11 2016

 OS/Arch:      linux/amd64

Server:

 Version:      1.13.0-rc2

 API version:  1.25

 Go version:   go1.7.3

 Git commit:   1f9b3ef

 Built:        Wed Nov 23 06:32:39 2016

 OS/Arch:      linux/amd64

/ $


Docker 容器如何随系统一同启动? 


--restart=always


参考官网文档:https://docs.docker.com/engine/reference/commandline/run/#restart-policies-restart


docker stats 显示的只有容器ID,怎么才能显示容器名字? 


docker stats $(docker ps --format='{{.Names}}')


我用的是阿里云 Ubuntu 14.04 主机,内核还是3.13,怎么办? 


其实 Ubuntu 14.04 官方维护的内核已经到 4.4 了,可以通过下面的命令升级内核:


sudo apt-get install -y --install-recommends linux-generic-lts-xenial


如何动态修改内存限制? 


Docker 1.10 之后支持动态修改,使用 docker update 命令,如:


docker update -m 300m


经常在各种 Docker 命令里看到 --label,label 是什么?干什么用的? 


Label 是键值对,是 metadata,是贯穿于 Docker 各个资源的,包括引擎、镜像、容器、卷、网络、Swarm 节点、服务等。


  • 键 key:格式要求只可以包含字母和数字,以及.,-。推荐使用类似于 Java 那种反向域名格式,如 com.example.mytag。


  • 值 value:格式必须是字符串,除了普通字符串外,还可以是 JSON, XML, CSV 或者 YAML,当然,需要先进行序列化。


当资源很少的时候,我们可以直接对一个个资源进行操作,但是,在管理很多资源的时候,这么做就变得不大现实。经常的需求是针对某一类的资源进行操作,而不是一个个的操作。这种情况,经常会使用 label 来帮助实现。


当创建一个资源的时候,可以指定这个资源的 label(一个资源可以有很多个 label),而当创建了很多个资源的时候,就可以通过过滤 label 的键、值来得到所需的资源列表。


比如,我们可以使用 docker run 运行一堆容器,在运行时,通过 label 指定容器是架构中的哪一部分。


  • 前端:--label type=frontend

  • 中间件:--label type=middleware

  • 存储:--label type=storage


在后期维护时,可以直接过滤显示想要的容器,比如我们只想看前端容器运行情况:


docker ps --filter label=type=frontend


而且,还可以进一步的和其它命令配合操作这组容器,比如我们需要停止所有前端容器:


docker stop $(docker ps -f label=type=frontend)


使用 label 在集群调度中也非常有用。


比如,我们可以在不同的 Docker 主机的引擎 dockerd 参数中,通过 label 来加入存储类型的信息,如:


  • 存储类型为 SSD:--label storage=ssd

  • 存储类型为 HDD:--label storage=hdd


对于数据库的服务,我们自然希望跑在 SSD 上以获得更大的性能,而日志、备份服务则希望跑在 HDD 上获得更高的容量。那么可以这么做:


docker service create \

    --name mysql \

    --constraint 'engine.labels.storage == ssd' \

    mysql


添加label以及过滤 


添加 label 大多格式都是在创建、修改资源时,使用 --label = 参数(部分命令提供了 -l 缩写形式)。value 可以省略,格式为 --label 。如果需要定义多组 label,只需多组 --label 即可。


过滤 label 则大多发生在列表命令中,使用 --filter label==,或者对于不关心 value 的情况,--filter label=(部分命令提供了 -f 的缩写形式)。


下面的列表,列出了支持 label 的命令(除非特殊声明,”添加”命令使用 --label 选项添加 label;”过滤”命令使用 --filter 过滤label):





除了上述资源外,docker events 也可以使用 label 过滤结果:https://docs.docker.com/engine/reference/commandline/events/


集群调度约束 


  • 一代 Swarm:使用环境变量添加约束


docker run:-e constraint:storage==sdd:https://docs.docker.com/swarm/scheduler/filter/#/how-to-write-filter-expressions


docker-compose.yml:使用 environment 来进行约束:https://docs.docker.com/compose/swarm/#/manual-scheduling


如:


version: "2"

services:

    redis:

        image: redis

        environment:

            - "constraint:storage==ssd"


  • 二代 Swarm


docker service create:--constraint value:https://docs.docker.com/engine/reference/commandline/service_create/#/specify-service-constraints---constraint


如下面的例子中,使用 Swarm 节点 的 label 进行约束(注意,这次用的不是引擎的label):


docker service create \

    --name web \

    --constraint 'node.labels.type == frontend' \

    nginx


都说不要用 root 去运行服务,但我看到的 Dockerfile 都是用 root 去运行,这不安全吧? 


并非所有官方镜像的 Dockerfile 都是用 root 用户去执行的。比如 mysql 镜像的执行身份就是 mysql 用户;redis 镜像的服务运行用户就是 redis;mongo 镜像内的服务执行身份是 mongo 用户;jenkins 镜像内是 jenkins 用户启动服务等等。所以说 “都是用 root 去运行” 是不客观的。


当然,这并不是说在容器内使用 root 就非常危险。容器内的 root 和宿主上的 root 不同,容器内的 root 虽然 uid 也默认为 0,但是却处于一个隔离的命名空间,而且被去掉了大量的特权。容器内的 root 是一个没有什么特权的用户,危险的操作基本都无法执行。


不过,如果用户可以打破这个安全保护,那就是另外一回事了。比如,如果用户挂载了宿主目录给容器,这就是打通了一个容器内的 root 操控宿主的一个通道,使得容器内的 root 可以修改所挂载的目录下的任何文件。


因为当前版本的 Docker 中,默认情况下容器的 user namespace 并未开启,所以容器内的用户和宿主用户共享 uid 空间。容器内的 uid 为 0 的 root,就被系统视为 uid=0 的宿主 root,因此磁盘读写时,具有宿主 root 同等读写权限。这也是为什么一般不推荐挂载宿主目录、特别是挂载宿主系统目录的原因之一。这一切只要定制镜像的时候,容器内不使用 root 启动服务就没这个问题了。


当然,上面说的问题只是默认情况下 user namespace 不会启用的问题。dockerd 有一个 --userns-remap 参数,只要配置了这个参数,就可以确保容器内的 uid 是独立命名空间,容器内的 uid 变到宿主的时候,会被 remap 到另一个范围。因此,容器内的 uid=0 的 root 将完全跟 root 没有任何关系,仅仅是个普通用户而已。


相关信息请参考官方文档:


  • --userns-remap 的介绍:https://docs.docker.com/engine/reference/commandline/dockerd/#/daemon-user-namespace-options


  • Docker 安全:https://docs.docker.com/engine/security/security/


我在容器里运行 systemctl start xxx 怎么报错啊? 


如果在容器内使用 systemctl 命令,经常会发现碰到这样的错误:


Failed to get D-Bus connection: Operation not permitted


这很正常,因为 systemd 是完整系统的服务启动、维护的系统服务程序,而且需要特权去执行。但是容器不是完整系统,既没有配合的服务,也没有特权,所以自然用不了。


如果你碰到这样的问题,只能再次提醒你,Docker 不是虚拟机。试图在容器里执行 systemctl 命令的,大多都是还没有搞明白容器和虚拟机的区别,因为看到了可以有 Shell,就以为这是个虚拟机,试图重复自己在完整系统上的体验。这是用法错误,不要把 Docker 当做虚拟机去用,容器有自己的用法。


Docker 不是虚拟机,容器只是受限进程。


容器内根本不需要后台服务,也不需要服务调度和维护,自然也不需要 systemd。容器只有一个主进程,也就是应用进程。容器的生存周期就是围绕着这个主进程而存在的,所以所试图启动的后台服务,应该改为直接在前台运行,根本不需要也不应该使用 systemctl 命令去在后台加载。日志之类的也是直接从 stdout/stderr 输出,而不是走 journald。


容器内的时间和宿主不一致,怎么同步啊? 


问这个问题的人往往混淆了时间和时区的概念。


时间是从 epoch 到当前的秒数或者毫秒数,全球都一样,这是绝对值;而时区则是由于地理位置差异、行政区划导致各地显示时间的差异。


对于 Docker 容器而言,根本不存在宿主和容器的时间差异问题,因为他们使用的是同一个内核、同一个时钟,二者完全一样,所以根本不存在同步问题。还是那句话 Docker 不是虚拟机。


所看到的差异,如果细心一点,很可能会发现其实根本不是时间同步问题:


$ docker run -it ubuntu bash

root@08c6ad41f343:/# date

Tue Dec 13 01:36:37 UTC 2016


注意到 UTC 了么,这是说使用的是国际标准 0 时区 的时间显示,因此这只是显示所用的时区设置差异问题。而且之前如果稍微注意一下,就会发现所谓时间不一致,实际上是整整差了 8 个小时,还记得中学地理课上讲的中国时区是多少么?是 +8 时区,所以自然和 0 时区 差了 8 个小时。应该很快就意识到是自己的时区设错了(或者偷懒没设)导致。


解决办法很简单,设置时区即可。一般情况直接设置环境变量 TZ 就够了,比如:


$ docker run -it -e TZ=Asia/Shanghai ubuntu bash

root@8e6d6c588328:/# date

Tue Dec 13 09:41:21 CST 2016


看到了么?时区调整到了 CST,也就是China Standard Time - 中国标准时间,因此显示就正常了。


这仅仅是调整容器内系统环境的时区,大部分程序都会遵循这个标准。但是有些应用并不遵守这类约定,会使用自己的时区设置。


一般应用、服务的配置文件里一般都有时区选项,应该根据自己需求把中国时区配上。其实大部分的服务都应该这样做,这样才可以确保无论如何更换系统及环境,应用所用时区可以保持一致。比如,PHP 配置文件中的:


[Date]

date.timezone = Asia/Shanghai


很多应用都有自己的时区设置,应该去了解一下并且进行设置,不要总用默认值。


一些人在配置服务的时候很懒惰,只要默认能用即可,而不会一一检查每一个配置的默认值是否和自己期望一致,这是很不专业的做法,正是这种不专业才导致了出现了这种问题。所以做事情,一定要让自己以专业的视角和态度看问题。


我想让我的程序平滑退出,为什么截获 SIGTERM 信号不管用啊? 


docker stop, docker service rm 在停止容器时,都会先发 SIGTERM 信号,等待一段时间(默认为 10 秒)后,如果程序没响应,则强行 SIGKILL 杀掉进程。


这样应用进程就有机会平滑退出,在接收到 SIGTERM 后,可以去 Flush 缓存、完成文件读写、关闭数据库连接、释放文件资源、释放锁等等,然后再退出。所以试图截获 SIGTERM 信号的做法是对的。


但是,可能在截获 SIGTERM 时却发现,却发现应用并没有收到 SIGTERM,于是盲目的认为 Docker 不支持平滑退出,其实并非如此。


还记得我们提到过,Docker 不是虚拟机,容器只是受限进程,而一个容器只应该跑一个主进程的说法么?如果你发现你的程序没有截获到 SIGTERM,那就很可能你没有遵循这个最佳实践的做法。因为 SIGTERM 只会发给主进程,也就是容器内 PID 为 1 的进程。


至于说主进程启动的那些子进程,完全看主进程是否愿意转发 SIGTERM 给子进程了。所以那些把 Docker 当做虚拟机用的,主进程跑了个 bash,然后 exec 进去启动程序的,或者来个 & 让程序跑后台的情况,应用进程必然无法收到 SIGTERM。


还有一种可能是在 Dockerfile 中的 CMD 那行用的是 shell 格式写的命令,而不是 exec 格式。还记得前面提到过的 shell 格式的命令,会加一个 sh -c 来去执行么?因此使用 shell 格式写 CMD 的时候,PID 为 1 的进程是 sh,而它不转发信号,所以主程序收不到。


明白了道理,解决方法就很简单,换成 exec 格式,并且将主进程执行文件放在第一位即可。这也是为什么之前推荐 exec 格式的原因之一。

来源:大桥下的蜗牛

原文:http://t.cn/RI8whG6

题图:来自谷歌图片搜索

版权:本文版权归原作者所有

你可能感兴趣的:(docker)