Showing posts with label real-time. Show all posts
Showing posts with label real-time. Show all posts

Analyze log files from several servers in real-time (update: whois, firewall)

First, we setup a machine to analyze the logs:


# install missing packages (e.g. with Ubuntu 12.10)
apt-get install netcat logtop

# create a ramdisk with 1GB for storing the logs
mkdir /ramdisk
mount -t tmpfs -o nosuid,noexec,noatime,size=1G none /ramdisk

# receive logs on port 8080
ncat --ssl -l -k 8080 > /ramdisk/access.log
# open second terminal
tail -f /ramdisk/access.log | logtop

# clean up the ramdisk from time to time
echo >/ramdisk/access.log

Second, we setup the web servers:


# install missing packages
apt-get install netcat

# send logs to analyzer-ip:8080
tail -f /var/log/apache2/access.log | ncat --send-only --ssl <analyzer-ip> 8080
Besides access.log, we can also monitor other log files using different port numbers.

Let's start watching the requests coming in:

Instead of analyzing each line separately, we can also aggregate all requests by client IPs:


tail -f /ramdisk/access.log | awk -Winteractive '{print $1}' | logtop
Or we can aggregate all requests by URLs:

tail -f /ramdisk/access.log | awk -Winteractive '{print $7}' | logtop
Or filter all requests by a user agent:

# show only iPad
tail -f /ramdisk/access.log | grep iPad | logtop

# don't show Google, Msn, Bing
tail -f /ramdisk/access.log | grep -Ev 'Googlebot|bingbot|msnbot' | logtop
To extend the IP address with its owner, we write a small PHP script:

// whois.php
<?php
$fp = fopen('php://stdin', 'r');
while (!feof($fp)) {
list($ip, $null) = explode(' ', fgets($fp), 2);
if (!isset($whois[$ip])) {
$who = shell_exec('whois '.escapeshellarg($ip));
preg_match_all('!(?:descr|orgname|organization|country|owner).*:\s+(.+)!im',
$who, $m);
$whois[$ip] = ' '.str_pad($ip, 15).' '.$m[1][0].' '.$m[1][1]."\n";
}
echo $whois[$ip];
}
fclose($fp);
and run:

apt-get install whois logtop php5-cli
tail -f /ramdisk/access.log | php whois.php | logtop

To send uptime messages every 5 seconds, we can use:


# @analyzer
ncat --ssl -l -k 8081 > /ramdisk/uptime.log
tail -f /ramdisk/uptime.log
# @webserver
while true; do echo -n `hostname`; uptime; sleep 5; done | ncat --send-only ...
# or free disk space, replace uptime with: df -h | grep sda

To configure a firewall on Ubuntu, we can use ufw:


# start firewall, block incoming connections
ufw enable
# allow incoming connections on port 80
ufw allow 80/tcp
# allow limited connections on port 22 (max. 6 connections in 30 seconds)
ufw limit 22/tcp
# show firewall status
ufw status verbose
ufw show listening

# block all new connections from IP 192.168.1.66
ufw insert 1 deny from 192.168.1.66

# remove blocking rule for IP 192.168.1.66
ufw delete deny from 192.168.1.66

Note: If your servers are connected by a secure network (e.g. VPN), you can skip --ssl and certificates.

Coming next: ncat with ssl-certificates

How to implement a real-time chat server in PHP using Server-Sent Events (update: added C benchmark)

Lessons learned:
  • A web server written in PHP can give more than 10000 req/s on small hardware
  • A web server written in PHP is not slower than being written in Java (without threads)
  • A web server written in PHP is 30 percent slower than being written in C (without threads)
  • Realtime applications can be developed in PHP without problems

PHP normally runs inside a web server like Apache or nginx. This keeps all requests separate from each other and does not allow sharing memory or connections. To implement a chat server, the browser has to poll the server regularly for new data. The data is stored in a database and looked up for each request. This is very slow, takes a lot of resources on the server and does not give messages in realtime.
Newer browsers support data being pushed from the server to the client. There are two techniques used: WebSockets (full-duplex) and Server-Sent Events (push notifications)
Using these techniques, one connection stays open for each client and the server sends (pushes) messages whenever new data is available. To handle many connections – that stay open for a long time – and share messages between these connections, we run PHP as a standalone process on the shell and implement a web server directly in PHP.
In the browser, we use Server-Sent Events to receive messages in realtime and AJAX requests to send new messages. We allow more than one connection (=browser tab) per user. We allow sending messages to all users or one specific user. If the user is offline, the message will be kept in memory for later delivery.

Here is the code:

// Usage: run "php server.php" and open a few browser tabs with "http://<ip>:8000/".

// server.php #1 html page
$html = "<html>
<body>
<script>
var inactive = 0;
// show 'username (x)' in document title when there are
// new messages and the tab is not visible
window.onfocus = function(){
inactive = 0;
document.title = document.forms[0].nick.value;
};
window.onblur = function(){ inactive = 1; };
var html = function(s){ return s.replace(/>/g,'>').replace(/</g,'<'); };
</script>

<!-- nickname form, start receiving messages on submit -->
<form onsubmit=\"
var source = new EventSource('/'+this.nick.value);
source.onmessage = function(event){
document.getElementById('content').innerHTML += html(event.data)+'<br>';
if (inactive) document.title = document.forms[0].nick.value+' ('+(inactive++)+')';
};
this.style.display = 'none';
document.forms[1].style.display = '';
document.forms[1].msg.focus();
document.title = this.nick.value;
return false;
\">
<input type='text' name='nick' required='true' autofocus='true' />
<input type='submit' value='Choose nickname'/>
</form>

<!-- message form, send new message as ajax request -->
<form onsubmit=\"
var ajax = new XMLHttpRequest();
ajax.open('GET', 'http://localhost:8000/msg/' + escape(document.forms[0].nick.value) +
'/'+escape(this.msg.value));
ajax.send();
this.msg.value = this.msg.value.substr(0, this.msg.value.indexOf(' ')+1);
this.msg.focus();
return false;
\" style='display:none;'>
<input type='text' name='msg' required='true' placeholder='nickname message' style='width:250px;' />
<input type='submit' value='Send message' />
<input type='button' value='Clear chat' onclick=\"document.getElementById('content').innerHTML='';\" />
</form>

<div id='content'><!-- chat messages --></div>
</body>";

// server.php #2 socket server (port 8000)
$socket = stream_socket_server("tcp://0.0.0.0:8000", $errno, $err) or die($err);
$conns = array($socket);
$conn_ids = array(0);
$conn_user = array();
$msgs = array();

// server loop
while (true) {
$reads = $conns;
// get number of connections with new data
$mod = stream_select($reads, $write, $except, 5);
if ($mod===false) break;

foreach ($reads as $read) {
if ($read===$socket) {
$conn = stream_socket_accept($socket);
$recv = fread($conn, 1024);
if (empty($recv)) continue;

if (strpos($recv, "GET / ")===0) {
// serve static html page from memory
fwrite($conn, "HTTP/1.1 200 OK\r\n". "Connection: close\r\n".
"Content-Type: text/html; charset=UTF-8\r\n\r\n");
fwrite($conn, $html);
stream_socket_shutdown($conn, STREAM_SHUT_RDWR);

} else if (strpos($recv, "GET /msg/")===0) {
// ajax request: send a message
// syntax: GET /msg/user_from/user_to%20message
// e.g. GET /msg/john/mary%20hello
stream_socket_shutdown($conn, STREAM_SHUT_RDWR);
preg_match("!GET /msg/([^/]+)/(\S+)!", $recv, $match);
$user = $match[1];
$match[2] = urldecode($match[2]);
if (!strpos($match[2], " ")) continue;
list($target, $msg) = explode(" ", $match[2], 2);

if ($target=="all") {
// send message to all users
foreach ($conns as $i=>$conn) {
if ($i!=0) fwrite($conn, "data: ".$user." to all: ".$msg."\n\n");
}

} else if (isset($conn_user[$target])) {
// send message to one user and to the originator
if ($target!=$user) foreach ($conn_user[$target] as $conn) {
fwrite($conn, "data: ".$user.": ".$msg."\n\n");
}
if (isset($conn_user[$user])) foreach ($conn_user[$user] as $conn) {
fwrite($conn, "data: You to ".$target.": ".$msg."\n\n");
}

} else {
// user is offline, keep message in memory for later delivery
if (!isset($msgs[$target])) $msgs[$target] = "";
$msgs[$target] .= "data: ".$user." (".@date("Y-m-d H:i")."): ".$msg."\n\n";
foreach ($conn_user[$user] as $conn) {
fwrite($conn, "data: You to ".$target." (offline): ".$msg."\n\n");
}
}

} else if (strpos($recv, "text/event-stream")===false) {
// block other requests like favicon.ico
stream_socket_shutdown($conn, STREAM_SHUT_RDWR);

} else {
// login as new user
// syntax: GET /username e.g. GET /john
preg_match("!GET /(\S+)!", $recv, $match);
if (!isset($match[1])) continue;
$user = $match[1];
echo "connect ".$user." from ".stream_socket_get_name($conn, true)."\n";

fwrite($conn, "HTTP/1.1 200 OK\r\n". "Connection: close\r\n".
"Content-Type: text/event-stream\r\n\r\n");
fwrite($conn, "data: Welcome ".$user."!\n\n");
fwrite($conn, "data: now online: ".implode(", ", array_keys($conn_user))."\n\n");

// deliver messages sent when user was offline
if (isset($msgs[$user])) {
fwrite($conn, $msgs[$user]);
unset($msgs[$user]);
}
// notify other users
foreach ($conns as $i=>$c) {
if ($i!=0) fwrite($c, "data: ".$user." has joined.\n\n");
}
// register connection in pool
$conns[] = $conn;
$conn_ids[] = $user;
// allow multiple connections for 1 user
$conn_user[$user][] = $conn;
}
} else {
$data = fread($read, 1024);
if ($data=="" or $data===false) {
// user/browser closed connection
if ($data!==false) stream_socket_shutdown($read, STREAM_SHUT_RDWR);
$conn_id = array_search($read, $conns, true);
unset($conns[$conn_id]);

// unregister connection for user
$user = $conn_ids[$conn_id];
unset($conn_ids[$conn_id]);
$conn_id = array_search($read, $conn_user[$user], true);
unset($conn_user[$user][$conn_id]);

if (empty($conn_user[$user])) {
unset($conn_user[$user]);
// notify other users
foreach ($conns as $i=>$c) {
if ($i!=0) fwrite($c, "data: ".$user." has left.\n\n");
} } } } } }
Please note that this implementation does not cover user authentication or saving messages to disk or database. Also, connection errors or timeouts are not handled on the browser side.
This code is just for demonstration of the concepts and the performance, so it is not OOP and it should not be used in production. Also it does not implement a complete HTTP stack or handle mime types.

To test the performance of our chat server, let's do some "GET / HTTP/1.1" and serve the static HTML output. ab works good here because it covers all possible scenarios.

php server.php (PHP 5.3.10, 3.4 GHz single core QEMU, VQ7 from Hetzner)

# serve static content
ab -n 10000 -c 10 http://localhost:8000/
Requests per second: 11601.30 [#/sec] (mean)
(memory usage is about 12.5M)

# receive messages (1st shell)
curl -sH ": text/event-stream" http://localhost:8000/john >/dev/null
# send messages (2nd shell)
ab -n 10000 -c 10 http://localhost:8000/msg/mary/john%20hello
Requests per second: 9872.04 [#/sec] (mean)

javac Server.java && java Server (IcedTea7 2.3.3, 1.7.0_09)

ab -n 10000 -c 10 http://localhost:8000/
Requests per second: 10440.45 [#/sec] (mean)

// Server.java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class Server {
private static String readFile(String pathname) throws IOException {
File file = new File(pathname);
StringBuilder fileContents = new StringBuilder((int) file.length());
Scanner scanner = new Scanner(file);
try {
while (scanner.hasNextLine())
fileContents.append(scanner.nextLine() + "\n");
return fileContents.toString();
} finally {
scanner.close();
}
}

public static void main(String[] args) throws Exception {
final String html = readFile("test.html");
try (ServerSocket socket = new ServerSocket(8000)) {
while (true) {
final Socket client = socket.accept();
try (Socket c = client) {
while (true) {
DataInputStream dis = new DataInputStream(c.getInputStream());
DataOutputStream dos = new DataOutputStream(c.getOutputStream());
BufferedReader in = new BufferedReader(new InputStreamReader(dis));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(dos));
String recv = in.readLine();
if (recv != null && recv.indexOf("GET / ") == 0)
out.write("HTTP/1.1 200 OK\r\nConnection: close\r\n"
+ "Content-Type: text/html; charset=UTF-8\r\n\r\n" + html);
// using StringBuilder was slower ...
out.close();
in.close();
}
} catch (IOException e) {
// ok
}
}
} catch (IOException e) {
System.out.println(e);
} } }

gcc -o server.bin server.c && ./server.bin (gcc 4.6.3)

ab -n 10000 -c 10 http://localhost:8000/
Requests per second: 15254.44 [#/sec] (mean)
(memory usage is about 0.5M)

// server.c
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
int server, instance;
socklen_t clilen;
char buffer[256];
struct sockaddr_in srv, cli;
char html[] = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: text/html;\
charset=UTF-8\r\n\r\n\
<html>\
<body>\
<script>\
var inactive = 0;\
...
</body>";
int html_len = strlen(html);

server = socket(AF_INET, SOCK_STREAM, 0);
bzero((char *) &srv, sizeof(srv));
srv.sin_family = AF_INET;
srv.sin_addr.s_addr = INADDR_ANY;
srv.sin_port = htons(8000);
int opt = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
if (bind(server, (struct sockaddr *) &srv, sizeof(srv)) < 0) {
perror("ERROR: bind");
exit(1);
}
listen(server,5);
clilen = sizeof(cli);
do {
instance = accept(server, (struct sockaddr *) &cli, &clilen);
if (instance < 0) continue;
bzero(buffer,256);
read(instance,buffer,256);
if (strstr(buffer, "GET / ")) write(instance, html, html_len);
close(instance);
} while(1);
return 0;
}

Apache (v2.2.22, from disk with logging enabled)

ab -n 10000 -c 10 http://localhost/public/test.html
Requests per second: 6491.84 [#/sec] (mean)

node test.js (node.js v0.6.12)

ab -n 10000 -c 10 http://localhost:8000/
Requests per second: 5554.03 [#/sec] (mean)

// test.js
var str = "<html>\
<body>\
<script>\
var inactive = 0;\
...
</body>";
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(str);
}).listen(8000, '127.0.0.1');
node test2.js (node.js v0.6.12)

ab -n 10000 -c 10 http://localhost:8000/
Requests per second: 9075.90 [#/sec] (mean)

// test2.js
var str = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n\
<html>\
<body>\
<script>\
var inactive = 0;\
...
</body>";
var net = require('net');
var server = net.createServer();
server.listen(8000, '127.0.0.1');
server.on('connection', function(sock) {
sock.end(str);
});