Docker 是时下最为流行的开源容器技术,而 Coding 又是从来以革命者先驱的姿态拥抱新技术的,所以我们也与 Docker 擦出了火花,有爱有恨,有苦涩的过去时光,也有甜蜜的未来规划。
注:本文容器仅仅代表 linux 容器,不包含其他操作系统的相关技术,更不是什么瓶子罐子之类的。:)
容器依赖高版本 linux kernel 提供的如下几种技术实现:
linux namespace 实现容器隔离 例如可以在进程 PID,网络等级别上实现隔离。
namespaces机制提供一种资源隔离方案。PID,IPC,Network等系统资源不再是全局性的,而是属于某个特定的namespace。每个namespace下的资源对于其他namespace下的资源都是透明,不可见的。因此在操作系统层面上看,就会出现多个相同pid的进程。
例如,我们可以在 /proc/PID/ns 目录下找到一些 namespace 相关的信息:
关于 namespace 这里就不再做过多介绍,有兴趣的读者可以自行去查找相关资料
想了又想,觉得这个概念还是没啥好说的,因为我觉得你去 docker 的官方网站看一看,或者随便找个搜索引擎搜一搜都比我解释的清楚。这里谈一谈个人认为的 Docker 的一些看法吧。
docker 是一种容器技术,又在容器技术之上创造了很多新的实用的功能和概念,例如 image,registry,layer,RESTful API 等等。docker 的特点是简单能快速上手,目标是要解决 Build,Ship ,Run。这也是 docker 官方的 slogan。
Google 是最早研究容器技术的公司,并在内部有大规模部署,libcontainer 是 docker 最初根据 Google 提出的一些关于容器相关的论文等做出得一个实现库。当然实现不止 docker 一个 ,还有例如 rocket ,以及很多大公司内部的一些实现等。为解决容器技术分裂化,各大技术厂商组建了 opencontainer 联盟,希望能统一容器标准。随后 docker 把 libcontainer 捐献给了 opencontainer联盟。
源码位于: https://github.com/opencontainers/runc 的 libcontainer 目录中
我觉得 docker 优势中最重要的一点,也是 docker 官方的标语 Build, Ship, Run ,简化标准化了应用生命周期流程。
而如果配合编排系统后,还能做到如下几点:
* 使快速的分布式大规模部署成为可能
* 使基础资源和应用分离成为了可能
* 使弹性计算可以做的更好
有了容器,要想在一定规模的硬件计算资源上应用,往往需要做容器编排,例如定义哪些容器在哪些服务器上运行,相互之间的关系是什么,负载均衡,热备等等的一系列操作。时下有如下这些编排软件或框架比较流行,这里做一个简要介绍:
Coding 踏入容器技术的大门最早是演示平台( 基于 CloudFoundry 的 PaaS 系统 ),演示平台依赖容器技术进行应用隔离。但是一开始 Coding 并没有系统中其他功能服务中使用容器技术,随着 DevOps 发展,越来越发觉容器技术是必不可少的一环,是未来发展的方向,开始在其他功能和服务中逐渐使用 docker 来取代一些传统的做法。
Coding 开始使用 Docker 后的一个观念就是 everything in docker。所以我们也把之前在本地跑得程序构建过程移到了 docker 中。我们给每个应用写了一个构建脚本,这个脚本就负责启动一个 docker container 然后在里面把项目构建完成, 得到一个 ready to run 的package ,可能是一个 jar 包,可能是一个 可执行程序,也可能是一个下载完依赖的 文件夹;然后编写一个 Dockerfile 用以描述该应用在什么样的环境下如何运行。直接交付 一个 docker image 给运维
示例 build.sh 文件
VERSION="test"
cur_dir=`pwd`
mkdir -p ./.src/target
cd ../../../CodingBlog
docker run -it -v "`pwd`:/app:rw" -v "$HOME/.gradle:/root/.gradle:rw" java:8-jdk sh -c "cd /app && ./gradlew clean build"
cp ./build/libs/CodingBlog-1.0.jar $cur_dir/.src/target/CodingBlog-1.0.jar
mkdir -p /data/git
cd $cur_dir
echo "$VERSION" > ./.src/.version
这个文件的执行结果就是要在特定的目录下生成特定的 readytorun 的jar 包,好处是执行者只需要安装有 docker 即可执行,不需在额外安装 jdk gradle 等等。注意上面一个小细节:把 .gralde 目录挂载进去,可以将依赖的 jar 包缓存下来,加快下次构建速度。
示例 Dockerfile 文件
```
FROM java:8-jdk
COPY ./.src/target/RepoManager-1.0.jar /app/
RUN useradd -m -u 1000 coding
USER coding
WORKDIR /app
CMD [ "java", "-server", /
"-Xms2048m", "-Xmx2048m","-XX:+UseFastAccessorMethods", /
"-jar", "./CodingBlog-1.0.jar" /
]
```
没有什么特别的就是 java -jar 运行就好了。
显而易见的,利用 docker,我们轻而易举的完成了应用构建的标准化,以及应用交付流程的简化。
另外,我们为了 image 的统一管理,搭建了 docker registry ,目前是 V1版本,方便给生产环境的快速分发。
Coding 在参考调研过目前的一些容器编排系统之后,决定自行写一套简易的符合 Coding 现状的编排系统,其实类似于 compose 或者之前的 fig 的概念,即用配置文件定义容器如何编排,包括容器配置,环境变量等等信息,然后写一个程序读取这个配置文件,再来操作 docker daemon。
我们的配置示例:
job: <
name: "repo-manager-5"
image: "repo-manager:latest"
run_on_host: "git-5"
envs: <
key: "port"
value: "8866"
>
volumes: <
container_path: "/data"
host_path: "/data"
read_only: false
>
>
我们以 job 的形式组织一个应用实例,目前并没有使用容器技术中的诸如 CPU 内存限制等,而且目前 所有的 docker 容器网络都配置为 host 模式,一方面是出于性能考虑,另一方面,也是为了与之前老的系统兼容,方便迁移。
我们写了一个命令行工具,可以用来读取这个文件,并操作 docker daemon。有诸如 start ,stop,restart,update,log,shell,inspect 这些功能。
例如,执行 update 操作,会列出当前的 image 列表,选择后,就可以进行全自动更新。
我们在开发团队内部推行一套 Coding-local 的机制,即使用一个 Vagrant VM,配合 Coding-local 这样一套构建脚本和 Dockerfile,得以让任何一个同事都可以执行很简单的几部操作就可以在一个虚拟机里面运行起整个 Coding 系统,方便不熟悉,却又需要使用的同事也能快速上手开发。
为保持生产环境和开发环境一致性,我们在 coding-local 环境里面也使用 job 这样的方式,以及 build.sh 构建脚本 Dockerfile 这样的方式来支撑整个系统,唯独少了 docker-registry, 因为在 coding-local 里面不存在 image 分发的问题。
* docker 容器在 stdout 有大量内容传输的时候,docker daemon 的内存会疯长,直到 docker daemon 被 OOM kill。
我们的应对办法:
1. 向docker 官方提 issue
2. 分析源码尝试找出原因
3. 放弃使用 stdout 进行数据传输改为网络接口
* docker daemon 在频繁创建启动停止删除容器后,会残留很多垃圾文件,久而久之会导致磁盘 inode 节点被耗尽。该问题目前还没有找到特别好的应对办法
* docker 是推荐单个容器里面只有一个进程的,但是容器技术本质上是没这个限制,然后 如果在 docker 容器里面 fork 很多进程,会在系统中出现很多僵尸进程,最终导致 docker daemon 出现问题。