رفتن به محتوای اصلی

Pedram Sarani

یادداشت‌های SRE و DevOps

بازگشت به فهرست درس‌ها

مدل ذهنی Kubernetes networking؛ از Pod IP تا Service و Gateway

وقتی ارتباطی در Kubernetes خراب می‌شود، فقط دیدن YAML کافی نیست. این بخش از مسیر یادگیری کمک می‌کند مسیر واقعی ترافیک را از IPهای اصلی تا DNS، CNI، kube-proxy، conntrack، NetworkPolicy و Gateway بهتر ببینیم

در کوبرنتیز، Networking فقط چند فیلد داخل manifest نیست.

وقتی در یک manifest چیزی مثل Service، Gateway، Pod یا NetworkPolicy می‌بینیم، فقط تعریف سطح API را می‌بینیم؛ نه مسیری را که packet واقعاً طی می‌کند.
در زمان اجرا، ترافیک از چند لایه عبور می‌کند: از network namespace و interfaceهای داخل پاد شروع می‌شود، روی routeها و kernel هر نود جلو می‌رود، با DNS و Service و EndpointSlice به مقصد نزدیک‌تر می‌شود، و در نهایت به kube-proxy، CNI، conntrack و گاهی Gateway یا Ingress controller گره می‌خورد.

این اشتباه است که فکر کنیم اگر در YAML یک Service تعریف شده، پس حتماً یک مقصد واقعی با همان IP جایی داخل کلاستر وجود دارد. در عمل، خیلی وقت‌ها چیزی که در manifest می‌بینیم یک abstraction است؛ نه process است، نه container، و حتی همیشه هم یک interface واقعی پشتش نیست.

در این بخش قرار نیست همه جزئیات نتورک کوبرنتیز را باز کنیم. هدف فعلاً ساختن یک نقشه ذهنی درست است. چیزی که بعداً کمک می‌کند در مقاله‌های بعدی، مسیر پکت را مرحله‌به‌مرحله دنبال کنیم و وسط راه قاطی نکنیم که الان DNS وسط ماجراست، یا Service، یا CNI، یا conntrack.

چرا به مدل ذهنی نیاز داریم؟

در خطاهای واقعی، سؤال اصلی معمولاً این نیست که «آیا manifest درست نوشته شده یا نه؟».سؤال مهم‌تر این است که request واقعاً از کجا عبور کرده است.

در خطاهای واقعی، معمولاً مشکل فقط یک جا نیست. ممکن است Service ساخته شده باشد، اما EndpointSlice پشت آن خالی بماند. ممکن است DNS جواب بدهد، ولی بک‌اند هنوز آماده نباشد. گاهی kube-proxy قانون‌های لازم را ساخته، اما CNI مسیر بین نودها را درست جلو نبرده است. گاهی هم پاد مقصد سالم است، ولی NetworkPolicy اجازه عبور نمی‌دهد. از آن طرف، ممکن است اتصال‌های جدید به بک‌اند جدید برسند، اما اتصال‌های قدیمی به‌خاطر وضعیت قبلی در conntrack همچنان مسیر قبلی را نگه دارند و هزاران مورد دیگر …

برای همین بحث شبکه را باید به‌جای یک manifest ساده، مثل یک مسیر چندلایه ببینیم.

در ساده‌ترین مدل، هر درخواست در کوبرنتیز با چند سؤال اصلی روبه‌رو است:

  • اول باید ببینیم مقصد یک IP واقعی پاد است یا یک Service IP مجازی.
  • اگر مقصد با نام صدا زده شده، باید ببینیم DNS آن را به چه آدرسی برمی‌گرداند.
  • بعد مشخص می‌کنیم ترافیک داخل همین نود می‌ماند یا باید از نود خارج شود.
  • اگر مقصد Service است، باید EndpointSliceها را ببینیم و بک‌اند واقعی را پیدا کنیم.
  • بعد نوبت kube-proxy یا جایگزین آن است؛ یعنی جایی که قانون‌های لازم برای مسیر Service ساخته می‌شوند.
  • حالا باید مسیر CNI، مجوز NetworkPolicy و وضعیت conntrack را جداگانه بررسی کنیم.
  • در آخر هم مسیر برگشت را با مسیر رفت تطبیق می‌دهیم، چون خیلی از خطاها دقیقاً همین‌جا خودش را نشان می‌دهد.

این سؤال‌ها در production واقعاً به درد می‌خورند. شاید وسط incident فرصت نداشته باشیم همه این لایه‌ها را با آرامش بررسی کنیم، اما اگر این مدل ذهنی را از قبل داشته باشیم، troubleshooting از حدس‌زدن تبدیل می‌شود به دنبال‌کردن یک مسیر مشخص.(اگر هم وقت دارید که خوب پس عشق و حال به راهه)

چهار نوع IP مهم در کوبرنتیز

برای فهم نتورک در کوبرنتیز، اول باید فرق چند نوع IP را درست بفهمیم.

در یک cluster معمولی، معمولاً با این دسته‌ها سروکار داریم:

  • آدرس Pod
  • آدرس Service
  • آدرس Node
  • آدرس بیرونی یا external address

این‌ها ممکن است از دور شبیه هم به نظر برسند، اما نقش یکسانی ندارند. خود کوبرنتیز هم برای پادها، Serviceها و نودها معمولاً از بازه‌های IP جداگانه استفاده می‌کند. آدرس پاد را معمولاً CNI یا network plugin مدیریت می‌کند، آدرس Service را kube-apiserver از محدوده Serviceها اختصاص می‌دهد، و آدرس نود معمولاً از کیوبلت و وضعیت خود host می‌آید(که تو ایران تقریبا اکثر مواقع همین است) با این حال اگر کلاستر روی cloud، private cloud یا یک provider خارجی مثل OpenStack اجرا شود، cloud-controller-manager هم می‌تواند در مدیریت آدرس‌ها، routeها یا منابعی مثل LoadBalancer نقش داشته باشد.

جدول مقایسه IPهای مهم

نوع آدرسمتعلق به چیست؟چه کسی معمولاً آن را اختصاص می‌دهد؟مقصد واقعی است؟کاربرد اصلی
آدرس Podخود PodCNI یا network pluginبله، معمولاً مقصد واقعی workload استارتباط مستقیم با Pod و مقصد نهایی بیشتر مسیرهای داخلی
آدرس Service یا ClusterIPآبجکت Servicekube-apiserver از محدودهٔ Service CIDRمعمولاً نه، یک virtual IP استورودی پایدار برای رسیدن به backendهای پشت Service
آدرس Nodeماشین، VM یا instance عضو clusterkubelet و وضعیت host؛ در cloud یا private cloud ممکن است cloud-controller-manager هم نقش داشته باشدبله، متعلق به host استارتباط با node، مسیرهای بین nodeها، NodePort و بعضی مسیرهای ورودی
آدرس Externalنقطهٔ ورودی بیرون از clustercloud provider، LoadBalancer، MetalLB، Gateway، Ingress یا زیرساخت شبکهبستگی داردورود traffic از بیرون cluster به workload داخلی
Four IP

آدرس Pod چیست؟

آدرس پاد آدرسی است که به خود پاد داده می‌شود.

هر پاد network namespace خودش را دارد. containerهای داخل یک پاد همان network namespace را share می‌کنند؛ یعنی از نگاه شبکه، containerهای داخل یک پاد یک network namespace مشترک دارند و می‌توانند از طریق localhost با هم حرف بزنند.

وقتی یک Pod به Pod دیگر وصل می‌شود و مقصد را با IP صدا می‌زند، در حالت عادی Service در مسیر logical آن connection نیست. پکت از network namespace مبدأ خارج می‌شود، وارد شبکه node می‌شود، و بعد بسته به اینکه Pod مقصد روی همان node است یا روی node دیگر، از مسیر لوکال یا مسیر بین nodeها عبور می‌کند.

اینجا CNI نقش اصلی را بازی می‌کند. CNI یا همان network plugin باید کاری کند که Podها بتوانند با هم ارتباط داشته باشند. اینکه این ارتباط با route ساده، bridge، overlay، tunneling، eBPF، VXLAN، Geneve یا روش‌های دیگر انجام شود، به پیاده‌سازی CNI و topology شبکه بستگی دارد.

کوبرنتیز خودش یک مدل پایه‌ای برای شبکه تعریف می‌کند که اغلب با عنوان «IP-per-Pod» شناخته می‌شود: هر پاد یک IP منحصربه‌فرد در سطح cluster می‌گیرد، و همه پادها باید بتوانند بدون NAT با هم ارتباط داشته باشند. کوبرنتیز فقط این مدل را الزام می‌کند؛ پیاده‌سازی واقعی آن (route، bridge، overlay یا eBPF) بر عهده CNI است. همین مدل پایه است که باعث می‌شود مسیر Service، Pod-to-Pod و حتی NetworkPolicy در فصل‌های بعدی قابل‌فهم باشند.

آدرس Service چیست؟

آدرس Service یا ClusterIP معمولاً یک IP مجازی است.

وقتی یک Service از نوع ClusterIP می‌سازیم، کوبرنتیز برای آن یک IP از محدوده Serviceها اختصاص می‌دهد. این محدوده معمولاً با --service-cluster-ip-range برای kube-apiserver تنظیم می‌شود.

کلاینت ها می‌توانند به این IP وصل شوند کوبرنتیز ترافیک را به یکی از پادهای پشت آن Service هدایت می‌کند. اما این IP مثل ای پی پاد نیست. آدرس Service معمولاً روی یک interface واقعی، به‌عنوان یک مقصد مستقل، وجود ندارد.

در مدل عمومی، kube-proxy با منطق packet processing، مثل iptables، nftables یا IPVS، یک virtual IP را در data plane پیاده‌سازی می‌کند و traffic را به endpoint مناسب redirect می‌کند

پس این جمله خیلی مهم است:
آدرس Service مقصد نهایی نیست؛ آدرس Service یک ورودی پایدار برای رسیدن به backendهای واقعی است.برای همین وقتی یک پاد به ClusterIP یک Service وصل می‌شود، packet در نهایت باید به ای پی یکی از backendها تبدیل شود. این تبدیل معمولاً با NAT یا مکانیزمی شبیه به آن در data plane انجام می‌شود.

آدرس Node چیست؟

آدرس Node آدرس خود ماشین، VM یا instance‌ای است که kubelet و container runtime روی آن اجرا می‌شوند.

آدرس نود را معمولاً جایی می‌بینیم که خود ماشین در مسیر مهم است؛ مثلاً وقتی کامپوننت‌های کلاستر باید با نود حرف بزنند، ترافیک بین نودها جابه‌جا می‌شود، Service از نوع NodePort یا بعضی مسیرهای LoadBalancer از نودها به‌عنوان نقطه ورود استفاده می‌کنند، یا ابزارهای مانیتورینگ و دسترسی مدیریتی سراغ خود host می‌روند. بعضی مسیرهای CNI هم به همین آدرس وابسته‌اند، چون زیرساخت شبکه را روی خود نود آماده می‌کنند.

وقتی از NodePort استفاده می‌کنیم، کوبرنتیز روی هر نود یک port مشخص را برای دسترسی به Service باز می‌کند. البته جزئیات دقیق اینکه این port روی چه addressهایی در دسترس باشد، به kube-proxy مود و تنظیمات NodePort بستگی دارد. برای مثال، در nftables مود رفتار پیش‌فرض NodePort با iptables مود تفاوت‌هایی دارد. در iptabels، نودپورت ها به‌صورت پیش‌فرض روی همه local addressها در دسترس‌اند، اما در nftables مقدار پیش‌فرض به سمت primary addressهای نود محدودتر شده است، مگر اینکه تنظیمات را تغییر بدهیم.

پس آدرس Node را نباید با Pod یا Service قاطی کنیم. آدرس Node متعلق به host است، نه workload.

آدرس بیرونی یا external IP چیست؟

external IP یعنی آدرسی که از بیرون cluster دیده می‌شود.
این آدرس ممکن است از یک LoadBalancer در محیط cloud بیاید. در bare metal هم ممکن است چیزی مثل MetalLB آن را فراهم کند؛ یعنی ابزاری که آدرس را در لایه ۲ یا با BGP اعلام می‌کند. گاهی هم نقطه ورود را کنترلر Ingress، پیاده‌سازی Gateway یا حتی مسیر شبکه‌ای داخلی سازمان فراهم می‌کند.وقتی traffic از بیرون وارد cluster می‌شود، معمولاً اول به یک نقطه ورودی می‌رسد.

در عمل، این نقطه ورود شکل‌های مختلفی دارد. گاهی یک Service از نوع NodePort یا LoadBalancer در مسیر است. گاهی ترافیک اول به کنترلر Ingress یا کنترلر Gateway می‌رسد. در بعضی محیط‌ها هم یک reverse proxy بیرونی یا یک لودبالانسر سازمانی یا ابری جلوی کلاستر قرار می‌گیرد.

در این حالت، مسیر request فقط با دیدن Service یا Gateway مشخص نمی‌شود. باید بدانیم external load balancer به کجا forward می‌کند، controller مربوطه چه ruleهایی ساخته، Service پشت آن چیست، و در نهایت backendهای واقعی کدام پادها هستند. در نهایت تمام این مسیرها برای این است که درخواست به اپلیکیشن ما برسد؛ اما اینکه در این مسیر چند بار NAT انجام می‌شود، source IP حفظ می‌شود یا نه، و traffic از کدام node وارد می‌شود، به طراحی ورودی cluster بستگی دارد.

مسیر اول: ارتباط مستقیم Pod به Pod

ساده‌ترین مسیر این است که یک Pod مستقیماً به ای پی یک Pod دیگر وصل شود.

در این حالت، مسیر ذهنی چنین است:

  • مبدا یک پروسس داخل container است.
  • مسیر ترافیک از network namespace همان پاد خارج می‌شود.
  • پکت وارد مسیر شبکه‌ای نود می‌شود.
  • پاد مقصد اگر روی همان نود باشد، مسیر لوکال طی می‌شود.
  • اگر مقصد روی نود دیگری باشد، CNI باید route یا tunnel لازم را فراهم کند.
  • در نهایت پکت به network namespace مربوط به Pod مقصد می‌رسد.
  • و اخر جواب از مسیر مناسب به پاد مبدا برمی‌گردد.

در این مسیر، DNS و Service الزاماً دخیل نیستند؛ مگر اینکه مقصد ابتدا با نام resolve شده باشد یا client به‌جای ای پی از نام Service استفاده کرده باشد.

این مسیر برای فهم پایه‌ای مهم است، اما در پروداکشن کمتر پیش می‌آید applicationها مستقیماً به ای پی پاد یکدیگر وابسته شوند. دلیلش ساده است: پادها ephemeral هستند. ممکن است حذف شوند، دوباره ساخته شوند، روی نود دیگری بالا بیایند و IP جدید بگیرند.

برای همین کوبرنتیز معمولاً ما را به سمت Service می‌برد؛ یعنی برای ارتباط پایدار بین workloadها، تقریباً همیشه از Service استفاده می‌کنیم، نه از ای پی مستقیم پاد.

POD To POD

مسیر دوم: ارتباط Pod به Service

رایج‌ترین مسیر داخلی در کوبرنتیز این است که یک Pod به یک Service وصل شود.

مثلاً یک application داخل namespace خودش می‌خواهد به backend دیتابیس یا یک API داخلی وصل شود. معمولاً به‌جای اینکه ای پی پاد مقصد را بداند، از نام Service استفاده می‌کند.

اینجا مسیر ذهنی تقریباً این‌طوری می‌شود:

  • برنامه داخل پاد یک نام مثل نام Service را صدا می‌زند.
  • فرایند resolver داخل پاد از DNS مربوط به cluster سؤال می‌پرسد.
  • سیستم DNS نام Service را به ClusterIP یا record مناسب برمی‌گرداند.
  • کلاینت به ClusterIP و port مربوط به Service وصل می‌شود.
  • روی نود رول های kube-proxy یا جایگزین آن، ترافیک مربوط به ای پی Service را مچ می‌کنند.
  • یک endpoint واقعی از بین backendهای Service انتخاب می‌شود.
  • مقدار مقصد به ای پی و port واقعی backend تغییر می‌کند.
  • در نهایت پکت به پاد مقصد می‌رسد.
  • مسیر response با کمک state موجود در data plane و conntrack به client برمی‌گردد.
pod-to-service-path

در این مدل، DNS فقط نام را به آدرس مناسب تبدیل می‌کند. برای Serviceهای معمولی، این آدرس معمولاً ClusterIP است. DNS خودش backend واقعی را برای هر پکت انتخاب نمی‌کند. انتخاب backend معمولاً در لایه دیتاپلین Service انجام می‌شود.

کوبرنتیز برای Serviceها و پادها DNS record می‌سازد و kubelet طوری DNS داخل پادها را تنظیم می‌کند که کانتینرها بتوانند Serviceها را با نام پیدا کنند. همچنین queryهای بدون namespace معمولاً در namespace همان پاد جست‌وجو می‌شوند.(بیشتر در این مورد جلوتر صحبت میکنیم)

Service چطور backendهای واقعی را می‌شناسد؟

یک Service معمولاً با selector به پادها وصل می‌شود.

اما خود Service لیست backendهای واقعی را مستقیم داخل spec نگه نمی‌دارد. این کار با EndpointSlice انجام می‌شود.

EndpointSlice ها مجموعه‌ای از endpointهای پشت یک Service را نگه می‌دارند. برای Serviceهایی که selector دارند، control plane به‌صورت خودکار EndpointSlice می‌سازد و Podهایی را که با selector آن Service مچ می‌شوند، داخل EndpointSliceها قرار می‌دهد. EndpointSliceها بر اساس مواردی مثل IP family، protocol، port و نام Service گروه‌بندی می‌شوند.

پس مدل ذهنی درست این است:

Service می‌گوید: «من یک ورودی پایدار هستم.»
EndpointSlice می‌گوید: «backendهای واقعی فعلی این‌ها هستند.»
کامپوننت kube-proxy یا data plane معادل آن می‌گوید: «من ترافیک ورودی به Service را به یکی از این backendها هدایت می‌کنم.»
این تفکیک خیلی مهم است. چون در تی شوت ممکن است Service وجود داشته باشد، اما EndpointSlice خالی باشد. در این حالت DNS هم ممکن است نام Service را resolve کند، اما ترافیک به backend سالمی نرسد.

جایگاه kube-proxy دقیقاً کجای داستان است؟

در کلاسترهای سنتی یا معمول، kube-proxy روی هر node اجرا می‌شود و تغییرات Service و EndpointSlice را ازAPI server واچ می‌کند.
نقش kube-proxy این نیست که همیشه پکت را مثل یک پروکسی کلاسیک از خودش عبور بدهد. در مودهای رایج لینوکس، مثل iptables و nftables، خود kube-proxy پکت را عبور نمی‌دهد. این کامپوننت فقط رول‌ها و state لازم را روی نود آماده می‌کند؛ بعد از آن، بخش اصلی پردازش پکت را kernel انجام می‌دهد.
هر اینستنس از kube-proxy اضافه و حذف شدن Service و EndpointSlice را watch می‌کند و برای هر نود Service را طوری کانفیگ می‌کند که ترافیک به clusterIP و port آن Service کپچر شود و به یکی از endpointهای Service ریدایرکت شود.
حالتی که از iptables مود استفاده میکنیم kube-proxy اینجا ruleهایی ایجاد میکند که ترافیک مربوط به virtual IP را به ruleهای per-Service و بعد per-endpoint می‌فرستند. در نهایت ریدایرکت معمولاً با destination NAT به backend انجام می‌شود.\

تفاوت nftables، iptables و IPVS در همین مدل ذهنی

در این بخش لازم نیست وارد همه جزئیات proxy modeها شویم، اما باید جایگاهشان را بدانیم.کامپوننت kube-proxy روی لینوکس می‌تواند مودهای مختلفی داشته باشد.این مودها معمولاً شامل سه حالت اصلی هستند: حالت iptables، حالت nftables و حالت IPVS.

در هر سه مدل، هدف کلی یکی است: ترافیک مربوط به Service VIP باید به backend مناسب برسد. اما روش پیاده‌سازی فرق دارد. در iptables مود رول های netfilter با iptables ساخته می‌شوند. در nftables مود رول ها با nftables API ساخته می‌شوند. در IPVS مود kube-proxy از قابلیت load balancing در کرنل با IPVS استفاده می‌کند.

برای بررسی config مربوط به kube-proxy در بسیاری از کلاسترها، می‌توانیم از این command شروع کنیم:

root@pedram:~# kubectl -n kube-system get cm kube-proxy -o yaml

اگر مقدار مود خالی باشد، در بسیاری از نصب‌های استاندارد لینوکس معمولاً با رفتار پیش‌فرض iptables روبه‌رو می‌شویم. اما این جمله را نباید کورکورانه برای همه کلاسترها قطعی دانست. به طور مثال در کلاسترهایی که از Cilium kube-proxy replacement استفاده می‌کنند، ممکن است kube-proxy اصلاً وجود نداشته باشد یا نقش Service load balancing را eBPF انجام بدهد.پس همیشه علاوه بر config، باید componentهای واقعی نصب‌شده در cluster را هم بررسی کنیم.

kube-proxy

نقش conntrack چیست؟

conntrack بخشی از زیرساخت connection tracking در Linux networking است.

وقتی NAT انجام می‌شود، سیستم باید بداند پکت های برگشتی متعلق به کدام connection هستند و باید به چه شکلی برگردانده شوند. بدون نگه داشتن state، برگشت response در خیلی از مسیرهای NAT درست کار نمی‌کند.

پس conntrack را می‌توانیم یک جدول kernel برای flowها و connectionها بدانیم.

در کوبرنتیز ترافیک، مخصوصاً وقتی ClusterIP به ای پی پاد تبدیل می‌شود، conntrack کمک می‌کند mapping مربوط به connection حفظ شود. به همین دلیل، گاهی connectionهای قدیمی حتی بعد از تغییر endpointها همچنان رفتار قبلی را نشان می‌دهند، چون آن connection از قبل state داشته است.

این نکته در troubleshooting خیلی مهم است. اگر backendهای یک Service تغییر کنند، الزاماً تمام connectionهای موجود همان لحظه به backend جدید نمی‌روند. connectionهای جدید ممکن است backend جدید بگیرند، اما connectionهای قدیمی می‌توانند تا زمان بسته‌شدن یا expire شدن state قبلی، مسیر قبلی خودشان را ادامه بدهند.

در nftables مود هم بعضی رفتارهای مربوط به conntrack و workaroundهای تاریخی با iptables mode تفاوت دارد. همین تفاوت‌های کوچک، در خطاهای واقعی می‌توانند آدم را چند ساعت درگیر کنند، مخصوصاً وقتی فقط از روی YAML دنبال مشکل بگردیم.

جایگاه CNI در مسیر چیست؟

CNI مسئول این است که Podها بتوانند وارد شبکه شوند.

وقتی پاد ساخته می‌شود، runtime و kubelet با کمک CNI نتورک نیم اسپیس و interfaceهای لازم را آماده می‌کنند. CNI به پاد آدرس می‌دهد و مسیر ارتباط آن Pod با بقیه cluster را فراهم می‌کند.

اما همه CNIها یکسان کار نمی‌کنند. بعضی‌ها route ساده می‌سازند، بعضی overlay استفاده می‌کنند، بعضی eBPF محور هستند، بعضی NetworkPolicy را enforce می‌کنند، و بعضی هم فقط اتصال پایه‌ای را فراهم می‌کنند و برای policy به component دیگری نیاز دارند.

برای همین در troubleshooting باید CNI را جدا از Service ببینیم.

اگر Pod-to-Pod routing خراب باشد، Service هم درست کار نمی‌کند؛ چون Service در نهایت باید traffic را به ای پی واقعی برساند. اگر CNI نتواند ای پی پاد مقصد را route کند، داشتن Service و EndpointSlice سالم کافی نیست.

جایگاه NetworkPolicy در مسیر چیست؟

NetworkPolicy برای محدود کردن ترافیک در سطح L3/L4 استفاده می‌شود.

اما NetworkPolicy خودش به‌تنهایی ترافیک را محدود نمی‌کند. باید network plugin یا controllerای وجود داشته باشد که NetworkPolicy را پیاده‌سازی کند. اگر resource بسازیم اما networking solution ما آن را enforce نکند، ساختن آن resource اثر عملی ندارد.

ممکن است فکر کنیم چون NetworkPolicy نوشته شده، ارتباط‌ها محدود شده‌اند. اما اگر CNI یا policy engine آن را پشتیبانی نکند، عملاً همه‌چیز مثل قبل باز می‌ماند.

از نگاه مسیر معمولاً قبل از رسیدن ترافیک به اپلیکیشن یا هنگام خروج ترافیک از پاد enforce می‌شود، اما جزئیات دقیق enforcement به CNI بستگی دارد.

نقش DNS چیست و چه کاری نمی‌کند؟

DNS در کوبرنتیز برای Service Discovery استفاده می‌شود.

وقتی اپ به‌جای ای پی از name استفاده می‌کند، DNS کمک می‌کند آن نام به آدرس مناسب resolve شود. برای Serviceهای معمولی، این آدرس معمولاً ClusterIP است. برای headless Service رفتار فرق می‌کند و DNS می‌تواند recordهای مربوط به endpointها را برگرداند، اما این موضوع را اگر عمری باقی موند در ادامه و مبحث DNS جداگانه عمیق‌تر بررسی می‌کنیم.

نکته مهم این است که DNS جایگزین Service دیتاپلین نیست.

اگر یک پاد نام Service را resolve کند، تازه مقصد اولیه مشخص شده است. بعد از آن هنوز باید ببینیم درخواست چطور به backend واقعی می‌رسد، kube-proxy یا جایگزین آن چه ruleهایی دارد، EndpointSlice چه endpointهایی دارد، و CNI چطور پکت را به پاد مقصد می‌رساند.

پس وقتی می‌گوییم Service Discovery، منظورمان فقط DNS نیست. DNS فقط بخشی از Service Discovery است. خود Service، EndpointSlice و data plane هم در این مسیر نقش دارند.

جایگاه Gateway و Ingress در مدل چیست؟

تا اینجا بیشتر درباره ترافیک داخلی cluster صحبت کردیم.

اما وقتی کاینت از بیرون cluster وارد می‌شود، معمولاً یک لایه ورودی هم داریم. این لایه می‌تواندIngress، Gateway API، LoadBalancer، NodePort یا ترکیبی از آن‌ها باشد.

در مدل مدرن‌تر، Gateway API نقش مهمی دارد. این مدل با چند آبجکت اصلی کار می‌کند: GatewayClass می‌گوید کدام کنترلر مسئول Gateway است، Gateway نقطه ورود ترافیک را تعریف می‌کند، و routeهایی مثل HTTPRoute مشخص می‌کنند درخواست‌ها در نهایت به کدام بک‌اند برسند.

در HTTPRoute، مسیرهای HTTP از listenerهای Gateway شروع می‌شوند و بر اساس ruleهایی مثل host، path یا header به یک بک‌اند می‌رسند. در بیشتر سناریوها، این بک‌اند یک Service است؛ یعنی Gateway API تصمیم ورودی را می‌گیرد، اما رسیدن به پادهای واقعی همچنان از مسیر Service، EndpointSlice و data plane انجام می‌شود. اینکه پیاده‌سازی Gateway دقیقاً ترافیک را به ClusterIP بفرستد یا مستقیم‌تر با endpointهای پشت Service کار کند، به خود controller و implementation آن بستگی دارد.

این یعنی Gateway API خودش جایگزین Pod یا Service نیست. Gateway API لایه ورودی و routing را توصیف می‌کند، اما backend معمولاً هنوز یک Service است. Service هم در نهایت به EndpointSlice و ای پی های واقعی وصل می‌شود.

پس مسیر بیرون به داخل معمولاً چنین مدلی دارد:

  • کاربر یا کلاینت بیرونی به آدرس بیرونی وصل می‌شود.
  • این ترافیک معمولاً وارد لودبالانسر،Gateway یا یک نقطه ورود مشابه می‌شود.
  • بعد کنترلر Gateway یا کنترلر Ingress با توجه به host، مسیر، header یا ruleهای دیگر تصمیم می‌گیرد.
  • درخواست به Service بک‌اند هدایت می‌شود.
  • Service از پشت صحنه، endpoint واقعی را از EndpointSlice پیدا می‌کند.
  • پکت به پاد مقصد می‌رسد.
  • جواب هم از مسیر مناسب برمی‌گردد و به کلاینت می‌رسد.

چرا مسیر ترافیک فقط از manifest مشخص نمی‌شود؟

فایل Manifest وضعیت desired را نشان می‌دهد، نه تمام مسیر runtime را.

مثلاً در یک منیفست Service به ما می‌گوید selector چیست، port چیست، targetPort چیست و type آن چیست. اما از روی همان manifest به‌تنهایی:

  • وضعیت واقعی پادهای پشت selector مشخص نیست؛ ممکن است labelها درست باشند، اما پادها هنوز Ready نباشند.
  • وضعیت EndpointSlice از داخل manifest دیده نمی‌شود؛ ممکن است ساخته نشده باشد، خالی باشد یا endpoint ناسالم داشته باشد.
  • وضعیت sync شدن kube-proxy مشخص نیست؛ شاید آخرین تغییرات Service و EndpointSlice هنوز به رول‌های نود تبدیل نشده باشند.
  • سلامت مسیر CNI تا IP واقعی پاد مقصد از manifest معلوم نمی‌شود؛ ممکن است مشکل جایی بین نودها باشد.
  • اثر NetworkPolicy هم قابل دیدن نیست؛ ممکن است ingress یا egress در لایه policy بسته شده باشد.
  • وضعیت conntrack در فایل YAML وجود ندارد؛ ممکن است state قدیمی هنوز روی مسیر اتصال اثر بگذارد.
  • مسیر ورود درخواست مشخص نیست؛ شاید درخواست از همان نود آمده باشد، شاید از نود دیگری وارد شده و بعد به بک‌اند رسیده باشد.
  • نقطه ورود ترافیک بیرونی هم به‌تنهایی معلوم نمی‌شود؛ ممکن است مسیر از Gateway، Ingress، NodePort یا LoadBalancer آمده باشد.
  • وضعیت source IP هم در manifest مشخص نیست؛ ممکن است در یکی از لایه‌های مسیر حفظ شده یا تغییر کرده باشد.

برای همین باید همیشه بین سه سطح فرق بگذاریم:

  • سطح API objectها
  • سطح control plane و controllerها
  • سطح data plane و packet واقعی اگر فقط سطح اول را ببینیم، ممکن است مشکل را اشتباه تشخیص بدهیم.

مدل سه‌لایه برای فکر کردن به Kubernetes networking

برای اینکه ذهن منظم بماند شبکه در کوبرنتیز را می‌توانیم در سه لایه ببینیم.

three-layer-model

لایه اول: تعریف و intent

در این لایه objectهایی را می‌بینیم که ما یا controllerها در API server تعریف کرده‌ایم. اما خودشان الزاماً پکت را جابه‌جا نمی‌کنند.

لایه دوم: controller و sync

در این لایه componentها objectها را دنبال می‌کنند و state لازم را می‌سازند. این لایه باعث می‌شود intent موجود در API به تنظیمات قابل اجرا تبدیل شود.

لایه سوم: data plane واقعی

در این لایه پکت واقعاً حرکت می‌کند. بیشتر مشکل‌های سخت در همین فاصله بین لایه اول و لایه سوم رخ می‌دهند. YAML درست است، اما پکت مسیر درست را نمی‌رود.

نتیجه

مبحث شبکه در کوبرنتیز زمانی قابل فهم می‌شود که آن را به چند object جداگانه محدود نکنیم.

آدرس پاد، آدرس مربوط به Service، آدرس نود و آدرس بیرونی هرکدام نقش جداگانه‌ای دارند. Service یک ورودی پایدار می‌سازد، اما مقصد واقعی معمولاً خود پاد است. EndpointSlice بک‌اندهای فعلی را نگه می‌دارد. kube-proxy یا جایگزین آن، مسیر Service VIP را در دیتاپلین پیاده‌سازی می‌کند. CNI مسیر شبکه‌ای پادها را فراهم می‌کند. DNS نام‌ها را قابل استفاده می‌کند. NetworkPolicy اجازه ارتباط را کنترل می‌کند. conntrack هم وضعیت لازم برای اتصال‌ها و NAT را نگه می‌دارد.

یادداشت فنی

این نسخه با مستندات رسمی کوبرنتیز درباره cluster networking، DNS، Service، EndpointSlice، kube-proxy، NetworkPolicy و Gateway API تطبیق داده شده است. با این حال، رفتار واقعی همیشه باید با نسخه کوبرنتیز، نوع CNI، مودهای واقعی kube-proxy، تنظیمات Service و پیاده‌سازی Gateway یا Ingress همان cluster بررسی شود.

منابع و مطالعهٔ بیشتر

  1. Cluster Networkingمستندات رسمی

    Kubernetes Docskubernetes.ioبازبینی: ۴ تیر ۱۴۰۵

    مدل رسمی شبکه کلاستر، ارتباط پادها، بازه‌های IP و مسئولیت پیاده‌سازی شبکه توسط CNI یا plugin شبکه را توضیح می‌دهد.

  2. Serviceمستندات رسمی

    Kubernetes Docskubernetes.ioبازبینی: ۴ تیر ۱۴۰۵

    توضیح رسمی Service، ClusterIP، بک‌اندهای پشت Service و مدل expose کردن applicationهای داخل کلاستر.

  3. EndpointSlicesمستندات رسمی

    Kubernetes Docskubernetes.ioبازبینی: ۴ تیر ۱۴۰۵

    توضیح رسمی EndpointSlice و اینکه چطور endpointهای پشت Service را نگه می‌دارد و جایگزین مدل قدیمی Endpoints شده است.

  4. DNS for Services and Podsمستندات رسمی

    Kubernetes Docskubernetes.ioبازبینی: ۴ تیر ۱۴۰۵

    توضیح رسمی DNS recordهای مربوط به Serviceها و پادها و نقش DNS در Service Discovery.

  5. Virtual IPs and Service Proxiesمستندات رسمی

    Kubernetes Docskubernetes.ioبازبینی: ۴ تیر ۱۴۰۵

    منبع اصلی برای رفتار Service VIP، نقش kube-proxy، مودهای iptables، nftables و IPVS و تفاوت‌های پیاده‌سازی data plane.

  6. Network Policiesمستندات رسمی

    Kubernetes Docskubernetes.ioبازبینی: ۴ تیر ۱۴۰۵

    توضیح رسمی NetworkPolicy، محدودسازی ingress و egress و وابستگی اجرای policy به plugin شبکه.

  7. Gateway APIمستندات رسمی

    Kubernetes Docskubernetes.ioبازبینی: ۴ تیر ۱۴۰۵

    توضیح رسمی Gateway API، آبجکت‌هایی مثل GatewayClass، Gateway و Route و نقش آن‌ها در مسیر ورودی ترافیک.

  8. Linux Kernel Docsdocs.kernel.orgبازبینی: ۴ تیر ۱۴۰۵

    منبع رسمی کرنل لینوکس برای تنظیمات nf_conntrack و connection tracking که پشت خیلی از رفتارهای NAT و stateful networking دیده می‌شود.