تو این پست قراره در مورد لایه ها و ساختار ایمیجها در داکر صحبت کنیم و اینکه با ایجاد Dockerfile در هر مرحله چه اتفاقاتی رخ میده.
و در آخرهم برای آشنایی بیشتر با ساختار لایه ها، متوجه میشین که درایور ذخیرهسازی AUFS چیه و چه کاربردی داره.
مروری کلی بر ساختار ایمیج
به خاطر داشته باشین که هر ایمیج داکر از یک سری لایه تشکیل شده.
لایه های ایمیج، فایلهایی هستند که از اجرای برخی دستورات ایجاد میشن.
لایهها شامل تغییرات فایلها هستن و میشه از اونها بین ایمیجهای مختلف استفاده کرد.
مثلا برای ساخت ایمیج جدید نیازی به کپی کردن همهی فایلهای ایمیجقبلی نیست و تنها کافیه لایهی جدید، تغییرات فایلها رو بسازه. با این کار زمان ایجاد ایمیج جدید کاهش پیدا میکنه و در مصرف دیسک سخت هم صرفهجویی میشه.
محتویات هر لایه در Docker host رو میتونین در آدرس /var/lib/docker/aufs/diff مشاهده کنین.
بنابراین اگر کانتینر داکر رو اجرا کنین و بخواین ساختار یک ایمیج رو متوجه بشین، لازمه که جواب این سوالها رو بدونین:
- لایه در داکر به چه معناست؟
- لایهها رو کجا میتونین پیدا کنین؟
- چرا از لایهها استفاده میکنیم؟
در بخشهای بعدی به بررسی دقیق هر یک از سوالات بالا میپردازیم.
اما قبل از اون ساختار ایمیجها و کانتینرها در داکر رو توضیح می دیم. درک چگونگی مدیریت دادههای داکر در ایمیجها و کانتینرها به شما کمک میکنه، بهترین روش رو برای طراحی کانتینرها انتخاب کنین و از مشکلات عملکردی در اجرای برنامهها جلوگیری کنین.
ایمیجها و لایهها
همون طور که در بخش قبلی هم گفتیم ایمیجهای داکر لایههای مختلفی دارن و هر لایه هم نشوندهندهی یک دستور در داکر فایله. هر لایه به جز لایهی کانتینر نهایی، که روی بقیهی لایهها قرار میگیره، صرفا خواندنیست.
هر لایه در واقع یک فایله که تغییراتی نسبت به لایه قبلی در اون رخ داده.
این لایهها روی هم قرار گرفتن.
حالا زمانی که کانتینر جدید ایجاد میکنین یک لایهی نوشتنی جدید در بالای لایههای زیرین اضافه میکنین که به این لایه غالبا "container layer" گفته میشه.
تغییراتی مانند افزودن فایلهای جدید، ویرایش فایلهای موجود و حذف فایلها، در container layer در حال اجرا و قابل نوشتن اعمال میشه.
نمودار زیر یک کانتینر رو بر اساس ایمیج اوبونتو 15.04 نشون میده.

(بر اساس ایمیج اوبونتو 15.04 )
کانتینر و لایهها
به یاد داشته باشین که از هر ایمیج میتونین صدها کانتینر بسازین و هر کانتینر، نمونهای راهاندازی شده از ایمیج خود است. برای آشنایی بیشتر با کانتینرها میتونین این پست سکو رو هم مطالعه کنین.
حالا تفاوت اصلی کانتینر و ایمیج در لایهی قابل نوشتن بالاست.
همهی عملیات نوشتن مثل افزودن دادههای جدید یا اصلاح دادههای موجود در این لایهی قابل نوشتن ذخیره میشه.
با حذف کانتینر، لایه قابل نوشتار هم حذف میشه، اما لایههای ایمیج که در زیر قرار گرفتن، بدون تغییر باقی میمونن.
از اونجایی که هر کانتینر، container layer قابل نوشتار مخصوص به خودش رو داره و تمام تغییرات به طور جداگانه در این container layer ذخیره میشه، چند کانتینر میتونن ایمیج یکسانی رو بین خودشون به اشتراک بذارن (ایمیج اصلی هیچ وقت تغییر نمیکنه) و از دادههای اون ایمیج استفاده کرده و در عین حال دادههای خودشون رو هم حفظ میکنن.
نمودار زیر کانتینرهایی رو نشون میده که ایمیج اوبونتو 15.04 یکسانی رو بین خودشون به اشتراک میذارن.

