Docker实战(三十)Dockerfile最佳实践总结

这次重构Docker镜像也参考了网上许多关于Dockerfile编写的建议和技巧。本文主要翻译了官方给出的Dockerfile编写的建议,以及总结了一些网上Dockerfile编写的建议和技巧。

一般准则和建议

容器应该是”短暂的”

由Dockerfile定义的image生成的容器应尽可能短暂。 通过”短暂的”,我们意味着容器它可以被stop和destroyed,一个新的容器的构建可以使用绝对最小的设置和配置。

使用.dockerignore

在大多数情况下,最好将每个Dockerfile放在一个空目录中。 然后,仅添加构建Dockerfile所需的文件。 要增加构建的性能,可以通过将.dockerignore文件添加到该目录来排除文件和目录。

避免安装不必要的安装包

应该尽量减少容器的复杂性,依赖性,文件的大小,构建的次数,所以应该尽量避免安装不必要的安装包。

每个容器应该只有一个进程 “one process per container”

将应用程序解耦到多个容器中可以更轻松地水平扩展和重新使用容器。 例如,Web应用程序堆栈可能由三个独立的容器组成,每个容器具有自己独特的映像,以解耦的方式管理web application, database, memory cache。

如果容器之间有依赖关系,应该使用Docker Network解决容器之间的通信。

最小化镜像的层数

在Dockerfile可读性和保持最少数据层之间找到平衡。一定要慎重引入新的数据层。

排序多行参数

只要有可能,通过以安装的软件包的字母数字来排序。 这将帮助你避免重复的包,并使列表更容易更新。 这也使得PR更容易阅读和审查。 在反斜杠(\)之前添加空格也有帮助。

构建缓存

在构建image的过程中,Docker将按照指定的顺序逐步执行你的Dockerfile中的指令。随着每条指令的检查,Docker将在其缓存中查找可重用的现有image,而不是创建一个新的(重复)image。如果你不想使用缓存,可以在docker build命令中使用–no-cache=true选项。

但是,如果你确实让Docker使用其缓存,那么了解何时会找到匹配的image是非常重要的。 Docker将遵循的基本规则如下:

  • 从基础image开始就已经在缓存中了,将下一条指令与从该基础image导出的所有子image进行比较,以查看其中一条是否使用完全相同的指令构建。如果没有,则缓存无效。

  • 在大多数情况下,只需将Dockerfile中的指令与其中一个子image进行比较即可。但是,某些说明需要更多的检查和解释。

  • 对于ADD和COPY指令,将检查image中文件的内容,并为每个文件计算校验和。在这些校验和中不考虑文件的最后修改和最后访问的时间。在缓存查找期间,将校验和与现有image中的校验和进行比较。如果文件(如内容和元数据)中有任何变化,则缓存无效。

  • 除了ADD和COPY命令之外,缓存检查将不会查看容器中的文件来确定缓存匹配。例如,当处理RUN apt-get -y update命令时,不会检查在容器中更新的文件以确定是否存在高速缓存命中。在这种情况下,只需使用命令字符串本身来查找匹配。

一旦缓存无效,所有后续的Dockerfile命令将生成新的映像,并且高速缓存将不被使用。

Dockerfile的一些建议

FROM

只要有可能,使用当前的官方存储库作为你的image的基础。 我们建议使用Debian镜像,因为它是非常严格的控制,并保持最小(目前在150 mb),而仍然是一个完整的分布。

LABEL

给image添加label标签,能够更好的按照项目组织image信息,这个暂时没用到过。

RUN

可以在多行上分隔长度或复杂的RUN语句,并以反斜杠分隔。

应该避免运行RUN apt-get upgrade or dist-upgrade,因为基本映像中的许多”essential”程序包将无法在非特权容器内升级。尽量使用apt-get install -y foo更新一个特定的包。

请务必将RUN apt-get update与apt-get install组合在同一个RUN语句中。例如:

1
2
3
4
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo

如果在RUN语句中单独使用apt-get update会导致缓存问题和随后的apt-get install说明失败。例如,说你有一个Docker文件:

1
2
3
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl

构建image后,所有图层都在Docker缓存中。假设你以后通过添加额外的包来修改apt-get install:

1
2
3
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl nginx

Docker将初始和修改的指令看作是相同的,并重新使用先前步骤的缓存。因此,apt-get update不会执行,因为构建使用缓存版本。因为apt-get update没有运行,你的构建可能会有一个过时的curl和nginx包版本。

使用RUN apt-get update && apt-get install -y可确保你的Dockerfile安装最新的软件包版本,无需进一步的编码或手动干预。这种技术被称为“缓存破解”。你还可以通过指定包版本来实现缓存清除。这被称为版本固定,例如:

1
2
3
4
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo=1.3.*

版本锁定强制构建检索特定版本,而不管缓存中有什么。这种技术还可以减少由于所需软件包中意外的更改导致的故障。

如果image以前使用过旧版本,则指定新版本会导致apt-get update的缓存破坏,并确保新版本的安装。在每行上列出包也可以防止包重复中的错误。

