Caches used in NodeBB
-
This will be a post about the various caches in NodeBB and how they work.
What is caching?
There are 4 different caches, I will go over each one and explain how they work and how they help make nodebb faster. But before that let's remember what a cache is and how they help:
In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere. A cache hit occurs when the requested data can be found in a cache, while a cache miss occurs when it cannot. Cache hits are served by reading data from the cache, which is faster than recomputing a result or reading from a slower data store; thus, the more requests that can be served from the cache, the faster the system performs.
To be cost-effective and to enable efficient use of data, caches must be relatively small. Nevertheless, caches have proven themselves in many areas of computing, because typical computer applications access data with a high degree of locality of reference. Such access patterns exhibit temporal locality, where data is requested that has been recently requested already, and spatial locality, where data is requested that is stored physically close to data that has already been requested.
Source: https://en.wikipedia.org/wiki/Cache_(computing)
Cache Module used in NodeBB
All the caches in nodebb use the nodejs module https://www.npmjs.com/package/lru-cache,
A cache object that deletes the least-recently-used items.
It let's you cache things and drop things out of the cache if they are not used frequently.Let's go chronologically and see what caches there are and why they were added.
Post Cache
(added in 2015)
Out of all the caches this one was the most obvious. When a user types a post the content of that post is stored as is in the database. In NodeBB's case this is markdown but other formats can be used as well. Storing the text as the user typed it makes it easy to implement things like editing, since we can just display the same content in the composer. But when we need to display the post as part of the webpage we need to convert it into html. This process happens in :
Posts.parsePost = async function (postData) { if (!postData) { return postData; } postData.content = String(postData.content || ''); const cache = require('./cache'); const pid = String(postData.pid); const cachedContent = cache.get(pid); if (postData.pid && cachedContent !== undefined) { postData.content = cachedContent; return postData; } const data = await plugins.hooks.fire('filter:parse.post', { postData: postData }); data.postData.content = translator.escape(data.postData.content); if (data.postData.pid) { cache.set(pid, data.postData.content); } return data.postData; };
To turn the post content into html the hook
filter:parse.post
is used bynodebb-plugin-markdown
https://github.com/NodeBB/nodebb-plugin-markdown/blob/master/index.js#L152-L158. This is done entirely in the nodebb process and blocks the cpu since it is not an async IO operation. When displaying a topic we do this for 20 posts(1 page) and if 20 users are loading the same topic we are doing 400 parses to generate the same output. If the posts are longer the process takes more time as well.So it was a no-brainer to cache the parsed content since it rarely changes and we can just display the cached html content. As you can see from the first piece of code if the post content is cached
filter:parse.post
isn't fired and we don't spend time in the plugins.This cache makes topic pages faster and reduces the cpu usage of the nodebb process.
Group Cache
(added in 2016)
Next up is the group cache, this cache is used in the groups module and caches all group membership checks.
Groups.isMember = async function (uid, groupName) { if (!uid || parseInt(uid, 10) <= 0 || !groupName) { return false; } const cacheKey = `${uid}:${groupName}`; let isMember = Groups.cache.get(cacheKey); if (isMember !== undefined) { return isMember; } isMember = await db.isSortedSetMember(`group:${groupName}:members`, uid); Groups.cache.set(cacheKey, isMember); return isMember; };
When you load any page in NodeBB there is almost always a privilege check to see if you can see certain content or perform certain actions. Can this user see all categories? Can this user post a reply to this topic? All of these questions are answered by checking if the user is part of a specific group.
After the initial setup of the forum group membership rarely changes so it was a perfect candidate for caching.
Unlike the post cache which lowers the cpu usage of the nodebb process, this cache reduces the calls made to the database. Instead of making one or more database calls on every navigation we just make them once and store the results.
Object Cache
(added in 2017)
The reason why this cache is named
Object cache
is because it caches the results ofdb.getObject(s)
calls.module.getObjectsFields = async function (keys, fields) { if (!Array.isArray(keys) || !keys.length) { return []; } const cachedData = {}; const unCachedKeys = cache.getUnCachedKeys(keys, cachedData); let data = []; if (unCachedKeys.length >= 1) { data = await module.client.collection('objects').find( { _key: unCachedKeys.length === 1 ? unCachedKeys[0] : { $in: unCachedKeys } }, { projection: { _id: 0 } } ).toArray(); data = data.map(helpers.deserializeData); } const map = helpers.toMap(data); unCachedKeys.forEach((key) => { cachedData[key] = map[key] || null; cache.set(key, cachedData[key]); }); if (!Array.isArray(fields) || !fields.length) { return keys.map(key => (cachedData[key] ? { ...cachedData[key] } : null)); } return keys.map((key) => { const item = cachedData[key] || {}; const result = {}; fields.forEach((field) => { result[field] = item[field] !== undefined ? item[field] : null; }); return result; }); };
Every time we load a page we make calls to the database to load the post/topic/user/category objects. These all use the call
db.getObjects
. Since these objects rarely change and are requested frequently it makes sense to cache these as well.This lowers the load on the database significantly. Once a topic has been accessed most of the data required will be in the cache for anyone else loading the same topic.
Local Cache
(added in 2018)
The last cache is a bit different than the others as it was added to cache different types of things and can be used from plugins as well. I will give two examples of how this is used in nodebb to make pages faster.
The first one is the list of categories. Whenever someone loads the /categories page we load a list of category ids. Since the list of categories rarely changes we cache it in the local cache.
Categories.getAllCidsFromSet = async function (key) { let cids = cache.get(key); if (cids) { return cids.slice(); } cids = await db.getSortedSetRange(key, 0, -1); cids = cids.map(cid => parseInt(cid, 10)); cache.set(key, cids); return cids.slice(); };
Another use of this cache is for plugin settings. Whenever a plugin loads settings with
await meta.settings.get('mypluginid');
the result is loaded and cached.Settings.get = async function (hash) { const cached = cache.get(`settings:${hash}`); if (cached) { return _.cloneDeep(cached); } const [data, sortedLists] = await Promise.all([ db.getObject(`settings:${hash}`), db.getSetMembers(`settings:${hash}:sorted-lists`), ]); const values = data || {}; await Promise.all(sortedLists.map(async (list) => { const members = await db.getSortedSetRange(`settings:${hash}:sorted-list:${list}`, 0, -1); const keys = members.map(order => `settings:${hash}:sorted-list:${list}:${order}`); values[list] = []; const objects = await db.getObjects(keys); objects.forEach((obj) => { values[list].push(obj); }); })); const result = await plugins.hooks.fire('filter:settings.get', { plugin: hash, values: values }); cache.set(`settings:${hash}`, result.values); return _.cloneDeep(result.values); };
Out of all the caches used this one will have the highest hit rate as you can see from the screenshot. Because the things stored in this cache pretty much never change during regular operation, once the list of categories is loaded it will always be served from the cache unless you add or reorder categories.
ACP Page
(/admin/advanced/cache)
On the ACP cache page you can enable/disable the cache, see the hit rate for each cache and even download the contents of the cache as json.
You can also empty the cache here, this is sometimes useful if you have to make changes to the database directly via CLI, for example if you make a change in the database to a topic title and if that topic is cached you won't see the changes until you clear the
object cache
.Hope this answers any questions about the caches in NodeBB!
-
It was also informative for me, since a lot of the caching work was done independent by @baris. It is a huge benefit to have someone so dedicated to efficiency that someone like myself can just use the appropriate
src/api
(or even directly access library methods) and know that the call has been pre-optimized. -
@baris I have a question about the local cache.
It reads to me that requiring
./src/cache
gives me access to the local cache that I can sort of treat like a catch-all bucket whose oldest entries are dropped when full.So I would just throw all my stuff in there and hope for the best.
When would be a good time to implement my own cache via
./src/cache/lru
or./src/cache/ttl
? If I want more fine-grained control over the size of the cache and want to ensure my staler entries don't get dropped as often? Any other considerations? -
If you are going to store a lot of stuff I would use
./src/cacheCreate
to create my own,./src/cache
is for a few stuff that never changes like the list of categories etc. If you put a lot of your own stuff in there like urls for link-previews it will negatively effect the performance. For stuff like that just create your own lru cache. -
-
-
@baris thanks for these info
So, this is how you implemented the fast loading of the total vote count of the topics?
Total vote count on topic list
@phenomlab maybe try with below /* eslint-disable no-await-in-loop */ /* globals require, console, process */ 'use strict'; const nconf = require('nconf')...
NodeBB Community (community.nodebb.org)