چند قدم با Docker Swarm – قسمت اول

به مجموعه ای از چنتا نود که با هم کار میکنن و منابعشون رو با هم به اشتراک میذارن برای اینکه سرویس دهی درستی داشته باشیم و DownTime نداشته باشیم، میگن کلاستر (البته این تعریف یخورده آبکیه. بهترش رو سرچ کنین).

کلاستر ساختن هم قاعده و روش داره و همینطور متخصص خودش رو هم داره. برای مثال از ابزارهایی نظیر کوبرنیتیز (K8S) و داکر سوارم برای کلاسترینگ برنامه های container base استفاده میشه. از docker-compose هم برای مقیاس های کوچیکتر و روی یه نود استفاده میشه.

البته docker-compose برای موارد دیگه ای هم استفاده میشه. مثلا شما میخوای برنامه که با PHP و MYSQL و NGINX نوشتی رو، روی یه سرور بیاری بالا. دوتا راه داری:

  • نصب دستی تک تک برنامه ها و صرف کردن زمان نسبتا زیاد + کانفیگ برای هرکدوم
  • نوشتن کانفیگ ها برای یک دفعه و استفاده از اونها از طریق docker-compose.

قبلا من دستی نصب میکردم. اما الان ارزش داکر کمپوز رو بیشتر میدونم و وقتمو تلف نمیکنم 🙂

نکته: در صورتی که رم سرور شما از ۲ گیگ کمتره، داکر کمپوز نمیتونه ران بشه. راه حل؟ دونه دونه کانتینر ها رو دستی ران کنین!

خب از بحث دور نشیم. داکر سوارم ابزاری برای کلاسترینگه. نسبت به داکر کمپوز امکانات بیشتری در اختیار شما میذاره اما نسبت به K8S خیلی بچه‌گانه و کوچولوئه. اصلا نمیشه قیاس کرد. ولی خوبه که یاد بگیریم (من برای شروع کار یاد گرفتم). پیش نیاز اون هم یادگیری خود داکر و داکر-کمپوز هست. چون سینتکس ها و ساختار همونه وبرخی از دستورات فرق می‌کنن.

برای یادگیری هم سخت نگیرید. داکیومنت اصلی:

https://docs.docker.com/engine/swarm

من از KataCoda کمک گرفتم. آموزش داکر سوارم در katacoda:

https://www.katacoda.com/courses/docker-orchestration

قسمت اول راه اندازی کلاستر:

https://www.katacoda.com/courses/docker-orchestration/getting-started-with-swarm-mode

سوارم مود چیه اصلا؟
ب
ه شما این توانایی رو میده که کانتینر ها رو روی چندین هاست داکری دیپلوی و مدیریت کنید به همراه overlay network و لودبالانسر داخلی (ingress). این نکته حائز اهمیته که سوارم مود به عنوان بخشی از داکر (نسخه کامندلاین) هست. پس نیاز نیست چیزی رو اضافه نصب کنین.

سه تا کانسپت جدید:
1- نود(Node): یه اینستنس از داکر انجین هست که به سوارم کانکت شده. نودها یا منیجر هستن یا ورکر.

  • منیجرها مشخص میکنن که کدوم کانتینر روی کدوم نود اجرا بشه .
  • ورکرها هم اون دستورات رو اجرا میکنن. به صورت دیفالت،‌ هر منیجر یه ورکر هم محسوب میشه.

2- سرویس(Service): تسکی هست که توسط ورکرها اجرا میشه. مثالش هم میشه یه سرویس که http سرور هست (مثل nginx) و روی کانتینری بالا میاد که روی ۳ نود در حال اجراست.

3- لودبالانسر(LoadBalacer): خود داکر شامل یه لودبالانسر هست که ریکوئست ها رو روی کانتینر های مختلف یه سرویس (اصطلاحا رپلیکا) پخش و پردازش کنه.

نکته: همیشه اولین نودی که سوارم رو فعال میکنه، میشه Leader. توی پروداکشن بین ۳ تا ۵ نود رو بعنوان منیجر انتخاب کنین تا از HA مطمئن باشین. (HA = High Availability)

سوال: اگر نخوایم این داکیومنت (این صفحه) رو بخونیم، چی باید بخونیم؟