توجه: اگر برای دسترسی مشترک به دادهی مشابه، به چند ایمیج نیاز دارین، میتونین این داده رو در یک Docker volume ذخیره کنین و اون رو در کانتینر تون mount کنین.
خب حالا تو قسمت بعدی میخوایم با یک مثال شما رو با دستورات Dockerfile بیشتر آشنا کنیم و به سوالاتی که در ابتدای این پست گفته شده، پاسخ بدیم.
لایه در داکر به چه معناست؟
کانتینرهای داکر بلوکهای نگهدارنده برای اپلیکیشنها هستن.
هر کانتینر، ایمیجیست که لایه خواندنی/ نوشتنی اون در بالای دستهای از لایههای فقط خواندنی قرار گرفته.
وقتی برای ساخت ایمیج، دستورات موجود در Dockerfile اجرا میشه، این لایهها هم ساخته میشن.
به این لایهها ایمیجهای میانی هم گفته میشه.
بررسی Dockerfile
به عنوان مثال برای ایجاد ایمیج برنامهی تحت وب node.js، کل Dockerfile ای که نوشتیم رو با هم مرور میکنیم.
این کدها، دستورات اجرا شده برای ایجاد ایمیج هستن که مفهوم هر خط در قسمت بعدی توضیح داده شده.
FROM node:argon (1)
# Create app directory
RUN mkdir -p /usr/src/app (2)
WORKDIR /usr/src/app (3)
# Install app dependencies
COPY package.json /usr/src/app/ (4)
RUN npm install (5)
# Bundle app source
COPY . /usr/src/app (6)
EXPOSE 8080 (7)
CMD [ "npm", "start" ] (8)
(1) : یعنی ایمیجبیس ایمیج جدید چی باشه (node:argon) و با این ایمیج کارمون رو شروع میکنیم.
(2): این دستور با محتویاتش اجرا میشه.
(3) : یک دایرکتوری در /usr در کانتینر ایجاد میشه و قراره همهی کارها تو این مسیر انجام بشه.
(4) : فایل package.json به داخل کانتینر (در مسیر usr/src/app) کپی میشه.
(5) : برای اجرای دستورات لینوکسی از این دستور استفاده میشه.
(6) : علامت . نشوندهندهی دایرکتوری جاری و جایی است که اپلیکیشن ما هم اونجاست.
حالا با نوشتن دستور COPY . /usr/src/app کدی که نوشتین، در داخل کانتینر کپی میشه.
(7) : با این دستور پورت 8080 از کانتینر باز میشه
(فرض کردیم که کد node.js ای که نوشته شده در این پورت سرویس میده).
(8) : دستور پیشفرض استارت کانتینر رو نشون میده.
بررسی لایه های مختلف
همونطور که در نمونه کد زیر مشاهده میکنین، اگر داکر بخواد از Dockerfile ای که نوشتین، کانتینر رو بسازه، هر مرحلهای که برای اینکار انجام میشه، با یک فرمان اجرا شده در Dockerfile برابری میکنه.
هر لایه، فایل ایجاد شده از اجرای اون دستور رو نشون میده.
زمانی که لایه ایجاد شد، شناسهی تصادفی به اون لایه اختصاص داده میشه. مثلا آیدی لایهی مرحله اول برابر است با 530c750a346e.
$ docker build -t expressweb .
Step 1 : FROM node:argon
argon: Pulling from library/node...
...
Status: Downloaded newer image for node:argon
---> 530c750a346e
Step 2 : RUN mkdir -p /usr/src/app
---> Running in 5090fde23e44
---> 7184cc184ef8
Removing intermediate container 5090fde23e44
Step 3 : WORKDIR /usr/src/app
---> Running in 2987746b5fba
---> 86c81d89b023
Removing intermediate container 2987746b5fba
Step 4 : COPY package.json /usr/src/app/
---> 334d93a151ee
Removing intermediate container a678c817e467
Step 5 : RUN npm install
---> Running in 31ee9721cccb
---> ecf7275feff3
Removing intermediate container 31ee9721cccb
Step 6 : COPY . /usr/src/app
---> 995a21532fce
Removing intermediate container a3b7591bf46d
Step 7 : EXPOSE 8080
---> Running in fddb8afb98d7
---> e9539311a23e
Removing intermediate container fddb8afb98d7
Step 8 : CMD npm start
---> Running in a262fd016da6
---> fdd93d9c2c60
Removing intermediate container a262fd016da6
Successfully built fdd93d9c2c60
حالا ببینیم در هر گام چه اتفاقاتی رخ میده؟
در step1 ورژن argon ایمیج node با دستور pull از داکرهاب گرفته میشه.
در step2 دستور mkdir -p /usr/src/app ، در کانتینر موقتی با آیدی 5090fde23e44 اجرا میشه و در انتهای این مرحله چون جای دیگری ازش استفاده نشده این کانتینر حذف میشه.
در اینجا یک لایه به ایمیج اضافه میشه.
….
و در step8 هم دستورات در یک کانتینر موقتی با آیدی a262fd016da6 اجرا میشه و در انتهای این مرحله، چون جای دیگری ازش استفاده نشده این کانتینر میانی هم حذف میشه.
و در پایان هم ایمیجی با آیدی fdd93d9c2c60 ساخته میشه و آمادهی استفاده است.
مشاهدهی لایههای ایمیج
بعد از ساخت ایمیج، میتونین تمام لایههای تشکیلدهندهی ایمیج رو با دستور docker history مشاهده کنین.
ستون ایمیج (ایمیج یا لایهی میانی) در جدول زیر، UUID تولید شده مربوط به هر لایه رو نشون میده.
docker history <image>
$ docker history expressweb
IMAGE CREATED CREATED BY SIZE
fdd93d9c2c60 2 days ago /bin/sh -c CMD ["npm" "start"] 0 B
e9539311a23e 2 days ago /bin/sh -c EXPOSE 8080/tcp 0 B
995a21532fce 2 days ago /bin/sh -c COPY dir:50ab47bff7 760 B
ecf7275feff3 2 days ago /bin/sh -c npm install 3.439 MB
334d93a151ee 2 days ago /bin/sh -c COPY file:551095e67 265 B
86c81d89b023 2 days ago /bin/sh -c WORKDIR /usr/src/app 0 B
7184cc184ef8 2 days ago /bin/sh -c mkdir -p /usr/src/app 0 B
530c750a346e 2 days ago /bin/sh -c CMD ["node"] 0 B
وقتی دستور docker run اجرا بشه، یک ایمیج تبدیل به کانتینر میشه. یعنی کانتینر اجرا میشه:
docker run expressweb
تصویر زیر ساختاری از کانتینر ایجاد شده از فرمان run رو نشون میده.
همونطور که گفتیم کانتینر یک لایهی قابل نوشتن داره که در بالای لایههای ایمیج قرار میگیره.
از اون جایی که لایههای پایین ایمیج فقط خواندنی هستن، این لایهی قابل نوشتن به شما اجازه میده تغییرات رو در کانتینر اعمال کنین.

