mirror of
https://github.com/hanzoai/papers.git
synced 2026-10-22 17:20:17 +00:00
716 lines
24 KiB
TeX
716 lines
24 KiB
TeX
\documentclass[11pt,twocolumn]{article}
|
|
\usepackage{shared/hanzocover}
|
|
|
|
\usepackage[utf8]{inputenc}
|
|
\usepackage[T1]{fontenc}
|
|
\usepackage{amsmath,amssymb}
|
|
\usepackage{graphicx}
|
|
\usepackage{hyperref}
|
|
\usepackage{cite}
|
|
\usepackage{algorithm}
|
|
\usepackage{algorithmic}
|
|
\usepackage{listings}
|
|
\input{shared/lstlang}
|
|
\usepackage{xcolor}
|
|
|
|
\lstset{
|
|
basicstyle=\ttfamily\small,
|
|
keywordstyle=\color{blue},
|
|
commentstyle=\color{gray},
|
|
stringstyle=\color{red},
|
|
breaklines=true,
|
|
frame=single
|
|
}
|
|
|
|
\title{Verus: Real-Time Commerce Analytics at Scale}
|
|
|
|
\author{Zach Kelling\\
|
|
Hanzo Industries\\
|
|
\texttt{[email protected]}}
|
|
|
|
\date{November 2015}
|
|
|
|
\begin{document}
|
|
|
|
\luxcoverpage
|
|
|
|
\begin{abstract}
|
|
We present Verus, a real-time analytics system designed for e-commerce workloads. Traditional analytics architectures batch-process events with multi-hour latency, limiting merchants' ability to respond to emerging trends, detect anomalies, and optimize campaigns in flight. Verus processes commerce events with sub-second latency while maintaining the analytical flexibility of batch systems. The architecture combines stream processing for real-time aggregation with columnar storage for ad-hoc queries, unified through a SQL interface that abstracts the underlying complexity. We describe the system design, evaluate performance on production workloads, and demonstrate applications including live dashboards, cohort analysis, and anomaly detection. Production deployments process over 50,000 events per second with query latency under 200ms for common dashboard queries.
|
|
\end{abstract}
|
|
|
|
\section{Introduction}
|
|
|
|
E-commerce generates events at high velocity: page views, product interactions, cart modifications, and transactions flow continuously. Extracting actionable insights from this stream requires analytics systems that balance three competing requirements:
|
|
|
|
\begin{enumerate}
|
|
\item \textbf{Freshness}: Insights must reflect recent events. Stale data delays response to problems (site errors, fraud spikes) and opportunities (trending products, effective campaigns).
|
|
\item \textbf{Flexibility}: Merchants need ad-hoc queries, not just pre-defined reports. Investigating anomalies requires drilling into arbitrary dimensions.
|
|
\item \textbf{Scale}: Event volumes grow with business success. Systems must scale horizontally without architectural changes.
|
|
\end{enumerate}
|
|
|
|
Traditional approaches force trade-offs. Data warehouses (Redshift, BigQuery) provide flexibility but with batch latency. Stream processors (Storm, Flink) offer freshness but limited query expressiveness. Lambda architectures \cite{marz2015} combine both but double operational complexity.
|
|
|
|
Verus achieves all three requirements through a unified architecture. Events stream into a real-time aggregation layer that maintains pre-computed rollups for common queries. Simultaneously, events land in columnar storage for ad-hoc analysis. A query planner routes requests to the optimal layer based on query characteristics.
|
|
|
|
Our contributions are:
|
|
\begin{enumerate}
|
|
\item A hybrid architecture combining stream processing and columnar storage with automatic query routing.
|
|
\item Materialized view maintenance for sub-second dashboard queries.
|
|
\item Cohort analysis operators for retention and lifetime value computation.
|
|
\item Anomaly detection integrated into the analytics pipeline.
|
|
\end{enumerate}
|
|
|
|
\section{Background}
|
|
|
|
\subsection{Commerce Analytics Requirements}
|
|
|
|
E-commerce analytics spans multiple use cases:
|
|
|
|
\textbf{Operational dashboards.} Real-time visibility into key metrics: orders per minute, revenue, conversion rate, error rate. Latency requirement: seconds.
|
|
|
|
\textbf{Marketing attribution.} Understanding which channels and campaigns drive conversions. Requires joining touchpoints across sessions. Latency requirement: minutes to hours.
|
|
|
|
\textbf{Cohort analysis.} Comparing behavior across customer segments defined by acquisition date, first purchase category, or other attributes. Latency requirement: hours.
|
|
|
|
\textbf{Ad-hoc investigation.} Drilling into anomalies or exploring hypotheses. Query patterns unpredictable. Latency requirement: seconds to minutes.
|
|
|
|
\subsection{Event Model}
|
|
|
|
Commerce events follow a common structure:
|
|
|
|
\begin{lstlisting}[language=Python,caption=Event schema]
|
|
@dataclass
|
|
class CommerceEvent:
|
|
event_id: str # UUID
|
|
event_type: str # page_view, add_to_cart, purchase, etc.
|
|
timestamp: datetime # Event time
|
|
user_id: Optional[str] # Authenticated user
|
|
session_id: str # Session identifier
|
|
|
|
# Dimensions
|
|
store_id: str
|
|
device_type: str # desktop, mobile, tablet
|
|
country: str
|
|
referrer: str
|
|
utm_source: Optional[str]
|
|
utm_medium: Optional[str]
|
|
utm_campaign: Optional[str]
|
|
|
|
# Measures (event-type specific)
|
|
product_id: Optional[str]
|
|
product_category: Optional[str]
|
|
quantity: Optional[int]
|
|
revenue: Optional[Decimal]
|
|
\end{lstlisting}
|
|
|
|
Event volumes vary widely. A small merchant might generate 1,000 events per day; a large retailer during a flash sale might generate 100,000 events per second.
|
|
|
|
\subsection{Existing Approaches}
|
|
|
|
\textbf{Batch data warehouses.} Systems like Amazon Redshift, Google BigQuery, and Snowflake excel at complex analytical queries over historical data. They process data in batches, typically with hour-scale latency. Query flexibility is excellent; freshness is poor.
|
|
|
|
\textbf{Stream processors.} Apache Storm, Flink, and Kafka Streams process events in real-time. They excel at continuous aggregations but struggle with ad-hoc queries requiring historical context.
|
|
|
|
\textbf{OLAP cubes.} Pre-aggregated cubes (Druid, Pinot, ClickHouse) offer fast queries over predefined dimensions. They trade flexibility for speed—queries outside the cube's schema require falling back to raw data.
|
|
|
|
\textbf{Lambda architecture.} Combining batch and stream layers provides both freshness and flexibility but doubles infrastructure complexity. Maintaining consistency between layers is notoriously difficult.
|
|
|
|
\section{System Design}
|
|
|
|
\subsection{Architecture Overview}
|
|
|
|
Verus comprises three layers:
|
|
|
|
\begin{enumerate}
|
|
\item \textbf{Ingestion layer}: Receives events, validates, and routes to processing.
|
|
\item \textbf{Processing layer}: Maintains real-time aggregations and writes to storage.
|
|
\item \textbf{Query layer}: Routes queries to optimal backend and merges results.
|
|
\end{enumerate}
|
|
|
|
\begin{figure}[h]
|
|
\centering
|
|
\fbox{\parbox{0.9\linewidth}{
|
|
\centering
|
|
\textit{[Architecture diagram]}\\[0.5em]
|
|
Events $\rightarrow$ Kafka $\rightarrow$ Stream Processor $\rightarrow$ Redis (aggregates)\\
|
|
\hspace{4em} $\searrow$ Columnar Store (raw events)\\[0.5em]
|
|
Query Router $\rightarrow$ Redis / Columnar / Both
|
|
}}
|
|
\caption{Verus architecture overview}
|
|
\label{fig:arch}
|
|
\end{figure}
|
|
|
|
\subsection{Ingestion Layer}
|
|
|
|
Events arrive via HTTP endpoints or Kafka consumers:
|
|
|
|
\begin{lstlisting}[language=Python,caption=Event ingestion]
|
|
class EventIngester:
|
|
def __init__(self, kafka_producer, validator):
|
|
self.producer = kafka_producer
|
|
self.validator = validator
|
|
|
|
async def ingest(self, event: CommerceEvent) -> None:
|
|
# Validate schema and business rules
|
|
self.validator.validate(event)
|
|
|
|
# Enrich with server-side data
|
|
event.server_timestamp = datetime.utcnow()
|
|
event.geo = geoip.lookup(event.ip_address)
|
|
|
|
# Partition by store for ordering guarantees
|
|
partition_key = event.store_id
|
|
|
|
await self.producer.send(
|
|
topic="commerce-events",
|
|
key=partition_key,
|
|
value=event.to_json()
|
|
)
|
|
\end{lstlisting}
|
|
|
|
Events partition by store ID, ensuring ordering within a store while enabling parallel processing across stores.
|
|
|
|
\subsection{Stream Processing Layer}
|
|
|
|
The stream processor maintains real-time aggregations:
|
|
|
|
\subsubsection{Aggregation Windows}
|
|
|
|
We maintain aggregations at multiple time granularities:
|
|
|
|
\begin{itemize}
|
|
\item \textbf{1-minute windows}: Operational dashboards.
|
|
\item \textbf{1-hour windows}: Intraday trends.
|
|
\item \textbf{1-day windows}: Daily summaries.
|
|
\end{itemize}
|
|
|
|
Windows use event time with allowed lateness of 5 minutes. Late events update existing windows; very late events are logged but excluded.
|
|
|
|
\subsubsection{Materialized Views}
|
|
|
|
Common query patterns are pre-computed as materialized views:
|
|
|
|
\begin{lstlisting}[language=SQL,caption=Materialized view definition]
|
|
CREATE MATERIALIZED VIEW revenue_by_source AS
|
|
SELECT
|
|
store_id,
|
|
TUMBLE(event_time, INTERVAL '1' MINUTE) AS window,
|
|
utm_source,
|
|
COUNT(*) AS order_count,
|
|
SUM(revenue) AS total_revenue,
|
|
COUNT(DISTINCT user_id) AS unique_customers
|
|
FROM events
|
|
WHERE event_type = 'purchase'
|
|
GROUP BY store_id, window, utm_source;
|
|
\end{lstlisting}
|
|
|
|
The stream processor maintains these views incrementally:
|
|
|
|
\begin{lstlisting}[language=Python,caption=Incremental aggregation]
|
|
class MaterializedViewProcessor:
|
|
def __init__(self, view_def, state_store):
|
|
self.view_def = view_def
|
|
self.state = state_store
|
|
|
|
def process(self, event):
|
|
if not self.view_def.filter(event):
|
|
return
|
|
|
|
# Compute aggregation key
|
|
key = self.view_def.group_key(event)
|
|
|
|
# Update state
|
|
current = self.state.get(key) or self.view_def.initial_state()
|
|
updated = self.view_def.aggregate(current, event)
|
|
self.state.put(key, updated)
|
|
|
|
# Emit to downstream
|
|
self.emit(key, updated)
|
|
\end{lstlisting}
|
|
|
|
\subsubsection{State Management}
|
|
|
|
Aggregation state resides in Redis for fast access:
|
|
|
|
\begin{lstlisting}[language=Python,caption=Redis state store]
|
|
class RedisStateStore:
|
|
def __init__(self, redis_client, ttl_seconds):
|
|
self.redis = redis_client
|
|
self.ttl = ttl_seconds
|
|
|
|
def get(self, key: str) -> Optional[Dict]:
|
|
data = self.redis.get(key)
|
|
return json.loads(data) if data else None
|
|
|
|
def put(self, key: str, value: Dict) -> None:
|
|
self.redis.setex(key, self.ttl, json.dumps(value))
|
|
|
|
def increment(self, key: str, field: str, delta: float) -> float:
|
|
return self.redis.hincrbyfloat(key, field, delta)
|
|
\end{lstlisting}
|
|
|
|
State keys encode view name, dimensions, and time window:
|
|
|
|
\begin{verbatim}
|
|
verus:revenue_by_source:store_123:google:2015-06-15T14:30
|
|
\end{verbatim}
|
|
|
|
\subsection{Columnar Storage Layer}
|
|
|
|
Raw events persist to columnar storage for ad-hoc queries:
|
|
|
|
\subsubsection{Storage Format}
|
|
|
|
We use Apache Parquet with the following optimizations:
|
|
|
|
\begin{itemize}
|
|
\item \textbf{Partitioning}: By store ID and date for partition pruning.
|
|
\item \textbf{Sorting}: By timestamp within partitions for range queries.
|
|
\item \textbf{Compression}: Snappy for balance of speed and size.
|
|
\item \textbf{Statistics}: Min/max per column for predicate pushdown.
|
|
\end{itemize}
|
|
|
|
\begin{lstlisting}[language=Python,caption=Parquet writer]
|
|
class ParquetWriter:
|
|
def __init__(self, base_path, partition_cols):
|
|
self.base_path = base_path
|
|
self.partition_cols = partition_cols
|
|
self.buffers = {} # partition -> event buffer
|
|
|
|
def write(self, event):
|
|
partition = self.compute_partition(event)
|
|
self.buffers.setdefault(partition, []).append(event)
|
|
|
|
if len(self.buffers[partition]) >= BATCH_SIZE:
|
|
self.flush(partition)
|
|
|
|
def flush(self, partition):
|
|
events = self.buffers.pop(partition)
|
|
path = f"{self.base_path}/{partition}/data_{uuid4()}.parquet"
|
|
|
|
table = pa.Table.from_pylist(events, schema=EVENT_SCHEMA)
|
|
pq.write_table(table, path, compression='snappy')
|
|
\end{lstlisting}
|
|
|
|
\subsubsection{Query Engine}
|
|
|
|
Ad-hoc queries execute on columnar storage via a distributed query engine (we use Presto):
|
|
|
|
\begin{lstlisting}[language=SQL,caption=Ad-hoc query example]
|
|
SELECT
|
|
product_category,
|
|
COUNT(*) AS views,
|
|
COUNT(DISTINCT session_id) AS sessions,
|
|
SUM(CASE WHEN event_type = 'purchase' THEN 1 ELSE 0 END) AS purchases
|
|
FROM events
|
|
WHERE store_id = 'store_123'
|
|
AND event_date = '2015-06-15'
|
|
AND device_type = 'mobile'
|
|
GROUP BY product_category
|
|
ORDER BY views DESC
|
|
LIMIT 20;
|
|
\end{lstlisting}
|
|
|
|
\subsection{Query Routing Layer}
|
|
|
|
The query router examines incoming queries and routes to the optimal backend:
|
|
|
|
\begin{lstlisting}[language=Python,caption=Query routing]
|
|
class QueryRouter:
|
|
def __init__(self, redis_client, presto_client, view_catalog):
|
|
self.redis = redis_client
|
|
self.presto = presto_client
|
|
self.views = view_catalog
|
|
|
|
def execute(self, query: str) -> ResultSet:
|
|
parsed = parse_sql(query)
|
|
|
|
# Check if query matches a materialized view
|
|
view_match = self.views.match(parsed)
|
|
|
|
if view_match and self.is_recent_enough(parsed, view_match):
|
|
# Serve from Redis
|
|
return self.query_redis(view_match, parsed)
|
|
elif view_match:
|
|
# Merge Redis (recent) + Presto (historical)
|
|
return self.merge_results(
|
|
self.query_redis(view_match, parsed),
|
|
self.query_presto(parsed)
|
|
)
|
|
else:
|
|
# Full scan on Presto
|
|
return self.query_presto(parsed)
|
|
|
|
def is_recent_enough(self, parsed, view):
|
|
# Check if query time range fits in Redis TTL
|
|
query_start = parsed.get_time_range()[0]
|
|
redis_start = datetime.utcnow() - timedelta(seconds=view.ttl)
|
|
return query_start >= redis_start
|
|
\end{lstlisting}
|
|
|
|
\section{Analytical Capabilities}
|
|
|
|
\subsection{Live Dashboards}
|
|
|
|
Dashboards query materialized views for sub-second response:
|
|
|
|
\begin{lstlisting}[language=SQL,caption=Dashboard query]
|
|
-- Orders and revenue, last hour, by minute
|
|
SELECT
|
|
window_start,
|
|
SUM(order_count) AS orders,
|
|
SUM(total_revenue) AS revenue
|
|
FROM mv_revenue_by_minute
|
|
WHERE store_id = 'store_123'
|
|
AND window_start >= NOW() - INTERVAL '1' HOUR
|
|
GROUP BY window_start
|
|
ORDER BY window_start;
|
|
\end{lstlisting}
|
|
|
|
Query latency: 15--50ms (served entirely from Redis).
|
|
|
|
\subsection{Cohort Analysis}
|
|
|
|
Cohort analysis groups users by acquisition characteristics and tracks behavior over time:
|
|
|
|
\begin{lstlisting}[language=SQL,caption=Cohort retention query]
|
|
WITH cohorts AS (
|
|
SELECT
|
|
user_id,
|
|
DATE_TRUNC('week', MIN(event_time)) AS cohort_week
|
|
FROM events
|
|
WHERE event_type = 'purchase'
|
|
AND store_id = 'store_123'
|
|
GROUP BY user_id
|
|
),
|
|
activity AS (
|
|
SELECT
|
|
e.user_id,
|
|
c.cohort_week,
|
|
DATE_TRUNC('week', e.event_time) AS activity_week
|
|
FROM events e
|
|
JOIN cohorts c ON e.user_id = c.user_id
|
|
WHERE e.event_type = 'purchase'
|
|
AND e.store_id = 'store_123'
|
|
)
|
|
SELECT
|
|
cohort_week,
|
|
DATE_DIFF('week', cohort_week, activity_week) AS weeks_since_cohort,
|
|
COUNT(DISTINCT user_id) AS active_users
|
|
FROM activity
|
|
GROUP BY cohort_week, weeks_since_cohort
|
|
ORDER BY cohort_week, weeks_since_cohort;
|
|
\end{lstlisting}
|
|
|
|
We provide cohort analysis as a built-in function:
|
|
|
|
\begin{lstlisting}[language=SQL,caption=Cohort analysis function]
|
|
SELECT * FROM COHORT_ANALYSIS(
|
|
events,
|
|
cohort_date := DATE_TRUNC('week', first_purchase_date),
|
|
activity_date := DATE_TRUNC('week', event_time),
|
|
user_id := user_id,
|
|
metric := COUNT(DISTINCT order_id),
|
|
filters := [store_id = 'store_123', event_type = 'purchase']
|
|
);
|
|
\end{lstlisting}
|
|
|
|
\subsection{Funnel Analysis}
|
|
|
|
Funnel analysis tracks conversion through defined steps:
|
|
|
|
\begin{lstlisting}[language=SQL,caption=Conversion funnel]
|
|
SELECT * FROM FUNNEL(
|
|
events,
|
|
steps := [
|
|
event_type = 'page_view' AND page = '/products',
|
|
event_type = 'product_view',
|
|
event_type = 'add_to_cart',
|
|
event_type = 'checkout_start',
|
|
event_type = 'purchase'
|
|
],
|
|
window := INTERVAL '30' MINUTE,
|
|
group_by := [device_type, utm_source]
|
|
);
|
|
\end{lstlisting}
|
|
|
|
The funnel operator uses session-based windowing to track users through steps:
|
|
|
|
\begin{lstlisting}[language=Python,caption=Funnel computation]
|
|
def compute_funnel(events, steps, window, group_by):
|
|
results = defaultdict(lambda: [0] * len(steps))
|
|
|
|
for session in group_by_session(events):
|
|
# Track furthest step reached per session
|
|
step_times = [None] * len(steps)
|
|
|
|
for event in sorted(session.events, key=lambda e: e.timestamp):
|
|
for i, step_predicate in enumerate(steps):
|
|
if step_predicate(event):
|
|
if i == 0 or (step_times[i-1] and
|
|
event.timestamp - step_times[i-1] <= window):
|
|
step_times[i] = event.timestamp
|
|
|
|
# Increment counts for reached steps
|
|
group_key = tuple(getattr(session, g) for g in group_by)
|
|
for i, t in enumerate(step_times):
|
|
if t is not None:
|
|
results[group_key][i] += 1
|
|
|
|
return results
|
|
\end{lstlisting}
|
|
|
|
\subsection{Anomaly Detection}
|
|
|
|
Verus integrates anomaly detection into the analytics pipeline:
|
|
|
|
\subsubsection{Statistical Anomalies}
|
|
|
|
We detect anomalies using seasonal decomposition:
|
|
|
|
\begin{equation}
|
|
y_t = T_t + S_t + R_t
|
|
\end{equation}
|
|
|
|
where $T_t$ is trend, $S_t$ is seasonal component, and $R_t$ is residual. Anomalies are residuals exceeding threshold:
|
|
|
|
\begin{equation}
|
|
|R_t| > k \cdot \sigma_R
|
|
\end{equation}
|
|
|
|
\begin{lstlisting}[language=Python,caption=Anomaly detection]
|
|
class AnomalyDetector:
|
|
def __init__(self, sensitivity=3.0):
|
|
self.k = sensitivity
|
|
|
|
def detect(self, series: pd.Series) -> List[Anomaly]:
|
|
# Seasonal decomposition
|
|
decomposition = seasonal_decompose(series, period=24*7) # Weekly
|
|
residual = decomposition.resid.dropna()
|
|
|
|
# Detect anomalies
|
|
threshold = self.k * residual.std()
|
|
anomalies = []
|
|
|
|
for timestamp, value in residual.items():
|
|
if abs(value) > threshold:
|
|
anomalies.append(Anomaly(
|
|
timestamp=timestamp,
|
|
expected=series[timestamp] - value,
|
|
actual=series[timestamp],
|
|
severity=abs(value) / threshold
|
|
))
|
|
|
|
return anomalies
|
|
\end{lstlisting}
|
|
|
|
\subsubsection{Alert Integration}
|
|
|
|
Anomalies trigger alerts through configurable channels:
|
|
|
|
\begin{lstlisting}[language=SQL,caption=Alert configuration]
|
|
CREATE ALERT order_drop AS
|
|
SELECT
|
|
store_id,
|
|
COUNT(*) AS order_count,
|
|
AVG(COUNT(*)) OVER (
|
|
PARTITION BY store_id
|
|
ORDER BY window_start
|
|
ROWS BETWEEN 168 PRECEDING AND 1 PRECEDING -- Last week
|
|
) AS expected
|
|
FROM mv_orders_by_hour
|
|
GROUP BY store_id, window_start
|
|
HAVING order_count < expected * 0.5 -- 50% drop
|
|
WITH NOTIFICATION slack_webhook, email;
|
|
\end{lstlisting}
|
|
|
|
\section{Evaluation}
|
|
|
|
\subsection{Experimental Setup}
|
|
|
|
We evaluated Verus on production workloads from three merchants:
|
|
|
|
\begin{itemize}
|
|
\item \textbf{Small}: 10K events/day, 5 materialized views.
|
|
\item \textbf{Medium}: 1M events/day, 15 materialized views.
|
|
\item \textbf{Large}: 100M events/day, 30 materialized views.
|
|
\end{itemize}
|
|
|
|
Infrastructure:
|
|
\begin{itemize}
|
|
\item Stream processing: 3-node Flink cluster.
|
|
\item State store: 3-node Redis cluster.
|
|
\item Columnar storage: S3 + 10-node Presto cluster.
|
|
\end{itemize}
|
|
|
|
\subsection{Ingestion Throughput}
|
|
|
|
\begin{table}[h]
|
|
\centering
|
|
\caption{Sustained ingestion throughput}
|
|
\label{tab:throughput}
|
|
\begin{tabular}{lr}
|
|
\hline
|
|
Configuration & Events/second \\
|
|
\hline
|
|
Single partition & 8,500 \\
|
|
10 partitions & 52,000 \\
|
|
100 partitions & 485,000 \\
|
|
\hline
|
|
\end{tabular}
|
|
\end{table}
|
|
|
|
Throughput scales linearly with partitions. The large merchant's peak load (50K events/second) is well within capacity.
|
|
|
|
\subsection{Query Latency}
|
|
|
|
\begin{table}[h]
|
|
\centering
|
|
\caption{Query latency by type (ms)}
|
|
\label{tab:query_latency}
|
|
\begin{tabular}{lrrr}
|
|
\hline
|
|
Query Type & p50 & p95 & p99 \\
|
|
\hline
|
|
Dashboard (Redis) & 18 & 45 & 82 \\
|
|
Dashboard (merged) & 125 & 280 & 520 \\
|
|
Ad-hoc (simple) & 850 & 2,100 & 4,500 \\
|
|
Ad-hoc (complex) & 3,200 & 8,500 & 15,000 \\
|
|
Cohort analysis & 5,500 & 12,000 & 25,000 \\
|
|
\hline
|
|
\end{tabular}
|
|
\end{table}
|
|
|
|
Dashboard queries served from Redis meet interactive requirements (< 100ms p99). Ad-hoc queries trade latency for flexibility.
|
|
|
|
\subsection{Freshness}
|
|
|
|
We measured end-to-end latency from event occurrence to query visibility:
|
|
|
|
\begin{table}[h]
|
|
\centering
|
|
\caption{Data freshness (seconds)}
|
|
\label{tab:freshness}
|
|
\begin{tabular}{lrrr}
|
|
\hline
|
|
Metric & p50 & p95 & p99 \\
|
|
\hline
|
|
Materialized views & 1.2 & 2.8 & 5.1 \\
|
|
Columnar storage & 45 & 120 & 180 \\
|
|
\hline
|
|
\end{tabular}
|
|
\end{table}
|
|
|
|
Materialized views update within seconds. Columnar storage has higher latency due to micro-batching for write efficiency.
|
|
|
|
\subsection{Cost Analysis}
|
|
|
|
\begin{table}[h]
|
|
\centering
|
|
\caption{Monthly infrastructure cost (USD)}
|
|
\label{tab:cost}
|
|
\begin{tabular}{lrrr}
|
|
\hline
|
|
Component & Small & Medium & Large \\
|
|
\hline
|
|
Kafka & 50 & 150 & 800 \\
|
|
Stream processing & 100 & 400 & 2,500 \\
|
|
Redis & 75 & 250 & 1,200 \\
|
|
Storage (S3) & 5 & 50 & 500 \\
|
|
Query engine & 200 & 600 & 3,000 \\
|
|
\hline
|
|
Total & 430 & 1,450 & 8,000 \\
|
|
\hline
|
|
\end{tabular}
|
|
\end{table}
|
|
|
|
Cost scales sub-linearly with volume due to shared infrastructure and compression benefits at scale.
|
|
|
|
\subsection{Comparison with Alternatives}
|
|
|
|
We compared Verus against pure-batch (BigQuery) and pure-stream (custom Flink) approaches:
|
|
|
|
\begin{table}[h]
|
|
\centering
|
|
\caption{Approach comparison}
|
|
\label{tab:comparison}
|
|
\begin{tabular}{lccc}
|
|
\hline
|
|
Metric & BigQuery & Flink-only & Verus \\
|
|
\hline
|
|
Dashboard latency & 2--5s & 20ms & 20ms \\
|
|
Ad-hoc latency & 2--10s & N/A & 1--15s \\
|
|
Freshness & 1--4 hours & < 1s & < 5s \\
|
|
Query flexibility & High & Low & High \\
|
|
Operational complexity & Low & High & Medium \\
|
|
\hline
|
|
\end{tabular}
|
|
\end{table}
|
|
|
|
Verus achieves the freshness of stream processing with the flexibility of batch analytics.
|
|
|
|
\section{Case Studies}
|
|
|
|
\subsection{Flash Sale Monitoring}
|
|
|
|
A fashion retailer used Verus to monitor a flash sale event:
|
|
|
|
\begin{itemize}
|
|
\item Real-time dashboard showed orders/minute, inventory levels, and error rates.
|
|
\item Anomaly detection alerted when payment failures spiked 3$\times$ normal.
|
|
\item Root cause analysis (ad-hoc query) identified a specific payment provider issue.
|
|
\item Response time from detection to mitigation: 4 minutes.
|
|
\end{itemize}
|
|
|
|
Without real-time analytics, the issue would have persisted for hours until batch reports surfaced it.
|
|
|
|
\subsection{Marketing Attribution}
|
|
|
|
An electronics retailer analyzed marketing effectiveness:
|
|
|
|
\begin{itemize}
|
|
\item Cohort analysis compared customers acquired through different channels.
|
|
\item Lifetime value calculation showed paid search customers had 2.3$\times$ higher LTV than social.
|
|
\item Budget reallocation based on insights increased ROAS by 45\%.
|
|
\end{itemize}
|
|
|
|
\section{Related Work}
|
|
|
|
Druid \cite{druid2014} pioneered real-time OLAP with column-oriented storage and bitmap indexes. Verus extends this with richer analytical operators (cohorts, funnels) and hybrid query routing.
|
|
|
|
ClickHouse \cite{clickhouse2016} offers excellent single-node performance for analytical queries. Verus provides horizontal scalability and stream processing integration.
|
|
|
|
The Lambda architecture \cite{marz2015} inspired our hybrid approach. We simplify operations by unifying the query interface and automating routing decisions.
|
|
|
|
\section{Conclusion}
|
|
|
|
Verus demonstrates that commerce analytics can achieve sub-second freshness without sacrificing query flexibility. The hybrid architecture—stream processing for materialized views, columnar storage for ad-hoc queries, intelligent routing between them—provides the best of both approaches.
|
|
|
|
Production deployments show the system handles 50,000+ events per second while serving dashboard queries in under 100ms. Merchants gain real-time visibility into their business without the operational complexity of maintaining separate batch and stream systems.
|
|
|
|
Future work will extend Verus with machine learning integration (anomaly detection models, forecasting) and support for streaming SQL standards (KSQL, Flink SQL).
|
|
|
|
\begin{thebibliography}{9}
|
|
|
|
\bibitem{marz2015}
|
|
N. Marz and J. Warren, \textit{Big Data: Principles and Best Practices of Scalable Real-Time Data Systems}, Manning, 2015.
|
|
|
|
\bibitem{druid2014}
|
|
F. Yang et al., ``Druid: A Real-time Analytical Data Store,'' \textit{SIGMOD}, 2014.
|
|
|
|
\bibitem{clickhouse2016}
|
|
A. Milovidov, ``ClickHouse: New Open Source Columnar Database,'' \textit{HighLoad++}, 2016.
|
|
|
|
\bibitem{kafka2011}
|
|
J. Kreps, N. Narkhede, and J. Rao, ``Kafka: A Distributed Messaging System for Log Processing,'' \textit{NetDB Workshop}, 2011.
|
|
|
|
\bibitem{flink2015}
|
|
P. Carbone et al., ``Apache Flink: Stream and Batch Processing in a Single Engine,'' \textit{IEEE Data Engineering Bulletin}, vol. 38, no. 4, 2015.
|
|
|
|
\bibitem{parquet2013}
|
|
J. LeFevre et al., ``Parquet,'' \textit{Apache Software Foundation}, 2013.
|
|
|
|
\end{thebibliography}
|
|
|
|
\end{document}
|