Synchronous File IO in Node.js
Posted by Dave Eddy on Mar 26 2013 - tags: techDoes calling
fs.writeFileSync
trigger a synchronous write to the file system?
If you are familiar with Node.js, or have at least heard of it, you’ve most likely heard that it uses non-blocking IO, and lets you do work asynchronously. One of the most basic APIs that Node provides is for the file system; With this API, you can read, write, remove, etc. files and do other file system related tasks and modifications.
This API follows a standard pattern of exposing 2 functions for each operation: one for asynchronous work, and the other for synchronous work. For example, if you want to read a file in Node you can do so asynchronously:
var fs = require('fs');
fs.readFile('/etc/passwd', function(err, buf) {
console.log(buf.toString());
});
Node will continue executing any javascript code it encounters while reading the file. Once all javascript is done being executed and the file is ready, it will run the anonymous function and print the file contents.
You can do the same task synchronously:
var fs = require('fs');
var contents = fs.readFileSync('/etc/passwd').toString();
console.log(contents);
In this example, contents
will be set to the contents of the file, and no
javascript code will be executed while the file is being read.
The first approach is done asynchronously, and will return immediately to not block your code from running. The second is done synchronously, and will halt execution until the task has completed. The same 2 types of functions exist for writing, renaming, deleting, etc. files.
Synchronous Writes
So the question is, does calling fs.writeFileSync
actually trigger a
synchronous write to the file system? In the userland Node process, it’s
synchronous in the sense that execution of any javascript is halted, but what
about in the Kernel? An asynchronous write is a very different thing from a
synchronous write to a file system.
For the rest of this blog post I’ll be speaking within the context of the Illumos Kernel, and the ZFS File System.
There are a couple ways to answer this question. The most obvious way is to
pull the Node.js source code, find the functions that talk to the file
system that fs.js
uses, and see how they are called. I haven’t done much
work on the Node core, and know it could (and most likely would) take a long time
to find the code I was looking for. Instead, I’ll just use DTrace to
answer the question, and see exactly what Node is doing.
DTrace to the Rescue
I wrote a couple test programs that exercise these file system functions. Using DTrace, we’ll be able to see what flags a file is opened with, which will show whether the operations are synchronous or not.
fs.writeFile()
// writefile.js
var fs = require('fs');
fs.writeFile('/tmp/fs.tmp', '', function(err) {
if (err) throw err;
});
This script exercises Node’s asynchronous file writing mechanism. Using
DTrace, we can print the flags that were passed to open(2)
for that specific
file. Then, using fileflags, we can turn that decimal into the
symbolic names that make up the decimal (see open(2)
for more information).
$ sudo dtrace -qn 'syscall::open*:entry /pid == $target && copyinstr(arg0) == "/tmp/fs.tmp"/ { printf("%s: %d", probefunc, arg1); }' -c 'node writefile.js'
open64: 769
$ fileflags 769
769: O_WRONLY|O_CREAT|O_TRUNC
The first command tells DTrace to run node writefile.js
, and look for any of
the open family of syscalls. If the first argument to open (the pathname)
matches the file we are writing to, print out the exact syscall fired, and the
flags decimal.
It turns out that open64(2)
was called for our file, given the following
options.
O_WRONLY
: open write-onlyO_CREAT
: create the file if it doesn’t existO_TRUNC
: truncate the file
Fairly standard options to open a file. Since none of the options are for
synchronous IO (O_SYNC
, O_DSYNC
, etc.) this file write is asynchronous to
ZFS, and the call to write(2)
returns before the data is guaranteed to be
sitting on stable storage.
Node’s asynchronous fs.writeFile
does indeed do an asynchronous write to the
file system.
fs.writeFileSync()
So what about Node’s synchronous file writing mechanism, is it an actual synchronous write to the file system?
// writefilesync.js
var fs = require('fs');
fs.writeFileSync('/tmp/fs.tmp', '');
This script will block the event loop while the data is written to the file (or so we think), as it uses Node’s synchronous file writing mechanism.
$ sudo dtrace -qn 'syscall::open*:entry /pid == $target && copyinstr(arg0) == "/tmp/fs.tmp"/ { printf("%s: %d", probefunc, arg1); }' -c 'node writefilesync.js'
open64: 769
$ fileflags 769
769: O_WRONLY|O_CREAT|O_TRUNC
Same commands as above, and the same output.
Node’s fs.writeFileSync
does NOT initiate a synchronous write to the file
system.
From the perspective of a Node program, we know the same thing when a call to
fs.writeFileSync
returns, as we know when the callback to fs.writeFile
is
fired. We know the underlying call, write(2)
has returned; We do NOT know
that the data has made it to stable storage. The only difference then, is that
one function blocks Node’s event loop, while the other allows it to continue
processing events.
fs.createWriteStream()
Another mechanism that allows file IO is to create, and write to, a
Node WritableStream
.
// writestream.js
var fs = require('fs');
fs.createWriteStream('/tmp/fs.tmp');
$ sudo dtrace -qn 'syscall::open*:entry /pid == $target && copyinstr(arg0) == "/tmp/fs.tmp"/ { printf("%s: %d", probefunc, arg1); }' -c 'node writestream.js'
open64: 769
$ fileflags 769
769: O_WRONLY|O_CREAT|O_TRUNC
Same output as above, again. This mechanism opens the file with the same flags
as both fs.writeFile
and fs.writeFileSync
.
fs.appendFile()
So writing to a file uses the same flags for opening the file, what about appending? Same drill as above
// apendfile.js
var fs = require('fs');
fs.appendFile('/tmp/fs.tmp', '', function(err) {
if (err) throw err;
});
$ sudo dtrace -qn 'syscall::open*:entry /pid == $target && copyinstr(arg0) == "/tmp/fs.tmp"/ { printf("%s: %d", probefunc, arg1); }' -c 'node appendfile.js'
open64: 265
$ fileflags 265
265: O_WRONLY|O_APPEND|O_CREAT
So the flags are different, that’s a good sign. O_TRUNC
has been swapped out
for O_APPEND
, since we are no longer truncating the file to 0 bytes and
instead are appending to it.
Again, like all the commands above, fs.appendFile
opens the file for
asynchronous IO.
fs.appendFileSync()
Last but not least let’s test out the synchronous version of appendFile
.
// appendfilesync.js
var fs = require('fs');
fs.appendFileSync('/tmp/fs.tmp', '');
$ sudo dtrace -qn 'syscall::open*:entry /pid == $target && copyinstr(arg0) == "/tmp/fs.tmp"/ { printf("%s: %d", probefunc, arg1); }' -c 'node appendfilesync.js'
open64: 265
$ fileflags 265
265: O_WRONLY|O_APPEND|O_CREAT
Same as fs.appendFile
; the file is NOT opened for synchronous writes.
Common Flags
Let’s use a simple C program to open a file using fopen(3C)
to see what flags
it uses.
/* fs.c */
#include <stdio.h>
int main(int argc, char **argv) {
FILE *file = fopen("/tmp/fs-c.tmp", "w");
}
Then run it with the same command as above to see what flags the file was opened with.
$ cc fs.c -o fs
$ sudo dtrace -qn 'syscall::open*:entry /pid == $target && copyinstr(arg0) == "/tmp/fs-c.tmp"/ { printf("%s: %d", probefunc, arg1); }' -c './fs'
open: 769
$ fileflags 769
769: O_WRONLY|O_CREAT|O_TRUNC
Sure enough, the same flags as opening a file for writing in Node land.
Results
fs.writeFileSync
is synchronous in the sense that it blocks the event loop
while it executes. It does NOT ask the Kernel to do a synchronous write to the
underlying file system.
This script will block the event loop while the data is written to the file (or so we think)…
None of the functions above open files for synchronous IO. Because of this,
all we know is that the call to write(2)
returns, not that the data has been
written to the file system and flushed to stable storage. Don’t get tripped
up on the names, fs.writeSync
doesn’t synchronously write to the file system.
If you want to open a file for synchronous IO, you’ll have to use the lower
level fs functions that Node offers such as fs.open()
and fs.fsync()
.