Là một phần trong nghiên cứu của tôi, tôi viết một máy chủ echo/IP echo tải cao trong Java. Tôi muốn phục vụ khoảng 3-4 nghìn khách hàng và xem các tin nhắn tối đa có thể mỗi giây mà tôi có thể rút ra khỏi nó. Kích thước thư là khá nhỏ - tối đa 100 byte. Công việc này không có mục đích thực tế - chỉ là một nghiên cứu.Java Máy chủ NIO TCP tải cao
Theo nhiều bản trình bày mà tôi đã thấy (các tiêu chuẩn HornetQ, các cuộc đàm phán LMAX Disruptor, vv), các hệ thống tải cao thế giới thực có xu hướng phục vụ hàng triệu giao dịch mỗi giây (tôi tin Disruptor đã đề cập khoảng 6 triệu và Hornet) - 8.5). Ví dụ: this post nói rằng có thể đạt tới 40 triệu MPS. Vì vậy, tôi lấy nó như là một ước tính sơ bộ về những gì phần cứng hiện đại nên có khả năng.
Tôi đã viết máy chủ NIO đơn luồng đơn giản nhất và chạy thử nghiệm tải. Tôi đã rất ngạc nhiên rằng tôi chỉ có thể nhận được khoảng 100k MPS trên localhost và 25k với mạng thực tế. Con số trông khá nhỏ. Tôi đã thử nghiệm trên Win7 x64, lõi i7. Nhìn vào tải CPU - chỉ có một lõi đang bận (được mong đợi trên một ứng dụng đơn luồng), trong khi phần còn lại ngồi nhàn rỗi. Tuy nhiên, ngay cả khi tôi tải tất cả 8 lõi (bao gồm cả ảo), tôi sẽ không có hơn 800 nghìn MPS - thậm chí không gần 40 triệu :)
Câu hỏi của tôi là: một mẫu điển hình để phục vụ một lượng lớn thông điệp cho khách hàng là gì ? Tôi có nên phân phối tải mạng qua một số ổ cắm khác nhau bên trong một JVM duy nhất và sử dụng một số loại cân bằng tải như HAProxy để phân phối tải cho nhiều lõi không? Hoặc tôi nên xem xét việc sử dụng nhiều Selectors trong mã NIO của tôi? Hoặc thậm chí có thể phân phối tải giữa nhiều JVM và sử dụng Chronicle để xây dựng một liên lạc giữa các quá trình giữa chúng? Sẽ thử nghiệm trên một hệ điều hành serverside thích hợp như CentOS làm cho một sự khác biệt lớn (có thể nó là Windows mà làm chậm mọi thứ xuống)?
Dưới đây là mã mẫu của máy chủ của tôi. Nó luôn luôn trả lời với "ok" cho bất kỳ dữ liệu đến. Tôi biết rằng trong thế giới thực, tôi cần phải theo dõi kích thước của thông điệp và chuẩn bị rằng một thông điệp có thể được chia nhỏ giữa nhiều lần đọc nhưng tôi muốn giữ mọi thứ siêu đơn giản ngay bây giờ.
public class EchoServer {
private static final int BUFFER_SIZE = 1024;
private final static int DEFAULT_PORT = 9090;
// The buffer into which we'll read data when it's available
private ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE);
private InetAddress hostAddress = null;
private int port;
private Selector selector;
private long loopTime;
private long numMessages = 0;
public EchoServer() throws IOException {
this(DEFAULT_PORT);
}
public EchoServer(int port) throws IOException {
this.port = port;
selector = initSelector();
loop();
}
private void loop() {
while (true) {
try{
selector.select();
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
SelectionKey key = selectedKeys.next();
selectedKeys.remove();
if (!key.isValid()) {
continue;
}
// Check what event is available and deal with it
if (key.isAcceptable()) {
accept(key);
} else if (key.isReadable()) {
read(key);
} else if (key.isWritable()) {
write(key);
}
}
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
private void accept(SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Client is connected");
}
private void read(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
// Clear out our read buffer so it's ready for new data
readBuffer.clear();
// Attempt to read off the channel
int numRead;
try {
numRead = socketChannel.read(readBuffer);
} catch (IOException e) {
key.cancel();
socketChannel.close();
System.out.println("Forceful shutdown");
return;
}
if (numRead == -1) {
System.out.println("Graceful shutdown");
key.channel().close();
key.cancel();
return;
}
socketChannel.register(selector, SelectionKey.OP_WRITE);
numMessages++;
if (numMessages%100000 == 0) {
long elapsed = System.currentTimeMillis() - loopTime;
loopTime = System.currentTimeMillis();
System.out.println(elapsed);
}
}
private void write(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer dummyResponse = ByteBuffer.wrap("ok".getBytes("UTF-8"));
socketChannel.write(dummyResponse);
if (dummyResponse.remaining() > 0) {
System.err.print("Filled UP");
}
key.interestOps(SelectionKey.OP_READ);
}
private Selector initSelector() throws IOException {
Selector socketSelector = SelectorProvider.provider().openSelector();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
InetSocketAddress isa = new InetSocketAddress(hostAddress, port);
serverChannel.socket().bind(isa);
serverChannel.register(socketSelector, SelectionKey.OP_ACCEPT);
return socketSelector;
}
public static void main(String[] args) throws IOException {
System.out.println("Starting echo server");
new EchoServer();
}
}
40 triệu giao dịch mỗi giây ** trên mỗi máy chủ ** ?! Họ phải trả lời bằng một byte đơn. –
Tôi tin rằng đó là không có logic kinh doanh - chỉ có một loạt các thông điệp. Nhưng có, đó là những gì tôi đã nhìn thấy trong bài viết đó. Con số tuyệt vời. – Juriy
Bạn không cần đợi OP_WRITE trước khi có thể viết. Bạn chỉ cần làm điều đó sau khi bạn đã có một chiều dài bằng không viết. Bạn không cần phải hủy khóa trước hoặc sau khi đóng kênh. – EJP