分类目录归档:容器技术

Borg

4月份Google发了一篇介绍大规模集群管理系统Borg的论文『Large-scale cluster management at Google with Borg』,因为我近期也在做相关方面的工程,所以就在第一时间读了一遍,对Borg的架构及其特性有了一个大概的了解。

论文大致可分为7个部分:

1. Introduction(User perspective):用户视角的Borg介绍
2. Borg architecture: Borg整体架构
3. Scalability:Borg系统可扩展性
4. Availability:Borg系统本身的高可用性和用户服务的高可用性
5. Utilization:资源利用率
6. Isolation:隔离性
7. Lessons:经验教训

1. Introduction(User perspective)

Borg是什么?

Borg是一个集群管理工具,可以管理、调度、启动、重启、监控应用程序。可以想象成它是一个集群操作系统,产品或服务无需关心硬件层的差异,只需声明所需的资源类型和配额即可。Borg有以下特性:

1) 物理资源利用率高。
2) 服务器共享,在进程级别做隔离。
3) 应用高可用,故障恢复时间短。
4) 调度策略灵活。
5) 应用接入和使用方便。提供了完备的Job描述语言,服务发现,实时状态监控和诊断工具。

对应用来说,它能带来的几点好处:

1) 对外隐藏底层资源管理和调度、故障处理等。
2) 实现应用的高可靠和高可用。
3) 足够弹性,支持应用跑在成千上万的机器上。

Borg定义了几个概念:

1) Workload

Borg将运行其上的服务(workload)分为两类:
* prod:在线任务,长期运行、对延时敏感、面向终端用户等,比如Gmail,Google Docs, Web Search服务等。
* non-prod:离线任务,也称为批处理任务(batch),比如一些分布式计算服务等。

2) Cell

Cell是一个逻辑上的概念,它是一系列服务器的集合。一个Cell上跑一个集群管理系统Borg。其实在Cell的概念之上还有一个Cluster的概念,一个Cluster可以有一个或多个Cell(规模、用途各异),并且归属于一个IDC。Cell的规模有大有小,一个中等规模的Cell大约含有1w台服务器。一般来说,Cell中的服务器都是异构的,它们可能有着不同的规格(CPU, RAM, disk, network)、处理器类型,有着性能和容量上的差异等。

通过定义Cell可以让Borg对服务器资源进行统一抽象,作为用户就无需知道自己的应用跑在哪台机器上,也不用关心资源分配、程序安装、依赖管理、健康检查及故障恢复等。

3) Job和Task

用户以Job的形式提交应用部署请求。一个Job包含一个或者多个相同的Task,每个Task运行相同的一份应用程序,Task数量就是应用的副本数。Job也是逻辑上的概念,Task才是Borg系统的可调度单元。每个Task可以对应到Linux上的一组进程,这组进程运行在LXC容器里,Google解释了采用容器技术的原因:1. 硬件虚拟化成本太高。2. Borg开始研发那会儿,虚拟化技术并不成熟。

每个Job可以定义一些属性、元信息和优先级,优先级涉及到抢占式调度过程。Borg对Job的定义了如下4个优先级:monitoring, production, batch, and best effort(test),其中monitoring和production属于prod workload,batch和test属于non-prod workload。高优先级可以抢占低优先级,相同优先级的Job之间不能互相抢占,另外,prod workload的Job不能被抢占,也就是说,能被抢占的只有低优先级的batch和test任务。

以下是Job和Task的生命周期状态图:
Job的生命周期

从状态图中可见,无论是pending状态还是running状态的任务,Borg都支持它们动态更新,实时生效,这依赖于描述Job和Task所采用的BCL语言(GCL的一个变种)特性,具体原理和实现不明。

4) Naming

Borg的服务发现通过BNS(Borg name service)来实现。Task调度和部署完毕后,Borg把Task的主机名和端口号写入Chubby(解决分布式环境下一致性问题的文件系统),服务调用方通过RPC系统去自动发现服务。举例说明一个Task在服务发现配置中心的命名规则:
50.jfoo.ubar.cc.borg.google.com可表示在一个名为cc的Cell中由用户ubar部署的一个名为jfoo的Job下的第50个Task。

