Entity Framework 4 est-il une bonne solution pour un site Web public avec potentiellement 1000 visites/seconde?
À ma connaissance, EF est une solution viable pour la plupart des sites Web plus petits ou intranet, mais ne serait pas facilement évolutif pour quelque chose comme un site Web communautaire populaire (je sais SO utilise LINQ to SQL, mais .. Je voudrais plus d'exemples/preuves ...)
Maintenant, je suis au carrefour de choisir soit une approche ADO.NET pure soit EF4. Pensez-vous que l'amélioration de la productivité des développeurs avec EF vaut la perte de performances et l'accès granulaire d'ADO.NET (avec les procédures stockées)? Des problèmes graves auxquels un site Web à fort trafic pourrait être confronté, utilisaient-ils EF?
Merci d'avance.
Cela dépend un peu de la quantité d'abstraction dont vous avez besoin. Tout est un compromis; par exemple, EF et NHibernate introduisent une grande flexibilité pour représenter les données dans des modèles intéressants et exotiques - mais en conséquence ils ajoutent des frais généraux. Frais généraux notables.
Si vous n'avez pas besoin pour pouvoir basculer entre les fournisseurs de base de données et les différentes dispositions de table par client, et si vos données sont principalement lues , et si vous n'avez pas besoin d'utiliser le même modèle dans EF, SSRS, ADO.NET Data Services, etc. - alors si vous voulez des performances absolues comme mesure clé, vous pourriez faire loin pire que de regarder pimpant . Dans nos tests basés à la fois sur LINQ-to-SQL et EF, nous constatons que EF est significativement plus lent en termes de performances de lecture brutes, probablement en raison de la couches d'abstraction (entre modèle de stockage, etc.) et matérialisation.
Chez SO, nous sommes obsédés par les performances brutes et nous sommes heureux de prendre le coup de développement de perdre une certaine abstraction afin de gagner en vitesse. En tant que tel, notre principal outil pour interroger la base de données est dapper . Cela nous permet même d'utiliser notre modèle LINQ-to-SQL préexistant, mais simplement: c'est beaucoup plus rapide. Dans les tests de performances, il s'agit essentiellement des mêmes performances que l'écriture manuelle de tout le code ADO.NET (paramètres, lecteurs de données, etc.), mais sans risque de se tromper de nom de colonne. Il est cependant basé sur SQL (bien qu'il soit heureux d'utiliser des SPROC si c'est le poison que vous avez choisi). L'avantage de ceci est qu'il n'y a aucun traitement supplémentaire impliqué, mais il est un système pour les personnes qui aiment SQL . Ce que je considère: pas une mauvaise chose!
Une requête typique, par exemple, pourrait être:
int customerId = ...
var orders = connection.Query<Order>(
"select * from Orders where CustomerId = @customerId ",
new { customerId }).ToList();
ce qui est pratique, sûr pour les injections, etc. - mais sans tonnes de goo lecteur de données. Notez que bien qu'il puisse gérer des partitions horizontales et verticales pour charger des structures complexes, il ne prendra pas en charge le chargement paresseux (mais: nous sommes de grands fans du chargement très explicite - moins de surprises).
Notez dans cette réponse que je ne dis pas que EF n'est pas approprié pour un travail à volume élevé; simplement: je sais que dapper est à la hauteur.
La question "quel ORM dois-je utiliser" vise vraiment la pointe d'un énorme iceberg en ce qui concerne la stratégie globale d'accès aux données et l'optimisation des performances dans une application à grande échelle.
Toutes les choses suivantes ( à peu près par ordre d'importance) vont affecter le débit, et toutes sont gérées (parfois de différentes manières) par la plupart des principaux frameworks ORM:
Conception et maintenance de la base de données
Il s'agit, dans une large mesure, du déterminant le plus important du débit d'une application ou d'un site Web piloté par les données, et souvent totalement ignoré par les programmeurs.
Si vous n'utilisez pas de techniques de normalisation appropriées, votre site est condamné. Si vous n'avez pas de clés primaires, presque toutes les requêtes seront lentes. Si vous utilisez des anti-modèles bien connus tels que l'utilisation de tables pour les paires valeur-clé (AKA Entity-Attribute-Value) sans raison valable, vous exploserez le nombre de lectures et d'écritures physiques.
Si vous ne profitez pas des fonctionnalités de la base de données, telles que la compression de page, FILESTREAM
stockage (pour les données binaires), SPARSE
colonnes, hierarchyid
pour les hiérarchies, et ainsi de suite (tous les exemples SQL Server), alors vous ne verrez nulle part les performances que vous pourriez voir.
Vous devriez commencer à vous soucier de votre stratégie d'accès aux données - après vous avez conçu votre base de données et vous êtes convaincu qu'elle est aussi bonne que possible, du moins pour le moment.
Désireux contre chargement paresseux
La plupart des ORM ont utilisé une technique appelée chargement paresseux pour les relations, ce qui signifie que par défaut, il chargera une entité (ligne de table) à la fois et effectuera un aller-retour dans la base de données à chaque fois qu'il doit charger une ou plusieurs lignes liées (clé étrangère).
Ce n'est pas une bonne ou une mauvaise chose, cela dépend plutôt de ce qui va réellement être fait avec les données et de ce que vous en savez d'avance. Parfois, le chargement paresseux est absolument la bonne chose à faire. NHibernate, par exemple, peut décider de ne rien demander du tout et de simplement générer un proxy pour un ID particulier. Si vous n'avez besoin que de l'ID elle-même, pourquoi devrait-elle en demander plus? D'un autre côté, si vous essayez d'imprimer une arborescence de chaque élément d'une hiérarchie à 3 niveaux, le chargement différé devient une opération O (N²), ce qui est extrêmement mauvais pour les performances .
Un avantage intéressant de l'utilisation du "SQL pur" (c'est-à-dire des requêtes/procédures stockées ADO.NET brutes) est qu'il vous oblige à réfléchir exactement aux données nécessaires pour afficher un écran ou une page donnée. Les ORM et les fonctionnalités de chargement différé ne vous empêchent pas de le faire, mais ils le font vous donnent la possibilité d'être ... eh bien, lazy, et exploser accidentellement le nombre de requêtes que vous exécutez. Vous devez donc comprendre les fonctionnalités de chargement dynamique de vos ORM et être toujours vigilant quant au nombre de requêtes que vous envoyez au serveur pour une demande de page donnée.
Mise en cache
Tous les principaux ORM conservent un cache de premier niveau, AKA "Identity Cache", ce qui signifie que si vous demandez deux fois la même entité par son ID, cela ne nécessite pas un deuxième aller-retour, et aussi (si vous avez correctement conçu votre base de données ) vous donne la possibilité d'utiliser une concurrence optimiste.
Le cache L1 est assez opaque dans L2S et EF, vous devez en quelque sorte avoir confiance que cela fonctionne. NHibernate est plus explicite à ce sujet (Get
/Load
vs Query
/QueryOver
). Pourtant, tant que vous essayez d'interroger autant que possible par ID, vous devriez être bien ici. Beaucoup de gens oublient le cache L1 et recherchent à plusieurs reprises la même entité encore et encore par autre chose que son ID (c'est-à-dire un champ de recherche). Si vous devez le faire, vous devez enregistrer l'ID ou même l'entité entière pour les recherches futures.
Il existe également un cache de niveau 2 ("cache de requête"). NHibernate a ce intégré. Linq to SQL et Entity Framework ont requêtes compilées , ce qui peut aider à réduire considérablement les charges du serveur d'applications en compilant l'expression de la requête elle-même, mais elle ne met pas les données en cache. Microsoft semble considérer cela comme un problème d'application plutôt qu'un problème d'accès aux données, et c'est un point faible majeur de L2S et EF. Inutile de dire que c'est aussi un point faible du SQL "brut". Afin d'obtenir de très bonnes performances avec pratiquement n'importe quel ORM autre que NHibernate, vous devez implémenter votre propre façade de mise en cache.
Il existe également une "extension" de cache L2 pour EF4 qui est d'accord, mais pas vraiment un remplacement de gros pour un cache au niveau de l'application.
Nombre de requêtes
Les bases de données relationnelles sont basées sur sets de données. Ils sont vraiment bons pour produire grand quantités de données en peu de temps, mais ils sont loin d'être aussi bons en termes de requête - latence parce il y a une certaine quantité de frais généraux impliqués dans chaque commande. Une application bien conçue doit tirer parti des atouts de ce SGBD et essayer de minimiser le nombre de requêtes et maximiser la quantité de données dans chacune.
Maintenant, je ne dis pas d'interroger la base de données entière lorsque vous n'avez besoin que d'une seule ligne. Ce que je dis, c'est que si vous avez besoin des lignes Customer
, Address
, Phone
, CreditCard
et Order
toutes à la en même temps afin de servir une seule page, alors vous devriez demander pour tous en même temps, ne pas exécuter chaque requête séparément. Parfois, c'est pire que cela, vous verrez du code qui interroge le même Customer
enregistrement 5 fois de suite, d'abord pour obtenir le Id
, puis le Name
, puis le EmailAddress
, alors ... c'est ridiculement inefficace.
Même si vous devez exécuter plusieurs requêtes qui fonctionnent toutes sur des ensembles de données complètement différents, il est généralement plus efficace de tout envoyer à la base de données sous la forme d'un "script" unique et de lui faire renvoyer plusieurs ensembles de résultats. C'est la surcharge qui vous préoccupe, pas la quantité totale de données.
Cela peut sembler du bon sens, mais il est souvent très facile de perdre la trace de toutes les requêtes en cours d'exécution dans différentes parties de l'application; votre fournisseur d'adhésion interroge les tables d'utilisateurs/de rôles, votre action d'en-tête interroge le panier, votre action de menu interroge le tableau du plan du site, votre action de barre latérale interroge la liste des produits présentés, puis votre page est peut-être divisée en quelques zones autonomes distinctes qui interrogez séparément les tableaux Historique des commandes, Récemment consultés, Catégorie et Inventaire, et avant de le savoir, vous exécutez 20 requêtes avant même de commencer à diffuser la page. Cela détruit complètement les performances.
Certains frameworks - et je pense principalement à NHibernate ici - sont incroyablement intelligents à ce sujet et vous permettent d'utiliser quelque chose appelé futures qui regroupe des requêtes entières et essaie de les exécuter toutes en même temps, au dernier minute possible. AFAIK, vous êtes seul si vous voulez le faire avec l'une des technologies Microsoft; vous devez l'intégrer dans votre logique d'application.
Indexation, prédicats et projections
Au moins 50% des développeurs à qui je parle et même certains administrateurs de base de données semblent avoir des problèmes avec le concept de couverture des index. Ils pensent, "eh bien, le Customer.Name
la colonne est indexée, donc chaque recherche que je fais sur le nom doit être rapide. "Sauf que cela ne fonctionne pas de cette façon à moins que Name
index couvre la colonne spécifique que vous Dans SQL Server, cela se fait avec INCLUDE
dans le CREATE INDEX
déclaration.
Si vous utilisez naïvement SELECT *
partout - et c'est plus ou moins ce que chaque ORM fera sauf si vous spécifiez explicitement le contraire en utilisant une projection - alors le SGBD peut très bien choisir d'ignorer complètement vos index car ils contiennent des colonnes non couvertes. Une projection signifie que, par exemple, au lieu de faire ceci:
from c in db.Customers where c.Name == "John Doe" select c
Vous faites cela à la place:
from c in db.Customers where c.Name == "John Doe"
select new { c.Id, c.Name }
Et cela, pour la plupart des ORM modernes, lui demandera d'aller uniquement interroger les colonnes Id
et Name
qui sont vraisemblablement couvertes par l'index (mais pas les Email
, LastActivityDate
, ou toutes les autres colonnes que vous avez pu y coller).
Il est également très facile de supprimer complètement les avantages de l'indexation en utilisant des prédicats inappropriés. Par exemple:
from c in db.Customers where c.Name.Contains("Doe")
... semble presque identique à notre requête précédente, mais en fait, il en résultera une analyse complète de la table ou de l'index car elle se traduit par LIKE '%Doe%'
. De même, une autre requête qui semble étrangement simple est:
from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
En supposant que vous avez un index sur BirthDate
, ce prédicat a de bonnes chances de le rendre complètement inutile. Notre hypothétique programmeur ici a évidemment tenté de créer une sorte de requête dynamique ("ne filtrer la date de naissance que si ce paramètre a été spécifié"), mais ce n'est pas la bonne façon de le faire. Écrit comme ceci à la place:
from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
... maintenant le moteur DB sait comment paramétrer cela et faire une recherche d'index. Une modification mineure, apparemment insignifiante, de l'expression de requête peut affecter considérablement les performances.
Malheureusement, LINQ en général rend trop facile l'écriture de mauvaises requêtes comme ceci parce que parfois les fournisseurs sont capables de deviner ce que vous essayez de faire et d'optimiser la requête, et parfois ils ne le sont pas. Donc vous vous retrouvez avec frustrant incohérent résultats qui auraient été aveuglément évidents (pour un DBA expérimenté, de toute façon) si vous veniez d'écrire du SQL ancien.
Fondamentalement, tout se résume au fait que vous devez vraiment surveiller à la fois le SQL généré et les plans d'exécution auxquels ils mènent, et si vous n'obtenez pas les résultats escomptés, n'ayez pas peur de contourner le Couche ORM de temps en temps et code manuel SQL. Cela vaut pour any ORM, pas seulement EF.
Transactions et verrouillage
Avez-vous besoin d'afficher des données actuelles jusqu'à la milliseconde? Peut-être - cela dépend - mais probablement pas. Malheureusement, Entity Framework ne vous donne pas nolock
, vous ne pouvez utiliser que READ UNCOMMITTED
au niveau transaction (pas au niveau de la table). En fait, aucun des ORM n'est particulièrement fiable à ce sujet; si vous voulez faire des lectures sales, vous devez descendre au niveau SQL et écrire des requêtes ad hoc ou des procédures stockées. Donc, ce qui se résume, encore une fois, à la facilité avec laquelle vous pouvez le faire dans le cadre.
Entity Framework a parcouru un long chemin à cet égard - la version 1 d'EF (dans .NET 3.5) était horrible, il était incroyablement difficile de percer l'abstraction des "entités", mais maintenant vous avez ExecuteStoreQuery = et Traduire , donc ce n'est vraiment pas trop mal. Faites-vous des amis avec ces gars parce que vous les utiliserez beaucoup.
Il y a aussi le problème du verrouillage d'écriture et des interblocages et la pratique générale de maintenir les verrous dans la base de données le moins de temps possible. À cet égard, la plupart des ORM (y compris Entity Framework) ont tendance à être mieux que le SQL brut car ils encapsulent le modèle nité de travail , qui dans EF est - SaveChanges . En d'autres termes, vous pouvez "insérer" ou "mettre à jour" ou "supprimer" des entités au contenu de votre cœur, quand vous le souhaitez, en sachant qu'aucune modification ne sera réellement poussée dans la base de données jusqu'à ce que vous validiez l'unité de travail.
Notez qu'un UOW est pas analogue à une transaction de longue durée. L'UOW utilise toujours les fonctionnalités de concurrence optimiste de l'ORM et suit toutes les modifications en mémoire. Aucune instruction DML n'est émise avant la validation finale. Cela maintient les temps de transaction aussi bas que possible. Si vous créez votre application à l'aide de SQL brut, il est assez difficile d'obtenir ce comportement différé.
Ce que cela signifie spécifiquement pour EF: rendez vos unités de travail aussi grossières que possible et ne les engagez pas tant que vous n'en avez absolument pas besoin. Faites cela et vous vous retrouverez avec un conflit de verrouillage beaucoup plus faible que vous n'utiliseriez des commandes ADO.NET individuelles à des moments aléatoires.
EF est parfaitement adapté aux applications à trafic élevé/hautes performances, tout comme tous les autres frameworks conviennent aux applications à trafic élevé/hautes performances. Ce qui compte, c'est la façon dont vous l'utilisez. Voici une comparaison rapide des frameworks les plus populaires et des fonctionnalités qu'ils offrent en termes de performances (légende: N = non pris en charge, P = partiel, Y = oui/pris en charge):
| L2S | EF1 | EF4 | NH3 | ADO
+-----+-----+-----+-----+-----
Lazy Loading (entities) | N | N | N | Y | N
Lazy Loading (relationships) | Y | Y | Y | Y | N
Eager Loading (global) | N | N | N | Y | N
Eager Loading (per-session) | Y | N | N | Y | N
Eager Loading (per-query) | N | Y | Y | Y | Y
Level 1 (Identity) Cache | Y | Y | Y | Y | N
Level 2 (Query) Cache | N | N | P | Y | N
Compiled Queries | Y | P | Y | N | N/A
Multi-Queries | N | N | N | Y | Y
Multiple Result Sets | Y | N | P | Y | Y
Futures | N | N | N | Y | N
Explicit Locking (per-table) | N | N | N | P | Y
Transaction Isolation Level | Y | Y | Y | Y | Y
Ad-Hoc Queries | Y | P | Y | Y | Y
Stored Procedures | Y | P | Y | Y | Y
Unit of Work | Y | Y | Y | Y | N
Comme vous pouvez le voir, EF4 (la version actuelle) ne s'en sort pas trop mal, mais ce n'est probablement pas le meilleur si les performances sont votre principale préoccupation. NHibernate est beaucoup plus mature dans ce domaine et même Linq to SQL fournit des fonctionnalités d'amélioration des performances que EF ne propose toujours pas. ADO.NET brut va souvent être plus rapide pour des scénarios d'accès aux données très spécifiques, mais, lorsque vous assemblez tous les éléments, il n'offre vraiment pas beaucoup d'avantages importants que vous obtenir des différents cadres.
Et, juste pour être sûr que je sonne comme un record cassé, rien de tout cela n'a la moindre importance si vous ne concevez pas correctement votre base de données, votre application et vos stratégies d'accès aux données. Tous les éléments dans le tableau ci-dessus sont pour amélioration performances au-delà de la ligne de base; la plupart du temps, la ligne de base elle-même est celle qui a le plus besoin d'amélioration.
Edit: Sur la base d'une excellente réponse @Aaronaught, j'ajoute quelques points de ciblage des performances avec EF. Ces nouveaux points sont préfixés par Edit.
La plus grande amélioration des performances dans les sites Web à fort trafic est obtenue par la mise en cache (= tout d'abord en évitant tout traitement de serveur Web ou interrogation de base de données) suivie d'un traitement asynchrone pour éviter le blocage des threads pendant les requêtes de base de données.
Il n'y a pas de réponse à toute épreuve à votre question car elle dépend toujours des exigences d'application et de la complexité des requêtes. La vérité est que la productivité des développeurs avec EF cache une complexité derrière laquelle, dans de nombreux cas, conduit à une mauvaise utilisation de EF et à des performances terribles. L'idée que vous pouvez exposer une interface abstraite de haut niveau pour l'accès aux données et qu'elle fonctionnera en douceur dans tous les cas ne fonctionne pas. Même avec ORM, vous devez savoir ce qui se passe derrière l'abstraction et comment l'utiliser correctement.
Si vous n'avez pas d'expérience avec EF, vous rencontrerez de nombreux défis en matière de performances. Vous pouvez faire beaucoup plus d'erreurs lorsque vous travaillez avec EF par rapport à ADO.NET. De plus, il y a beaucoup de traitement supplémentaire effectué dans EF, donc EF sera toujours beaucoup plus lent que ADO.NET natif - c'est quelque chose que vous pouvez mesurer par une simple application de preuve de concept.
Si vous souhaitez obtenir les meilleures performances d'EF, vous devrez très probablement:
MergeOption.NoTracking
SqlCommand
contenant plusieurs insertions, mises à jour ou suppressions, mais avec EF, chacune de ces commandes sera exécutée dans un aller-retour distinct vers la base de données.GetByKey
dans l'API ObjectContext ou Find
dans l'API DbContext) pour interroger le cache en premier. Si vous utilisez Linq-to-entity ou ESQL, cela créera un aller-retour vers la base de données et après cela, il retournera l'instance existante du cache.Je ne sais pas si SO utilise toujours L2S. Ils ont développé un nouvel ORM open source appelé Dapper et je pense que le principal point derrière ce développement était l'augmentation des performances.