1 điểm bởi GN⁺ 2025-04-13 | 1 bình luận | Chia sẻ qua WhatsApp
  • Đ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
  • supervisor khở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_server là một kho key-value, trong đó store lưu cặp key-value còn lookup tìm giá trị của key
  • Callback cốt lõi handle_call cập nhật trạng thái Dict với yêu cầu store, và tra key trong trạng thái với yêu cầu lookup
  • 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ầu storelookup đến đồng thời
  • Điểm quan trọng là bản thân handle_call hoàn toàn tuần tự
    • Đồng thời được che giấu bên trong thành phần gen_server tổ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
  • 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
  • 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_event là 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_fsm là behaviour state machine về sau được đổi tên thành gen_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_server nào cũng có thể được triển khai bằng gen_statem, và ngược lại
  • application gồ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ụng
  • release đó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!”

  • supervisor là 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_logger bị 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
  • permanent, 500, worker nghĩ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_server có thể được xem dưới dạng sau
    • Input -> 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 Input rồ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
  • gen_event có thể được triển khai bằng cách cho phép đăng ký callback cho các loại event cụ thể trong queue
  • supervisor có thể được đơn giản hóa bằng cách bọc lời gọi hàm gen_server trong 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
  • applicationrelease quan 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_servergen_statem về 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

 
GN⁺ 2025-04-13
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

    • Tôi đã chuyển từ một công ty dùng backend Elixir sang một công ty dùng Node.js. Ban đầu tôi gần như chưa dùng Node.js nên khá trung lập
      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
    • Các khối xây dựng cơ bản mà Erlang và các hậu duệ của nó cung cấp rất dễ xử lý, nên cũng dễ kiểm thử
      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
    • C node đang bị đánh giá thấp. Chúng tôi đang dùng C node dựa trên Cgo để giao tiếp giữa các service Go và Elixir chạy trong cùng một Kubernetes pod. Tài liệu của Erlang và thư viện C cũng khá ổn
    • Tôi tò mò cộng đồng nhìn nhận thế nào về lý do vì sao HN có vẻ thích Erlang/Elixir nhưng điều đó lại không dẫn đến việc áp dụ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

    • Erlang không phải CSP mà là mô hình actor. https://en.wikipedia.org/wiki/Actor_model
      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
    • Có ai xem Erlang là CSP không? Tôi luôn nghĩ không phải vậy, mà là mô hình riêng của nó, trong đó các actor có định danh giao tiếp trực tiếp với nhau. Tôi nghĩ CSP gần hơn với các process ẩn danh dùng channel messaging
      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
    • Các quản lý tạo ra câu chuyện riêng của họ cho hợp với bầu không khí
  • 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

    • Trong ngữ cảnh này, thấy từ “fired” rất lạ. “laid off” phù hợp hơn
      “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

    • Tôi luôn nghĩ một ngôn ngữ lập trình trực quan chạy trên BEAM sẽ rất thú vị
    • Ở mức cao, tôi tự hỏi liệu có thể xem FBP giống với các process Erlang có luồng message một chiều hay không
  • 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

    • Alan Kay từng nói rằng sự đơn giản đạt được bằng cách chọn các khối xây dựng phức tạp hơn một chút. Erlang có vẻ làm đúng như vậy
    • Tôi nhớ đến bài nói Simple VS Easy của Rich Hickey
    • Câu này khiến tôi muốn dành thêm thời gian cho Erlang. Tôi đã thử sơ qua, và ít nhất bề ngoài thì tôi thích cú pháp Erlang hơn Elixir
  • 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

    • Vẫn phải cẩn trọng với supervision tree và việc khởi động lại một phần của cây. Ví dụ, nếu toàn bộ process hệ điều hành Erlang đột ngột chết rồi được khởi động lại, hệ thống có thể hoạt động bình thường, nhưng nếu chỉ một phần của cây process Erlang được khởi động lại, dữ liệu có thể bắt đầu bị hỏng
      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
    • Tôi tự hỏi cách giải thích đó khác gì với xử lý ngoại lệ
  • 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/

    • Tôi không có ý định chuyển hẳn sang Elixir hay Gleam. Vì hệ sinh thái đó được xây dựng xoay quanh Phoenix framework và các dịch vụ/database bên ngoài
      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/

    • Tiếp theo, câu hỏi tự nhiên là: “Vì sao các ngôn ngữ và hệ thống khác có cách tiếp cận và ý tưởng như vậy lại thành công trong việc được thị trường chấp nhận, còn Erlang/Elixir thì không?”
      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 thích Go nhất, nhưng tôi nghi ngờ câu nói “Go có channel nên không cần handle_event/2”. Channel kiểu nào? Quan trọng hơn, channel là cục bộ trong process, muốn đưa lên mạng thì cần mã keo
      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

    • Tôi đồng ý ở một mức nào đó. Nhìn Erlang lần đầu, mỗi tính năng của Erlang dường như đều có thứ tương tự trong các hệ thống khác
      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 chưa dùng đủ nhiều, nhưng tôi nghĩ BEAM phần nào tránh được vấn đề mà Ian Cooper tái phát hiện trong “Where Did It All Go Wrong?”, tức là microservice không tối ưu được ma sát bên trong module/giữa các module của hệ thống
      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