web-dev-qa-db-ja.com

Entity Frameworkは高トラフィックのWebサイトに適していますか?

Entity Framework 4は、1秒あたり1000ヒットの可能性がある公開Webサイトの優れたソリューションですか?

私の理解では、EFはほとんどの小規模またはイントラネットのWebサイトに実行可能なソリューションですが、人気のあるコミュニティWebサイトのようなものには簡単に拡張できません(SOはLINQ to SQLを使用していますが、もっと例/証明をお願いします...)

現在、純粋なADO.NETアプローチまたはEF4を選択するという岐路に立っています。 EFによる開発者の生産性の向上は、(ストアドプロシージャを使用した)ADO.NETのパフォーマンスの低下と詳細なアクセスに見合う価値があると思いますか?トラフィックの多いWebサイトが直面する可能性のある深刻な問題は、EFを使用していましたか?

前もって感謝します。

177
niaher

それはあなたがどれほど抽象化するかが必要なに少し依存します。すべてが妥協です。たとえば、EFとNHibernateは、興味深いエキゾチックなモデルでデータを表現するための優れた柔軟性をもたらしますが、結果としてdoオーバーヘッドが追加されます。顕著なオーバーヘッド。

データベースプロバイダーとクライアントごとの異なるテーブルレイアウトを切り替えることができない必要がない場合、およびデータが主にreadの場合、そして、EF、SSRS、ADO.NET Data Servicesなどで同じモデルを使用する必要がない場合-重要な指標として絶対的なパフォーマンスが必要な場合は、見るよりもはるかに悪い dapper 。 LINQ-to-SQLとEFの両方に基づくテストでは、EFは大幅におそらく抽象化レイヤー(ストレージモデルなど)と実体化のために、未加工の読み取りパフォーマンスの点で遅いことがわかります。

ここSOでは、生のパフォーマンスについて強迫的であり、スピードを上げるためにいくつかの抽象化を失うという開発上の打撃を受けて満足しています。そのため、データベースをクエリするための主なツールは dapper です。これにより、既存のLINQ-to-SQLモデルを使用することもできますが、簡単に言えば、ヒープが高速になります。パフォーマンステストでは、基本的に、すべてのADO.NETコード(パラメーター、データリーダーなど)を手動で書き込むのとまったく同じパフォーマンスですが、列名が誤って取得されるリスクはありません。ただし、SQLベースです(選択した毒の場合は、SPROCを使用してもかまいません)。これの利点はno追加の処理が必要になることですが、SQLが好きな人のためのシステムです。私が考えるのは悪いことではありません!

たとえば、一般的なクエリは次のようになります。

int customerId = ...
var orders = connection.Query<Order>(
    "select * from Orders where CustomerId = @customerId ",
    new { customerId }).ToList();

これは便利で注入安全などですが、大量のデータリーダーのgooはありません。水平および垂直の両方のパーティションを処理して複雑な構造をロードすることはできますが、遅延ロードはサポートされないことに注意してください(ただし、非常に明示的なロードの大ファンです-サプライズが少ない)。

この回答では、EF is n'tが大量の作業に適しているとは言っていません。単に:I knowそのdapperはそれまでです。

152
Marc Gravell

「どのORMを使用すべきか」という質問は、大規模なアプリケーションでの全体的なデータアクセス戦略とパフォーマンスの最適化に関して、巨大な氷山の一角を狙っています。

以下のすべて(おおまかには重要度の高い順に)がスループットに影響を与え、それらすべては(ほとんどの場合、さまざまな方法で)主要なORMフレームワークによって処理されます。

  1. データベースの設計とメンテナンス

    これは、大まかに言って、データ駆動型アプリケーションまたはWebサイトのスループットの最も重要な決定要因の1つであり、プログラマーによって完全に無視されることがよくあります。

    適切な正規化手法を使用しないと、サイトが破壊されます。主キーがない場合、ほとんどすべてのクエリは非常に遅くなります。正当な理由なく、キーと値のペアのテーブル(別名Entity-Attribute-Value)を使用するなど、よく知られたアンチパターンを使用すると、物理的な読み取りと書き込みの数が急激に増加します。

    ページ圧縮、FILESTREAMストレージ(バイナリデータ用)、SPARSE列、階層用のhierarchyidなど、データベースが提供する機能を利用しない場合は、など(すべてのSQL Serverの例)、couldが見ているパフォーマンスの近くには何も表示されません。

    データアクセス戦略について心配する必要がありますafterデータベースを設計し、少なくとも当面は、それが可能な限り優れていると確信しました。

  2. Eager対Lazy Loading

    ほとんどのORMは、関係に対してlazy loadingと呼ばれる手法を使用しました。つまり、デフォルトで一度に1つのエンティティ(テーブル行)をロードし、そのたびにデータベースへの往復を行います1つまたは複数の関連する(外部キー)行をロードする必要があります。

    これは良いことでも悪いことでもありません。むしろ、実際にデータで何が行われるか、および事前にどれだけ知っているかによって異なります。ときどき遅延読み込みが絶対に正しいことです。たとえば、NHibernateは、何も照会しないようにを決定する場合がありますと特定のIDのproxyを生成するだけです。必要なのがID自体だけである場合、なぜそれ以上必要なのですか?一方、3レベルの階層にあるすべての要素のツリーを印刷しようとすると、遅延読み込みはO(N²)オペレーションになり、extremelyパフォーマンスが低下します。 。

    「純粋なSQL」(つまり、生のADO.NETクエリ/ストアドプロシージャ)を使用することの興味深い利点の1つは、基本的に、特定の画面またはページを表示するために必要なデータを正確に考える必要があることです。 ORMと遅延読み込み機能はpreventこれを実行しませんが、それらはdoになる機会を与えます...まあ、lazy、そして実行するクエリの数を誤って爆発させます。したがって、ORMのイーガーロード機能を理解し、特定のページリクエストに対してサーバーに送信するクエリの数に常に注意する必要があります。

  3. キャッシング

    すべての主要なORMは、1次キャッシュ(別名「IDキャッシュ」)を維持します。つまり、同じエンティティをIDで2回要求した場合、2回目のラウンドトリップは必要ありません(データベースを正しく設計した場合) )は、楽観的並行性を使用する機能を提供します。

    L1SキャッシュはL2SとEFではかなり不透明であり、機能していることを信頼する必要があります。 NHibernateはそれについてより明確です(Get/LoadQuery/QueryOver)。それでも、できるだけIDでクエリを実行する限り、ここでは問題ありません。多くの人がL1キャッシュを忘れて、ID以外のもの(つまり、ルックアップフィールド)で同じエンティティを繰り返し何度も検索しています。これを行う必要がある場合は、今後の検索のためにIDまたはエンティティ全体を保存する必要があります。

    レベル2キャッシュ(「クエリキャッシュ」)もあります。 NHibernateにはこの機能が組み込まれています。 Linq to SQLとEntity Frameworkには コンパイル済みクエリ があり、クエリ式自体をコンパイルすることでアプリサーバーの負荷を大幅に削減できますが、データはキャッシュされません。 Microsoftはこれをデータアクセスの問題ではなくアプリケーションの問題と見なしているようで、これはL2SとEFの両方の主な弱点です。言うまでもなく、これは「生の」SQLの弱点でもあります。基本的にNHibernate以外のORMで本当に優れたパフォーマンスを得るには、独自のキャッシュファサードを実装する必要があります。

    okayであるEF4のL2キャッシュ「拡張」もありますが、実際にはアプリケーションレベルのキャッシュの大規模な置き換えではありません。

  4. クエリの数

    リレーショナルデータベースは、データのsetsに基づいています。短時間でlargeの量のデータを生成することは非常に得意ですが、クエリの観点ではlatencyほど優れていません。すべてのコマンドにはある程度のオーバーヘッドが伴います。適切に設計されたアプリは、このDBMSの強みを発揮し、クエリの数を最小限に抑え、それぞれのデータ量を最大化する必要があります。

    行が1つだけ必要なときに、データベース全体をクエリするように言っているわけではありません。私が言っているのは、CustomerAddressPhoneCreditCardOrderのすべての行が同時に1つのページを提供するために、それらすべてを同時にaskする必要があります。各クエリを個別に実行しないでください。それよりも悪い場合もあります。同じCustomerレコードを5回続けてクエリし、最初にId、次にName、次にEmailAddress、それから...それは途方もなく非効率的です。

    完全に異なるデータのセットを操作する複数のクエリを実行する必要がある場合でも、通常はすべてを単一の「スクリプト」としてデータベースに送信し、複数の結果セットを返すようにする方がより効率的です。データの総量ではなく、心配するオーバーヘッドです。

    これは常識のように聞こえるかもしれませんが、多くの場合、アプリケーションのさまざまな部分で実行されているすべてのクエリを見失うことは非常に簡単です。メンバーシッププロバイダーはユーザー/ロールテーブルをクエリし、ヘッダーアクションはショッピングカートをクエリし、メニューアクションはサイトマップテーブルをクエリし、サイドバーアクションは注目製品リストをクエリし、ページはいくつかの独立した自律領域に分割されます。 Order History、Recently Viewed、Category、Inventoryの各テーブルを個別にクエリします。それを知る前に、ページの提供を開始する前に20のクエリを実行しています。パフォーマンスを完全に破壊するだけです。

    一部のフレームワーク(ここでは主にNHibernateについて考えています)はこれについて非常に賢く、 futures と呼ばれるものを使用して、クエリ全体をバッチ処理し、可能な限り最後にすべてを一度に実行しようとすることができます。私の知る限り、Microsoftテクノロジのいずれかでこれを実行したい場合は、ご自分で行ってください。アプリケーションロジックに組み込む必要があります。

  5. 索引付け、述語、および予測

    私が話している開発者の少なくとも50%と、一部のDBAでさえ、インデックスをカバーするという概念に問題があるようです。彼らは、「まあ、Customer.Name列にはインデックスが付けられているので、私が名前に対して行うすべての検索は高速でなければならない」と考えています。 Nameインデックスcovers探している特定の列でない限り、この方法では機能しません。 SQL Serverでは、これはCREATE INDEXステートメントのINCLUDEで行われます。

    すべての場所でSELECT *を単純に使用している場合-そして、プロジェクションを使用して明示的に指定しない限り、すべてのORMが行うことは多かれ少なかれ-カバーされていない列が含まれているため、DBMSはインデックスを完全に無視することを選択する可能性があります。 。射影とは、たとえば、これを行う代わりに、

    from c in db.Customers where c.Name == "John Doe" select c
    

    代わりにこれを行います:

    from c in db.Customers where c.Name == "John Doe"
    select new { c.Id, c.Name }
    

    そして、これは、ほとんどの最新のORMに対して、おそらくインデックスでカバーされているIdおよびName列のみに移動してクエリするように指示します(ただし、EmailLastActivityDate、またはたまたまそこに留まっている他の列)。

    また、不適切な述語を使用することで、インデックス作成のメリットを完全に打ち消すことも非常に簡単です。例えば:

    from c in db.Customers where c.Name.Contains("Doe")
    

    ...前のクエリとほとんど同じに見えますが、実際にはLIKE '%Doe%'に変換されるため、テーブル全体またはインデックススキャンになります。同様に、疑わしいほど単純に見える別のクエリは次のとおりです。

    from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
    

    BirthDateにインデックスがあると仮定すると、この述語は完全に役に立たない可能性があります。ここで私たちの仮想プログラマーは、一種の動的クエリを作成しようとしました(「そのパラメーターが指定されている場合は、誕生日のみをフィルターに掛ける」)が、これは正しい方法ではありません。代わりにこのように書かれました:

    from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
    

    ...これで、DBエンジンはこれをパラメーター化してインデックスシークを行う方法を認識しました。クエリ式にわずかな変更を加えたと思われるわずかな変更が、パフォーマンスに大きく影響する可能性があります。

    残念ながら、LINQでは一般に、このような悪いクエリを作成するのは非常に簡単です。なぜなら、sometimesプロバイダーは、何をしようとしているかを推測し、クエリを最適化できますが、そうでない場合もあるからです。つまり、単純に古いSQLを作成しただけでは、(経験豊富なDBAにとってはとにかく)盲目的に明白であるはずのinconsistentの結果にイライラします。

    基本的に、すべては、生成されたSQLとそれらがもたらす実行計画の両方に注意を払う必要があるという事実に帰着します。期待した結果が得られない場合でも、回避することを恐れないでください。 ORMレイヤーをときどきSQLを手動でコーディングします。これは、EFだけでなく、anyORMにも当てはまります。

  6. トランザクションとロック

    現在のミリ秒までのデータを表示する必要がありますか?多分-それは依存します-しかしおそらくそうではありません。残念ながら、 Entity Frameworkはnolock を提供しません。READ UNCOMMITTEDtransactionレベルでのみ使用できます(テーブルレベルではありません)。実際、どのORMもこれについて特に信頼できるものはありません。ダーティリードを実行する場合は、SQLレベルにドロップダウンして、アドホッククエリまたはストアドプロシージャを作成する必要があります。つまり、結局のところ、フレームワーク内でそれを実行するのがいかに簡単かということです。

    Entity Frameworkはこの点で長い道のりを歩んできました-EFのバージョン1(.NET 3.5)はひどいもので、「エンティティ」の抽象化を打ち破るのが信じられないほど困難になりましたが、今では ExecuteStoreQuery および 翻訳 なので、それほど悪くはありません。あなたは彼らをたくさん使うことになるので、これらの人たちと友達を作りましょう。

    また、書き込みロックとデッドロックの問題、およびデータベース内のロックをできるだけ短時間保持する一般的な方法もあります。この点で、ほとんどのORM(Entity Frameworkを含む)は、実際の生のSQLよりもbetterになる傾向があります。これは、 作業単位 パターンをカプセル化するためです。EFでは SaveChanges 。つまり、必要なときにいつでもエンティティを「挿入」、「更新」、または「削除」して、作業ユニットをコミットするまで変更がデータベースにプッシュされないという知識で安全を確保できます。

    UOWは、長時間実行トランザクションに類似したnotであることに注意してください。 UOWは引き続きORMの楽観的同時実行機能を使用し、すべての変更を追跡しますin memory。最後のコミットまで、単一のDMLステートメントは発行されません。これにより、トランザクション時間が最小限に抑えられます。生のSQLを使用してアプリケーションを構築する場合、この遅延された動作を実現することは非常に困難です。

    これがEFにとって特に意味すること:作業単位をできるだけ粗くし、絶対に必要になるまでコミットしないでください。これを行うと、個々のADO.NETコマンドをランダムに使用する場合よりもはるかに低いロック競合が発生します。

結論として:

他のすべてのフレームワークが高トラフィック/高性能アプリケーションに適しているのと同じように、EFは高トラフィック/高性能アプリケーションに完全に適しています。重要なのは、それをどのように使用するかです。最も人気のあるフレームワークと、パフォーマンスの観点から提供される機能の簡単な比較を以下に示します(凡例:N =サポートされていない、P =一部、Y =はい/サポートされている):

                                | 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

ご覧のとおり、EF4(現在のバージョン)はそれほど悪くはありませんが、パフォーマンスが主な関心事である場合は、おそらく最高ではありません。 NHibernateはこの分野ではるかに成熟しており、Linq to SQLでも、EFにはまだないパフォーマンス向上機能がいくつかあります。生のADO.NETは、specificデータアクセスシナリオの方が高速になることがよくありますが、すべての要素を組み合わせた場合、実際にはそれほど重要なメリットはありません。さまざまなフレームワークから取得します。

そして、私が壊れたレコードのように聞こえることを完全に確認するために、データベース、アプリケーション、およびデータアクセス戦略を適切に設計しなければ、これはほとんど問題になりません。上のグラフのすべての項目は、ベースラインを超えるimprovingパフォーマンス用です。ほとんどの場合、ベースライン自体が最も改善が必要なものです。

216
Aaronaught

編集:@Aaronaughtのすばらしい回答に基づいて、EFでパフォーマンスをターゲットにしたポイントをいくつか追加しています。これらの新しいポイントには、編集のプレフィックスが付いています。


トラフィックの多いWebサイトでのパフォーマンスの最大の改善は、キャッシュ(=まずWebサーバーの処理やデータベースのクエリを回避する)と、それに続くデータベースのクエリの実行中のスレッドのブロックを回避するための非同期処理によって実現されます。

それは常にアプリケーションの要件とクエリの複雑さに依存するため、あなたの質問に対する弾丸を証明する答えはありません。真実は、EFを使用した開発者の生産性によって複雑さが隠され、多くの場合、EFの不適切な使用とひどいパフォーマンスにつながります。データアクセス用に高レベルの抽象化されたインターフェイスを公開できるというアイデアは、すべてのケースでスムーズに機能し、機能しません。 ORMを使用する場合でも、抽象化の背後で何が起こっているか、およびそれを正しく使用する方法を知っている必要があります。

EFの経験がない場合は、パフォーマンスを処理するときに多くの課題に直面します。 EFを使用する場合、ADO.NETと比較してはるかに多くの間違いを犯す可能性があります。また、EFでは多くの追加処理が行われるため、EFは常にネイティブADO.NETよりも大幅に遅くなります。これは、単純な概念実証アプリケーションで測定できるものです。

EFから最高のパフォーマンスを得たい場合は、おそらく次のことを行う必要があります。

  • SQLプロファイラーを使用してデータアクセスを慎重に修正し、LINQ-to-ObjectsではなくLinq-to-entitiesを正しく使用しているかどうかをLINQクエリで確認します。
  • MergeOption.NoTrackingなどの高度なEF最適化機能を慎重に使用する
  • 場合によってはESQLを使用する
  • 頻繁に実行されるプリコンパイルクエリ
  • EFキャッシュラッパーを利用して、一部のクエリの「2次レベルキャッシュ」のような機能を取得することを検討してください。
  • パフォーマンスの改善を必要とする、頻繁に使用されるプロジェクションまたは集計の一部のシナリオでは、SQLビューまたはカスタムマップされたSQLクエリ(EDMXファイルの手動メンテナンスが必要)を使用します
  • LinqまたはESQLで定義されたときに十分なパフォーマンスを提供しない一部のクエリには、ネイティブSQLおよびストアドプロシージャを使用します
  • 編集:クエリを慎重に使用する-すべてのクエリは、データベースへの個別の往復を行います。 EFv4は、実行されたデータベースコマンドごとに複数の結果セットを使用できないため、クエリのバッチ処理はありません。 EFv4.5は、マップされたストアドプロシージャの複数の結果セットをサポートします。
  • 編集:データ変更を慎重に処理します。再び EFはコマンドのバッチ処理を完全に欠いています 。したがって、ADO.NETでは、複数の挿入、更新、または削除を含む単一のSqlCommandを使用できますが、EFでは、そのようなすべてのコマンドがデータベースへの個別のラウンドトリップで実行されます。
  • 編集:アイデンティティマップ/アイデンティティキャッシュを慎重に操作します。 EFには、最初にキャッシュを照会する特別なメソッド(ObjectContext APIのGetByKeyまたはDbContext APIのFind)があります。 Linq-to-entitiesまたはESQLを使用すると、データベースへのラウンドトリップが作成され、その後、キャッシュから既存のインスタンスが返されます。
  • 編集:熱心な読み込みを慎重に使用します。 巨大なデータセット が1つ生成されるため、必ずしもwin-winソリューションとは限りません。ご覧のように、それは多くの追加の複雑さであり、それが全体のポイントです。 ORMを使用すると、マッピングと実体化が簡単になりますが、パフォーマンスを処理する場合は、はるかに複雑になり、トレードオフを行う必要があります。

SOがまだL2Sを使用しているかどうかはわかりません。彼らは Dapper と呼ばれる新しいオープンソースORMを開発しました。この開発の背後にある主なポイントはパフォーマンスの向上でした。

38
Ladislav Mrnka