Quần Cam Blog

Poolboy và kĩ thuật pooling trong Erlang/Elixir

poll

Pool là gì?

Pooling là một kĩ thuật được sử dụng rộng rãi trong lập trình để giúp giữ tài nguyên sẵn dùng trong hệ thống, thay vì khởi tạo và giải phóng mỗi lần sử dụng. Tài nguyên ở đây có thể là kết nối tới database, sockets, hoặc threads, v.v.

Dùng pool có thể giúp tăng performance ứng dụng, giảm latency (đối với các thao tác đòi hỏi cost lớn), giúp khống chế rate limit (HTTP requests) và các tài nguyên dùng chung.

Chú ý: Hãy phát âm là /puːl/ (nhấn mạnh âm l), đừng chỉ đọc là pu:, nó sẽ ra thành nghĩa khác không được sạch sẽ lắm -> 💩.

Để tui ví dụ một phát để các bạn chưa hiểu có thể dễ hình dung. Nhà Quần Cam tuy nghèo nhưng cũng ráng mua được 7 chiếc Lamborghini cho con đi học. Tuy nhiên Quần chỉ sử dụng được 2 chiếc nên quyết định cho thuê chạy Grab 5 chiếc còn lại kiếm thêm thu nhập. Xe ít nhưng nhu cầu thuê thì nhiều, vì vậy Quần đã thuê thằng Poo để quản lý xe.

Thằng Poo sắp xếp kho xe của Quần gồm 5 chiếc chính thức và 2 chiếc dự bị (dự trù cho giờ cao điểm), với 3 API chính:

public Pool(int capacity, int overflow) {};
public Car checkout() {};
public boolean checkin(Car car) {};
  • Khi có ai đó thuê xe, họ sử dụng API checkout():

    • nếu còn dư xe, Poo sẽ lấy một chiếc xe cho họ.
    • nếu hết xe, Poo sẽ báo với Quần Cam cho mượn 2 chiếc dự bị.
    • nếu hết cả xe dự bị, Poo sẽ ném 💩 vào mặt người thuê (… e hèm! văng lỗi).
  • Khi có ai đó trả xe, họ sẽ sử dụng API checkin():

    • nếu đó là xe dự bị, Poo sẽ trả lại cho Quần Cam.
    • nếu đó là xe thường, Poo sẽ cất lại vào kho.

Rất đơn giản đúng không các bạn!

Poolboy

Poolboy gần như là thư viện pooling mặc định khi lập trình Erlang/Elixir. Thư viện này được sử dụng trong khá nhiều phần mềm và thư viện nổi tiếng như Ecto, Riak của Basho, Phoenix framework, v.v.

Ở bài này tui có ý định hướng dẫn cách sử dụng Poolboy, bạn có thể xem hướng dẫn ở ElixirSchool nếu chưa tìm hiểu. Ở đây tui sẽ đi vào cách implementation của Poolboy, cụ thể là 3 thao tác mà tui đã kể ở ví dụ ở trên: init, checkout, checkin.

init

Khi Poolboy khởi tạo, nó thực ra là một GenServer process (hừm … giải thích không khác gì “chị tôi là phụ nữ”). Trạng thái khởi tạo (State) của nó bao gồm:

  • supervisor: Supervisor quản lý tất cả những Worker process của bạn.
  • workers: tài nguyên trong pool, ở đây là những chiếc xe trong ví dụ ở trên, hoặc HTTP/DB connections trong thực tế.
  • waiting: hàng đợi các đối tượng chờ checkout.
  • MaxOverflowoverflow: hai biến int để handle overflow.

Cũng như những implementation ở ngôn ngữ khác, các workers sẽ được pre-populate, Sup supervisor sẽ spawn các worker process con dựa trên các thông tin WorkerMod mà bạn đã truyền vào ban đầu. Mỗi worker cũng được link với pool process.

link/1 là một chức năng error-handling trong Erlang. Với hai process được link với nhau, khi một process đột tử, process kia sẽ nhận được một EXIT signal để tuỳ ý xử lý lỗi. link/1 được sử dụng rất nhiều trong Erlang, supervision tree là một trong những ứng dụng của nó.

checkout

Khi có một process (giả sử tên From) cần checkout, Poolboy sẽ kiểm tra các trường hợp y hệt như thằng Poo trong ví dụ.

  • nếu còn dư worker: Poolboy sẽ trả về cho From ngay tắp lự, đồng thời monitor nó.
  • nếu hết worker: Poolboy sẽ kiểm tra MaxOverflow cho phép, spawn thêm worker nếu được phép và trả về cho From.
  • nếu tràn worker: đây là điểm khác biệt của Poolboy so với một connection pool mà tui biết trong Ruby là connection_pool: thay vì văng lỗi ngay, Poolboy sẽ thêm From vào waiting queue, và tạm thời không reply cho nó.

monitor là một chức năng tương tự như link/1, tuy nhiên monitor/2 chỉ có một chiều. Ví dụ khi bạn gọi monitor(Type, B) trong process A, có nghĩa là A sẽ được notify khi B đột tử. Chiều ngược lại không đúng.

checkin

Khi có một worker được trả lại, Poolboy thay vì ném nó trở lại workers pool như thường lệ, nó sẽ kiểm tra waiting queue, và reply lại cho From đầu tiên trong hàng đợi.

Poolboy implement waiting queue này như thế nào?

TL;DR Sử dụng :noreply trong handle_callgen_server:reply/2.

-module(poolboy).
-behaviour(gen_server).

handle_call(checkout, {FromPid, _} = From, State) ->
    {noreply, State}.

handle_cast({checkin, Worker}, State) ->
    From = queue:out(State#state.waiting),
    gen_server:reply(From, Worker),
    {noreply, State}.

Ở đoạn code trên, khi return noreply trong gen_server:handle_call, gen_server sẽ không trả lời ngay cho caller process mà tiếp tục loop cho đến khi tài nguyên trở nên sẵn sàng sẽ chủ động reply lại cho From cách dùng gen_server:reply/2.

Thông thường khi implement timeout cho Pool với ngôn ngữ khác ví dụ như Ruby, cách tui thường làm là viết một cái loop ở phía client, liên tục poll vào Pool và hỏi.

có xe chưa Quần?
có xe chưa Quần?
có xe chưa Quần?
có xe chưa Quần?
có xe chưa Quần?
có xe chưa Quần?
có xe chưa Quần?
có xe chưa Quần?

Với Erlang VM và OTP, Poolboy chọn cách ngược lại, thay vì để client làm công việc polling, pool sẽ “chủ động” thông báo cho client (chỉ cần lót iPad ngồi hóng trong hàng đợi) khi tài nguyên có sẵn.

Rất thông minh đúng không?

Hẳn bạn sẽ đặt câu hỏi là From sẽ phải chờ đến bao giờ? Khi checkout tài nguyên từ pool, bạn phải định nghĩa ra một timeout. Khi timeout, From sẽ bị rút khỏi queue. Thao tác rút này có time complexity là O(n).

Bài viết này sẽ giúp tui tăng lương thế nào?

Bài viết này được viết trong hoàn cảnh Bitcoin lao dốc, lòng người hoang mang, xã hội điêu đứng. Tác giả cũng không nằm ngoài guồng quay đó, mặc dù không mua coin nào.

Hy vọng bài viết này giúp các bạn hiểu được cơ chế bên trong Poolboy hoạt động thể nào.

Mọi thao tác donate coin vui lòng gửi qua token ZG9udCBiZSBhIGZvb2w=.


NGUY HIỂM! KHU VỰC NHIỀU GIÓ!
Khuyến cáo giữ chặt bàn phím và lướt thật nhanh khi đi qua khu vực này.
Chức năng này hỗ trợ markdown và các thứ liên quan.
linxGnu chém vào lúc

<script>alert('add')</script>

qcam chém vào lúc

Nice try!

linxGnu chém vào lúc

&lt;/p&gt;&lt;script&gt;alert('add')&lt;/script&gt;

linxGnu chém vào lúc

a < b

linxGnu chém vào lúc

<script>alert('test2');</script>

Bài viết cùng chủ đề

IO data và Vectored IO

Bài viết giới thiệu về IO data, Vectored I/O và tối ưu hóa hệ thống dùng Elixir bằng cách tận dụng Vectored I/O.

[Elixir RSS Reader] Phần 1 - HTTP client

Đây là phần 1 của loạt bài viết hướng dẫn học Elixir của mình qua việc viết một RSS reader. Ở phần này mình sẽ viết về GenServer.

Elixir/Erlang, Actors model và Concurrency

Bài viết này sẽ cho bạn cái nhìn tổng quát về concurrency, mô hình actor và Elixir/Erlang, một thực thể áp dụng mô hình này sẽ giúp bạn xây dựng một concurrent, distributed và fault tolerant như thế nào.