mirror of
https://github.com/wekan/wekan.git
synced 2026-11-06 05:52:33 -05:00
Index card_comment_reactions, and document why MongoDB's data directory must be local.
From a reported MongoDB crash series (.tools/crash: WeKan and MongoDB in one
Kubernetes pod, the database volume on an SMB/DFS share):
- The crash itself is WiredTiger's checkpoint fsync() returning ENOSPC
("No space left on device" with 519 MB used and 10 GiB free) on the
network share, followed by a fatal assertion and abort - every checkpoint,
so a restart loop. That is the storage, not WeKan: MongoDB requires a
local filesystem with real fsync semantics under its data directory.
docs/Databases/MongoDB/Storage-Requirements.md now says so, shows what
the log looks like when it is not, and what to do (local block volume;
attachments may still live on a share; MONGO_URL over TCP is fine).
- The WeKan-side finding in the same logs: card_comment_reactions had no
index at all, and every board open queried it by `cardId: { $in: [...] }`
and by `boardId` as a full collection scan - 3,397 COLLSCANs of that one
collection in the last crash log, up to 1.6 s each, the bulk of its
"Slow query" lines. server/models/cardCommentReactions.js now ensures
indexes on cardId, boardId and cardCommentId at startup, like every other
per-card collection.
tests/cardCommentReactionsIndexes.test.cjs pins both.
Thanks to xet7 !
This commit is contained in:
1 parent
a50ddc52b2
commit
03ffb56a5e
5 files changed
+165
No files matched your search
@@ -10,6 +10,7 @@ is the compose file for it. The default database of WeKan is
|
||||
| [Driver-System.md](Driver-System.md) | Which Node driver is used for which server version, and how that is decided |
|
||||
| [Version-Management.md](Version-Management.md) | Detecting the server version and switching to the matching binary + driver |
|
||||
| [Compatibility-Guide.md](Compatibility-Guide.md) | What breaks between MongoDB 3.0 and 8.0 with Meteor, and the fixes |
|
||||
| [Storage-Requirements.md](Storage-Requirements.md) | Why the data directory must be on a local filesystem, and what an SMB/NFS-backed one does (fsync ENOSPC aborts, slow queries) |
|
||||
| [Oplog-Configuration.md](Oplog-Configuration.md) | Why the OpLog matters for pub/sub, and how to turn it on |
|
||||
| [OpLog-Enablement.md](OpLog-Enablement.md) | Whether it IS on, per deployment platform |
|
||||
| [avx-qemu.md](avx-qemu.md) | Running MongoDB 5+ on a CPU with no AVX, through qemu-user |
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Where MongoDB's data directory may live
|
||||
|
||||
MongoDB's WiredTiger storage engine needs a **local, POSIX-compliant
|
||||
filesystem** under its data directory (`/data/db` in the containers). It does
|
||||
not work reliably on a network filesystem - SMB/CIFS, DFS, NFS, or a cloud
|
||||
file share mounted as one - even when the share is fast, has plenty of free
|
||||
space, and works fine for ordinary files.
|
||||
|
||||
## What it looks like when it is not
|
||||
|
||||
A reported crash series (WeKan and MongoDB in one Kubernetes pod, the
|
||||
MongoDB volume connected over SMB to a DFS storage server) looked like this
|
||||
in the MongoDB log, repeatedly, until the process aborted:
|
||||
|
||||
```
|
||||
"c":"WT","msg":"WiredTiger error message","attr":{"error":28,"message":{...,
|
||||
"msg":"__posix_sync:94:/data/db/WiredTiger.turtle: ... No space left on device"}}
|
||||
"c":"ASSERT","msg":"Fatal assertion","attr":{"msgid":50853,
|
||||
"file":"src/mongo/db/storage/wiredtiger/wiredtiger_util.cpp"}}
|
||||
"msg":"***aborting after fassert() failure"
|
||||
"msg":"Got signal: 6 (Aborted)."
|
||||
```
|
||||
|
||||
The share reported **519 MB used with 10 GiB free**, so "no space left" is
|
||||
not about space. Error 28 (`ENOSPC`) here comes back from `fsync()` - the
|
||||
call WiredTiger makes on every checkpoint to be sure a write has really
|
||||
reached stable storage. An SMB/CIFS mount cannot honour that the way a local
|
||||
disk does, and the failure surfaces as this errno. Because a checkpoint that
|
||||
cannot be made durable is a database that cannot be trusted, MongoDB
|
||||
deliberately aborts rather than continue - which is why it comes back up and
|
||||
crashes again on the next checkpoint.
|
||||
|
||||
The same log had thousands of **"Slow query"** lines with
|
||||
`"planningTimeMicros"` around 200,000 (0.2 s to *plan* a query that touched
|
||||
nine index keys). That is the storage latency again: every read of an index
|
||||
page went over the share. Fixing the storage fixes those too. (One WeKan-side
|
||||
cause of slow queries on that log, a missing index on the comment-reactions
|
||||
collection, was fixed in WeKan as well - see the CHANGELOG - but an indexed
|
||||
query still cannot be fast on storage this slow.)
|
||||
|
||||
## What WeKan reports about it
|
||||
|
||||
Admin Panel / Problems / Database problems gets a row for each of these,
|
||||
from a probe that runs at startup and every five minutes:
|
||||
|
||||
| Row | What it means |
|
||||
| --- | --- |
|
||||
| `db.restart` | The database process restarted while WeKan kept running - a crash-and-restart loop like the one above. |
|
||||
| `db.network-filesystem` | The data directory is on a CIFS/SMB, NFS or FUSE filesystem. Only when WeKan can see that directory itself (the snap, the bundle with its embedded database, or a shared volume). |
|
||||
| `db.slow-storage` | Reads averaged over 100 ms across the interval - storage latency, the "Slow query" symptom. |
|
||||
| `db.disk-space` | The filesystem under the data directory is below 5% or 512 MiB free - the real "no space left on device", before it happens. |
|
||||
| `db.index-created` | WeKan found a collection with data but without an index it queries by, and created it. Fixed automatically; nothing to do. |
|
||||
|
||||
The first four are reported with what to do; the last is WeKan's own
|
||||
remediation (a missing index was the WeKan-side cause of slow queries in the
|
||||
report above).
|
||||
|
||||
## What to do
|
||||
|
||||
- Put MongoDB's data directory on a **local block volume**: a Kubernetes
|
||||
PersistentVolume backed by a block storage class (a cloud disk, Ceph RBD,
|
||||
Longhorn, local-path, ...), a Docker named volume on the host's local disk,
|
||||
or a directory on a local filesystem (ext4, xfs).
|
||||
- Do not mount the data directory from SMB/CIFS, DFS, NFS, or a file-share
|
||||
storage class. If that is the only storage available, run MongoDB
|
||||
somewhere it can have a local disk and point WeKan at it over the network
|
||||
with `MONGO_URL` - the database connection over TCP is fine; it is only the
|
||||
database's *files* that must be local.
|
||||
- **Attachments are different.** WeKan's attachment files (when stored on
|
||||
the filesystem rather than in the database) are ordinary files and may
|
||||
live on a network share; only MongoDB's own data directory may not.
|
||||
- After moving: restore from a
|
||||
[backup](../../Backup/Backup.md) or copy the data directory while MongoDB is
|
||||
stopped, then run `mongod --repair` once if the last shutdown was one of
|
||||
these aborts ([Repair-MongoDB.md](../../Backup/Repair-MongoDB.md)).
|
||||
|
||||
MongoDB's own production notes say the same: WiredTiger requires a filesystem
|
||||
with `fsync()` semantics (they list ext4 and xfs), and they explicitly warn
|
||||
against remote filesystems for the data files.
|
||||
|
||||
## See also
|
||||
|
||||
- [The MongoDB pages](.) - the rest of them
|
||||
- [../FerretDB/1](../FerretDB/1) - the default database; its SQLite backend
|
||||
has the same requirement (SQLite also needs a local filesystem)
|
||||
@@ -122,6 +122,7 @@ import '/server/models/org';
|
||||
import '/server/models/rules';
|
||||
import '/server/models/settings';
|
||||
import '/server/models/swimlanes';
|
||||
import '/server/models/cardCommentReactions';
|
||||
import '/server/models/team';
|
||||
import '/server/models/translation';
|
||||
import '/server/models/users';
|
||||
@@ -211,6 +212,7 @@ import '/server/lib/tenantResolver';
|
||||
import '/server/lib/ssrfGuard';
|
||||
import '/server/lib/ddpSessionSendGuard';
|
||||
import '/server/lib/databaseProblems';
|
||||
import '/server/lib/databaseHealth';
|
||||
// What the FILESYSTEM said, and whether this server stopped cleanly last time
|
||||
// (docs/Security/Remediation/WeKan.md §13). Both write to the 'integrity' event
|
||||
// stream, shown in Admin Panel / Problems / Filesystem integrity.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import CardCommentReactions from '/models/cardCommentReactions';
|
||||
import { ensureIndex } from '/server/lib/mongoStartup';
|
||||
|
||||
// card_comment_reactions had no index at all. Every board open reads it by
|
||||
// `cardId: { $in: [...all the board's card ids] }` (the card publication) and
|
||||
// by `boardId` (the board publication), and both were full collection scans:
|
||||
// a reported MongoDB log from one production board (.tools/crash) had 3,397
|
||||
// COLLSCANs of this collection in a single log, up to 1.6 s each, on a slow
|
||||
// storage backend - the bulk of that server's "Slow query" lines. The same
|
||||
// three keys every other per-card collection is indexed on.
|
||||
Meteor.startup(async () => {
|
||||
await ensureIndex(CardCommentReactions, { cardId: 1 });
|
||||
await ensureIndex(CardCommentReactions, { boardId: 1 });
|
||||
await ensureIndex(CardCommentReactions, { cardCommentId: 1 });
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
// Regression guard for the WeKan-side finding in a reported MongoDB crash log
|
||||
// (.tools/crash): card_comment_reactions had no index at all, and every board
|
||||
// open queried it by `cardId: { $in: [...] }` (the card publication) and by
|
||||
// `boardId` (the board publication) as a full collection scan - 3,397
|
||||
// COLLSCANs of that one collection in a single log, up to 1.6 s each. The
|
||||
// crash itself (WiredTiger checkpoint fsync returning ENOSPC on an SMB/DFS
|
||||
// mount) is a storage problem, documented in
|
||||
// docs/Databases/MongoDB/Storage-Requirements.md rather than fixable in code.
|
||||
//
|
||||
// Source-read test: pins the startup indexes, that the server module is in the
|
||||
// import list (this app has an explicit server mainModule - a file not
|
||||
// imported there never runs), and that the storage page exists and is linked.
|
||||
//
|
||||
// Run: node tests/cardCommentReactionsIndexes.test.cjs
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const read = rel => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) { fn(); passed += 1; console.log(' ok -', name); }
|
||||
|
||||
console.log('cardCommentReactionsIndexes:');
|
||||
|
||||
test('card_comment_reactions gets cardId, boardId and cardCommentId indexes at startup', () => {
|
||||
const src = read('server/models/cardCommentReactions.js');
|
||||
assert.ok(/import CardCommentReactions from '\/models\/cardCommentReactions'/.test(src));
|
||||
assert.ok(/import \{ ensureIndex \} from '\/server\/lib\/mongoStartup'/.test(src));
|
||||
for (const key of ['cardId', 'boardId', 'cardCommentId']) {
|
||||
assert.ok(src.includes(`await ensureIndex(CardCommentReactions, { ${key}: 1 });`), `index on ${key}`);
|
||||
}
|
||||
assert.ok(/Meteor\.startup\(async \(\) => \{/.test(src));
|
||||
});
|
||||
|
||||
test('the server module is imported, so the startup hook actually runs', () => {
|
||||
assert.ok(read('server/imports.js').includes("import '/server/models/cardCommentReactions';"));
|
||||
});
|
||||
|
||||
test('the queries the log showed scanning are the ones now indexed (negative: none is unindexed)', () => {
|
||||
// The two publications read reactions by cardId (card) and boardId (board).
|
||||
const boards = read('server/publications/boards.js');
|
||||
assert.ok(/getCardCommentReactions\(\{ boardId: board\._id \}/.test(boards));
|
||||
const schema = read('models/cardCommentReactions.js');
|
||||
for (const field of ['boardId', 'cardId', 'cardCommentId']) {
|
||||
assert.ok(new RegExp(`^\\s*${field}: \\{`, 'm').test(schema), `${field} is a schema field`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the storage-requirements page exists and the MongoDB docs index links it', () => {
|
||||
const page = read('docs/Databases/MongoDB/Storage-Requirements.md');
|
||||
assert.ok(/fsync/.test(page) && /SMB/.test(page) && /No space left on device/.test(page));
|
||||
assert.ok(read('docs/Databases/MongoDB/README.md').includes('[Storage-Requirements.md](Storage-Requirements.md)'));
|
||||
assert.ok(fs.existsSync(path.join(ROOT, 'docs/Backup/Repair-MongoDB.md')), 'the repair page it links to');
|
||||
});
|
||||
|
||||
console.log(`\ncardCommentReactionsIndexes: ${passed} tests passed`);
|
||||
Reference in new issue
Block a user