لایههای ایجاد شدهی ایمیج توسط هر دستور در Dockerfile، به همراه UUID در هر لایه نشان داده شده است. لایهی قابل نوشتن کانتینر در بالا قرار دارد.
لایهها رو در کجا میتونین پیدا کنین؟
برای کاوش در هر لایه و مشاهدهی محتوای هر کدوم از لایهها، باید لایهها رو در Docker host و در مسیر زیر مشاهده کنین:
/var/lib/docker/aufs
حالا اگر داکر رو در سیستم عامل OSX اجرا کنین، در واقع Docker host شما ماشین مجازی لینوکسه که docker machine نامیده میشه.
-در سیستم عامل OSX به دستور ssh ، docker machine رو اضافه کنین تا بتونین دایرکتوری aufs رو مشاهده کنین:
$ docker-machine ssh default
## .
## ## ## ==
## ## ## ## ## ===
/"""""""""""""""""\___/ ===
~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ / ===- ~~~
\______ o __/
\ \ __/
\____\_______/
_ _ ____ _ _
| |__ ___ ___ | |_|___ \ __| | ___ ___| | _____ _ __
| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__|
| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__| < __/ |
|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_|
Boot2Docker version 1.12.3, build HEAD : 7fc7575 - Thu Oct 27 17:23:17 UTC 2016
Docker version 1.12.3, build 6b644ec
docker@default:~$ df -h
Filesystem Size Used Available Use% Mounted on
tmpfs 896.2M 192.1M 704.1M 21% /
tmpfs 497.9M 0 497.9M 0% /dev/shm
/dev/sda1 17.9G 2.4G 14.6G 14% /mnt/sda1
cgroup 497.9M 0 497.9M 0% /sys/fs/cgroup
Users 464.8G 110.0G 354.8G 24% /Users
/dev/sda1 17.9G 2.4G 14.6G 14% /mnt/sda1/var/lib/docker/aufs
docker@default:~$ ls /mnt/sda1/var/lib/docker/aufs
diff layers mnt
دایرکتوری /var/lib/docker/aufs به سه دایرکتوری mnt ،layers ،diff اشاره میکنه که:
- لایههای ایمیج و محتویاتش در دایرکتوری diff ذخیره میشن.
- نحوهی چیدمان لایهها در دایرکتوری layers قابل مشاهده است.
- و کانتینرهای در حال اجرا در دایرکتوری mount ،mnt شدن.
آشنایی با درایور ذخیرهسازی AUFS
حالا بد نیست، برای درک بهتری از چگونگی کار لایهها، با درایور ذخیرهسازی AUFS و مفاهیم کلیدی مرتبط با AUFS بیشتر آشنا بشیم:
Union Mount
به ترکیب دایرکتوریهای متعدد در یک دایرکتوری، union mounting گفته میشه و تمام محتویات دایرکتوریها در یک سیستم فایل واحد قرار میگیره.
AUFS
AUFS (مخفف سیستمفایل یکپارچه چندلایهای پیشرفته)، میتونه یک union mount رو برای سیستمفایلهای لینوکس پیادهسازی کنه.
یعنی AUFS سیستمفایلی لایهایست که میتونه چندین دایرکتوری رو روی هم قرار بده و برای نمایش یک سیستمفایل واحد، چندین لایه رو با هم ادغام میکنه.
برای رسیدن به این هدف، AUFS از union mount استفاده میکنه.
درایور ذخیرهسازی AUFS
درایور ذخیره سازی AUFS، لایههای ایمیج داکر رو با استفاده از سیستم union mount پیادهسازی میکنه.
AUFS Branches
به هر لایهی ایمیج داکر، یک AUFS Branches گفته میشه.
استفاده از سیستمفایلهای Union بسیار جالبه.
چون این سیستمفایلها، فایلهای هر لایه ایمیج رو با هم ترکیب میکنن و اونها رو به عنوان یک دایرکتوری واحد صرفا خواندنی، در union mount point نمایش میدن.
حالا اگه تو لایههای مختلف فایلهای تکراری وجود داشته باشه، اون چیزی که نشون داده میشه فایل موجود در لایهی بالاتره.
در شکل زیر هر لایه از ایمیج اوبنتو (که به اون AUFS branch گفته میشه)، به همراه فایلهایش روی Docker host و در سیستمفایل Union ذخیره شده.
علاوه بر این، لایهها به صورت یکپارچه در union mount point نشان داده شده که بر روی container layer قابل نوشتن قرار گرفتن.

و سخن آخر اینکه
چرا برای داکر از سیستم union mount استفاده میکنیم؟
با استفاده از سیستمفایل union، به هر لایه اجازه میدین که مجددا بتونه توسط تعداد نامحدودی ایمیج مورد استفاده قرار بگیره.
با این کار از فضای دیسک به طور بهینه استفاده شده و ایمیج ها سریعتر ساخته میشن.
علاوه بر این لایه خواندنی نوشتنی بالا این اجازه رو میده تا بتونین ایمیج رو در صورت لزوم تغییر بدین و لایههای فقط خواندنی زیرین به دلیل ایزوله کردن محتویات سیستم فایل، یکپارچگی کانتینر رو حفظ میکنن.
اگه این مطالب براتون جالب بود، میتونین برای آشنایی بیشتر منابع زیر رو هم مطالعه کنین:
- OverlayFS به عنوان جانشین احتمالی AUFS
- داکیومنت های بیشتر در مورد ایمیج و کانتینرها و درایور AUFS
- ساختار یک کانتینر
منابع:
Digging into Docker layers
About storage drivers
Layering of Docker images