Kinesis, Firehose, Athena, Glue và họ analytics: clickstream của shop-app thành báo cáo
Tiếp đi: analytics trên AWS. Marketing muốn biết "khách xem sản phẩm nào rồi bỏ giỏ" và "sản phẩm nào được xem nhiều nhất mỗi giờ". Hiện shop-app chỉ có bảng orders trong RDS; mình định thêm bảng page_views và INSERT mỗi lần user xem sản phẩm. Có ổn không?
Ý tưởng tự nhiên, và cũng là sai lầm số một khi dev backend bắt đầu làm analytics: nhét dữ liệu phân tích vào database giao dịch. Bài này đưa clickstream của shop-app đi một đường riêng: app đẩy event → Kinesis → S3 → Athena query SQL. Đó cũng là "bộ ba" mà đề SAA hỏi nhiều nhất trong domain analytics. Phần còn lại của họ analytics (Redshift, EMR, OpenSearch, QuickSight, Lake Formation) chỉ cần nhớ từ khóa, ta để ở bảng cuối.
1. Bài toán: 2 câu hỏi của marketing
Câu hỏi Dữ liệu cần
1. Sản phẩm nào xem nhiều nhất mỗi giờ? mọi lượt xem (page_view) có productId, timestamp
2. Khách xem rồi bỏ giỏ (add_to_cart không checkout)? chuỗi event theo session: view → add_to_cart → (không) checkout
Quy mô: shop-app ~2,000 request/phút giờ cao điểm → ~1 triệu page_view/ngày, ~300 MB JSON/ngày
Nhu cầu đọc: báo cáo theo giờ/ngày, truy vấn ad-hoc, không cần real-time dưới 1 phút (trừ dashboard "đang hot")
2. Vấn đề của "thêm bảng page_views vào RDS"
- Ghi tốn tài nguyên DB giao dịch: 1 triệu INSERT/ngày cộng index, VACUUM (xem bài MVCC) ăn vào IOPS của
shop-dbvốn chỉ db.t3.micro; checkout chậm vì analytics. - Query phân tích quét toàn bảng (
GROUP BY product_id, date_trunc('hour', ts)) khóa buffer pool; row-store không hợp với aggregate trên hàng trăm triệu dòng. - Dung lượng và chi phí: lưu 2 năm clickstream trong RDS gp3 đắt hơn S3 nhiều lần, và backup RDS cũng phình theo.
- Mất event khi DB bận: nếu INSERT đồng bộ trong request và DB chậm, request của user chậm theo; nếu bất đồng bộ không có buffer, event rơi khi app restart.
- Khó mở rộng: mai muốn đưa thêm log ALB, event SQS
shop-order-eventsvào cùng chỗ để join thì RDS không phải nơi làm điều đó.
3. Giải pháp: tách đường "analytics" khỏi "transaction"
OLTP (giao dịch) OLAP / analytics
RDS shop-db ←── checkout S3 data lake ←── Firehose ←── Kinesis / app ←── page_view event
row store, index, ACID Parquet, partition theo ngày/giờ, rẻ, không giới hạn
query theo khóa, ms Athena/Redshift quét hàng tỷ dòng, giây → phút
Bốn dịch vụ chính và thuật ngữ đề thi:
| Dịch vụ | Vai trò | Thuật ngữ cần biết |
|---|---|---|
| Kinesis Data Streams | Stream real-time, nhiều consumer đọc lại được, giữ dữ liệu 24 giờ (mặc định) đến 365 ngày | shard (1 MB/s in, 2 MB/s out, 1,000 record/s), partition key, sequence number, on-demand vs provisioned capacity, enhanced fan-out (2 MB/s riêng mỗi consumer, push), KCL (Kinesis Client Library), resharding, ProvisionedThroughputExceededException = hot shard |
| Amazon Data Firehose (tên cũ Kinesis Data Firehose) | Giao hàng gần real-time, fully managed, không có shard, buffer rồi ghi vào S3 / Redshift / OpenSearch / HTTP endpoint (Datadog, Splunk) | buffer size (1–128 MB) & interval (0–900 s), near real-time (không phải real-time), transform bằng Lambda, convert JSON→Parquet/ORC, dynamic partitioning, không lưu dữ liệu (không replay), tự scale |
| AWS Glue | Serverless ETL + Data Catalog (metastore Hive-compatible dùng chung cho Athena, EMR, Redshift Spectrum) | crawler (quét S3 suy ra schema), database/table trong catalog, Glue job (Spark/Python shell), Glue Studio, bookmark (chỉ xử lý dữ liệu mới), DataBrew (no-code), Glue Data Quality |
| Amazon Athena | SQL serverless trực tiếp trên S3 (engine Trino), trả tiền theo TB scan | partition, columnar (Parquet/ORC), compression, partition projection, workgroup (giới hạn chi phí), federated query (query RDS/DynamoDB), CTAS, result bucket |
Nguyên tắc tiết kiệm Athena mà đề luôn hỏi: Parquet + partition + compression giảm TB scan hàng chục lần → giảm tiền hàng chục lần.
Kinesis Data Streams vs Firehose (phân biệt số 1 trong đề):
Cần real-time (ms), nhiều app cùng đọc, replay, tự viết consumer (KCL, Lambda, Flink) → Data Streams
Chỉ cần "đổ vào S3/Redshift/OpenSearch" gần real-time, không muốn vận hành, không replay → Firehose
Cần cả hai: Data Streams làm nguồn → Firehose làm một consumer đổ vào S3
Cho shop-app: real-time chưa cần, consumer duy nhất là "đổ vào S3" → app gửi thẳng vào Firehose bằng SDK PutRecord. Nếu sau này cần dashboard "đang hot" real-time hoặc fraud detection, chèn Data Streams vào trước.
4. Cấu hình và code từng bước
Bước 1: Firehose stream shop-clickstream → S3, buffer 5 MB / 60 giây, nén GZIP, partition theo giờ (bước nâng cao: convert Parquet qua Glue schema ở Lab).
aws firehose create-delivery-stream --profile shop-lab \
--delivery-stream-name shop-clickstream --delivery-stream-type DirectPut \
--extended-s3-destination-configuration '{
"RoleARN": "arn:aws:iam::'$ACCOUNT_ID':role/shop-firehose-role",
"BucketARN": "arn:aws:s3:::shop-app-logs-'$ACCOUNT_ID'",
"Prefix": "clickstream/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/",
"ErrorOutputPrefix": "clickstream-errors/!{firehose:error-output-type}/",
"BufferingHints": { "SizeInMBs": 5, "IntervalInSeconds": 60 },
"CompressionFormat": "GZIP"
}' --tags Key=Project,Value=shop-app Key=Env,Value=lab
Prefix dạng year=…/month=… là Hive-style partition: Athena hiểu ngay và chỉ đọc thư mục cần.
Bước 2: Spring Boot gửi event bằng SDK v2. Không gửi đồng bộ trong request; gom vào buffer và đẩy batch bằng PutRecordBatch (tối đa 500 record / 4 MB một lần).
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>firehose</artifactId>
</dependency>
public record PageViewEvent(String eventType, String sessionId, Long userId, long productId,
String category, Instant ts, String userAgent) {}
@Component
public class ClickstreamPublisher {
private static final Logger log = LoggerFactory.getLogger(ClickstreamPublisher.class);
private final FirehoseClient firehose = FirehoseClient.builder()
.region(Region.AP_SOUTHEAST_1).build(); // credential: role EC2/ECS qua default chain
private final ObjectMapper mapper;
private final BlockingQueue<PageViewEvent> queue = new LinkedBlockingQueue<>(10_000);
private final String streamName;
public ClickstreamPublisher(ObjectMapper mapper, @Value("${shop.clickstream.stream}") String streamName) {
this.mapper = mapper; this.streamName = streamName;
}
/** gọi từ controller: không block request */
public void publish(PageViewEvent e) {
if (!queue.offer(e)) log.warn("clickstream buffer full, dropping event"); // analytics được phép rơi, checkout thì không
}
@Scheduled(fixedDelay = 1000)
void flush() {
List<PageViewEvent> batch = new ArrayList<>(500);
queue.drainTo(batch, 500);
if (batch.isEmpty()) return;
List<Record> records = batch.stream()
.map(e -> Record.builder().data(SdkBytes.fromUtf8String(toJsonLine(e))).build())
.toList();
PutRecordBatchResponse res = firehose.putRecordBatch(
PutRecordBatchRequest.builder().deliveryStreamName(streamName).records(records).build());
if (res.failedPutCount() > 0) {
// retry những record lỗi (throttling) – đưa lại queue
for (int i = 0; i < res.requestResponses().size(); i++)
if (res.requestResponses().get(i).errorCode() != null) queue.offer(batch.get(i));
log.warn("firehose failed {} records", res.failedPutCount());
}
}
private String toJsonLine(PageViewEvent e) {
try { return mapper.writeValueAsString(e) + "\n"; } // mỗi record một dòng → Athena đọc JSON Lines
catch (JsonProcessingException ex) { throw new UncheckedIOException(ex); }
}
}
Ký tự \n cuối record là chi tiết hay quên: Firehose chỉ nối byte lại, không tự thêm xuống dòng; thiếu nó file S3 sẽ là một dòng JSON dính liền và Athena đọc sai.
@GetMapping("/products/{id}")
public ProductDto view(@PathVariable long id, HttpSession session, HttpServletRequest req,
@AuthenticationPrincipal(expression = "id") Long userId) {
ProductDto p = productService.get(id);
clickstream.publish(new PageViewEvent("page_view", session.getId(), userId, id,
p.category(), Instant.now(), req.getHeader("User-Agent")));
return p;
}
Role shop-app-ec2-role cần firehose:PutRecord, firehose:PutRecordBatch trên ARN stream này. Nếu đã dùng Spring Cloud AWS, có thể dùng KinesisTemplate (cho Data Streams); Firehose thì SDK trực tiếp như trên là gọn nhất.
Bước 3: Glue crawler tạo bảng. Crawler quét s3://shop-app-logs-…/clickstream/, suy ra schema JSON, nhận ra partition year/month/day/hour, tạo table clickstream trong database shop_analytics.
aws glue create-database --profile shop-lab --database-input '{"Name":"shop_analytics"}'
aws glue create-crawler --profile shop-lab --name shop-clickstream-crawler \
--role arn:aws:iam::$ACCOUNT_ID:role/shop-glue-role --database-name shop_analytics \
--targets '{"S3Targets":[{"Path":"s3://shop-app-logs-'$ACCOUNT_ID'/clickstream/"}]}' \
--schedule "cron(5 * * * ? *)" \
--tags Project=shop-app,Env=lab
aws glue start-crawler --profile shop-lab --name shop-clickstream-crawler
Schedule mỗi giờ để thêm partition mới. Cách rẻ hơn và không cần crawler: partition projection trong Athena (khai báo pattern của partition, Athena tự suy ra, không cần metadata).
Bước 4: Athena trả lời 2 câu hỏi của marketing.
-- Câu 1: top sản phẩm theo giờ, chỉ quét partition hôm nay
SELECT hour, productid, count(*) AS views
FROM shop_analytics.clickstream
WHERE year = '2026' AND month = '10' AND day = '21' AND eventtype = 'page_view'
GROUP BY hour, productid
ORDER BY hour, views DESC;
-- Câu 2: session có add_to_cart mà không checkout trong ngày
WITH s AS (
SELECT sessionid,
max(CASE WHEN eventtype = 'add_to_cart' THEN 1 ELSE 0 END) AS added,
max(CASE WHEN eventtype = 'checkout' THEN 1 ELSE 0 END) AS bought
FROM shop_analytics.clickstream
WHERE year = '2026' AND month = '10' AND day = '21'
GROUP BY sessionid)
SELECT count(*) AS abandoned_sessions, round(100.0 * count(*) / (SELECT count(*) FROM s WHERE added = 1), 1) AS abandon_pct
FROM s WHERE added = 1 AND bought = 0;
Chạy bằng CLI:
QID=$(aws athena start-query-execution --profile shop-lab \
--query-string "SELECT eventtype, count(*) FROM shop_analytics.clickstream WHERE year='2026' AND month='10' GROUP BY eventtype" \
--work-group shop-analytics \
--result-configuration OutputLocation=s3://shop-app-logs-$ACCOUNT_ID/athena-results/ \
--query QueryExecutionId --output text)
aws athena get-query-results --profile shop-lab --query-execution-id $QID --query 'ResultSet.Rows[].Data[].VarCharValue'
aws athena get-query-execution --profile shop-lab --query-execution-id $QID --query 'QueryExecution.Statistics.DataScannedInBytes'
Bước 5: Chuyển sang Parquet để rẻ hơn 10×. Hai cách: (a) Firehose record format conversion JSON → Parquet dùng schema từ Glue table (bật ở Lab); (b) Athena CTAS định kỳ chuyển dữ liệu cũ:
CREATE TABLE shop_analytics.clickstream_parquet
WITH (format = 'PARQUET', parquet_compression = 'SNAPPY',
external_location = 's3://shop-app-logs-ACCOUNT/clickstream-parquet/',
partitioned_by = ARRAY['day'])
AS SELECT eventtype, sessionid, userid, productid, category, ts, useragent,
concat(year, '-', month, '-', day) AS day
FROM shop_analytics.clickstream;
Cột-store chỉ đọc cột trong SELECT, nén tốt; query "top productId" chỉ đụng 2 cột thay vì cả JSON. Đây là nội dung câu "reduce Athena cost" trong đề.
5. Cách hoạt động
shop-app (EC2/ECS) Amazon Data Firehose S3 shop-app-logs-$ACCOUNT_ID
┌──────────────────┐ PutRecordBatch ┌─────────────────────────┐ PUT ┌───────────────────────────────────┐
│ controller │ ────────────────► │ shop-clickstream │ ──────► │ clickstream/year=2026/month=10/ │
│ └ publish() │ ≤500 rec/4 MB │ buffer 5 MB | 60 s │ gzip │ day=21/hour=09/shop-clickstream-│
│ BlockingQueue │ │ (Lambda transform) │ │ 1-2026-10-21-09-00-12-xxxx.gz │
│ └ flush() 1 s │ │ (→ Parquet qua Glue) │ │ clickstream-errors/… │
└──────────────────┘ └─────────────────────────┘ └──────────────┬────────────────────┘
│ quét schema, partition
▼
Glue Data Catalog
db shop_analytics · table clickstream
partitions (year,month,day,hour)
│ metadata
▼
Marketing ──SQL──► Athena workgroup shop-analytics ──đọc partition cần──► S3 ──kết quả──► athena-results/
(trả tiền theo TB scan) │
QuickSight dashboard ◄──────────── SPICE / direct query ───────────────────────────────────┘
Dòng thời gian một event: user xem sản phẩm 09:00:12 → vào queue → 09:00:13 PutRecordBatch → Firehose giữ trong buffer → 09:01:12 (interval 60 s) ghi file .gz vào hour=09/ → 10:05 crawler thêm partition hour=09 (hoặc partition projection khỏi chờ) → query thấy ngay. Độ trễ end-to-end 1–65 phút tùy crawler: đó là nghĩa của "near real-time".
6. So sánh kiểu đề thi
| Tiêu chí | Kinesis Data Streams | Firehose | SQS | MSK (Kafka) |
|---|---|---|---|---|
| Mô hình | Stream, nhiều consumer đọc độc lập, replay | Delivery pipeline tới đích | Queue, 1 consumer xử lý 1 message | Stream, Kafka API |
| Giữ dữ liệu | 24 giờ → 365 ngày | Không (buffer rồi giao) | Tối đa 14 ngày, xóa khi ack | Theo cấu hình (ngày → vô hạn) |
| Thứ tự | Theo shard (partition key) | Không đảm bảo | FIFO queue có | Theo partition |
| Scale | Shard (provisioned) hoặc on-demand | Tự động | Tự động | Broker/partition tự quản |
| Độ trễ | ~200 ms (70 ms với enhanced fan-out) | ≥ 60 s (hoặc 0 s buffer với một số đích) | ms | ms |
| Dùng khi | Real-time analytics, nhiều consumer, replay, Flink | Đổ vào S3/Redshift/OpenSearch/Splunk | Decouple job, task queue | Đã dùng Kafka, cần Kafka ecosystem (8.6) |
| Tiêu chí | Athena | Redshift | EMR | OpenSearch |
|---|---|---|---|---|
| Bản chất | SQL serverless trên S3 | Data warehouse (cột, MPP), cluster hoặc Serverless | Cluster Spark/Hadoop/Hive/Presto managed | Search & log analytics (fork Elasticsearch), Kibana → OpenSearch Dashboards |
| Dữ liệu ở | S3 (và federated) | Load vào Redshift (COPY) hoặc Spectrum query S3 | HDFS/S3 (EMRFS) | Index trong cluster |
| Trả tiền | TB scan | Node-giờ (RA3) / RPU-giờ (Serverless) | Instance-giờ (Spot rẻ), EMR Serverless theo vCPU-giờ | Instance-giờ, Serverless OCU |
| Từ khóa đề | "ad-hoc", "serverless", "query logs in S3", "pay per query", "no infrastructure" | "data warehouse", "BI complex joins", "petabyte", "fast repeated queries", "Spectrum", "concurrency scaling" | "Spark", "Hadoop", "custom big data framework", "Hive", "machine learning at scale", "Spot for task nodes" | "full-text search", "log analytics dashboards", "near real-time search", "Kibana/OpenSearch Dashboards" |
| Vận hành | Không | Trung bình (Serverless: thấp) | Cao (Serverless: thấp) | Trung bình |
Quy tắc chọn trong đề: ad-hoc + S3 + rẻ + không vận hành → Athena; warehouse, BI nặng, join phức tạp lặp lại → Redshift; phải chạy Spark/Hadoop → EMR; tìm kiếm text/log dashboards → OpenSearch.
Bảng "từ khóa đề → dịch vụ" cho phần còn lại (không cần lab, chỉ cần nhận ra):
| Từ khóa trong đề | Dịch vụ | Ghi nhớ |
|---|---|---|
| "BI dashboards", "visualize", "embed charts", "ML insights", "pay-per-session" | QuickSight | Serverless BI; SPICE là engine in-memory; nguồn Athena/Redshift/RDS/S3 |
| "centralized data lake permissions", "column/row-level security across Athena/Redshift/EMR", "fine-grained access to S3 data" | Lake Formation | Lớp quyền trên Glue Catalog; blueprint ingest; thay bucket policy + IAM rối |
| "transform streaming data with SQL/Java in real time", "windowed aggregation on Kinesis", "Flink" | Managed Service for Apache Flink (tên cũ Kinesis Data Analytics) | Real-time ETL/aggregation; Firehose chỉ transform đơn giản qua Lambda |
| "video streams from cameras", "ML on video" | Kinesis Video Streams | Ít ra đề |
| "existing Kafka applications", "Kafka API compatibility", "Kafka Connect" | Amazon MSK (Managed Streaming for Kafka), MSK Serverless | So với Kinesis: MSK giữ dữ liệu lâu hơn, message tới 1 MB (cấu hình lớn hơn), Kafka tooling |
| "query S3 data from Redshift without loading", "exabyte" | Redshift Spectrum | Dùng Glue Catalog, cluster Redshift vẫn cần |
| "query across RDS, DynamoDB, S3 in one SQL" | Athena Federated Query | Connector Lambda |
| "catalog metadata", "schema discovery", "Hive metastore" | Glue Data Catalog + crawler | Dùng chung Athena/EMR/Redshift Spectrum/Lake Formation |
| "serverless ETL", "Spark job without cluster", "process only new data" | Glue ETL job (+ job bookmark) | EMR nếu cần kiểm soát cluster/thư viện |
| "clean data visually without code", "data preparation by analysts" | Glue DataBrew | No-code |
| "big data on Spot", "Hadoop ecosystem", "HBase", "Presto on own cluster" | EMR (master / core / task node; task node = Spot) | Core node giữ HDFS → không Spot; task node Spot |
| "search product catalog", "autocomplete", "log analytics with dashboards" | OpenSearch Service | Chi tiết Elasticsearch ở 19.7; shop-app tìm sản phẩm ở bài full-text search |
| "SQL on S3 object inside Lambda/app, filter subset of one object" | S3 Select (đang bị hạn chế, ít ra đề mới) | Dùng Athena thay |
| "exchange / subscribe third-party datasets" | AWS Data Exchange | Ít ra đề |
| "move data between AWS services on schedule" (cũ) | Data Pipeline (đã legacy) → dùng Glue / Step Functions / MWAA (Airflow) | Nhận ra là đáp án sai |
7. Khi nào dùng / không dùng
- Dùng Firehose → S3 → Athena khi: cần báo cáo theo giờ/ngày, dữ liệu event/log, team không có người vận hành cluster. Chi phí gần 0 lúc không query. Đây là "stack analytics mặc định" cho startup và cho shop-app.
- Thêm Kinesis Data Streams trước Firehose khi: cần consumer thứ hai real-time (Lambda phát hiện gian lận, Flink tính "đang hot" theo cửa sổ 1 phút), hoặc cần replay.
- Chuyển lên Redshift khi: nhiều analyst query liên tục, join nhiều bảng, dashboard cần giây; Athena bắt đầu tốn hơn vì quét lặp lại cùng dữ liệu. Redshift Serverless là đường ít đau nhất.
- Không dùng Kinesis cho message cần xử lý từng cái một với ack (đơn hàng) → đó là SQS (22.11). Không dùng Firehose làm "queue" vì không replay.
- Không dùng RDS cho clickstream, như đã phân tích. Nhưng dữ liệu đơn hàng trong RDS vẫn cần vào data lake để join với clickstream: dùng DMS CDC sang S3 hoặc Athena Federated Query (22.18 nói về DMS).
Mình xem bucket thấy mỗi giờ Firehose đẻ ra 60 file gzip vài trăm KB. Nghe nói Athena sợ "small files". Có đúng không, và nên chỉnh buffer thế nào? Với lại partition theo hour có quá nhỏ không?
Đúng, và đây là chủ đề mà người làm data lake nào cũng va vào trong tháng đầu. Athena (và mọi engine trên S3) phải mở từng object: mỗi file là một request GetObject, list partition là ListObjects. Hàng chục nghìn file 200 KB chậm hơn nhiều so với vài trăm file 128 MB dù tổng byte bằng nhau, vì chi phí là số lần mở file chứ không chỉ byte quét.
1. Kích cỡ file lý tưởng
Mục tiêu: 128 MB – 1 GB mỗi file, Parquet/ORC, Snappy hoặc ZSTD
shop-app: ~300 MB JSON/ngày ≈ 30 MB Parquet/ngày → 1 file/ngày là đủ!
→ partition theo hour là quá nhỏ cho quy mô này; theo day là hợp lý
→ khi lên 100× (30 GB/ngày) thì mới cần hour
Quy tắc chọn partition: mỗi partition nên có ≥ 100 MB dữ liệu và câu query hay dùng phải lọc được trên cột đó. Partition quá mịn làm số file nổ và crawler/metadata chậm; quá thô làm quét thừa.
2. Chỉnh Firehose
- Buffer tối đa 128 MB / 900 giây. Với 300 MB/ngày (~12 MB/giờ), đặt
SizeInMBs=128, IntervalInSeconds=900→ ~4 file/giờ, vẫn nhiều file nhỏ nhưng chấp nhận được cho dữ liệu "nóng" hôm nay. - Bật record format conversion sang Parquet ngay ở Firehose (cần Glue table làm schema): file nhỏ hơn 5–10×, Athena đọc cột.
- Dynamic partitioning nếu muốn partition theo field trong record (ví dụ
category), không chỉ theo timestamp.
3. Compaction cho dữ liệu cũ
Dữ liệu hôm nay chấp nhận file nhỏ để tươi; dữ liệu từ hôm qua trở về trước gom lại. Cách rẻ nhất là Athena CTAS/INSERT INTO chạy hàng ngày (qua EventBridge Scheduler → Lambda hoặc Step Functions):
INSERT INTO shop_analytics.clickstream_parquet
SELECT eventtype, sessionid, userid, productid, category, ts, useragent, '2026-10-20' AS day
FROM shop_analytics.clickstream
WHERE year='2026' AND month='10' AND day='20';
-- rồi lifecycle S3 xóa/đưa clickstream/.../day=20/ (JSON thô) sang Glacier sau 7 ngày
Cách "chuẩn hơn" khi dữ liệu lớn: Glue job Spark với bookmark, hoặc table format Apache Iceberg (Athena hỗ trợ) có OPTIMIZE table REWRITE DATA tự compaction và cho UPDATE/DELETE trên S3 (cần cho GDPR "xóa dữ liệu user X").
4. Kiểm tra bằng số
aws athena get-query-execution --query-execution-id $QID \
--query 'QueryExecution.Statistics.[DataScannedInBytes,TotalExecutionTimeInMillis]'
# So sánh cùng query trên clickstream (JSON gz) và clickstream_parquet: thường thấy byte giảm 10–20×, thời gian giảm 3–5×
Workgroup có thể đặt per-query data scan limit (ví dụ 10 GB) để một câu SELECT * quên WHERE không đốt tiền.
Team mình đã dùng Kafka ở chương Messaging. Lên AWS, Kinesis Data Streams và Amazon MSK khác nhau thế nào về shard/partition, giữ dữ liệu và chi phí? Nếu đã có code Kafka producer/consumer thì chọn gì?
Bạn hiểu đúng rằng chúng cùng một họ "partitioned log" (8.6): Kinesis shard tương ứng Kafka partition, partition key tương ứng message key, sequence number tương ứng offset, KCL tương ứng consumer group. Khác biệt nằm ở ba chỗ: giới hạn cứng, mô hình giá, và API.
| Tiêu chí | Kinesis Data Streams | Amazon MSK |
|---|---|---|
| Đơn vị scale | Shard: 1 MB/s ghi, 2 MB/s đọc (chia cho mọi consumer, trừ enhanced fan-out), 1,000 record/s. On-demand mode tự scale tới 200 MB/s ghi mặc định | Partition trên broker; throughput theo broker type và số broker; MSK Serverless tự scale |
| Kích cỡ record | 1 MB | 1 MB mặc định, cấu hình lớn hơn |
| Giữ dữ liệu | 24 giờ mặc định, tối đa 365 ngày (tốn thêm) | Tùy, có tiered storage gần vô hạn |
| Đọc lại | Có (trim horizon / at timestamp) | Có (offset) |
| API / client | AWS SDK, KCL, KPL, Lambda event source, Flink | Kafka client bất kỳ (Spring Kafka giữ nguyên), Kafka Connect, Schema Registry (Glue) |
| Vận hành | Gần 0; resharding thủ công ở provisioned | AWS lo broker/ZooKeeper-KRaft/patch; bạn lo partition, topic config, consumer lag |
| Giá | Shard-giờ + PUT payload unit (25 KB) hoặc on-demand theo GB; rẻ cho lưu lượng nhỏ | Broker-giờ + storage (chạy 24/7 dù không có traffic); Serverless theo cluster-giờ + GB |
| Tích hợp AWS | Sâu: Firehose, Lambda, Flink, EventBridge Pipes | Lambda event source, Firehose nguồn MSK, Flink |
Chọn thế nào
Đã có code Kafka, cần Kafka Streams/Connect/exactly-once transaction, giữ dữ liệu dài → MSK (hoặc MSK Serverless)
Lưu lượng nhỏ–vừa, muốn serverless thật, tích hợp Lambda/Firehose, không có đội Kafka → Kinesis Data Streams
Chỉ cần đổ vào S3/OpenSearch → Firehose (không cần cả hai)
Đề thi nói "existing Kafka applications / minimal code change" → MSK
Đề thi nói "real-time", "multiple consumers", "shards", "Lambda" → Kinesis
Lưu ý kỹ thuật khi viết consumer Kinesis bằng Java: KCL 2.x cần một bảng DynamoDB để lưu checkpoint (giống __consumer_offsets), và mỗi shard chỉ nên có một worker; ProvisionedThroughputExceededException là dấu hiệu hot shard do partition key lệch (ví dụ dùng category làm key khi 70% lượt xem là "điện thoại") – sửa bằng key phân tán hơn (sessionId) hoặc on-demand mode.
Cho mình bài lab để tự tay làm phần này nhé.
Lab 22.17: Clickstream shop-app → Firehose → S3 → Glue → Athena
Mục tiêu
- Tạo Firehose stream
shop-clickstreamghi GZIP JSON vàos3://shop-app-logs-$ACCOUNT_ID/clickstream/với partition Hive-style theo giờ. - Gửi ≥ 2,000 event từ một chương trình Java (hoặc CLI) và thấy file xuất hiện trong S3 sau ≤ 60 giây.
- Glue crawler tạo table
shop_analytics.clickstreamvới 4 partition key; Athena trả lời "top 5 sản phẩm" và "tỉ lệ bỏ giỏ". - Chuyển sang Parquet bằng CTAS và đo
DataScannedInBytesgiảm.
Chi phí & thời gian
- Thời gian: 60–75 phút.
- Ngoài Free Tier: Firehose tính theo GB ingest (khoảng vài cent/GB, lab < 1 MB), Glue crawler tính theo DPU-giờ (tối thiểu 10 phút, khoảng 0.07–0.1 USD/lần chạy), Athena khoảng 5 USD/TB scan (lab quét vài MB ≈ 0). S3 trong Free Tier. Tổng lab < 0.5 USD nếu chạy crawler 2–3 lần.
- Không cần EC2/ALB: gửi event từ máy local bằng profile
shop-lab.
Chuẩn bị
- Bucket
shop-app-logs-$ACCOUNT_ID(lab 22.6). Java 21 + Maven trên máy local nếu chạy producer Java (có phương án CLI thay thế).
export AWS_REGION=ap-southeast-1
export ACCOUNT_ID=$(aws sts get-caller-identity --profile shop-lab --query Account --output text)
export BUCKET=shop-app-logs-$ACCOUNT_ID
Các bước
Bước 1: Role cho Firehose ghi S3.
cat > fh-trust.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"firehose.amazonaws.com"},
"Action":"sts:AssumeRole","Condition":{"StringEquals":{"sts:ExternalId":"$ACCOUNT_ID"}}}]}
EOF
aws iam create-role --profile shop-lab --role-name shop-firehose-role --assume-role-policy-document file://fh-trust.json \
--tags Key=Project,Value=shop-app Key=Env,Value=lab
cat > fh-policy.json <<EOF
{"Version":"2012-10-17","Statement":[
{"Effect":"Allow","Action":["s3:AbortMultipartUpload","s3:GetBucketLocation","s3:GetObject","s3:ListBucket",
"s3:ListBucketMultipartUploads","s3:PutObject"],
"Resource":["arn:aws:s3:::$BUCKET","arn:aws:s3:::$BUCKET/*"]},
{"Effect":"Allow","Action":["glue:GetTable","glue:GetTableVersion","glue:GetTableVersions"],"Resource":"*"},
{"Effect":"Allow","Action":["logs:PutLogEvents"],"Resource":"arn:aws:logs:$AWS_REGION:$ACCOUNT_ID:log-group:/aws/kinesisfirehose/*"}]}
EOF
aws iam put-role-policy --profile shop-lab --role-name shop-firehose-role --policy-name s3-write --policy-document file://fh-policy.json
sleep 10 # IAM eventual consistency
Console: IAM → Roles → Create role → Custom trust policy. (Console Firehose cũng tự tạo role nếu bạn tạo stream bằng Console.)
Bước 2: Tạo Firehose stream.
aws firehose create-delivery-stream --profile shop-lab \
--delivery-stream-name shop-clickstream --delivery-stream-type DirectPut \
--extended-s3-destination-configuration "{
\"RoleARN\": \"arn:aws:iam::$ACCOUNT_ID:role/shop-firehose-role\",
\"BucketARN\": \"arn:aws:s3:::$BUCKET\",
\"Prefix\": \"clickstream/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/hour=!{timestamp:HH}/\",
\"ErrorOutputPrefix\": \"clickstream-errors/!{firehose:error-output-type}/!{timestamp:yyyy-MM-dd}/\",
\"BufferingHints\": { \"SizeInMBs\": 1, \"IntervalInSeconds\": 60 },
\"CompressionFormat\": \"GZIP\",
\"CloudWatchLoggingOptions\": { \"Enabled\": true, \"LogGroupName\": \"/aws/kinesisfirehose/shop-clickstream\", \"LogStreamName\": \"S3Delivery\" }
}" --tags Key=Project,Value=shop-app Key=Env,Value=lab
aws logs create-log-group --profile shop-lab --log-group-name /aws/kinesisfirehose/shop-clickstream 2>/dev/null
aws logs create-log-stream --profile shop-lab --log-group-name /aws/kinesisfirehose/shop-clickstream --log-stream-name S3Delivery 2>/dev/null
aws firehose describe-delivery-stream --profile shop-lab --delivery-stream-name shop-clickstream \
--query 'DeliveryStreamDescription.DeliveryStreamStatus' # CREATING → ACTIVE (~1 phút)
Console: Amazon Data Firehose → Create Firehose stream → Source: Direct PUT, Destination: S3 → Bucket prefix / Buffer hints / Compression.
Buffer 1 MB / 60 s chỉ để lab thấy kết quả nhanh; prod đặt 64–128 MB / 300–900 s như câu hỏi phụ đã bàn.
Bước 3: Gửi event giả lập. Cách A – Java (project nhỏ clickstream-producer, một file):
// pom: software.amazon.awssdk:firehose (dùng BOM software.amazon.awssdk:bom) + com.fasterxml.jackson.core:jackson-databind
public class ClickstreamProducer {
record Event(String eventType, String sessionId, Long userId, long productId, String category, String ts, String userAgent) {}
static final String[] TYPES = {"page_view","page_view","page_view","page_view","add_to_cart","checkout"};
static final String[] CATS = {"phone","laptop","audio","accessory"};
public static void main(String[] args) throws Exception {
var mapper = new ObjectMapper();
var rnd = new Random();
try (var fh = FirehoseClient.builder().region(Region.AP_SOUTHEAST_1)
.credentialsProvider(ProfileCredentialsProvider.create("shop-lab")).build()) {
for (int batch = 0; batch < 10; batch++) {
List<Record> records = new ArrayList<>();
for (int i = 0; i < 200; i++) {
long productId = 1 + (long) Math.floor(Math.abs(rnd.nextGaussian()) * 20) % 100; // lệch: vài sản phẩm hot
var e = new Event(TYPES[rnd.nextInt(TYPES.length)], "s-" + rnd.nextInt(400),
rnd.nextBoolean() ? (long) rnd.nextInt(1000) : null, productId,
CATS[(int) (productId % CATS.length)], Instant.now().toString(), "lab-producer/1.0");
records.add(Record.builder().data(SdkBytes.fromUtf8String(mapper.writeValueAsString(e) + "\n")).build());
}
var res = fh.putRecordBatch(b -> b.deliveryStreamName("shop-clickstream").records(records));
System.out.printf("batch %d sent, failed=%d%n", batch, res.failedPutCount());
}
}
}
}
Cách B – CLI (chậm hơn, đủ cho lab): 300 record lẻ bằng put-record.
for i in $(seq 1 300); do
T=$([ $((i % 6)) -eq 0 ] && echo add_to_cart || ([ $((i % 13)) -eq 0 ] && echo checkout || echo page_view))
P=$(( (RANDOM % 7) * (RANDOM % 7) % 50 + 1 ))
REC="{\"eventType\":\"$T\",\"sessionId\":\"s-$((RANDOM % 80))\",\"userId\":$((RANDOM % 500)),\"productId\":$P,\"category\":\"cat-$((P % 4))\",\"ts\":\"$(date -u +%FT%TZ)\",\"userAgent\":\"cli\"}"
aws firehose put-record --profile shop-lab --delivery-stream-name shop-clickstream \
--record "{\"Data\":\"$(printf '%s\n' "$REC" | base64 -w0)\"}" --query RecordId --output text >/dev/null
done; echo done
Lưu ý CLI: Data phải base64 (CLI v2 với --cli-binary-format raw-in-base64-out cũng được), và vẫn phải có \n cuối mỗi record.
Bước 4: Chờ ≤ 60 giây, xem file trong S3.
sleep 70
aws s3 ls s3://$BUCKET/clickstream/ --recursive --profile shop-lab --human-readable | tail -5
# clickstream/year=2026/month=10/day=21/hour=03/shop-clickstream-1-2026-10-21-03-12-45-....gz
aws s3 cp s3://$BUCKET/$(aws s3 ls s3://$BUCKET/clickstream/ --recursive --profile shop-lab | tail -1 | awk '{print $4}') - --profile shop-lab | gunzip | head -3
Console: S3 → bucket → clickstream/ → vào từng cấp year/month/day/hour.
Bước 5: Glue database + crawler.
cat > glue-trust.json <<'EOF'
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"glue.amazonaws.com"},"Action":"sts:AssumeRole"}]}
EOF
aws iam create-role --profile shop-lab --role-name shop-glue-role --assume-role-policy-document file://glue-trust.json \
--tags Key=Project,Value=shop-app Key=Env,Value=lab
aws iam attach-role-policy --profile shop-lab --role-name shop-glue-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole
aws iam put-role-policy --profile shop-lab --role-name shop-glue-role --policy-name s3-read --policy-document "{
\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetObject\",\"s3:ListBucket\"],
\"Resource\":[\"arn:aws:s3:::$BUCKET\",\"arn:aws:s3:::$BUCKET/*\"]}]}"
sleep 10
aws glue create-database --profile shop-lab --database-input '{"Name":"shop_analytics","Description":"shop-app analytics lab"}'
aws glue create-crawler --profile shop-lab --name shop-clickstream-crawler \
--role arn:aws:iam::$ACCOUNT_ID:role/shop-glue-role --database-name shop_analytics \
--targets "{\"S3Targets\":[{\"Path\":\"s3://$BUCKET/clickstream/\"}]}" \
--configuration '{"Version":1.0,"Grouping":{"TableGroupingPolicy":"CombineCompatibleSchemas"}}' \
--tags Project=shop-app,Env=lab
aws glue start-crawler --profile shop-lab --name shop-clickstream-crawler
# chờ 1–3 phút
aws glue get-crawler --profile shop-lab --name shop-clickstream-crawler --query 'Crawler.[State,LastCrawl.Status]'
aws glue get-table --profile shop-lab --database-name shop_analytics --name clickstream \
--query 'Table.[StorageDescriptor.Columns[].Name, PartitionKeys[].Name]'
# [["eventtype","sessionid","userid","productid","category","ts","useragent"],["year","month","day","hour"]]
Console: AWS Glue → Crawlers → Create crawler → Data source S3 path → IAM role → Target database → Run.
Bước 6: Athena workgroup có giới hạn scan, rồi query.
aws athena create-work-group --profile shop-lab --name shop-analytics \
--configuration "ResultConfiguration={OutputLocation=s3://$BUCKET/athena-results/},EnforceWorkGroupConfiguration=true,BytesScannedCutoffPerQuery=1073741824" \
--tags Key=Project,Value=shop-app Key=Env,Value=lab
q() { # helper: chạy query, chờ, in kết quả + byte scan
local QID=$(aws athena start-query-execution --profile shop-lab --work-group shop-analytics \
--query-execution-context Database=shop_analytics --query-string "$1" --query QueryExecutionId --output text)
while [ "$(aws athena get-query-execution --profile shop-lab --query-execution-id $QID --query QueryExecution.Status.State --output text)" = "RUNNING" ]; do sleep 2; done
aws athena get-query-results --profile shop-lab --query-execution-id $QID --query 'ResultSet.Rows[].Data[].VarCharValue' --output text
aws athena get-query-execution --profile shop-lab --query-execution-id $QID --query 'QueryExecution.Statistics.[DataScannedInBytes,TotalExecutionTimeInMillis]' --output text
}
q "SELECT eventtype, count(*) c FROM clickstream GROUP BY eventtype ORDER BY c DESC"
q "SELECT productid, count(*) views FROM clickstream WHERE eventtype='page_view' GROUP BY productid ORDER BY views DESC LIMIT 5"
q "WITH s AS (SELECT sessionid, max(CASE WHEN eventtype='add_to_cart' THEN 1 ELSE 0 END) added, max(CASE WHEN eventtype='checkout' THEN 1 ELSE 0 END) bought FROM clickstream GROUP BY sessionid) SELECT count(*) abandoned, round(100.0*count(*)/(SELECT count(*) FROM s WHERE added=1),1) pct FROM s WHERE added=1 AND bought=0"
Console: Athena → Query editor → Workgroup shop-analytics → Database shop_analytics → chạy SQL, tab "Query stats" thấy Data scanned.
Bước 7: CTAS sang Parquet và so sánh.
q "CREATE TABLE clickstream_parquet WITH (format='PARQUET', parquet_compression='SNAPPY', external_location='s3://$BUCKET/clickstream-parquet/', partitioned_by=ARRAY['day']) AS SELECT eventtype, sessionid, userid, productid, category, ts, useragent, concat(year,'-',month,'-',day) AS day FROM clickstream"
q "SELECT productid, count(*) views FROM clickstream_parquet WHERE eventtype='page_view' GROUP BY productid ORDER BY views DESC LIMIT 5"
# So DataScannedInBytes với query ở bước 6: Parquet chỉ đọc 2 cột
Kiểm tra kết quả
# Firehose có nhận và giao
aws cloudwatch get-metric-statistics --profile shop-lab --namespace AWS/Firehose --metric-name IncomingRecords \
--dimensions Name=DeliveryStreamName,Value=shop-clickstream --statistics Sum --period 300 \
--start-time $(date -u -d '30 min ago' +%FT%TZ) --end-time $(date -u +%FT%TZ) --query 'Datapoints[].Sum'
aws cloudwatch get-metric-statistics --profile shop-lab --namespace AWS/Firehose --metric-name DeliveryToS3.Success \
--dimensions Name=DeliveryStreamName,Value=shop-clickstream --statistics Average --period 300 \
--start-time $(date -u -d '30 min ago' +%FT%TZ) --end-time $(date -u +%FT%TZ) --query 'Datapoints[].Average' # 1.0
# Không có file lỗi
aws s3 ls s3://$BUCKET/clickstream-errors/ --profile shop-lab # trống
# Partition trong catalog
aws glue get-partitions --profile shop-lab --database-name shop_analytics --table-name clickstream --query 'Partitions[].Values' --output text
Mong đợi: IncomingRecords ≈ số event đã gửi (2,000 hoặc 300); 3 query Athena trả kết quả trong 2–5 giây; CTAS Parquet quét ít byte hơn rõ rệt.
Thử thêm
- Firehose → Parquet trực tiếp:
aws firehose update-destinationvớiDataFormatConversionConfigurationtrỏ tới Glue tableclickstream(schema),OutputFormatConfiguration.Serializer.ParquetSerDe. Gửi thêm event, thấy file.parquet. Lưu ý buffer tối thiểu khi convert là 64 MB. - Transform bằng Lambda: gắn Lambda
shop-clickstream-enrich(Java 21) thêm fielddeviceTypetừ userAgent; Firehose gọi Lambda theo batch, Lambda trả record vớiresult: "Ok" | "Dropped" | "ProcessingFailed". - Partition projection thay crawler:
ALTER TABLEthêmprojection.enabled=true, projection.year.type=integer, projection.year.range=2026,2030, …, storage.location.template=s3://…/clickstream/year=${year}/month=${month}/day=${day}/hour=${hour}/; tắt crawler mà vẫn query được partition mới. - Chèn Kinesis Data Streams: tạo stream
shop-clickson-demand, đổi Firehose source sangKinesisStreamAsSource, producer dùngKinesisClient.putRecordsvới partition keysessionId; thêm Lambda consumer thứ hai đếm "đang hot" vào DynamoDBshop-carts… hoặc chỉ in log. - QuickSight: đăng ký bản Standard (có trial), dataset từ Athena
clickstream_parquet, vẽ bar chart top sản phẩm. Nhớ hủy đăng ký sau lab vì tính theo user/tháng.
Dọn tài nguyên
aws firehose delete-delivery-stream --profile shop-lab --delivery-stream-name shop-clickstream
aws glue delete-crawler --profile shop-lab --name shop-clickstream-crawler
aws glue delete-table --profile shop-lab --database-name shop_analytics --name clickstream_parquet
aws glue delete-table --profile shop-lab --database-name shop_analytics --name clickstream
aws glue delete-database --profile shop-lab --name shop_analytics
aws athena delete-work-group --profile shop-lab --work-group shop-analytics --recursive-delete-option
aws s3 rm s3://$BUCKET/clickstream/ --recursive --profile shop-lab
aws s3 rm s3://$BUCKET/clickstream-parquet/ --recursive --profile shop-lab
aws s3 rm s3://$BUCKET/athena-results/ --recursive --profile shop-lab
aws s3 rm s3://$BUCKET/clickstream-errors/ --recursive --profile shop-lab
aws logs delete-log-group --profile shop-lab --log-group-name /aws/kinesisfirehose/shop-clickstream
aws iam delete-role-policy --profile shop-lab --role-name shop-firehose-role --policy-name s3-write
aws iam delete-role --profile shop-lab --role-name shop-firehose-role
aws iam delete-role-policy --profile shop-lab --role-name shop-glue-role --policy-name s3-read
aws iam detach-role-policy --profile shop-lab --role-name shop-glue-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole
aws iam delete-role --profile shop-lab --role-name shop-glue-role
# Nếu đã thử thêm: xóa Kinesis stream (shard-giờ tính tiền), Lambda, hủy QuickSight subscription
aws kinesis delete-stream --profile shop-lab --stream-name shop-clicks 2>/dev/null
Nhắc: Kinesis Data Streams provisioned tính tiền theo shard-giờ dù không có dữ liệu; QuickSight tính theo user/tháng; crawler đặt schedule sẽ chạy đều và tốn DPU; Redshift/OpenSearch nếu bạn tò mò tạo thử là những dịch vụ đắt theo giờ.
Phỏng vấn và đề thi SAA hỏi về Kinesis, Athena và họ analytics thì sao?
Q1. Bạn thiết kế pipeline clickstream cho ứng dụng Java thế nào? Vì sao không ghi vào database chính?
🇻🇳 Tiếng Việt:
Em tách analytics khỏi database giao dịch: app gom event vào buffer trong bộ nhớ và đẩy batch bằng
PutRecordBatchvào Firehose, không gọi đồng bộ trong request và chấp nhận rơi event khi buffer đầy vì analytics không phải checkout. Firehose buffer rồi ghi Parquet nén vào S3 với partition theo ngày, Glue Data Catalog giữ schema, Athena query SQL trả tiền theo byte quét, QuickSight vẽ dashboard. Không ghi vào RDS vì hàng triệu INSERT và query aggregate quét bảng lớn sẽ ăn IOPS, phình backup và làm chậm giao dịch; row-store không hợp OLAP.
🇬🇧 English:
I separate analytics from the transactional database: the app buffers events in memory and ships them in batches with
PutRecordBatchto Firehose, never synchronously in the request path, and it accepts dropping events when the buffer is full because analytics is not checkout. Firehose buffers and writes compressed Parquet to S3 partitioned by day, the Glue Data Catalog holds the schema, Athena runs SQL billed per byte scanned, and QuickSight builds dashboards. I avoid RDS because millions of inserts and large aggregate scans would consume IOPS, bloat backups and slow transactions; a row store is the wrong shape for OLAP.
Q2. Kinesis Data Streams và Firehose khác nhau thế nào? Khi nào dùng cái nào?
🇻🇳 Tiếng Việt:
Data Streams là stream thật: dữ liệu giữ 24 giờ đến 365 ngày, nhiều consumer đọc độc lập và đọc lại được, scale bằng shard hoặc on-demand, latency vài trăm mili giây, mình tự viết consumer bằng KCL, Lambda hay Flink. Firehose là dịch vụ giao hàng: nhận record, buffer theo MB hoặc giây, tùy chọn transform bằng Lambda và chuyển sang Parquet, rồi ghi vào S3, Redshift, OpenSearch hay HTTP endpoint; không lưu, không replay, không shard, near real-time từ 60 giây. Cần real-time và nhiều consumer thì Data Streams; chỉ cần đổ vào kho thì Firehose; cần cả hai thì Data Streams làm nguồn cho Firehose.
🇬🇧 English:
Data Streams is a true stream: data is retained from 24 hours up to 365 days, multiple consumers read independently and can replay, it scales by shards or on-demand, latency is a few hundred milliseconds, and you write consumers with KCL, Lambda or Flink. Firehose is a delivery service: it receives records, buffers by size or time, optionally transforms with Lambda and converts to Parquet, then writes to S3, Redshift, OpenSearch or an HTTP endpoint; no storage, no replay, no shards, near real-time from 60 seconds. For real-time and multiple consumers use Data Streams; to land data in a store use Firehose; for both, Data Streams feeds Firehose.
Q3. Làm sao giảm chi phí và thời gian query Athena?
🇻🇳 Tiếng Việt:
Athena tính theo byte quét nên mọi cách đều nhằm đọc ít byte hơn: dùng định dạng cột Parquet hoặc ORC để chỉ đọc cột trong SELECT, nén Snappy hoặc ZSTD, partition theo cột hay lọc như ngày và luôn lọc trên partition trong WHERE, gom file nhỏ thành file 128 MB đến 1 GB, dùng partition projection thay crawler cho partition theo thời gian, và đặt giới hạn byte quét trong workgroup để chặn câu SELECT * quên WHERE. Với query lặp lại nhiều, cân nhắc CTAS ra bảng tổng hợp hoặc chuyển sang Redshift.
🇬🇧 English:
Athena bills per byte scanned, so everything aims at reading fewer bytes: columnar Parquet or ORC so only selected columns are read, Snappy or ZSTD compression, partitioning by commonly filtered columns such as date and always filtering on partitions in WHERE, compacting small files into 128 MB to 1 GB files, partition projection instead of crawlers for time-based partitions, and a per-query byte limit in the workgroup to stop a SELECT * without WHERE. For heavily repeated queries, pre-aggregate with CTAS or move to Redshift.
Q4. Khi nào chọn Redshift thay Athena, và EMR thay Glue?
🇻🇳 Tiếng Việt:
Athena cho truy vấn ad-hoc, không vận hành, dữ liệu để yên trên S3, trả theo query. Redshift khi có nhiều analyst query liên tục, join phức tạp nhiều bảng, cần giây ổn định và BI nặng; Redshift Spectrum vẫn query được S3 cho dữ liệu lạnh, Serverless giảm vận hành. Glue cho ETL serverless bằng Spark khi mình không cần kiểm soát cluster; EMR khi cần Hadoop ecosystem đầy đủ, thư viện riêng, tuning cluster, hoặc job rất lớn chạy trên Spot để rẻ, với core node giữ HDFS và task node dùng Spot.
🇬🇧 English:
Athena is for ad-hoc queries with no operations, data staying in S3, paid per query. Redshift fits many analysts querying continuously, complex multi-table joins, consistent sub-second to second latency and heavy BI; Spectrum still queries S3 for cold data and Serverless cuts operations. Glue is serverless Spark ETL when you do not need cluster control; EMR is for the full Hadoop ecosystem, custom libraries, cluster tuning or very large jobs running cheaply on Spot, with core nodes holding HDFS and task nodes on Spot.
Góc thi SAA-C03
Câu 1: Một công ty thu thập clickstream từ website và cần lưu vào Amazon S3 để phân tích hàng ngày bằng SQL. Dữ liệu không cần truy vấn real-time. Giải pháp nào đáp ứng với LEAST operational overhead?
A. Kinesis Data Streams với ứng dụng KCL trên EC2 ghi vào S3; dùng Redshift để query.
B. Amazon Data Firehose ghi vào S3 với chuyển đổi Parquet; dùng Glue crawler và Athena để query.
C. Ghi vào Amazon RDS rồi export snapshot sang S3 hàng ngày.
D. Amazon MSK với Kafka Connect S3 sink; dùng EMR Presto để query.
Đáp án: B. "Vào S3", "không real-time", "SQL", "least operational overhead" = Firehose + Athena, cả hai serverless. A: phải vận hành consumer EC2 và cluster Redshift. C: RDS cho clickstream sai mô hình, export snapshot không phải pipeline analytics. D: MSK + EMR là hai thứ phải vận hành nhiều nhất.
Câu 2: Một ứng dụng dùng Kinesis Data Streams với 1 shard. Producer bắt đầu nhận ProvisionedThroughputExceededException trong giờ cao điểm. Hai consumer đọc stream cũng bị chậm. Giải pháp nào giải quyết cả hai vấn đề với MOST cost-effective?
A. Chuyển sang Firehose.
B. Tăng số shard (hoặc chuyển on-demand mode) và dùng enhanced fan-out cho consumer.
C. Tăng retention lên 7 ngày.
D. Thay bằng SQS FIFO queue.
Đáp án: B. Lỗi throughput = thiếu shard (1 MB/s ghi mỗi shard); consumer chậm vì 2 MB/s đọc bị chia cho 2 consumer → enhanced fan-out cho mỗi consumer 2 MB/s riêng. A: Firehose không phải stream có consumer tự viết. C: retention không liên quan throughput. D: SQS thay đổi mô hình (không có nhiều consumer đọc độc lập cùng dữ liệu).
Câu 3: Một công ty lưu 50 TB log ứng dụng dạng JSON nén trong S3 và dùng Athena để query. Chi phí Athena tăng cao và query chậm. Solutions architect nên khuyến nghị gì để giảm chi phí NHIỀU NHẤT?
A. Chuyển dữ liệu sang Apache Parquet và partition theo ngày.
B. Tăng kích cỡ instance của Athena.
C. Chuyển dữ liệu sang S3 Glacier Instant Retrieval.
D. Bật S3 Transfer Acceleration.
Đáp án: A. Athena tính theo byte quét; cột-store + partition giảm byte quét hàng chục lần. B: Athena serverless, không có instance. C: storage class không đổi byte quét, Glacier IR còn tốn phí truy xuất. D: Transfer Acceleration là cho upload đường dài.
Cách nhớ ngắn gọn
- OLTP (RDS) tách khỏi OLAP (S3 lake). Event đi: app → (Kinesis) → Firehose → S3 Parquet → Glue Catalog → Athena → QuickSight.
- Streams = real-time, shard, replay, nhiều consumer. Firehose = near real-time, không shard, không lưu, đổ vào đích.
- Athena rẻ khi: Parquet + partition + nén + file to + lọc trên partition. Workgroup giới hạn byte.
- Redshift = warehouse; EMR = Spark/Hadoop tự kiểm soát; Glue = ETL serverless + catalog; OpenSearch = search/log; Lake Formation = quyền cho data lake; Flink = SQL/Java real-time trên stream; MSK = Kafka.
| Từ khóa trong đề | Dịch vụ |
|---|---|
| real-time ingest, shards, multiple consumers, replay, Lambda consumer | Kinesis Data Streams |
| load streaming data into S3/Redshift/OpenSearch/Splunk, near real-time, no management | Amazon Data Firehose |
| SQL/Flink on streaming data, windowed aggregation | Managed Service for Apache Flink |
| Kafka compatibility, existing Kafka apps | Amazon MSK |
| ad-hoc SQL on S3, serverless, pay per query | Athena |
| schema discovery, metadata catalog, serverless ETL | Glue (crawler, Data Catalog, jobs) |
| data warehouse, BI, petabyte, complex joins, Spectrum | Redshift |
| Hadoop/Spark/Hive cluster, Spot task nodes | EMR |
| full-text search, log analytics dashboards | OpenSearch Service |
| BI dashboards, visualization, SPICE | QuickSight |
| centralized fine-grained data lake permissions | Lake Formation |
| reduce Athena cost / faster queries | Parquet/ORC + partition + compression |
Tự kiểm tra
Vì sao không ghi clickstream vào RDS của shop-app?
Hàng triệu INSERT và query aggregate quét bảng lớn tranh IOPS với giao dịch, phình dung lượng và backup; row-store không hợp OLAP. Tách sang S3 data lake với Firehose/Athena rẻ và không ảnh hưởng checkout.
Kinesis Data Streams và Firehose khác nhau ở 3 điểm chính?
Streams: lưu 24 giờ–365 ngày và replay được, nhiều consumer tự viết, scale bằng shard; real-time. Firehose: không lưu, không replay, không shard, tự scale, chỉ giao vào đích (S3, Redshift, OpenSearch, HTTP) gần real-time với buffer.
Ba việc làm giảm chi phí Athena nhiều nhất?
Định dạng cột Parquet/ORC, partition theo cột lọc (và lọc trên partition trong WHERE), nén. Thêm: gom file nhỏ, partition projection, giới hạn byte trong workgroup.
Glue crawler làm gì và thay bằng gì nếu không muốn chạy định kỳ?
Crawler quét S3, suy ra schema và partition, ghi vào Glue Data Catalog để Athena/EMR/Redshift Spectrum dùng. Thay bằng Athena partition projection (khai báo pattern partition) hoặc tạo table/partition bằng DDL.
Khi nào chọn Redshift thay Athena, MSK thay Kinesis?
Redshift khi nhiều query lặp, join phức tạp, BI nặng cần hiệu năng ổn định (warehouse). MSK khi đã có code/tooling Kafka, cần Kafka Streams/Connect, giữ dữ liệu dài; Kinesis khi muốn serverless và tích hợp Lambda/Firehose sâu.