Improve cache performance by compressing records (#14833)

* Utils to compress/decompress data
Gzip was chosen because we want smaller data but quick algorithm since this will be ran for every request

* Compress system cache

* Decompress system cache

* Set/Get compressed cache for individual requests

* Switch from gzip to snappy, use json compression too

* Fix cache exp set/get

* Remove unused import

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
José Varela
2022-08-04 22:35:27 +01:00
committed by GitHub
parent 1300f5c3de
commit cc343fdf91
9 changed files with 203 additions and 14 deletions

View File

@@ -4,6 +4,7 @@ import env from './env';
import logger from './logger';
import { getConfigFromEnv } from './utils/get-config-from-env';
import { validateEnv } from './utils/validate-env';
import { compress, decompress } from './utils/compress';
let cache: Keyv | null = null;
let systemCache: Keyv | null = null;
@@ -50,10 +51,33 @@ export async function setSystemCache(key: string, value: any, ttl?: number): Pro
const { systemCache, lockCache } = getCache();
if (!(await lockCache.get('system-cache-lock'))) {
await systemCache.set(key, value, ttl);
await setCacheValue(systemCache, key, value, ttl);
}
}
export async function getSystemCache(key: string): Promise<Record<string, any>> {
const { systemCache } = getCache();
return await getCacheValue(systemCache, key);
}
export async function setCacheValue(
cache: Keyv,
key: string,
value: Record<string, any> | Record<string, any>[],
ttl?: number
) {
const compressed = await compress(value);
await cache.set(key, compressed, ttl);
}
export async function getCacheValue(cache: Keyv, key: string): Promise<any> {
const value = await cache.get(key);
if (!value) return undefined;
const decompressed = await decompress(value);
return decompressed;
}
function getKeyvInstance(ttl: number | undefined, namespaceSuffix?: string): Keyv {
switch (env.CACHE_STORE) {
case 'redis':

View File

@@ -1,5 +1,5 @@
import { RequestHandler } from 'express';
import { getCache } from '../cache';
import { getCache, getCacheValue } from '../cache';
import env from '../env';
import asyncHandler from '../utils/async-handler';
import { getCacheControlHeader } from '../utils/get-cache-headers';
@@ -23,7 +23,7 @@ const checkCacheMiddleware: RequestHandler = asyncHandler(async (req, res, next)
let cachedData;
try {
cachedData = await cache.get(key);
cachedData = await getCacheValue(cache, key);
} catch (err: any) {
logger.warn(err, `[cache] Couldn't read key ${key}. ${err.message}`);
if (env.CACHE_STATUS_HEADER) res.setHeader(`${env.CACHE_STATUS_HEADER}`, 'MISS');
@@ -34,7 +34,7 @@ const checkCacheMiddleware: RequestHandler = asyncHandler(async (req, res, next)
let cacheExpiryDate;
try {
cacheExpiryDate = (await cache.get(`${key}__expires_at`)) as number | null;
cacheExpiryDate = (await getCacheValue(cache, `${key}__expires_at`))?.exp;
} catch (err: any) {
logger.warn(err, `[cache] Couldn't read key ${`${key}__expires_at`}. ${err.message}`);
if (env.CACHE_STATUS_HEADER) res.setHeader(`${env.CACHE_STATUS_HEADER}`, 'MISS');

View File

@@ -1,6 +1,6 @@
import { RequestHandler } from 'express';
import ms from 'ms';
import { getCache } from '../cache';
import { getCache, setCacheValue } from '../cache';
import env from '../env';
import asyncHandler from '../utils/async-handler';
import { getCacheKey } from '../utils/get-cache-key';
@@ -33,8 +33,8 @@ export const respond: RequestHandler = asyncHandler(async (req, res) => {
const key = getCacheKey(req);
try {
await cache.set(key, res.locals.payload, ms(env.CACHE_TTL as string));
await cache.set(`${key}__expires_at`, Date.now() + ms(env.CACHE_TTL as string));
await setCacheValue(cache, key, res.locals.payload, ms(env.CACHE_TTL as string));
await setCacheValue(cache, `${key}__expires_at`, { exp: Date.now() + ms(env.CACHE_TTL as string) });
} catch (err: any) {
logger.warn(err, `[cache] Couldn't set key ${key}. ${err}`);
}

12
api/src/utils/compress.ts Normal file
View File

@@ -0,0 +1,12 @@
import { compress as compressSnappy, uncompress as uncompressSnappy } from 'snappy';
import { compress as compressJSON, decompress as decompressJSON } from '@directus/shared/utils';
export async function compress(raw: Record<string, any> | Record<string, any>[]): Promise<Buffer> {
if (!raw) return raw;
return await compressSnappy(compressJSON(raw));
}
export async function decompress(compressed: Buffer): Promise<any> {
if (!compressed) return compressed;
return decompressJSON((await uncompressSnappy(compressed, { asBuffer: false })) as string);
}

View File

@@ -2,7 +2,7 @@ import { Accountability, Permission, SchemaOverview } from '@directus/shared/typ
import { deepMap, parseFilter, parseJSON, parsePreset } from '@directus/shared/utils';
import { cloneDeep } from 'lodash';
import hash from 'object-hash';
import { getCache, setSystemCache } from '../cache';
import { getCache, getSystemCache, setSystemCache } from '../cache';
import getDatabase from '../database';
import { appAccessMinimalPermissions } from '../database/system-data/app-access-permissions';
import env from '../env';
@@ -13,7 +13,7 @@ import { mergePermissionsForShare } from './merge-permissions-for-share';
export async function getPermissions(accountability: Accountability, schema: SchemaOverview) {
const database = getDatabase();
const { systemCache, cache } = getCache();
const { cache } = getCache();
let permissions: Permission[] = [];
@@ -21,7 +21,7 @@ export async function getPermissions(accountability: Accountability, schema: Sch
const cacheKey = `permissions-${hash({ user, role, app, admin, share_scope })}`;
if (env.CACHE_PERMISSIONS !== false) {
const cachedPermissions = await systemCache.get(cacheKey);
const cachedPermissions = await getSystemCache(cacheKey);
if (cachedPermissions) {
if (!cachedPermissions.containDynamicData) {

View File

@@ -3,7 +3,7 @@ import { Accountability, Filter, SchemaOverview } from '@directus/shared/types';
import { parseJSON, toArray } from '@directus/shared/utils';
import { Knex } from 'knex';
import { mapValues } from 'lodash';
import { getCache, setSystemCache } from '../cache';
import { getSystemCache, setSystemCache } from '../cache';
import { ALIAS_TYPES } from '../constants';
import getDatabase from '../database';
import { systemCollectionRows } from '../database/system-data/collections';
@@ -20,7 +20,6 @@ export async function getSchema(options?: {
}): Promise<SchemaOverview> {
const database = options?.database || getDatabase();
const schemaInspector = SchemaInspector(database);
const { systemCache } = getCache();
let result: SchemaOverview;
@@ -28,7 +27,7 @@ export async function getSchema(options?: {
let cachedSchema;
try {
cachedSchema = (await systemCache.get('schema')) as SchemaOverview;
cachedSchema = (await getSystemCache('schema')) as SchemaOverview;
} catch (err: any) {
logger.warn(err, `[schema-cache] Couldn't retrieve cache. ${err}`);
}