Chuyển đổi từ Batch sang Micro-Batch Streaming: Bài học xương máu khi xây dựng Pipeline Delta Index

04 tháng 5, 2026·12 phút đọc

Bài viết này chia sẻ hành trình chuyển đổi một pipeline delta index từ xử lý batch theo lịch trình sang mô hình micro-batch streaming liên tục bằng Spark Structured Streaming. Tác giả giải thích lý do tại sao streaming cấp độ bản ghi bị từ chối, cách sử dụng watermark dựa trên phân vùng thay thế các tín hiệu hoàn thành mong manh trên S3, và chiến lược coi việc khởi động lại là một phần của thiết kế để cải thiện tính dự đoán.

Chuyển đổi từ Batch sang Micro-Batch Streaming: Bài học xương máu khi xây dựng Pipeline Delta Index

Nhiều pipeline dữ liệu được mô tả là hệ thống batch thực tế đã hoạt động ở chế độ gần như liên tục. Chúng xử lý dữ liệu gia tăng thường xuyên, thường với các cửa sổ thời gian chồng lấn, và tồn tại chủ yếu để giảm thiểu khoảng trống về tính mới (freshness) giữa các lần tính toán lại đầy đủ nhưng ít tần suất hơn.

Bài viết này mô tả việc di chuyển một hệ thống như vậy — một tập hợp các công việc batch theo lịch trình chịu trách nhiệm tạo ra chỉ mục delta (delta index) được sử dụng trong pipeline truy xuất tìm kiếm và quảng cáo — sang mô hình micro-batch chạy liên tục.

Các công việc này đã được chuyển sang mô hình micro-batch chạy liên tục sử dụng Spark Structured Streaming ở chế độ micro-batch, không phải để đạt được xử lý thời gian thực theo từng bản ghi, mà là để loại bỏ độ trễ lập lịch và cải thiện tính dự đoán vận hành.

Bối cảnh: Thách thức với Pipeline Batch truyền thống

Hệ thống lập chỉ mục của chúng tôi bao gồm hai pipeline với trách nhiệm rất khác nhau.

Pipeline full index (chỉ mục đầy đủ) xây dựng lại toàn bộ chỉ mục Solr từ đầu để bao gồm tất cả quảng cáo và siêu dữ liệu cùng với các tín hiệu hành vi. Nó nặng nề, tốn kém và tốn thời gian, khiến việc thực hiện thường xuyên là không khả thi. Một lần xây dựng lại đầy đủ mất khoảng hai đến ba giờ, theo sau là xác nhận và triển khai khiến tổng thời gian lên tới khoảng năm giờ.

Để đưa các thay đổi vào sản xuất giữa các lần chạy full index, chúng tôi dựa vào pipeline delta index. Pipeline này chỉ xử lý các bản cập nhật gia tăng cho quảng cáo và chiến dịch, và được giữ nhỏ một cách có chủ đích. Về lý thuyết, cách tiếp cận này cho phép các bản cập nhật lan truyền nhanh chóng. Tuy nhiên, trên thực tế, pipeline delta được lập lịch trình và thực hiện bên ngoài như một lời gọi công việc rời rạc mỗi lần. Theo thời gian, một số vấn đề đã xuất hiện:

  • Dữ liệu gia tăng đến ngay sau một lần chạy theo lịch trình thường phải chờ gần một khoảng thời gian lập lịch đầy đủ trước khi được xử lý.
  • Tiến độ được theo dõi ở cấp độ công việc, vì vậy các lỗi yêu cầu thực thi lại toàn bộ cửa sổ lập lịch, ngay cả khi chỉ một phần của nó chưa hoàn thành.
  • Trong các đợt cập nhật bùng nổ, thời lượng batch tăng lên, làm giảm hoặc loại bỏ khoảng thời gian nhàn rỗi giữa các lần chạy.

Vấn đề cốt lõi không phải là bản thân việc batch, mà là sự kết hợp giữa các ranh giới lập lịch thô sơ và ngữ nghĩa tiến trình ở cấp độ công việc. Cuối cùng, rõ ràng là độ trễ lập lịch batch và chi phí điều phối, chứ không phải chi phí xử lý, mới là những yếu tố chính góp phần vào độ trễ về tính mới.

