constraint ارضا شده بود. با این حال zone خالی بود.

kubernetes scheduling opentelemetry finops mechanism

collector رو pod به pod restart کرده بودم تا یه routing ی zone-aware converge بشه، یکی‌یکی، تا هیچ‌کدوم capacity از دست نده. سه تا deletion ی سالم بعد، دو تا pod از همون Deployment روی یه node نشسته بودن و zone ی خودش خالی بود.

روی تک‌تک این Deployment ها تو این namespace یه spread constraint ی hard هست. تمام کاری که می‌کنه جلوگیری از همون وضعیتیه که من تازه تولید کرده بودم. و کل مدت ارضا شده بود.

constraint واقعاً چی قول می‌ده

یه topologySpreadConstraint فقط یه بار evaluate می‌شه، موقعی که pod schedule می‌شه، و بعدش هیچ‌وقت. Kubernetes هیچ rebalance ای انجام نمی‌ده. وقتی pod بالا میاد، scheduler به ازای هر مقدار از topology key، pod هایی رو که با labelSelector ی constraint می‌خونن می‌شمره، skew رو در مقابل maxSkew حساب می‌کنه، و بر همون اساس جای pod رو مشخص می‌کنه. بعد از اون لحظه هیچ‌چیزی دوباره evaluate نمی‌شه؛ pod همون‌جایی می‌مونه که فرود اومده تا اینکه چیزی پاکش کنه.

یعنی «ارضا شدن» یه ادعای همزمان درباره‌ی دو چیزه: یه شمارش، و جمعیتی که روش شمارش انجام شده. تو مورد من شمارش درست بود. جمعیت نبود.

آدم وسوسه می‌شه سراغ whenUnsatisfiable: DoNotSchedule بره و constraint رو strict تر بخونه. مشکل از اون نیست، و مشکل من هم از اون نبود. strictness هیچ‌وقت ورودی نبود. جمعیت بود.

ایراد اول: rollout روی هم می‌چینه

اولین راهی که جمعیت دروغ می‌گه از داخل زمان رد می‌شه.

طول یه rolling update برای یه مدت کوتاه دو تا ReplicaSet وجود داره: outgoing که داره خالی می‌شه و incoming که داره پر می‌شه. یه labelSelector ی خالی هر دو رو match می‌کنه. در نتیجه scheduler داره pod های جدید رو در مقابل جمعیتی می‌گذاره که pod های در آستانه‌ی ناپدید شدن هم توشون هست، یه skew می‌بینه که نمی‌تونه ارضاش کنه، و دو تا replica رو روی یه node جا می‌ده. بعدش همون‌جا بی‌نهایت می‌مونن، چون هیچ‌چیزی دوباره evaluate نمی‌شه، و rollout بعدی هم دوباره همین کارو می‌کنه.

راه‌حل یه خطه:

matchLabelKeys:
- pod-template-hash

scheduler قبل از شمردن، pod-template-hash ی خود pod رو به selector اضافه می‌کنه، که جمعیت رو به ReplicaSet ی خود pod محدود می‌کنه. sibling های در حال مردن دیگه شمرده نمی‌شن و rollout به شکل عادی پخش می‌شه. از Kubernetes ۱.۳۰ به بعد GA ـه، و اون‌قدر ارزونه که روی هر spread constraint یی که selector ش هویت per-workload نداره جاشه.

ایراد دوم: label ی مشترک سه تا Deployment رو توی یه استخر می‌ندازه

راه دوم که جمعیت دروغ می‌گه از کنار رد می‌شه، و اون نسخه‌ی شدیدشه.

یه operator سه تا Deployment ی collector رو تو این namespace مدیریت می‌کنه، سه تا، دو تا و دو تا replica. روی همه‌شون app.kubernetes.io/component: opentelemetry-collector رو ست می‌کنه، چون معنی همین label همینه: اینکه یه چیز چیه، نه اینکه کدوم چیزه.

هر کدوم از این سه تا Deployment یه spread constraint حمل می‌کنه که روی همون component label انتخاب می‌کنه. پس تک‌تکشون skew شون رو روی جمع هر هفت تا pod حساب می‌کنن. سه تا constraint، یه جمعیت مشترک، و هیچ‌کدوم replica های خودش رو نمی‌شماره.

طول restart ی pod به pod من، توزیع تجمیعی هر هفت تا pod بین zone ها ۳/۱/۲ بود. از دید union، zone ی تک‌pod مینیمم بود، پس filter ی skew مدام اون‌جا pod راه می‌داد. از دید اون Deployment ی سه‌replica ای، اون zone از قبل یه pod داشت، و pod ی که من تازه پاک کرده بودم مال اون بود، پس replacement هاش رو از zone ی خالی خودش دور می‌کرد. دو تا از replica هاش آخرش روی یه node تلنبار شدن.

