Docker环境的CI/CD持续集成持续交付

Docker的最大好处之一是能够带来无缝的CI/CD流程;容器是Docker镜像的运行着的只读实例,更新容器仅仅需要更新镜像文件,然后从这个更新过的镜像重新部署容器。还有免费的工具可以监控镜像repository,并且在检测到镜像有更新的时候立即重新部署一个容器。但是,运行容器,创建以及更新镜像,这仍然是手动的。

将CI/CD的概念带入下一个级别是需要创建开发流水线,将软件交付流程里的所有步骤自动化。典型的单服务流水线包括如下基本步骤:

  • 构建 – 初始化构建过程,将源码转变为编译好的artifact,并且打包到Docker镜像里

  • 测试 – 使用支持你的框架的任意测试工具在Docker容器里运行单元测试。

  • 推送 – 将测试过的Docker镜像推送到Docker registry服务,比如Docker Hub上。

  • 部署 -从registry服务下载docker镜像到相应的staging/production Docker环境里。

  • 运行 – 从一个或者多个镜像里初始化容器或者服务

CI/CD的下一个逻辑上的扩展是通过webhook将开发代码repository完全集成到开发流水线上;这样,当事件,比如代码repository里发生代码提交或者merge时,就会自动执行构建-部署流水线流程。有了这样的集成,无论开发人员什么时候提交代码到repository里,几秒钟后,带有开发人员变更的Docker容器就会生成,之后就可以用于实时的集成测试了。

Emerging Technology Partners调研并且验证了当前市场上能够提供最全面的容器为中心的CI/CD特性的解决方案。我们的方案可以提供集成测试所需的完整的环境。每个build里UI测试或者性能测试都是测试的一部分,让大家可以完整测试任意提交或者PR。使用我们的解决方案,开发人员和测试任意可以快速发现regression,在staging前修复这些问题,加速了开发周期,并且节省了时间和精力。

如果你负责内部的应用程序开发团队,并且在测试或者发布频率上挣扎,那么可以给ETP电话,我们可以向你展示容器中心的CI/CD可以如何集成到你的开发流程里。

Jenkins & Docker持续集成实践

前言

持续集成(CI/CD)是一种软件开发实践。用于帮助团队成员频繁、快速的集成,测试他们的工作成果,以尽快发现集成错误。 更频繁、更早的集成意味着更早的发现问题。通过持续集成,及时发现和解决代码故障,提高代码质量,减少故障处理成本等等。

常见持续集成工具

目前持续集成的生态越来越完善,工具也有很多,开源的或商业的。如:

最最流行的,也是使用最多的 Jenkins

有着持续集成DNA的ThoughtWorks GO。理念:”Deployment as pipeline” (华为容器平台应该是基于GO做的二次开发实现)

  • Atlassian工具链之一的Bamboo (数人云应该是基于Banboo实现的CI/CD)

  • 与Gitlab紧密集成的Gitlab CI

  • 专为开源打造的Travis CI,与Github紧密集成

  • 使用 python 语言实现的Buildbot,相信 pythoner 看到会喜欢

  • 我们的选型是 Jenkins,所以我们来看下 Jenkins

Jenkins

Jenkins 特点

  • Jenkins是开源的应用最广泛的持续集成工具,支持CI, CD;

  • Jenkins有很多插件,而且用户也可以自定义插件,可扩展性非常强;

  • Jenkins对Docker支持非常好,有一套完善的Docker插件;

  • Jenkins2.0开始支持Pipeline,一个非常强大的插件,使用基于Groovy的DSL,支持CI/CD流水线;

  • Jenkins 基于 Java 语言开发;

Jenkins 几个概念

  • master 是jenkins安装和运行的地方,它负责解析job脚本,处理任务,调度计算资源;

  • agent 负责处理从master分发的任务;

  • executor 就是执行任务的计算资源,它可以在master或者agent上运行。多个executor也可以合作执行一些任务;

  • job 任务,用来定义具体的构建过程;

  • Groovy 是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。由于其运行在 JVM 上的特性,Groovy 可以使用其他 Java 语言编写的库。Jenkins 用Groovy作为DSL;

  • pipeline 流水线即代码(Pipeline as Code),通过编码而非配置持续集成/持续交付(CI/CD)运行工具的方式定义部署。流水线使得部署是可重现、可重复的;

流水线包括节点(Node)、阶段(Stage)和步骤(Step)。

流水线执行在节点上。节点是Jenkins安装的一部分。流水线通常包含多个阶段。一个阶段包含多个步骤。流水线上手指南可以查看到更多的内容。

node 在Pipeline中的context中,node是 job 运行的地方。 node会给job创建一个工作空间。工作空间就是一个文件目录,这是为了避免跟资源相关的处理互相产生影响。工作空间是node创建的,在node里的所有step都执行完毕后会自动删除。

stage 阶段,stage是一个任务执行过程的独立的并且唯一的逻辑块,pipeline定义在语法上就是由一系列的stage组成的。 每一个stage逻辑都包含一个或多个step。

step 步骤,一个step是整个流程中的一系列事情中的一个独立的任务,step是用来告诉Jenkins如何做。

  • Jenkinfile Jenkins支持创建流水线。它使用一种基于Groovy的流水线领域特定语言(Pipeline DSL)的简单脚。而这些脚本,通常名字叫Jenkinsfile。它定义了一些根据指定参数执行简单或复杂的任务的步骤。流水线创建好后,可以用来构建代码,或者编排从代码提交到交付过程中所需的工作。Jenkins中的Jenkinsfile有点类似Docker中的Dockfile的感觉。

Jenkins 部署

Jenkins组件其实非常少,安装部署也非常简单。 Jenkins按角色就两类: master节点和slave节点。master安装完成后,在控制台中添加节点即可。

下面以Dcoker的部署方式为例说一下Jenkins的部署:

master 节点

查看 docker hub 中 jenkins 的 image

[root@k3128v /home/huomingming]# docker search jenkinsNAME                               DESCRIPTION                                     STARS     OFFICIAL   AUTOMATEDjenkins Official Jenkins Docker image                   2600      [OK]       stephenreed/jenkins-java8-maven-git   Automated build that provides a continuous...   53 

可以看到第一个是官方提供的,所以我们选择这个即可。

拉取 jenkins image

docker pull jenkins

启动 jenkins 容器

jenkins没有数据库,所有数据都是存放在文件中的,首先在本地创建jenkins数据目录,用于保存jenkins的数据 这个目录需要定期的备份,用于容灾(当前jenkins容器所在节点由于不可抗因素无法使用时,可以在新机器上使用备份的数据启动新的jenkins master节点)。

sudo mkdir /var/jenkins
sudo chown 1000:1000 /var/jenkins
sudo docker run -p 8080:8080 -p 50000:50000 -v /var/jenkins:/var/jenkins_home --name my_jenkins -d jenkins

这样jenkins就成功跑起来了。可以直接通过机器的8080端口访问jenkins. 本地的/var/jenkins就相当于容器里jenkins用户的用户主目录,所以要保证该目录的权限为uid为j1000的用户目录。

配置 jenkins

启动完 jenkins master 后,在浏览器中数据输入 http://jenkins_master_ip:8080 登录 Jenkins 控制台进行接下来的安装和配置。 具体图就不贴出来了~~

查看jenkins的版本

java -jar /usr/share/jenkins/jenkins.war --version

slave 节点

安装 java jdk

yun install java-1.8.0-openjdk

创建 jenkins 用户

$ useradd -m jenkins -d /home/jenkins$ passwd jenkins

创建工作目录

mkdir /data/jenkinschown jenkins.jenkins /data/jenkins

添加 jenkins 用户到 docker 用户组

sudo usermod -a -G docker jenkins

配置 ssh 互信,master免密码登陆slave

master 有多种管理 slave 的方式,我们选择SSH方式 在master节点中,切换到jenkins用户 ssh-keygen -t rsa 创建秘钥对 把公钥拷贝到slave节点

scp ~/.ssh/id_rsa.pub jenkins@slave_ip:~/.ssh/authorized_keys

确保在scp前,slave节点根目录下.ssh目录已存在

chmod 700 authorized_keys

使用 jenkins 来构建 docker 是需要安装插件的。那我们需要安装哪些插件呢?

Jenkins 有哪些 Docker 的 Plugins

未分类

是非常丰富的,但并不是我们都能用的上,所以需要根据你使用的环境和平台来选择适合自己的 plugin 安装就可以了。 每个 plugin 都需要适配 Jenkins 的版本,且每个 plugin 也需要依赖一些其它 plugin,上面都已经做了标注,需要配套来用。

这里介绍几个常用的 Docker 插件

Docker Commons Plugin

其基本功能:

  • API for managing Docker image and container fingerprints

  • Credentials and location of Docker Registry

  • Credentials and location of Docker Daemon (aka Docker Remote API)

  • ToolInstallation for Docker CLI clients

  • DockerImageExtractor extension point to get Docker image relations from jobs

  • Simple UI referring related image fingerprints in Docker builds

Docker Plugins

该插件是将 docker 作为 jenkins 的 slave 来使用,来在 docker 容器种完成项目的build,build 完成后该容器 slave 容器就会被销毁。所有的工作都是在 slave 容器内完成。

docker-build-step

该插件在 build 过程种增加了对 Docker 命令的支持。

Docker Pipeline Plugin

基于 Docker Commons Plugin 实现的一些上层的基于 Docker 的 Pipeline 编排

Docker Hub Notification Trigger Plugin

该插件提供了当registry中的image发生变化时触发build新镜像的功能。

CloudBees Docker Build and Publish

该插件提供了通过 Dockerfile 来构建项目并将生成的镜像上传到镜像仓库的功能。

CloudBees Docker Custom Build Environment

This plugin allows the definition of a build environment for a job using a Docker container.

该插件适用于 “自由风格的软件项目”,如图:

未分类

CloudBees Docker Traceability

用于追踪通过jenkins启停的容器的事件

Kubernetes

This plugin allows Jenkins agents to be dynamically provisioned on a Kubernetes cluster. The aim of the Kubernetes plugin is to be able to use a Kubernetes cluster to dynamically provision a Jenkins agent (using Kubernetes scheduling mechanisms to optimize the loads), run a single build, then tear-down that slave.

与 Kubernetes 结合,通过 kubernetes 提供 jenkins 的 slave 节点。利用 kubernetes 的调度功能提供快速的启停 slave 节点执行 build 等任务。目前是0.11版本,稳定性有待验证。

Jenkins 有没有 API?

因为,我们不是直接在 Jenkins 的 Dashbord 来操作, 而是集成到现有平台,所以我们需要使用它的API。

Jenkins的Remote API以REST的形式进行提供。例如,我们搭建的Jenkins站点为http://myjenkins.com:8080。那么,访问http://myjenkins.com:8080/api 即可查看到该站点所有可用的API;

查询操作

未分类

执行一些动作

未分类

例如,我要创建一个 job,名字为 my_job

my_job的配置文件

<?xml version='1.0' encoding='UTF-8'?>
<project>
 <actions/>
 <description></description>
 <keepDependencies>false</keepDependencies>
 <properties/>
 <scm class="hudson.scm.NullSCM"/>
 <canRoam>true</canRoam>
 <disabled>false</disabled>
 <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
 <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
 <triggers class="vector"/>
 <concurrentBuild>false</concurrentBuild>
 <builders/>
 <publishers/>
 <buildWrappers/>
</project>

调用 api 创建 my_job

curl -X POST http://www.xxx.xxx/jenkins/createItem?name=my_job  --user uname:pass --data-binary "my_job_config.xml" -H "Content-Type: application/xml"

然后,你就可以在 Jenkins dashboard 上看到这个 job 了。它的管理页面为http://myjenkins.com:8080/job/my_job。那么我们访问 /my_job/api/ 即可查看到该job可用的API。

更多的 api 介绍可以参考Jenkins的官方wiki,这里只是个引子,在此就不过多进行介绍。

在docker swarm集群中使用HEALTHCHECK解决服务更新不可用的问题

这是我们使用自建 docker swarm 集群后在部署时遇到的一个问题,使用 docker service update 命令更新服务时,

docker service update -d=false --force service_name

在更新的过程中服务有短暂的时间不能访问。

该服务中运行的是 asp.net core web api ,所使用的 Dockerfile 如下:

FROM microsoft/aspnetcore:1.1.2
ARG PROJECT
WORKDIR /app
COPY ${PROJECT}/publish .
RUN echo "dotnet ${PROJECT}.dll --urls http://*:80" > run.sh

通过在服务更新期间在另外一个容器中运行下面的 curl 命令捕捉这个问题:

while true;do curl -sSf -w '%{http_code}' cloud_api/alive;sleep 2;done

service update 期间不能访问所更新的服务时,curl 会出现下面的输出:

curl: (7) Failed to connect to cloud_api port 80: Connection refused
000

怀疑是容器启动后,asp.net core web api 站点没有立即开始工作,dotnet 命令启动站点也需要一定的时间。

针对这个怀疑点,在 Dockcefile 中添加 HEALTHCHECK 指令,这样可以让 docker 在容器启动后对容器内应用进行健康检查,检查通过才将容器投入使用。

HEALTHCHECK --interval=5s --timeout=20s 
   CMD curl -fs localhost/alive || exit 1

注:localhost/alive 是容器内应用实现的一个健康检查 url 。

添加 HEALTHCHECK 后重新构建镜像并部署,执行 service update 命令问题没有出现,搞定!

docker swarm集群搭建及使用shipyard UI管理

一、规划

1、swarm01作为manager节点,swarm02和swarm03作为worker节点。

# cat /etc/hosts
127.0.0.1   localhost
192.168.139.175  swarm01 
192.168.139.176  swarm02 
192.168.139.177  swarm03

2、配置SSH免密登陆

# ssh-keygen -t rsa -P ''
# ssh-copy-id -i .ssh/id_rsa.pub [email protected]
# ssh-copy-id -i .ssh/id_rsa.pub [email protected]

二、安装docker和ansible

1、安装配置ansible

# yum -y install ansible
# cat /etc/ansible/hosts | grep -v ^# | grep -v ^$
[node]
192.168.139.176
192.168.139.177
# sed -i "s/SELINUX=enforcing/SELINUX=disabled" /etc/selinux/config
# ansible node -m copy -a 'src=/etc/selinux/config dest=/etc/selinux/'
# systemctl stop firewalld
# systemctl disable firewalld
# ansible node -a 'systemctl stop firewalld'
# ansible node -a 'systemctl disable firewalld'

注:这里选择关闭防火墙,实际环境中可自行开放端口。

2、安装docker

  • 在manager节点安装docker
# yum install -y yum-utils device-mapper-persistent-data lvm2
# yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# yum list docker-ce --showduplicates | sort -r
# yum -y install docker-ce
# docker --version
Docker version 17.06.0-ce, build 02c1d87
# systemctl start docker
# systemctl status docker
# systemctl enable docker
  • 使用ansible在worker节点安装docker
# ansible node -m copy -a 'src=/etc/yum.repos.d/docker-ce.repo dest=/etc/yum.repos.d/'
# ansible node -m yum -a "state=present name=docker-ce"
# ansible node -a 'docker --version'
192.168.139.173 | SUCCESS | rc=0 >>
Docker version 17.06.0-ce, build 02c1d87
192.168.139.174 | SUCCESS | rc=0 >>
Docker version 17.06.0-ce, build 02c1d87
# ansible node -a 'systemctl start docker'
# ansible node -a 'systemctl status docker'
# ansible node -a 'systemctl enable docker'

三、配置docker swarm集群

1、创建docker swarm集群

# docker swarm init --listen-addr 0.0.0.0
Swarm initialized: current node (a1tno675d14sm6bqlc512vf10) is now a manager.
To add a worker to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-3sp9uxzokgr252u1jauoowv74930s7f8f5tsmm5mlk5oim359e-dk52k5uul50w49gbq4j1y7zzb 192.168.139.175:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

2、查看节点

# docker node ls
ID                           HOSTNAME      STATUS      AVAILABILITY   MANAGER STATUS
a1tno675d14sm6bqlc512vf10 *  swarm01        Ready         Active           Leader

3、查看加入集群manager管理节点的命令

# docker swarm join-token manager
To add a manager to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-3sp9uxzokgr252u1jauoowv74930s7f8f5tsmm5mlk5oim359e-7tdlpdnkyfl1bnq34ftik9wxw 192.168.139.175:2377

4、查看加入集群worker节点的命令

# docker swarm join-token worker
To add a worker to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-3sp9uxzokgr252u1jauoowv74930s7f8f5tsmm5mlk5oim359e-dk52k5uul50w49gbq4j1y7zzb 192.168.139.175:2377

5、将前面规划的两个worker节点加入集群

# docker swarm join --token SWMTKN-1-3sp9uxzokgr252u1jauoowv74930s7f8f5tsmm5mlk5oim359e-dk52k5uul50w49gbq4j1y7zzb 192.168.139.175:2377
This node joined a swarm as a worker.

6、查看worker节点是否已加入集群

# docker node ls
ID                        HOSTNAME    STATUS  AVAILABILITY  MANAGER STATUS
7zkbqgrjlsn8c09l3fagtfwre     swarm02  Ready      Active              
a1tno675d14sm6bqlc512vf10 *   swarm01  Ready      Active         Leader
apy9zys2ch4dlwbmgdqwc0pn3     swarm03  Ready      Active

7、查看docker swarm的管理网络

# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
05efca714d2f        bridge              bridge              local
c9cd9c37edd7        docker_gwbridge     bridge              local
10ac9e48d81b        host                host                local
n60tdenc5jy7        ingress             overlay             swarm
a9284277dc18        none                null                local

这里,一个docker swarm集群就搭建好了

四、搭建docker swarm的UI—Portainer

Portainer地址:https://portainer.io/。

1、使用该命令部署Portainer

# docker service create 
--name portainer 
--publish 9000:9000 
--constraint 'node.role == manager' 
--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock 
portainer/portainer 
-H unix:///var/run/docker.sock
# docker images |grep portainer
portainer/portainer  latest  07cde96d4789   2 weeks ago  10.4MB
# docker service ls                    ###查看集群列表
ID              NAME        MODE     REPLICAS      IMAGE             PORTS
p5bo3n0fmqgz  portainer  replicated    1/1   portainer/portainer:latest   *:9000->9000/tcp

