Skip to content

Commit 9ebcc4c

Browse files
committed
optimized load_blocks_for_chunks and find_chunks_by_dedup_key queries
- Replace mongo-to-pg translation in load_blocks_for_chunks with a raw LATERAL + OFFSET 0 query that forces per-chunk index lookups on idx_btree_datablocks_chunk, reducing execution from ~2600ms to ~2ms for 38 chunks. - Combine the two-step find_chunks_by_dedup_key into a single JOIN query that eliminates an extra DB round-trip while the planner reliably picks nested loop + index scans on both tables. Signed-off-by: Danny Zaken <[email protected]>
1 parent 15a601d commit 9ebcc4c

2 files changed

Lines changed: 284 additions & 17 deletions

File tree

src/server/object_services/md_store.js

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* Copyright (C) 2016 NooBaa */
2-
/*eslint max-lines: ["error", 2200]*/
2+
/*eslint max-lines: ["error", 2210]*/
33
'use strict';
44

55
/** @typedef {typeof import('../../sdk/nb')} nb */
@@ -1545,22 +1545,49 @@ class MDStore {
15451545
if (!dedup_keys?.length) return [];
15461546

15471547
const query = `
1548-
SELECT *
1549-
FROM ${this._chunks.name}
1550-
WHERE
1551-
(data ->> 'system' = $1
1552-
AND data ->> 'bucket' = $2
1553-
AND (data ->> 'dedup_key' = ANY($3)
1554-
AND data ? 'dedup_key')
1555-
AND (data->'deleted' IS NULL OR data->'deleted' = 'null'::jsonb))
1556-
ORDER BY _id DESC;
1557-
`;
1548+
SELECT
1549+
c.data AS chunk_data,
1550+
b.data AS block_data
1551+
FROM ${this._chunks.name} c
1552+
JOIN ${this._blocks.name} b
1553+
ON b.data ->> 'chunk' = c.data ->> '_id'
1554+
AND b.data ? 'chunk'
1555+
AND (b.data->'deleted' IS NULL OR b.data->'deleted' = 'null'::jsonb)
1556+
WHERE c.data ->> 'system' = $1
1557+
AND c.data ->> 'bucket' = $2
1558+
AND c.data ->> 'dedup_key' = ANY($3)
1559+
AND c.data ? 'dedup_key'
1560+
AND (c.data->'deleted' IS NULL OR c.data->'deleted' = 'null'::jsonb)
1561+
ORDER BY c._id DESC
1562+
`;
15581563
const values = [`${bucket.system._id}`, `${bucket._id}`, dedup_keys];
15591564

15601565
try {
15611566
const res = await this._chunks.executeSQL(query, values);
1562-
const chunks = res.rows.map(row => decode_json(this._chunks.schema, row.data));
1563-
await this.load_blocks_for_chunks(chunks);
1567+
1568+
const chunks_map = new Map();
1569+
const all_blocks = [];
1570+
1571+
for (const row of res.rows) {
1572+
const chunk_id_str = row.chunk_data._id;
1573+
if (!chunks_map.has(chunk_id_str)) {
1574+
chunks_map.set(chunk_id_str, decode_json(this._chunks.schema, row.chunk_data));
1575+
}
1576+
if (row.block_data) {
1577+
all_blocks.push(decode_json(this._blocks.schema, row.block_data));
1578+
}
1579+
}
1580+
1581+
const chunks = Array.from(chunks_map.values());
1582+
1583+
const blocks_by_chunk = _.groupBy(all_blocks, 'chunk');
1584+
for (const chunk of chunks) {
1585+
const blocks_by_frag = _.groupBy(blocks_by_chunk[chunk._id.toHexString()], 'frag');
1586+
for (const frag of chunk.frags) {
1587+
frag.blocks = blocks_by_frag[frag._id.toHexString()] || [];
1588+
}
1589+
}
1590+
15641591
return chunks;
15651592
} catch (err) {
15661593
dbg.error('Error while finding chunks by dedup_key. error is ', err);
@@ -1911,10 +1938,23 @@ class MDStore {
19111938
*/
19121939
async load_blocks_for_chunks(chunks, sorter) {
19131940
if (!chunks || !chunks.length) return;
1914-
const blocks = await this._blocks.find({
1915-
chunk: { $in: db_client.instance().uniq_ids(chunks, '_id'), $exists: true },
1916-
deleted: null,
1917-
});
1941+
const chunk_ids = db_client.instance().uniq_ids(chunks, '_id').map(id => id.toString());
1942+
1943+
const query = `
1944+
SELECT d.*
1945+
FROM unnest($1::text[]) AS chunk_id,
1946+
LATERAL (
1947+
SELECT *
1948+
FROM ${this._blocks.name}
1949+
WHERE data->>'chunk' = chunk_id
1950+
AND data ? 'chunk'
1951+
AND (data->'deleted' IS NULL OR data->'deleted' = 'null'::jsonb)
1952+
OFFSET 0
1953+
) d
1954+
`;
1955+
const res = await this._blocks.executeSQL(query, [chunk_ids]);
1956+
const blocks = res.rows.map(row => decode_json(this._blocks.schema, row.data));
1957+
19181958
const blocks_by_chunk = _.groupBy(blocks, 'chunk');
19191959
for (const chunk of chunks) {
19201960
const blocks_by_frag = _.groupBy(blocks_by_chunk[chunk._id.toHexString()], 'frag');

src/test/integration_tests/db/test_md_store.js

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,11 +384,22 @@ mocha.describe('md_store', function() {
384384
frag_size: 10,
385385
dedup_key: Buffer.from('noobaa')
386386
};
387+
const block = {
388+
_id: md_store.make_md_id(),
389+
system: system_id,
390+
bucket: bucket._id,
391+
node: md_store.make_md_id(),
392+
chunk: chunk._id,
393+
frag: chunk.frags[0]._id,
394+
size: 10,
395+
};
387396
await md_store.insert_chunks([chunk]);
397+
await md_store.insert_blocks([block]);
388398
const chunksArr = await md_store.find_chunks_by_dedup_key(bucket, [Buffer.from('noobaa').toString('base64')]);
389399
assert(Array.isArray(chunksArr));
390400
assert(chunksArr.length >= 1);
391401
assert(chunksArr[0].frags[0]?._id?.toString() === chunk.frags[0]._id.toString());
402+
assert(chunksArr[0].frags[0].blocks.length >= 1);
392403
});
393404

394405
mocha.it('test find_chunks_by_dedup_key - dedup_key doesnt exist in DB', async () => {
@@ -408,6 +419,94 @@ mocha.describe('md_store', function() {
408419
assert(chunksArr.length === 0);
409420
});
410421

422+
mocha.it('find_chunks_by_dedup_key - multiple chunks with multiple frags and blocks', async () => {
423+
if (config.DB_TYPE !== 'postgres') return;
424+
const bucket = { _id: md_store.make_md_id(), system: { _id: system_id } };
425+
const frag1a = { _id: md_store.make_md_id() };
426+
const frag1b = { _id: md_store.make_md_id() };
427+
const frag2a = { _id: md_store.make_md_id() };
428+
const chunk1 = {
429+
_id: md_store.make_md_id(), system: system_id, bucket: bucket._id,
430+
frags: [frag1a, frag1b], size: 10, frag_size: 5,
431+
dedup_key: Buffer.from('multi_test_key1'),
432+
};
433+
const chunk2 = {
434+
_id: md_store.make_md_id(), system: system_id, bucket: bucket._id,
435+
frags: [frag2a], size: 20, frag_size: 20,
436+
dedup_key: Buffer.from('multi_test_key2'),
437+
};
438+
const blocks = [
439+
{ _id: md_store.make_md_id(), system: system_id, bucket: bucket._id,
440+
node: md_store.make_md_id(), chunk: chunk1._id, frag: frag1a._id, size: 5 },
441+
{ _id: md_store.make_md_id(), system: system_id, bucket: bucket._id,
442+
node: md_store.make_md_id(), chunk: chunk1._id, frag: frag1a._id, size: 5 },
443+
{ _id: md_store.make_md_id(), system: system_id, bucket: bucket._id,
444+
node: md_store.make_md_id(), chunk: chunk1._id, frag: frag1b._id, size: 5 },
445+
{ _id: md_store.make_md_id(), system: system_id, bucket: bucket._id,
446+
node: md_store.make_md_id(), chunk: chunk2._id, frag: frag2a._id, size: 20 },
447+
];
448+
await md_store.insert_chunks([chunk1, chunk2]);
449+
await md_store.insert_blocks(blocks);
450+
451+
const dedup_keys = [
452+
Buffer.from('multi_test_key1').toString('base64'),
453+
Buffer.from('multi_test_key2').toString('base64'),
454+
];
455+
const result = await md_store.find_chunks_by_dedup_key(bucket, dedup_keys);
456+
457+
assert(result.length === 2);
458+
const res_chunk1 = result.find(c => c._id.toString() === chunk1._id.toString());
459+
const res_chunk2 = result.find(c => c._id.toString() === chunk2._id.toString());
460+
assert(res_chunk1);
461+
assert(res_chunk2);
462+
assert(res_chunk1.frags[0].blocks.length === 2);
463+
assert(res_chunk1.frags[1].blocks.length === 1);
464+
assert(res_chunk2.frags[0].blocks.length === 1);
465+
});
466+
467+
mocha.it('find_chunks_by_dedup_key - excludes deleted chunks', async () => {
468+
if (config.DB_TYPE !== 'postgres') return;
469+
const bucket = { _id: md_store.make_md_id(), system: { _id: system_id } };
470+
const chunk = {
471+
_id: md_store.make_md_id(), system: system_id, bucket: bucket._id,
472+
frags: [{ _id: md_store.make_md_id() }], size: 10, frag_size: 10,
473+
dedup_key: Buffer.from('deleted_chunk_key'),
474+
};
475+
const block = {
476+
_id: md_store.make_md_id(), system: system_id, bucket: bucket._id,
477+
node: md_store.make_md_id(), chunk: chunk._id, frag: chunk.frags[0]._id, size: 10,
478+
};
479+
await md_store.insert_chunks([chunk]);
480+
await md_store.insert_blocks([block]);
481+
await md_store.delete_chunks_by_ids([chunk._id]);
482+
483+
const dk = Buffer.from('deleted_chunk_key').toString('base64');
484+
const result = await md_store.find_chunks_by_dedup_key(bucket, [dk]);
485+
assert(result.length === 0);
486+
});
487+
488+
mocha.it('find_chunks_by_dedup_key - excludes deleted blocks', async () => {
489+
if (config.DB_TYPE !== 'postgres') return;
490+
const bucket = { _id: md_store.make_md_id(), system: { _id: system_id } };
491+
const chunk = {
492+
_id: md_store.make_md_id(), system: system_id, bucket: bucket._id,
493+
frags: [{ _id: md_store.make_md_id() }], size: 10, frag_size: 10,
494+
dedup_key: Buffer.from('deleted_block_key'),
495+
};
496+
const block = {
497+
_id: md_store.make_md_id(), system: system_id, bucket: bucket._id,
498+
node: md_store.make_md_id(), chunk: chunk._id, frag: chunk.frags[0]._id, size: 10,
499+
};
500+
await md_store.insert_chunks([chunk]);
501+
await md_store.insert_blocks([block]);
502+
await md_store.delete_blocks_by_ids([block._id]);
503+
504+
const dk = Buffer.from('deleted_block_key').toString('base64');
505+
const result = await md_store.find_chunks_by_dedup_key(bucket, [dk]);
506+
// chunk has no non-deleted blocks, so INNER JOIN excludes it
507+
assert(result.length === 0);
508+
});
509+
411510
});
412511

413512

@@ -433,6 +532,134 @@ mocha.describe('md_store', function() {
433532

434533
});
435534

535+
mocha.describe('load_blocks_for_chunks', function() {
536+
537+
mocha.it('loads blocks and groups them into chunk.frags[].blocks', async function() {
538+
if (config.DB_TYPE !== 'postgres') return;
539+
const bid = md_store.make_md_id();
540+
const frag1 = { _id: md_store.make_md_id() };
541+
const frag2 = { _id: md_store.make_md_id() };
542+
const chunk = {
543+
_id: md_store.make_md_id(), system: system_id, bucket: bid,
544+
frags: [frag1, frag2], size: 10, frag_size: 5,
545+
};
546+
const blocks = [
547+
{ _id: md_store.make_md_id(), system: system_id, bucket: bid,
548+
node: md_store.make_md_id(), chunk: chunk._id, frag: frag1._id, size: 5 },
549+
{ _id: md_store.make_md_id(), system: system_id, bucket: bid,
550+
node: md_store.make_md_id(), chunk: chunk._id, frag: frag1._id, size: 5 },
551+
{ _id: md_store.make_md_id(), system: system_id, bucket: bid,
552+
node: md_store.make_md_id(), chunk: chunk._id, frag: frag2._id, size: 5 },
553+
];
554+
await md_store.insert_chunks([chunk]);
555+
await md_store.insert_blocks(blocks);
556+
557+
await md_store.load_blocks_for_chunks([chunk]);
558+
559+
assert(chunk.frags[0].blocks.length === 2);
560+
assert(chunk.frags[1].blocks.length === 1);
561+
assert(chunk.frags[1].blocks[0]._id.toString() === blocks[2]._id.toString());
562+
});
563+
564+
mocha.it('handles multiple chunks at once', async function() {
565+
if (config.DB_TYPE !== 'postgres') return;
566+
const bid = md_store.make_md_id();
567+
const chunk1 = {
568+
_id: md_store.make_md_id(), system: system_id, bucket: bid,
569+
frags: [{ _id: md_store.make_md_id() }], size: 10, frag_size: 10,
570+
};
571+
const chunk2 = {
572+
_id: md_store.make_md_id(), system: system_id, bucket: bid,
573+
frags: [{ _id: md_store.make_md_id() }], size: 20, frag_size: 20,
574+
};
575+
const blocks = [
576+
{ _id: md_store.make_md_id(), system: system_id, bucket: bid,
577+
node: md_store.make_md_id(), chunk: chunk1._id, frag: chunk1.frags[0]._id, size: 10 },
578+
{ _id: md_store.make_md_id(), system: system_id, bucket: bid,
579+
node: md_store.make_md_id(), chunk: chunk2._id, frag: chunk2.frags[0]._id, size: 20 },
580+
{ _id: md_store.make_md_id(), system: system_id, bucket: bid,
581+
node: md_store.make_md_id(), chunk: chunk2._id, frag: chunk2.frags[0]._id, size: 20 },
582+
];
583+
await md_store.insert_chunks([chunk1, chunk2]);
584+
await md_store.insert_blocks(blocks);
585+
586+
await md_store.load_blocks_for_chunks([chunk1, chunk2]);
587+
588+
assert(chunk1.frags[0].blocks.length === 1);
589+
assert(chunk2.frags[0].blocks.length === 2);
590+
});
591+
592+
mocha.it('sets empty blocks array for chunks with no blocks', async function() {
593+
if (config.DB_TYPE !== 'postgres') return;
594+
const chunk = {
595+
_id: md_store.make_md_id(), system: system_id, bucket: md_store.make_md_id(),
596+
frags: [{ _id: md_store.make_md_id() }], size: 10, frag_size: 10,
597+
};
598+
await md_store.insert_chunks([chunk]);
599+
600+
await md_store.load_blocks_for_chunks([chunk]);
601+
602+
assert(Array.isArray(chunk.frags[0].blocks));
603+
assert(chunk.frags[0].blocks.length === 0);
604+
});
605+
606+
mocha.it('excludes deleted blocks', async function() {
607+
if (config.DB_TYPE !== 'postgres') return;
608+
const bid = md_store.make_md_id();
609+
const chunk = {
610+
_id: md_store.make_md_id(), system: system_id, bucket: bid,
611+
frags: [{ _id: md_store.make_md_id() }], size: 10, frag_size: 10,
612+
};
613+
const live_block = {
614+
_id: md_store.make_md_id(), system: system_id, bucket: bid,
615+
node: md_store.make_md_id(), chunk: chunk._id, frag: chunk.frags[0]._id, size: 10,
616+
};
617+
const deleted_block = {
618+
_id: md_store.make_md_id(), system: system_id, bucket: bid,
619+
node: md_store.make_md_id(), chunk: chunk._id, frag: chunk.frags[0]._id, size: 10,
620+
};
621+
await md_store.insert_chunks([chunk]);
622+
await md_store.insert_blocks([live_block, deleted_block]);
623+
await md_store.delete_blocks_by_ids([deleted_block._id]);
624+
625+
await md_store.load_blocks_for_chunks([chunk]);
626+
627+
assert(chunk.frags[0].blocks.length === 1);
628+
assert(chunk.frags[0].blocks[0]._id.toString() === live_block._id.toString());
629+
});
630+
631+
mocha.it('applies sorter when provided', async function() {
632+
if (config.DB_TYPE !== 'postgres') return;
633+
const bid = md_store.make_md_id();
634+
const chunk = {
635+
_id: md_store.make_md_id(), system: system_id, bucket: bid,
636+
frags: [{ _id: md_store.make_md_id() }], size: 10, frag_size: 10,
637+
};
638+
const blocks = [
639+
{ _id: md_store.make_md_id(), system: system_id, bucket: bid,
640+
node: md_store.make_md_id(), chunk: chunk._id, frag: chunk.frags[0]._id, size: 300 },
641+
{ _id: md_store.make_md_id(), system: system_id, bucket: bid,
642+
node: md_store.make_md_id(), chunk: chunk._id, frag: chunk.frags[0]._id, size: 100 },
643+
{ _id: md_store.make_md_id(), system: system_id, bucket: bid,
644+
node: md_store.make_md_id(), chunk: chunk._id, frag: chunk.frags[0]._id, size: 200 },
645+
];
646+
await md_store.insert_chunks([chunk]);
647+
await md_store.insert_blocks(blocks);
648+
649+
const sorter = (a, b) => a.size - b.size;
650+
await md_store.load_blocks_for_chunks([chunk], sorter);
651+
652+
const sizes = chunk.frags[0].blocks.map(b => b.size);
653+
assert.deepStrictEqual(sizes, [100, 200, 300]);
654+
});
655+
656+
mocha.it('returns early on empty input', async function() {
657+
await md_store.load_blocks_for_chunks([]);
658+
await md_store.load_blocks_for_chunks(null);
659+
await md_store.load_blocks_for_chunks(undefined);
660+
});
661+
});
662+
436663
mocha.describe('dedup-index', function() {
437664
mocha.it('get_dedup_index_size()', async function() {
438665
return md_store.get_dedup_index_size();

0 commit comments

Comments
 (0)