این همون بخشیه که ارزش نشستن و فکر کردنه. constraint کل مدت ارضا شده بود، و scheduler کل مدت درست عمل می‌کرد. بهش گفته شده بود pod هایی رو بشماره که نباید می‌شمرد، شمردشون، و به skew ی محاسبه‌شده‌ش هم پایبند بود. نقص هیچ‌وقت تو mechanism نبود. تو تعریف جمعیت بود.

راه‌حل، هویت per-workload تو selector ـه. operator به ازای هر Deployment یه instance label ست می‌کنه، پس constraint این می‌شه:

topologySpreadConstraints:
- maxSkew: 1
  topologyKey: topology.kubernetes.io/zone
  whenUnsatisfiable: DoNotSchedule
  labelSelector:
    matchLabels:
      app.kubernetes.io/component: opentelemetry-collector
      app.kubernetes.io/instance: <the deployment's own instance>   # محدود شده
  matchLabelKeys:
  - pod-template-hash                                               # اضافه شده

instance label رو از یه pod ی زنده بخون، نه اینکه key رو حدس بزنی. operator ها بر اساس kind لیبل می‌زنن، و label ی که یه kind رو شناسایی می‌کنه دقیقاً key ی غلطیه برای spread.

اون repair که به جواب غلط converge می‌کنه

repair ی استاندارد برای یه Deployment ی تلنبار‌شده اینه که pod های دوبل رو یکی‌یکی پاک کنی. filter ی skew فقط همون node هایی رو قبول می‌کنه که تعدادشون با مینیمم فعلی برابره، پس هر replacement مجبوره روی خالی‌ترین node ی مجاز بیفته. deterministic ـه، نه شانس، و من روز قبلش همین رو روی یه نسخه‌ی شکل‌متفاوت از همین مشکل استفاده کرده بودم.

با selector ی درهم، اون repair یه تله‌ست. هر deletion همون مینیمم غلط رو دوباره حساب می‌کنه، روی همون جمعیت غلط، و replacement رو همون‌جای غلط می‌ندازه. دو بار دیدم همون pod برمی‌گرده روی همون node، تا اینکه قبول کردم خود repair داره یه چیزی رو فرض می‌کنه که constraint بی‌صدا شکسته بودش: اینکه filter چیزی رو می‌شمره که فکر می‌کنی می‌شماره.

راه خروج اینه که انتخاب غلط رو برای یه cycle ی scheduling غیرقانونی کنی. node یی که مدام انتخاب می‌شه رو cordon کن، pod رو پاک کن، تا تنها جای‌های مجازی که می‌مونه جای‌های درست باشه، و بعدش uncordon کن. این یه fix نیست؛ یه فراره. fix همون selector ـه.

یه تله‌ی دیگه تو همین خانواده، چون دقیقاً وسط همین جور investigation ها گاز می‌گیره: یه apply ی Helm قبل از اینکه rollout ی ش راه انداخته تموم بشه برمی‌گرده، چون maxUnavailable: 1 باعث می‌شه دوتا از سه تا available حساب بشن. apply می‌گفت done در حالی که یه pod هنوز Pending بود. rollout ها رو خودت چک کن؛ خوش‌بینی apply رو به ارث نبر.

fix، تأییدشده توسط همون case ی fail شده

هر دو خط رفتن تو: label ی per-instance تو matchLabels، و pod-template-hash تو matchLabelKeys. بعدش هم Deployment ی تلنبار‌شده رو دستی repair نکردم، و این عمدی بود.

rolling update ی که fix رو اعمال کرد، دقیقاً همون case ی fail شده رو از نو اجرا کرد. همون semantics ی constraint، همون namespace، همون skew ی اولیه، همون تک موقعیتی که دو تا pod روی یه node تولید کرده بود. Deployment بدون هیچ کمکی با یه pod تو هر zone برگشت. وقتی deployment ی خود fix بازتولید خود failure باشه، verification و regression test رو تو یه رویداد می‌گیری.

همین rollout ضمناً سهم connection های هم‌zone از کل estate رو برد به ۹۹ درصد، چون تک‌تک pod های جایگزین‌شده تو zone ی خودشون resolve شدن. موضوع اینه: با یه load balancer ی zonal، cross-zone خاموش، و externalTrafficPolicy: Local، یه zone بدون pod یعنی یه node ی load balancer بدون هیچ healthy target ی، و routing ی zone-affine هیچی به client های اون zone نمی‌ده. یه bug ی scheduling که ظاهرش cosmetic ـه، هم یه bug ی routing ـه هم یه bug ی هزینه. یه deploy ی معمولی هر سه‌تاش رو یه‌جا درست کرد.

کجا generalize می‌شه

selector یه تعریف جمعیت به شمار میاد، نه یه اسم. هر کی هر وقت یکی‌ش رو می‌نویسه، سوال این نیست که «workload من رو match می‌کنه یا نه»، سوال اینه که «مجموعه‌ی کامل چیزهایی که match می‌کنه چیه»، و جواب این دو تا سوال به شکل‌هایی از هم فاصله می‌گیرن که هیچ‌وقت تو همون object یی که داری ویرایش می‌کنی پیدا نمی‌شن.

نسخه‌ی ملایم این دروغ sidecar ـه: pod ی metrics ی تک‌replica ی یه chart معمولاً همون label ی name ی چیزی که منظورت بود رو حمل می‌کنه، پس شمارش شاملش می‌شه، و یه spread ی کج‌وکوله، maxSkew: 1 رو ارضا می‌کنه در حالی که یه zone واقعاً خالی می‌مونه. اگه pod هایی که می‌خوای هیچ label ی متمایز از خودشون ندارن، exclusion باید بیان بشه، نه ضمنی بمونه:

labelSelector:
  matchLabels:
    app.kubernetes.io/name: <the chart>
  matchExpressions:
  - key: app.kubernetes.io/component
    operator: DoesNotExist

نسخه‌ی شدیدش sibling Deployment هایی‌ه که یه label ی kind جمعشون کرده، و هر چی هم strict بشه درستش نمی‌کنه، چون strictness روی شمارشی اعمال می‌شه که روی مجموعه‌ی غلط انجام شده.

عادت تشخیصی که هر دو رو می‌گیره: قبل از اعتماد به یه spread constraint، چیزی که scheduler واقعاً می‌شماره رو فهرست کن، و از روی pod های زنده فهرستش کن، kubectl get pods --show-labels، نه از spec ی که نوشتی. spec بهت می‌گه چی می‌خواستی. pod ها بهت می‌گن چی گرفتی، و این دو تا تنها fact های سیستم‌ان، و فقط وقتی با هم موافق‌ان که selector درست باشه.

یه constraint ی ارضا شده فقط یه شمارش رو proof می‌کنه. فقط intent تصمیم می‌گیره که اون شمارش روی همون چیزی بوده که می‌خواستی یا نه.

$ cat CLICKHOUSE .md
· 9 دقیقه مطالعه

۱۳۶ میلیون PUT برای ۱۷ GiB داده

object storage به ازای هر operation پول می‌گیره، و یه part ی ClickHouse روی disk ی S3 یه object نیست، به ازای هر column یه object ـه. پس هزینه‌ی یه cold tier تابع اینه که چند تا part وجود داره، نه اینکه چند byte توشونه، و هر setting ای که merge ها رو گرسنه بذاره می‌شه یه خط روی صورت‌حساب. دو تا default ی chart دقیقاً همین کارو کردن، و fix ی که جلوش رو گرفت هیچ‌وقت commit نشده بود.

clickhouse s3 finops observability mechanism
$ cat GIT .md
· 7 دقیقه مطالعه

۸۴ تا repository ناپدید شدن. راه‌حل mkdir بود.

S3 چیزی به اسم directory خالی نداره، و تو یه git repository ی کاملاً packed شده، directory های refs دقیقاً همون‌ان: خالی. یه sync ی فایل‌به‌فایل همه‌ی object ها رو سالم منتقل کرد ولی دو تا directory یی که git لازم داره تا به چیزی بگه repository رو drop کرد، پس از ۵۱۷ تا، ۸۴ تا به شکل خوانا‌نشدنی برگشتن در حالی که database هنوز می‌گفت commit دارن. health check های migration هم کل مدت سبز موندن، چون هیچ‌کدوم هیچ‌وقت یه repository رو باز نمی‌کنن.

git aws s3 gitlab mechanism
$ cat KAFKA .md
· 8 دقیقه مطالعه

ارور ۲۵ ساعت قدیمی بود. کلاینت همه‌ی این مدت سالم بود.

یه wrapper ی Kafka client که recovery اش یعنی شمردن error ها و panic کردن از یه threshold گذشته، recovery ییه که با traffic هم‌قدمه: stream processor ها تو چند ثانیه ازش رد می‌شن، producer های کم‌ترافیک هیچ‌وقت، و برای همینه که آخرین error رو latch می‌کنن و تا بی‌نهایت همون رو برمی‌گردونن، در حالی که همه‌ی signal های سلامت سبزن. سرنخ هم تو خود رقم‌های اروره: elapsed-time یه ارور که بین چند بار رخ‌دادن byte-identical باشه، یه event ی کش‌شده‌ست، نه یه خرابی ی تکرار‌شونده.

kafka resilience mechanism reliability observability