Pour faciliter la compréhension de ce dont parle cet article, commençons par le changement de la statistique quotidienne moyenne de Stack Overflow. Les chiffres suivants proviennent des statistiques au 12 novembre 2013 :
- L’équilibreur de charge acceptait 148 084 833 requêtes HTTP
- Parmi celles-ci, 36 095 312 étaient des pages chargées
- 833 992 982 627 octets (776 Go) de trafic HTTP sont utilisés pour envoyer
- Un total de 286 574 644 032 octets (267 Go) de données ont été reçus
- Un total de 1 125 992 557 312 octets (1 048 Go) de données ont été envoyés
- 334 572 103 requêtes SQL (y compris uniquement depuis les requêtes HTTP)
- 412 865 051 demandes Redis
- 3 603 418 demandes de moteurs de balises
- Cela a pris 558 224 585 ms (155 heures) sur les requêtes SQL
- Il a fallu 99 346 916 ms (27 heures) pour les demandes Redis
- J’ai passé 132 384 059 ms (36 heures) sur la demande de moteur d’étiquettes
- Il a fallu 2 728 177 045 ms (757 heures) pour ASP.Net traitement du processus
Les données suivantes montrent les évolutions des statistiques au 9 février 2016, afin que vous puissiez comparer :
- Requêtes HTTP reçues par l’équilibreur de charge : 209 420 973 (+61 336 090)
- 66 294 789 (+30 199 477) dont la page se charge
- Données HTTP envoyées : 1 240 266 346 053 (+406 273 363 426) octets (1,24 To)
- Volume total de données reçues : 569 449 470 023 (+282 874 825 991) octets (569 Go)
- Quantité totale de données envoyées : 3 084 303 599 266 (+1 958 311 041 954) octets (3,08 To)
- Requêtes SQL (uniquement issues des requêtes HTTP) : 504 816 843 (+170 244 740)
- Résultats du cache Redis : 5 831 683 114 (+5 418 818 063)
- Recherches élastiques : 17 158 874 (non suivies en 2013)
- Demandes de moteurs de tag : 3 661 134 (+57 716)
- Temps cumulé consommé pour exécuter des requêtes SQL : 607 073 066 (+48 848 481 481 ms) (168 heures)
- Temps de passage du cache Redis : 10 396 073 (-88 950 843) ms (2,8 heures)
- Temps consommé par les demandes de moteur de balises : 147 018 571 (+14 634 512 ms) (40,8 heures)
- Temps consommé lors du traitement ASP.Net : 1 609 944 301 (-1 118 232 744 ms) (447 heures)
- 22,71 ms (-5,29) ms 49 180 275 pages de numéros temps moyen de rendu (dont 19,12 ms consommés en ASP.Net)
- 11,80 ms (-53,2) 6 370 076 premières pages temps moyen de rendu (dont 8,81 ms consommés en ASP.Net)
Vous vous demandez peut-être pourquoi ASP.Net traite 61 millions de demandes de plus par jour tout en réduisant le temps de traitement de 757 heures (par rapport à 2013). Cela est principalement dû aux améliorations que nous avons apportées à nos serveurs début 2015, ainsi qu’à beaucoup d’optimisation des performances intégrées à l’application. N’oubliez pas : la performance reste un argument de vente. Si vous êtes plus curieux des détails matériels spécifiques, ne vous inquiétez pas, je vous donnerai les détails matériels spécifiques des serveurs utilisés pour faire tourner ces sites sous forme d’annexe dans le prochain article (je mettrai à jour ce lien quand le moment sera venu).
Alors, qu’est-ce qui a changé ces deux dernières années ? Pas grand-chose, juste remplacer quelques serveurs et équipements réseau. Voici un aperçu des serveurs utilisés pour faire fonctionner votre site web aujourd’hui (notez comment ils ont évolué depuis 2013)
- 4 serveurs Microsoft SQL Server (dont 2 utilisent du nouveau matériel)
- 11 serveurs web IIS (nouveau matériel)
- 2 serveurs Redis (nouveau matériel)
- 3 serveurs Tag Engine (dont 2 utilisent du nouveau matériel)
- 3 serveurs Elasticsearch (identiques à ci-dessus)
- 4 serveurs d’équilibrage de charge HAProxy (2 ajoutés pour supporter CloudFlare)
- 2 périphériques réseau (Nexus 5596 core + 2232TM Fabric Extender, tous les appareils mis à niveau à 10Gbps de bande passante)
- 2 x pare-feu Fortinet 800C (remplace les Cisco 5525-X ASA)
- 2 routeurs Cisco ASR-1001 (remplacent les routeurs Cisco 3945)
- 2 routeurs Cisco ASR-1001-x (NEUFS !) )
Que faut-il pour que Stack Overflow fonctionne ? Peu de choses ont changé depuis 2013, mais grâce aux optimisations et au nouveau matériel mentionné ci-dessus, nous n’avons désormais besoin que d’un seul serveur web. Nous avons déjà testé cette situation sans le vouloir, avec plusieurs succès. Notez : je viens de dire que ça marche, je n’ai pas dis-le que c’était une bonne idée. Mais à chaque fois que cela arrive, c’est assez intéressant.
Maintenant que nous avons quelques chiffres de référence sur les idées de mise à l’échelle des serveurs, voyons comment nous avons créé ces pages web intéressantes. Peu de systèmes existent complètement de manière indépendante (et les nôtres ne font pas exception), et sans une vision globale qui intègre ces parties, la signification de la planification architecturale est grandement réduite. Notre objectif est de saisir la situation globale. De nombreux articles approfondiront chaque domaine spécifique à l’avenir. Cet article est simplement un résumé de la structure logique du matériel clé, et le prochain article contiendra des détails spécifiques sur ce matériel.
Si vous voulez voir à quoi ressemble ce matériel aujourd’hui, voici quelques photos que j’ai prises de l’Armoire A (l’Armoire B est exactement identique) lors de la mise à niveau du serveur en février 2015 :
Maintenant, plongeons dans la disposition architecturale. Voici un résumé de l’architecture logique des principaux systèmes existants :
Principes de base
Voici quelques principes courants qui n’ont pas besoin d’être introduits à leur tour :
- Tout a des sauvegardes redondantes.
- Tous les serveurs et appareils réseau disposent d’au moins deux connexions à bande passante de 10 Gbps.
- Tous les serveurs disposent de deux sources d’alimentation qui fournissent l’alimentation via deux unités UPS, deux générateurs derrière eux, et deux alimentations de tension du réseau.
- Tous les serveurs disposent d’une sauvegarde redondante située dans les Racks A et B.
- Tous les serveurs et services ont des sauvegardes redondantes doubles dans un centre de données séparé (au Colorado), bien que je couvre principalement New York.
- Tout a des sauvegardes redondantes.
Internet
D’abord, il faut trouver notre site web, qui est un problème de DNS. Trouver des sites web est rapide, donc nous confions maintenant cela à CloudFlare car ils ont des serveurs DNS aux quatre coins du monde. Nous mettons à jour les enregistrements DNS via des API, et ils sont responsables de la « gestion » du DNS. Cependant, dans nos esprits de méchants, nous avons toujours nos propres serveurs DNS à cause de problèmes de confiance profondément enracinés. Quand l’apocalypse est apocalyptique – peut-être à cause de la GPL, de Punyon ou de problèmes de cache – et que les gens veulent encore programmer pour détourner leur attention, nous passons à nos propres serveurs DNS.
Une fois que votre navigateur trouve notre cachette, le trafic HTTP de nos quatre FAI (Level 3, Zayo, Cogent et Lightower à New York) entre dans l’un de nos quatre routeurs avancés. Nous utilisons le Border Gateway Protocol (BGP, un protocole très standard) pour faire du peer-to-peer le trafic des fournisseurs réseau afin de le contrôler et d’offrir la manière la plus efficace d’accéder à nos services. Les routeurs ASR-1001 et ASR-1001-X sont divisés en deux groupes, chacun devant utiliser le mode actif/actif pour gérer le trafic des deux fournisseurs réseau – ici il y a des sauvegardes redondantes. Bien qu’ils aient tous la même bande passante physique de 10 Gbps, le trafic extérieur reste indépendant du trafic du VLAN externe et est connecté séparément à l’équilibrage de charge. Après que le trafic soit passé par le routeur, vous arriverez au load balancer.
Je pense qu’il est temps de mentionner que nous avons MPLS avec une bande passante de 10 Gbps entre les deux centres de données, même si cela ne concerne pas directement les services de sites web. Nous utilisons cette technologie pour effectuer la réplication hors site et la récupération rapide des données afin de gérer certaines urgences. « Mais Nick, il n’y a aucune redondance là-dedans ! » Eh bien, d’un point de vue technique, vous avez raison (dans un sens positif), c’est vraiment un point unique d’échec à ce niveau. Mais attendez ! Via le fournisseur réseau, nous disposons également de deux routes OSPF supplémentaires (MPLS est le premier choix, et ce sont les deuxième et troisième choix pour des raisons de coût). Chacun des ensembles d’appareils mentionnés précédemment sera connecté au centre de données du Colorado en conséquence pour équilibrer la charge du trafic réseau en cas de basculement. Bien sûr, nous aurions pu connecter ces deux ensembles d’appareils entre eux, de sorte qu’il y aurait quatre ensembles de chemins, mais oublions cela, passons à autre chose.
Répartition de charge (HAProxy)
L’équilibrage de charge est implémenté avec HAProxy 1.5.15, fonctionnant sous CentOS 7 (notre version préférée de Linux). Et ajouter le protocole de transmission sécurisée TLS (SSL) sur HAProxy. Nous surveillons également HAProxy 1.7, qui assurera immédiatement le support du protocole HTTP/2.
Contrairement à d’autres serveurs avec deux connexions réseau LACP de 10 Gbps, chaque équilibreur de charge dispose de deux connexions de 10 Gbps : une pour le réseau externe et l’autre pour la DMZ. Ces serveurs disposent de 64 Go ou plus de mémoire pour gérer plus efficacement la couche protocolaire SSL. Lorsque nous pouvons mettre en cache et réutiliser plus de sessions TLS en mémoire, nous consommons moins de ressources de calcul en nous connectant au même client. Cela signifie que nous pouvons rétablir les sessions de manière plus rapide et moins coûteuse. La mémoire est tellement bon marché que c’est un choix facile.
L’équilibrage de charge lui-même est facile à configurer. Nous écoutons différents sites web sur plusieurs adresses IP différentes (principalement pour des raisons de gestion des certificats et du DNS), puis nous redirigeons le trafic vers différents backends (principalement en fonction des en-têtes d’hôte). La seule chose que nous faisons ici est de limiter le débit et de récupérer certaines informations d’en-tête (de la couche web) pour se connecter aux messages de journal système de HAProxy, ainsi nous pouvons enregistrer les métriques de performance pour chaque requête. Nous en parlerons en détail plus tard.
Couche web (IIS 8.5, ASP.Net MVC 5.2.3 et .Net 4.6.1)
L’équilibrage de charge rédistribue le trafic entre 9 de ce que nous appelons le serveur web principal (01-09) et 2 serveurs web de développement (10-11, notre environnement de test). Le serveur principal gère Stack Overflow, Careers et tous les sites Stack Exchange, tandis que meta.stackoverflow.com et meta.stackexchange.com fonctionnent sur deux autres serveurs. L’application principale de questions-réponses elle-même est multi-locataire, ce qui signifie qu’une seule application gère toutes les requêtes du site Q&R. En d’autres termes, nous pouvons faire tourner toute l’application Q&A sur un seul pool d’applications sur un seul serveur. D’autres applications comme Careers, API v2, Mobile API, etc., sont indépendantes. Voici ce que vous voyez dans IIS pour les serveurs maître et développement :
Voici la répartition de la couche web de Stack Overflow telle que vue dans Opserver (notre tableau de bord interne de surveillance) :
Et voici la consommation de ressources de ces serveurs web :
J’expliquerai plus en détail dans un prochain article sur les raisons pour lesquelles nous fournissons autant de ressources, en mettant l’accent sur la construction progressive, la marge de manœuvre et la redondance.
Couche de service (IIS, ASP.Net MVC 5.2.3, . NET 4.6.1 et HTTP. SYS)
À côté de la couche web se trouve la couche de service. Ils fonctionnent également sur IIS 2012 sous Windows 8.5R2. Cette couche exécute certains services internes qui prennent en charge la couche web et d’autres systèmes internes de l’environnement de production. Les deux principaux services sont : « Stack Server », qui exécute un moteur de balises et est basé sur http.sys (et non sur IIS) ; API Providence (basée sur IIS). Un fait intéressant : j’ai dû corréler les deux processus pour se connecter à des sockets différents, car le Stack Server accédait très fréquemment aux caches L2 et L3 lors de la mise à jour de la liste des problèmes toutes les deux minutes.
Les machines exécutant ces services sont essentielles pour le moteur de balisage et les API backend, elles doivent donc être redondantes, mais pas 9x redondantes. Par exemple, nous chargeons tous les articles et leurs tags depuis la base de données toutes les n minutes (actuellement 2 minutes), ce qui n’est pas bas. Nous ne voulons pas répéter cette opération de chargement 9 fois à la couche web, 3 fois est suffisant pour nous. Nous utilisons également différentes configurations matérielles pour ces serveurs afin d’optimiser mieux les caractéristiques de calcul et de charge des données du moteur de balises et des tâches d’index élastique (également exécutées dans cette couche). Le « moteur de tags » lui-même est un sujet relativement complexe qui sera abordé dans un article dédié. Le principe de base est que lorsque vous accédez à l’adresse /questions/tagged/java, vous consultez le moteur de tagging pour obtenir les questions qui y correspondent. Le moteur gère toutes les correspondances d’étiquettes sauf /search, donc partout, y compris la nouvelle navigation, reçoit des données via ce service.
Mise en cache & publication/abonnement (Redis)
Nous avons utilisé Redis à certains endroits, et il a une stabilité solide comme un roc. Bien qu’il y ait jusqu’à 160 milliards d’opérations par mois, le CPU par instance ne dépasse pas 2 %, ce qui est généralement inférieur :
Nous utilisons Redis pour les systèmes de cache de niveau L1/L2. Le niveau « L1 » est le cache HTTP qui fonctionne dans un serveur web ou toute application similaire. Le niveau « L2 » sert à obtenir les données via Redis après la panne du cache précédent. Nos données sont stockées au format Protobuf, implémenté via protobuf-dot-net écrit par Marc Gravel. Pour le client Redis, nous avons utilisé la bibliothèque StackExchange.Redis, qui est une bibliothèque open source développée en interne. Si un serveur web ne se connecte pas aux caches L1 et L2, il récupère les données de ses sources de données (requêtes de base de données, appels API, etc.) et sauvegarde les résultats dans le cache local et Redis. Le serveur suivant peut manquer du cache L1 lors de la récupération des mêmes données, mais il récupérera les données dans L2/Redis, éliminant ainsi le besoin de requêtes de base de données ou d’appels API.
Nous utilisons aussi beaucoup de sites Q&R, chacun avec son propre cache L1/L2 : key comme préfixe dans le cache L1 et ID de base de données dans le cache L2/Redis. Nous aborderons ce sujet dans de futurs articles.
En plus des deux principaux serveurs Redis (un maître et un esclave) exécutant toutes les instances du site, nous avons également mis en place une instance pour l’apprentissage automatique (principalement pour des raisons de mémoire) en utilisant deux autres serveurs esclaves dédiés. Ce groupe de serveurs sert à fournir des services tels que la recommandation de questions sur la page d’accueil et l’amélioration de l’appariement des emplois. Cette plateforme s’appelle Providence, et Kevin Montrose en a écrit.
Le serveur principal Redis dispose de 256 Go de RAM (environ 90 Go utilisés), et le serveur Providence dispose de 384 Go de mémoire (environ 125 Go utilisés).
Redis n’est pas seulement dédié à la mise en cache, il dispose aussi d’un mécanisme de publication et d’abonnement où un serveur peut publier un message et d’autres abonnés peuvent recevoir le message (y compris Redis depuis les clients en aval sur le serveur). Nous utilisons ce mécanisme pour vider le cache L1 sur d’autres services afin de maintenir la cohérence du cache sur le serveur web. Mais il a une autre utilité importante : les websockets.
Websockets (NetGain)
Nous utilisons des websockets pour transmettre des mises à jour en temps réel aux utilisateurs, telles que les notifications dans la barre supérieure, les votes, la nouvelle navigation, les nouvelles réponses, les commentaires, et plus encore.
Le serveur socket lui-même fonctionne sur la couche web, en utilisant des sockets natifs. Il s’agit d’une application très petite basée sur notre implémentation de bibliothèque open source : StackExchange.NetGain. Aux heures de pointe, nous avions environ 500 000 connexions websocket simultanées, ce qui fait beaucoup de navigateurs. Petite anecdote : certains de ces navigateurs sont ouverts depuis plus de 18 mois, et il faudra trouver quelqu’un pour vérifier si ces développeurs sont toujours en vie. Le graphique suivant montre le schéma de la concurrence websocket cette semaine :
Pourquoi utiliser des websockets ? À notre échelle, c’est bien plus efficace que les sondages. De cette façon, nous pouvons simplement envoyer plus de données avec moins de ressources et être plus en temps réel pour les utilisateurs. Cette approche n’est cependant pas sans problèmes : ports temporaires, manutentions de fichiers épuisées sur les équilibreurs de charge, sont des problèmes très intéressants, et nous en parlerons plus tard.
Recherche (Elasticsearch)
Spoiler : Il n’y a pas grand-chose à réveiller ici. La couche web utilise Elasticsearch 1.4 et implémente un client ultra-léger et haute performance StackExchange.Elastic. Contrairement à la plupart des choses, nous ne prévoyons pas de rendre cette partie open source, simplement parce qu’elle expose un très petit sous-ensemble des API dont nous avons besoin. Je suis sûr que rendre la carte publique l’emporte sur la perte et ne fera que semer la confusion pour les développeurs. Nous utilisons elastic :/search dans ces endroits pour calculer les questions connexes et donner des suggestions lors des questions.
Chaque cluster Elastic (un pour chaque centre de données) contient 3 nœuds, chacun avec son propre indice. Le site Carrières propose également quelques index supplémentaires. Un aspect un peu moins standard de notre configuration en cercles élastiques est que notre cluster de 3 serveurs est un peu plus puissant que la configuration habituelle : chaque serveur utilise un stockage SSD, 192 Go de mémoire, un double réseau de 10 Gbps de bande passante.
Le même domaine d’application que Stack Server (oui, .Net Core nous a fait circuler ici) héberge aussi un moteur de tags, qui utilise aussi Elasticsearch pour l’indexation continue. Ici, nous utilisons une petite astuce, comme utiliser ROWVERSION dans SQL Server (la source de données) pour comparer avec le document « dernier emplacement » dans Elastic. Comme il est apparemment séquentiel, il nous est facile d’explorer et d’indexer le contenu s’il est modifié après la dernière visite.
La principale raison pour laquelle nous utilisons Elasticsearch plutôt que des technologies comme la recherche SQL en texte intégral est sa scalabilité et sa rentabilité. SQL est relativement cher sur les processeurs, tandis qu’Elastic est beaucoup moins cher et propose beaucoup de nouvelles fonctionnalités récemment. Pourquoi ne pas utiliser Solr ? Nous devons chercher à travers le réseau (avec plusieurs index en même temps), et Solr ne prend pas en charge ce scénario au moment de nos décisions. La raison pour laquelle nous n’avons pas encore utilisé la 2.x, c’est que les types ont beaucoup changé dans la 2.x, ce qui signifie que nous devons tout réindexer si nous voulons mettre à jour. Je n’ai tout simplement pas assez de temps pour planifier les changements d’exigences et les migrations.
Base de données (SQL Server)
Nous utilisons SQL Server comme source unique de vérité. Toutes les données dans Elastic et Redis proviennent de SQL Server. Nous avons deux clusters SQL Server et sommes configurés avec des groupes de disponibilité AlwaysOn. Chaque cluster dispose d’un serveur principal à New York (qui prend presque toute la charge) et d’un serveur réplique, en plus d’un serveur réplique au Colorado (notre centre de données de reprise après sinistre). Toutes les opérations de copie sont asynchrones.
Le premier cluster est un ensemble de serveurs Dell R720xd, chacun avec 384 Go de mémoire, un SSD PCIe de 4 To, et deux processeurs 12 cœurs. Cela inclut Stack Overflow, Sites (c’est un mauvais nom, je vous expliquerai plus tard), PRIZM, et la base de données de Mobile.
Le second cluster est un ensemble de serveurs Dell R730xd, chacun avec 768 Go de mémoire, un SSD PCIe de 6 To d’espace, et deux processeurs 8 cœurs. Ce cluster contient toutes les autres bases de données, y compris Carrières, Open ID, Chat, journaux d’exceptions et autres sites de questions-réponses (par exemple, Super User, Server Fault, etc.).
Au niveau de la base de données, nous voulons maintenir une utilisation du CPU à un niveau très bas, même si en pratique l’utilisation du CPU sera légèrement plus élevée lorsque certains problèmes de cache prévus surviennent (ce que nous sommes en train de dépanner). Actuellement, NY-SQL02 et 04 sont les serveurs principaux et 01 et 03 sont les serveurs répliques, et nous les avons redémarrés aujourd’hui à cause de la mise à jour du SSD. Voici comment ils se sont comportés ces dernières 24 heures :
Notre utilisation du SQL est très simple. Simple signifie rapide. Bien que certaines requêtes puissent être perversées, notre interaction avec SQL elle-même se fait de manière assez native. Nous avons un peu de Linq2SQL hérité, mais tous nos nouveaux développements utilisent Dapper, notre framework micro-ORM open source qui utilise POCO. Laissez-moi expliquer autrement : Stack Overflow n’a qu’une seule procédure stockée dans sa base de données, et je vais supprimer cette dernière procédure stockée restante pour la remplacer par du code.
Bibliothèque
Eh bien, changeons d’avis, voici des choses qui peuvent vous aider plus directement. J’en ai déjà parlé certaines, mais je vais vous donner une liste des nombreuses bibliothèques .Net open source que nous maintenons et que tout le monde utilise. Nous les rendons open source car ils n’impliquent pas de valeur métier centrale, mais ils peuvent aider les développeurs du monde entier. J’espère que vous pourrez les utiliser maintenant :
- Dapper (.Net Core) – Un cadre micro-ORM haute performance pour ADO.Net
- StackExchange.Redis – Un client Redis haute performance
- MiniProfiler – un profileur léger que nous utilisons sur chaque page (prend également en charge Ruby, Go et Node)
- Exceptionnel – Pour la journalisation des erreurs en SQL, JSON, MySQL, etc
- Jil – Sérialisation et désérialiseur JSON haute performance
- Sigil – .Net CIL Generation Helper (utilisé lorsque C# n’est pas assez rapide)
- NetGain – Serveur websocket haute performance
- Opserver – Tableau de bord de surveillance qui interroge directement la plupart des systèmes et peut récupérer des informations auprès d’Orion, Bosun ou WMI
- Bosun – Système de surveillance en arrière-plan, écrit en Go
|