Bản chất của Erlang không phải là lightweight process và truyền thông điệp (2023)
(stevana.github.io)- Điểm cốt lõi giúp Erlang có thể xây dựng các hệ thống phân tán đáng tin cậy không nằm ở bản thân lightweight process, mà ở behaviours — thứ khái quát hóa các mẫu lặp lại trong xử lý đồng thời và lỗi
- behaviours cung cấp các điểm triển khai giống như interface, nhưng đồng thời che giấu xử lý đồng thời bên trong các thành phần chung như
gen_server, khiến mã ứng dụng gần với logic tuần tự hơn - Luận án của Joe Armstrong bàn về trường hợp xây dựng một hệ thống lớn như AXD301 chỉ bằng các thành phần nhỏ như
gen_server,gen_event,gen_fsm,supervisor,application,release supervisorkhởi động lại các process bị lỗi theo các chiến lược nhưone_for_one,one_for_all; triết lý “Let it crash!” chuyển trách nhiệm khôi phục sang cây supervisor- behaviours dưới dạng state machine giúp kiểm thử mô phỏng và kiểm chứng hình thức dễ hơn, cho phép lập trình viên tập trung vào ngữ nghĩa của vấn đề thay vì độ phức tạp của đồng thời và simulator
Vấn đề Erlang muốn giải quyết
- Erlang khởi đầu như một công cụ để xây dựng hệ thống phân tán đáng tin cậy
- Ban đầu, nó là một thư viện Prolog để tạo hệ thống phân tán đáng tin cậy; sau đó trở thành một biến thể của Prolog rồi thành một ngôn ngữ độc lập
- Nó được dùng để lập trình tổng đài điện thoại của Ericsson, xử lý lưu lượng ở quy mô hàng trăm triệu người trong thập niên 80–90 cùng các SLA nghiêm ngặt
- Ericsson đã cấm sử dụng Erlang vào năm 1998; nhóm phát triển lập luận rằng nếu đã cấm thì hãy công bố nó dưới dạng mã nguồn mở, và Ericsson đã làm vậy
- Joe Armstrong là một trong những nhân vật chủ chốt trong thiết kế và triển khai Erlang; năm 2002 ông bắt đầu viết luận án tiến sĩ tại SICS và hoàn thành Making reliable distributed systems in the presence of software errors vào năm 2003
- Luận án này tập trung vào các ý tưởng đứng sau Erlang và kinh nghiệm xây dựng hệ thống phân tán đáng tin cậy, hơn là toán học hay lý thuyết
- Ý tưởng lớn của Erlang không phải là lightweight process và truyền thông điệp, mà là các thành phần tổng quát mà Erlang gọi là behaviours
behaviours: sự kết hợp giữa interface và hạ tầng
- Erlang behaviours là một tập các chữ ký kiểu có thể có nhiều triển khai, giống như interface trong Java hoặc Go
- Khi lập trình viên cung cấp phần triển khai cho interface đó, họ có thể dùng các hàm tổng quát được viết nhắm tới interface ấy
- Khác biệt cốt lõi là behaviours không chỉ là interface đơn thuần mà còn cung cấp cả mã hạ tầng
- Lập trình viên ứng dụng viết ngữ nghĩa của vấn đề, tức business logic
- Mã hạ tầng như xử lý đồng thời được behaviour tự động cung cấp
- behaviours chứa các best practice do những chuyên gia giàu kinh nghiệm viết ra
- Nếu toàn bộ hệ thống tái sử dụng một tập behaviour nhỏ, khi phần triển khai behaviour được cải thiện thì hệ thống cũng có thể được cải thiện mà không cần đổi mã
- Việc dùng behaviour áp đặt cấu trúc, giúp kiểm thử và kiểm chứng hình thức dễ hơn
gen_server: đồng thời được che giấu sau mã tuần tự
- Ví dụ
gen_serverlà một kho key-value, trong đóstorelưu cặp key-value cònlookuptìm giá trị của key - Callback cốt lõi
handle_callcập nhật trạng tháiDictvới yêu cầustore, và tra key trong trạng thái với yêu cầulookup - Khi cung cấp triển khai này cho
gen_server, ta có một server có thể xử lý các yêu cầustorevàlookupđến đồng thời - Điểm quan trọng là bản thân
handle_callhoàn toàn tuần tự- Đồng thời được che giấu bên trong thành phần
gen_servertổng quát - Mã ứng dụng gần với dạng nhận trạng thái và input rồi tạo ra trạng thái mới và output
- Đồng thời được che giấu bên trong thành phần
- Luận án của Joe Armstrong mở rộng cách tiếp cận này sang
gen_server,gen_event,gen_fsm,supervisor,application,release
Trường hợp AXD301 và quy mô của behaviours
- Joe Armstrong dùng tổng đài điện thoại AXD301 của Ericsson làm ví dụ
- Dự án AXD301 bao gồm các thành phần sau
- 122 instance
gen_server - 36 instance
gen_event - 10 instance
gen_fsm - 20 supervisor
- 6 application
- Toàn bộ được đóng gói thành một release
- 122 instance
- AXD301 là một hệ thống có hơn 1 triệu dòng mã Erlang
- Việc một hệ thống ở quy mô này được cấu thành từ một tập behaviour nhỏ trở thành bằng chứng cốt lõi cho cấu trúc của Erlang
Vai trò của các behaviours khác
gen_eventlà một trình quản lý sự kiện tổng quát- Nó đăng ký các event handler, và khi event manager nhận được thông điệp liên quan thì chạy handler
- Joe Armstrong nêu logging lỗi như một ví dụ sử dụng
- Logger ví dụ có thể ghi lại và báo cáo năm thông điệp lỗi gần nhất
gen_fsmlà behaviour state machine về sau được đổi tên thànhgen_statem- Phù hợp hơn để triển khai protocol
- Protocol thường được đặc tả dưới dạng state machine
- Có thể xem bất kỳ
gen_servernào cũng có thể được triển khai bằnggen_statem, và ngược lại
applicationgồm cây supervisor và các yếu tố còn lại cần thiết cho việc phân phối ứng dụngreleaseđóng gói một hoặc nhiều application- Nó cũng bao gồm mã xử lý upgrade
- Nếu upgrade thất bại, cần có khả năng rollback về trạng thái ổn định trước đó
supervisor và “Let it crash!”
supervisorlà process kiểm tra xem các process khác có hoạt động bình thường hay không- Nếu một process được giám sát bị lỗi, supervisor có thể khởi động lại nó theo chiến lược được định nghĩa trước
- Chiến lược
{one_for_one, 5, 1000}trong ví dụ có nghĩa là- Nếu một trong
packet_assembler,kv,simple_loggerbị lỗi, chỉ process bị lỗi được khởi động lại - Nếu cần khởi động lại hơn 5 lần trong 1000 giây, bản thân supervisor sẽ thất bại
- Nếu một trong
permanent, 500, workernghĩa là process đó là một worker phải luôn sống, và supervisor sẽ cho 500ms để shutdown bình thường khi cố khởi động lại- Với chiến lược
one_for_all, nếu một process thất bại thì tất cả process con đều được khởi động lại - supervisor có thể giám sát supervisor khác, và không nhất thiết phải chạy trên cùng một máy tính
- Cấu trúc này hoạt động ở cấp thread/lightweight process chứ không phải cấp container Docker, nên khó có thể xem nó chỉ là “Kubernetes”
- “Let it crash!” giả định rằng process bị lỗi sẽ được khởi động lại
- Chương trình chỉ biểu diễn đường đi bình thường
- Nếu có vấn đề trên đường đi bình thường, nó không tự phục hồi mà crash
- Một chương trình khác ở cấp cao hơn trong cây supervisor sẽ đảm nhận xử lý
- Bằng chứng về độ ổn định dài hạn của AXD301 không được thu thập một cách có hệ thống, nhưng luận án của Joe Armstrong có ghi rằng một slide PowerPoint nêu một khách hàng lớn đã vận hành hệ thống 11 node với độ tin cậy 99.9999999%
- Cách thu được con số đó không được tài liệu hóa
- Các số liệu trong báo cáo downtime khác cũng kèm lưu ý rằng phương pháp tính không rõ ràng
Nếu triển khai behaviours trong ngôn ngữ khác
- Chỉ lightweight process và truyền thông điệp không đủ để khiến Erlang phù hợp với các hệ thống đáng tin cậy
- Cách giải thích trung thực hơn là cấu trúc do behaviours cung cấp và cách cấu trúc đó dẫn tới phần mềm đáng tin cậy
- Chữ ký interface của
gen_servercó thể được xem dưới dạng sauInput -> State -> (State, Output)- Nghĩa là nhận input và trạng thái hiện tại rồi tạo ra trạng thái mới và output
- Có thể hình dung cách biến chữ ký tuần tự này thành xử lý request đồng thời như sau
- HTTP server chuyển request thành
Inputrồi đưa vào queue - Event loop lấy input khỏi queue và truyền cho triển khai tuần tự
- Output được dùng làm response trả về client
- Để hỗ trợ nhiều
gen_server, có thể đặt tên cho từng server và kèm tên cùng input trong request
- HTTP server chuyển request thành
gen_eventcó thể được triển khai bằng cách cho phép đăng ký callback cho các loại event cụ thể trong queuesupervisorcó thể được đơn giản hóa bằng cách bọc lời gọi hàmgen_servertrong exception handler, rồi thông báo cho supervisor khi có exception- Nếu supervisor không chạy trên cùng máy tính, mọi thứ sẽ phức tạp hơn
applicationvàreleasequan trọng vì cấu hình, triển khai và upgrade là những vấn đề khó, nhưng không được đi sâu
behaviours và khả năng kiểm thử
- Cấu trúc của Erlang behaviours dẫn tới kiểm thử mô phỏng và kiểm chứng hình thức
- Mối quan tâm gần đây là kiểm thử mô phỏng hệ thống phân tán kiểu FoundationDB
- Kiểm thử mô phỏng chạy hệ thống trong một thế giới được mô phỏng, nơi simulation kiểm soát hoàn toàn thời điểm gửi thông điệp mạng
- FoundationDB đã tạo ra một biến thể C++ có actor, hoặc một ngôn ngữ lập trình riêng, để phục vụ kiểm thử mô phỏng
- Có thể xem cách tiếp cận này đã đi khá xa chỉ với kiểu state machine sau
Input -> State -> (State, [Output])[Output]là chuỗi output
- Simulator quản lý thông điệp bằng priority queue theo thời gian đến
- Lấy ra một thông điệp
- Tiến đồng hồ tới thời gian đến của thông điệp đó
- Chuyển thông điệp cho state machine nhận
- Sinh thời gian đến mới cho các thông điệp output rồi đưa chúng trở lại queue
- Nếu mọi thứ đều tất định và thời gian đến được tạo bằng seed, có thể khám phá nhiều interleaving và thu được lỗi có thể tái hiện
- Cách này nhanh hơn Jepsen vì thông điệp được xử lý trong bộ nhớ và đồng hồ nhảy tới thời gian đến thay vì chờ timeout
- Dạng state machine này được gọi là “network normal form”, dẫn tới suy đoán rằng mọi chương trình nhận/gửi qua mạng đều có thể được refactor về dạng này
- Việc
gen_servervàgen_statemvề cơ bản có cùng cấu trúc kiểu có thể được xem là tín hiệu rằng cấu trúc này không phải tùy tiện
Kiểm chứng hình thức và cấu trúc state machine
- Joe Armstrong từng nói trong một talk rằng triển khai đúng distributed leader election là việc khó
- Nếu có simulator, vấn đề này có thể được đơn giản hóa đáng kể
- Giống như wind tunnel hữu ích trong chế tạo máy bay, ta có thể kiểm thử các điều kiện cực đoan như mạng không ổn định hoặc mất điện trước production
- Simulator này có thể là tổng quát hoặc được tham số hóa cho behaviours
- Lập trình viên không cần tự viết simulator riêng
- Độ phức tạp được che giấu sau behaviour, giống như mã đồng thời của
gen_server
- Về kiểm thử của FoundationDB, Kyle “aphyr” Kingsbury từng tweet theo ý rằng các bài test của FoundationDB trông nghiêm ngặt hơn nhiều so với các bài test Jepsen của ông
- Kiểm chứng hình thức cũng dễ hơn nếu chương trình được viết dưới dạng state machine
- Công việc model checking bằng TLA+ của Lamport giả định đặc tả là state machine
- Kleppmann chỉ ra cách dùng quy nạp cấu trúc dựa trên cấu trúc state machine để xử lý vấn đề state explosion
- Cấu trúc rút ra từ Erlang behaviours có thể được dùng lại để giải quyết dễ hơn những vấn đề mà Joe Armstrong từng cho là khó
Công việc liên quan và tài liệu tham khảo
- Các công việc liên quan lấy ý tưởng từ Erlang gồm
- Công việc lấy ý tưởng từ LMAX Disruptor của Martin Thompson và aeron để tạo event loop nhanh cho behaviours chạy trên đó
- Công việc thêm async I/O vào kiểu state machine
- Công việc triển khai chi tiết hơn supervisors
- swapping hot code của state machine
- Tài liệu tham khảo gồm luận án tiến sĩ của Joe Armstrong, OTP design principles, tài liệu
gen_server,gen_event,gen_statem,supervisor,application, release - Về quan hệ giữa actor model và Erlang, bài viết tóm lược rằng các nhà phát triển Erlang đã tạo ra Erlang khi chưa biết actor model, và bài báo của Carl Hewitt ghi lại các khác biệt giữa process của Erlang và actor model
- Akka có “actors” và supervisor trees, nhưng dường như không có thứ tương ứng với các Erlang behaviours khác; khái niệm “behavior” của Akka cũng khác với Erlang behaviours
1 bình luận
Các ý kiến trên Hacker News
Điều đáng kinh ngạc ở Erlang và BEAM là chiều sâu của các tính năng. Trong bài gốc, Behaviour/Interface là ấn tượng cốt lõi, nhưng với tôi, điều lớn hơn là khi xây dựng các hệ thống phức tạp, nó làm được với ít tài nguyên phát triển hơn nhiều so với các ngôn ngữ khác
Bản thân OTP cũng có rất nhiều thứ bên trong. Tôi từng thử biên dịch Elixir để chạy trên thiết bị iOS, và không chỉ dùng quy trình release của Erlang mà còn dùng thư viện ei để biên dịch một Node bằng C, rồi cho nó giao tiếp như một mạng phân tán thông thường với các node Erlang khác như Erlang, Elixir, Gleam, v.v.
Ngoài ra, thông qua thư viện rpc của Erlang, tôi còn có thể gọi hàm của ứng dụng Elixir từ C. Có overhead mã hóa/giải mã và FFI chắc sẽ nhanh hơn, nhưng nó vẫn nằm gọn trong ngân sách độ trễ, và tôi có thể dựng được một tính năng mà trước đó thậm chí chưa từng nghe đến chỉ trong vài ngày
Ý lớn hơn là Erlang đã giải quyết từ hàng chục năm trước nhiều vấn đề mà các stack công nghệ hiện đại đang vật lộn, xét cả về quy mô lẫn chi phí triển khai. HN có phần dành tình cảm kiểu câu click cho Erlang/Elixir nhưng điều đó không dẫn đến việc áp dụng thực tế, và có những công ty đang đốt tiền để triển khai những thứ mà trong stack Erlang vốn được nhận miễn phí theo mặc định
Dự án tôi làm là pipeline dữ liệu backend, lượng dữ liệu xử lý cũng không nhiều, vậy mà việc cô lập chính xác bug cốt lõi lại khó đến mức khó tin
Trong quá trình đó, tôi hiểu ra nhiều đặc tính của Node.js, và khi so sánh với Elixir/Erlang/OTP, tôi đi đến kết luận rằng Node.js về mặt thiết kế có độ tin cậy thấp
Tôi cũng làm Ruby khá nhiều và từng đụng đến Python, nhưng nhiều nền tảng ngôn ngữ thế hệ hiện nay gặp khó khăn trong việc xây dựng hệ thống phân tán đáng tin cậy. BEAM VM và nền tảng OTP đã giải quyết được phần đó rồi
Ví dụ, GenServer, thứ đóng vai trò worker trong phần lớn hệ thống BEAM, rốt cuộc là một cấu trúc gọi nhiều hàm với các tham số đơn giản
Vì vậy, có thể kiểm thử bằng cách gọi trực tiếp các hàm đó, truyền tham số thủ công rồi chỉ cần kiểm tra đầu ra. Không cần thiết lập một hệ thống kiểm thử phức tạp để xử lý mã bất đồng bộ, hay phải dừng lại trong test để chờ công việc hoàn tất
Đây là điểm các junior hay bỏ lỡ, nhưng một khi nhận ra thì khá giải phóng
Tôi từng thấy vài người, chủ yếu là các quản lý, nói rằng họ sẽ viết sách dựa trên kinh nghiệm của chúng tôi. Điều luôn khiến tôi bực là mỗi người lại xem một phần khác nhau là cốt lõi cho lý do công việc của chúng tôi thành công. Những thứ tôi cảm thấy là thiết yếu thì họ thường thu nhỏ lại thành mức “có thì tốt”
Ở đây cũng vậy, có người nói lightweight process và message passing không phải là nước sốt bí mật, nhưng lại bỏ lỡ việc góc nhìn xem Erlang như các tiến trình tuần tự giao tiếp với nhau (CSP) là thứ không thể tách rời khỏi các đặc tính đó. Thế nhưng họ vẫn lặp đi lặp lại việc nhắc CSP như một phần của nước sốt bí mật
Ví dụ như lập trình viên ứng dụng viết mã tuần tự còn tính đồng thời được ẩn bên trong behaviour, hoặc logic nghiệp vụ là tuần tự nên thành viên mới dễ bắt đầu, hoặc supervisor và triết lý “let it crash” tạo ra hệ thống đáng tin cậy
Behaviour thú vị, từng phổ biến trong thập niên 80 và giải quyết vấn đề mà đến thập niên 2000 một số nơi vẫn còn xử lý, nhưng trong Erlang nó vừa là mục đích vừa là phương tiện. Nó là cách triển khai các đặc tính khác đó, nhưng tôi không biết liệu Erlang bắt buộc phải như vậy thì mới còn đúng chất Erlang hay không
CSP là nhánh đã truyền cảm hứng cho Go channel thông qua occam và các ngôn ngữ khác. Khác biệt rõ nhất là cách đồng bộ trên channel không có buffer, và còn có những khác biệt như pattern matching trên mailbox trong mô hình actor
Tranh luận CSP với mô hình actor khá thú vị vì bề ngoài trông giống nhau nhưng hàm ý thực tế lại rất khác
Tôi thấy mô hình actor hợp lý hơn, nhưng điều này có thể khác rất nhiều tùy từng người
Tôi vào đây vì muốn biết thêm tại sao Ericsson ngừng dùng Erlang, và tại sao Joe bị cho thôi việc
Nhìn ngắn gọn thì Ericsson đã chuyển các dự án mới sang Java, và có vẻ vì thế Erlang bị đẩy ra rìa. Sau đó Joe và các đồng nghiệp lập Bluetail vào năm 1998, rồi được Nortel mua lại
Nortel là một gã khổng lồ viễn thông từng chiếm khoảng một phần ba giá trị của Sở giao dịch chứng khoán Toronto. Năm 2000, giá cổ phiếu lên tới 125 đô la mỗi cổ phiếu, nhưng đến năm 2002 đã rơi xuống dưới 1 đô la. Đó là một phần của cú sụp dot-com, và cùng với sự sụt giảm mạnh trong chi tiêu viễn thông, Nortel bị ảnh hưởng đặc biệt nặng
Có vẻ an toàn hơn khi xem việc Joe mất việc là tình huống “bộ phận của ông ấy bị ngập nước trước trên con tàu đang chìm”. Nortel đã sa thải 60.000 người, tức hơn hai phần ba tổng nhân lực. Đợt sa thải đó không phải tín hiệu rằng Joe làm không tốt vai trò của mình, cũng không cho thấy đơn vị kinh doanh kém hiệu quả, mà là một biện pháp quy mô lớn trong thế tuyệt vọng
“fired” mang phán xét giá trị mạnh và tạo sắc thái rằng người đó bị sa thải có lý do vì lỗi của họ. Ngay cả nếu thực sự là vậy thì tác giả bài gốc cũng không thể biết, và cũng không cần biết
Tôi bắt đầu nhìn lại Erlang vì lightweight process và message passing, còn cho đến giờ behaviour chỉ là thứ phụ
Dự án này là đưa lập trình dựa trên luồng (FBP) trực quan vào Erlang. FBP có vẻ như được tạo ra cho Erlang, nên tôi khá ngạc nhiên khi không thấy implementation nào có sẵn
Công cụ tôi chủ yếu dùng cho FBP là Node-RED, nên ý tưởng cơ bản là gắn frontend Node-RED vào backend Erlang và biến mọi node thành process. Frontend Node-RED rất phù hợp để mô hình hóa việc truyền message giữa các node, nên có thể map 1:1 rất đơn giản sang process và message của Erlang
Tôi đã triển khai một số chức năng cơ bản, và bắt đầu biến các luồng thành unit test để từ từ xây dựng thêm tính năng. Sẽ rất tốt nếu tương thích 100% với Node-RED, backend Node.js. Chi tiết có trong kho GitHub → https://github.com/gorenje/erlang-red
Nhìn chung Erlang hợp với việc này đến mức đáng kinh ngạc, và tôi ngạc nhiên vì sao chưa ai làm thứ tương tự. Hay là đã có rồi?
[1] = https://jpaulm.github.io/fbp/index.html
Với tôi, sức mạnh của Erlang/Elixir không nằm hẳn ở việc triển khai mô hình actor, pattern matching xuất phát từ Prolog, tính bất biến, hay bản thân behaviour, mà ở ý chí của Joe muốn cho thấy rằng có thể làm được nhiều hơn với ít thứ hơn
Đây là một hệ thống tính toán được thiết kế tốt và đã được kiểm chứng, có mức độ nhất quán hiếm thấy ở các ngôn ngữ khác, cũng như trong thế giới “web”. Nó không hoàn hảo, nhưng khá ấn tượng
Đáng tiếc là trong thế giới phần mềm, tôi thấy có sự thiếu công nhận và thiếu chấp nhận đáng kể đối với những gì sự đơn giản có thể đem lại. Sự phức tạp khiến người ta trở thành chuyên gia, giúp nhà quản lý biện minh cho các đội lớn và nhiều cuộc họp, và giữ cho chuyên gia tiếp tục là chuyên gia
Erlang được phát triển vào thời điểm các công ty cố gắng triển khai giải pháp phần mềm với ít người hơn và hiệu năng hạn chế hơn. Trong vài thập kỷ sau đó, tiền đổ vào lĩnh vực này, khiến giá trị “ít hơn nghĩa là nhiều hơn theo cách tốt cho tất cả mọi người” trở nên kém hấp dẫn hơn
Khái niệm thú vị nhất trong Erlang/BEAM là phục hồi cục bộ được tích hợp ngay từ đầu
Khi gặp trạng thái không mong đợi, thay vì giết toàn bộ process hoặc tiếp tục chạy và chấp nhận hư hại, nó quay về một trạng thái tốt đã biết ở mức độ chi tiết nhất có thể
Ý tưởng này từng được nghiên cứu từ lâu dưới tên “microreboots” và cũng liên quan đến “crash-only software”, nhưng chỉ Erlang/BEAM biến nó thành khái niệm hạng nhất trong hệ thống vận hành
Erlang cung cấp mô hình tốt để xử lý những vấn đề này, nhưng không có nghĩa là bạn có thể tắt não hoàn toàn. Nếu bước vào với suy nghĩ cứ để nó restart là mọi thứ sẽ ổn, bạn có thể bị bỏng
Erlang, OTP, BEAM cung cấp nhiều hơn behaviour. VM gần với một kernel ảo có supervisor, process được cô lập, và distributed mode xử lý nhiều máy vật lý/ảo như một pool tài nguyên duy nhất
OTP cũng cung cấp nhiều mode hữu ích như Mnesia, một database, atomic counter dùng cho caching và các bảng ETS. Runtime còn hỗ trợ hot reload bytecode để áp dụng patch mà không dừng hệ thống. Cú pháp không thân thiện với screen reader, nhưng vẫn đọc được
Nền tảng gần với BEAM về khả năng xử lý tài nguyên của nhiều máy như một pool duy nhất mà tôi nghĩ đến chỉ có Apache Mesos[1]
Hơn một năm trước, công ty tư vấn cá nhân của tôi đã chọn Erlang làm ngôn ngữ backend. Kể từ đó, tôi bắt đầu khám phá bên trong BEAM, chẳng hạn như đổi stack dựa trên TCP sang QUIC hoặc tích hợp patch Rust
Đây là lựa chọn thật sự tuyệt vời cho các hệ thống nhẹ, throughput cao, gần như không thất bại trừ khi kernel panic hoặc mất điện. Hiện tôi đang xây dựng phần mềm rất bận rộn và có tính đồng thời cao như tracker sản xuất phim/game và pipeline manager, đồng thời cũng đang chuẩn bị R&D cho dịch vụ quản lý bệnh viện tư nhân
[1]: https://mesos.apache.org/
Khả năng cao là tôi sẽ phải tự duy trì các binding hoặc implementation nội bộ cho những thứ không được bảo trì ở phía Elixir
Ngoài ra còn có nhiều syntactic sugar, và người dùng có một nỗi ám ảnh khá lạ với việc trừu tượng hóa mọi thứ thành giao diện DSL
Có lý do cho câu hỏi: “Tại sao các nhà thiết kế ngôn ngữ và thư viện chỉ sao chép ý tưởng tiến trình nhẹ và truyền thông điệp, mà không đánh cắp cấu trúc phía sau Erlang behaviour?”
Chữ ký hàm của Erlang behaviour gắn rất chặt với các tính năng khác của Erlang, đặc biệt là cách sử dụng tính bất biến rất khác thường. Đó cũng là lý do server cần một lời gọi init riêng, và để quản lý trạng thái hoạt động theo cùng cách thì nó cũng cần một hình dạng rất rõ ràng
Nhưng để đạt cùng mục tiêu trong ngôn ngữ khác, gần như luôn có rất nhiều trường hợp không nên sao chép y nguyên những gì Erlang làm. Nếu ai đó nói “đã port gen_server sang ngôn ngữ khác” mà lại đưa ra đúng cùng một interface như Erlang, tôi sẽ xem đó là một bản port chưa hiểu sâu Erlang đang làm gì
Khi tôi đưa ý tưởng supervisor tree sang Go[1] cũng vậy, tôi làm theo cách phù hợp với Go. Trong Go hiện đại, interface đúng cho thứ “có thể được giám sát” không phải chữ ký giống Erlang, mà chỉ là như sau
type Service interface {
Serve(context.Context)
}
Trong Go, vậy là đủ, và chỉ nên dùng đến mức đó. Ở ngôn ngữ khác có thể khác. Go có channel nên không cần “handle_event/2”, và nên dùng channel. Không phải vì channel tốt hơn hay tệ hơn, mà vì ngôn ngữ đó vận hành như vậy
Ở ngôn ngữ khác có thể dùng thứ khác, và trong hạ tầng khác, thay vì “gọi handle_event/2”, cũng có thể gửi vào Kafka hoặc cloud event bus. Điểm cốt lõi là xây dựng hệ thống hướng sự kiện, chứ không phải sao chép chính xác cách triển khai của Erlang
Cộng đồng Erlang có vấn đề là quá tin chắc rằng cách chính xác Erlang làm có gì đó cực kỳ đặc biệt, và nếu làm khác thì lập tức sai và không đáng tin cậy. Năm 2005 có thể đúng như vậy, nhưng năm 2025 thì không
Đã có thời Erlang gần như là câu trả lời hợp lý duy nhất, nhưng vấn đề của năm 2025 là phải tìm đường giữa một biển câu trả lời. Tôi rất khuyến nghị học từ Erlang để xây dựng phần mềm đáng tin cậy, nhưng phản đối mạnh việc port mù quáng chính xác cách Erlang đạt được điều đó sang ngôn ngữ khác. Trong gần như mọi bối cảnh ngôn ngữ khác, đó là đáp án sai. Ngay cả các ngôn ngữ bất biến khác cũng có cấu trúc đủ khác để không thể sao chép nguyên xi
[1]: https://jerf.org/iri/post/2930/
Với tư cách người dùng Elixir trong công việc, đây chính là câu hỏi thú vị nhất về Erlang đối với tôi
Nhìn vào thành công lớn của k8s, Kafka và các sản phẩm hệ thống phân tán của AWS, rõ ràng nhu cầu về công cụ hỗ trợ thiết kế hệ thống đồng thời đáng tin cậy là rất lớn. Vậy tại sao Erlang/Elixir lại không giành được phần đó?
Tôi thường thảo luận với bạn bè, nhưng vẫn không biết câu trả lời
Tôi nghĩ Erlang đã trừu tượng hóa việc xử lý thông điệp qua mạng. Hơn nữa, tôi đã thấy 3–4 biến thể của pattern được đề xuất cho những thứ như server chạy lâu dài
Tôi hoàn toàn đồng ý rằng khi port nên dùng các thành phần mang tính idiom của ngôn ngữ. Nhưng ngôn ngữ có thể có các cơ chế ẩn khiến ta đánh mất bản chất có giá trị trong quá trình port. Có thể gọi đó là một dạng phản chủ nghĩa tương đối trong ngôn ngữ lập trình
“Channel à? Cứ bọc nó bằng X là được” có thể gây hại cho khả năng tương tác hơn nhiều so với vẻ ngoài. Ví dụ, nhìn vào http.Handler của Go: nó đơn giản, nhưng việc nó nằm trong thư viện chuẩn có hàm ý thực tế rất lớn. Nhờ đó hình thành một hệ sinh thái middleware nhìn chung tương thích với nhau mà không cần điều phối trước
io.Reader và những thứ liên quan cũng tương tự. Có thể xem các interface cực kỳ đơn giản như vậy còn giá trị hơn cả phần triển khai
Tôi đoán rằng nếu Erlang đã xác định đúng nhiều interface cho hệ thống phân tán đáng tin cậy, thì chính điều đó có thể là yếu tố làm cho toàn bộ hệ thống trở nên khả thi
Tôi không đồng ý. Interface là một khái niệm nhỏ có thể gắn vào bất kỳ ngôn ngữ nào. Ngay cả trong ngôn ngữ không có cú pháp interface chính thức, vẫn có thể mô phỏng nó trong không gian chương trình
Lý do BEAM thành công là vì nó có thể chạy 1 triệu process trên một node, dễ biểu diễn các máy trạng thái phân tán phức tạp, và có thể khởi động lại một phần hệ thống mà không gián đoạn. Ngoài ra còn nhiều lý do khác
Tôi thật sự không nghĩ behaviour/interface là mảnh ghép cốt lõi nhất
Heap process cô lập thì có thể dùng process của OS; supervisor tree thì dùng Kubernetes; truyền thông điệp thì trong Java có thể tạo bằng hai thread và một shared queue; hot code loading thì Java cũng làm được; xử lý độ trễ thấp thì nếu tinh chỉnh LMAX Disruptor có khi còn thắng Erlang, đại loại vậy
Nhưng điểm mấu chốt là gom tất cả những thứ đó vào một nền tảng hoặc thư viện. Process của OS thì nặng, không dễ chạy 2 triệu cái trên server. Dùng green thread hoặc promise thì mất heap cô lập
Kubernetes cũng dùng được ở mức nào đó, nhưng không xử lý tốt supervisor tree lồng nhau. Có thể làm được, nhưng ngoài code ra sẽ xuất hiện đủ thứ như pod, controller, volume
Có thể truyền thông điệp bằng các thư viện actor của nhiều ngôn ngữ, nhưng chúng không tích hợp minh bạch việc pattern matching trong receive hay gửi sang thread khác trên node khác
Hot code loading cũng có thể làm được, nhưng vấn đề là xử lý cấu trúc dữ liệu runtime và trạng thái như thế nào. Erlang được xây dựng quanh điều đó, và vì gen_server có trạng thái bất biến và tường minh, nó có callback để nâng cấp không chỉ code mà cả chính trạng thái
Tôi không nói BEAM loại bỏ vấn đề này, nhưng có vẻ nó làm giảm độ dốc của đường thẳng. Các idiom và tính năng tự nhất quán, bao gồm triển khai, tự phục hồi và cân bằng tải, giúp giảm ma sát giữa các module
Nó cho phép 12 kỹ sư dễ dàng quản lý 30 endpoint, đồng thời tạo ra một hệ thống mà diện tích bề mặt vẫn có thể tuân theo quy luật lũy thừa
Tôi không đồng ý với nội dung bài viết này. Behaviour có được là nhờ kiến trúc nền tảng của hệ thống
Behaviour không phải là interface, mà gần với đối tượng trừu tượng trong các ngôn ngữ như Java hơn. Nó triển khai các chức năng cơ bản và tự hoàn chỉnh ẩn sau interface cộng tác, nhưng sẽ không làm được nhiều nếu thiếu hạ tầng nền tảng đảm bảo rằng mỗi process được tách biệt hoàn toàn với các process khác, mọi process đều có thể được đóng an toàn mà không rò rỉ bộ nhớ hay tài nguyên, và không thể chia sẻ các con trỏ nguy hiểm giữa hai process
Điều Joe trình bày trong bài báo là cách xây dựng một hệ thống đáng tin cậy, hơn nữa là một hệ thống phân tán đáng tin cậy ở mức nào đó, từ một tập các khối Lego cho trước
Để triển khai đúng những thứ như vậy thì cần Erlang VM, và không thể triển khai trọn vẹn trên các VM khác. Nếu không có phần đường ống bên dưới, supervisor tree sẽ bị rò rỉ. Trong Java, bạn không thể giết một thread đang giữ tài nguyên rồi kỳ vọng mọi thứ luôn ổn, và cũng thiếu cách để giám sát các process khác nhau