프로젝트 진행 목표 및 과정

멘토님께서 Redis라는 큰 오픈소스의 이슈를 해결하기에는 4주라는 기간은 너무 짧다고 판단하여서, 해당 기간 동안 Redis에 대한 학습에 집중하기로 결정하셨습니다.

 

우선, 프로젝트 시작 전에 2주 동안은 오픈소스 커뮤니티에서 제공하는 유튜브 강의를 시청하여 git에 대한 기본적인 이해를 정리하는 시간을 가졌습니다. Git은 협업 및 버전 관리를 위해 필수적인 도구이며, 오픈소스 프로젝트에 기여할 때 필수적인 요소입니다. 따라서, 기본적인 Git의 개념과 사용법을 숙지하고자 이러한 학습을 진행했습니다.

 

다음으로, 나머지 4주 동안은 Redis에 대한 깊은 학습에 집중하였습니다. 이 기간 동안에는 Redis의 주요 기능과 용도를 학습하고, 실제로 어떻게 사용되는지에 대한 심도 있는 이해를 갖도록 노력하고 있습니다.

또한, Redis의 소스 코드를 분석하고 멘토님께서 주시는 과제를 수행하면서 레디스의 코드를 실제 수정하고 디벨롭하면서 실전 경험을 쌓고있습니다. 이를 통해 오픈소스 프로젝트에 기여하는 데 필요한 기술과 지식을 획득하고, 더 나아가 Redis와 관련된 이슈를 해결하는 데 기여할 수 있는 역량을 키우는 것이 목표입니다.

이러한 과정을 통해 Redis에 대한 전문 지식을 쌓고, 오픈소스에 기여할 수 있는 능력을 키우는 것이 이 프로젝트의 목표입니다. 더불어, Git을 활용한 협업 및 버전 관리에 대한 이해도 함께 높이고자 합니다. 이를 통해 프로젝트 진행 과정에서 필요한 기술과 역량을 갖추어, 효과적으로 오픈소스 프로젝트에 기여할 수 있는 개발자로 성장하는 것이 최종 목표입니다.

온⸱오프라인 모임을 통한 기여 및 활동 내역

Redis의 개념을 잘 모르기에 멘토님이 말씀해주시는 것을 기록하고 그 이외의 파생해서 조금 더 공부가 필요한 것은 따로 공부해서 정리하였습니다.

레디스 캐싱전략

💡
2주차 온라인 미팅: https://dev-hiro.tistory.com/13
NoSQL: https://dev-hiro.tistory.com/10
과제3: https://dev-hiro.tistory.com/9
과제3 해설: https://dev-hiro.tistory.com/11
과제4: https://dev-hiro.tistory.com/14

활동소감

이 강의를 통해 DB와 같은 개념은 이론적으로만 알고 있었던 저에게 실제 코드 레벨에서의 이해와 적용 방법을 배울 수 있어서 매우 유익했습니다.

 

레디스는 많은 기업에서 사용되는 인메모리 데이터 구조 저장소로서, 그 유연성과 뛰어난 성능으로 유명합니다. 그런데도 실제로 그 내부 동작 메커니즘을 알고 코드 레벨에서 어떻게 활용할 수 있는지에 대해 배우는 것은 새로운 경험이었습니다. 이를 통해 레디스가 어떻게 작동하는지에 대한 이해가 더 깊어졌고, 이를 통해 개발 프로젝트에서 레디스를 보다 효율적으로 활용할 수 있을 것 같습니다.

 

특히, 이러한 강의를 통해 오픈소스에 대한 기여에 대한 관심이 더 커졌습니다. 다음에는 Redis에 대해 더 깊게 공부하고, 커뮤니티에 기여하는 것이 목표입니다. 오픈소스 프로젝트에 참여함으로써 다른 사람들과 지식을 공유하고 성장할 수 있는 기회를 얻을 수 있을 것으로 기대됩니다.

또한, 이를 통해 제 개인적인 기술 스택을 향상시키고, 전반적인 개발 커리어에 도움이 될 것으로 기대됩니다.

앞으로 Redis를 더 깊게 공부하고 기여하는 과정에서 어려움이 있을지도 모르지만, 그 과정에서 더 많은 것을 배우고 성장할 수 있는 기회라는 것을 인식하고 있습니다. 다양한 프로젝트와 커뮤니티에서의 경험을 통해 개발자로서의 역량을 향상시키고, 다른 이들과 함께 협업하며 성공을 이끌어 나갈 수 있기를 기대합니다.

 

이러한 강의를 열어주신 Open Up에 감사드리며, Redis 강사님이신 강대명 멘토님께도 감사드립니다.

제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁

레디스 내부에서 해시를 다루는 방법

Redis에 주요 자료구조 이해

server.c/initServer

  • 서버에 필요한 내용 초기화
  • signal handler 설정
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSignalHandlers();
  • thread io => thread pool
  • Event Loop 설정
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
    if (server.el == NULL) {
        serverLog(LL_WARNING,
            "Failed creating the event loop. Error message: '%s'",
            strerror(errno));
        exit(1);
    }
server.db = zmalloc(sizeof(server)*server.dbnum);
💡이벤트 루프
Redis에서 이벤트 루프(Event Loop)는 비동기 네트워크 서버로서 클라이언트 요청을 처리하고,
비동기 작업을 관리하는 핵심 메커니즘. 이벤트 루프는 주로 네트워크 IO(READ, WRITE...)를 처리하는 데 사용.

Redis는 싱글 스레드로 동작하므로 이벤트 루프는 모든 클라이언트의 요청을 효율적으로 처리하는 데 중요한 역할.
모든 작업처리는 단일 콜스택에서 이루어지고 비동기 처리는 Queue를 이용하여 이벤트 루프방식으로 동작.

여러 개의 소켓이 동시에 연결되어 있고, 이들을 관찰하면서 들어오는 작업을 처리.
이를 통해 Redis는 스레드 동기화, 컨텍스트 스위칭으로 발생하는 리로스 경합 및 오버헤드, 복잡성을 방지.

SELECT

  • 디비 번호를 바꾸어서 사용할 수 있는 명령어.(0~15까지)
  • flushdb
    • DB 내용 전체 바꾸기
    • 레디스 클라이언트는 디비 번호가 0. 다른 디비를 사용할 수 없음
    • createIntConfig 설정
  • 자료구조들 초기화

내부적으로 사용하는 것.

listCreate

  • list를 생성
list *listCreate(void)
{
    struct list *list;

    if ((list = zmalloc(sizeof(*list))) == NULL)
        return NULL;
    list->head = list->tail = NULL;
    list->len = 0;
    list->dup = NULL;
    list->free = NULL;
    list->match = NULL;
    return list;
}

dictCreate

typedef struct dictEntry {
    void *key;                 // 항목의 키
    void *val;                 // 항목의 값
    struct dictEntry *next;    // 같은 해시 버킷의 다음 항목을 가리키는 포인터
} dictEntry;

typedef struct dictType {
    unsigned int (*hashFunction)(const void *key);                // 키를 해싱하는 함수
    void *(*keyDup)(void *privdata, const void *key);             // 키를 복사하는 함수
    void *(*valDup)(void *privdata, const void *obj);             // 값을 복사하는 함수
    int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 키를 비교하는 함수
    void (*keyDestructor)(void *privdata, void *key);             // 키를 해제하는 함수
    void (*valDestructor)(void *privdata, void *obj);             // 값을 해제하는 함수
} dictType;

typedef struct dict {
    dictEntry **table;         // 해시 테이블의 버킷 배열
    dictType *type;            // 테이블에서 사용할 함수들을 정의하는 dictType 구조체
    unsigned long size;        // 해시 테이블의 버킷 수
    unsigned long sizemask;    // 해시 테이블 크기 마스크 (size - 1)
    unsigned long used;        // 해시 테이블에 저장된 항목 수
    void *privdata;            // 함수 호출에 사용할 사용자 정의 데이터
} dict;

/* Create a new hash table */
static dict *dictCreate(dictType *type, void *privDataPtr) {
    dict *ht = hi_malloc(sizeof(*ht));
    if (ht == NULL)
        return NULL;

    _dictInit(ht,type,privDataPtr);
    return ht;
}

/* Initialize the hash table */
static int _dictInit(dict *ht, dictType *type, void *privDataPtr) {
    _dictReset(ht);
    ht->type = type;
    ht->privdata = privDataPtr;
    return DICT_OK;
}
  • hashTable 생성
  • dictEntry
    • dictEntry 구조체는 해시 테이블의 각 버킷에 저장되는 개별 항목을 나타냄.
      체인법을 사용하여 충돌을 처리.
       
    • key: 해시 테이블에서 항목을 식별하는 데 사용되는 키
    • val: 키에 해당하는 값
    • next: 해시 충돌이 발생했을 때 동일한 버킷 내의 다음 항목을 가리킴. 이를 통해 체인 형태로 연결된 항목들을 관리.
  • dictType
    • dictType 구조체는 해시 테이블에서 사용하는 함수들을 정의.
      키와 값의 처리 방식(복사, 비교, 해제 등)을 커스터마이징하는 데 사용.
       
    • 함수 포인터 사용 => 다형성 제공
    • hashFunction: 주어진 키에 대해 해시 값을 계산
    • keyDup: 키를 복사
    • valDup: 값을 복사
    • keyCompare: 두 키를 비교
    • keyDestructor: 키를 해제
    • valDestructor: 값을 해제
  • dict
    • dict 구조체는 실제 해시 테이블을 나타냄.
      해시 테이블은 키-값 쌍을 저장하는 버킷 배열과 기타 메타 데이터를 포함.
    • table: 해시 테이블의 버킷 배열로, 각 버킷은 dictEntry 포인터를 가짐.
    • type: 이 해시 테이블에서 사용할 함수들을 정의하는 dictType 구조체를 가리킴.
    • size: 해시 테이블의 현재 크기(버킷 수).
    • sizemask: 해시 테이블 크기에서 버킷 인덱스를 계산할 때 사용하는 마스크 값입. 일반적으로 size - 1입니다.
    • used: 현재 해시 테이블에 저장된 항목의 수.
    • privdata: 함수 호출 시 사용할 수 있는 사용자 정의 데이터.
  • 생성시에 dictType을 넣어줌
    • _dictInit에서 타입설정
    • dictFind()
      • dictSize는 동일
      • dictCompareKeys() => 키 비교 함수
      • dbDictType
        • sds인지, robj인지 확인
  • serverDb
    • 자료구조 설정.

createZset

  • sorted set 설정
  • dictAddRaw
    • 키를 집어 넣는 것.

특정 slot에 포함된 데이터를 가져오려면?

  • 선형으로 검색해서 모두 들고 오기(Full Scan)
    • 레디스는 싱글 스레드이기에 멈춰있어야함.
  • 처음부터 slot 별로 데이터를 따로 관리
    • 16384개의 dict
    • 위의 문제를 해결하기 위해 kvstore 생성

참조

더보기

 

 

제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁

데이터의 영속성


  • 레디스는 스냅샷 기능을 제공하여 메모리의 내용을 .rdb 파일로 저장. 해당 시점으로 복구 가능.

기본-복제 아키텍처를 제공(복구 기능 및 확장)


  • 기본-복제 아키텍처를 사용하여 비동기식 복제를 지원하여 데이터가 여러 복제 서버에 복제 될 수 있음.
  • 따라서 main 서버가 장애가 발생하는 경우 요청이 여러 서버로 분산될 수 있음으로 향상된 읽기 성능과 더 빠른 복구 기능 사용.
  • scalue-up, scale-in, scale-out을 제공하여 클러스터를 확장하여 일관된 성능과 안정성을 제공.

Single Thread


레디스는 싱글 스레드 아키텍처를 사용. ⇒ 한번의 1개의 명령어만 실행

  • Memcached는 멀티 스레드 지원.
  • 하나의 요청이 병목이 되면 그 다음 요청이 계속 blocking 상태이므로 O(n)의 시간복잡도를 보여줌.
    • O(n)의 시간복잡도를 갖는 명령어: Keys, flushall, FLUSHDB, Delte COLLECTIONS, Get All COLLECTIONS
  • Event Loop를 이용하여 요청을 수행
    • 실제 명령에 대한 작업은 커널 IO 레벨에서 멀티플렉싱을 통해 처리하여 동시성을 보장.
    • 커널 IO에서는 스레드 풀을 이용

Redis에서의 CRUD


READ

  • 레디스 서버에서 사용자가 요청한 데이터가 있는지 확인.
  • 데이터가 존재하는 경우.
    • 만료 여부를 확인.
    • 정보 반환
  • 데이터가 만료되었거나 존재하지 않는 경우
    • 데이터가 만료되었을 경우, 데이터 삭제.
    • 메인서버에 요청.
    • 메인 서버로부터 받은 데이터를 캐싱 및 DB에 저장한 후 데이터를 반환 후 종료

CUD

  • 데이터에 변화가 생겼으므로 해당하는 값은 캐싱된 데이터가 아닌 실시간 정보를 보내주어야 함.
  • 방문자의 CUD를 메인 서버에 요청.
  • 메인 서버는 요청받은 CUD를 처리하고 업데이트
  • 변경되기 이전의 데이터 값은 레디스 서버에서 찾아서 삭제 후 종료.

