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

Going crazy with Node.JS and CakePHP

Jan 13, 2015

Download

Technology

Learning about Node.JS and how to integrate it with CakePHP for unmatched performance
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
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