2012-12-27 14 views
46

Oto, co próbuję zrobić: Zajmuję się tworzeniem serwera http Node.js, który będzie utrzymywał długie połączenia dla celów wypychania (współpraca z redis) z dziesiątków tysięcy urządzeń mobilnych klientów na jednym komputerze.Długie połączenia z Node.js, jak zmniejszyć zużycie pamięci i zapobiec wyciekom pamięci? Również związane z V8 i webtit-devtools

środowisko testowe:

1.80GHz*2 CPU/2GB RAM/Unbuntu12.04/Node.js 0.8.16 

Po raz pierwszy użyłem „express” moduł, z którego mogłem osiągnąć około 120k równoczesnych połączeń przed zamiany wykorzystywane co oznacza, że ​​pamięć RAM nie jest wystarczające. Następnie przełączyłem się na macierzysty moduł "http", uzyskałem współbieżność do około 160 tys. Ale zdałem sobie sprawę, że wciąż jest zbyt wiele funkcji, których nie potrzebuję w macierzystym module http, więc zamieniłem go na macierzysty moduł "net" (to znaczy, że muszę obsługiwać protokół http sam, ale to jest w porządku). teraz mogę osiągnąć około 250k równoczesnych połączeń na pojedynczą maszynę.

Oto podstawowa struktura moich kodów:

var net = require('net'); 
var redis = require('redis'); 

var pendingClients = {}; 

var redisClient = redis.createClient(26379, 'localhost'); 
redisClient.on('message', function (channel, message) { 
    var client = pendingClients[channel]; 
    if (client) { 
     client.res.write(message); 
    } 
}); 

var server = net.createServer(function (socket) { 
    var buffer = ''; 
    socket.setEncoding('utf-8'); 
    socket.on('data', onData); 

    function onData(chunk) { 
     buffer += chunk; 
     // Parse request data. 
     // ... 

     if ('I have got all I need') { 
      socket.removeListener('data', onData); 

      var req = { 
       clientId: 'whatever' 
      }; 
      var res = new ServerResponse(socket); 
      server.emit('request', req, res); 
     } 
    } 
}); 

server.on('request', function (req, res) { 
    if (res.socket.destroyed) {    
     return; 
    } 

    pendingClinets[req.clientId] = { 
     res: res 
    }; 

    redisClient.subscribe(req.clientId); 

    res.socket.on('error', function (err) { 
     console.log(err); 
    }); 

    res.socket.on('close', function() { 
     delete pendingClients[req.clientId]; 

     redisClient.unsubscribe(req.clientId); 
    }); 
}); 

server.listen(3000); 

function ServerResponse(socket) { 
    this.socket = socket; 
} 
ServerResponse.prototype.write = function(data) { 
    this.socket.write(data); 
} 

Wreszcie, oto moje pytania:

  1. Jak mogę zmniejszyć zużycie pamięci, tak aby zwiększyć współbieżność dalej?

  2. Naprawdę nie mam pojęcia, jak obliczyć wykorzystanie pamięci w procesie Node.js. Wiem, że Node.js jest zasilany przez Chrome V8, jest api process.memoryUsage() i zwraca trzy wartości: rss/heapTotal/heapUsed, jaka jest różnica między nimi, która część powinna dotyczyć więcej, i jaki jest dokładnie skład pamięci używanej przez proces Node.js?

  3. Martwiłem się o wyciek pamięci, mimo że wykonałem kilka testów i nie wydaje się, aby wystąpił problem. Czy są jakieś punkty, na które powinienem zwrócić uwagę lub jakieś rady?

  4. znalazłem dokument o V8 hidden class, jak to opisano, to znaczy, kiedy tylko dodać obiekt nazwany przez ClientID do mojego obiektu globalnego pendingClients podobnie jak moich kodów powyżej, nie będzie nowej ukryty klasa być generowane? Dawka spowoduje wyciek pamięci?

  5. Użyłem webkit-devtools-agent do analizy mapy sterty procesu Node.js. Rozpocząłem proces i zrobiłem migawkę sterty, a następnie wysłałem do niej 10k żądań i rozłączyłem je później, po czym wziąłem migawkę sterty ponownie. Użyłem perspektywy porównanie, aby zobaczyć różnicę między tymi dwoma migawkami. Oto, co mam: enter image description here Czy ktoś mógłby to wyjaśnić? Liczba i rozmiar (array)/(skompilowany kod)/(string)/Command/Array znacznie się zwiększył, co to znaczy?

EDIT: Jak uruchomić test ładowania?
1. Po pierwsze, zmodyfikowałem niektóre parametry zarówno na maszynie serwerowej, jak i na komputerach klienckich (aby uzyskać więcej niż 60 000 współbieżności potrzeba więcej niż jednego komputera klienckiego, ponieważ jedna maszyna ma tylko 60k + portów (reprezentowanych przez 16 bitów) co najwyżej)
1.1 .Zarówno jeden serwer i komputery klienckie, I zmodyfikowane deskryptor używać tych poleceń w powłoce, gdy program Test zostanie przeprowadzony:

ulimit -Hn 999999 
ulimit -Sn 999999 

1,2. Na maszynie serwera zmodyfikowałem również niektóre parametry jądra związane z siecią/tcp, najważniejsze to:

net.ipv4.tcp_mem = 786432 1048576 26777216 
net.ipv4.tcp_rmem = 4096 16384 33554432 
net.ipv4.tcp_wmem = 4096 16384 33554432 

1.3. Co do maszyn klienckich:

net.ipv4.ip_local_port_range = 1024 65535 

2. Po drugie, napisałem zwyczaj symulować program kliencki przy użyciu node.js, ponieważ większość narzędzi testowych obciążenie, AB, oblężenie, etc, są dla krótkich połączeń, ale jestem używając długich połączeń i mają specjalne wymagania.
3. Następnie uruchomiłem program serwera na jednym komputerze, a trzy programy klienta na pozostałych trzech oddzielnych komputerach.

EDIT: zrobiłem dotrzeć 250k jednoczesnych połączeń na jednym komputerze (2GB RAM), ale okazało się, że nie jest bardzo sensowne i praktyczne. Ponieważ gdy połączenie jest połączone, po prostu pozwoliłem, aby połączenie było w toku, nic więcej. Gdy próbowałem wysłać do nich odpowiedź, liczba współbieżności spadła do około 150 tys. Jak obliczyłem, jest około 4 KB więcej pamięci na połączenie, myślę, że jest to powiązane z net.ipv4.tcp_wmem, które ustawiłem na , ale nawet ja zmodyfikowałem go na mniejsze, nic się nie zmieniło. Nie rozumiem dlaczego.

EDIT: Właściwie teraz jestem bardziej zainteresowany, ile pamięci za połączenie TCP używa i jaki jest dokładnie skład pamięci używanej przez jednego połączenia? Według moich danych testowych:

150k współbieżności zużywanej około 1800m RAM (z wolnego -m wyjściowego), a proces node.js miał około 600M RSS

Potem uznał, że:

  • (1800M - 600M)/150K = 8K, to jest wykorzystanie pamięci stos TCP ziaren pojedynczego związku, składa się z dwóch części: odczytu bufora (4KB) + (bufor zapisu 4KB) (Właściwie, to nie pasuje do mojego ustawienie net.ipv4.tcp_rmem i net.ipv4.tcp_wmem powyżej, w jaki sposób system określić, ile pamięci w użyciu dla tych buforów?)

  • 600M/150k = 4k, to jest zużycie pamięci node.js pojedynczego połączenia

mam rację? Jak mogę zmniejszyć wykorzystanie pamięci w obu aspektach?

Jeśli gdziekolwiek nie opisałem dobrze, daj mi znać, dopracowuję to! Wszelkie wyjaśnienia lub porady zostaną docenione, dzięki!

+2

Pierwsze wrażenie jest takie, że 250k na maszynie z tymi specyfikacjami jest niesamowite. Być może nadszedł czas, aby skupić się na tym, aby wszyscy użytkownicy martwili się teraz. = P – tehgeekmeister

+0

Jak mierzysz liczbę współbieżnych połączeń? – tehgeekmeister

+0

Uwaga boczna: najlepiej jest trzymać się mniejszej liczby konkretnych pytań, pytając o stronę stosu. Otrzymasz więcej odpowiedzi w ten sposób. – tehgeekmeister

Odpowiedz

5
  1. Myślę, że nie należy martwić się o dalsze zmniejszenie zużycia pamięci.Od tego odczytu, który zawierasz, wydaje się, że jesteś całkiem bliski minimalnej wyobrażalnej rzeczy (interpretuję to jako w bajtach, co jest standardem, gdy jednostka nie jest określona).

  2. To jest bardziej szczegółowe pytanie, niż mogę odpowiedzieć, ale oto co jest RSS. Sterta to miejsce, w którym pamięć dynamicznie przydzielana pochodzi z systemów uniksowych, co najlepiej rozumiem. Tak więc, suma sterty wydaje się być, że wszystko zostanie przydzielone na stercie dla twojego użycia, podczas gdy używana sterta to ilość przydzielonych przez ciebie zasobów.

  3. Twoje zużycie pamięci jest całkiem dobre i nie wydaje się, że masz przeciek. Nie martwiłbym się jeszcze. =]

  4. Nie wiem.

  5. Ta migawka wydaje się być uzasadniona. Spodziewam się, że niektóre z przedmiotów stworzonych na podstawie wniosków zostały zebrane, a inne nie. Widzisz, że nie ma nic ponad 10 tysięcy obiektów, a większość tych obiektów jest całkiem mała. Nazywam to dobrem.

Zastanawiam się, jak bardzo to testujesz. Próbowałem wcześniej wykonać tak potężne testy obciążenia, a większość narzędzi po prostu nie jest w stanie wygenerować tego rodzaju obciążenia dla Linuksa, z powodu ograniczeń liczby otwartych deskryptorów plików (zazwyczaj około tysiąca na proces domyślnie). Ponadto, po użyciu gniazda nie jest ono natychmiast dostępne do ponownego użycia. Jak pamiętam, zajmuje to znaczną część minuty, aby znów można było użyć. Pomiędzy tym a faktem, że normalnie widziałem, że limit deskryptorów otwartych dla całego systemu ustawiono gdzieś poniżej 100k, nie jestem pewien, czy możliwe jest otrzymanie tak dużego obciążenia na niezmodyfikowanym pudełku, czy wygenerowanie go w jednym pudełku. Ponieważ nie wspomniałeś o takich krokach, myślę, że być może będziesz musiał zbadać testowanie obciążenia, aby upewnić się, że robi to, co myślisz.

+0

Zaktualizowałem wpis o tym, jak uruchomić test. Wspomniałeś, że "raz używane gniazdo nie jest od razu dostępne do użytku", nie ma takich problemów w moim scenariuszu, sinus używam długich połączeń. I myślę, że mówisz o efemerycznym porcie po stronie klienta, a nie o "gniazdku". –

+0

Err, miałem na myśli deskryptor pliku. Myślę. Może. Doprawdy, docieramy do granic tego, jak dobrze rozumiem te rzeczy. – tehgeekmeister

+0

Usunąłem to ograniczenie poleceniem ** ulimit **. I dziękuję ci za pomoc, przy okazji, nie jestem native speakerem, może jest coś, czego nie wyjaśniłem dobrze, daj mi znać, zrobię co w mojej mocy. :) –

2

Kilka uwag:

Czy trzeba owinąć res w obiekcie {res: res} można po prostu przypisać go bezpośrednio

pendingClinets[req.clientId] = res; 

EDIT inny ~ mikro optymalizacji, które mogą pomóc

server.emit('request', req, res); 

przekazuje dwa argumenty do "request", ale Twój requester naprawdę potrzebuje tylko odpowiedzi "res".

res['clientId'] = 'whatever'; 
server.emit('request', res); 

czasie, gdy kwota rzeczywistych danych pozostaje taki sam, mając mniej argumentu 1 na liście „żądanie” koparki argumenty pozwoli Ci zaoszczędzić wskaźnik odniesienia (kilka bajtów). Ale kilka bajtów podczas przetwarzania setek tysięcy połączeń może się sumować. Zapiszesz także niewielki narzut procesora na przetwarzanie dodatkowego argumentu w wywołaniu emisji.

+0

Tak, przypisz go bezpośrednio będzie działać na teraz, więc spróbuję. Jeśli chodzi o zdarzenie "error", oficjalny dokument Node.js mówi: ** Zdarzenie "close" zostanie wywołane bezpośrednio po tym zdarzeniu **, więc myślę, że moje podejście jest w porządku. Dzięki za te notatki. –

+0

@Aaron Wang - Powinienem z RTFM to zrobić, wydelegowałem go i dodałem kolejną małą optymalizację, która może pomóc ci trochę skrzypić z serwera. –

+0

Przypisywanie ** res ** bezpośrednio do ** pendingClients ** obiektu zapisuje trochę pamięci, około 20M co 60k połączeń, dzięki! Jeśli chodzi o twoją nową notatkę, dlaczego robię to w ten sposób, że dostarczam ten sam interfejs z oficjalnym modułem http, a właściwie użyłem ** req ** do śledzenia informacji o żądaniu w logowaniu, ale nie pokazałem tych szczegółów w powyższych kodach dla uproszczenia. Innym powodem jest to, że ** req ** będzie zbiorem śmieci, więc nie martwię się o to. –

Powiązane problemy