Giới thiệu

Trong bài trước TSDB, tôi đã đề cập rằng chúng ta ghi incoming samples vào Write-Ahead-Log (WAL) trước để đảm bảo độ bền và khi WAL này bị cắt bớt, một checkpoint được tạo. Trong bài viết này, chúng ta sẽ thảo luận ngắn gọn về những điều cơ bản của WAL và sau đó đi sâu vào cách WAL và checkpoints được thiết kế trong TSDB của Prometheus.

Cơ bản về WAL

WAL là log tuần tự các sự kiện xảy ra trong cơ sở dữ liệu. Trước khi ghi/sửa/xóa dữ liệu trong cơ sở dữ liệu, sự kiện trước tiên được ghi lại (thêm vào) vào WAL, sau đó các thao tác cần thiết mới được thực hiện trong cơ sở dữ liệu.

Dù vì lý do gì, nếu máy tính hoặc chương trình gặp sự cố, bạn sẽ có các sự kiện được ghi lại trong WAL này và có thể phát lại theo cùng thứ tự để khôi phục dữ liệu. Điều này đặc biệt hữu ích cho cơ sở dữ liệu in-memory, nơi nếu cơ sở dữ liệu gặp sự cố, toàn bộ dữ liệu trong bộ nhớ sẽ bị mất nếu không có WAL.

Điều này được sử dụng rộng rãi trong cơ sở dữ liệu quan hệ để đảm bảo độ bền (Durability trong ACID) cho cơ sở dữ liệu. Tương tự, Prometheus có WAL để đảm bảo độ bền cho Head block. Prometheus cũng sử dụng WAL để khởi động lại một cách nhẹ nhàng nhằm khôi phục trạng thái in-memory.

Trong ngữ cảnh của Prometheus, WAL chỉ được sử dụng để ghi lại các sự kiện và khôi phục trạng thái in-memory khi khởi động. Nó không tham gia vào bất kỳ hoạt động đọc hoặc ghi nào khác.

Ghi vào WAL trong Prometheus TSDB

Các loại bản ghi

Yêu cầu ghi trong TSDB bao gồm các giá trị label của series và các samples tương ứng . Điều này cung cấp cho chúng ta hai loại bản ghi, Series và Samples.

Bản ghi Series bao gồm các giá trị label của tất cả series trong yêu cầu ghi. Việc tạo series sẽ tạo ra một tham chiếu duy nhất có thể được sử dụng để tra cứu series. Do đó, bản ghi Samples chứa tham chiếu của series tương ứng và danh sách các samples thuộc chuỗi đó trong yêu cầu ghi.

Loại bản ghi cuối cùng là Tombstones được sử dụng cho các yêu cầu xóa. Nó chứa tham chiếu series đã xóa cùng với khoảng thời gian cần xóa.

Định dạng của các bản ghi này có thể được tìm thấy tại đây , chúng tôi sẽ không thảo luận về chúng trong bài đăng trên blog.

Ghi dữ liệu

Bản ghi Samples được ghi cho tất cả các yêu cầu ghi có chứa sample. Bản ghi Series chỉ được ghi một lần cho một series khi chúng ta nhìn thấy nó lần đầu tiên (do đó cần “tạo” nó trong Head).

Nếu yêu cầu ghi chứa một series mới, bản ghi Series luôn được ghi trước bản ghi Samples, nếu không, trong quá trình phát lại, tham chiếu series trong bản ghi Samples sẽ không trỏ đến bất kỳ chuỗi nào nếu bản ghi Samples được đặt trước Series.

Bản ghi Series được viết sau khi tạo series trong Head để lưu trữ tham chiếu trong bản ghi, trong khi bản ghi Samples được viết trước khi thêm samples vào Head.

Chỉ một bản ghi Series và Samples được ghi cho mỗi yêu cầu ghi bằng cách nhóm tất cả các time series khác nhau (và các samples của các time series khác nhau) vào cùng một bản ghi. Nếu series cho tất cả các mẫu trong yêu cầu đã tồn tại trong Head, chỉ một bản ghi Samples được ghi vào WAL.

Khi nhận được yêu cầu xóa, chúng tôi không xóa ngay lập tức khỏi bộ nhớ. Chúng tôi lưu trữ một thứ gọi là “tombstones” (bia mộ), cho biết series đã xóa và khoảng thời gian xóa. Chúng tôi ghi một bản ghi Tombstones vào WAL trước khi xử lý yêu cầu xóa.

Nó trông như thế nào trên đĩa

WAL được lưu trữ theo tuần tự các tệp được đánh số, mỗi tệp có dung lượng mặc định là 128MB. Tệp WAL ở đây được gọi là “segment”.

data
└── wal
    ├── 000000
    ├── 000001
    └── 000002

Kích thước của một tệp được giới hạn để việc thu gom rác các tệp cũ trở nên đơn giản hơn. Như bạn có thể đoán, số thứ tự luôn tăng.

WAL truncation và Checkpoint

Chúng ta cần thường xuyên xóa các phân đoạn WAL cũ, nếu không disk sẽ bị lấp đầy và khởi động TSDB sẽ mất rất nhiều thời gian vì nó phải phát lại tất cả các sự kiện trong WAL này (nơi hầu hết sẽ bị loại bỏ vì nó cũ). Nói chung, bất kỳ dữ liệu nào không còn cần thiết, bạn muốn loại bỏ nó.

WAL truncation

Việc cắt bớt WAL được thực hiện ngay sau khi khối Head bị cắt bớt. Các tệp không thể bị xóa ngẫu nhiên và việc xóa sẽ diễn ra đối với N tệp đầu tiên mà không tạo ra khoảng trống trong chuỗi.

Vì các yêu cầu ghi có thể ngẫu nhiên, nên việc xác định phạm vi thời gian của samples trong một phân đoạn WAL mà không duyệt qua tất cả các bản ghi là không dễ dàng hoặc hiệu quả. Vì vậy, chúng tôi xóa 2/3 phân đoạn đầu tiên.

data
└── wal
    ├── 000000
    ├── 000001
    ├── 000002
    ├── 000003
    ├── 000004
    └── 000005

Trong ví dụ trên, các tập tin 000000 000001 000002 000003 sẽ bị xóa.

Có một vấn đề ở đây: các bản ghi chuỗi chỉ được ghi một lần, vì vậy nếu bạn vô tình xóa các phân đoạn WAL, bạn sẽ mất các bản ghi đó và do đó không thể khôi phục lại các chuỗi đó khi khởi động. Ngoài ra, có thể có các samples trong 2/3 phân đoạn đầu tiên đó chưa bị cắt khỏi Head, do đó bạn cũng mất chúng. Đây chính là lúc checkpoints phát huy tác dụng.

Checkpoint

Trước khi cắt bớt WAL, chúng ta tạo một “checkpoint” từ các phân đoạn WAL cần xóa. Bạn có thể coi checkpoint như một WAL đã được lọc. Hãy xem xét nếu việc cắt bớt Head xảy ra đối với dữ liệu trước thời gian T, lấy ví dụ về bố cục WAL ở trên, thao tác tạo điểm kiểm tra sẽ duyệt qua tất cả các bản ghi theo 000000 000001 000002 000003thứ tự và:

  1. Xóa tất cả các bản ghi của series không còn trong Head.
  2. Loại bỏ tất cả các samples trước thời hạn T.
  3. Xóa tất cả các bản ghi tombstone trong khoảng thời gian trước T.
  4. Giữ lại các series, samples và tombstone còn lại theo cách bạn tìm thấy trong WAL (giống thứ tự xuất hiện trong WAL).

Hoạt động xóa cũng có thể là hoạt động ghi lại trong khi xóa các mục không cần thiết khỏi bản ghi (vì một bản ghi có thể chứa nhiều hơn một series, sample hoặc tombstone).

Bằng cách này, bạn sẽ không bị mất series, samples và tombstones vẫn còn trong Head. Checkpoint được đặt tên theo checkpoint.X với X là phân đoạn cuối cùng mà checkpoint được tạo (00003 tại đây; bạn sẽ biết lý do tại sao chúng tôi lại làm như vậy trong phần tiếp theo).

Sau khi cắt bớt WAL và tạo checkpoint, các tệp trên đĩa trông giống như thế này (checkpoint trông giống như một WAL khác):

data
└── wal
    ├── checkpoint.000003
    |   ├── 000000
    |   └── 000001
    ├── 000004
    └── 000005

Nếu có bất kỳ checkpoints cũ hơn, chúng sẽ bị xóa vào thời điểm này.

Replaying WAL