有了服务发现,即使Task下线、被抢占、故障恢复等情况出现,整个服务仍然可用的,当然服务前提是多副本的。

2. Borg architecture

Borg的整体架构分成两部分:Borgmaster和Borglet。整体架构如下:
Borg架构图

1. Borgmaster:Borgmaster是整个Borg系统逻辑上的中心节点,它又分成两个部分,Borgmaster主进程和scheduler调度进程。职责划分如下:

1) Borgmaster主进程:
* 处理客户端RPC请求,比如创建Job,查询Job等。
* 维护系统组件和服务的状态,比如服务器、Task等。
* 负责与Borglet通信。

2) scheduler进程:

用户提交Job后,Borgmaster主进程将Job信息持久化到Paxos存储池中,并且将其Task丢到一个Pending队列里。由scheduler进程定期轮询Pending队列,对Task进行调度执行。调度策略为:从高到低,相同优先级采用round-robin策略。

调度过程分为两阶段:

阶段一: feasibility checking,初步的调度可行性分析,确定一批符合调度要求的服务器。
阶段二: scoring,根据一些算法对阶段一中筛选出的服务器进行打分,选出最适合调度的一台机器。scoring过程可考虑的因素较多,比如可以考虑怎么调度才能使被抢占的低优先级任务数量最少,考虑目标服务器已经存在目标任务的包,考虑目标任务的跨域(power and failure domain)部署,考虑在目标机器上进行高低优先级任务的混合部署。

Borg前后经历了三种scoring模型:
1) E-PVM: 对多维度的资源类型降维,最终得到一个一维的数值。这是一种"worst fit"模型,各服务器负载尽量平均,好处是能够容忍应用的峰值,但是会增加资源碎片,导致大型应用找不到合适服务器部署。
2) "best fit": 与第一类“worst fit”模型相反,它会尽量利用服务器资源,优先将已分配服务器资源跑满。这种模型的好处是不会产生很多资源碎片,坏处是要求应用申请时的配额一定要准确,否则可能被kill掉。另外这种模型对存在峰值的应用类型容忍能力有限。
3) hybrid: 它综合了"worst fit"和"best fit",Borg目前采用的是这种模型,它的优势在于避免产生无法利用的资源碎片。所谓资源碎片,是指服务器某类资源耗尽,但是其他类型资源充足。比如说内存已经分配完,但是CPU使用率仍旧很低,那么这部分CPU资源是无法被利用的,就产生了CPU碎片。

可见从理论上看Borg的调度算法很完善,但是过于完善却不利于实际应用,因为每次调度都会涉及大量计算。Google从实用型角度做了一些优化:

1) Score caching: 对一次完整计算过后结果进行缓存,当服务器或者任务的状态未发生变更或者变更很少时,直接采用缓存数据,避免重复计算。
2) Equivalence classes: 因为一个Job下的所有Task都是等价的,所以可以采用等价类的思路,无需为Job中每个Task进行feasibility checking和scoring,调度多个相同的Task只需计算一次。
3) Relaxed randomization: 引入一些随机性,即每次随机选择一些机器,只要符合需求的服务器数量达到一定值时,就可以停止计算,无需每次对Cell中所有服务器进行feasibility checking。

经过上述3点优化,实验结果表明:一次未经优化的调度过程需要3天,优化后只需几百秒!另外,在实际应用中,一个Job从用户提交,到最后得到调度和执行,平均消耗时间是25s,包安装占了其中的80%时间。

2. Borglet

Borglet是部署在所有服务器上的Agent,负责接收Borgmaster进程的指令。

3. Scalability

