안녕하세요. LINE VOOM AI 조직의 서버 개발자 박찬우, 양유성입니다. 이번 글의 제목은 'Massive AI Real-time Embedding을 효율적으로 다루기'입니다. 제목을 간결하게 만들다 보니 추상적인 제목이 되었는데요. 조금 더 자세히 말씀드리자면 AI에 사용되는 실시간 임베딩을 제공하는 서버를 구축하면서 성능은 높게, 인프라 비용은 적게 들도록 구축하기 위한 과정과 결과라고 말씀드릴 수 있습니다.
이 글은 비단 AI뿐만 아니라 대용량 데이터를 실시간으로 서비스해야 하는 모든 분야의 서버를 구축할 때 필요한 내용을 담고 있습니다(임베딩 자체를 생성하는 내용은 담고 있지 않습니다). 서버를 구축할 때 항상 우리를 고민하게 만드는 성능과 비용 절감에 관한 이 글의 내용이 많은 분들께 참고가 되기를 바라며 글을 시작하겠습니다.
이번 글에서 구체적인 수치는 대부분 생략했습니다. 개발자 각자가 다뤄야 할 데이터가 같지 않으므로 구체적인 수치를 제시하기보다는 문제 접근 방법과 해결 과정을 자세히 설명해 여러분이 쉽게 방법을 재현하고 효과를 체감할 수 있도록 글을 구성했습니다.
프로젝트 소개
이번 프로젝트를 한 문장으로 설명하면 '대용량 임베딩을 실시간으로 AI 모델에 제공하기 위한 프로젝트'입니다. AI 모델의 필요에 맞는 조건의 임베딩을 실시간으로 제공하는 서버를 구축하면서, 빠른 응답 속도와 높은 TPS를 실현하고 동시에 인프라 비용까지 최소화하는 것을 목표로 잡았습니다.
프로젝트 목표
프로젝트 목표를 중요도 순으로 하나씩 구체적으로 소개하겠습니다.
1. 높은 TPS(transactions per second) 달성
이번 프로젝트에서 가장 중요한 목표는 요구 TPS를 달성하는 일이었습니다. TPS가 달성되지 않으면 서비스가 불가능하기 때문입니다. AI 모델에서 요구하는 TPS는 절대적인 수치만 보면 다른 LINE 서비스에서도 평범히 볼 수 있는 수준이었지만, 자세히 분석해 보니 다른 서비스의 TPS에 수십, 수백 배를 곱한 것과 비슷한 수준이었습니다. 그 이유는 아래 임베딩의 데이터 특성을 설명하면서 말씀드리겠습니다.
2. 빠른 응답 속도
아무리 좋은 서비스도 응답 시간이 느려 사용자가 답답함을 느낀다면 결코 좋은 사용자 경험을 줄 수 없습니다. 아래에서 다시 말씀드리겠지만 데이터 크기가 커서 응답 속도가 느린 편이었기 때문에 이를 단축하는 것이 중요했습니다.
3. 인프라 비용 절감
높은 TPS를 실현하기 위해서는 필연적으로 대규모의 인프라를 사용하게 되는데요. 그럴수록 비용 절감이 중요해지면서 프로젝트의 주요 목표 중 하나가 됩니다. 예를 들어, VM 10대 규모의 서비스에서 서버 성능 향상과 효율화를 통해 5대로 절감하는 것과, VM 100대 규모의 서비스에서 50대로 절감하는 것은 그 비율은 같을지라도 실제 비용 절감 효과는 10배 차이 나기 때문입니다.
임베딩이란?
먼저 이 글에 자주 등장하는 키워드인 임베딩이 무엇인지 설명하겠습니다.
AI와 임베딩
설명을 시작하기 전에 임베딩의 예시를 보여드리겠습니다.
"data": [
{
"embedding": [
1.543545822800004554,
-0.014464245600309352,
-0.021545555220005484,
...
-2.547132266452536e-05,
-1.5454545875425444544,
-1.0452722143541654544
],
}
],
보시다시피 사람이 무슨 뜻인지 알아보기 힘든 숫자 배열인데요. 임베딩은 원래 사람을 위한 것이 아니라 AI를 위한 것입니다. 임베딩은 단어, 이미지, 동영상 등의 실제 개체를 컴퓨터가 처리할 수 있는 형태로 표현한 것인데요. 콤마로 구분된 숫자 하나하나가 차원이고 이 숫자 배열 전체는 벡터라고 생각한다면, 각 임베딩을 n차원의 공간에 표시할 수 있습니다.
이를 기반으로 개체 A의 임베딩과 개체 B의 임베딩을 공간에 표시한 뒤 서로 간의 거리를 측정해 서로 얼마나 유사한지 판단할 수 있습니다. 이미지를 예로 들면, 검은 고양이 사진과 하얀 고양이 사진은 얼룩말 사진보다 서로 인접한 공간에 표시될 것입니다. 이처럼 임베딩은 AI가 개체 간 유사도를 평가할 때 사용하는 필수 요소입니다.
실시간 임베딩이란?
AI에 임베딩이 필요한 시점은 크게 두 가지로 나눌 수 있습니다. 하나는 모델을 학습시킬 때이고, 다른 하나는 학습된 모델을 실제 서비스에 적용한 후 사용자의 요청을 받을 때입니다. 후자의 경우에는 사용자의 요청에 빨리 응답해야 하기 때문에 서버는 모델이 요청한 대량의 임베딩을 매우 빠른 속도로 제공할 수 있어야 하며, 이때 사용하는 임베딩을 실시간 임베딩(real-time embedding)이라고 합니다.
임베딩의 데이터 특성
임베딩이 더 높은 차원으로 구성될수록, 구성하고 있는 값의 범위가 클수록 AI 모델은 보다 정확히 판단할 수 있지만, 이는 임베딩이 더 큰 데이터가 된다는 뜻이기도 합니다. 더욱이 AI 모델은 이런 임베딩을 단건으로 요청하지 않습니다. 서버 관점에서 큰 임베딩이 동시에 여러 건 입출력된다는 것은 대용량 I/O가 발생한다는 의미입니다.
프로젝트에 적합한 DB 찾기
프로젝트를 시작하면서 가장 먼저 진행한 일은 DB 선정이었습니다. 프로젝트의 매우 높은 성능 요구 수준을 달성하기 위해서는 고성능 DB가 꼭 필요했기 때문입니다.
필요한 DB의 특성 정리하기
필요한 DB의 특성을 먼저 파악해야 이 기 준에 따라 어떤 DB를 사용할지 판단할 수 있기 때문에 구체적으로 어떤 특성이 필요한지 아래와 같이 정리했습니다.
RDB 혹은 NoSQL 어느 쪽이든 무관
이번 프로젝트는 실시간 임베딩을 키-값 구조로 저장하고 키(서브 키(sub-key) 포함)로 조회하는 것으로 충분했습니다. 저장된 데이터의 관계성을 조회할 용도가 아니기 때문에 NoSQL 종류의 DB도 사용할 수 있었습니다.
샤딩 지원
QPS(query per second) 수준이 변하더라도 탄력적으로 대량의 I/O를 빠른 속도로 지원하고, 확장이 필요할 때 무중단으로 스케일 아웃하기 위해서는 샤딩이 지원돼야 합니다. 특히 네이티브 샤딩을 지원하는 DB가 좋습니다. MySQL과 같이 별도로 샤딩을 구현할 수 있는 경우도 있지만, 효율성과 유지 보수를 고려할 때 네이티브 샤딩을 지원하는 DB가 유리합니다.
네이티브 샤딩을 전제로 설계된 DB는 여러 노드에 걸쳐 데이터가 적재되기 때문에 요청이 노드 전체에 고르게 퍼져 그만큼 네트워크 부하가 나뉜다는 이점이 있는데요. 이 이점은 특히 임베딩처럼 네트워크 대역폭을 많이 점유하는 데이터를 저장할 때 매우 중요한 특성입니다. 이런 DB로 Redis Cluster, MongoDB Sharded Cluster가 대표적인 예입니다.
다 건 입출력 특화 여부
여러 임베딩을 한 번에 대량 조회해야 하므로 DB 차원에서 특화된 명령어를 지원해 주면서, 해당 명령어가 단건 명령어보다 성능이 좋은 쪽이 유리했습니다. 대표적으로 Redis의 mget
이나 hmget
명령어는 개별 get
요청을 반복하는 것보다 수 배에서 수십 배까지 성능이 좋습니다.
빠른 응답 시간
모든 서비스가 그렇지만 실시간 임베딩 서버는 특히 응답 시간이 중요한데요. 일반적으로 메모리를 기반으로 한 DB들이 가장 빠른 응답 시간을 보여주며 Redis Cluster나 Memcached가 대표적인 예입니다.
QPS 단위당 구축비용
스케일 아웃이 가능한 대부분의 DB는 비용을 투입할수록 성능이 향상되기 때문에 어느 DB든 목표 QPS를 달성할 수 있습니다. 하지만 비용을 생각하면 투입 비용 대비 QPS가 높은 DB를 골라야 하며, 특히 이번 프로젝트는 요구되는 QPS가 굉장히 높았기 때문에 QPS 단위당 구축 비용이 더욱 적어야 했습니다. QPS 단위당 구축 비용은 Redis Cluster가 MongoDB나 MySQL보다 월등히 적습니다.
Reactive Driver 지원
서버를 Reactor로 구현하고 DB가 Reactive 드라이버를 지원하면 Reactive Processing의 우수한 성능을 누릴 수 있습니다. Redis와 MongoDB, Cassandra, 아직 일부 기능이 제한적이지만 MySQL도 R2DBC라는 Reactive 드라이버를 지원합니다.
DB 선정 - Redis Cluster
저희에게 필요한 DB 특성을 정리한 결과 Redis Cluster를 메인 DB로 사용하는 것으로 결정했습니다.
Redis Cluster는 캐시용 아닌가요?
이런 질문을 하시는 분도 계실 것이라고 생각합니다. 실제로 Redis Cluster는 메모리 기반 DB이므로 서버가 재부팅되면 데이터가 유실될 수 있기에 일반적으로 캐시 용도로 많이 사용합니다.
하지만 저희는 아래와 같이 조금 더 깊이 고민하고 살펴본 결과 데이터 저장용으로 사용해도 무방하다는 결론을 내렸습니다.
- 먼저 Redis Cluster는 복제를 통해 장애 극복 기능(fail over)과 고가용성(high availability, HA)을 지원합니다. Primary - Replica 구조로 작동하다가 Primary 장애 시 장애 극복 기능이 작동해 자동으로 Replica가 Primary로 승격되기 때문에 Primary와 Replica에 동시에 문제가 발생하지만 않는다면 큰 문제가 없습니다. 개인적으로 Redis Cluster를 10여 년 전부터 사용해 오면서 장애 극복 기능이 작동한 상황을 겪은 적은 손에 꼽을 정도이고, 완전 장애로 서비스가 장시간 중단되거나 데이터가 유실된 적은 없습니다. 장애 극복 기능 작동 시 서비스 중단 시간은 10~30초 정도이고, 이때 누락된 쓰기 요청이 있다면 별도로 로그 등을 이용해 복구할 수 있습니다.
- 최악의 경우에 데이터가 유실되더라도 복구할 수 있도록 설계할 수 있습니다. 서비스에 따라서 외부에서 데이터를 다시 주입해 유실된 데이터를 채울 수 있으며, 평상 시에 따로 백업용 DB에 이중 쓰기를 해서 이를 활용할 수도 있습니다.
- DB 장애 발생 시 서킷 브레이커를 통해 관련 모듈을 폐쇄하고 사용자에게 대체 데이터를 제공할 수 있습니다. 이렇게 조치하면 장애 레벨이 전면 장애가 아닌 일부 기능 장애 수준이 되므로 서비스에 미치는 영향이 줄어듭니다.
Redis Cluster의 효용(월등한 QPS와 응답 성능)이 메모리 기반 DB라는 위험을 초과한다면, Redis Cluster를 데이터 저장용으로 사용하는 것이 유리합니다. 이때 위험은 위 1번 요소를 감안한 발생 확률이어야 하고, 2번과 3번 방식을 통해서 위험의 정도를 낮출 수 있다는 점을 고려해 평가해야 하는데요. 저희는 평가 결과 실시간 임베딩용 DB로써 Redis Cluster를 채택 시 효용이 위험보다 월등히 크다고 판단했습니다.
Redis Cluster를 캐시로 사용하면 되지 않나요?
아래 두 가지 안을 한 번에 여러 건을 조회하고 모든 응답이 완료됐을 때 데이터를 사용해야 하는 경우라면 체감할 수 있는 차이가 매우 큽니다.
- A: Redis Cluster를 캐시로 사용하고 다른 DB를 메인으로 사용
- B: Redis Cluster를 메인으로 사용하고 다른 DB에 이중 쓰기해 백업으로 사용
예를 들어 키가 서로 다른 100개의 임베딩을 요청하고 응답을 받았을 때 A와 B, 각 상황에 대한 키별 응답 시간이 아래와 같다고 가정하겠습니다.
A: (10, 14, 15, 120, 15, 112, ... ,10)
B: (10, 12, 15, 12, 11, 15, ... , 20)
(단위: ms)
이때 최종 응답 시간이 A는 MAX(A) = 120ms, B는 MAX(B) = 20ms입니다. 여기서 A에만 있는 100ms을 넘는 응답 시간은 캐시 히트가 안 돼 다른 DB에서 응답한 시간이 포함된 시간입니다. 즉 A와 같이 캐시 히트율이 100%가 아닌 상황에서 다른 DB의 응답 속도가 늦다면, B와 비교해 응답 시간 측면에서 매우 불리해집니다. 따라서 빠른 응답 속도가 중요했던 이번 프로젝트에는 A처럼 Redis Cluster를 캐시로 사용하는 방법은 적합하지 않았습니다.
Redis Cluster를 어떻게 사용할 것인가?
선정된 DB인 Redis Cluster를 시스템 요구사항에 맞게 사용하기 위한 과정을 알아보겠습니다.
데이터 모델링
데이터 모델링(이하 모델링)은 Redis Cluster에 실제로 어떤 형태로 데이터를 저장할 것인지 결정하는 단계입니다.
모델링할 때는 아래 관점에서 충분히 고민한 후 결정해야 합니다.
- Redis Cluster는 키를 해싱해 데이터를 분산 적재합니다. 따라서 키가 고르게 분산돼야 높은 성능을 얻을 수 있습니다. 키가 분산되지 않아 특정 슬롯으로 요청이 몰리는 현상을 핫스팟(hot spot)이라고 하며, 핫스팟이 발생할 경우 성능이 대폭 하락하고 샤딩의 의미가 퇴색됩니다.
- 'Big Key Issue'(참고)가 발생하지 않도록 해야 합니다. Big Key Issue가 발생하면 성능 저하는 물론 서비스 장애까지 발생할 수 있습니다.
- 서비스의 데이터 접근 패턴을 고려해 최적화해야 합니다.
- Redis에서 지원되는 데이터 타입을 고려해 설계해야 합니다.
저희는 위와 같은 사항을 고려해 모델링했고, Redis hashes를 데이터 타입으로 채택했습니다.
Redis hashes를 선택한 이유
Redis hashes를 선택한 이유는 다음과 같은 이점이 있기 때문입니다.
- AI에서 사용하는 임베딩에는 여러 종류가 있고, 한 개의 키로 동시에 여러 종류의 임베딩을 조회하는 경우가 많습니다. 이때 Redis hashes를 사용하면 키 아래 서브 키를 둘 수 있기 때문에 여러 종류의 임베딩을 한 번에 읽을 수 있습니다.
- Redis hashes의 단점으로 지적되는 서브 키별로 TTL(time to live)을 설정할 수 없다는 문제는 서비스 요건상 크게 문제 되지 않았습니다.
- Redis에서 지원하는 hmget 명령어를 사용하면 필요한 서브 키의 전체 혹은 부분 집합으로 데이터를 읽을 수 있습니다. 이런 특징은 데이터 접근 패턴 관점에서 큰 이점입니다.
mget vs hmget
Redis hashes를 사용하지 않고 임베딩 유형별로 키를 더 분산해서 적재한 다음 mget 명령어로 한 번에 읽는 방법도 가능했는데요. 임베딩 유형이 추가될 수록 키가 비례해서 증가한다는 단점이 있었고, Redis hashes를 사용하는 것이 키를 기준으로 데이터를 한 번에 관리하기 편하다는 점을 고려해 데이터 타입으로 Redis hashes를 선택하고 명령어로 hmget을 선택했습니다.
네트워크 트래픽과 데이터 크기 줄이기
네트워크가 문제가 된다고요?
서버 개발을 하면서 네트워크 대역폭을 신경 쓸 일이 흔히 발생하지는 않습니다. 여기서 네트워크란 호출 서버와 응답 서버 구간, 로드 밸런서와 서버 구간, DB와 서버 구간, 서버 내 개별 파드의 네트워크 등 모든 것을 의미합니다.
네트워크가 문제 될 정도라면 사용자의 요청이 수십만 TPS 이상이고 서버와 DB 구간의 QPS는 수십만에서 100만 단위가 되는 게 일반적입니다. 하지만 이번에는 훨씬 적은 TPS에서도 네트워크 트래픽이 염려됐습니다. 이유는 매우 큰 데이터를 대량 조회해야 했기 때문입니다.
이를 체험해 보려면 성능 좋은 DB군과 서버군을 준비하고 서버에서 제한 조건 없이 대량의 데이터를 읽도록 한 후 네트워크와 서버의 반응을 관찰해 보면 됩니다. 감시 설정이 잘 돼 있는 인프라 환경이라면 수많은 경고 메시지와 연락을 받게 되실 겁니다. 그만큼 중대한 문제를 야기할 수 있습니다(설마 운영 환경에서 시도하지는 않으시리라 믿습니다).