本篇文章說明連線數上限相關設定,以及實測 ActionCable 預設模式、Standalone 模式和 AnyCable 三種方案的效能比較。

準備

首先建立一個 Rails 專案來測試 ActionCable 連線效能,先寫一個簡單的 Channel

1
2
3
4
5
class MessageChannel < ApplicationCable::Channel
def subscribed
stream_for 'message_channel'
end
end

config/environments/development.rb 加上

1
config.action_cable.disable_request_forgery_protection = true

然後寫一個 Node Client 程式來測試,使用套件 @anycable/webws

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import WebSocket from 'ws';
import { createCable } from '@anycable/web';

const URL = 'ws://localhost:3000/cable'; // ActionCable 位址
let COUNT = 10000; // 建立的連線數

class Client {
connected = 0;
received = 0;

createSubscription() {
const cable = createCable(URL, {
websocketImplementation: WebSocket
});
const subscription = cable.subscribeTo('MessageChannel');
subscription.on('message', (msg) => {
if (this.received === 0) {
console.time('message');
}
this.received += 1;
process.stdout.write(`received: ${this.received} / ${COUNT}\r`);
if (this.received === COUNT) {
console.log('');
console.timeEnd('message');
}
});

return new Promise((resolve) => {
subscription.on('connect', () => {
this.connected += 1;
process.stdout.write(`connected: ${this.connected} / ${COUNT}\r`);
if (this.connected === COUNT) {
console.log('');
console.timeEnd('connect');
}
resolve(subscription);
});
});
}

async start() {
console.time('connect');
for (let i = 0; i < COUNT; ++i) {
await this.createSubscription();
}
}
}

new Client().start();

上面加上了計算連接和接收訊息的時間。原本一開始使用 Ruby 的 action_cable_client 套件來寫 Client,但是 Standalone 模式的時候不知道為什麼沒辦法正常運作,後來改成使用 Node 的套件。

連線數上限

Mac

File Descriptors

首先在自己的 Mac 電腦中測試,運行 rails 之後執行 Node 程式,會發現連線數停在兩百多就沒辦法再增加。查看 Rails Log 會看到錯誤訊息:

1
2024-05-19 16:57:24 +0800 Listen loop: #<Errno::EMFILE: Too many open files - accept(2)>

而且這時候嘗試開啟網頁,會打不開。這是因為每個連線會開啟一個檔案,而 Unix 系統對 Process 使用資源有一些限制,輸入下面指令查看限制:

1
ulimit -a

會出現下面資訊

1
2
3
4
5
6
7
8
9
-t: cpu time (seconds)              unlimited
-f: file size (blocks) unlimited
-d: data seg size (kbytes) unlimited
-s: stack size (kbytes) 8176
-c: core file size (blocks) 0
-v: address space (kbytes) unlimited
-l: locked-in-memory size (kbytes) unlimited
-u: processes 2666
-n: file descriptors 256

其中 file descriptors 項目在 Mac 中預設是 256,所以連線數差不多到這數字就無法再增加。我們在目前兩個終端機 (Rails Server 和 Node Client) 個別輸入下面指令將的限制修改:

1
ulimit -n 10000

臨時阜

重新跑一次就可以發現連線數可以超過兩百。接著改成輸入一個更大的數字,例如兩萬,會發現連線數會停在一萬六左右沒辦法繼續增加,這是因為 Mac 的系統限制,輸入下面指令查詢:

1
sysctl net.inet.ip.portrange.first net.inet.ip.portrange.last

會顯示

1
2
net.inet.ip.portrange.first: 49152
net.inet.ip.portrange.last: 65535

這是 Mac 預設的臨時阜範圍,可以發現範圍大約 16383 個 Port 可以使用。可以輸入下面指令暫時修改範圍:

1
sudo sysctl -w net.inet.ip.portrange.first=32768

Ubuntu

File Descriptors

查詢和設定同 Mac,不過 Ubuntu 預設是 1024,使用 ulimit 指令最大只可改成 10001。超過會出現錯誤:

1
ulimit: open files: cannot modify limit: Operation not permitted

如果要永久修改,編輯 /etc/security/limits.conf 加入:

1
2
* soft nofile 10000
* hard nofile 10000

這設定方式也可以超過 10001 的限制。

臨時阜

在 Ubuntu 中使用下面指令查詢

1
sysctl net.ipv4.ip_local_port_range

應該會看到

1
net.ipv4.ip_local_port_range = 32768	60999

所以預設是大概 28231 個 Port 可以使用。可以輸入下面指令暫時修改範圍:

1
sudo sysctl -w net.ipv4.ip_local_port_range="30000 60999"

要永久修改,編輯 /etc/sysctl.conf,加入下面這行:

1
net.ipv4.ip_local_port_range = 30000 60999

Nginx

File Descriptors

使用 Nginx 也有 File Descriptors 的限制,可能會看到下面錯誤:

1
accept4() failed (24: Too many open files)

可以修改 nginx.conf,加入:

1
worker_rlimit_nofile 10000;

連線數

另外還會有連線數限制,可能會看到下面錯誤:

1
768 worker_connections are not enough

則要修改 nginx.conf events 區塊:

1
2
3
events {
worker_connections 10000;
}

效能測試

實測 10000 個連線完成所需時間,和發送訊息全部接收到的時間。簡單發送訊息如下:

1
MessageChannel.broadcast_to('message_channel', 'test')

測試環境為 Mac 連線本機的情況。分別測試下面三種方案 (使用 Redis adapter):

  1. 預設的 ActionCable
  2. Standalone ActionCable
  3. AnyCable

以及測試不同程序數和執行緒數的影響,AnyCable 固定會跑兩個程序,所以只調整 rpc_pool_size 做測試。

模式程序執行緒連線時間訊息時間
預設的 ActionCable1535.91 s981 ms
預設的 ActionCable11035.30 s1156 ms
預設的 ActionCable2539.34 s689 ms
Standalone ActionCable155.73 s747 ms
Standalone ActionCable1106.97 s867 ms
Standalone ActionCable257.09 s424 ms
AnyCable23050.05 s187 ms
AnyCable26050.23 s153 ms

測試結果

  1. 使用 Standalone ActionCable 比預設情況效能提升,在佔滿連線的時候也不會影響網頁和 API 使用。
  2. AnyCable 傳送訊息效能最好。
  3. 執行緒數沒有影響。
  4. ActionCable 程序數對於連線速度沒有影響,但對於傳送訊息速度可以提升。
  5. 另外測試時發現,連線數對記憶體影響不大,沒有明顯變化。

另外如果把上面的 Client 程式的 await 拿掉改成下面,會變成同時 Request:

1
2
3
4
5
6
async start() {
console.time('connect');
for (let i = 0; i < COUNT; ++i) {
this.createSubscription();
}
}

在這情況下 ActionCable 還是能正常運作,但 AnyCable 在同時大量 Request 時就無法運作,會一直沒辦法連線成功。所以使用 Standalone ActionCable 方案是不錯的選擇。