Google在Scalability部分低调地炫耀了一把,他们在论文中宣称,目前还不确定关于中心化架构的Borg系统的极限在哪里。关于“炫耀”,在论文中多处可见,比如作者多次提到Borg已稳定运行10多年,比如作者在介绍架构设计的时候提到虽然Borg不是业界第一个集群管理系统的解决方案,但在量级上却无人出其左右。目前,一个Borg系统能处理的服务器数量级在万台这个级别,每分钟处理的Task数量超过10000,部署Borgmaster的机器规格为10-14核、50GB内存。

扩展性从三个方面的设计上说明:

1). Borgmaster主进程有5个副本,一主四从。每个副本都在内存中维护一份整个集群的状态,另外,集群状态也会被持久化到一个分布式的高可用Paxos存储系统上。在Cell初始化或者主节点挂掉的情况下,存活的副本会进行Paxos选举,选举新的主节点。主节点会在Chubby(提供了分布式的锁服务)获得一把锁来标记自己的主节点身份。所有能涉及改变集群状态的操作都由主节点完成,比如用户提交Job、结束Task执行或者机器下线之类的操作,主节点可读可写,从节点只读。主节点选举和failover的过程大概会持续10秒,在一些大规格Cell中可能会需要1分钟。

2). scheduler进程也是多副本运行的设计,一主多从。副本个数没有说明,应该没有限制。scheduler进程做的事情是状态无关的,从节点定期从主节点上获取当前集群的状态变更部分(含已分配和待调度任务之类的信息),合并到本地的集群状态副本中,然后根据已有信息对任务进行调度计算,计算完毕将调度结果通知主节点,由主节点来决定是否进行调度。比如主节点发现scheduler副本本地缓存的集群状态已经过时,那么计算出来的调度结果也是无效的,那么就不会采纳。也就是说,scheduler从节点只计算,不进行实际调度。

3). Borgmaster采用轮询的方式定期从Borglet获取该节点的状态。之所以采用"拉"模型,而不是Borglet的“推”模型,Google的考虑是:在Borg管理的集群规模上,采用“推”模型将很难进行流量控制,但是“拉”模型可以通过动态改变轮询周期来进行控制,另外,在“推”模型中,当Borgmaster从故障中恢复时,会瞬间产生流量风暴。其实在我看来,采用“推”还是“拉”模型,可能更多的是由Borgmaster本身是否带状态来决定的。Borg采用的是带状态的master设计(区分主从),因此如果Borglet采用了“推”模型,当产生上述的流量风暴时便不可控,如果Borgmaster是无状态的,那么通过一些负载均衡技术就很容易化解所谓的recovery storms,便无需考虑流量控制。此外,每个Borgmaster从节点都有自己负责轮询和通信的固定数量的Borglet,这是通过进程内部的link shard模块实现的,Borglet每次都会上报自己的全量数据,Borgmaster从节点收到全量数据后,与自己本地缓存的集群状态进行比对,如果发现该Borglet有状态变化,则把状态变化的增量部分汇报给Borgmaster主节点,由其进行最终的集群状态变更操作,经过从节点对数据的预处理后,主节点能少处理很多信息。

4. Availability

高可用性设计分为两个方面:

1) 应用高可用

* 被抢占的non-prod任务放回pending queue,等待重新调度。
* 多副本应用跨故障域部署。所谓故障域有大有小,比如相同机器、相同机架或相同电源插座等,一挂全挂。
* 对于类似服务器或操作系统升级的维护操作,避免大量服务器同时进行。
* 支持幂等性,支持客户端重复操作。
* 当服务器状态变为不可用时,要控制重新调度任务的速率。因为Borg无法区分是节点故障还是出现了短暂的网络分区,如果是后者,静静地等待网络恢复更利于保障服务可用性。
* 当某种"任务@服务器"的组合出现故障时,下次重新调度时需避免这种组合再次出现,因为极大可能会再次出现相同故障。
* 记录详细的内部信息,便于故障排查和分析。

保障应用高可用的关键性设计原则是:无论何种原因,即使Borgmaster或者Borglet挂掉、失联,都不能杀掉正在运行的服务(Task)。

2) Borg系统高可用

实践中,Borgmaster的可用性达到了4个9(99.99%)。