这就部署好了

2、浏览器输入http://localhost:9000进入该UI界面,如下所示,第一次进入Portainer,配置8位数的admin密码

未分类

密码修改完成后点击“validate”验证

未分类

如下图所示,输入admin用户名和密码进入Portainer

未分类

首页如下

未分类

查看swarm节点模块

未分类

这里可以在images模块pull镜像,在这里我pull了nginx

未分类

在Services模块下创建nginx服务,Services > Add service,这里创建三个副本,并将80端口映射出去,最后点击“Create Service”创建服务

未分类

刷新服务列表,查看是否创建成功

未分类

3、使用命令进行确认

# docker images | grep nginx
nginx   latest    b8efb18f159b     7 days ago     107MB
# ansible node -m shell -a 'docker images|grep nginx'
192.168.139.177 | SUCCESS | rc=0 >>
nginx   latest    b8efb18f159b     8 days ago     107MB
192.168.139.176 | SUCCESS | rc=0 >>
nginx   latest    b8efb18f159b     8 days ago     107MB
# docker service ls        ###查看服务的任务列表
ID             NAME     MODE     REPLICAS   IMAGE        PORTS
emrs3rj73bwh  Nginx  replicated   3/3    nginx:latest  *:80->80/tcp
p5bo3n0fmqgz  portainer  replicated  1/1  portainer/portainer:latest   *:9000->9000/tcp
# docker service ps Nginx  
ID                  NAME                IMAGE               NODE         
0smpndfx0bwc        Nginx.1             nginx:latest        swarm03      
werrrzlyfbf1        Nginx.2             nginx:latest        swarm01   
l7puro0787cj        Nginx.3             nginx:latest        swarm02 

DESIRED STATE       CURRENT STATE          ERROR               PORTS      
Running          Running 15 minutes ago                                   
Running          Running 15 minutes ago                                  
Running          Running 15 minutes ago

未分类

五、搭建docker swarm的UI—Shipyard

Shipyard的UI也是比较简单的,但是比较反复,它需要在每个节点都pull相应镜像才能加入Shipyard的UI。

1、先pull相应镜像到本地,这里我使用的是网易蜂巢的镜像,很快而且镜像也是比较新的

# docker pull hub.c.163.com/library/alpine:latest
# docker pull hub.c.163.com/library/rethinkdb:latest
# docker pull hub.c.163.com/longjuxu/microbox/etcd:latest
# docker pull hub.c.163.com/wangjiaen/shipyard/docker.io/shipyard/docker-proxy:latest
# docker pull hub.c.163.com/library/swarm:latest
# docker pull hub.c.163.com/wangjiaen/shipyard/docker.io/shipyard/shipyard:latest

2、给这些镜像新建一个tag标签

# docker tag 7328f6f8b418 alpine
# docker tag 4a511141860c rethinkdb
# docker tag 6aef84b9ec5a microbox/etcd 
# docker tag cfee14e5d6f2 shipyard/docker-proxy
# docker tag 0198d9ac25d1 swarm
# docker tag 36fb3dc0907d shipyard/shipyard

3、使用如下命令搭建Shipyard的UI

# curl -sSL https://shipyard-project.com/deploy | bash -s
Deploying Shipyard
 -> Starting Database
 -> Starting Discovery
 -> Starting Cert Volume
 -> Starting Proxy
 -> Starting Swarm Manager
 -> Starting Swarm Agent
 -> Starting Controller
Waiting for Shipyard on 192.168.139.175:8080
..
Shipyard available at http://192.168.139.175:8080
Username: admin Password: shipyard

4、根据提示输入http://localhost:8080,输入用户名admin,密码shipyard进入shipyard

未分类

5、进入shipyard首页容器界面

未分类

6、进入nodes模块查看,这里现在只有manager节点

未分类

7、在worker节点上pull并tag镜像,即是重复如上的第①和第②步,之后,在该worker节点上输入如下命令将其加入shipyard

# curl -sSL https://shipyard-project.com/deploy | ACTION=node DISCOVERY=etcd://192.168.139.175:4001 bash -s 
Adding Node
 -> Starting Cert Volume
 -> Starting Proxy
 -> Starting Swarm Manager
 -> Starting Swarm Agent
Node added to Swarm: 192.168.139.176

未分类

其他节点同理。

对比两种UI,其实都是比较简单的,个人认为Portainer较好,在manager节点pull一个镜像即可搭建UI。

问题:

  • manager:
# docker swarm init --advertise-addr 192.168.139.175
  • worker:
# docker swarm join --token SWMTKN-1-4dwtfbdvjmuf3limglbpy66k85ply2cn66hd0ugsaxfed5fj1d-3rp33pedt9k7ewpfizbzc9bvi 192.168.139.175:2377
Error response from daemon: Timeout was reached before node was joined. The attempt to join the swarm will continue in the background. Use the "docker info" command to see the current swarm status of your node.

出现worker节点无法加入集群的问题,这里需要设置监听地址全零。

使用Docker或者docker-compose搭建gitlab

一. 直接下载docker-ce

1. 拉取gitlab/gitlab-ce

Randy:~ Randy$ docker pull gitlab/gitlab-ce
Using default tag: latest

2. 运行gitlab/gitlab-ce

Randy:~ Randy$ docker run -d --publish 8443:443 --publish 8001:80 --publish 8022:22 --name gitlab-randy --restart always gitlab/gitlab-ce

其中使用publish参数映射host主机上的8443到docker-ce上面的443端口,并制定使用bash。此处可以使用–volume参数来指定docker-ce中的文件到host主机上的路径以便于升级,此处仅作为测试使用。

3. 验证http://127.0.0.1:8001/可以登录,首次登录需要修改密码

二. 使用Docker-compose方式

这种方式是比较灵活,也是官方推荐的安装方式。实际上就是把启动docker的参数配置到文件中,并通过docker-compose up调用。步骤如下:

1. 下载docker-compose.yml文件

wget https://raw.githubusercontent.com/sameersbn/docker-gitlab/master/docker-compose.yml
[root@randysun ~]# wget https://raw.githubusercontent.com/sameersbn/docker-gitlab/master/docker-compose.yml
--2017-08-06 11:36:43--  https://raw.githubusercontent.com/sameersbn/docker-gitlab/master/docker-compose.yml
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 151.101.72.133
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|151.101.72.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3836 (3.7K) [text/plain]
Saving to: ‘docker-compose.yml’

100%[============================================================================>] 3,836       --.-K/s   in 0s      

2017-08-06 11:36:44 (64.7 MB/s) - ‘docker-compose.yml’ saved [3836/3836]

2. 在第一步中下载好的文件路径下运行docker-compose up

[root@randysun ~]# docker-compose up
Creating network "root_default" with the default driver
Pulling postgresql (sameersbn/postgresql:9.6-2)...
9.6-2: Pulling from sameersbn/postgresql
c60055a51d74: Downloading [=>                                                 ]  1.615MB/65.69MB
755da0cdb7d2: Download complete
969d017f67e6: Download complete
37c9a9113595: Download complete
a3d9f8479786: Download complete
e43d9de53575: Downloading [==================>                                ]  1.754MB/4.702MB
cddf24084b61: Download complete
f23b95c5f17c: Waiting
3965edbc705f: Waiting
4e6c393fb7f5: Waiting

3. 验证

使用Docker打造MySQL私有云

前言

  • 前几月经常看到有 MySQL 到底能不能放到 Docker 里跑的各种讨论。这样做是错的!这样做是对的!说错的理由也说了一大堆,说对的思想也很明确。大家都有道理。但是我本人觉得这样的讨论落地意义不大。因为对与错还是要实践来得出的。

  • 所以同程旅游也很早开始了 MySQL 的 Docker 化实践,到目前已经有超一千多个 MySQL 实例在 Docker 平台安全稳定地跑着,DB 运维能力发生了质的提高(DBA 再也不用担心删库跑路了)。

  • 当然这样是不是可以证明之前的讨论结论——是对的。我想也不一定,因为我们还只是一只在学飞行的小鸟,还要更多的学习,所以我们特将我们在 MySQL 的 Docker 化上的实践分享给大家。

