Skip to content

Commit f7f5d95

Browse files
knopers8justonedev1
authored andcommitted
Toy model for imitating O2 software memory usage in K8s
Closes OCTRL-1079
1 parent bf6011a commit f7f5d95

File tree

4 files changed

+532
-0
lines changed

4 files changed

+532
-0
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# O2 software toy model
2+
3+
This directory contains a toy model which pretends to be O2 software in terms of how it uses memory and runs on a K8s cluster.
4+
The manifest file declares an init container which creates a shared memory segment and two identical `memory-user` containers which write to anonymous and/or shared memory with specified rates.
5+
One can set memory requests and limits to the containers and observe the behaviour.
6+
7+
The manifest also includes a `mem-guard` container, which monitors anonymous memory usage of `memory-user`s and kills them when they go above a set threshold.
8+
9+
See the manifest and environment variables inside for configuring memory consumption rates and thresholds.
10+
11+
See OCTRL-1079 for more context and info which was understood using this model.
12+
## Running
13+
14+
1. Add `allocate.py` and `mem_guard.py` scripts to a configmap. This way we can reuse the official python image as-is.
15+
16+
```
17+
kubectl create configmap allocate-script \
18+
--from-file=allocate.py=./allocate.py \
19+
--from-file=mem_guard.py=./mem_guard.py \
20+
-o yaml --dry-run=client | kubectl apply -f -
21+
```
22+
23+
2. Apply the manifest
24+
25+
```
26+
kubectl apply -f mem-allocate-test.yaml
27+
```
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import mmap
4+
import os
5+
import sys
6+
import time
7+
8+
9+
def parse_args():
10+
p = argparse.ArgumentParser(
11+
description="Memory load generator: private allocation + shmem(tmpfs file) mmap read/write."
12+
)
13+
14+
# Private memory
15+
p.add_argument("--private-rate-bps", type=int, default=0,
16+
help="Private memory allocation+fill rate in bytes/sec. 0 disables.")
17+
p.add_argument("--private-bytes", type=int, default=0,
18+
help="Optional cap for private bytes to allocate. 0 means no cap (grow forever).")
19+
p.add_argument("--private-chunk-bytes", type=int, default=4 * 1024 * 1024,
20+
help="Chunk size for private allocations (bytes). Default 4MiB.")
21+
p.add_argument("--page-touch-bytes", type=int, default=4096,
22+
help="Stride for touching/filling memory to force residency. Default 4096.")
23+
24+
# Shmem mmap activity
25+
p.add_argument("--shmem-path", type=str, default="",
26+
help="Path to a shmem/tmpfs-backed file (e.g. /dev/shm/segment.bin).")
27+
p.add_argument("--shmem-bytes", type=int, default=0,
28+
help="Ensure shmem file size is at least this many bytes (create/truncate). 0 = don't change size.")
29+
p.add_argument("--shmem-read-rate-bps", type=int, default=0,
30+
help="Read rate from shmem mapping in bytes/sec. Only reads if > 0.")
31+
p.add_argument("--shmem-write-rate-bps", type=int, default=0,
32+
help="Write rate to shmem mapping in bytes/sec. Only writes if > 0.")
33+
34+
# Loop control
35+
p.add_argument("--tick-ms", type=int, default=50,
36+
help="Control loop tick in milliseconds. Default 50ms.")
37+
p.add_argument("--report-every-s", type=float, default=2.0,
38+
help="Print status every N seconds. Default 2s.")
39+
40+
return p.parse_args()
41+
42+
43+
def touch_buffer(buf: bytearray, stride: int):
44+
"""Touch/dirty memory with given stride to force page allocation."""
45+
n = len(buf)
46+
if n == 0:
47+
return
48+
v = int(time.time_ns() & 0xFF)
49+
for i in range(0, n, stride):
50+
buf[i] = (buf[i] + v + (i & 0xFF)) & 0xFF
51+
52+
53+
def ensure_file_size(path: str, size: int):
54+
fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o666)
55+
try:
56+
os.ftruncate(fd, size)
57+
finally:
58+
os.close(fd)
59+
60+
61+
def main():
62+
args = parse_args()
63+
64+
for name, v in [
65+
("--private-rate-bps", args.private_rate_bps),
66+
("--shmem-read-rate-bps", args.shmem_read_rate_bps),
67+
("--shmem-write-rate-bps", args.shmem_write_rate_bps),
68+
]:
69+
if v < 0:
70+
print(f"{name} must be >= 0", file=sys.stderr)
71+
return 2
72+
73+
if args.private_chunk_bytes <= 0:
74+
print("--private-chunk-bytes must be > 0", file=sys.stderr)
75+
return 2
76+
if args.page_touch_bytes <= 0:
77+
print("--page-touch-bytes must be > 0", file=sys.stderr)
78+
return 2
79+
80+
tick = args.tick_ms / 1000.0
81+
report_every = args.report_every_s
82+
83+
# Private memory state
84+
private_chunks: list[bytearray] = []
85+
private_allocated = 0
86+
87+
# Shmem state
88+
shmem_fd = None
89+
shmem_mm = None
90+
shmem_size = 0
91+
shmem_r_off = 0
92+
shmem_w_off = 0
93+
shmem_read_total = 0
94+
shmem_written_total = 0
95+
96+
need_shmem = (args.shmem_read_rate_bps > 0) or (args.shmem_write_rate_bps > 0)
97+
if need_shmem:
98+
if not args.shmem_path:
99+
print("shmem activity requested but --shmem-path is empty", file=sys.stderr)
100+
return 2
101+
102+
if args.shmem_bytes > 0:
103+
try:
104+
ensure_file_size(args.shmem_path, args.shmem_bytes)
105+
except Exception as e:
106+
print(f"Failed to create/size shmem file: {e}", file=sys.stderr)
107+
return 2
108+
109+
# For mmap write, we need RDWR; for read-only, RDONLY is enough
110+
open_flags = os.O_RDONLY if args.shmem_write_rate_bps == 0 else os.O_RDWR
111+
try:
112+
shmem_fd = os.open(args.shmem_path, open_flags)
113+
st = os.fstat(shmem_fd)
114+
shmem_size = st.st_size
115+
if shmem_size <= 0:
116+
raise RuntimeError("shmem file size is 0; set --shmem-bytes to something > 0")
117+
118+
access = mmap.ACCESS_READ if args.shmem_write_rate_bps == 0 else mmap.ACCESS_WRITE
119+
shmem_mm = mmap.mmap(shmem_fd, shmem_size, access=access)
120+
except Exception as e:
121+
if shmem_fd is not None:
122+
try:
123+
os.close(shmem_fd)
124+
except Exception:
125+
pass
126+
print(f"Failed to open/mmap shmem file: {e}", file=sys.stderr)
127+
return 2
128+
129+
last = time.monotonic()
130+
last_report = last
131+
132+
private_budget = 0.0
133+
shmem_read_budget = 0.0
134+
shmem_write_budget = 0.0
135+
136+
# A tiny accumulator so reads can't be optimized away
137+
checksum = 0
138+
139+
try:
140+
while True:
141+
now = time.monotonic()
142+
dt = now - last
143+
last = now
144+
145+
private_budget += args.private_rate_bps * dt
146+
shmem_read_budget += args.shmem_read_rate_bps * dt
147+
shmem_write_budget += args.shmem_write_rate_bps * dt
148+
149+
# --- Private allocation (allocate + touch) ---
150+
if args.private_rate_bps > 0:
151+
cap = args.private_bytes
152+
while private_budget >= 1:
153+
if cap > 0 and private_allocated >= cap:
154+
private_budget = 0
155+
break
156+
157+
chunk = args.private_chunk_bytes
158+
if cap > 0:
159+
remaining = cap - private_allocated
160+
if remaining <= 0:
161+
private_budget = 0
162+
break
163+
if remaining < chunk:
164+
chunk = remaining
165+
166+
if private_budget < chunk:
167+
chunk = int(private_budget)
168+
if chunk < 64 * 1024:
169+
break
170+
171+
buf = bytearray(chunk)
172+
touch_buffer(buf, args.page_touch_bytes)
173+
private_chunks.append(buf)
174+
private_allocated += chunk
175+
private_budget -= chunk
176+
177+
# --- Shmem mmap write: touches mapped pages, should increase RES/SHR ---
178+
if shmem_mm is not None and args.shmem_write_rate_bps > 0:
179+
while shmem_write_budget >= 1:
180+
to_write = int(shmem_write_budget)
181+
if to_write <= 0:
182+
break
183+
184+
if shmem_w_off >= shmem_size:
185+
shmem_w_off = 0
186+
187+
n = min(to_write, shmem_size - shmem_w_off)
188+
if n <= 0:
189+
shmem_w_off = 0
190+
break
191+
192+
# Touch each page once (or more if n is large) to fault pages in.
193+
stride = 4096
194+
v = int(time.time_ns() & 0xFF)
195+
end = shmem_w_off + n
196+
for off in range(shmem_w_off, end, stride):
197+
shmem_mm[off] = (shmem_mm[off] + v) & 0xFF
198+
199+
shmem_w_off = end
200+
shmem_written_total += n
201+
shmem_write_budget -= n
202+
203+
if shmem_w_off >= shmem_size:
204+
shmem_w_off = 0
205+
206+
# --- Shmem mmap read: faults pages in (RES/SHR can still rise), no dirtying ---
207+
if shmem_mm is not None and args.shmem_read_rate_bps > 0:
208+
while shmem_read_budget >= 1:
209+
to_read = int(shmem_read_budget)
210+
if to_read <= 0:
211+
break
212+
213+
if shmem_r_off >= shmem_size:
214+
shmem_r_off = 0
215+
216+
n = min(to_read, shmem_size - shmem_r_off)
217+
if n <= 0:
218+
shmem_r_off = 0
219+
break
220+
221+
stride = 4096
222+
end = shmem_r_off + n
223+
for off in range(shmem_r_off, end, stride):
224+
checksum ^= shmem_mm[off]
225+
226+
shmem_r_off = end
227+
shmem_read_total += n
228+
shmem_read_budget -= n
229+
230+
if shmem_r_off >= shmem_size:
231+
shmem_r_off = 0
232+
233+
# --- Reporting ---
234+
if now - last_report >= report_every:
235+
last_report = now
236+
parts = [f"private_allocated={private_allocated}B"]
237+
if shmem_mm is not None:
238+
parts.append(f"shmem_size={shmem_size}B")
239+
parts.append(f"shmem_written_total={shmem_written_total}B")
240+
parts.append(f"shmem_read_total={shmem_read_total}B")
241+
parts.append(f"checksum={checksum}")
242+
parts.append(f"shmem_path={args.shmem_path}")
243+
print(" ".join(parts), flush=True)
244+
245+
time.sleep(tick)
246+
247+
except KeyboardInterrupt:
248+
print("\nExiting.", file=sys.stderr)
249+
finally:
250+
if shmem_mm is not None:
251+
try:
252+
shmem_mm.close()
253+
except Exception:
254+
pass
255+
if shmem_fd is not None:
256+
try:
257+
os.close(shmem_fd)
258+
except Exception:
259+
pass
260+
261+
return 0
262+
263+
264+
if __name__ == "__main__":
265+
raise SystemExit(main())

0 commit comments

Comments
 (0)