뒤로
한상엽
한상엽 ·

REDIS를 통해 주어진 IP의 범위를 빠르게 탐색하기.

안녕하세요, 필락시스 한상엽입니다.

오늘은 Phylaxis Proxy 개발 과정 중, 주어진 IP의 범위를 효율적으로 탐색하는 방법을 찾아가게 된 과정을 이야기해보려 해요.

왜, IP의 범위를 탐색하죠?

저희 방화벽 소프트웨어는 HTTP 로그를 남겨요.

로그에는 해당하는 IP의 국가, 통신사 등의 정보도 함께 담기게 될 거에요.

여기서 자주 하시는 질문이 있어요.

Q: 외부 API 호출해서 해당 IP 정보 가져오면 되는 것 아닌가요?

A: 아닙니다.

저도 물론 외부 API를 호출해서 편하게 개발할 수 있다면 참 좋을텐데...

슬프게도 Phylaxis Proxy는 초당 N Million Requests를 처리하도록 설계하고 있기 때문에, 수백만 요청을 외부 API에 요청하게 된다면, 그 자체가 민폐일 뿐더러, 만약 유료 API라면 과금이 엄청날 거에요.

그럼 로컬에 저장하고 불러오자!

좋아요, IP를 로컬에 저장하고 불러오면 위와 같은 대참사는 벌어지지 않을 거에요.

자 그런데, 잠시만요.

IPv4는 4개의 8비트로 이루어진 주소에요.

그렇다면... 총 아이피의 갯수는... 2^32... 즉

4,294,967,296개...?

아이코, 43억개에 달하는 IP를 각각 데이터베이스에 담을 수도 없을 테지만,

만약 담았다 한들, 쿼리가 매우 곤란해질 것 같아요.

보통 IP는 블럭으로 묶어서 할당한다! 이 점을 이용!

괜찮아요.

IP는 보통 블럭으로 묶여서 할당되어 있거든요!
가장 흔한 단위는, /24 단위인데요, 이는 256개의 아이피를 담고 있어요.

/23이면 512, /22라면 1024개가 될거에요.

좋아요! 그럼 블럭 단위로 저장하면 되겠네요.

자, "192.168.0.0/24"와 같이 저장하면...

어..? "192.168.0.12"를 어떻게 탐색하지..?

IP는 10진수의 정수로 변환할 수 있다!

맞다! IP는 10진수의 정수로 변환할 수 있어요.

간단하게 설명하자면,

IP는 32비트의 이진수이고, 점분리 십진수 표현방식으로 쓰여지고 있어요.

그렇다면, 10진수로 변환할수도 있겠네요!!

자,, 그럼 코드를 한 번 써볼까요

func ipToInt(ip string) uint32 {
	octets := strings.Split(ip, ".")
	var intIP uint32
	for i := 0; i < 4; i++ {
		octet, err := strconv.Atoi(octets[i])
		if err != nil {
			return 0
		}
		intIP += uint32(octet) << uint(24-(8*i))
	}
	return intIP
}

짠, 간단한 Go 코드 몇줄로 IP를 10진수 정수로 변환하는 코드에요.

그럼,, 이걸 이제 DB에 넣어서 범위 탐색만 하면 되겠는걸요!!


빠른 탐색을 위해, 인메모리 KV 데이터베이스를 사용하자

이걸 MongoDB, PostgreSQL에 올려서 처리한다면...
수백만 요청이 쏟아질 때 분명 병목현상이 발생할 거에요.

우리는 트랜잭션도 필요 없고, 복잡한 쿼리도 필요 없으니까

온 메모리에서 빠르게 동작하는 REDIS를 사용하기로 했어요!

image.png

위 코드를 통해, KEY의 고유성을 확보하고 IP 데이터를 삽입했어요.

쿼리는 SCORE 범위 역순으로 탐색하도록, REDIS의 "ZREVRANGEBYSCORE"을 사용했어요.

country := ZRevRangeByScoreRDB("ipData2", "-inf", strconv.Itoa(int(ipToInt(c.IP()))))

짠, 이렇게 하면 IP를 10진수로 변환하고, 해당 수를 SCORE의 시작, 끝을 무한으로 지정한다음 역순으로 쿼리하고 리밋을 1로 탐색하면, 가장 가까운 IP 범위의 시작이 나와요!

이를 통해, 저장해둔 국가, 통신사 등의 정보를 REDIS에서 받아올 수 있었어요.

7

댓글

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.