Distributed Node #5

Caching

Two Sum problem

Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.

Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Output: Because nums[0] + nums[1] == 9, we return [0, 1].

There are only two hard things in Computer Science: cache invalidation and naming things.

Phil Karlton (Netscape developer)

Twitter feed problem

How would you design a twitter?

Naive solution

GET feed

SELECT all tweets WHERE

userId IN (SELECT all friends of the user)

         ORDER BY ...
         LIMIT ...

1

3

2

Response time

Single point of failure

Max queries per second

Availability

Performance

Consistancy

A better solution

LB

Tweets services

Feeds cache cluster

Sharded SQL cluster

Gizzard 

POST new post

Recompute all friends feeds

Save to redis cluster

GET feed

Has cache?

?

Written in the ANSI C

 110000 SETs, about 81000 GETs per second.

Supports rich data types

Multi-utility tool

Operations are atomic

A databaase

Features

Benchmark

Redis vs Memcached

Play with Redis

$ docker run -d redis
$ docker exec -it <container id> /bin/bash
$ redis-cli
> ping
> CONFIG GET * 

 In Redis, there is a configuration file (redis.conf) available at the root directory of Redis. Although you can get and set all Redis configurations by Redis CONFIG command

Datatypes

Strings

Hashes

Sets

Lists

Sorted Sets

Strings

> set hello world
> get hello

> getrange hello 1 2

> strlen hello

> getset hello aaaaaa

> get hello

> set hello adasdasd ex 10

Play with strings

HashMaps

redis 127.0.0.1:6379> HMSET aaa name redis
description "redis basic commands for caching" likes 20 visitors 23000 
OK 
redis 127.0.0.1:6379> HGETALL aaa  
1) "name" 
2) "redis" 
3) "description" 
4) "redis basic commands for caching" 
5) "likes" 
6) "20" 
7) "visitors" 
8) "23000"

Sets

redis 127.0.0.1:6379> SADD tutorials redis 
(integer) 1 
redis 127.0.0.1:6379> SADD tutorials mongodb 
(integer) 1 
redis 127.0.0.1:6379> SADD tutorials mysql 
(integer) 1 
redis 127.0.0.1:6379> SADD tutorials mysql 
(integer) 0 
redis 127.0.0.1:6379> SMEMBERS tutorials  
1) "mysql" 
2) "mongodb" 
3) "redis"

Sorted Sets

redis 127.0.0.1:6379> ZADD tutorials 1 redis 
(integer) 1 
redis 127.0.0.1:6379> ZADD tutorials 2 mongodb 
(integer) 1 
redis 127.0.0.1:6379> ZADD tutorials 3 mysql 
(integer) 1 
redis 127.0.0.1:6379> ZADD tutorials 3 mysql 
(integer) 0 
redis 127.0.0.1:6379> ZADD tutorials 4 mysql 
(integer) 0 
redis 127.0.0.1:6379> ZRANGE tutorials 0 10 WITHSCORES  
1) "redis" 
2) "1" 
3) "mongodb" 
4) "2" 
5) "mysql" 
6) "4" 

Redis Sorted Sets are similar to Redis Sets with the unique feature of values stored in a set. The difference is, every member of a Sorted Set is associated with a score

Lists

127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"
127.0.0.1:6379> rpush mylist a
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"
3) "a"
127.0.0.1:6379> rpop mylist
"a"
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"
127.0.0.1:6379> rpush mylist 1 2 3 4 5 "foo bar"
(integer) 8
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"
3) "1"
4) "2"
5) "3"
6) "4"
7) "5"
8) "foo bar"

Lists are useful for a number of tasks, two very representative use cases are the following:

  • Remember the latest updates posted by users into a social network.
  • Communication between processes, using a consumer-producer pattern where the producer pushes items into a list, and a consumer (usually a worker) consumes those items and executed actions. Redis has special list commands to make this use case both more reliable and efficient.

+

  redis:
    container_name: redis
    image: redis
    ports:
      - '6379:6379'

Add Redis to a compose file

yarn add cache-manager cache-manager-ioredis
yarn add -D @types/ioredis @types/cache-manager

Install cache-manager

import * as redisStore from 'cache-manager-ioredis';