背景介绍

  • 同程旅游早期的数据库都以 MSSQL 为主,这个产品有个特点就是 UI 操作很棒。但是批量和自动化管理很难做,人力的工作很多。后来逐渐替换为 MySQL 后也是按照传统的运维方式管理。导致大部分的工作需要人肉运维。

  • 当然像我们早期使用过的 MSSQL 也是有优点的:就是单机性能比较好,在当年那个资源不够的年代里我们常可以在高可用的实例上运行多个库。这种情况下物理机数量与实例数量还是比较可控的,相对数量比较少,人肉运维完全可以应对。

  • 但是 MSSQL 的缺陷也很多,比如做水平拆分比较困难,导致数据库成为系统中最大的一个瓶颈。但在我们使用 MySQL+ 中间件(我们做这个中间件也是下了不少心思的,以后可以分享一下)做水平拆分后就开始解决了这个瓶颈。

  • 水平拆分的引入也带来了一个小缺点,就是会造成数据库实例数量大幅上升。举个例子我们做 1024 分片的话一般是做 32 个 node,一主一从是必须的(大部分情况是一主两从),那么至少 64 个实例,再加上应急扩展和备份用的节点那就更多了(中间件的开发者更希望是 1024 片就是 1024 个实例)。

  • 一次上线做一个 32node 分片扩展从库,两个 DBA 足足花了 4 个小时。另外,如果做单机单实例那肯定更不行了,别的不说,成本也会是个大问题,且物理机的资源也未能最大化利用。况且因为 MySQL 单体的性能没优势所以分片居多所以大部分情况下并不是每个库都能跑满整个物理机的。即使有部分能跑满整机资源的库,它的多节点备份,环境一致性和运维动作统一等问题也会让 DBA 一头糟,忙碌又容易出错的工作其实是无意义的。

  • 有了单机多实例运行 MySQL 实例的需求。单机多实例要思考的主要问题就是如果进行资源隔离和限制,实现方案有很多,怎么选?KVM,Docker,Cgroups 是目前的可以实现隔离主流方案。

  • KVM 对一个 DB 的隔离来说太重了,性能影响太大,在生产环境用不合适。这是因为 MySQL 运行的就是个进程而且对 IO 要求比较高,所以 KVM 不满足要求 (虽然优化以后 IO 能有点提升)。

  • cgroups 比较轻,虽然隔离性不是很高,但对于我们的 MySQL 多实例隔离来说是完全够用了(Docker 的资源限制用的就是 cgroups)。但是我们还想针对每个 MySQL 实例运行额外的管理进程 (比如监控等等)。用 cgroups 实现起来会比较复杂,并且我们还想让实例管理和物理机区分开,那 cgroups 也放弃。

  • 至于 Docker,那就很不错了,那些裸用 cgroups 的麻烦它都给搞定了。并且有 API 可以提供支持,开发成本低。而且我们可以基于 Docker 镜像来做部署自动化,那么环境的一致性也可轻松解决。所以最终我们选择了 Docker 作为云平台的资源隔离方案 (当然过程中也做了很多性能、稳定性等的适配工作,这里就不赘述了)。

下面两个图可以形象展示这款产品带来的革命性意义:

未分类

当然要能称之为云,那么平台最基本的要求就是具备资源计算、资源调度功能,且资源分配无需人工参与。对用户来讲,拿到的应该是直接可用的资源,并且天生自带高可用、自动备份、监控告警、慢日志分析等功能,无需用户关心资源背后的事情。其次才是各种日常的 DBA 运维操作需求服务化输出。下面我们就来讲讲我们这个平台是如何一步步实现的。

平台实现过程

站在巨人的肩膀上

我一直认为评价一款数据库的优劣,不能只评价数据库本身。我们要综合它的周边生态是否健全,比如:高可用方案、备份方案、日常维护难度、人才储备等等。当然对于一个云平台也一样,所以我们进行了短平快的试错工作,将平台分为多期版本开发。第一个版本的开发周期比较短,主要用来试验,所以我们要尽可能运用已有的开源产品来实现我们的需求,或者对已有开源产品进行二次开发以后实现定制化的需求。以下是我们当时用到的部分开源产品和技术。

未分类

下面选几个产品简单说一下我们通过它实现什么功能:

  • Percona:我们的备份、慢日志分析、过载保护等功能都是基于 pt-tools 工具包来实现的。

  • Prometheus:性能优越且功能强大的 TSDB,用于实现整个平台实例的监控告警。缺点是没有集群功能,单机性能是个瓶颈 (虽然单机的处理能力已经很强了),所以我们在业务层面进行了 DB 拆分,实现了分布式存储及扩展。

  • Consul:分布式的服务发现和配置共享软件,配合 prometheus 实现监控节点注册。

  • Python:管理 Docker 容器中 MySQL 实例的 agent 以及部分操作脚本。

  • Docker:承载 MySQL 实例并实现资源隔离和资源限制。

总体架构

未分类

容器调度系统如何选择

容器调度的开源产品主要有 Kubernetes 和 mesos,但是我们并没有选用这两个。主要原因是我们内部已经开发了一套基于 Docker 的资源管理、调度的系统,至今稳定运行 2 年多了。这套架构稍作修改是符合需求的。

另外第三方的资源调度系统兼容我们目前的高可用架构,其他自动化管理有些难度,同时资源分配策略也需要定制化。所以最终还是选择采用了自研的资源调度管理。适合自己现状的需求才是最好的。当然后面有机会做到计算调度和存储调度分离的情况下我们可能会转向 Kubernetes 的方案。

工作原理

我们就拿创建集群来举例吧。当平台发起一个创建集群的任务后,首先会根据集群规模 (一主一从还是一主多从,或者是分片集群) 确定要创建的实例数量,然后根据这个需求按照我们的资源筛选规则 (比如主从不能在同一台机器、内存配置不允许超卖等等),从现有的资源池中匹配出可用资源,然后依次创建主从关系、创建高可用管理、检查集群复制状态、推送集群信息到中间件 (选用了中间件的情况下) 控制中心、最后将以上相关信息都同步到 CMDB。

以上的每一个工作都是通过服务端发送消息到 agent,然后由 agent 执行对应的脚本,脚本会返回指定格式的执行结果,这些脚本是由 DBA 开发的。这种方式的优势在于,DBA 比任何人都了解数据库,所以通过这种方式可以有效提升项目开发效率,也能让 DBA 参与到项目当中去。开发只需要写前台逻辑,DBA 负责后端具体执行的指令。如果未来功能有变更或迭代的话,只需要迭代脚本即可,维护量极小。

资源的调度分配原则

经过对同程多年的 DB 运维数据分析得到如下经验:

  • CPU 最大超卖 3 倍,内存不超卖;

  • 同一机房优先选择资源最空闲的机器;

  • 主从角色不允许在同一台机器上;

  • 若有 VIP 需求的主从端口需要一致,无 VIP 需求直接对接中间件的无端口一致的限制;

  • 分片的集群将节点分布在多台物理机上;

产品分类

未分类

核心功能

未分类

以上是已经上线的部分核心功能,还有很多功能就不再一一展示。

备份恢复系统

未分类

备份工具我们是用 percona-xtrabackup。通过流备份的方式将数据备份到远端的备份服务器。备份服务器有多台,分别按照所属机房划分。

我们提供了手工备份和定时备份来满足不同场景的需求。多实例备份一定要关注磁盘 IO 和网络,所以我们的备份策略会限制单个物理机上并行备份的数量,另外单个机房备份任务队列的并行度也有控制,确保并行备份任务始终保持到我们指定的数量。

假如整个机房并行的是 50 个任务,那么这 50 个当中如果有 5 个提前备份完成,那么会新加入 5 个等待备份的任务进入这个备份队列。我们后来改造了备份的存储方式,直接将备份流入分式存储。

监控告警系统

未分类

在上线这套云平台前,我们还是用传统的 zabbix 来实现监控告警的。zabbix 的功能的确非常强大,但是后端的数据库是个瓶颈,当然可以通过数据库拆分的方式解决。

数据库要监控的指标比较多,如果采集的项目比较多,zabbix 就需要加 proxy,架构越来越复杂,再加上和我们平台对接的成本比较高,对一些复杂的统计类查询 (95 值、预测值等) 性能比较差。

所以我们选了一款 TSDB——prometheus,这是一款性能极强、极其适合监控系统使用的时序性数据库。prometheus 优点就是单机性能超强。但凡事又有两面性,它的缺点就是不支持集群架构 (不过我们解决了扩展的问题,下面会讲到)。

prometheus 的使用应该是从一年前就开始的,那时候我们只是把它作为辅助的监控系统来使用的,随着逐渐熟悉,越来越觉得这个是容器监控的绝佳解决方案。所以在上云平台的时候就选择了它作为整个平台的监控系统。

监控数据采集

prometheus 是支持 pushgateway 和 pull 的方式。我们选用了 pull 的方式。因为结构简单,开发成本低的同时还能和我们的系统完美对接。consul 集群负责注册实例信息和服务信息,比如 MySQL 实例主从对应的服务、Linux 主从对应的服务、容器注册对应的服务。然后 prometheus 通过 consul 上注册的信息来获取监控目标,然后去 pull 监控数据。监控客户端是以 agent 的形式存在,prometheus 通过 HTTP 协议获取 agent 端采集到的数据。

监控指标画图

不得不说 grafana 是监控画图界的扛把子,功能齐全的度量仪表盘和图形编辑器,经过简单配置就能完成各种监控图形的展示。然后我们打通了云平台和 grafana 的关联,用户在云平台需要查看实例或集群信息,只要点击按钮即可。

未分类

告警管理

告警管理分为:告警发送、告警接收人管理、告警静默等功能。prometheus 有一个告警发送模块 alertmanager,我们通过 webhook 的方式让 alertmanager 把告警信息发送到云平台的告警 API,然后在云平台来根据后面的逻辑进行告警内容发送。

alertmanager 推过来的只是实例纬度的告警,所以我们结合告警平台的实例相关信息,会拼出一个多维信息的告警内容。让 DBA 一看就知道是谁的哪个集群在什么时间触发了什么等级的什么告警。告警恢复后也会再发一次恢复的通知。

