
Instant 1.0, phần phụ trợ cho các ứng dụng được mã hóa AI
Instant 1.0, a backend for AI-coded apps
Instant 1.0 đã ra mắt! Bài luận này trình bày một loạt bản trình diễn nhằm giải thích lý do tại sao chúng tôi cho rằng Instant là chương trình phụ trợ tốt nhất cho các ứng dụng được mã hóa bằng AI. Chúng tôi cũng đề cập đến kiến trúc giúp tất cả hoạt động.
Sau 4 năm, chúng tôi sẽ phát hành Instant 1.0!
Ngay lập tức biến tác nhân mã hóa yêu thích của bạn thành trình tạo ứng dụng toàn diện. Và chúng tôi hoàn toàn là nguồn mở. [1]
Chúng tôi khẳng định rằng Instant là chương trình phụ trợ tốt nhất mà bạn có thể sử dụng cho các ứng dụng được mã hóa AI.
Trong bài đăng này, chúng tôi sẽ thực hiện hai việc. Trước tiên, chúng tôi sẽ hiển thị cho bạn một loạt bản demo để bạn có thể tự đánh giá. Thứ hai, chúng ta sẽ đề cập đến kiến trúc.
Những hạn chế đằng sau phần phụ trợ theo thời gian thực, có quan hệ và nhiều bên thuê đã thúc đẩy chúng tôi hướng tới một số lựa chọn thiết kế thú vị. Chúng tôi đã xây dựng cơ sở dữ liệu nhiều người thuê trên Postgres và công cụ đồng bộ hóa trong Clojure. Chúng tôi sẽ đề cập đến cách thức hoạt động của tất cả những điều này và những gì chúng tôi đã học được cho đến nay.
Bắt tay vào thôi.
Bản demo
Khi chọn Instant, bạn nhận được ba lợi ích:
- Bạn có thể tạo số lượng ứng dụng không giới hạn và chúng không bao giờ bị đóng băng.
- Bạn có công cụ đồng bộ hóa để ứng dụng của bạn hoạt động ngoại tuyến, hoạt động theo thời gian thực và cảm thấy nhanh chóng.
- Và khi bạn cần thêm tính năng, bạn đã tích hợp sẵn dịch vụ: xác thực, lưu trữ tệp, hiện diện và luồng.
Để hiểu ý của chúng tôi, tôi sẽ đi sâu vào từng điểm và cho bạn biết chúng trông như thế nào.
Ứng dụng không giới hạn
Theo truyền thống, khi muốn lưu trữ ứng dụng trực tuyến, bạn phải trả tiền cho máy ảo hoặc bạn bị giới hạn. Nhiều dịch vụ giới hạn số lượng ứng dụng miễn phí bạn có thể tạo và đóng băng chúng khi chúng không hoạt động. Quá trình rã đông thường có thể mất hơn 30 giây và đôi khi mất cả phút.
Chúng tôi nghĩ điều này thật tệ. Vì vậy, với Instant, bạn có thể quay bao nhiêu dự án tùy thích và chúng tôi sẽ không bao giờ đóng băng chúng.
Chúng tôi có thể thực hiện việc này vì Instant được thiết kế dành cho nhiều người thuê. Khi bạn tạo một dự án mới, chúng tôi sẽ không tạo máy ảo. Chúng tôi chỉ chèn một vài hàng cơ sở dữ liệu vào một phiên bản có nhiều đối tượng thuê.
Nếu ứng dụng của bạn không hoạt động thì bạn sẽ không phải trả phí điện toán hoặc bộ nhớ nào cả. Và khi nó hoạt động, nó chỉ tốn thêm vài kilobyte RAM — trái ngược với hàng trăm megabyte cần thiết cho máy ảo.
Điều này có nghĩa là bạn thực sự có thể tạo các ứng dụng không giới hạn. Trên thực tế, quy trình này hiệu quả đến mức chúng tôi có thể tạo một ứng dụng cho bạn ngay trong bài luận này. Không cần đăng ký.
Nếu nhấp vào nút, bạn sẽ nhận được phần phụ trợ riêng biệt:
Và nhờ đó chúng tôi có phần phụ trợ. Bao gồm cả hành trình khứ hồi tới máy tính của bạn, toàn bộ quá trình mất vài trăm mili giây. Thời gian thực tế:
Bạn nhận được ID ứng dụng công khai để xác định chương trình phụ trợ của mình và Mã thông báo quản trị riêng cho phép bạn thực hiện các thay đổi đặc quyền. Điều này cung cấp cho bạn cơ sở dữ liệu quan hệ, công cụ đồng bộ hóa và các dịch vụ bổ sung mà chúng tôi đã đề cập, như xác thực và lưu trữ.
Kết hợp vô số ứng dụng với đại lý và bạn sẽ bắt đầu xây dựng một cách khác biệt. Ngày nay, bạn đã có thể sử dụng các tác nhân để tạo ra nhiều ứng dụng. Với Instant, bạn sẽ không bao giờ bị cản trở việc đưa chúng vào sản xuất.
Công cụ đồng bộ hóa
Nhưng một khi bạn đã tạo một ứng dụng, bạn làm cách nào để làm cho ứng dụng đó hoạt động tốt?
Thật dễ dàng để xây dựng một ứng dụng CRUD truyền thống. Chỉ cần nhờ một đại lý thực hiện một số hoạt động di chuyển cơ sở dữ liệu, điểm cuối phụ trợ và cửa hàng phía máy khách. Nhưng thật khó để làm cho những ứng dụng này thú vị.
So sánh ứng dụng CRUD truyền thống với các ứng dụng hiện đại như Linear, Notion và Figma. Các ứng dụng hiện đại có nhiều người chơi, chúng hoạt động ngoại tuyến và cho cảm giác nhanh. Nếu bạn thay đổi việc cần làm trong Tuyến tính, nó sẽ thay đổi ở mọi nơi. Nếu bạn ngoại tuyến trong Notion, bạn vẫn có thể đánh dấu tài liệu của mình. Khi bạn tô màu một hình trong Figma, nó không chờ máy chủ mà bạn chỉ nhìn thấy hình đó.
Những loại ứng dụng này cần cơ sở hạ tầng tùy chỉnh. Đối với thời gian thực, bạn thêm các máy chủ websocket trạng thái. Đối với chế độ ngoại tuyến, bạn lưu trữ bộ đệm trong IndexedDB. Và để có những cập nhật tích cực, bạn tìm ra cách áp dụng và hoàn tác các đột biến trong ứng dụng khách.
Linear, Notion và Figma đều đã xây dựng cơ sở hạ tầng tùy chỉnh để xử lý việc này. Với tư cách là một ngành, chúng tôi gọi các công cụ đồng bộ hóa cơ sở hạ tầng của họ là [2]. Các nhà phát triển viết giao diện người dùng và truy vấn dữ liệu của họ như thể nó có sẵn tại địa phương. Công cụ đồng bộ hóa xử lý toàn bộ hoạt động quản lý dữ liệu.
Nếu các ứng dụng hiện đại cần công cụ đồng bộ hóa thì bạn không cần phải xây dựng chúng từ đầu.
Vì vậy, chúng tôi đã xây dựng công cụ đồng bộ hóa tổng quát trong Instant. Theo mặc định, mọi ứng dụng đều có chế độ nhiều người chơi, chế độ ngoại tuyến và các bản cập nhật tích cực.
Bạn có thể tự mình thử. Vì chúng tôi đã tạo phần phụ trợ riêng biệt nên hãy tiếp tục và sử dụng nó:
Xây dựng phần phụ trợ để dùng thử bản demo trực tiếp.
Những gì bạn đang thấy là hai iframe hiển thị ứng dụng việc cần làm. Chúng được hỗ trợ bởi chương trình phụ trợ bạn vừa tạo (chúng tôi đã chuyển iframe ID ứng dụng của bạn).
Bây giờ, nếu bạn thêm việc cần làm vào một khung nội tuyến thì nó sẽ hiển thị trong khung nội tuyến khác. Nếu bạn ngoại tuyến, bạn có thể thực hiện các thay đổi và chúng sẽ đồng bộ hóa với nhau. Bạn có thể thử hạ cấp mạng của mình và các thay đổi sẽ vẫn diễn ra nhanh chóng.
Và đây là mã phụ trợ của ứng dụng việc cần làm:
Đó là khoảng
25
dòng. Điều này thậm chí còn ngắn gọn hơn so với khi bạn xây dựng một ứng dụng CRUD truyền thống. Bạn sẽ cần phải viết các điểm cuối phụ trợ và các cửa hàng giao diện người dùng. Thay vào đó, bạn chỉ cần thực hiện các truy vấn và giao dịch trực tiếp trong giao diện người dùng của mình.
db.useQuery cho phép bạn viết các truy vấn quan hệ và chúng luôn được đồng bộ hóa. db.transact cho phép bạn thực hiện các thay đổi và nó hoạt động ngoại tuyến.
Điều này sẽ tốt hơn cho bạn với tư cách là người xây dựng: mã dễ hiểu và dễ bảo trì. Điều đó tốt hơn cho người dùng của bạn: họ có được một ứng dụng thú vị. Và nó tốt hơn cho các đại lý của bạn. Công cụ đồng bộ hóa là một công cụ trừu tượng chặt chẽ [3], do đó, tổng đài viên có thể sử dụng chúng để viết mã ngắn gọn hơn với ít mã thông báo hơn và ít lỗi hơn.
Dịch vụ bổ sung
Bạn đã thấy đồng bộ hóa dữ liệu nhưng chưa dừng lại ở đó. Các ứng dụng thường cần nhiều thứ hơn là đồng bộ hóa dữ liệu.
Ví dụ: hiện tại, mọi người mở ứng dụng demo của chúng tôi đều thấy cùng một nhóm việc cần làm. Nếu chúng ta muốn thêm xác thực hoặc quyền thì sao? Chúng tôi cũng có thể muốn hỗ trợ tải tệp lên hoặc phần “ai đang trực tuyến”. Hoặc có thể chúng tôi sẽ thêm trợ lý AI và sẽ cần cơ sở hạ tầng để truyền mã thông báo đến khách hàng.
Đây là những tính năng phổ biến mà hầu hết ứng dụng đều cần. Nhưng thường thì chúng ta phải kết hợp các dịch vụ khác nhau lại với nhau để có được chúng. Điều đó không chỉ gây khó chịu mà còn đưa ra một mức độ phức tạp mới. Khi bạn quản lý nhiều dịch vụ, bạn quản lý nhiều nguồn thông tin đáng tin cậy.
Vì vậy, để giúp cải tiến ứng dụng của bạn dễ dàng hơn, chúng tôi đã tích hợp nhiều dịch vụ phổ biến vào Instant. Mỗi dịch vụ được xây dựng để hoạt động cùng nhau như một hệ thống tích hợp duy nhất.
Để hiểu rõ hơn về các dịch vụ này, hãy xem lại ứng dụng việc cần làm của chúng tôi, nhưng lần này chúng tôi sẽ thêm tính năng hỗ trợ tải tệp lên:
Xây dựng phần phụ trợ để thử bản trình diễn tải tệp lên.
Cách truyền thống để thực hiện việc này là gì? Trước tiên, chúng tôi sẽ tạo bảng tệp trong giao dịch của mình cơ sở dữ liệu và liên kết nó với todos. Nhưng sau đó chúng tôi sẽ cần lưu trữ các đốm màu tệp thực tế nên có thể chúng tôi sẽ thêm S3.
Sau khi thêm S3, chúng tôi có nhiều nguồn thông tin đáng tin cậy cần xử lý. Ví dụ: nếu chúng tôi xóa một việc cần làm, chúng tôi sẽ cần chạy một trình xử lý nền để loại bỏ blob tương ứng trong S3.
Với Instant, tất cả những điều này không thành vấn đề.
Theo mặc định, bạn nhận được Bộ nhớ tệp và đối tượng tệp chỉ là các hàng trong cơ sở dữ liệu của bạn. Chúng cũng giống như bất kỳ thực thể nào khác: bạn có thể tạo chúng, liên kết chúng với dữ liệu khác và chạy truy vấn theo thời gian thực đối với chúng.
Điều này có nghĩa là bạn thậm chí có thể tạo quy tắc xóa CASCADE để bạn có thể nói “khi bạn xóa việc cần làm, hãy xóa tệp”. Không cần nhân viên nền. Thay vì có nhiều nguồn sự thật, bạn sẽ có được một cơ sở dữ liệu tích hợp. Cơ sở hạ tầng dùng chung xử lý tất cả các trường hợp biên bên trong [4].
Và đây chỉ là Bộ nhớ tức thì. Bạn cũng nhận được Auth. Bạn có thể sử dụng Magic Code, OAuth và Guest Auth ngay lập tức. Ngoài ra, khi người dùng đăng ký, họ cũng chỉ là các hàng trong cơ sở dữ liệu của bạn.
Nếu bạn muốn chia sẻ con trỏ, chỉ báo đang nhập hoặc điểm đánh dấu 'ai đang trực tuyến', bạn có thể sử dụng Hiện diện tức thì.
Và nếu bạn cần chia sẻ các luồng lâu dài, bạn sẽ nhận được Luồng tức thì.
Nếu bạn tò mò, chúng tôi có rất nhiều ví dụ thực tế mà bạn có thể thử trên trang công thức nấu ăn. Bạn sẽ nhận thấy rằng hầu hết các dịch vụ này yêu cầu ít thiết lập và ít mã. Cả bạn và đại lý của bạn đều có thể di chuyển nhanh hơn và làm cho ứng dụng của bạn có nhiều tính năng hơn. Bạn không cần phải tìm kiếm các nhà cung cấp khác nhau và xử lý việc đồng bộ hóa dữ liệu hai chiều.
Phần thưởng: Những gì bạn có thể làm, người đại diện của bạn cũng có thể làm
Trong suốt bài luận này, có thể bạn đã tự hỏi, tất cả các bản demo này hoạt động như thế nào?
Chà, Instant hoàn toàn có lập trình. Bạn có thể tạo ứng dụng, đẩy lược đồ và cập nhật quyền thông qua API hoặc CLI. Bài luận này sử dụng API nhưng có thể nhân viên hỗ trợ của bạn sẽ sử dụng CLI.
Hầu hết bạn không cần phải nhấp vào bất kỳ trang tổng quan nào. Người đại diện của bạn chỉ có thể thay mặt bạn thực hiện các hành động.
Tại thời điểm này, chúng tôi hy vọng bạn đủ hào hứng để đăng ký.
Về mặt kỹ thuật, bạn thậm chí không cần phải đăng ký để chơi thử nhưng chúng tôi nhận thấy rằng nếu bạn làm vậy, bạn sẽ có nhiều khả năng tiếp tục tham gia hơn. Vì vậy chúng tôi thực sự khuyến khích bạn làm như vậy!
Và nếu bạn muốn các đại lý của mình chơi với Instant ngay lập tức thì đây là một số điều bạn có thể làm:
# Điều này tạo ra bước khởi đầu mới cho bạn trong NextJS, Tanstack, Bun, Vite hoặc Expo
# Đại lý của bạn sẽ có mọi thứ cần thiết để xây dựng
npx tạo-ứng dụng tức thì
# Nếu hiện có ứng dụng, bạn cũng có thể thêm kỹ năng hữu ích của chúng tôi và thông báo cho đại lý của mình
# để tạo một số tính năng mới
kỹ năng npx thêm instantdb/skills
Và với điều đó, chúng ta có thể đi sâu vào kiến trúc hỗ trợ tất cả những điều này.
Kiến trúc
Có ba điều độc đáo về cách hoạt động của Instant. Chúng tôi có SDK khách hàng, Phần phụ trợ Clojure và Cơ sở dữ liệu nhiều người thuê.