Đầu tiên, chúng ta lặp lại các bản ghi theo thứ tự từ checkpoint cuối cùng (checkpoint có số lớn nhất được liên kết với nó là điểm cuối cùng). Đối với checkpoint.XX cho chúng ta biết chúng ta cần tiếp tục phát lại từ phân đoạn WAL nào, và đó là X+1. Vì vậy, trong ví dụ trên, sau khi phát lại checkpoint.000003, chúng ta tiếp tục phát lại từ phân đoạn WAL 000004.

Bạn có thể đang thắc mắc tại sao chúng ta cần theo dõi số phân đoạn trong điểm kiểm tra trong khi chúng ta vẫn xóa các phân đoạn WAL trước đó. Vấn đề là, việc tạo checkpoint và xóa các phân đoạn WAL không phải là nguyên tử. Bất cứ điều gì cũng có thể xảy ra ở giữa và ngăn chặn việc xóa các phân đoạn WAL. Vì vậy, chúng ta sẽ phải phát lại các 2/3 phân đoạn WAL bổ sung đã bị xóa, khiến việc phát lại chậm hơn.

Khi nói về các bản ghi riêng lẻ, các hành động sau đây sẽ được thực hiện trên chúng:

  1. Series: Tạo series trong Head với cùng tham chiếu như được đề cập trong bản ghi (để chúng ta có thể so khớp các samples sau này). Có thể có nhiều bản ghi series cho cùng một series, Prometheus xử lý điều này bằng cách ánh xạ các tham chiếu.
  2. Samples: Thêm samples từ bản ghi này vào Head. Tham chiếu trong bản ghi cho biết series nào cần thêm vào. Nếu không tìm thấy series nào cho tham chiếu, sample sẽ bị bỏ qua.
  3. Tombstones: Lưu trữ tombstones đó trở lại Head bằng cách sử dụng tham chiếu để xác định series.

Chi tiết về việc đọc ghi từ WAL

Khi các yêu cầu ghi đến với khối lượng lớn, bạn nên tránh ghi ngẫu nhiên vào đĩa để tránh khuếch đại ghi. Ngoài ra, khi đọc bản ghi, bạn cần đảm bảo rằng nó không bị hỏng (điều này rất dễ xảy ra khi tắt máy đột ngột hoặc đĩa bị lỗi).

Prometheus có một triển khai WAL tổng quát, trong đó một bản ghi chỉ là một lát cắt byte và người gọi phải đảm nhiệm việc mã hóa bản ghi. Để giải quyết hai vấn đề trên, gói WAL thực hiện như sau:

  1. Dữ liệu được ghi vào đĩa theo từng trang một. Mỗi trang có dung lượng 32KiB. Nếu bản ghi lớn hơn 32KiB, nó sẽ được chia thành các phần nhỏ hơn, mỗi phần sẽ nhận được một tiêu đề bản ghi WAL để kế toán xác định xem phần đó là phần cuối, phần đầu hay phần giữa của bản ghi (một bản ghi sẽ nhận được tiêu đề bản ghi WAL ngay cả khi nó vừa với trang).
  2. Một checksum của bản ghi được thêm vào cuối để phát hiện bất kỳ lỗi nào trong khi đọc.

Gói WAL đảm nhiệm việc ghép nối liền mạch các phần bản ghi và kiểm tra checksum của bản ghi trong khi lặp lại các bản ghi để phát lại.

Theo mặc định, các bản ghi WAL không được nén nhiều (hoặc không được nén chút nào). Vì vậy, gói WAL cung cấp tùy chọn nén các bản ghi bằng Snappy (hiện đã được bật theo mặc định). Thông tin này được lưu trữ trong tiêu đề bản ghi WAL, do đó, các bản ghi đã nén và chưa nén có thể tồn tại cùng nhau nếu bạn định bật hoặc tắt tính năng nén.

Code tham khảo

Việc triển khai WAL, lấy bản ghi dưới dạng lát cắt byte và thực hiện tương tác đĩa cấp thấp, có trong tsdb/wal/wal.go. Tệp này có triển khai cho cả việc ghi bản ghi byte và lặp lại bản ghi (một lần nữa dưới dạng lát cắt byte).

tsdb/record/record.go chứa nhiều bản ghi khác nhau cùng với logic mã hóa và giải mã của nó.

Logic checkpoint có trong tsdb/wal/checkpoint.go.

tsdb/head.go chứa phần còn lại:

  1. Tạo và mã hóa các bản ghi và gọi lệnh ghi WAL.
  2. Gọi checkpoint và cắt bớt WAL.
  3. Phát lại các bản ghi WAL, giải mã chúng và khôi phục trạng thái in-memory.

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.