* Borgmaster组件多副本设计。
* 采用一些简单的和底层(low-level)的工具来部署Borg系统实例,避免引入过多的外部依赖。
* 每个Cell的Borg均独立部署,避免不同Borg系统相互影响。

5. Utilization

解决资源利用率问题是类似Borg这样的集群管理系统存在的核心价值,从已有资料来看,只有Borg完全做到了,并且能给出数据佐证。百度的Matrix也号称做到了,具体成效待实践证明。要提升资源利用率,“混部”是一个很好的解决思路,Borg和Matrix也是采用这样的思路,通过将在线任务(prod)和离线任务(non-prod, batch)混合部署,空闲时,离线任务可以充分利用计算资源,繁忙时,在线任务通过抢占的方式保证优先得到执行,合理地利用资源。这背后需要一个很完备的调度算法做支撑,遗憾的时,Borg论文中并没有过多涉及相关的技术,只能从一些数据中了解Borg带来的收益:

* 98%的服务器实现了混部。
* 90%的服务器中跑了超过25个Task和4500个线程。
* 在一个中等规模的Cell里,在线任务和离线任务独立部署比混合部署所需的服务器数量多出约20%-30%。可以简单算一笔账,Google的服务器数量在千万级别,按20%算也是百万级别,大概能省下的服务器采购费用就是百亿级别了,这还不包括省下的机房等基础设施和电费等费用。

6. Isolation

隔离性从两方面讨论:

1. 安全性隔离:采用chroot jail实现。Borg时代还没有namespace技术,chroot可以认为是一个简略版的mnt namespace。
2. 性能隔离:采用基于cgroup的容器技术实现。前文已经提到,Borg将任务类型分成在线和离线两种,在线任务(prod)是延时敏感(latency-sensitive)型的,优先级高,而离线任务(non-prod,batch)优先级低,Borg通过不同优先级之间的抢占式调度来优先保障在线任务的性能,牺牲离线任务。另一方面,Borg将资源类型也分成两类,可压榨的(compressible)和不可压榨的(non-compressible)。compressible类型的资源比如CPU、磁盘IO带宽,当这类资源成为瓶颈时,Borg不会Kill掉相应的任务。non-compressible类型的资源比如内存、磁盘空间,当这类资源成为瓶颈时,Borg会Kill掉相应的任务。

7. Lessons

Google在论文中总结了Borg系统设计的优缺点,并在它的开源版本Kubernetes(k8s)中进行了传承和改善。

关于一些不好的设计:
* Job和Task之间的关系过于耦合,不能灵活指定Task。k8s中引入label机制,可以通过label指定任何Task(k8s中称为Pod)集合。
* 部署在同一台服务器上的Task共享一个IP,通过端口号区分彼此。这使得所有Task共享服务器上的端口空间,使得端口本身也成为一种竞争资源,成为调度的一个考虑因素。k8s中每个Pod独享IP和该IP上的整个端口空间。
* Borg系统中存在一些特权用户,使用法变得复杂。

关于一些好的设计:
* 对用户暴露内部的运行和调试日志,用户可以自行排查。
* 微服务架构设计。

虽说Google在k8s中重新设计了一些Borg中不好的地方,但那都是一些皮毛,真正核心的东西比如资源调度根本没在k8s中体现。

以上是我对Borg论文学习总结,中间夹带了一些自己的理解,如有错误,还请指正。

--EOF--

Docker集群性能数据监控与简单选型

Docker要在生产环境上使用,监控工具必不可少。传统物理机或着虚拟机通常可以通过部署一些agent收集数据,上报到中心节点进行数据聚合和存储,再通过一些UI工具进行可视化展现。与之相比,容器因为它的轻量级特性,一个容器内部通常只包含一个服务进程,通过在容器内部部署额外的性能数据采集agent非常不优雅;另一个原因,top、free、sysstat等常用性能诊断命令在容器中往往拿到的是宿主机的数据,无法准确获取容器数据。因此各种在宿主机上采集容器数据的工具(链)冒了出来。

