Lors des deux précédents articles (disponibles ici et là), j'ai évoqué à plusieurs reprises la notion de NUMA sans trop la détailler. NUMA (NonUniform Memory Access) est devenue l'une des architectures matérielles la plus utilisée dans la conception des serveurs. Je profite donc de cet article pour revenir sur son intégration dans Solaris (notamment les versions 10 et 11).
En théorie, une machine NUMA est composée d'une ou plusieurs nodes disposant chacune d'un certain nombre de CPU(s) et d'une certaine quantité de mémoire. Toutes les nodes sont interconnectées entre elles et partagent l'intégralité mémoire de la machine. Les temps d'accès mémoire contenue dans une node sont inférieurs aux temps d'accès mémoire contenue dans une node distante (modèle de latence).
Un modèle de latence consiste le plus souvent à un ou plusieurs locality groups (lgrps). Chaque lgrp est constitué :
- d'une ou plusieurs CPU(s)
- d'une ou plusieurs pages de mémoire physiques
A noter qu'il existe plusieurs modèles de latence plus ou moins complexes (différents niveaux de latence).
Solaris est "aware" de l'architecture matérielle NUMA : les différentes nodes sont des éléments connus du système Solaris. L'association mémoire / CPUs est connue de Solaris sous la forme de groupe de localité (locality group).
Le framework MPO (Memory Placement Optimization) est un composant essentiel à l'architecture NUMA. Pour une application en cours de fonctionnement, le système Solaris tente de l'exécuter sur une CPU la plus proche de la mémoire pour minimiser le plus possible les temps d'accès à celle-ci.
Quelques notions importantes à connaître :
- Pour chaque nouveau thread un locality group est sélectionné (home lgrp)
- L'algorithme d'ordonnancement tentera d'exécuter un thread sur une des CPUs de son home lgrp
- La classe d'ordonnancement RT n'est pas prise en compte dans cet algorithme
Un thread peut changer de home lgrp uniquement si :
- Le locality group est détruit
- Le thread est binder sur une autre CPU d'un autre locality group
Plusieurs paramètres sont disponibles pour modifier le comportement des lgrps. Voir la section suivante : Locality Group Parameters dans Oracle Solaris Tunable Parameters Reference Manuel.
Maintenant, passons un peu à la pratique... Pour connaître le nombre de lgrps disponible sur un système, il existe deux méthodes : en utilisant la commande kstat ou en passant par mdb :
# kstat -m lgrp
module: lgrp instance: 1
name: lgrp1 class: misc
alloc fail 294068397
cpus 8
crtime 343.726918435
default policy 0
load average 11296
lwp migrations 1309
next-seg policy 0
[...]
# mdb -k
Loading modules: [...]
> ::walk cpu |::print cpu_t cpu_lpl |::print lgrp_t lgrp_id !sort -u
lgrp_id = 0x6bdd
lgrp_id = 0x8356
lgrp_id = 0x840b
lgrp_id = 0xa232
La macro lgrp disponible dans mdb permet d'obtenir bien plus d'informations sur les lgrps dont notamment le nombre de CPUs et leurs répartitions. Attention cette macro n'est pas disponible dans toutes les updates de Solaris 10 :
# mdb -k
Loading modules: [...]
> ::lgrp
LGRPID ADDR PARENT PLATHAND #CPU CPUS
0 fffffffffbc20580 0 DEFAULT 0
1 fffffffffbc0d440 fffffffffbc20580 0 8 0-7
2 fffffffffbc0d4a8 fffffffffbc20580 1 8 8-15
3 fffffffffbc0d510 fffffffffbc20580 2 8 16-23
4 fffffffffbc0d578 fffffffffbc20580 3 8 24-31
Les statistiques des lgrps sont disponibles facilement via la commande kstat. Par exemple pour le lgrp 3, il suffit de saisir la commande suivante :
$ kstat -m lgrp -i 3
module: lgrp instance: 3
name: lgrp3 class: misc
alloc fail 557701
cpus 8
crtime 345.278084135
default policy 0
load average 48125
lwp migrations 41345
next-seg policy 0
next-touch policy 9959223943
pages avail 33554432
pages failed to mark 0
pages failed to migrate from 0
pages failed to migrate to 0
pages free 6976717
pages installed 33554432
pages marked for migration 0
pages migrated from 0
pages migrated to 0
random policy 5496062
round robin policy 0
snaptime 8170819.92639302
span process policy 0
span psrset policy 0
Pour obtenir le home lgrp d'un thread en cours d'exécution, il suffit d'utiliser mdb (l'adresse du lgrp est stockée dans la structure kthread_t) :
# mdb -k
Loading modules: [...]
> 0t26116::pid2proc |::walk thread |::print kthread_t t_lpl |::print struct lgrp_ld lpl_lgrpid
lpl_lgrpid = 0x4
Lors de la création d'un thread, il suffit d'utiliser le one-liner Dtrace ci-dessous pour connaître rapidement son home lgrp :
# dtrace -qn 'thread_create:return { printf("Created thread (PID %d LWP %d) with home lgroup %d\n", pid, tid, curthread->t_lpl->lpl_lgrpid); }"
Created thread (PID 26539 LWP 1) with home lgroup 3
Created thread (PID 26743 LWP 1) with home lgroup 2
Created thread (PID 26745 LWP 1) with home lgroup 4
Created thread (PID 5913 LWP 1) with home lgroup 3
Created thread (PID 26745 LWP 1) with home lgroup 4
Created thread (PID 18609 LWP 26478) with home lgroup 3
Created thread (PID 18609 LWP 26478) with home lgroup 3
Created thread (PID 26757 LWP 1) with home lgroup 4
Created thread (PID 2473 LWP 7) with home lgroup 4
Created thread (PID 26754 LWP 1) with home lgroup 4
^C
Created thread (PID 26763 LWP 1) with home lgroup 1
Created thread (PID 26771 LWP 1) with home lgroup 2
En théorie, un thread sera le plus souvent exécuté sur les CPUs disponibles dans son home lgrp. Pour vérifier cela, j'utilise un petit script Dtrace (inspiré du script getlgrp.d disponible dans Solaris Internals - attention j'ai du modifié quelque peu le code pour qu'il fonctionne sur les dernières versions de Solaris 10) :
# mdb -k
Loading modules: [...]
> 0t11517::pid2proc |::walk thread |::print kthread_t t_lpl |::print struct lgrp_ld lpl_lgrpid
lpl_lgrpid = 0x4
# ./lgrp.d 11517
^C
Threads CPUs and lgrps for PID 11517
TID LGRP CPUID COUNT
================================
1 2 8 2
1 1 2 4
1 1 7 5
1 1 3 6
1 1 5 6
1 1 6 6
1 2 9 6
1 2 10 6
1 2 13 6
1 1 0 7
1 2 11 7
1 1 4 8
1 2 12 8
1 2 15 8
1 1 1 9
1 3 16 9
1 3 20 9
1 3 22 9
1 2 14 10
1 3 21 10
1 3 17 11
1 3 23 11
1 3 19 12
1 3 18 13
1 4 26 216
1 4 31 226
1 4 28 246
1 4 24 247
1 4 29 298
1 4 25 308
1 4 27 342
1 4 30 370
Dans cet exemple, le processus 11517 contient un seul lwp. On constate que celui-ci est exécuté majoritairement sur les différentes CPUs du lgrp 4. L'algorithme du MPO effectue correctement son travail pour favoriser l'exécution du processus sur son home lgrp.
Lors de la création d'un fils (processus ou thread), le système Solaris tente de sélectionner une CPU dans le home lgrp du père. Cependant si les ressources souhaitées (notamment mémoire) ne sont pas disponibles, le système choisit alors un nouveau home lgrp pour ce fils.
Pour observer ces évènements, j'utilise la fonction lgrp_move_thread() :
# ./lgrpmove.d
Thread 1 (pid 13222) from Home lgrp 4 rehomed to New lgrp 3
Thread 1 (pid 18776) from Home lgrp 1 rehomed to New lgrp 2
Thread 1 (pid 18774) from Home lgrp 1 rehomed to New lgrp 4
Thread 864 (pid 18800) from Home lgrp 4 rehomed to New lgrp 2
Thread 1 (pid 18816) from Home lgrp 3 rehomed to New lgrp 1
Thread 1 (pid 18822) from Home lgrp 2 rehomed to New lgrp 1
Thread 1 (pid 15869) from Home lgrp 4 rehomed to New lgrp 3
Thread 1 (pid 15944) from Home lgrp 2 rehomed to New lgrp 1
Thread 1 (pid 18843) from Home lgrp 2 rehomed to New lgrp 3
Thread 1 (pid 15869) from Home lgrp 4 rehomed to New lgrp 3
Thread 1 (pid 15869) from Home lgrp 4 rehomed to New lgrp 3
Thread 1 (pid 18864) from Home lgrp 4 rehomed to New lgrp 1
Thread 1 (pid 15944) from Home lgrp 2 rehomed to New lgrp 1
Thread 1 (pid 18855) from Home lgrp 3 rehomed to New lgrp 2
Thread 1 (pid 18880) from Home lgrp 2 rehomed to New lgrp 3
^C
Thread 1 (pid 18863) from Home lgrp 1 rehomed to New lgrp 3
Thread 1 (pid 18857) from Home lgrp 4 rehomed to New lgrp 3
Dans Solaris 11, nous disposons deux nouvelles commandes lgrpinfo et plgrp pour consulter et changer le home lgrp d'un thread (Je vous encourage à consulter avec attention les pages de manuels de ces deux nouvelles commandes).
La commande lgrpinfo affiche toutes les informations sur les lgrps :
# lgrpinfo
lgroup 0 (root):
Children: 1 2
CPUs: 0-11
Memory: installed 48G, allocated 2.8G, free 45G
Lgroup resources: 1 2 (CPU); 1 2 (memory)
Latency: 70197
lgroup 1 (leaf):
Children: none, Parent: 0
CPUs: 0 2 4 6 8 10
Memory: installed 24G, allocated 915M, free 23G
Lgroup resources: 1 (CPU); 1 (memory)
Load: 0.168
Latency: 48058
lgroup 2 (leaf):
Children: none, Parent: 0
CPUs: 1 3 5 7 9 11
Memory: installed 24G, allocated 1.9G, free 22G
Lgroup resources: 2 (CPU); 2 (memory)
Load: 0.0259
Latency: 48058
Pour obtenir le home lgrp d'un thread, il suffit simplement simplement de saisir la commande plgrp :
# plgrp $$
PID/LWPID HOME
2947/1 2
L'exécution d'une application bénéficiant du meilleur placement est un axe d'optimisation de plus en plus important. Plusieurs recherches vont dans ce sens : après MPO dans Solaris 9 et 10, Solaris 11 utilise aussi des algorithmes de placements pour les I/O (NUMA I/O). Affaire à suivre...
Ci-joint quelques références sur ce sujet :
- Chapitre 16 "Support for NUMA and CMT Hardware" dans Solaris internals
- Chapitre 3 "Scheduling Classes and the Dispatcher" dans Solaris internals
- OpenSolaris NUMA Project
- Querying locality groups (Darryl Gove's blog)