Redis 캐싱전략


웹 서비스 환경에서 시스템 성능 향상을 기대하는 기술 일반적으로 캐시는 메모리(RAM)을 사용하기에 DB보다
훨씬 빠르게 데이터를 응답할 수 있어서 이용자에게 빠르게 서비스를 제공 가능

하지만 기본적으로 RAM 용량은 16~32G 정도라, 데이터를 모두 캐시에 저장해버리면 용량 부족 현상이 일어나 시스템이 다운 될 수 있기에 어느 종류의 데이터를 캐시에 저장할지, 얼만큼의 데이터를 캐시에 저장할지, 얼마동안 오래된 데이터를 캐시에서 제거하는지에 대한 전략이 필요.

캐시 읽기 전략


Look Aside (Lazy Loding)

캐시를 옆에 두고 필요할 때만 데이터를 캐시에 로드하는 전략

 

Look Aside Diagram

  • Cache Aside 패턴이라고 부르기도 함.
  • 데이터를 찾을 때 우선적으로 캐시를 확인하는 법
    • 없으면 DB에서 조회.
  • 반복적인 읽기가 많은 시스템에서 적합
  • 캐시와 DB가 분리되어 가용되기 때문에
    • 원하는 데이터만 별도로 구성하여 캐시에 저장
    • 캐시 장애 대비 구성이 되어 있음.
      • 캐시(Redis)가 다운되더라도 DB에서 데이터를 가져올 수 있어, 서비스에 큰 장애는 없음.
  • Cache Store와 Data Store(DB) 간 데이터 정합성 문제가 발생할 수 있음
    • DB에서 캐시로 데이터를 미리 넣어주는 작업을 진행하기도 함. => Cache Warming
💡 Cache Warming
미리 Cache로부터 DB의 데이터를 밀어 넣어두는 작업을 의미.
이 작업을 수행하지 않으면 서비스 초기에 트래픽 급증 시 대량의 cache miss가 발생하여 데이터베이스 부하가 급증할 수 있음.(Thundering Herd)
다만, 캐시 자체는 용량이 작아 무한정으로 데이터를 들고 있을 수는 없어 일정 시간이 지나면 expire 되는데, 그러면 다시 Thundering Herd가 발생될 수 있기 때문에 캐시의 TTL을 잘 조정할 필요가 있음.

 

  • flow
    • cache hit
      • 어플리케이션에서 레디스로 데이터를 읽어옴
    • cache miss
      • 데이터베이스에 데이터 요청
      • 데이터를 데이터베이스에 저장.
  • 장점
    • 데이터 접근 시간을 줄임.
    • 처리량을 늘림

Read Through 패턴

캐시에서만 데이터를 읽어오는 전략(inline cache)

Read Through

  • Look Aside와 비슷하지만 데이터 동기화를 라이브러리 또는 캐시 제공자에게 위임하는 방식
  • 데이터를 조회하는데에 있어 속도가 느림
  • 데이터 조회를 캐시에만 의존하므로, Redis가 다운될 경우 서비스 이용에 차질.
  • 캐시와 DB 간의 데이터 동기화가 이루어져 데이터 정합성은 항상 일치
  • 읽기가 많은 서비스에 적합
  • flow
    • cache Store에 검색하는 데이터가 있는지
    • cache hit
      • 데이터 반환
    • cache miss
      • 캐시에서 DB에 데이터를 조회
      • 캐시를 업데이트
      • 데이터 반환
  • 장점
    • 직접적인 데이터베이스의 접근을 최소화
    • 데이터 정합성 일치
    • read에 소모되는 자원 최소화
  • 캐시가 문제가 발생했을 때 서비스에 차질이 있을 수 있으므로 Replication 또는 Cluster로 구성해서 가용성을 높여야 한다.

캐시 쓰기 전략


Write Back 패턴

Write Behind 패턴으로 부르기도 함
캐시와 DB 동기화를 비동기적으로.

Write Back

  • 캐시와 DB 동기화를 비동기적으로 관리하기에 동기화 과정이 생략
  • 데이터를 저장할 때 DB에 바로 쿼리하지 않고, 캐시에 모아서 일정 주기 배치 작업을 통해 DB에 반영
  • 캐시에 모아놨다가 DB에 쓰기 때문에 쓰기 쿼리 회수 비용과 부하를 줄일 수 있음.
  • Write가 빈번하면서 Read 하는데 많은 양의 Resource가 소모되는 서비스에 적합.
  • 데이터 정합성 확보
  • 자주 사용되지 않는 불필요한 리소스 저장.
  • 캐시에서 오류가 발생하면 데이터를 영구 손실.
  • flow
    • 모든 데이터를 Cache Store에 저장
    • 일정 시간이 지난 뒤 DB에 저장
  • 캐시가 일종의 Queue의 역할을 겸하게 됨.
  • 장점
    • DB 쓰기 횟수 비용과 부하를 줄일 수 있음
      • 캐시에 데이터를 모았다가 한번에 DB에 저장하기 때문에
    • 데이터베이스에 장애가 발생하더라도 지속적인 서비스를 제공할 수 있도록 보장 가능
  • Replication이나 Cluster 구조를 적용함으로써 cache 서비스의 가용성을 높이는 것이 좋고
  • Read-Through와 결합하면 가장 최근에 업데이트된 데이터를 항상 캐시에서 사용할 수 있는 혼합 워크로드에 적합.

Write Through 패턴

데이터베이스와 Cache에 동시에 데이터를 저장하는 전략.
Cache Store에도 반영하고 Data Store에 동시에 반영하는 방식

Write Through 패턴

  • 데이터를 저장할 때 먼저 캐시에 저장한 다음 바로 DB에 저장.(모아놓았다가 나중에 저장이 아닌 바로 저장)
  • Read Through와 마찬가지로 DB 동기화 작업을 캐시에게 위임.
  • DB와 캐시가 항상 동기화가 되어 있어, 캐시의 데이터는 항상 최신으로 유지
    • 데이터의 일관성을 유지할 수 있어서 안정적
  • 데이터의 유실이 발생하면 안되는 상황에 적합
  • 자주 사용되지 않는 불필요한 리소스 저장
  • 매 요청마다 두번의 Write가 발생하게 됨으로써 빈번한 생성 및 수정이 발생하는 서비스에서는 성능 이슈 발생
  • 기억장치 속도가 느릴경우, 데이터를 기록할 때 CPU가 대기하는 시간이 필요하기 때문에 성능 감소.

Write Around 패턴

모든 데이터는 DB에 저장.

Write Around

  • Write Through보다 훨씬 빠름
  • 모든 데이터는 DB에 저장(캐시를 갱신하지 않음)
  • Cache miss가 발생하는 경우에만 DB와 캐시에도 데이터를 저장.
  • 따라서 캐시와 DB내의 데이터가 다를 수 있음(데이터 불일치)

캐시 읽기 + 쓰기 전략 조합


  • Look Aside + Write Around 조합
    • 가장 일반적인 조합
  • Read Through + Write Aound 조합
    • 항상 DB에 쓰고, 캐시에서 읽을때 항상 DB에서 먼저 읽어오므로 데이터 정합성 이슈에 대한 완벽한 안전 장치를 구성할 수 있음.
  • Read Through + Write Through 조합
    • 데이터를 쓸 때 항상 캐시에 먼저 쓰므로 읽어올 때 최신 캐시 데이터 보장
    • 데이터를 쓸 때 항상 캐시에서 DB로 보내므로, 데이터 정합성 보장.

캐시 저장 방침


캐시 솔루션은 자주 사용되면서 자주 변경되지 않는 데이터의 경우 캐시 서버에 적용하여 반영할 경우 높은 성능향상을 이뤄낼 수 있음.
이를 Cache Hit Rating이라고 한다.
  • 캐시는 메모리에 저장되는 형태를 선호
  • 수십기가정도의 저장소를 보유하게 되며, 자주 사용되는 데이터를 어떻게 뽑아 캐시에 저장하고 자주 사용하지 않느 데이터를 어떻게 제거해 나갈 것이냐를 지속적으로 고민해야할 필요성이 있다.
  • 캐시를 저장하는 기준은 자주 사용되며 자주 변경되지 않는 데이터
  • 캐시는 언제든지 데이터가 날라갈 수 있는 휘발성
  • 데이터의 유실 또는 정합성의 문제도 항상 고려

캐시 제거 방침


  • 캐시 데이터의 경우 캐시 서버에만 단독으로 저장되는 경우도 있지만, 대부분 영구 저장소에 저장된 데이터의 복사본으로 동작하는 경우가 많음.
  • 2차 저장소(영구 저장소)에 저장되어 있는 데이터와 캐시 솔루션의 데이터를 동기화하는 작업이 반드시 필요.
    • 개발 과정에 고려사항이 늘어난다는 점을 기억.
  • 캐시 서버와 데이터베이스에 저장되는 데이터의 commit 시점에 대한 고려.
  • 캐시 만료 정책이 제대로 구현되어 있지 않는 경우, 클라이언트의 데이터가 변경되었음에도 오래된 정보가 캐싱되어있어 오래된 정보를 사용할 수 있다는 문제점이 발생.
    • 캐시된 데이터가 기간 만료되면 캐시에서 데이터가 제거되고, 응용 프로그램은 원래 데이터 저장소에서 데이터를 검색해야함.
    • 캐시 만료 주기가 너무 짧으면 데이터는 너무 빨리 제거되고, 캐시를 사용하는 이점이 줄어듦.
    • 반대로 너무 길면 데이터가 변경될 가능성과 메모리 부족 현상이 발생하거나, 자주 사용되어야 하는 데이터가 제거되는 등의 역효과

캐시 공유 방식 지침


  • 각 애플리케이션 인스턴스가 캐시에서 데이터를 읽고 수정 가능.
  • 애플리케이션이 캐시에 보유하는 데이터를 수정해야 하는 경우, 애플리케이션의 한 인스턴스가 만드는 업데이트가 다른 인스턴스가 만든 변경을 덮지 않도록 해야함.
    • 데이터의 정합성 문제
  • 먼저 캐시 데이터를 변경하기 전에 데이터가 검색된 이후 변경되지 않았는지 확인.
    • 변경되지 않았다면 즉시 업데이트.
    • 변경되었다면 업데이트 여부를 애플리케이션 레벨에서 결정하도록 수정
  • 캐시 데이터를 업데이트하기 전에 Lock을 잡는 방식
    • 조회성 업무를 처리하는 서비스에 Lock으로 인한 대기 현상이 발생
    • 데이터의 사이즈가 작아 빠르게 업데이트가 가능한 업무와 빈번한 업데이트가 발생하는 상황에 적용하기 용이.

 

참조

'Database > Redis' 카테고리의 다른 글

[Redis] Redis Data Type  (0) 2024.05.16
[Redis] Redis 기본  (0) 2024.05.10
[Redis] NoSQL이란  (0) 2024.04.30
[OSSCA2024] Redis 과제 3  (0) 2024.04.25
제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁

 

String 유형


문자열을 Value로 사용하는 것은 가장 단순한 형태.
문자열을 Value로 사용하고, html 문자열을 캐시하는 식의 활용 방식이 있음.

특징

  • Value에 string, number들을 저장. => 저장시 별도의 타입이 존재하지 않음.

SET

  • 값을 세팅할 때는 SET을 사용
# 값이 이미 있으면 실패 : nx
# 값이 이미 있을 때만 성공 : xx
SET [keyName] [value]

GET

  • 값을 찾아올 때는 GET을 사용.
GET [keyName]

 

INCR, DECR

  • 값이 정수인 경우, INCR과 DECR(increment, decrement) 명령어 활용 가능.
INCR [keyName]
DECR [keyName]

 

EXISTS

  • value의 존재 여부를 확인
  • 있으면 1, 없으면 0을 반환
EXIST [key1] [key2] ...

DEL

  • 해당 key를 삭제
  • 삭제에 성공했으면 1, 실패하면 0 반환
DEL [key1] [key2] ...

EXPIRE

  • TTL 설정
  • 단위는 초
EXPIRE [keyName] 5

 

Lists


특징

  • Value에 list를 저장.

lpush / rpush

  • 왼쪽에 값을 넣어주거나 오른쪽에 값을 넣어줌.

lrange

  • 값을 조회. 이때 -1은 모두 가져오라는 뜻

rpop

  • 이를 이용해 queue 구현 가능

rbpop

  • 순차적인 분산 작업 구현 가능
  • rpop과 비슷하나 데이터가 없다면 데이터가 들어올 때까지 block 상태로 대기

 

Sets


특징

  • Value에 set을 저장.
  • list는 중복을 허용하나 set은 중복이 안됨.

SADD

  • 집합(Set)에 하나 이상의 멤버를 추가
    • SADD test_sets 1test_sets라는 이름의 집합에 멤버로 1을 추가

SMEMBERS

  • 집합(Set)에 모든 멤버를 반환
    • SMEMBERS test_sets test_sets라는 이름의 집합에 모든 멤버 반환

 

Sorted sets


특징

  • Value에 set을 저장.
  • 중복 불가능 하며, scores 필드를 기준으로 정렬됨
  • 중복이 안되면 동일한 value를 넣으면 기존 데이터의 score를 덮어서 데이터 순서가 바뀜.

zadd

  • Sorted Set에 멤버와 점수를 추가하는 데 사용
    • test_ssets라는 이름의 Sorted Set에 멤버 1과 점수 1을 추가. => 성공(1)
    • zadd test_ssets "a" 2은 에러발생
      • Sorted Set은 멤버의 순서를 기준으로 정렬하는데, 문자열은 정렬되지 않는 데이터 타입이기 때문.
    • 마지막으로 zadd test_ssets "0" 2와 같이 문자열 "0"을 멤버로 추가한 경우
      • 이 경우는 정상적으로 처리되며, 정수로 간주하여 추가.
      • 새로운 멤버가 추가되었으므로 (integer) 0이 반환.

zrange

  • zrange 명령어는 Sorted Set의 멤버들을 조회하는 명령어.
  • zrange test_ssets 0 -1 명령어는 test_ssets Sorted Set의 첫 번째 멤버부터 마지막 멤버까지 모두 조회. 

 

Hashes


특징

  • Hashes key/value 목록을 가짐.

hset

  • 명령어는 해시에 필드와 값을 설정
  • HSET htest username hihtest라는 이름의 해시에 username 필드에 hi 값을 설정합니다. 반환된 정수 값 1은 새 필드가 생성되었음을 나타냄
  • 같은 방식으로 HSET htest userpwd asdfhtest 해시에 userpwd 필드에 asdf 값을 설정

hget

  • 해시에서 특정 필드의 값을 가져옴.
  • HGET htest usernamehtest 해시에서 username 필드의 값을 가져옵니다. 여기서는 "hi"가 반환됩니다.
  • HGET htest temp는 존재하지 않는 필드인 temp의 값을 가져오려고 시도합니다. 이 경우, 해당 필드가 존재하지 않기 때문에 nil이 반환됩니다.

hgetall

  • 해시의 모든 필드와 값을 반환
  • HGETALL htesthtest 해시의 모든 필드와 값을 가져옵니다. 여기서는 "username", "hi", "userpwd", "asdf"가 번갈아가면서 반환

 

Bitmaps


특징

  • Value에 bit값 저장
  • 512MB 용량으로 2^32(42억)개의 bit 값들을 저장
  • boolean 옵션으로 많이 사용.(회원마다 공지 조회 여부)

setbit

  • setbit 명령어는 주어진 오프셋(offset)에 비트 값을 설정하는데 사용.
  • 여기서 "test_bits"라는 이름의 비트열(Bitfield)에 대해 첫 번째 비트(오프셋 0)를 1로 설정.
  • 새로운 비트가 설정되었으므로 (integer) 0을 반환.

getbit

  • getbit 명령어는 주어진 오프셋에 해당하는 비트 값을 가져오는데 사용.
  • "test_bits" 비트열의 첫 번째 비트(오프셋 0)의 값.
  • 비트 값이 1로 설정되어 있으므로 (integer) 1이 반환.

 

 

참조

'Database > Redis' 카테고리의 다른 글

[Redis] Redis 고급 및 캐싱전략  (0) 2024.05.16
[Redis] Redis 기본  (0) 2024.05.10
[Redis] NoSQL이란  (0) 2024.04.30
[OSSCA2024] Redis 과제 3  (0) 2024.04.25
제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁

Redis


Key-Value Store의 하나의 DB
Remote Dictionary Server의 약자로, 원격 Dictionary 자료구조 서버라는 뜻
  • Key로 올 수 있는 자료형은 기본적으로 String이지만, Value는 다양한 타입을 지원함.
  • 메모리 기반 데이터베이스이기 때문에, Disk를 기반으로 하는 RDBMS보다 read가 빠름.
💡 잠깐! RDBMS도 쿼리를 통해 조회해오면, 메모리에 존재하는 Buffered Cache를 이용한다고 알고 있어요. (= Cache Hit)        Buffered Cache를 활용할 때의 RDBMS와 레디스는 조회 시간 차이가 없나요?

경험상 RDBMS에서 동일 select문 n회 조회할 때(cache hit)와 Redis의 조회 속도를 비교해보면 Redis가 더 빠른데요, 과거 AWS에서 monggo DB 담당하시는 이덕현 개발자님이 세미나에서 “RDBMS는 데이터의 직렬화, 역직렬화 과정이 있기 때문에 레디스보다 더 느린 것 같다"고 추론했다는 언급을 하신 적 있습니다.

 

자바 해시 맵과의 비교

공통점

  • 레디스, 해시 맵 모두 Key-Value 형태
  • 메모리 베이스 -> 디스크보다 빠른 접근 가능.
  • 원하는 Value를 원하는 자료구조로 사용 가능

차이점

분산 환경에서의 장점

  • 유저의 수가 늘어나 서버가 증설될 때 해시맵 데이터를 참조해야할 때

자바 해시맵을 이용한 분산환경

  • 자바 해시맵은 프로세스마다 메모리가 달라, 원격 프로세스간 동일한 해시 맵 데이터를 참조해야할 때 동기화가 어려움.

 

레디스를 이용한 분산환경

  • 별도의 레디스 서버를 구성하고, 해당 레디스에서 값을 꺼내 쓴다면 메모리 기반 데이터 구조의 빠른 응답성 데이터 정합성을 가져올 수 있음.

DBMS로서의 장점

  • Redis는 영속성을 제공하기에 다양한 옵션도 제공 가능
  • TTL 설정: 일정 시간이 지나면 데이터 삭제, 용량이 작은 메모리를 효율적으로 관리.
  • 분산 데이터 저장소 구성: Redis Cluster 등 분산환경에서 안정적인 데이터 관리
  • 보안 체계: 악성 스크립트 공격으로부터 안전 보장. TLS 지원.

레디스 특징

  • 쿼리를 사용할 필요 없이 명령어로 가능.
  • 데이터를 디스크에 쓰는 구조가 아니라 메모리에서 데이터를 처리하기 때문에 속도가 빠름.(인메모리 기반)
  • 여러 자료 구조를 지원(String, Lists, Sets, Sorted Sets, Hashes ...)

RESP 프로토콜

RESP(REdis Serialization Protocol)은 Redis 클라이언트가 Redis 서버와 TCP 커넥션을 맺어 통신할 때 사용하는 프로토콜.

더보기

RESP는 Redis의 client-server 통신에서만 사용.
Redis Cluster에서 노드간의 통신은 RESP가 아닌 binary protocol을 사용.

Request-Response 모델

Redis는 기본적으로 다음의 2가지 경우를 제외하고 Request-Response 모델을 사용.

  • Pipelining
    • 여러개의 명령어(Command)를 한번에 보내고 모든 답장이 올 때까지 기다림.
  • Pub/Sub
    • 어떤 Client가 특정 channel을 subscribe하면 push protocol로 전환되어 더이상 명령어를 보내지 않음.

