Chase bumped a question on Stack Exchange my way last night on redirects. The question was simple enough: how do you take a request for https://website1.com/user=1234 and redirect to https://website2.com/user=1234? This can (and if this is the sole logic, probably should) be done in a local traffic policy. But this is a post about iRules, so I'll explore that solution below.

At a hight level, here is what needs to happen:

If the host is website1.com and the uri begins with /user= with a user id immediately following, then redirect that request to website2.com and preserve the user id.

Now that I know the what, I can move on to the how. That’s where things get interesting. There is at least two solutions for each component that needs to be evaluated.

  • For the host comparison, we can use either the == or eq operator.
  • To match how the URI starts to /user=, we can use the custom iRules operator starts_with, or use the native Tcl string match command starts_with is based on in its place.
  • To test that both the host and the URI pattern match, we can use either the iRules and or the native Tcl logical && operator.
In Tcl, everything is a string, but how the interpreter acts on those strings is different matter, as you’ll see below. To test performance, I’ll keep the iRule logic very small so that all the active logic is occurring in the lines I’m testing. I’ll use the apache bench (ab) tool from a linux shell to run the queries, all with a URL of http://192.168.102.131/user=1234.

Host Comparison

In my example here I am simply looking at a string in “website1.com”. Whereas with Tcl using the == operator will work, the interpreter performs a math operation (eq in the first example) instead of a string operation (streq in the second example), as you can see in the byte code below (note this is in tclsh, not in iRules.)

% set x "website1.com"
website1.com
% tcl::unsupported::disassemble script { if { $x == "host" } {} }
ByteCode 0x0x972dc18, refCt 1, epoch 3, interp 0x0x96f78f8 (epoch 3)
  Source " if { $x == \"host\" } {} "
  Cmds 1, src 24, inst 15, litObjs 4, aux 0, stkDepth 2, code/src 0.00
  Commands 1:
      1: pc 0-13, src 1-23
  Command 1: "if { $x == \"host\" } {} "
    (0) push1 0         # "x"
    (2) loadScalarStk
    (3) push1 1         # "host"
    (5) eq
    (6) jumpFalse1 +6   # pc 12
    (8) push1 2         # ""
    (10) jump1 +4       # pc 14
    (12) push1 3        # ""
    (14) done

%  tcl::unsupported::disassemble script { if { $x eq "host" } {} }
ByteCode 0x0x972dea8, refCt 1, epoch 3, interp 0x0x96f78f8 (epoch 3)
  Source " if { $x eq \"host\" } {} "
  Cmds 1, src 24, inst 15, litObjs 4, aux 0, stkDepth 2, code/src 0.00
  Commands 1:
      1: pc 0-13, src 1-23
  Command 1: "if { $x eq \"host\" } {} "
    (0) push1 0         # "x"
    (2) loadScalarStk
    (3) push1 1         # "host"
    (5) streq
    (6) jumpFalse1 +6   # pc 12
    (8) push1 2         # ""
    (10) jump1 +4       # pc 14
    (12) push1 3        # ""
    (14) done

Reconstructing these simple tests as iRules commands, I have the following:

when HTTP_REQUEST timing on {
# Operation 1: eq OR ==
# if { [HTTP::host] eq "website1.com" } {}
# if { [HTTP::host] == "website1.com" } {}
  HTTP::respond 200 content "ok"
}

Uncommenting each if statement one at a time and running the ab command ab -n 10000 http://192.168.102.131/user=1234, I show a 19.9% reduction in average CPU cycles by using eq over ==. You mileage may vary by platform, but should still show a reduction.

URI Matching

I can't evaluate the byte code here as starts_with is an iRules feature and is not part of native Tcl. But we can still use the iRules timing functionality. The two command options are:

when HTTP_REQUEST timing on {
# Operation 2: starts_with OR string match
#  if { [HTTP::uri] starts_with "/user=" } {}
#  if { [string match "/user=*" [HTTP::uri]] } {}
  HTTP::respond 200 content "ok"
}

Using the string match command here results in a reduction of 4.6% in average CPU cycles. That may not be enough of a savings for you to justify the less readable string match command, but if performance is king, there's a slight but sure victor.

Logical AND Between Conditions

 I also can’t evaluate the byte code here as and is not a recognized logical operator in native Tcl. First, the commands:

when HTTP_REQUEST timing on {
# Operation 3: and OR &&
# if { 1 and 1 } {}
# if { 1 && 1 } {}
  HTTP::respond 200 content "ok"
}

The performance results show that when comparing numbers (the 1/true or 0/false result of the previous two operations), using && over and nets a 10.1% savings in average CPU cycles.

Putting It All Together

Now that all three individual operations have been optimized, for fun I'll combine the worst options and the best options and look at the combined results. The commands:

when HTTP_REQUEST timing on {
# Combined Operations
# Sub-optimal choices
# if { ([HTTP::host] == "website1.com") and ([HTTP::uri] starts_with "/user=") } {}
# Optimal choices
# if { ([HTTP::host] eq "website1.com") && ([string match "/user=*" [HTTP::uri]]) } {}
  HTTP::respond 200 content [HTTP::host]
}

With this configuration of optimal choices, I show a reduction in average CPU cycles of 22.7%!

Conclusion

Before closing, I have a couple notes.

  • Optimizing the usage of the Tcl language (and the iRules specific changes) doesn't necessarily require any changes to the logic or structure of your iRules and can yield big results! Be a student of the tools you use.
  • More tests are better. I'd recommend you hit your iRules at double the normal load factor to get a solid feel for what the average CPU cycles are going to be.
  • For testing and understanding, use the time and tcl::unsupported::disassemble script commands in the tclsh where you can, substituting variables for built-in iRules cached values where necessary.
  • For performance issues bigger than a single command or event, you can use the iRules tracing tools detailed in part 1, part 2, and part 3.