Avoiding load average spike alerts on Munin monitoring

Munin is a server monitoring tool written in Perl. In this post I’ll introduce some monitoring basics and how to avoid unnecessary monitoring alerts on temporary server conditions.

Munin has vibrant plugin community. You can easily write your own plugins, even using shell script. Plugins are autodiscovered, so dropping a script file on the server is enough to create your own monitoring graph. Munin master data collection is driven by cron and by default outputs static HTML files.  It is very easy to setup and secure, being immune to web attacks e.g. what most legacy PHP systems suffer.

Munin has its downsides, too. Like with most open source projects, Munin documentation is rough and not very helpful. The configuration file has its own format and discovering potential variables, values and opportunities is cumbersome. Munin scaling might not be ideal for large operations, both computer-wise and management-wise, but it’s ok up to 10-20 servers. Also Munin alert mechanism is quite naive, and you cannot have alert trigger math functions like e.g. with Zabbix.

This brings to the problem: because Munin lacks any kind of math it can perform on monitoring data, it alerts immediately when some value is out of bounds. This may result to unnecessary alerts on temporary conditions which really don’t need system administrator action.

1. Monitoring your server CPU resources

Screen Shot 2014-09-24 at 14.37.00

Server CPU resource sufficiency is best monitored with load average. When the load average gets too high, your server tasks are delayed and your response times start to suffer. However there might be various causes for temporary load average spikes, which resolve itself and should not be subject for alerting your devops team.

  • Brute force attacks before security starts mitigating them (short period before IPs are blocked)
  • Some periodical task takes more resources than usually
  • Network connection issues or such cause temporary thundering herd problem

Most of these issues are resolved automatically, within seconds or minutes when they begin. However, because Munin naively monitors only the latest load value, it cannot have logic to determine whether the load alert is genuine or temporary. In Zabbix this problem can be avoided by monitoring the minimum load value over a time period.

2. Smoothing monitored value over a time period

Because Munin does not support trigger functions, one must calculate the load average smoothing on the server-side. Below is a sample Python script which will monitoring the minimum load average over four Munin report cycles (20 minutes). Thus, it will alert you only if the load has stayed too high over 20 minutes.

There exists a Python framework to build Munin plugins. But because the use case is so simple, the script can be self-contained.

#!/usr/bin/python
#
# Munin plugin for getting the minimum sytem load avg. of 20 minutes.
#
# We do not want to alert devops if the high load situation
# resolves itself in few minutes.
#
# Compared to load avg (what is usually monitored by default),
# load minimum smoothes out load spikes caused by
#
# - Brute force attacks before security starts mitigating them
#
# - Timed jobs
#
# - Lost of network connection
#
# All these are visible in max load of time period, avg. load of time period,
# but do not affect min load (the base load level).
#
# Because Munin does not support trigger filtering functions, like e.g. Zabbix,
# we do the minimum load filtering on the server side by taking samples and
# then calculating out the minimum.
#
# Installation:
#
#   Put or symlink this script to /etc/munin/plugins/minload
#   chmod a+rx /etc/munin/plugins/minload
#   service munin-node restart
#
# Testing:
#
#   sudo -u munin munin-run minload
#
#

from __future__ import print_function

import os
import json
import io

__author__ = "Mikko Ohtamaa <mikko@opensourcehacker.com>"
__license__ = "MIT"

# This is the file which tracks the load data
PERSISTENT_STORAGE = "/tmp/munin-minload.json"

# Read or construct data persitent data
try:
    data = json.load(io.open(PERSISTENT_STORAGE, "rb"))
except:
    data = []

# munin-cron calls the server every 5 minutes
# well keep the last 4 entries, take min,
# so we get minimum for 20 minutes

data.append(os.getloadavg()[0])
data = data[-4:]
min_load = min(data)

with io.open(PERSISTENT_STORAGE, "wb") as f:
    json.dump(data, f)

print("""graph_title Load min 20 minutes
graph_vlabel load
graph_category system
rate.label rate
rate.value {}
rate.warning 20
rate.critical 40
graph_info The minimum load of the system for last 20 minutes""".format(min_load))

 

 

 

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

Linux server ghetto duplication

This blog post is about how to duplicate your Linux services and configuration from one server to another. We use simple and hackable SSH, rsync and shell scripting to copy the necessary files to make a cold spare from your existing server installation.

Screen Shot 2014-03-26 at 00.17.09

1. Preface

