![]() |
Database »
Database »
General
Advanced
License: The Code Project Open License (CPOL)
Identifying NHibernate-Related Bottlenecks through Performance MonitoringBy Peter WeissbrodA distilled methodology for detecting and isolating NHibernate-related performance and scalability issues |
SQL, C# 1.0, C# 2.0, .NET, Win2K, WinXP, Win2003, Vista, ASP.NET, SQL 2000, VS.NET2003, DBA, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
My work requires skills balanced somewhat evenly between a DBA and a software engineer. Through this position, I've learned some common gaps of misunderstanding between the two parties. NHibernate is great for the software engineer because it abstracts away most details of the data layer. However, much like any ORM framework, the caveat it carries is that you must pay attention to the way it interacts with your database.
This is the first part of two posts to help enable your NHibernate-based application meet your performance and scalability needs. This is a focus of effectively gathering data and understanding about your bottleneck. The second part can be found here.
First and foremost, it's important to correctly identify your performance issue before considering solutions because you need to be able to quantify the difference in performance your changes have made. Many books and applications already exist to help you load test applications. The following section suggests a distilled methodology using basic tools to identify data-layer bottlenecks.
SQL Server (Developer Edition Preferred): This is the same distribution of your (enterprise/standard) production server except it's built for an isolated development environment. The specific tools we will be using include:
SQL Profiler- The one and only! We use this extensively to view every database call NHibernate is sending to the database as well as the time and CPU cost each call creates. For those new to SQL profiler, make sure to learn the basics.
Query analyzer- We use SQL profiler to gather the collection of database calls in our troublesome areas. We use query analyzer to dissect and evaluate the performance of atomic database calls by viewing their compiled execution plans. Optimizing specific queries is an art in itself, reaching beyond the scope of this article. To learn the basics of how to properly read and interpret an execution plan through query analyzer, consider starting here for an introduction.
.NET profiler: On a higher level, we need some program capable of reporting performance from the perspective of the .NET side of our application, preferably an execution trace showing us method calls and execution times in the form of an execution tree path. From my experience, I can recommend programs such as JetBrains DotTrace, AQTime, and ACT.
Stress Generator: Feel free to select your favorite third party utility, but the desired functionality you will need is the ability to hammer your target use case or method a variable number of times with a variable number of concurrent requests. To maximize control and focus, consider writing some custom load-testing module if you don't already have one. I've attached an example project in Visual Studio 2005 that should help guide you in the right direction. This is a skeleton console application, complete with a command line, threadpool, and mock setup that we have used in the past. Hopefully, you'll spend less time building load testing code, and more time experimenting!
Perfmon: An invaluable performance metrics tool already built into your Windows system. All kinds of great counters can yield information, but too much information can muddy the waters. Use the following performance counters when running your scalability tests:
SQLServer:Locks::Average wait time : The telltale sign of poor scalability, this shows how long a given thread is waiting for a lock to be released before it can complete its job.
Memory::Page Faults/sec: How often does the database have to read from the disk versus memory? This can give hints into improving your indexing.
Isolate your test environment: Performance testing is an experiment. Just like any proper experiment, we need to set up a controlled environment. It's important to eliminate any potential outside noise. Try to run the app as a standalone on your local machine if possible.
When you know the "use case" area you need to tune, but are not sure which specific methods are problematic, use the following steps to gather more specific data.
The data from this .NET profile will give us insight into which method calls stand out as sore thumbs. Once we've found the target methods, you can use the following tests to drill down to the real culprits.
Given a target method, it's time to find how this method impacts your database in a single call.
Is there a single database call that's taking a long time? Are there more database calls than you would expect? Take note, and try to find out why this is happening, but resist the temptation to jump to conclusions just yet.
Make a few graphs that display execution time (in terms of the .NET profile or query profile) versus thread count. Each graph has a certain number of method calls (a graph for 1, 10, 100, 1000, etc.):
If scalability is your problem, your graphs will look somewhat like this:
Good scalability is indicated by a reduction in execution time as threads are introduced. Of course, there is a threshold to the number of threads you can add before "context-switching" overhead hinders your performance (aka "thrashing"), but this is natural, and dependent on the hardware and OS. The following is an example of good scaling up to 5 threads:
How many concurrent threads can your app manage before it starts to suffer? Is this threshold acceptable enough to meet your requirements?
At this point, save your .NET traces, your profiler traces, and your scalability results. Be sure you can re-run these tests in the future to accurately quantify your gains in performance once you're done adding turbo boosts to your app!
The following SQL will help you find the most frequently used queries:
SELECT
DISTINCT cast(textdata as varchar(150)) as textdata,
avg(duration) as avg_duration,
count(duration) as Occurences
FROM
[<yourTraceTableHere>]
GROUP BY
Cast(textdata as VarChar(150))
ORDER BY
count(duration)desc
And this will help you find your more inefficient queries:
SELECT
DISTINCT cast(textdata as varchar(150)) as textdata,
avg(duration) as avg_duration,
count(duration) as Occurences
FROM
[<yourTraceTableHere>]
GROUP BY
Cast(textdata as VarChar(150))
ORDER BY
Avg(duration)desc
Sort your SQL profile traces by CPU cost, execution time to find expensive queries. Note any of the following:
Are you loading more data than you really need in some queries?
Are you calling the database more than necessary?
List out what problems you suspect and order them by severity. At this point, it's tempting to implement solutions, but make sure you have a good grasp on precisely where your problem lies as well as the overall time cost it poses before you begin.
What kind of common problems can ORM introduce into your system?
All too often, there's an excessive amount of I/O "chat" between your .NET and database tiers. You call for an entity layer object, and NHibernate automatically "hydrates" this object for you from values in your database. What kind of work actually goes in to hydrating an object?
Now that's a non-trivial amount of I/O work! Any good DBA will preach to you that it's bad practice to load more data than you really need. Think about this the next time you make a DAO call.
Is there a roundtrip to the database for every single object you load? If so, it brings forth some considerable overhead cost in the form of database-layer communication. The end result? Things look good in your initial tests, but later you will find the scalability is abysmal. The solution? Decrease the "chat" between your tiers by increasing the granularity of your database calls. The next chapter will explain how to manage this problem.
Sometimes, you may want a specific object, but due to your mapping definitions, you always get more data than you really need. I've seen the "Greedy" loading problem come about in three different ways:
My next section will finally get into the details of how to control your NHibernate data layer access for better granularity, more efficient locking, caching schemes, isolated searches, and yes, stored procedures/functions.
Can be found here.
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 10 Jun 2007 Editor: Deeksha Shenoy |
Copyright 2007 by Peter Weissbrod Everything else Copyright © CodeProject, 1999-2009 Web22 | Advertise on the Code Project |