728x90
본 문서는 Discord Migrates Trillions of Messages from Cassandra to ScyllaDB 아티클을 정리한 글입니다.

디스코드에서 트래픽과 메세지가 증가하면서 기존 메세지 스토리지인 Cassandra에서 ScyllaDB로 마이그레이션한 내용을 정리합니다.

 

Discord 시작: MongoDB

MongoDB → Cassandra: What we were doing

 

2015년 개발 당시 Discord는 MongoDB 를 사용

  • a single MongoDB replica set

2015년 11월, 사용이 증가함에 따라 저장된 메세지가 1억 건에 달했고 이 때 성능 문제가 발생

  • 데이터/인덱스가 메모리(RAM) 크기를 넘어섬
  • latency가 예측 불가능해짐

메세지의 수는 지속적으로 증가

  • 2016년 7월: 일 4천만 개
  • 2016년 12월: 일 1억 개

 

MongoDB 를 사용했을 때 발생했던 문제점과 새로운 DB 선택 요구사항

MongoDB → Cassandra: Choosing the Right Database

 

MongoDB 를 사용했을 때 발생했던 문제점

매우 랜덤하게 읽기 작업이 발생

  • 읽기/쓰기 비율이 5:5

음성 채팅이 많은 서버(디스코드 내 organization/workspace): 연간 1000개 메세지 발생

  • 소수 메세지 조회 → 디스크에 많은 랜덤 검색 발생 → 캐시 효율 감소

비공개 채팅이 많은 서버(디스코드 내 organization/workspace): 연간 10~100만개의 메세지 발생

  • 요청은 최근 메세지만 조회, 회원 수는 100명 미만 → 요청 비율이 낮고, 캐시 적중률(히트)이 낮음

대규모 공개 Discord 서버(디스코드 내 organization/workspace): 연간 수백만 개의 메세지 발생

  • 최근 한 시간 내에 발생하는 메세지(데이터)를 조회하는 경우가 많음 → 캐시 히트가 높음

또한 앞으로 랜덤 검색을 유발하는 기능들이 추가될 예정


DB 선택 요구사항

  • 선형 수평 확장: 솔루션 재검토하거나 데이터를 수동으로 재배치(re-shard)하고 싶지 않음
  • 자동 장애 조치: 스스로 복구될 수 있는 시스템
  • 낮은 유지보수 비용: 데이터가 증가하면 노드만 추가
  • 검증된 기술: 새 기술을 좋아하지만 너무 새롭지 않은 기술
  • 예측 가능한 성능: p95의 응답 시간이 80ms 이하, Redis 또는 Memcached에 메세지를 캐시하고 싶지 않음
  • blob 저장소 아닌 저장소: 초당 수천 개의 메세지가 작성되기 때문에 blob 직렬화/역직렬화는 비효율적임
  • 오픈 소스: 타사에 의존하고 싶지 않음

 

Cassandra 선택

Cassandra → ScyllaDB: Our Cassandra Troubles


2017년: 12개 노드로 시작, 수 십억 개의 메세지를 저장

 

적용 후 좋았으나 GC가 10초동안 발생하는 문제가 발생하기도 함

  • 툼스톤(데이터 삭제 작업)때문에 발생한 문제
    • 툼스톤 기간: 10일에서 2일로 축소
    • 빈 버킷을 조회하지 않도록 함

 

2022년: 177개 노드, 메세지는 수 조개에 도달

 

많은 동시 읽기 → 핫 파티션 → 성능 문제 발생: 대기 시간 예측 불가

  • 카산드라: 읽기 비용 > 쓰기 비용
  • 읽기는 Memtable(메모리)에 데이터가 없으면 SSTable(파일)을 조회

유지보수 비용 증가

  • SSTable 압축에 따른 성능 문제 → gossip dance 운영 작업
  • gossip dance: 클러스터 내 노드 중 한 대를 가져와서 트래픽을 받지 않고, 파일을 압축하고, 다시 클러스터에 돌려보내는 작업을 반복

 

아키텍처 변경

Cassandra → ScyllaDB: Changing Our Architecture


Cassandra → ScyllaDB

  • GC로 인해 발생하는 지연 시간 문제 감소

