Xây dựng dịch vụ web có tính sẵn sàng cao mà không cần cơ sở dữ liệu
(blog.screenshotbot.io)- Screenshotbot sử dụng trạng thái RAM của tiến trình web server làm kho lưu trữ dữ liệu mà không cần DB riêng, và khôi phục trạng thái sau sự cố bằng snapshot cùng transaction log
- Giảm việc tuần tự hóa SQL, các lượt đi-về DB và dịch vụ tác vụ nền riêng biệt để đơn giản hóa trải nghiệm phát triển với một tiến trình duy nhất, đồng thời tận dụng index trong bộ nhớ và xử lý dựa trên thread
- Tính sẵn sàng cao được đảm bảo bằng thuật toán đồng thuận Raft, sao chép transaction log sang 3 server; khi leader gặp sự cố, leader mới tiếp quản yêu cầu trong vài giây
- Triển khai thực tế kết hợp Common Lisp, bknr.datastore, bknr.cluster mã nguồn mở, Baidu Braft và EFS; server thường chỉ khởi động lại khoảng 1–2 tháng một lần
- Dù CI của khách hàng enterprise nổi tiếng gọi API hàng trăm lần cho mỗi commit và Pull Request, mức sử dụng CPU trên máy 4 core 16GB tối đa chỉ khoảng 20%, phần lớn đến từ xử lý ảnh
Kiến trúc xử lý trạng thái RAM như cơ sở dữ liệu
- Các dịch vụ web truyền thống thường chọn một web framework như Rails, Django, Node cùng với một cơ sở dữ liệu như MySQL, PostgreSQL, MongoDB
- Trong 10 năm qua, đã có những thay đổi về môi trường khiến cách tiếp cận này đáng được xem xét lại
- Ổ đĩa trở nên nhanh hơn nhờ NVMe
- Các ổ đĩa như EBS/EFS trở nên bền vững hơn
- RAM rẻ hơn, khiến hầu hết dữ liệu của startup có thể nằm gọn trong RAM
- Có thể thuê máy với hàng trăm core
- Năm 2014, thuật toán đồng thuận Raft được công bố và số lượng triển khai vững chắc tăng lên
- Kiến trúc này gộp dịch vụ web và instance cơ sở dữ liệu lại, sử dụng trạng thái bộ nhớ của tiến trình như một cơ sở dữ liệu
- Không tuần tự hóa dữ liệu để lưu như SQLite, mà trực tiếp xử lý các object và field trong RAM như trạng thái ứng dụng
Explore: Cách đơn giản hóa vòng lặp sản phẩm ban đầu
- Nếu mọi dữ liệu nằm trong RAM, không cần tuần tự hóa thành SQL query, và cũng giảm kiến trúc nhiều frontend server cùng kết nối vào một DB duy nhất
- Khi tải tăng, có thể ứng phó bằng cách dùng server lớn hơn với nhiều RAM và CPU hơn
- Index có thể dùng các cấu trúc như hash table trong bộ nhớ thay vì B-tree được tối ưu cho độ trễ đĩa
- Với Screenshotbot, index dùng functional collections đóng vai trò quan trọng đối với khả năng mở rộng
- Vì truy vấn dữ liệu là đọc từ RAM, không cần các cấu trúc đặc biệt hay Async-IO để giảm lượt đi-về DB
- Tác vụ nền trở thành các thread chạy trong cùng một tiến trình lớn, và xử lý đồng thời phần lớn có thể giải quyết bằng mutex và condition variable trong bộ nhớ
Khôi phục sự cố: snapshot và transaction log
- Để phòng trường hợp tiến trình crash, toàn bộ trạng thái RAM được lưu định kỳ dưới dạng snapshot
- Các thay đổi sau snapshot gần nhất được ghi trước xuống đĩa dưới dạng transaction log, rồi mới thay đổi trạng thái bộ nhớ
foo.setBar(2)ghi một transaction rằng fieldbarcủafoođã đổi thành 2, rồi mới thay đổi field thực tếnew Foo()ghi một transaction rằng object Foo đã được tạo, rồi mới trả về object mới
- Khi khởi động lại, hệ thống đọc snapshot trước, rồi phát lại transaction log để khôi phục trạng thái
- Thay đổi index không cần đưa vào transaction log
- Ví dụ, nếu field
barcủaFoocó index,setBarsẽ cập nhật index; việc cập nhật này cũng diễn ra lại trong quá trình load snapshot hoặc phát lại transaction
- Ví dụ, nếu field
Dạng mã mà một tiến trình duy nhất cho phép
- Vì mọi request được xử lý trong cùng một tiến trình, có thể lưu closure trong bộ nhớ server để dùng cho việc xử lý request trang
- URL dạng
https://screenshotbot.io/n/nnnnnnncủa Screenshotbot được ánh xạ tới các closure nội bộ - Do closure giữ tham chiếu object, nhu cầu truyền object ID hoặc tuần tự hóa object qua mỗi lần chuyển trang được giảm xuống
- Phạm vi debug, profiling và monitoring cũng thu hẹp còn một dịch vụ
- Không còn các điểm quan sát DB riêng như MySQL slow query log
- Nếu một dịch vụ sập thì site cũng sập, nhưng khi chỉ có một dịch vụ và một server, khả năng xảy ra lỗi cũng được xem là thấp hơn
- Nếu server chết, AWS có thể dựng server mới trong vài phút
- Mã test cũng dễ hơn vì không cần mock cơ sở dữ liệu
Expand: Đảm bảo tính sẵn sàng cao bằng Raft
- Nếu khách hàng rủi ro cao yêu cầu độ sẵn sàng 99,999%, kiến trúc một server là không đủ
- Khi server gặp sự cố, có thể mất vài phút cho đến khi AWS dựng lại
- Tiến trình cũng có thể mất vài phút để khôi phục snapshot từ đĩa
- Trong quá trình redeploy, server có thể ngừng trong nhiều phút do dịch vụ khởi động lại
- Thuật toán đồng thuận Raft sao chép transaction log của web server/cơ sở dữ liệu, vốn là một finite state machine
- Khi sao chép sang 3 máy, nếu leader gặp sự cố, leader mới sẽ được bầu trong vài giây và tiếp tục xử lý request
- Có thể biến một dịch vụ đơn giản thành cơ sở dữ liệu có tính sẵn sàng cao mà không làm thay đổi đáng kể cách developer viết code
- Với cấu hình dựa trên Raft, cũng có thể rolling deploy mà không cần dừng server
Extract: Sharding và nút thắt dự kiến
- Ở giai đoạn xử lý nhiều khách hàng lớn hơn, có thể áp dụng sharding như cách các doanh nghiệp lớn dùng với cơ sở dữ liệu
- Mỗi shard có thể là một cluster riêng
- Screenshotbot hiện đã cung cấp cluster riêng cho từng khách hàng enterprise
- Meta đã chuyển sang Raft để xử lý sao chép cluster MySQL, còn Screenshotbot dùng cách tương tự mà không cần cơ sở dữ liệu riêng
- Ứng viên nút thắt chính là khả năng mở rộng của commit-thread
- Các thread đọc được song song hóa tốt
- Một commit-thread áp dụng từng transaction một
- Vì Raft commit nhiều transaction cùng lúc xuống đĩa, độ trễ đĩa không quan trọng
- Điểm đáng lo là khi chi phí CPU của việc áp dụng transaction vượt quá hiệu năng của một core đơn lẻ
- Trong trường hợp đó, có thể profile chi phí commit để chuyển một phần công việc ra ngoài transaction thread hoặc cân nhắc sharding
Stack thực tế của Screenshotbot
- Screenshotbot sử dụng Common Lisp
- Triển khai ban đầu dùng MySQL, nhưng do khó xử lý concurrency với MySQL nên đã thay bằng bknr.datastore
- bknr.datastore là thư viện cung cấp cho Common Lisp kiến trúc đã mô tả ở giai đoạn Explore
- Trong kiến trúc này, các thread trong một tiến trình duy nhất xử lý request web, nên multithreading mạnh là yếu tố quan trọng
- Vì yêu cầu này, Ruby hoặc Python được xem là không phù hợp
- Do duy trì closure trong bộ nhớ server, khó khởi động lại server thường xuyên
- Khi khởi động lại sẽ mất closure
- Thay vào đó, code được cập nhật bằng hot reloading trong tiến trình đang chạy
- Common Lisp cung cấp các tính năng như
reinitialize-instance, một chuẩn cập nhật object hiện có khi định nghĩa class thay đổi
Cluster, lưu trữ file và quy mô vận hành
- Hiện server thường khởi động lại khoảng 1–2 tháng một lần
- Khi cần khởi động lại, hệ thống thực hiện rolling restart trong cluster Raft
- Mỗi bản cài đặt dùng cluster 3 server, cấu hình này cho phép một server bị down
- Hiện không dùng Kubernetes và được xem là chưa cần thiết
- Triển khai Raft là thư viện tự xây dựng trên bknr.datastore
- bknr.cluster đã được phát hành mã nguồn mở
- Nội bộ sử dụng Braft của Baidu
- Braft xử lý snapshot nền, nhờ đó server vẫn tiếp tục xử lý request trong khi tạo snapshot
- Các file ảnh hoặc blob không nên đưa vào datastore được lưu trên EFS mà ba server cùng chia sẻ
- EFS là NFS có tính sẵn sàng cao
- Được xem là dễ làm việc hơn S3 vì không cần xử lý riêng các điều kiện lỗi
- Vì ghi xuống đĩa mà không tương tác với server bên ngoài, khả năng test cũng cao hơn
Hiệu năng hiện tại và phạm vi áp dụng
- Screenshotbot hỗ trợ một số khách hàng enterprise lớn và một khách hàng đặc biệt nổi tiếng
- Hệ thống chạy trong CI của khách hàng đó, nhận hàng trăm request API cho mỗi commit và Pull Request
- Dù có tải này, xử lý request chỉ cần máy 4 core 16GB là đủ
- Server sao chép cũng dùng máy tương tự và phần lớn ở trạng thái idle
- Mức sử dụng CPU tối đa khoảng 20%
- Phần lớn mức sử dụng CPU đến từ xử lý ảnh
- Điều quan trọng là không thiết kế dựa trên quy mô lớn hơn quy mô cần thiết
- Nếu chọn Common Lisp, cấu hình liên quan có thể dùng qua Screenshotbot OSS
1 bình luận
Ý kiến trên Hacker News
Kiến trúc này đại khái giống cách HashiCorp tạo ra Nomad, Consul và Vault (tôi là một trong những người bảo trì Nomad). Chắc chắn đây là một kiến trúc khác thường, nhưng khi đã quen thì trải nghiệm của lập trình viên khá tốt
Trạng thái trong bộ nhớ có thể được giữ theo bất kỳ dạng nào bạn muốn, nên bạn có thể tự tạo các hàm lập chỉ mục và truy vấn phù hợp với ứng dụng. Có thể dùng SQLite
:memory:cho Raft FSM, nhưng nếu có thể tự xây hoặc tìm được một kho lưu trữ giao dịch trong bộ nhớ (chúng tôi dùng go-memdb của riêng mình), việc đọc trạng thái chỉ còn là một lời gọi hàm. Việc ngăn đọc dữ liệu cũ hoặc thiên lệch về ghi cũng đơn giản. Mọi đối tượng được ghi đều có chỉ mục Raft, nên có thể tạo API kiểu “truy vấn đối tượng foo trên follower, nhưng đợi ít nhất đến chỉ mục 123”. Nó loại bỏ rất nhiều “ma thuật” mà thường ta đẩy vào RDBMS hoặc kho lưu trữ bên ngoàiTuy nhiên, tôi sẽ thận trọng khi chọn kiến trúc này cho một startup mới ngoài lĩnh vực “hạ tầng”. Vì về thực chất, bạn đang tạo ra cơ sở dữ liệu riêng. Bạn phải chọn kỹ hoặc tự viết các thành phần nền tảng như RPC giữa các node, tính bền vững trên đĩa, kho lưu trữ trạng thái giao dịch trong bộ nhớ. Nâng cấp đặc biệt khó, vì mã mới có thể cố ghi vào log Raft những entity mà node phiên bản cũ không hiểu. Tệ hơn nữa là cách xử lý thay đổi khiến node cũ hiểu sai. Không có bữa trưa miễn phí
:memory:cho Raft FSM” từng là thiết kế mặc định mà rqlite[1] dùng trong khoảng 7 năm đầu. Nhưng rqlite đã chuyển sang SQLite dựa trên đĩa, và khi dùng chế độ WAL cùngPRAGMA synchronous=OFF[2], nó nhanh ngang hoặc đủ gần với ghi vào RAM. Nhờ đó cũng tránh được các giới hạn của cơ sở dữ liệu SQLite:memory:, một trong số đó là giới hạn kích thước tối đa 2GB. Đáng lẽ tôi nên dùng chế độ đĩa ngay từ đầu, giờ mới nhận raVì rqlite dùng cùng thư viện Raft[3] như Nomad, có thể bạn đã biết một phần chuyện này
Vấn đề nâng cấp thực sự tồn tại. Tôi tò mò không biết Nomad có hay gặp trong thực tế không. Trong 10 năm phát triển rqlite, việc đưa vào một kiểu Raft Entry mới là rất hiếm, và người dùng thực tế chỉ gặp một lần. Một cách xử lý là trước tiên phát hành một phiên bản hiểu kiểu mới nhưng tuyệt đối không ghi kiểu đó, rồi sau khi phiên bản ấy đã được triển khai hoàn toàn mới nâng lên phiên bản thực sự ghi kiểu mới. Tuy nhiên thực tế tôi chưa từng làm vậy, và nó cũng đòi hỏi kỷ luật từ người dùng cuối
[1] https://www.rqlite.io
[2] Nghe có vẻ nguy hiểm, nhưng trong thiết kế hiện tại của rqlite, khi khởi động nó tái dựng hoàn toàn cơ sở dữ liệu SQLite chính từ log Raft (log Raft được fsync ở mỗi lần ghi). Vì vậy ngay cả khi cơ sở dữ liệu SQLite bị hỏng do mất điện chẳng hạn, điều đó cũng không quá quan trọng vì cơ sở dữ liệu SQLite không phải kho dữ liệu có thẩm quyền của rqlite
[3] https://github.com/hashicorp/raft
Tuy nhiên, khi bắt đầu dùng sao chép ở giai đoạn Expand thì đúng là sẽ phát sinh các bài toán kỹ thuật. Dù vậy, tất cả đều là những vấn đề có thể giải quyết. Trong Common Lisp có thể hot reload mã, nên một số migration trở nên dễ hơn nhiều, điều này cũng giúp ích
Vài chục năm trước, PG từng viết rằng ở Viaweb ông không dùng cơ sở dữ liệu, và thấy kỳ lạ khi ứng dụng web lại trở thành giống như frontend của cơ sở dữ liệu, trong khi ứng dụng desktop thì không như vậy[0]. HN cũng không dùng cơ sở dữ liệu
Nhưng vì các ứng dụng desktop và mobile hiện đại thường dùng cơ sở dữ liệu, thường là SQLite, nên điều đó không còn hoàn toàn đúng nữa. Lý do là việc lưu trữ và truy vấn dữ liệu quan hệ đã cho thấy khá hữu ích trong một phạm vi ứng dụng rất rộng
[0] https://www.paulgraham.com/vwfaq.html
Trong vwfaq vẫn có nội dung nói rằng dữ liệu được đọc từ đĩa, và cũng có câu “khởi động một tiến trình để phản hồi yêu cầu HTTP”. Ở đây, “cơ sở dữ liệu” có vẻ chỉ một máy chủ riêng để duy trì dữ liệu bền vững, cùng kiến trúc phải giao tiếp với một máy chủ khác để lấy dữ liệu đó
Theo định nghĩa này thì tất nhiên SQLite bị loại khỏi khái niệm cơ sở dữ liệu. Ngoài ra, nếu bạn đã đọc dữ liệu từ đĩa thì hoặc là bạn đang dùng cơ sở dữ liệu, hoặc là đã tự triển khai một lớp lưu trữ bền vững tạm bợ. Vấn đề là nếu khi khởi động ứng dụng bạn đọc dữ liệu từ SQLite thì có còn được xem là đang dùng cơ sở dữ liệu hay không
Vấn đề của cách nghĩ này là nó bỏ qua sự thật rằng cốt lõi của cơ sở dữ liệu là lưu và lấy dữ liệu theo cách thuận tiện mà không phải bận tâm đến các chi tiết cấp thấp. Lưu dữ liệu vào cơ sở dữ liệu không có nghĩa là phải dựng một instance Postgres ở đâu đó rồi lấy dữ liệu qua web. Nếu bạn giữ toàn bộ dữ liệu trong bộ nhớ và có một tiến trình lưu snapshot ra đĩa bằng cấu trúc dữ liệu dạng log-structured, thì xin chúc mừng: bạn vừa tạo ra cơ sở dữ liệu riêng của mình
Thứ hai, trong 20 năm qua, truy vấn RDBMS đã đơn giản hơn rất nhiều. Đã xuất hiện đủ loại ORM và row mapper giúp giảm mã lặp
Các tính năng nâng cao như tìm kiếm toàn văn bản, hữu ích cho ứng dụng desktop và mobile, cũng đã xuất hiện. Ngày nay dùng RDBMS cho ứng dụng desktop là một lựa chọn tốt
Web đã lớn hơn rất nhiều, và ngày nay nhiều thực hành kiểu này sẽ không còn hiệu quả. Nếu vừa nói chuyện với khách hàng vừa đẩy bản sửa trực tiếp lên máy production với gánh nặng kiểm thử ở mức hiện nay, hẳn nhiều người ở đây sẽ nghi ngờ tình trạng tâm thần của tôi
Tôi hiểu mong muốn thử nghiệm điều thú vị, nhưng làm thế này chỉ để không phải học những phần cơ bản nhất của MySQL hay Postgres thì có vẻ là một sự lãng phí thời gian khổng lồ. Chỉ cần xây trên đó là xong, đặc biệt nếu chạy trên public cloud
Tôi không bị thuyết phục bởi các lập luận về độ trễ round-trip tăng lên hay vấn đề đồng thời. Vấn đề sau có các cách giải quyết đơn giản như tuning cơ bản hoặc tách riêng những khách hàng gây ồn ào. Trong một bài khác trên blog của họ, họ nói về khả năng thêm 10 triệu hàng mỗi ngày và các thách thức indexing, nhưng điều đó đúng nghĩa là chẳng là gì cả. Ngay cả khi gấp 10 lần con số đó, tôi cũng không nghĩ đó là lý do chính đáng để phải engineering một giải pháp tùy biến
Cho đến khi thật sự cần, “worse is better” vẫn đúng; và khi thời điểm đó đến, bạn sẽ biết chắc. Lúc ấy bạn cũng sẽ biết nút thắt nằm ở đâu, nên có thể phản ứng khôn ngoan hơn thay vì over-engineer ngay từ đầu
Nhưng việc blogger cho rằng lý do không chọn SQLite là vì nó có thể có các tính năng không cần thiết thì thật khó hiểu. Điều đó vô lý và không biện minh được gì
Bài viết đọc như bắt đầu từ một giải pháp tệ cho một vấn đề giả định, rồi tuyệt vọng kéo ra các lập luận gượng ép để bác bỏ giải pháp hiển nhiên
Tôi tự hỏi liệu suy nghĩ của mình có sai không, hay nỗ lực bổ sung gây tranh cãi nhưng có vẻ không cần thiết này thực ra là một phần của sản phẩm và là cách để thành công trong không gian này
Mở đầu bằng câu “không phải tôi đang nói đến thứ như SQLite, nơi dữ liệu vẫn bị tuần tự hóa”, nhưng rốt cuộc lại kết thúc bằng một transaction log tự chế vẫn cần tuần tự hóa và cũng phải sao chép, nghe khá lạ. Việc nhân bản cơ sở dữ liệu vốn cũng làm như vậy
Nếu toàn bộ tải dồn vào một máy chủ, thì cứ chạy cơ sở dữ liệu trên máy chủ đó và quên đi những thứ kiểu “kiến trúc đặc biệt để giảm lượt đi-về đến cơ sở dữ liệu” là được. Nếu toàn bộ dữ liệu nằm vừa trong RAM, bạn có thể dùng ramdisk cho cơ sở dữ liệu nếu muốn, rồi sao chép sang lưu trữ bền vững bằng công cụ tiêu chuẩn. Như thế mới thực sự đơn giản
Vì vậy về cơ bản nó giống https://litestream.io/. Nhờ cấu hình Raft rõ ràng, việc chuyển đổi có thể nhanh hơn. Tôi không dùng Litestream nên không biết các khác biệt tinh tế, nhưng nghe rất giống
Bỏ qua sự đơn giản hóa quá mức này, bản thân ý tưởng thì tôi khá thích, và bài viết cũng thuyết phục khá tốt về mặt khái niệm. Với nhiều hệ thống, ngay cả khi thành công bất thường, nó vẫn sẽ mở rộng đủ để gánh phần lớn hoặc toàn bộ hoạt động kinh doanh, và hiệu năng sẽ tốt đến mức vô lý so với gần như mọi phương án thay thế
Cứ cold start cơ sở dữ liệu rồi chạy một câu
selectkhá lớn hai lần là thấyCơ sở dữ liệu giao dịch đơn giản ở giai đoạn Expand và Extract, nhưng ở giai đoạn Explore nó lại tạo thêm gánh nặng vì khiến bạn tập trung vào vấn đề hạ tầng thay vì sản phẩm. Ở giai đoạn Explore chưa có khách hàng, nên cũng chưa có dữ liệu, và độ tin cậy của dữ liệu cũng không quan trọng
Cách giữ mọi thứ trong bộ nhớ với bknr.datastore (không sao chép) là đơn giản ở giai đoạn Explore. Nhưng khi bước vào giai đoạn Expand, nó phát sinh gánh nặng vận hành để bảo đảm tính nhất quán dữ liệu
Tuy vậy, đến lúc đạt tới giai đoạn Expand thì bạn đã kiểm chứng được sản phẩm và đã viết khá nhiều code. Viết lại toàn bộ bằng cơ sở dữ liệu giao dịch là không hợp lý; thêm nhân bản Raft lên trên sẽ dễ hơn
Lập luận của bài này khiến tôi bối rối. Đây là cách làm cho ứng dụng có trạng thái đơn giản hơn và nhanh hơn à?
Tiền đề thì yếu, còn tuyên bố thì quá đà. Tác giả phóng đại độ khó của tuần tự hóa để khiến một lập luận yếu trông mạnh hơn
Những người như vậy thường khó làm việc cùng. Thật mừng là họ đã tìm được một startup để đánh chìm, để tôi khỏi phải đối phó
Khi bắt đầu một dự án mới, cấu trúc dữ liệu thường là “danh sách các mục có thuộc tính”. Ví dụ hiện tôi đang viết một ứng dụng fitness, dữ liệu là danh sách bài tập; mỗi bài tập có tiêu đề, mô tả, URL video và các thuộc tính khác
Thường thì tôi bắt đầu bằng cách đặt các mục đó vào các file YAML trong thư mục
data. Thực tế là một phương ngữ YAML tùy chỉnh, loại bỏ những điểm kỳ quặc của YAML gốc. Mỗi giá trị là chuỗi, không có chuyển đổi kiểu “ma thuật”. Tạo mục mới chỉ làvim crunches.yamlrồi nhập dữ liệu; chỉnh sửa và xóa trong cấu trúc dữ liệu này cũng rất dễKhi dự án lớn hơn, thường tôi sẽ tạo schema DB và chuyển các mục sang MariaDB hoặc SQLite
Lần này tôi định chuyển các mục (bài tập) sang cột JSON trong DB SQLite. Lưu mọi thuộc tính của một mục vào một trường JSON duy nhất, rồi viết một trình duyệt DB nhỏ cho phép chỉnh sửa trường JSON như YAML. Mục tiêu là giữ sự tiện lợi khi chỉnh sửa dữ liệu con người đọc được
Việc viết trình duyệt DB có vẻ khá thẳng tiến. Dùng một chút ncurses để duyệt bảng, chọn một bảng, duyệt các hàng, chèn/xóa hàng. Khi chỉnh sửa trường thì mở Vim. Nếu trường là JSON, chuyển sang YAML trước khi đưa vào Vim; khi người dùng thoát Vim thì chuyển ngược lại thành JSON
Phần đầu bài viết mô tả về cơ bản khá giống cách các máy NUMA từng hoạt động (ví dụ SGI Altix hoặc UV). Ngoài ra, lợi thế họ nêu là độ trễ thấp và khả năng song song hóa công việc bằng multithreading trên RAM lớn. Clustering xuất hiện như một phương án chi phí thấp thay cho những cỗ máy hơn 1 triệu USD. Nó cũng có điểm tương đồng với tính bền vững của AS/400, nơi ứng dụng chỉ cần ghi vào bộ nhớ và dữ liệu được ánh xạ xuống đĩa một cách trong suốt
Giờ thì chúng ta như đang quay ngược thời gian để lấy lại lợi ích của các máy NUMA dạng cluster bằng phần cứng rẻ tiền. Trong quá trình đó cũng có những cải tiến, và bài viết đọc khá thú vị
Một kỹ thuật khác trước đây là loại bỏ TCP/IP stack bên trong cluster để tránh các vấn đề liên quan. Những giải pháp như Active Messages là một lớp mỏng nằm trên phần cứng. Cũng có các thiết kế router mạng tích hợp tính nhất quán mạnh. Họ có thể làm được khá nhiều việc
Khi lớn lên, cũng có cơ hội về phần cứng. Ở phía CPU, SGI đã làm hai việc. Máy NUMA mở rộng số CPU và RAM trong một hệ thống, và cắm FPGA trực tiếp vào bus bộ nhớ để dùng làm bộ tăng tốc tùy chỉnh. Cuối cùng, một số bài báo khoa học máy tính đã sửa tập lệnh của bộ xử lý, mạng nội bộ trong chip, v.v. để loại bỏ hoặc giảm nút thắt multithreading. Những chip như OpenPiton tăng số lõi bằng các core mở và có thể tùy biến (ví dụ: 32 core)
“Hãy tưởng tượng ta có thể tạo ra những thứ tuyệt vời đến mức nào nếu không cần tuần tự hóa dữ liệu thành các truy vấn SQL” — điều đó đã tồn tại trong các triển khai mô hình actor[0] đủ trưởng thành, chẳng hạn Akka Event Sourcing[1]. Nó cũng xử lý vấn đề tiếp theo:
“Nhưng phần quan trọng là làm sao khôi phục khi tiến trình bị crash. Câu trả lời rất dễ: định kỳ chụp snapshot toàn bộ RAM là được”
Về bản chất, việc này đã được giải quyết mà không cần tạo ra một “kiến trúc mới cho phát triển web”. Cũng có các nỗ lực mã nguồn mở khám phá giao thức RAFT bằng actor ở đây[2] và đây[3]
0 - https://en.wikipedia.org/wiki/History_of_the_Actor_model
1 - https://doc.akka.io/docs/akka/current/typed/persistence.html
2 - https://github.com/Michael-Dratch/RAFT_Implementation
3 - https://github.com/invkrh/akka-raft
Nếu chọn các thứ như Cosmos DB, MongoDB, DynamoDB làm persistence provider, bạn cũng có thể truy vấn trạng thái đã được lưu bền vững
https://learn.microsoft.com/en-us/dotnet/orleans/grains/grai...
https://learn.microsoft.com/en-us/dotnet/orleans/grains/tran...
https://learn.microsoft.com/en-us/dotnet/orleans/grains/even...
Ý nghĩ đầu tiên của tôi là: “À, đây là cách mình từng làm khi viết bằng Common Lisp; thú vị là có người đã tái khám phá kỹ thuật đó”
Nhưng hóa ra không phải vậy, chỉ là còn có thêm dân Lisp mà thôi
“RAM rất rẻ” có thể là hiểu lầm lớn nhất của giới lập trình viên
SSD đã tốt hơn 100–10000 lần xét theo throughput hoặc IOPS, và hiệu năng trên mỗi đô la của vCPU cũng tăng 20–50 lần. Từ 45/32nm giờ đã xuống 5nm/3nm, và số lệnh xử lý trên mỗi chu kỳ xung nhịp cũng cao hơn nhiều
Nhưng giá RAM gần như không giảm nhiều như CPU hay SSD. Nó có thể đã nhanh hơn nhiều, có thể cắm được nhiều bộ nhớ hơn nhờ chip mật độ cao hơn, và số kênh có thể đã tăng từ dual-channel lên 8 hoặc 12 kênh. Nhưng nếu nhìn giá spot DRAM giai đoạn 2008–2022, mức giá DRAM thấp nhất đã ba lần ở quanh cùng mức khoảng 2,8 USD/GB. Trong cùng giai đoạn, theo chu kỳ giá dao động lên xuống trong khoảng 6–8 USD/GB. Nói cách khác, nếu bạn mua DRAM ở đáy hoặc đỉnh trong khoảng 15 năm qua, thì bỏ qua lạm phát, giá sẽ vẫn tương tự nhau ở mức khoảng ±10–20%
Chỉ đến giữa năm 2022, rào cản 2,8 USD/GB mới bị phá vỡ và giá rơi xuống gần 1 USD/GB, rồi ổn định quanh khoảng 2 USD/GB với DDR5
Giờ thì bạn có thể lắp 4TB RAM vào một máy chủ. Nhưng điều đó không có nghĩa DRAM cực rẻ. Lập trình viên trung bình hoặc lập trình viên ở Big Tech giờ kiếm được nhiều hơn hẳn so với năm 2010, nên họ cảm thấy RAM dễ chi trả hơn nhiều. Thực tế là ngay cả ở mức đáy trong 15 năm qua, giá DRAM cùng lắm chỉ giảm hơn 2 lần một chút. Và nhiều khả năng giá DRAM sẽ lại tăng vọt trong 1–2 năm tới
Lấy ví dụ đơn giản: nếu 20 năm trước một node nhất định bị giới hạn ở 16GB RAM, thì để có 4TB RAM trong hệ thống sẽ cần 256 node (chưa tính overhead của từng OS)
Ngày nay, một node đơn lẻ có thể chứa toàn bộ 4TB đó trong một chassis
Tổng chi phí của bản thân chip RAM có thể không thay đổi, nhưng chi phí để thực sự sử dụng lượng RAM đó trong một hệ thống vật lý đã giảm mạnh