6.3 KiB
6.3 KiB
Context
See proposal.md for background and problem motivation.
The architecture is constrained to pure Python without web frameworks (FastAPI, Flask, Starlette, or Django). The pipeline must ingest high-throughput telecom CDRs, decouple ingestion from processing via Kafka, normalize heterogeneous fields, micro-batch into ClickHouse with at-least-once delivery guarantees, and provide complete operational observability via Prometheus metrics and structured JSON logging.
Goals / Non-Goals
Goals:
- Implement a non-blocking HTTP ingestion server using Python standard library
asyncioprimitives. - Enforce backpressure using bounded async queues (
429 Too Many Requests). - Publish to Kafka topic
cdr.rawusingraw_idpartition keys withconfluent-kafka. - Build a resilient worker that normalizes phone numbers (E.164 via
phonenumbers), maps RATs, generates deterministic UUIDv5cdr_ids, and safely routes bad data tocdr.raw.dlq. - Micro-batch inserts into ClickHouse (
ReplacingMergeTree) with configurable count (1,000) and time (2s) flush thresholds. - Guarantee at-least-once delivery by committing Kafka offsets only after ClickHouse write ACK.
- Expose dual-service Prometheus
/metricsand aggregate public/statsvia an internal worker HTTP endpoint. - Emit structured JSON logs across all services.
- Containerize all services in
docker-compose.ymlwith health checks andcondition: service_healthy.
Non-Goals:
- Web-based visualization UI (analytical queries and verification scripts provide reporting).
- Schema Registry broker integration (deferred from optional bonus challenges).
- Distributed multi-worker consumer coordination (single worker instance fulfills baseline requirements).
Decisions
1. Framework-less asyncio HTTP Server
- Choice: Implement the HTTP listener using
asyncio.start_serverwith a lightweight, zero-dependency HTTP/1.1 request parser. - Rationale: Strictly satisfies the constraint of "pure Python without frameworks" while delivering high concurrent connection handling and native integration with
asyncio.Queuefor backpressure. - Alternatives Considered:
http.server.ThreadingHTTPServer: Standard library, but thread-per-request model has higher memory overhead and makes bounded async backpressure coordination harder.FastAPI/Starlette: Disallowed by project constraints.
2. Service Separation and Stats Aggregation
- Choice: Run
Ingestion APIandProcessing Workeras separate Docker containers. The public/statsendpoint is hosted by the Ingestion API, which queries a lightweight internal HTTP endpoint on the Worker (GET http://worker:8001/internal/stats) and aggregates with local ingestion metrics. - Rationale: Isolates HTTP ingestion from processing CPU/network bottlenecks. Enables independent scaling and lifecycle management.
- Alternatives Considered:
- Single combined monolithic process: Violates explicit requirement for separate services.
- Querying ClickHouse directly for
/stats: Incurs database query overhead and does not report real-time in-flight buffer size or consumer lag.
3. Kafka Client and Partitioning Strategy
- Choice: Use
confluent-kafkawithraw_idas the message key when publishing tocdr.raw. - Rationale:
confluent-kafkais built onlibrdkafkaand offers superior throughput over pure Python clients. Usingraw_idensures deterministic partition affinity for duplicate CDR submissions while spreading traffic evenly across the 3 partitions via Murmur2 hashing. - Alternatives Considered:
caller_numas key: Risk of partition hotspots if certain subscribers generate disproportionate traffic.null(round-robin): Loses record-level partition affinity.
4. ClickHouse Client and Idempotent Deduplication
- Choice: Use
clickhouse-connectHTTP client targeting tabletelecom.cdr_recordswith engineReplacingMergeTree(processed_at). - Rationale: ClickHouse recommends HTTP batch inserts.
ReplacingMergeTreecombined with deterministic UUIDv5cdr_idensures that if a batch is replayed after a worker crash prior to offset commit, duplicate records with identical order keys are safely merged without data duplication. - Alternatives Considered:
clickhouse-driver(native TCP protocol): Slightly higher client-side setup complexity;clickhouse-connectis the official modern ClickHouse client.- Standard
MergeTree: Does not deduplicate replayed batches, violating idempotency.
5. Micro-Batching and Fault Tolerance Strategy
- Choice: Worker accumulates records into an in-memory batch. Flushes when
len(batch) >= BATCH_SIZE(default 1,000) or elapsed time >=BATCH_FLUSH_INTERVAL_SECONDS(default 2s). - Failure Handling:
- If ClickHouse write times out, worker retries with exponential backoff up to
CH_RETRY_MAX_ATTEMPTS(default 3, initial delay 500ms, factor 2.0). - If all retries fail, the entire batch is wrapped and routed to
cdr.raw.dlq, and offsets are committed to prevent pipeline stall. - On
SIGTERM, consumer stops, remaining buffer is flushed to ClickHouse, offsets are committed, and worker exits cleanly.
- If ClickHouse write times out, worker retries with exponential backoff up to
6. Structured JSON Logging
- Choice: Standard library Python logging with a custom JSON formatter producing fields:
timestamp,level,service,raw_id, andevent. - Rationale: Clean, zero-dependency implementation producing machine-readable logs compatible with modern observability stacks.
Risks / Trade-offs
- [Risk] High ingestion bursts saturate in-memory queue → [Mitigation] The bounded
asyncio.Queuerejects requests with429 Too Many Requestsas soon as capacity is reached, protecting process memory. - [Risk] ClickHouse outage stalls worker → [Mitigation] Retries are bounded to 3 attempts with exponential backoff. If ClickHouse remains unreachable, batch is offloaded to DLQ and offsets committed to prevent infinite consumer lag loops.
- [Risk] Worker crashes between ClickHouse ACK and Kafka commit → [Mitigation] Re-consumed batch is re-inserted;
ReplacingMergeTree(processed_at)deduplicates rows sharing the order key(service_type, radio_access_type, cell_id, session_start_time, msisdn). - [Risk] Phone number parsing performance overhead → [Mitigation] Use
phonenumberslibrary with fast-path prefix checks (00,011,+) before full parsing.