Tại sao Streaming gây tranh cãi nội bộ

Khi streaming được đề xuất lần đầu tiên, sự phản đối không phải về tính đúng đắn hay hiệu suất, mà là về vận hành. Các kỹ sư cấp cao đã đưa ra những lo ngại hoàn toàn hợp lý:

  • Các công việc streaming chạy dài khó lý luận về mặt vận hành hơn.
  • Hành vi phục hồi có thể khó dự đoán.
  • Các lỗi có xu hướng tồn tại thay vì kết thúc sạch sẽ.
  • Gánh nặng trực ban (on-call) thường tăng thay vì giảm.

Mọi động thái chuyển sang streaming phải giải quyết trực tiếp các rủi ro vận hành này.

Bắt đầu sai lầm: Streaming cấp độ bản ghi

Giống như nhiều đội nhóm khác, chúng tôi ban đầu bắt đầu với streaming cấp độ bản ghi. Trên giấy tờ, cách tiếp cận này trông giống như giải pháp "đúng đắn" nhất. Tuy nhiên, trong thực tế, nó nhanh chóng trở nên rõ ràng là không cần thiết và rủi ro đối với pipeline này.

Logic lập chỉ mục giả định tính hoàn chỉnh của batch và hoạt động ở cấp độ nhóm sản phẩm hoặc mục thay vì ở cấp độ bản ghi quảng cáo riêng lẻ. Việc chuyển sang streaming theo từng bản ghi sẽ giới thiệu các trạng thái cập nhật một phần, trong đó một số quảng cáo được cập nhật nhưng biểu diễn chỉ mục được nhóm lại chưa nhất quán hoàn toàn.

Điều quan trọng là, doanh nghiệp không cần tính tức thời theo từng bản ghi. Những gì họ cần là ngừng chờ đợi lịch trình batch. Đây là nhận thức lớn đầu tiên: Streaming cấp độ bản ghi đang giải quyết một vấn đề chúng tôi không có đồng thời tạo ra những vấn đề chúng tôi không muốn.

Hội tụ về Micro-Batch Streaming

Chúng tôi chuyển sang micro-batch streaming một cách có chủ đích. Mục tiêu không phải là xử lý bản ghi liên tục, mà là tính sẵn sàng liên tục. Micro-batching cho phép chúng tôi loại bỏ các khoảng trống lập lịch trong khi vẫn giữ lại ngữ nghĩa hướng tới batch ở những nơi quan trọng.

Về mặt vận hành, công việc được cấu hình với khoảng thời gian kích hoạt cố định khoảng 30 giây. Mỗi lần thực hiện kích hoạt theo một chuỗi có giới hạn và xác định:

  1. Xác định thời gian hiện tại.
  2. Tính toán phân vùng mới nhất nên được xem xét đủ điều kiện dựa trên thời gian và quy tắc phân vùng.
  3. So sánh phân vùng đó với watermark hiện tại (phân vùng được xác nhận lần cuối).
  4. Nếu nhiều phân vùng đang chờ xử lý, chuyển thẳng sang phân vùng đủ điều kiện mới nhất thay vì xử lý các phân vùng trung gian.
  5. Xử lý tối đa một phân vùng giới hạn cho mỗi chu kỳ kích hoạt.

Tiến độ do đó được thúc đẩy bởi thời gian và hướng tới tính mới thay vì tuần tự nghiêm ngặt. Pipeline này ưu tiên tính mới bằng cách chuyển thẳng sang phân vùng có sẵn mới nhất, chấp nhận rằng các trạng thái trung gian sẽ được bao phủ ngầm bởi các tính toán cửa sổ chồng lấn.

Nguồn và Đích: Object Storage không phải là tùy chọn

Cả nguồn và đích của pipeline đều là object storage (lưu trữ đối tượng). Dữ liệu gia tăng đến dưới dạng các tệp hoặc phân vùng, được xử lý và ghi lại trước khi được tiêu thụ hạ lưu. Hệ thống dựa vào các đường dẫn phân vùng theo thời gian như /năm/tháng/ngày/giờ/phút.

Object storage thiếu ngữ nghĩa tiến trình theo từng bản ghi gốc. Việc hoàn thành được suy ra, không được đảm bảo. Các danh sách tệp có thể không hoàn chỉnh do tính nhất quán cuối cùng (eventual consistency).

Bắt đầu sai lầm: Tệp thành công và Marker hoàn thành

Nỗ lực đầu tiên của chúng tôi cho việc nhập streaming đã tái sử dụng logic tệp thành công (success-file) và marker hoàn thành từ pipeline batch. Trong mô hình batch theo lịch trình, cách tiếp cận này hoạt động khá tốt. Tuy nhiên, trong một công việc streaming chạy liên tục, mô hình này bị phá vỡ.

Chúng tôi gặp các vấn đề như:

  • Các marker hoàn thành xuất hiện muộn hoặc không nhất quán do tính hiển thị không nguyên tử giữa tệp dữ liệu và marker.
  • Các danh sách phân vùng tạm thời không hoàn chỉnh, ngay cả khi dữ liệu đã được ghi.
  • Công việc streaming thăm dò liên tục các marker đã tồn tại nhưng chưa hiển thị.

Rõ ràng là tín hiệu hoàn thành dựa trên tệp quá mong manh đối với một hệ thống cần tiến trình xác định và dựa trên thời gian.

Mẫu hình: Tiến trình xác định với Trigger dựa trên tốc độ

Chúng tôi thay thế việc phát hiện tệp thành công bằng một trigger dựa trên tốc độ thực thi mỗi 30 giây. Hệ thống không đợi một tín hiệu hoàn thành cụ thể, mà tiến triển xác định dựa trên thời gian và so sánh watermark.

Không có thông báo hệ thống tệp, không có marker hoàn thành, và không có logic "đợi cho đến khi phân vùng đủ cũ". Mỗi chu kỳ kích hoạt làm cùng một việc đơn giản:

  • Liệt kê các phân vùng hiện có thể nhìn thấy trong object storage.
  • Xác định phân vùng mới nhất có thể nhìn thấy dựa trên thứ tự thời gian.
  • So sánh nó với watermark.
  • Xử lý phân vùng mới nhất và nâng cao watermark nếu nó mới hơn.
  • Nếu không, thoát ngay lập tức.

Chỉ có phân vùng mới nhất được xử lý. Điều này làm cho tiến trình trở nên xác định: Pipeline chuyển tiếp bất cứ khi nào một phân vùng mới hơn trở nên hiển thị và không bao giờ bị chặn đợi một tệp marker cụ thể xuất hiện.

Mẫu hình: Xử lý độ trễ bằng cách chọn Tính mới

Độ trễ (lag) đưa ra một câu hỏi không rõ ràng: Điều gì sẽ xảy ra nếu nhiều phân vùng mới trở nên hiển thị giữa các chu kỳ kích hoạt? Trong mô hình streaming tuần tự nghiêm ngặt, hệ thống sẽ xử lý từng phân vùng chưa xử lý theo thứ tự. Cách tiếp cận đó đã bị từ chối có chủ đích ở đây. Thay vào đó, quy tắc rất đơn giản và nhất quán:

  • Liệt kê phân vùng mới nhất có thể nhìn thấy.
  • So sánh nó với watermark.
  • Nếu nó mới hơn, chỉ xử lý phân vùng mới nhất đó.
  • Bỏ qua bất kỳ phân vùng trung gian nào.

Hệ thống được thiết kế để ưu tiên tính mới (freshness-driven). Nếu các phân vùng P1, P2 và P3 trở nên hiển thị trong khi watermark ở P0, lần thực hiện tiếp theo sẽ xử lý P3, không phải P1 và P2.

Hành vi này an toàn vì delta index hoạt động trên một cửa sổ trượt chồng lấn. Mỗi lần chạy delta tính toán lại một cửa sổ thời gian gần đây thay vì áp dụng các thay đổi gia tăng nhỏ. Các cửa sổ này chồng lấn nhau, vì vậy hầu hết các phân vùng bị bỏ qua đều được bao phủ tự nhiên trong các lần chạy sau.

Mẫu hình: Khởi động lại bằng cách nhảy sang Mới nhất

Vì tiến trình được định nghĩa nghiêm ngặt theo thuật ngữ "phân vùng mới nhất có thể nhìn thấy so với watermark", hành vi khởi động lại không cần bất kỳ logic phát lại nào. Khi khởi động lại, công việc sẽ:

  • Tính toán lại phân vùng mới nhất có thể nhìn thấy trong object storage.
  • So sánh nó với watermark đã lưu trữ.
  • Nếu mới hơn, chỉ xử lý phân vùng mới nhất đó.
  • Nếu không, thoát và đợi chu kỳ kích hoạt tiếp theo.

Không có nỗ lực phát lại các phân vùng trung gian hay tái tạo một danh sách các công việc bị tụt hậu. Cùng quy tắc ưu tiên tính mới được áp dụng nhất quán trong cả thực thi ổn định và phục hồi, điều này làm cho các lần khởi động lại có thể dự đoán và nhanh chóng.

Thực thi liên tục và Áp lực bộ nhớ

Chạy liên tục trong một công việc streaming dựa trên JVM với thời gian sống dài đã phơi bày những vấn đề mà các công việc batch theo lịch trình chưa bao giờ gặp phải. Theo thời gian, việc sử dụng bộ nhớ heap tăng dần mặc dù tốc độ đầu vào ổn định, các tạm dừng Garbage Collection trở nên thường xuyên hơn và thời gian hoàn thành micro-batch trở nên ít dự đoán hơn.

Mẫu hình: Khởi động lại có kế hoạch như một công cụ vận hành

Thay vì cố gắng chống lại hành vi này, chúng tôi chấp nhận nó. Công việc được thiết kế để tự động khởi động lại, ngay cả khi khỏe mạnh, với chu kỳ 24 giờ. Việc khởi động lại có kế hoạch đạt được những điều sau:

  • Giải phóng bộ nhớ tích lũy.
  • Đặt lại trạng thái thực thi nội bộ.
  • Cho phép mã mới được chọn mà không cần sự can thiệp thủ công.

Thử giữ cho công việc chạy vô thời hạn chứng tỏ khó khăn hơn là khởi động lại nó một cách sạch sẽ. Lựa chọn thiết kế này giải quyết nhiều lo ngại vận hành ban đầu.

Kết quả và Kết luận

Sau khi di chuyển, độ trễ end-to-end giảm khoảng 50%. Độ trễ tồi tệ nhất, ví dụ, được giảm từ khoảng mười phút xuống còn 30 giây, chủ yếu do loại bỏ độ trễ của bộ lập lịch và điều phối chứ không phải do thay đổi chi phí xử lý mỗi batch.

Trong môi trường sản xuất, nơi tính mới của delta ảnh hưởng trực tiếp đến khả năng hiển thị của quảng cáo, chúng tôi quan sát thấy:

  • Sự lan truyền nhanh hơn của các bản cập nhật gia tăng.
  • Hành vi phục hồi có thể dự đoán.
  • Ít sự cố liên quan đến bộ nhớ hơn.
  • Việc triển khai các thay đổi mã dễ dàng hơn.

Kết quả này đạt được mà không cần cam kết với streaming cấp độ bản ghi. Cách tiếp cận này phù hợp nhất với các hệ thống hướng tới batch, nơi tính mới gia tăng là quan trọng, nhưng tính tức thời theo từng bản ghi thì không. Nó hoạt động đặc biệt tốt khi việc nhập dữ liệu dựa vào object storage, nơi các tín hiệu hoàn thành được suy ra và tính nhất quán cuối cùng làm cho sự phối hợp dựa trên tệp trở nên mong manh.

Bài học lớn nhất là thiết kế streaming tốt nhất là thiết kế hoạt động đáng tin cậy trong sản xuất, không phải thiết kế trông thanh lịch nhất trong lý thuyết.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