市面上关于Docker的监控工具五花八门,但是相对成熟的却又很少。Google的cAdvisor已经是属于较成熟的一类,它会在每个宿主机节点上部署cAdvisor进程,默认对外暴露8080端口,用户可通过其提供的Web服务访问当前节点和各容器的性能数据(CPU、内存、网络、磁盘、文件系统等等),非常详细。展现方式有两种,一种是API,返回标准JSON格式数据,另一种是友好的图表方式,如下图:
cAdvisor

默认cAdvisor是将数据缓存在内存中,因此数据展示能力有限,当然它也提供不同的持久化存储后端:云端的Google BigQuery或者本地端的InfluxDB,通过-storage_driver启动参数指定。

当使用InfluxDB作为cAdvisor的存储后端时,cAdvisor会在InfluxDB中建立一个名为stats的series,所有容器性能数据都存储到这张表里,主要记录数据包括time、tx_errors、rx_bytes、container_name、rx_errors、fs_limit、memory_usage、memory_working_set、fs_device、cpu_cumulative_usage、machine、tx_bytes、fs_usage等等。

在一个大规模部署的Docker集群中,往往会选用各种集群管理和Docker编配工具,比如KubernetesFigSwarm等。网上各种Docker集群工具的选型文章也多得很,可以参考阅读。只能说,由Google在主推,并且有其闭源版本Borg在Google内部稳定运行10多年的口碑,Kubernetes(k8s)虽然出来得晚,但是社区发展得相当快,大有后来居上的势头。

那么假设选择了k8s作为Docker集群管理工具,容器性能数据监控如何选型?

k8s会在每个node(minor)节点上部署一个Kubelet,默认暴露10250端口,Kubelet主要负责容器的生命周期管理和状态维护,以及监控数据采集。实际上,Kubelet也是通过cAdvisor来采集容器性能数据的,所以需要在Kubelet的启动参数中增加--cadvisor_port参数,它表示本地的cAdvisor服务端口号。Google同时也提供了开源组件heapster,用作对Docker集群的监控,heapster原生支持k8s和CoreOS中容器的性能数据采集。当heapster配合k8s运行时,需要指定kubernetes_master的地址,heapster通过k8s得到所有node节点地址,然后通过访问对应的node ip和端口号(10250)来调用目标节点Kubelet的HTTP接口,再由Kubelet调用cAdvisor服务获取该节点上所有容器的性能数据,并依次返回到heapster进行数据聚合。heapster聚合的metric可分为以下几类:

uptime
cpu/usage
memory/usage
memory/page_faults
memory/working_set
network/rx
network/rx_errors
network/tx
network/tx_errors
filesystem/usage

同cAdvisor类似,heaspter也支持多种存储后端,比如默认的memory,表示存内存,另外还有influxdb、bigquery、gcm,可由-sink启动参数指定。如果持久化到InfluxDB,那么根据metric的分类,InfluxDB会生成以下series:

cpu/usage_ns_cumulative
filesystem/usage
memory/page_faults_gauge
memory/usage_bytes_gauge
memory/working_set_bytes_gauge
network/rx_bytes_cumulative
network/rx_errors_cumulative
network/tx_bytes_cumulative
network/tx_errors_cumulative
uptime_ms_cumulative

这里需要注意的是,metric的分类有两种,一种是类似cpu使用时间、网络流入流出量,聚合的是累计值,在serie名称中用cumulative表示,另一种是类似内存使用量,聚合的是瞬时值,在serie名称中用gauge表示。

有了InfluxDB的数据持久化存储之后,剩下的就是数据的展现功能,可以说,任何支持InfluxDB作为存储后端的UI系统(dashboard)都可以用来作为展现层,比如InfluxDB官方自带的图形界面,也可以对接Grafana,页面相当酷炫。作为一个平台管理人员,用heapster(cAdvisor) + InfluxDB + Grafana应该可以取得不错的用户体验,但是如果把k8s用在多租户的场景下,由于Grafana的可编程能力有限,对接起来不是很合适,较好地方式还是基于InfluxDB的原始数据(支持类SQL查询)自己实现UI,这样与容器集群管理平台的契合度也高一些。

