revise June 2024 container orchestration articles
Build and deploy / deploy (push) Successful in 16s

This commit is contained in:
2026-07-31 16:01:21 +03:00
parent 3faaaa0992
commit 48049ee0ca
8 changed files with 861 additions and 1 deletions
+655
View File
@@ -0,0 +1,655 @@
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
const p = (text) => '<p>' + text + '</p>';
const h2 = (text) => '<h2>' + text + '</h2>';
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
function plainText(content) {
return content
.replace(/<[^>]+>/g, ' ')
.replaceAll('&nbsp;', ' ')
.replaceAll('&quot;', '"')
.replaceAll('&#039;', "'")
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&amp;', '&')
.replace(/\s+/g, ' ')
.trim();
}
function bodyText(content) {
return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
}
const sources = [
{
title: 'Kubernetes v1.30: Uwubernetes, 17.04.2024',
url: 'https://kubernetes.io/blog/2024/04/17/kubernetes-v1-30-release/',
note: 'Первичное объявление Kubernetes Release Team фиксирует доступность v1.30 17 апреля 2024, то есть до июня 2024. Оно помогает задать историческую границу версии, но не говорит, какая версия, feature gate, controller flags или аддоны есть в чужом кластере.',
},
{
title: 'Kubernetes documentation: Resource Management for Pods and Containers, snapshot 07.03.2024',
url: 'https://github.com/kubernetes/website/blob/ab0631a407aa6bcf6241818d7dd55aa164dc806f/content/en/docs/concepts/configuration/manage-resources-containers.md',
note: 'Первичный неизменяемый снимок документации до июня 2024: scheduler использует request при выборе Node, kubelet и runtime применяют limit, а limit без request может стать request. Это описание механизма Kubernetes, не рекомендация конкретных чисел CPU или memory.',
},
{
title: 'Kubernetes documentation: Horizontal Pod Autoscaling, snapshot 26.03.2024',
url: 'https://github.com/kubernetes/website/blob/0568c8af60bf24624f1ac5275969cebf672e874e/content/en/docs/tasks/run-application/horizontal-pod-autoscale.md',
note: 'Первичный снимок HPA до июня 2024: CPU utilization считается относительно request; при отсутствии нужного request controller не действует по этой метрике. Документ также описывает обработку missing и not-yet-ready Pod, но не доказывает наличие metrics API либо controller settings в конкретной среде.',
},
{
title: 'Kubernetes documentation: Pod Lifecycle and readiness gates, snapshot 24.05.2024',
url: 'https://github.com/kubernetes/website/blob/77407a728ac7d93e1601a767f52b3c8a53b0fd79/content/en/docs/concepts/workloads/pods/pod-lifecycle.md',
note: 'Первичная документация описывает Ready, ContainersReady и readinessGates: custom condition должна быть True вместе с готовностью контейнеров. Она не задаёт семантику доменной condition, owner-а этой condition или надёжность внешней зависимости.',
},
{
title: 'Kubernetes documentation: Configure Liveness, Readiness and Startup Probes, snapshot 13.04.2024',
url: 'https://github.com/kubernetes/website/blob/2a6ec81df0a8b7e89dfc8af0e2651d56f4e11b28/content/en/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes.md',
note: 'Первичный снимок probes до июня 2024: readiness определяет допуск к Service traffic, startup probe задерживает запуск liveness и readiness. Это не обещание, что проба измеряет latency, throughput, dependency health или реальную capacity.',
},
];
function sourceList() {
return '<ul>' + sources.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
}
function revision(meta, parts) {
const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + '\n' + sourceList();
const proseLength = bodyText(contentHtml).length;
if (proseLength < 5000 || proseLength > 15000) {
throw new Error(meta.slug + ': основной текст вне диапазона 5 000–15 000 знаков: ' + proseLength);
}
return Object.freeze({ ...meta, contentHtml, proseLength });
}
const MODEL_VERSION = 'synthetic-container-load-profile-v1';
const INPUT_KIND = 'synthetic-container-load-input-v1';
const REPORT_KIND = 'synthetic-container-load-report-v1';
const PLAN_KIND = 'synthetic-container-load-review-draft-v1';
const SYNTHETIC_SCOPE = 'p76-container-orchestration-2024-06';
const MODEL_LIMIT = 'versioned-fixed-synthetic-js-objects-in-memory-only-no-files-no-cluster-no-ci-no-network-no-http-no-trace-no-production-no-telemetry';
const FIXED_CASES = Object.freeze({
'fixed-steady-http-v1': Object.freeze({
version: MODEL_VERSION,
label: 'steady HTTP work with a declared CPU request and a ready application path',
declaredManifest: Object.freeze({
version: 'synthetic-manifest-v1',
cpuRequestMillicores: 500,
cpuLimitMillicores: 1000,
memoryRequestMiB: 384,
memoryLimitMiB: 768,
readiness: 'application-ready-after-declared-initialization',
autoscaling: Object.freeze({ metric: 'cpu-utilization-percent-of-request', targetAverageUtilization: 70, minReplicas: 2, maxReplicas: 6 }),
}),
managedLoad: Object.freeze({
version: 'synthetic-load-profile-v1',
shape: 'steady-http-cpu-bound',
startup: 'bounded-by-application-contract',
dominantResource: 'cpu',
requestFitQuestion: 'does the declared request cover the agreed steady unit of work',
burstPolicy: 'not-modelled-as-real-traffic',
}),
syntheticObservedPodBehavior: Object.freeze({
version: 'synthetic-pod-observation-v1',
source: 'embedded-fixed-js-object-not-telemetry',
ready: true,
syntheticCpuMillicores: 450,
syntheticMemoryMiB: 310,
event: 'no-real-pod-event-is-represented',
}),
expectedVerdict: 'review-profile-request-and-hpa-denominator',
}),
'fixed-memory-growth-v1': Object.freeze({
version: MODEL_VERSION,
label: 'batch-shaped memory growth while a readiness result is false',
declaredManifest: Object.freeze({
version: 'synthetic-manifest-v1',
cpuRequestMillicores: 250,
cpuLimitMillicores: 500,
memoryRequestMiB: 256,
memoryLimitMiB: 512,
readiness: 'false-when-the-application-declares-it-cannot-serve',
autoscaling: Object.freeze({ metric: 'cpu-utilization-percent-of-request', targetAverageUtilization: 70, minReplicas: 2, maxReplicas: 8 }),
}),
managedLoad: Object.freeze({
version: 'synthetic-load-profile-v1',
shape: 'bounded-batch-with-memory-retention-question',
startup: 'not-the-dominant-question',
dominantResource: 'memory',
requestFitQuestion: 'is retained working-set ownership bounded before replica count is changed',
burstPolicy: 'not-modelled-as-real-traffic',
}),
syntheticObservedPodBehavior: Object.freeze({
version: 'synthetic-pod-observation-v1',
source: 'embedded-fixed-js-object-not-telemetry',
ready: false,
syntheticCpuMillicores: 130,
syntheticMemoryMiB: 470,
event: 'no-real-restart-or-oom-event-is-represented',
}),
expectedVerdict: 'separate-memory-lifetime-from-cpu-replica-action',
}),
'fixed-warmup-request-missing-v1': Object.freeze({
version: MODEL_VERSION,
label: 'warmup CPU burst with an autoscaling percentage but no declared CPU request',
declaredManifest: Object.freeze({
version: 'synthetic-manifest-v1',
cpuRequestMillicores: null,
cpuLimitMillicores: null,
memoryRequestMiB: 384,
memoryLimitMiB: 768,
readiness: 'false-until-startup-contract-is-satisfied',
autoscaling: Object.freeze({ metric: 'cpu-utilization-percent-of-request', targetAverageUtilization: 65, minReplicas: 1, maxReplicas: 5 }),
}),
managedLoad: Object.freeze({
version: 'synthetic-load-profile-v1',
shape: 'startup-cpu-burst-then-request-serving',
startup: 'warming-work-is-separate-from-serving-work',
dominantResource: 'cpu-during-warmup',
requestFitQuestion: 'which declared request makes a utilization percentage meaningful',
burstPolicy: 'controller-startup-settings-are-not-read',
}),
syntheticObservedPodBehavior: Object.freeze({
version: 'synthetic-pod-observation-v1',
source: 'embedded-fixed-js-object-not-telemetry',
ready: false,
syntheticCpuMillicores: 600,
syntheticMemoryMiB: 340,
event: 'no-real-metric-sample-or-controller-decision-is-represented',
}),
expectedVerdict: 'reject-percent-target-without-declared-cpu-request',
}),
});
function hasOnlyKeys(value, allowed) {
return value && typeof value === 'object' && !Array.isArray(value)
&& Object.keys(value).every((key) => allowed.includes(key));
}
function rejected(reason) {
return Object.freeze({
kind: REPORT_KIND,
accepted: false,
syntheticOnly: true,
reason,
modelVersion: MODEL_VERSION,
modelLimit: MODEL_LIMIT,
});
}
export function createFixedSyntheticContainerLoadInput(caseId) {
if (!Object.hasOwn(FIXED_CASES, caseId)) {
return Object.freeze({ kind: 'unknown-synthetic-container-load-input', synthetic: false, caseId });
}
return Object.freeze({
kind: INPUT_KIND,
synthetic: true,
modelVersion: MODEL_VERSION,
scope: SYNTHETIC_SCOPE,
mode: 'fixed-memory-only',
caseId,
});
}
function outcomeFor(fixed) {
if (fixed.expectedVerdict === 'review-profile-request-and-hpa-denominator') {
return Object.freeze({
verdict: fixed.expectedVerdict,
symptom: 'a percentage target can look precise while the request that defines its denominator was copied without a workload profile',
cause: 'the declaration, managed shape and synthetic observation have not yet been compared as one contract',
check: 'compare declared 500m request with the agreed steady unit and keep the 70 percent target explicitly relative to that request',
action: 'prepare only a human review question; do not infer real capacity, a real scale action or a safe value from this record',
readinessMeaning: 'Ready in this record only means the synthetic application path is allowed to receive Service traffic',
});
}
if (fixed.expectedVerdict === 'separate-memory-lifetime-from-cpu-replica-action') {
return Object.freeze({
verdict: fixed.expectedVerdict,
symptom: 'a false readiness result and synthetic memory growth can be mistaken for a request to add CPU replicas',
cause: 'CPU utilization scaling, memory lifetime and traffic admission answer different questions',
check: 'separate the declared memory boundary, the owner of retained work and the readiness meaning before considering a replica action',
action: 'prepare only a bounded memory-lifetime investigation question; do not label the synthetic value as OOM, telemetry or a production incident',
readinessMeaning: 'Unready means no Service traffic in the documented mechanism; it does not identify a root cause by itself',
});
}
return Object.freeze({
verdict: fixed.expectedVerdict,
symptom: 'a CPU utilization target exists although the fixed declaration has no CPU request',
cause: 'the percentage has no declared request denominator for the targeted Pod in this synthetic record',
check: 'reject the capacity conclusion and ask which request, raw metric contract and startup boundary are actually intended',
action: 'prepare only a manifest-and-profile review question; do not treat synthetic warmup CPU as a controller decision or a real metric sample',
readinessMeaning: 'The fixed not-ready state separates warmup from serving, while real controller startup settings remain intentionally unknown',
});
}
/**
* Inspects one named fixed object embedded in this module. It never reads a
* manifest file, a cluster, CI, network, HTTP response, trace, telemetry or
* production state. Values named syntheticObservedPodBehavior are teaching
* literals, not kubectl output, CI output, metrics or telemetry.
*/
export function inspectSyntheticContainerLoad(input) {
if (!input || input.synthetic !== true || input.kind !== INPUT_KIND) return rejected('synthetic-fixed-input-required');
const allowed = ['kind', 'synthetic', 'modelVersion', 'scope', 'mode', 'caseId'];
if (!hasOnlyKeys(input, allowed)) return rejected('unexpected-input-field');
if (input.modelVersion !== MODEL_VERSION) return rejected('unexpected-model-version');
if (input.scope !== SYNTHETIC_SCOPE) return rejected('unexpected-synthetic-scope');
if (input.mode !== 'fixed-memory-only') return rejected('fixed-memory-mode-required');
if (!Object.hasOwn(FIXED_CASES, input.caseId)) return rejected('unknown-fixed-synthetic-case');
const fixed = FIXED_CASES[input.caseId];
const outcome = outcomeFor(fixed);
return Object.freeze({
kind: REPORT_KIND,
accepted: true,
syntheticOnly: true,
reason: 'fixed-versioned-synthetic-js-object-compared',
modelVersion: MODEL_VERSION,
modelLimit: MODEL_LIMIT,
scope: SYNTHETIC_SCOPE,
caseId: input.caseId,
caseLabel: fixed.label,
declaredManifest: fixed.declaredManifest,
managedLoad: fixed.managedLoad,
syntheticObservedPodBehavior: fixed.syntheticObservedPodBehavior,
verdict: outcome.verdict,
symptom: outcome.symptom,
cause: outcome.cause,
check: outcome.check,
action: outcome.action,
readinessMeaning: outcome.readinessMeaning,
evidence: Object.freeze({
source: 'embedded-fixed-versioned-synthetic-js-objects-only',
manifestFile: 'not-read',
cluster: 'not-read',
ci: 'not-run',
network: 'not-used',
http: 'not-used',
trace: 'not-read',
telemetry: 'not-read',
production: 'not-contacted',
}),
productionEffect: 'not-attempted',
});
}
function canonicalReportFor(caseId) {
return inspectSyntheticContainerLoad(createFixedSyntheticContainerLoadInput(caseId));
}
/**
* Creates a review draft only after comparing an in-memory report with its
* canonical fixed object. It does not patch, apply, query or scale anything.
*/
export function planSyntheticContainerLoadReview(report) {
if (!report || report.kind !== REPORT_KIND || report.syntheticOnly !== true || report.accepted !== true) {
return Object.freeze({ accepted: false, syntheticOnly: true, reason: 'accepted-synthetic-report-required', modelVersion: MODEL_VERSION, modelLimit: MODEL_LIMIT });
}
const allowed = [
'kind', 'accepted', 'syntheticOnly', 'reason', 'modelVersion', 'modelLimit', 'scope', 'caseId', 'caseLabel',
'declaredManifest', 'managedLoad', 'syntheticObservedPodBehavior', 'verdict', 'symptom', 'cause', 'check',
'action', 'readinessMeaning', 'evidence', 'productionEffect',
];
if (!hasOnlyKeys(report, allowed)) {
return Object.freeze({ accepted: false, syntheticOnly: true, reason: 'unexpected-report-field', modelVersion: MODEL_VERSION, modelLimit: MODEL_LIMIT });
}
if (report.modelVersion !== MODEL_VERSION || report.scope !== SYNTHETIC_SCOPE || !Object.hasOwn(FIXED_CASES, report.caseId)) {
return Object.freeze({ accepted: false, syntheticOnly: true, reason: 'untrusted-synthetic-report-scope', modelVersion: MODEL_VERSION, modelLimit: MODEL_LIMIT });
}
const canonical = canonicalReportFor(report.caseId);
if (JSON.stringify(canonical) !== JSON.stringify(report)) {
return Object.freeze({ accepted: false, syntheticOnly: true, reason: 'report-does-not-match-fixed-object', modelVersion: MODEL_VERSION, modelLimit: MODEL_LIMIT });
}
const actionsByVerdict = Object.freeze({
'review-profile-request-and-hpa-denominator': Object.freeze([
'record-synthetic-request-profile-question',
'record-synthetic-hpa-denominator-question',
'require-separate-authorized-real-evidence-before-any-change',
]),
'separate-memory-lifetime-from-cpu-replica-action': Object.freeze([
'record-synthetic-memory-lifetime-question',
'record-synthetic-readiness-semantics-question',
'reject-replica-count-as-a-memory-root-cause-conclusion',
]),
'reject-percent-target-without-declared-cpu-request': Object.freeze([
'record-synthetic-missing-request-question',
'record-synthetic-startup-boundary-question',
'reject-percent-capacity-conclusion-until-a-contract-exists',
]),
});
return Object.freeze({
kind: PLAN_KIND,
accepted: true,
syntheticOnly: true,
reason: 'canonical-fixed-synthetic-review-draft',
modelVersion: MODEL_VERSION,
modelLimit: MODEL_LIMIT,
sourceReport: canonical,
verdict: canonical.verdict,
actions: actionsByVerdict[canonical.verdict],
manifest: 'not-read-or-written',
cluster: 'not-read-or-changed',
ci: 'not-run',
network: 'not-used',
http: 'not-used',
trace: 'not-read',
telemetry: 'not-read',
production: 'not-contacted',
productionEffect: 'not-attempted',
});
}
/**
* Discards only an authenticated in-memory review draft. No manifest, cluster,
* CI, HTTP endpoint, trace, telemetry or production object is touched.
*/
export function rollbackSyntheticContainerLoadReview(plan) {
if (!plan || plan.kind !== PLAN_KIND || plan.syntheticOnly !== true || plan.accepted !== true || !plan.sourceReport) {
return Object.freeze({ restored: false, syntheticOnly: true, reason: 'no-accepted-synthetic-review-draft' });
}
const canonical = planSyntheticContainerLoadReview(plan.sourceReport);
if (canonical.accepted !== true || JSON.stringify(canonical) !== JSON.stringify(plan)) {
return Object.freeze({ restored: false, syntheticOnly: true, reason: 'draft-does-not-match-canonical-fixed-object' });
}
return Object.freeze({
restored: true,
syntheticOnly: true,
reason: 'synthetic-review-draft-discarded',
manifest: 'not-read-or-written',
cluster: 'not-read-or-changed',
ci: 'not-run',
network: 'not-used',
http: 'not-used',
trace: 'not-read',
telemetry: 'not-read',
production: 'not-contacted',
productionEffect: 'not-attempted',
});
}
export function runContainerOrchestrationFixture() {
const steady = inspectSyntheticContainerLoad(createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'));
const memory = inspectSyntheticContainerLoad(createFixedSyntheticContainerLoadInput('fixed-memory-growth-v1'));
const warmup = inspectSyntheticContainerLoad(createFixedSyntheticContainerLoadInput('fixed-warmup-request-missing-v1'));
const nonSynthetic = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), synthetic: false });
const manifestFileInput = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), manifestFile: 'deployment.yaml' });
const clusterInput = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), cluster: 'real-cluster-name' });
const ciInput = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), ci: 'pipeline-url' });
const networkInput = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), network: 'https://not-used.example' });
const httpInput = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), http: { response: 'not-read' } });
const traceInput = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), trace: 'trace-id' });
const productionInput = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), production: true });
const badVersion = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), modelVersion: 'v99' });
const badScope = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), scope: 'some-other-scope' });
const badMode = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), mode: 'read-cluster' });
const unknownCase = inspectSyntheticContainerLoad({ ...createFixedSyntheticContainerLoadInput('fixed-steady-http-v1'), caseId: 'real-workload' });
const steadyPlan = planSyntheticContainerLoadReview(steady);
const memoryPlan = planSyntheticContainerLoadReview(memory);
const warmupPlan = planSyntheticContainerLoadReview(warmup);
const forgedVerdict = planSyntheticContainerLoadReview({ ...steady, verdict: 'scale-now' });
const forgedObservation = planSyntheticContainerLoadReview({ ...memory, syntheticObservedPodBehavior: { ...memory.syntheticObservedPodBehavior, ready: true } });
const unexpectedReportField = planSyntheticContainerLoadReview({ ...steady, kubectlOutput: 'not-accepted' });
const restored = rollbackSyntheticContainerLoadReview(memoryPlan);
const forgedRestore = rollbackSyntheticContainerLoadReview({ ...memoryPlan, actions: ['apply-real-manifest'] });
const reportAsDraft = rollbackSyntheticContainerLoadReview(memory);
return Object.freeze({
assertions: Object.freeze({
usesVersionedFixedObjects: steady.modelVersion === MODEL_VERSION && steady.declaredManifest.version === 'synthetic-manifest-v1' && steady.managedLoad.version === 'synthetic-load-profile-v1',
acceptsSteadyProfile: steady.accepted === true && steady.verdict === 'review-profile-request-and-hpa-denominator',
preservesRequestAndTargetRelation: steady.declaredManifest.cpuRequestMillicores === 500 && steady.declaredManifest.autoscaling.targetAverageUtilization === 70 && steady.managedLoad.dominantResource === 'cpu',
labelsSyntheticPodValueAsNotTelemetry: steady.syntheticObservedPodBehavior.source === 'embedded-fixed-js-object-not-telemetry' && steady.evidence.telemetry === 'not-read',
acceptsMemoryProfile: memory.accepted === true && memory.verdict === 'separate-memory-lifetime-from-cpu-replica-action',
keepsReadinessDistinctFromMemoryCause: memory.syntheticObservedPodBehavior.ready === false && memory.readinessMeaning.includes('does not identify a root cause'),
refusesToInventAnOomEvent: memory.syntheticObservedPodBehavior.event === 'no-real-restart-or-oom-event-is-represented' && memory.productionEffect === 'not-attempted',
acceptsWarmupProfile: warmup.accepted === true && warmup.verdict === 'reject-percent-target-without-declared-cpu-request',
detectsMissingCpuRequest: warmup.declaredManifest.cpuRequestMillicores === null && warmup.check.includes('reject the capacity conclusion'),
keepsControllerSettingsUnknown: warmup.managedLoad.burstPolicy === 'controller-startup-settings-are-not-read' && warmup.evidence.cluster === 'not-read',
rejectsNonSyntheticInput: nonSynthetic.accepted === false && nonSynthetic.reason === 'synthetic-fixed-input-required',
rejectsManifestFileInput: manifestFileInput.accepted === false && manifestFileInput.reason === 'unexpected-input-field',
rejectsClusterInput: clusterInput.accepted === false && clusterInput.reason === 'unexpected-input-field',
rejectsCiInput: ciInput.accepted === false && ciInput.reason === 'unexpected-input-field',
rejectsNetworkInput: networkInput.accepted === false && networkInput.reason === 'unexpected-input-field',
rejectsHttpInput: httpInput.accepted === false && httpInput.reason === 'unexpected-input-field',
rejectsTraceInput: traceInput.accepted === false && traceInput.reason === 'unexpected-input-field',
rejectsProductionInput: productionInput.accepted === false && productionInput.reason === 'unexpected-input-field',
rejectsWrongVersion: badVersion.accepted === false && badVersion.reason === 'unexpected-model-version',
rejectsWrongScope: badScope.accepted === false && badScope.reason === 'unexpected-synthetic-scope',
rejectsReadClusterMode: badMode.accepted === false && badMode.reason === 'fixed-memory-mode-required',
rejectsUnknownCase: unknownCase.accepted === false && unknownCase.reason === 'unknown-fixed-synthetic-case',
plansSteadyReviewOnly: steadyPlan.accepted === true && steadyPlan.actions.includes('require-separate-authorized-real-evidence-before-any-change'),
plansMemoryReviewOnly: memoryPlan.accepted === true && memoryPlan.actions.includes('reject-replica-count-as-a-memory-root-cause-conclusion'),
plansWarmupReviewOnly: warmupPlan.accepted === true && warmupPlan.actions.includes('reject-percent-capacity-conclusion-until-a-contract-exists'),
planHasNoOperationalEffect: warmupPlan.manifest === 'not-read-or-written' && warmupPlan.cluster === 'not-read-or-changed' && warmupPlan.ci === 'not-run' && warmupPlan.network === 'not-used' && warmupPlan.http === 'not-used' && warmupPlan.trace === 'not-read' && warmupPlan.production === 'not-contacted',
rejectsForgedVerdict: forgedVerdict.accepted === false && forgedVerdict.reason === 'report-does-not-match-fixed-object',
rejectsForgedObservation: forgedObservation.accepted === false && forgedObservation.reason === 'report-does-not-match-fixed-object',
rejectsUnexpectedReportField: unexpectedReportField.accepted === false && unexpectedReportField.reason === 'unexpected-report-field',
rollbackDiscardsCanonicalDraftOnly: restored.restored === true && restored.manifest === 'not-read-or-written' && restored.cluster === 'not-read-or-changed' && restored.telemetry === 'not-read',
rollbackRejectsForgedDraft: forgedRestore.restored === false && forgedRestore.reason === 'draft-does-not-match-canonical-fixed-object',
reportCannotBeRolledBackAsDraft: reportAsDraft.restored === false && reportAsDraft.reason === 'no-accepted-synthetic-review-draft',
}),
samples: Object.freeze({
steady, memory, warmup, nonSynthetic, manifestFileInput, clusterInput, ciInput, networkInput, httpInput,
traceInput, productionInput, badVersion, badScope, badMode, unknownCase, steadyPlan, memoryPlan, warmupPlan,
forgedVerdict, forgedObservation, unexpectedReportField, restored, forgedRestore, reportAsDraft,
}),
});
}
const fixtureExample = [
"import {",
" createFixedSyntheticContainerLoadInput,",
" inspectSyntheticContainerLoad,",
" planSyntheticContainerLoadReview,",
" runContainerOrchestrationFixture,",
"} from './upgrade-2024-06.mjs';",
'',
"const input = createFixedSyntheticContainerLoadInput('fixed-steady-http-v1');",
'const report = inspectSyntheticContainerLoad(input);',
'const review = planSyntheticContainerLoadReview(report);',
'',
'if (!Object.values(runContainerOrchestrationFixture().assertions).every(Boolean)) {',
" throw new Error('fixed synthetic fixture failed');",
'}',
'',
'console.log({ verdict: report.verdict, actions: review.actions });',
'',
'// Только versioned fixed synthetic JS-объекты в памяти.',
'// Не читаются файлы, кластер, CI, сеть, HTTP, trace или production.',
'// syntheticObservedPodBehavior — не kubectl, не CI и не telemetry.',
].join('\n');
const fixtureCommand = fixtureExample + '\n\nnode web/scripts/upgrade-2024-06.mjs --verify-fixture\n\n# PASS подтверждает только согласованность fixed synthetic objects и отрицательных веток.';
const practice = revision({
slug: 'editorial-2024-06-practice-container-orchestration',
title: 'Управление контейнерной нагрузкой: минимальная инженерная схема',
categories: ['Kubernetes', 'DevOps'],
cover: '/assets/editorial/2024/container-orchestration-2024-pod-lifecycle.svg',
excerpt: 'Практический маршрут, который связывает resource requests и limits, readiness и HPA с профилем приложения — без выдачи учебной fixture за состояние кластера.',
readingMinutes: 12,
}, [
p('В манифест нового HTTP-сервиса часто попадает знакомая пара: CPU request 500m, limit 1000m, readiness probe и HPA с целью 70 процентов. В день выпуска это выглядит как законченная настройка. Но у приложения есть прогрев, очередь, память на рабочий набор и момент, когда оно действительно способно отвечать. Если числа пришли из соседнего сервиса, а не из профиля этой работы, scheduler резервирует одно, HPA считает процент от другого, а Service отправляет трафик по третьему сигналу. Цена — неверная граница ресурсов и неверная реакция на false readiness. '),
p('Вторая дорогая ошибка появляется после первой тревоги: limit объявляют запасом производительности, а readiness — универсальной проверкой здоровья. В Kubernetes request участвует в размещении, limit задаёт границу исполнения, а readiness решает допуск к Service traffic. Эти механизмы не заменяют друг друга. CPU target HPA в процентах также имеет конкретный знаменатель — request. Поэтому начальная инженерная схема должна сравнить три записи: что декларация разрешает Pod, какую нагрузку приложение обещает выдерживать как единица работы и что наблюдение должно означать, прежде чем его использовать.'),
h2('Симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> Pod помещается на Node, но после включения трафика растёт ожидание; либо HPA меняет replica count, а доступных backend не прибавляется.',
'<strong>Причина.</strong> Request, limit, readiness и autoscaling настроены как независимые фрагменты. Профиль приложения — CPU-bound serving, memory-retaining batch или warmup — не назван.',
'<strong>Проверка.</strong> Для каждого container выпишите request и limit отдельно, затем объясните одной фразой, что делает readiness, какую работу отражает metric и от какого request считается percentage target.',
'<strong>Действие.</strong> Сначала зафиксируйте маленький profile contract. Затем отдельно решите, какая проверка допускает трафик, какая граница останавливает ресурс и какие evidence разрешены для изменения значения. Не увеличивайте replicas как ответ на ещё не классифицированный симптом.',
]),
h2('Три слоя одного решения'),
p('Первый слой — декларация. В ней container получает CPU и memory request и limit. Официальная документация Kubernetes для исторического среза v1.30 говорит, что scheduler использует request при выборе Node, а kubelet и runtime применяют limit. Это уже достаточно, чтобы не называть limit «резервом для размещения». У container может оказаться больше свободного ресурса, чем request, если Node позволяет; limit относится к границе, а не к обещанию пропускной способности. Отдельно опасен незаметный случай: если limit задан, а request нет, admission может использовать limit как request. Его нельзя угадывать по шаблону — нужно записать, что именно считается effective request в разрешённой проверке.'),
p('Второй слой — управляемая нагрузка. Для web-сервиса это не строка «CPU 70%», а договор: что считается одной обслуживаемой единицей, как ведут себя startup и steady serving, какой ресурс ограничивает путь первым и где заканчивается область owner-а. Для batch-пути полезнее назвать время жизни рабочего набора, чем копировать CPU target из HTTP-сервиса. Для долгого warmup важно отделить инициализацию от готовности принимать запросы. Profile не обязан предсказывать всю production-кривую; его задача — сделать явным, какое предположение сейчас проверяется и какое наблюдение могло бы его опровергнуть.'),
p('Третий слой — наблюдаемое поведение Pod. Оно должно быть именовано точно: condition Ready, результат readiness probe, полученная метрика, restart, latency или очередь — не взаимозаменяемые слова. В этой статье нет снятых показаний. Учебная fixture ниже хранит значения только как versioned fixed synthetic JS-объекты в памяти и прямо называет их не-telemetry. Такой пример годится для проверки логики сопоставления, но не для вывода о живом Pod, kubectl, CI или HPA. Реальные evidence требуют отдельного разрешённого пути, владельца и временной границы.'),
table('Сопоставление декларации, профиля и результата проверки', ['Слой', 'Рабочий вопрос', 'Что не доказывает', 'Следующее решение'], [
['Request', 'какой ресурс scheduler должен зарезервировать для объявленной единицы работы', 'не доказывает фактический расход, latency или готовность приложения', 'сверить с profile и effective admission result в отдельной проверке'],
['Limit', 'какую верхнюю границу исполнения допускает container', 'не является target HPA и не обещает, что memory ошибка проявится мгновенно', 'разделить CPU boundary, memory boundary и recovery policy'],
['Readiness', 'когда Pod допускается к Service traffic', 'не измеряет capacity, throughput и не заменяет liveness', 'определить один обслуживаемый контракт и owner failure semantics'],
['HPA metric', 'какая измеряемая величина меняет desired replicas', 'не превращает отсутствующий request в meaningful percentage', 'зафиксировать denominator, source metric и handling missing data'],
['Observed behavior', 'какой факт разрешённо собран и что он означает', 'не может быть заменён fixture, screenshot или случайным логом', 'связать с гипотезой до действия над manifest'],
]),
figure('/assets/editorial/2024/container-orchestration-2024-pod-lifecycle.svg', 'Вертикальная схема связывает профиль приложения с CPU и memory requests и limits, затем с состояниями Pod Pending, Running, Ready и Unready. Она отделяет допуск Service traffic от границы ресурсов и не показывает данные реального кластера.', 'Схема показывает порядок рассуждения: профиль задаёт вопросы к manifest, а Ready отвечает только за допуск к трафику. Это не снимок Pod, Node, HPA, CI или telemetry.'),
h2('Readiness — сигнал допуска, а не диагноз'),
p('Readiness probe сообщает kubelet, когда container готов начать принимать трафик; unready Pod не должен быть backend для Service. Это полезный, но узкий смысл. Проверка может сказать «не обслуживаю» во время прогрева, во время controlled drain или когда приложению нужна обязательная зависимость. Она не обязана сказать, почему это произошло. Если readiness начинает проверять каждую внешнюю систему без принятого failure policy, краткая проблема зависимости способна вынуть сразу все backend из трафика. Если, наоборот, probe отвечает success до завершения прогрева, Service получает Pod, который ещё не держит целевой контракт. В обоих случаях увеличение maxReplicas не исправляет неверный смысл самого сигнала.'),
h2('Requests, limits и autoscaling нельзя читать в одиночку'),
p('CPU request влияет и на размещение, и на resource utilization HPA. Для targetAverageUtilization controller берёт usage относительно relevant CPU request; если у Pod нет требуемого request, utilization не определён и autoscaler не действует по этой метрике. Поэтому target 70 процентов не означает «70 процентов Node» и не означает «70 процентов limit». Это 70 процентов именно выбранного request. Изменение request без пересмотра profile одновременно меняет placement economics и числовой смысл HPA target. Это не аргумент никогда не менять request; это аргумент менять их одной review-карточкой.'),
p('Memory требует ещё большей аккуратности. Request помогает scheduler решить, можно ли разместить Pod; limit задаёт предел container. Но память не всегда сообщает проблему той же формой и тем же временем, что CPU. Нельзя нарисовать универсальный коэффициент «memory limit = request × два» и назвать его безопасным. Сначала надо назвать владельца cache, batch или retained object, способ ограничить рост и действие после превысившего договор события. Autoscaling по CPU может быть полезен для serving workload, но сам по себе не объясняет memory lifetime. Если synthetic observation показывает false readiness и высокую synthetic memory величину, корректный следующий вопрос — о границе памяти и значении readiness, не о немедленном scale-up.'),
h2('Исполнимый учебный пример без доступа к системе'),
p('Следующий код не читает манифест и не выполняет API-вызов. Его вход — только известный case id. После него report хранит fixed declaration, fixed managed load и fixed synthetic observed behavior, а plan создаёт лишь текстовые review-actions. Extra field, похожее на имя файла, cluster, CI, network, HTTP, trace или production, fixture отклоняет. Так пример не становится скрытой заменой разрешённой диагностики.'),
code(fixtureCommand),
p('У case <code>fixed-steady-http-v1</code> synthetic CPU 450m не является usage из metrics server и не разрешает менять request 500m. Он нужен лишь чтобы показать отношения внутри заданного объекта: HPA percentage относится к request, а не к limit. Case <code>fixed-memory-growth-v1</code> намеренно не выдаёт synthetic memory за OOM event. Case <code>fixed-warmup-request-missing-v1</code> отклоняет percentage conclusion, потому что CPU request в object равен null. PASS fixture означает только то, что эти границы не были случайно стёрты кодом.'),
h2('Упорядоченный маршрут для настоящей проверки'),
ol([
'<strong>Назвать workload.</strong> Запишите owner, serving или batch shape, startup work, dominant resource и условие, при котором работа должна перестать принимать трафик.',
'<strong>Разложить manifest.</strong> Выпишите per-container request и limit; не суммируйте их в одно «мощность Pod». Отдельно выясните разрешённым способом admission defaults и effective values.',
'<strong>Определить readiness contract.</strong> Скажите, какой дешёвый факт означает «можно обслуживать», а какой не должен делать Pod permanently unready. Назначьте owner изменения semantic.',
'<strong>Проверить metric contract.</strong> Для HPA укажите metric type, denominator или raw target, scope, min/max и поведение при missing values. Не выводите это из fixture.',
'<strong>Собрать ограниченное evidence.</strong> Выберите разрешённую среду, период и source. Отличите Pod condition, usage, restart, latency и queue, прежде чем сопоставлять их с profile.',
'<strong>Менять одну гипотезу.</strong> Измените request, limit, probe или autoscaling policy только с измеримым вопросом, stop condition и rollback. Затем повторите проверку того же контракта.',
]),
h2('Ограничения, rollback и следующий проверяемый шаг'),
p('Эта схема не назначает CPU и memory для реального сервиса, не читает feature gates, controller flags, metrics API, admission policy, Node allocatable, logs или trace. Она не подтверждает, что Service, EndpointSlice, HPA или конкретная probe настроены в чьей-либо среде. В частности, историческая документация HPA описывает default окна initial readiness delay и CPU initialization period, но они являются cluster-wide controller settings; учебная модель их не предполагает и не подменяет доступом к control plane. Для реального изменения нужны отдельные права, security review, dry run, evidence, owner и rollback plan.'),
p('Rollback здесь должен быть таким же конкретным, как изменение. Для request или limit это не «вернуть числа назад» вслепую: сначала нужно понять, какие Pods уже получили новую декларацию и что считалось успехом. Для readiness изменение может вернуть traffic раньше или позже ожидаемого, поэтому его проверяют отдельно от HPA. Следующий проверяемый шаг — создать одностраничную profile card для одного workload: declaration per container, one serving contract, one dominant resource hypothesis, metric denominator, evidence source и stop condition. Пока карточка не заполнена, это design task, а не настройка capacity.'),
h2('Историческая граница июня 2024'),
p('Материал использует только первичные Kubernetes источники, которые существовали до июня 2024: release v1.30 от 17 апреля и зафиксированные commits документации от марта до мая. Из них взяты ограниченные факты о requests, limits, probes, readiness gates и HPA. Все числа, Pod states и outcomes в учебном наборе — versioned fixed synthetic JS-объекты в памяти. Они не являются манифестом, kubectl, CI result, HTTP response, trace, telemetry или production observation.'),
]);
const mechanism = revision({
slug: 'editorial-2024-06-mechanism-container-orchestration',
title: 'Управление контейнерной нагрузкой: модель, ограничения и границы',
categories: ['Kubernetes', 'DevOps'],
cover: '/assets/editorial/2024/container-orchestration-2024-capacity-curve.svg',
excerpt: 'Модель для связи request и limit с HPA и readiness: почему процент CPU имеет знаменатель, Ready не равен capacity, а кривая без источника не является наблюдением.',
readingMinutes: 13,
}, [
p('У команды есть Deployment с HPA на средний CPU 70 процентов. После замены container image startup стал тяжелее, Pod коротко переходит Ready, а потом снова становится Unready. В ответ кто-то предлагает увеличить CPU limit и maxReplicas. Цена — спутать HPA signal с traffic eligibility и увеличить число Pod без понимания причины. Если не отделить request от limit и readiness от metric, рост replicas только усложнит разбор.'),
p('Другая ситуация ещё тише: в manifest нет CPU request, но стоит utilization target. Проблема скрыта именно в знакомом проценте, поэтому его считают рабочим. Однако в официальной модели Kubernetes CPU utilization HPA вычисляется относительно request; когда у container нет нужного request, controller не предпринимает действие по этой метрике. Цена — не только отсутствие scale. Команда получает число, которое выглядит как policy, но не имеет declared denominator, а затем ищет объяснение в synthetic или случайном графике вместо контракта.'),
h2('Симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> HPA target, resource limits и readiness существуют, но разные участники объясняют ими разные события: placement, traffic, CPU saturation и restart.',
'<strong>Причина.</strong> Один Pod воспринимают как одну шкалу capacity. В действительности scheduler, runtime, Service и HPA отвечают на разные входы и в разные моменты.',
'<strong>Проверка.</strong> Постройте четыре связи: profile → request, limit → resource boundary, readiness → traffic eligibility, metric → replica proposal. У каждой связи должны быть source, owner и failure meaning.',
'<strong>Действие.</strong> Оставьте в policy только значения с объявленным смыслом. Если request отсутствует, utilization target не додумывают; если readiness false, не называют это автоматически resource incident; если metric missing, не рисуют real curve из fixture.',
]),
h2('Модель: один manifest, четыре управляющих контура'),
p('Контур размещения начинается с request. Scheduler рассматривает requests при выборе Node, а для Pod общая величина по ресурсу складывается из container requests. Это правило не говорит, что container будет всё время потреблять request; оно описывает reservation contract для scheduling. Поэтому request должен соответствовать объявленной единице работы: например, serving unit и допустимому concurrency, а не просто «значению, после которого Pod когда-то перестал Pending». If sidecar, init или main container имеют разные роли, они требуют отдельных строк и явного общего вывода, а не одного красивого числа в ticket.'),
p('Контур ограничения начинается с limit. Kubelet и runtime применяют CPU и memory limits, но поведение ресурса не следует смешивать. В исторической версии документации описано, что runtime применяет memory limit с OOM error при попытке выделить больше разрешённого; реализация ограничения может быть reactive или enforcement, а детали зависят от runtime. Отсюда практический вывод скромнее популярных советов: CPU limit не является HPA target, memory limit не является прогнозом peak, а изменение обоих не заменяет проверку application lifetime. В учебном наборе нет реального runtime и потому нет утверждения о throttling, OOM, eviction или restart какого-либо Pod.'),
p('Контур traffic использует readiness. Kubelet переводит container в ready по readiness probe; когда Pod не ready, он исключается из Service load balancers. Pod lifecycle добавляет ещё одну границу: readiness gate требует, чтобы containers были ready и все указанные custom conditions стали True. Это удобно, когда приложение действительно нуждается в дополнительном явном соглашении. Но custom condition не даёт автоматической семантики: «feature loaded», «schema usable» и «dependency healthy» должны принадлежать owner-у и иметь failure policy. Gate, который никто не может привести в True при аварии, превращает capacity policy в скрытый single point of delay.'),
p('Контур replica proposal — HPA. Он периодически сопоставляет current metric с desired metric и формирует desired replicas. Для resource utilization CPU значение является процентом от requests контейнеров targeted Pod. Для raw target и custom или external metric интерфейс другой, а metrics API должен существовать отдельно. HPA не проверяет бизнес-готовность и не обещает, что новый Pod уже serving. Поэтому accurate design question звучит не «какой процент поставить», а «какое измерение репрезентирует pressure именно после readiness, с какой задержкой и при каком denominator». Если это неизвестно, placeholder target честнее, чем случайная кривая.'),
table('Четыре контура и их неверные подмены', ['Контур', 'Вход', 'Выход', 'Частая неверная подмена'], [
['Placement', 'CPU и memory request per container', 'возможность scheduler разместить Pod', 'request называют фактическим расходом или лимитом'],
['Resource boundary', 'CPU и memory limit', 'ограничение runtime для container', 'limit называют запасом HPA либо гарантийным throughput'],
['Traffic eligibility', 'readiness probe и readiness gates', 'допуск Pod к Service traffic', 'Ready называют доказательством latency или capacity'],
['Replica proposal', 'metric, target, min/max, HPA algorithm', 'desired replica count', 'desired count считают числом ready backend и root-cause diagnosis'],
['Evidence', 'разрешённый metric, condition или controlled test', 'подтверждение или опровержение profile', 'fixture или screenshot выдают за telemetry'],
]),
figure('/assets/editorial/2024/container-orchestration-2024-capacity-curve.svg', 'Синтетическая кривая показывает CPU как отношение к declared request: target HPA 70 процентов и CPU limit 200 процентов request — разные линии. Подпись отмечает, что значения являются fixed teaching data, а не метриками кластера.', 'Кривая объясняет единицы измерения. Она намеренно не обещает SLA, throughput, p95 latency, фактический scale-up или capacity какого-либо runtime.'),
h2('Почему процент CPU меняет смысл при изменении request'),
p('Представьте declared request 500m и targetAverageUtilization 70. Это формирует target около 350m usage на Pod в модели utilization, а не 70 процентов Node и не 70 процентов CPU limit 1000m. Если request увеличить до 1000m, то тот же target обозначает уже другую рабочую точку. Параллельно scheduler станет резервировать больше. Если request уменьшить, scale signal станет более чувствительным, но placement станет плотнее; это не автоматически хорошо или плохо. Значение можно менять только вместе с hypothesis о workload, потому что один YAML field служит двум механизмам.'),
p('Тут важна граница источника. Документация говорит, что HPA при missing CPU request не действует по resource utilization, и объясняет, как controller консервативно учитывает missing и not-yet-ready Pod для направления scale. Она не даёт права считать, что в конкретной среде работают default flags, metrics server доступен или metric собран нужного качества. В частности, `initial-readiness-delay` и CPU initialization period — параметры controller manager, а не свойства manifest. Их нельзя захардкодить в статью как универсальную задержку. Вместо этого profile должен открыть вопрос: когда serving metric становится репрезентативной и кто имеет право подтвердить это в выбранной среде.'),
h2('Readiness и HPA встречаются, но не становятся одним сигналом'),
p('HPA учитывает readiness при обработке CPU metrics во время инициализации. Это не превращает readiness probe в autoscaling metric. Probe может быть корректной для traffic и совершенно непригодной для оценки future load: она отвечает только success или failure конкретного check. А CPU metric может быть корректной для replica proposal и не сообщать, что Pod умеет совершить важный доменный шаг. Полезно держать два вопроса рядом: «можно ли отправить запрос?» и «есть ли pressure, которое оправдывает изменение desired replicas?». Они могут получить разные ответы и всё ещё быть здоровой системой.'),
h2('Исполнимый объект вместо вымышленных операционных данных'),
p('В учебном примере механизм проверяется только на закрытом наборе object literals. Input содержит model version, scope, mode fixed-memory-only и case id. Любой file-like, cluster-like, CI-like, network-like, HTTP-like, trace-like или production-like field отклоняется до анализа. Это намеренно уже, чем реальный capacity tool: fixture должна показать отношения между значениями, а не притвориться CLI. Значение <code>syntheticObservedPodBehavior</code> имеет собственный marker <code>embedded-fixed-js-object-not-telemetry</code>.'),
code(fixtureCommand),
p('В case <code>fixed-warmup-request-missing-v1</code> есть synthetic CPU 600m и false readiness. Model verdict не говорит «нужно больше Pod». Он говорит, что CPU percentage target нельзя интерпретировать без declared CPU request и что controller startup settings намеренно не читались. В case <code>fixed-memory-growth-v1</code> false readiness не превращается в OOM event, потому что никаких runtime event не было получено. Это не слабость fixture: отрицание неразрешённого вывода является её главным учебным результатом.'),
h2('Упорядоченный маршрут проектирования механизма'),
ol([
'<strong>Определить application profile.</strong> Назовите serving path, startup path, unit of work, dominant resource, concurrency boundary и owner-а выбора.',
'<strong>Связать profile с request.</strong> Для каждого container объясните, зачем ему CPU и memory request. Проверьте effective defaults отдельным разрешённым способом, а не по предположению о limit.',
'<strong>Сформулировать limits как boundaries.</strong> Укажите, какое поведение ожидается при приближении к CPU и memory границе, кто реагирует и чего rollback не восстановит.',
'<strong>Разделить probes.</strong> Напишите отдельные contracts для startup, readiness и liveness; если есть readiness gate, добавьте producer condition и recovery path.',
'<strong>Выбрать HPA metric.</strong> Укажите metric API, target type, denominator, min/max, behavior window и условия, при которых metric не следует считать evidence.',
'<strong>Проверить одну связь.</strong> В согласованной среде сравните один declared input с одним разрешённым observation. Не меняйте request, readiness и HPA одновременно.',
'<strong>Закрыть change record.</strong> Сохраните observed result, limitation, owner, stop condition и rollback. Только затем решайте, стал ли новый profile default.',
]),
h2('Ограничения и следующий проверяемый шаг'),
p('Этот разбор не выбирает тип metric, values probes, containers count, Node size, CPU/memory ratio или правильный maxReplicas. Он не смотрит в admission controller, resource quota, PodDisruptionBudget, EndpointSlice, runtime cgroups, Metrics Server, custom adapter, dashboard, log, trace или production. Он также не обещает, что autoscaling устраняет queueing, memory retention, downstream saturation или dependency failure. Такой вывод возможен только после разрешённого evidence collection с временной и workload-boundary, а не после чтения historical documentation.'),
p('Следующий проверяемый шаг — взять один уже согласованный workload profile и сформировать matrix из пяти строк: per-container requests, per-container limits, readiness and startup conditions, HPA metric denominator, allowed evidence source. Для каждой строки добавьте «что этот сигнал не доказывает». После этого выберите ровно одну uncertainty для проверки в изолированной среде. Если она касается profile, не меняйте policy; если касается metric, не переписывайте probe. Маленькая изолированная проверка лучше общего scale-up, потому что сохраняет причинную связь.'),
h2('Историческая граница июня 2024'),
p('Материал опирается на Kubernetes v1.30 release и snapshots официальной документации, датированные мартом, апрелем и маем 2024. Они были доступны до июня 2024 и ограничивают утверждения о request, limit, probes, readiness gates и HPA. Синтетическая capacity curve, profile cards и outcomes не описывают cluster state. Это fixed in-memory teaching material: без файлов, кластера, CI, сети, HTTP, trace, telemetry и production.'),
]);
const field = revision({
slug: 'editorial-2024-06-field-container-orchestration',
title: 'Управление контейнерной нагрузкой: диагностика, решение и проверка',
categories: ['Kubernetes', 'DevOps'],
cover: '/assets/editorial/2024/container-orchestration-2024-readiness-gate.svg',
excerpt: 'Полевой маршрут для различения request и limit, traffic readiness и HPA signal: три синтетические карточки вместо вымышленных показаний реального кластера.',
readingMinutes: 12,
}, [
p('На разбор приходит фраза: «Pod стал Unready, увеличим maxReplicas». Она кажется практичной, пока не задать первый вопрос: Unready по какому контракту? Если readiness скрывает Pod от Service traffic во время прогрева, а HPA смотрит на CPU относительно request, то number of replicas и traffic eligibility живут в разных контурах. Цена быстрого решения — новый манифест без понятной гипотезы: один Pod может быть не готов по зависимости, другой ещё стартует, а третьему вообще не определён CPU request. После этого любой график становится поводом спорить, а не доказательством причины.'),
p('Ещё один знакомый вывод звучит так: «CPU высок, значит capacity не хватает». Проблема в том, что в этой статье нельзя делать даже такой учебный вывод без declared denominator. HPA percentage — не число само по себе: для CPU utilization ему нужен resource request. Если в декларации request отсутствует, а в synthetic object есть CPU 600m, это не kubectl top, не CI, не telemetry и не команда увеличить replicas. Цена подмены особенно неприятна в warmup: команда тратит время на scale policy, хотя сначала нужно разделить startup work, serving work и metric contract. Полевой разбор начинается не с команды к кластеру, а с карточки сравнения.'),
h2('Симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> Один и тот же признак — high CPU, false readiness или memory near limit — хотят использовать как достаточную причину изменить replicas.',
'<strong>Причина.</strong> Manifest, managed load и observed behavior не записаны рядом; слово «Pod» скрывает разные states и разные механизмы Kubernetes.',
'<strong>Проверка.</strong> Для каждого случая сначала определите type signal: declared field, readiness condition, resource metric, restart event, latency или queue. Затем назовите, что signal не доказывает.',
'<strong>Действие.</strong> Выберите одну проверяемую гипотезу и один разрешённый evidence source. Если signal не имеет contract, зафиксируйте blocker вместо изменения limit, probe или HPA.',
]),
h2('Три карточки, не три истории о реальной среде'),
p('Ниже нет incident report. Это три versioned fixed synthetic JS-объекта из учебной fixture. У каждого есть <code>declaredManifest</code>, <code>managedLoad</code> и <code>syntheticObservedPodBehavior</code>. Последний явно помечен как <code>embedded-fixed-js-object-not-telemetry</code>. Его numbers нельзя переносить в capacity calculator, использовать как SLO, называть output kubectl или сравнивать с реальным production. Зато на этих карточках можно проверить дисциплину: сначала прочитать заявленную границу, потом profile, потом смысл synthetic observation и только после этого сформулировать next question.'),
table('Синтетические карточки учебной модели и допустимый verdict', ['Case id', 'Декларация и profile', 'Synthetic behavior', 'Допустимый verdict'], [
['fixed-steady-http-v1', 'CPU request 500m, target 70%, steady HTTP CPU-bound shape', 'Ready true, synthetic CPU 450m', 'сверить profile, request и HPA denominator; не делать capacity claim'],
['fixed-memory-growth-v1', 'memory request 256Mi, limit 512Mi, bounded batch with retention question', 'Ready false, synthetic memory 470Mi', 'разделить memory lifetime, readiness semantic и replica action; не называть OOM'],
['fixed-warmup-request-missing-v1', 'CPU request null, CPU utilization target 65%, warmup before serving', 'Ready false, synthetic CPU 600m', 'отклонить percentage conclusion до declared request и startup contract'],
['Любой внешний field', 'file, cluster, CI, network, HTTP, trace или production', 'не принимается fixture', 'отклонить input; fixture не является reader или operator'],
]),
h2('Карточка 1: стабильный serving не делает число автоматически верным'),
p('В <code>fixed-steady-http-v1</code> declaration фиксирует request 500m, limit 1000m и target 70 процентов request. Managed profile называет нагрузку steady HTTP и dominant resource CPU. Synthetic observation — Ready true, CPU 450m, memory 310Mi. Из этих данных нельзя заключить, что сервис реально выдерживает нагрузку или что 500m достаточно. Но можно увидеть правильный порядок: 70 процентов имеет declared denominator 500m, а limit 1000m не меняет этот denominator. Если команда меняет request, она обязана пересмотреть и scheduling contract, и HPA interpretation, а не только нарисовать новый threshold.'),
p('Практическая проверка после согласования прав должна узнать не «какой Pod красивее на графике», а покрывает ли request определённую serving unit и соответствует ли metric именно этой unit. Если CPU usage взят после readiness и во время выбранного сценария, его можно сопоставить с profile. Если usage пришёл из startup, sidecar, stale interval или неизвестного selector, он может не отвечать на вопрос. Фикстура не делает эту проверку, потому что она не читает cluster, network, HTTP, trace или telemetry. Она оставляет owner-у сформулированный вопрос и отказывается открыть данные за него.'),
h2('Карточка 2: false readiness не является именем причины'),
p('В <code>fixed-memory-growth-v1</code> profile подчёркивает memory retention question, а readiness false означает только «приложение объявило, что не может обслуживать». Documentation Kubernetes говорит, что unready Pod не получает Service traffic; это действие routing layer, а не диагноз. Synthetic memory 470Mi при limit 512Mi не говорит, что в реальном runtime уже произошёл OOM, throttling, eviction или restart. Учебный пример специально хранит строку <code>no-real-restart-or-oom-event-is-represented</code>, чтобы не позволить превратить близость к limit в выдуманный incident.'),
p('Рациональная следующая ветка — разложить lifetime памяти: cache, batch buffer, response aggregation, connection pool или другой owner. Затем определить, когда readiness должен false, как он становится true снова и не зависит ли эта condition от traffic, которого Pod уже не получает. Только после этого можно обсуждать request/limit или число replicas. CPU HPA может оставить synthetic memory cause нетронутой: он принимает решение по своему metric, а не собирает memory root cause. Если возникают и memory pressure, и traffic loss, это два сигнала для связи с profile, не приглашение сложить их в одно магическое значение.'),
h2('Карточка 3: warmup без request блокирует процентный вывод'),
p('В <code>fixed-warmup-request-missing-v1</code> заявлен targetAverageUtilization 65, но CPU request null. Historical HPA documentation прямо связывает CPU utilization с request и указывает, что при отсутствии relevant request autoscaler не действует по этому metric. Следовательно, object не должен пройти через «CPU 600m — значит scale up». Synthetic 600m лишь служит проверкой отрицательного пути. Первым вопросом становится: какая serving unit даёт CPU request смысл, а вторым — какие controller startup settings и metric timing реально подтверждены в отдельной среде.'),
figure('/assets/editorial/2024/container-orchestration-2024-readiness-gate.svg', 'Схема readiness gate: ContainersReady и custom condition True вместе формируют Ready и допуск к Service traffic. Отдельный блок показывает, что HPA CPU metric сравнивается с request и не равен readiness signal; это учебная схема, а не состояние кластера.', 'Readiness gate добавляет условие к traffic eligibility, но не превращает condition в метрику capacity. Схема не содержит service endpoints, controller flags, logs, trace или production data.'),
h2('Исполнимый пример проверяет отказ от скрытого доступа'),
p('Фикстура легко запускается локально, потому что не требует credentials, namespace, kubeconfig, filesystem, HTTP или CI. Она не вызывает `kubectl`, не создаёт Pod, не читает manifest и не делает HTTP probe. Case id выбирает только заранее записанный object literal. Внешний field отклоняется: поэтому не получится незаметно передать имя deployment.yaml, cluster name, CI URL, network endpoint, trace ID или production marker. Это не ограничение production-инструмента; это честная граница учебного кода.'),
code(fixtureCommand),
p('Проверка сравнивает весь canonical report, а не один verdict. Если подменить <code>syntheticObservedPodBehavior.ready</code>, добавить <code>kubectlOutput</code> или заменить verdict на <code>scale-now</code>, plan отказывает. Rollback принимает только untouched synthetic review draft и возвращает «not read or changed» для manifest и cluster. Такой механизм не доказывает, что реальный rollback безопасен. Он доказывает только, что пример не превратился в канал управления окружением, пока читатель тренирует сравнение трёх слоёв.'),
h2('Упорядоченный маршрут диагностики после получения разрешения'),
ol([
'<strong>Ограничить scope.</strong> Назначьте один workload, owner, изолированную среду, период проверки и разрешённые источники. Не начинайте с массового scan Pod.',
'<strong>Снять declaration.</strong> Зафиксируйте per-container requests и limits, readiness/startup configuration, HPA metric type и target. Отличите written field от effective admission result.',
'<strong>Написать profile.</strong> Назовите serving unit, startup unit, concurrency, dominant resource, dependency policy и meaning Ready/Unready.',
'<strong>Собрать один signal.</strong> Получите по разрешённому маршруту condition либо metric либо controlled test. Не склеивайте их в один verdict до interpretation.',
'<strong>Сравнить с contract.</strong> Скажите, согласуется ли signal с одной declared hypothesis, какие unknown остаются и что signal не доказывает.',
'<strong>Выбрать действие.</strong> Разрешите ровно одно изменение, только если есть owner, expected result, stop condition и rollback. Для missing request или unknown readiness semantics action — blocker.',
'<strong>Повторить тот же вопрос.</strong> После изменения проверьте именно первоначальную hypothesis тем же type evidence; иначе результат нельзя сопоставить.',
]),
h2('Ограничения, rollback и следующий проверяемый шаг'),
p('Материал не подсказывает команду для кластера и не заменяет ручной review production policy. В нём нет наблюдения за actual pod lifecycle, Node pressure, Service endpoints, metrics server, autoscaler behavior, CI job, logs или distributed trace. Там нет утверждения, что range request/limit подходит для чьего-то языка, runtime, image, cache или external dependency. Даже официальный documentation snapshot объясняет generic semantics, но не знает admission webhooks и runtime configuration конкретного владельца. Поэтому нельзя превратить chart, fixture PASS или article table в permission изменить manifest.'),
p('Rollback для реального изменения должен содержать snapshot исходной декларации, owner решения, condition для остановки, временную границу и проверку side effect. Откат HPA не исправляет невнятную readiness condition; возврат limit не рассказывает, что произошло с application memory; изменение probe не восстанавливает evidence. Следующий проверяемый шаг — провести review одной profile card с platform owner и application owner. На выходе нужны: declared request denominator, separate definition of Ready, named allowed metric source и explicit blocker для непроверенной части. Если хотя бы одна строка остаётся «кажется», change не готов.'),
h2('Историческая граница июня 2024'),
p('К июню 2024 уже были опубликованы Kubernetes v1.30 и использованные здесь первичные documentation snapshots. Их положения применены узко: request и limit имеют разные роли; readiness управляет traffic eligibility; readiness gates добавляют named conditions; CPU utilization HPA относится к request и имеет обработку not-yet-ready Pod. Три карточки, values и verdict учебной модели являются только fixed synthetic JS-объектами в памяти. Они не имитируют и не выдают себя за cluster query, kubectl, CI, telemetry, HTTP, trace или production data.'),
]);
export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
function verifyFixture() {
const report = runContainerOrchestrationFixture();
const failed = Object.entries(report.assertions).filter(([, value]) => value !== true).map(([key]) => key);
if (failed.length > 0) {
process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n');
process.exitCode = 1;
return;
}
const count = Object.keys(report.assertions).length;
process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n');
}
if (process.argv.includes('--verify-fixture')) verifyFixture();
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');