未分类

alertmanager 也是功能强大的工具,支持告警抑制、告警路由策略、发送周期、静默告警等等。有需要可以自行配置。但是这种和平台分离的管理方式不是我们想要的,所以就想把 alertmanager 对告警信息处理的这部分功能集成到云平台内。

但是官方文档并没有提及到 alertmanager 的 API,通过对源码的分析,我们找到了告警管理相关的 API。然后 alertmanager 的原生 UI 上操作的功能完美移植到了我们的云平台,同时新增了实例相关集群名称、负责人等更多纬度的信息。

下面是一些操作样例:

当前告警:

未分类

添加告警静默:

未分类

已创建的静默规则:

未分类

慢日志分析系统

未分类

慢日志的收集是通过 pt-query-digest 每小时进行本地分析,分析完成以后将结果写入慢日志存储的数据库来完成的。当然如果用户需要立刻查看当前慢日志的情况,也可以在界面点击慢日志分析。分析完成后可以在 UI 界面点击慢日志查看,就能看到该实例的慢日志分析结果。它同时集成了 explain、查看 table status 等功能。

集群管理

集群管理作为该平台的核心功能之一,占据了整个平台 70% 的工作。这些功能就是 DBA 运维中经常需要用到的。我们的设计思路是以集群为单位,所以同时只能操作一个集群上的实例。这样就不会在一个页面上显示过多无用的信息,看着乱还有可能导致误操作。看了下图中的这些功能就能更明白为什么要这么设计了。

未分类

图中只是一部分,还有部分未展示出的功能 (集成中间件、Dashboard、黑屏诊断窗口等),在后版中功能更多。

高可用

高可用方案我们使用了目前最流行的 MySQL 高可用方案 MHA。MHA 的优缺点就不在这里讲了,有 DBA 同学的应该都已经很熟悉了。这里我说一下我们基于同程业务做的调整。

GTID

因为我们主要使用的 MariaDB,但是 MHA 最新版本也是不能支持 MariaDB 的 GTID 切换。所以我们在原有的基础上做了改进,支持了 MariaDB 的 GTID。使用 GTID 以后灵活切换是一个方面,另外一个方面是 sync_master_info 和 sync_relay_log_info 就不需要设置成 1 了 (MariaDB 不支持写 table,只能写 file),极大减少了从库复制带来的 IOPS。

切换时调整相关参数

我们在切换时调整 sync_binlog 和 innodb_flush_log_at_trx_commit 参数,这两个参数是决定数据落盘方式的,默认大家都是设置双 1。这样相对数据最安全,但是 IO 也最高。

云服务的多实例部署会导致一台物理机上既有 master 又有 slave。我们肯定不希望 slave 产生太高的 IO 影响到同机器的其他 slave(虽然可以 IO 隔离,但是优先降低不必要 IO 才靠谱)。所以理论上来说 Master 上面设置双 1,slave 则可以不这样设置。但是切换后原来的 salve 可能会变成了 master。所以我们默认 slave 非双 1,在 MHA 切换的时候会自动将新 master 的这两个参数设置为 1。

哨兵

我们在多个点部署了哨兵服务。这个哨兵是一个简单的 API 服务,带上响应的参数可以请求到指定的实例。当 MHA manager 检测到有 Master 无法连接时,会触发 secondary check 机制,带着 master 相关信息请求哨兵节点的 API,根据哨兵节点返回情况,若超过半数无法连接则切换。否则放弃切换。

高可用切换对接 DB 中间件

未分类

DB 中间件和 DB 通过物理 IP 连接,当发生高可用切换时将最新的 Master IP、Master port 信息推送到 DB 中间件控制中心,DB 中间件拿到配置后立刻下发并生效。

实例、库迁移

未分类

迁移功能初衷是为了将平台外的实例或者库迁移到平台里面来,后来随着逐渐使用发现这个功能可挖掘的空间很大,比如可以做平台内库表拆分等需求。实现原理也很简单,用 mydumper 将指定数据备份下来以后,再用 myloader 恢复到指定数据库。

这是一个全量的过程,增量复制用的是用我们自己开发的一个支持并行复制的工具,这个工具还支持等幂处理,使用更灵活。没有用原生复制的原因是,假如要将源实例多个库中的一个库迁移到目标实例,那么原生复制就需要对 binlog 做复制过滤,这里面涉及到配置修改,实例重启,所以果断不考虑。

实现过程并没有高大上,但是完全满足需求。当然 mydumper 和 myloader 也有一些问题,我们也做了小改动以后才实现的。后面我们计划用流的方式去做数据导出导入 (类似于阿里开源的 datax)。

迁移完成,增量无延迟的情况下,大家会关心迁移前后数据一致性的问题,我们提供了自研的数据校验工具。实测 300G 的数据校验时间约为 2 至 3 分钟,快慢取决于开多少线程。

屏蔽底层物理资源

对用户来讲,平台提供的是一个或一组数据库服务,不需要关系后端的实例是在哪台机器上。资源计算和调度全部由系统的算法进行管理。

提升资源利用率 (CPU、内存)

通过单机多实例,CPU 资源可超卖,有效提高 CPU 资源的利用。内存资源未超卖,但是可以控制到每个实例的内存使用,确保每个实例都能有足够的内存。若有剩余内存,则继续分配容器即可,不 OOM 的情况下压榨内存资源。

提升运维效率

效率的提升得益于标准化以后带来的自动化。批量运维的成本很低。以前部署一套分片集群需要花费将近 6 个小时 (不包含对接中间件的 1 到 2 个小时),而现在只需要 5 分钟即可部署完成。并且部署完成以后会将提供一套中间件 +DB 分片集群的服务。

精细化管理

平台上线后有效提高了资源利用率,同时我们按照 1 库 1 实例的方式,可以有效避免不同库的压力不均导致相互影响的问题。并且性能监控也能精准到库级别。

用iptables关闭docker映射到host上的端口

未分类

docker可以让我们很方便地安装本地服务。但同时,默认的docker的设置使这些端口可以很轻松地从remote访问。
理想的做法是利用nginx把docker 默认打开的端口反向代理到别的端口,然后对新的端口进行用户验证保护。

屏蔽外部端口访问我们很自然而然就想到了使用iptables。

目的

我们在运行某个container的时候,使用了端口映射,例如

$ docker docker run --name myservice -p 5000:5000 some_image_name

我们想要仅允许在服务器上通过localhost:5000访问docker container的服务,而屏蔽掉外部通过myhostname:5000来访问服务的request。

错误的尝试

$ sudo iptables -A INPUT -p tcp -m tcp --dport 5000 -j ACCEPT

尝试在浏览器输入myhostname:5000,发现依然可以访问到5000端口。

这个是为什么呢?

选择正确的chain

第一次通过简单搜索谷歌,复制粘贴的方法失败了。看来还得静下来看一下iptables这个东西。

这篇文章https://securitynik.blogspot.jp/2016/12/docker-networking-internals-how-docker_16.html

很好解释了docker中iptables的用法。

然而iptables的概念太多。我们把范围缩小到filter这个table中。

简要来说,我们通常需要处理的是三条chain(INPUT, FORWARD, OUTPUT)。所有的package 通过相应的chain,chain中的规则进行match。如果有match的规则,则执行规则规定的动作,否则继续向后访问规则,最后最后如果没有匹配的规则,则使用chain的默认的policy来处理。 默认的Policy可以是 ACCEPT 或者 DROP。分别表示接受和丢弃该Package。 被Drop的表现通常是,在浏览器上,显示load中但是总是无法出来结果。

当我们启动docker deamon,挂上docker container的时候,docker会在FORWARD chain中追加叫DOCKER和DOCKER-ISOLATION的自定义CHAIN。

由此可见。我们要追加的规则应该追加在FORWARD chain而不是在INPUT chain。

正确的做法是

$ sudo iptables -I FORWARD -p tcp -m tcp --dport 5000 -j ACCEPT

但是注意,docker每次重启之后都会把自定义的DOCKER chain 插到FORWARD chain的第一个。所以,我们不如把这个规则写到DOCKER chain中。

$ sudo iptables -I DOCKER -p tcp -m tcp --dport 5000 -j ACCEPT

其他疑惑

  • 为什么iptables知道特定的外部reqeust需要走的是FORWARD而不是INPUT的chain?

这个是因为在filter table之前由NAT table替换了走向docker的request的destination。并不是所有到本机的请求都是走INPUT chain的,想象一下如果本机是NAT的gateway,那么很显然,大部分package需要转发到下面去。

  • 为什么经常有两条一摸一样的规则?

光用iptables -L的话会看到几乎完全一样的两行,我们查看具体内容需要iptables -v,这样可以看到in out两个参数,这两个参数分别为in的网卡端口,和out的网卡端口。

CentOS 7.3 Docker私有仓库Harbor安装部署

1、部署环境

  • Centos7.3 x64
  • docker-ce-17.06.0
  • docker-compose-1.15.0
  • Python-2.7.5(系统默认)

2、Docker及Docker-compose安装

 yum install -y yum-utils device-mapper-persistent-data lvm2
 yum-config-manager 
    --add-repo 
    https://download.docker.com/linux/centos/docker-ce.repo
 yum-config-manager --enable docker-ce-edge
 yum makecache fast
 systemctl start docker 
 systemctl enable docker

