J'utilise une application Django avec nginx et uwsgi. Voici comment exécuter uwsgi:
Sudo uwsgi -b 25000 --chdir=/www/python/apps/pyapp --module=wsgi:application --env Django_SETTINGS_MODULE=settings --socket=/tmp/pyapp.socket --cheaper=8 --processes=16 --harakiri=10 --max-requests=5000 --vacuum --master --pidfile=/tmp/pyapp-master.pid --uid=220 --gid=499
Configurations de & nginx:
server {
listen 80;
server_name test.com
root /www/python/apps/pyapp/;
access_log /var/log/nginx/test.com.access.log;
error_log /var/log/nginx/test.com.error.log;
# https://docs.djangoproject.com/en/dev/howto/static-files/#serving-static-files-in-production
location /static/ {
alias /www/python/apps/pyapp/static/;
expires 30d;
}
location /media/ {
alias /www/python/apps/pyapp/media/;
expires 30d;
}
location / {
uwsgi_pass unix:///tmp/pyapp.socket;
include uwsgi_params;
proxy_read_timeout 120;
}
# what to serve if upstream is not available or crashes
#error_page 500 502 503 504 /media/50x.html;
}
Voici le problème. Lorsque je fais "ab" (ApacheBenchmark) sur le serveur, j'obtiens les résultats suivants:
version nginx: version nginx: nginx/1.2.6
version uwsgi: 1.4.5
Server Software: nginx/1.0.15
Server Hostname: pycms.com
Server Port: 80
Document Path: /api/nodes/mostviewed/8/?format=json
Document Length: 8696 bytes
Concurrency Level: 100
Time taken for tests: 41.232 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 8866000 bytes
HTML transferred: 8696000 bytes
Requests per second: 24.25 [#/sec] (mean)
Time per request: 4123.216 [ms] (mean)
Time per request: 41.232 [ms] (mean, across all concurrent requests)
Transfer rate: 209.99 [Kbytes/sec] received
Lors de l'exécution au niveau de concurrence 500
oncurrency Level: 500
Time taken for tests: 2.175 seconds
Complete requests: 1000
Failed requests: 50
(Connect: 0, Receive: 0, Length: 50, Exceptions: 0)
Write errors: 0
Non-2xx responses: 950
Total transferred: 629200 bytes
HTML transferred: 476300 bytes
Requests per second: 459.81 [#/sec] (mean)
Time per request: 1087.416 [ms] (mean)
Time per request: 2.175 [ms] (mean, across all concurrent requests)
Transfer rate: 282.53 [Kbytes/sec] received
Comme vous pouvez le voir ... toutes les demandes sur le serveur échouent avec des erreurs de dépassement de délai ou "Client déconnecté prématurément" ou:
writev(): Broken pipe [proto/uwsgi.c line 124] during GET /api/nodes/mostviewed/9/?format=json
Voici un peu plus sur mon application: Fondamentalement, c'est une collection de modèles qui reflètent les tables MySQL qui contiennent tout le contenu. Au frontend, j'ai Django-rest-framework qui sert du contenu json aux clients.
J'ai installé Django-profiling & Django debug toolbar pour voir ce qui se passe. Sur Django-profiling voici ce que j'obtiens lors de l'exécution d'une seule requête:
Instance wide RAM usage
Partition of a set of 147315 objects. Total size = 20779408 bytes.
Index Count % Size % Cumulative % Kind (class / dict of class)
0 63960 43 5726288 28 5726288 28 str
1 36887 25 3131112 15 8857400 43 Tuple
2 2495 2 1500392 7 10357792 50 dict (no owner)
3 615 0 1397160 7 11754952 57 dict of module
4 1371 1 1236432 6 12991384 63 type
5 9974 7 1196880 6 14188264 68 function
6 8974 6 1076880 5 15265144 73 types.CodeType
7 1371 1 1014408 5 16279552 78 dict of type
8 2684 2 340640 2 16620192 80 list
9 382 0 328912 2 16949104 82 dict of class
<607 more rows. Type e.g. '_.more' to view.>
CPU Time for this request
11068 function calls (10158 primitive calls) in 0.064 CPU seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.064 0.064 /usr/lib/python2.6/site-packages/Django/views/generic/base.py:44(view)
1 0.000 0.000 0.064 0.064 /usr/lib/python2.6/site-packages/Django/views/decorators/csrf.py:76(wrapped_view)
1 0.000 0.000 0.064 0.064 /usr/lib/python2.6/site-packages/rest_framework/views.py:359(dispatch)
1 0.000 0.000 0.064 0.064 /usr/lib/python2.6/site-packages/rest_framework/generics.py:144(get)
1 0.000 0.000 0.064 0.064 /usr/lib/python2.6/site-packages/rest_framework/mixins.py:46(list)
1 0.000 0.000 0.038 0.038 /usr/lib/python2.6/site-packages/rest_framework/serializers.py:348(data)
21/1 0.000 0.000 0.038 0.038 /usr/lib/python2.6/site-packages/rest_framework/serializers.py:273(to_native)
21/1 0.000 0.000 0.038 0.038 /usr/lib/python2.6/site-packages/rest_framework/serializers.py:190(convert_object)
11/1 0.000 0.000 0.036 0.036 /usr/lib/python2.6/site-packages/rest_framework/serializers.py:303(field_to_native)
13/11 0.000 0.000 0.033 0.003 /usr/lib/python2.6/site-packages/Django/db/models/query.py:92(__iter__)
3/1 0.000 0.000 0.033 0.033 /usr/lib/python2.6/site-packages/Django/db/models/query.py:77(__len__)
4 0.000 0.000 0.030 0.008 /usr/lib/python2.6/site-packages/Django/db/models/sql/compiler.py:794(execute_sql)
1 0.000 0.000 0.021 0.021 /usr/lib/python2.6/site-packages/Django/views/generic/list.py:33(paginate_queryset)
1 0.000 0.000 0.021 0.021 /usr/lib/python2.6/site-packages/Django/core/paginator.py:35(page)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/Django/core/paginator.py:20(validate_number)
3 0.000 0.000 0.020 0.007 /usr/lib/python2.6/site-packages/Django/core/paginator.py:57(_get_num_pages)
4 0.000 0.000 0.020 0.005 /usr/lib/python2.6/site-packages/Django/core/paginator.py:44(_get_count)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/Django/db/models/query.py:340(count)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/Django/db/models/sql/query.py:394(get_count)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/Django/db/models/query.py:568(_prefetch_related_objects)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/Django/db/models/query.py:1596(prefetch_related_objects)
4 0.000 0.000 0.020 0.005 /usr/lib/python2.6/site-packages/Django/db/backends/util.py:36(execute)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/Django/db/models/sql/query.py:340(get_aggregation)
5 0.000 0.000 0.020 0.004 /usr/lib64/python2.6/site-packages/MySQLdb/cursors.py:136(execute)
2 0.000 0.000 0.020 0.010 /usr/lib/python2.6/site-packages/Django/db/models/query.py:1748(prefetch_one_level)
4 0.000 0.000 0.020 0.005 /usr/lib/python2.6/site-packages/Django/db/backends/mysql/base.py:112(execute)
5 0.000 0.000 0.019 0.004 /usr/lib64/python2.6/site-packages/MySQLdb/cursors.py:316(_query)
60 0.000 0.000 0.018 0.000 /usr/lib/python2.6/site-packages/Django/db/models/query.py:231(iterator)
5 0.012 0.002 0.015 0.003 /usr/lib64/python2.6/site-packages/MySQLdb/cursors.py:278(_do_query)
60 0.000 0.000 0.013 0.000 /usr/lib/python2.6/site-packages/Django/db/models/sql/compiler.py:751(results_iter)
30 0.000 0.000 0.010 0.000 /usr/lib/python2.6/site-packages/Django/db/models/manager.py:115(all)
50 0.000 0.000 0.009 0.000 /usr/lib/python2.6/site-packages/Django/db/models/query.py:870(_clone)
51 0.001 0.000 0.009 0.000 /usr/lib/python2.6/site-packages/Django/db/models/sql/query.py:235(clone)
4 0.000 0.000 0.009 0.002 /usr/lib/python2.6/site-packages/Django/db/backends/__init__.py:302(cursor)
4 0.000 0.000 0.008 0.002 /usr/lib/python2.6/site-packages/Django/db/backends/mysql/base.py:361(_cursor)
1 0.000 0.000 0.008 0.008 /usr/lib64/python2.6/site-packages/MySQLdb/__init__.py:78(Connect)
910/208 0.003 0.000 0.008 0.000 /usr/lib64/python2.6/copy.py:144(deepcopy)
22 0.000 0.000 0.007 0.000 /usr/lib/python2.6/site-packages/Django/db/models/query.py:619(filter)
22 0.000 0.000 0.007 0.000 /usr/lib/python2.6/site-packages/Django/db/models/query.py:633(_filter_or_exclude)
20 0.000 0.000 0.005 0.000 /usr/lib/python2.6/site-packages/Django/db/models/fields/related.py:560(get_query_set)
1 0.000 0.000 0.005 0.005 /usr/lib64/python2.6/site-packages/MySQLdb/connections.py:8()
..etc
Cependant, Django-debug-toolbar affiche les éléments suivants:
Resource Usage
Resource Value
User CPU time 149.977 msec
System CPU time 119.982 msec
Total CPU time 269.959 msec
Elapsed time 326.291 msec
Context switches 11 voluntary, 40 involuntary
and 5 queries in 27.1 ms
Le problème est que "top" montre que la moyenne de charge augmente rapidement et que le benchmark Apache que j'ai exécuté à la fois sur le serveur local et à partir d'une machine distante sur le réseau montre que je ne traite pas beaucoup de requêtes/seconde. Quel est le problème? c'est aussi loin que je pouvais atteindre lors du profilage du code, il serait donc apprécié que quelqu'un puisse indiquer ce que je fais ici.
Edit (23/02/2013): Ajout de plus de détails basés sur la réponse d'Andrew Alcock: Les points qui nécessitent mon attention/réponse sont (3) (3) J'ai exécuté "afficher les variables globales" sur MySQL et a découvert que les configurations MySQL avaient 151 pour le paramètre max_connections, ce qui est plus que suffisant pour servir les travailleurs que je commence pour uwsgi.
(3) (4) (2) La seule demande que je présente est la plus lourde. Il exécute 4 requêtes selon Django-debug-toolbar. Ce qui se passe, c'est que toutes les requêtes s'exécutent en: 3,71, 2,83, 0,88, 4,84 ms respectivement.
(4) Ici, vous parlez de pagination mémoire? si oui, comment savoir?
(5) Sur 16 employés, 100 taux de simultanéité, 1000 demandes, la moyenne de charge monte à ~ 12 J'ai effectué les tests sur différents nombres de travailleurs (le niveau de concurrence est de 100):
Comme vous pouvez le voir, plus nous avons de travailleurs, plus nous avons de charge sur le système. Je peux voir dans le journal du démon de uwsgi que le temps de réponse en millisecondes augmente lorsque j'augmente le nombre de travailleurs.
Sur 16 travailleurs, l'exécution de 500 demandes de niveau d'accès simultané uwsgi démarre la journalisation des erreurs:
writev(): Broken pipe [proto/uwsgi.c line 124]
La charge monte également à ~ 10 également. et les tests ne prennent pas beaucoup de temps car les réponses non-2xx sont 923 sur 1000, c'est pourquoi la réponse ici est assez rapide car elle est presque vide. Ce qui est également une réponse à votre point n ° 4 dans le résumé.
En supposant que ce à quoi je fais face ici est une latence du système d'exploitation basée sur les E/S et la mise en réseau, quelle est l'action recommandée pour augmenter cette échelle? nouveau matériel? plus grand serveur?
Merci
EDIT 1 Vu le commentaire que vous avez 1 noyau virtuel, en ajoutant des commentaires sur tous les points pertinents
EDIT 2 Plus d'informations de Maverick, donc j'élimine les idées exclues et développe les problèmes confirmés.
EDIT 3 Rempli plus de détails sur la file d'attente de requêtes uwsgi et les options de mise à l'échelle. Amélioration de la grammaire.
EDIT 4 Mises à jour de Maverick et améliorations mineures
Les commentaires sont trop petits, alors voici quelques réflexions:
S'exécutant à une concurrence de 100, les tests prennent 41 secondes à 24 requêtes/sec. Vous avez 16 processus (threads), donc chaque demande est traitée environ 700 ms. Compte tenu de votre type de transaction, cela représente long fois par demande. Cela peut être dû au fait que:
Il y a considérable latence autour de la base de données, soit:
La répartition entre les coûts du système et du processeur utilisateur est inhabituellement élevée dans le système, bien que le processeur total soit faible. Cela implique que la plupart du travail dans Django est lié au noyau, comme la mise en réseau ou le disque. Dans ce scénario, il peut s'agir de coûts de réseau (par exemple, recevoir et envoyer des requêtes HTTP et recevoir et envoyer des requêtes à DB). Parfois, ce sera élevé en raison de paging. S'il n'y a pas de pagination en cours, vous n'avez probablement pas à vous en préoccuper du tout.
Donc, en résumé:
Il y a une grande latence, donc soit
Inadéquation entre les processus (16) et les cœurs de processeur (1): si la moyenne de charge est> 3, c'est probablement trop de processus. Réessayez avec un plus petit nombre de processus
Si la moyenne de charge <3, elle peut être dans la base de données, alors profilez la base de données pour voir s'il y a des charges de petites requêtes (provoquant additivement la latence) ou une ou deux instructions SQL sont le problème
Développer des idées
Votre charge moyenne> 10 sur une seule machine à noyau est vraiment désagréable et (comme vous l'observez) entraîne beaucoup de changements de tâches et un comportement lent général. Personnellement, je ne me souviens pas avoir vu une machine avec une charge moyenne de 19 (que vous avez pour 16 processus) - félicitations pour l'avoir si élevée;)
Les performances de la base de données sont excellentes, je dirais donc que tout est clair maintenant.
Pagination : Pour répondre à votre question sur la façon de voir la pagination - vous pouvez détecter la pagination du système d'exploitation de plusieurs manières. Par exemple, en haut, l'en-tête comporte des entrées et des sorties de page (voir la dernière ligne):
Processus: 170 au total, 3 en cours d'exécution, 4 bloqués, 163 en veille, 927 threads 15:06:31 Charge moyenne: 0,90, 1,19, 1,94 Utilisation du processeur: 1,37% utilisateur, 2,97% sys, 95,65% SharedLibs inactifs: 144M résidents, 0B données, 24M linkedit. MemRegions: 31726 au total, 2541M résidents, 120M privés, 817M partagés. PhysMem: 1420M câblé, 3548M actif, 1703M inactif, 6671M utilisé, 1514M libre. VM: 392G vsize, 1286M framework vsize, 1534241 (0) pageins, 0(0) Réseaux: paquets: 789684/288M en entrée, 912863/482M en sortie. Disques: 739807/15G en lecture, 996745/24G en écriture.
Nombre de processus : Dans votre configuration actuelle, le nombre de processus est façon trop élevé. Redimensionnez le nombre de processus à 2 . Nous pourrions augmenter cette valeur plus tard, en fonction du décalage de la charge de ce serveur.
Emplacement d'Apache Benchmark : La moyenne de charge de 1,85 pour un processus me suggère que vous exécutez le générateur de charge sur la même machine que uwsgi - est-ce que correct?
Si c'est le cas, vous devez vraiment l'exécuter à partir d'une autre machine, sinon les tests ne sont pas représentatifs de la charge réelle - vous prenez la mémoire et le CPU des processus Web pour les utiliser dans le générateur de charge. De plus, les 100 ou 500 threads du générateur de charge stresseront généralement votre serveur d'une manière qui ne se produit pas dans la vie réelle. En effet, cela pourrait être la raison pour laquelle l'ensemble du test échoue.
Emplacement de la base de données : La moyenne de charge pour un processus suggère également que vous exécutez la base de données sur la même machine que les processus Web - est-ce correct?
Si je ne me trompe pas sur la base de données, la première et meilleure façon de commencer la mise à l'échelle consiste à déplacer la base de données vers une autre machine. Nous le faisons pour deux raisons:
Un serveur de base de données a besoin d'un profil matériel différent d'un nœud de traitement:
Affinité CPU: Il est préférable que le CPU ait une moyenne de charge de 1,0 et que les processus aient une affinité pour un seul cœur. Cela maximise l'utilisation du cache du processeur et minimise les frais généraux de commutation de tâches. En séparant la base de données et les nœuds de traitement, vous appliquez cette affinité dans HW.
500 accès concurrents avec des exceptions La file d'attente des demandes dans le diagramme ci-dessus est au plus 100 - si uwsgi reçoit une demande lorsque la file d'attente est pleine, la demande est rejetée avec une erreur 5xx. Je pense que cela se produisait dans votre test de charge de 500 concurrents - essentiellement la file d'attente remplie avec les 100 premiers threads, puis les 400 autres threads ont émis les 900 demandes restantes et ont reçu immédiatement des erreurs 5xx.
Pour traiter 500 demandes par seconde, vous devez vous assurer de deux choses:
--listen
argument à uwsgi
J'imagine que uwsgi a la file d'attente définie sur un plus petit nombre pour mieux gérer les attaques DDoS; si elles sont placées sous une charge énorme, la plupart des demandes échouent immédiatement avec presque aucun traitement permettant à la boîte dans son ensemble de toujours répondre aux administrateurs.
Conseils généraux pour la mise à l'échelle d'un système
Votre considération la plus importante est probablement de maximiser le débit . Un autre besoin possible de minimiser le temps de réponse, mais je n'en discuterai pas ici. En maximisant le débit, vous essayez de maximiser le système, pas les composants individuels; certaines diminutions locales pourraient améliorer le débit global du système (par exemple, effectuer un changement qui ajoute une latence au niveau Web afin d'améliorer les performances de la base de données est un gain net).
Sur les spécificités:
top
et votre outil de surveillance MySQL préféré. Vous devez pouvoir profiler. Le déplacement de la base de données vers une machine distincte introduira une latence supplémentaire (plusieurs ms) par demande, alors attendez-vous à augmenter légèrement le nombre de processus au niveau Web pour conserver le même débit.uswgi
est suffisamment grande pour gérer une rafale de trafic à l'aide de --listen
argument. Cela devrait être plusieurs fois le nombre maximal de requêtes par seconde en régime permanent que votre système peut gérer.Au niveau web/application: Équilibrez le nombre de processus avec le nombre de cœurs CPU et la latence inhérente au processus. Trop de processus ralentissent les performances, trop peu signifie que vous n'utiliserez jamais pleinement les ressources du système. Il n'y a pas de point d'équilibrage fixe, car chaque application et modèle d'utilisation est différent, alors comparez et ajustez. À titre indicatif, utilisez la latence des processus, si chaque tâche a:
Vérifiez top
pendant le test pour vous assurer que votre utilisation du processeur est supérieure à 90% (pour chaque cœur) et vous avez une moyenne de charge légèrement supérieure à 1,0. Si la moyenne de charge est plus élevée, réduisez les processus. Si tout se passe bien, à un moment donné, vous ne pourrez pas atteindre cet objectif, et DB pourrait maintenant être le goulot d'étranglement
Veuillez exécuter des tests de référence beaucoup plus longtemps qu'une minute (5-10 au moins), vous n'obtiendrez vraiment pas beaucoup d'informations d'un test aussi court. Et utilisez le plugin carbone d'uWSGI pour pousser les statistiques vers le serveur carbone/graphite (vous en aurez besoin), vous aurez beaucoup plus d'informations pour le débogage.
Lorsque vous envoyez 500 demandes simultanées à votre application et qu'elle ne peut pas gérer une telle charge, la file d'attente d'écoute sur chaque backend sera remplie assez rapidement (c'est 100 demandes par défaut), vous voudrez peut-être augmenter cela, mais si les travailleurs ne peuvent pas traiter demande que la file d'attente rapide et d'écoute (également connue sous le nom de backlog) soit pleine, la pile réseau linux abandonnera la demande et vous commencerez à obtenir des erreurs.
Votre premier test de référence indique que vous pouvez traiter une seule demande en ~ 42 ms, de sorte qu'un seul travailleur pourrait traiter au plus 1000 ms/42 ms = ~ 23 demandes par seconde (si la base de données et les autres parties de la pile d'applications ne ralentissaient pas à mesure que la concurrence augmentait) . Donc, pour traiter 500 demandes simultanées, vous auriez besoin d'au moins 500/23 = 21 travailleurs (mais en réalité, je dirais au moins 40), vous n'en avez que 16, pas étonnant que cela casse sous une telle charge.
EDIT: J'ai un taux mixte avec simultanéité - au moins 21 employés vous permettront de traiter 500 demandes par seconde, pas 500 demandes simultanées. Si vous voulez vraiment traiter 500 demandes simultanées, vous avez simplement besoin de 500 employés. Sauf si vous exécutez votre application en mode asynchrone, consultez la section "Gevent" dans les documents uWSGI.
PS. uWSGI est livré avec un excellent équilibreur de charge avec autoconfiguration backend (lire les documents sous "Subscription Server" et "FastRouter"). Vous pouvez le configurer d'une manière qui vous permet de connecter à chaud un nouveau backend selon vos besoins, vous démarrez simplement les travailleurs sur un nouveau nœud et ils s'abonnent à FastRouter et commencent à recevoir des demandes. Il s'agit de la meilleure méthode de mise à l'échelle horizontale. Et avec les backends sur AWS, vous pouvez automatiser cela afin que de nouveaux backends soient démarrés rapidement en cas de besoin.
Ajouter plus de travailleurs et obtenir moins de r/s signifie que votre demande "est purement CPU" et qu'il n'y a pas IO attend qu'un autre travailleur puisse utiliser pour servir une autre demande.
Si vous souhaitez évoluer, vous devrez utiliser un autre serveur avec plus (ou plus) de processeurs.
Cependant, il s'agit d'un test synthétique, le nombre de r/s que vous obtenez est la limite supérieure de la demande exacte que vous testez, une fois en production, il y a beaucoup plus de variables qui peuvent affecter les performances.