73 lines
6.3 KiB
Markdown
73 lines
6.3 KiB
Markdown
## 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 `asyncio` primitives.
|
|
- Enforce backpressure using bounded async queues (`429 Too Many Requests`).
|
|
- Publish to Kafka topic `cdr.raw` using `raw_id` partition keys with `confluent-kafka`.
|
|
- Build a resilient worker that normalizes phone numbers (E.164 via `phonenumbers`), maps RATs, generates deterministic UUIDv5 `cdr_id`s, and safely routes bad data to `cdr.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 `/metrics` and aggregate public `/stats` via an internal worker HTTP endpoint.
|
|
- Emit structured JSON logs across all services.
|
|
- Containerize all services in `docker-compose.yml` with health checks and `condition: 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_server` with 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.Queue` for 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 API` and `Processing Worker` as separate Docker containers. The public `/stats` endpoint 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-kafka` with `raw_id` as the message key when publishing to `cdr.raw`.
|
|
- **Rationale**: `confluent-kafka` is built on `librdkafka` and offers superior throughput over pure Python clients. Using `raw_id` ensures deterministic partition affinity for duplicate CDR submissions while spreading traffic evenly across the 3 partitions via Murmur2 hashing.
|
|
- **Alternatives Considered**:
|
|
- `caller_num` as 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-connect` HTTP client targeting table `telecom.cdr_records` with engine `ReplacingMergeTree(processed_at)`.
|
|
- **Rationale**: ClickHouse recommends HTTP batch inserts. `ReplacingMergeTree` combined with deterministic UUIDv5 `cdr_id` ensures 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-connect` is 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.
|
|
|
|
### 6. Structured JSON Logging
|
|
- **Choice**: Standard library Python logging with a custom JSON formatter producing fields: `timestamp`, `level`, `service`, `raw_id`, and `event`.
|
|
- **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.Queue` rejects requests with `429 Too Many Requests` as 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 `phonenumbers` library with fast-path prefix checks (`00`, `011`, `+`) before full parsing.
|
|
|