Redis Loadable Modules System

Salvatore Sanfilippo

Redis 로드 가능한 모듈 시스템

원문은 Salvatore Sanfilippo님이 에 게재했습니다. 이 블로그 구독하기

시간문제였고 결국 일어나고 말았다. 7년 전 Redis 1.0 릴리스 노트에서 나는 앞으로의 흥미로운 기능 중 하나로 “loadable modules”를 언급한 바 있다. 당시에는 그런 기능에 정말 관심이 많았지만, 세월이 흐르면서 Redis에 로드 가능한 모듈을 추가한다는 아이디어에 점점 더 회의적이 되었다. 아마도 그럴 만한 이유가 있었을 것이다.

모듈은 시스템에서 가장 흥미로운 기능이면서 동시에 가장 문제가 많은 기능이 될 수 있다. 버전 간 API 비호환성, 품질 낮은 모듈로 인한 시스템 충돌, 확장 가능한 시스템의 정체성 상실 등이 발생할 수 있는 문제들이다. 그래서 수년 동안 나는 어떻게든 Redis에 모듈 추가를 피했고, Lua 스크립팅은 그 추가를 미루는 좋은 도구였다. 동시에 수년간의 스크립팅 경험은 스크립팅이 기존 기능들을 “조합”하는 방법일 뿐, 시스템이 애초에 설계되지 않은 사용 사례까지 시스템의 역량을 확장하는 방법은 아니라는 것을 보여주었다.

모듈에 대한 이전 시도들은 또한 Redis와 로드 가능한 모듈을 결합할 때의 주요 애로사항 중 하나가 모듈이 Redis 코어에 바인딩되는 방식이라는 것을 보여주었다. 여러 면에서 Redis는 데이터베이스라기보다 프로그래밍 언어에 더 가깝다. Redis를 제대로 확장하려면 모듈이 시스템의 내부 API에 접근할 수 있어야 한다. Redis 코어 함수를 모듈에 직접 노출하면 엄청난 문제가 생긴다. 모듈이 Redis의 내부 세부사항에 의존하기 시작하는 것이다. Redis 코어가 진화하면 모듈을 다시 작성해야 한다. 이는 깨지기 쉬운 모듈 생태계를 만들거나 Redis 코어의 진화를 멈추게 한다. Redis 내부는 진화를 멈출 수 없고, 모듈 개발자들도 내부 변화에 계속 발맞추기 위해 모듈을 계속 수정할 수는 없다(과거 일부 인기 시스템에서 실제로 일어났고 결과는 좋지 않았다).

이런 교훈들을 염두에 둔 채, 나는 앞으로 몇 달간의 로드맵에 대해 이야기하기 위해 Redis Labs에서 미팅을 하려고 카타니아를 떠나 텔아비브로 향하는 비행기에 올랐다. 우리 대화 주제 중 하나는 로드 가능한 모듈이었다. 비행 중에 나는 Redis 코어와 모듈 API를 진정으로 분리하면서도 Redis 데이터 구조를 직접 조작할 수 있는 저수준 접근을 유지하는 것이 가능한지 자문해 보았다. 그래서 나는 즉시 뭔가를 코딩하기 시작했다. 내가 원했던 것은 미래를 위한 극단적인 수준의 API 호환성이었다. 그래서 오늘 작성된 모듈이 Redis 코어의 변화와 관계없이 동일한 API로 4년 후에도 동작할 수 있도록 하는 것이었다. 나는 또한 바이너리 호환성을 원했다. 그래서 4년 된 모듈이 재컴파일할 필요조차 없이 새로운 Redis 버전에서도 그대로 *로드*되어 예상대로 동작할 수 있도록 하고 싶었다.

