Ten Steps to iRules Optimization
Ever wondered whether to use switch or if/else? How about the effects of regular expressions? Or even what the overhead of variables are? We've come up with a list of the top 10 optimization techniques you can follow to write a healthier happier iRule.
#1 - Use a Profile First
This one seems obvious enough but we continually get questions on the forums about iRules that are already built into the product directly. iRules does add extra processing overhead and will be slower than executing the same features in native code. Granted, they won't be that slow, but any little bit you can save will help you in the long run. So, if you aren't doing custom conditional testing, let the profiles do the work. The following common features implemented in iRules can be handled by native profiles:
- HTTP header insert and erase
- HTTP fallback
- HTTP compress uri <exclude|include>
- HTTP redirect rewrite
- HTTP insert X-Forwarded-For
- HTTP ramcache uri <exclude|include|pinned>
- Stream profile for content replacement
- Class profile for URI matching.
Even if you do need to do some conditional testing, several of the profiles such as the stream profile will allow you to enable/disable and control the expression directly from the iRule.
#2 - A Little Planning Goes A Long Way
iRules are fun and it is really easy to find yourself jumping right in and coding without giving proper thought to what you are trying to accomplish. Just like any software project, it will pay off in the long run if you come up with some sort of feature specification and try to determine the best way to implement it. Common questions you can ask yourself include:
- What are the protocols involved?
- What commands should I use to achieve the desired result?
- How can I achieve the desired result in the least amount of steps?
- What needs to be logged?
- Where and how will I test it?
By taking a few minutes up front, you will save yourself a bunch of rewrites later on.
#3 - Understand Control Statements
Common control statements include if/elseif/else, switch, and the iRule matchclass/findclass commands with datagroups. The problem is that most of these do the same thing but, depending on their usage, their performance can vary quite a bit. Here are some general rules of thumb when considering which control statement to use:
- Always think: "switch", "data group", and then "if/elseif" in that order.
If you think in this order, then in most cases you will be better off. - Use switch for < 100 comparisons.
Switches are fastest for fewer than 100 comparisons but if you are making changes to the list frequently, then a data group might be more manageable. - Use data groups for > 100 comparisons.
Not only will your iRule be easier to read by externalizing the comparisons, but it will be easier to maintain and update. - Order your if/elseif's with most frequent hits at the top.
If/elseif's must perform the full comparison for each if and elseif. If you can move the most frequently hit match to the top, you'll have much less processing overhead. If you don't know which hits are the highest, think about creating a Statistics profile to dynamically store your hit counts and then modify you iRule accordingly. - Combine switch/data group/if's when appropriate.
No one said you have to pick only one of the above. When it makes since, embed a switch in an if/elseif or put a matchclass inside a switch. - User your operators wisely. "equals" is better than "contains", "string match/switch-glob" is better than regular expressions.
The "equals" operator checks to see if string "a" is contained in string "b". If you are trying to do a true comparison, the overhead here is unnecessary - use the "equals" command instead. As for regular expressions, see #4. - Use break, return, and EVENT::disable commands to avoid unnecessary iRule processing.
Since iRules cannot be broken into procedures they tend to be lengthy in certain situations. When applicable use the "break", "return", or "EVENT::disable" commands to halt further iRule processing.
Now, these are not set in stone but just general rules of thumb so use your best judgement. There will always be exceptions to the rule and it is best if you know the cost/benefits to make the right decision for you.
#4 - Regex is EVIL!
Regex's are cool and all, but are CPU hogs and should be considered pure evil. In most cases there are better alternatives.
Bad
when HTTP_REQUEST { if { [regex {^/myPortal} [HTTP::uri] } { regsub {/myPortal} [HTTP::uri] "/UserPortal" newUri HTTP::uri $newUri }
Good
when HTTP_REQUEST { if { [HTTP::uri] starts_with "/myPortal" } { set newUri [string map {myPortal UserPortal [HTTP::uri]] HTTP::uri $newUri }
But sometimes they are a necessary evil as illustrated in the CreditCardScrubber iRule. In the cases where it would take dozens or more commands to recreate the functionality in a regular expression, then a regex might be your best option.
when HTTP_RESPONSE_DATA { # Find ALL the possible credit card numbers in one pass set card_indices [regexp -all -inline -indices \ {(?:3[4|7]\d{2})(?:[ ,-]?(?:\d{5}(?:\d{1})?)){2}|(?:4\d{3})(?:[ ,-]?(?:\d{4})){3}|(?:5[1-5]\d{2})(?:[ ,-]?(?:\d{4})){3}|(?:6011)(?:[ ,-]?(?:\d{4})){3}} \ [HTTP::payload]] }
#5 - Don't Use Variables
The use of variables does incur a small amount of overhead that in some cases can be eliminated. Consider the following example where the value of the Host header and the URI are stored in variables and later are compared and logged. These take up additional memory for each connection.
when HTTP_REQUEST { set host [HTTP::host] set uri [HTTP::uri] if { $host equals "bob.com" } { log "Host = $host; URI = $uri" pool http_pool1 } }
Here is the exact same functionality as above but without the overhead of the variables.
when HTTP_REQUEST { if { [HTTP::host] equals "bob.com" } { log "Host = [HTTP::host]; URI = [HTTP::uri]" pool http_pool1 } }
You may ask yourself: "Self, who cares about a spare 30 bytes or so?" Well, for a single connection, it's likely no big deal but you need to think in terms of 100's of 1000's of connections per second. That's where even the smallest amount of overhead you can save will add up. Removing those variables could gain you a couple thousand connections per second on your application.
And never fear, there is no more overhead in using the builtin commands like [HTTP::host] over a varable reference as both are just direct pointers to the data in memory.
#6 - Use Variables
Now that I've told you not to use variables, here's the case where using variables makes sense. When you need to do repetitive costly evaluations on a value and can avoid it by storing that evaluation in a variable, then by all means do so.
Bad - Too many memory allocations
when HTTP_REQUEST { if { [string tolower [HTTP::uri]] starts_with "/img" } { pool imagePool } elseif { ([string tolower [HTTP::uri]] ends_with ".gif") || ([string tolower [HTTP::uri]] ends_with ".jpg") } { pool imagePool } }
Bad - The length of a variable name can actually consume more overhead due to the way TCL stores variables in lookup tables.
when HTTP_REQUEST { set theUriThatIAmMatchingInThisiRule [string tolower [HTTP::uri]] if { $theUriThatIAmMatchingInThisiRule starts_with "/img" } { pool imagePool } elseif { ($theUriThatIAmMatchingInThisiRule ends_with ".gif") || ($theUriThatIAmMatchingInThisiRule ends_with ".jpg") } { pool imagePool } }
Good - Keep your variables names to a handful of characters.
when HTTP_REQUEST { set uri [string tolower [HTTP::uri]] if { $uri starts_with "/img" } { pool imagePool } elseif { ($uri ends_with ".gif") || ($uri ends_with ".jpg") } { pool imagePool } }
#7 - Understand Polymorphism
TCL is polymorphic in that variables can "morph" back and forth from data type to data type depending on how it is used. This makes loosly-typed languages like TCL easier to work with since you don't need to declare variables as a certain type such as integer or string but it does come at a cost. You can minimize those costs by using the correct operators on your data types.
- Use the correct operator for the correct type.
- Use eq,ne when comparing strings.
- Use ==,!= when comparing numbers
- Use [IP::addr] when comparing IP addresses.
If you aren't careful, things may not be what they seem as illustrated below
set x 5 if { $x == 5 } { } # evaluates to true if { $x eq 5 } { } # evaluates to true if { $x == 05 } { } # evaluates to true if { $x eq 05 } { } # evaluates to false
#8 - Timing
The timing iRule command is a great tool to help you eek the best performance out of your iRule. CPU cycles are stored on a per event level and you can turn "timing on" for any and all of your events. These CPU metrics are stored and can be retrieved by the bigpipe CLI, the administrative GUI, or the iControl API. The iRule Editor can take these numbers and convert them into the following statistics
- CPU Cycles/Request
- Run Time (ms)/Request
- Percent CPU Usage/Request
- Max Concurrent Requests
Just make sure you turn those timing values off when you are done optimizing your iRule as that will have overhead as well!
#9 - Use The Community
DevCentral is a great resource whether you are just starting your first iRule or a seasoned veteran looking for advice on something you may have overlooked. The forums and wikis can be a life saver when trying to squeeze every bit of performance out of your iRule.
#10 - Learn about the newest features
Read the release documentation for new BIG-IP releases as we are constantly adding new functionalty to make your iRules faster. For instance, look for the upcoming "class" command to make accessing your data groups lightning fast.
Conclusion
Take these 10 tips with you when you travel down the road of iRule development and you will likley have a faster iRule to show for it. As always, if you have any tips that you've found to be helpful when optimizing your iRules, please pass them along and we'll add them to this list.
- Javier_Checa_41NimbostratusNice one Joe!!
- Thanks! Hope it helps...