데이터 서비스 API(레이어) 추가

  • 동일 데이터에 대한 여러 요청을 한 번에 DB로 보냄 → DB로 갈 쿼리 수를 줄여서 DB 부하를 감소
  • 일관성 해시(consistence hashing) 사용으로 동일 데이터에 대한 요청은 동일 데이터 서비스로 보냄

 

결과

2022년 5월 기준

  • Cassandra 177개 노드 → ScyllaDB 72개 노드
    • 노드 별 평균 4TB 디스크 → 9TB 디스크
  • 메세지 히스토리 읽기 p99: 40-125ms → 15ms
  • 메시지 작성 p99: 5-70ms → 5ms
  • 온콜 대응이 줄어듦

 

 

Reference

728x90
728x90

상황

  • MySQL에서 IN 절 안에 조회하고 싶은 컬럼의 리스트를 넣은 후 조건으로 설정한 리스트의 순서대로 반환받고 싶을 때
  • ex.
조회해야 하는 Member들의 id와 반환되어야 할 순서로 정렬된 리스트를 입력받는다.
Member를 조회하고, 입력받은 순서에 맞게 정렬해서 Member List를 리턴한다.

 

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    // Example 1
    public List<Member> getMembers1(List<Long> memberIds) {

        List<Member> members = memberRepository.findAllById(memberIds); // ①

        List<Member> sortedMembers = new ArrayList<>(); 
        for (Long memeberId: memberIds) {
            Optional<Member> optionalMember = members.stream()  // ②
                    .filter(m -> m.getId().equals(id))
                    .findAny();

            optionalMember.ifPresent(sortedMembers::add);       // ③
        }

        return sortedMembers;
    }

    // Example 2
    public List<Member> getMembers2(List<Long> memberIds) {
        
        List<Member> members = memberRepository.findAllById(memberIds); // ①

        Map<Long, Member> memberMap = new HashMap<>();
        members.forEach(member -> memberMap.put(member.getId(), user)); // ②

        List<Member> sortedMembers = new ArrayList<>();
        for (Long memeberId: memberIds) {
            Member member = memberMap.get(memeberId);                   // ③

            if (member != null)
                sortedMembers.add(member);
        }

        return sortedMembers;
    }
}
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

    ...

    @Override
    List<T> findAllById(Iterable<ID> ids);

    ...
}
  • Example 1
    • ① : id list를 조회
    • ② : 조회한 멤버 리스트에서 알맞는 id의 회원을 조회
    • ③ : 해당 회원을 리스트에 추가
  • Example 2
    • ① : id list를 조회
    • ② : 조회한 멤버 리스트를 Map 형태로 보관
    • ③ : Map에서 아이디를 조회해서 리스트에 추가

해결

  • 지금의 나는 코드를 작성할 때 Example1이나 Example2를 생각했을 거고, Example2를 활용해서 값을 반환했을 것 같다.
  • 하지만 이 방법은 매우 비효율적이다. DB에서 값을 조회하고, 그다음 다시 반복문을 실행시켜야 한다는 점과 굳이 불필요한 Map 객체를 생성해서 처리해야 한다.
  • 따라서 이 부분을 애초에 쿼리를 실행할 때 정렬 순서를 조건으로 설정해서 값을 반환받을 수 있다면 불필요한 반복문 실행이나 객체 생성이 필요 없을 수 있다.

기존 쿼리

SELECT * FROM member WHERE id IN (2, 1, 3);
  • 조회할 아이디 값과 정렬된 리스트가 2,1,3 일 때

해결

SELECT * FROM member WHERE id IN (2, 1, 3) ORDER BY FIELD(id, 2, 1, 3);
  • ORDER BY FEILD를 설정한다.
    • ORDER BY FELID('컬럼명', '정렬 순서 1', '정렬 순서 2', '정렬 순서 3', ...)

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query(value = "SELECT * FROM member WHERE id IN (?1) ORDER BY FIELD(id, ?1);", nativeQuery = true)
    List<Member> findMembersByIdOrderByFeild(String ids); // 리스트를 문자열로 변환
}

 

728x90

+ Recent posts