The method describes here is quite crude and most suitable for making a spare installation of your existing server. In the case you lose your production server, you can boot your cold spare, point your (fail-over) IP address to the spare server and keep business going – something you might want to do if you run mom’n’pop web hosting business. Because of the simplicity of the method it works on default Linux installations, bare metal servers and old-fashioned file systems like Ext4.

The instructions here have been tested with Ubuntu Linux 12.04, but should work with other versions with minor modifications. I have used this method successfully  with Heztner hosting (highly recommended for all the cheapskates out there) by having one production machine and one spare machine. The spare is mirrored weekly. Daily Duplicity backups can be restored on the spare if week is too long data loss period. Though in my case the servers are bare metal, the method works for VPSes as well.

The duplication script might be also useful for setting up a staging server from your production server for software developer and testing.

2. More powerful duplication tools

More fail safe, more engineer-oriented duplication approaches exist, but usually require additional preparation and tuning on the top of the default Linux installation. Applying these to existing running Linux installation might be tricky.

3. Building and running the duplication script

This script is run on the target server (cold spare) and it will clone the content of the source server (actual production machine) on itself. It uses SSH keys and SSH agent to create the initial connection, so make sure you are familiar with them.

Assumptions

  • The target server must be clean Linux installation, the same exact version as on your source server.
  • Your server has standard /etc/passwd user account management. This is copied first so that we correctly preserve file ownership (related ServerFault discussion).
  • Services you run (PHP, Django, Plone, Node.js, you-name-it) are under /srv as recommended by Linux filesystem hierarchy standard
  • Source and target MySQL servers must have initialy same password for the root. Set this when the script runs apt-get install mysql-server for the first time.

Limitations

  • The first run is interactive, as apt-get install asks bunch of stuff for packages like MySQL and Postfix.
  • MySQL dumping and loading the dump does not guarantee all of your data survives intact, especially when skip-lock option is used for the performance reason. For ordinary CMS operations this isn’t a big issue.
  • If you other services beside MySQL needing special flush or lock handling follow the related instructions.

For MySQL duplication make sure you have the following /root/.my.cnf file on the source server, as it allows you to interact with MySQL:

[mysqldump]
user=root
password=YOUR PASSWORD HERE

[client]
user=root
password=YOUR PASSWORD HERE

Run the script inside a screen, because the duplication process may take a while and you do not want to risk losing the process because your local internet connectivity issue.

scp mirror-ubuntu.bash targetserver:/tmp
ssh targetserver
screen 
cd /tmp
bash mirror-ubuntu.bash

4. Testing the spare server

After the duplication script has successfully finished mirroring the server you want to check if the services can be successful started and interacted on the cold spare.

Change internet-facing IP addresses in the related services to be the public IP address of the spare server. E.g. the following files might need changes:

  • /etc/default/varnish
  • /etc/apache2/ports.conf
  • /etc/apache2/sites-available/*

Spoof your local DNS to point the spare server on the tested sites. Edit your local /etc/hosts and add spoofed IP addresses like:

1.2.3.4 www.service1.example.com www.service2.example.com opensourcehacker.com

Access the sites from your web browser, see that database interaction works (login) and file interaction works (upload and download files).

5. mirror-ubuntu.bash

#!/bin/bash
#
# Linux server ghetto duplication script
# Copyright 2014 Mikko Ohtamaa http://opensourcehacker.com
# Licensed under MIT
#

# Everything is copied from this server
SOURCE=root@myserv.example.com

# Our network-traffic and speed optimized rsync command
RSYNC="rsync -a --inplace --delete --compress-level=9"

# Which marker string we use to detect custom init.d scripts
INIT_SCRIPT_MARKER="### BEGIN INIT INFO"

# As we will run in screen we need to detach
# from the forwarded SSH agent session and we use a local
# SSH key to perform the operations.
# Also overriding /root destroys our key.
# Create a key we use for the remaining operations.
TEMP_SSH_KEY=/tmp/mirror_rsa

# The software stack might have touched the following places.
# This list is compliled through trial-and-error,
# sweat and cursing.
# We cannot take /etc as a whole, because it contains
# some computer specific stuff (hard disk ids, etc.)
# and copying it as a whole leads to unbootable system.
CHERRYPICKED_ETC_TARGETS=(\
    "/etc/default" \
    "/etc/varnish" \
    "/etc/apache2" \
    "/etc/ssl" \
    "/etc/nginx" \
    "/etc/postfix" \
    "/etc/php5" \
    "/etc/cron.d" \
    "/etc/cron.daily" \
    "/etc/cron.monthly" \
    "/etc/cron.weekly" \
    "/etc/init.d")

# Create a key without a passphrase
# and put the public key on the source server
rm $TEMP_SSH_KEY 2>&1 > /dev/null
ssh-keygen -N '' -f $TEMP_SSH_KEY
ssh-copy-id -i $TEMP_SSH_KEY $SOURCE
# Detach from the currently used SSH agent
# by starting a session specific to this shell
eval `ssh-agent`
ssh-add $TEMP_SSH_KEY

# Assume the system have same Ubuntu base installation and no extra repositories configured.
# Bring target system up to date.
apt-get update -y
apt-get upgrade -y

# TODO: check that the kernel uname is same
# on the source and the target systems

# This is somewhat crude method to try to install all the packages on the source server.
# If the package is missing or replaced this command will happily churn over it
# (apt-get may fail). This MAY cause user interaction with packages
# like Postfix and MySQL which prompt for initial password. Not sure
# what would be the best way to handle this?
ssh $SOURCE dpkg --get-selections|grep --invert-match deinstall|cut -f 1|while read pkg
do
    apt-get install -y $pkg
done

# As some packages might have changed due to version upgrades and
# deb renames the following step needs interaction
ssh $SOURCE dpkg --get-selections|grep --invert-match deinstall|cut -f 1

# Copy user credentials first to make sure we
# get the user permissions and ownership correctly.
# http://serverfault.com/a/583336/74975
echo "Copying users"
$RSYNC $SOURCE:/etc/passwd /etc
$RSYNC $SOURCE:/etc/shadow /etc
$RSYNC $SOURCE:/etc/group /etc
$RSYNC $SOURCE:/etc/gshadow /etc

# Copy home so we have user home folders available
# Skip duplicity backup signatures
echo "Copying root"
$RSYNC $SOURCE:/root / --exclude=/root/.cache
# lost+found content is generated by fsck, uninteresting
echo "Copying home"
$RSYNC $SOURCE:/home / --exclude=/home/lost+found

echo "Copying /etc targets"
for i in "${CHERRYPICKED_ETC_TARGETS[@]}"
do
   $RSYNC $SOURCE:$i /etc
done

# Most of your service specific stuff should come here
echo "Copying /srv"
$RSYNC $SOURCE:/srv /

# Make sure stuff which went to /etc/init.d gets correctly reflecte across runlevels,
# as our /srv stuff has placed its own init scripts
for i in  /etc/init.d/*
do
    service=`basename $i`
    # Separate from upstart etc. jobs
    if grep --quiet "$INIT_SCRIPT_MARKER" $i ; then
        update-rc.d $service defaults
    fi
done

# Copy MySQL databases.
# Assume source and target root can connect to MySQL without a password.
# You need to set up /root/.my.cnf file on the source server first
# for the passwordless action.
# http://stackoverflow.com/a/9293090/315168
echo "Copying MySQL databases"
ssh -C $SOURCE mysqldump \
    --skip-lock-tables \
    --add-drop-table \
    --add-drop-database \
    --compact \
    --all-databases \
    > /root/all-mysql.sql

# MySQL dump restore woes
# http://stackoverflow.com/a/21087946/315168
mysql -u root -e "SET FOREIGN_KEY_CHECKS = 0; source /root/all-mysql.sql ; SET FOREIGN_KEY_CHECKS = 1;"

# Remove the key we used for the duplication
rm $TEMP_SSH_KEY

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

Varnish at the front of WordPress + Apache and Plone CMS virtual hosts

When moving some sites to a new server I upgraded the Varnish cache server configuration serving this setup. Here are my notes how one can use Varnish at the front of virtual hosting.

The setup is following

  • Multiple sites are hosted on the same server. The sites are mix of PHP of Plone sites.
  • Varnish accepts HTTP requests in port 80 and forwards request to the corresponding backend through HTTP proxying
  • Our virtual host rules capture domain names with or without www-prefix, or with any subdomain name prefix
  • Apache runs in non-standard port localhost:81, serving PHP and WordPress. WordPress caching rules are defined in Apache <virtualhost> config file.
  • Every Plone site runs in its own port and process. Plone uses VirtualHostMonster to rewrite publicly facing site URLS. Plone caching HTTP headers are set by plone.app.caching addon.
  • We do extensive cookie sanitization for PHP (generic), WordPress and Plone.  Google Analytics etc. cookies don’t bust the cache and we can still login to WordPress and Plone as admin
  • As a special trick, there is cleaned cookie debugging trick through HTTP response headers

1-IMG_2677

Don’t worry, Varnish can handle little load

 

Pros

  • Blazingly fast, as Varnish is
  • With Plone’s plone.app.caching, one does not need to touch configuration files but Plone caching HTTP headers can be configured through-the-web

Cons

  • Varnish does not have Nginx or Apache style virtual host configuration file facilities by default and making includes is little bit tricky: With many virtualhost the default.vcl config file grows long.
  • Because WordPress cannot do static resource serving as smartly as Plone, which has unique URLs for all static media revisions, you need to purge Varnish manually from command line if you update any static media files like CSS, JS or images.

Varnish /etc/varnish/default.vcl example for Varnish 3.0:

#
# This backend never responds... we get hit in the case of bad virtualhost name
#
backend default {
    .host = "127.0.0.1";
    .port = "55555";
}

backend myplonesite {
    .host = "127.0.0.1";
    .port = "6699";
}

#
# Apache running on server port 81
#
backend apache {
    .host = "127.0.0.1";
    .port = "81";
}

#
# Gues which site / virtualhost we are diving into.
# Apache, Nginx or Plone directly
#
sub choose_backend {

    # WordPress site
    if (req.http.host ~ "^(.*\.)?opensourcehacker\.com(:[0-9]+)?$") {
        set req.backend = apache;
    }

    # Example Plone site
    if (req.http.host ~ "^(.*\.)?myplonesite\.fi(:[0-9]+)?$") {
        set req.backend = myplonesite;

        # Zope VirtualHostMonster
        set req.url = "/VirtualHostBase/http/" + req.http.host + ":80/Plone/VirtualHostRoot" + req.url;

    }

}

sub vcl_recv {

    #
    # Do Plone cookie sanitization, so cookies do not destroy cacheable anonymous pages.
    # Also, make sure we do not destroy WordPress admin and login cookies in the proces
    #
    if (req.http.Cookie && !(req.url ~ "wp-(login|admin)")) {
        set req.http.Cookie = ";" + req.http.Cookie;
        set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
        set req.http.Cookie = regsuball(req.http.Cookie, ";(statusmessages|__ac|_ZopeId|__cp|php|PHP|wordpress_(.*))=", "; \1=");
        set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");

        if (req.http.Cookie == "") {
            remove req.http.Cookie;
        }
    }

    call choose_backend;

    if (req.request != "GET" &&
      req.request != "HEAD" &&
      req.request != "PUT" &&
      req.request != "POST" &&
      req.request != "TRACE" &&
      req.request != "OPTIONS" &&
      req.request != "DELETE") {
        /* Non-RFC2616 or CONNECT which is weird. */
        return (pipe);
    }
    if (req.request != "GET" && req.request != "HEAD") {
        /* We only deal with GET and HEAD by default */
        return (pass);
    }
    if (req.http.Authorization || req.http.Cookie) {
        /* Not cacheable by default */
        return (pass);
    }
    return (lookup);
}

sub vcl_fetch {

    /* Use to see what cookies go through our filtering code to the server */
    /* set beresp.http.X-Varnish-Cookie-Debug = "Cleaned request cookie: " + req.http.Cookie; */

    if (beresp.ttl <= 0s ||
        beresp.http.Set-Cookie ||
        beresp.http.Vary == "*") {
        /*
         * Mark as "Hit-For-Pass" for the next 2 minutes
         */
        set beresp.ttl = 120 s;
        return (hit_for_pass);
    }
    return (deliver);
}

#
# Show custom helpful 500 page when the upstream does not respond
#
sub vcl_error {
  // Let's deliver a friendlier error page.
  // You can customize this as you wish.
  set obj.http.Content-Type = "text/html; charset=utf-8";
  synthetic {"
  <?xml version="1.0" encoding="utf-8"?>
  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  <html>
    <head>
      <title>"} + obj.status + " " + obj.response + {"</title>
      <style type="text/css">
      #page {width: 400px; padding: 10px; margin: 20px auto; border: 1px solid black; background-color: #FFF;}
      p {margin-left:20px;}
      body {background-color: #DDD; margin: auto;}
      </style>
    </head>
    <body>
    <div id="page">
    <h1>Sivu ei ole saatavissa</h1>
    <p>Pahoittelemme, mutta palvelua ei ole saatavilla.</p>
    <hr />
    <h4>Debug Info:</h4>
    <pre>Status: "} + obj.status + {"
Response: "} + obj.response + {"
XID: "} + req.xid + {"</pre>
      </div>
    </body>
   </html>
  "};
  return(deliver);
}

WordPress does not support setting HTTP response headers natively like Plone. We set them in Apache virtual host configuration file in /etc/apache2/sites-enabled:

<VirtualHost 127.0.0.1:81>

    ServerName opensourcehacker.com
    ServerAlias www.opensourcehacker.com
    ServerAdmin mikko@opensourcehacker.com

    LogFormat       combined
    TransferLog     /var/log/apache2/opensourcehacker.com.log

    # Basic WordPress setup

    Options +Indexes FollowSymLinks +ExecCGI

    DocumentRoot /srv/php/opensourcehacker/wordpress

    <Directory /srv/php/opensourcehacker/wordpress>
        Options FollowSymlinks
        AllowOverride All
    </Directory>

    AddType text/css .css
    AddType application/x-httpd-php .php .php3 .php4 .php5
    AddType application/x-httpd-php-source .phps

    #
    # Set expires headers manually
    #
    ExpiresActive On
    ExpiresByType text/html A0
    ExpiresByType image/gif A3600
    ExpiresByType image/png A3600
    ExpiresByType image/image/vnd.microsoft.icon A3600
    ExpiresByType image/jpeg A3600
    ExpiresByType text/css A3600
    ExpiresByType text/javascript A3600
    ExpiresByType application/x-javascript A3600

</VirtualHost>

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

Useful duckduckgo searches for developers

DuckDuckGo.com is a web search engine. The superior value of DuckDuckGo comes from its ability of doing specialized searches. For non-specialized searches it uses Bing as the generic web search backend. In this blog post I describe how you can optimize your web and software development workflow with DuckDuckGo and its specialized searches.

Bing is almost on par with Google for English search results. However, it still leaves much improvement for non-English search results like ones with my native language Finnish. Thus, one cannot use DuckDuckGo for your everyday searches, like for shopping in Finland. You still need to go to Google for those. To work around this issue I have dual search engine configuration in my Firefox.

Image

  • If I want to search from Google I type the search on the address bar (left side).
  • If I want a specialized search I type into the DuckDuckGo search box. (right side) You can install it for your browser from a button on the bottom right corner of duckduckgo.com.
  • Please note that this setup does not work with Safari which cannot do address bar searches.  Chrome also lacks the search box widget out of the box. There is something called Search Box for Chrome, but I have not tried to configure it for DuckDuckGo on Chrome (I use Chrome only as the web development browser).

1. Specialized searches

DuckDuckGo provides something called !bang syntax. You start your search query with a bang (!) and a keyword. In this case DuckDuckGo performs a special search, not a generic web search. Usually this means DuckDuckGo uses a site specific search engine skipping the web search altogether. Here are example queries which you will find very useful.

!mdn iframe – Search term iframe on Mozilla Developer Network. Useful for all HTML, CSS and Javascript related searches. MDN is the most hiqh quality information source for all web development related matters. Using this search skips low quality sites like w3schools.com. Also works: !javascript and !css.

Image

Example what happens when you search !mdn iframe in duckduckgo.com search box

!python socket – Search “socket” on docs.python.org for Python documentation. (no one remembers Python APIs)

!pypi vvv – Search a package named vvv on pypi.python.org (a validation and linting integration for your software development projects)

!jq offset – Search offset function in jQuery documentation

!gh sevabot – Search Github for projects called “sevabot” (It’s a very handy Skype bot with HTTP interface)

!so video canvas – Search stackoverflow.com questions related to “video” and “canvas” (how to draw HTML5 video frames on <canvas>)

!php open – Try to figure out how to open a file in PHP (there are always open positions in non-PHP related software development jobs if you feel your life is misery)

!man zsh – UNIX manual page for zsh command. (check also our tuned ZSH configuration)

!ubuntuforums zsh – Search ubuntuforums.com for threads related to ZSH

!yt wesibussi – Search YouTube videos called “wesibussi” (I was young and it was fun)

As you can see, you can also copy-paste the DuckDuckGo.com search link in chat (IRC) easily for less experienced peers to educate them how to do power searches.

For the full bang queries list see the official duckduckgo.com source. You will find useful pointers for other programming languages (Perl, Ruby, Java, C#, etc).

 

 

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+