本文档介绍了构建高效镜像的优秀实践和方法。
10年积累的成都网站设计、网站制作经验,可以快速应对客户对网站的新想法和需求。提供各种问题对应的解决方案。让选择我们的客户得到更好、更有力的网络服务。我虽然不认识你,你也不认识我。但先做网站设计后付款的网站建设流程,更有湖北免费网站建设让你可以放心的选择与我们合作。
Docker通过从Dockerfile(按顺序包含构建给定镜像所需的所有命令的文本文件)读取命令来自动构建镜像。Dockerfile遵循特定的格式和一组命令,您可以在Dockerfile reference中找到这些命令。
Docker镜像由只读层组成,每个只读层表示Dockerfile指令。这些层被堆叠起来,每一层都是前一层变化的增量。考虑一下这个Dockerfile:
- FROM ubuntu:18.04
- COPY . /app
- RUN make /app
- CMD python /app/app.py
每一个指令会创建一个层:
当您运行一个镜像并生成一个容器时,您将在底层之上添加一个新的可写层("容器层")。对正在运行的容器所做的所有更改,例如写入新文件、修改现有文件和删除文件,都被写入这个可写容器层。
创建临时容器
Dockerfile定义的镜像应该生成尽可能"短暂"的容器。所谓"临时性",是指容器可以停止和销毁,然后用绝对最小的设置和配置重新构建和替换。
理解构建上下文
当您发出docker构建命令时,当前工作目录称为构建上下文。默认情况下, Dockerfile在当前目录,但是您可以使用file标志(-f)指定一个不同的位置。无论Dockerfile实际位于何处,当前目录中文件和目录的所有递归内容都作为构建上下文发送到Docker守护进程。
构建上下文:
为构建上下文创建一个目录并将cd放入其中。将"hello"写入一个名为hello的文本文件中,并创建一个运行cat的Dockerfile。从构建上下文中构建镜像(.):
- mkdir myproject && cd myproject
- echo "hello" > hello
- echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
- docker build -t helloapp:v1 .
将Dockerfile和hello移到单独的目录中,并构建镜像的第二个版本(不依赖于上一个构建的缓存)。使用-f指向Dockerfile并指定构建上下文的目录:
- mkdir -p dockerfiles context
- mv Dockerfile dockerfiles && mv hello context
- docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context
无意中包含了构建镜像所不需要的文件,会导致构建上下文和镜像大小变大。这可以增加构建镜像的时间、拖放镜像的时间和容器运行时大小。要查看构建上下文的大小,请在构建Dockerfile时查看类似这样的消息:
- Sending build context to Docker daemon 187.8MB
通过stdin使用Dockerfile 管道
Docker能够通过使用本地或远程构建上下文通过stdin管道传输Dockerfile来构建镜像。通过stdin管道传输Dockerfile对于执行一次性构建非常有用,不需要将Dockerfile写入磁盘,或者在生成Dockerfile的情况下,不应该在生成后保存Dockerfile。
为了方便起见,本节中的示例使用here文档【http://tldp.org/LDP/abs/html/here-docs.html】,但是可以使用在stdin上提供Dockerfile的任何方法。
例如: 下面的命令是等价的:
- echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
- docker build -<
- FROM busybox
- RUN echo "hello world"
- EOF
您可以用您喜欢的方法或者最适合您用例的方法来替代这些例子。
使用STDIN中的DOCKERFILE构建镜像,而不发送构建上下文
使用此语法可以从stdin中用Dockerfile构建映像,而不需要发送额外的文件作为构建上下文。连字符(-)占据路径的位置,指示Docker从stdin而不是目录中读取构建上下文(其中只包含Dockerfile):
- docker build [OPTIONS] –
下面的示例使用通过stdin传递的Dockerfile构建一个镜像。没有文件作为构建上下文发送到守护进程。
- docker build -t myimage:latest -<
- FROM busybox
- RUN echo "hello world"
- EOF
在Dockerfile不需要将文件复制到镜像中的情况下,省略构建上下文是非常有用的,并且可以提高构建速度,因为没有文件被发送到守护进程。
注意:如果使用这种语法,尝试构建使用COPY或ADD的Dockerfile将会失败。下面的例子说明了这一点:
- # create a directory to work in
- mkdir example
- cd example
- # create an example file
- touch somefile.txt
- docker build -t myimage:latest -<
- FROM busybox
- COPY somefile.txt .
- RUN cat /somefile.txt
- EOF
- # observe that the build fails
- ...
- Step 2/3 : COPY somefile.txt .
- COPY failed: stat /var/lib/docker/tmp/docker-builder249218248/somefile.txt: no such file or directory
使用STDIN中的DOCKERFILE从本地构建上下文构建
使用此语法可以使用本地文件系统上的文件构建映像,但要使用stdin中的Dockerfile。语法使用(-f或--file)选项指定要使用的Dockerfile,使用连字符(-)作为文件名,指示Docker从stdin中读取Dockerfile:
- docker build [OPTIONS] -f- PATH
下面这个例子我们用当前目录作为构建上下文,并且构建镜像用到的Dockerfile是通过stdin传进去的。例子在这里【http://tldp.org/LDP/abs/html/here-docs.html】
- # create a directory to work in
- mkdir example
- cd example
- # create an example file
- touch somefile.txt
- # build an image using the current directory as context, and a Dockerfile passed through stdin
- docker build -t myimage:latest -f- . <
- FROM busybox
- COPY somefile.txt .
- RUN cat /somefile.txt
- EOF
使用STDIN中的DOCKERFILE从远程构建上下文构建
使用此语法,使用来自远程git存储库的文件(使用来自stdin的Dockerfile)构建一个镜像。语法使用(-f或--file)选项指定要使用的Dockerfile,使用连字符(-)作为文件名,指示Docker从stdin中读取Dockerfile:
- docker build [OPTIONS] -f- PATH
当您希望从不包含Dockerfile的存储库构建镜像,或者希望使用自定义Dockerfile构建镜像,而不需要维护存储库的分支时,这种语法非常有用。
下面的示例使用来自stdin的Dockerfile构建一个镜像,并添加hello.c文件从git 仓里库【https://github.com/docker-library/hello-world】
- docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <
- FROM busybox
- COPY hello.c .
- EOF
- Note:
当使用远程Git存储库作为构建上下文构建镜像时,Docker在本地 执行存储库的Git clone,并将这些文件作为构建上下文发送给守护进程。该特性要求git安装在运行docker构建命令的主机上。
使用.dockerignore忽略不需要的文件
要排除与构建不相关的文件(不需要调整资源库),请使用.dockerignore文件。该文件支持类似于.gitignore文件的排除模式。 更多信息请查看【https://docs.docker.com/engine/reference/builder/#dockerignore-file】
使用多级构建
多阶段构建允许您大幅度减小最终映像的大小,而不必费力地减少中间层和文件的数量。
因为镜像是在构建过程的最后阶段构建的,所以可以通过利用构建缓存最小化镜像层。【https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache】
例如,如果您的构建包含多个层,您可以将它们排序从更改频率较低的层(以确保构建缓存可重用)到更改频率较高的层:
下面是一个构建golang应用的Dockerfile 文件:
- FROM golang:1.11-alpine AS build
- # Install tools required for project
- # Run `docker build --no-cache .` to update dependencies
- RUN apk add --no-cache git
- RUN go get github.com/golang/dep/cmd/dep
- # List project dependencies with Gopkg.toml and Gopkg.lock
- # These layers are only re-built when Gopkg files are updated
- COPY Gopkg.lock Gopkg.toml /go/src/project/
- WORKDIR /go/src/project/
- # Install library dependencies
- RUN dep ensure -vendor-only
- # Copy the entire project and build it
- # This layer is rebuilt when a file changes in the project directory
- COPY . /go/src/project/
- RUN go build -o /bin/project
- # This results in a single layer image
- FROM scratch
- COPY --from=build /bin/project /bin/project
- ENTRYPOINT ["/bin/project"]
- CMD ["--help"]
不安装不必要的包
为了减少复杂、依赖、文件尺寸和构建时间,避免安装额外的和不需要的包。一个高水准的Dockerfile必须要注意这些细节。
解耦
每个容器应该只有一个关注点。将应用程序解耦到多个容器可以更容易地水平伸缩和重用容器。例如,web应用程序栈可能由三个独立的容器组成,每个容器都有自己独特的镜像,以解耦的方式管理web应用程序、数据库和内存缓存。
限制每个容器只运行一个进程是一个很好的经验法则。但是,这并不准确。因为很多应用都会有很多进程。比如,Celery就会有很多worker进程。Apache每个request就会有一个进程。容器自己也有init进程。
所以,用你的严谨和专业来保持容器尽可能的干净和模块化。如果容器彼此依赖,可以使用Docker容器网络来确保这些容器能够通信。
保存最小数量的层
在老一点的docker版本中,保持层数的最少是非常重要的,因为要保证性能。
为了减少这样的限制,增加了一下的特性:
命令行参数排序
只要方便,可以通过对多行参数进行字母数字排序来简化后面的更改。这有助于避免包的重复,并使列表更容易更新。这也使得PRs更容易阅读和审查。在反斜杠(\)之前添加空格也有帮助。
下面是一个参数排列的例子:
- RUN apt-get update && apt-get install -y \
- bzr \
- cvs \
- git \
- mercurial \
- subversion
利用构建缓存
在构建映像时,Docker逐步读取 Dockerfile中的指令,并且按照顺序执行。在检查每条指令时,Docker会在缓存中查找可以重用的现有镜像,而不是创建一个新的(重复的)镜像。
如果,你就是不想用cache,可以使用—no-cache=true来关闭在执行docker build的时候。当然,如果你开启了cacha,docker 在构建是找到缓存,如果没有匹配到,就创建新的镜像。 Docker遵循的基本规则如下:
一旦缓存失效,所有后续的Dockerfile命令都会生成新的镜像,而缓存则不被使用。
这些建议旨在帮助您创建一个高效且可维护的Dockerfile。
FROM
只要可能,使用当前的官方镜像作为你的镜像的基础镜像。我们推荐Alpine镜像【https://hub.docker.com/_/alpine/】,因为编写这个镜像是非常严格的,并且很小(目前小于5 MB),但仍然是一个完整的Linux发行版。
LABEL
您可以将标签添加到镜像中,以帮助按项目组织镜像、记录许可信息、帮助实现自动化或出于其他原因。对于每个标签,用LABEL标记开始,用一个或者多个键值对 。下面的示例显示了不同的可接受格式。解释性注释是内联的。
必须引用带空格的字符串,否则必须转义空格。内部引号字符(")也必须转义。
- # Set one or more individual labels
- LABEL com.example.version="0.0.1-beta"
- LABEL vendor1="ACME Incorporated"
- LABEL vendor2=ZENITH\ Incorporated
- LABEL com.example.release-date="2015-02-12"
- LABEL com.example.version.is-production=""
一个镜像可以有多个标签。在Docker 1.10之前,建议将所有标签合并到一个标签指令中,以防止创建额外的层。这不再需要,但是仍然支持组合标签。
- # Set multiple labels on one line
- LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
上面的这个例子还可以写成下面这样:
- # Set multiple labels at once, using line-continuation characters to break long lines
- LABEL vendor=ACME\ Incorporated \
- com.example.is-beta= \
- com.example.is-production="" \
- com.example.version="0.0.1-beta" \
- com.example.release-date="2015-02-12"
- RUN
使用反斜杠(\) 来分隔独立的命令行可以使RUN命令更有可读性、易于维护。
APT-GET
Apt-get 命令是很多Docker经常使用的命令。因为,他是安装各种包必须使用的命令。
避免运行apt-get升级和distl -upgrade,因为来自父镜像的许多"基本"包无法在非特权容器中升级。如果父镜像中包含的包过期了,请联系它的维护人员。如果您知道有一个特定的包foo需要更新,那么使用apt-get install -y foo自动更新。
始终将RUN apt-get update与apt-get install组合在同一个RUN语句中。例如:
- RUN apt-get update && apt-get install -y \
- package-bar \
- package-baz \
- package-foo
在RUN语句中单独使用apt-get update会导致缓存问题,随后的apt-get安装指令会失败。例如,假设您有一个Dockerfile:
- FROM ubuntu:18.04
- RUN apt-get update
- RUN apt-get install -y curl
当构建完镜像后,所有的层都已经被缓存了,假设之后你修改了apt-get install 增加了其他的包:
- FROM ubuntu:18.04
- RUN apt-get update
- RUN apt-get install -y curl nginx
Docker将初始指令和修改后的指令视为相同的,并重用前面步骤中的缓存。因此,apt-get更新不会执行,因为构建使用缓存的版本。由于apt-get更新没有运行,您的构建可能会得到一个过时版本的curl和nginx包。
使用RUN apt-get update && apt-get install -y确保您的Dockerfile安装最新的包版本,而无需进一步编码或手动干预。这种技术称为"缓存破坏"。还可以通过指定包版本来实现缓存崩溃。这就是所谓的版本固定,例如:
- RUN apt-get update && apt-get install -y \
- package-bar \
- package-baz \
- package-foo=1.3.*
版本固定强制构建以检索特定版本,而不管缓存中的内容是什么。这种技术还可以减少由于所需包中的意外更改而导致的故障。
下面是一个格式良好的运行指令,演示了所有apt-get 的优秀实践。
- 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/*
s3cmd指定了一个新的版本。如果之前的镜像安装的是一个旧的版本。apt-get update 会导致缓存失效,从而安装新的版本。
在这样的条件下,当你清除apt缓存并且移除/var/lib/apt/lists 目录,来减小文件尺寸。当RUN 声明以apt-get update开始,在执行apt-get install的时候,缓存依然会被刷新。
注:
Debian和ubuntu的官方镜像会自动运行apt-get clecn命令。所以不需要显示调用。
使用管道
有些运行命令依赖于使用管道字符(|)将一个命令的输出管道到另一个命令的能力,如下例所示:
- RUN wget -O - https://some.site | wc -l > /number
Docker使用/bin/sh -c解释器执行这些命令,解释器只计算管道中最后一个操作的退出代码来确定是否成功。在上面的示例中,只要wc -l命令成功,即使wget命令失败,这个构建步骤就会成功并生成一个新映像。
如果您希望命令在管道中的任何阶段由于错误而失败,请预先设置-o pipefail &&,以确保意外错误防止构建意外成功。例如:
- RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
注:
不是所有的shell都支持 –o pipfail 选项
在基于debian的镜像上使用dash shell的情况下,可以考虑使用exec形式的RUN显式地选择一个支持pipefail选项的shell。例如:
- RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
CMD
CMD指令应该用于运行镜像所包含的软件,以及任何参数。CMD几乎总是以CMD["executable"、"param1"、"param2"…]的形式使用。因此,如果镜像是用于服务的,比如Apache和Rails,您将运行类似CMD ["apache2","-DFOREGROUND "]的东西。实际上,对于任何基于服务的镜像,都推荐使用这种形式的指令。
在大多数其他情况下,应该为CMD提供一个交互式shell,如bash、python和perl。例如,CMD ["perl"、"-de0"], CMD ("python"),或CMD ("php","-a")。使用这种形式意味着,当您执行像docker run - python这样的东西时,您将被放入一个可用的shell中,准备就绪。CMD应该很少与ENTRYPOINT一起以CMD ["param", "param"]的方式使用,除非您和您的预期用户已经非常熟悉ENTRYPOINT的工作方式。
EXPOSE
EXPOSE指令指示容器监听连接的端口。因此,您应该为您的应用程序使用公共的、传统的端口。例如,包含Apache web服务器的镜像使用 80端口,而包含MongoDB的映像将使用 27017 端口,以此类推。
对于外部访问,用户可以使用一个标志执行docker run,该标志指示如何将指定的端口映射到他们选择的端口。对于容器链接,Docker为从接收容器返回到源容器的路径提供了环境变量(即MYSQL_PORT_3306_TCP)。
ENV
为了使新软件更容易运行,可以使用ENV更新容器安装的软件的PATH环境变量。例如,ENV PATH /usr/local/nginx/bin:$PATH确保CMD ["nginx"]正常工作。
ENV指令对于提供特定于您希望封装的服务的所需环境变量也很有用,比如Postgres的PGDATA。
最后,ENV还可以用来设置常用的版本号,以便更容易维护版本,如下例所示:
- 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指令,从而自动地在容器中神奇地弹出软件版本。
每个ENV行创建一个新的中间层,就像RUN命令一样。这意味着,即使您在未来的层中取消了环境变量的设置,它仍然保留在这个层中,并且它的值可以被转储。您可以通过创建一个Dockerfile(如下所示)来测试它,然后构建它。
- FROM alpine
- ENV ADMIN_USER="mark"
- RUN echo $ADMIN_USER > ./mark
- RUN unset ADMIN_USER
- $ docker run --rm test sh -c 'echo $ADMIN_USER'
- mark
为了防止这种情况发生,并真正取消对环境变量的设置,可以使用一个带有shell命令的RUN命令,在一个单层中设置、使用和取消对变量的设置。你可以用;和& &。如果使用第二种方法,并且其中一个命令失败,docker构建也会失败。这通常是个好主意。使用\作为Linux Dockerfiles的行延续字符可以提高可读性。您还可以将所有命令放入shell脚本中,并让RUN命令运行该shell脚本。
- FROM alpine
- RUN export ADMIN_USER="mark" \
- && echo $ADMIN_USER > ./mark \
- && unset ADMIN_USER
- CMD sh
- docker run --rm test sh -c 'echo $ADMIN_USER'
ADD 或者COPY
虽然ADD和COPY在功能上是相似的,但是一般来说,COPY是首选的。这是因为它比ADD更透明,COPY只支持将本地文件基本复制到容器中,而ADD的一些特性(比如只本地的tar提取和远程URL支持)不是很有效。因此,ADD的最佳用途是将本地tar文件自动提取到映像中,如ADD rootfs.tar.xz / 。
如果有多个Dockerfile步骤使用与上下文不同的文件,请分别复制它们,而不是一次全部复制。这确保只有在特定需要的文件发生更改时,每个步骤的构建缓存才会失效(强制重新运行该步骤)。
例如:
- COPY requirements.txt /tmp/
- RUN pip install --requirement /tmp/requirements.txt
- COPY . /tmp/
将COPY . /tmp/放到RUN前面,会使缓存失效???
由于镜像的大小很重要,因此强烈反对使用ADD从远程url获取包;您应该使用curl或wget来代替。这样,你可以删除你不再需要的文件后,他们已经被提取出来,你不需要添加另一层在您的镜像。例如,你应该避免做以下事情:
- 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
- 我们用下面的命令取代:
- 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
如果不需要提取tar (文件、目录)的话,应该始终使用COPY。
ENTRYPOINT
ENTRYPOINT的最佳用法是设置镜像的主命令,允许像运行该命令一样运行该镜像(然后使用CMD作为默认标志)。
让我们从命令行工具s3cmd的镜像示例开始:
ENTRYPOINT ["s3cmd"]
CMD ["--help"]
现在,这个镜像可以像这样运行:
- $ docker run s3cmd
也可以传参数执行:
- $ docker run s3cmd ls s3://mybucket
这很有用,因为镜像的名字可以同时作为对二进制文件的引用,如上面的命令所示。
ENTRYPOINT指令也可以与helper脚本结合使用,允许它以类似于上面命令的方式运行,即使在启动工具时可能需要不止一个步骤。
例如,Postgres官方镜像使用以下脚本作为其入口点:
- #!/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 "$@"
注:
设置应用的PID为1,这样,PG会结构linux的任何信号。
helper脚本被复制到容器中,并在容器开始时通过ENTRYPOINT运行:
- COPY ./docker-entrypoint.sh /
- ENTRYPOINT ["/docker-entrypoint.sh"]
- CMD ["postgres"]
这个脚本允许用户以多种方式与Postgres交互。
它可以简单地启动Postgres:
- $ docker run postgres
或者,它可以用来运行Postgres并将参数传递给服务器:
- $ docker run postgres postgres –help
最后,它也可以用来启动一个完全不同的工具,如Bash:
- $ docker run --rm -it postgres bash
VOLUME
卷指令应该用于公开由docker容器创建的任何数据库存储区域、配置存储或文件/文件夹。强烈建议对镜像的任何可变和或用户可服务的部分使用VOLUME。
USER
如果服务可以在没有特权的情况下运行,请使用USER将其更改为非root用户。首先在Dockerfile中创建用户和组,使用类似于RUN groupadd -r postgres && useradd——no-log-init -r -g postgres postgres的东西。
镜像中的用户和组被分配一个不确定的UID/GID,因为"下一个"UID/GID被分配,而不考虑镜像的重建。因此,如果它是必须要使用的,您应该分配一个显式的UID/GID。
由于Go archive/tar包在处理稀疏文件时存在一个未解决的bug,试图在Docker容器中创建一个UID非常大的用户可能会导致磁盘耗尽,因为容器层中的/var/log/faillog中填充了NULL(\0)字符。一个解决方案是将——no-log-init标志传递给useradd。Debian/Ubuntu adduser包装器不支持这个标志。
避免安装或使用sudo,因为它具有不可预知的TTY和信号转发行为,可能会导致问题。如果您绝对需要类似于sudo的功能,比如将守护进程初始化为根进程,但以非根进程的形式运行它,那么可以考虑使用"gosu"。
最后,为了减少层次和复杂性,避免频繁地来回切换用户。
WORKER
为了清晰和可靠,您应该始终为您的WORKDIR使用绝对路径。此外,您应该使用WORKDIR,而不是像RUN cd…&& do-something这样的指令,这些指令很难阅读、排除故障和维护。
ONBUILD
ONBUILD命令在当前Dockerfile构建完成后执行。ONBUILD在从当前镜像派生的任何子镜像中执行。将ONBUILD命令看作是父Dockerfile给子Dockerfile的一条指令。
Docker构建在子Dockerfile中的任何命令之前执行ONBUILD命令。
ONBUILD对于将从给定镜像构建的镜像非常有用。例如,您可以对一个语言堆栈镜像使用ONBUILD,该镜像可以在Dockerfile中构建用该语言编写的任意用户软件,正如您可以在Ruby的ONBUILD变体中看到的那样。
使用ONBUILD构建的镜像应该有一个单独的标记,例如:ruby:1.9-onbuild或ruby:2.0-onbuild。
在ONBUILD中添加或复制时要小心。如果新构建的上下文缺少正在添加的资源,则"onbuild"镜像将灾难性地失败。如上面建议的那样,添加一个单独的标记,通过允许Dockerfile作者做出选择,可以帮助缓解这种情况。
文章题目:编写Dockerfiles的优秀实践
文章URL:http://www.36103.cn/qtweb/news13/22163.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联