W ramach moich badań piszę na serwerze Java o wysokim obciążeniu, serwer echo TCP/IP. Chcę obsłużyć około 3-4 tys. Klientów i zobaczyć maksymalną możliwą liczbę wiadomości na sekundę, którą mogę wycisnąć. Rozmiar wiadomości jest dość mały - do 100 bajtów. Ta praca nie ma żadnego praktycznego celu - tylko badania.Serwer Java NIO o dużym obciążeniu TCP
Zgodnie z wieloma prezentacjami, jakie widziałem (testy porównawcze HornetQ, LMAX Disruptor itp.), Rzeczywiste systemy o dużym obciążeniu obsługują miliony transakcji na sekundę (uważam, że Disruptor wspomniał o 6 milach i - 8,5). Na przykład, this post stwierdza, że możliwe jest osiągnięcie do 40M MPS. Tak więc potraktowałem to jako przybliżoną ocenę tego, do czego powinien mieć nowoczesny sprzęt.
Napisałem najprostszy jednoniciowy serwer NIO i uruchomiłem test obciążenia. Byłem trochę zaskoczony, że mogę dostać tylko około 100k MPS na localhost i 25k z rzeczywistą siecią. Liczby wyglądają na małe. Testowałem na Win7 x64, core i7. Patrząc na obciążenie procesora - tylko jeden rdzeń jest zajęty (co jest oczekiwane w aplikacji jednowątkowej), podczas gdy reszta pozostaje bezczynna. Jednak nawet jeśli załaduję wszystkie 8 rdzeni (w tym wirtualne), nie będę miał więcej niż 800k MPS - nawet blisko 40 milionów :)
Moje pytanie brzmi: jaki jest typowy wzorzec służący do przesyłania ogromnych ilości wiadomości do klientów ? Czy powinienem dystrybuować ładowanie sieci przez kilka różnych gniazd wewnątrz pojedynczej maszyny JVM i użyć jakiegoś systemu równoważenia obciążenia, takiego jak HAProxy, aby rozdzielić obciążenia na wiele rdzeni? Czy powinienem spojrzeć w kierunku używania wielu selektorów w moim kodzie NIO? A może nawet rozłożyć obciążenie między wiele maszyn JVM i używać Kroniki do budowania komunikacji między procesami między nimi? Czy testowanie na odpowiednim systemie operacyjnym, takim jak CentOS, robi dużą różnicę (może to Windows spowalnia działanie)?
Poniżej znajduje się przykładowy kod mojego serwera. Zawsze odpowiada "ok" na wszystkie przychodzące dane. Wiem, że w prawdziwym świecie musiałbym śledzić rozmiar wiadomości i być przygotowanym na to, że jedna wiadomość może być podzielona na wiele odczytów, ale na razie chciałbym zachować rzeczy super proste.
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 milionów transakcji na sekundę ** na serwer ** ?! Muszą odpowiadać jednym bajtem. –
Uważam, że było to bez logiki biznesowej - tylko w obie strony wiadomości. Ale tak, to właśnie widziałem w tym poście. Niesamowite liczby. – Juriy
Nie musisz czekać na OP_WRITE, zanim będziesz mógł pisać. Trzeba to zrobić tylko po napisaniu o zerowej długości. Nie trzeba anulować klucza przed lub po zamknięciu kanału. – EJP