imports: [
    ConfigModule.forRoot(),
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
    CacheModule.register({
      store: redis,
      host: process.env.REDIS_HOST,
      port: process.env.REDIS_PORT,
      ttl: 0,
    }),
  ],

Setup a users module

constructor(
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
    @Inject('IUsersService') private readonly usersService: IUsersService,
  ) {
    let i = 0;
    setInterval(() => {
      this.cacheManager
        .set('asd', i++)
        .then(() => console.log('Write', i - 1))
        .then(() => this.cacheManager.get('asd'))
        .then((r) => console.log('Read', r))
        .catch((e) => console.log('!!!!!!!!', e));
    }, 5000);
  }

Inject a cache manager 

  @Get()
  @UseInterceptors(CacheInterceptor)
  @CacheTTL(1000)
  async findAll() {
    await new Promise((r) => setTimeout(r, 10000));
    console.log('done');
    return this.usersService.findAll();
  }

Add cache interceptor 

And experiment


@Module({
  imports: [
    ConfigModule.forRoot(),
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
    CacheModule.register({
      store: redis,
      host: process.env.REDIS_HOST,
      port: process.env.REDIS_PORT,
      ttl: 0,
    }),
  ],
  controllers: [UsersController],
  providers: [
    {
      provide: 'IUsersService',
      useClass: UsersService,
    },
    {
      provide: APP_INTERCEPTOR,
      useClass: CacheInterceptor,
    },
  ],
})
export class UsersModule {}

Add global interceptor 


  @Post()
  @UseInterceptors(CacheClearInterceptor)
  create(@Body() createUserDto: CreateUserDto) {
    this.logger.log(
      'Someone is creating a user' + JSON.stringify(createUserDto),
    );
    return this.usersService.create(createUserDto);
  }

Add clear cache interceptor 

import { Observable } from 'rxjs';
import {
  Inject,
  CACHE_MANAGER,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Injectable,
} from '@nestjs/common';
import { Cache } from 'cache-manager';
import { tap } from 'rxjs/operators';

@Injectable()
export class CacheClearInterceptor implements NestInterceptor {
  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {
    return next.handle().pipe(
      tap((a) => {
        console.log(`After...`, a);
        console.log(this.cacheManager.reset());
      }),
    );
  }
}

Add clear cache interceptor 

Access to all Redis commands?

import { Cache, Store } from 'cache-manager';
import { RedisStore } from './RedisStore';
import Redis from 'ioredis';
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';

export interface RedisStore extends Store {
  getClient(): Redis.Commands;
}


@Injectable()
export class RedisMediator {
  private client: Redis.Commands;

  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {
    this.client = (<RedisStore>this.cacheManager.store).getClient();
  }

  getClient() {
    return this.client;
  }
}

Create new provider

providers: [
    RedisMediator,
    {
      provide: 'IUsersService',
      useClass: UsersService,
    },
    {
      provide: APP_INTERCEPTOR,
      useClass: CacheInterceptor,
    },
  ],

Inject it to controller

