Giới thiệu

Trong bốn bài viết trước, chúng ta đã tìm hiểu về cách dữ liệu được lưu trữ trong TSDB. Giờ là lúc tìm hiểu cách truy vấn dữ liệu. Trong bài này, chúng ta sẽ xem xét 3 loại truy vấn được thực hiện trên persistent blocks và tóm tắt về Head block.

Phần 4 là điều kiện tiên quyết cho bài này, thảo luận về cách dữ liệu được lưu trữ trong các khối liên tục.

Đừng nhầm lẫn truy vấn này với truy vấn PromQL. Trong bài viết này, chúng ta sẽ xem xét các truy vấn TSDB cấp thấp được sử dụng để lấy dữ liệu thô từ TSDB. Công cụ PromQL thực hiện các truy vấn TSDB để lấy dữ liệu thô và thực thi logic PromQL trên đó. Vì vậy, chúng ta đang làm việc ở cấp độ thấp hơn công cụ PromQL.

Các loại truy vấn TSDB

Có 3 loại truy vấn mà chúng tôi chạy trên persistent blocks tại thời điểm viết bài đăng trên blog này.

  1. LabelNames(): trả về tất cả các tên label duy nhất có trong khối.
  2. LabelValues(name): trả về tất cả các giá trị label có thể có cho tên label name như được thấy trong index.
  3. Select([]matcher): trả về samples cho phần bộ so khớp được chỉ định cho series. Chúng ta sẽ nói thêm về các bộ so khớp này sau.

Trước khi chạy bất kỳ truy vấn nào trên khối, chúng ta tạo một thứ gọi là Querier trên khối, chứa thời gian tối thiểu (mint) và thời gian tối đa (maxt) để truy vấn được chạy. Thời gian mintmaxt chỉ áp dụng cho truy vấn Select, trong khi hai giá trị còn lại luôn xem xét tất cả các giá trị trong khối.

Chúng ta sẽ thảo luận về cách kết hợp kết quả từ nhiều khối sau khi xem xét cả 3 loại truy vấn.

LabelNames()

Lệnh này trả về tất cả các tên label duy nhất có trong khối. Tóm lại, trong chuỗi {a="b", c="d"}, các tên label là "a"và "c".

Trong Phần 4, có đề cập rằng Label Offset Table không còn được sử dụng nữa và chỉ được viết để tương thích ngược. Do đó, cả hai đều LabelNames() được LabelValues() sử dụng Postings Offset Table.

Khi index của khối được tải khi khởi động (hoặc tạo khối), chúng tôi lưu trữ bản đồ map[labelName][]postingOffset của tên label vào danh sách vị trí của một số giá trị label trong postings offset table (mỗi 32 giây tại thời điểm này, bao gồm giá trị label đầu tiên và cuối cùng). Việc chỉ lưu trữ một phần giá trị giúp tiết kiệm bộ nhớ. Bản đồ này được tạo bằng cách lặp qua tất cả các mục nhập Postings Offset Tablekhi tải khối.

Bây giờ bạn có thể hình dung cách chúng ta lấy tên label – chỉ cần lặp lại bản đồ in-memory này để tìm khóa và bạn sẽ có tên label. Chúng được sắp xếp trước khi trả về. Điều này hữu ích cho các gợi ý tự động hoàn thành truy vấn trên UI.

LabelValues(name)

Chúng ta đã thấy ở trên rằng chúng ta lưu trữ vị trí của giá trị label đầu tiên và cuối cùng trong bộ nhớ cho tất cả các tên label. Do đó, đối với truy vấn LabelValues(name), chúng ta lấy vị trí giá trị label đầu tiên và cuối cùng cho label đã cho name và lặp lại trên đĩa giữa hai vị trí đó để lấy tất cả các giá trị label cho tên label đó. Một tóm tắt khác ở đây: tất cả các giá trị label cho một tên label được lưu trữ cùng nhau theo thứ tự từ điển trong Postings Offset Table.

Ví dụ nếu series trong khối là {a="b1", c="d1"}{a="b2", c="d2"}và {a="b3", c="d3"}, thì LabelValues("a") sẽ cho ra ["b1", "b2", "b3"]LabelValues("c") sẽ cho ra ["d1", "d2", "d3"].

Điều này một lần nữa giúp ích cho các gợi ý tự động hoàn thành truy vấn.

Select([]matcher)

Truy vấn này giúp lấy các samples TSDB thô từ series được mô tả bởi các bộ so khớp đã cho. Trước khi tìm hiểu về truy vấn này, chúng ta cần biết bộ so khớp là gì.

