قبل از اینکه یک ویژگی وارد تولید شود، در این روزهایی که ارکسترهای پیچیده و CI/CD وجود دارد، از تعهد تا آزمایش و تحویل راه طولانی در پیش است. قبلاً، میتوانستید فایلهای جدید را از طریق FTP آپلود کنید (دیگر کسی این کار را نمیکند، درست است؟)، و فرآیند «استقرار» چند ثانیه طول میکشید. اکنون باید یک درخواست ادغام ایجاد کنید و مدت زیادی صبر کنید تا این ویژگی به کاربران برسد.
بخشی از این مسیر ساخت یک تصویر داکر است. گاهی مونتاژ چند دقیقه طول می کشد، گاهی ده ها دقیقه که به سختی می توان آن را عادی نامید. در این مقاله، ما یک برنامه ساده را انتخاب می کنیم که آن را در یک تصویر بسته بندی می کنیم، چندین روش را برای سرعت بخشیدن به ساخت اعمال می کنیم و به تفاوت های ظریف نحوه کار این روش ها نگاه می کنیم.
ما تجربه خوبی در ایجاد و پشتیبانی از وب سایت های رسانه ای داریم:
ما به GitLab مستقر می شویم. ما تصاویر را جمع آوری می کنیم، آنها را به GitLab Registry فشار می دهیم و آنها را برای تولید عرضه می کنیم. طولانی ترین چیز در این لیست مونتاژ تصاویر است. به عنوان مثال: بدون بهینه سازی، هر ساخت Backend 14 دقیقه طول کشید.
در نهایت مشخص شد که ما دیگر نمی توانیم اینگونه زندگی کنیم و نشستیم تا بفهمیم چرا جمع آوری تصاویر اینقدر طول می کشد. در نتیجه ما موفق شدیم زمان مونتاژ را به 30 ثانیه کاهش دهیم!
برای این مقاله، برای اینکه به محیط Reminder گره نخوریم، به مثالی از اسمبل کردن یک اپلیکیشن Angular خالی می پردازیم. بنابراین، بیایید برنامه خود را ایجاد کنیم:
ng n app
PWA را به آن اضافه کنید (ما پیشرو هستیم):
ng add @angular/pwa --project app
در حالی که یک میلیون بسته npm در حال دانلود است، بیایید بفهمیم که تصویر docker چگونه کار می کند. داکر امکان بسته بندی برنامه ها و اجرای آنها را در یک محیط ایزوله به نام کانتینر فراهم می کند. به لطف انزوا، می توانید چندین کانتینر را به طور همزمان روی یک سرور اجرا کنید. کانتینرها بسیار سبک تر از ماشین های مجازی هستند زیرا مستقیماً روی هسته سیستم اجرا می شوند. برای اجرای یک کانتینر با برنامه خود، ابتدا باید یک تصویر ایجاد کنیم که در آن همه آنچه برای اجرای برنامه ما لازم است را بسته بندی کنیم. در اصل، یک تصویر یک کپی از سیستم فایل است. به عنوان مثال، Dockerfile را در نظر بگیرید:
FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
Dockerfile مجموعه ای از دستورالعمل ها است. با انجام هر یک از آنها، Docker تغییرات را در سیستم فایل ذخیره می کند و آنها را روی موارد قبلی قرار می دهد. هر تیم لایه مخصوص به خود را ایجاد می کند. و تصویر تمام شده لایه هایی است که با هم ترکیب شده اند.
آنچه مهم است بدانید: هر لایه Docker می تواند حافظه پنهان را ذخیره کند. اگر از آخرین بیلد چیزی تغییر نکرده باشد، به جای اجرای دستور، داکر یک لایه آماده را می گیرد. از آنجایی که افزایش اصلی در سرعت ساخت به دلیل استفاده از کش خواهد بود، هنگام اندازه گیری سرعت ساخت به طور خاص به ساخت یک تصویر با کش آماده توجه خواهیم کرد. بنابراین، گام به گام:
- ما تصاویر را به صورت محلی حذف می کنیم تا اجرای قبلی روی تست تاثیری نداشته باشد.
docker rmi $(docker images -q)
- ما برای اولین بار بیلد را راه اندازی می کنیم.
time docker build -t app .
- ما فایل src/index.html را تغییر می دهیم - کار یک برنامه نویس را تقلید می کنیم.
- بیلد را برای بار دوم اجرا می کنیم.
time docker build -t app .
اگر محیط ساخت تصاویر به درستی پیکربندی شده باشد (اطلاعات بیشتر در مورد آن در زیر)، پس با شروع ساخت، Docker از قبل دارای یک دسته حافظه پنهان در هیئت مدیره خواهد بود. وظیفه ما این است که یاد بگیریم چگونه از کش استفاده کنیم تا ساخت در سریع ترین زمان ممکن انجام شود. از آنجایی که فرض میکنیم اجرای یک بیلد بدون حافظه نهان فقط یک بار اتفاق میافتد - همان بار اول - بنابراین میتوانیم سرعت آن زمان اول را نادیده بگیریم. در آزمایشات، اجرای دوم بیلد برای ما مهم است، زمانی که کش ها از قبل گرم شده اند و ما آماده پختن کیک خود هستیم. با این حال، برخی از نکات نیز بر ساخت اول تأثیر می گذارد.
اجازه دهید Dockerfile که در بالا توضیح داده شد را در پوشه پروژه قرار داده و ساخت را شروع کنیم. همه لیست ها برای سهولت در خواندن فشرده شده اند.
$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Status: Downloaded newer image for node:12.16.2
Step 2/5 : WORKDIR /app
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:20:09.664Z - Hash: fffa0fddaa3425c55dd3 - Time: 37581ms
Successfully built c8c279335f46
Successfully tagged app:latest
real 5m4.541s
user 0m0.000s
sys 0m0.000s
محتویات src/index.html را تغییر می دهیم و برای بار دوم اجرا می کنیم.
$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Step 2/5 : WORKDIR /app
---> Using cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:26:26.587Z - Hash: fffa0fddaa3425c55dd3 - Time: 37902ms
Successfully built 79f335df92d3
Successfully tagged app:latest
real 3m33.262s
user 0m0.000s
sys 0m0.000s
برای اینکه ببینیم تصویر را داریم یا نه، دستور را اجرا کنیم docker images
:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 79f335df92d3 About a minute ago 1.74GB
قبل از ساخت، docker تمام فایلها را در شرایط فعلی میگیرد و به دیمون خود ارسال میکند Sending build context to Docker daemon 409MB
. متن ساخت به عنوان آخرین آرگومان دستور build مشخص می شود. در مورد ما، این دایرکتوری فعلی است - "."، - و Docker همه چیزهایی را که در این پوشه داریم می کشد. 409 مگابایت زیاد است: بیایید در مورد چگونگی رفع آن فکر کنیم.
کاهش زمینه
برای کاهش زمینه، دو گزینه وجود دارد. یا تمام فایل های مورد نیاز برای اسمبلی را در یک پوشه جداگانه قرار دهید و زمینه docker را به این پوشه اشاره کنید. این ممکن است همیشه راحت نباشد، بنابراین می توان استثنائاتی را مشخص کرد: آنچه که نباید به متن کشیده شود. برای انجام این کار، فایل .dockerignore را در پروژه قرار دهید و مواردی را که برای ساخت مورد نیاز نیست مشخص کنید:
.git
/node_modules
و دوباره بیلد را اجرا کنید:
$ time docker build -t app .
Sending build context to Docker daemon 607.2kB
Step 1/5 : FROM node:12.16.2
Step 2/5 : WORKDIR /app
---> Using cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Time: 37313ms
Successfully built 4942f010792a
Successfully tagged app:latest
real 1m47.763s
user 0m0.000s
sys 0m0.000s
607.2 کیلوبایت بسیار بهتر از 409 مگابایت است. همچنین اندازه تصویر را از 1.74 به 1.38 گیگابایت کاهش دادیم:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 4942f010792a 3 minutes ago 1.38GB
بیایید سعی کنیم اندازه تصویر را بیشتر کاهش دهیم.
ما از آلپاین استفاده می کنیم
راه دیگر برای صرفه جویی در اندازه تصویر، استفاده از یک تصویر والد کوچک است. تصویر والدین تصویری است که تصویر ما بر اساس آن تهیه شده است. لایه پایین با دستور مشخص می شود FROM
در داکرفایل در مورد ما، ما از یک تصویر مبتنی بر اوبونتو استفاده می کنیم که قبلاً nodejs نصب شده است. و وزنش...
$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutes ago 916MB
... تقریبا یک گیگابایت. با استفاده از یک تصویر مبتنی بر لینوکس آلپاین می توانید حجم صدا را به میزان قابل توجهی کاهش دهید. Alpine یک لینوکس بسیار کوچک است. تصویر docker برای nodejs مبتنی بر alpine تنها 88.5 مگابایت وزن دارد. پس بیایید تصویر پر جنب و جوش خود را در خانه ها جایگزین کنیم:
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
ما مجبور شدیم مواردی را که برای ساخت اپلیکیشن ضروری است را نصب کنیم. بله، Angular بدون پایتون ¯(°_o)/¯ نمی سازد
اما حجم تصویر به 150 مگابایت کاهش یافت:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest aa031edc315a 22 minutes ago 761MB
از این هم جلوتر برویم.
مونتاژ چند مرحله ای
همه آنچه در تصویر است آن چیزی نیست که در تولید به آن نیاز داریم.
$ docker run app ls -lah
total 576K
drwxr-xr-x 1 root root 4.0K Apr 16 19:54 .
drwxr-xr-x 1 root root 4.0K Apr 16 20:00 ..
-rwxr-xr-x 1 root root 19 Apr 17 2020 .dockerignore
-rwxr-xr-x 1 root root 246 Apr 17 2020 .editorconfig
-rwxr-xr-x 1 root root 631 Apr 17 2020 .gitignore
-rwxr-xr-x 1 root root 181 Apr 17 2020 Dockerfile
-rwxr-xr-x 1 root root 1020 Apr 17 2020 README.md
-rwxr-xr-x 1 root root 3.6K Apr 17 2020 angular.json
-rwxr-xr-x 1 root root 429 Apr 17 2020 browserslist
drwxr-xr-x 3 root root 4.0K Apr 16 19:54 dist
drwxr-xr-x 3 root root 4.0K Apr 17 2020 e2e
-rwxr-xr-x 1 root root 1015 Apr 17 2020 karma.conf.js
-rwxr-xr-x 1 root root 620 Apr 17 2020 ngsw-config.json
drwxr-xr-x 1 root root 4.0K Apr 16 19:54 node_modules
-rwxr-xr-x 1 root root 494.9K Apr 17 2020 package-lock.json
-rwxr-xr-x 1 root root 1.3K Apr 17 2020 package.json
drwxr-xr-x 5 root root 4.0K Apr 17 2020 src
-rwxr-xr-x 1 root root 210 Apr 17 2020 tsconfig.app.json
-rwxr-xr-x 1 root root 489 Apr 17 2020 tsconfig.json
-rwxr-xr-x 1 root root 270 Apr 17 2020 tsconfig.spec.json
-rwxr-xr-x 1 root root 1.9K Apr 17 2020 tslint.json
با docker run app ls -lah
ما یک کانتینر بر اساس تصویر خود راه اندازی کردیم app
و دستور را در آن اجرا کرد ls -lah
، پس از آن ظرف کار خود را به پایان رساند.
در تولید ما فقط به یک پوشه نیاز داریم dist
. در این مورد، فایل ها به نحوی باید در خارج داده شوند. می توانید برخی از سرورهای HTTP را روی nodejs اجرا کنید. اما ما آن را آسان تر خواهیم کرد. یک کلمه روسی که دارای چهار حرف "y" است را حدس بزنید. درست! Ynzhynyksy. بیایید با nginx یک عکس بگیریم، یک پوشه در آن قرار دهیم dist
و یک پیکربندی کوچک:
server {
listen 80 default_server;
server_name localhost;
charset utf-8;
root /app/dist;
location / {
try_files $uri $uri/ /index.html;
}
}
ساخت چند مرحله ای به ما در انجام همه اینها کمک می کند. بیایید Dockerfile خود را تغییر دهیم:
FROM node:12.16.2-alpine3.11 as builder
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
FROM nginx:1.17.10-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/static.conf /etc/nginx/conf.d
COPY --from=builder /app/dist/app .
حالا دو دستورالعمل داریم FROM
در Dockerfile، هر یک از آنها یک مرحله ساخت متفاوت را اجرا می کنند. اولی را صدا زدیم builder
، اما با شروع از آخرین FROM، تصویر نهایی ما آماده می شود. آخرین مرحله این است که آرتیفکت اسمبلی خود را در مرحله قبل در تصویر نهایی با nginx کپی کنیم. اندازه تصویر به میزان قابل توجهی کاهش یافته است:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 2c6c5da07802 29 minutes ago 36MB
بیایید ظرف را با تصویر خود اجرا کنیم و مطمئن شویم که همه چیز کار می کند:
docker run -p8080:80 app
با استفاده از گزینه -p8080:80، پورت 8080 را در دستگاه میزبان خود به پورت 80 داخل ظرفی که nginx در آن اجرا می شود، ارسال کردیم. در مرور گر باز کنید
کاهش حجم تصویر از 1.74 گیگابایت به 36 مگابایت زمان تحویل برنامه شما را به طور قابل توجهی کاهش می دهد. اما به زمان مونتاژ برگردیم.
$ time docker build -t app .
Sending build context to Docker daemon 608.8kB
Step 1/11 : FROM node:12.16.2-alpine3.11 as builder
Step 2/11 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
---> Using cache
Step 3/11 : WORKDIR /app
---> Using cache
Step 4/11 : COPY . .
Step 5/11 : RUN npm ci
added 1357 packages in 47.338s
Step 6/11 : RUN npm run build --prod
Date: 2020-04-16T21:16:03.899Z - Hash: fffa0fddaa3425c55dd3 - Time: 39948ms
---> 27f1479221e4
Step 7/11 : FROM nginx:stable-alpine
Step 8/11 : WORKDIR /app
---> Using cache
Step 9/11 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
Step 10/11 : COPY nginx/static.conf /etc/nginx/conf.d
---> Using cache
Step 11/11 : COPY --from=builder /app/dist/app .
Successfully built d201471c91ad
Successfully tagged app:latest
real 2m17.700s
user 0m0.000s
sys 0m0.000s
تغییر ترتیب لایه ها
سه مرحله اول ما در حافظه پنهان (اشاره Using cache
). در مرحله چهارم تمامی فایل های پروژه کپی شده و در مرحله پنجم وابستگی ها نصب می شوند RUN npm ci
- به اندازه 47.338 ثانیه. چرا اگر وابستگی ها به ندرت تغییر می کنند، هر بار دوباره نصب شوند؟ بیایید بفهمیم که چرا آنها ذخیره نشده اند. نکته این است که Docker لایه به لایه بررسی می کند تا ببیند دستور و فایل های مرتبط با آن تغییر کرده اند یا خیر. در مرحله چهارم تمامی فایل های پروژه خود را کپی می کنیم و البته در بین آن ها تغییراتی نیز وجود دارد، بنابراین داکر نه تنها این لایه را از کش نمی گیرد، بلکه تمامی فایل های بعدی را نیز از حافظه پنهان خارج می کند! بیایید تغییرات کوچکی در Dockerfile ایجاد کنیم.
FROM node:12.16.2-alpine3.11 as builder
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build --prod
FROM nginx:1.17.10-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/static.conf /etc/nginx/conf.d
COPY --from=builder /app/dist/app .
ابتدا package.json و package-lock.json کپی می شوند، سپس وابستگی ها نصب می شوند و تنها پس از آن کل پروژه کپی می شود. در نتیجه:
$ time docker build -t app .
Sending build context to Docker daemon 608.8kB
Step 1/12 : FROM node:12.16.2-alpine3.11 as builder
Step 2/12 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
---> Using cache
Step 3/12 : WORKDIR /app
---> Using cache
Step 4/12 : COPY package*.json ./
---> Using cache
Step 5/12 : RUN npm ci
---> Using cache
Step 6/12 : COPY . .
Step 7/12 : RUN npm run build --prod
Date: 2020-04-16T21:29:44.770Z - Hash: fffa0fddaa3425c55dd3 - Time: 38287ms
---> 1b9448c73558
Step 8/12 : FROM nginx:stable-alpine
Step 9/12 : WORKDIR /app
---> Using cache
Step 10/12 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
Step 11/12 : COPY nginx/static.conf /etc/nginx/conf.d
---> Using cache
Step 12/12 : COPY --from=builder /app/dist/app .
Successfully built a44dd7c217c3
Successfully tagged app:latest
real 0m46.497s
user 0m0.000s
sys 0m0.000s
46 ثانیه به جای 3 دقیقه - خیلی بهتر! ترتیب صحیح لایه ها مهم است: ابتدا آنچه را که تغییر نمی کند، سپس آنچه به ندرت تغییر می کند و در نهایت آنچه اغلب تغییر می کند کپی می کنیم.
بعد، چند کلمه در مورد مونتاژ تصاویر در سیستم های CI/CD.
استفاده از تصاویر قبلی برای کش
اگر از نوعی راه حل SaaS برای ساخت استفاده کنیم، ممکن است کش محلی Docker تمیز و تازه باشد. برای اینکه به داکر مکانی برای گرفتن لایه های پخته شده بدهید، تصویر ساخته شده قبلی را به او بدهید.
بیایید مثالی از ساخت اپلیکیشن خود در GitHub Actions بزنیم. ما از این کانفیگ استفاده می کنیم
on:
push:
branches:
- master
name: Test docker build
jobs:
deploy:
name: Build
runs-on: ubuntu-latest
env:
IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/app
IMAGE_TAG: ${{ github.sha }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login to GitHub Packages
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $TOKEN
- name: Build
run: |
docker build
-t $IMAGE_NAME:$IMAGE_TAG
-t $IMAGE_NAME:latest
.
- name: Push image to GitHub Packages
run: |
docker push $IMAGE_NAME:latest
docker push $IMAGE_NAME:$IMAGE_TAG
- name: Logout
run: |
docker logout docker.pkg.github.com
تصویر در دو دقیقه و 20 ثانیه مونتاژ شده و به بستههای GitHub منتقل میشود:
حالا بیلد را طوری تغییر می دهیم که بر اساس تصاویر ساخته شده قبلی از کش استفاده شود:
on:
push:
branches:
- master
name: Test docker build
jobs:
deploy:
name: Build
runs-on: ubuntu-latest
env:
IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/app
IMAGE_TAG: ${{ github.sha }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login to GitHub Packages
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $TOKEN
- name: Pull latest images
run: |
docker pull $IMAGE_NAME:latest || true
docker pull $IMAGE_NAME-builder-stage:latest || true
- name: Images list
run: |
docker images
- name: Build
run: |
docker build
--target builder
--cache-from $IMAGE_NAME-builder-stage:latest
-t $IMAGE_NAME-builder-stage
.
docker build
--cache-from $IMAGE_NAME-builder-stage:latest
--cache-from $IMAGE_NAME:latest
-t $IMAGE_NAME:$IMAGE_TAG
-t $IMAGE_NAME:latest
.
- name: Push image to GitHub Packages
run: |
docker push $IMAGE_NAME-builder-stage:latest
docker push $IMAGE_NAME:latest
docker push $IMAGE_NAME:$IMAGE_TAG
- name: Logout
run: |
docker logout docker.pkg.github.com
ابتدا باید به شما بگوییم که چرا دو دستور راه اندازی می شوند build
. واقعیت این است که در یک مونتاژ چند مرحله ای تصویر حاصل مجموعه ای از لایه ها از آخرین مرحله خواهد بود. در این صورت لایه های لایه های قبلی در تصویر قرار نمی گیرند. بنابراین، هنگام استفاده از تصویر نهایی از یک بیلد قبلی، داکر قادر نخواهد بود لایههای آماده برای ساخت تصویر با nodejs (مرحله سازنده) را پیدا کند. برای حل این مشکل، یک تصویر میانی ایجاد می شود $IMAGE_NAME-builder-stage
و به بسته های GitHub منتقل می شود تا بتوان از آن در ساخت بعدی به عنوان منبع کش استفاده کرد.
کل زمان مونتاژ به یک و نیم دقیقه کاهش یافت. نیم دقیقه صرف بالا کشیدن تصاویر قبلی می شود.
پیش تصویربرداری
راه دیگر برای حل مشکل یک کش Docker تمیز این است که برخی از لایه ها را به یک Dockerfile دیگر منتقل کنید، آن را جداگانه بسازید، آن را به رجیستری Container فشار دهید و از آن به عنوان پدر استفاده کنید.
ما تصویر nodejs خود را برای ساخت یک برنامه Angular ایجاد می کنیم. Dockerfile.node را در پروژه ایجاد کنید
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
ما یک تصویر عمومی را در Docker Hub جمع آوری و ارسال می کنیم:
docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latest
اکنون در Dockerfile اصلی خود از تصویر تمام شده استفاده می کنیم:
FROM exsmund/node-for-angular:latest as builder
...
در مثال ما، زمان ساخت کاهش نیافته است، اما اگر پروژه های زیادی دارید و مجبور به نصب وابستگی های مشابه در هر یک از آنها هستید، تصاویر از پیش ساخته شده می توانند مفید باشند.
ما چندین روش را برای سرعت بخشیدن به ساخت تصاویر داکر بررسی کردیم. اگر می خواهید استقرار سریع انجام شود، از این در پروژه خود استفاده کنید:
- کاهش زمینه؛
- استفاده از تصاویر کوچک والدین؛
- مونتاژ چند مرحله ای؛
- تغییر ترتیب دستورالعمل ها در Dockerfile برای استفاده موثر از حافظه نهان.
- راه اندازی کش در سیستم های CI/CD.
- ایجاد اولیه تصاویر
امیدوارم این مثال نحوه عملکرد Docker را روشنتر کند و بتوانید استقرار خود را بهینه پیکربندی کنید. به منظور بازی با نمونه های مقاله، یک مخزن ایجاد شده است
منبع: www.habr.com