going crazy with node.js and cakephp

48
Going crazy with Node.js and CakePHP CakeFest 2011 Manchester, UK Mariano Iglesias @mgiglesias

Upload: mariano-iglesias

Post on 13-Jan-2015

15.108 views

Category:

Technology


3 download

DESCRIPTION

Learning about Node.JS and how to integrate it with CakePHP for unmatched performance

TRANSCRIPT

Page 1: Going crazy with Node.JS and CakePHP

Going crazy with Node.js and CakePHP

CakeFest 2011

Manchester, UK

Mariano Iglesias @mgiglesias

Page 2: Going crazy with Node.JS and CakePHP

Hello world!

Hailing from Miramar, Argentina CakePHP developer since 2006 Worked in countless projects

Contact me if you are looking for work gigs!

A FOSS supporter, and contributor CakePHP 1.3 book recently published Survived Node Knockout 2011

Page 3: Going crazy with Node.JS and CakePHP

Node.js... that's not CakePHP!

If there's something I'd like you to learn it'd be...

There are different solutions to different problems!

CakePHP

Python

Node.js

C++

NGINx / Lighttpd

Page 4: Going crazy with Node.JS and CakePHP

What's the problem?

What's an app normally doing? What can I do then?

Add caching Add workers Faster DB Vertical scale: add more resources Horizontal scale: add more servers

Still can't get n10K concurrent users?

Page 5: Going crazy with Node.JS and CakePHP

Threads vs eventshttp://blog.wefaction.com/a-little-holiday-present

Page 6: Going crazy with Node.JS and CakePHP

What is Node.js?

In a nutshell, it's JavaScript on the server

V8 JavaScript engine

Evented I/O+

=

Page 7: Going crazy with Node.JS and CakePHP

V8 Engine

Property access through hidden classes Machine code Garbage collection

Performance is kinghttp://code.google.com/apis/v8/design.html

Page 8: Going crazy with Node.JS and CakePHP

Evented I/O

libeio: async I/O libev: event loop

libuv: wrapper for libev and IOCP

db.query().select('*').from('users').execute(function() { fs.readFile('settings.json', function() { // ... });});

Page 9: Going crazy with Node.JS and CakePHP

Libuv == Node.exe

http_simple (/bytes/1024) over 1-gbit network, with 700 concurrent connections:

windows-0.5.4 : 3869 r/swindows-latest : 4990 r/slinux-latest-legacy : 5215 r/slinux-latest-uv : 4970 r/s

Page 10: Going crazy with Node.JS and CakePHP

More stuff

buffer: large portions of data c-ares: async DNS child_process: spawn(), exec(), fork()

(0.5.x) crypto: OpenSSL http_parser: high performance HTTP

parser timer: setTimeout(), setInterval()

Page 11: Going crazy with Node.JS and CakePHP

Should I throw away CakePHP?

Remember...

There are different solutions to different problems!

Page 12: Going crazy with Node.JS and CakePHP

First node.js server

var http = require('http');

http.createServer(function(req, res) { res.writeHead(200, { 'Content-type': 'text/plain' }); res.end('Hello world!');}).listen(1337);

console.log('Server running at http://localhost:1337');

Page 13: Going crazy with Node.JS and CakePHP

Understanding the event loop

There is a single thread running in Node.js

No parallel execution... for YOUR code

var http = require('http');

http.createServer(function(req, res) { console.log('New request'); // Block for five seconds var now = new Date().getTime(); while(new Date().getTime() < now + 5000) ; // Response res.writeHead(200, { 'Content-type': 'text/plain' }); res.end('Hello world!');}).listen(1337);

console.log('Server running at http://localhost:1337');

Page 14: Going crazy with Node.JS and CakePHP

What about multiple cores?

:1337

:1338:1339

The load balancer approach

The OS approach

var http = require('http'), cluster = ...;var server = http.createServer(function(req, res) { res.writeHead(200, { 'Content-type': 'text/plain' }); res.end('Hello world!');});cluster(server).listen(1337);

Page 15: Going crazy with Node.JS and CakePHP

Packaged modules

$ curl http://npmjs.org/install.sh | sh$ npm install db-mysql

There are more than 3350 packages, and more than 14 are added each day

Page 16: Going crazy with Node.JS and CakePHP

Packaged modules

var m = require('./module');m.sum(1, 3, function(err, res) { if (err) { return console.log('ERROR: ' + err); } console.log('RESULT IS: ' + res);});

exports.sum = function(a, b, callback) { if (isNaN(a) || isNaN(b)) { return callback(new Error('Invalid parameter')); } callback(null, a+b);};

Page 17: Going crazy with Node.JS and CakePHP

Frameworks are everywhere

Multiple environments Middleware Routing View rendering Session support

http://expressjs.com

Page 18: Going crazy with Node.JS and CakePHP

Multiple environments

var express = require('express');var app = express.createServer();

app.get('/', function(req, res) { res.send('Hello world!');});

app.listen(3000);console.log('Server listening in http://localhost:3000');

app.configure(function() { app.use(express.bodyParser());});

app.configure('dev', function() { app.use(express.logger());});

$ NODE_ENV=dev node app.js

Page 19: Going crazy with Node.JS and CakePHP

Middleware

function getUser(req, res, next) { if (!req.params.id) { return next(); } else if (!users[req.params.id]) { return next(new Error('Invalid user')); } req.user = users[req.params.id]; next();}

app.get('/users/:id?', getUser, function(req, res, next) { if (!req.user) { return next(); } res.send(req.user);});

Page 20: Going crazy with Node.JS and CakePHP

View renderingapp.configure(function() { app.set('views', __dirname + '/views'); app.set('view engine', 'jade');});

app.get('/users/:id?', function(req, res, next) { if (!req.params.id) { return next(); } if (!users[req.params.id]) { return next(new Error('Invalid user')); }

res.send(users[req.params.id]);});

app.get('/users', function(req, res) { res.render('index', { layout: false, locals: { users: users } });});

html body h1 Node.js ROCKS ul - each user, id in users li a(href='/users/#{id}') #{user.name}

views/index.jade

Page 21: Going crazy with Node.JS and CakePHP

node-db

What's the point? Supported databases Queries

Manual API

JSON types Buffer

http://nodejsdb.org

Page 22: Going crazy with Node.JS and CakePHP

node-db

var mysql = require('db-mysql');new mysql.Database({ hostname: 'localhost', user: 'root', password: 'password', database: 'db'}).connect(function(err) { if (err) { return console.log('CONNECT error: ', err); } this.query(). select(['id', 'email']). from('users'). where('approved = ? AND role IN ?', [ true, [ 'user', 'admin' ] ]). execute(function(err, rows, cols) { if (err) { return console.log('QUERY error: ', err); } console.log(rows, cols); });});

Page 23: Going crazy with Node.JS and CakePHP

Let's get to work

Page 24: Going crazy with Node.JS and CakePHP

Sample application

Basic CakePHP 2.0 app JSON endpoint for latest messages

Page 25: Going crazy with Node.JS and CakePHP

Why are we doing this?

CakePHP: 442.90 trans/sec

Node.js: 610.09 trans/sec

Node.js & Pool: 727.19 trans/sec

Node.js & Pool & Cluster: 846.61 trans/sec

CakePHP Node.js Node.js & Pool Node.js & Pool & Cluster0

100

200

300

400

500

600

700

800

900

Tra

ns

/ se

c (b

igg

er

==

be

tter)

$ siege -d1 -r10 -c25

Page 26: Going crazy with Node.JS and CakePHP

Sample application

CREATE TABLE `users`( `id` char(36) NOT NULL, `email` varchar(255) NOT NULL, `password` text NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `email` (`email`));

CREATE TABLE `messages` ( `id` char(36) NOT NULL, `from_user_id` char(36) NOT NULL, `to_user_id` char(36) NOT NULL, `message` text NOT NULL, `created` datetime NOT NULL, PRIMARY KEY (`id`), KEY `from_user_id` (`from_user_id`), KEY `to_user_id` (`to_user_id`), CONSTRAINT `messages_from_user` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`), CONSTRAINT `messages_to_user` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`));

Page 27: Going crazy with Node.JS and CakePHP

Sample applicationhttp://cakefest3.loc/messages/incoming/4e4c2155-e030-477e-985d-

18b94c2971a2

[{

"Message": {"id":"4e4d8cf1-15e0-4b87-a3fc-

62aa4c2971a2","message":"Hello Mariano!"

},"FromUser": {

"id":"4e4c2996-f964-4192-a084-19dc4c2971a2",

"name":"Jane Doe"},"ToUser": {"name":"Mariano Iglesias"}

},{

"Message": {"id":"4e4d8cf5-9534-49b9-8cba-

62bf4c2971a2","message":"How are you?"

},"FromUser": {

"id":"4e4c2996-f964-4192-a084-19dc4c2971a2",

"name":"Jane Doe"},"ToUser": {"name":"Mariano Iglesias"}

}]

Page 28: Going crazy with Node.JS and CakePHP

CakePHP codeclass MessagesController extends AppController { public function incoming($userId) { $since = !empty($this->request->query['since']) ? urldecode($this->request->query['since']) : null; if ( empty($since) || !preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $since) ) { $since = '0000-00-00 00:00:00'; }

$messages = ...

$this->autoRender = false; $this->response->type('json'); $this->response->body(json_encode($messages)); $this->response->send(); $this->_stop(); }}

Page 29: Going crazy with Node.JS and CakePHP

CakePHP code$messages = $this->Message->find('all', array( 'fields' => array( 'Message.id', 'Message.message', 'FromUser.id', 'FromUser.name', 'ToUser.name' ), 'joins' => array( array( 'type' => 'INNER', 'table' => 'users', 'alias' => 'FromUser', 'conditions' => array('FromUser.id = Message.from_user_id') ), array( 'type' => 'INNER', 'table' => 'users', 'alias' => 'ToUser', 'conditions' => array('ToUser.id = Message.to_user_id') ), ), 'conditions' => array( 'Message.to_user_id' => $userId, 'Message.created >=' => $since ), 'order' => array('Message.created' => 'asc'), 'recursive' => -1));

Page 30: Going crazy with Node.JS and CakePHP

Node.js code: expressvar express = require('express'), mysql = require('db-mysql'), port = 1337;

var app = express.createServer();app.get('/messages/incoming/:id', function(req, res){ var r = ...

var userId = req.params.id; if (!userId) { return r(new Error('No user ID provided')); }

var since = req.query.since ? req.query.since : false; if (!since || !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(since)) { since = '0000-00-00 00:00:00'; }

new mysql.Database(...).connect(function(err) { if (err) { return r(err); } ... });});app.listen(port);console.log('Server running at http://localhost:' + port);

Page 31: Going crazy with Node.JS and CakePHP

Node.js code: express

var r = function(err, data) { if (err) { console.log('ERROR: ' + err); res.writeHead(503); return res.end(); }

res.charset = 'UTF-8'; res.contentType('application/json'); res.header('Access-Control-Allow-Origin', '*'); res.send(data);};

Avoids the typical:XMLHttpRequest cannot load URL. Origin URL is not allowed by

Access-Control-Allow-Origin

Page 32: Going crazy with Node.JS and CakePHP

Node.js code: node-dbdb.query().select({ 'Message_id': 'Message.id', 'Message_message': 'Message.message', 'FromUser_id': 'FromUser.id', 'FromUser_name': 'FromUser.name', 'ToUser_name': 'ToUser.name'}).from({'Message': 'messages'}).join({ type: 'INNER', table: 'users', alias: 'FromUser', conditions: 'FromUser.id = Message.from_user_id'}).join({ type: 'INNER', table: 'users', alias: 'ToUser', conditions: 'ToUser.id = Message.to_user_id'}).where('Message.to_user_id = ?', [ userId ]).and('Message.created >= ?', [ since ]).order({'Message.created': 'asc'}).execute(function(err, rows) { ...});

Page 33: Going crazy with Node.JS and CakePHP

Node.js code: node-dbfunction(err, rows) { db.disconnect(); if (err) { return r(err); }

for (var i=0, limiti=rows.length; i < limiti; i++) { var row = {}; for (var key in rows[i]) { var p = key.indexOf('_'), model = key.substring(0, p), field = key.substring(p+1); if (!row[model]) { row[model] = {}; } row[model][field] = rows[i][key]; } rows[i] = row; }

r(null, rows);}

Page 34: Going crazy with Node.JS and CakePHP

Long polling

Reduce HTTP requests Open one request and wait for

responsefunction fetch() { $.ajax({ url: ..., async: true, cache: false, timeout: 60 * 1000, success: function(data) { ... setTimeout(fetch(), 1000); }, error: ... });}

Page 35: Going crazy with Node.JS and CakePHP

Bonus tracks

Page 36: Going crazy with Node.JS and CakePHP

#1Pooling connections

Page 37: Going crazy with Node.JS and CakePHP

Pooling connections

var mysql = require('db-mysql'), generic_pool = require('generic-pool');var pool = generic_pool.Pool({ name: 'mysql', max: 30, create: function(callback) { new mysql.Database({ ... }).connect(function(err) { callback(err, this); }); }, destroy: function(db) { db.disconnect(); }});pool.acquire(function(err, db) { if (err) { return r(err); } ... pool.release(db);});

https://github.com/coopernurse/node-pool

Page 38: Going crazy with Node.JS and CakePHP

#2Clustering express

Page 39: Going crazy with Node.JS and CakePHP

Clustering express

var cluster = require('cluster'), port = 1337;cluster('app'). on('start', function() { console.log('Server running at http://localhost:' + port); }). on('worker', function(worker) { console.log('Worker #' + worker.id + ' started'); }). listen(port);

http://learnboost.github.com/cluster

var express = require('express'), generic_pool = require('generic-pool');

var pool = generic_pool.Pool({ ... });

module.exports = express.createServer();module.exports.get('/messages/incoming/:id', function(req, res) { pool.acquire(function(err, db) { ... });});

Page 40: Going crazy with Node.JS and CakePHP

Clustering express

Page 41: Going crazy with Node.JS and CakePHP

#3Dealing with parallel tasks

Page 42: Going crazy with Node.JS and CakePHP

Dealing with parallel tasks

Asynchronous code can get complex to manage

Async offers utilities for collections Control flow

series(tasks, [callback]) parallel(tasks, [callback]) waterfall(tasks, [callback])

https://github.com/caolan/async

Page 43: Going crazy with Node.JS and CakePHP

Dealing with parallel tasksvar async = require('async');

async.waterfall([ function(callback) { callback(null, 4); }, function(id, callback) { callback(null, { id: id, name: 'Jane Doe' }); }, function(user, callback) { console.log('USER: ', user); callback(null); }]);

$ node app.jsUSER: { id: 4, name: 'Jane Doe' }

Page 44: Going crazy with Node.JS and CakePHP

#4Unit testing

Page 45: Going crazy with Node.JS and CakePHP

Unit testing

Export tests from a module Uses node's assert module:

ok(value) equal(value, expected) notEqual(value, expected) throws(block, error) doesNotThrow(block, error)

The expect() and done() functions

https://github.com/caolan/nodeunit

Page 46: Going crazy with Node.JS and CakePHP

Unit testing

var nodeunit = require('nodeunit');exports['group1'] = nodeunit.testCase({ setUp: function(cb) { cb(); }, tearDown: function(cb) { cb(); }, test1: function(test) { test.equals(1+1, 2); test.done(); }, test2: function(test) { test.expect(1);

(function() { test.equals('a', 'a'); })();

test.done(); }});

$ nodeunit tests.js

nodeunit.js✔ group1 – test1✔ group1 – test2

Page 47: Going crazy with Node.JS and CakePHP

Questions?

Page 48: Going crazy with Node.JS and CakePHP

Thanks!You rock!

@mgiglesias

http://marianoiglesias.com.ar