curl -L https://github.com/docker/compose/releases/download/1.15.0/docker-compose-`uname -s`-`uname -m` &gt; /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

3、Habor部署配置

wget https://github.com/vmware/harbor/releases/download/v1.1.2/harbor-offline-installer-v1.1.2.tgz
tar xf harbor-offline-installer-v1.1.2.tgz
cd harbor/

vim harbor.cfg
hostname = hub.wow
其他默认(http协议)

./install.sh
安装成功后,可以通过http://hub.wow/访问

未分类

4、Docker客户端使用

由于Harbor默认使用的http协议,故需要在Docker client上的Dockerd服务增加–insecure-registry hub.wow
Centos7修改方式为:

vim /lib/systemd/system/docker.service
ExecStart=/usr/bin/dockerd  --insecure-registry hub.wow

systemctl daemon-reload
systemctl reload docker
[root@localhost harbor]# docker login -u admin -p Harbor12345 hub.wow
官方仓库下载busybox镜像
[root@localhost harbor]# docker pull busybox 
[root@localhost harbor]# docker images
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
busybox                     latest              efe10ee6727f        2 weeks ago         1.13MB
本地基于busybox:latest创建标记hub.wow/busybox:latest
[root@localhost harbor]# docker tag busybox:latest hub.wow/project_name/busybox:latest
推送本地镜像busybox:latest 到hub.wow私有仓库
[root@localhost harbor]# docker push hub.wow/project_name/busybox:latest

5、Harbor服务管理

cd harbor/
docker-compose -f ./docker-compose.yml [ up|down|ps|stop|start ]

Docker Registry的安装及镜像管理方法

Why Docker Registry?

有时我们的服务器无法访问互联网,或者你不希望将自己的镜像放到公网当中,那么你就需要Docker Registry,它可以用来存储和管理自己的镜像。

How to install Docker Registry?

其实创建私有的Docker仓库非常简单,只需要运行一个Registry容器即可,该容器存储在Docker HUB中。

docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry registry
  • -d 是后台启动容器。

  • -p 将容器的 5000 端口映射到 Host 的 5000 端口。5000 是 registry 服务端口。

  • -v 将容器 /var/lib/registry目录映射到宿主机的/myregistry,用于存放镜像数据。

How to use Docker Registry?

理论上我们已经搭建好了自己的私有镜像仓库,但这时候还有一些问题需要我们解决。

在pull或者push镜像时报拒绝连接的错误。

这是由于Registry为了安全性考虑,默认是需要证书支持的,证书这块我倒没有研究过,可以通过一个简单的办法解决。创建或者修改/etc/docker/daemon.json文件,并在其中写入

{
    "insecure-registries": ["<ip>:5000"] 
}

其中为安装了Registry的机器ip地址。需要注意的是在安装registry的节点和客户端需要访问私有Registry的节点都需要执行此步操作。

修改文之后执行以下命令重启节点docker。

systemctl daemon-reload
systemctl restart docker

我是在ubuntu16环境进行的上述操作,其他系统可以参考相应命令。

通过 docker tag重命名镜像,使之与registry匹配。

docker tag wsf <ip>:5000/wsf:v1

给wsf镜像重命名为:5000/wsf:v1,v1为版本号,前面必须要加上域名或IP地址(运行上面registry容器的地址)和端口号。

其实Docker HUB与我们要建立的私有Registry没有本质的区别。

docker run ubuntu 

语句从官方hub中寻找镜像,它是

docker run docker.io/library/ubuntu 

的简写。

docker.io即是上面的ip地址,端口号为80省略。

上传镜像

docker push <ip>:5000/wsf:v1

下载镜像

docker pull <ip>:5000/wsf:v1

上传和下载镜像都可以在任意能访问Registry的节点上进行。前提是此节点必须进行上述的修改daemon.json操作。

查看Registry中所有镜像信息

curl http://<ip>:5000/v2/_catalog

此语句会返回一个json,包含当前Registry中存储的镜像信息。

总结

本文简单为大家介绍了Docker Registry的相关细节,有了这个东西就能更好的实现docker的多主机管理和我们自定义镜像的管理。

配置docker限制容器对cpu 内存和IO的资源使用

在使用 docker 运行容器时,一台主机上可能会运行几百个容器,这些容器虽然互相隔离,但是底层却使用着相同的 CPU、内存和磁盘资源。如果不对容器使用的资源进行限制,那么容器之间会互相影响,小的来说会导致容器资源使用不公平;大的来说,可能会导致主机和集群资源耗尽,服务完全不可用。

docker 作为容器的管理者,自然提供了控制容器资源的功能。正如使用内核的 namespace 来做容器之间的隔离,docker 也是通过内核的 cgroups 来做容器的资源限制。这篇文章就介绍如何使用 docker 来限制 CPU、内存和 IO,以及对应的 cgroups 文件。

NOTE:如果想要了解 cgroups 的更多信息,可以参考 kernel 文档 或者其他资源。

我本地测试的 docker 版本是 17.03.0 社区版:

➜  stress docker version
Client:
 Version:      17.03.0-ce
 API version:  1.26
 Go version:   go1.7.5
 Git commit:   60ccb22
 Built:        Thu Feb 23 11:02:43 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.03.0-ce
 API version:  1.26 (minimum version 1.12)
 Go version:   go1.7.5
 Git commit:   60ccb22
 Built:        Thu Feb 23 11:02:43 2017
 OS/Arch:      linux/amd64
 Experimental: false

使用的是 ubuntu 16.04 系统,内核版本是 4.10.0:

➜  ~ uname -a
Linux cizixs-ThinkPad-T450 4.10.0-28-generic #32~16.04.2-Ubuntu SMP Thu Jul 20 10:19:48 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

NOTE: 不同版本和系统的功能会有差异,具体的使用方法和功能解释请以具体版本的 docker 官方文档为准。

我们使用 stress 容器来产生 CPU、内存和 IO 的压力,具体的使用请参考它的帮助文档。

1、CPU 资源

主机上的进程会通过时间分片机制使用 CPU,CPU 的量化单位是频率,也就是每秒钟能执行的运算次数。为容器限制 CPU 资源并不能改变 CPU 的运行频率,而是改变每个容器能使用的 CPU 时间片。理想状态下,CPU 应该一直处于运算状态(并且进程需要的计算量不会超过 CPU 的处理能力)。

docker 限制 CPU Share

docker 允许用户为每个容器设置一个数字,代表容器的 CPU share,默认情况下每个容器的 share 是 1024。要注意,这个 share 是相对的,本身并不能代表任何确定的意义。当主机上有多个容器运行时,每个容器占用的 CPU 时间比例为它的 share 在总额中的比例。举个例子,如果主机上有两个一直使用 CPU 的容器(为了简化理解,不考虑主机上其他进程),其 CPU share 都是 1024,那么两个容器 CPU 使用率都是 50%;如果把其中一个容器的 share 设置为 512,那么两者 CPU 的使用率分别为 67% 和 33%;如果删除 share 为 1024 的容器,剩下来容器的 CPU 使用率将会是 100%。

总结下来,这种情况下,docker 会根据主机上运行的容器和进程动态调整每个容器使用 CPU 的时间比例。这样的好处是能保证 CPU 尽可能处于运行状态,充分利用 CPU 资源,而且保证所有容器的相对公平;缺点是无法指定容器使用 CPU 的确定值。

docker 为容器设置 CPU share 的参数是 -c –cpu-shares,它的值是一个整数。

我的机器是 4 核 CPU,因此使用 stress 启动 4 个进程来产生计算压力:

➜  stress docker run --rm -it stress --cpu 4
stress: info: [1] dispatching hogs: 4 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 12000us
stress: dbug: [1] --> hogcpu worker 4 [7] forked
stress: dbug: [1] using backoff sleep of 9000us
stress: dbug: [1] --> hogcpu worker 3 [8] forked
stress: dbug: [1] using backoff sleep of 6000us
stress: dbug: [1] --> hogcpu worker 2 [9] forked
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [10] forked

在另外一个 terminal 使用 htop 查看资源的使用情况:

未分类

从上图中可以看到,CPU 四个核资源都达到了 100%。四个 stress 进程 CPU 使用率没有达到 100% 是因为系统中还有其他机器在运行。

为了比较,我另外启动一个 share 为 512 的容器:

➜  stress docker run --rm -it -c 512 stress --cpu 4 
stress: info: [1] dispatching hogs: 4 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 12000us
stress: dbug: [1] --> hogcpu worker 4 [6] forked
stress: dbug: [1] using backoff sleep of 9000us
stress: dbug: [1] --> hogcpu worker 3 [7] forked
stress: dbug: [1] using backoff sleep of 6000us
stress: dbug: [1] --> hogcpu worker 2 [8] forked
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [9] forked

因为默认情况下,容器的 CPU share 为 1024,所以这两个容器的 CPU 使用率应该大致为 2:1,下面是启动第二个容器之后的监控截图:

未分类

两个容器分别启动了四个 stress 进程,第一个容器 stress 进程 CPU 使用率都在 54% 左右,第二个容器 stress 进程 CPU 使用率在 25% 左右,比例关系大致为 2:1,符合之前的预期。

