BITFIELD 100개 더
원문은 Salvatore Sanfilippo님이 에 게재했습니다. 이 블로그 구독하기
오늘 Redis가 7주년을 맞았다. 이를 조금이나마 기념하고자 지난 며칠간 재미있는 코딩 마라톤을 펼치며 BITFIELD라는 새롭고 살짝 미친 명령어를 구현했다.
이 명령어의 핵심 자체는 새로운 것이 아니다. 과거에 나와 다른 사람들이 제안한 적이 있지만 진지하게 다뤄진 적은 없었고, 아이디어는 늘 조금 이상하게 보였다. Redis에는 이미 비트 연산이 있다. 특정 사용자들은 이를 아주 좋아한다. 많은 데이터를 압축적으로 표현하는 좋은 방법이기 때문이다. 하지만 지금까지는 각 비트를 개별적으로 다뤘다. 비트를 설정하고, 테스트하고, 가져오고, 범위 내에서 설정된 비트 수를 세는 식이었다.
비트필드를 구현하면 어떨까? 짧든 길든 임의 크기의 정수를 임의의 오프셋에 저장하는 것이다. 그러면 Redis 문자열을 5비트 부호 있는 정수의 배열처럼 쓸 수 있고, 단 한 비트도 낭비하지 않을 수 있다.
며칠 전 Redis Labs의 Yoav Steinberg가 비트 오프셋에 저장된 임의 크기 정수에 대한 일련의 명령어를 보다 진지한 방식으로 제안해 왔다. 이메일을 읽고 나는 미소를 지었다. 일종의 비밀스러운 꿈이었기 때문이다. Yoav의 제안과 Redis Labs 엔지니어들의 다른 피드백을 바탕으로, 나는 타입 정의를 위한 짧은 이름을 사용하고 오버플로 의미를 매우 세밀하게 제어할 수 있는, 서브 커맨드를 가진 단일 명령어의 초기 사양을 작성했다.
계획은 오늘 릴리스하는 것이었기에, 몇 분 전에 첫 번째 구현을 마쳤다. Redis가 생일에 우리가 실제로 일을 한다는 것을 알아주길 바라는 마음에서다.
그렇게 탄생한 BITFIELD 명령어는 다음과 같은 서브 커맨드를 지원한다:
SET <type> <offset> <value> — 지정된 값을 설정하고 이전 값을 반환한다.
GET <type> <offset> — 지정된 값을 가져온다.
INCRBY <type> <offset> <increment> — 지정된 카운터를 증가시킨다.
추가로 OVERFLOW라는 메타 명령어가 있는데, 뒤따를 명령어들의 오버플로 의미를 설정하는 데 사용된다(짐작했겠지만). 그래서 OVERFLOW는 여러 번 지정할 수 있다:
OVERFLOW SAT — 포화(Saturation) 모드로, 어느 한쪽으로 오버플로가 발생하면 정수를 해당 방향의 최댓값으로 포화시킨다.
OVERFLOW WRAP — 일반적인 래핑(wrap around)이다. 흥미로운 점은 부호 있는 정수에서도 가장 작은 값이나 가장 큰 값 쪽으로 래핑되면서 동작한다는 것이다.
OVERFLOW FAIL — 이 모드에서는 값이 오버플로될 경우 연산 자체가 수행되지 않는다.
정수 타입은 비트 수 앞에 “u” 또는 “i” 접두사를 붙여 지정할 수 있다. 예를 들어 u8, i5, u20, i53은 모두 유효한 타입이다. 한 가지 제한이 있다. 현재 Redis 프로토콜이 64비트 부호 없는 정수를 반환할 수 없기 때문에 u64는 지정할 수 없다.
몇 가지 예제를 볼 차례다. 8비트 부호 없는 정수를 증가시키려면 다음과 같이 할 수 있다:
127.0.0.1:6379> BITFIELD mykey incrby u8 100 1
1) (integer) 3이는 오프셋 100(비트맵에서 101번째 비트)에 있는 8비트 부호 없는 정수를 증가시키는 것이다.
하지만 오프셋을 지정하는 다른 방법도 있다. 오프셋 앞에 “#”을 붙이는 것이다. 이는 “문자열을 지정된 크기의 카운터 배열로 다루고, N번째 카운터를 설정하라”는 의미다. 기본적으로 8비트 타입과 함께 #10을 사용하면 오프셋은 8*10을 곱해 얻어지며, 이렇게 하면 오프셋 계산을 직접 하지 않고도 여러 카운터에 독립적으로 접근할 수 있다:
127.0.0.1:6379> BITFIELD mykey incrby u8 #0 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u8 #0 1
1) (integer) 2
127.0.0.1:6379> BITFIELD mykey incrby u8 #1 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u8 #1 1
1) (integer) 2오버플로를 제어할 수 있다는 점도 흥미롭다. 예를 들어 1비트 부호 없는 카운터는 기본 오버플로 정책인 “wrap”에서 실제로 0과 1 사이를 토글한다:
127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 0
127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 0보다시피 0과 1이 번갈아 나타난다.
포화(saturation)도 유용할 수 있다:
127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -3
127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -6
127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -8
127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -8보다시피 3씩 감소시켜도 -8 아래로 내려가지 않는다.
한 명령어 안에 여러 연산을 담을 수 있다는 점에 유의하자. 항상 결과 배열을 반환한다:
127.0.0.1:6379> BITFIELD mykey get i4 100 set u8 200 123 incrby u8 300 1
1) (integer) -8
2) (integer) 123
3) (integer) 7이 명령어의 의도된 용도는 실시간 분석, A/B 테스팅, 정수의 오버플로를 활용해 매번 사용자에게 조금씩 다른 것을 보여주는 것이다. 이렇게 많은 작은 카운터들을 공유되고 메모리 효율적인 방식으로 패킹하는 것은 여러 방식으로 활용될 수 있지만, 이는 Redis 커뮤니티의 재능 있는 프로그래머들에게 숙제로 남겨둔다.
이 명령어는 향후 몇 주 안에 Redis 안정 버전에 백포트될 예정이므로, 머지않아 사용할 수 있게 될 것이다.
구현이 궁금한가? 아마 생각보다 더 복잡할 것이다: https://github.com/antirez/redis/commit/70af626d613ebd88123f87a941b0dd3570f9e7d2
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기