داکر رجیستری، مخزنی از ایمیجها با ورژنهای مختلف است که به افراد اجازه میده با دسترسی به این مخزن، از ایمیج موردنظرشون استفاده کنن و کانتینرشون رو اجرا کنن. docker hub یکی از معروفترین رجیستریهای عمومی داکر برای آپلود و کار با ایمیجهاست. این رجیستری امکاناتی مثل خدمات میزبانی و رجیستریهای عمومی و خصوصی رو در اختیار کاربران قرار میده.
بیشتر افراد و شرکتها برای استقرار نرمافزارها و سرویسهاشون ترجیح میدن رجیستری خصوصی خودشون رو داشته باشن. در این حالت به جای قرار دادن مستقیم کلاسترها در داکرهاب عمومی، میتونن کلاسترهای فرضی خودشون رو به یک داکر رجیستری خصوصی وصل کنن.
حالا اگه از داکر رجیستری خودتون برای push کردن و انجام به روزرسانیهای مداوم استفاده میکنین، احتمالاً متوجه شدین که به مرور زمان، فضای mount رجیستری، پر میشه و حالا وقت اون رسیده که بتونین رجیستریتون رو پاکسازی کنین. در این پست میخوایم نحوهی انجام این کار رو بهتون آموزش بدیم.
چرا باید پاکسازی رو انجام بدین؟
در حالی که نصب و میزبانی از داکر رجیستری بسیار ساده است و به راحتی با دستور docker یا با استفاده از docker-compose-file قابل انجامه، مدیریت خوشهی داکر و پاک کردن ایمیجهای قدیمی و غیرضروری داکر، میتونه یک کار چالش برانگیز باشه، اما در وهلهی اول چرا باید پاکسازی رو انجام بدین؟
داکر رجیستری، ایمیجهای داکر رو روی فایل سیستم ذخیره میکنه. پس همیشه باید فضای ذخیرهسازی کافی روی میزبان داکر رجیستری وجود داشته باشه تا بتونین ایمیجهای داکر رو ذخیره کنین و مطمئن باشین که همیشه برای ایمیجهای جدید داکر، فضای ذخیرهی کافی وجود داره.
بسته به اندازهی پروژه ممکنه هر روز صدها ایمیج داکر ساخته بشه و به همراه سورسکدها در اختیار کاربر قرار بگیره. بعد از تست هر ایمیج، میشه اونو از رجیستری پاک یا حذف کرد. در حقیقت، میشه گفت اکثر ایمیجهای داکر رو میشه حذف کرد، به جز ایمیجهایی که دارای tag های ویژهای هستن، تگهایی مثل develop ،master ،latest و version.
در این پست، نحوهی تنظیم یک bash script رو برای خودکارسازی عملیات پاکسازی داکر رجیستری توضیح میدیم. حالا ببینیم برای تنظیم این script، چه کارهایی رو باید انجام بدین؟!
- اول باید داکر رجیستری رو به کمک docker-compose file زیر مستقر کنین. با نوشتن دستورات زیر، یک کانتینر داکر رجیستری، به صورت محلی شروع به کار میکنه:
$> git clone git@github.com:wshihadeh/docker-registry.git
$> cd docker-registry
$> docker-compose up -d
- قدم بعدی اینه که برای این داکر رجیستری، script پاکسازی رو پیادهسازی کنین.
قبل از نوشتن script پاکسازی، میخوایم بگیم که چطوری داکر رجیستری، ایمیجهای داکر رو در فایل سیستم ذخیره میکنه، و چه ابزاری رو میتونین برای انجام پاکسازی در داکر استفاده کنین؟!
آشنایی با ساختار یک ایمیج
همون طور که در تصویر زیر میبینین، یک ایمیج داکر میتونه چند تا tag مختلف داشته باشه. علاوه بر این هر ایمیج، یک digest داره که مقداری منحصر به فرد برای اون ایمیج محسوب میشه.
هر ایمیج توسط این digest شناسایی میشه. اگه دو ایمیج با نام یکسان myImage داشته باشین و اونا رو با master tag، به رجیستری Push کنین، در داخل داکر رجیستری، توسط دو digest مختلف شناسایی میشن.
یک tag هم از چند لایه تشکیل شده. به لیستی از لایهها برای اون digest خاص، یک manifest گفته میشه.
علاوه بر این هر لایه با یک blob در ارتباطه.
حالا لایههایی وجود دارن که توسط هیچ کدوم از tag ها استفاده نمیشن، به این لایهها، لایههای بلااستفاده گفته میشه. در این صورت blob های مربوطه هم بلااستفاده محسوب میشن. در نتیجه برای کم کردن فضای دیسک، میتونین این لایهها رو حذف کنین.
همونطور که میبینین، لایهها بین manifest ها (tag ها) به اشتراک گذاشته میشن.
هر کدوم از manifest ها به لایهای اشاره میکنن. حالا تا زمانی که یک manifest به لایهای اشاره میکنه، نمیتونین اون لایه رو پاک/حذف کنین.
ذخیرهسازی ایمیجها در فایل سیستم
حالا که با ساختار ایمیجها آشنا شدین، میتونیم بگیم که داکر رجیستری، با استفاده از objectهای زیر، ایمیجها رو در فایل سیستم ذخیره میکنه:
- Blobs: لایههای واقعی که در کنار همدیگه، یک ایمیج داکر رو تشکیل میدن. blobها فایلهایی هستن که فضای دیسک رو زیاد اشغال میکنن و توسط Garbage Collection، پاکسازی میشن.
- Image Manifest: متادیتایی که میتونه blobها رو به ترتیب صحیح در ایمیج داکر به هم مرتبط کنه.
- Manifest List: لیستی از Image manifestها، برای یک یا چند سیستم عامل. (یک manifest list، از ایمیجهایی ایجاد شده که برای استفاده در سیستمهای مختلف، دارای عملکردی یکسان هستند). این object رو میشه با یک tag مشخص کرد.
- Tag: این object به یک image) manifest یا list) اشاره میکنه.
Garbage collection چیست و چه کاری انجام میده؟
داکر رجیستری، دستوری به نام garbage-collect داره که میتونه عملیات پاکسازی در فایل سیستم داکر رجیستری رو انجام بده. با این حال، این دستور، blobها رو فقط زمانی از فایل سیستم حذف میکنه، که هیچ manifest ای، به اونها اشاره نکرده باشه.
پس در زمان نوشتن script پاکسازی، باید مراقب حذف referenceهای مربوط به blobهای قدیمی و بلااستفاده باشین. برای سادهتر کردن این کار، script پاکسازی شما باید کارهای زیر رو به ترتیب انجام بده:
۱. tagهای قدیمی داکر رو حذف کنه: تمام tagهای قدیمیتر از ۳۰ روز، باید از رجیستری حذف بشن (میتونیم برای tagهای ویژه، استثنا قائل بشیم).
Manifest .۲هایی که به هیچ وجه tag گذاری نشدند رو، حذف کنه: این manifestها فضای دیسک رو اشغال میکنن.
۳. دستور garbage-collect رو اجرا کنه، تا blobها و لایههای بلااستفاده، پاکسازی بشه.
حالا مشخص شد که چه چیزهایی باید در script پاکسازی، پیادهسازی بشه. قبل از اینکه بخوایم هر کدوم از مراحل بالا رو توضیح بدیم، بیاین بخشهای مختلف script رو با هم بررسی کنیم.
آشنایی با دستورات مختلف در script پاکسازی
در تنظیم script پاکسازی، باید آیتمهای زیر رو در نظر بگیرین (این تنظیمات میتونه از یک محیط به محیط دیگه متفاوت باشه):
REGISTRY_URL: این دستور، نشوندهنده HTTP URL رجیستری است.
REGISTRY_DIR: این مسیر، نشوندهنده پوشه دادههای رجیستری است.
MAX_AGE_SECONDS: این عدد نشوندهنده حداکثر طول عمر یک tag در ثانیه است.
DOCKER_REGISTRY_NAME: در این دستور، نام کانتینر رجیستری داکر قرار میگیره.
DOCKER_REGISTRY_CONFIG: در این دستور، مسیر config file رجیستری قرار میگیره.
DRY_RUN: این دستور یک flag بولین، برای پشتیبانی از اجرای dry-run پاکسازی است.
EXCLUDE_TAGS: این دستور لیست tagهایی رو که باید از پاکسازی خارج بشن، تعریف میکنه.
انجام مراحل پاکسازی
در بخش قبلی گفتیم که script پاکسازی، باید سه اقدام رو برای عملیات پاکسازی انجام بده. در ادامه هر کدوم از این مراحل رو با هم بررسی میکنیم:
۱. حذف tagهای قدیمی داکر از فایل سیستم
اولین کاری که این script باید انجام بده، اینه که tagهای قدیمی داکر رو از فایل سیستم حذف کنه. برای انجام این مرحله، باید بدونین که tagها چطوری در فایل سیستم ذخیره میشن.
- داکر رجیستری، ایمیجها رو در مسیر زیر (در فایل سیستم) ذخیره میکنه:
${REGISTRY_DIR}/docker/registry/v2/repositories
مثلا، اگه داکر رجیستری، میزبان دو ایمیج داکر به اسمهای nginx و redis و فضای نامی به اسم shih باشه، پوشهی shih در مسیر بالا، با دو زیر پوشه به اسمهای nginx و redis وجود خواهد داشت.
-در فایل سیستم، tagهای یک ایمیج، در پوشهی ایمیج و در مسیر زیر ذخیره میشن:
${image_folder}/_manifests/tags/*
-تصویر بعدی ساختار فایل یک رجیستری رو نشون میده که میزبان ایمیج mysql است:
در زیر نمونه کدی برای آشنایی با تابع remove_image_tags آورده شده است:
REPO_DIR=${REGISTRY_DIR}/docker/registry/v2/repositories
# Loop over all namespaces/repositories
for repo in $REPO_DIR/*; do
# Loop over images in a namespace
for image in $repo/*; do
for tag_path in $image/_manifests/tags/*; do
local tag=$(basename $tag_path)
if ! [[ $tag =~ $EXCLUDE_TAGS ]]; then
if (($tag_age > $MAX_AGE_SECONDS)); then
rm -rf ${tag_path}
fi
fi
done
done
done
با توجه به نمونه کد بالا و اطلاعات گفته شده در رابطه با ساختار فایل tagهای داکر، میتونین مراحل زیر رو برای پاکسازی همهی tagهای قدیمی دنبال کنین:
- ایجاد Loop روی همهی tagها در همهی ایمیجها.
- برای tagهایی که در عملیات پاکسازی وجود دارن، کارهایی که در کد بالا گفته شده رو انجام بدین.
- اگر طول عمر tagها، بیشتر از $MAX_AGE_SECONDS باشه، tagها رو حذف کنین.
۲.حذف همه manifestهای بدون tag
قدم بعدی، حذف همهی manifestهای بدون tag است.
برای اینکه بتونین manifestهای بلااستفادهای که هیچ tagای بهشون اشاره نکرده رو پیدا کرده و حذف کنین، خیلی مهمه که ساختار رجیستری رو برای هر دو آیتم manifestها و tagها، به خوبی بررسی کنین. با اینکار میتونین بدون پاک کردن یا خراب کردن اطلاعات رجیستری داکر، عمل پاکسازی رو انجام بدین.
-داکر رجیستری، برای هر کدوم از ایمیجهای push شده به رجیستری، سه زیر پوشه ایجاد میکنه. مثلا، اگه شما در فضای نام shih، یک ایمیج mysql رو به رجیستری push کنین، در مسیر
{REGISTRY_DIR}/docker/registry/v2/repositories}$، ساختار فایل زیر رو مشاهده میکنین:
-همونطور که در تصویر بالا میبینین، رجیستری داکر سه زیر پوشه رو برای ایمیج mysql ایجاد کرده:
layers _: این پوشه شامل زیرپوشههایی برای هر کدوم از لایهها در ایمیج داکر است. علاوه بر این مپ یا لینکی به digest مربوطه رو هم نشون میده.
manifests_: این پوشه، دو زیر پوشهی tags و revisions رو ذخیره میکنه:
- دایرکتوری revisions، شامل تمام revisionهای ایمیج داکر میشه (حتی ایمیجهایی که هیچ tagای بهشون اشاره نکرده باشه).
- از طرف دیگه، در دایرکتوری tags، برای هر کدوم از tagهای ایمیج، یک زیر پوشه وجود داره.
tagها فقط لینکی به revisionهای ایمیج هستن. در نتیجه، داکر رجیستری، ایمیجهای داکر رو بر اساس tagها و revisionsها، pull میکنه. مثلا، هر دو دستور زیر برای pull کردن ایمیجهای داکر معتبر هستن:
docker pull myregistry:5000/shih/mysql:5.9
docker pull myregistry:5000/shih/mysql@sha256:c3490dcf10ffb6530c
uploads_: دایرکتوری موقتی docker _uploads، موقع docker push مورد استفاده قرار میگیره. معمولاً این دایرکتوری باید خالی باشه.
تابع delete_mainifests_without_tags
در زیر نمونه کدی برای آشنایی با تابع delete_mainifests_without_tags آورده شده است:
manifests_without_tags = diff(_manifests/revisions, _manifests/tags)
for manifest in $manifests_without_tags; do
repos == manifest_repos(manifest)
for repo in $repos
delete_manifest(manifest)
done
done
حالا با توجه به نمونه کد بالا، برای پیدا کردن همه manifestها و از بین بردن اونها، میتونین مراحل زیر رو دنبال کنین:
- پیدا کردن همه manifestsهایی که هیچ tagای ندارن:
manifestهایی که در پوشه revisions وجود دارن، اما tagهای مربوطه رو نشون نمیدن، همون manifestهای بدون tag در نظر گرفته شده و با خیال راحت حذف میشن. - بعد روی Manifestهای پیدا شده، loop بزنین و کارهایی که در کد بالا گفته شده رو انجام بدین.
- تمام ریپازیتوری/فضاینامهایی که حاوی manifest مورد نظر هستن رو پیدا کنین.
- manifest رو از هر کدوم از فضاینامهای پیدا شده، حذف کنین.
-حالا در کد بالا، تابع Delete_manifest رو مشاهده میکنین که به دو روش قابل اجراست:
- با ارسال یک درخواست DELETE API به داکر رجیستری، (مشابه آدرس زیر):
${REGISTRY_URL}/v2/${repo}/manifests/sha256:${hash}
- یا با حذف مستقیم فایلها از فایل سیستم (الگوریتم پیدا کردن tagهای حاوی manifest، به یک مرحلهی اضافی نیاز داره)، که به کمک دستورهای زیر انجام میشه:
REPO_DIR=${REGISTRY_DIR}/docker/registry/v2/repositories
rm -r $REPO_DIR/${repo}/_manifests/tags/${tag}/index/sha256/${hash}
rm -r $REPO_DIR/${repo}/_manifests/revisions/sha256/${hash}
۳. اجرای دستور garbage-collect
در نهایت، بعد از حذف کامل tag های قدیمی و manifestهای بدون tag، اسکریپت، دستور garbage-collect رو درون کانتینر داکر رجیستری، اجرا میکنه. برای انجام این مرحله، از دستور زیر استفاده میشه:
docker exec -i $REGISTRY /bin/registry garbage-collect $CONFIG
نمونه script پاکسازی
در این پست با مراحل انجام پاکسازی در داکر رجیستری آشنا شدین. در ادامه مثال کاملی از script پاکسازی رو مشاهده میکنین که در این لینک گیتهاب، هم میتونین بهش دسترسی داشته باشین.
کدهای زیر روش پیشنهادی گفته شده برای انجام پاکسازی داکر رجیستری رو به طور کامل نشون میده. اینscript با یکسری تنظیمات کلی شروع میشه که میتونه برای مطابقت پیدا کردن با نیازهای داکر رجیستریهای مختلف، تغییر پیدا کنه. همچنین این اسکریپت شامل همهی فانکشنها و ابزارهای لازم برای انجام پاکسازی میشه و از اجرای dry-run هم پشتیبانی میکنه.
#!/bin/bash -e
#
# automatically Cleanup old docker images and tags
#
# Configs
: ${REGISTRY_URL:=http://127.0.0.1:5000}
: ${REGISTRY_DIR:=./data}
: ${MAX_AGE_SECONDS:=$((30 * 24 * 3600))} # 30 days
: ${DOCKER_REGISTRY_NAME:=registry_web}
: ${DOCKER_REGISTRY_CONFIG:=/etc/docker/registry/config.yml}
: ${DRY_RUN:=false}
EXCLUDE_TAGS="^(\*|master|develop|latest|stable|(v|[0-9]\.)[0-9]+(\.[0-9]+)*)$"
REPO_DIR=${REGISTRY_DIR}/docker/registry/v2/repositories
# In doubt fall back to dry mode
[[ $DRY_RUN != "false" ]] && DRY_RUN=true
_curl() {
curl -fsS ${CURL_INSECURE_ARG} "$@"
}
# parse yyyymmddHHMMSS string into unix timestamp
datetime_to_timestamp() {
echo "$1" | awk 'BEGIN {OFS=""} { print substr($1,0,4), "-", substr($1,5,2), "-", substr($1,7,2), " ", substr($1,9,2), ":", substr($1,11,2), ":", substr($1,13,2) }'
}
run_garbage() {
echo "Running garbage-collect command ..."
local dry_run_arg=
$DRY_RUN && dry_run_arg=--dry-run
docker exec -i $DOCKER_REGISTRY_NAME /bin/registry garbage-collect $DOCKER_REGISTRY_CONFIG $dry_run_arg > /dev/null
}
remove_old_tags() {
echo "Start Remove Old Tags ..."
local repo_path image_path
TAG_COUNT=0
for repo_path in $REPO_DIR/*; do
local repo=$(basename $repo_path)
echo "Current repo: $repo"
for image_path in $repo_path/*; do
local image=$(basename $image_path)
remove_image_tags "$repo" "$image"
done
echo
done
}
remove_image_tags() {
local repo=$1
local image=$2
echo "- Cleanup image $repo/$image"
local tag_path
for tag_path in $REPO_DIR/$repo/$image/_manifests/tags/*; do
local tag=$(basename $tag_path)
# Do not clenup execluded tags
if ! [[ $tag =~ $EXCLUDE_TAGS ]]; then
# get timestamp from tag folder
local timestamp=$(date -d @$(stat -c %Y $tag_path) +%Y%m%d%H%M%S)
# parse yyyymmddHHMMSS string into unix timestamp
timestamp=$(date -d "$(datetime_to_timestamp "$timestamp")" +%s)
local now=$(date +%s)
# check if the tag is old enough to delete
if ((now - timestamp > $MAX_AGE_SECONDS)); then
if $DRY_RUN; then
echo "To be Deleted >> rm -rf ${tag_path}"
else
echo "Deleted: $tag"
TAG_COUNT=$((TAG_COUNT+1))
rm -rf ${tag_path}
fi
fi
fi
done
}
delete_manifests_without_tags(){
cd ${REPO_DIR}
local manifests_without_tags=$(
comm -23 <(
find . -type f -path "./*/*/_manifests/revisions/sha256/*/link" |
grep -v "\/signatures\/sha256\/" |
awk -F/ '{print $(NF-1)}' |
sort -u
) <(
find . -type f -path './*/*/_manifests/tags/*/current/link' |
xargs sed 's/^sha256://' |
sort -u
)
)
CURRENT_COUNT=0
FAILED_COUNT=0
TOTAL_COUNT=$(echo ${manifests_without_tags} | wc -w | tr -d ' ')
if [ ${TOTAL_COUNT} -gt 0 ]; then
echo -n "Found ${TOTAL_COUNT} manifests. "
if $DRY_RUN; then
echo "Run without --dry-run to clean up"
else
echo "Starting to clean up"
fi
local manifest
for manifest in ${manifests_without_tags}; do
local repos=$(
find . -path "./*/*/_manifests/revisions/sha256/${manifest}/link" |
sed 's#^./\(.*\)/_manifest.*$#\1#'
)
for repo in $repos; do
if $DRY_RUN; then
echo "Would have run: _curl -X DELETE ${REGISTRY_URL}/v2/${repo}/manifests/sha256:${manifest} > /dev/null"
else
if _curl -X DELETE ${REGISTRY_URL}/v2/${repo}/manifests/sha256:${manifest} > /dev/null; then
CURRENT_COUNT=$((CURRENT_COUNT+1))
else
FAILED_COUNT=$((FAILED_COUNT+1))
fi
fi
done
done
else
echo "No manifests without tags found. Nothing to do."
fi
}
print_summary(){
if $DRY_RUN; then
echo "DRY_RUN over"
else
echo "Job done"
echo "Removed ${TAG_COUNT} tags."
echo "Removed ${CURRENT_COUNT} of ${TOTAL_COUNT} manifests."
[ ${FAILED_COUNT} -gt 0 ] && echo "${FAILED_COUNT} manifests failed. Check for curl errors in the output above."
echo "Disk usage before and after:"
echo "${DF_BEFORE}"
echo
echo "${DF_AFTER}"
fi
}
start_cleanup(){
$DRY_RUN && echo "Running in dry-run mode. Will not make any changes"
#Check registry dir
if [ ! -d ${REPO_DIR} ]; then
echo "REPO_DIR doesn't exist. REPO_DIR=${REPO_DIR}"
exit 1
fi
#correct registry url (remove trailing slash)
REGISTRY_URL=${REGISTRY_URL%/}
#run curl with --insecure?
[ "$CURL_INSECURE" == "true" ] && CURL_INSECURE_ARG=--insecure
#verify registry url
if ! _curl -m 3 ${REGISTRY_URL}/v2/ > /dev/null; then
echo "Could not contact registry at ${REGISTRY_URL} - quitting"
exit 1
fi
DF_BEFORE=$(df -Ph)
remove_old_tags
delete_manifests_without_tags
run_garbage
DF_AFTER=$(df -Ph)
print_summary
}
start_cleanup