0

Kỹ thuật Script Query: "Vũ khí tối thượng" khi các câu truy vấn thông thường báo tay

Nếu anh em làm backend và phải thường xuyên làm việc với dữ liệu lớn, chắc hẳn chúng ta đều quen thuộc với các phép truy vấn tiêu chuẩn như SELECT... WHERE trong SQL, hay các query match, term, range trong Elasticsearch. Chúng giải quyết được 90% nhu cầu bài toán hàng ngày và chạy cực kỳ tối ưu vì tận dụng được Index.

Nhưng cuộc đời developer đâu phải lúc nào cũng màu hồng. Sẽ có những ngày đẹp trời, requirement từ business dội xuống những logic "ngang trái" mà các query tiêu chuẩn đành "bó tay".

Đó là lúc chúng ta phải nhờ đến Script Query (Truy vấn bằng mã kịch bản). Hôm nay, mình sẽ lấy ví dụ thực tế trên Elasticsearch (với ngôn ngữ Painless) – nơi mà kỹ thuật này được sử dụng cực kỳ phổ biến – để chia sẻ với anh em nhé.

1. Bối cảnh khi query thông thường rơi vào bế tắc

Để mình kể cho anh em nghe một bài toán thực tế. Trong quá trình làm việc với hệ thống bán vé tự động AFC cho tuyến Metro, team mình phải xử lý một lượng transaction log khổng lồ mỗi ngày. Mọi thứ rất êm đẹp cho đến khi có một yêu cầu filter dữ liệu thế này: "Hãy tìm tất cả các giao dịch mà thời gian hành khách quẹt thẻ đi ra (tap-out) trừ đi thời gian quẹt thẻ đi vào (tap-in) lớn hơn 120 phút, đồng thời tính toán thêm một mức phí phạt động (penalty fee) dựa trên tỷ lệ của từng nhà ga cụ thể".

Nếu kéo toàn bộ dữ liệu về Application (Node.js hay Golang) để dùng vòng lặp for tính toán thì toang, sập RAM chắc chắn. Mà query trực tiếp thì DB không có sẵn trường duration hay penalty_fee (vì tỷ lệ phạt thay đổi liên tục, không thể lưu cứng vào database lúc insert được).

Giải pháp? Bơm thẳng một đoạn code logic vào trong Database để nó tự tính. Đó chính là Script Query.

2. Script Query hoạt động như thế nào?

Nói một cách đơn giản, Script Query cho phép bạn viết những đoạn mã lập trình thực thụ (có khai báo biến, điều kiện if/else, vòng lặp, các phép toán phức tạp...) lồng ngay bên trong câu truy vấn Database.

Thay vì chỉ so sánh giá trị tĩnh, Database sẽ lấy từng bản ghi (document), chạy đoạn script của bạn trên bản ghi đó, tính toán ra kết quả on-the-fly (ngay lúc truy vấn) rồi mới quyết định xem bản ghi đó có match hay không.

Ví dụ một đoạn Script Query bằng ngôn ngữ Painless trong Elasticsearch để giải quyết bài toán trừ thời gian ở trên:

GET /metro_transactions/_search
{
  "query": {
    "script": {
      "script": {
        "source": """
          // Tính thời gian chênh lệch bằng phút
          def duration = (doc['tap_out_time'].value.toInstant().toEpochMilli() - doc['tap_in_time'].value.toInstant().toEpochMilli()) / 60000;
          
          // Trả về true nếu thời gian lớn hơn tham số truyền vào
          return duration > params.max_duration;
        """,
        "params": {
          "max_duration": 120
        }
      }
    }
  }
}

} Nhìn xịn không? Anh em có thể nhét cả một rổ logic vào đây. Nhưng khoan vội mừng...

3. Cạm bẫy: "Con dao hai lưỡi" mang tên Performance

Quyền lực càng lớn, trách nhiệm càng cao. Script Query là một "vũ khí tối thượng", nhưng nếu lạm dụng, nó sẽ biến thành "quả bom" dội ngược lại hệ thống của bạn.

Tại sao? Vì Script Query thường không thể sử dụng Index (Inverted Index).

Khi bạn chạy một câu query bình thường, DB tìm trong Index và ra kết quả trong vài mili-giây. Còn khi chạy Script, DB buộc phải lôi từng document ra, nạp vào bộ nhớ, rồi chạy đoạn code của bạn trên từng document đó (quá trình này gọi là Table Scan hoặc Full Scan).

Hồi mới dùng, mình từng hồn nhiên vã một cái Script Query rà soát qua vài triệu log giao dịch mà không có điều kiện lọc (filter) nào đi kèm. Kết quả là CPU của con server ES dựng đứng lên 100%, các service khác gọi vào bị timeout hàng loạt.

4. Kinh nghiệm "tồn tại" khi dùng Script Query

Từ những lần sập hệ thống đó, mình rút ra được vài nguyên tắc sống còn khi làm việc với kỹ thuật này:

  • Luôn thu hẹp phạm vi dữ liệu trước (Filter first): Đừng bao giờ chạy Script Query trên toàn bộ database. Hãy dùng các query thông thường (term, range) để thu hẹp số lượng bản ghi xuống mức tối thiểu (ví dụ: chỉ lấy log của ngày hôm nay, của một nhà ga cụ thể), sau đó mới đưa Script Query vào làm lớp lọc cuối cùng.
  • Luôn dùng tham số (Params): Đừng bao giờ hard-code giá trị (như số 120 phút) trực tiếp vào trong chuỗi source của script. Hãy truyền nó qua block params (như ví dụ code của mình ở trên). Lý do là Database sẽ cache lại đoạn script đã được biên dịch (compile). Nếu bạn truyền giá trị thẳng vào script, mỗi lần giá trị đổi là DB lại phải tốn CPU compile lại đoạn code đó từ đầu.
  • Tính toán trước (Pre-computation) nếu có thể: Trở lại bài toán Metro của mình, nếu cái duration (thời gian đi) là thứ thường xuyên phải query, thì cách tốt nhất không phải là dùng Script Query. Cách tốt nhất là sửa lại code Backend lúc insert log, tự tính luôn thời gian đó và lưu vào một trường mới tên là duration_minutes. Disk space rẻ hơn CPU rất nhiều! Chỉ dùng Script Query cho những bài toán logic động không thể lưu tĩnh.

Lời kết

Script Query giống như một người lính cứu hỏa: chỉ nên gọi khi thực sự có cháy lớn và các phương pháp thông thường không thể dập được. Hiểu rõ bản chất của nó, biết khi nào nên dùng và khi nào nên tránh sẽ giúp anh em thiết kế ra những hệ thống vừa linh hoạt lại vừa chịu tải tốt.

Anh em có từng phải viết những đoạn query "dài như cái sớ" nào để giải quyết logic phức tạp chưa? Cùng chia sẻ ở dưới nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí