Scalabilità
Il termine scalabilità viene spesso usato in modo improprio. Per questa sezione viene fornita una doppia definizione:
- La scalabilità è la possibilità di usare completamente la potenza di elaborazione disponibile in un sistema multiprocessore (2, 4, 8, 32 o più processori).
- La scalabilità è la possibilità di gestire un numero elevato di client.
Queste due definizioni correlate vengono comunemente definite aumento delle prestazioni. La fine di questo argomento fornisce suggerimenti sull'aumento del numero di istanze.
Questa discussione è incentrata esclusivamente sulla scrittura di server scalabili, non su client scalabili, perché i server scalabili sono requisiti più comuni. Questa sezione illustra anche la scalabilità nel contesto dei server RPC e RPC. Le procedure consigliate per la scalabilità, ad esempio la riduzione dei conflitti, l'evitare frequenti mancati riscontri nella cache nelle posizioni di memoria globali o l'evitare la condivisione falsa, non vengono descritte qui.
Quando una chiamata RPC viene ricevuta da un server, la routine del server (routine manager) viene chiamata su un thread fornito da RPC. RPC usa un pool di thread adattivi che aumenta e diminuisce man mano che il carico di lavoro varia. A partire da Windows 2000, il core del pool di thread RPC è una porta di completamento. La porta di completamento e il relativo utilizzo da RPC vengono ottimizzati per le routine del server di contesa da zero a bassa contesa. Ciò significa che il pool di thread RPC aumenta in modo aggressivo il numero di thread di manutenzione se alcuni vengono bloccati. Opera sulla convinzione che il blocco è raro e se un thread viene bloccato, si tratta di una condizione temporanea che viene risolta rapidamente. Questo approccio consente l'efficienza per i server a contesa ridotta. Ad esempio, un server RPC void che opera su un server a otto processori a 550 MHz a cui si accede tramite una rete SAN (High Speed System Area Network) serve più di 30.000 chiamate void al secondo da oltre 200 client remoti. Questo rappresenta più di 108 milioni di chiamate all'ora.
Il risultato è che il pool di thread aggressivo ottiene effettivamente nel modo in cui la contesa sul server è elevata. Per illustrare, si supponga che un server pesante usato per accedere in remoto ai file. Si supponga che il server adotti l'approccio più semplice: legge/scrive semplicemente il file in modo sincrono nel thread in cui tale RPC richiama la routine del server. Si supponga inoltre di avere un server a quattro processori che gestisce molti client.
Il server inizierà con cinque thread (questo in realtà varia, ma per semplicità vengono usati cinque thread). Quando RPC preleva la prima chiamata RPC, invia la chiamata alla routine del server e la routine del server genera l'I/O. Raramente, perde la cache dei file e quindi blocca l'attesa del risultato. Non appena viene bloccato, il quinto thread viene rilasciato per raccogliere una richiesta e viene creato un sesto thread come hot standby. Supponendo che ogni decimo operazione di I/O non contenga la cache e blocchi per 100 millisecondi (un valore di tempo arbitrario) e supponendo che il server a quattro processori gestisca circa 20.000 chiamate al secondo (5.000 chiamate per processore), una modellazione semplicistica prevede che ogni processore genererà circa 50 thread. Si presuppone che una chiamata che blocchi ogni 2 millisecondi e dopo 100 millisecondi il primo thread venga liberato nuovamente, in modo che il pool si stabilizzi a circa 200 thread (50 per processore).
Il comportamento effettivo è più complesso, poiché il numero elevato di thread causerà cambi di contesto aggiuntivi che rallentano il server e rallentano anche la velocità di creazione di nuovi thread, ma l'idea di base è chiara. Il numero di thread si alza rapidamente man mano che i thread nel server iniziano a bloccarsi e ad attendere qualcosa(ad esempio un I/O o l'accesso a una risorsa).
RPC e la porta di completamento che controlla le richieste in ingresso tenteranno di mantenere il numero di thread RPC utilizzabili nel server in modo che siano uguali al numero di processori nel computer. Ciò significa che in un server a quattro processori, quando un thread torna a RPC, se sono presenti quattro o più thread RPC utilizzabili, il quinto thread non è autorizzato a raccogliere una nuova richiesta e invece si troverà in uno stato di hot standby nel caso di uno dei blocchi attualmente utilizzabili. Se il quinto thread attende abbastanza a lungo come hot standby senza il numero di thread RPC utilizzabili scendendo al di sotto del numero di processori, verrà rilasciato, ovvero il pool di thread diminuisce.
Si immagini un server con molti thread. Come spiegato in precedenza, un server RPC termina con molti thread, ma solo se i thread si bloccano spesso. In un server in cui spesso i thread bloccano, un thread che torna a RPC viene presto estratto dall'elenco di hot standby, perché a tutti i thread attualmente utilizzabili viene assegnata una richiesta di elaborazione. Quando un thread si blocca, il dispatcher del thread nel kernel passa il contesto a un altro thread. Questo commutatore di contesto utilizza da solo i cicli della CPU. Il thread successivo eseguirà codice diverso, accederà a strutture di dati diverse e avrà uno stack diverso, il che significa che la frequenza di riscontri nella cache della memoria (le cache L1 e L2) sarà molto inferiore, con conseguente rallentamento dell'esecuzione. I numerosi thread in esecuzione contemporaneamente aumentano la contesa per le risorse esistenti, ad esempio heap, sezioni critiche nel codice del server e così via. Questo aumenta ulteriormente la contesa man mano che i convogli sul modulo delle risorse. Se la memoria è bassa, la pressione della memoria esercitata dal numero elevato e crescente di thread causerà errori di pagina, che aumentano ulteriormente la velocità con cui i thread bloccano e causano la creazione di altri thread. A seconda della frequenza con cui si blocca e della quantità di memoria fisica disponibile, il server può stabilizzarsi a un livello di prestazioni inferiore con una frequenza di cambio di contesto elevata oppure può peggiorare fino al punto in cui accede solo ripetutamente al disco rigido e al cambio di contesto senza eseguire alcuna operazione effettiva. Questa situazione non verrà visualizzata sotto carico di lavoro leggero, naturalmente, ma un carico di lavoro pesante porta rapidamente il problema alla superficie.
Come può essere impedito? Se si prevede che i thread blocchino, dichiarare le chiamate come asincrone e dopo che la richiesta entra nella routine del server, accodarla a un pool di thread di lavoro che usano le funzionalità asincrone del sistema di I/O e/o RPC. Se il server esegue a sua volta chiamate RPC, effettuare tali chiamate asincrone e assicurarsi che la coda non sia troppo grande. Se la routine del server esegue l'I/O dei file, usare l'I/O asincrona per accodare più richieste al sistema di I/O e avere solo pochi thread accodarli e raccogliere i risultati. Se la routine del server esegue di nuovo l'I/O di rete, usare di nuovo le funzionalità asincrone del sistema per inviare le richieste e raccogliere le risposte in modo asincrono e usare il minor numero possibile di thread. Al termine dell'I/O o la chiamata RPC effettuata al server, completare la chiamata RPC asincrona che ha recapitato la richiesta. In questo modo il server potrà essere eseguito con il minor numero possibile di thread, aumentando così le prestazioni e il numero di client che un server può gestire.
RPC può essere configurato per l'uso con Bilanciamento carico di rete (NLB) se bilanciamento carico di rete è configurato in modo che tutte le richieste provenienti da un determinato indirizzo client vengano inviate allo stesso server. Poiché ogni client RPC apre un pool di connessioni (per altre informazioni, vedere RPC e rete), è essenziale che tutte le connessioni dal pool del client specificato finissono nello stesso computer server. Se questa condizione viene soddisfatta, un cluster bilanciamento carico di rete può essere configurato per funzionare come un server RPC di grandi dimensioni con scalabilità potenzialmente eccellente.