비행이 끝날 무렵 나는 이미 동작하는 무언가를 담은 “modules” 브랜치를 가지고 텔아비브에 도착했다. 우리는 API가 어떻게 동작할지 함께 논의했고, 결국 모두가 Redis 내부를 직접 조작할 수 있는 것이 근본적인 기능이라는 데 동의했다. 우리가 이루고자 했던 것은 Redis 개발자들이 Redis 네이티브 커맨드만큼 강력하고, 또 그만큼 빠른 커맨드를 만들 수 있게 하는 것이었다. 이는 단순히 Redis 커맨드를 호출하는 고수준 API만으로는 달성할 수 없다. 너무 느리고 제한적이기 때문이다. Lua가 이미 할 수 있는 일만 할 수 있는 Redis 모듈 시스템을 만드는 것은 의미가 없다. “이 키에 연결된 값을 가져와 줘, 타입이 뭐지? 이 값에 대해 이런 저수준 연산을 수행해 줘. 이 위치에 있는 정렬된 집합(sorted set)에 대한 커서를 줘, 다음 요소로 이동해 줘” 와 같이 말할 수 있어야 한다. 이러한 저수준 접근을 위한 중간 계층으로 동작하는 API를 만드는 것은 까다롭지만, 분명히 가능하다.

나는 집으로 돌아와 즉시 모듈 시스템 작업에 착수했다. 몇 주가 지나자 나는 이미 흥미로운 모듈을 개발할 만큼 충분히 기능적인 프로토타입을 갖게 되었다. 데이터 타입에 대한 저수준 접근, 필요할 때 래퍼 없이 문자열 내부를 직접 다룰 수 있는 문자열 DMA, 복제 API, 흥미로운 정렬된 집합 이터레이터 API 등과 같은 저수준 기능들을 갖추고 있었다. 매우 유망한 시작처럼 보였지만, 프로젝트는 다소 “비밀”이었다. 처음에는 실현 가능성이 불분명했기 때문이다. 또한 API가 극도로 불안정하고 변경될 가능성이 큰 동안 모두가 모듈 개발을 시작하는 것을 피하고 싶었다.

완전하지는 않지만 이 과정의 결과는 매우 유망하므로, 오늘 나는 Redis Conference 2016에서 이 새로운 기능을 발표하며, 코드는 막 “unstable” 브랜치에 푸시되었다. 하지만 API를 좀 살펴보자…

모듈이 무엇을 할 수 있고 어떻게 동작하는지에 대한 아주 간단한 예시는 다음과 같다. 이는 한 리스트에서 다른 리스트로 요소를 옮기는 “list splice” 연산을 구현한 것이다:

int HelloListSpliceAuto_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    if (argc != 4) return RedisModule_WrongArity(ctx);

    RedisModule_AutoMemory(ctx);

    RedisModuleKey *srckey = RedisModule_OpenKey(ctx,argv[1],
        REDISMODULE_READ|REDISMODULE_WRITE);
    RedisModuleKey *dstkey = RedisModule_OpenKey(ctx,argv[2],
        REDISMODULE_READ|REDISMODULE_WRITE);

    /* Src and dst key must be empty or lists. */
    if ((RedisModule_KeyType(srckey) != REDISMODULE_KEYTYPE_LIST &&
         RedisModule_KeyType(srckey) != REDISMODULE_KEYTYPE_EMPTY) ||
        (RedisModule_KeyType(dstkey) != REDISMODULE_KEYTYPE_LIST &&
         RedisModule_KeyType(dstkey) != REDISMODULE_KEYTYPE_EMPTY))
    {
        return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE);
    }

    long long count;
    if ((RedisModule_StringToLongLong(argv[3],&count) != REDISMODULE_OK) ||
        (count < 0))
    {
        return RedisModule_ReplyWithError(ctx,"ERR invalid count");
    }

    while(count-- > 0) {
        RedisModuleString *ele;

        ele = RedisModule_ListPop(srckey,REDISMODULE_LIST_TAIL);
        if (ele == NULL) break;
        RedisModule_ListPush(dstkey,REDISMODULE_LIST_HEAD,ele);
    }

    size_t len = RedisModule_ValueLength(srckey);
    RedisModule_ReplyWithLongLong(ctx,len);
    return REDISMODULE_OK;
}