constructor(
    private redis: RedisMediator,
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
    @Inject('IUsersService') private readonly usersService: IUsersService,
  ) {
    redis
      .getClient()
      .lrange('mylist', 0, -1)
      .then((r) => console.log('>>>', r));
    redis
      .getClient()
      .llen('mylist')
      .then((r) => console.log('>>>', r));
    redis
      .getClient()
      .hmset(
        'mymap',
        new Map([
          ['hello', '1'],
          ['world', '2'],
        ]),
      )
      .then((r) => console.log('>>>', r));

    redis
      .getClient()
      .hgetall('mymap')
      .then((r) => console.log('>>>', r));
    
    ...
import * as redis from 'cache-manager-ioredis';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { RedisService } from 'nestjs-redis';

@Module({
  imports: [
    ConfigModule.forRoot(),
    CacheModule.registerAsync({
      useFactory: (redisService: RedisService) => {
        return {
          store: redis,
          redisInstance: redisService.getClient(),
        };
      },
      inject: [RedisService],
    }),
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
  ],
  controllers: [UsersController],
  providers: [
    {
      provide: 'IUsersService',
      useClass: UsersService,
    },
    {
      provide: APP_INTERCEPTOR,
      useClass: CacheInterceptor,
    },
  ],
})
export class UsersModule {}
 constructor(
    @Inject('IUsersService') private readonly usersService: IUsersService,
    redisService: RedisService,
  ) {
    redisService.getClient().set('hello', 'world');
  }
import { RedisModule } from 'nestjs-redis';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    UsersModule,
    ConfigModule.forRoot(),
    MongooseModule.forRoot(
      process.env.DATABASE_URL,
      process.env.MONGO_PASSWORD
        ? {
            authSource: 'admin',
            user: process.env.MONGO_USER,
            pass: process.env.MONGO_PASSWORD,
          }
        : undefined,
    ),
    RedisModule.register({
      host: process.env.REDIS_HOST,
      port: +process.env.REDIS_PORT,
      password: process.env.REDIS_PASSWORD,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

It would be nice if someone watched for the cluster

Sentinel

Master

Svc 2

Svc 1

Svc 3

Slave 2

Slave 1

New master?

Sentinel 1

Sentinel 2

Sentinel 3

Vote for 1

Vote for 1

Vote for 2

Quorum has chosen Slave 1 as a new Master

microk8s helm repo add bitnami https://charts.bitnami.com/bitnami
microk8s helm install my-release \
    --set sentinel.enabled=true \
    --set cluster.slaveCount=3 \
    bitnami/redis
echo $(kubectl get secret --namespace default my-release-redis -o jsonpath="{.data.redis-password}" | base64 --decode)

Setup redis cluster

k delete pods my-release-redis-node-0 --grace-period=0 --force

Simulate a problem on the first node

Kill the first pod

1:X 20 Feb 2021 17:29:31.802 # +sdown sentinel 17842c5dd5c0ce41063ddf37c45de74f9021022d 10.1.156.139 26379 @ mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:31.857 # +odown master mymaster 10.1.156.139 6379 #quorum 2/2
1:X 20 Feb 2021 17:29:31.857 # +new-epoch 1
1:X 20 Feb 2021 17:29:31.857 # +try-failover master mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:31.865 # +vote-for-leader 2436318487f97c202d62546d96fcb81c16122c4d 1
1:X 20 Feb 2021 17:29:31.880 # 0fe6c937f6043ba64d0cbca3d3c2b26bb78c39d8 voted for 2436318487f97c202d62546d96fcb81c16122c4d 1
1:X 20 Feb 2021 17:29:31.955 # +elected-leader master mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:31.955 # +failover-state-select-slave master mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:32.008 # +selected-slave slave 10.1.156.140:6379 10.1.156.140 6379 @ mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:32.008 * +failover-state-send-slaveof-noone slave 10.1.156.140:6379 10.1.156.140 6379 @ mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:32.108 * +failover-state-wait-promotion slave 10.1.156.140:6379 10.1.156.140 6379 @ mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:32.314 # +promoted-slave slave 10.1.156.140:6379 10.1.156.140 6379 @ mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:32.314 # +failover-state-reconf-slaves master mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:32.365 * +slave-reconf-sent slave 10.1.156.141:6379 10.1.156.141 6379 @ mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:32.955 # -odown master mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:33.350 * +slave-reconf-inprog slave 10.1.156.141:6379 10.1.156.141 6379 @ mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:33.350 * +slave-reconf-done slave 10.1.156.141:6379 10.1.156.141 6379 @ mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:33.441 # +failover-end master mymaster 10.1.156.139 6379
1:X 20 Feb 2021 17:29:33.441 # +switch-master mymaster 10.1.156.139 6379 10.1.156.140 6379
1:X 20 Feb 2021 17:29:33.441 * +slave slave 10.1.156.141:6379 10.1.156.141 6379 @ mymaster 10.1.156.140 6379

Node 2 sentinel logs

How Redis achieves persistence

RDB snapshot: Is a point-in-time snapshot of all your dataset, that is stored in a file in disk and performed at specified intervals. This way, the dataset can be restored on startup.

AOF log: Is an Append Only File log of all the write commands performed in the Redis server. This file is also stored in disk, so by re-running all the commands in their order, a dataset can be restored on startup.

Thank You!

Distributed Node #5

By Vladimir Vyshko

Distributed Node #5

Caching

  • 531