Redis의 Request-Response 프로토콜은 다음과 같이 동작.

  • 클라이언트가 Bulk String의 Array 타입으로 명령어를 서버로 전송.
  • 서버는 클라이언트가 보낸 명령어에 맞는 타입으로 응답을 보냄.

RESP의 Data Types

RESP는 5가지의 데이터 타입이 존재.

데이터의 타입은 데이터의 첫번째 바이트를 통해 구분.

RESP를 통해 오고가는 Req와 Resp는 항상 "\r\n"으로 끝남.

  • Simple Strings: "+"
    • Simple Strings 타입은 binary-safe하지 않은 일반 문자열을 전송할 때 사용하는 데이터 타입
    • binary-safe한 문자열을 전송하려면 Bulk String타입으로 전송
    • 문자열은 Newline을 포함할 수 없음.
    • ex) "+OK\r\n"
  • Errors: "-"
    • 에러 정보에 대한 타입
    • 관습적으로 에러의 이름을 먼저 쓰고 발생 원인을 뒤에 적음.
    • ex) "-ERR unknown command 'foobar'\r\n" "-ERR unknown command 'foobar'\r\n"
  • Integers: ":"
    • 숫자의 크기는 signed 64bit 범위 내
    • INCR, LLEN, LASTSAVE와 같은 명령어에 대한 응답
    • 일부 명령어는 true/false의 의미로 1/0을 씀.
  • Bulk Strings: "$"
    • binary-safe한 문자열을 타나낼 때 사용하는 타입
    • "$"에 이어 문자열의 길이가 주어지고 "\r\n" 이후 실제 문자열
    • 빈 문자열을 나타낼 땐 "$0\r\n\r\n" 를 사용
    • null 값을 나타낼 때는 Null Bulk String이라고하며, "$-1\r\n" 를 사용
  • Arrays: "*"
    • 10진수로 배열의 크기와 "\r\n" 이 나옴.
    • 배열 내 원소는 각각 특정한 타입을 가질 수가 있으며, 그 타입은 모두 달라도 됨.
    • "*3\r\n:1\r\n+2\r\n$4\r\nbulk\r\n"
    • Null Array: "*-1\r\n"
    • 중첩된 Array도 가능

 

 

Redis 설치하기


Redis 공식 홈페이지, getting-started에서 설명.

Mac에서 설치하기

# brew가 없다면 https://brew.sh/

brew --version
brew install redis

# 설치를 완료했다면 Redis Server 실행
redis-server

# Redis Server에 접근하는 client 실행
redis-cli
  • 옵션을 주지 않고 부팅했을 때 Redis의 default 포트는 6379
  • cli를 실행하지 않고도, telnet으로 접속 가능 -> telnet 0 6379

hello시 첫 값들이 세팅되어 있음.

Key를 잡을 때 주의점

  • Key의 최대 길이는 512MB.(value도 마찬가지)
  • 매우 긴 키는 좋지 않음.
    • 1MB길이의 키는 메모리 관리 측면 뿐만 아니라 키를 조회할 때 고비용의 키 비교 로직을 실행해야 할 수 있기에 좋지 않음.
    • 키가 너무 길다면 SHA-1를 통해 해싱
    • 레디스를 사용하는 이유는 빠른 응답속도…!
  • 매우 짧은 키 역시 좋지 않음
    • 가독성을 해치는 키는 별로 좋지 않음.
  • 고정된 스키마를 활용
    • .이나 -주로 사용
    • comment:4321:reply.to, comment:4321:reply-to

참조

Redis, 자바 해시맵 비교: https://sihyung92.oopy.io/database/redis/1
RESP 프로토콜: https://redis.io/docs/latest/develop/reference/protocol-spec/#resp-simple-strings

'Database > Redis' 카테고리의 다른 글

[Redis] Redis 고급 및 캐싱전략  (0) 2024.05.16
[Redis] Redis Data Type  (0) 2024.05.16
[Redis] NoSQL이란  (0) 2024.04.30
[OSSCA2024] Redis 과제 3  (0) 2024.04.25
제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁

과제 4 설명


echo2 abc라고 보내면 abc가 응답이 아닌 echo2_abc가 응답으로 오도록

  • bulk 단위
    • Respv2 기준 하나의 명령을 나타내는 block
    • ex) $4\r\npong\r\n
  • echo abc 라고 명령을 입력했을 때
    • c → argc = 2 =>명령어의 길이
    • c → argv[0] = “echo” => 첫번째 명령어
    • c → argb[1] = “abc” => 두번째 명령어

robj(server object)

  • type: 4bit
    • 2^4 ⇒ 16개의 타입 가능
  • encoding: 4bit
  • lru: 24bit
    • lru, lfu 쓸때 모두 사용.
  • type + encoding + lru 총 32bit/4byte
  • refcount: 똑같은거 시키면 +1, 0이 되면 지워짐
  • void * ptr ⇒ 8byte
  • 총 16byte의 메모리 공간을 할당 받게됨.

argc, argv가 만들어지는 곳

  • processMultibulk…에서 생성
  • createStringObject() 또는 createObject()에서 만듦

SDS

이전 포스팅에서도 작성했지만 한번 더 작성.

  • 서버 시스템에서는 메모리를 아끼기 위해.
  • 동적으로 길이를 사용하기에 변수에 따라서 스트링의 길이를 조절하기 위해.
  • redis에서 사용하고 있는 구조체
  • sds의 사이즈를 가변적으로 가르키기위해.

c에서의 메모리의 값을 읽는 스킬

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct memory_header {
    unsigned long size;
    char ptr[];
}memheader;

char *STRING = "hello, OSSCA!";
void test(char *ptr) {
    printf("%s\n", ptr);
    memheader *mh = (memheader *)(ptr-sizeof(memheader));
    printf("size: %d\n", mh->size);
}

int main(int argc, char *argv[]) {
    printf("%d\n", sizeof(memheader));

    memheader *mh = (memheader *)malloc(sizeof(memheader) + 16);
    mh->size = strlen(STRING);
    memcpy(mh->ptr, STRING, mh->size);
    mh->ptr[mh->size] = 0;

    test(mh->ptr);
    return 0;
}
8
hello, OSSCA!
size: 13
  • 뒷 부분만 전달했음에도 size를 읽어올 수 있음.
  • 메모리를 다루는 언어인 C라서 가능한 스킬.
  • sdsHdrSize()는 실제 메모리의 사이즈를 알려주는데, 이를 통해서 값을 가져올 수 있음.

과제 4 수행


실제 과제 풀이 해답은 여러개가 있을 수 있다.

  1. robj를 변경하는 방법
  2. addreplyBulkSds()를 사용하는 방법
  3. addReplySds()를 사용하는 방법