限制容器能使用的 CPU 核数

上面讲述的 -c –cpu-shares 参数只能限制容器使用 CPU 的比例,或者说优先级,无法确定地限制容器使用 CPU 的具体核数;从 1.13 版本之后,docker 提供了 –cpus 参数可以限定容器能使用的 CPU 核数。这个功能可以让我们更精确地设置容器 CPU 使用量,是一种更容易理解也因此更常用的手段。

–cpus 后面跟着一个浮点数,代表容器最多使用的核数,可以精确到小数点二位,也就是说容器最小可以使用 0.01 核 CPU。比如,我们可以限制容器只能使用 1.5 核数 CPU:

➜  ~ docker run --rm -it --cpus 1.5 stress --cpu 3
stress: info: [1] dispatching hogs: 3 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 9000us
stress: dbug: [1] --> hogcpu worker 3 [7] forked
stress: dbug: [1] using backoff sleep of 6000us
stress: dbug: [1] --> hogcpu worker 2 [8] forked
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [9] forked

在容器里启动三个 stress 来跑 CPU 压力,如果不加限制,这个容器会导致 CPU 的使用率为 300% 左右(也就是说会占用三个核的计算能力)。实际的监控如下图:

未分类

可以看到,每个 stress 进程 CPU 使用率大约在 50%,总共的使用率为 150%,符合 1.5 核的设置。

如果设置的 –cpus 值大于主机的 CPU 核数,docker 会直接报错:

➜  ~ docker run --rm -it --cpus 8 stress --cpu 3
docker: Error response from daemon: Range of CPUs is from 0.01 to 4.00, as there are only 4 CPUs available.
See 'docker run --help'.

如果多个容器都设置了 –cpus ,并且它们之和超过主机的 CPU 核数,并不会导致容器失败或者退出,这些容器之间会竞争使用 CPU,具体分配的 CPU 数量取决于主机运行情况和容器的 CPU share 值。也就是说 –cpus 只能保证在 CPU 资源充足的情况下容器最多能使用的 CPU 数,docker 并不能保证在任何情况下容器都能使用这么多的 CPU(因为这根本是不可能的)。

限制容器运行在某些 CPU 核

现在的笔记本和服务器都会有多个 CPU,docker 也允许调度的时候限定容器运行在哪个 CPU 上。比如,我的主机上有 4 个核,可以通过 –cpuset 参数让容器只运行在前两个核上:

➜  ~ docker run --rm -it --cpuset-cpus=0,1 stress --cpu 2
stress: info: [1] dispatching hogs: 2 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 6000us
stress: dbug: [1] --> hogcpu worker 2 [7] forked
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [8] forked

这样,监控中可以看到只有前面两个核 CPU 达到了 100% 使用率。

未分类

–cpuset-cpus 参数可以和 -c –cpu-shares 一起使用,限制容器只能运行在某些 CPU 核上,并且配置了使用率。

限制容器运行在哪些核上并不是一个很好的做法,因为它需要实现知道主机上有多少 CPU 核,而且非常不灵活。除非有特别的需求,一般并不推荐在生产中这样使用。

CPU 信息的 cgroup 文件

所有和容器 CPU share 有关的配置都在 /sys/fs/cgroup/cpu/docker// 目录下面,其中 cpu.shares 保存了 CPU share 的值(其他文件的意义可以查看 cgroups 的官方文档):

➜  ~ ls /sys/fs/cgroup/cpu/docker/d93c9a660f4a13789d995d56024f160e2267f2dc26ce676daa66ea6435473f6f/   
cgroup.clone_children  cpuacct.stat   cpuacct.usage_all     cpuacct.usage_percpu_sys   cpuacct.usage_sys   cpu.cfs_period_us  cpu.shares  notify_on_release
cgroup.procs           cpuacct.usage  cpuacct.usage_percpu  cpuacct.usage_percpu_user  cpuacct.usage_user  cpu.cfs_quota_us   cpu.stat    tasks
➜  ~ cat /sys/fs/cgroup/cpu/docker/d93c9a660f4a13789d995d56024f160e2267f2dc26ce676daa66ea6435473f6f/cpu.shares 
1024

和 cpuset(限制 CPU 核)有关的文件在 /sys/fs/cgroup/cpuset/docker/ 目录下,其中 cpuset.cpus 保存了当前容器能使用的 CPU 核:

➜  ~ ls /sys/fs/cgroup/cpuset/docker/d93c9a660f4a13789d995d56024f160e2267f2dc26ce676daa66ea6435473f6f/
cgroup.clone_children  cpuset.cpus            cpuset.mem_exclusive   cpuset.memory_pressure     cpuset.mems                      notify_on_release
cgroup.procs           cpuset.effective_cpus  cpuset.mem_hardwall    cpuset.memory_spread_page  cpuset.sched_load_balance        tasks
cpuset.cpu_exclusive   cpuset.effective_mems  cpuset.memory_migrate  cpuset.memory_spread_slab  cpuset.sched_relax_domain_level

➜  ~ cat /sys/fs/cgroup/cpuset/docker/d93c9a660f4a13789d995d56024f160e2267f2dc26ce676daa66ea6435473f6f/cpuset.cpus
0-1

–cpus 限制 CPU 核数并不像上面两个参数一样有对应的文件对应,它是由 cpu.cfs_period_us 和 cpu.cfs_quota_us 两个文件控制的。如果容器的 –cpus 设置为 3,其对应的这两个文件值为:

➜  ~ cat /sys/fs/cgroup/cpu/docker/233a38cc641f2e4a1bec3434d88744517a2214aff9d8297e908fa13b9aa12e02/cpu.cfs_period_us 
100000
➜  ~ cat /sys/fs/cgroup/cpu/docker/233a38cc641f2e4a1bec3434d88744517a2214aff9d8297e908fa13b9aa12e02/cpu.cfs_quota_us 
300000

其实在 1.12 以及之前的版本,都是通过 –cpu-period 和 –cpu-quota 这两个参数控制容器能使用的 CPU 核数的。前者表示 CPU 的周期数,默认是 100000,单位是微秒,也就是 1s,一般不需要修改;后者表示容器的在上述 CPU 周期里能使用的 quota,真正能使用的 CPU 核数就是 cpu-quota / cpu-period,因此对于 3 核的容器,对应的 cpu-quota 值为 300000。

2、内存资源

默认情况下,docker 并没有对容器内存进行限制,也就是说容器可以使用主机提供的所有内存。这当然是非常危险的事情,如果某个容器运行了恶意的内存消耗软件,或者代码有内存泄露,很可能会导致主机内存耗尽,因此导致服务不可用。对于这种情况,docker 会设置 docker daemon 的 OOM(out of memory) 值,使其在内存不足的时候被杀死的优先级降低。另外,就是你可以为每个容器设置内存使用的上限,一旦超过这个上限,容器会被杀死,而不是耗尽主机的内存。

限制内存上限虽然能保护主机,但是也可能会伤害到容器里的服务。如果为服务设置的内存上限太小,会导致服务还在正常工作的时候就被 OOM 杀死;如果设置的过大,会因为调度器算法浪费内存。因此,合理的做法包括:

  • 为应用做内存压力测试,理解正常业务需求下使用的内存情况,然后才能进入生产环境使用
  • 一定要限制容器的内存使用上限
  • 尽量保证主机的资源充足,一旦通过监控发现资源不足,就进行扩容或者对容器进行迁移
  • 如果可以(内存资源充足的情况),尽量不要使用 swap,swap 的使用会导致内存计算复杂,对调度器非常不友好

docker 限制容器内存使用量

在 docker 启动参数中,和内存限制有关的包括(参数的值一般是内存大小,也就是一个正数,后面跟着内存单位 b、k、m、g,分别对应 bytes、KB、MB、和 GB):

  • -m –memory:容器能使用的最大内存大小,最小值为 4m
  • –memory-swap:容器能够使用的 swap 大小
  • –memory-swappiness:默认情况下,主机可以把容器使用的匿名页(anonymous page)swap 出来,你可以设置一个 0-100 之间的值,代表允许 swap 出来的比例
  • –memory-reservation:设置一个内存使用的 soft limit,如果 docker 发现主机内存不足,会执行 OOM 操作。这个值必须小于 –memory 设置的值
  • –kernel-memory:容器能够使用的 kernel memory 大小,最小值为 4m。
  • –oom-kill-disable:是否运行 OOM 的时候杀死容器。只有设置了 -m,才可以把这个选项设置为 false,否则容器会耗尽主机内存,而且导致主机应用被杀死

关于 –memory-swap 的设置必须解释一下,–memory-swap 必须在 –memory 也配置的情况下才能有用。

  • 如果 –memory-swap 的值大于 –memory,那么容器能使用的总内存(内存 + swap)为 –memory-swap 的值,能使用的 swap 值为 –memory-swap 减去 –memory 的值
  • 如果 –memory-swap 为 0,或者和 –memory 的值相同,那么容器能使用两倍于内存的 swap 大小,如果 –memory 对应的值是 200M,那么容器可以使用 400M swap
  • 如果 –memory-swap 的值为 -1,那么不限制 swap 的使用,也就是说主机有多少 swap,容器都可以使用