--EOF--

Docker容器和镜像存储机制

Docker的存储机制采用了非常灵活的模块化设计,目前支持5种存储引擎,分别为aufs、btrfs、device mapper、vfs和overlay。它们的共同特点都是实现了graphdriver.Driver接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type ProtoDriver interface {
  String() string
  //创建layer
  Create(id, parent string) error 
  //删除layer
  Remove(id string) error 
  //mount, 获取容器挂载点
  Get(id, mountLabel string) (dir string, err error) 
  //umount文件系统
  Put(id string) 
  Exists(id string) bool
  Status() [][2]string
  Cleanup() error
}
 
type Driver interface {
  ProtoDriver
  Diff(id, parent string) (archive.Archive, error)
  Changes(id, parent string) ([]archive.Change, error) 
  ApplyDiff(id, parent string, diff archive.ArchiveReader)(size int64,err error)
  DiffSize(id, parent string) (size int64, err error)
}

所以,只要实现了存储驱动接口定义的方法,就可以扩展出一种存储引擎。想要更换存储引擎有两种方法:
1. docker daemon进程启动时指定-s参数:docker –s aufs。
2. 修改配置文件:DOCKER_OPTS="--storage-driver=aufs"。

在Docker存储模型中,bootfs同宿主机,rootfs则是由多个镜像层和一个容器层构成,其中镜像层只读,容器层可读写,多个镜像层之间有父子关系,下层作为上层镜像的父镜像,最下面的镜像称为base images(基础镜像),相关定义可以参考官网解释
docker image layer

aufs是Docker最早支持的一种存储引擎,它的工作机制和优缺点在前文『aufs文件系统』中已有介绍,aufs能将不同的目录挂载到某一目录下,将各个源目录下的内容联合到目标目录下,并可对不同目录进行权限控制。这个特性非常契合Docker的存储模型:
1. 镜像分层模型对应aufs中的分支(源目录)。
2. 镜像层对应aufs的ro分支,只读;容器层对应aufs的rw分支,可读写。

默认配置下,Docker镜像和容器存储路径($DOCKROOT)位于/var/lib/docker,如果选择的是aufs文件系统作为存储引擎,那么它的子目录树结构(基于docker 1.4.1)应该如下:

1
2
3
4
5
6
7
8
# tree .
├── aufs
│   ├── diff   镜像和容器每层的差异内容
│   ├── layers   镜像和容器每层的继承关系
│   └── mnt  容器挂载点
├── containers  容器配置文件,环境变量和日志文件
├── graph 镜像详情、大小等
└── repositories-aufs  镜像摘要

举例说明,当前本地镜像库有一个ubuntu:14.04的镜像,它的层级关系如下:

1
2
3
4
5
6
# docker images -t
└─511136ea3c5a Virtual Size: 0 B
  └─3b363fd9d7da Virtual Size: 192.5 MB
    └─607c5d1cca71 Virtual Size: 192.7 MB
      └─f62feddc05dc Virtual Size: 192.7 MB
        └─8eaa4ff06b53 Virtual Size: 192.7 MB Tags: ubuntu:14.04

那么在aufs/diff目录(相对于$DOCKROOT,下同)下会有以各个层级id命名的目录,每个目录存储着与它父镜像之间的差异:

1
2
3
4
5
6
7
# ls -l aufs/diff/
total 20
drwxr-xr-x 21 3b363fd9d7dab4db9591058a3f43e806f6fa6f7e2744b63b2df4b84eadb0685a
drwxr-xr-x  2 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158
drwxr-xr-x  6 607c5d1cca71dd3b6c04327c3903363079b72ab3e5e4289d74fb00a9ac7ec2aa
drwxr-xr-x  2 8eaa4ff06b53ff7730c4d7a7e21b4426a4b46dee064ca2d5d90d757dc7ea040a
drwxr-xr-x  3 f62feddc05dc67da9b725361f97d7ae72a32e355ce1585f9a60d090289120f73

