00320b169a9c66d728bfe185d86de2944a54423d
[ndcode_site.git] / blog / 20220114 / index.html.jst
1 return async env => {
2   let post = await _require('/_lib/post.jst') 
3
4   await post(
5     env,
6     // head
7     async _out => {},
8     // body
9     async _out => {
10       p {'Normally I would write a blog post coincident with the release of a new version of the public website you are reading; in this case there is some heavy refactoring going on and so I have decided to just document the process of the work and let it be viewed and interacted with later when ready for release.'}
11
12       h4 {'NoSQL discussion'}
13
14       p {'One of the early decisions I made when teaching myself web development was that I would not be using SQL for the backend. I simply believe the disadvantages outweigh the advantages for what I am trying to do. And I’m probably not the only person with this view, because the NoSQL movement has been gaining traction.'}
15
16       p {
17         'SQL pros:'
18         ul {
19           li {'Complex queries are possible: range queries, joins, and the like.'}
20           li {'SQL server can serve multiple webservers, increasing scalability.'}
21         }
22       }
23
24       p {
25         'SQL cons:'
26         ul {
27           li {'Deployment is a hassle, need to make sure the SQL server process is configured and running before launching the webserver process.'}
28           li {'SQL queries are submitted as code and need to be tokenized and parsed at the server for every request, which is not efficient.'}
29           li {'Administration has a steep learning curve, for example you cannot just view or edit the database file—you need to craft queries for troubleshooting and activities like migration or structure upgrades.'}
30           li {'Relational database capabilities are overkill for many trivial applications, such as storing a username against a session key.'}
31         }
32       }
33
34       p {'I want my application to be as self-contained and self-configuring as possible, so basically I would just check out the website’s repository on the intended Virtual Private Server (VPS) and launch it. In a traditional configuration you have a long list of other services to configure first, such as PHP, PHP-FPM, MySQL, etc, etc, and setting up or migrating to a new server could be days of work with tedious troubleshooting. There are of course cases where these activities have a payoff (such as if you require a highly scalable website), but my case is not one of them.'}
35
36       h4 {'First try—all JSON approach'}
37
38       p {
39         'Therefore, for a first cut I simply stored my website’s data as JSON files in a hidden folder of the site’s directory. The hidden folder and JSON files were created as required if not already existent. And a few sophisticated features were built around this to make things workable:'
40         ul {
41           li {'After first access, each JSON file was cached in RAM for the lifetime of the webserver process.'}
42           li {'After modification, the JSON files were not written back immediately, but a timer was set to guarantee modified data was written in 5 seconds or less.'}
43           li {'Writeback was done to a temporary file followed by a renaming step, and if the site crashed or lost power during renaming it could recover.'}
44         }
45       }
46
47       p {
48         'Admittedly, I could have constructed a similar system using '
49         tt {'SQLite'}
50         ', and I would then have access to complex queries which I do not with JSON, but I felt the added complexity was not worthwhile. If I need to do complex queries, I will simply write them in Javascript, including the maintenance of any indices needed to carry out the queries efficiently. This will give me complete control, rather than simply hoping the SQL server optimizes the indices and queries well.'
51       }
52
53       p {'My JSON database was sufficient for building the prototype blog and online store that you can interact with now, but it did suffer from a serious problem which is that upon successful launch, there could be thousands of customers and/or orders and the database could potentially grow to a huge size. Then it would not be feasible to hold the entire file in memory or write it every 5 seconds.'}
54
55       h4 {'Second try—log-structured JSON approach'}
56
57       p {'To solve the scalability issue, my plan has always been to allow the JSON tree to be read into memory lazily as required (and cached, with some kind of cache policy implemented), and for modifications to be written to the end of the file. Thus the file would be log-structured and simply keep growing forever, except for the intervention of a daily cleaning routine which would copy all the live (still reachable) parts of the log into a new log to reduce the size.'}
58
59       p {
60         'The database I created with this approach is called '
61         tt {'LogJSON'}
62         '. It means a database that appears to the calling code like a huge JSON tree, but one which can be accessed efficiently, and which has transaction ability to avoid concurrent updates corrupting the tree. The log-structuring is more or less hidden from the calling code, which provides the illusion of a mutable tree, even though data once written is immutable in the backend.'
63       }
64
65       p {
66         'Why log-structured? Mainly for simplicity and efficiency. If you allow deletion in a data store, then you now have to track empty space and you have fragmentation to worry about. In a filesystem such as '
67         tt {'ext4'}
68         ', a good deal of the complexity arises from this. Another example of an allocator with free-space management is '
69         tt {'malloc()'}
70         ' in C. But '
71         tt {'malloc()'}
72         ' is slow, as it needs to scan the free list at each allocation (at least in the traditional implementation), and memory-inefficient, as it does not support fragmented allocations in the way that '
73         tt {'ext4'}
74         ' does.'
75       }
76
77       p {
78         'A more efficient approach to dealing with fragmentation and dead objects, that was popularized with object-oriented languages like '
79         tt {'Java'}
80         ' and '
81         tt {'Smalltalk'}
82         ', is to allocate new objects contiguously from a region of spare memory instead of scanning for a free spot every time, and perform a periodic garbage collection to reclaim free space. It was controversial at the time because of garbage collection pauses, but if you can perform garbage collection concurrently with normal processing, there should be no significant impact.'
83       }
84
85       p {'For my application, it seemed simpler to implement a daily copy from old to new log than to deal with free-space management. And there are certain other benefits, such as having a series of snapshots of the database to aid in trouble-shoooting. I also liked the idea of the log being human-readable (traditional for web developers, who like ASCII formats such as JSON rather than binary formats), and this would have complicated the free-space management considerably.'}
86
87       h4 {
88         tt {'LogJSON'}
89         ' implementation'
90       }
91
92       h5 {'Root object'}
93
94       p {
95         'The structure of the log is extraordinarily simple, it basically just writes the new root JSON object to the end of the log each time, encoded by '
96         tt {'Buffer.from(JSON.stringify(...), \'utf-8\')'}
97         ', and then surrounded by angle brackets ‘'
98         tt {'<'}
99         '’ and ‘'
100         tt {'>'}
101         '’. The purpose of the angle brackets is to let us resynchronize to the latest written root object after the system is rebooted (including for power loss) or the server is restarted. Here is a very simple example of a '
102         tt {'LogJSON'}
103         ' file:'
104       }
105
106       pre {
107         `<"Hello, world">
108 <"Go away">
109 `
110       }
111
112       p {
113         'In the above example, two transactions were undertaken on an empty file. The first one initialized the root to '
114         tt {'"Hello, world"'}
115         ' (a Javascript string, which is valid JSON), the second one overwrite the root with '
116         tt {'"Go away"'}
117         ' (also valid JSON). Now suppose the server had crashed during the writing of the second transaction, leaving the file looking something like:'
118       }
119
120       pre {
121         `<"Hello, world">
122 <"Go aw`
123       }
124
125       p {
126         'Then, upon restarting the server, it would resynchronize to the last occurrence of ‘'
127         tt {'>'}
128         '’ that is followed by a newline, and the root object would appear to be '
129         tt {'"Hello, world"'}
130         '. The requirement that ‘'
131         tt {'>'}
132         '’ be followed by a newline, prevents it being tricked by occurrences of ‘'
133         tt {'>'}
134         '’ inside strings, where newline has to be encoded as ‘'
135         tt {'\\n'}
136         '’ rather than a newline character itself.'
137       }
138
139       h5 {'References to objects'}
140
141       p {'Whilst the above method of writing the root object allows for a log-structured JSON file and transaction-based atomic updates to that file, we still need a method to allow lazy access to the required parts of the tree and to allow updates without rewriting the entire tree. This is done by having a special way of writing the JSON '
142         tt {'Array'}
143         ' and '
144         tt {'Object'}
145         ' types (basically those JSON elements that can contain further JSON).'
146       }
147
148       p {
149         'We will use Array as an example. Suppose a root element of '
150         tt {'["Hello", "Goodbye"]'}
151         ' needs to be written in an empty file. It will be done in two parts. Firstly, the element itself will be written, without angle brackets. And then a new root element will be written, with angle brackets, that contains a '
152         i {'reference'}
153         ' to the just-written element, rather than the full text of the element itself:'
154       }
155  
156       pre {
157         `[
158   "Hello",
159   "Goodbye"
160 ]
161 <[
162   0,
163   26
164 ]>
165 `
166       }
167
168       p {
169         'A reference is always constructed as a 2-element list, here '
170         tt {'[0, 26]'}
171         ', containing the file pointer and length of the previously written JSON.'
172       }
173
174       p {'So in the above example, it’s possible to open the file, synchronize to the root element using the angle brackets, and then read the root element as a reference to an unread portion of the file, without actually having to read the contents of the root element until they are required.'}
175
176       h5 {'Nested objects'}
177
178       p {
179         'To present a more realistic example, we will consider a deeper tree. The root of the tree will be a JSON '
180         tt {'Object'}
181         ' type which is keyed by a person\'s nickname. This in turn leads to a JSON '
182         tt {'Object'}
183         ' type containing some details about the person. The plain JSON file to be encoded in LogJSON format is:'
184       }
185
186       pre {
187         `{
188   "jane": {
189     "name": "Jane Doe",
190     "address": "123 Main St, Smallville",
191     "age": 30
192   },
193   "mike": {
194     "name": "Mike Rho",
195     "address": "125 Main St, Smallville",
196     "age": 32
197   }
198 }
199 `
200       }
201
202       p {'And the encoded version in LogJSON is:'}
203
204       pre {
205         `{
206   "name": "Jane Doe",
207   "address": "123 Main St, Smallville",
208   "age": 30
209 }
210 {
211   "name": "Mike Rho",
212   "address": "125 Main St, Smallville",
213   "age": 32
214 }
215 {
216   "jane": [
217     0,
218     77
219   ],
220   "mike": [
221     78,
222     77
223   ]
224 }
225 <[
226   156,
227   65
228 ]>
229 `
230       }
231
232       p {
233         'We can see that the process of looking up a user can now be more efficient than plain JSON. First we read the root object '
234         tt {'[156, 65]'}
235         ' and the referred-to object containing the keys '
236         tt {'"jane"'}
237         ' and '
238         tt {'"mike"'}
239         '. Then, we find the person we are looking for, let’s say Mike, and the reference '
240         tt {'[78, 77]'}
241         ' to Mike’s record. And we only need to read Mike’s record to proceed, we do not need Jane’s record at this stage. We do know there is a user '
242         tt {"jane"}
243         ', but her details are irrelevant to the current query.'
244       }
245
246       h5 {'Modifying the tree'}
247
248       p {
249         'Now suppose Mike’s address is changing. Recalling that the '
250         tt {'LogJSON'}
251         ' file, once written, is immutable, we need to write a '
252         i {'new'}
253         ' record for Mike containing the changed address, as follows:'
254       }
255
256       pre {
257         `{
258   "name": "Mike Rho",
259   "address": "20 Another St, Smallville",
260   "age": 32
261 }
262 `
263       }
264
265       p {
266         'And then we need to rewrite the parent object so that the nickname '
267         tt {'"jane"'}
268         ' will still point to her original record, but the nickname '
269         tt {'"mike"'}
270         ' will redirect to his newly written record:'
271       }
272
273       pre {
274         `{
275   "jane": [
276     0,
277     77
278   ],
279   "mike": [
280     240,
281     79
282   ]
283 }
284 `
285       }
286
287       p {'Finally, we need to rewrite the root object to point to the newly written subtree:'}
288
289       pre {
290         `<[
291   320,
292   66
293 ]>
294 `
295       }
296
297       p {'And of course the file will not be considered modified (the old root will be active) until the new root has been fully written.'}
298
299       h5 {'Modified tree‒full example'}
300
301       p {
302         'After the '
303         tt {'LogJSON'}
304         ' file creation and then modification described in the previous section, the entire file looks like this:'
305       }
306
307       pre {
308         `{
309   "name": "Jane Doe",
310   "address": "123 Main St, Smallville",
311   "age": 30
312 }
313 {
314   "name": "Mike Rho",
315   "address": "125 Main St, Smallville",
316   "age": 32
317 }
318 {
319   "jane": [
320     0,
321     77
322   ],
323   "mike": [
324     78,
325     77
326   ]
327 }
328 <[
329   156,
330   65
331 ]>
332 {
333   "name": "Mike Rho",
334   "address": "20 Another St, Smallville",
335   "age": 32
336 }
337 {
338   "jane": [
339     0,
340     77
341   ],
342   "mike": [
343     240,
344     79
345   ]
346 }
347 <[
348   320,
349   66
350 ]>
351 `
352       }
353
354       p {
355         'We can see that it contains a snapshot of how the file looked after each modification, since if you read the previous root element '
356         tt {'[156, 65]'}
357         ' it would lead to Mike’s old address, but if you read the latest root element '
358         tt {'[320, 66]'}
359         ' it would lead to Mike’s new address. In either case Jane’s address will be the same. A disadvantage of this scheme is that the supporting elements such as the '
360         tt {'Object'}
361         ' containing the list of nicknames, and the root, must be rewritten each time, which wastes space. But the garbage collector tidies this periodically.'
362       }
363
364       h4 {'Database maintenance'}
365
366       p {
367         'Although the database is human-readable, it’s still somewhat difficult to follow, especially given that file viewers generally do not display the file positions in decimal alongside the text. Therefore, for database maintenance, you can use the tools '
368         tt {'logjson_to_json'}
369         ' and '
370         tt {'json_to_logjson'}
371         ' provided within the proposed '
372         tt {'node.js'}
373         ' '
374         tt {'LogJSON'}
375         ' package. These tools give you a manual way to view and edit the tree, and perform garbage collection if you need to‒but note that they cannot be run on a live database, you need to stop your webserver first.'
376       }
377
378       p {
379         'For example, running '
380         tt {'logjson_to_json'}
381         ' on the complete file of the previous example gives this JSON:'
382       }
383
384       pre {
385         `{
386   "jane": {
387     "name": "Jane Doe",
388     "address": "123 Main St, Smallville",
389     "age": 30
390   },
391   "mike": {
392     "name": "Mike Rho",
393     "address": "20 Another St, Smallville",
394     "age": 32
395   }
396 }
397 `
398       }
399
400       p {
401         'And running it back through '
402         tt {'json_to_logjson'}
403         ' into a clean log file gives a compacted version of the log:'
404       }
405
406       pre {
407         `{
408   "name": "Jane Doe",
409   "address": "123 Main St, Smallville",
410   "age": 30
411 }
412 {
413   "name": "Mike Rho",
414   "address": "20 Another St, Smallville",
415   "age": 32
416 }
417 {
418   "jane": [
419     0,
420     77
421   ],
422   "mike": [
423     78,
424     79
425   ]
426 }
427 <[
428   158,
429   65
430 ]>
431 `
432       }
433
434       h4 {'Garbage collection'}
435
436       p {'I have implemented the garbage collection in a rather sophisticated way, taking advantage of the immutability of the log file once written in order that garbage collection can proceed in the background without disturbing the operation of the website.'}
437
438       p {'A garbage collection is triggered every midnight in Universal Time Coordinated (UTC), that is, when the date rolls over. A new log file is constructed by copying all the live data from the old log file into it. And then transaction processing is briefly paused whilst we atomically rename the old log file out of the way to a dated name (contains the year, month and day in UTC) and rename the new log file over the old one. If the system crashes during renaming it will recover at the next restart.'}
439
440       p {'The synchronization magic happens by atomically grabbing the current root object and copying everything reachable from it into the new log file, but allowing more transactions to be written into the old log file simultaneously. It then atomically grabs the updated root and copies that as well, and continues to do so until both files are up-to-date and it can perform the atomic replacement.'}
441
442       h4 {'Concurrency'}
443
444       p {
445         'For the time being I have made transactions to the database mutually exclusive, by implementing a mutex in the running webserver. Much improvement would be possible here, for instance by allowing read transactions to be concurrent and only using the mutex for write transactions, or by an even more sophisticated concurrency management that can consider different elements or keys of a JSON '
446         tt {'Array'}
447         ' or '
448         tt {'Object'}
449         ' as not conflicting.'
450       }    
451
452       p {'Currently there is a bit of a risk that if I do not implement good try/catch handling in my code, then a webserver request will crash, leaving the webserver running but the mutex in ‘held’ state. This would hang the webserver, requiring a manual restart. I could implement some sort of disaster recovery, but there is probably no point at this time, I will simply use try/catch and make sure transactions are rolled back if not completed.'}
453  
454       h4 {'Conclusion'}
455
456       p {'The log-structured format provides a useful way of maintaining huge JSON files. It remains to complete the documentation and release the source code and NPM package. It also remains to implement a stress test of my website, once up and running, to simulate thousands of user accounts and multiple logins to the same account all performing transactions simultaneously. Therefore, things are still experimental at this stage. I enjoy writing this blog to update on those experiments.'}
457     },
458     // scripts
459     async _out => {}
460   )
461 }