- Nanit từng sử dụng AWS S3 trong pipeline xử lý video để phân tích giấc ngủ của em bé, nhưng với hàng nghìn lượt tải lên mỗi giây, chi phí yêu cầu PutObject chiếm phần lớn tổng chi phí
- Ngoài ra, do giới hạn lưu tối thiểu 1 ngày của quy tắc S3 Lifecycle, họ phải trả phí lưu trữ 24 giờ cho các video thực tế được xử lý chỉ trong vòng 2 giây
- Để giải quyết, họ xây dựng N3, hệ thống lưu trữ in-memory dựa trên Rust, và chỉ dùng S3 làm bộ đệm tràn
- N3 hoàn toàn tương thích với pipeline xử lý hiện có thông qua SQS FIFO, đồng thời vẫn duy trì đảm bảo thứ tự nghiêm ngặt và độ tin cậy
- Kết quả là tiết kiệm khoảng 500.000 USD mỗi năm và có được một kiến trúc đơn giản nhưng ổn định
Bối cảnh
Tổng quan về pipeline xử lý video
- Camera của Nanit ghi lại các đoạn video, yêu cầu S3 presigned URL từ Camera Service rồi tải trực tiếp lên S3
- AWS Lambda đăng object key vào hàng đợi SQS FIFO (shard theo baby_uid), sau đó các pod xử lý video lấy dữ liệu từ SQS, tải từ S3 về và thực hiện suy luận trạng thái giấc ngủ
- Ưu điểm của thiết lập này
- Điểm hạ cánh trên S3 + hàng đợi SQS tách biệt việc camera tải lên với xử lý video, giúp tránh mất video ngay cả khi bảo trì hoặc downtime tạm thời
- Không cần tự quản lý trực tiếp tính sẵn sàng và độ bền nhờ S3
- SQS FIFO + group ID giúp giữ nguyên thứ tự theo từng em bé, còn các node xử lý phần lớn có thể duy trì trạng thái stateless
- Quy tắc S3 Lifecycle đảm nhiệm việc garbage collection nên không cần theo dõi các video đã xử lý
Vì sao cần thay đổi
- Chi phí PutObject là yếu tố chi phối: video là các đối tượng sống ngắn chỉ tồn tại ở vùng hạ cánh trong vài giây để được xử lý, nhưng ở quy mô hàng nghìn lượt tải lên mỗi giây thì chi phí yêu cầu trên mỗi object trở thành nguồn chi phí lớn nhất
- Nếu tăng tần suất chia chunk (gửi nhiều chunk nhỏ hơn) để giảm độ trễ, thì mỗi chunk bổ sung lại tạo thêm một yêu cầu PutObject nên chi phí tăng tuyến tính
- Lưu trữ bị tính phí lần hai: dù xử lý hoàn tất chỉ trong khoảng 2 giây, quy tắc xóa Lifecycle vẫn khiến họ bị tính chi phí lưu trữ khoảng 24 giờ
- Cần một thiết kế có thể tránh chi phí trên mỗi object ở đường đi bình thường, giảm tối đa lưu trữ kiểu “trả tiền để chờ”, nhưng vẫn giữ được độ tin cậy và đảm bảo thứ tự nghiêm ngặt
Kế hoạch
-
Nguyên tắc thiết kế
- Đơn giản nhờ kiến trúc: loại bỏ độ phức tạp ở cấp độ thiết kế, thay vì dựa vào các triển khai quá tinh vi
- Tính đúng đắn: phải là sản phẩm thay thế hoàn chỉnh và trong suốt đối với phần còn lại của pipeline
- Tối ưu cho đường đi bình thường: thiết kế cho trường hợp phổ biến, và dùng S3 làm lưới an toàn cho edge case; vì thuật toán xử lý có thể chịu được các khoảng trống thỉnh thoảng xuất hiện nên ưu tiên sự đơn giản hơn là xây dựng các đảm bảo phức tạp
-
Các động lực thiết kế
- Đối tượng sống ngắn: segment chỉ tồn tại trong vùng hạ cánh vài giây
- Thứ tự: cần sequencing nghiêm ngặt theo từng em bé, không xử lý bản mới hơn trước
- Thông lượng: hàng nghìn lượt tải lên mỗi giây, mỗi segment 2-6 MB
- Giới hạn phía client: camera có số lần retry giới hạn, không thể giả định sẽ gửi lại
- Vận hành: phải chấp nhận backlog hàng triệu mục trong lúc bảo trì hoặc scale up
- Không thay đổi firmware: phải hoạt động với camera hiện có
- Khả năng chấp nhận mất mát: có thể chấp nhận các khoảng trống rất nhỏ, vì thuật toán có thể che lấp
- Chi phí: tránh chi phí S3 trên mỗi object ở đường đi bình thường, giảm tối thiểu chi phí lưu trữ “trả tiền để chờ”
Tổng quan thiết kế (đường đi bình thường của N3 + S3 overflow)
-
Kiến trúc
- N3 là một vùng hạ cánh tùy biến chỉ giữ video trong bộ nhớ đúng khoảng thời gian cần để xử lý rút hết dữ liệu (khoảng 2 giây), và chỉ dùng S3 khi N3 không xử lý nổi tải
- Hai thành phần
- N3-Proxy (stateless, giao diện kép)
- Bên ngoài (kết nối Internet): nhận tải lên từ camera thông qua presigned URL
- Bên trong (private): cấp presigned URL cho Camera Service
- N3-Storage (stateful, chỉ nội bộ): lưu các segment đã tải lên trong RAM và đưa vào SQS bằng download URL có thể định tuyến trực tiếp đến pod
- Các pod xử lý video lấy dữ liệu từ SQS FIFO rồi tải xuống từ hệ lưu trữ mà URL chỉ tới, có thể là N3 hoặc S3
-
Luồng bình thường (Happy Path)
- Camera yêu cầu upload URL từ Camera Service
- Camera Service yêu cầu presigned URL từ API nội bộ của N3-Proxy
- Camera tải video lên endpoint bên ngoài của N3-Proxy
- N3-Proxy chuyển tiếp sang N3-Storage
- N3-Storage giữ video trong bộ nhớ và đưa vào SQS với download URL trỏ về chính nó
- Pod xử lý tải từ N3-Storage về để xử lý
-
Fallback hai tầng
- Tầng 1: fallback ở mức proxy (theo từng request)
- Nếu N3-Storage không thể nhận upload do áp lực bộ nhớ, backlog xử lý, pod lỗi..., N3-Proxy sẽ tải lên S3 thay cho camera
- Camera đã nhận N3 URL trước khi sự cố được phát hiện
- Tầng 2: reroute ở mức cluster (toàn bộ traffic)
- Nếu N3-Proxy hoặc N3-Storage không khỏe, Camera Service sẽ ngừng cấp N3 URL và trả trực tiếp S3 presigned URL
- Toàn bộ traffic sẽ chảy sang S3 cho đến khi N3 phục hồi
-
Vì sao tách thành hai thành phần
- Phạm vi ảnh hưởng của sự cố: nếu storage sập, proxy vẫn có thể định tuyến sang S3; nếu proxy sập thì chỉ traffic ở node đó bị ảnh hưởng, còn toàn bộ cluster storage vẫn an toàn
- Hồ sơ tài nguyên: proxy thiên về CPU/network (TLS termination), storage thiên về bộ nhớ (giữ video), nên cần loại instance và cách scale khác nhau
- Bảo mật: storage không bao giờ chạm Internet
- An toàn rollout: cập nhật proxy (stateless) không đụng đến storage (đang giữ dữ liệu hoạt động)
Xác thực thiết kế
-
Những gì cần xác thực
- Dung lượng và sizing: thời lượng tải lên thực tế trên các mạng client, lượng compute và kích thước bộ đệm upload cần thiết
- Mô hình lưu trữ: có thể giữ mọi thứ trong RAM hay cần đĩa
- Khả năng phục hồi: cách load balancing giá rẻ và xử lý node lỗi
- Chính sách vận hành: yêu cầu về GC, kỳ vọng retry, và liệu xóa khi GET có đủ không
- Những ẩn số chưa biết: các edge case nào sẽ xuất hiện khi ý tưởng gặp thực tế
-
Cách tiếp cận 1: stress test tổng hợp
- Xây dựng một load generator để đẩy hệ thống đến giới hạn với nhiều mức đồng thời, client chậm, tải kéo dài và downtime xử lý
- Mục tiêu: tìm điểm giới hạn, xác định bottleneck ngoài dự tính và có baseline mang tính quyết định cho việc lập kế hoạch năng lực
-
Cách tiếp cận 2: PoC production (mirror mode)
- Bài test tổng hợp không thể sao chép hành vi camera thực tế: Wi‑Fi không ổn định, nhiều phiên bản firmware, điều kiện mạng khó lường
- Mirror mode:
n3-proxy trước tiên ghi vào S3 (để giữ nguyên production), sau đó cũng ghi vào N3-Storage của PoC (kết nối với SQS canary + video processor)
- Cohort mục tiêu: theo phiên bản firmware / danh sách Baby-UID
- Data parity: so sánh trạng thái giấc ngủ giữa PoC và production, điều tra sai khác
- Quan sát hệ thống: dashboard theo từng đường đi (N3 vs S3), độ sâu hàng đợi, độ trễ/RPS, error budget, phân tích egress
- Feature flag (dùng Unleash) rất quan trọng: cho phép chuyển cohort theo thời gian thực mà không cần deploy, thử trên lát cắt hẹp như firmware cũ hoặc camera Wi‑Fi yếu, rồi hoàn nguyên ngay nếu có vấn đề
-
Những gì họ phát hiện
- Bottleneck: TLS termination tiêu tốn phần lớn CPU, còn AWS burstable networking bị throttle sau khi cạn credit
- Lưu trữ chỉ dùng bộ nhớ là khả thi: từ phân bố thời gian upload thực tế và mức đồng thời, họ xác nhận working set có thể nằm trong RAM với biên an toàn, không cần đĩa
- Overhead của TCP timestamp: khoảng 85% tổng số byte truyền đi là ACK frame, và việc tắt TCP timestamp (
sysctl -w net.ipv4.tcp_timestamps=0) giúp giảm 12 byte cho mỗi ACK
- Rủi ro: nếu truyền quá nhiều byte trên cùng một socket, số thứ tự có thể bị wrap, dẫn đến gộp nhầm các gói đến muộn và gây hỏng dữ liệu
- Giảm thiểu: (1) dùng socket mới cho mỗi lần upload, (2) tái tạo socket giữa
n3-proxy ↔ n3-storage sau khoảng 1 GB dữ liệu truyền
- Rò rỉ bộ nhớ: sau lần phát hành đầu tiên, bộ nhớ của
n3-proxy tăng đều
- Profiling bằng
jemalloc cho thấy mức tăng nằm ở buffer BytesMut của hyper theo từng kết nối
- Một số kết nối client bị treo giữa chừng khi truyền mà không được dọn dẹp, khiến buffer còn sót lại và bộ nhớ tiếp tục tăng
- Cách sửa: làm cho socket có vòng đời ngắn và áp giới hạn thời gian
- Tắt keep-alive: đóng kết nối ngay sau khi hoàn tất mỗi lần upload
- Siết timeout: đặt timeout cho header/socket để hủy các upload bị treo và giải phóng buffer
Lưu trữ
-
Lưu trữ in-memory
- Họ bắt đầu từ con đường đơn giản nhất: lưu trữ in-memory để tránh tuning I/O và dùng cấu trúc dữ liệu trực quan
- Dùng
Arc<DashMap<Ulid, Bytes>> để lưu video; mỗi upload làm tăng bytes_used, mỗi download sẽ xóa video và giảm bộ đếm
- Khi vượt quá khoảng 80% dung lượng, hệ thống bắt đầu từ chối upload để tránh OOM, đồng thời báo cho
n3-proxy ngừng ký upload URL
- Có thể dùng handle
control để tạm dừng thủ công việc upload và garbage collection
-
Khởi động lại một cách graceful
- Vì storage chỉ nằm trong bộ nhớ nên cần tránh làm rơi dữ liệu đang xử lý khi restart
- Quy trình graceful restart
- Pod nhận
SIGTERM (StatefulSet rolling từng pod một)
- Pod chuyển sang trạng thái Not Ready và bị rút khỏi Service, nên không có upload mới
- Vẫn tiếp tục phục vụ download cho những video đã upload trước đó
- Khi download dừng lại (không còn lần đọc gần đây → xử lý đã drain xong)
- Chờ các request đang mở hoàn tất
- Restart rồi mới chuyển sang pod tiếp theo
- Trong điều kiện bình thường, pod sẽ drain xong trong vài giây
-
GC
- Hai cơ chế dọn dẹp được dùng song song
- Xóa khi download: xóa video ngay sau khi tải xuống; PoC cho thấy không có trường hợp re-download, và do video processor tự retry nội bộ nên không cần giữ dữ liệu hay theo dõi trạng thái “đã xử lý”
- TTL GC cho các trường hợp bị bỏ sót: cơ chế xóa khi download không xử lý được các segment mà processor bỏ qua (không được tải xuống → không bị xóa)
- Họ bổ sung TTL GC gọn nhẹ: định kỳ quét DashMap trong bộ nhớ và xóa các mục cũ hơn ngưỡng cấu hình (ví dụ vài giờ)
- Maintenance mode: trong thời gian downtime xử lý có kế hoạch, có thể tạm dừng GC qua điều khiển nội bộ để tránh xóa video trong lúc consumer đang dừng
Kết luận
-
Thành quả chính
- Dùng S3 làm bộ đệm fallback và N3 làm vùng hạ cánh chính, họ đạt mức tiết kiệm khoảng 500.000 USD mỗi năm mà vẫn giữ hệ thống đơn giản và đáng tin cậy
- Insight cốt lõi: đa số quyết định “build vs buy” tập trung vào tính năng, nhưng ở quy mô lớn thì yếu tố kinh tế thay đổi hoàn toàn bài toán
- Với đối tượng sống ngắn (khoảng 2 giây trong điều kiện bình thường), không cần replication hay durability quá tinh vi; một kho in-memory đơn giản là đủ chạy
- Khi xử lý bị chậm hoặc bảo trì làm kéo dài vòng đời object, lúc đó mới cần đảm bảo độ tin cậy của S3
- Lợi ích của cả hai phía: N3 xử lý hiệu quả đường đi bình thường, còn S3 cung cấp độ bền khi object cần tồn tại lâu hơn
- Nếu N3 có vấn đề như áp lực bộ nhớ, pod crash hay lỗi cluster, upload sẽ fail over mượt mà sang S3
-
Yếu tố tạo nên thành công
- Xác định rõ bài toán từ trước: ràng buộc, giả định và ranh giới giúp tránh trượt phạm vi
- Xác thực sớm bằng PoC mirror mode: phát hiện bottleneck (TLS, network throttling) và kiểm chứng giả định trước khi cam kết
- Tránh over-engineering và phải quay đầu
-
Khi nào nên tự xây thứ như thế này
- Hãy cân nhắc hạ tầng tùy biến khi có quy mô đủ lớn để tạo ra khoản tiết kiệm đáng kể, và đồng thời có các ràng buộc cụ thể cho phép một giải pháp đơn giản
- Nỗ lực kỹ thuật để xây và duy trì hệ thống phải nhỏ hơn phần chi phí hạ tầng được cắt giảm
- Trong trường hợp của Nanit, các yêu cầu cụ thể như kho tạm thời, khả năng chấp nhận mất mát và fallback sang S3 đã giúp họ xây được thứ đủ đơn giản để giữ chi phí bảo trì thấp
- Nếu thiếu một trong hai yếu tố đó, hãy tiếp tục dùng managed service
- Có làm lại không? Có, vì hệ thống đang chạy ổn định trong production, và thiết kế fallback cho phép tránh phức tạp mà không phải hy sinh độ tin cậy
3 bình luận
Tôi hơi thắc mắc là liệu có thể để EC2 hoặc pod EKS trực tiếp nhận upload video rồi xử lý hay không.
Nếu đã làm tới mức tạo cả proxy thì có vẻ cũng hoàn toàn có thể autoscaling EKS theo tải của pod.
Thông thường xử lý video không cần phải đưa toàn bộ file lên in-memory; nếu tạo file tạm trên SSD cục bộ của từng instance rồi xử lý thì có cảm giác là cũng chẳng cần cả phương án fallback sang S3.
Có vẻ như đây là một ví dụ về việc dùng serverless và S3 không đúng cách.
Nhưng hình như cách giải quyết còn kỳ lạ hơn nữa.
Ý kiến trên Hacker News
Bài viết thực sự rất hữu ích. Mình rất thích khi họ chia sẻ quá trình tiếp cận kỹ thuật như thế này
Dù bản thân không trực tiếp gặp đúng vấn đề đó, chỉ riêng việc nhìn cách họ tiếp cận cũng đã học được rất nhiều
Nói thật thì, có lẽ mọi thứ đã gọn gàng hơn rất nhiều nếu ngay từ đầu không dùng serverless
Cảm giác như họ cố nhét dữ liệu chỉ tồn tại vài giây vào mô hình serverless của AWS, nên mới phát sinh chi phí và độ phức tạp không cần thiết
Dù vậy, việc chuyển sang giải pháp dựa trên bộ nhớ vẫn là một lựa chọn tốt
Họ có nói TLS handshake tốn nhiều CPU, nhưng có lẽ đó không phải nút thắt chính
Dù sao thì việc thử một thiết kế hệ thống tối ưu theo workflow như vậy vẫn rất thú vị
Thực ra, thay vì đúng như tiêu đề là “tự triển khai S3”, đây giống một kiến trúc đặt bộ nhớ đệm trong RAM ở phía trước S3 hơn
Rất ngầu, nhưng không phải là một bản thay thế S3 hoàn chỉnh do tự xây
Dù tiêu đề là gì thì đây vẫn là một dự án thú vị
Nói theo kiểu HN thì, mình muốn bàn về chính công ty Nanit hơn
Nanit vận hành camera theo dõi em bé dựa trên đám mây. Toàn bộ video và âm thanh đều được tải lên mà không có E2EE
Phần cứng thì đắt, và gần như không dùng được nếu không có thuê bao. Hơn nữa còn phải mua thêm cái giá đỡ $200 thì mới mở được tính năng theo dõi giấc ngủ
Điều đáng tiếc là mô hình như vậy cuối cùng lại càng củng cố mô hình phụ thuộc vào đám mây
Dù vậy, việc giảm phụ thuộc vào S3 và chuyển sang storage tự xây như bài này vẫn là một bước đi tốt
Các sản phẩm khác thì app không ổn định. Nếu có giải pháp ưu tiên local + E2EE thì tốt, nhưng thực tế tính dễ dùng vẫn quan trọng hơn
Nếu thực sự muốn E2EE thì phải phân tích ở local rồi chỉ tải kết quả lên
Bài này cho cảm giác như họ tự tạo ra vấn đề rồi lại tự chúc mừng vì đã giải được nó
Thà ngay từ đầu bán phần cứng lưu trữ local thì đã đơn giản và rẻ hơn
Thiết kế xoay quanh đám mây giờ có vẻ là cách tiếp cận kiểu 2015
Bài viết rất hay, nhưng mình cũng tò mò mức tiết kiệm chi phí nếu triển khai ‘delete on read’ ngay trên S3 sẽ ra sao
Nếu S3 tính phí theo từng giây thì số tiền tiết kiệm có thể cũng khá đáng kể
Ngoài ra, giải pháp này về bản chất cũng khá giống tùy chọn ‘reduced redundancy’ của S3
Họ nói tiết kiệm được $500.000, nhưng không rõ tổng chi phí là bao nhiêu
Ý nghĩa sẽ rất khác nếu đó là $500.001 trên tổng $500.000, hay $500.000 trên tổng $55 triệu
Có cảm giác họ đã chọn một kiến trúc sai ngay từ đầu, rồi sau đó trét cache lên để vá
Không có lý do gì để tải các đoạn video trung bình 2 giây lên S3 ngoài việc lưu bản sao dự phòng
Nếu xử lý trực tiếp trên server thì có lẽ đã bỏ được cả S3, SQS lẫn Lambda
Không hiểu vì sao một vấn đề đơn giản như vậy lại bị làm cho phức tạp đến thế
Đây giống như một bài học kinh điển kiểu “hãy tập trung phát triển ứng dụng và đơn giản hóa hạ tầng”
Có lẽ nhét cache trực tiếp vào trong server xử lý video còn tốt hơn
Có lẽ tiêu đề đúng hơn nên là “đã dùng S3 sai cách”
Cuối cùng họ đã tự làm một memory store, trong khi có lẽ chỉ cần dùng Redis là xong
Nếu hệ thống tự xây đó bị down thì video sẽ biến mất à?
Nếu ngay từ đầu gửi qua Kinesis hoặc SQS thì đã tốt hơn nhiều rồi