이 있는데 2, 3번에 대해서만 보이면

addReplySds()를 사용

void echo2Command(client *c) {
    sds reply = sdscatfmt(sdsempty(), "echo2_%s", c->argv[1]->ptr);
    addReplyLongLongWithPrefix(c, sdslen(reply), '$');
    addReplySds(c, reply);
    addReplyProto(c, "\r\n", 2);
}
  • client 객체로부터 받은 명령어를 sdscatfmt()를 통해 새로운 문자열을 생성.
    • sdsempty()
      • sds 타입의 새로운 빈 문자열을 생성하는 함수.
      • sdsnewlen()을 통해 sds를 새로 생성
    • sdscatfmt()
      • sds 타입의 변수에 서식 지정된 문자열을 추가하는 함수.
      • 형식 지정자(format specifier)를 사용하여 문자열을 생성하고, 해당 문자열을 SDS에 추가합니다. 서식 지정자는 %s, %d, %f 등과 같이 다양한 형태로 사용될 수 있으며, 이를 통해 다양한 타입의 데이터를 문자열로 변환하여 SDS에 추가 가능.
    • addReplyLongLongWithPrefix()
      • Redis 클라이언트에게 정수 값을 포함하는 응답을 보내느 함수.
      • 주어진 정수 값(sds의 길이)이 작을 경우 프로토콜에서 사용되는 header를 공유 객체로 사용.
        • 이렇게 함으로써 메모리 절약.
        • 공유된 헤더를 사용함으로써 응답을 생성.
      • 생성된 응답을 클라이언트에게 전달하기 위해 addReplyProto() 함수를 호출
    • addReplySds()
      • Redis 클라이언트의 버퍼에 s를 추가하는 함수.
      • PrepareClientToWrite()
        • 클라이언트가 쓰기 작업을 준비할 수 있는 상태인지 확인.
      • _addReplyToBufferOrList()
        • flag와 type를 통해 client와의 상태를 확인.
        • _addReplyToBuffer() 함수를 호출하여 클라이언트의 응답 버퍼(client->buf)에 응답을 추가.
      • 이후 sds는 더이상 사용되지 않으므로 메모리를 해제하여 메모리 leak을 방지
    • addReplyProto()
      • Redis 클라이언트에게 프로토콜 형식의 응답을 보내는 역할. s와 len을 이용하여 클라이언트의 버퍼에 응답을 추가.
      • 여기서는 명령어의 종료 역할.
      • PrepareClientToWrite() 실행.
      • _addReplyToBufferOrList() 실행.

다른 방법

/**
 * sdsnewlen을 통해 생성하는 방법
 */
sds reply = sdsnewlen("echo2_", 6); // "echo2_" 문자열을 포함한 SDS 생성
reply = sdscatfmt(reply, "%s", c->argv[1]->ptr); // 뒤에 인자 추가


/**
 * sdssetlen을 통해 생성하는 방법
 */
sds reply = sdsnewlen(NULL, 6); // 초기 크기가 6인 빈 SDS 생성
memcpy(reply, "echo2_", 6); // "echo2_" 문자열 복사
sdssetlen(reply, 6); // SDS 길이 설정
reply = sdscatfmt(reply, "%s", c->argv[1]->ptr); // 뒤에 인자 추가

이외의 다른 방법으로도 가능하다.

addReplyBulkSds()를 사용

void echo2Command(client *c) {
    sds reply = sdscatfmt(sdsempty(), "echo2_%s", c->argv[1]->ptr);
    addReplyBulkSds(c, reply);   
}
  • 그 이후 생성된 새로운 문자열을 클라이언트에게 반환하게 위해 addReplyBulkSds()를 사용.

addReplyBulkSds()

/* Add sds to reply (takes ownership of sds and frees it) */
void addReplyBulkSds(client *c, sds s)  {
    addReplyLongLongWithPrefix(c,sdslen(s),'$');
    addReplySds(c,s);
    addReplyProto(c,"\r\n",2);
}
  • 실제 위의 구현한 방법과 동일한 flow로 진행된다.
127.0.0.1:6379> 
127.0.0.1:6379> echo 123
"123"
127.0.0.1:6379> echo2 123
"echo2_123"
127.0.0.1:6379>

원하는 대로 결과가 잘 출력된 것을 볼 수 있다.

제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁

이번 과제는 지난 과제 3이였던 echo 명령을 개선하는 과제이다.

이번 힌트는 sds 구조체 분석이다. 

과제 3 세팅

먼저 이전 과제 3번을 세팅하면

이전 과제에서는 직접 command.def 파일에 정의했다면 json 파일을 통해 정의하는 방법으로 진행하겠다.

src/commands 아래에 echo2.json 파일을 다음과 같이 생성한다. document와 함께 def 파일을 정의하기 위해 redis에서는 다음과 같이 json으로 빌드 시에 def 파일에 정의할 수 있도록 하였다.

{
    "ECHO2": {
        "summary": "Returns the given string.",
        "complexity": "O(1)",
        "group": "connection",
        "since": "1.0.0",
        "arity": 2,
        "function": "echo2Command",
        "command_flags": [
            "LOADING",
            "STALE",
            "FAST"
        ],
        "acl_categories": [
            "CONNECTION"
        ],
        "reply_schema": {
            "description": "The given string adding string echo2",
            "type": "string"
        },
        "arguments": [
            {
                "name": "message",
                "type": "string"
            }
        ]
    }
}

그 이후 utils/ 폴더의 generate-command-json.pygenerate-command-code.py 를 실행하면 command.def 파일에 정의가 된다.

command.def

이후 이제 server.h와 server.c에서 echo2Command를 정의한다.

server.h
server.c

가 되면 과제 3은 완료가 되었다!

과제 4


함수 실행 flow

먼저 함수 실행 flow를 분석해보면

다음과 같다. 간략하게 설명하면

  • connGetPrivateData: client 객체를 생성
  • ProcessInputBuffer: protocol에 따라 Inline으로 실행할 지 Multibulk로 실행할 지 판단.
  • lookupCommand: 지금 실행하는 함수를 Map에서 조회. 실행하는 함수 이름을 key로.
  • call: 함수를 실행.

이다.

구조체 분석

함수를 다음과 같이 구성하고 echo2 123을 실행해보면 shell에 *2\r\n$5\r\necho2\r\n$3\r\n123\r\n이 출력된다.

그래서 나는 querybuf를 변경하면 될 것이라 생각했고, querybufclient 구조체의 멤버이므로 client는 다음과 같이 이루어져 있었다.

server.h에 정의되어 있는 client 구조체

sds 구조체(Simple Dynamic String)