aufs/layers目录下有以各个层级id命名的文件,文件内容为该层所有的祖先镜像id。例如

1
2
3
4
5
# cat aufs/layers/8eaa4ff06b53ff7730c4d7a7e21b4426a4b...
f62feddc05dc67da9b725361f97d7ae72a32e355ce1585f9a60d090289120f73
607c5d1cca71dd3b6c04327c3903363079b72ab3e5e4289d74fb00a9ac7ec2aa
3b363fd9d7dab4db9591058a3f43e806f6fa6f7e2744b63b2df4b84eadb0685a
511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158

graph目录中存储的是每一层镜像的详细信息和大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# tree graph/
graph/
├── 3b363fd9d7dab4db9591058a3f43e806f6fa6f7e2744b63b2df4b84eadb0685a
│   ├── json
│   └── layersize
├── 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158
│   ├── json
│   └── layersize
├── 607c5d1cca71dd3b6c04327c3903363079b72ab3e5e4289d74fb00a9ac7ec2aa
│   ├── json
│   └── layersize
├── 8eaa4ff06b53ff7730c4d7a7e21b4426a4b46dee064ca2d5d90d757dc7ea040a
│   ├── json
│   └── layersize
└── f62feddc05dc67da9b725361f97d7ae72a32e355ce1585f9a60d090289120f73
    ├── json
    └── layersize

其中,json为该层元信息,layersize为该层大小。

此时用ubuntu:14.04镜像起一个容器:

1
2
# docker run -it -d ubuntu:14.04 /bin/bash
4262b0311350933de15936136b4c9142635782f8fd1a015d2fd2d6c54ed05efb

新建容器的操作会在aufs下三个子目录中分别新建两个以容器id为名的文件/目录,例如4262b031135...和4262b031135...-init,其中4262b031135...-init表示容器的初始层,它记录的信息和ubuntu:14.04镜像的最上层一致。所有在新建容器中的文件操作最终都会记录到aufs/diff/4262b031135...目录中,比如:
1. 新建文件,修改文件。
2. 删除文件和目录,以.wh.{obj}标记。在联合文件系统中被称为除白(Whiteout)对象。
3. 删除目录后新建,以.wh..wh..opq标记。在联合文件系统中被称为不透明(Opaque directory)对象。

aufs/mnt目录是容器的挂载点,通过df命令和mount -v命令进行确认,另外容器的操作日志、环境变量、元信息等也会在containers目录以容器id命名的目录中。容器运行后,可以在sysfs目录中找到对应的从上到下的镜像层次结构,读写权限一目了然(si可以通过mount -v查询):

1
2
3
4
5
6
7
8
# cat /sys/fs/aufs/si_13ac476258e8c5e8/br*
/var/lib/docker/aufs/diff/4262b031135...=rw
/var/lib/docker/aufs/diff/4262b031135...-init=ro
/var/lib/docker/aufs/diff/8eaa4ff06b5...=ro
/var/lib/docker/aufs/diff/f62feddc05d...=ro
/var/lib/docker/aufs/diff/607c5d1cca7...=ro
/var/lib/docker/aufs/diff/3b363fd9d7d...=ro
/var/lib/docker/aufs/diff/511136ea3c5...=ro

aufs为Docker镜像存储带来了可重用性、权限分明、层次清晰等优点后,也带来了它的固有缺陷:
1. 写时复制(Copy On Write),性能不够好。
2. 最大层数限制(127层)。

关于绕开最大层数限制,在『aufs文件系统』中已有讨论,这里针对Docker的使用场景再进行一次归纳:
1. 更换docker存储驱动类型:device mapper, btrfs ...
2. 重新编译aufs: CONFIG_AUFS_BRANCH_MAX_32767=y
3. 精简Dockerfile指令:RUN指令合并,脚本化
4. docker export & docker import

--EOF--