如果限制容器的内存使用为 64M,在申请 64M 资源的情况下,容器运行正常(如果主机上内存非常紧张,并不一定能保证这一点):

➜  docker run --rm -it -m 64m stress --vm 1 --vm-bytes 64M --vm-hang 0
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogvm worker 1 [7] forked
stress: dbug: [7] allocating 67108864 bytes ...
stress: dbug: [7] touching bytes in strides of 4096 bytes ...
stress: dbug: [7] sleeping forever with allocated memory
.....

而如果申请 100M 内存,会发现容器里的进程被 kill 掉了(worker 7 got signal 9,signal 9 就是 kill 信号)

➜  docker run --rm -it -m 64m stress --vm 1 --vm-bytes 100M --vm-hang 0
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogvm worker 1 [7] forked
stress: dbug: [7] allocating 104857600 bytes ...
stress: dbug: [7] touching bytes in strides of 4096 bytes ...
stress: FAIL: [1] (415) <-- worker 7 got signal 9
stress: WARN: [1] (417) now reaping child worker processes
stress: FAIL: [1] (421) kill error: No such process
stress: FAIL: [1] (451) failed run completed in 0s

关于 swap 和 kernel memory 的限制就不在这里过多解释了,感兴趣的可以查看官方的文档。

内存信息的 cgroups 文件

对于 docker 来说,它的内存限制也是存放在 cgroups 文件系统的。对于某个容器,你可以在 sys/fs/cgroup/memory/docker/ 目录下看到容器内存相关的文件:

➜  ls /sys/fs/cgroup/memory/docker/b067fa0c58dcdd4fa856177fac0112655b605fcc9a0fe07e36950f0086f62f46 
cgroup.clone_children  memory.kmem.failcnt             memory.kmem.tcp.limit_in_bytes      memory.max_usage_in_bytes        memory.soft_limit_in_bytes  notify_on_release
cgroup.event_control   memory.kmem.limit_in_bytes      memory.kmem.tcp.max_usage_in_bytes  memory.move_charge_at_immigrate  memory.stat                 tasks
cgroup.procs           memory.kmem.max_usage_in_bytes  memory.kmem.tcp.usage_in_bytes      memory.numa_stat                 memory.swappiness
memory.failcnt         memory.kmem.slabinfo            memory.kmem.usage_in_bytes          memory.oom_control               memory.usage_in_bytes
memory.force_empty     memory.kmem.tcp.failcnt         memory.limit_in_bytes               memory.pressure_level            memory.use_hierarchy

而上面的内存限制对应的文件是 memory.limit_in_bytes:

➜  cat /sys/fs/cgroup/memory/docker/b067fa0c58dcdd4fa856177fac0112655b605fcc9a0fe07e36950f0086f62f46/memory.limit_in_bytes
67108864

3、IO 资源(磁盘)

对于磁盘来说,考量的参数是容量和读写速度,因此对容器的磁盘限制也应该从这两个维度出发。目前 docker 支持对磁盘的读写速度进行限制,但是并没有方法能限制容器能使用的磁盘容量(一旦磁盘 mount 到容器里,容器就能够使用磁盘的所有容量)。

➜  ~ docker run -it --rm ubuntu:16.04 bash

root@5229f756523c:/# time $(dd if=/dev/zero of=/tmp/test.data bs=10M count=100 && sync)
100+0 records in
100+0 records out
1048576000 bytes (1.0 GB) copied, 3.82859 s, 274 MB/s

real    0m4.124s
user    0m0.000s
sys 0m1.812s

限制磁盘的权重

通过 –blkio-weight 参数可以设置 block 的权重,这个权重和 –cpu-shares 类似,它是一个相对值,取值范围是 10-1000,当多个 block 去屑磁盘的时候,其读写速度和权重成反比。

不过在我的环境中,–blkio-weight 参数虽然设置了对应的 cgroups 值,但是并没有作用,不同 weight 容器的读写速度还是一样的。github 上有一个对应的 issue,但是没有详细的解答。

–blkio-weight-device 可以设置某个设备的权重值,测试下来虽然两个容器同时读的速度不同,但是并没有按照对应的比例来限制。

限制磁盘的读写速率

除了权重之外,docker 还允许你直接限制磁盘的读写速率,对应的参数有:

  • –device-read-bps:磁盘每秒最多可以读多少比特(bytes)
  • –device-write-bps:磁盘每秒最多可以写多少比特(bytes)

上面两个参数的值都是磁盘以及对应的速率,格式为 :[unit],device-path 表示磁盘所在的位置,限制 limit 为正整数,单位可以是 kb、mb 和 gb。

比如可以把设备的度速率限制在 1mb:

$ docker run -it --device /dev/sda:/dev/sda --device-read-bps /dev/sda:1mb ubuntu:16.04 bash
root@6c048edef769:/# cat /sys/fs/cgroup/blkio/blkio.throttle.read_bps_device 
8:0 1048576
root@6c048edef769:/# dd iflag=direct,nonblock if=/dev/sda of=/dev/null bs=5M count=10
10+0 records in
10+0 records out
52428800 bytes (52 MB) copied, 50.0154 s, 1.0 MB/s

从磁盘中读取 50m 花费了 50s 左右,说明磁盘速率限制起了作用。

另外两个参数可以限制磁盘读写频率(每秒能执行多少次读写操作):

  • –device-read-iops:磁盘每秒最多可以执行多少 IO 读操作
  • –device-write-iops:磁盘每秒最多可以执行多少 IO 写操作

上面两个参数的值都是磁盘以及对应的 IO 上限,格式为 :,limit 为正整数,表示磁盘 IO 上限数。

比如,我们可以让磁盘每秒最多读 100 次:

➜  ~ docker run -it --device /dev/sda:/dev/sda --device-read-iops /dev/sda:100 ubuntu:16.04 bash
root@2e3026e9ccd2:/# dd iflag=direct,nonblock if=/dev/sda of=/dev/null bs=1k count=1000
1000+0 records in
1000+0 records out
1024000 bytes (1.0 MB) copied, 9.9159 s, 103 kB/s

从测试中可以看出,容器设置了读操作的 iops 为 100,在容器内部从 block 中读取 1m 数据(每次 1k,一共要读 1000 次),共计耗时约 10s,换算起来就是 100 iops/s,符合预期结果。

写操作 bps 和 iops 与读类似,这里就不再重复了,感兴趣的可以自己实验。

磁盘信息的 cgroups 文件

容器中磁盘限制的 cgroups 文件位于 /sys/fs/cgroup/blkio/docker/ 目录:

➜  ~ ls /sys/fs/cgroup/blkio/docker/1402c1682cba743b4d80f638da3d4272b2ebdb6dc6c2111acfe9c7f7aeb72917/                               
blkio.io_merged                   blkio.io_serviced                blkio.leaf_weight                blkio.throttle.io_serviced        blkio.time_recursive   tasks
blkio.io_merged_recursive         blkio.io_serviced_recursive      blkio.leaf_weight_device         blkio.throttle.read_bps_device    blkio.weight
blkio.io_queued                   blkio.io_service_time            blkio.reset_stats                blkio.throttle.read_iops_device   blkio.weight_device
blkio.io_queued_recursive         blkio.io_service_time_recursive  blkio.sectors                    blkio.throttle.write_bps_device   cgroup.clone_children
blkio.io_service_bytes            blkio.io_wait_time               blkio.sectors_recursive          blkio.throttle.write_iops_device  cgroup.procs
blkio.io_service_bytes_recursive  blkio.io_wait_time_recursive     blkio.throttle.io_service_bytes  blkio.time                        notify_on_release

其中 blkio.throttle.read_iops_device 对应了设备的读 IOPS,前面一列是设备的编号,可以通过 cat /proc/partitions 查看设备和分区的设备号;后面是 IOPS 上限值:

➜  ~ cat /sys/fs/cgroup/blkio/docker/1402c1682cba743b4d80f638da3d4272b2ebdb6dc6c2111acfe9c7f7aeb72917/blkio.throttle.read_iops_device 
8:0 100

blkio.throttle.read_bps_device 对应了设备的读速率,格式和 IOPS 类似,只是第二列的值为 bps:

➜  ~ cat /sys/fs/cgroup/blkio/docker/9de94493f1ab4437d9c2c42fab818f12c7e82dddc576f356c555a2db7bc61e21/blkio.throttle.read_bps_device 
8:0 1048576

总结

从上面的实验可以看出来,CPU 和内存的资源限制已经是比较成熟和易用,能够满足大部分用户的需求。磁盘限制也是不错的,虽然现在无法动态地限制容量,但是限制磁盘读写速度也能应对很多场景。

至于网络,docker 现在并没有给出网络限制的方案,也不会在可见的未来做这件事情,因为目前网络是通过插件来实现的,和容器本身的功能相对独立,不是很容易实现,扩展性也很差。docker 社区已经有很多呼声,也有 issue 是关于网络流量限制的: issue 26767、issue 37、issue 4763。

资源限制一方面可以让我们为容器(应用)设置合理的 CPU、内存等资源,方便管理;另外一方面也能有效地预防恶意的攻击和异常,对容器来说是非常重要的功能。如果你需要在生产环境使用容器,请务必要花时间去做这件事情。