querybuf는 sds 구조체로 이루어져 있어 해당 sds 구조체를 보면 다음과 같다.

sds.h

단순히 문자열을 sds라고 칭한 것인데, 조금 더 분석해보자면

  • 문자열을 단순히 char*만 사용하면 문자열의 길이를 확인할 때 항상 저장된 메모리의 크기를 확인해야 하기에 성능 상의 문제점이 발생할 수 있음.
    • sdshdr 구조체를 사용함으로써, 저장된 문자열의 길이를 확인하는데 O(1)의 조회가 됨.
  • 실제로 데이터가 저장되는 부분은 char*로 저장되어 있는 sds.
  • 새로운 문자열을 생성하려면 sdsnewLen함수를 이용.

과제 수행

echo2Command 이후 addReplyBulk를 분석하면 다음과 같다. 

틀릴 확률이 높지만 파악한대로 설명을 이어나가면

  • addReplyBulkLen: 명령어의 길이를 파악
  • addReply: 실제 argu 값을 반환
  • addReplyProto: 마무리 작업.

을 한다고 파악이 되었다. 예를 들어 echo2 123이면 

  • addReplyBulkLen: 123의 길이인 3을 파악하고
  • addReply: 123이라는 값을 반환하고
  • addReplyproto: 명령어를 종료.

한다고 생각했다. 따라서 상위 함수인 addReplyBulk에 client 객체에 querybuf만 변화시켜주면 된다고 생각했는데 되질 않았다.

강의를 듣고 수정해보도록 하겠다.

💡 참고
redis sds관련: https://djlee118.tistory.com/109

 

제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁

Cache 적용 후 Select Query 비교


DML에 따른 캐시 성능 / 12시경 캐시 적용

  • 캐시는 얼마나 Hit 되는지가 중요한 요소
  • SELECT는 캐시 Hit 이후 평균 시간이 급격하게 감소
  • UPDATE는 캐시 Hit가 있을 수 없으므로 캐시 적용 전과 비슷.

CPU 사용량

  • 서비스에 따라 다르지만 SELECT가 DB에 도착하지 않으므로(캐시를 사용하므로) CPU 사용량이 줄어듦.
    • DB에 레플리카를 만들어서 SELECT
    • WRITE에는 다른 트릭을 적용해서 CPU 사용량을 줄일 수 있음.
💡 Performance Tool
gatling, locust, 엥그라이드, jmeter가 존재. 최근에는 gatling, locust가 많이 사용하는 추세

분산 캐시


  • 데이터가 많다면, 하나의 Cache Server가 아니라 여러 캐시 서버를 구성할 수 있음.
  • ex) Redis With Range, Redis with PreShard, Redis with Consistent Hashing

상황

데이터를 나누는 기준이 중요

  • 서버는 계속 늘어나기에 데이터를 나누는 기준이 필요
    • 데이터가 재분배가 계속되면 안좋음.

  • 전체를 찾는건 서버에 부하

 

1. 모듈러(%2, %3 ... %N)

  • 서버 추가시 데이터 재분배가 일어나야함.

2. Range로 구분하는 방법

  • 초창기에 들어오는 유저는 활동량이 많고 이벤트시 들어오는 유저는 활동량이 없음.
  • 활동적인 유저는 서버 1에만, 이벤트 시 들어오는 유저는 서버 2에만 들어가기에 서비스 분배에 부적합
  • 서버의 부하도가 다름.

 

3. Preshard

  • hash값으로 균등하게

 

 

 

Session Store


Session Store로 저장하는 법

  • Session Store(로그인 토큰 같은 정보 저장)로 Redis를 많이 사용
  • Session을 개별 서버나, 클러스터링이 아닌, 외부 스토리지(Redis)에 저장.
  • Session Store가 죽으면 정보가 사라짐.

Distribution Lock


  • 분산 락으로 동작
    • Optimistic Lock으로 동작
    • Key가 존재하면 대기하는 형태
      • 바로 실패로 구성할지, Spinlock 형태로 동작할 지(Redisson 구현체) 등 구현에 따라 다름.
  • setnx
    • 키(데이터)가 없을때만 쓸 수 있음.
    • 키가 있으면 실패
    • 락을 얻음.
    • 락을 풀기 위해 key를 지우면 됨.
  • t_string.c/setGenericCommand
    • found가 false일 때만 사용할 수 있음
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    long long milliseconds = 0; /* initialized to avoid any harmness warning */
    int found = 0;
    int setkey_flags = 0;

    if (expire && getExpireMillisecondsOrReply(c, expire, flags, unit, &milliseconds) != C_OK) {
        return;
    }

    if (flags & OBJ_SET_GET) {
        if (getGenericCommand(c) == C_ERR) return;
    }

    found = (lookupKeyWrite(c->db,key) != NULL);

    if ((flags & OBJ_SET_NX && found) ||
        (flags & OBJ_SET_XX && !found))
    {
        if (!(flags & OBJ_SET_GET)) {
            addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
        }
        return;
    }
    // ...
}
127.0.0.1:6379> setnx ossca 2024
(integer) 1
127.0.0.1:6379> setnx ossca 123
(integer) 0
127.0.0.1:6379> get ossca
"2024"
127.0.0.1:6379> set ossca 4/26
OK
127.0.0.1:6379> get ossca
"4/26"
  • 처음에는 key가 ossca인 데이터가 없으므로 성공적으로 만들어졌기에 1이 반환됨.
  • 두번째는 ossca라는 값이 있으므로 실패를 뜻하는 0이 반환됨.
  • get을 통해 2024 값을 잘 읽어오고
  • set은 lock과 관계 없이 설정이 된다.
  • 프로세스가 지워지면 ttl을 5초 걸어두어 그 전에 접속하면 된다. 그 후에는 key가 사라진다.

Leaderboard


  • Sorted Set(zset)이 score를 저장할 수 있기에 redis로 활용함.
    • score, key 순
    • zrange zkey1 0 -1 하면 오름차순으로 랭킹이 나옴
    • src/t_zset.c
      • double 형태로 인자를 받음
      • zslCreateNode()
      • 근사값이므로 값이 오차가 발생할 수 있음
127.0.0.1:6379> zadd zkey1 100 one
(integer) 1
127.0.0.1:6379> zadd zkey1 1000 two
(integer) 1
127.0.0.1:6379> zrange zkey1 0 -1
1) "one"
2) "two"
127.0.0.1:6379> zadd zkey1 500 three
(integer) 1
127.0.0.1:6379> zrange zkey1 0 -1
1) "one"
2) "three"
3) "two"
127.0.0.1:6379> zrange zkey1 0 -1 withscores
1) "one"
2) "100"
3) "three"
4) "500"
5) "two"
6) "1000"

 

+ Recent posts