@mark1russell7/kind 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/build/index.d.ts +7 -1
- package/build/index.js +41 -1
- package/build/index.js.map +1 -1
- package/build/{judge-CDDCr1Z8.d.ts → judge-CKhPqBku.d.ts} +8 -2
- package/build/judge-Cf97eOJm.js.map +1 -1
- package/build/judge.d.ts +1 -1
- package/image/Dockerfile +1 -1
- package/image/agent.mjs +184 -3
- package/image/client.mjs +24 -8
- package/package.json +4 -2
- package/src/conntrack.ts +99 -0
- package/src/drops.ts +63 -0
package/README.md
CHANGED
|
@@ -72,6 +72,14 @@ The tests read three real records of `/1` (`test/records/`): a cell that holds,
|
|
|
72
72
|
`openKindLab(cluster, image)` (`lab.ts`) starts the instruments of a cluster:
|
|
73
73
|
|
|
74
74
|
- **The agent of each node** is a privileged container in the PID namespace of the node container. It signals a process of a pod by its PID on the node. It writes `cpu.max` in the cgroup of a container, and a flag file in the root of a container.
|
|
75
|
+
- From 0.1.4, it also reads the conntrack table of the node each second. It writes a `stale` line for each entry to the ClusterIP that keeps its NAT to a pod that the rules dropped.
|
|
76
|
+
- It reads `/proc/net/nf_conntrack`, or else it asks netlink with the `conntrack` tool of the node. The kernel of a GitHub runner has no `/proc/net/nf_conntrack`. A dump through netlink walks each bucket of the table of the kernel with the softirqs off. A segment that comes in that walk waits. Thus each read is one dump, and `kind.yml` gives the table 16384 buckets, not 262144.
|
|
77
|
+
- A SYN on the port of such an entry goes to that pod, whatever the rules say. An entry in TIME_WAIT is not stale, because a SYN reopens it with a new lookup.
|
|
78
|
+
- It writes a `stalled` line for each handshake that stopped for one second or more: a SYN with no reply, or a SYN-ACK with no ACK. On the node of the client, the line gives the backend of the DNAT. On the node of that backend, the line tells which half of the handshake did not arrive.
|
|
79
|
+
- Each `rules-alive` line gives the source of the table (`ct`). It counts the reads since the last line, and the entries of the ClusterIP in each state. A read that fails stops the reads, and `ct.error` tells why. The lab drops the lines from before the first measurement, thus an early failure shows only in `ct.error`.
|
|
80
|
+
- `ct.syn` gives up to three entries in SYN_SENT, with the backend of their NAT and their life. A new SYN has the life 120 s, and an older one less.
|
|
81
|
+
- Each second, the agent reads the counters of dropped packets (`drops.ts`), and it writes a `drops` line with each counter that grew. For the node, these are the drops of the backlogs, the squeezes of the softirqs, and the drops of each veth.
|
|
82
|
+
- For each client pod, the agent reads `/proc/<pid>/net`. These counters are the IP discards, the sends with no route, the failed connects, the drops of its interfaces, and its neighbors with no address. A SYN that does not get to the conntrack of the node leaves its trace in one of them.
|
|
75
83
|
- **The rules of each node.** The agent reads the rules of kube-proxy in the namespaces of the node each 50 ms: `iptables-save`, `nft list table ip kube-proxy`, or `/proc/net/ip_vs`. It writes a line when the backends of the Service change (`backends.ts`). A backend of IPVS with the weight zero takes no new connection, thus it is not a backend.
|
|
76
84
|
- **The injector of each node** is the image of the harness, in the PID namespace of the node. `crictl` on the node gives the PID of a container, and `nsenter -n` enters the network namespace of its pod. The injector of the harness then applies its rules there.
|
|
77
85
|
- **The watcher** is a container on the network of the cluster. It watches the pods, the EndpointSlices and the events through the API, and it follows the log of each pod of the cell.
|
|
@@ -79,6 +87,8 @@ The tests read three real records of `/1` (`test/records/`): a cell that holds,
|
|
|
79
87
|
|
|
80
88
|
Each pod of a cluster shares the kernel of the Docker VM. Thus each stamp of the agent, the injector, the watcher, the clients, the service and the prober is on the same CLOCK_MONOTONIC.
|
|
81
89
|
|
|
90
|
+
Before each run, the lab deletes the conntrack entries in SYN_SENT to the ClusterIP on each node (`forgetHandshakes`). A note of the run gives each count. Such an entry is a SYN with no answer, and it keeps its NAT for 120 s. A new SYN from the same port goes where the first one went, and it gets no answer either. The clients send requests between the runs too. Thus the restore of one run left such entries, and a client that took the port again in the next run timed out (T07, 2026-10-17).
|
|
91
|
+
|
|
82
92
|
## The image
|
|
83
93
|
|
|
84
94
|
The lab image (`image/`) holds the service of a cell on `@mark1russell7/service`, its clients, the agent and the watcher, on Node 24.21.0. Its base is pinned by digest, and `npm-lock.json` pins the packages from npm. `stageLabImage()` stages its context, and `labImage()` tags the image by its content.
|
|
@@ -87,7 +97,7 @@ The lab image (`image/`) holds the service of a cell on `@mark1russell7/service`
|
|
|
87
97
|
- **From npm**, the stage packs the installed versions of the same packages. The package holds the files of the image, and `src/compose.ts` and `src/backends.ts`, for that.
|
|
88
98
|
- **The service** (`app.mjs`) is a Node HTTP server. Its one controller needs Redis when the cell has Redis. Its health server gives the probes, and it declares each stage of the shutdown. `compose.ts` composes the service from the declaration of the cell, and `lint()` reads the same composition.
|
|
89
99
|
- **The seams** of the service are a flag file that fails its liveness check, and SIGUSR2. SIGUSR2 blocks its event loop for the time in a file.
|
|
90
|
-
- **The clients** (`client.mjs`) send requests on new connections to the ClusterIP. Each request gives one line with its outcome: ok, 503, refused, reset, timeout or unreachable.
|
|
100
|
+
- **The clients** (`client.mjs`) send requests on new connections to the ClusterIP. Each request gives one line with its outcome: ok, 503, refused, reset, timeout or unreachable. From 0.1.3, each line also gives the local port of its connection (`lp`), a refused one's too. A conntrack entry is per port. Thus the requests on the port of a failure show the connection before it, and the pod that it went to.
|
|
91
101
|
|
|
92
102
|
## The triggers
|
|
93
103
|
|
package/build/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as SliceLine, $t as goDuration, A as TriggerV1, At as showBand, B as RunFacts, Bt as Replica, C as KindCellResult, Ct as LOG_LABEL, D as PredictionV1, Dt as bandOf, E as LoadSample, Et as SERVER, F as KindDecision, Ft as KubeletProbe, G as referenceOf, Gt as KUBE_PROXY_MODES, H as factsOf, Ht as Trigger, I as KindVerdict, It as PodSettings, J as Line, Jt as KubeProxyMode, K as unitOf, Kt as KindConfig, L as LAST_SLACK, Lt as Prediction, M as upgrade, Mt as Declaration, N as upgradeCell, Nt as KindCell, O as RunCheckV1, Ot as seconds, P as KIND_NOISE, Pt as KindRun, Q as RulesLine, Qt as cidrRange, R as Propagated, Rt as RedisDeclaration, S as KindCellRecordV1, St as HEALTH_PORT, T as KindVerdictV1, Tt as REDIS, U as judgeKind, Ut as TriggerKind, V as checkRun, Vt as Template, W as propagationOf, Wt as Workload, X as RecordLine, Xt as NodeImage, Y as PodLine, Yt as NODE_IMAGES, Z as Request, Zt as cidrOverlap, _ as ClusterManifest, _t as sliceDropOf, a as STAGES, at as failuresOf, b as FORMAT_V1, bt as transitionOf, c as readRecord, ct as outcomesOf, d as calibrationOf, dt as recordsOf, en as kindConfig, et as backendsAt, f as cellsTable, ft as requestsOf, g as BandV1, gt as signalsOf, h as timeSpread, ht as signalOf, i as quantileOf, in as scenarioProblems, it as emptyOf, j as judge, jt as Band, k as RunFactsV1, kt as secondsOf, l as Judged, lt as podsAt, m as predictionText, mt as rulesOf, n as medianInterval, nn as nodeNames, nt as deletionOf, o as kindCellRecordSchema, ot as intakeOf, p as medianText, pt as restartsAt, q as Endpoint, qt as KindScenario, r as middle, rn as scenario, rt as dropOf, s as kindCellRecordV1Schema, st as kernelCounts, t as RELATIVE, tn as kubeProxyPatch, tt as byNode, u as TimeSpread, ut as podsOf, v as EnvironmentManifest, vt as sliceEmptyOf, w as KindCellV1, wt as PORT, x as KindCellRecord, xt as CLIENT, y as FORMAT, yt as slicesOf, z as RunCheck, zt as Reference } from "./judge-
|
|
1
|
+
import { $ as SliceLine, $t as goDuration, A as TriggerV1, At as showBand, B as RunFacts, Bt as Replica, C as KindCellResult, Ct as LOG_LABEL, D as PredictionV1, Dt as bandOf, E as LoadSample, Et as SERVER, F as KindDecision, Ft as KubeletProbe, G as referenceOf, Gt as KUBE_PROXY_MODES, H as factsOf, Ht as Trigger, I as KindVerdict, It as PodSettings, J as Line, Jt as KubeProxyMode, K as unitOf, Kt as KindConfig, L as LAST_SLACK, Lt as Prediction, M as upgrade, Mt as Declaration, N as upgradeCell, Nt as KindCell, O as RunCheckV1, Ot as seconds, P as KIND_NOISE, Pt as KindRun, Q as RulesLine, Qt as cidrRange, R as Propagated, Rt as RedisDeclaration, S as KindCellRecordV1, St as HEALTH_PORT, T as KindVerdictV1, Tt as REDIS, U as judgeKind, Ut as TriggerKind, V as checkRun, Vt as Template, W as propagationOf, Wt as Workload, X as RecordLine, Xt as NodeImage, Y as PodLine, Yt as NODE_IMAGES, Z as Request, Zt as cidrOverlap, _ as ClusterManifest, _t as sliceDropOf, a as STAGES, at as failuresOf, b as FORMAT_V1, bt as transitionOf, c as readRecord, ct as outcomesOf, d as calibrationOf, dt as recordsOf, en as kindConfig, et as backendsAt, f as cellsTable, ft as requestsOf, g as BandV1, gt as signalsOf, h as timeSpread, ht as signalOf, i as quantileOf, in as scenarioProblems, it as emptyOf, j as judge, jt as Band, k as RunFactsV1, kt as secondsOf, l as Judged, lt as podsAt, m as predictionText, mt as rulesOf, n as medianInterval, nn as nodeNames, nt as deletionOf, o as kindCellRecordSchema, ot as intakeOf, p as medianText, pt as restartsAt, q as Endpoint, qt as KindScenario, r as middle, rn as scenario, rt as dropOf, s as kindCellRecordV1Schema, st as kernelCounts, t as RELATIVE, tn as kubeProxyPatch, tt as byNode, u as TimeSpread, ut as podsOf, v as EnvironmentManifest, vt as sliceEmptyOf, w as KindCellV1, wt as PORT, x as KindCellRecord, xt as CLIENT, y as FORMAT, yt as slicesOf, z as RunCheck, zt as Reference } from "./judge-CKhPqBku.js";
|
|
2
2
|
import { Quantity } from "@mark1russell7/quantity";
|
|
3
3
|
import { CheckFn, Finding, HealthServer, Poll, Rule, Scenario, ServiceBuilder, ServiceDescription, Stage, Timing, TransportImpl, Verdict } from "@mark1russell7/service";
|
|
4
4
|
import { RedisClient } from "@mark1russell7/service/redis";
|
|
@@ -186,6 +186,12 @@ interface KindLab {
|
|
|
186
186
|
netnsOf(node: string, pid: number): Promise<number>;
|
|
187
187
|
/** This method gives the path of the cgroup of a process, as the agent reaches it through the root of the node. */
|
|
188
188
|
cgroupOf(node: string, pid: number): Promise<string>;
|
|
189
|
+
/**
|
|
190
|
+
* This method deletes the conntrack entries in SYN_SENT to a ClusterIP and port on each node, and
|
|
191
|
+
* gives the count of each node. Such an entry is a SYN with no answer. It keeps its NAT for 120 s,
|
|
192
|
+
* and a new SYN from the same port goes where it went.
|
|
193
|
+
*/
|
|
194
|
+
forgetHandshakes(ip: string, port: number): Promise<Record<string, number>>;
|
|
189
195
|
close(): Promise<void>;
|
|
190
196
|
}
|
|
191
197
|
/**
|
package/build/index.js
CHANGED
|
@@ -803,7 +803,9 @@ const FILES = [
|
|
|
803
803
|
["agent.mjs", "image/agent.mjs"],
|
|
804
804
|
["watch.mjs", "image/watch.mjs"],
|
|
805
805
|
["compose.ts", "src/compose.ts"],
|
|
806
|
-
["backends.ts", "src/backends.ts"]
|
|
806
|
+
["backends.ts", "src/backends.ts"],
|
|
807
|
+
["conntrack.ts", "src/conntrack.ts"],
|
|
808
|
+
["drops.ts", "src/drops.ts"]
|
|
807
809
|
];
|
|
808
810
|
const shell = process.platform === "win32";
|
|
809
811
|
/** This function gives each file of a staged context, with its path in the context, in a stable sequence. */
|
|
@@ -897,6 +899,14 @@ const labImage = () => {
|
|
|
897
899
|
return built;
|
|
898
900
|
};
|
|
899
901
|
//#endregion
|
|
902
|
+
//#region src/conntrack.ts
|
|
903
|
+
/** This function gives the count of entries that `conntrack -D` deleted, from its last line: "N flow entries have been deleted". */
|
|
904
|
+
const deletedOf = (text) => {
|
|
905
|
+
const match = /(\d+) flow entr(?:y|ies) ha(?:s|ve) been deleted/.exec(text);
|
|
906
|
+
if (match === null) throw new Error(`conntrack -D gave no count: ${text.trim().slice(0, 200)}`);
|
|
907
|
+
return Number(match[1]);
|
|
908
|
+
};
|
|
909
|
+
//#endregion
|
|
900
910
|
//#region src/lab.ts
|
|
901
911
|
/** These kinds of kernel record stay in the raw record of a run. The lab counts the other kinds and drops them. */
|
|
902
912
|
const KEPT_KERNEL = /* @__PURE__ */ new Set([
|
|
@@ -1070,6 +1080,33 @@ const openKindLab = async (cluster, image) => {
|
|
|
1070
1080
|
`printf '%s' '${file}' > /tmp/rules.json`
|
|
1071
1081
|
]);
|
|
1072
1082
|
},
|
|
1083
|
+
async forgetHandshakes(ip, port) {
|
|
1084
|
+
const out = {};
|
|
1085
|
+
for (const [node, { agent }] of sidecars) {
|
|
1086
|
+
const argv = [
|
|
1087
|
+
"nsenter",
|
|
1088
|
+
"-t",
|
|
1089
|
+
"1",
|
|
1090
|
+
"-m",
|
|
1091
|
+
"-n",
|
|
1092
|
+
"--",
|
|
1093
|
+
"conntrack",
|
|
1094
|
+
"-D",
|
|
1095
|
+
"-p",
|
|
1096
|
+
"tcp",
|
|
1097
|
+
"--orig-dst",
|
|
1098
|
+
ip,
|
|
1099
|
+
"--dport",
|
|
1100
|
+
String(port),
|
|
1101
|
+
"--state",
|
|
1102
|
+
"SYN_SENT"
|
|
1103
|
+
];
|
|
1104
|
+
const result = await agent.exec(argv);
|
|
1105
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) throw new Error(`${argv.join(" ")} exited ${result.exitCode}: ${result.output.trim().slice(0, 300)}`);
|
|
1106
|
+
out[node] = deletedOf(result.output);
|
|
1107
|
+
}
|
|
1108
|
+
return out;
|
|
1109
|
+
},
|
|
1073
1110
|
async act(node, spec) {
|
|
1074
1111
|
const stdout = await output(sidecarOf(node).agent, [
|
|
1075
1112
|
"node",
|
|
@@ -2034,6 +2071,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
2034
2071
|
"rollout"
|
|
2035
2072
|
].includes(cell.trigger.kind) ? null : replicas[0];
|
|
2036
2073
|
const paused = cell.template === "T07" && cell.trigger.kind === "freeze-process" ? cluster.nodes[1] ?? cluster.nodes[0] : null;
|
|
2074
|
+
const cleared = await lab.forgetHandshakes(ctx.clusterIp, 80);
|
|
2037
2075
|
lab.take();
|
|
2038
2076
|
for (const key of Object.keys(lab.dropped)) delete lab.dropped[key];
|
|
2039
2077
|
const start = await lab.now();
|
|
@@ -2042,6 +2080,8 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
2042
2080
|
let resume = null;
|
|
2043
2081
|
if (cell.trigger.kind === "scale-in") ctx.expected = cell.pod.replicas - 1;
|
|
2044
2082
|
const notes = [];
|
|
2083
|
+
const forgotten = Object.entries(cleared).filter(([, count]) => count > 0);
|
|
2084
|
+
if (forgotten.length > 0) notes.push(`before the run, the lab deleted SYN_SENT entries of the ClusterIP: ${forgotten.map(([node, count]) => `${count} on ${node}`).join(", ")}`);
|
|
2045
2085
|
for (const step of plan.inject) {
|
|
2046
2086
|
const stamp = await doStep(ctx, step, replicas, background);
|
|
2047
2087
|
if (stamp !== null && fault === null) fault = stamp.t0;
|