Redis 로더블 모듈 시스템
시간문제였지만 결국 일어났습니다. 7년 전 Redis 1.0 릴리스 노트에서 저는 앞으로 추가하면 흥미로울 기능 중 하나로 ‘로더블 모듈’을 언급한 적이 있습니다. 당시에는 그 기능에 정말 관심이 많았지만, 시간이 지나면서 Redis에 로더블 모듈을 추가하는 아이디어에 점점 더 회의적이 되었습니다. 아마 그럴 만한 이유가 있었을 겁니다.
모듈은 시스템에서 가장 흥미로운 기능이 될 수 있는 동시에 가장 문제가 많은 기능이 될 수도 있습니다. 버전 간 API 비호환성, 품질이 낮은 모듈로 인한 시스템 충돌, 확장 가능한 시스템이 겪는 정체성 결여 등이 발생할 수 있는 문제들입니다. 그래서 저는 수년간 Redis에 모듈을 추가하지 않으려 애썼고, Lua 스크립팅은 그 도입을 미루는 데 유용한 도구였습니다. 동시에 수년간 스크립팅을 사용해 본 결과, 스크립팅은 기존 기능을 ‘조합’하는 방법일 뿐 애초에 고려하지 않았던 사용 사례까지 시스템의 역량을 확장하는 방법은 아니라는 사실이 분명해졌습니다.
이전에 모듈을 시도했던 경험에서도 Redis와 로더블 모듈을 결합할 때 가장 큰 어려움 중 하나는 모듈이 Redis 코어와 결합되는 방식이라는 점이 드러났습니다. 여러모로 Redis는 데이터베이스라기보다 프로그래밍 언어에 가깝습니다. Redis를 제대로 확장하려면 모듈이 시스템의 내부 API에 접근할 수 있어야 합니다. Redis 코어 함수를 모듈에 직접 노출하면 큰 문제가 생깁니다. 모듈이 Redis 내부 구현에 의존하게 되기 때문입니다. Redis 코어가 발전하면 모듈을 다시 작성해야 합니다. 이는 생태계를 취약하게 만들거나, 반대로 Redis 코어의 진화를 멈추게 합니다. Redis 내부는 진화를 멈출 수 없고, 모듈 개발자가 내부 구조 변화에 맞춰 계속 모듈을 수정할 수도 없습니다. 과거 일부 유명한 시스템에서 이런 일이 있었고 결과는 좋지 않았습니다.
이런 교훈들을 염두에 두고 저는 앞으로 몇 달간의 로드맵을 논의하기 위해 Redis Labs에서 회의를 하러 카타니아를 떠나 텔아비브로 향하고 있었습니다. 대화 주제 중 하나가 로더블 모듈이었습니다. 비행 중에 저는 Redis 코어와 모듈 API를 진정으로 분리하면서도 Redis 자료구조를 직접 조작할 수 있는 저수준 접근을 유지할 방법이 있을지 자문해 보았습니다. 그래서 곧바로 코딩을 시작했습니다. 제가 원한 것은 미래를 위한 극단적인 수준의 API 호환성이었습니다. 오늘 작성한 모듈이 Redis 코어가 어떻게 변하든 4년 뒤에도 동일한 API로 동작하도록 하는 것이었습니다. 더 나아가 바이너리 호환성까지 원했습니다. 4년 된 모듈을 재컴파일조차 하지 않고 새로운 Redis 버전에서 그대로 *로드*해 예상대로 동작하게 하고 싶었습니다.
비행이 끝날 무렵 저는 이미 동작하는 무언가를 담은 ‘modules’ 브랜치를 가지고 텔아비브에 도착했습니다. 우리는 함께 API가 어떻게 동작해야 할지 논의했고, 결국 모두가 Redis 내부를 직접 조작할 수 있어야 한다는 것이 핵심 기능이라는 데 동의했습니다. 우리가 이루고자 한 것은 Redis 개발자들이 Redis 네이티브 명령어만큼 강력하고 빠른 명령어를 만들 수 있게 하는 것이었습니다. Redis 명령어를 호출하는 고수준 API만으로는 이를 달성할 수 없습니다. 너무 느리고 제약이 많기 때문입니다. Lua가 이미 할 수 있는 일만 하는 Redis 모듈 시스템을 만드는 것은 의미가 없습니다. “이 키에 연결된 값을 가져와 줘, 타입이 뭐지? 이 값에 대해 이런 저수준 연산을 수행해 줘. 이 위치에 있는 정렬된 집합(sorted set)에 대한 커서를 줘, 다음 요소로 이동해 줘” 같은 일을 할 수 있어야 합니다. 이런 저수준 접근을 위한 중간 계층으로 동작하는 API를 만드는 일은 까다롭지만 분명 가능합니다.
집으로 돌아온 저는 곧바로 모듈 시스템 작업에 착수했습니다. 2주 정도 지나자 흥미로운 모듈을 개발할 수 있을 만큼 충분히 기능하는 프로토타입이 나왔습니다. 자료형에 대한 저수준 접근, 필요할 때 래퍼 없이 문자열 내부를 직접 다룰 수 있는 문자열 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 문서는 여기에서 확인할 수 있습니다(완벽하지는 않지만 익숙해지기에는 충분합니다):
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 4.0 RC가 나온 이후에는 수년간 API를 절대 깨뜨리지 않겠다는 큰 약속과 함께하므로, 모듈 작업이 헛되지 않을 것입니다. 모듈 등록 시 특정 API 버전을 명시하도록 되어 있어 API를 *개선*하면서도 하위 호환성을 유지한 채 새로운 버전의 API를 출시하는 것이 가능하다는 점도 참고해 주시기 바랍니다.
곧 Redis 프로토콜을 사용하는 서버에 redis-cli를 이용해 모듈을 등록할 수 있는 Modules Directory가 마련될 예정입니다. 아쉽게도 제때 완성하지는 못했지만, 몇 주면 될 일입니다.
앞으로 일어날 일이 정말 정말 기대됩니다! 모듈은 클라이언트와 마찬가지로 바자(Bazaar) 모델을 따를 것이므로 ‘공식 모듈’은 없을 것입니다. 좋은 모듈은 분명 사용될 것이고, 모두 Redis 사이트에 목록으로 올라갈 것이며, 아마도 GitHub 스타 수 등으로 순위가 매겨질 것입니다.
많은 사용자가 모듈 생태계에 참여해, 코어 안에서 해결하는 것이 현명하지는 않지만 모듈에는 아주 잘 맞는 매우 구체적인 사용 사례들을 Redis가 해결할 수 있도록 해주시길 바랍니다.
API가 아직 유연한 지금 여러분의 피드백이 필요합니다. 어떻게 생각하시는지 알려주세요!
글을 무작위로 읽기