Deploying Node - Js - Sample Chapter
Deploying Node - Js - Sample Chapter
Deploying Node - Js - Sample Chapter
ee
Sandro Pasquali
P U B L I S H I N G
C o m m u n i t y
$ 44.99 US
29.99 UK
pl
Deploying Node.js
Deploying Node.js
Sa
m
D i s t i l l e d
Deploying Node.js
Learn how to build, test, deploy, monitor, and maintain your
Node.js applications at scale
E x p e r i e n c e
Sandro Pasquali
Preface
Over the past few years, Node.js has found its way into the technology stack
of Fortune 500 companies, mobile-first start-ups, successful Internet-based
businesses, and other enterprises. In addition to validating its power as a platform,
this success has exposed a shortage of tools for managing, deploying, and
monitoring Node.js applications. Even though the Node.js community is open and
collaborative, comprehensive information on how professional Node developers
design, test, and push their code into production environments is hard to find.
This book is an attempt to close that gap in knowledge by explaining and
documenting techniques and tools that working Node.js developers can use to create
scalable, smart, robust, and maintainable software for the enterprise.
After a brief introduction to Node and the philosophy behind its design, you will
learn how to install and update your applications on local servers and across the
cloud. This foundation is then built upon by proceeding step by step through load
balancing and other scaling techniques, explaining how to handle sudden changes in
traffic volume and shape, implement version control, and design memory-efficient,
stateful, distributed applications.
Once you've completed the groundwork essential to creating production-ready
systems, you will need to test and deploy them. Filled with real-world code
examples, this book is structured as a progressive workbook explaining strategies
for success to be used during each stage of the testing, deploying, monitoring, and
maintaining marathon that every successful piece of software runs.
When you've finished this book, you will have learned a set of reusable patterns
directly applicable to solving the problems that you are dealing with today and will
have laid the foundation to make confident decisions about how to build and deploy
your next project.
Preface
Managing Memory
and Space
Today's developer has easy access to surprisingly inexpensive storage solutions.
The movement away from monolithic systems toward composed and distributed
ones has certain advantages, yet inevitably introduces a few new problems. The
availability of cheap storage should not be an excuse to push everything you can
into memory or onto a disk without any limit, for instance. Also, where does
the state reside in such a system? Does a cluster of servers share a common
database connection? How is data synchronized in such a setup? If you are
using a shared-nothing noSQL architecture, how are state changes communicated
across all actors?
There are many considerations. Always seeking to use a minimum of resources is
a good guiding principle. In this chapter, we will look at ways to reduce the cost of
data storage in your Node programs, including tips on writing efficient, optimized
code. Certain strategies for efficiently sharing data across distributed servers will
be discussed, including caching strategies, microservices, interprocess messaging,
and other techniques to keep your systems fast, light, and scalable. Examples
demonstrating how to use tokens to manage user session data efficiently at scale
and storing extensive user activity data compactly using Redis will help you put
these ideas into practice.
[ 91 ]
Microservices
Any nontrivial, network-based application is composed of several independent
subsystems that must cooperate to fulfill the business or other requirements of the
larger system. For example, many web applications present browser-based interfaces
composed of one or several libraries and/or UI frameworks translating user actions
against JavaScript controllers into formalized network requests issued across several
web protocols. These ultimately communicate with any number of servers running
programs that implement various sorts of business logicall sharing one or several
databases, perhaps across several data centers. These initiate and coordinate even
longer chains of requests.
Because there is no absolute right way to build software, every design is biased
toward one or a few key principles, in particular, principles guiding how a system
should scale, which normally affects how it is deployed. A few of the key principles
informing the Node communitymodular systems composed of small programs
that do one thing well and are event-driven, I/O focused, and network focused
align closely with those underpinning microservices.
[ 92 ]
Chapter 4
A system should be broken down into many small services that each do one
thing and no more. This helps with clarity.
Independent services are easy to replicate (or cull). Scaling (in both
directions) is a natural feature of microservice architectures as new nodes can
be added or removed as necessary. This also enables easy experimentation,
where prototype services can be tested, new features can be tested or
deployed temporarily, and so on.
The idea is simple: smaller services are easy to reason about individually,
encouraging correctness of specifications (little or no gray area) and clarity of
APIs (constrained sets of output follow constrained sets of input). Being stateless
and decoupled, services promote system composability, help with scaling and
maintainability, and are easier to deploy. Also, very precise, discrete monitoring
of these sorts of systems is possible.
[ 93 ]
Redis pub/sub
In the previous chapter, we discussed the use of message queues, an excellent
technique for rapid cross-process communication. Redis offers an interface allowing
connected clients to subscribe to a particular channel and broadcast messages to that
channel. This is generally described as a publish/subscribe paradigm. When you
do not need more complex message exchanges and brokers but a simple and fast
notification network, pub/sub works well.
Let's set up a basic pub/sub example and then move on to an example of using
pub/sub to create a microservice architecture where many components doing
a particular job are passed requests for their services and pass back resultsall
coordinated via Redis.
First, let's look at the most basic example of pub/suba script that demonstrates
how to subscribe to a channel and how to publish to that channel:
var redis = require("redis");
var publisher = redis.createClient();
var subscriber = redis.createClient();
subscriber.subscribe('channel5');
subscriber.on('message', function(channel, message) {
console.log('channel: ', channel)
console.log('message: ', message)
})
subscriber.on('subscribe', function() {
publisher.publish('channel5', 'This is a message')
})
We are using Matt Ranney's Redis npm module. Find out more
at https://github.com/mranney/node_redis.
To create both a publisher and a subscriber, we create two Redis clients. Note that,
once a subscribe or psubscribe (more on psubscribe later) command is issued
to a client, that client will enter subscriber mode, no longer accepting standard Redis
commands. Typically, you will create two clients: one listening for messages on
subscribed channels and the other a standard Redis client used for all other commands.
[ 94 ]
Chapter 4
Also note that we must wait for the subscribe event to be fired on the subscriber
client prior to publishing any messages. Redis does not hold a queue of published
messages, which involves waiting for subscribers. A message for which there are no
subscribers is simply dropped. The following is based on the Redis documentation:
"published messages are characterized into channels, without knowledge of
what (if any) subscribers there may be. Subscribers express interest in one or more
channels, and only receive messages that are of interest, without knowledge of what
(if any) publishers there are. This decoupling of publishers and subscribers can
allow for greater scalability and a more dynamic network topology."
So, we must wait for a subscriber prior to publishing. Once that subscription is made,
we can publish to the channel5 channel, and the subscriber handle listening on
that channel receives our message:
channel: channel5
message: This is a message
Let's take this a little further by creating two distinct Node processes, each
performing a simple (micro) service. We'll build a calculator service with two
operationsadd and subtract. A separate, dedicated process will perform each
operation, and the two-way communication between the calculator service and its
helper services will be managed by Redis pub/sub.
First, we design two Node programs, one that adds and one that subtracts. We'll only
show the adder here:
var redis = require("redis");
var publisher = redis.createClient();
var subscriber = redis.createClient();
subscriber.subscribe('service:add');
subscriber.on('message', function(channel, operands) {
var result = JSON.parse(operands).reduce(function(a, b) {
return a + b;
})
publisher.publish('added', result);
})
subscriber.on('subscribe', function() {
process.send('ok')
})
The subtraction program is nearly identical, differing only in the channel it listens
on and the calculation it performs. These two services exist in the add.js and
subtract.js files.
[ 95 ]
We can see what this service does. When it receives a message on the service:add
channel, it will fetch the two operands passed to it, add them, and publish the result
to the added channel. As we'll soon see, the calculator service will listen for results on
the added channel. Also, you will notice a call to process.sendthis is used
to notify the calculator service that the add service is ready. This will make more
sense shortly.
Now, let's build the calculator.js service itself:
var redis = require("redis");
var publisher = redis.createClient();
var subscriber = redis.createClient();
var child_process = require('child_process');
var add = child_process.fork('add.js');
var subtract = child_process.fork('subtract.js');
add.on('message', function() {
publisher.publish('service:add', JSON.stringify([7,3]))
})
subtract.on('message', function() {
publisher.publish('service:subtract', JSON.stringify([7,3]))
})
subscriber.subscribe('result:added')
subscriber.subscribe('result:subtracted')
subscriber.on('message', function(operation, result) {
console.log(operation + ' = ', result);
});
The main calculator service forks two new processes running the add.js and
subtract.js microservices. Typically, in a real system, the creation of these other
services would be done independently, perhaps even on completely separate
machines. This simplification is useful for our example, but it does demonstrate
a simple way to create vertical scaling across cores. Clearly, each child process in
Node on which fork has been used comes with a communication channel built in,
allowing child processes to communicate with their parents as seen in the calculator
service's use of add.on() and substract.on(...) and in our calculation services
with process.send().
[ 96 ]
Chapter 4
Once the calculator service receives notice that its dependent services are ready, it
publishes a request for work to be done on the service:add and service:subtract
channels by passing operands. As we saw earlier, each service listens on its own
channel and performs the work requested, publishing a result that this calculator
service can then receive and use. When calculator.js is executed, the following
will be displayed in your terminal:
result:subtracted = 4
result:added = 10
Earlier, we mentioned the psubscribe method. The p prefix signifies pattern and
is useful when you want to subscribe to channels using a typical glob pattern. For
example, rather than the calculator service subscribing to two channels with the
common result: prefix, we can simplify it as follows:
subscriber.psubscribe('result:*')
subscriber.on('pmessage', function(operation, result) {
console.log(operation + ' = ', result);
})
Now, any additional service can publish results with the result: prefix and can
be picked up by our calculator. Note that the p prefix must also be reflected in the
pmessage event listener.
The call to seneca() starts up a service that will listen on port 8080 on localhost
for patterns rendered in the JSON formatone of either { operation: "sayHello"
} or { operation: "sayGoodbye" }. We also create a client object connected to
the Seneca service on 8080 and have that client act against those patterns. When this
program is executed, you will see Hello! and Goodbye! displayed in your terminal.
Because the Seneca service is listening on HTTP by default, you can achieve the same
result by making a direct call over HTTP, operating against the /act route:
curl -d '{"operation":"sayHello"}' http://localhost:8080/act
// {"message":"Hello!"}
Now, let's replicate the calculator application developed earlier, this time using
Seneca. We're going to create two services, each listening on a distinct port, with
one performing addition and the other performing subtraction. As in the previous
calculator example, each will be started as an individual process and called remotely.
Create an add.js file as follows:
require('seneca')()
.add({
operation:'add'
},
function(args, done) {
var result = args.operands[0] + args.operands[1];
done(null, {
result : result
[ 98 ]
Chapter 4
})
})
.listen({
host:'127.0.0.1',
port:8081
})
Next, create a subtract.js file identical to add.js, changing only its operation
parameter and, of course, its algorithm:
...
.add({
operation:'subtract'
},
...
var result = args.operands[0] - args.operands[1];
...
To demonstrate the usage of these services, create a calculator.js file that binds
a client to each service on its unique port and acts against them. Note that you must
create distinct Seneca clients:
var add = require('seneca')().client({
host:'127.0.0.1',
port:8081
})
var subtract = require('seneca')().client({
host:'127.0.0.1',
port:8082
})
add.act({
operation:'add',
operands: [7,3]
},
function(err, op) {
console.log(op.result)
})
subtract.act({
operation:'subtract',
operands: [7,3]
[ 99 ]
Just as with the previous example, we can make a direct HTTP call:
curl -d '{"operation":"add","operands":[7,3]}' http://127.0.0.1:8081/act
// {"result":10}
By building out your calculator in this way, each operation can be isolated into its
own service, and you can add or remove functionality as needed without affecting
the overall program. Should a service develop bugs, you can fix and replace it
without stopping the general calculator application. If one operation requires more
powerful hardware or more memory, you can shift it to its own server without
stopping the calculator application or altering your application logicyou only need
to change the IP address of the targeted service. In the same way, it is easy to see
how, by stringing together the database, authentication, transaction, mapping, and
other services, they can be more easily modeled, deployed, scaled, monitored, and
maintained than if they were all coupled to a centralized service manager.
Chapter 4
When a request is made to localhost:8000, the somefile.js file is read off the
filesystem in its entirety and returned to the client. That is the desired effectbut
there is a slight problem. Because the entire file is being pushed into a buffer prior
to being returned, an amount of memory equal to the byte size of the file must be
allocated on each request. While the operation is itself asynchronous (allowing other
operations to proceed), just a few requests for a very large file (of several MB, for
example) can overflow the memory and take down the Node process.
Node excels at creating scalable web services. One of the reasons for this is the focus
on providing robust Stream interfaces.
A better strategy is to stream the file directly to the HTTP response object (which is a
writable stream):
http.createServer(function(req, res) {
fs.createReadStream('./static_buffered.js').pipe(res);
}).listen(8000)
In addition to requiring less code, data is sent (piped) directly to the out stream,
using very little memory.
[ 101 ]
On the other hand, we can use Stream to enable a very nice and composable pipeline
of transformations. There are several ways to achieve this goal (such as with
Transform Stream), but we'll just create our own transformer.
This script will take an input from process.stdin and convert what is received to
uppercase, piping the result back to process.stdout:
var Stream = require('stream')
var through = new Stream;
through.readable = true;
through.writable = true;
through.write = function(buf) {
through.emit('data', buf.toString().toUpperCase())
}
through.end = function(buf) {
arguments.length && through.write(buf)
through.emit('end')
}
process.stdin.pipe(through).pipe(process.stdout);
Understanding prototypes
JavaScript is an Object-oriented (OO) prototype-based language. It is important
for you to understand what this means and how this sort of design is more memory
efficient than many traditional OO language designs when used correctly. Because
the storage of state data within Node processes is a common practice (such as
connection data lookup tables within a socket server), we should leverage the
prototypal nature of the language to minimize memory usage. What follows is a brief
but pointed comparison of the classical inheritance-based object model and the object
system that JavaScript provides in terms of memory usage and efficiency.
In class-based systems, a class contains instructions on how to create instances of
itself. In other words, a class describes a set containing objects built according to
a class specification, which includes things such as default values for attributes of
constructed objects. To create an instance of a class, there must be a class definition
that describes how to build that instance. Classes can also inherit properties from
each other, creating new instance blueprints that share characteristics with other
blueprintsan inheritance model describing the provenance of objects.
[ 102 ]
Chapter 4
Note that both instances now maintain an identical attribute structure. Additionally,
the property x of both point instances has been copied from the base point class.
Importantly, notice that the value of x has been copied to each instance even though
this attribute value is identical in both instances.
Objects in a prototypal language do not require a class to define their composition.
For example, an object in JavaScript can be created literally:
var myPoint = {
x : 100,
y : 50
}
Not requiring the storage of a class definition prior to creating an object instance is
already more memory efficient. Now, consider this use of prototypes to replicate the
inheritance-based example discussed previously. In the following code, we see how a
single object, myPoint, is passed as the first object to Object.create, which returns
a new object with myPoint as its prototype:
var myPoint = {
x: 100,
y: 50
}
var pointA = Object.create(myPoint, {
y: 100
})
var pointA = Object.create(myPoint, {
y: 200
})
[ 103 ]
Note that each point instance does not store copies of attributes, the value of which
is not explicitly declared. Prototypal systems employ message delegation, not
inheritance. When a point instance receives the message give me x, and it cannot
satisfy that request, it delegates the responsibility for satisfying that message to its
prototype (which, in this case, does have a value for x). It should be obvious that,
in real-world scenarios with large and complex objects, the ability to share default
values across many instances without redundantly copying identical bytes will
lead to a smaller memory footprint. Additionally, these instances can themselves
function as prototypes for other objects, continuing a delegation chain indefinitely
and enabling elegant object graphs using only as much memory as necessary to
distinguish unique object properties.
Memory efficiency also speeds up instantiation. As should be clear from the
preceding code, delegating responsibility for messages to a prototype implies that
your extended receiver requires a smaller instance footprintfewer slots need to be
allocated per object. The following are two construction function definitions:
var rec1 = function() {}
rec1.prototype.message = function() { ... }
var rec2 = function() {
this.message = function() { ... }
}
[ 104 ]
Chapter 4
Even with these simple definitions, many instances built from the first constructor
will consume much less memory than an equal number of instances constructed
from the secondnew Rec1() will complete well before new Rec2() due to the
redundant copying seen in the second prototype-less constructor.
You can see a performance comparison of the two instantiation
methods at http://jsperf.com/prototype-speeds.
Use prototypes intelligently to reduce memory usage in your objects and to lower
instantiation times. Determine the static or infrequently changed attributes and
methods of your objects and put those into prototypes. This will allow you to create
thousands of objects quickly, while reducing redundancy.
[ 105 ]
Any key in a Redis database can store (2^32 - 1) bits or just under 512 MiB. This
means that there are approximately 4.29 billion columns, or offsets, that can be set per
key. This is a large number of data points referenced by a single key. We can set bits
along these ranges to describe the characteristics of an item we would like to track,
such as the number of users who have viewed a given article. Furthermore, we can use
bit operations to gather other dimensions of information, such as what percentage of
viewers of an article are female. Let's look at a few examples.
This key represents article 324 on a specific date, efficiently storing the unique user
IDs of viewers on that day by flipping a bit at an offset corresponding to the user's
assigned ID. Whenever a user views an article, fetch that user's ID, use that number
as an offset value, and use the setbit command to set a bit at that offset:
redis.setbit('article:324:01-03-2014', userId, 1)
In what follows, we're going to demonstrate how to use Redis bitops to efficiently
store and analyze data. First, let's create data for three articles:
var redis = require('redis');
var client = redis.createClient();
var multi = client.multi();
// Create three articles with randomized hits representing user views
var id = 100000;
while(id--) {
multi.setbit('article1:today', id, Math.round(Math.random(1)));
multi.setbit('article2:today', id, Math.round(Math.random(1)));
multi.setbit('article3:today', id, Math.round(Math.random(1)));
}
multi.exec(function(err) {
// done
})
[ 106 ]
Chapter 4
Here, we simply created three Redis keys, 'article (1-3):today', and randomly
set 100,000 bits on each keyeither 0 or 1. Using the technique of storing user
activity based on user ID offsets, we now have sample data for a hypothetical day of
traffic against three articles.
We're using Matt Ranney's node_redis module (https://
github.com/mranney), which supports the Redis multi construct,
allowing the execution of several instructions in one pipeline rather
than suffering the cost of calling each individually. Always use
multi when performing several operations in order to speed up
your operations. Note also how the ordering guarantees provided by
Redis ensure ordered execution and how its atomicity guarantees that
either all or none of the instructions in a transaction will succeed. See
http://redis.io/topics/transactions.
To count the number of users who have viewed an article, we can use bitcount:
client.bitcount('article1:today', function(err, count) {
console.log(count)
})
This is straightforward: the number of users who saw the article equals the number
of bits set on the key. Now, let's count the total number of article views:
client.multi([
["bitcount", "article1:today"],
["bitcount", "article2:today"],
["bitcount", "article3:today"]
]).exec(function(err, totals) {
var total = totals.reduce(function(prev, cur) {
return prev + cur;
}, 0);
console.log("Total views: ", total);
})
These are very useful and direct ways to glean information from bit representations.
Let's go a little further and learn about filtering bits using bitmasks and the AND,
OR, and XOR operators.
First, we create a mask that isolates a specific user stored at the key 'user123',
containing a single positive bit at offset 123 (again, representing the user's ID). The
results of an AND operation on two or more bit representations is not returned as a
value by Redis but rather written to a specified key, which is given in the preceding
example as '123:sawboth'. This key contains the bit representation that answers
the question whether both the article keys contain bit representations that also have a
positive bit at the same offset as the 'user123' key.
What if we wanted to find the total number of users who have seen at least one
article? The bitop OR works well in this case:
client.multi([
['bitop', 'OR','atleastonearticle','article1:today',
'article2:today','article3:today'],
['bitcount', 'atleastonearticle']
]).exec(function(err, results) {
console.log("At least one: ", results[1]);
});
[ 108 ]
Chapter 4
Here, the 'atleastonearticle' key flags bits at all offsets that were set in any one
of the three articles.
We can use these techniques to create a simple recommendation engine. For
example, if we are able to determine via other means that two articles are similar
(based on tags, keywords, and so on), we can find all users that have read one and
recommended the other. To do this, we will use XOR in order to find all users that
have read the first article or the second article, but not both. We then break that set
into two lists: those who have read the first article and those who have read the
second article. We can then use these lists to offer recommendations:
client.multi([
['bitop','XOR','recommendother','article1:today',
'article2:today'],
['bitop','AND','recommend:article1','recommendother',
'article2:today'],
['bitop','AND','recommend:article2','recommendother',
'article1:today'],
['bitcount', 'recommendother'],
['bitcount', 'recommend:article1'],
['bitcount', 'recommend:article2'],
['del', 'recommendother', 'recommend:article1',
'recommend:article2']
]).exec(function(err, results) {
// Note result offset due to first 3 setup ops
console.log("Didn't see both articles: ", results[3]);
console.log("Saw article2; recommend article1: ", results[4]);
console.log("Saw article1; recommend article2: ", results[5]);
})
While it is not necessary, we also fetch a count of each list and delete the result keys
when we are done.
The total number of bytes occupied by a binary value in Redis is calculated by dividing
the largest offset by 8. This means that storing access data for even 1,000,000 users on
one article requires 125 KBnot a lot. If you have 1,000 articles in your database, you
can store full-access data for 1,000,000 users in 125 MBagain, not a very large amount
of memory or storage to spend in return for such a rich set of analytics data. Also, the
amount of storage needed can be precisely calculated ahead of time.
View the code bundle for an example of building a like this page
service, where we use a bookmarklet to trigger likes on any URL
using bit operations to store the time at which each like occurs
(offsetting by the current second on a given day).
[ 109 ]
Other useful ways to deploy bitwise ideas are easy to find. Consider that if
we allocate 86,400 bits to a key (the number of seconds in a day) and set a bit
corresponding to the current second in the day, whenever a particular action is
performed (such as a login), we have spent 86400 / 8 / 1000 = 10.8 KB to store login
data that can easily be filtered using bitmasks to deliver analytics data.
As an exercise, use bitmasks to demonstrate gender breakdown in article readership.
Assume that we have stored two keys in Redis, one reflecting the user IDs identified
as female and the other as male:
users:female : 00100001011000000011110010101...
users:male : 11011110100111111100001101010...
[ 110 ]
Chapter 4
You have inserted the value 123 into a HyperLogLog key, and the number returned
(1) is the cardinality of that key's set. Click on the same button a few times given
that this structure maintains a count of unique values, the number should not change.
Now, try adding random values. You will see the numbers returned go up. Regardless
of how many entries you make in the log key, the same amount of memory will be
used. This sort of predictability is great when scaling out your application.
You can find the index.html page describing this client interface in the code
bundle. All that the client needs to do is send an XHR request to localhost:8080/
log/<some value>. Feel free to browse the code. More to the point, let's look at how
the relevant route handler is defined on the server to insert values into HyperLogLog
and retrieve log cardinality:
var
var
var
var
http
= require('http');
redis = require('redis');
client = redis.createClient();
hyperLLKey = 'hyper:uniques';
...
http.createServer(function(request, response) {
var route
= request.url;
[ 111 ]
= route.match(/^\/log\/(.*)/);
...
if(val) {
val = val[1];
return client.pfadd(hyperLLKey, val, function() {
client.pfcount(hyperLLKey, function(err, card) {
respond(response, 200, JSON.stringify({
count: err ? 0 : card
}))
})
});
}
}).listen(8080)
After validating that we have received a new value on the /log route, we add that
value to hyperLLKey using the PFADD command (in Redis, if a key does not exist
when performing an insert operation, it is automatically created). Once inserted
successfully, the key is queried for its PFCOUNT, and the updated set's cardinality is
returned to the client.
In addition, the PFMERGE command lets you merge (create the union of) several
HyperLogLog sets and fetch the cardinality of the resulting set. The following code
will result in a cardinality value of 10:
var redis = require('redis');
var client= redis.createClient();
var multi = client.multi();
client.multi([
['pfadd', 'merge1', 1, 2, 3, 4, 5, 6, 10],
['pfadd', 'merge2', 1, 2, 3, 4, 5, 6, 7, 8, 9],
['pfmerge', 'merged', 'merge1', 'merge2'],
['pfcount', 'merged'],
['del', 'merge1', 'merge2', 'merged']
]).exec(function(err, result) {
console.log('Union set cardinality', result[3]);
});
The ability to approximate the cardinality of merged sets brings to mind the sort of
efficient analytics possibilities we saw when exploring bitwise operations. Consider
HyperLogLog when counts of many unique values are useful analytically and an
imprecise but very closely approximated count is sufficient (such as tracking the
number of users who logged in today, the total number of pages viewed, and so on).
[ 112 ]
Chapter 4
Optimizing JavaScript
The convenience of a dynamic language is in avoiding the strictness that compiled
languages impose. For example, you need not explicitly define object property
types and can actually change those property types at will. This dynamism makes
traditional compilation impossible but opens up interesting new opportunities for
exploratory languages, such as JavaScript. Nevertheless, dynamism introduces
a significant penalty in terms of execution speeds when compared to statically
compiled languages. The limited speed of JavaScript has regularly been identified as
one of its major weaknesses.
V8 attempts to achieve the sorts of speeds with JavaScript that one observes for
compiled languages. V8 attempts to compile JavaScript into native machine code
rather than interpreting bytecode or using other just-in-time techniques. Because the
precise runtime topology of a JavaScript program cannot be known ahead of time
(the language is dynamic), compilation consists of a two-stage, speculative approach:
1. Initially, a first-pass compiler converts your code into a runnable state
as quickly as possible. During this step, type analysis and other detailed
analysis of the code is deferred, achieving fast compilationyour JavaScript
can begin executing as close to instantly as possible. Further optimizations
are accomplished during the second step.
[ 113 ]
[ 114 ]
Chapter 4
If you try to run this normally, you receive an Unexpected Token errorthe modulo
(%) symbol cannot be used within an identifier name in JavaScript. What is this strange
method with a % prefix? It is a V8 native command, and we can turn to the execution of
these types of functions using the --allow-natives-syntax flag as follows:
node --allow-natives-syntax program.js
// foo
Now, consider the following code, which uses native functions to assert
information about the optimization status of the square function using the
%OptimizeFunctionOnNextCall native method:
var operand = 3;
function square() {
return operand * operand;
}
// Make first pass to gather type information
square();
// Ask that the next call of #square trigger an optimization attempt;
// Call
%OptimizeFunctionOnNextCall(square);
square();
Create a file using the preceding code and execute it using the following command:
node --allow-natives-syntax --trace_opt --trace_deopt myfile.js
[ 115 ]
We can see that V8 has no problem optimizing the square function as the operand is
declared once and never changed. Now, append the following lines to your file and
run it again:
%OptimizeFunctionOnNextCall(square);
operand = 3.01;
square();
On this execution, following the optimization report given earlier, you should now
receive something like the following output:
**** DEOPT: square at bailout #2, address 0x0, frame size 8
[deoptimizing: begin 0x2493d0fca8d9 square @2]
...
[deoptimizing: end 0x2493d0fca8d9 square => node=3, pc=0x29edb8164b46,
state=NO_REGISTERS, alignment=no padding, took 0.033 ms]
[removing optimized code for: square]
This very expressive optimization report tells the story very clearlythe
once-optimized square function was de-optimized following the change we
made in one number's type. You are encouraged to spend time writing code
and to test it using these methods now and as you move through this section.
[ 116 ]
Chapter 4
Avoid mixing types in arrays. It is always better to have a consistent data type,
such as all integers or all strings. Also, avoid changing types in arrays or in property
assignments after initialization, if possible. V8 creates blueprints of objects by creating
hidden classes to track types, and, when those types change, the optimization
blueprints will be destroyed and rebuiltif you're lucky. See the following link for
more information:
https://developers.google.com/v8/design
Sparse arrays are bad for this reason: V8 can either use a very efficient linear storage
strategy to store (and access) your array data, or it can use a hash table (which is
much slower). If your array is sparse, V8 must choose the less efficient of the two.
For the same reason, always start your arrays at the zero index. Also, don't ever use
delete to remove elements from an array. You are simply inserting an undefined
value at that position, which is just another way of creating a sparse array. Similarly,
be careful about populating an array with empty valuesensure that the external
data you are pushing into an array is not incomplete.
Try not to pre-allocate large arraysgrow as you go. Similarly, do not pre-allocate
an array and then exceed that size. You always want to avoid spooking V8 into
turning your array into a hash table.
V8 creates a new hidden class whenever a new property is added to an object
constructor. Try to avoid adding properties after an object is instantiated. Initialize
all members in constructor functions in the same order. Same properties + same order =
same object.
Remember that JavaScript is a dynamic language that allows object (and object
prototype) modifications after instantiation. Since the shape and volume of an object
can, therefore, be altered after the fact, how does V8 allocate memory for objects? It
makes certain reasonable assumptions. After a set number of objects is instantiated
from a given constructor (I believe 8 is the trigger number), the largest of these is
assumed to be of the maximum size, and all further instances are allocated that
amount of memory (and the initial objects are similarly resized). A total of 32 fast
property slots is then allocated to each instance based on this assumed maximum size.
Any extra properties are slotted into a (slower) overflow property array that can be
resized to accommodate any further new properties.
[ 117 ]
With objects, just as with arrays, try as much as possible to define the shape of your
data structures in a futureproof manner, with a set number of properties, types, and
so on.
Functions
Functions are typically called often and should be one of your prime optimization
focuses. Functions containing try-catch constructs are not optimizable, nor are
functions containing other unpredictable constructs, such as with and eval. If, for
some reason, your function is not optimizable, keep its use to a minimum.
A very common optimization error involves the use of polymorphic functions.
Functions that accept variable function arguments will be de-optimized. Avoid
polymorphic functions.
Caching strategies
Caching, generally, is the strategy of creating easily accessible intermediate versions
of assets. When retrieving an asset is expensivein terms of time, processor cycles,
memory, and so onyou should consider caching that asset. For example, if a list
of Canadian provinces must be fetched from your database each time a person from
that country visits, it is a good idea to store that list in a static format, obviating
the expensive operation of running a database query on each visit. A good caching
strategy is essential to any web-based application that serves large numbers of
rendered data views, be they HTML pages or JSON structures. Cached content can
be served cheaply and quickly.
Whenever you deploy content that doesn't change often, you most likely want to
cache your files. Two general types of static assets are commonly seen. Assets such
as a company logo, existing as-is in a content folder, will change very rarely. Other
assets do change more often but much less frequently than on every request of the
asset. This second class encompasses such things as CSS style sheets, lists of user
contacts, latest headlines, and so on. Creating a reliable and efficient caching system
is a nontrivial problem:
"There are only two hard things in Computer Science: cache invalidation and
naming things."
Phil Karlton
[ 118 ]
Chapter 4
In this section, we'll look at two strategies to cache your application content. First,
we'll look at using Redis as an in-memory key-value cache for regularly used JSON
data, learning about the Redis key expiry and key scanning. Finally, we'll investigate
how to manage your content using the CloudFlare content delivery network (CDN),
in the process learning something about using Node to watch for file changes and
then invalidating a CDN cache when change events are detected.
Typically, our caching layer will be decoupled from any given server, so here we
design a constructor that expects Redis's connection and authentication information.
Note the prefix argument. To instantiate a cache instance, use the following code:
var cache = new Cache({ prefix: 'articles:cache' });
[ 119 ]
Also note that we're going to implement the cache API using Promises via the
bluebird library (https://github.com/petkaantonov/bluebird).
Getting a cached value is straightforward:
Cache.prototype.get = function(key) {
key = this.prefix + key;
var client = this.client;
return new Promise(function(resolve, reject) {
client.hgetall(key, function(err, result) {
err ? reject() : resolve(result);
});
});
};
All cache keys will be implemented as Redis hashes, so a GET operation will involve
calling hmget on a key. The Promises-powered API now enables the following easyto-follow syntax:
cache.get('sandro').then(function(val) {
console.log('cached: ' + val);
}).catch() {
console.log('unable to fetch value from cache');
})
[ 120 ]
Chapter 4
When val is received, we reflect its key-value map in the Redis hash stored at key.
The optional third argument, ttl, allows a flag to be set in Redis to expire this key
after a number of seconds, flagging it for deletion. The key bit of code in this.
expire is the following:
client.expire(key, ttl, function(err, ok) { // ...flagged for
removal }
Note the scan method we are using to target and delete keys matching our cache
prefix. Redis is designed for efficiency, and, as much as possible, its designers aim
to avoid adding slow features. Unlike other databases, Redis has no advanced find
method of searching its keyspace, with developers limited to keys and basic glob
pattern matching. Because it's common to have many millions of keys in a Redis
keyspace, operations using keys, unavoidably or through sloppiness, can end up
being punitively expensive because a long operation blocks other operations
transactions are atomic, and Redis is single-threaded.
[ 121 ]
The scan method allows you to fetch limited ranges of the keyspace in an iterative
manner, enabling (nonblocking) asynchronous keyspace scanning. The scan object
itself is stateless, passing only a cursor indicating whether there are further records
to be fetched. Using this technique, we are able to clean out all keys prefixed with
our target cache key (pattern: this.prefix + '*'). On each scan iteration, we
queue up any returned keys for deletion using the multi.del function, continuing
until the scanner returns a zero value (indicating that all sought keys have been
returned), at which point we delete all those keys in one command.
Tie these methods together:
cache.set('deploying', { foo: 'bar' })
.then(function() {
return cache.get('deploying');
})
.then(function(val) {
console.log(val); // foo:bar
return cache.clear();
})
.then(cache.close.bind(cache));
This is a simple caching strategy to get you started. While managing key expiration
yourself is a perfectly valid technique, as you move into larger production
implementations, consider configuring Redis's eviction policies directly. For example,
you will want to set the maxmemory value in redis.conf to some maximum upper
bound for the cache memory and configure Redis to use one of the six documented
eviction policies when memory limits are reached, such as Least Recently Used (LRU).
For more information, visit: http://redis.io/topics/lru-cache.
[ 122 ]
Chapter 4
In our example, we will serve (and modify) a single index.html file. For this
example, we will create a simple server:
var indexFile = './index.html';
http.createServer(function(request, response) {
var route = request.url;
if(route === "/index.html") {
response.writeHead(200, {
"content-type": "text/html",
"cache-control": "max-age=31536000"
[ 123 ]
Note how max-age is set on the cache-control header. This will indicate to
CloudFlare that we want this file cached.
With the server set up, we will now add the following purge method:
function purge(filePath, cb) {
var head = config.protocol + '://';
var tail = config.domain + '/' + filePath;
// foo.com && www.foo.com each get a purge call
var purgeFiles = [
head + tail,
head + config.subdomain + '.' + tail
];
var purgeTrack = 2;
purgeFiles.forEach(function(pf) {
cloudflareClient.zoneFilePurge(config.domain, pf,
function(err) {
(--purgeTrack === 0) && cb();
});
});
};
When this method is passed a file path, it asks CloudFlare to purge its cache of this
file. Note how we must use two purge actions to accommodate subdomains.
With purging set up, all that is left to do is watch the filesystem for changes. This can
be accomplished via the fs.watch command:
fs.watch('./index.html', function(event, filename) {
if(event === "change") {
purge(filename, function(err) {
console.log("file purged");
});
}
});
[ 124 ]
Chapter 4
Now, whenever the index.html file is changed, our CDN will flush its
cached version. Create that file, start up the server, and point your browser to
localhost:8080, bringing up your index file. In your browser's developer console,
inspect the response headersyou should see a CF-Cache-Status: MISS record.
This means that CloudFlare (CF) has fetched and served the original file from your
serveron the first call, there is no cached version yet, so the cache was missed.
Reload the page. The same response header should now read CF-Cache-Status:
HIT. Your file is cached!
Go ahead and change the index file in some way. When you reload your browser,
the changed version will be displayedits cached version has been purged, the file
has been fetched once again from your server, and you will see the MISS header
value again.
You will want to expand this functionality to include a larger group of files
and folders. To learn more about fs.watch, go to http://nodejs.org/api/
fs.html#fs_fs_watch_filename_options_listener.
Managing sessions
The HTTP protocol is stateless. Any given request has no information about previous
requests. For a server, this means that determining whether two requests originated
from the same browser is not possible without further work. That's fine for general
information, but targeted interactions require a user to be verified via some sort of
unique identifier. A uniquely identified client can then be served targeted content
from lists of friends to advertisements.
This semipermanent communication between a client (often a browser) and a server
persists for a period of timeat least until the client disconnects. That period of time
is understood as a session. An application that manages sessions must be able to
create a unique user session identifier, track the activity of an identified user during
that session, and disconnect that user when requested or for some other reason, such
as on reaching a session limit.
In this section, we'll implement a JSON Web Token (JWT) system for session
management. JWT's have an advantage over traditional cookie-based sessions in that
they do not require the server to maintain a session store as JWTs are self-contained.
This greatly helps with deployments and scaling. They are also mobile friendly and
can be shared between clients. While a new standard, JWTs should be considered as
a simple and scalable session storage solution for your applications.
[ 125 ]
One particular advantage of JWTs is that servers are no longer responsible for
maintaining access to a common database of credentials as only the issuing authority
needs to validate an initial signin. There is no need to maintain a session store when
you are using JWTs. The issued token (think of it as an access card) can, therefore,
be used within any domain (or server) that recognizes and accepts it. In terms of
performance, the cost of a request is now the cost of decrypting a hash versus the
cost of making a database call to validate credentials. We also avoid the problems we
can face using cookies on mobile devices, such as cross-domain issues (cookies are
domain-bound), certain types of request forgery attacks, and so on.
Let's look at the structure of a JWT and build a simple example demonstrating how
to issue, validate, and otherwise use JWTs to manage sessions.
A JWT token has the following format:
<base64-encoded header>.<base64-encoded claims>.<base64-encoded
signature>
Chapter 4
A header simply describes the tokenits type and encryption algorithm. Take the
following code as an example:
{
"typ":"JWT",
"alg":"HS256"
}
Here, we declare that this is a JWT token, which is encrypted using HMAC SHA-256.
See http://nodejs.org/api/crypto.html for more
information about encryption and how to perform encryption
with Node. The JWT specification itself can be found at http://
self-issued.info/docs/draft-ietf-oauth-json-webtoken.html. Note that the JWT specification is in a draft state at
the time of writing this, so changes may be made in the future.
The claims segment outlines security and other constraints that should be checked
by any service receiving the JWT. Check the specification for a full accounting.
Typically, a JWT claims manifest will want to indicate when the JWT was issued,
who issued it, when it expires, who the subject of the JWT is, and who should accept
the JWT:
{
"iss" : "http://blogengine.com",
"aud" : ["http://blogsearch.com", "http://blogstorage"],
"sub" : "blogengine:uniqueuserid",
"iat" : "1415918312",
"exp" : "1416523112",
"sessionData" : "<some data encrypted with secret>"
}
The iat (issued-at) and exp (expires) claims are both set to numeric values indicating
the number of seconds since the Unix epoch. The iss (issuer) should be a URL
describing the issuer of the JWT. Any service that receives a JWT must inspect the
aud (audience), and that service must reject the JWT if it does not appear in the
audience list. The sub (subject) of the JWT identifies the subject of the JWT, such as
the user of an applicationa unique value that is never reassigned, such as the name
of the issuing service and a unique user ID.
Finally, useful data is attached using a key-value pairing of your choice. Here,
let's call the token data sessionData. Note that we need to encrypt this datathe
signature segment of a JWT prevents tampering with session data, but JWTs are not
themselves encrypted (you can always encrypt the entire token itself though).
[ 127 ]
We'll implement the server code next. For now, note that we have a send method
that expects, at some point, to have a global token set for it to pass along when
making requests. The initial /login is where we ask for that token.
[ 128 ]
Chapter 4
Using the Express web framework, we create the following server and /login route:
var express = require('express');
...
var jwt = require('jwt-simple');
var app = express();
app.set('jwtSecret', 'shhhhhhhhh');
app.post('/login', auth, function(req, res) {
var nowSeconds
= Math.floor(Date.now()/1000);
var plus7Days
= nowSeconds + (60 * 60 * 24 * 7);
var token = jwt.encode({
"iss" : "http://blogengine.com",
"aud" : ["http://blogsearch.com", "http://blogstorage"],
"sub" : "blogengine:uniqueuserid",
"iat" : nowSeconds,
"exp" : plus7Days
}, app.get('jwtSecret'));
res.send({
token : token
})
})
Note that we store jwtsecret on the app server. This is the key that is used when
we are signing tokens. When a login attempt is made, the server will return the result
of jwt.encode, which encodes the JWT claims discussed previously. That's it. From
now on, any client that mentions this token to the correct audience will be allowed
to interact with any services those audience members provide for a period expiring
7 days from the date of issue. These services will implement something like the
following code:
app.post('/someservice', function(req, res) {
var token = req.get('Authorization').replace('Bearer ', '');
var decoded = jwt.decode(token, app.get('jwtSecret'));
var now = Math.floor(Date.now()/1000);
if(now > decoded.exp) {
return res.end(JSON.stringify({
error : "Token expired"
}));
}
res.send(<some sort of result>);
})
[ 129 ]
Here, we are simply fetching the Authorization header (stripping out Bearer) and
decoding via jwt.decode. A service must at least check for token expiry, which we
do here by comparing the current number of seconds from the epoch to the token's
expiry time.
Using this simple framework, you can create an easily scalable authentication/session
system using a secure standard. No longer required to maintain a connection to a
common credentials database, individual services (deployed perhaps as microservices)
can use JWTs to validate requests, incurring little CPU latency or memory cost.
Summary
We covered a lot of ground in this chapter. Best practices for writing efficient
JavaScript that the V8 interpreter can handle properly were outlined, including
an exploration of garbage collection, the advantages of Node streams, and how
JavaScript prototypes should be deployed in order to save memory. Continuing with
the theme of reducing storage costs, we explored various ways in which Redis can
help with storing large amounts of data in a space-efficient way.
Additionally, we looked at strategies to build composable, distributed systems. In the
discussion on microservices, we touched on approaches to network many individual
services and build the networks they can use to communicate with each otherfrom
pub/sub to Seneca's pattern and action models. Joined with the examples of caching
techniques, a reasonably complete picture of the issues you might want to consider
when planning out resource management in your application was established.
After you build up a reasonably sophisticated architecture, it becomes more and
more necessary to build probes and other monitoring tools to stay on top of what
is going on. In the next chapter, we'll build tools to help you trace the changing
topology of running applications.
[ 130 ]
www.PacktPub.com
Stay Connected: