#!/usr/bin/env python3 """tests/platform_probe.py — диагностика транспортного слоя платформы (HTTP контейнер). Проверяет, как платформа (шлюз/ingress/DDoS-Guard) пропускает трафик к сервису, НЕЗАВИСИМО от бизнес-логики SQS. Запускать и на ВМ, и на локальной машине для сравнения путей. Только stdlib + boto3. Блоки: A — через /health: burst 500 параллельных, большие заголовки, медленное тело, keep-alive x50, HTTP/1.0, HEAD, POST. B — через SQS API (транспорт): SendMessage 32KB, Receive 10x32KB, long-poll 20s (фактическая длительность), chunked-тело. """ import http.client import json import os import socket import ssl import sys import threading import time import uuid import boto3 from botocore.config import Config ENDPOINT = os.environ.get("ENDPOINT_URL", "https://sqs.containerk8s.dev.nubes.ru") HOST = ENDPOINT.replace("https://", "").replace("http://", "").rstrip("/") PORT = 443 REGION = os.environ.get("REGION", "us-east-1") MODE = os.environ.get("PROBE_MODE", "unknown") results = {} def _conn(): c = http.client.HTTPSConnection(HOST, PORT, timeout=30) return c def record(name, value): results[name] = value print("%s: %s" % (name, json.dumps(value, ensure_ascii=False)), flush=True) def http_get(path, headers=None, method="GET", body=None): c = _conn() try: c.request(method, path, body=body, headers=headers or {}) r = c.getresponse() data = r.read() return r.status, dict(r.getheaders()), data finally: c.close() # --- A1: burst 500 параллельных /health --- def a1_burst(): n = 500 ok = err = 0 lat = [] lock = threading.Lock() def one(): nonlocal ok, err t0 = time.time() try: st, _, _ = http_get("/health") with lock: lat.append(time.time() - t0) if st == 200: ok += 1 else: err += 1 except Exception: with lock: err += 1 threads = [threading.Thread(target=one) for _ in range(n)] t0 = time.time() for t in threads: t.start() for t in threads: t.join() total = time.time() - t0 lat.sort() record("A1_burst_500", { "ok": ok, "err": err, "total_s": round(total, 2), "p50": round(lat[len(lat) // 2], 3) if lat else None, "p95": round(lat[int(len(lat) * 0.95)], 3) if lat else None, "max": round(lat[-1], 3) if lat else None, }) # --- A2: большие заголовки --- def a2_big_headers(): for kb in (8, 16, 64): h = {"User-Agent": "platform-probe", "X-Pad": "x" * (kb * 1024)} try: st, _, _ = http_get("/health", headers=h) record("A2_header_%dkb" % kb, st) except Exception as e: record("A2_header_%dkb" % kb, type(e).__name__ + ": " + str(e)[:80]) # --- A3: медленное тело (1 байт / 0.5с, Content-Length 10) --- def a3_slow_body(): try: ctx = ssl.create_default_context() raw = ctx.wrap_socket(socket.create_connection((HOST, PORT), timeout=15), server_hostname=HOST) raw.sendall(b"POST /health HTTP/1.1\r\nHost: %s\r\nContent-Length: 10\r\nConnection: close\r\n\r\n" % HOST.encode()) t0 = time.time() for _ in range(10): raw.sendall(b"x") time.sleep(0.5) resp = raw.recv(65536) dt = time.time() - t0 raw.close() first_line = resp.split(b"\r\n", 1)[0].decode("latin1", "replace") if resp else "(no response)" record("A3_slow_body", {"resp": first_line, "t_s": round(dt, 2)}) except Exception as e: record("A3_slow_body", type(e).__name__ + ": " + str(e)[:80]) # --- A4: keep-alive 50 запросов на одном соединении --- def a4_keepalive(): ok = err = 0 c = _conn() try: for _ in range(50): try: c.request("GET", "/health") r = c.getresponse() r.read() if r.status == 200: ok += 1 else: err += 1 except Exception: err += 1 break finally: c.close() record("A4_keepalive_50", {"ok": ok, "err": err}) # --- A5: HTTP/1.0, HEAD, POST --- def a5_variants(): try: ctx = ssl.create_default_context() raw = ctx.wrap_socket(socket.create_connection((HOST, PORT), timeout=15), server_hostname=HOST) raw.sendall(b"GET /health HTTP/1.0\r\nHost: %s\r\n\r\n" % HOST.encode()) resp = raw.recv(4096) raw.close() record("A5_http1.0", resp.split(b"\r\n", 1)[0].decode("latin1", "replace") if resp else "(no response)") except Exception as e: record("A5_http1.0", type(e).__name__ + ": " + str(e)[:80]) try: st, _, _ = http_get("/health", method="HEAD") record("A5_head", st) except Exception as e: record("A5_head", type(e).__name__ + ": " + str(e)[:80]) try: st, _, _ = http_get("/health", method="POST", body=b"x") record("A5_post", st) except Exception as e: record("A5_post", type(e).__name__ + ": " + str(e)[:80]) # --- B6: SendMessage 32KB (большие тела платформа вешает на ~51с — MTU, см. HISTORY) --- def b6_big_send(sqs, url): t0 = time.time() try: sqs.send_message(QueueUrl=url, MessageBody="x" * (32 * 1024)) record("B6_send_32kb", {"ok": True, "t_s": round(time.time() - t0, 3)}) except Exception as e: record("B6_send_32kb", {"ok": False, "err": type(e).__name__ + ": " + str(e)[:80], "t_s": round(time.time() - t0, 3)}) # --- B8: Receive 10 x 32KB --- def b8_big_receive(sqs, url): for _ in range(10): sqs.send_message(QueueUrl=url, MessageBody="y" * (32 * 1024)) t0 = time.time() try: r = sqs.receive_message(QueueUrl=url, MaxNumberOfMessages=10, WaitTimeSeconds=1) msgs = r.get("Messages", []) total = sum(len(m["Body"]) for m in msgs) record("B8_receive_10x32kb", {"ok": True, "msgs": len(msgs), "bytes": total, "t_s": round(time.time() - t0, 3)}) for m in msgs: try: sqs.delete_message(QueueUrl=url, ReceiptHandle=m["ReceiptHandle"]) except Exception: pass except Exception as e: record("B8_receive_10x32kb", {"ok": False, "err": type(e).__name__ + ": " + str(e)[:80], "t_s": round(time.time() - t0, 3)}) # --- B9: long-poll 20s — фактическая длительность --- def b9_longpoll(sqs, url): t0 = time.time() try: sqs.receive_message(QueueUrl=url, MaxNumberOfMessages=1, WaitTimeSeconds=20) record("B9_longpoll_20s", {"ok": True, "t_s": round(time.time() - t0, 2)}) except Exception as e: record("B9_longpoll_20s", {"ok": False, "err": type(e).__name__ + ": " + str(e)[:80], "t_s": round(time.time() - t0, 2)}) # --- B7: chunked-тело --- def b7_chunked(): try: ctx = ssl.create_default_context() raw = ctx.wrap_socket(socket.create_connection((HOST, PORT), timeout=15), server_hostname=HOST) raw.sendall(b"POST /health HTTP/1.1\r\nHost: %s\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n" % HOST.encode()) raw.sendall(b"5\r\nhello\r\n") raw.sendall(b"5\r\nworld\r\n") raw.sendall(b"0\r\n\r\n") resp = raw.recv(4096) raw.close() record("B7_chunked", resp.split(b"\r\n", 1)[0].decode("latin1", "replace") if resp else "(no response)") except Exception as e: record("B7_chunked", type(e).__name__ + ": " + str(e)[:80]) def main(): sqs = boto3.client("sqs", endpoint_url=ENDPOINT, region_name=REGION, config=Config(connect_timeout=10, read_timeout=30, retries={"max_attempts": 1})) q = "platprobe-%s" % int(time.time()) url = sqs.create_queue(QueueName=q)["QueueUrl"] print("queue: %s" % url, flush=True) try: a1_burst() a2_big_headers() a3_slow_body() a4_keepalive() a5_variants() b6_big_send(sqs, url) b7_chunked() b8_big_receive(sqs, url) b9_longpoll(sqs, url) finally: try: sqs.delete_queue(QueueUrl=url) except Exception: pass print("MODE=%s ENDPOINT=%s" % (MODE, ENDPOINT), flush=True) print("SUMMARY: %s" % json.dumps(results, ensure_ascii=False), flush=True) if __name__ == "__main__": sys.exit(main())