Skip to content

SNS 새소식 발행하기(Spring scheduler + Cassandra) -1

2012/01/17

SNS 에서는 대부분 새소식(페이스북의 담벼락같은) 기능이 필요합니다.

누군가의 상태가 변했거나(생일, 프로필 업데이트 등등),

누군가와 인터렉션이 발생했거나(내 사진에 댓글이 달렸거나, 메시지를 받았거나 등등),

누군가와의 관계가 변했거나(친구 요청이 왔거나, 친구를 수락해줬거나 등등)

위에 처럼 새로운 소식에 대해 이용자에게 바로바로 알려줌으로써 서비스에 중요한 기능중에 하나가 됩니다.

하지만 위와 같은 기능은 친구수가 많아지고 알려줘야할 새소식의 항목이 많아질 수록 시스템에 엄청난 부하가 생기게 됩니다. 왜냐 하면 M명의 친구를 가진 N명의 사용자가 1개의 새소식을 만든다고 하더라고 N X M 개의 DB insert와 노티 메시지를 발송해야 하기때문입니다.

그리고 더욱 우려되는 케이스는 전체 가입자에 대한 공지사항(앱의 업데이트, 새로운 이벤트 등)에 대해 새소식으로 보여줄려는 케이스 입니다.

결국 새소식에 관한 데이터를 저장하기 위한 데이터베이스는 Cassandra를 사용하기로 했는데 이유는 우선 유연한 확장성과 쓰기 및 읽기 속도가 빠르기 때문이었으며, 정렬이 시간순 정렬만 필요하고 join 등이 필요 없기 때문에 RDBMS보다 적합하다고 생각했습니다.

Column Family는 4개로 설계했습니다.

1. 관리자에 의해 생성되는 전체 공지사항을 저장하는 Column Family

Notice : {
    group_type : {     // group_type : 공지사항을 읽을 그룹(전체 또는 일부 특정 그룹)을 정의합니다
        TimeUUID : { // 1차 key 를 TimeUUID로 해서 시간순 정렬이 되도록 하고 작성된 시간을 알수 있게 합니다.
            “msg_type” : “msg”   // msg_type : msg 타입(공지사항) 인지를 구분하고, msg : 공지 내용을 저장.
            src : “source”          //source : url 이나 공지글 ID 저장
            …
        }
        TimeUUID : {
            “type” : “msg”
            sid : “source”
            …
        }
        …
    }
    …
}

2. 개인별 Feed 글 저장하는 Column Family

Feed : {
    userno : {     // 글을 소유하는 userno
        TimeUUID : {
            “msg_type” : “userno” // msg_type : msg 타입(생일, 프로필업데이트,댓글 등등), 새소식을 발생시킨 userno
            sid : “source”
            …
        }
        TimeUUID : {
            “msg_type” : “userno” // msg_type : msg 타입(생일, 프로필업데이트,댓글 등등), 새소식을 발생시킨 userno
            sid : “source”
            …
        }
    }
    …
}

3. 개인별 Feed에 대한 버전 정보를 저장하는 Column Family

User : {
    userno : {    
        notice_ver : TimeUUID // 사용자가 읽은 마지막 공지사항의 TimeUUID을 저장합니다.
        feed_ver : TimeUUID  // 사용자가 읽은 자신의 마지막 feed의 TimeUUID을 저장합니다. 
    }
    …
}

4. 개인별 읽어야할 Feed 개수 정보를 저장하는 Column Family

Counter : {
    userno : {
        cnt : “count” // CounterColumnType 으로 정의하여 무결성을 쉽게 보장하도록 합니다.
    }
}

Schema 참고 :
create column family Counter
    with comparator = UTF8Type
    and default_validation_class = CounterColumnType
    and column_metadata =
    [{
      column_name : 'cnt',
      validation_class : CounterColumnType,
      replicate_on_write : true
     }];
 

그리고 간단하게 대략적인 시퀀스다이어그램을 그려보면 
 

 1. 공지사항(Notice)
이미지를 클릭하시면 원본크기로 보실수 있습니다.
위에 처럼 관리자에 의해 공지사항을 입력 요청이 들어오면 전체사용자의 Feed CF에 직접 넣는 것이 아니라 우선 Notice CF에 한번만 insert 합니다. 그리고 전체 가입자를 select 해와서 노티를 발송합니다.

2. 새소식(Feed)
이미지를 클릭하시면 원본크기로 보실수 있습니다.
 
Feed가 발생하면 to에 해당하는 사람들을 select 해서 그 사람들의 Feed CF에 해당 내용을 저장을 합니다.
그리고 Counter CF에 카운트를 1씩 올려줍니다.

3. Feed 조회 요청
이미지를 클릭하시면 원본크기로 보실수 있습니다.
사용자는 노티를 받았을때 바로 들어올수도 있고 나중에 한참 지난뒤에 들어올수도 있습니다. 언제든 조회 요청이 들어오면 우선 사용자의 User CF에서 사용자의 notice 버전을 조회한 후에 Notice CF에서 그 버전 이후에 생성된 글을 조회 합니다. 그리고 새로운 Notice가 존재한다면 사용자의 Feed CF에 저장하고 feed 버전 이후에 데이터를 Feed CF에서 다시 조회하여 Notice + Feed 정보를 리턴해줍니다. 또한, User CF를 업데이트 해주어서 이미 Notice CF에서 Feed CF로 복사한 데이터는 다음 조회시 부터는 복사가 안되게끔 해 줍니다.

즉, 중요한 요점은 공지사항같은 경우 입력 당시 모든 사용자의 Feed CF에 저장하지 않고 조회 당시 비교해서 입력하므로써 부하를 분산시키는 것 입니다. 또한, Feed CF에 Notice CF의 데이터를 복사하는 이유는 Feed CF에 저장되면서 정렬이 끝나기 때문에 조회시에는 다른 작업없이 바로 리턴이 가능합니다. 만약 복사하지 않는 다면 매 조회시 마다 Feed CF와 Notice CF의 데이터를 각각 조회해서 정렬해서 리턴해주는 작업을 해주어야 합니다.

하지만, 아직 큰 문제가 있는데 전체 사용자에게 노티를 보내거나 어떤 사용자가 Feed 를 생성했을때 to에 해당하는 대상이 많은 경우 여전히 응답시간이 늦고 부하가 많게 됩니다.

이를 해결하기 위해 생각했던 방법은 비동기식 처리이며, 여러 방법중 Spring scheduler를 이용했습니다.

자세한 내용은 다음 글에….^^
 

 

 

 

No comments yet

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중

%d 블로거가 이것을 좋아합니다: