forked from snxraven/ravenscott-blog
209 lines
8.1 KiB
Markdown
209 lines
8.1 KiB
Markdown
<!-- lead -->
|
||
Finding an efficient way to cache data into memory.
|
||
|
||
# Building a Fast and Efficient Minecraft Server Cache Manager in Node.js
|
||
|
||
In this article, we will take an in-depth look at how to build a cache manager in Node.js to drastically improve the performance of your application, specifically for listing Minecraft servers. Without caching, the response time for listing all the servers was over 2 minutes due to multiple heavy computations and I/O operations.
|
||
|
||
With the introduction of a caching mechanism, this time was reduced to an impressive 2 milliseconds. Let’s dive into the code and understand how this was achieved.
|
||
|
||
## Overview of the System
|
||
|
||
The purpose of this system is to manage a list of Minecraft servers efficiently. The core functionality includes fetching server information, such as the server's MOTD (Message of the Day), online status, game version, and operational details (e.g., ops, whitelist, banned players).
|
||
|
||
The data is sourced from Docker containers, local file systems, and remote authentication services.
|
||
|
||
### Problem Statement
|
||
|
||
When you’re dealing with numerous Minecraft servers, querying real-time server data can become very expensive in terms of performance.
|
||
|
||
Each query requires:
|
||
|
||
- Accessing container information via Docker.
|
||
- Checking server online status via network requests.
|
||
- Fetching MOTD and other server metadata.
|
||
- Interacting with the file system to fetch or generate tokens.
|
||
|
||
These operations, especially when scaled to multiple servers, can significantly slow down the response time. Without caching, the requests took over 2 minutes to process.
|
||
|
||
This system mitigates those delays by introducing an efficient in-memory cache.
|
||
|
||
## The Caching Mechanism
|
||
|
||
The system employs a caching layer that stores various pieces of server information (MOTD, online status, etc.) in memory.
|
||
|
||
This avoids repeated heavy I/O and network operations, thus drastically reducing the request time.
|
||
|
||
### Key Components of the Cache
|
||
|
||
1. **MOTD Cache:** Stores the Message of the Day for each server.
|
||
2. **Online Status Cache:** Stores whether a server is online or offline.
|
||
3. **Token Cache:** Stores authentication tokens for server owners.
|
||
4. **Container Info Cache:** Caches Docker container details for each server.
|
||
5. **Ops, Whitelist, Banned Players Caches:** Caches server-specific administrative data.
|
||
|
||
These caches are refreshed periodically and stored in an in-memory object, reducing the need for redundant calls to external systems.
|
||
|
||
## Code Walkthrough: The Cache Manager
|
||
|
||
### Directory and File Structure
|
||
|
||
- **Cache Directory:** The `cacheDir` (in this case, `/home/cache`) stores JSON files that provide metadata for each Minecraft server.
|
||
|
||
Each file corresponds to a specific server and includes connection details and server-specific configuration data.
|
||
|
||
```javascript
|
||
const cacheDir = "/home/cache";
|
||
const cache = {
|
||
motd: {},
|
||
online: {},
|
||
token: {},
|
||
containerInfo: {},
|
||
ops: {},
|
||
whitelist: {},
|
||
whitelistEnable: {},
|
||
banned: {}
|
||
};
|
||
```
|
||
|
||
The `cache` object holds the in-memory data for all servers, avoiding the need to repeatedly fetch this data from external sources.
|
||
|
||
### Server Listing Function
|
||
|
||
Here’s the code for listing all servers, including how caching is used to optimize performance:
|
||
|
||
```javascript
|
||
exports.list_all_servers = async function (req, res) {
|
||
let secret_key = req.params["secret_key"];
|
||
|
||
if (secret_key !== config.secret_key) {
|
||
return res.status(401).json({ success: false, error: "Unauthorized." });
|
||
}
|
||
|
||
try {
|
||
const files = await fs.promises.readdir(cacheDir);
|
||
let jumpnode_servers = [];
|
||
|
||
for (const file of files) {
|
||
const filePath = path.join(cacheDir, file);
|
||
const data = await jsonfile.readFile(filePath);
|
||
const serverName = path.basename(file, path.extname(file)).replace('.mc', '');
|
||
|
||
// Skip certain servers
|
||
if (serverName.includes(".link")) {
|
||
continue;
|
||
}
|
||
|
||
// Fetch data from cache or fallback to fetching from the source
|
||
const connectString = data.connect;
|
||
const motd = cache.motd[connectString]?.value ?? null;
|
||
const online = cache.online[connectString]?.value ?? false;
|
||
const token = cache.token[serverName]?.value ?? null;
|
||
const containerInfo = cache.containerInfo[serverName]?.value ?? null;
|
||
const ops = cache.ops[serverName]?.value ?? null;
|
||
const whitelist = cache.whitelist[serverName]?.value ?? null;
|
||
const whitelistEnable = cache.whitelistEnable[serverName]?.value ?? null;
|
||
const banned = cache.banned[serverName]?.value ?? null;
|
||
|
||
if (!containerInfo) {
|
||
continue; // Skip servers with no container info
|
||
}
|
||
|
||
// Server information is stored in jumpnode_servers array
|
||
const serverInfo = {
|
||
serverName,
|
||
gameVersion: containerInfo.Config.Image.split(':').pop(),
|
||
connect: data.connect,
|
||
ownersToken: token,
|
||
ops,
|
||
whitelist,
|
||
whitelistEnable,
|
||
banlist: banned,
|
||
motd,
|
||
online
|
||
};
|
||
|
||
jumpnode_servers.push(serverInfo);
|
||
}
|
||
|
||
return res.json({ success: true, servers: jumpnode_servers });
|
||
} catch (err) {
|
||
console.error("Error reading cache directory:", err);
|
||
return res.status(500).json({ success: false, error: err.message });
|
||
}
|
||
};
|
||
```
|
||
|
||
This function first checks the secret key for authentication. If the secret key is valid, it proceeds to list all servers.
|
||
|
||
The server data is retrieved from the in-memory cache, avoiding the need to re-fetch the data from slow external sources like Docker, network connections, and the file system.
|
||
|
||
### Caching Core Functions
|
||
|
||
#### Fetching MOTD
|
||
|
||
The function `fetchMOTD` sends a custom status request packet to the server and waits for its response.
|
||
|
||
The result is cached, so future requests can serve the MOTD immediately without querying the server again.
|
||
|
||
```javascript
|
||
async function fetchMOTD(connectString) {
|
||
const client = new net.Socket();
|
||
const serverPort = connectString.split(':')[1];
|
||
|
||
// Sends the packet to get the MOTD
|
||
client.connect(serverPort, 'my-mc.link', () => {
|
||
client.write(Buffer.from([0xFE, 0x01]));
|
||
});
|
||
|
||
// Process and cache the response
|
||
client.on('data', (data) => {
|
||
const response = data.toString('utf8').split('\x00\x00\x00');
|
||
if (response.length >= 6) {
|
||
const motd = response[3].replace(/\u0000/g, '');
|
||
cache.motd[connectString] = { value: motd, timestamp: Date.now() };
|
||
}
|
||
client.destroy();
|
||
});
|
||
}
|
||
```
|
||
|
||
#### Fetching Container Info
|
||
|
||
The `fetchContainerInfo` function interacts with Docker to fetch container details for each Minecraft server.
|
||
|
||
Dockerode is used to communicate with the Docker API. The result is cached in memory to avoid repeatedly querying Docker for container data.
|
||
|
||
```javascript
|
||
async function fetchContainerInfo(serverName) {
|
||
const container = docker.getContainer(serverName);
|
||
const containerInfo = await container.inspect();
|
||
cache.containerInfo[serverName] = { value: containerInfo, timestamp: Date.now() };
|
||
}
|
||
```
|
||
|
||
#### Other Cache Functions
|
||
|
||
Similar functions exist for fetching online status, tokens, ops, whitelist, and banned player lists, all of which follow the same caching pattern.
|
||
|
||
## Cache Update Strategy
|
||
|
||
The cache is updated by periodically calling the `updateCache` function, which refreshes the cache every 60 seconds:
|
||
|
||
```javascript
|
||
setInterval(updateCache, 60000);
|
||
```
|
||
|
||
This ensures that the cache is kept relatively up-to-date while still providing the performance benefits of avoiding repeated heavy I/O operations.
|
||
|
||
The cache holds both the value and a timestamp, allowing future improvements like time-based cache invalidation if needed.
|
||
|
||
## Findings
|
||
|
||
By introducing an in-memory caching system, this application was able to reduce request times from over 2 minutes to just 2 milliseconds.
|
||
|
||
This system efficiently caches key server data, eliminating the need to repeatedly query external services like Docker, network services, and file systems.
|
||
|
||
This approach is especially useful in applications with high I/O overhead and network latency, allowing for faster and more responsive interactions.
|
||
|
||
This caching strategy could be adapted and extended further, for instance by adding cache expiration policies or integrating Redis for distributed caching in a multi-node architecture. |