Skip to content

Commit a563193

Browse files
committed
Finish up analysis and edits for clarity.
1 parent 8dc758c commit a563193

File tree

1 file changed

+44
-21
lines changed

1 file changed

+44
-21
lines changed

nginx-log-analysis.ipynb

+44-21
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
"source": [
2222
"By Jess Johnson [http://grokcode.com](http://grokcode.com)\n",
2323
"\n",
24-
"This notebook goes through an analysis of Nginx access logs in order to do capacity planning. I'm looking at access logs for [Author Alcove](http://authoralcove.com) which was hit by a big traffic spike when it spent around 24 hours at the top of [/r/books](http://reddit.com/r/books/). The site was hugged to death by reddit and many visitors couldn't access the site due to 50x errors. So let's estimate how much extra capacity would be needed to survive this spike.\n",
24+
"This notebook analyzes Nginx access logs in order to estimate the capacity needed to survive a traffic spike. I'm looking at access logs for [Author Alcove](http://authoralcove.com) which was hit by a big traffic spike when it spent around 24 hours at the top of [/r/books](http://reddit.com/r/books/). The site was hugged to death by reddit. Visitors experienced very slow load times, and many people couldn't access the site at all due to 50x errors. So let's estimate how much extra capacity would be needed to survive this spike.\n",
2525
"\n",
2626
"The source for this notebook is located on [github](https://github.com/grokcode/ipython-notebooks). See a mistake? Pull requests welcome.\n",
2727
"\n",
28-
"Thanks to Nikolay Koldunov for his [notebook on Apache log analysis](http://nbviewer.ipython.org/github/koldunovn/nk_public_notebooks/blob/master/Apache_log.ipynb), and thanks to my bro Aaron for the much needed optimism and server optimization tips while everything was on fire."
28+
"Thanks to Nikolay Koldunov for his [notebook on Apache log analysis](http://nbviewer.ipython.org/github/koldunovn/nk_public_notebooks/blob/master/Apache_log.ipynb), and thanks to my bro Aaron for the much needed optimism and server optimization tips while everything was on fire.\n",
29+
"\n",
30+
"OK let's get started."
2931
]
3032
},
3133
{
@@ -80,7 +82,7 @@
8082
"cell_type": "markdown",
8183
"metadata": {},
8284
"source": [
83-
"We will also use [apachelog](https://code.google.com/p/apachelog/), which is a module for parsing apache logs, but it works fine with nginx logs if we give it the right format string. You can install it with `pip install apachelog`. "
85+
"We will also use [apachelog](https://code.google.com/p/apachelog/), which is a module for parsing apache logs, but it works fine with nginx logs as long as we give it the right format string. You can install it with `pip install apachelog`. "
8486
]
8587
},
8688
{
@@ -105,7 +107,7 @@
105107
"cell_type": "markdown",
106108
"metadata": {},
107109
"source": [
108-
"I started out by doing some command line preproccessing on the log in order to remove bots. I used `egrep -v` to filter out the bots that were hitting the site the most often. These were Googlebot, Bingbot, the New Relic uptime checker, Buidu spider, and a few others. A more careful approach would filter out everything on one of the known bot lists ([like this one](http://www.robotstxt.org/db.html)), but I'm going to play it a bit fast and loose.\n",
110+
"I started out by doing some command line preprocessing on the log in order to remove bots. I used `egrep -v` to filter out the bots that were hitting the site the most often. These were Googlebot, Bingbot, the New Relic uptime checker, Buidu spider, and a few others. A more careful approach would filter out everything on one of the known bot lists ([like this one](http://www.robotstxt.org/db.html)), but I'm going to play it a bit fast and loose.\n",
109111
"\n",
110112
"First of all let's get a sample line out of the `access.log` and try to parse it. Here is a description of the codes in the log format we are working with:\n",
111113
"\n",
@@ -468,6 +470,15 @@
468470
"Analysis"
469471
]
470472
},
473+
{
474+
"cell_type": "markdown",
475+
"metadata": {},
476+
"source": [
477+
"Let's graph the data to visualize what is happening.\n",
478+
"\n",
479+
"First let's increase the graph size."
480+
]
481+
},
471482
{
472483
"cell_type": "code",
473484
"collapsed": false,
@@ -480,6 +491,13 @@
480491
"outputs": [],
481492
"prompt_number": 11
482493
},
494+
{
495+
"cell_type": "markdown",
496+
"metadata": {},
497+
"source": [
498+
"Now let's graph the requests hitting the web server. `10t` will use a 10 minute interval size, so each point on the graph shows the number of requestes in a 10 minute window."
499+
]
500+
},
483501
{
484502
"cell_type": "code",
485503
"collapsed": false,
@@ -513,7 +531,7 @@
513531
"cell_type": "markdown",
514532
"metadata": {},
515533
"source": [
516-
"Above we see that we were peaking at around 4500 requests every 10 minutes, or about 450 requests a minute. There was a quick ramp up as the site climbed to the top position on /r/books, then a drop off overnight (for US timezones), another small peak the next day as people woke up, and then a decline as the site fell back down the /r/books page.\n",
534+
"Above we see that we were peaking at around 4500 requests every 10 minutes, or about 450 requests a minute. There was a quick ramp up as the site climbed to the top position on /r/books, then a drop off overnight (for US timezones), another peak the next day as people woke up, and then a decline as the site fell back down the /r/books page.\n",
517535
"\n",
518536
"Let's see how the server held up by looking at some response codes."
519537
]
@@ -596,9 +614,9 @@
596614
"cell_type": "markdown",
597615
"metadata": {},
598616
"source": [
599-
"OK that doesn't look too bad. The vast majority of requests were served a 200 response.\n",
617+
"OK that doesn't look too bad. The vast majority of requests were served a 200 (OK) response.\n",
600618
"\n",
601-
"Let's graph the response codes over time, with a sample timespan of 1 hour."
619+
"Let's graph the most common response codes over time, with a sample timespan of 1 hour."
602620
]
603621
},
604622
{
@@ -788,7 +806,7 @@
788806
"cell_type": "markdown",
789807
"metadata": {},
790808
"source": [
791-
"This doesn't look too bad either, but it's hard to make sense of what is going on with so many different response codes. Let's group them into success (webserver handled it as expected) and failure (webserver error or client closed request).\n",
809+
"This doesn't look too bad either, but it's hard to make sense of what is going on with so many different response codes. Let's group them into success (web server handled it as expected) and failure (web server error or client closed request).\n",
792810
"\n",
793811
" 200 413236 Success OK \n",
794812
" 502 39769 Failure Bad Gateway\n",
@@ -1051,7 +1069,15 @@
10511069
"cell_type": "markdown",
10521070
"metadata": {},
10531071
"source": [
1054-
"It looks like about 2,000 requests / hour is all uwsgi can handle without starting to throw errors. That is only about 1 request every two seconds so that is a pretty big problem."
1072+
"It looks like about 2,000 requests / hour is all uwsgi can handle before it starts to throw errors. That is only about 1 request every two seconds so that is a pretty big problem.\n",
1073+
"\n",
1074+
"We needed to handle 10,000 requests / hour, but we were only able to handle 2,000 requests / hour. So it looks like we would need roughly 5x the capacity to handle a spike of this size. This is a little bit misleading though because many visitors abandoned the site after hitting 50X errors and long page loads.\n",
1075+
"\n",
1076+
"Here is a view of the same traffic spike from Google Analytics.\n",
1077+
"\n",
1078+
"![Google Analytics Pageviews vs. Pages per Visit](files/data/google-analytics.png)\n",
1079+
"\n",
1080+
"During the spike, pages per visit were roughly half of what they normally were, so let's double the 5x estimate and plan for 10x the capacity to handle this load."
10551081
]
10561082
},
10571083
{
@@ -1066,15 +1092,11 @@
10661092
"cell_type": "markdown",
10671093
"metadata": {},
10681094
"source": [
1069-
"The easy solution is to upgrade the server running uwsgi and increase the number of workers handling requests. At first glance it looks like we would need roughly 5x the capacity to handle a spike of this size. This is a little bit misleading though because many visitors abandoned the site after hitting 50X errors and long page loads.\n",
1070-
"\n",
1071-
"Here is a view of the same traffic spike from Google Analytics.\n",
1095+
"One solution is to upgrade the server running uwsgi and use ten times the number of workers to handle requests, but let's see if we can be a bit smarter.\n",
10721096
"\n",
1073-
"![Google Analytics Pageviews vs. Pages per Visit](files/data/google-analytics.png)\n",
1074-
"\n",
1075-
"Pages per visit are roughly half of what they normally are during the spike, so let's double the 5x estimate and plan for 10x the capacity to handle this load.\n",
1097+
"Another solution is to optimize the web app by shortening long-running requests, but I have already picked most of the low hanging fruit there. \n",
10761098
"\n",
1077-
"There are a couple of choices for optimization here. The most obvious way to handle it is to just beef up the server running the app and increase the number of uwsgi workers, but let's see if we can be a bit smarter. There are also optimizations to be made to the web app to shorten long running requests, but I have already picked most of the low hanging fruit there. Let's investigate to see if there are any opportunities to prevent requests from hitting uwsgi in the first place.\n",
1099+
"Let's investigate to see if there are any opportunities to prevent requests from hitting uwsgi in the first place.\n",
10781100
"\n",
10791101
"First let's get a count pages with the most requests.\n"
10801102
]
@@ -1143,6 +1165,8 @@
11431165
"cell_type": "markdown",
11441166
"metadata": {},
11451167
"source": [
1168+
"There isn't much that can be done with the POST requests - they need to hit uwsgi so we can save new accounts or ratings in the database. So let's move on to the GET requests. There are two basic techniques for having nginx serve these pages. First, we could have uwsgi store the fully rendered page in memcached or similar, then have nginx try to pull the page from memcached and falling back to uwsgi if the page wasn't in the cache. The second idea is to have uwsgi create a static file, and then let nginx serve that if it exists. Unfortunately, in this case both of those solutions are problematic. It is beyond the scope of this notebook to go into details (hopefully I will have a separate blog post on that soon), but the gist is that for most of these pages, the content changes depending on who is viewing them, so they can't readily be cached at the page level.\n",
1169+
"\n",
11461170
"The biggest gain would be to make the homepage static. The homepage will redirect to a user's recommended page if they are already logged in, but we could possibly detect logged in users with nginx via the http headers and only serve the static page to logged out visitors. Let's see what proportion of visitors who hit the homepage were logged in already."
11471171
]
11481172
},
@@ -1177,10 +1201,9 @@
11771201
"cell_type": "markdown",
11781202
"metadata": {},
11791203
"source": [
1180-
"About (19331 / (3053 + 946 + 19331)) * 100 = 83% of visitors were logged out. So that nginx configuration change would have saved a bit more than 40,000 requests.\n",
1204+
"Of the visitors who were able to access the homepage, (19331 / (3053 + 946 + 19331)) * 100 = 83% were logged out. So that nginx configuration change would have saved a bit more than 40,000 requests.\n",
11811205
"\n",
1182-
"Out of how many total requests?\n",
1183-
"\n"
1206+
"Out of how many total?"
11841207
]
11851208
},
11861209
{
@@ -1210,7 +1233,7 @@
12101233
"cell_type": "markdown",
12111234
"metadata": {},
12121235
"source": [
1213-
"So that optimization would prevent 13% of requests from even reaching uwsgi.\n",
1236+
"So that change alone would offload 13% of uwsgi's request load to nginx. That's a good start.\n",
12141237
"\n",
12151238
"Another thing that jumps out is that `/apple-touch-icon.png` and `/apple-touch-icon-precomposed.png` don't actually exist have to be passed off to uwsgi before they 404. Setting up nginx to serve anything ending in `.png` will save some requests. Let's see if there are any other files like that."
12161239
]
@@ -1281,7 +1304,7 @@
12811304
"source": [
12821305
"That is another 3.5% of requests.\n",
12831306
"\n",
1284-
"TODO: Explain a bit about the problems with caching other files."
1307+
"Between those two changes we will save 16.5% of requests from hitting uwsgi. It isn't enough to prevent the need for a server upgrade, but it does get us a bit closer."
12851308
]
12861309
}
12871310
],

0 commit comments

Comments
 (0)