هلپ سوارم مود! که توضیحات برای هر دستور رو بهمون میده:

docker swarm --help
docker swarm –help

مهمترین دستورش هم:

docker swarm init

هستش. که سوارم رو ایجاد میکنه و یه توکن میسازه و به شما میده که نود های دیگری رو اگر خواستیم اضافه کنیم. این توکن رو باید درجای امنی نگهداری کنیم که اگر خواستیم کلاسترمون رو توسعه بدیم، از طریق این توکن نود اضافه کنیم.

نکته: به نودی که شروع کننده‌ی کار هست، اصطلاحا Master یا Leader میگن.

وقتی init میزنی
  • ویژگی: وقتی یکی از نودها کرش کنه، سوارم تشخیص میده و کانتینر هایی که روی اون نود هستن رو دستور میده که روی نود دیگه ای بالا بیان. که اینطوری HA حفظ بشه و اوکی باشیم (این آپشن رو کوبرنیتیز داره. خیلیم خوبترشو داره. یعنی کرک و پشمی برای آدم نمیمونه)

خب. سوارم رو ایجاد کردیم. چجوری نود ها با هم حرف میزنن و دیتا تبادل می‌کنن؟ داکر در مود سوارم که باشه، از یه پورت اضافی برای صحبت نود ها با همدیگه استفاده میکنه: 2377
خیلی مهمه که این پورت از طریق پابلیک قابل دسترسی نباشه! پس بهتره که از شبکه های خصوصی یا VPN برای ارتباط بین نودها استفاده بشه.

خب. میخوایم یه نود دیگه رو بعنوان ورکر اضافه کنیم. توکن رو از قبل داریم و در ادامه:

docker swarm join —token YOUR_TOKEN_HERE YOUR_IP:2377

مثلا آیپی اولین نودی که ایجاد کردیم 192.168.1.110 بود و توکن رو هم گرفتیم در مرحله قبل، توی نود جدید اینطوری دستور میدیم:

docker swarm join --token SWMTKN-1-2z6ucdd8d8ahwvj3r58i8b7w28owpwnx4412wjt96pekcmc3eu-6z75n5hvl2umo9x7m39pu1lfq 192.168.1.110:2377

الان یه نود با نقش worker رجیستر شد. اگر خواستیم نقشش Manager باشه چی؟

docker swarm join-token manager

با دستور بالا، میتونین توکن برای نقش manager رو بگیرید. کامندی که به شما نشون میده رو توی نود موردنظرتون بزنین که رجیستر بشه.

اگر خواستید برای نقش worker دوباره توکن بگیرین (مثلا توکنی که در ابتدا داده شد رو از دست دادین):

توجه: این دستورها رو من روی نودی میزنم که در ابتدا کلاستر رو init کرده

docker swarm join-token worker

نکته: بهتره که نسخه‌ی داکر روی همه‌ی نودها یکی باشه.

اگر خواستیم نودهایی که به کلاستر اضافه کردیم رو ببینیم (کل نودها):

docker node ls
docker node ls

اگر بخوایم یه نود رو از کلاستر خارج کنیم، داخل هرنودی که میخوایم خارج بشه اینو میزنیم:

docker swarm leave -f

که خودش از کلاستر خارج میشه. اما شاید بخوایم از نودهایی که نقش Manager رو دارن این کار رو انجام بدیم:

docker node rm NODE_ID
OR
docker node rm NODE_NAME

هردو دستور کار میکنن. هرکدوم رو راحتین استفاده کنین.

ساخت Network Overlay

این شبکه میاد و باعث ارتباط کانتینر ها و نودها با هم میشه (انگار همشون روی یه هاست هستن). نوعی ارتباط امن روی بستر TCP هست و طراحی شده تا برای دیپلوی هایی که بر پایه کلود و در مقیاس بزرگ انجام میشه، استفاده بشه. بهش میگن VxLAN که یکی از فیچر های کرنل لینوکسه.

چجوری یه شبکه بسازیم؟

docker network create -d overlay NET_NAME

در اینجا:

  • از فلگ d برای مشخص کردن driver مشخص میشه که در اینجا از نوع overlay هست. البته انواع مختلفی داریم. نظیر bridge , host, null, overlay
  • به جای NET_NAME هم اسم شبکه رو می‌نویسید. میتونه دلخواه باشه. هرچی دوست دارین 🙂

دیپلوی کردن یک سرویس

به صورت دیفالت داکر از مدل spread replication استفاده میکنه برای اینکه تصمیم بگیره کدوم کانتینر باید روی کدوم هاست اجرا بشه. این مدل به ما اطمینان میده که کانتینرها بصورت مساوی روی کلاستر پخش و اجرا میشن. طوریکه به نودی فشار نیاد و اگر نودی از کلاستر خارج شد، اون اینستنس (کانتینر) هایی که روی اون نود بودن، روی بقیه نودها پخش بشن.

کانسپت جدیدی از سرویس ها استفاده میشه تا کانتینر ها رو روی کلاستر اجرا کنه. این کانسپت، یه کانسپت با درجه اولویت بالاتری نسبت به کانتینرهاست. چرا؟
چون یه سرویس (دقت کنید که هر سرویس چندین کانتینر داره. مثلا وقتی میگیم nginx به عنوان سرویس با ۲ تا رپلیکا اجرا بشه یعنی یه سرویس داریم که ۲ تا کانتینر از nginx داره) اجازه میده به شما که مشخص کنی اپلیکیشن ها چطوری باید روی این کلاستر دیپلوی بشن. با آپدیت شدن سرویس، داکر کانتینر های مورد نیاز رو به صورت مدیریت شده و خیلی شیک و مجلسی آپدیت میکنه.

مثال: میخوایم یه سرویس http رو بیاریم بالا. اسمش رو میذاریم HTTP و برای اینکه مطمين باشیم همیشه بالاست و HA هست، بجای یه نسخه ما براش ۲ تا رپلیکا درست میکنیم.
لود بالانسر ما این دوتا را روی پورت 80 بالانس میکنه. (در واقع داکر ریکوئست ها رو بین کانیتنر های موجود (برای این کار) بالانس میکنه) اسم شبکه رو گذاشتیم skynet

docker service create —name http —network skynet —replicas 2 -p 80:80 katacoda/docker-http-server

پورت 80 رو از کلاستر به کانتینر مپ کردیم. ۲ تا رپلیکا دادیم واسم ایمیج هم katacoda/docker-http-server هست.

docker service create

سرویس هایی که ران هستن روی کلاستر رو با کامند زیر میشه دید:

docker service ls
docker service ls

وقتی هم کانتینری استارت بشه با کامند معروفی که این پایین میذارم، میشه دید. این دستور مختص کانتینرهای داخل هرنود هست. یعنی اینکه شما نمیتونی روی نود مستر این دستور رو بزنی و انتظار داشته باشی کل کانتینرهای کلاستر رو لیست کنه 🙂

docker ps

سرویس به شما اجازه میده که سلامتی و وضعیت کلاستر و اپهایی که روی اون ران هست رو بازرسی (اینسپکت) کنین. مثلا یه اپلیکیشن خاص رو بخواید بررسی کنین:

docker service ps YOUR_SERVICE_NAME

مثال:

docker service ps http

دیدن جزئیات و کانفیگ های یه سرویس:

docker service inspect --pretty YOUR_SERVICE_NAME

که به صورت YAML نشون میده. اگر میخواید JSON باشه، فلگ pretty رو بهش ندید.
مثال:

docker service inspect --pretty app
YAML Format

دیدن جزئیات یه سرویس با فرمت JSON:

docker service inspect app
[
    {
        "ID": "qa4fs5hit7nq43hktpcir04zg",
        "Version": {
            "Index": 13
        },
        "CreatedAt": "2021-09-26T18:37:16.37608264Z",
        "UpdatedAt": "2021-09-26T18:37:16.411913979Z",
        "Spec": {
            "Name": "app",
            "Labels": {},
            "TaskTemplate": {
                "ContainerSpec": {
                    "Image": "nginx:1.21.1-alpine",
                    "Init": false,
                    "StopGracePeriod": 10000000000,
                    "DNSConfig": {},
                    "Isolation": "default"
                },
                "Resources": {
                    "Limits": {},
                    "Reservations": {}
                },
                "RestartPolicy": {
                    "Condition": "any",
                    "Delay": 5000000000,
                    "MaxAttempts": 0
                },
                "Placement": {},
                "ForceUpdate": 0,
                "Runtime": "container"
            },
            "Mode": {
                "Replicated": {
                    "Replicas": 1
                }
            },
            "UpdateConfig": {
                "Parallelism": 1,
                "FailureAction": "pause",
                "Monitor": 5000000000,
                "MaxFailureRatio": 0,
                "Order": "stop-first"
            },
            "RollbackConfig": {
                "Parallelism": 1,
                "FailureAction": "pause",
                "Monitor": 5000000000,
                "MaxFailureRatio": 0,
                "Order": "stop-first"
            },
            "EndpointSpec": {
                "Mode": "vip",
                "Ports": [
                    {
                        "Protocol": "tcp",
                        "TargetPort": 80,
                        "PublishedPort": 80,
                        "PublishMode": "ingress"
                    }
                ]
            }
        },
        "Endpoint": {
            "Spec": {
                "Mode": "vip",
                "Ports": [
                    {
                        "Protocol": "tcp",
                        "TargetPort": 80,
                        "PublishedPort": 80,
                        "PublishMode": "ingress"
                    }
                ]
            },
            "Ports": [
                {
                    "Protocol": "tcp",
                    "TargetPort": 80,
                    "PublishedPort": 80,
                    "PublishMode": "ingress"
                }
            ],
            "VirtualIPs": [
                {
                    "NetworkID": "55xjjsrgfb8erw3d5zdtb1fge",
                    "Addr": "10.0.0.3/24"
                }
            ]
        }
    }
]

پ.ن: حالتی که JSON میده برای مطالعه چشمی یخورده اذیت میکنه ولی به درد میکروسرویس میخوره. مثلا یه میکروسرویس نوشتی که بیاد و inspect یه سرویس رو بهت با وبسرویس برگردونه. قاعدتا JSON فرمت خیلی بهتریه نسبت به YAML. از YAML برای مطالعه چشمی استفاده کنین بهتره.

یه نکته خیلی مهم: وقتی نودی به عنوان ورکر به کلاستر اضافه شد، اکثر دستورات مدیریتی رو نمیتونه و دسترسی نداره که نمایش بده. کارها رو باید با منیجر جلو ببریم.

مشاهده تسکهایی(سرویس) که یه نود داره ران میکنه (با نود منیجر ران بشه):
دستور کلی:

docker node ps NODE_ID
docker node ps NODE_NAME

هر دو دستور جواب میدن و قبلا گفتم که میتونین ID و اسم هرنود رو از دستور زیر پیدا کنین:

docker node ls

مثال:

docker node ps mahdi-desktop
OR
docker node ps 55gzihya91ajt2a72d6qwwdr1
docker node ps

ممکنه بخوایم همین منیجری که توش هستیم رو ببینیم چیا داره ران میکنه: بجای NODE_ID از عبارت self استفاده میکنیم:

docker node ps self
docker node ps self

Service Scale

یک سرویس به ما این توانایی رو میده که مشخص کنیم (مقیاس) که چنتا اینستنس از یک تسک باید روی کلاستر ران بشه همونطوری که میفهمه کدوم کانتینر باید استارت/استاپ/حذف بشه.

در حال حاضر مشخص کردن scale دستی هست. البته که میشه api رو به یه داشبورد خارجی متصل کرد.

docker service scale SERVICE_NAME=COUNT

حالا مثلا میخوایم سرویس app که از قبل یه رپلیکا داشت رو تبدیل کنیم به ۵ تا رپلیکا:

docker service scale app=5
docker service scale

سوالی که پیش میاد اینه که حالا وقتی ما ۵ تا رپلیکا داریم، چجوری ریکوئستها بینشون پخش میشه؟
جواب: عزیزانم! خود سوارم این کار رو انجام میده از طریق loadBalancer ای که داره (گمونم ingress هست) و دمش گرم.

تمرین: الان سرویس app با ۵ تا رپلیکا ران شده. به ۳ تا رپلیکا دیگه نیاز نداریم. چجوری کمش کنیم؟

جواب: همونطوری که اضافه کردیم، کم میکنیم =)

docker service scale app=2

پایان بخش اول از آموزش KataCoda