Matcher

Bộ so khớp sẽ cho biết tổ hợp giá trị tên label cần khớp trong một series. Ví dụ: bộ so khớp a="b" sẽ yêu cầu chọn tất cả các series có cặp label a="b".

Có 4 loại bộ so khớp

  1. Bằng nhau labelName="<value>": tên label phải khớp chính xác với giá trị đã cho.
  2. Không bằng nhau labelName!="<value>": tên label không được khớp chính xác với giá trị đã cho.
  3. Regex Equal labelName=~"<regex>": giá trị label cho tên label phải thỏa mãn regex đã cho.
  4. Regex không bằng labelName!~"<regex>": giá trị label cho tên label không được thỏa mãn regex đã cho.

labelName là tên label đầy đủ và không cho phép sử dụng regex ở đó. Bộ so khớp regex phải khớp toàn bộ giá trị label chứ không phải một phần vì nó được gắn với ^(?:<regex>)$ trước khi sử dụng.

Giả sử các chuỗi là

  • s1 ={job="app1", status="404"}
  • s2 ={job="app2", status="501"}
  • s3 ={job="bar1", status="402"}
  • s4 ={job="bar2", status="501"}

Dưới đây là một số ví dụ về trình so khớp

  • status="501"-> (s2, s4)
  • status!="501"-> (s1, s3)
  • job=~"app.*"-> (s1, s2)
  • job!~"app.*"-> (đoạn 3, đoạn 4)

Và khi có >1 bộ so khớp, thì đó là phép toán AND (tức là giao điểm) giữa tất cả các bộ so khớp.

  • job=~"app.*", status="501"-> (s1, s2) ∩ (s2, s4) -> (s2)
  • job=~"bar.*", status!~"5.."-> (s3, s4) ∩ (s1, s3) -> (s3)

Select samples

Bước đầu tiên là lấy series mà các bộ so khớp khớp với nhau. Chúng ta cần lấy tất cả series của từng bộ so khớp và cuối cùng là giao nhau.

Chúng ta đã thấy trong phần 4 rằng “posting” là series ID cho chúng ta biết vị trí của thông tin series trong index Postings Offset Table và Postings i cùng đưa ra tất cả posting cho một cặp label-value.

Postings cho một matcher

Nếu là một bộ so khớp Equal, chẳng hạn a="b", chúng ta sẽ lấy trực tiếp vị trí danh sách postings từ postings offset table. Vì chúng ta chỉ lưu trữ vị trí cho một số giá trị label của một tên, chúng ta sẽ lấy hai giá trị nằm giữa, "b" rơi vào tên label a, và lặp lại các mục giữa chúng cho đến khi tìm thấy "b". Mục a="b" trong offset table sẽ trỏ đến một danh sách posting, bao gồm tất cả các id chuỗi chứa a="b". Nếu không có mục nào như vậy trong offset table, thì đó là một danh sách postings trống cho bộ so khớp.

Đối với Regex Equal a=~"<rgx>", chúng ta phải lặp qua tất cả các giá trị label của a trong Postings Offset Table và kiểm tra điều kiện của bộ so khớp. Chúng ta lấy danh sách postings của tất cả các mục đã khớp và hợp nhất (union) để có được danh sách postings đã được sắp xếp cho bộ so khớp này. Lấy ví dụ job=~"app.*" ở trên, chúng ta tìm thấy job="app1" -> (s1) và job="app2" -> (s2), và sau khi hợp nhất, chúng ta có job=~"app.*" -> (s1, s2).

Với Not Equal a!="b" và Regex Not Equal a!~"<rgx>", cách chúng ta sử dụng nó trong nội bộ có đôi chút khác biệt. Chúng ta lấy Equal và Regex Equal cho Not Equal và Regex Not Equal tương ứng (tức là a!="b" trở thành a="b" và a!~"<rgx>" trở thành a=~"<rgx>"), vì việc lấy tất cả những gì không khớp có thể khá phức tạp trong thực tế. Do đó, bạn không thể sử dụng một bộ so khớp phủ định độc lập trong truy vấn, bạn cần có ít nhất một bộ so khớp Equal hoặc Regex Equal . Chúng ta lấy các giá trị này sau khi chuyển đổi và thực hiện phép trừ tập hợp. Xem ví dụ bên dưới.

Postings cho nhiều matcher

Sử dụng quy trình trên, trước tiên chúng ta sẽ có được danh sách postings cho tất cả các bộ so khớp riêng lẻ. Và, tương tự như những gì chúng ta đã thảo luận về bộ so khớp trước đó, chúng ta giao nhau chúng để cuối cùng có được danh sách postings (series) thỏa mãn tất cả các bộ so khớp. Lưu ý sự thay đổi trong phép toán tập hợp khi chúng ta có một bộ so khớp phủ định.

job=~"bar.*", status!~"5.*"

->(job=~"bar.*") ∩ (status!~"5.*")

->(job=~"bar.*") - (status=~"5.*")

->((job="bar1") ∪ (job="bar2")) - (status="501")

->((s3) ∪ (s4)) - (s2, s4)

-> (s3, s4) - (s2, s4)->(s3)

Tương tự như vậy, nếu các phép so khớp là a="b", c!="d", e=~"f.*", g!~"h.*", thì các phép toán thiết lập sẽ là ((a="b") ∩ (e=~"f.*")) - (c="d") - (g=~"h.*").

Nhận samples cuối cùng

Khi chúng ta có tất cả các series ID (postings) cho các đối sánh, chúng ta chỉ cần xem xét từng cái một và thực hiện như sau

  1. Đi tới series trong bảng Series được biểu thị bằng series ID.
  2. Chọn tất cả các tham chiếu khối từ series đó trùng với phạm vi thời gian mint do maxt mà truy vấn chỉ định.
  3. Tạo một trình lặp để lặp qua các phần này từ thư mục chunks cho các mẫu giữa mint và maxt.

Cuối cùng Select([]matcher) trả về các trình lặp sample cho tất cả series khớp với các trình so khớp. Các series được sắp xếp theo cặp label của chúng.

Một số triển khai bổ sung

  • Khi lấy các postings cho một trình so khớp, tất cả postings cho tất cả các mục khớp không được đưa vào bộ nhớ cùng một lúc. Vì index được ánh xạ bộ nhớ từ đĩa, postings được lặp lại một cách chậm rãi và được hợp nhất để có được danh sách cuối cùng.
  • Tất cả các trình lặp sample cho tất cả các series không được trả về trước bởi Select([]matcher); kết quả có thể là hàng trăm nghìn series. Chúng hoạt động tương tự như trên. Một trình lặp được trả về, lặp lại series từng cái một, tạo ra trình lặp sample của nó. Và trình lặp sample cũng tải các khối một cách lười biếng khi được yêu cầu.

Truy vấn nhiều block

Khi bạn có nhiều khối chồng chéo với phần mint xuyên suốt maxt của bộ truy vấn, bộ truy vấn thực chất là một bộ truy vấn hợp nhất, chứa các bộ truy vấn cho từng khối riêng lẻ. 3 truy vấn này giờ đây thực sự thực hiện các thao tác sau:

  1. LabelNames(): lấy tên label đã sắp xếp từ tất cả các khối và thực hiện hợp nhất N chiều.
  2. LabelValues(name): lấy giá trị label từ tất cả các khối và thực hiện hợp nhất N chiều.
  3. Select([]matcher): lấy trình lặp series từ tất cả các khối bằng phương thức Select và thực hiện lại phép hợp nhất N chiều lười biếng theo kiểu trình lặp. Điều này khả thi vì các trình lặp series riêng lẻ trả về series theo thứ tự được sắp xếp theo cặp label .

Truy vấn Head block

Head block lưu trữ toàn bộ bản đồ các cặp label-value và tất cả danh sách postings trong bộ nhớ (một ví dụ về biểu diễn Go map[labelName]map[labelValue]postingsList), do đó không cần phải đặc biệt cẩn thận khi truy cập chúng. Quy trình còn lại để thực hiện 3 truy vấn vẫn giữ nguyên với bản đồ và danh sách postings.

Code tham khảo

tsdb/index/index.go có mã để thực hiện các truy vấn LabelNames() và LabelValues(name) trên persistent block và cũng để lấy danh sách postings đã hợp nhất cho tên label và giá trị đã cho (không phải chính trình so khớp).

tsdb/querier.go có mã để thực hiện truy vấn  Select([]matcher)trên persistent block bao gồm lọc các giá trị label cho các bộ so khớp trước khi yêu cầu chỉ mục cho danh sách postings.

tsdb/chunks/chunks.go có mã để lấy các chunks từ đĩa.

tsdb/head.go có mã để thực hiện cả 3 truy vấn trên Head block.

tsdb/db.go và storage/merge.go có mã cho truy vấn được hợp nhất khi có nhiều khối liên quan đến truy vấn.

Leave a Reply

This site uses cookies to offer you a better browsing experience. By browsing this website, you agree to our use of cookies.