Chúng tôi vận hành cả một hệ thống auth trên những kho lưu trữ chỉ biết get và put
Một hệ thống auth trông có vẻ đòi hỏi một cơ sở dữ liệu quan hệ. Người dùng, vai trò, phiên đăng nhập, OAuth grant, refresh token, tất cả tham chiếu chéo lẫn nhau. Hệ thống của chúng tôi thì không dùng cái nào như vậy. Nó chạy trên Azure Table Storage, một kho lưu trữ chỉ cho bạn một partition key, một row key, và gần như không gì khác. Không join, không secondary index nào đáng kể, không toán tử increment, không giao dịch nhiều dòng theo kiểu bạn đã quen. Đó là một lựa chọn có chủ đích. Đây là những gì lựa chọn đó mang lại, những pattern giúp nó hoạt động, và cả phần mà SQL hẳn sẽ thực sự dễ dàng hơn.
Lý do làm vậy là chi phí và sự đơn giản trong vận hành. Không có connection pool nào để cạn kiệt, không có database instance nào để định kích thước, vá lỗi hay fail over, và bạn được mở rộng theo từng partition miễn phí. Giữ một bảng gần như không tốn gì và chẳng có thứ gì để mà cạn cả. Cái giá phải trả là một kho lưu trữ không có query planner, thứ chỉ có lợi khi các access pattern của bạn dễ đoán đúng bằng mức mà kho lưu trữ này ngờ nghệch. Access pattern của auth thì đúng như vậy, nên toàn bộ thiết kế dựa hẳn vào điều đó.
Cái key làm luôn công việc mà một bố cục bảng lẽ ra phải làm. Không có join, bạn thiết kế key sao cho chính key là câu truy vấn. Bạn cố tình denormalize, chọn partition sao cho các lượt đọc nóng đều là point-get, và mã hóa danh tính tổ hợp thẳng vào row key, ví dụ "{pk}|{rk}" cho những bản ghi cần một danh tính ghép trong một kho chỉ cung cấp đúng một chỗ cho row key. Câu hỏi "tôi tra cứu cái này bằng cách nào" phải được trả lời ngay lúc bạn thiết kế key, chứ không phải lúc truy vấn. Với auth thì ổn thôi, vì các access pattern đã biết rõ và chúng không trôi dạt: lấy một user theo id, lấy các grant của một user, tiêu thụ một code. Bạn không đi khám phá dữ liệu. Bạn biết trước mọi câu hỏi.
Bạn tự giữ change log của riêng mình. Mỗi dòng đều mang một Timestamp do máy chủ quản lý, và ta rất dễ bị cám dỗ dựng backup tăng dần dựa trên nó: kéo về mọi thứ có Timestamp gt watermark. Cách đó chạy tốt trong bản demo và chết ngay trên môi trường production, vì kho lưu trữ chỉ đánh index cho partition key và row key, ngoài ra không gì khác. Timestamp không nằm trong bất kỳ index nào, nên lọc theo nó sẽ phải soi từng dòng trong bảng. Đó là một full scan khoác lên bộ áo của một câu truy vấn, và nó chậm đi từng ngày một khi bảng lớn lên. Thế nên thay vào đó kho lưu trữ tự ghi change log của chính nó. Mỗi lần thay đổi, mỗi upsert và mỗi delete, đều thêm một dòng vào một bảng change log lấy tên bảng logic làm khóa, với tổ hợp "{pk}|{rk}" làm row key và một cờ đánh dấu dòng đó được ghi hay bị xóa. Giờ đây "có gì thay đổi kể từ watermark" là một lượt đọc một partition nhỏ, có index, thay vì một lượt quét cả bảng, và bản backup chỉ point-read đúng những dòng thực sự có biến động. Bạn dựng lại change-data-capture từ những lượt ghi thông thường, và nó mở rộng theo lượng thay đổi chứ không theo kích thước của bảng.
Xử lý đồng thời mà không cần giao dịch. Ở đây không có SELECT ... FOR UPDATE. Thay vào đó bạn có đúng một nguyên thủy nguyên tử: lượt ghi có điều kiện, trao cho bạn dưới dạng một ETag. Bạn chỉ ghi một dòng nếu bản sao bạn đọc chưa bị đổi ngầm bên dưới. Mọi thứ cần an toàn khi bị truy cập đồng thời đều được diễn đạt bằng optimistic concurrency cộng với thử lại khi có xung đột. Ví dụ rõ nhất là bộ đếm khóa tài khoản. AccessFailedCount cần tăng lên một cách nguyên tử, nhưng kho lưu trữ lại không có phép increment. Vậy nên bạn đọc dòng đó, tăng bộ đếm, ghi lại với điều kiện dựa trên ETag bạn vừa đọc, và thử lại nếu có ai đó nhanh tay hơn bạn. Vòng lặp đó là cách bạn khiến một bộ đếm trở nên nguyên tử trên một kho lưu trữ vốn không có bộ đếm. Bắn năm mươi mật khẩu sai cùng một lúc thì cả năm mươi lần đều được ghi nhận, vì xung đột được phát hiện và thử lại chứ không bị mất.
Một lượt xóa có thể là một cái khóa. Tiêu thụ một lần, một authorization code hay một token dùng một lần phải được đổi đúng một lần duy nhất, chính là một lượt xóa có điều kiện. Đó là một ETag conditional-delete: nếu xóa thành công thì bạn đã thắng cuộc đua và được phép đi tiếp; nếu thất bại vì dòng đó đã biến mất rồi, thì ai đó đã tiêu thụ nó trước và bạn dừng lại. Chính lượt xóa là cái khóa. Cùng ý tưởng đó làm luôn việc leader election, xây trên một blob lease, một cái khóa được tạo ra từ một lượt ghi nguyên tử chứ không phải từ một dịch vụ điều phối riêng. Bạn gần như không bao giờ cần một dịch vụ khóa khi bản thân lượt ghi đã là nguyên tử.
Việc xóa phải là công dân hạng nhất, và đó là một nửa lý do change log tồn tại. Một kho key-value không có delete marker: một dòng bị xóa chỉ đơn giản là bốc hơi, nên bất cứ thứ gì quét tìm thay đổi cũng chẳng bao giờ thấy được là nó đã ra đi. Một bản backup dựa vào timestamp của dòng sẽ lặng lẽ kéo người dùng đã bị xóa đi tiếp mãi mãi. Change log ghi lại việc xóa đúng như một lượt xóa, nên khi phục hồi nó sẽ phát lại việc xóa thay vì làm sống lại bản ghi. Kiểu hỏng đó, một bản backup trung thành mang trở lại tất cả những người bạn đã xóa, tự nó là cả một câu chuyện và xứng đáng có một bài viết riêng, nhưng pattern này thuộc về danh sách ở đây: trong một kho lưu trữ không có delete log, bạn tự giữ lấy delete log.
Giờ đến mặt bên kia của sổ cái, những gì bạn phải từ bỏ. Bạn từ bỏ các truy vấn tùy hứng: một access pattern thực sự mới có thể đồng nghĩa với một thiết kế key mới hoặc một full scan, vì chẳng có query planner nào cứu bạn. Bạn từ bỏ join, nên bạn phải tự tay duy trì các bản sao đã denormalize và tự chịu trách nhiệm về tính nhất quán của chúng. Bạn từ bỏ giao dịch nhiều dòng, nên bạn thiết kế mọi bất biến sao cho nằm gọn trong một item duy nhất, vì tính nguyên tử ở mức một item là tất cả những gì bạn được trao. Nếu dữ liệu của bạn mang tính quan hệ sâu sắc, hoặc các access pattern còn chưa biết và vẫn đang dịch chuyển, thì đây là công cụ sai và nó sẽ khiến bạn khốn khổ.
Và đó chính xác là lý do nó hợp với auth. Một cơ sở dữ liệu quan hệ đáng đồng tiền của nó nhờ trả lời được những câu hỏi bạn còn chưa nghĩ ra. Một hệ thống auth thì không có những câu hỏi đó. Tập hợp những thứ bạn cần hỏi, lấy user này, tiêu thụ code này, bầu leader này, backup những gì đã thay đổi, là nhỏ, đã biết, và ổn định. Hãy từ bỏ cái query planner mà đằng nào bạn cũng chẳng định dùng, và đổi lại bạn có được một tầng lưu trữ gần như không tốn gì để vận hành và chẳng có gì phải fail over. Với phần lớn phần mềm thì đó là một cuộc đổi chác tồi. Với cái này, đó là lựa chọn đúng.
Những pattern này chính là storage engine bên dưới Authagonal: một thiết kế key-value gần như không tốn gì để vận hành, và đó là một phần lớn lý do vì sao chúng tôi đưa mọi tính năng vào mọi gói.