Grisha Trubetskoy

Notes to self.

Time Series Accuracy - Graphite vs RRDTool

| Comments

Back in my ISP days, we used data stored in RRDs to bill our customers. I wouldn’t try this with Graphite. In this write up I try to explain why it is so by comparing the method of recording time series used by Graphite, with the one used by RRDTool.

Graphite uses Whisper to store data, which in the FAQ is portrayed as a better alternative to RRDTool, but this is potentially misleading, because the flexibility afforded by the design of Whisper comes at the price of inaccuracy.

A time series is most often described as a sequence of (time, value) tuples [1]. The most naive method of recording a time series is to store timestamps as is. Since the data points might arrive at arbitrary and inexact intervals, to correlate the series with a particular point in time might be tricky. If data points are arriving somewhere in between one minute bounaries (as they always naturally would), to answer the question of what happened during a particular minute would require specifying a range, which is not as clean as being able to specify a precise value. To join two series on a range is even more problematic.

One way to improve upon this is to divide time into equal intervals and assign data points to the intervals. We could then use the beginning of the interval instead of the actual data point timestamp, thereby giving us more uniformity. For example, if our interval size is 10 seconds (I may sometimes refer to it as the step), we could divide the entire timeline starting from the beginning of the epoch and until the end of universe into 10 second slots. Since the first slot begins at 0, any 10-second-step time series will have slots starting at the exact same times. Now correlation across series or other time values becomes much easier.

Calculating the slot is trivially easy: time - time % step (% being the modulo operator). There is, however, a subtle complexity lurking when it comes to storing the datapoint with the adjusted (or aligned) timestamp. Graphite simply changes the timestamp of the data point to the aligned one. If multiple data points arrive in the same step, then the last one “wins”.

On the surface there is little wrong with Graphite’s approach. In fact, under right circumstances, there is absolutely nothing wrong with it. Consider the following example:

1
2
3
4
5
6
7
Graphite, 10 second step.

Actual Time   Aligned Time
1430701282    1430701280     50  <-- This data point is lost
1430701288    1430701280     10
1430701293    1430701290     30
1430701301    1430701300     30

Let’s pretend those values are some system metric like the number of files open. The consequence of the 50 being dropped is that we will never know it existed, but towards the end of the 10 second interval it went down to 10, which is still a true fact. If we really wanted to know about the variations within a 10 second interval, we should have chosen a smaller step, e.g. 1 second. By deciding that the step is going to be 10 seconds, we thus declared that variations within a smaller period are of no interest to us, and from this perspective, Graphite is correct.

But what if those numbers are the price of a stock: there may be hundreds of thousand of trades within a 10 second interval, yet we do not want to (or cannot, for technical reasons) record every single one of them? In this scenario having the last value override all previous ones doesn’t exactly seem correct.

Enter RRDTool which uses a different method. RRDTool keeps track of the last timestamp and calculates a weight for every incoming data point based on time since last update or beginning of the step and the step length. Here is what the same sequence of points looks like in RRDTool. The lines marked with a * are not actual data points, but are the last value for the preceding step, it’s used for computing the value for the remainder of the step after a new one has begun.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RRDTool, 10 second step.

  Time          Value       Time since  Weight  Adjusted   Recorded
                            last                value      value
  1430701270    0           N/A
* 1430701280    50         10s          1       50* 1= 50
                                                           50
  1430701282    50          2s          .2      50*.2= 10
  1430701288    10          6s          .6      10*.6= 6
* 1430701290    30          2s          .2      30*.2= 6
                                                           10+6+6= 22
  1430701293    30          3s          .3      30*.3= 9
* 1430701300    30          7s          .7      30*.7= 21
                                                           9+21=   30
  1430701301    30   # this data point is incomplete

Note, by the way, that the Whisper FAQ says that “RRD will store your updates in a temporary workspace area and after the minute has passed, aggregate them and store them in the archive”, which to me sounds like there is some sort of a temporary storage area holding all the unsaved updates. In fact, to be able to compute the weighted average, RRD only needs to store the time of the last update and the current sum, i.e. exactly just two variables, regardless of the number of updates in a single step. This is evident from the above figure.

So to compare the results of the two tools:

1
2
3
4
5
Time Slot     Graphite    RRDTool
1430701270       N/A        50
1430701280       10         22
1430701290       30         30
1430701300       N/A        N/A

Before you say “so what, I don’t really understand the difference”, let’s pretend that those numbers were actually the rate of sale of trinkets from our website (per second). Here is a horizontal ascii-art rendition of our timeline, 0 is 1430701270.

1
2
3
4
0         10        20        30    time (seconds)
+.........+.........+.........+.....
|           |     |    |       |
0           50    10   30      30   data points

At 12 seconds we recorded selling 50 trinkets per second. Assuming we started selling at the beginning of our timeline, i.e. 12 seconds earlier, we can state that during the first step we sold exactly 500 trinkets. Then 2 seconds into the second step we sold another 100 (we’re still selling at 50/s). Then for the next 6 seconds we were selling at 10/s, thus another 60 trinkets, and for the last 2 seconds of the slot we sold another 60 at 30/s. In the third step we were selling steadily at 30/s, thus exactly 300 were sold.

Comparing RRDTool and Graphite side-by-side, the stories are quite different:

1
2
3
4
5
6
7
8
Trinkets per second and sold:
   Time Slot     Graphite Trinkets     RRDTool Trinkets
1. 1430701270      N/A      N/A          50      500
2. 1430701280       10      100          22      220 (100+60+60)
3. 1430701290       30      300          30      300
4. 1430701300       30      N/A          N/A     N/A
                          -----                -----
   TOTAL SOLD:              400                 1020

Two important observations here:

  1. The totals are vastly different.
  2. The rate recorded by RRDTool for the second slot (22/s), yields exactly the number of trinkets sold during that period: 220.

Last, but hardly the least, consider what happens when we consolidate data points into larger intervals by averaging the values. Let’s say 20 seconds, twice our step. If we consolidate the second and the third steps, we would get:

1
2
Graphite:  average(10,30) = 20  => 400 trinkets in 20 seconds
RRDTool:   average(22,30) = 26  => 520 trinkets in 20 seconds

Since the Graphite numbers were off to begin with, we have no reason to trust the 400 trinkets number. But using the RRDTool data, the new number happens to still be 100% accurate even after the data points have been consolidated. This is a very useful property of rates in time series. It also explains why RRDTool does not permit updating data prior to the last update: RRD is always accurate.

As an exercise, try seeing it for yourself: pretent the value of 10 in the second step never arrived, which should make the final value of the second slot 34. If the 10 arrived some time later, averaging it in will not give you the correct 22.

Whisper allows past updates, but is quasi-accurate to begin with - I’m not sure I understand which is better - inaccurate data with a data point missing, or the whole inaccurate data. RRD could accomplish the same thing by adding some --inaccurate flag, though it would seem like more of a bug than a feature to me.

If you’re interested in learning more about this, I recommend reading the documentation for rrdtool create, in particular the “It’s always a Rate” section, as well as this post by Alex van den Bogaerdt.

P.S. After this post was written, someone suggested that instead of storing a rate, we coud store a count delta. In other words, instead of recording that we’re selling 10 trinkets per second for the past 6 seconds, we would store the total count of trinkets sold, i.e. 60. At first this seems like the solution to being able to update historical data accurately: if later we found out that we sold another 75 trinkets in the second time slot, we could just add it to the total and all would be well and most importantly accurate.

Here is the problem with this approach: note that in the previous sentence I had to specify that the additional trinkets were sold in the second time slot, a small, but crucial detail. If time series data point is a timestamp and a value, then there isn’t even a way to relay this information in a single data point - we’d need two timestamps. On the other hand if every data point arrived with two timestamps, i.e. as a duration, then which to store, rate or count, becomes a moot point, we can infer one from the other.

So perhaps another way of explaining the historical update problem is that it is possible, but the datapoint must specify a time interval. This is something that neither RRDTool or Graphite currently support, even though it’d be a very useful feature in my opinion.

[1] Perhaps the biggest misconception about time series is that it is a series of data points. What time series represent is continuous rather than descrete, i.e. it’s the line that connects the points that matters, not the specific points themselves, they are just samples at semi-random intervals that help define the line. And as we know, a line cannot be defined by a single point.

On Time Series

| Comments

Is it even a thing?

Time Series is on its way to becoming a buzzword in the Information Technology circles. This has to do with the looming Internet of Things which shall cause the Great Reversal of Internet whereby upstream flow of data produced by said Things is expected to exceed the downstream flow. Much of this data is expected to be of the Time Series kind.

This, of course, is a money-making opportunity of the Big Data proportions all over again, and I predict we’re going to see a lot of Time Series support of various shapes and forms appearing in all manners of (mostly commercial) software.

But is there really such a thing as the problem specifically inherent to Time Series data which warrants a specialized solution? I’ve been pondering this for some time now, and I am still undecided. This here is my attempt at arguing that TS is not a special problem and that it can be done by using a database like PostgreSQL.

Influx of data and write speeds

One frequently cited issue with time series data is that it arrives in large volumes at a steady pace which renders buffered writes useless. The number of incoming data streams can also be large typically causing a disk seek per stream and further complicating the write situation. TS data also has a property where often more data is written than read because it’s possible for a datapoint to be collected and examined only once, if ever. In short, TS is very write-heavy.

But is this unique? For example logs have almost identical properties. The real question here is whether our tried and true databases such as PostgreSQL are ill-equipped to deal with large volumes of incoming data requiring an alternative solution.

When considering incoming data I am tempted to imagine every US household sending it, which, of course, would require massive infrastructure. But this (unrealistic) scenario is not a TS data problem, it’s one of scale, the same one from which the Hadoops and Cassandras of this world were born. What is really happening here is that TS happens to be yet another thing that requires the difficult to deal with “big data” infrastructure and reiterates the need for an easy-to-setup horizontally scalable database (which PostgreSQL isn’t).

The backfill problem

This is the problem of having to import vast amounts of historical data. For example OpenTSDB goes to great lengths to optimize back-filling by structuring it in specific ways and storing compressed blobs of data.

But just like the write problem, it’s not unique to TS. It is another problem that is becoming more and more pertinent as our backlogs of data going back to when we stopped using paper keep growing and growing.

Downsampling

Very often TS data is used to generate charts. This is an artifact of the human brain being spectacularly good at interpreting a visual representation of a relationship between streams of numbers while nearly incapable of making sense of data in tabular form. When plotting, no matter how much data is being examined, the end result is limited to however many pixels are available on the display. Even plotting aside, most any use of time series data is in an aggregated form.

The process of consolidating datapoints into a smaller number (e.g. the pixel width of the chart), sometimes called downsampling, involves aggregation around a particular time interval or simply picking every Nth datapoint.

As an aside, selecting every Nth row of a table is an interesting SQL challenge, in PostgreSQL it looks like this (for every 100th row):

1
2
3
 SELECT time, data FROM
   (SELECT *, row_number() OVER (ORDER BY time) as n FROM data_points) dp
      WHERE dp.n % 100 = 0 ORDER BY time

Aggregation over a time interval similar to how InfluxDB does it with the GROUP BY time(1d) syntax can be easily achieved via the date_trunc('day', time).

Another aspect of downsampling is that since TS data is immutable, there is no need to repeatedly recompute the consolidated version. It makes more sense to downsample immediately upon the receipt of the data and to store it permanently in this form. RRDTool’s Round-Robin database is based entirely on this notion. InfluxDB’s continuous queries is another way persistent downsampling is addressed.

Again, there is nothing TS-specific here. Storing data in summary form is quite common in the data analytics world and a “continuous query” is easily implemented via a trigger.

Derivatives

Sometimes the data from various devices exists in the form of a counter, which requires the database to derive a rate by comparing with a previous datapoint. An example of this is number of bytes sent over a network interface. Only the rate of change of this value is relevant, not the number itself. The rate of change is the difference with the previous value divided over the time interval passed.

Referring to a previous row is also a bit tricky but perfectly doable in SQL. It can accomplished by using windowing functions such as lag().

1
2
3
4
5
6
SELECT time,
  (bytes - lag(bytes, 1) OVER w) / extract(epoch from (time - lag(time, 1) OVER w))::numeric
    AS bytes_per_sec
  FROM data_points
  WINDOW w AS (ORDER BY time)
  ORDER BY time

Expiration

It is useful to downsample data to a less granular form as it ages, aggregating over an ever larger period of time and possibly purging records eventually. For example we might want to store minutely data for a week, hourly for 3 months, daily for 3 years and drop all data beyond 3 years.

Databases do not expire rows “natively” like Cassandra or Redis, but it shouldn’t be too hard to accomplish via some sort of a periodic cron job or possibly even just triggers.

Heartbeat and Interval Filling

It is possible for a time series stream to pause, and this can be interpreted in different ways: we can attempt to fill in missing data, or treat it as unknown. More likely we’d want to start treating it as unknown after some period of silence. RRDTool addresses this by introducing the notion of a heartbeat and the number of missed beats before data is treated as unknown.

Regardless of whether the value is unknown, it is useful to be able to fill in a gap (missing rows) in data. In PostgreSQL this can be accomplished by a join with a result set from the generate_series() function.

Data Seclusion

With many specialized Time Series tools the TS data ends up being secluded in a separate system not easily accessible from the rest of the business data. You cannot join your customer records with data in RRDTool or Graphite or InfluxDB, etc.

Conclusion

If there is a problem with using PosgreSQL or some other database for Time Series data, it is mainly that of having to use advanced SQL syntax and possibly requiring some cookie-cutter method for managing Time Series, especially when it is a large number or series and high volume.

There is also complexity in horizontally scaling a relational database because it involves setting up replication, sharding, methods for recovery from failure and balancing the data. But these are not TS-specific problems, they are scaling problems.

Having written this up, I’m inclined to think that perhaps there is no need for a specialized “Time Series Database”, instead it can be accomplished by an application which uses a database for storage and abstracts the users from the complexities of SQL and potentially even scaling, while still allowing for direct access to the data via the rich set of tools that a database like PostgreSQL provides.

How InfluxDB Stores Data

| Comments

A nice, reliable, horizontally scalable database that is designed specifically to tackle the problem of Time Series data (and does not require you to stand up a Hadoop cluster) is very much missing from the Open Source Universe right now.

InfluxDB might be able to fill this gap, it certainly aims to.

I was curious about how it structures and stores data and since there wasn’t much documentation on the subject and I ended up just reading the code, I figured I’d write this up. I only looked at the new (currently 0.9.0 in RC stage) version, the previous versions are significantly different.

First of all, InfluxDB is distributed. You can run one node, or a bunch, it seems like a more typical number may be 3 or 5. The nodes use Raft to establish consensus and maintain data consistency.

InfluxDB feels a little like a relational database in some aspects (e.g. it has a SQL-like query language) but not in others.

The top level container is a database. An InfluxDB database is very much like what a database is in MySQL, it’s a collection of other things.

“Other things” are called data points, series, measurements, tags and retention policies. Under the hood (i.e. you never deal with them directly) there are shards and shard groups.

The very first thing you need to do in InfluxDB is create a database and at least one retention policy for this database. Once you have these two things, you can start writing data.

A retention policy is the time period after which the data expires. It can be set to be infinite. A data point, which is a measurement consisting of any number of values and tags associated with a particular point in time, must be associated with a database and a retention policy. A retention policy also specifies the replication factor for the data point.

Let’s say we are tracking disk usage across a whole bunch of servers. Each server runs some sort of an agent which periodically reports the usage of each disk to InfluxDB. Such a report might look like this (in JSON):

1
2
3
4
5
6
{"database" : "foo", "retentionPolicy" : "bar",
 "points" : [
   {"name" : "disk",
    "tags" : {"server" : "bwi23", "unit" : "1"},
    "timestamp" : "2015-03-16T01:02:26.234Z",
    "fields" : {"total" : 100, "used" : 40, "free" : 60}}]}

In the above example, “disk” is a measurement. Thus we can operate on anything “disk”, regardless of what “server” or “unit” it applies to. The data point as a whole belongs to a (time) series identified by the combination of the measurement name and the tags.

There is no need to create series or measurements, they are created on the fly.

To list the measurements, we can use SHOW MEASUREMENTS:

1
2
3
4
> show measurements
name            tags    name
----            ----    ----
measurements            disk

We can use SHOW SERIES to list the series:

1
2
3
4
> show series
name    tags    id      server   unit
----    ----    --      -------  ----
disk            1       bw123    1

If we send a record that contains different tags, we automatically create a different series (or so it seems), for example if we send this (note we changed “unit” to “foo”):

1
2
3
4
5
6
{"database" : "foo", "retentionPolicy" : "bar",
 "points" : [
   {"name" : "disk",
    "tags" : {"server" : "bwi23", "foo" : "bar"},
    "timestamp" : "2015-03-16T01:02:26.234Z",
    "fields" : {"total" : 100, "used" : 40, "free" : 60}}]}

we get

1
2
3
4
5
> show series
name    tags    id      foo     server  unit
----    ----    --      ---     ------  ----
disk            1               bwi23   1
disk            2       bar     bwi23

This is where the distinction between measurement and series becomes a little confusing to me. In actuality (from looking at the code and the actual files InfluxDB created) there is only one series here called “disk”. I understand the intent, but not sure that series is the right terminology here. I think I’d prefer if measurements were simply called series, and to get the equivalent of SHOW SERIES you’d use something like SHOW SERIES TAGS. (May be I’m missing something.)

Under the hood the data is stored in shards, which are grouped by shard groups, which in turn are grouped by retention policies, and finally databases.

A database contains one or more retention policies. Somewhat surprisingly a retention policy is actually a bucket. It makes sense if you think about the problem of having to expire data points - you can remove them all by simply dropping the entire bucket.

If we declare a retention policy of 1 day, then we can logically divide the timeline into a sequence of single days from beginning of the epoch. Any incoming data point falls into its corresponding segment, which is a retention policy bucket. When clean up time comes around, we can delete all days except for the most current day.

To better understand the following paragraphs, consider that having multiple nodes provides the option for two things: redundancy and distribution. Redundancy gives you the ability to lose a node without losing any data. The number of copies of the data is controlled by the replication factor specified as part of the retention policy. Distribution spreads the data across nodes which allows for concurrency: data can be written, read and processed in parallel. For example if we become constrained by write performance, we can solve this by simply adding more nodes. InfluxDB favors redundancy over distribution when having to choose between the two.

Each retention policy bucket is further divided into shard groups, one shard group per series. The purpose of a shard group is to balance series data across the nodes of the cluster. If we have a cluster of 3 nodes, we want the data points to be evenly distributed across these nodes. InfluxDB will create 3 shards, one on each of the nodes. The 3 shards comprise the shard group. This is assuming the replication factor is 1.

But if the replication factor was 2, then there needs to be 2 identical copies of every shard. The shard copies must be on separate nodes. With 3 nodes and replication factor of 2, it is impossible to do any distribution across the nodes - the shard group will have a size of 1, and contain 1 shard, replicated across 2 nodes. In this set up, the third node will have no data for this particular retention policy.

If we had a cluster of 5 nodes and the replication factor of 2, then the shard group can have a size of 2, for 2 shards, replicated across 2 nodes each. Shard one replicas could live on nodes 1 and 3, while shard two replicas on nodes 2 and 4. Now the data is distributed as well as redundant. Note that the 5th node doesn’t do anything. If we up the replication factor to 3 then just like before, the cluster is too small to have any distribution, we only have enough nodes for redundancy.

As of RC15 distributed queries are not yet implemented, so you will always get an error if you have more than one shard in a group.

The shards themselves are instances of Bolt db - a simple to use key/value store written in Go. There is also a separate Bolt db file called meta which stores the metadata, i.e. information about databases, retention policies, measurements, series, etc.

I couldn’t quite figure out the process for typical cluster operations such as recovery from node failure or what happens (or should happen) when nodes are added to existing cluster, whether there is a way to decommission a node or re-balance the cluster similar to the Hadoop balancer, etc. I think as of this writing this has not been fully implemented yet, and there is no documentation, but hopefully it’s coming soon.

Ruby, HiveServer2 and Kerberos

| Comments

Recently I found myself needing to connect to HiveServer2 with Kerberos authentication enabled from a Ruby app. As it turned out rbhive gem we were using did not have support for Kerberos authentication. So I had to roll my own.

This post is to document the experience of figuring out the details of a SASL/GSSAPI connection before it is lost forever in my neurons and synapses.

First, the terminology. The authentication system that Hadoop uses is Kerberos. Note that Kerberos is not a network protocol. It describes the method by which authentication happens, but not the format of how to send Kerberos tickets and what not over the wire. For that, you need SASL and GSSAPI.

SASL is a generic protocol designed to be able to wrap just about any authentication handshake. It’s very simple: the client sends a START followed by some payload, and expects an OK, BAD or COMPLETE from the server. OK means that there are more steps to this conversation, BAD is self-explanatory and COMPLETE means “I’m satisfied”. The objective is to go from START via a series of OK’s to each side sending the other a COMPLETE.

SASL doesn’t define the payload of each message. The payload is specified by GSSAPI protocol. GSSAPI is another generic protocol. Unlike SASL it is actually very complex and covers a variety of authentication methods, including Kerberos.

The combination of SASL and GSSAPI and what happens at the network layer is documented in RFC4752.

Bottom line is you need to read at least four RFC’s to be able to understand every detail of this process: RFC4120, RFC2222, RFC2743 and RFC4752. Fun!

The Handshake in Ruby

First, you’ll need some form of binding to the GSSAPI libraries. I’ve been using the most excellent GSSAPI gem by Dan Wanek which wraps the MIT GSSAPI library.

If you follow the code in sasl_client_transport.rb, you’ll see the following steps are required to establish a connection.

First, we instantiate a GSSAPI object passing it the remote host and the remote principal. Note that there is no TCP port number to be specifies anywhere, because this isn’t to establish a TCP connection, but only for Kerberos host authentication. (Kerberos requires that not only the client authenticates itself to the host, but also that the host authenticates itself to the client.)

1
2
# Thrift::SaslClientTransport.initialize()
@gsscli = GSSAPI::Simple.new(@sasl_remote_host, @sasl_remote_principal)

The rest of the action takes place in the initiate_hand_shake_gssapi() method.

First, we call @gsscli.init_context() with no arguments. This call creates a token based on our current Kerberos credentials. (If there are no credentials in our cache, this call will fail).

1
  token = @gsscli.init_context

Next we compose a SASL message which consists of START (0x01) followed by payload length, followed by the actual payload, which is the SASL mechanism name: ‘GSSAPI’. Without waiting for response, we also send an OK (0x02) and the token returned from init_context().

1
2
3
4
5
  header = [NEGOTIATION_STATUS[:START], @sasl_mechanism.length].pack('cl>')
  @transport.write header + @sasl_mechanism
  header = [NEGOTIATION_STATUS[:OK], token.length].pack('cl>')
  @transport.write header + token
  status, len = @transport.read(STATUS_BYTES + PAYLOAD_LENGTH_BYTES).unpack('cl>')

Next we read 5 bytes of response. The first byte is the status returned from the server, which hopefully is OK, followed by the length of the payload, and then we read the payload itself:

1
2
3
4
5
6
7
8
  status, len = @transport.read(STATUS_BYTES + PAYLOAD_LENGTH_BYTES).unpack('cl>')
  case status
  when NEGOTIATION_STATUS[:BAD], NEGOTIATION_STATUS[:ERROR]
    raise @transport.to_io.read(len)
  when NEGOTIATION_STATUS[:COMPLETE]
    raise "Not expecting COMPLETE at initial stage"
  when NEGOTIATION_STATUS[:OK]
    challenge = @transport.to_io.read len

The payload is a challenge created for us by the server. We can verify this challenge by calling init_context() a second time, this time passing in the challenge to verify it:

1
2
3
4
    challenge = @transport.to_io.read len
    unless @gsscli.init_context(challenge)
      raise "GSSAPI: challenge provided by server could not be verified"
    end

If the challenge verifies, then it is our turn to send an OK (with an empty payload this time):

1
2
    header = [NEGOTIATION_STATUS[:OK], 0].pack('cl>')
    @transport.write header

At this point in the SASL ‘conversation’ we have verified that the server is who they claim to be.

Next the server sends us another challenge, this one is so that we can authenticate ourselves to the server and at the same time agree on the protection level for the communication channel.

We need to decrypt (“unwrap” in the GSSAPI terminology) the challenge, examine the protection level and if it is acceptable, encrypt it on our side and send it back to the server in a SASL COMPLETE message. In this particular case we’re agreeable to any level of protection (which is none in case of HiveServer2, i.e. the conversation is not encrypted). Otherwise there are additional steps that RFC4752 describes whereby the client can select an acceptable protection level.

1
2
3
4
5
6
7
8
9
10
11
12
    status2, len = @transport.read(STATUS_BYTES + PAYLOAD_LENGTH_BYTES).unpack('cl>')
    case status2
    when NEGOTIATION_STATUS[:BAD], NEGOTIATION_STATUS[:ERROR]
      raise @transport.to_io.read(len)
    when NEGOTIATION_STATUS[:COMPLETE]
      raise "Not expecting COMPLETE at second stage"
    when NEGOTIATION_STATUS[:OK]
      challenge = @transport.to_io.read len
      unwrapped = @gsscli.unwrap_message(challenge)
      rewrapped = @gsscli.wrap_message(unwrapped)
      header = [NEGOTIATION_STATUS[:COMPLETE], rewrapped.length].pack('cl>')
      @transport.write header + rewrapped

The server should then respond with COMPLETE as well, at which point we’re done with the authentication process and cat start sending whatever we want over this connection:

1
2
3
4
5
6
7
8
9
10
      status3, len = @transport.read(STATUS_BYTES + PAYLOAD_LENGTH_BYTES).unpack('cl>')
      case status3
      when NEGOTIATION_STATUS[:BAD], NEGOTIATION_STATUS[:ERROR]
        raise @transport.to_io.read(len)
      when NEGOTIATION_STATUS[:COMPLETE]
        @transport.to_io.read len
        @sasl_complete = true
      when NEGOTIATION_STATUS[:OK]
        raise "Failed to complete GSS challenge exchange"
      end

Graceful Restart in Golang

| Comments

Update (Apr 2015): Florian von Bock has turned what is described in this article into a nice Go package called endless.

If you have a Golang HTTP service, chances are, you will need to restart it on occasion to upgrade the binary or change some configuration. And if you (like me) have been taking graceful restart for granted because the webserver took care of it, you may find this recipe very handy because with Golang you need to roll your own.

There are actually two problems that need to be solved here. First is the UNIX side of the graceful restart, i.e. the mechanism by which a process can restart itself without closing the listening socket. The second problem is ensuring that all in-progress requests are properly completed or timed-out.

Restarting without closing the socket

  • Fork a new process which inherits the listening socket.
  • The child performs initialization and starts accepting connections on the socket.
  • Immediately after, child sends a signal to the parent causing the parent to stop accepting connecitons and terminate.

Forking a new process

There is more than one way to fork a process using the Golang lib, but for this particular case exec.Command is the way to go. This is because the Cmd struct this function returns has this ExtraFiles member, which specifies open files (in addition to stdin/err/out) to be inherited by new process.

Here is what this looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
file := netListener.File() // this returns a Dup()
path := "/path/to/executable"
args := []string{
    "-graceful"}

cmd := exec.Command(path, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = []*os.File{file}

err := cmd.Start()
if err != nil {
    log.Fatalf("gracefulRestart: Failed to launch, error: %v", err)
}

In the above code netListener is a pointer to net.Listener listening for HTTP requests. The path variable should contain the path to the new executable if you’re upgrading (which may be the same as the currently running one).

An important point in the above code is that netListener.File() returns a dup(2) of the file descriptor. The duplicated file descriptor will not have the FD_CLOEXEC flag set, which would cause the file to be closed in the child (not what we want).

You may come across examples that pass the inherited file descriptor number to the child via a command line argument, but the way ExtraFiles is implemented makes it unnecessary. The documentation states that “If non-nil, entry i becomes file descriptor 3+i.” This means that in the above code snippet, the inherited file descriptor in the child will always be 3, thus no need to explicitely pass it.

Finally, args array contains a -graceful option: your program will need some way of informing the child that this is a part of a graceful restart and the child should re-use the socket rather than try opening a new one. Another way to do this might be via an environment variable.

Child initialization

Here is part of the program startup sequence

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    server := &http.Server{Addr: "0.0.0.0:8888"}

    var gracefulChild bool
    var l net.Listever
    var err error

    flag.BoolVar(&gracefulChild, "graceful", false, "listen on fd open 3 (internal use only)")

    if gracefulChild {
        log.Print("main: Listening to existing file descriptor 3.")
        f := os.NewFile(3, "")
        l, err = net.FileListener(f)
    } else {
        log.Print("main: Listening on a new file descriptor.")
        l, err = net.Listen("tcp", server.Addr)
    }

Signal parent to stop

At this point we’re ready to accept requests, but just before we do that, we need to tell our parent to stop accepting requests and exit, which could be something like this:

1
2
3
4
5
6
7
if gracefulChild {
    parent := syscall.Getppid()
    log.Printf("main: Killing parent pid: %v", parent)
    syscall.Kill(parent, syscall.SIGTERM)
}

server.Serve(l)

In-progress requests completion/timeout

For this we will need to keep track of open connections with a sync.WaitGroup. We will need to increment the wait group on every accepted connection and decrement it on every connection close.

1
var httpWg sync.WaitGroup

At first glance, the Golang standard http package does not provide any hooks to take action on Accept() or Close(), but this is where the interface magic comes to the rescue. (Big thanks and credit to Jeff R. Allen for this post).

Here is an example of a listener which increments a wait group on every Accept(). First, we “subclass” net.Listener (you’ll see why we need stop and stopped below):

1
2
3
4
5
type gracefulListener struct {
    net.Listener
    stop    chan error
    stopped bool
}

Next we “override” the Accept method. (Nevermind gracefulConn for now, it will be introduced later).

1
2
3
4
5
6
7
8
9
10
11
func (gl *gracefulListener) Accept() (c net.Conn, err error) {
    c, err = gl.Listener.Accept()
    if err != nil {
        return
    }

    c = gracefulConn{Conn: c}

    httpWg.Add(1)
    return
}

We also need a “constructor”:

1
2
3
4
5
6
7
8
9
func newGracefulListener(l net.Listener) (gl *gracefulListener) {
    gl = &gracefulListener{Listener: l, stop: make(chan error)}
    go func() {
        _ = <-gl.stop
        gl.stopped = true
        gl.stop <- gl.Listener.Close()
    }()
    return
}

The reason the function above starts a goroutine is because this cannot be done in our Accept() above since it will block on gl.Listener.Accept(). The goroutine will unblock it by closing file descriptor.

Our Close() method simply sends a nil to the stop channel for the above goroutine to do the rest of the work.

1
2
3
4
5
6
7
func (gl *gracefulListener) Close() error {
    if gl.stopped {
        return syscall.EINVAL
    }
    gl.stop <- nil
    return <-gl.stop
}

Finally, this little convenience method extracts the file descriptor from the net.TCPListener.

1
2
3
4
5
func (gl *gracefulListener) File() *os.File {
    tl := gl.Listener.(*net.TCPListener)
    fl, _ := tl.File()
    return fl
}

And, of course we also need a variant of a net.Conn which decrements the wait group on Close():

1
2
3
4
5
6
7
8
type gracefulConn struct {
    net.Conn
}

func (w gracefulConn) Close() error {
    httpWg.Done()
    return w.Conn.Close()
}

To start using the above graceful version of the Listener, all we need is to change the server.Serve(l) line to:

1
2
netListener = newGracefulListener(l)
server.Serve(netListener)

And there is one more thing. You should avoid hanging connections that the client has no intention of closing (or not this week). It is better to create your server as follows:

1
2
3
4
5
server := &http.Server{
        Addr:           "0.0.0.0:8888",
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 16}

Mod_python Performance Part 2: High(er) Concurrency

| Comments

Tl;dr

As is evident from the table below, mod_python 3.5 (in pre-release testing as of this writing) is currently the fastest tool when it comes to running Python in your web server, and second-fastest as a WSGI container.

Server Version Req/s % of httpd static Notes
nxweb static file 3.2.0-dev 512,767 347.1 % “memcache”:false. (626,270 if true)
nginx static file 1.0.15 430,135 291.1 % stock CentOS 6.3 rpm
httpd static file 2.4.4, mpm_event 147,746 100.0 %
mod_python handler 3.5, Python 2.7.5 125,139 84.7 %
uWSGI 1.9.18.2 119,175 80.7 % -p 16 –threads 1
mod_python wsgi 3.5, Python 2.7.5 87,304 59.1 %
mod_wsgi 3.4 76,251 51.6 % embedded mode
nxweb wsgi 3.2.0-dev, Python 2.7.5 15,141 10.2 % posibly misconfigured?

The point of this test

I wanted to see how mod_python compares to other tools of similar purpose on high-end hardware and with relatively high concurrency. As I’ve written before you’d be foolish to base your platform decision on these numbers because speed in this case matters very little. So the point of this is just make sure that mod_python is in the ballpark with the rest and that there isn’t anything seriously wrong with it. And surprisingly, mod_python is actually pretty fast, fastest, even, though in its own category (a raw mod_python handler).

Test rig

The server is a 24-core Intel Xeon 3GHz with 64GB RAM, running Linux 2.6.32 (CentOS 6.3).

The testing was done with httpress, which was chosen after having tried ab, httperf and weighttp. The exact command was:

1
httpress -n 5000000 -c 120 -t 8 -k http://127.0.0.1/

Concurrency of 120 was chosen as the highest number I could run across all setups without getting strange errors. “Strange errors” could be disconnects, delays and stuck connections, all tunable by anything from Linux kernel configuration to specific tool configs. I very much wanted concurrency to be at least a few times higher but it quickly became apparent that getting to that level would require very significant system tweaking for which I just didn’t have the time. 120 concurrent requests is nothing to sneeze at though: if you sustained this rate for a day of python handler serving, you’d have processed 10,812,009,600 requests (on a single server!).

I should also note that in my tweaking of various configurations I couldn’t get the requests/s numbers any significantly higher than what you see above. Increasing concurrency and number of workers mostly increased errors rather than r/s, which is also interesting because it’s important how gracefuly each of these tools fails, but failure mode is a whole different subject.

The tests were done via the loopback (127.0.0.1) because having tried hitting the server from outside it became apparent that the network was the bottleneck.

Keepalives were in use (-k), which means that all of the 5 million requests are processed over only about fifty thousand TCP connections. Without keepalives this would be more of the Linux kernel test because the bulk of the work establishing and taking down a connection happens in the kernel.

Before running the 5 million requests I ran 100,000 as a “warm up”.

This post does not include the actual code for the WSGI app and mod_python handlers because it was same as in my last post on mod_python performance testing.

Why httpress

ab simply can’t run more than about 150K requests per second, so it couldn’t adequately test nxweb and nginx static file serving.

httperf looked promising at first, but as is noted here its requests per second cannot be trusted because it gradually increases the load.

weighttp seemed good, but somehow got stuck on idle but not yet closed connections which affected the request/s negatively.

httpress claimed that it “promptly timeouts stucked connections, forces all hanging connections to close after the main run, does not allow hanging or interrupted connections to affect the measurement”, which is just what I needed. And it worked really great too.

The choice of contenders

mod_python and mod_wsgi are the obvious choices, uWSGI/Nginx combo is known as a low-resource and fast alternative. I came across nxweb while looking at httpress (it’s written by the same person (Yaroslav Stavnichiy), it looks to be the fastest (open source) web server currently out there, faster than (closed source) G-WAN, even.

Specific tool notes

The code used for testing and the configs were essentially same as what I used in my previous post on mod_python performance testing. The key differences are listed below.

Apache

The key config on Apache was:

1
2
3
ThreadsPerChild 25    # default
StartServers 16
MinSpareThreads 400

MinSpareThreads ensures that Apache starts all possible processes and threads on startup (25 * 16 = 400) so that there is no ramp up period and it’s tsunami-ready right away.

uWSGI

The comparison with uWSGI isn’t entriely appropriate because it was running listening on a unix domain socket behind Nginx. The -p 16 –threads 1 (16 worker processes with a single thread each) was chosen as the best performing option after some experimentation. Upping -p to 32 reduced r/s to 86233, 64 to 47296. Upping –threads to 2 (with 16 workers) reduced r/s to 55925 (by half, which is weird - mod_python has no problems with 25 threads). –single-interpreter didn’t seem to have any significant impact.

The actual uWSGI command was:

1
uwsgi -s logs/uwsgi.sock --pp htdocs  -M -p 16 --threads 1 -w mp_wsgi -z 30 -l 120 -L

A note on the uWSGI performance. Initially it seemed to be outperforming the mod_python handler by nearly a factor of two. Then after all kinds of puzzled head-scratching, I decided to verify that every hit ran my Python code - I did this by writing a dot to a file and making sure that the file size matches the number of hits in the end. It turned out that about one third of the requests from Nginx to uWSGI were erroring out, but httpress didn’t see them as errors. So if you’re going to attempt to replicate this, watch out for this condition. EDIT: Thanks to uWSGI’s author Roberto De Loris’ help, it turned out that this was a result of misconfiguration on my part - the -l parameter should be set higher than 120. (This explains how I arrived at 120 as the concurrency chosen for the test too). The request/s number and uWSGI’s position in my table is still correct.

Nginx

The relevant parts of the nginx config were (Note: this is not the complete config for brevity):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
worker_processes 24;
...
events {
  worker_connections 1024;
}
...
http {
  server_tokens off;
  keepalive_timeout 65;
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;

  access_log /dev/null main;
...
  upstream uwsgi {
     ip_hash;
     server unix:logs/uwsgi.sock;
  }
...

Conclusion

Mod_python is plenty fast. Considering that unlike with other contenders large parts of the code are written in Python and thus are readable and debuggable by not just C programmers, it’s quite a feat.

I was surprised by Apache’s slow static file serving compared to Nginx and Nxweb (the latter, although still young and in development seems like a very cool web server).

On the other hand I am not all that convinced that the Nginx/uWSGI set up is as cool as it is touted everywhere. Unquestionably Nginx is a super solid server and Apache has some catching up to do when it comes to acting as a static file server or a reverse proxy. But when it comes to serving Python-generated content, my money would be on Apache rather than uWSGI. The “low” 120 concurrency level for this test was largely chosen because of uWSGI (Apache started going haywire on me at about 400+ concurrent connections). EDIT: Thanks to Roberto’s comment, this turned out to be an error on my part (see comments). uWSGI can handle higher concurrencies if -l is set higher.

It’s also interesting that on my laptop a mod_python handler outperformed the Apache static file, but it wasn’t the case on the big server.

I didn’t do Python 3 testing, it would be interesting to see how much difference it makes as well.

I realize this post may be missing key config data - I had to leave out a lot because of time contraints (and my lazyness) - so if you see any obvious gaps, please comment, I will try to address them.

P.S. Did I mention mod_python 3.5 supports Python 3? Please help me test it!

Separate Request and Response or a Single Request Object?

| Comments

Are you in favor of a single request object, or two separate objects: request and response? Could it be that the two options are not contradictory or even mutually exclusive?

I thouhgt I always was in favor of a single request object which I expressed on the Web-SIG mailing list thread dating back to October 2003 (ten years ago!). But it is only now that I’ve come to realize that both proponents of a single object and two separate objects were correct, they were just talking about different things.

The confusion lies in the distinction between what I am going to term a web application and a request handler.

A request handler exists in the realm of an HTTP server, which (naturally) serves HTTP requests. An HTTP request consists of a request (a method, such as “GET”, along with some HTTP headers and possibly a body) and a response (a status line, some HTTP headers and possibly a body) sent over the TCP connection. There is a one-to-one correspondence between a request and a response established by the commonality of the connection. An HTTP request is incomplete if the response is missing, and a response cannot exist without a request. (Yes, the term “request” is used to denote both the request and response, as well as just the request part of the request, and that’s confusing).

A web application is a layer on top of the HTTP request handler. A web application operates in requests and responses as well, but those should not be confused with the HTTP request/response pairs.

Making the conceptual distinction between a web application request and an HTTP request is difficult because both web applications and request handlers use HTTP headers and status to accomplish their objectives. The difference is that strictly speaking a web application does not have to use HTTP and ideally should be protocol-agnostic, though it is very common for a web application to rely on HTTP-specific features these days. Not every HTTP request exists as part of a web application. But because it is difficult to imagine a web application without HTTP, we tend to lump the two concepts together. It is also exacerbated by the fact that HTTP headers carry both application-specific and HTTP-specific information.

A good example of the delineation between a web application response and an HTTP response is handling of an error condition. A web application error is typically not an HTTP error. Imagine an “invalid login” page. It is a web application error, but not an HTTP error. An “invalid login” page should send a “200 OK” status line and a body explaining that the credentials supplied were not valid. But then HTTP provides its own authentication mechanism, and an HTTP “401 Unauthorized” (which ought not be used by web applications) is often misunderstood as something that web applications should incorporate into how they do things.

Another example of a place where the line gets blurry is a redirect. A redirect is quite common in a web application, and it is typically accomplished by way of an HTTP redirect (3XX status code), yet the two are not the same. An HTTP redirect, for example, may happen unbeknownst to the web application for purely infrastructural reasons, and a web application redirect does not always cause an HTTP redirect.

Yet another example: consider a website serving static content where same URI responds with different content according to the Accept-Language header in the request. Is this a “web application”? Hardly. Could you have some Python (or whatever you favorite language is) help along with this process? Certainly. Wouldn’t this code be part of a “web application”? Good question. It is not uncommon for a web application to consider the Accept-Language header in its response. You could also accomplish this entirely in an http server by configuring it correctly. Sometimes whether something is a web application just depends on how you’re looking at it, but you do have to decide for yourself which it is.

Getting to the original problem, the answer to the question of whether to use separate response/request objects or not depends very much on which realm you’re operating in. A request handler only needs one request object representing the HTTP request because it is conceptually similar to a file - you don’t typically open a file twice once for reading and once for writing. Whereas a web application, which may chose between different responses depending on what’s in the request is possibly best served with two separate objects.

I think that misunderstanding of what a “web application” is happens to be the cause of a lot of bad decisions plaguing the world of web development these days. It is not uncommon to see people get stuck on low-level HTTP subtleties while referring to web application issues and vise-versa. We’d all get along better if we took some time to think about the distinction between web applications and HTTP request handlers.

P.S. This will get even more complicated when HTTP 2.0 comes around where responses may exist without a request. And I haven’t even mentioned SSL/TLS.

My Thoughts on WSGI

| Comments

I’m not very fond of it. Here is why.

CGI Origins

WSGI is based on CGI, as the “GI” (Gateway Interface) suggests right there in the name.

CGI solved a very important problem using the very limited tools at hand available at the time. Though CGI wasn’t a standard, it was ubiquitous in the early days of the WWW, despite its inherent slowness and other limitations. It became popular because it worked with any language, was easy to turn on and provided such a thick wall of isolation that admins could turn it on for their users without too much concern for problems caused by user-generated CGI scripts.

There is now an RFC (RFC3875) describing CGI, but I hazard that Ken Coar wrote the RFC not because he thought CGI was great, but rather out of discontent with the present state of affairs - everyone was using CGI, yet there never was a formal document describing it.

So if I were to attempt to unite all Python web applications under the same standard, CGI wouldn’t be the first thing I would consider. There are other efforts at solving the same problem in more elegant ways which could be used as a model, e.g. (dare I mention?) Java Servlets.

Headers

CGI dictated that HTTP headers be passed to the CGI script by way of environment variables. The same environment that contain your $PATH and $TERM. (Note this also explains the origin of the term environment in WSGI - in HTTP there is no request environment, there is simply a request). So as to not clash with any other environment variables, CGI would prepend HTTP_ to every header name. It also swapped dashes with underscores because dashes are not allowed in shell variable names. And because environment variables in DOS and Unix are typically case-insensitive, they were capitalized. Thus "content-type" would become "HTTP_CONTENT_TYPE".

And how much sense applying the same transformation make in the realm in which WSGI operates? The headers are typically read by the webserver and stored in some kind of a structure, which ought to be directly accessible so the application can get headers in the original, unmodified format. For example in Apache this would be the req.headers_in table. What is the benefit of combing through that structure converting every key to some capitalized HTTP_ string at every request? Why are WSGI developers forced to use env['HTTP_CONTENT_LENGTH'] rather than env['Content-length']?

Another thing about the environment is that the WSGI standard states that it must be a real Python dictionary, thereby dictating that a memory allocation happen to satisfy this requirement, at every request.

start_response()

In order to be able to write anything to the client a WSGI application must envoke the start_response() function passed to it which would return a write() method.

Ten points for cuteness here, but the practicality of this solution eludes me. This is certainly a clever way to make the fact that the start of a response is an irreversible action in HTTP because the headers are sent first, but seriosly - do programmers who code at this level not know it? Why can’t the header sending part happen implicitly at the first write(), and why can’t an application write without sending any headers?

There is also another problem here - function calls are relatively expensive in Python. The requirement that the app must beg for the write object every time introduces a completely unnecessary function call.

The request object with a write() method should simply be passed in. This is how it has always worked in mod_python (cited in PEP3333 a number of times!).

Error handling

First, I must confess that after re-reading the section of the PEP3333 describing the exc_info argument several times I still can’t say I grok what it’s saying. Looking at some implementations out there I am releived to know I am not the only one.

But the gist of it that an exception can be supplied along with some headers. It seems to me there is confusion between HTTP errors and Python errors here, the two are not related. What is the expected outcome of passing a Python exception to an HTTP server? The server would probably convert it to a 500 Internal Server Error (well it only has so many possibilities to chose from), and what’s the point of that?

Wouldn’t the outcome be same if the application simply raised an exception?

If the spec wanted to provide means for the application Python errors to somehow map to HTTP errors, why not define a special exception class which could be used to send HTTP errors? What was wrong with mod_python’s:

1
raise apache.SERVER_RETURN, apache.HTTP_INTERNAL_SERVER_ERROR

I think it’s simple and self-explanatory.

Other things

What is wsgi.run_once, why does it matter and why should the web server provide it? What would be a good use case for such a thing?

There is a long section describing “middleware”. Middleware is a wrapper, an example of the pipeline design pattern and there doesn’t seem to be anything special with this concept that the WSGI spec should even mention it. (I also object to the term “middleware” - my intuition suggests it’s a layer between “hardware” and “software”, not a wrapper.)

SCRIPT_NAME and PATH_INFO

Perhaps the most annoying part of CGI were these two mis-understood variables, and sadly WSGI uses them too.

Remember that in CGI we always had a script. A typical CGI script resided somewhere on the filesystem to which the request URI maps. As part of serving the request the server traversed the URI mapping each element to an element of the filesystem path to locate the script. Once the script was found, the portion of the URI used thus far was assigned to the SCRIPT_NAME variable, while the remainder of the URI got assigned to PATH_INFO.

But where is the script in WSGI? Is my Python module the script? What relatioship does there exist between the request URI and the (non-existent) script?

Bottom line

I am not convinced that there should be a universal standard for Python web applications to begin with. I think that what we refer to as “web applications” is still not very well understood by us programmers.

But if we are to have one, I think that WSGI approach is not the right one. It brings the world of Python web development to the lowest common denominator - CGI and introduces some problems of its own on top of it.

Other notes

What is the Gateway in CGI

I did some digging into the etymology of “Common Gateway Interface”, because I wanted to know what the original author (Rob McCool) meant by it when he came up with it. From reading this it’s apparent that he saw it as the Web daemon’s gateway to an outside program:

“For example, let’s say that you wanted to “hook up” your Unix database to the World Wide Web, to allow people from all over the world to query it. Basically, you need to create a CGI program that the Web daemon will execute to transmit information to the database engine, and receive the results back again and display them to the client. This is an example of a gateway, and this is where CGI, currently version 1.1, got its origins.”

I always perceived it the other way around, I thought the “gateway” was a gateway to the web server. I think that when Phillip J. Eby first proposed the name WSGI he was under the same misperception as I.

Mod_python: The Long Story

| Comments

This story started back in 1996. I was in my early twenties, working as a programmer at a small company specializing in on-line reporting of certain pharmaceutical data.

There was a web-based application (which was extremely cool considering how long ago this was), but unfortunately it was written in Visual Basic by a contractor and I was determined to do something about it. As was very fashionable at the time, I was very pro Open Source, had Linux running on my home 386 and had recently heard Guido’s talk at the DC Linux user group presenting his new language he called Python. Python seemed like a perfect alternative to the VB monstrosity.

I spent a few weeks quietly in my cubicle learning Python and rewriting the whole app in it. (Back in those days this is how programmers worked, there was no “agile” and “daily stand ups”, everyone understood that things take time. I miss those days very much). Python was fantastic, and soon the app was completely re-written.

Then I realized that explaining what I’ve been working on to my bosses might be a bit of a challenge. You see, for a while there nobody knew that the web app they’ve been using had been re-written in Python, but sooner or later I would have to reveal the truth and, more importantly, justify my decision. I needed a good reason, and stuff about object-oriented programming, clean code, open source, etc would have fallen on deaf ears.

Just around that time the Internet Programming with Python book came out, and in it there was a chapter on how to embed the Python interpreter in the Netscape Enterprise web server. The idea seemed very intriguing to me and it might have contained exactly the justification I was looking for - it would make the app faster. (“Faster” is nearly as good as “cheaper” when it comes to selling to the management). I can’t say that I knew much C back then, but with enough tinkering around I was able to make something work, and lo and behold it was quite noticeably faster.

And so a few days later I held a presentation in the big conference room regarding this new tool we’ve started using called Python which can crunch yall’s numbers an order of magnitude faster than the Microsoft product we’ve been using. And oh, by the way, I quickly hacked something together last night - let’s do a live demo, look how fast this is! They were delighted.

Little did they know, the app had been running in Python for months, and the reason for the speed up had little to do with the language itself. It was all because I was able to embed the interpreter within the web server. Then I thought that to make it all complete I would make my little tool open source and put it on my website free for everyone to use. I called it NSAPy as a combination of the Netscape Server API and Python.

But I didn’t stop there, and soon I was able to replicate this on an Apache web server, which was taking the Internet by storm back then. The name mod_python came naturally since there already was a mod_perl.

Things were going very well back then. These were the late nineties, the dawn of e-commerce on the World Wide Web. I started working for a tiny ISP which soon transformed into a humongous Web Hosting company, we ran millions of sites, built new data centers with thousands of servers pushing gigabits of traffic and (in short) were taking over the world (or so it seemed). With the rise of our company’s stock price, me and my colleagues were on our way to becoming millionaires. Mod_python was doing very well too. It had a busy website, a large and very active mailing list and an ever growing number of devoted users. I went to various Open Source conferences to present about it (and couldn’t really believe that without exception everyone knew what mod_python was).

Then came 2001. We just bought a house and our second son was not even a year old when one beautiful sunny summer day I was summoned to a mandatory meeting. In that meeting about two thirds of our office was let go. Even though we all felt it was coming, it was still a shock. I remember coming home that morning and having to explain my wife that I’d just been fired. This after constant all-nighters, neglect for family life under the excuse of having the most important job doing the most important thing and changing the world and rants about how we’d be all set financially in just a year or two. In my personally opinion the 2007 financial crash was nothing compared to the dot-com bust. Everyone was getting laid off everywhere, the Internet became a dirty word, software development was being outsourced to India.

For the next couple of years I made a living doing contracting work here and there. Needless to say, mod_python wasn’t exactly at the top of my priority list. But it was getting ever more popular, the mailing list busier, though it didn’t make any money (for me at least). I tried my best to keep everything running in whatever spare time I had, answering emails and releasing new versions periodically. Finding time for it was increasingly difficult, especially given that most of the work I was doing had nothing to do with mod_python or Python.

One day I had this thought that donating mod_python to the Apache Software Foundation would ensure its survival, even if I can no longer contribute. And so it was done. Initially things went very well - the donation did affiliate mod_python with the solid reputation of Apache and that was great. Mod_python gained a multitude more users and most importantly contributors.

At the same time my life was becoming ever more stressful. Free time for mod_python hacking was getting more and more scarce until there was none. I also think I was experiencing burnout. Answering questions on the mailing list became an annoyance. I had to read through enormous threads with proposals for various features or how things ought to work and respond to them, and it was just never ending. It wasn’t fun anymore.

I also felt that people didn’t understand what mod_python was and that I’m not able to explain it very well. (For what it’s worth, I still feel this way). In my mind it was primarily an interface to the Apache internals, but since making every structure and API accessible from within Python was impractical, only selected pieces were exposed. Secondly, mod_python provided means to perform certain things that were best done in Apache, e.g. global locking, caching. Lastly, it provided certain common tasks but implemented in Apache-specific ways (using Apache pools, APR, etc.) for maximal performance; things like cookies and sessions fell into that category. Publisher and PSP didn’t strictly belong in mod_python, but were there for the sake of battery-includedness - you could build a rudimentary app without any additional tool.

The rest of the world saw it as a web-development framework. It wasn’t a particularly good one, especially when it came to development, because it required root privileges to run. It also didn’t do a very good job at reloading changed modules very well which complicated development. A very considerable effort was put in by one of the contributors to address the particular issue of module loading and caching, and I never thought it to be important because to me restarting Apache seemed like the answer, I didn’t think that people without root access would ever use mod_python.

As I was growing more disinterested in mod_python it got to a point where I just let it be. I would skim through emails from people I trusted and responded affirmatively to whatever they proposed without giving it much thought. I didn’t see any point in keeping and defending my vision for mod_python. I think that by about 2006 or so I was so disconnected I no longer had a good grasp of what the latest features of mod_python were being worked on. Not sure if it was my lack of interest or that other contributors felt burned out as well, but new commits slowed down to a trickle and stopped eventually, and my quarterly reports to the ASF Board became a cut-and-paste email of “no new activity”.

This is where the negative aspect of the ASF patronage begun to surface. Sadly, the ASF rules are that projects and their community must be active, and soon the project got moved to the attic. And even though I kept telling myself that I couldn’t care less, I must admit it hurt. The attic is a like a one-way trash can - once there, a project cannot go back, other than through the incubation process.

Fast forward to 2013. Why get back to hacking on it? First of all I got tired of “mod_python is dead” plastered all over the web. Every time I see some kid who wasn’t old enough to speak back when I first released it tweet that it is this or that, I can’t help but take it a little personally. It’s an open source project people, it’s only dead if you do not contribute to it.

For the skeptics in the crowd I most certainly disagree that mod_python as a concept is dead, I’d even argue that its time hasn’t come yet. The vision has not changed. Mod_python is still an interface to Apache which lets you take advantage of its versatile architecture to do some very powerful things. It’s not quite a web development framework, and it’s not even a tool for running your favorite web development framework in production (though it can certainly do that quite nicely).

These days there is more demand than ever for high volume servers that do not have a user interface and thus do not need a WSGI framework to power them - I think this is one of the areas where mod_python could be most useful. There are also all kinds of possibilities for using Apache and mod_python for distributed computing and big data stuff taking advantage of the fact that Apache is an excellent job supervisor - anyone up for writing a map/reduce framework in mod_python?

I must also note that hacking on it in the past weeks has been fun once again. I wanted to get up to speed with the latest on Python 3 and Apache internals, especially the event/epoll stuff and this has been a great way to do just that. I also very much enjoy that I can once again do whatever I want without any scrutiny.

If there is one thing I’ve learned it’s that few open source projects can exist without their founders’ continuous involvement. The Little Prince once said - “You become responsible forever for what you have tamed”. It seems like mod_python is my rose and if I don’t water it, no one will.

P.S. Did I mention mod_python now supports Python 3? Please help me test it!

Mod_python Performance and Why It Matters Not.

| Comments

TL;DR: mod_python is faster than you think.

Tonight I thought I’d spend some time looking into how the new mod_python fares against other frameworks of similar purpose. In this article I am going to show the results of my findings, and then I will explain why it really does not matter.

I am particularly interested in the following:

  • a pure mod_python handler, because this is as fast as mod_python gets.
  • a mod_python wsgi app, because WSGI is so popular these days.
  • mod_wsgi, because it too runs under Apache and is written entirely in C.
  • uWSGI, because it claims to be super fast.
  • Apache serving a static file (as a point of reference).

The Test

I am testing this on a CentOS instance running inside VirtualBox on an early 2011 MacBook Pro. The VirtualBox has 2 CPU’s and 6GB of RAM allocated to it. Granted this configuration can’t possibly be very performant [if there is such a word], but it should be enough to compare.

Real-life performance is very much affected by issues related to concurrency and load. I don’t have the resources or tools to comprehensively test such scenarios, and so I’m just using concurrency of 1 and seeing how fast each of the afore-listed set ups can process small requests.

I’m using mod_python 3.4.1 (pre-release), revision 35f35dc, compiled against Apache 2.4.4 and Python 2.7.5. Version of mod_wsgi is 3.4, for uWSGI I use 1.9.17.1.

The Apache configuration is pretty minimal (It could probably trimmed even more, but this is good enough):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LoadModule unixd_module /home/grisha/mp_test/modules/mod_unixd.so
LoadModule authn_core_module /home/grisha/mp_test/modules/mod_authn_core.so
LoadModule authz_core_module /home/grisha/mp_test/modules/mod_authz_core.so
LoadModule authn_file_module /home/grisha/mp_test/modules/mod_authn_file.so
LoadModule authz_user_module /home/grisha/mp_test/modules/mod_authz_user.so
LoadModule auth_basic_module /home/grisha/mp_test/modules/mod_auth_basic.so
LoadModule python_module /home/grisha/src/mod_python/src/mod_python/src/mod_python.so

ServerRoot /home/grisha/mp_test
PidFile logs/httpd.pid
ServerName 127.0.0.1
Listen 8888
MaxRequestsPerChild 1000000

<Location />
      SetHandler mod_python
      PythonHandler mp
      PythonPath "sys.path+['/home/grisha/mp_test/htdocs']"
</Location>

I should note that <Location /> is there for a purpose - the latest mod_python forgoes the map_to_storage phase when inside a <Location> section, so this makes it a little bit faster.

And the mp.py file referred to by the PythonHandler in the config above looks like this:

1
2
3
4
5
6
7
8
from mod_python import apache

def handler(req):

    req.content_type = 'text/plain'
    req.write('Hello World!')

    return apache.OK

As the benchmark tool, I’m using the good old ab, as follows:

1
$ ab -n 10  http://localhost:8888/

For each test in this article I run 500 requests first as a “warm up”, then another 500K for the actual measurement.

For the mod_python WSGI handler test I use the following config (relevant section):

1
2
3
4
5
<Location />
    PythonHandler mod_python.wsgi
    PythonPath "sys.path+['/home/grisha/mp_test/htdocs']"
    PythonOption mod_python.wsgi.application mp_wsgi
</Location>

And the mp_wsgi.py file looks like this:

1
2
3
4
5
6
7
8
9
def application(environ, start_response):
    status = '200 OK'
    output = 'Hello World!'

    response_headers = [('Content-type', 'text/plain'),
                        ('Content-Length', str(len(output)))]
    start_response(status, response_headers)

    return [output]

For mod_wsgi test I use the exact same file, and the config as follows:

1
2
LoadModule wsgi_module /home/grisha/mp_test/modules/mod_wsgi.so
WSGIScriptAlias / /home/grisha/mp_test/htdocs/mp_wsgi.py

For uWSGI (I am not an expert), I first used the following command:

1
2
3
/home/grisha/src/mp_test/bin/uwsgi \
   --http 0.0.0.0:8888 \
   -M -p 1 -w mysite.wsgi -z 30 -l 120 -L

Which yielded a pretty dismal result, so I tried using a unix socket -s /home/grisha/mp_test/uwsgi.sock and ngnix as the front end as described here, which did make uWSGI come out on top (even if proxied uWSGI is an orange among the apples).

The results, requests per second, fastest at the top:

1
2
3
4
5
6
| uWSGI/nginx         | 2391 |
| mod_python handler  | 2332 |
| static file         | 2312 |
| mod_wsgi            | 2143 |
| mod_python wsgi     | 1937 |
| uWSGI --http        | 1779 |

What’s interesting and unexpected at first is that uWSGI and the mod_python handler perform better than sending a static file, which I expected to be the fastest. On a second thought though it does make sense, once you consider that no (on average pretty expensive) filesystem operations are performed to serve the request.

Mod_wsgi performs better than the mod_python WSGI handler, and that is expected, because the mod_python version is mostly Python, vs mod_wsgi’s C version.

I think that with a little work mod_python wsgi handler could perform on par with uWSGI, though I’m not sure the effort would be worth it. Because as we all know, premature optimization is the root of all evil.

Why It Doesn’t Really Matter

Having seen the above you may be tempted to jump on the uWSGI wagon, because after all, what matters more than speed?

But let’s imagine a more real world scenario, because it’s not likely that all your application does is send "Hello World!".

To illustrate the point a little better I created a very simple Django app, which too sends "Hello World!", only it does it using a template:

1
2
3
4
def hello(request):
    t = get_template("hello.txt")
    c = Context({'name':'World'})
    return HttpResponse(t.render(c))

Using the mod_python wsgi handler (the slowest), we can process 455 req/s, using uWSGI (the fastest) 474. This means that by moving this “application” from mod_pyhton to uWSGI we would improve performance by a measley 5%.

Now let’s add some database action to our so-called “application”. For every request I’m going to pull my user record from the Django auth_users table:

1
2
3
4
5
6
7
from django.contrib.auth.models import User

def hello(request):
    grisha = User.objects.get(username='grisha')
    t = get_template("hello.txt")
    c = Context({'name':str(grisha)[0:5]}) # world was 5 characters
    return HttpResponse(t.render(c))

Now we are down to 237 req/s for the mod_python WSGI handler and 245 req/s in uWSGI, and the difference between the two has shrunk to just over 3%.

Mind you, our “application” still has less than 10 lines of code. In a real-world situation the difference in performance is more likely to amount to less than a tenth of a percent.

Bottom line: it’s foolish to pick your web server based on speed alone. Factors such as your comfort level with using it, features, documentation, security, etc., are far more important than how fast it can crank out “Hello world!”.

Last, but not least, mod_python 3.4.1 (used in this article) is ready for pre-release testing, please help me test it!