Ứng dụng của bạn gửi truy vấn và giao dịch trực tiếp tới SDK khách hàng. Nó chịu trách nhiệm giải quyết các truy vấn của bạn ngoại tuyến và áp dụng các giao dịch ngay khi bạn thực hiện chúng.
Sau đó, SDK khách sẽ giao tiếp với Phần cuối của Clojure. Phần cuối Clojure lưu giữ các truy vấn theo thời gian thực. Nó thực hiện các giao dịch và tìm ra khách hàng nào cần biết về chúng. Nó cũng triển khai tất cả các dịch vụ bổ sung: quyền, xác thực, hiện diện, lưu trữ và luồng.
Cuối cùng, Phần cuối của Clojure gửi các truy vấn và giao dịch đến một Phiên bản Postgres duy nhất. Chúng tôi coi Postgres là cửa hàng Bộ ba có nhiều người thuê và phân tách mọi cơ sở dữ liệu theo ID ứng dụng một cách hợp lý.
Đó là bản phác thảo hệ thống của chúng tôi. Bây giờ hãy tìm hiểu sâu hơn.
SDK ứng dụng khách
Thiết kế đằng sau SDK khách được thúc đẩy bởi hai hạn chế: chúng tôi cần một hệ thống hoạt động ngoại tuyến và chúng tôi cần nó để hỗ trợ các bản cập nhật lạc quan.
Đại khái chúng ta đã kết thúc ở đây:

DB được lập chỉ mục
Hãy bắt đầu với ô rõ ràng nhất. Nếu muốn hiển thị ứng dụng ngoại tuyến, chúng tôi cần một nơi để dữ liệu tồn tại trong các lần làm mới.
Đối với web, bạn không có quá nhiều lựa chọn. IndexedDB là ứng cử viên tốt nhất. Bạn có thể lưu trữ nhiều megabyte dữ liệu và thậm chí bạn còn có một số khả năng truy vấn hạn chế.
Vì vậy, chúng tôi đã chọn IndexedDB [5]. Câu hỏi tiếp theo là chúng tôi sẽ lưu trữ loại dữ liệu nào ở đó?
Ba cửa hàng
Hãy xem xét truy vấn như “Hiển thị cho tôi tất cả các việc cần làm đang mở và tệp đính kèm của chúng”. Đây là cách bạn sẽ viết nó trong Instant:
{
việc cần làm: {
$: { ở đâu: { xong: false ,
tệp đính kèm: { ,
,
Nếu chỉ muốn có bộ đệm chỉ đọc, chúng tôi có thể lưu trữ bất kỳ nội dung nào mà máy chủ trả về cho chúng tôi. Nhưng chúng tôi không chỉ muốn có bộ nhớ đệm chỉ đọc.
Chúng tôi cần ứng dụng khách phản hồi các hành động trước khi máy chủ xác nhận chúng. Ví dụ: nếu người dùng thêm một việc cần làm mới thì truy vấn của chúng tôi sẽ cập nhật ngay lập tức.
Điều đó có nghĩa là khách hàng cần hiểu các truy vấn. Vì vậy, những gì khách hàng của chúng tôi thực sự cần là một cơ sở dữ liệu. Cơ sở dữ liệu có thể xử lý các mệnh đề trong đó (tức là 'xong là sai') và các mối quan hệ ('todos và tệp đính kèm của họ').
Một tùy chọn là sử dụng SQLite. Chúng tôi có thể lưu trữ các bảng chuẩn hóa ở đó — như todos và files — và chạy SQL trên chúng. Nhưng điều này quá nặng nề. SQLite có dung lượng nén khoảng 300 KB. Đối với hầu hết các ứng dụng, việc thêm phần phụ thuộc nặng nề như vậy sẽ không có ý nghĩa gì.
Sau một số điều tra, chúng tôi đã phát hiện ra Triple store và Datalog.
Ba cửa hàng cho phép bạn lưu trữ dữ liệu dưới dạng bộ [thực thể, thuộc tính, giá trị]. Đây là giao diện của những việc cần làm bên trong cửa hàng Triple:
Nhiệm vụ nhóm / #42
Xuất hàng!
Cấu trúc thống nhất này có thể mô hình hóa cả thuộc tính và mối quan hệ. Sau khi dữ liệu được lưu trữ theo cách này, bạn có thể sử dụng Datalog để thực hiện truy vấn dựa trên dữ liệu đó.
Datalog là công cụ truy vấn dựa trên logic. Nó trông như thế này:
InstaQL
{ todos: { $: {where: { done: } } }
Nhật ký dữ liệu
[[[T AG_380]]?việc cần làm"xong"][[T AG_387]] [?todo?attr[[TA G_394]]?val]
Cú pháp có vẻ kỳ lạ nhưng Datalog rất mạnh mẽ. Nó có thể hỗ trợ các mệnh đề và quan hệ cũng như SQL. Và nó rất đơn giản để thực hiện. Trên thực tế, bạn có thể viết một công cụ Datalog cơ bản với chưa đầy một trăm dòng mã [6].
Vì vậy, chúng tôi đã xây dựng một cửa hàng Triple và công cụ Datalog. Điều này cho phép chúng tôi đánh giá các truy vấn hoàn toàn trong ứng dụng khách mà không cần phải đợi máy chủ.
Nếu người dùng tạo việc cần làm mới, chúng tôi có những thứ mình cần để chạy lại truy vấn và quan sát thay đổi ngay lập tức. Vâng, gần như vậy. Chúng tôi cần một cách để áp dụng các thay đổi cho truy vấn của mình.
Hàng đợi đang chờ xử lý
Chúng tôi không thể chỉ thay đổi kết quả tại chỗ. Chúng ta cũng phải quan tâm đến máy chủ.
Ví dụ: điều gì sẽ xảy ra nếu máy chủ từ chối giao dịch của chúng tôi? Nếu chúng tôi thay đổi kết quả truy vấn, sẽ không có cách nào để chúng tôi hoàn tác thay đổi đó. [7]
Đó là lúc Hàng chờ chờ xuất hiện. Khi người dùng thực hiện thay đổi, chúng tôi không áp dụng thay đổi đó trực tiếp cho cửa hàng Triple. Thay vào đó, chúng tôi theo dõi sự thay đổi trong một hàng đợi riêng biệt.
Để đáp ứng bất kỳ truy vấn nào, chúng tôi có thể áp dụng các thay đổi đang chờ xử lý cho ba cửa hàng của mình và xem kết quả:
+
Hàng đợi đang chờ xử lý
=
Lựa chọn này thúc đẩy chúng tôi biến cửa hàng Triple của mình thành bất biến. Bằng cách này, chúng ta có thể áp dụng thay đổi và tạo ra một cửa hàng Triple mới, thay vì thay đổi cửa hàng đã cam kết. Để thực hiện được điều này, chúng tôi bao bọc API giao dịch bằng tính chất đột biến, một thư viện dành cho những thay đổi không thể thay đổi trong Javascript [8].
Với điều đó, chúng tôi đã hoàn tác. Nếu máy chủ trả về lỗi, chúng tôi chỉ cần xóa thay đổi khỏi hàng đợi đang chờ xử lý và hoàn tác các công việc ngay lập tức.
Phần thưởng: InstaQL
Tuy nhiên, bạn có thể nhận thấy rằng Truy vấn tức thì không giống Datalog. Thay vào đó, chúng được viết bằng ngôn ngữ mà chúng tôi gọi là InstaQL:
{
việc cần làm: {
$: { ở đâu: { xong: false ,
tệp đính kèm: { ,
,
Chúng tôi thực hiện điều này vì chúng tôi cho rằng cách thuận tiện nhất để các ứng dụng truy vấn dữ liệu là mô tả hình dạng của phản hồi mà chúng đang tìm kiếm.
Ý tưởng này rất khó khăn lấy cảm hứng từ GraphQL. Sự khác biệt chính trong cách triển khai của chúng tôi là đường cú pháp. Thay vì giới thiệu một ngữ pháp cụ thể, InstaQL được xây dựng dựa trên các đối tượng javascript đơn giản. Lựa chọn này cho phép người dùng bỏ qua bước xây dựng và cho phép họ tạo các truy vấn theo chương trình [9].
Lò phản ứng
Nhờ đó, chúng ta đã có cái nhìn đầy đủ về SDK ứng dụng khách!
Người dùng viết các truy vấn InstaQL và các truy vấn này sẽ được chuyển thành Datalog. Các truy vấn đó được đáp ứng bởi các cửa hàng Triple, nơi kết hợp các thay đổi từ hàng đợi đang chờ xử lý. Dữ liệu được lưu vào bộ nhớ đệm vào IndexedDB.
Có rất nhiều lựa chọn thú vị được tạo ra chỉ từ hai ràng buộc!
Câu hỏi cuối cùng dành cho khách hàng là: làm thế nào để tất cả các hộp này liên kết với nhau?
Đó là lúc Lò phản ứng xuất hiện. Đây là máy trạng thái chính điều phối tất cả các quy trình khác nhau này. Khi một ứng dụng muốn có một truy vấn, Reactor chịu trách nhiệm xem xét IndexedDB và liên lạc với máy chủ. Nó xử lý khi Internet ngoại tuyến hoặc các thay đổi đang chờ xử lý không thành công.
Lò phản ứng giao tiếp với máy chủ thông qua ổ cắm web. Nó gửi yêu cầu truy vấn và giao dịch, đồng thời máy chủ gửi kết quả và tính mới từ cơ sở dữ liệu.
Điều này đưa chúng ta đến máy chủ.
Phần cuối của Clojure
Thiết kế đằng sau phần phụ trợ được thúc đẩy bởi hai ràng buộc: chúng tôi cần thực hiện các truy vấn có tính phản hồi và chúng tôi cần phải công bằng về tài nguyên nhiều bên thuê.
Đây là cách hệ thống ngoại hình:

Cửa hàng truy vấn
Hãy bắt đầu bằng cách suy nghĩ xem điều gì sẽ xảy ra khi người dùng yêu cầu một truy vấn.
Đầu tiên, máy chủ có thể tiếp tục và hỏi cơ sở dữ liệu. Trong một hệ thống không quốc tịch, đó sẽ là phần cuối của câu chuyện. Chúng tôi có thể trả lời phản hồi của mình và kết thúc ngày hôm đó.
Nhưng hãy nhớ, các truy vấn của chúng tôi phải có tính phản hồi. Để làm được điều đó, chúng tôi cần một nơi để lưu trữ những truy vấn mà người dùng đã thực hiện mà truy vấn. Đó chính là mục đích của Cửa hàng truy vấn:

Nếu chúng tôi chỉ theo dõi các truy vấn và kết nối ổ cắm đã yêu cầu chúng thì về nguyên tắc, chúng tôi sẽ có những gì chúng tôi cần để khiến ứng dụng có phản hồi. Ví dụ: chúng tôi có thể theo dõi mọi giao dịch và làm mới mọi truy vấn. Điều đó sẽ hiệu quả nhưng cơ sở dữ liệu của chúng tôi sẽ bị tấn công bởi rất nhiều thư rác.
Lý tưởng nhất là chúng ta chỉ nên thay đổi các truy vấn mà cần cần thay đổi.
Chủ đề
Chúng tôi đã tìm kiếm ý tưởng và tìm thấy kiến trúc đằng sau Luna của Asana [10] và LiveGraph [11] của Figma rất hứa hẹn. Asana đã viết về cách họ biến các truy vấn thành các nhóm “chủ đề”. Đại khái, một chủ đề mô tả phần chỉ mục mà truy vấn được đề cập quan tâm.
Đối với những nội dung như “Cung cấp cho tôi tất cả việc cần làm”, bạn có thể tưởng tượng một chủ đề có nội dung: “Theo dõi tất cả các cập nhật đối với TodosIndex”.
Chúng tôi đã điều chỉnh ý tưởng này vào hệ thống của mình. Khi chạy truy vấn, chúng tôi cũng tạo ra một nhóm chủ đề mà nó quan tâm:

Đây là chủ đề của chúng tôi về “Xem tất cả việc cần làm”:

Bây giờ chúng ta có cấu trúc dữ liệu có thể sử dụng để mô tả các phần phụ thuộc cho một truy vấn. Bước tiếp theo là theo dõi các giao dịch và tìm những truy vấn bị ảnh hưởng này.
Trình vô hiệu hóa
Đó là lúc trình vô hiệu hóa xuất hiện. Trình vô hiệu hóa theo dõi WAL (Nhật ký ghi trước) của Postgres.
Chúng tôi cũng có thể lấy các mục WAL và tạo chủ đề từ chúng. Ví dụ: nếu chúng tôi có bản cập nhật như “Set todo.done = false for id = 42’”, chúng tôi có thể chuyển đổi nó:

Điều này giúp chúng tôi có được loại cấu trúc chủ đề giống hệt như các truy vấn của chúng tôi tạo ra. Bây giờ chúng ta có thể kết hợp chúng lại với nhau và khám phá những gì đã cũ:

Phiên bản 0 của chúng tôi cho thuật toán này rất kém hiệu quả. Chúng tôi sẽ thực hiện so sánh N^2 một cách hiệu quả từ mọi chủ đề giao dịch với mọi chủ đề truy vấn. Nhưng bạn có thể trực giác được các vectơ chủ đề này có thể tuân theo các chỉ mục như thế nào. Bây giờ chúng ta giữ chúng trong một cấu trúc giống như cây. Chúng tôi chỉ so sánh các tập hợp con và cắt tỉa sớm. [12]
Với điều đó, chúng tôi có thể lấy mục nhập WAL và làm mới các truy vấn dựa trên chúng. Bước tiếp theo là song song hóa.
Hàng đợi được nhóm
Vì cơ sở dữ liệu của chúng tôi có nhiều người thuê nên WAL của chúng tôi bao gồm các bản cập nhật từ nhiều ứng dụng.
Để thuật toán vô hiệu hóa hoạt động, các giao dịch trong một ứng dụng phải được xử lý tuần tự và theo thứ tự. Tuy nhiên, chúng tôi chắc chắn có thể song song hóa việc vô hiệu hóa trên các ứng dụng khác nhau.
Chúng tôi cần một số cách để đảm bảo trật tự trong một ứng dụng duy nhất và song song giữa các ứng dụng. Chúng tôi cũng cần đảm bảo rằng một ứng dụng có lưu lượng truy cập cao không ngốn hết tài nguyên.
Đây là lúc tính năng trừu tượng hóa Hàng đợi được nhóm xuất hiện:

Mỗi ứng dụng có hàng đợi phụ riêng. Điều này đảm bảo rằng tất cả các mục của một ứng dụng cụ thể đều được xử lý tuần tự.
Tuy nhiên, Worker có thể lấy từ nhiều hàng đợi phụ khác nhau. Điều này cho phép chúng tôi song song hóa việc vô hiệu hóa trên các ứng dụng.
Khi chúng tôi đẩy một mục WAL vào hàng đợi được nhóm, mục đó sẽ được thêm vào hàng đợi con của ứng dụng nhưng thứ tự chung của hàng đợi con không thay đổi. Điều này giúp cho ngay cả khi một ứng dụng đang thêm hàng nghìn mục mỗi giây thì các ứng dụng khác vẫn có cơ hội như nhau để được người xác nhận không hợp lệ chọn.
Cấu trúc dữ liệu này hóa ra lại rất hữu ích cho chúng tôi và đã lan rộng khắp cơ sở mã, bao gồm cả Trình quản lý phiên.
Trình quản lý phiên và lời khen ngợi dành cho Clojure và JVM
Điều này đưa chúng ta đến với người điều phối chính bên trong hệ thống. Khi SDK khách mở kết nối websocket, trình quản lý phiên sẽ nhận các thông báo:

Công việc của Người quản lý phiên là gắn kết mọi thứ lại với nhau. Nó tạo ra các truy vấn phản ứng, chạy các quyền và chuyển các yêu cầu đến các dịch vụ khác.
Lưu ý rằng phần trừu tượng của Hàng đợi được nhóm cũng xuất hiện ở đây. Nếu các máy khách khác nhau bắt đầu tấn công chương trình phụ trợ, Hàng đợi được nhóm sẽ đảm bảo song song hóa nhiều nhất có thể, đồng thời ngăn chặn một ổ cắm xấu ngốn tất cả tài nguyên.
Và với điều này, đây có thể là nơi thích hợp để tạm dừng và ca ngợi Clojure và JVM. Chúng đã mang lại thắng lợi to lớn cho chúng tôi trong việc xây dựng cơ sở hạ tầng này.
Đầu tiên, Clojure có các tính năng cơ bản đồng thời tuyệt vời và có các luồng thực. Điều này cho phép chúng tôi mở rộng quy mô hơn nữa với các máy lớn hơn và giúp chúng tôi tránh việc chia tách hệ thống quá sớm. Các phần trừu tượng cũng thực sự đơn giản và dễ sáng tác. Ví dụ: hàng đợi được nhóm của chúng tôi chỉ có 215 dòng mã [13]
Thứ hai, JVM có một hệ sinh thái phát triển mạnh và chúng tôi thực sự yêu thích các thư viện. Ví dụ: chúng tôi cần một cách để người dùng xác định quyền trong Instant. Chúng tôi muốn một ngôn ngữ có thể sandbox nhanh chóng và dễ dàng. Sau một hồi tìm kiếm, chúng tôi đã phát hiện ra CEL của Google. Rất may CEL Java đã có sẵn và chúng tôi có thể lấy nó ra khỏi kệ.
Và thứ ba, Clojure rất phù hợp cho DSL và lập trình thử nghiệm. Khi bắt đầu xây dựng Instant, chúng tôi phải khám phá rất nhiều nội dung trừu tượng này và việc sử dụng chúng trong REPL là một công cụ hữu ích.
Nhiều người chê bai DSL nhưng tôi nghĩ chúng tôi không thể xây dựng Instant nếu không có chúng. Trường hợp điển hình: truy vấn nhiều người thuê. Chúng tôi cần làm cho cơ sở dữ liệu của mình có nhiều người thuê. Để làm được điều đó chúng ta cần phải viết một số câu lệnh SQL khá phức tạp. Thay vì thực hiện việc này bằng tay, chúng tôi đã tạo một DSL vừa giúp bạn dễ dàng suy luận, vừa đảm bảo rằng bạn có thể chuyển ID ứng dụng.
Và điều này đưa chúng ta đến Cơ sở dữ liệu nhiều người thuê.
Cơ sở dữ liệu nhiều người thuê
Cơ sở dữ liệu của chúng tôi cũng được thúc đẩy bởi hai hạn chế: chúng tôi cần một cách để tạo ra cơ sở dữ liệu mới với chi phí thấp và chúng tôi cần nó có tính chất quan hệ.
Chúng ta đã kết thúc ở đây lên:

Bảng bộ ba
Hãy bắt đầu với câu hỏi: làm cách nào chúng tôi có thể cho phép người dùng tạo nhiều cơ sở dữ liệu khác nhau?
Con đường thẳng tiến nhất có lẽ là khởi động máy ảo Postgres. Nhưng như chúng tôi đã đề cập, máy ảo có rất nhiều chi phí về RAM. Không có cách nào bền vững để hỗ trợ số lượng ứng dụng không giới hạn nếu bạn đang sử dụng máy ảo.
Một tùy chọn khác là sử dụng lược đồ Postgres. Chúng tôi có thể tạo các bảng khác nhau cho các ứng dụng khác nhau và sau đó lập bản đồ xem ai có thể xem nội dung gì. Điều này sẽ hiệu quả, nhưng Postgres không được thiết kế để mở rộng quy mô bằng bảng. Từ nghiên cứu của mình, chúng tôi thấy rằng sau khoảng 6000 bảng, Postgres bắt đầu gặp sự cố: bạn gặp vấn đề với số lượng tệp được tạo trên đĩa và pg_dump và autovacuum bắt đầu không thành công.
Điều này có ý nghĩa. Ứng dụng Postgres trung bình có một vài bảng lớn, không có nhiều bảng nhỏ, điều đó có nghĩa là các bảng lớn sẽ được tối ưu hóa. Chà, nếu các bảng lớn hoạt động được, điều gì sẽ xảy ra nếu chúng ta chuyển vấn đề này thành một bảng khổng lồ?
Và điều này đưa chúng ta trở lại…Ba cửa hàng!
Chúng hoạt động tốt trên máy khách vì chúng là một cơ sở dữ liệu đơn giản hỗ trợ các truy vấn quan hệ. Chúng tôi nghĩ rằng điều này cũng có thể phù hợp với chúng tôi ở Postgres. Vì vậy, chúng tôi đã thêm bảng bộ ba:
Xem dưới dạng:
Tất cả dữ liệu nằm trong một bảng bộ ba và chúng được cách ly về mặt logic bởi một app_id.
Ví dụ: nếu chúng tôi muốn lấy post_1 từ ứng dụng blog thì chúng tôi có thể tạo một truy vấn SQL trông giống như đại khái như thế này:
chọn *
từ nhân ba
ở đâu app_id = 'blog' và instance_id = 'post_1' và attr_id trong (bài đăng/id, bài đăng/title)
Cùng với đó, việc tạo cơ sở dữ liệu mới thực sự miễn phí. Như chúng tôi đã đề cập trong phần trình diễn, đó là một vài hàng trong cơ sở dữ liệu.
Lợi ích đáng ngạc nhiên
Sự lựa chọn của chúng tôi cũng mang lại một số lợi ích đáng ngạc nhiên.
Vì chúng tôi tự quản lý các cột nên chúng tôi có thể tối ưu hóa trải nghiệm của nhà phát triển.
Ví dụ: Postgres khóa bảng khi bạn tạo cột. Vì chúng tôi đã tự triển khai các cột nên chúng tôi có thể làm cho chúng không bị khóa.
Khi bạn xóa một cột trong Postgres, dữ liệu sẽ biến mất. Nhưng chúng tôi nghĩ điều này quá nguy hiểm trong thế giới đặc vụ. Vì vậy, chúng tôi đã triển khai xóa mềm ở cấp độ cột. Ngay cả khi một tác nhân lừa đảo xóa các cột của bạn, bạn vẫn có thể hoàn tác việc đó và lấy lại tất cả dữ liệu của mình sau một phần nghìn giây.
Đây là những lợi ích nhưng tất nhiên cũng có chi phí.
Chỉ mục một phần
Hãy xem xét trường hợp một người dùng nói: “Tôi muốn bài đăng của mình có một ‘sên’ độc đáo”. Trong Postgres thật dễ dàng để tạo các cột độc đáo. Nhưng vì chúng tôi đang triển khai các cột của riêng mình nên chúng tôi phải tự thực hiện việc này.
Đây chính là lúc các chỉ mục một phần ra tay giải cứu. Chúng tôi có thể thêm các dấu boolean vào bảng bộ ba của mình:
tên_bảng: bộ ba
app_id | thực thể_id | attr_id | giá trị | cột_unique | ...
Sau khi có được điều đó, chúng ta có thể tạo một chỉ mục một phần cho toàn bộ bảng, được bật lên bởi điểm đánh dấu:
tạo duy nhất chỉ mục Unique_columns
bật gấp ba(app_id, cột, giá trị) ở đâu cột_unique
Bây giờ nếu người dùng cố gắng chèn hai bài đăng có cùng một phần mở rộng:
app_id | object_id | cột | giá trị | cột_unique
'blog' | 1 | 'sên' | 'xin chào' | true
'blog' | 2 | 'sên' | 'xin chào' | đúng
Chỉ mục unique_columns kích hoạt và ngăn chặn điều đó!
Và thủ thuật tương tự này giúp các truy vấn của chúng tôi hiệu quả hơn. Ví dụ: nếu chúng tôi muốn tìm các bài đăng có slug 'hello', chúng tôi có thể tạo truy vấn này:
chọn instance_id
từ gấp ba
ở đâu app_id = 'blog' và attr_id = 'sên' và giá trị = 'xin chào' và cột_unique;
Và chúng tôi có thể mở rộng mẫu này cho toàn bộ phạm vi truy vấn: cột duy nhất, chỉ mục, ngày tháng, tham chiếu, v.v.
Chỉ cần sử dụng các chỉ mục một phần và dựa vào Postgres để thực hiện các truy vấn phù hợp đã mang lại hiệu quả cho chúng tôi trong một thời gian. Nhưng sau khi chúng tôi đạt quy mô vài trăm triệu bộ dữ liệu, Postgres bắt đầu gặp sự cố.
Bản phác thảo đếm tối thiểu
Nếu bạn là chuyên gia Postgres đang đọc nội dung này thì có thể bạn đã tạm dừng khi xem bảng bộ ba đó. Trong giới Postgres, đây được gọi là mẫu EAV và thường không được khuyến khích.
Điều này không được khuyến khích vì Postgres dựa vào bảng và cột để thống kê.
Những thống kê đó cho phép trình lập kế hoạch truy vấn quyết định chỉ mục nào hiệu quả nhất và chỉ mục nào sẽ kết hợp theo thứ tự nào.
Khi bạn giữ tất cả dữ liệu trong một bảng, Postgres sẽ mất thông tin về cơ sở tần số trong tập dữ liệu. Nó không thể phân biệt được sự khác biệt giữa một cột có 10 giá trị riêng biệt và một cột có 10 triệu giá trị.
Để giải quyết vấn đề này, chúng tôi đã bắt đầu theo dõi số liệu thống kê của mình. Chúng tôi sử dụng cấu trúc dữ liệu được gọi là bản phác thảo đếm tối thiểu, giúp chúng tôi ước tính tần số cho các cột. Nếu bạn tò mò về cách thức hoạt động của tính năng này, chúng tôi đã viết một bài luận về nó [14].
Chúng tôi có thể cung cấp các số liệu thống kê đó cho công cụ truy vấn của mình và làm cho các truy vấn đó hoạt động hiệu quả trở lại.
Công cụ truy vấn
Điều này đưa chúng ta đến với công cụ truy vấn.
Cho đến nay tôi đã giới thiệu cho bạn các truy vấn SQL đơn giản và dễ hiểu. Nhưng hãy tưởng tượng việc dịch các truy vấn InstaQL phức tạp hơn. Ngay cả một truy vấn có mệnh đề Where cũng sẽ bắt đầu có CTE trong đó. Sau đó, bạn sẽ muốn sử dụng những số liệu thống kê đó để quyết định nên bật chỉ mục nào.
Đó là công việc của công cụ truy vấn. Nó sử dụng các truy vấn InstaQL cũng như các bản phác thảo đếm số phút và tạo các kế hoạch truy vấn SQL:
InstaQL
{ việc cần làm: { $: { ở đâu: { xong: } } }
Postgres
WITH done_triples NHƯ ( CHỌN entity_id TỪ bộ ba WHERE app_id = 'instaTuyến tính' VÀ ave VÀ attr_id = 'việc cần làm' VÀ giá trị = ), todo_data NHƯ ( SELECT t.entity_id, t.attr_id, t.value TỪ bộ ba t THAM GIA done_triples d BẬT t.entity_id = d.entity_id WHERE t.app_id = 'instaliner' ) CHỌN * FROM todo_data
Công cụ này được viết trong phần phụ trợ của Clojure. Chúng tôi lấy rất nhiều cảm hứng từ công cụ truy vấn của Postgres. Đôi khi những truy vấn này có thể trông dài một cách đáng kinh ngạc, nhưng chúng tôi thực sự ngạc nhiên về khả năng xử lý chúng của Postgres. Chúng tôi chuyển một số gợi ý bằng pg_hint_plan và Postgres chỉ cần thực hiện và tạo ra kết quả.
Bốn năm hình thành
Và nó bao trùm cơ sở dữ liệu, bao trùm toàn bộ hệ thống của chúng tôi!
Chúng tôi hy vọng bạn thấy điều này thú vị! Đây là một lao động của tình yêu. Chúng tôi xây dựng Instant vì chúng tôi muốn hỗ trợ thế hệ thợ xây tiếp theo. Bất kỳ sản phẩm nào chúng tôi xây dựng đều được xây dựng bằng Instant và hàng nghìn nhà phát triển đã tin cậy để vận hành cơ sở hạ tầng cốt lõi của họ.
Nếu bạn đang xây dựng với các đại lý, tôi nghĩ bạn sẽ thích sử dụng chúng tôi.
Chúng tôi hy vọng bạn cho chúng tôi dùng thử và tham gia cùng chúng tôi Bất hòa.
Chú thích cuối trang
-
Từng dòng mã đằng sau công ty đều tồn tại trên GitHub, bao gồm bài đăng ↩
này -
Nikita đã viết một bài đăng blog tuyệt vời về điều này tại đây ↩
-
LLM đã tìm hiểu về Instant trong dữ liệu đào tạo của họ nhưng thực sự không có nhiều điều để tìm hiểu. Các truy vấn và giao dịch có DSL có thể dự đoán được. ↩
-
Sự thật thú vị là các tệp của bạn vẫn được lưu trữ trong S3. Tuy nhiên, vì cả hai dịch vụ đều được xây dựng cùng nhau nên hệ thống có thể thay mặt bạn xử lý đồng bộ hóa dữ liệu hai chiều! ↩
-
Trên React Native, chúng tôi sử dụng bộ lưu trữ không đồng bộ-không đồng bộ vì nó có sẵn trên Expo Go. Tuy nhiên, API để lưu trữ có thể cắm được nên bạn có thể thay thế API này khá dễ dàng. ↩
-
Sẽ còn có nhiều vấn đề hơn nữa. Hãy xem trang công cụ đồng bộ hóa, đặc biệt là bản demo giải quyết xung đột. ↩
-
Tính năng này rất hữu ích trên trang Explorer của chúng tôi. Bạn có thể chuyển đổi giữa nhiều bộ lọc và chúng tôi sẽ tự động tạo truy vấn cho bộ lọc đó. ↩
-
Chúng tôi thậm chí còn làm được một số điều thú vị hơn nữa. Ví dụ: chúng tôi lấy mệnh đề Where và chuyển đổi chúng thành các chương trình nhỏ để lọc bổ sung. ↩
Tác giả: stopachka