Skip to content

فصل ۵ — Podها

در فصل‌های قبل دربارهٔ containerize کردن برنامه صحبت کردیم، اما در استقرارهای واقعی برنامه‌های containerize‌شده اغلب می‌خواهید چند برنامه را در یک واحد atomic واحد colocate کنید و روی یک ماشین schedule شوند.

مثال متداول چنین استقراری در شکل ۵-۱ نشان داده شده: containerای که درخواست‌های وب را serve می‌کند و containerای که filesystem را با مخزن Git راه‌دور همگام می‌کند.

شکل ۵-۱. مثالی از یک Pod با دو container و filesystem مشترک

در ابتدا ممکن است وسوسه شوید web server و Git synchronizer را در یک container واحد بپیچید. اما با نگاه دقیق‌تر دلایل جداسازی روشن می‌شود. اول، دو container نیازهای بسیار متفاوتی در مصرف منابع دارند. مثلاً memory. چون web server درخواست‌های کاربر را serve می‌کند، می‌خواهیم همیشه در دسترس و پاسخگو باشد. از طرف دیگر Git synchronizer واقعاً user-facing نیست و کیفیت سرویس «best effort» دارد.

فرض کنید Git synchronizer نشت memory دارد. باید مطمئن شویم Git synchronizer نمی‌تواند memoryای را که برای web server می‌خواهیم مصرف کند بگیرد، چون این می‌تواند عملکرد web server را تحت تأثیر قرار دهد یا حتی سرور را crash کند.

این نوع جداسازی منابع دقیقاً همان چیزی است که containerها برای آن طراحی شده‌اند. با جدا کردن دو برنامه در دو container جدا، می‌توانیم عملکرد قابل‌اعتماد web server را تضمین کنیم.

البته دو container کاملاً همزیستی دارند؛ معنا ندارد web server را روی یک ماشین و Git synchronizer را روی ماشین دیگر schedule کنیم. در نتیجه Kubernetes چند container را در یک واحد atomic به نام Pod گروه‌بندی می‌کند. (نام با تم نهنگ containerهای Docker هماهنگ است، چون Pod هم گروهی از نهنگ‌هاست.)

اگرچه مفهوم چنین sidecarها وقتی برای اولین بار در Kubernetes معرفی شد بحث‌برانگیز یا گیج‌کننده به نظر می‌رسید، بعداً توسط برنامه‌های مختلف برای deploy زیرساختشان پذیرفته شد. مثلاً چند پیاده‌سازی Service Mesh از sidecar برای تزریق مدیریت شبکه به Pod برنامه استفاده می‌کنند.

Podها در Kubernetes

Pod نمایانگر مجموعه‌ای از containerهای برنامه و volumeهایی است که در همان محیط اجرا اجرا می‌شوند. Podها، نه containerها، کوچک‌ترین artifact قابل deploy در کلاستر Kubernetes هستند. یعنی همهٔ containerهای یک Pod همیشه روی همان ماشین می‌نشینند.

هر container داخل Pod در cgroup خودش اجرا می‌شود، اما تعدادی namespace لینوکس را به اشتراک می‌گذارند.

برنامه‌هایی که در همان Pod اجرا می‌شوند IP address و فضای پورت یکسان (network namespace)، hostname یکسان (UTS namespace) دارند و می‌توانند با کانال‌های ارتباط بین‌پردازشی بومی روی System V IPC یا POSIX message queues (IPC namespace) ارتباط برقرار کنند. اما برنامه‌ها در Podهای مختلف از هم جدا هستند؛ IP address، hostname و موارد بیشتری متفاوت دارند. containerهای Podهای مختلف روی همان node تقریباً مثل سرورهای مختلف هستند.

تفکر با Podها

یکی از رایج‌ترین سؤال‌ها در پذیرش Kubernetes این است: «چه چیزی را در یک Pod بگذارم؟»

گاهی افراد Pod را می‌بینند و فکر می‌کنند: «آها! container WordPress و container پایگاه داده MySQL باید در همان Pod باشند.» اما این نوع Pod در واقع نمونهٔ anti-pattern برای ساخت Pod است. دو دلیل دارد.

اول، WordPress و پایگاه داده‌اش واقعاً همزیستی نیستند. اگر container WordPress و container پایگاه داده روی ماشین‌های مختلف بیفتند، همچنان می‌توانند مؤثر کار کنند چون از طریق اتصال شبکه ارتباط برقرار می‌کنند. دوم، لزوماً نمی‌خواهید WordPress و پایگاه داده را به‌عنوان یک واحد scale کنید. WordPress خود عمدتاً stateless است و ممکن است بخواهید frontendهای WordPress را در پاسخ به بار frontend با ساخت Podهای WordPress بیشتر scale کنید. scale کردن پایگاه داده MySQL خیلی سخت‌تر است و احتمالاً منابع اختصاص‌یافته به یک Pod MySQL را افزایش می‌دهید. اگر containerهای WordPress و MySQL را در یک Pod واحد گروه کنید، مجبورید همان استراتژی scaling را برای هر دو container استفاده کنید که خوب جور درنمی‌آید.

به‌طور کلی سؤال درست هنگام طراحی Pod این است: «آیا این containerها اگر روی ماشین‌های مختلف بیفتند درست کار می‌کنند؟» اگر جواب «نه» است، Pod گروه‌بندی درست containerهاست. اگر «بله» است، چند Pod احتمالاً راه‌حل درست است. در مثال ابتدای فصل، دو container از طریق filesystem محلی تعامل دارند. اگر containerها روی ماشین‌های مختلف schedule شوند عملاً نمی‌توانند درست کار کنند.

در بخش‌های باقی‌ماندهٔ فصل نحوهٔ ساخت، introspection، مدیریت و حذف Podها در Kubernetes را توضیح می‌دهیم.

Pod Manifest

Podها در Pod manifest توصیف می‌شوند. Pod manifest فقط نمایش متنی object API Kubernetes است. Kubernetes به پیکربندی declarative باور قوی دارد. پیکربندی declarative یعنی وضعیت مطلوب دنیا را در پیکربندی می‌نویسید و آن پیکربندی را به سرویسی می‌دهید که اقداماتی انجام می‌دهد تا وضعیت مطلوب، وضعیت واقعی شود.

پیکربندی declarative با پیکربندی imperative متفاوت است، جایی که فقط مجموعه‌ای از اقدامات (مثلاً apt-get install foo) برای تغییر دنیا انجام می‌دهید. سال‌ها تجربهٔ production به ما آموخته نگه‌داشتن رکورد نوشته‌شده از وضعیت مطلوب سیستم به سیستم قابل‌مدیریت‌تر و قابل‌اعتمادتر منجر می‌شود. پیکربندی declarative مزایای متعددی دارد، از جمله code review برای پیکربندی‌ها و مستندسازی وضعیت فعلی دنیا برای تیم‌های توزیع‌شده. علاوه بر این، پایهٔ همهٔ رفتارهای self-healing در Kubernetes است که برنامه‌ها را بدون اقدام کاربر در حال اجرا نگه می‌دارد.

سرور API Kubernetes Pod manifestها را می‌پذیرد و پردازش می‌کند قبل از ذخیره در persistent storage (etcd). scheduler هم از API Kubernetes برای یافتن Podهایی که به node schedule نشده‌اند استفاده می‌کند. سپس Podها را روی nodeها با توجه به منابع و محدودیت‌های دیگر بیان‌شده در Pod manifest قرار می‌دهد.

چند Pod می‌توانند روی همان ماشین قرار بگیرند تا منابع کافی باشد. اما schedule کردن چند replica از همان برنامه روی همان ماشین برای قابلیت اطمینان بدتر است، چون ماشین یک failure domain واحد است. در نتیجه scheduler Kubernetes تلاش می‌کند Podهای یک برنامه روی ماشین‌های مختلف برای قابلیت اطمینان در حضور چنین failureهایی توزیع شوند. پس از schedule روی node، Podها جابه‌جا نمی‌شوند و باید صریحاً destroy و دوباره schedule شوند.

چند نمونه از یک Pod را می‌توان با تکرار workflow توصیف‌شده deploy کرد. اما ReplicaSetها (فصل ۹) برای اجرای چند نمونه از یک Pod مناسب‌ترند. (برای اجرای یک Pod هم بهترند، اما بعداً به آن می‌پردازیم.)

ساخت Pod

ساده‌ترین راه ساخت Pod دستور imperative kubectl run است. مثلاً برای اجرای همان سرور kuard:

bash
$ kubectl run kuard --generator=run-pod/v1 \
  --image=gcr.io/kuar-demo/kuard-amd64:blue

وضعیت این Pod را با این دستور ببینید:

bash
$ kubectl get pods

ابتدا ممکن است container را Pending ببینید، اما در نهایت به Running می‌رود؛ یعنی Pod و containerهایش با موفقیت ساخته شده‌اند.

فعلاً این Pod را با این دستور حذف کنید:

bash
$ kubectl delete pods/kuard

حالا به نوشتن Pod manifest کامل با دست می‌رویم.

ساخت Pod Manifest

Pod manifestها را می‌توان با YAML یا JSON نوشت، اما YAML معمولاً ترجیح داده می‌شود چون کمی بیشتر برای انسان قابل ویرایش است و امکان افزودن comment دارد. Pod manifestها (و objectهای دیگر API Kubernetes) باید واقعاً مثل source code با آن‌ها رفتار شود و commentها به توضیح Pod به اعضای جدید تیم که برای اولین بار می‌بینند کمک می‌کنند.

Pod manifestها شامل چند فیلد و attribute کلیدی هستند: بخش metadata برای توصیف Pod و labelهایش، بخش spec برای توصیف volumeها و فهرست containerهایی که در Pod اجرا می‌شوند.

در فصل ۲ kuard را با دستور Docker زیر deploy کردیم:

bash
$ docker run -d --name kuard \
  --publish 8080:8080 \
  gcr.io/kuar-demo/kuard-amd64:blue

نتیجهٔ مشابهی با نوشتن مثال ۵-۱ در فایلی به نام kuard-pod.yaml و سپس استفاده از دستورات kubectl برای بارگذاری آن manifest به Kubernetes حاصل می‌شود.

مثال ۵-۱. kuard-pod.yaml

yaml
apiVersion: v1
kind: Pod
metadata:
  name: kuard
spec:
  containers:
    - image: gcr.io/kuar-demo/kuard-amd64:blue
      name: kuard
      ports:
        - containerPort: 8080
          name: http
          protocol: TCP

اجرای Podها

در بخش قبل Pod manifest ساختیم که می‌تواند Pod اجرای kuard را راه بیندازد. با دستور kubectl apply یک نمونه از kuard را launch کنید:

bash
$ kubectl apply -f kuard-pod.yaml

Pod manifest به سرور API Kubernetes ارسال می‌شود. سیستم Kubernetes سپس آن Pod را روی node سالم کلاستر schedule می‌کند، جایی که توسط process daemon kubelet نظارت می‌شود. اگر هنوز همهٔ قطعات متحرک Kubernetes را نمی‌فهمید نگران نباشید؛ در طول کتاب جزئیات بیشتری می‌آید.

فهرست Podها

حالا که Pod در حال اجرا داریم، بیشتر دربارهٔ آن بدانیم. با ابزار خط فرمان kubectl همهٔ Podهای در حال اجرا در کلاستر را فهرست می‌کنیم. فعلاً باید فقط همان Podی باشد که در گام قبل ساختیم:

bash
$ kubectl get pods
NAME       READY            STATUS    RESTARTS    AGE
kuard      1/1              Running   0           44s

نام Pod (kuard) که در فایل YAML قبلی دادیم را می‌بینید. علاوه بر تعداد containerهای آماده (۱/۱)، خروجی وضعیت، تعداد restartهای Pod و سن Pod را هم نشان می‌دهد.

اگر بلافاصله پس از ساخت Pod این دستور را زدید ممکن است ببینید:

NAME           READY       STATUS     RESTARTS    AGE
kuard          0/1         Pending    0           1s

وضعیت Pending یعنی Pod ارسال شده اما هنوز schedule نشده.

اگر خطای مهم‌تری رخ دهد (مثلاً تلاش برای ساخت Pod با container imageای که وجود ندارد)، در فیلد status هم فهرست می‌شود.

به‌طور پیش‌فرض ابزار خط فرمان kubectl سعی می‌کند در اطلاعاتی که گزارش می‌دهد مختصر باشد، اما با پرچم‌های خط فرمان اطلاعات بیشتری بگیرید. افزودن -o wide به هر دستور kubectl کمی اطلاعات بیشتر چاپ می‌کند (در حالی که هنوز سعی می‌کند در یک خط بماند). افزودن -o json یا -o yaml objectهای کامل را به‌صورت JSON یا YAML چاپ می‌کند.

جزئیات Pod

گاهی نمای تک‌خطی به‌خاطر اختصار کافی نیست. علاوه بر این Kubernetes eventهای متعددی دربارهٔ Podها نگه می‌دارد که در event stream هستند، نه به object Pod وصل.

برای اطلاعات بیشتر دربارهٔ Pod (یا هر object Kubernetes) از دستور kubectl describe استفاده کنید. مثلاً برای توصیف Podی که قبلاً ساختیم:

bash
$ kubectl describe pods kuard

اطلاعات زیادی دربارهٔ Pod در بخش‌های مختلف چاپ می‌شود. در بالا اطلاعات پایه:

Name:                  kuard
Namespace:             default
Node:                  node1/10.0.15.185
Start Time:            Sun, 02 Jul 2017 15:00:38 -0700
Labels:                <none>
Annotations:           <none>
Status:                Running
IP:                    192.168.199.238
Controllers:           <none>

سپس اطلاعاتی دربارهٔ containerهای در حال اجرا در Pod:

Containers:
  kuard:
    Container ID: docker://055095…
    Image:         gcr.io/kuar-demo/kuard-amd64:blue
    Image ID:      docker-pullable://gcr.io/kuar-demo/kuard-amd64@sha256:a580…
    Port:          8080/TCP
    State:         Running
      Started:     Sun, 02 Jul 2017 15:00:41 -0700
    Ready:         True
    Restart Count: 0
    Environment:   <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-cg5f5 (ro)

در نهایت eventهای مرتبط با Pod، مثل زمان schedule شدن، pull شدن image و اگر/وقتی به‌خاطر fail شدن health checkها restart شد:

Events:
  Seen From             SubObjectPath              Type     Reason    Message
  ---- ----             -------------              -------- ------    -------
  50s default-scheduler                            Normal   Scheduled Success…
  49s kubelet, node1    spec.containers{kuard}     Normal   Pulling   pulling…
  47s kubelet, node1    spec.containers{kuard}     Normal   Pulled    Success…
  47s kubelet, node1    spec.containers{kuard}     Normal   Created   Created…
  47s kubelet, node1    spec.containers{kuard}     Normal   Started   Started…

حذف Pod

وقتی وقت حذف Pod است، یا با نام:

bash
$ kubectl delete pods/kuard

یا با همان فایلی که برای ساخت استفاده کردید:

bash
$ kubectl delete -f kuard-pod.yaml

وقتی Pod حذف می‌شود، بلافاصله kill نمی‌شود. اگر kubectl get pods بزنید می‌بینید Pod در وضعیت Terminating است. همهٔ Podها دورهٔ grace برای خاتمه دارند. به‌طور پیش‌فرض ۳۰ ثانیه است. وقتی Pod به Terminating می‌رود دیگر درخواست جدید نمی‌گیرد. در سناریوی serving، دورهٔ grace برای قابلیت اطمینان مهم است چون به Pod اجازه می‌دهد هر درخواست فعالی که ممکن است در حال پردازش باشد قبل از خاتمه تمام کند.

مهم است توجه کنید وقتی Pod را حذف می‌کنید، هر داده‌ای که در containerهای مرتبط با آن Pod ذخیره شده هم حذف می‌شود. اگر می‌خواهید داده در چند نمونه از Pod بماند، باید از PersistentVolumeها استفاده کنید که در پایان این فصل توضیح داده می‌شوند.

دسترسی به Pod

حالا که Pod در حال اجراست، به دلایل مختلف می‌خواهید به آن دسترسی پیدا کنید. شاید بخواهید سرویس وب در حال اجرا در Pod را بارگذاری کنید. شاید logها را برای دیباگ مشکل ببینید یا حتی دستورات دیگر در زمینهٔ Pod برای دیباگ اجرا کنید. بخش‌های زیر راه‌های مختلف تعامل با کد و دادهٔ در حال اجرا داخل Pod را توضیح می‌دهند.

استفاده از Port Forwarding

بعداً در کتاب نشان می‌دهیم چگونه service را به دنیا یا containerهای دیگر با load balancerها expose کنید — اما اغلب فقط می‌خواهید به Pod خاصی دسترسی پیدا کنید، حتی اگر ترافیک را روی اینترنت serve نمی‌کند.

برای این کار از پشتیبانی port-forwarding داخلی API و ابزارهای خط فرمان Kubernetes استفاده کنید.

وقتی اجرا می‌کنید:

bash
$ kubectl port-forward kuard 8080:8080

تونل امنی از ماشین محلی، از طریق master Kubernetes، به نمونهٔ Pod روی یکی از worker nodeها ساخته می‌شود.

تا وقتی دستور port-forward در حال اجراست، می‌توانید به Pod دسترسی پیدا کنید (در این مورد رابط وب kuard) در http://localhost:8080.

اطلاعات بیشتر با Logها

وقتی برنامه به دیباگ نیاز دارد، فراتر از describe رفتن مفید است تا بفهمید برنامه چه می‌کند. Kubernetes دو دستور برای دیباگ containerهای در حال اجرا دارد. دستور kubectl logs logهای فعلی را از نمونهٔ در حال اجرا دانلود می‌کند:

bash
$ kubectl logs kuard

افزودن پرچم -f باعث stream پیوستهٔ logها می‌شود.

kubectl logs همیشه سعی می‌کند logها را از container در حال اجرای فعلی بگیرد. افزودن پرچم --previous logها را از نمونهٔ قبلی container می‌گیرد. این مثلاً وقتی مفید است که containerها به‌خاطر مشکل در startup مداوم restart می‌شوند.

اگرچه kubectl logs برای دیباگ یک‌بارهٔ containerها در production مفید است، معمولاً استفاده از سرویس log aggregation مفیدتر است. چند ابزار log aggregation متن‌باز مثل fluentd و elasticsearch و ارائه‌دهندگان cloud logging متعدد وجود دارد. سرویس‌های log aggregation ظرفیت بیشتری برای نگه‌داری logهای طولانی‌تر و قابلیت‌های جستجو و فیلتر غنی‌تر دارند. در نهایت اغلب امکان تجمیع logها از چند Pod در یک نما را می‌دهند.

اجرای دستورات در Container با exec

گاهی logها کافی نیستند و برای فهمیدن واقعی وضعیت باید در زمینهٔ خود container دستور اجرا کنید. برای این کار:

bash
$ kubectl exec kuard date

با افزودن پرچم‌های -it session تعاملی می‌گیرید:

bash
$ kubectl exec -it kuard ash

کپی فایل به و از Containerها

گاهی باید فایل‌ها را از container راه‌دور به ماشین محلی برای کاوش عمیق‌تر کپی کنید. مثلاً می‌توانید با ابزاری مثل Wireshark visual captureهای tcpdump را ببینید. فرض کنید فایلی به نام /captures/capture3.txt داخل container در Pod دارید. می‌توانید آن را با امنیت به ماشین محلی کپی کنید:

bash
$ kubectl cp <pod-name>:/captures/capture3.txt ./capture3.txt

گاهی باید فایل از ماشین محلی به container کپی کنید. فرض کنید می‌خواهید $HOME/config.txt را به container راه‌دور کپی کنید:

bash
$ kubectl cp $HOME/config.txt <pod-name>:/config.txt

به‌طور کلی کپی فایل به container anti-pattern است. واقعاً باید محتوای container را immutable در نظر بگیرید. اما گاهی سریع‌ترین راه برای متوقف کردن خونریزی و بازگرداندن سرویس به سلامت است، چون سریع‌تر از build، push و rollout image جدید است. اما وقتی خونریزی متوقف شد، فوراً build image و rollout را انجام دهید، وگرنه تغییر محلی که به container دادید را در rollout منظم بعدی فراموش می‌کنید و بازنویسی می‌شود.

Health Checkها

وقتی برنامه را به‌صورت container در Kubernetes اجرا می‌کنید، به‌طور خودکار با health check فرآیند زنده نگه‌داشته می‌شود. این health check فقط اطمینان می‌دهد فرآیند اصلی برنامه همیشه در حال اجراست. اگر نباشد، Kubernetes آن را restart می‌کند.

اما در بیشتر موارد، بررسی سادهٔ فرآیند کافی نیست. مثلاً اگر فرآیند deadlock کرده و نمی‌تواند درخواست serve کند، health check فرآیند هنوز برنامه را سالم می‌داند چون فرآیند در حال اجراست.

برای این موضوع Kubernetes health check برای liveness برنامه معرفی کرد. health checkهای liveness منطق خاص برنامه (مثلاً بارگذاری صفحهٔ وب) را اجرا می‌کنند تا تأیید کنند برنامه فقط در حال اجرا نیست، بلکه درست کار می‌کند. چون این health checkهای liveness خاص برنامه هستند، باید آن‌ها را در Pod manifest تعریف کنید.

Liveness Probe

وقتی process kuard بالا و در حال اجراست، راهی برای تأیید اینکه واقعاً سالم است و نباید restart شود لازم است. liveness probeها per container تعریف می‌شوند؛ یعنی هر container داخل Pod جدا health-check می‌شود. در مثال ۵-۲ liveness probe به container kuard اضافه می‌کنیم که درخواست HTTP علیه مسیر /healthy روی container اجرا می‌کند.

مثال ۵-۲. kuard-pod-health.yaml

yaml
apiVersion: v1
kind: Pod
metadata:
  name: kuard
spec:
  containers:
    - image: gcr.io/kuar-demo/kuard-amd64:blue
      name: kuard
      livenessProbe:
        httpGet:
          path: /healthy
          port: 8080
        initialDelaySeconds: 5
        timeoutSeconds: 1
        periodSeconds: 10
        failureThreshold: 3
      ports:
        - containerPort: 8080
          name: http
          protocol: TCP

Pod manifest قبلی از probe نوع httpGet برای درخواست HTTP GET علیه endpoint /healthy روی پورت ۸۰۸۰ container kuard استفاده می‌کند. probe مقدار initialDelaySeconds برابر ۵ تنظیم می‌کند و تا ۵ ثانیه پس از ساخت همهٔ containerهای Pod فراخوانی نمی‌شود. probe باید در timeout یک ثانیه پاسخ دهد و کد وضعیت HTTP باید بزرگ‌تر یا مساوی ۲۰۰ و کمتر از ۴۰۰ باشد تا موفق محسوب شود. Kubernetes هر ۱۰ ثانیه probe را فراخوانی می‌کند. اگر بیش از سه probe متوالی fail شود، container fail می‌شود و restart می‌شود.

می‌توانید این را در صفحهٔ وضعیت kuard ببینید. Pod را با این manifest بسازید و port-forward به آن Pod کنید:

bash
$ kubectl apply -f kuard-pod-health.yaml
$ kubectl port-forward kuard 8080:8080

مرورگر را به http://localhost:8080 ببرید. تب «Liveness Probe» را بزنید. جدولی از همهٔ probeهایی که این نمونه از kuard دریافت کرده می‌بینید. اگر لینک «Fail» را بزنید، kuard شروع به fail کردن health checkها می‌کند. کافی است صبر کنید تا Kubernetes container را restart کند. آن وقت نمایش reset می‌شود و دوباره شروع می‌کند. جزئیات restart را با kubectl describe pods kuard پیدا کنید. بخش «Events» متنی شبیه زیر دارد:

Killing container with id docker://2ac946...:pod "kuard_default(9ee84...)"
container "kuard" is unhealthy, it will be killed and re-created.

اگرچه پاسخ پیش‌فرض به liveness check ناموفق restart کردن Pod است، رفتار واقعی توسط restartPolicy Pod تعیین می‌شود. سه گزینه برای restart policy وجود دارد: Always (پیش‌فرض)، OnFailure (فقط در liveness failure یا exit code غیرصفر فرآیند) یا Never.

Readiness Probe

البته liveness تنها نوع health check نیست. Kubernetes بین liveness و readiness تمایز قائل می‌شود. liveness تعیین می‌کند برنامه درست اجرا می‌شود. containerهایی که liveness check fail می‌کنند restart می‌شوند. readiness توصیف می‌کند container چه زمانی آمادهٔ serve کردن درخواست‌های کاربر است. containerهایی که readiness check fail می‌کنند از load balancerهای سرویس حذف می‌شوند. readiness probeها مشابه liveness probeها پیکربندی می‌شوند. serviceهای Kubernetes را در فصل ۷ با جزئیات بررسی می‌کنیم.

ترکیب readiness و liveness probeها کمک می‌کند فقط containerهای سالم در کلاستر اجرا شوند.

انواع Health Check

علاوه بر بررسی‌های HTTP، Kubernetes health checkهای tcpSocket را هم پشتیبانی می‌کند که socket TCP باز می‌کنند؛ اگر اتصال موفق باشد probe موفق است. این نوع probe برای برنامه‌های غیر HTTP مفید است؛ مثلاً پایگاه‌های داده یا APIهای غیر HTTP.

در نهایت Kubernetes اجازهٔ exec probe می‌دهد. این‌ها script یا برنامه را در زمینهٔ container اجرا می‌کنند. طبق قرارداد معمول، اگر script کد خروج صفر برگرداند probe موفق است؛ وگرنه fail. scriptهای exec اغلب برای منطق اعتبارسنجی سفارشی برنامه که در فراخوانی HTTP جا نمی‌شود مفیدند.

مدیریت منابع

بیشتر افراد به‌خاطر بهبودهای چشمگیر در بسته‌بندی image و deploy قابل‌اعتماد به container و orchestratorهایی مثل Kubernetes می‌آیند. علاوه بر primitiveهای برنامه‌محور که توسعهٔ سیستم توزیع‌شده را ساده می‌کنند، به‌همان اندازه مهم توانایی افزایش بهره‌وری کلی nodeهای compute که کلاستر را می‌سازند است. هزینهٔ پایهٔ بهره‌برداری از ماشین، مجازی یا فیزیکی، تقریباً ثابت است چه بیکار باشد چه کاملاً بارگذاری‌شده. در نتیجه اطمینان از فعال بودن حداکثری این ماشین‌ها کارایی هر دلار هزینهٔ زیرساخت را بالا می‌برد.

به‌طور کلی این کارایی را با معیار utilization می‌سنجیم. utilization به‌عنوان مقدار منبعی که فعال استفاده می‌شود تقسیم بر مقدار منبعی که خریداری شده تعریف می‌شود. مثلاً اگر ماشین یک-core بخرید و برنامه یک‌دهم core استفاده کند، utilization شما ۱۰٪ است.

با سیستم‌های scheduling مثل Kubernetes که resource packing را مدیریت می‌کنند، می‌توانید utilization را به بالای ۵۰٪ برسانید.

برای این کار باید به Kubernetes دربارهٔ منابع مورد نیاز برنامه بگویید تا Kubernetes بهترین packing containerها روی ماشین‌های خریداری‌شده را پیدا کند.

Kubernetes به کاربر اجازه می‌دهد دو معیار منبع مختلف مشخص کند. resource request حداقل منبع لازم برای اجرای برنامه را مشخص می‌کند. resource limit حداکثر منبعی که برنامه می‌تواند مصرف کند را مشخص می‌کند. هر دو در بخش‌های بعد با جزئیات بیشتر توضیح داده می‌شوند.

Resource Request: حداقل منابع لازم

در Kubernetes، Pod منابع لازم برای اجرای containerهایش را درخواست می‌کند. Kubernetes تضمین می‌کند این منابع برای Pod در دسترس باشند. رایج‌ترین منابع درخواستی CPU و memory هستند، اما Kubernetes از انواع منابع دیگر مثل GPU و بیشتر هم پشتیبانی می‌کند.

مثلاً برای درخواست اینکه container kuard روی ماشینی با نیم core آزاد بیفتد و ۱۲۸ مگابایت memory به آن اختصاص یابد، Pod را طبق مثال ۵-۳ تعریف می‌کنیم.

مثال ۵-۳. kuard-pod-resreq.yaml

yaml
apiVersion: v1
kind: Pod
metadata:
  name: kuard
spec:
  containers:
    - image: gcr.io/kuar-demo/kuard-amd64:blue
      name: kuard
      resources:
        requests:
          cpu: "500m"
          memory: "128Mi"
      ports:
        - containerPort: 8080
          name: http
          protocol: TCP

منابع per container درخواست می‌شوند، نه per Pod. مجموع منابع درخواستی Pod جمع منابع درخواستی همهٔ containerهای Pod است. دلیلش این است که در بسیاری موارد containerهای مختلف نیاز CPU بسیار متفاوتی دارند. مثلاً در Pod web server و data synchronizer، web server user-facing است و احتمالاً CPU زیادی می‌خواهد، در حالی که data synchronizer با خیلی کمتر هم می‌تواند کار کند.

جزئیات limit درخواست

requestها هنگام schedule کردن Podها به nodeها استفاده می‌شوند. scheduler Kubernetes اطمینان می‌دهد جمع همهٔ requestهای همهٔ Podها روی یک node از ظرفیت node تجاوز نکند. در نتیجه Pod تضمین می‌شود حداقل منابع درخواستی را هنگام اجرا روی node داشته باشد. مهم است «request» حداقل را مشخص می‌کند، نه سقف حداکثر مصرف منابع Pod. برای فهمیدن معنا، مثالی ببینیم.

تصور کنید containerای داریم که کدش سعی می‌کند از همهٔ coreهای CPU در دسترس استفاده کند. فرض کنید Pod با این container می‌سازیم که ۰.۵ CPU درخواست می‌کند. Kubernetes این Pod را روی ماشینی با مجموع ۲ core CPU schedule می‌کند.

تا وقتی تنها Pod روی ماشین است، هر ۲.۰ core در دسترس را مصرف می‌کند، با وجود اینکه فقط ۰.۵ CPU درخواست کرده.

اگر Pod دوم با همان container و همان درخواست ۰.۵ CPU روی ماشین بیفتد، هر Pod ۱.۰ core می‌گیرد.

اگر Pod سوم یکسان schedule شود، هر Pod ۰.۶۶ core می‌گیرد. در نهایت اگر Pod چهارم یکسان schedule شود، هر Pod ۰.۵ core درخواستی‌اش را می‌گیرد و node به ظرفیت می‌رسد.

requestهای CPU با قابلیت cpu-shares در kernel لینوکس پیاده‌سازی می‌شوند.

requestهای memory مشابه CPU handle می‌شوند، اما تفاوت مهمی هست. اگر container از memory request خود بیشتر باشد، OS نمی‌تواند memory را از process بگیرد چون allocate شده. در نتیجه وقتی سیستم memory تمام می‌شود، kubelet containerهایی را که مصرف memory از memory درخواستی‌شان بیشتر است terminate می‌کند. این containerها به‌طور خودکار restart می‌شوند، اما با memory کمتری روی ماشین برای مصرف container.

چون resource request در دسترس بودن منابع برای Pod را تضمین می‌کند، برای اطمینان از منابع کافی containerها در شرایط بار بالا حیاتی‌اند.

محدود کردن مصرف منابع با Limit

علاوه بر تنظیم منابع لازم Pod که حداقل منابع در دسترس را تعیین می‌کند، می‌توانید حداکثر مصرف منابع Pod را از طریق resource limit هم تنظیم کنید.

در مثال قبلی Pod kuard ساختیم که حداقل ۰.۵ core و ۱۲۸ مگابایت memory درخواست می‌کرد. در Pod manifest مثال ۵-۴ این پیکربندی را گسترش می‌دهیم و limit ۱.۰ CPU و ۲۵۶ مگابایت memory اضافه می‌کنیم.

مثال ۵-۴. kuard-pod-reslim.yaml

yaml
apiVersion: v1
kind: Pod
metadata:
  name: kuard
spec:
  containers:
    - image: gcr.io/kuar-demo/kuard-amd64:blue
      name: kuard
      resources:
        requests:
          cpu: "500m"
          memory: "128Mi"
        limits:
          cpu: "1000m"
          memory: "256Mi"
      ports:
        - containerPort: 8080
          name: http
          protocol: TCP

وقتی limit روی container تنظیم می‌کنید، kernel طوری پیکربندی می‌شود که مصرف از این limitها تجاوز نکند. container با limit CPU ۰.۵ core فقط ۰.۵ core می‌گیرد، حتی اگر CPU بیکار باشد. container با limit memory ۲۵۶ مگابایت memory اضافی نمی‌گیرد (مثلاً malloc fail می‌شود) اگر مصرف memory از ۲۵۶ مگابایت بیشتر شود.

ماندگاری داده با Volumeها

وقتی Pod حذف می‌شود یا container restart می‌شود، هر داده‌ای در filesystem container هم حذف می‌شود. اغلب خوب است چون نمی‌خواهید باقی‌مانده‌ای که برنامهٔ وب stateless شما نوشته بماند. در موارد دیگر دسترسی به ذخیرهٔ disk پایدار بخش مهمی از برنامهٔ سالم است. Kubernetes چنین ذخیرهٔ پایداری را مدل می‌کند.

استفاده از Volumeها با Podها

برای افزودن volume به Pod manifest دو بخش جدید به پیکربندی اضافه می‌شود. اول بخش جدید spec.volumes. این آرایه همهٔ volumeهایی را تعریف می‌کند که containerهای Pod manifest ممکن است به آن‌ها دسترسی داشته باشند. مهم است همهٔ containerها مجبور نیستند همهٔ volumeهای تعریف‌شده در Pod را mount کنند. دوم افزودن آرایهٔ volumeMounts در تعریف container. این آرایه volumeهایی را که به container خاص mount می‌شوند و مسیر mount هر volume را تعریف می‌کند. توجه کنید دو container مختلف در یک Pod می‌توانند همان volume را در مسیر mount متفاوت mount کنند.

manifest مثال ۵-۵ یک volume جدید به نام kuard-data تعریف می‌کند که container kuard آن را در مسیر /data mount می‌کند.

مثال ۵-۵. kuard-pod-vol.yaml

yaml
apiVersion: v1
kind: Pod
metadata:
  name: kuard
spec:
  volumes:
    - name: "kuard-data"
      hostPath:
        path: "/var/lib/kuard"
  containers:
    - image: gcr.io/kuar-demo/kuard-amd64:blue
      name: kuard
      volumeMounts:
        - mountPath: "/data"
          name: "kuard-data"
      ports:
        - containerPort: 8080
          name: http
          protocol: TCP

روش‌های مختلف استفاده از Volumeها با Podها

راه‌های متنوعی برای استفاده از داده در برنامه وجود دارد. چند مورد و الگوهای توصیه‌شده برای Kubernetes:

ارتباط/همگام‌سازی

در اولین مثال Pod دیدیم دو container چگونه از volume مشترک برای serve کردن سایت و همگام نگه‌داشتن آن با مکان Git راه‌دور استفاده کردند. برای این کار Pod از volume نوع emptyDir استفاده می‌کند. چنین volumeای به عمر Pod محدود است، اما بین دو container به اشتراک گذاشته می‌شود و پایهٔ ارتباط بین containerهای Git sync و web serving ماست.

Cache

برنامه ممکن است volumeای داشته باشد که برای performance ارزشمند است اما برای عملکرد درست برنامه لازم نیست. مثلاً شاید thumbnailهای از پیش رندرشدهٔ تصاویر بزرگ‌تر را نگه دارد. البته می‌توان از تصاویر اصلی بازسازی کرد، اما serve کردن thumbnailها گران‌تر می‌شود. می‌خواهید چنین cacheای از restart container به‌خاطر health-check failure بماند؛ پس emptyDir برای use case cache هم خوب کار می‌کند.

دادهٔ پایدار

گاهی volume را برای دادهٔ واقعاً پایدار استفاده می‌کنید — داده‌ای مستقل از عمر Pod خاص و باید بین nodeهای کلاستر جابه‌جا شود اگر node fail کند یا Pod به دلیلی به ماشین دیگر برود. برای این کار Kubernetes از volumeهای ذخیرهٔ شبکهٔ راه‌دور متنوع پشتیبانی می‌کند، از جمله پروتکل‌های پرکاربرد مثل NFS و iSCSI و همچنین ذخیرهٔ شبکهٔ cloud provider مثل Amazon Elastic Block Store، Azure Files and Disk Storage و Google Persistent Disk.

Mount کردن filesystem میزبان

برنامه‌های دیگر واقعاً به volume پایدار نیاز ندارند، اما به دسترسی به filesystem میزبان زیرین نیاز دارند. مثلاً ممکن است به filesystem /dev برای دسترسی block-level خام به دستگاه روی سیستم نیاز داشته باشند. برای این موارد Kubernetes از volume نوع hostPath پشتیبانی می‌کند که می‌تواند مکان‌های دلخواه روی worker node را به container mount کند.

مثال قبلی از نوع volume hostPath استفاده می‌کند. volume ساخته‌شده /var/lib/kuard روی میزبان است.

ماندگاری داده با دیسک‌های راه‌دور

اغلب می‌خواهید داده‌ای که Pod استفاده می‌کند با Pod بماند، حتی اگر روی host دیگر restart شود.

برای این کار می‌توانید volume ذخیرهٔ شبکهٔ راه‌دور را به Pod mount کنید. با ذخیرهٔ مبتنی بر شبکه، Kubernetes به‌طور خودکار ذخیرهٔ مناسب را mount و unmount می‌کند هر وقت Pod استفاده‌کننده از آن volume به ماشین خاصی schedule شود.

روش‌های متعددی برای mount volume از طریق شبکه وجود دارد. Kubernetes از پروتکل‌های استاندارد مثل NFS و iSCSI و همچنین APIهای ذخیرهٔ مبتنی بر cloud provider برای cloud providerهای اصلی (عمومی و خصوصی) پشتیبانی می‌کند. در بسیاری موارد cloud providerها دیسک را هم می‌سازند اگر از قبل وجود نداشته باشد.

مثال استفاده از سرور NFS:

yaml
...
# Rest of pod definition above here
volumes:
    - name: "kuard-data"
      nfs:
         server: my.nfs.server.local
         path: "/exports"

persistent volume موضوع عمیقی با جزئیات متعدد است: به‌ویژه نحوهٔ کار persistent volume، persistent volume claim و dynamic volume provisioning با هم. بررسی عمیق‌تر در فصل ۱۵ است.

جمع‌بندی نهایی

بسیاری از برنامه‌ها stateful هستند و باید هر داده‌ای را حفظ کنیم و دسترسی به volume ذخیرهٔ زیرین را صرف‌نظر از ماشینی که برنامه روی آن اجرا می‌شود تضمین کنیم. همان‌طور که قبلاً دیدیم، این با persistent volume پشتیبان‌گیری‌شده از ذخیرهٔ متصل به شبکه حاصل می‌شود. همچنین می‌خواهیم مطمئن شویم نمونهٔ سالم برنامه همیشه در حال اجراست؛ یعنی می‌خواهیم container اجرای kuard قبل از expose به کلاینت‌ها آماده باشد.

با ترکیب persistent volume، readiness و liveness probe و محدودیت‌های منابع، Kubernetes همهٔ آنچه برای اجرای قابل‌اعتماد برنامه‌های stateful لازم است را فراهم می‌کند. مثال ۵-۶ همهٔ این‌ها را در یک manifest جمع می‌کند.

مثال ۵-۶. kuard-pod-full.yaml

yaml
apiVersion: v1
kind: Pod
metadata:
  name: kuard
spec:
  volumes:
    - name: "kuard-data"
      nfs:
        server: my.nfs.server.local
        path: "/exports"
  containers:
    - image: gcr.io/kuar-demo/kuard-amd64:blue
      name: kuard
      ports:
        - containerPort: 8080
           name: http
           protocol: TCP
      resources:
        requests:
           cpu: "500m"
           memory: "128Mi"
        limits:
           cpu: "1000m"
           memory: "256Mi"
      volumeMounts:
        - mountPath: "/data"
           name: "kuard-data"
      livenessProbe:
        httpGet:
           path: /healthy
           port: 8080
        initialDelaySeconds: 5
        timeoutSeconds: 1
        periodSeconds: 10
        failureThreshold: 3
      readinessProbe:
        httpGet:
           path: /ready
           port: 8080
        initialDelaySeconds: 30
        timeoutSeconds: 1
        periodSeconds: 10
        failureThreshold: 3

خلاصه

Podها نمایانگر واحد atomic کار در کلاستر Kubernetes هستند. Podها از یک یا چند container تشکیل می‌شوند که همزیستی کار می‌کنند. برای ساخت Pod، Pod manifest می‌نویسید و با ابزار خط فرمان به سرور API Kubernetes ارسال می‌کنید (یا کمتر با فراخوانی مستقیم HTTP و JSON به سرور).

پس از ارسال manifest به سرور API، scheduler Kubernetes ماشینی می‌یابد که Pod در آن جا شود و Pod را به آن ماشین schedule می‌کند. پس از schedule، process daemon kubelet روی آن ماشین مسئول ساخت containerهای متناظر با Pod و انجام health checkهای تعریف‌شده در Pod manifest است.

وقتی Pod به node schedule شد، اگر آن node fail کند rescheduling رخ نمی‌دهد. علاوه بر این، برای ساخت چند replica از همان Pod باید آن‌ها را دستی بسازید و نام‌گذاری کنید. در فصل بعد object ReplicaSet را معرفی می‌کنیم و نشان می‌دهیم چگونه ساخت چند Pod یکسان را خودکار کنید و در صورت failure ماشین node دوباره ساخته شوند.