int RedisModule_OnLoad(RedisModuleCtx *ctx) {
    if (RedisModule_Init(ctx,"helloworld",1,REDISMODULE_APIVER_1)
        == REDISMODULE_ERR) return REDISMODULE_ERR;

    if (RedisModule_CreateCommand(ctx,"hello.list.splice.auto",
        HelloListSpliceAuto_RedisCommand,
        "write deny-oom",1,2,1) == REDISMODULE_ERR)
        return REDISMODULE_ERR;
}

깔끔한 API, 그리고 오용되기 쉽지 않은 API를 제공하기 위해 많은 노력이 기울여졌다. 예를 들어 자동 메모리 관리에 대한 지원이 있어, 커맨드가 동작하는 컨텍스트가 사용자가 명시적으로 해제하지 않은 객체들을 수집하여, 필요할 경우 커맨드가 반환될 때 해제한다. 이는 모듈 작성을 훨씬 더 간단하게 만든다.

API 문서는 여기에서 확인할 수 있다(완벽하지는 않지만 익숙해지기에는 충분하다):
https://github.com/antirez/redis/blob/unstable/src/modules/INTRO.md

그리고 API 레퍼런스는 여기에서 확인할 수 있다:
https://github.com/antirez/redis/blob/unstable/src/modules/API.md

그리고 커맨드의 간단한 예제들은 여기에서 확인할 수 있다:
https://github.com/antirez/redis/blob/unstable/src/modules/helloworld.c

API는 아직 완전하지도 안정적이지도 않으며, Redis의 다음 안정 버전(아마도 4.0)과 함께 릴리스될 예정이다. 하지만 이미 많은 일을 하기에 충분하며, 내 동료들은 역 인덱스부터 인증 시스템까지 매우 흥미로운 것들을 만들어 냈다. 앞으로 몇 주 안에 우리는 모든 빈틈을 메울 것이다. 예를 들어 아직 저수준 Set API가 없으므로, 당분간은 Call() 스타일 API를 사용해야 한다. 마찬가지로 이터레이터는 현재 정렬된 집합 타입에 대해서만 제공되며, 그 외에도 마찬가지다.

하지만 중요한 점은 이제 과정이 시작되었고 Redis가 확장 가능한 시스템이 되어가고 있다는 것이다. 이는 Redis 사용자에게 더 큰 힘을 줄 것이라고 생각한다. 자신들의 문제를 모델링하는 데 Redis를 사용함에 있어 프로젝트 자체보다 “더 빠르게” 나아갈 수 있는 힘을 말이다. Redis 4.0 RC가 나온 이후에는 앞으로 수년간 절대 API를 깨뜨리지 않을 것이라는 큰 약속과 함께, 모듈 작업이 헛되지 않도록 할 것이다. 모듈 등록 시 특정 API 버전을 요구하므로 우리는 API를 *개선할* 수 있으며, 동시에 하위 호환성을 유지하면서 새로운 버전의 API를 릴리스하는 것이 가능할 것이다.

곧 redis-cli를 사용해 Redis 프로토콜을 사용하는 서버에 모듈을 등록할 수 있는 Modules Directory가 생길 것이다. 아쉽게도 제때 완성하지는 못했지만, 몇 주면 될 일이다.

앞으로 일어날 일에 대해 정말 정말 기대된다! 모듈은 클라이언트와 마찬가지로 바자(Bazaar) 모델을 따를 것이므로 “공식 모듈”은 없을 것이다. 좋은 모듈은 분명히 사용될 것이고, 모두 Redis 사이트에 목록으로 올라갈 것이며, 아마도 GitHub 스타 수 등으로 순위가 매겨질 것이다.

많은 사용자가 모듈 생태계의 일원이 되어, 코어 내부에서 해결하는 것이 현명하지 않았지만 모듈에는 잘 맞는 매우 특정한 사용 사례들을 Redis가 해결할 수 있게 만들기를 바란다.

이제 API가 아직 가변적인 동안 여러분의 피드백이 필요하다. 어떻게 생각하는지 알려 달라!

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글