<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kafka | Yoonmee Hwang</title><link>https://devotto.github.io/tags/kafka/</link><atom:link href="https://devotto.github.io/tags/kafka/index.xml" rel="self" type="application/rss+xml"/><description>Kafka</description><generator>HugoBlox Kit (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Sun, 01 Feb 2026 00:00:00 +0000</lastBuildDate><image><url>https://devotto.github.io/media/icon_hu_1c0e9cb08cfb822a.png</url><title>Kafka</title><link>https://devotto.github.io/tags/kafka/</link></image><item><title>Movie Recommender System in Production</title><link>https://devotto.github.io/projects/movie-recommender/</link><pubDate>Sun, 01 Feb 2026 00:00:00 +0000</pubDate><guid>https://devotto.github.io/projects/movie-recommender/</guid><description>&lt;p&gt;Infrastructure and MLOps lead in a 5-person CMU team (17-645). Operated a movie recommender serving a simulated 1M-user streaming service across dev and prod Kubernetes environments, with Helm charts, a FastAPI inference service, GitHub Actions CI/CD, and Prometheus, Grafana, and Loki monitoring. Cut recommendation timeouts from 15-20% of requests to 3.3%.&lt;/p&gt;
&lt;h2 id="architecture"&gt;Architecture&lt;/h2&gt;
&lt;p&gt;The whole system runs on a single k3s VM, split into dev and prod namespaces off one Helm chart with separate values files. A Kafka stream feeds batch ingestion; the serving path stays independent of it so that ingestion problems never take down recommendations.&lt;/p&gt;
&lt;div class="mermaid"&gt;flowchart TB
User["User / grading bot"]
Kafka["Kafka stream"]
subgraph serve["Serving path"]
Traefik["Traefik ingress"]
API["FastAPI · 2 replicas&lt;br/&gt;recall → ranking"]
end
subgraph jobs["Scheduled CronJobs"]
DL["Dataloader · 15 min"]
TR["Trainer · nightly"]
EV["Evaluator · 6 h"]
KA["Kafka check · 5 min"]
end
DB[("SQLite&lt;br/&gt;core + telemetry")]
ART[("Model artifacts")]
subgraph obs["Observability"]
PROM["Prometheus"]
LOKI["Loki"]
GRAF["Grafana"]
end
User --&gt; Traefik --&gt; API
Kafka --&gt; DL
Kafka --&gt; KA
DL --&gt; DB
API --&gt; DB
DB --&gt; TR --&gt; ART
ART -. "auto-reload" .-&gt; API
EV --&gt; DB
API --&gt; PROM
API --&gt; LOKI
KA --&gt; PROM
PROM --&gt; GRAF
LOKI --&gt; GRAF
&lt;/div&gt;
&lt;p&gt;Model updates are zero-downtime: the trainer CronJob writes a new artifact to a shared volume, and the API reloads it by watching the file&amp;rsquo;s modification time, degrading gracefully if the artifact is briefly missing mid-write.&lt;/p&gt;
&lt;h2 id="making-it-fast"&gt;Making it fast&lt;/h2&gt;
&lt;p&gt;Recommendations were timing out on 15-20% of requests, falling back to non-personalized results. Two separate causes turned up.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;ranking stage&lt;/strong&gt; compared each candidate movie against every user who had liked it — for popular movies, thousands. Worst case ran past 30,000 similarity operations per request. Capping users scanned per movie, skipping users with fewer than 2 shared movies, and trimming the candidate set brought that to roughly 1,500 operations, a &lt;strong&gt;95% reduction&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;first request to each pod&lt;/strong&gt; took about 2 seconds, because the model loaded lazily. Preloading it during pod startup and adding a &lt;code&gt;startupProbe&lt;/code&gt; meant pods only receive traffic once the model is ready, which removed the cold-start penalty entirely.&lt;/p&gt;
&lt;p&gt;Measured in dev over a 60-request sample after the change:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Timeout rate&lt;/td&gt;
&lt;td&gt;15-20%&lt;/td&gt;
&lt;td&gt;3.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold-start latency&lt;/td&gt;
&lt;td&gt;~2,000ms&lt;/td&gt;
&lt;td&gt;&amp;lt;100ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P50 latency&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;3ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P95 latency&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;100ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests under 100ms&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="monitoring"&gt;Monitoring&lt;/h2&gt;
&lt;p&gt;Four signals, each on its own cadence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Service availability&lt;/strong&gt; — a CronJob scans the Kafka stream every 5 minutes for recommendation events and pushes an up/down gauge through the Prometheus Pushgateway, so the internal metric matches how the service is graded externally&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model accuracy&lt;/strong&gt; — an online evaluator computes hit-rate every 6 hours by joining recommendations against subsequent watches&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operating cost&lt;/strong&gt; — per-request movie license cost written to a telemetry table with running totals kept for O(1) dashboard reads&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data drift&lt;/strong&gt; — unique users and movies per 2-hour window as a leading indicator, plus schema validation on every external movie-API response&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alerts are deliberately narrow: only conditions that mean something is actually broken, such as no healthy API pods or a failed CronJob, routed to the infrastructure owner rather than the whole team. Cost and hit-rate are watched on dashboards instead, since neither is actionable at alerting timescales.&lt;/p&gt;
&lt;h2 id="delivery"&gt;Delivery&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Authored 92% of the CI/CD pipeline commits, across 8 GitHub Actions workflows&lt;/li&gt;
&lt;li&gt;Per-component Docker builds with git-SHA image tags on GHCR, chained so an image build triggers the matching Helm deployment&lt;/li&gt;
&lt;li&gt;Unit tests with coverage reporting gate every pull request; Grafana dashboards are synced from version control rather than edited in the UI&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="team-scope"&gt;Team scope&lt;/h2&gt;
&lt;p&gt;Drift detection and the core A/B testing logic were owned by teammates, and I owned the security analysis in the final milestone. My primary scope was the delivery pipeline, Kubernetes operations, and observability.&lt;/p&gt;</description></item></channel></rss>