下面是一个完整的运行指令,显示所有apt-get建议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RUN apt-get update && apt-get install -y \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
libcap-dev \
libsqlite3-dev \
mercurial \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.* \
&& rm -rf /var/lib/apt/lists/*

另外,可以通过删除/var/lib/apt/lists来清理apt缓存,减少了image大小,因为apt缓存不存储在图层中。由于RUN语句以apt-get update开头,所以在apt-get install之前,包缓存将始终被刷新。

注意:Debian和Ubuntu的图像自动运行apt-get clean,所以不需要显式调用。

使用管道pipes

一些RUN命令取决于使用管道字符(|)将一个命令的输出管道到另一个命令的能力,如以下示例所示:

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

Docker使用/bin/sh -c解释器执行这些命令,该解释器仅评估管道中最后一个操作的退出代码以确定成功。在上面的示例中,只要wc -l命令成功,即使wget命令失败,构建步骤也会成功并生成新映像。

如果你希望命令由于管道中任何阶段的错误而失败,请先设置-o pipefail &&以确保意外的错误会阻止构建无意中成功。例如:

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

注意:并非所有的shell都支持-o pipefail选项。在这种情况下(例如,破折号shell,它是基于Debian的映像的默认shell),请考虑使用exec的形式来显式选择一个支持pipefail选项的shell。例如:

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

CMD

CMD指令应用于运行image中包含的软件以及任何参数。 CMD几乎总是以CMD [“executable”, “param1”, “param2”…]的形式使用。 因此,如果image用于服务,例如Apache和Rails,则可以运行类似于CMD [“apache2”,”-DFOREGROUND”]的内容。 实际上,这种形式的指令是推荐用于任何基于服务的image。

在大多数其他情况下,应该给CMD一个交互式的shell,比如bash,python和perl。 例如,CMD [“perl”, “-de0”], CMD [“python”], or CMD [“php”, “-a”]。 使用这个表单意味着当你执行像docker run -it python时,你将被丢弃到一个可用的shell中。 CMD应该很少以CMD [“param”, “param”]的方式与ENTRYPOINT一起使用,除非你和你的用户已经非常熟悉ENTRYPOINT是如何工作的。

EXPOSE

EXPOSE指令指示容器将侦听连接的端口。 因此,你应该为应用程序使用通用的传统端口。 例如,包含Apache Web服务器的映像将使用EXPOSE 80,而包含MongoDB的映像将使用EXPOSE 27017等。

对于外部访问,你的用户可以使用指示如何将指定端口映射到所选端口的标志来执行docker运行。 对于容器链接,Docker提供环境变量(例如:MYSQL_PORT_3306_TCP)从目标容器到源容器的路径。

ENV

为了使新软件更容易运行,可以为你容器安装的软件使用ENV更新PATH环境变量。 例如,ENV PATH /usr/local/nginx/bin:$PATH将确保CMD [“nginx”]正常工作。

ENV指令也可用于提供特定于要集中化的服务的必需环境变量,例如Postgres的PGDATA。

最后,ENV也可用于设置常用的版本号,以便版本颠覆更容易维护,如下例所示:

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

类似于在程序中具有常量变量(与硬编码值相反),这种方法允许你修改单个ENV就能自动控制容器中的软件版本。

ADD or COPY

虽然ADD和COPY在功能上是相似的,但一般来说,COPY是首选的。这是因为它比ADD更透明。 COPY只支持将本地文件复制到容器中,而ADD具有一些不是很明显的功能(如本地的tar提取和远程URL支持)。因此,ADD的最佳用途是将本地tar文件自动提取到图像中,如:ADD rootfs.tar.xz /。

如果你有多个Dockerfile步骤可以使用上下文中的不同文件,单独COPY,而不是一次性复制全部文件。如果特定需要的文件更改,这将确保每一步的构建缓存仅被无效(强制该步骤重新运行)。

例如:

1
2
3
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/

结果就是RUN这步很少的缓存会失效,和把COPY . /tmp/放在RUN之前相比。

由于image大小很重要,因此使用ADD从远程URL获取包是非常不鼓励的,你应该使用curl或wget来代替。这样,你可以删除在解压后不再需要的文件,而不必在image中添加另一个图层。例如,你应该避免这样做:

1
2
3
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all

而应该这样

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

对于不需要ADD tar自动提取功能的其他项目(文件,目录),应始终使用COPY。

ENTRYPOINT

ENTRYPOINT的最佳用途是设置image的主命令,允许该image像该命令一样运行(然后使用CMD作为默认标志)。

我们从一个命令行工具s3cmd的图像的例子开始:

1
2
ENTRYPOINT ["s3cmd"]
CMD ["--help"]

现在可以像这样运行映像来显示命令的帮助:

1
$ docker run s3cmd

或使用正确的参数执行命令:

1
$ docker run s3cmd ls s3://mybucket

这是有用的,因为image名称可以作为二进制文件的参考,如上面的命令所示。

ENTRYPOINT指令也可以与辅助脚本组合使用,允许其以类似于上述命令的方式运行,即使启动工具可能需要多于一个步骤。

例如,Postgres Official Image使用以下脚本作为其ENTRYPOINT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
set -e
if [ "$1" = 'postgres' ]; then
chown -R postgres "$PGDATA"
if [ -z "$(ls -A "$PGDATA")" ]; then
gosu postgres initdb
fi
exec gosu postgres "$@"
fi
exec "$@"

注意:此脚本使用exec Bash命令,以便最终运行的应用程序成为容器的PID 1。这允许应用程序接收发送到容器的任何Unix信号。有关详细信息,请参阅ENTRYPOINT帮助。

帮助脚本被复制到容器中,并通过容器起始处的ENTRYPOINT运行:

1
2
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]

此脚本允许用户以多种方式与Postgres进行交互。

它可以简单地启动Postgres:

1
$ docker run postgres

或者,它可以用于运行Postgres并将参数–help传递给服务器:

1
$ docker run postgres postgres --help

最后,它也可以用来启动一个完全不同的工具,比如Bash:

1
$ docker run --rm -it postgres bash

Volume

应该使用VOLUME指令来暴露由docker容器创建的任何数据库存储区域,配置存储器或文件/文件夹。

User

如果服务可以无特权运行,请使用USER更改为非root用户。 可以使用RUN groupadd -r postgres && useradd -r -g postgres postgres可以创建一个普通用户。

注意:image中的用户和组获得非确定性的UID/GID,因为”next”UID/GID被分配,而不管image重建。 所以,如果是至关重要的,你应该分配一个显式的UID/GID。

你应避免安装或使用sudo,因为它具有不可预测的TTY和信号转发行为,可能导致比解决问题更多的问题。 如果你绝对需要类似于sudo的功能(例如,以root用户身份初始化守护程序,但以非root身份运行),则可以使用”gosu”。

最后,为了降低层次和复杂性,请避免频繁地切换USER。

WORKDIR

为了清晰可靠,你应该始终为WORKDIR使用绝对路径。 此外,应该使用WORKDIR,而不应该使用像RUN CD … && do-something这些难以阅读,排除故障和维护的指令。

ONBUILD

在当前的Dockerfile构建完成之后执行一个ONBUILD命令。 ONBUILD在从当前image派生的任何子image中执行。将ONBUILD命令视为父Dockerfile为子Dockerfile提供的指令。

Docker构建在子Dockerfile中的任何命令之前执行ONBUILD命令。

ONBUILD对于那些给定FROM的image构建是很有用的。例如,你可以使用ONBUILD作为语言堆栈image,可以在Dockerfile中构建用该语言编写的任意软件,就像在Ruby的ONBUILD变体中所看到的那样。

从ONBUILD构建的image应该有一个单独的标签,例如:ruby:1.9-onbuild或ruby:2.0-onbuild。

将ADD或COPY放在ONBUILD中时要小心。如果新版本的上下文缺少添加的资源,”ONBUILD”image将会失败。

其他建议汇总

移除构建依赖

其实官网的建议中也提到了,只是没有特别的强调。如果通过源码编译构建,你的镜像通常比需要的大很多。可能的话,在同一条RUN指令中,安装构建工具、构建软件,然后移除构建工具。这样可以减少image的大小。

选择gosu

gosu实用工具,通常用在ENTRYPOINT指令调用的脚本中,这些ENTRYPOINT指令位于官方镜像的Dockerfile中。它是个类sudo的简单工具,接受并运行特定用户的特定指令。但是gosu可以避免sudo怪异恼人的TTY和信号转发(signal-forwarding)行为。

不要在 Dockerfile 中修改文件的权限

因为 docker 镜像是分层的,任何修改都会新增一个层,修改文件或者目录权限也是如此。如果修改大文件或者目录的权限,会把这些文件复制一份,这样很容易导致镜像很大。

解决方案也很简单,要么在添加到 Dockerfile 之前就把文件的权限和用户设置好,要么在容器启动脚本(entrypoint)做这些修改。

这里我也是参考了DockerHub上一些官方镜像的写法。

apt-get注意点

一个是运行apt-get upgrade 会更新所有包到最新版本 —— 不能这样做的理由是它会妨碍Dockerfile构建的持久与一致性。

另一个是在不同的行之间运行apt-get update与apt-get install命令。不能这样做的原因是,只有apt-get update的代码会在构建过程中被缓存,而且你需要运行apt-get install命令的时候不会每次都被执行。因此,你需要将apt-get update跟所要安装的包都在同一行执行,来确保它们正确的更新。

使用docker exec而不是sshd

需要进入容器要使用docker exec命令,而不要单独安装sshd

参考文章:

Docker实战(二十九)DockerCompose搭建ELK集成环境问题汇总

今天记录一下在使用docker-compose构建ELK集成环境时遇到的坑,废话不多说了直接来踩坑。

docker网络冲突

在修改好ELK的docker-compose.yml配置文件后,尝试启动遇到网络冲突的问题,错误提示说”172.18.0.1”这个网络已经存在。

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
$ docker-compose up -d
Creating network "5x_elk_net" with driver "bridge"
ERROR: failed to allocate gateway (172.18.0.1): Address already in use
# 查看现在的docker网络,发现下面几个已经存在的网络配置
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
27822b9fb5c5 bridge bridge local
08b6d63e27d2 host host local
dd0874e0e097 none null local
bacc9a64bb83 test_default bridge local
# 依次查看这几个网络的配置,发现bacc9a64bb83这个容器的网络已经使用了"172.18.0.1"
$ docker network inspect bacc9a64bb83
[
{
"Name": "test_default",
"Id": "bacc9a64bb8323b2e53b1c85b4643061d38699227492f9174855202b6900252a",
"Created": "2017-04-21T10:29:37.26843596Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]

找到冲突的地方就好办,两种方式来解决:

  1. 删除已经存在的网络
  2. 更换docker-compose现有的网段

因为这个容器对我还有其他用处,所以这里我选择更换docker-compose的网络来解决

logstash-output-elasticsearch插件的host配置不支持特殊符号

下面是我的docker-compose.yml配置文件(篇幅原因,这里省略了一部分,只是用了ES的容器配置举例)。

docker-compose.yml配置文件链接:

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
version: '2'
services:
...
elasticsearch:
# 指定当前构建的Docker容器的镜像
image: birdben/elasticsearch_5.x:v2
restart: always
# 指定当前构建的Docker容器的名称
container_name: elasticsearch_5.x
networks:
elk_net:
# 指定当前构建的Docker容器的IP地址
ipv4_address: 172.20.0.5
# 指定当前构建的Docker容器的host配置
extra_hosts:
- "filebeat:172.20.0.2"
- "redis:172.20.0.3"
- "logstash:172.20.0.4"
- "elasticsearch:172.20.0.5"
- "kibana:172.20.0.6"
# 指定当前构建的Docker容器的volume挂在目录设置
volumes:
- /Users/yunyu/workspace_git/birdDocker/elk/5.x/volumes/elasticsearch/data:/usr/share/elasticsearch/data
- /Users/yunyu/workspace_git/birdDocker/elk/5.x/volumes/elasticsearch/config:/usr/share/elasticsearch/config
- /Users/yunyu/workspace_git/birdDocker/elk/5.x/volumes/elasticsearch/logs:/usr/share/elasticsearch/logs
# 指定当前构建的Docker容器对外开放的端口号映射
ports:
- "9200:9200"
- "9300:9300"
...

如果ES容器的container_name配置为”elasticsearch_5.x”,那么logstash需要在logstash.conf配置文件中使用host来指定ES的服务器为”elasticsearch_5.x”,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
output {
stdout {
codec => rubydebug
}
elasticsearch {
codec => "json"
hosts => ["elasticsearch_5.x:9200"]
index => "logstash-%{+YYYY.MM.dd}"
document_type => "%{type}"
workers => 1
flush_size => 20000
idle_flush_time => 10
}
}

启动Logstash之后,会如下报错

1
2
3
4
Sending Logstash's logs to /usr/share/logstash/logs which is now configured via log4j2.properties
[2017-05-06T11:09:25,349][INFO ][logstash.setting.writabledirectory] Creating directory {:setting=>"path.queue", :path=>"/usr/share/logstash/data/queue"}
[2017-05-06T11:09:25,382][INFO ][logstash.agent ] No persistent UUID file found. Generating new UUID {:uuid=>"e241d497-58e2-46de-9213-95088242255a", :path=>"/usr/share/logstash/data/uuid"}
[2017-05-06T11:09:25,771][ERROR][logstash.agent ] Cannot load an invalid configuration {:reason=>"bad URI(is not URI?): elasticsearch_5.x:9200"}

提示是”elasticsearch_5.x”是一个非法的URI地址,将docker-compose.yml配置文件的container_name和logstash.conf的host配置修改为”elasticsearch5x”之后就不会再报错了。所以推断logstash-output-elasticsearch插件对host要求比较严格,不支持一些特殊符号。

docker-compose配置的容器无法全部正常启动

使用docker-compose启动ELK的2.x版本服务都一切正常,但是换成ELK的5.x版本后,发现Filebeat,Logstash,Redis,Kibana服务都正常,只有ES的容器起来没有多久就自己挂掉了。
看了ES的日志也没有发现什么异常,单独启动ES5.x的容器却能正常使用。

后来实在没办法了,我尝试在docker-compose.yml配置文件中只留下ES的容器,这样运行也没问题。之后尝试一个一个将其他容器的配置加到docker-compose.yml配置文件,发现当Logstash5.x和ES5.x的容器同时启动,ES的容器就会出现上面自己挂掉的情况。

然后我又仔细查看了一下ES的日志文件,发现了一些区别:有问题的ES日志中多了一些GC的日志。

  • 有问题的ES日志
1
2
3
4
5
6
7
8
9
10
11
12
[2017-05-06T10:36:30,804][INFO ][o.e.n.Node ] [node-1] initialized
[2017-05-06T10:36:30,805][INFO ][o.e.n.Node ] [node-1] starting ...
[2017-05-06T10:36:31,162][WARN ][i.n.u.i.MacAddressUtil ] Failed to find a usable hardware address from the network interfaces; using random bytes: 37:86:d0:ae:ee:3d:71:88
[2017-05-06T10:36:31,437][INFO ][o.e.t.TransportService ] [node-1] publish_address {172.20.0.5:9300}, bound_addresses {[::]:9300}
[2017-05-06T10:36:31,457][INFO ][o.e.b.BootstrapChecks ] [node-1] bound or publishing to a non-loopback or non-link-local address, enforcing bootstrap checks
[2017-05-06T10:36:33,545][WARN ][o.e.m.j.JvmGcMonitorService] [node-1] [gc][young][2][2] duration [1.4s], collections [1]/[1.6s], total [1.4s]/[1.7s], memory [284.7mb]->[51.7mb]/[1.9gb], all_pools {[young] [266.2mb]->[11.2mb]/[266.2mb]}{[survivor] [18.4mb]->[32mb]/[33.2mb]}{[old] [0b]->[8.4mb]/[1.6gb]}
[2017-05-06T10:36:33,560][WARN ][o.e.m.j.JvmGcMonitorService] [node-1] [gc][2] overhead, spent [1.4s] collecting in the last [1.6s]
[2017-05-06T10:36:50,112][INFO ][o.e.n.Node ] [node-1] initializing ...
[2017-05-06T10:36:50,400][INFO ][o.e.e.NodeEnvironment ] [node-1] using [1] data paths, mounts [[/usr/share/elasticsearch/data (osxfs)]], net usable_space [6.1gb], net total_space [232.6gb], spins? [possibly], types [fuse.osxfs]
[2017-05-06T10:36:50,401][INFO ][o.e.e.NodeEnvironment ] [node-1] heap size [1.9gb], compressed ordinary object pointers [true]
[2017-05-06T10:36:50,415][INFO ][o.e.n.Node ] [node-1] node name [node-1], node ID [x7vSjbIKSdeUbHcAjXWPCw]
[2017-05-06T10:36:50,417][INFO ][o.e.n.Node ] [node-1] version[5.3.1], pid[1], build[5f9cf58/2017-04-17T15:52:53.846Z], OS[Linux/4.9.13-moby/amd64], JVM[Oracle Corporation/OpenJDK 64-Bit Server VM/1.8.0_121/25.121-b13]
  • 没有问题的ES日志
1
2
3
4
5
6
7
8
9
10
11
12
13
[2017-05-06T09:35:52,233][INFO ][o.e.n.Node ] [node-1] initialized
[2017-05-06T09:35:52,238][INFO ][o.e.n.Node ] [node-1] starting ...
[2017-05-06T09:35:52,408][WARN ][i.n.u.i.MacAddressUtil ] Failed to find a usable hardware address from the network interfaces; using random bytes: 4a:ab:e0:6f:82:87:b0:e5
[2017-05-06T09:35:52,569][INFO ][o.e.t.TransportService ] [node-1] publish_address {172.20.0.5:9300}, bound_addresses {[::]:9300}
[2017-05-06T09:35:52,592][INFO ][o.e.b.BootstrapChecks ] [node-1] bound or publishing to a non-loopback or non-link-local address, enforcing bootstrap checks
[2017-05-06T09:35:55,713][INFO ][o.e.c.s.ClusterService ] [node-1] new_master {node-1}{Z0Yoi2zfTl237aiVzEoOug}{N4z8452FTc-SArP7hh7h-g}{172.20.0.5}{172.20.0.5:9300}, reason: zen-disco-elected-as-master ([0] nodes joined)
[2017-05-06T09:35:55,779][INFO ][o.e.g.GatewayService ] [node-1] recovered [0] indices into cluster_state
[2017-05-06T09:35:55,790][INFO ][o.e.h.n.Netty4HttpServerTransport] [node-1] publish_address {172.20.0.5:9200}, bound_addresses {[::]:9200}
[2017-05-06T09:35:55,822][INFO ][o.e.n.Node ] [node-1] started
[2017-05-06T09:35:58,039][INFO ][o.e.c.m.MetaDataCreateIndexService] [node-1] [logstash-2017.05.06] creating index, cause [auto(bulk api)], templates [logstash], shards [5]/[1], mappings [_default_]
[2017-05-06T09:35:58,663][INFO ][o.e.c.m.MetaDataMappingService] [node-1] [logstash-2017.05.06/h2c9vcE2TaCdXgYxSjh0IA] create_mapping [log]
[2017-05-06T09:36:01,108][INFO ][o.e.c.m.MetaDataCreateIndexService] [node-1] [.kibana] creating index, cause [api], templates [], shards [1]/[1], mappings [server, config]
[2017-05-06T09:36:25,753][WARN ][o.e.c.r.a.DiskThresholdMonitor] [node-1] high disk watermark [90%] exceeded on [Z0Yoi2zfTl237aiVzEoOug][node-1][/usr/share/elasticsearch/data/nodes/0] free: 6.1gb[2.6%], shards will be relocated away from this node

经过上面的分析,我怀疑是我docker服务设置的内存大小无法支持我启动这么多的容器。后来发现ES和Logstash的5.x版本比2.x版本多了一个jvm.options的配置文件,主要是用来设置ES和Logstash的JVM的配置使用的,在这个配置文件里可以控制JVM的堆大小。这里将ES和Logstash的堆内存调小后,再使用docker-compose启动,ES5.x的容器已经能够正常启动了。

ES的jvm.options

1
2
3
4
5
6
7
-Xms2g
-Xmx2g
# 修改为
-Xms1g
-Xmx1g

Logstash的jvm.options

1
2
3
4
5
6
7
-Xms256m
-Xmx1g
# 修改为
-Xms256m
-Xmx256m

参考文章:

Docker实战(二十八)Docker的Volume挂载权限

最近在重构Docker镜像的时候,遇到了Volume挂载文件的权限问题。这里测试使用的是Elasticsearch官方提供的镜像。

Elasticsearch官方的Dockerfile文件

在制作自己的ES镜像的时候,参考了Elasticsearch官方的Dockerfile,有个地方没有弄明白,为什么Dockerfile和docker-entrypoint都要去chown下面的两个目录,在Dockerfile执行一次chown不就可以了吗?不执行chown会有什么问题呢?。

1
2
chown -R elasticsearch:elasticsearch: /usr/share/elasticsearch/data
chown -R elasticsearch:elasticsearch: /usr/share/elasticsearch/logs

带着上面的疑问,我开始做了下面的尝试,这里我在本地使用修改后的Elasticsearch官方的Dockerfile开始构建Docker镜像,然后和官方pull下来的镜像做对比。

先下载Elasticsearch官方的Docker镜像

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
# 下载Elasticsearch官方的Docker镜像
$ docker pull elasticsearch:5.3.1
# 运行Elasticsearch的Docker容器,并且挂载对应的data目录
$ docker run -d -v /Users/yunyu/workspace_git/birdDocker/elasticsearch/official_5/data:/usr/share/elasticsearch/data --name elasticsearch_official_5x elasticsearch:5.3.1
# 进入Docker容器
$ docker exec -it elasticsearch_official_5x /bin/bash
# 查看Docker容器内/usr/share/elasticsearch目录的权限
$ ls -lh /usr/share/elasticsearch
total 228K
-rw-r--r-- 1 root root 190K Apr 17 15:55 NOTICE.txt
-rw-r--r-- 1 root root 9.4K Apr 17 15:55 README.textile
drwxr-xr-x 2 root root 4.0K Apr 27 00:01 bin
drwxr-xr-x 1 elasticsearch elasticsearch 4.0K Apr 27 00:01 config
drwxr-xr-x 3 elasticsearch elasticsearch 102 May 4 06:20 data
drwxr-xr-x 2 root root 4.0K Apr 27 00:01 lib
drwxr-xr-x 1 elasticsearch elasticsearch 4.0K Apr 27 00:01 logs
drwxr-xr-x 12 root root 4.0K Apr 27 00:01 modules
drwxr-xr-x 2 root root 4.0K Apr 17 15:55 plugins
# 查看Docker容器内/usr/share/elasticsearch/data目录的权限
$ ls -lh /usr/share/elasticsearch/data/
total 0
drwxr-xr-x 3 root root 102 May 4 06:20 nodes
# 查看Elasticsearch进程
$ ps -ef | grep elasticsearch
elastic+ 1 0 16 06:19 ? 00:00:17 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java -Xms2g -Xmx2g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -server -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -Djdk.io.permissionsUseCanonicalPath=true -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true -XX:+HeapDumpOnOutOfMemoryError -Des.path.home=/usr/share/elasticsearch -cp /usr/share/elasticsearch/lib/elasticsearch-5.3.1.jar:/usr/share/elasticsearch/lib/* org.elasticsearch.bootstrap.Elasticsearch
root 105 94 0 06:21 ? 00:00:00 grep elasticsearch

使用Elasticsearch官方的Docker容器,又发现新的问题,因为Dockerfile中使用了chown -R elasticsearch:elasticsearch /usr/share/elasticsearch/data将该目录所有者修改为elasticsearch用户了,为什么/usr/share/elasticsearch/data目录下的文件和文件夹确实属于root用户呢?这个问题暂时先放一边,后面会给出解释,我们先继续之前的尝试。

注意:下面的尝试,每次都要从宿主机中删除挂载的目录,这样能避免docker-entrypoint.sh中执行chown修改目录的所属用户

在进行下面的尝试之前,我们需要先修改config/log4j2.properties配置文件,让elasticsearch的日志可以写入到日志文件中。(Elasticsearch5.x版本使用了log4j2,默认是只将日志输出到控制台的,这里和Elasticsearch2.x版本不同)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
status = error
appender.console.type = Console
appender.console.name = console
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n
appender.rolling.type = RollingFile
appender.rolling.name = rolling
appender.rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}.log
appender.rolling.layout.type = PatternLayout
appender.rolling.layout.pattern = [%d{ISO8601}][%-5p][%-25c] %.10000m%n
appender.rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}-%d{yyyy-MM-dd}.log
appender.rolling.policies.type = Policies
appender.rolling.policies.time.type = TimeBasedTriggeringPolicy
appender.rolling.policies.time.interval = 1
appender.rolling.policies.time.modulate = true
rootLogger.level = info
rootLogger.appenderRef.console.ref = console
rootLogger.appenderRef.all.ref = rolling

尝试一:删掉Dockerfile和docker-entrypoint.sh的chown语句

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
# 构建修改后的Elasticsearch的Docker镜像
$ docker build -t "birdben/elasticsearch:5.3.1" .
# 运行Elasticsearch的Docker容器,并且挂载对应的data目录
$ docker run -itd -v /Users/yunyu/workspace_git/birdDocker/elasticsearch/me/data:/usr/share/elasticsearch/data --name elasticsearch_me_5x birdben/elasticsearch:5.3.1
# 查看Docker容器的日志
$ docker logs 4353faea17cb
2017-05-04 06:07:39,353 main ERROR Unable to create file /usr/share/elasticsearch/logs/elasticsearch.log java.io.IOException: Permission denied
at java.io.UnixFileSystem.createFileExclusively(Native Method)
at java.io.File.createNewFile(File.java:1012)
at org.apache.logging.log4j.core.appender.rolling.RollingFileManager$RollingFileManagerFactory.createManager(RollingFileManager.java:463)
at org.apache.logging.log4j.core.appender.rolling.RollingFileManager$RollingFileManagerFactory.createManager(RollingFileManager.java:445)
at org.apache.logging.log4j.core.appender.AbstractManager.getManager(AbstractManager.java:112)
...
# 查看Docker容器内/usr/share/elasticsearch目录的权限
root@4353faea17cb:/usr/share/elasticsearch# ls -lh
total 228K
-rw-r--r-- 1 root root 190K Apr 17 15:55 NOTICE.txt
-rw-r--r-- 1 root root 9.4K Apr 17 15:55 README.textile
drwxr-xr-x 2 root root 4.0K May 4 06:05 bin
drwxr-xr-x 1 root root 4.0K May 4 06:06 config
drwxr-xr-x 3 root root 102 May 4 06:07 data
drwxr-xr-x 2 root root 4.0K May 4 06:05 lib
drwxr-xr-x 2 root root 4.0K May 4 06:05 logs
drwxr-xr-x 12 root root 4.0K May 4 06:05 modules
drwxr-xr-x 2 root root 4.0K Apr 17 15:55 plugins
# 查看Docker容器内/usr/share/elasticsearch/data目录的权限
$ ls -lh /usr/share/elasticsearch/data/
total 0
drwxr-xr-x 3 root root 102 May 4 06:33 nodes
# 查看Docker容器内/usr/share/elasticsearch/logs目录的权限(没有日志文件,因为上面没有权限的问题)
$ ls -lh /usr/share/elasticsearch/logs/
total 0
# 查看Elasticsearch进程
$ ps -ef | grep elasticsearch
elastic+ 1 0 11 06:33 ? 00:00:19 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java -Xms2g -Xmx2g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -server -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -Djdk.io.permissionsUseCanonicalPath=true -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true -XX:+HeapDumpOnOutOfMemoryError -Des.path.home=/usr/share/elasticsearch -cp /usr/share/elasticsearch/lib/elasticsearch-5.3.1.jar:/usr/share/elasticsearch/lib/* org.elasticsearch.bootstrap.Elasticsearch
root 106 92 0 06:36 ? 00:00:00 grep elasticsearch

这里elasticsearch进程是属于elasticsearch用户的,而/usr/share/elasticsearch/data和/usr/share/elasticsearch/logs目录都属于root用户,所以没有权限在/usr/share/elasticsearch/logs目录下创建elasticsearch.log日志文件,这点理解起来比较容易。

1
2
3
4
5
6
7
8
# 新建文档
$ curl -XPOST 'http://127.0.0.1:9200/user/1/1' -d '{"name":"birdben"}'
# 查看索引文件
$ ls -lh /usr/share/elasticsearch/data/nodes/0/indices/ycOyM3onRbeZAPlYMVze_w/0/index/
total 4.0K
-rw-r--r-- 1 root root 130 May 4 06:38 segments_1
-rw-r--r-- 1 root root 0 May 4 06:38 write.lock

可以看出新建文档的索引文件所属用户也是root。

尝试二:只删掉docker-entrypoint.sh的chown语句

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
# 构建修改后的Elasticsearch的Docker镜像
$ docker build -t "birdben/elasticsearch:5.3.1" .
# 运行Elasticsearch的Docker容器,并且挂载对应的data目录
$ docker run -itd -v /Users/yunyu/workspace_git/birdDocker/elasticsearch/me/data:/usr/share/elasticsearch/data --name elasticsearch_me_5x birdben/elasticsearch:5.3.1
# 查看Docker容器的日志,没有问题
$ docker logs 6db67f60ed6e
# 查看Docker容器内/usr/share/elasticsearch目录的权限
root@6db67f60ed6e:/usr/share/elasticsearch# ls -lh
total 228K
-rw-r--r-- 1 root root 190K Apr 17 15:55 NOTICE.txt
-rw-r--r-- 1 root root 9.4K Apr 17 15:55 README.textile
drwxr-xr-x 2 root root 4.0K May 4 07:02 bin
drwxr-xr-x 1 elasticsearch elasticsearch 4.0K May 4 07:02 config
drwxr-xr-x 3 root root 102 May 4 09:16 data
drwxr-xr-x 2 root root 4.0K May 4 07:02 lib
drwxr-xr-x 1 elasticsearch elasticsearch 4.0K May 4 09:16 logs
drwxr-xr-x 12 root root 4.0K May 4 07:02 modules
drwxr-xr-x 2 root root 4.0K Apr 17 15:55 plugins
# 查看Docker容器内/usr/share/elasticsearch/data目录的权限
$ ls -lh /usr/share/elasticsearch/data/
total 0
drwxr-xr-x 3 root root 102 May 4 06:33 nodes
# 查看Docker容器内/usr/share/elasticsearch/logs目录的权限(生成日志文件了,但是logs目录的所属用户还是root,但是config和logs的所属用户却是elasticsearch,因为在Dockerfile中chown更改目录的所属用户后,又使用Volume挂载了data目录,而挂载的目录的所属用户就会被就修改为root用户,这也就解释了为什么data所属用户是root,config和logs所属用户是elasticsearch)
$ ls -lh /usr/share/elasticsearch/logs/
total 8.0K
-rw-r--r-- 1 elasticsearch elasticsearch 5.3K May 4 09:19 elasticsearch.log
# 查看Elasticsearch进程
$ ps -ef | grep elasticsearch
elastic+ 1 0 4 09:16 ? 00:00:20 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java -Xms2g -Xmx2g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -server -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -Djdk.io.permissionsUseCanonicalPath=true -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true -XX:+HeapDumpOnOutOfMemoryError -Des.path.home=/usr/share/elasticsearch -cp /usr/share/elasticsearch/lib/elasticsearch-5.3.1.jar:/usr/share/elasticsearch/lib/* org.elasticsearch.bootstrap.Elasticsearch
root 104 94 0 09:23 ? 00:00:00 grep elasticsearch

这里elasticsearch进程也是属于elasticsearch用户的,而/usr/share/elasticsearch/data属于root用户,/usr/share/elasticsearch/config和/usr/share/elasticsearch/logs目录都属于elasticsearch用户,所以现在有权限在/usr/share/elasticsearch/logs目录下创建elasticsearch.log日志文件,这里猜测因为/usr/share/elasticsearch/logs没有挂载到宿主机,所以logs目录和目录下创建的elasticsearch.log日志文件都属于elasticsearch用户(因为Dockerfile中对logs目录进行了chown)。

1
2
3
4
5
6
7
8
# 新建文档
$ curl -XPOST 'http://127.0.0.1:9200/user/1/1' -d '{"name":"birdben"}'
# 查看索引文件
$ ls -lh /usr/share/elasticsearch/data/nodes/0/indices/meAhtSJXRl-cKzoqQZifBQ/0/index/
total 4.0K
-rw-r--r-- 1 root root 130 May 4 09:27 segments_1
-rw-r--r-- 1 root root 0 May 4 09:27 write.lock

可以看出新建文档的索引文件所属用户仍然是root。

下面我们证实一下我上面的猜测,/usr/share/elasticsearch/logs没有挂载到宿主机,所以logs目录和目录下创建的elasticsearch.log日志文件都属于elasticsearch用户,而不是root用户。这里推测一下,如果我把/usr/share/elasticsearch/logs挂载到宿主机,那logs目录和目录下的创建的elasticsearch.log日志文件就会属于root用户,而不是elasticsearch用户。(前提docker run的时候,-u使用的默认root用户,而不是elasticsearch用户)

尝试三:只删掉docker-entrypoint.sh的chown语句,然后挂载/usr/share/elasticsearch/logs目录

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
# 运行Elasticsearch的Docker容器,并且挂载对应的data目录
$ docker run -itd -v /Users/yunyu/workspace_git/birdDocker/elasticsearch/me/data:/usr/share/elasticsearch/data -v /Users/yunyu/workspace_git/birdDocker/elasticsearch/me/logs:/usr/share/elasticsearch/logs --name elasticsearch_me_5x birdben/elasticsearch:5.3.1
# 查看Docker容器的日志,没有问题
$ docker logs 17734a3549ad
# 查看Docker容器内/usr/share/elasticsearch目录的权限(果然和推测的一样,logs目录的所属用户变成了root)
root@17734a3549ad:/usr/share/elasticsearch# ls -lh
total 224K
-rw-r--r-- 1 root root 190K Apr 17 15:55 NOTICE.txt
-rw-r--r-- 1 root root 9.4K Apr 17 15:55 README.textile
drwxr-xr-x 2 root root 4.0K May 4 07:02 bin
drwxr-xr-x 1 elasticsearch elasticsearch 4.0K May 4 07:02 config
drwxr-xr-x 3 root root 102 May 4 09:37 data
drwxr-xr-x 2 root root 4.0K May 4 07:02 lib
drwxr-xr-x 3 root root 102 May 4 09:37 logs
drwxr-xr-x 12 root root 4.0K May 4 07:02 modules
drwxr-xr-x 2 root root 4.0K Apr 17 15:55 plugins
# 查看Docker容器内/usr/share/elasticsearch/data目录的权限(不变,和之前一样)
$ ls -lh /usr/share/elasticsearch/data/
total 0
drwxr-xr-x 3 root root 102 May 4 06:33 nodes
# 查看Docker容器内/usr/share/elasticsearch/logs目录的权限(这里也和推测的一样,logs目录下创建的elasticsearch.log日志文件也属于root用户)
$ ls -lh /usr/share/elasticsearch/logs/
total 8.0K
-rw-r--r-- 1 root root 4.3K May 4 09:39 elasticsearch.log
# 查看Elasticsearch进程(不变,和之前一样)
$ ps -ef | grep elasticsearch
elastic+ 1 0 9 09:37 ? 00:00:18 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java -Xms2g -Xmx2g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -server -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -Djdk.io.permissionsUseCanonicalPath=true -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true -XX:+HeapDumpOnOutOfMemoryError -Des.path.home=/usr/share/elasticsearch -cp /usr/share/elasticsearch/lib/elasticsearch-5.3.1.jar:/usr/share/elasticsearch/lib/* org.elasticsearch.bootstrap.Elasticsearch
root 106 94 0 09:40 ? 00:00:00 grep elasticsearch

新建文档也和之前一样(忽略)

通过上面的尝试结果,可以得出如下结论:

  • 即使在Dockerfile使用chown修改了目录的所属用户,但是只要目录被挂载到宿主机,则该目录的所属用户又会被修改为root用户。
  • 如果不在Dockerfile中进行chown操作,当使用elasticsearch用户启动进程时,是无法访问root用户的目录的(目录被挂载后,目录的所属用户被修改为root用户的除外)

OK,前面的尝试隐藏了一点,我没有做特殊说明,就是我们前面的尝试都使用的root用户启动的容器。

1
2
# 这里我们没有指定-u或者--user参数,默认就是使用root用户启动容器
$ docker run -itd -v /Users/yunyu/workspace_git/birdDocker/elasticsearch/me/data:/usr/share/elasticsearch/data --name elasticsearch_me_5x birdben/elasticsearch:5.3.1

所以docker-entrypoint.sh脚本中,有个if判断条件是不是root用户启动的容器”$(id -u)” = ‘0’,如果是root用户启动的容器,则使用gosu切换到elasticsearch启动elasticsearch进程。在这之前还进行了chown操作,将/usr/share/elasticsearch/data和/usr/share/elasticsearch/logs目录的所有者修改为elasticsearch用户。再回想下我们尝试一是把Dockerfile和docker-entrypoint.sh中的chown操作都删除掉了,所以elasticsearch进程才无法将日志写入到所属root用户的logs目录下。

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
#!/bin/bash
set -e
# Add elasticsearch as command if needed
if [ "${1:0:1}" = '-' ]; then
set -- elasticsearch "$@"
fi
# Drop root privileges if we are running elasticsearch
# allow the container to be started with `--user`
if [ "$1" = 'elasticsearch' -a "$(id -u)" = '0' ]; then
# Change the ownership of user-mutable directories to elasticsearch
for path in \
/usr/share/elasticsearch/data \
/usr/share/elasticsearch/logs \
; do
chown -R elasticsearch:elasticsearch "$path"
done
set -- gosu elasticsearch "$@"
#exec gosu elasticsearch "$BASH_SOURCE" "$@"
fi
# As argument is not related to elasticsearch,
# then assume that user wants to run his own process,
# for example a `bash` shell to explore this image
exec "$@"

这里可能有人会有疑问,那把Dockerfile中的chown操作删除,docker-entrypoint.sh中的chown操作加上的效果会不会和尝试二的结果一样呢?也可以正常将ES日志写入文件呢?我们再来尝试一下

尝试四:只删掉Dockerfile的chown语句,然后挂载/usr/share/elasticsearch/logs目录

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
# 构建修改后的Elasticsearch的Docker镜像
$ docker build -t "birdben/elasticsearch:5.3.1" .
# 运行Elasticsearch的Docker容器,并且挂载对应的data目录
$ docker run -itd -v /Users/yunyu/workspace_git/birdDocker/elasticsearch/me/data:/usr/share/elasticsearch/data -v /Users/yunyu/workspace_git/birdDocker/elasticsearch/me/logs:/usr/share/elasticsearch/logs --name elasticsearch_me_5x birdben/elasticsearch:5.3.1
# 查看Docker容器的日志,没有问题
$ docker logs ca6eb8e80593
# 查看Docker容器内/usr/share/elasticsearch目录的权限(因为这里是在docker-entrypoint.sh对data和logs进行chown,所以只有data和logs所属elasticsearch用户)
root@ca6eb8e80593:/usr/share/elasticsearch# ls -lh
total 224K
-rw-r--r-- 1 root root 190K Apr 17 15:55 NOTICE.txt
-rw-r--r-- 1 root root 9.4K Apr 17 15:55 README.textile
drwxr-xr-x 2 root root 4.0K May 4 10:54 bin
drwxr-xr-x 1 root root 4.0K May 4 10:54 config
drwxr-xr-x 3 elasticsearch elasticsearch 102 May 4 10:58 data
drwxr-xr-x 2 root root 4.0K May 4 10:54 lib
drwxr-xr-x 3 elasticsearch elasticsearch 102 May 4 10:58 logs
drwxr-xr-x 12 root root 4.0K May 4 10:54 modules
drwxr-xr-x 2 root root 4.0K Apr 17 15:55 plugins
# 查看Docker容器内/usr/share/elasticsearch/data目录的权限
$ ls -lh /usr/share/elasticsearch/data/
total 0
drwxr-xr-x 3 root root 102 May 4 10:58 nodes
# 查看Docker容器内/usr/share/elasticsearch/logs目录的权限
$ ls -lh /usr/share/elasticsearch/logs/
total 4.0K
-rw-r--r-- 1 root root 3.9K May 4 10:59 elasticsearch.log
# 查看Elasticsearch进程
$ ps -ef | grep elasticsearch
elastic+ 1 0 4 10:58 ? 00:00:21 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java -Xms2g -Xmx2g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -server -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -Djdk.io.permissionsUseCanonicalPath=true -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j.skipJansi=true -XX:+HeapDumpOnOutOfMemoryError -Des.path.home=/usr/share/elasticsearch -cp /usr/share/elasticsearch/lib/elasticsearch-5.3.1.jar:/usr/share/elasticsearch/lib/* org.elasticsearch.bootstrap.Elasticsearch
root 107 96 0 11:06 ? 00:00:00 grep elasticsearch
1
2
3
4
5
6
7
8
# 新建文档
$ curl -XPOST 'http://127.0.0.1:9200/user/1/1' -d '{"name":"birdben"}'
# 查看索引文件
$ ls -lh /usr/share/elasticsearch/data/nodes/0/indices/meAhtSJXRl-cKzoqQZifBQ/0/index/
total 4.0K
-rw-r--r-- 1 root root 130 May 4 11:07 segments_1
-rw-r--r-- 1 root root 0 May 4 11:07 write.lock

这里data和logs目录都属于elasticsearch用户,但是data和logs目录下的文件却都属于root用户,这是什么情况呢?

因为docker run运行容器的时候,没有指定-u或者–user参数,这样就默认使用root用户启动容器,而在卷中创建的文件和文件夹将具有与在容器中创建它们的用户(root用户)相同的uid:gid(数字)。 如果你在容器内添加一个用户,具有与容器相同的uid:gid,并将其作为该用户(elasticsearch用户)运行,就可以使在卷中创建的文件和文件夹将具有与在容器中创建它们的用户(elasticsearch用户)相同的uid:gid(数字)。

所以这里docker-entrypoint.sh中的chown也很重要,因为只有root用户启动容器(docker run -u root)的时候会执行chown操作,如果是使用elasticsearch用户启动容器(docker run -u elasticsearch)的时候就不会执行chown操作。所以此种情况需要在Dockerfile中先执行chown操作。

总结如下:

volume挂载的目录默认属于root用户,如果没有chown给其他用户的话,在Volume卷中创建的文件和文件夹将具有与在容器中创建它们的用户相同的uid:gid(数字)。

参考文章:

Docker实战(二十七)Docker容器之间的通信

最近在修改我以前写的Docker镜像,才发现我一直都没有把Docker用好,连Docker的容器之前如何通信都不知道。之前的做法是把不同的环境安装在一个Docker容器中,就不存在容器间通信的问题。但是Docker推荐的用法是一个Docker容器只运行一个进程,所以我将以前写的Docker镜像进行了重构。下面来总结下Docker容器之间的通信。

Docker的网络模式

docker目前支持以下5种网络模式:

docker run 创建 Docker 容器时,可以用 –net 选项指定容器的网络模式。

  • host模式 : 使用 –net=host 指定。与宿主机共享网络,此时容器没有使用网络的namespace,宿主机的所有设备,如Dbus会暴露到容器中,因此存在安全隐患。
  • container模式 : 使用 –net=container:NAME_or_ID 指定。指定与某个容器实例共享网络。
  • none模式 : 使用 –net=none 指定。不设置网络,相当于容器内没有配置网卡,用户可以手动配置。
  • bridge模式 : 使用 –net=bridge 指定,默认设置。此时docker引擎会创建一个veth对,一端连接到容器实例并命名为eth0,另一端连接到指定的网桥中(比如docker0),因此同在一个主机的容器实例由于连接在同一个网桥中,它们能够互相通信。容器创建时还会自动创建一条SNAT规则,用于容器与外部通信时。如果用户使用了-p或者-Pe端口端口,还会创建对应的端口映射规则。
  • 自定义模式 : 使用自定义网络,可以使用docker network create创建,并且默认支持多种网络驱动,用户可以自由创建桥接网络或者overlay网络。

默认是桥接模式,网络地址为172.17.0.0/16,同一主机的容器实例能够通信,但不能跨主机通信。

host模式

如果启动容器的时候使用 host 模式,那么这个容器将不会获得一个独立的 Network Namespace,而是和宿主机共用一个 Network Namespace。容器将不会虚拟出自己的网卡,配置自己的 IP 等,而是使用宿主机的 IP 和端口。

container模式

这个模式指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过 lo 网卡设备通信。

none模式

这个模式和前两个不同。在这种模式下,Docker 容器拥有自己的 Network Namespace,但是,并不为 Docker容器进行任何网络配置。也就是说,这个 Docker 容器没有网卡、IP、路由等信息。需要我们自己为 Docker 容器添加网卡、配置 IP 等。

bridge模式

bridge 模式是 Docker 默认的网络设置,此模式会为每一个容器分配 Network Namespace、设置 IP 等,并将一个主机上的 Docker 容器连接到一个虚拟网桥上。

当 Docker server 启动时,会在主机上创建一个名为 docker0 的虚拟网桥,此主机上启动的 Docker 容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。

接下来就要为容器分配 IP 了,Docker 会从 RFC1918 所定义的私有 IP 网段中,选择一个和宿主机不同的IP地址和子网分配给 docker0,连接到 docker0 的容器就从这个子网中选择一个未占用的 IP 使用。如一般 Docker 会使用 172.17.0.0/16 这个网段,并将 172.17.42.1/16 分配给 docker0 网桥(在主机上使用 ifconfig 命令是可以看到 docker0 的,可以认为它是网桥的管理接口,在宿主机上作为一块虚拟网卡使用)

当创建一个 Docker 容器的时候,同时会创建了一对 veth pair 接口(当数据包发送到一个接口时,另外一个接口也可以收到相同的数据包)。这对接口一端在容器内,即 eth0;另一端在本地并被挂载到 docker0 网桥,名称以 veth 开头(例如 vethAQI2QT)。通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。Docker 就创建了在主机和所有容器之间一个虚拟共享网络。

Docker bridge模式

同主机不同容器之间通信

这里同主机不同容器之间通信主要使用Docker桥接(Bridge)模式。该bridge接口在本地一个单独的Docker宿主机上运行,并且它是我们后面提到的所有三种连接方式的背后机制。

1
2
3
4
5
6
7
8
$ ifconfig docker0
docker0 Link encap:Ethernet HWaddr 56:84:7a:fe:97:99
inet addr:172.17.42.1 Bcast:0.0.0.0 Mask:255.255.0.0
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)

连接方式

  • 方式一:可以通过使用容器的IP地址来通信。这种方式会导致IP地址的硬编码,不方便迁移,并且容器重启后IP地址可能会改变,除非使用固定的IP地址。
  • 方式二:可以通过宿主机的IP加上容器暴露出的端口号来通信。这种方式比较单一,只能依靠监听在暴露出的端口的进程来进行有限的通信。
  • 方式三:可以使用容器名,通过docker的link机制通信。这种方式通过docker的link机制可以通过一个name来和另一个容器通信,link机制方便了容器去发现其它的容器并且可以安全的传递一些连接信息给其它的容器。使用name给容器起一个别名,方便记忆和使用。即使容器重启了,地址发生了变化,不会影响两个容器之间的连接。
1
2
# 查看容器的内部IP
$ docker inspect --format='{{.NetworkSettings.IPAddress}}' $CONTAINER_ID
1
2
3
4
5
6
7
# Elasticsearch容器
$ docker inspect --format='{{.NetworkSettings.IPAddress}}' 4d5e7a1058de
172.17.0.2
# Kibana容器
$ docker inspect --format='{{.NetworkSettings.IPAddress}}' 4f26e64bfe82
172.17.0.4

方式一:使用容器的IP地址来通信

1
2
3
4
5
6
# 进入Kibana容器
$ docker exec -it 4f26e64bfe82 /bin/bash
# 在Kibana容器使用ES容器的IP地址来访问ES服务
$ curl -XGET 'http://172.17.0.2:9200/_cat/health?pretty'
1493707223 06:40:23 ben-es yellow 1 1 11 11 0 0 11 0 - 50.0%

方式二:使用宿主机的IP加上容器暴露出的端口号来通信

1
2
3
4
5
6
# 进入Kibana容器
$ docker exec -it 4f26e64bfe82 /bin/bash
# 在Kibana容器使用宿主机的IP地址来访问ES服务(我这里本机的IP地址是10.10.1.129)
$ curl -XGET 'http://10.10.1.129:9200/_cat/health?pretty'
1493707223 06:40:23 ben-es yellow 1 1 11 11 0 0 11 0 - 50.0%

方式三:使用docker的link机制通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 先启动ES容器,并且使用--name指定容器名称为:elasticsearch_2.x_yunyu
$ docker run -itd -p 9200:9200 -p 9300:9300 --name elasticsearch_2.x_yunyu birdben/elasticsearch_2.x:v2
# 启动Kibana容器,并且使用--link指定关联的容器名称为ES的容器名称:elasticsearch_2.x_yunyu
$ docker run -itd -p 5601:5601 --link elasticsearch_2.x_yunyu --name kibana_4.x_yunyu birdben/kibana_4.x:v2
# 查看运行的容器
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS
4f26e64bfe82 birdben/kibana_4.x:v2 "docker-entrypoint..." 25 hours ago Up 15 minutes 0.0.0.0:5601->5601/tcp kibana_4.x_yunyu
4d5e7a1058de birdben/elasticsearch_2.x:v2 "docker-entrypoint..." 26 hours ago Up 19 hours 0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp elasticsearch_2.x_yunyu
# 在Kibana容器使用--link的容器名称来访问ES服务
$ curl -XGET 'http://elasticsearch_2.x_yunyu:9200/_cat/health?pretty'
1493707223 06:40:23 ben-es yellow 1 1 11 11 0 0 11 0 - 50.0%

实际上–link机制就是在Docker容器中的/etc/hosts文件中添加了一个ES容器的名称解析。有了这个名称解析后就可以不使用IP来和目标容器通信了,除此之外当目标容器重启,Docker会负责更新/etc/hosts文件,因此可以不用担心容器重启后IP地址发生了改变,解析无法生效的问题。

Kibana容器的/etc/hosts文件

1
2
3
4
5
6
7
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 4d5e7a1058de

ES容器的/etc/hosts文件

1
2
3
4
5
6
7
8
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 elasticsearch_2.x_yunyu 4d5e7a1058de
172.17.0.4 4f26e64bfe82

当docker引入网络新特性后,link机制变的有些多余,但是为了兼容早期版本,–link机制在默认网络上的功能依旧没有发生变化,docker引入网络新特性后,内置了一个DNS Server,但是只有用户创建了自定义网络后,这个DNS Server才会起作用。

跨主机不同容器之间通信

(待续)

使用DockerCompose

(待续)

参考文章:

AWK学习(二)

awk用法

注意:使用awk标准版可以不必安装gawk,使用gawk扩展功能必须要先安装gawk

1
2
3
4
5
# Ubuntu环境
$ sudo apt-get install gawk
# Mac环境
$ brew install gawk

awk命令行格式

1
2
3
4
5
# 方式一:awk命令直接指定过滤规则
awk [options] file ...
# 方式二:指定awk的脚本文件,脚本文件内是指定的过滤规则
awk [options] -f file ....

方式一:awk命令直接指定过滤规则

1
awk 'BEGIN print{"HEAD1\tHEAD2\tHEAD3\tHEAD4\n"} {print} END print{"END1\tEND2\tEND3\tEND4\n"}' test.txt

方式二:指定awk的脚本文件,脚本文件内是指定的过滤规则

1
awk -f command.awk marks.txt

command.awk

1
BEGIN print{"HEAD1\tHEAD2\tHEAD3\tHEAD4\n"} {print} END print{"END1\tEND2\tEND3\tEND4\n"} test.txt

awk结构

一个awk程序包含一系列的 模式 {动作指令} 或是函数定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 动作指令需要以{}引起来
$ awk 'BEGIN {print "start"} {print} END {print "end"}' test.txt
# BEGIN rule(s)
BEGIN
{
print "start"
}
# Rule(s)
{
# $0是隐含参数,输出整行内容
print $0
}
# END rule(s)
END
{
print "end"
}

awk原理

1). awk逐行扫描文件,从第一行到最后一行,寻找匹配特定模式的行,并在这些行上进行你想要的操作。
2). awk基本结构包括模式匹配(用于找到要处理的行)和处理过程(即处理动作)。
pattern {action}

提示:awk读取文件内容的每一行时,将对比改行是否与给定的模式相匹配,如果匹配则执行处理过程,否则对该行不做任何处理。

如果没有指定处理脚本,则把匹配的行显示到标准输出,即默认处理动作是print打印行;
如果没有指定模式匹配,则默认匹配所有数据。
3). awk有两个特殊的模式:BEGIN和END,他们被放置在没有读取任何数据之前以及在所有数据读取完成以后执行。

标准awk选项

1
2
3
# -v : 该选项将一个值赋予一个变量,它会在程序开始之前进行赋值,可以通过--dump-variables[=file]输出出来
$ awk -v bird=birdben 'BEGIN {print "bird=" bird}'
bird=birdben

标准awk内置变量

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# ARGC : awk命令行参数个数
$ awk 'BEGIN {print "ARGC=" ARGC}'
ARGC=1
$ awk 'BEGIN {print "ARGC=" ARGC}' test1 test2
ARGC=3
# ARGV : 命令行参数数组,存储命令行参数的数组,索引范围从0 - ARGC - 1。
$ awk 'BEGIN {print "ARGV[0]=" ARGV[0]}'
ARGV[0]=awk
$ awk 'BEGIN {print "ARGV[1]=" ARGV[1] "\t" "ARGV[2]=" ARGV[2]}' test1 test2
ARGV[1]=test1 ARGV[2]=test2
# 循环输出ARGV数组中的参数值
$ awk 'BEGIN {
for (i = 0; i <= ARGC - 1; i++) {
print "ARGV[" i "] = " ARGV[i]
printf "ARGV[%d] = %s\n", i, ARGV[i]
}
}' test1 test2
ARGV[0] = awk
ARGV[0] = awk
ARGV[1] = test1
ARGV[1] = test1
ARGV[2] = test2
ARGV[2] = test2
# 这里顺便说一下print和printf函数的区别
# print函数是不格式化直接输出函数,默认自动换行
# printf()函数是格式化输出函数,默认不会自动换行
# 上面是printf()函数的简写方式,完整的写法应该如下
$ awk 'BEGIN {
for (i = 0; i <= ARGC - 1; i++) {
print "ARGV[" i "] = " ARGV[i]
printf("ARGV[%d] = %s\n", i, ARGV[i])
}
}' test1 test2
ARGV[0] = awk
ARGV[0] = awk
ARGV[1] = test1
ARGV[1] = test1
ARGV[2] = test2
ARGV[2] = test2
# CONVFMT : 此变量表示数据转换为字符串的格式,其默认值为 %.6g
$ awk 'BEGIN { print "Conversion Format =" CONVFMT }'
Conversion Format = %.6g
# ENVIRON : 此变量是与环境变量相关的关联数组变量,以key-value的方式查看系统环境变量的值。
$ awk 'BEGIN { print ENVIRON["JAVA_HOME"] }'
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home
# FILENAME : 此变量表示当前文件名称。
# 注意:这里一定要是END才能读取到文件名,因为在BEGIN开始快还没有开始读取文件test.txt的内容,也就是FILENAME是未定义的。
$ awk 'END {print "FILENAME = " FILENAME}' test.txt
FILENAME = test.txt
# FS : 此变量表示输入的数据域之间的分隔符,其默认值是空格。你可以使用 -F 命令行选项改变它的默认值。
$ awk 'BEGIN {print "FS = " FS}' | cat -vte
FS = $
# NF : 此变量表示当前输入记录中域的数量。(简单理解,域:当前行用分隔符分开数据的就是数据域,如下面的例子,One,Two,Three都是数据域)
# 输出每一行的数据域数量
$ echo -e "One Two\nOne Two Three\nOne Two Three Four" | awk '{print "NF = " NF}'
NF = 2
NF = 3
NF = 4
# 输出每一行的数据域数量大于2的
$ echo -e "One Two\nOne Two Three\nOne Two Three Four" | awk 'NF > 2'
$ echo -e "One Two\nOne Two Three\nOne Two Three Four" | awk 'NF > 2 {print}'
One Two Three
One Two Three Four
# NR : 此变量表示当前记录的数量。
# 输出每一行的当前记录数量,也就是当前行的游标
$ echo -e "One Two\nOne Two Three\nOne Two Three Four" | awk '{print "NR = " NR}'
NR = 1
NR = 2
NR = 3
# 输出每一行的当前记录的游标大于2的
$ echo -e "One Two\nOne Two Three\nOne Two Three Four" | awk 'NR > 2 {print}'
One Two Three Four
# FNR : 该变量与 NR 类似,不过它是相对于当前文件而言的。此变量在处理多个文件输入时有重要的作用。每当从新的文件中读入时 FNR 都会被重新设置为 0。
$ awk '{print "FNR = " FNR "\t" "NR = " NR}' test.txt test1.txt
FNR = 1 NR = 1
FNR = 2 NR = 2
FNR = 3 NR = 3
FNR = 4 NR = 4
FNR = 5 NR = 5
FNR = 1 NR = 6
FNR = 2 NR = 7
FNR = 3 NR = 8
FNR = 4 NR = 9
FNR = 5 NR = 10
# OFMT : 此变量表示数值输出的格式,它的默认值为 %.6g。
$ awk 'BEGIN {print "OFMT = " OFMT}'
OFMT = %.6g
# OFS : 此变量表示输出域之间的分割符,其默认为空格。
$ awk 'BEGIN {print "OFS = " OFS}' | cat -vte
# 这里^I就是我们test.txt的分隔符:制表符\t
$ awk 'BEGIN {print "OFS = " OFS} {print}' test.txt | cat -vte
OFS = $
1^Ibirdben^I^Ibejing^I^I28$
2^Ierhuo^I^Ishanghai^I30$
3^Izhangsan^Ishanghai^I20$
4^Ilisi^I^Ishenzhen^I25$
5^Iwangwu^I^Ibeijing^I^I28$
# ORS : 此变量表示输出记录(行)之间的分割符,其默认值是换行符。
$ awk 'BEGIN {print "ORS = " ORS}' | cat -vte
ORS = $
$
# RLENGTH : 此变量表示 match 函数匹配的字符串长度。AWK 的 match 函数用于在输入的字符串中搜索指定字符串。
$ awk 'BEGIN { if (match("One Two Three", "re")) { print RLENGTH } }'
# RS : 此变量表示输入记录的分割符,其默认值为换行符。
$ awk 'BEGIN {print "RS = " RS}' | cat -vte
RS = $
$
# RSTART : 此变量表示由 match 函数匹配的字符串的第一个字符的位置。从1开始。
$ awk 'BEGIN { if (match("One Two Three", "Thre")) { print RSTART } }'
# SUBSEP : 此变量表示数组下标的分割行符,其默认值为 \034 。
$ awk 'BEGIN { print "SUBSEP = " SUBSEP }' | cat -vte
SUBSEP = ^\$
# $0 : 此变量表示整个输入记录。
$ awk '{print $0}' test.txt
1 birdben bejing 28
2 erhuo shanghai 30
3 zhangsan shanghai 20
4 lisi shenzhen 25
5 wangwu beijing 28
# $n : 此变量表示当前输入记录的第 n 个域,这些域之间由 FS 分割。
$ awk '{print $1 "\t" $2}' test.txt
1 birdben
2 erhuo
3 zhangsan
4 lisi
5 wangwu

gawk内置变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ARGIND : 此变量表示当前文件中正在处理的 ARGV 数组的索引值。
$ gawk '{
print "ARGIND = " ARGIND "\t" "FileName = " ARGV[ARGIND]
}' test.txt test1.txt
ARGIND = 1 FileName = test.txt
ARGIND = 1 FileName = test.txt
ARGIND = 1 FileName = test.txt
ARGIND = 1 FileName = test.txt
ARGIND = 1 FileName = test.txt
ARGIND = 2 FileName = test1.txt
ARGIND = 2 FileName = test1.txt
ARGIND = 2 FileName = test1.txt
ARGIND = 2 FileName = test1.txt
ARGIND = 2 FileName = test1.txt
# IGNORECASE : 当此变量被设置后,GAWK将变得大小写不敏感。
$ gawk 'BEGIN{IGNORECASE=1} /BIRDBEN/' test.txt
1 birdben bejing 28
# LINT : 此变量提供了在 GAWK 程序中动态控制 --lint 选项的一种途径。当这个变量被设置后, GAWK 会输出 lint 警告信息。如果给此变量赋予字符值 fatal,lint 的所有警告信息将会变了致命错误信息(fatal errors)输出,这和 --lint=fatal 效果一样。
# 设置LINT级别后,会检查awk语法并根据LINT设置的级别给出相应的提示信息
$ gawk 'BEGIN {LINT=1; a}'
gawk: cmd. line:1: warning: reference to uninitialized variable `a'
gawk: cmd. line:1: warning: statement has no effect

gawk选项

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
43
44
45
46
47
# --dump-variables[=file] : 该选项会输出排好序的全局变量列表和它们最终的值到文件中,默认的文件是 awkvars.out
$ gawk -v bird=birdben --dump-variables=bird_var.out 'BEGIN {print "bird="bird}'
bird=birdben
$ cat bird_var.out
ARGC: 1
ARGIND: 0
ARGV: array, 1 elements
BINMODE: 0
CONVFMT: "%.6g"
ENVIRON: array, 36 elements
ERRNO: ""
FIELDWIDTHS: ""
FILENAME: ""
FNR: 0
FPAT: "[^[:space:]]+"
FS: " "
FUNCTAB: array, 41 elements
IGNORECASE: 0
LINT: 0
NF: 0
NR: 0
OFMT: "%.6g"
OFS: " "
ORS: "\n"
PREC: 53
PROCINFO: array, 31 elements
RLENGTH: 0
ROUNDMODE: "N"
RS: "\n"
RSTART: 0
RT: ""
SUBSEP: "\034"
SYMTAB: array, 29 elements
TEXTDOMAIN: "messages"
bird: "birdben"
# --profile[=file] : 该选项会输出一份格式化之后的程序到文件中,默认文件是 awkprof.out
$ gawk --profile=bird_profile.out -v bird=birdben 'BEGIN {print "bird="bird}'
bird=birdben
$ cat bird_profile.out
# gawk profile, created Sat Mar 4 13:25:49 2017
# BEGIN rule(s)
BEGIN {
1 print "bird=" bird
}

awk条件判断

1
2
3
4
5
# if-else条件判断
if (condition)
action-1
else
action-2

awk循环用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# for循环语法
for (initialisation; condition; increment/decrement)
action
# while循环语法
while (condition)
action
# do-while循环语法
do
action
while (condition)
# Break : 用以结束循环过程。
# Continue : 用于在循环体内部结束本次循环,从而直接进入下一次循环迭代。
# Exit : 用于结束脚本程序的执行。
# Next : 用于跳过你所提供的所有剩下的模式和表达式,直接处理下一个输入行,帮助你阻止运行命令执行过程中多余的步骤。一般配合if-else使用。

awk内置函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 字符串函数
asort(arr [, d [, how] ]) : asort 函数使用 GAWK 值比较的一般规则排序 arr 中的内容,然后用以 1 开始的有序整数替换排序内容的索引。
asorti(arr [, d [, how] ]) : asorti 函数的行为与 asort 函数的行为很相似,二者的差别在于 aosrt 对数组的值排序,而 asorti 对数组的索引排序。
gsub(regex, sub, string) : gsub 是全局替换( global substitution )的缩写。它将出现的子串(sub)替换为 regx。第三个参数 string 是可选的,默认值为 $0,表示在整个输入记录中搜索子串。
index(str, sub) : index 函数用于检测字符串 sub 是否是 str 的子串。如果 sub 是 str 的子串,则返回子串 sub 在字符串 str 的开始位置;若不是其子串,则返回 0。str 的字符位置索引从 1 开始计数。
length(str) : length 函数返回字符串的长度。
match(str, regex) : match 返回正则表达式在字符串 str 中第一个最长匹配的位置。如果匹配失败则返回0。
split(str, arr, regex) : split 函数使用正则表达式 regex 分割字符串 str。分割后的所有结果存储在数组 arr 中。如果没有指定 regex 则使用 FS 切分。
sprintf(format, expr-list) : sprintf 函数按指定的格式( format )将参数列表 expr-list 构造成字符串然后返回。
strtonum(str) : strtonum 将字符串 str 转换为数值。 如果字符串以 0 开始,则将其当作十进制数;如果字符串以 0x 或 0X 开始,则将其当作十六进制数;否则,将其当作浮点数。
sub(regex, sub, string) : sub 函数执行一次子串替换。它将第一次出现的子串用 regex 替换。第三个参数是可选的,默认为 $0。
substr(str, start, l) : substr 函数返回 str 字符串中从第 start 个字符开始长度为 l 的子串。如果没有指定 l 的值,返回 str 从第 start 个字符开始的后缀子串。
tolower(str) : 此函数将字符串 str 中所有大写字母转换为小写字母然后返回。注意,字符串 str 本身并不被改变。
toupper(str) : 此函数将字符串 str 中所有小写字母转换为大写字母然后返回。注意,字符串 str 本身不被改变。
# 时间函数
systime : 此函数返回从 Epoch 以来到当前时间的秒数(在 POSIX 系统上,Epoch 为1970-01-01 00:00:00 UTC)。
mktime(datespec) : 此函数将字符串 dataspec 转换为与 systime 返回值相似的时间戳。 dataspec 字符串的格式为 YYYY MM DD HH MM SS。
strftime([format [, timestamp[, utc-flag]]]) : 此函数根据 format 指定的格式将时间戳 timestamp 格式化。

awk基本用法

示例test.txt文件内容

1
2
3
4
5
1 birdben bejing 28
2 erhuo shanghai 30
3 zhangsan shanghai 20
4 lisi shenzhen 25
5 wangwu beijing 28
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
43
44
45
46
47
48
49
50
51
52
53
# 输出test.txt文件中的内容
$ awk '{print}' test.txt
$ awk '{print $0}' test.txt
1 birdben bejing 28
2 erhuo shanghai 30
3 zhangsan shanghai 20
4 lisi shenzhen 25
5 wangwu beijing 28
# 输出test.txt文件中的指定列的内容
$ awk '{print $2}' test.txt
birdben
erhuo
zhangsan
lisi
wangwu
$ awk '{print $1 "\t" $2}' test.txt
1 birdben
2 erhuo
3 zhangsan
4 lisi
5 wangwu
# 输出test.txt文件中匹配的内容,下面两种方式是等价的
$ awk '/birdben/' test.txt
$ awk '/birdben/ {print}' test.txt
1 birdben bejing 28
# 输出test.txt文件中匹配的指定列的内容
$ awk '/birdben/ {print $1 "\t" $2}' test.txt
1 birdben
# 在最后输出test.txt文件中匹配的行数
$ awk '/birdben/{++matchCount} END {print "matchCount="matchCount}' test.txt
matchCount=1
# 添加列头然后输出
$ awk 'BEGIN {printf "No\tName\t\t\City\t\tAge\n"} {print}' test.txt
$ awk 'BEGIN {print "No\tName\t\t\City\t\tAge"} {print}' test.txt
No Name City Age
1 birdben bejing 28
2 erhuo shanghai 30
3 zhangsan shanghai 20
4 lisi shenzhen 25
5 wangwu beijing 28
# 输出字符超过20的内容
$ awk 'length($0) > 20' test.txt
$ awk 'length($0) > 20 {print}' test.txt
1 birdben bejing 28
3 zhangsan shanghai 20
5 wangwu beijing 28

参考文章:

Python和Java服务器通信实现的理解和比较

Python的WSGI和Java的Servlet API

Python的WSGI

最近在学习使用Python进行WebServer的编程,发现WSGI(Web Server Gateway Interface)的概念。PythonWeb服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是Python应用程序或框架和Web服务器之间的一种接口,已经被广泛接受,它已基本达成它的可移植性方面的目标。WSGI 没有官方的实现,因为WSGI更像一个协议。只要遵照这些协议,WSGI应用(Application)都可以在任何服务器(Server)上运行,反之亦然。

如果没有WSGI,你选择的Python网络框架将会限制所能够使用的 Web 服务器。

这就意味着,你基本上只能使用能够正常运行的服务器与框架组合,而不能选择你希望使用的服务器或框架。

那么,你怎样确保可以在不修改 Web 服务器代码或网络框架代码的前提下,使用自己选择的服务器,并且匹配多个不同的网络框架呢?为了解决这个问题,就出现了PythonWeb 服务器网关接口(Web Server Gateway Interface,WSGI)。

WSGI的出现,让开发者可以将网络框架与 Web 服务器的选择分隔开来,不再相互限制。现在,你可以真正地将不同的 Web 服务器与网络开发框架进行混合搭配,选择满足自己需求的组合。例如,你可以使用Gunicorn或Nginx/uWSGI或Waitress服务器来运行Django、Flask或Pyramid应用。正是由于服务器和框架均支持WSGI,才真正得以实现二者之间的自由混合搭配。

Java的Servlet API

下面将类比Java来说明一下:

如果没有Java Servlet API,你选择的Java Web容器(Java Socket编程框架实现)将会限制所能够使用的Java Web框架(因为没有Java Servlet API,那么SpringMVC可能会实现一套SpringMVCHttpRequest和SpringMVCHttpResponse标准,Struts2可能会实现一套Struts2HttpRequest和Struts2HttpResponse标准,如果Tomcat只支持SpringMVC的API,那么选择Tomcat服务器就只能使用SpringMVC的Web框架来写服务端代码)。

这就意味着,你基本上只能使用能够正常运行的服务器(Tomcat)与框架(SpringMVC)组合,而不能选择你希望使用的服务器或框架(比如:我要换成Tomcat + Struts2的组合)。

注意:这里假设没有Java Servlet API,这样就相当于SpringMVC和Struts2可能都要自己实现一套Servlet封装HttpRequest和HttpResponse,这样从SpringMVC更换成Struts2就几乎需要重写服务器端的代码。为了解决这个问题,Java提出了Java Servlet API协议,让所有的Web服务框架都实现此Java Servlet API协议来和Java Web服务器(例如:Tomcat)交互,而复杂的网络连接控制等等都交由Java Web服务器来控制,Java Web服务器用Java Socket编程实现了复杂的网络连接管理。

详细说说Python的WSGI

Python Web 开发中,服务端程序可以分为两个部分,一是服务器程序,二是应用程序。前者负责把客户端请求接收,整理,后者负责具体的逻辑处理。为了方便应用程序的开发,我们把常用的功能封装起来,成为各种Web开发框架,例如 Django, Flask, Tornado。不同的框架有不同的开发方式,但是无论如何,开发出的应用程序都要和服务器程序配合,才能为用户提供服务。这样,服务器程序就需要为不同的框架提供不同的支持。这样混乱的局面无论对于服务器还是框架,都是不好的。对服务器来说,需要支持各种不同框架,对框架来说,只有支持它的服务器才能被开发出的应用使用。

这时候,标准化就变得尤为重要。我们可以设立一个标准,只要服务器程序支持这个标准,框架也支持这个标准,那么他们就可以配合使用。一旦标准确定,双方各自实现。这样,服务器可以支持更多支持标准的框架,框架也可以使用更多支持标准的服务器。

  • 服务器端:

服务器必须将可迭代对象的内容传递给客户端,可迭代对象会产生bytestrings,必须完全完成每个bytestring后才能请求下一个。

  • 应用程序:

服务器程序会在每次客户端的请求传来时,调用我们写好的应用程序,并将处理好的结果返回给客户端。

总结:

  • Web Server Gateway Interface是Python编写Web业务统一接口。
  • 无论多么复杂的Web应用程序,入口都是一个WSGI处理函数。
  • Web应用程序就是写一个WSGI的处理函数,主要功能在于交互式地浏览和修改数据,生成动态Web内容,针对每个HTTP请求进行响应。

实现Python的Web应用程序能被访问的方式

要使 Python 写的程序能在 Web 上被访问,还需要搭建一个支持 Python 的 HTTP 服务器(也就是实现了WSGI server(WSGI协议)的Http服务器)。有如下几种方式:

  • 可以自己使用Python Socket编程实现一个Http服务器
  • 使用支持Python的开源的Http服务器(如:uWSGI,wsgiref,Mod_WSGI等等)。如果是使用Nginx,Apache,Lighttpd等Http服务器需要单独安装支持WSGI server的模块插件。
  • 使用Python开源Web框架(如:Flask,Django等等)内置的Http服务器(Django自带的WSGI Server,一般测试使用)
Python标准库对WSGI的实现

wsgiref 是Python标准库给出的 WSGI 的参考实现。simple_server 这一模块实现了一个简单的 HTTP 服务器。

Python源码中的wsgiref的simple_server.py正好说明上面的分工情况,server的主要作用是接受client的请求,並把的收到的请求交給RequestHandlerClass处理,RequestHandlerClass处理完成后回传结果给client

uWSGI服务器

uWSGI是一个Web服务器,它实现了WSGI协议、uwsgi、http等协议。注意uwsgi是一种通信协议,而uWSGI是实现uwsgi协议和WSGI协议的Web服务器。

Django框架内置的WSGI Server服务器

Django的WSGIServer继承自wsgiref.simple_server.WSGIServer,而WSGIRequestHandler继承自wsgiref.simple_server.WSGIRequestHandler

之前说到的application,在Django中一般是django.core.handlers.wsgi.WSGIHandler对象,WSGIHandler继承自django.core.handlers.base.BaseHandler,这个是Django处理request的核心逻辑,它会创建一个WSGIRequest实例,而WSGIRequest是从http.HttpRequest继承而来

Python和Java的类比

Python和Java的服务器结构
  • 独立WSGI server(实现了Http服务器功能) + Python Web应用程序
    • 例如:Gunicorn,uWSGI + Django,Flask
  • 独立Servlet引擎(Java应用服务器)(实现了Http服务器功能) + Java Web应用程序
    • 例如:Jetty,Tomcat + SpringMVC,Struts2
Python和Java服务器共同点
  • WSGI server(例如Gunicorn和uWSGI)

    • WSGI server服务器内部都有组建来实现Socket连接的创建和管理。
    • WSGI server服务器都实现了Http服务器功能,能接受Http请求,并且通过Python Web应用程序处理之后返回动态Web内容。
  • Java应用服务器(Jetty和Tomcat)

    • Java应用服务器内部都有Connector组件来实现Socket连接的创建和管理。
    • Java应用服务器都实现了Http服务器功能,能接受Http请求,并且通过Java Web应用程序处理之后返回动态Web内容。

参考文章:

Logstash学习(八)Logstash的metrics告警使用

最近为了提高系统的运行稳定性,在日志收集的过程中要求添加错误日志的告警,这里主要使用Logstash自带的metrics功能。Logstash可以在filter中根据某些字段进行日志的分类,如果某一类的日志出现次数不在正常范围,就会触发metrics event然后进行告警操作,这里我们只是使用简单的发邮件的告警方式。

Java的日志格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2017-02-14 14:36:40 [ INFO] - com.yunyu.birdben.task.RiskTask -RiskTask.java(97) -我是日志信息
2017-02-14 14:36:41 [ INFO] - com.yunyu.birdben.task.RiskTask -RiskTask.java(97) -我是日志信息
2017-02-14 14:36:42 [ INFO] - com.yunyu.birdben.task.RiskTask -RiskTask.java(97) -我是日志信息
2017-02-14 14:36:43 [ INFO] - com.yunyu.birdben.task.RiskTask -RiskTask.java(97) -我是日志信息
2017-02-14 14:36:44 [ INFO] - com.yunyu.birdben.task.RiskTask -RiskTask.java(97) -我是日志信息
2017-02-14 14:36:45 [ INFO] - com.yunyu.birdben.task.RiskTask -RiskTask.java(97) -我是日志信息
2017-02-14 14:36:46 [ INFO] - com.yunyu.birdben.task.OtherTask -OtherTask.java(97) -我是日志信息
2017-02-14 14:36:47 [ INFO] - com.yunyu.birdben.task.OtherTask -OtherTask.java(97) -我是日志信息
2017-02-14 14:36:48 [ INFO] - com.yunyu.birdben.task.OtherTask -OtherTask.java(97) -我是日志信息
2017-02-14 14:36:49 [ INFO] - com.yunyu.birdben.task.OtherTask -OtherTask.java(97) -我是日志信息
2017-02-14 14:36:50 [ INFO] - com.yunyu.birdben.task.OtherTask -OtherTask.java(97) -我是日志信息
2017-02-14 14:36:51 [ INFO] - com.yunyu.birdben.task.OtherTask -OtherTask.java(97) -我是日志信息
2017-02-14 14:36:52 [ INFO] - com.yunyu.birdben.task.OtherTask -OtherTask.java(97) -我是日志信息
2017-02-14 14:36:53 [ INFO] - com.yunyu.birdben.task.OtherTask -OtherTask.java(97) -我是日志信息

Grok表达式

1
2
JAVA_TIMESTAMP %{YEAR}-%{MONTHNUM}-%{MONTHDAY} %{TIME}
JAVA_LOGS %{JAVA_TIMESTAMP:timestamp} \[ %{DATA:level}\] - %{DATA:class_name} -%{DATA:file_name}.java\(%{DATA:line}\) -%{GREEDYDATA:msg}

日志中有两个文件RiskTask.java和OtherTask.java文件,我们的需求是5分钟内,如果RiskTask的日志一条都没有出现就发送告警邮件。

这里使用了三个新的插件metrics,ruby,email

  • metrics : 用来定时统计和生成metrics event的
  • ruby : 使用ruby代码来定制metrics event失效的条件
  • email : 不需要多说,就是用来发送告警邮件的

metrics插件

默认情况下或根据flush_interval,每5秒刷新一次指标。 指标在事件流中显示为新事件,并执行发生在事件流以及输出之后的任何过滤器。

一般来说,您需要为指标添加标记,并让输出显式查找该标记。

被刷新的事件将以以下方式包括每个计量器和计时器度量:

  • meter : 计量器度量

meter => [ “event_%{field_name}” ]

1
2
3
4
"[event_%{field_name}] [count]" - 事件的总数
"[event_%{field_name}] [rate_1m]" - 1分钟滑动窗口中的每秒事件率
"[event_%{field_name}] [rate_5m]" - 5分钟滑动窗口中的每秒事件率
"[event_%{field_name}] [rate_15m]" - 15分钟滑动窗口中的每秒事件率
  • timer : 计时器度量

timer => [ “thing”, “%{duration}” ]

1
2
3
4
5
6
7
8
9
"[thing] [count]" - 事件的总数
"[thing] [rate_1m]" - 1分钟滑动窗口中的每秒事件率
"[thing] [rate_5m]" - 5分钟滑动窗口中的每秒事件率
"[thing] [rate_15m]" - 15分钟滑动窗口中的每秒事件率
"[thing] [min]" - 此指标的最小值
"[thing] [max]" - 此指标的最大值
"[thing] [stddev]" - 此指标的标准差
"[thing] [mean]" - 这个指标的平均值
"[thing] [pXX]" - 此指标的第XX个百分位数(请参阅百分位数)
1
2
3
4
5
6
7
8
9
10
11
12
metrics {
# 定义metrics计数器数据保存的字段名 field_name的值就是上面Grok表达式解析出来的字段名
meter => [ "event_%{field_name}" ]
# 给该metrics添加tag标签,用于区分metrics
add_tag => [ "metric" ]
# 每隔5分钟统计一次(测试环境可以适当改小)
flush_interval => 300
# 每隔5分钟(flush_interval + 1秒)清空计数器(测试环境可以适当改小)
clear_interval => 301
# 10秒内的message数据才统计,避免延迟
ignore_older_than => 10
}

ruby插件

1
2
3
4
5
6
7
8
9
if "metric" in [tags] {
ruby {
# code是定义metrics的过滤规则,满足什么条件删除metric event日志
# 如果code为空,就是metric event不会被cancel,那么最终metric event会output到elasticsearch/stdout/email,如果不想每个metric event都触发告警事件,就只能通过ruby插件的code添加ruby代码来控制metric event的取消条件
# code => ""
# 如果status_code是500的日志count小于100条,就忽略此事件(即不发送任何消息)。
code => "event.cancel if event['event_500']['count'] < 100"
}
}

email插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 测试环境建议注释掉邮件发送,否则邮箱容易爆炸
email {
# stmp服务器地址
address => "smtpdm.aliyun.com"
# 发件人邮箱地址
username => "service@post.XXX.com"
# 发件人邮箱密码
password => "123456"
# 发件人邮箱
from => "service@post.XXX.com"
# 收件人邮箱
to => "birdben@XXX.com"
# 邮件主题
subject => "告警:风控任务未执行"
# 邮件内容
htmlbody => "告警内容:com.yunyu.birdben.task.RiskTask没有执行"
}

总结一下我所理解的metrics原理:

配置文件定义好metrics之后,Logstash每隔flushinterval设置的时间就会自动创建一个metrics event,可以把metrics event理解成是Logstash自己创建的一条新的日志,这条新的日志有个名称是event%{field_name}的字段(可能是event_A,event_B,fieldname根据Grok表达式解析出来的结果确定的),event%{field_name}的字段下有四个字段

  • “[event_%{field_name}] [count]” - 事件的总数
  • “[event_%{field_name}] [rate_1m]” - 1分钟滑动窗口中的每秒事件率
  • “[event_%{field_name}] [rate_5m]” - 5分钟滑动窗口中的每秒事件率
  • “[event_%{field_name}] [rate_15m]” - 15分钟滑动窗口中的每秒事件率

我们可以根据count(事件的总数)的值,来统计每隔flushinterval时间,我们要统计的event%{field_name}日志的数量。举个例子,如果field_name是status_code,那我们要统计的日志就是event_200,event_302,event_400,event_500等等。那么,event_200.count就是每隔flush_interval时间内,stats_code是200的事件个数,其他同理。如果metrics event被保存到ES索引中,那么查看到的ES结果就会类似下面的结构。

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
"_source": {
"@version": "1",
"@timestamp": "2017-02-15T11:06:37.402Z",
"message": "hadoop1",
"evnet_500": {
"count": 17,
"rate_1m": 3.4,
"rate_5m": 3.4,
"rate_15m": 3.4
},
"evnet_302": {
"count": 1074,
"rate_1m": 197.62554026237865,
"rate_5m": 211.24966828088344,
"rate_15m": 213.60997535145182
},
"event_200": {
"count": 10483,
"rate_1m": 982.4,
"rate_5m": 982.4,
"rate_15m": 982.4
},
"tags": [
"metric"
]
}

这里给metrics event添加了一个metric标签,这样方便与其他业务日志区分开,在后续的ruby处理,email发送邮件,存储ES时,都使用了tag中是否包含metric标签来判断,该日志是否为metrics event。如果是metrics event我们才进行ruby处理,进行event的条件过滤。如果是metrics event我们才发送邮件,并且不保存到ES索引中。

这里我建议使用event.count来作为判断依据,而不是使用rate。因为count更适合用于是判断日志的收集数量,而rate更适合用于判断日志的收集速率。

参考文章:

Elasticsearch学习(二)动态mapping使用

本文主要翻译自elastic.co的官方文档

Dynamic Mapping

Elasticsearch要索引文档,不必首先创建索引,定义映射类型并定义字段。你可以直接创建索引文档,索引,类型和字段将自动生成。

1
2
PUT data/counters/1
{ "count": 5 }

创建data索引,counters类型和名为count的字段,count字段的数据类型为long。

自动检测和添加新类型和字段称为dynamic mapping(动态映射)。 动态映射规则可以根据你的目的自定义:

  • default mapping(默认Mapping) : 配置要用于新映射类型的基本映射。
  • Dynamic field mappings(动态字段Mapping) : 动态检测的规则。
  • Dynamic templates(动态模板) : 自定义规则用于为动态添加的字段配置映射。

注意:Index Template允许你为新索引配置default mappings,settings,alias和warmer,无论是自动创建还是显式创建。

禁止根据类型自动创建Mapping

通过将index.mapper.dynamic设置为false,可以禁用根据类型自动创建Mapping,可以通过在config / elasticsearch.yml文件中设置默认值,也可以将每个索引设置为索引设置:

1
2
3
4
PUT /_settings
{
"index.mapper.dynamic":false
}
  • 禁用所有索引的根据类型自动创建Mapping。

无论此设置的值如何,仍然可以在创建索引或使用PUT映射API时显式添加Mapping。

default mapping

对于任何新映射类型,Default Mapping被用做基础的Mapping映射,可以通过向索引添加具有名称default的映射类型来定制,无论是在创建索引时还是在使用PUT映射API时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT my_index
{
"mappings": {
"_default_": {
"_all": {
"enabled": false
}
},
"user": {},
"blogpost": {
"_all": {
"enabled": true
}
}
}
}
  • default mapping将_all字段默认为disabled。
  • user类型从default继承设置。
  • blogpost类型覆盖默认值,并启用_all字段。

虽然default映射可以在创建索引之后更新,但是新默认值将仅影响之后创建的映射类型。

default映射可以与Index Template结合使用,以控制自动创建的索引中的动态创建的类型:

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
PUT _template/logging
{
"template": "logs-*",
"settings": { "number_of_shards": 1 },
"mappings": {
"_default_": {
"_all": {
"enabled": false
},
"dynamic_templates": [
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"type": "string",
"fields": {
"raw": {
"type": "string",
"index": "not_analyzed",
"ignore_above": 256
}
}
}
}
}
]
}
}
}
PUT logs-2015.10.01/event/1
{ "message": "error:16" }
  • logging template将匹配任何以logs-开头的索引
  • 将使用单个主分片创建匹配索引
  • 对于新的根据类型创建的Mapping,默认情况下禁用_all字段
  • String类型的字段将会被创建成analyzed的主字段,并且还有一个not_analyzed的.raw字段

Dynamic templates

Dynamic templates允许你可以自定义Mapping并且应用于动态添加的字段,其基于:

  • 由Elasticsearch检测的datetype,是否满足match_mapping_type
  • 该字段的名称,是否满足matchun_matchmatch_pattern
  • 该字段的完整的带.的路径,是否满足path_matchpath_unmatch

原始字段名{name}和检测到的数据类型{dynamic_type}模板变量可在Mapping格式中用作占位符。

注意:仅当字段包含具体值(不是空值或空数组)时,才会添加动态字段映射。这意味着如果在dynamic_template中使用了null_value选项,它将仅在第一个有具体值字段的文档之后应用。

Dynamic templates被指定为命名对象的数组:

1
2
3
4
5
6
7
8
9
"dynamic_templates": [
{
"my_template_name": {
... match conditions ...
"mapping": { ... }
}
},
...
]
  • my_template_name : 模板名称可以是任何字符串值。
  • match conditions : 匹配条件可以包括以下任何一个:match_mapping_typematchmatch_patternunmatchpath_matchpath_unmatch
  • mapping : 是设置匹配字段应使用的Mapping映射。

Template按顺序处理 - 第一个匹配模板即可。新的模板可以使用PUT映射API附加到列表的末尾。如果新模板与现有模板具有相同的名称,则它将替换旧版本。

match_mapping_type

match_mapping_type匹配由动态字段映射检测到的数据类型,换句话说,Elasticsearch认为字段应该具有的数据类型。只能自动检测以下数据类型:boolean,date,double,long,object,string。 它还接受*匹配所有数据类型。

例如,如果我们要将所有integer字段映射为integer而不是long,并将所有string字段同时analyzed和not_analyzed,则可以使用以下模板:

这里还需要重点看一下Dynamic field Mapping

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
PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"integers": {
"match_mapping_type": "long",
"mapping": {
"type": "integer"
}
}
},
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"type": "string",
"fields": {
"raw": {
"type": "string",
"index": "not_analyzed",
"ignore_above": 256
}
}
}
}
}
]
}
}
}
PUT my_index/my_type/1
{
"my_integer": 5,
"my_string": "Some string"
}
  • my_integer字段映射为integer。
  • my_string字段映射为analyzed的字符串,和一个not_analyzed的.raw字段。

match和unmatch

match参数使用匹配字段名称的模式,而unmatch使用模式排除匹配的字段。

以下示例匹配名称以long_开头的所有字符串字段(除去了以_text结尾的字符串),并将它们映射为long字段:

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
PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"longs_as_strings": {
"match_mapping_type": "string",
"match": "long_*",
"unmatch": "*_text",
"mapping": {
"type": "long"
}
}
}
]
}
}
}
PUT my_index/my_type/1
{
"long_num": "5",
"long_text": "foo"
}
  • long_num字段映射为long类型。
  • long_text字段使用default string mapping。

match_pattern

match_pattern参数调整match参数的行为,以便它支持对字段名称进行完全Java正则表达式匹配,而不是简单的通配符,例如:

1
2
"match_pattern": "regex",
"match": "^profit_\d+$"

path_match和path_unmatch

path_match和path_unmatch参数以与match和unmatch相同的方式工作,但在字段的完整的带.的路径上操作,而不仅仅是最终名称。some_object.*.some_field。

此示例将name对象中的任何字段的值复制到顶级full_name字段(middle字段除外):

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
PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"full_name": {
"path_match": "name.*",
"path_unmatch": "*.middle",
"mapping": {
"type": "string",
"copy_to": "full_name"
}
}
}
]
}
}
}
PUT my_index/my_type/1
{
"name": {
"first": "Alice",
"middle": "Mary",
"last": "White"
}
}

{name}和{dynamic_type}

在Mapping中{name}和{dynamic_type}占位符将替换为字段名称和检测到的动态类型。 以下示例将所有字符串字段设置为使用与字段名称相同的分析器,并对所有non-string字段禁用doc_values:

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
PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"named_analyzers": {
"match_mapping_type": "string",
"match": "*",
"mapping": {
"type": "string",
"analyzer": "{name}"
}
}
},
{
"no_doc_values": {
"match_mapping_type":"*",
"mapping": {
"type": "{dynamic_type}",
"doc_values": false
}
}
}
]
}
}
}
PUT my_index/my_type/1
{
"english": "Some English text",
"count": 5
}
  • english字段映射为english analyzer的string字段。
  • count字段映射为禁用doc_values的long字段。

参考文章:

JVM常用命令学习(二)jstat的使用

Java环境说明

注意:不同版本的JDK可能略有差异

1
2
3
4
$ java -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)

jstat命令

jstat(Java Virtual Machine Statistics Monitoring Tool)。jstat用于监控基于HotSpot的JVM,对其堆的使用情况进行实时的命令行的统计,使用jstat我们可以对指定的JVM做如下监控:

  • 类的加载及卸载情况
  • 查看新生代、老生代及持久代的容量及使用情况
  • 查看新生代、老生代及持久代的垃圾收集情况,包括垃圾回收的次数及垃圾回收所占用的时间
  • 查看新生代中Eden区及Survior区中容量及分配情况等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ jstat -help
Usage: jstat -help|-options
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
Definitions:
<option> An option reported by the -options option
<vmid> Virtual Machine Identifier. A vmid takes the following form:
<lvmid>[@<hostname>[:<port>]]
Where <lvmid> is the local vm identifier for the target
Java virtual machine, typically a process id; <hostname> is
the name of the host running the target Java virtual machine;
and <port> is the port number for the rmiregistry on the
target host. See the jvmstat documentation for a more complete
description of the Virtual Machine Identifier.
<lines> Number of samples between header lines.
<interval> Sampling interval. The following forms are allowed:
<n>["ms"|"s"]
Where <n> is an integer and the suffix specifies the units as
milliseconds("ms") or seconds("s"). The default units are "ms".
<count> Number of samples to take before terminating.
-J<flag> Pass <flag> directly to the runtime system.

通过help提示可以看出基本的命令格式

  • jstat [option] : 根据jstat统计的维度不同,可以使用如下表中的选项进行不同维度的统计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[option]参数:
- class : 用于查看类加载情况的统计
- compiler : 用于查看HotSpot中即时编译器编译情况的统计
- gc : 用于查看JVM中堆的垃圾收集情况的统计
- gccapacity : 用于查看新生代、老生代及持久代的存储容量情况
- gccause : 用于查看垃圾收集的统计情况(这个和-gcutil选项一样),如果有发生垃圾收集,它还会显示最后一次及当前正在发生垃圾收集的原因。
- gcnew : 用于查看新生代垃圾收集的情况
- gcnewcapacity : 用于查看新生代的存储容量情况
- gcold : 用于查看老生代及持久代发生GC的情况
- gcoldcapacity : 用于查看老生代的容量
- gcpermcapacity : 用于查看持久代的容量
- gcutil : 用于查看新生代、老生代及持代垃圾收集的情况
- printcompilation : HotSpot编译方法的统计
- h n : 用于指定每隔几行就输出列头,如果不指定,默认是只在第一行出现列头。
- J javaOption : 用于将给定的javaOption传给java应用程序加载器,例如,“-J-Xms48m”将把启动内存设置为48M。如果想查看可以传递哪些选项到应用程序加载器中
- t n : 用于在输出内容的第一列显示时间戳,这个时间戳代表的时JVM开始启动到现在的时间(注:在IBM JDK5中是没有这个选项的)。
- vmid : VM的进程号,即当前运行的java进程号。
- interval : 间隔时间,单位可以是秒或者毫秒,通过指定s或ms确定,默认单位为毫秒。
- count : 打印次数,如果缺省则打印无数次。

-class : 类加载情况的统计

列名 说明
Loaded 加载了的类的数量
Bytes 加载了的类的大小,单为Kb
Unloaded 卸载了的类的数量
Bytes 卸载了的类的大小,单为Kb
Time 花在类的加载及卸载的时间

-compiler : HotSpot中即时编译器编译情况的统计

列名 说明
Compiled 编译任务执行的次数
Failed 编译任务执行失败的次数
Invalid 编译任务非法执行的次数
Time 执行编译花费的时间
FailedType 最后一次编译失败的编译类型
FailedMethod 最后一次编译失败的类名及方法名

-gc : JVM中堆的垃圾收集情况的统计

列名 说明
S0C 新生代中Survivor space中S0当前容量的大小(KB)
S1C 新生代中Survivor space中S1当前容量的大小(KB)
S0U 新生代中Survivor space中S0容量使用的大小(KB)
S1U 新生代中Survivor space中S1容量使用的大小(KB)
EC Eden space当前容量的大小(KB)
EU Eden space容量使用的大小(KB)
OC Old space当前容量的大小(KB)
OU Old space使用容量的大小(KB)
PC Permanent space当前容量的大小(KB)
PU Permanent space使用容量的大小(KB)
YGC 从应用程序启动到采样时发生 Young GC 的次数
YGCT 从应用程序启动到采样时 Young GC 所用的时间(秒)
FGC 从应用程序启动到采样时发生 Full GC 的次数
FGCT 从应用程序启动到采样时 Full GC 所用的时间(秒)
GCT T 从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC

-gccapacity : 新生代、老生代及持久代的存储容量情况

列名 说明
NGCMN 新生代的最小容量大小(KB)
NGCMX 新生代的最大容量大小(KB)
NGC 当前新生代的容量大小(KB)
S0C 当前新生代中survivor space 0的容量大小(KB)
S1C 当前新生代中survivor space 1的容量大小(KB)
EC Eden space当前容量的大小(KB)
OGCMN 老生代的最小容量大小(KB)
OGCMX 老生代的最大容量大小(KB)
OGC 当前老生代的容量大小(KB)
OC 当前老生代的空间容量大小(KB)
PGCMN 持久代的最小容量大小(KB)
PGCMX 持久代的最大容量大小(KB)
PGC 当前持久代的容量大小(KB)
PC 当前持久代的空间容量大小(KB)
YGC 从应用程序启动到采样时发生 Young GC 的次数
FGC 从应用程序启动到采样时发生 Full GC 的次数

-gccause : 用于查看垃圾收集的统计情况,包括最近发生垃圾的原因

这个选项用于查看垃圾收集的统计情况(这个和-gcutil选项一样),如果有发生垃圾收集,它还会显示最后一次及当前正在发生垃圾收集的原因,它比-gcutil会多出最后一次垃圾收集原因以及当前正在发生的垃圾收集的原因。

列名 说明
LGCC 最后一次垃圾收集的原因,可能为“unknown GCCause”、“System.gc()”等
GCC 当前垃圾收集的原因

-gcnew : 新生代垃圾收集的情况

列名 说明
S0C 当前新生代中survivor space 0的容量大小(KB)
S1C 当前新生代中survivor space 1的容量大小(KB)
S0U S0已经使用的大小(KB)
S1U S1已经使用的大小(KB)
TT Tenuring threshold,要了解这个参数,我们需要了解一点Java内存对象的结构,在Sun JVM中,(除了数组之外的)对象都有两个机器字(words)的头部。第一个字中包含这个对象的标示哈希码以及其他一些类似锁状态和等标识信息,第二个字中包含一个指向对象的类的引用,其中第二个字节就会被垃圾收集算法使用到。在新生代中做垃圾收集的时候,每次复制一个对象后,将增加这个对象的收集计数,当一个对象在新生代中被复制了一定次数后,该算法即判定该对象是长周期的对象,把他移动到老生代,这个阈值叫着tenuring threshold。这个阈值用于表示某个/些在执行批定次数youngGC后还活着的对象,即使此时新生的的Survior没有满,也同样被认为是长周期对象,将会被移到老生代中。
MTT Maximum tenuring threshold,用于表示TT的最大值。
DSS Desired survivor size (KB).可以参与这里:http://blog.csdn.net/yangjun2/article/details/6542357
EC Eden space当前容量的大小(KB)
EU Eden space已经使用的大小(KB)
YGC 从应用程序启动到采样时发生 Young GC 的次数
YGCT 从应用程序启动到采样时 Young GC 所用的时间(单位秒)

-gcnewcapacity : 新生代的存储容量情况

列名 说明
NGCMN 新生代的最小容量大小(KB)
NGCMX 新生代的最大容量大小(KB)
NGC 当前新生代的容量大小(KB)
S0CMX 新生代中SO的最大容量大小(KB)
S0C 当前新生代中SO的容量大小(KB)
S1CMX 新生代中S1的最大容量大小(KB)
S1C 当前新生代中S1的容量大小(KB)
ECMX 新生代中Eden的最大容量大小(KB)
EC 当前新生代中Eden的容量大小(KB)
YGC 从应用程序启动到采样时发生 Young GC 的次数
FGC 从应用程序启动到采样时发生 Full GC 的次数

-gcold : 老生代及持久代发生GC的情况

列名 说明
PC 当前持久代容量的大小(KB)
PU 持久代使用容量的大小(KB)
OC 当前老年代容量的大小(KB)
OU 老年代使用容量的大小(KB)
YGC 从应用程序启动到采样时发生 Young GC 的次数
FGC 从应用程序启动到采样时发生 Full GC 的次数
FGCT 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT 从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC

-gcoldcapacity : 老生代的存储容量情况

列名 说明
OGCMN 老生代的最小容量大小(KB)
OGCMX 老生代的最大容量大小(KB)
OGC 当前老生代的容量大小(KB)
OC 当前新生代的空间容量大小(KB)
YGC 从应用程序启动到采样时发生 Young GC 的次数
FGC 从应用程序启动到采样时发生 Full GC 的次数
FGCT 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT 从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC

-gcpermcapacity : 持久代的存储容量情况

列名 说明
PGCMN 持久代的最小容量大小(KB)
PGCMX 持久代的最大容量大小(KB)
PGC 当前持久代的容量大小(KB)
PC 当前持久代的空间容量大小(KB)
YGC 从应用程序启动到采样时发生 Young GC 的次数
FGC 从应用程序启动到采样时发生 Full GC 的次数
FGCT 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT 从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC

-gcutil : 新生代、老生代及持代垃圾收集的情况

列名 说明
S0 Heap上的 Survivor space 0 区已使用空间的百分比
S1 Heap上的 Survivor space 1 区已使用空间的百分比
E Heap上的 Eden space 区已使用空间的百分比
O Heap上的 Old space 区已使用空间的百分比
P Perm space 区已使用空间的百分比
YGC 从应用程序启动到采样时发生 Young GC 的次数
YGCT 从应用程序启动到采样时 Young GC 所用的时间(单位秒)
FGC 从应用程序启动到采样时发生 Full GC 的次数
FGCT 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT 从应用程序启动到采样时用于垃圾回收的总时间(单位秒),它的值等于YGC+FGC

-printcompilation : HotSpot编译方法的统计

列名 说明
Compiled 编译任务执行的次数
Size 方法的字节码所占的字节数
Type 编译类型
Method 指定确定被编译方法的类名及方法名,类名中使名“/”而不是“.”做为命名分隔符,方法名是被指定的类中的方法,这两个字段的格式是由HotSpot中的“-XX:+PrintComplation”选项确定的。
1
2
3
4
5
6
7
8
9
10
11
$ jstat -gc 22549
S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
2560.0 512.0 0.0 384.0 8704.0 4758.8 11264.0 1326.3 21504.0 8845.8 7 0.014 2 0.061 0.075
$ jstat -class 22549
Loaded Bytes Unloaded Bytes Time
1543 2945.3 3 4.9 0.30
$ jstat -compiler 22549
Compiled Failed Invalid Time FailedType FailedMethod
235 0 0 0.70 0

参考文章: