کش controller-runtime واقعاً چطور کار میکنه
خلاصهٔ کاملتر
نویسندههای این مقاله توی وبلاگ رسمی Kubernetes میگن بیشتر رفتارهای عجیب کنترلرهای Go از یه مدل ذهنی مبهم از دل controller-runtime میاد. تصور رایج اینه که r.Get() مستقیم از kube-apiserver میپرسه و r.List() یه نمای زندهٔ دنیا برمیگردونه؛ در عمل ماجرا برعکسه. کلاینت روی یه کپی محلی کار میکنه که با list + watch پر میشه، پس خوندن تقریباً مجانیه — ولی قیمتش اینه که کنترلر میتونه بیسروصدا گیگابایتها حافظه بخوره و مرتب به خوندنهای کهنه بربخوره.
زیر پکیج cache یه لایهٔ نازک روی client-go نشسته: Reflector یه watch باز نگه میداره و تغییرها رو به شکل delta میریزه توی صف، Indexer همون کپی محلی کلاستره (یه map با کلید namespace/name و یه sync.RWMutex مشترک روی کل استور و ایندکسها)، و SharedIndexInformer همه رو به هم وصل میکنه. کلمهٔ کلیدی همون Shared هست: به ازای هر GVK فقط یه informer ساخته میشه، پس از دید API سرور مهم نیست چند تا Reconciler توی پروسهت زندگی میکنه.
یه تغییر تازه هم توی همین مسیر افتاده: صف بین Reflector و informer دیگه DeltaFIFO نیست و از client-go 1.36 اصلاً نمیشه برش گردوند. جایگزینش RealFIFO هست، یه slice تخت و کاملاً ترتیبدار بدون هیچ دِدوپی:
type RealFIFO struct {
// ...
items []Delta
}یعنی سه رویداد پشتسرهم روی یه آبجکت، سه بار handler رو صدا میزنه و هیچ حالت میانی حذف نمیشه.
حذف تکراریها یه لایه بالاتر، توی workqueue کنترلر اتفاق میافته که فقط کلید namespace/name رو نگه میداره؛ برای همین رگبار ایونت روی یه Pod در نهایت یکی دو تا Reconcile میشه. نکتهٔ مهمش اینه که استور قبل از handler نوشته میشه و تحویل هم غیرهمزمانه، پس وقتی داری آپدیت 1→2 رو هندل میکنی، Get از کش میتونه 3 یا حتی NotFound بده. آبجکت داخل استور رو هیچوقت «وضعیت لحظهٔ ایونت من» فرض نکن.
خوندنها از کش میان ولی نوشتنها (Create، Update، Patch، Delete) مستقیم به API سرور میرن. آبجکتی که از کش خوندی یه resourceVersion همراه داره و سرور موقع update چک میکنه که با نسخهٔ داخل etcd یکی باشه، وگرنه 409 Conflict میده — این کنترل همزمانی خوشبینانهست، نه باگ. به گفتهٔ نویسندهها انتظار read-after-write هم اشتباه رایج شمارهٔ یکه: بین Update و بهروز شدن کش یه پنجره هست که سقف زمانی تضمینشده نداره، پس Reconcile باید idempotent باشه و همیشه وضعیت فعلی رو ببینه.
نکات کلیدی:
- r.Get() و r.List() داخل Reconcile از کش محلی میخونن، نه از API سرور
- نوشتنها مستقیم به API سرور میرن و با resourceVersion محافظت میشن
- صف delta حالا RealFIFO هست: ترتیب کامل، بدون هیچ حذف تکراری
- دِدوپ فقط توی workqueue کنترلر انجام میشه و همونه که بار رو میگیره
- اندازهٔ کش و ایندکسهای ثبتشده مستقیم روی مصرف حافظه اثر میذارن
- APIReader کم لازم میشه، ولی برای خوندن قبل از mgr.Start() جایگزین نداره




