Voyager : 검색 엔진 기반 실시간 이슈 감지 시스템

서론

한국에 있는 Riot 기술팀은 리그 오브 레전드 한국 커뮤니티와 플레이어들에게 훌륭한 서비스를 제공하는 것이 최우선 사명으로, 이슈가 발생하면 최대한 신속하게 내용을 파악하고 대응하여야 합니다. 하지만 수많은 플레이어들이 한국 서버에서 게임을 즐기고 있고, 정기적인 패치를 통하여 게임이 지속적으로 변화하고 있기 때문에 만만치 않은 일입니다. 높은 서비스 품질을 유지하기 위하여 기술팀은 빠르게 심각성, 시급성, 빈도 등을 고려하여 각 이슈를 평가하고, 이에 맞추어 우선 순위를 정하여야만 합니다.

다행히 저희에겐 이 과제를 해결하는 데 도움이 되는 정보가 있습니다. 게시판과 1:1 문의를 통해 방대한 양의 귀중한 정보가 플레이어들로부터 직접 전달되고 있습니다. 문제를 겪는 플레이어들이 실시간으로 보내주는 글들은 이슈를 감지하고 해결하는 데 큰 도움이 될 수 있지만, 그러려면 이 방대한 양의 데이터를 효율적으로, 빠르게 다룰 수 있어야 합니다. 그럼 실시간으로 이렇게 엄청난 양의 데이터를 모니터링하고 분석하려면 어떻게 해야 할까요?

이제부터 저희가 Voyager라고 부르는 검색 엔진 기반 실시간 이슈 감지 시스템을 소개하겠습니다. 저희가 어떻게 이 시스템을 개발하고 개선하였는지를 설명한 뒤, 검색 엔진이 한국어를 처리하는 과정에서 발생한 문제점도 살펴볼 것입니다. 실시간 문자 인덱싱 및 처리에 관심이 있거나, 유사한 문제를 해결하려는 분들에게 유용하고 재미있는 정보가 될 것이라 기대합니다.

 

라이브 서비스 실시간 모니터링

우선 Voyager를 어떻게 사용하는지 보여드리는 것으로 시작하겠습니다.

Voyager

상단에 있는 그래프는 1:1 문의량 혹은 공식 홈페이지 게시글 수를 나타냅니다. 그래프 모양만 보면 한 눈에 플레이어들이 문제를 겪고 있음을 알 수 있습니다. 좌측 하단에는 문의글이나 게시글을 실시간으로 인덱싱하여 자주 사용된 단어를 보여주는 막대그래프가 위치합니다. 여기에 나타나는 단어는 라이브 이슈에 대응하는 데 큰 도움이 됩니다. 일례로 이 스크린샷이 찍힌 당시 로그인 큐가 길어지는 이슈가 발생하였고, “서버", “게임", “접속" 등이 나타남에 따라 기술팀이 게임 중 접속이 끊기는 상황을 상정하고 바로 대응할 수 있었습니다. 실제로 예상한 부분에 문제가 있었기 때문에 장애 시간을 단축하였습니다. 오른쪽에는 1:1 문의 분류에 따른 문의량을 나타내는 막대그래프가 있습니다. 어느 종류 문의가 많은지를 보면 더 정확한 예상을 할 수 있습니다. 이와 같이 실시간 텍스트 분석을 통하여 문제를 분석하고 해결책을 찾는 속도가 빨라집니다.

Voyager는 외부 요인으로 인하여 발생한 문제를 파악하는 데에도 활용할 수 있습니다. 일례로, 2014년 9월에 로그인 큐가 급격하게 쌓이면서 동시접속자 수가 급감하는 이슈가 발생하였는데, 서버에 문제가 있다는 징후는 전혀 없었습니다. 사내 인프라에 설치된 모니터링 시스템을 통해서는 아무런 단서도 얻지 못하고 있는 와중에 Voyager를 통하여 특정 회선 사업자명이 쓰이는 빈도가 올라가는 것을 확인하였습니다. 게임 서버가 아니라 인터넷 회선의 문제임이 밝혀진 덕분에 기술팀이 내부 원인을 찾기 위하여 더 많은 시간을 보내지 않고 상황을 적절하게 공지할 수 있었습니다.

 

Elasticsearch를 활용한 실시간 문자열 검색

1:1 문의를 SQL DB에 저장하고, 여기에 쿼리를 하여 위에서 설명한 일을 할 수 있다면 좋겠지만 현실적으로 그렇게 하기는 어렵습니다. 관계형 DB는 검색할 대상이 확실하게 정해져 있는 경우에는 효과적인 반면, 글 안에 있는 문자열을 검색할 때에는 매우 느립니다.

기존에는 1:1 문의 지원팀이 문자열 검색을 하려면 몇 분 이상 소요되기도 하였고, 때로는 이러한 검색이 DB 과부하로 이어져 문의 대응 업무가 지연되기도 하였습니다. 이런 식으로 서비스 품질을 저하시키는 해결책을 활용할 수는 없습니다.

Voyager는 모든 1:1 문의 내 키워드를 저장하고 검색하게 지원해 주는 텍스트 검색 엔진입니다. 모든 검색을 1초 내에 완료하여 해당 키워드를 포함하는 모든 문의를 추출합니다. 앞에서 나온 예처럼, 키워드 빈도, 연관 키워드, 시간당 연관 문의 수 등 유용한 통계도 제공합니다.

Voyager는 아파치 Lucene 프로젝트 기반의 분산형 오픈소스 텍스트 검색 엔진인 Elasticsearch를 사용합니다. Lucene과 Elasticsearch는 여러가지 장단점을 공유하지만, Elasticsearch 만의 장점은 분산형 시스템이라는 것입니다. 그렇기에 클러스터에서 어떤 노드를 제거하더라도 일부 지연이 발생하는 것 이외에는 아무런 문제 없이 서비스를 지속합니다. 또한 노드를 추가하면 전체 성능도 증가하여, 부하가 증가하더라도 쉽게 수용할 수 있습니다.

 

Kibana: 사용자 친화적 인터페이스 연동

Voyager
 

Elasticsearch와 더불어 HTML5 기반 in-browser UI를 제공하는 Kibana도 사용합니다. Kibana를 사용하여 Voyager는 현재 문의 트렌드를 파악할 수 있는 대시보드 UI를 제공합니다. 대시보드 내 정보는 패널 기반으로 표시되며, 패널은 쉽게 추가하고 위치를 조정할 수 있습니다.

위 스크린샷에 나와 있는 ‘KIND’ 패널은 문의를 종류별로 유입량에 따라 정렬하여 막대그래프로 이를 도식화한 정보를 보여줍니다. ‘TICKET TREND’ 패널은 키워드와 연관된 전체 문의량을 선형 그래프로 나타내어 줍니다. 사용자가 원하는 대로 각 패널 위치를 구성할 수 있고, 구성한 내용은 서버에 저장하여 사용자가 언제든 가장 관심있는 정보를 보기 용이하게 하였습니다.

이보다 더 좋은 점은 Kibana가 Elasticsearch 클러스터가 동작하는 것을 실시간으로 반영할 수 있다는 것입니다. 클러스터에서 모든 데이터를 다시 모아 분석할 필요가 없기에, 검색창에 ‘레오나’를 입력하는 순간 각 패널은 그에 맞추어 실시간으로 정보를 갱신합니다. 이로 인하여 저희 기술팀은 라이브 이슈에 대하여 일간 트렌드, 연관 검색어, 분류, 연관된 문의 등 직관적인 정보를 실시간으로 얻을 수 있습니다.

 

한국어 인덱싱 처리

Elasticsearch 같은 검색 엔진에서 한국어와 같은 교착어를 처리하려면 영어에 비하여 복잡한 절차를 거쳐야 합니다. 문장을 모두 키워드 배열로 잘라내어 키워드 단위로 저장한 내용을 쿼리로 검색하도록 지원하기 때문입니다.

어간과 접미사/어미 등 그 뒤에 붙는 문장 요소 사이에 공백이 없는 것이 한국어와 같은 교착어의 특징인데요, 예를 들면 한국어로 “바이는 정글러이다”라고 쓰이는 문장이 영어로는 “Vi is a jungler”가 됩니다. 여기에서 ‘는’은 접미사이고, ‘이다’는 어미인데, 영어 문장에서는 이렇게 명사가 아닌 부분도 ‘is’, ‘a’ 처럼 독립적인 단어로 존재합니다.

한국어를 인덱싱 할 때에는 접미사/어미 등을 제외하여 이들이 검색 결과 노이즈를 유발하는 것을 막아야 하겠지만, 이러한 부분을 어간에서 분리하기는 쉽지 않으며, 모호한 상황도 많습니다. 사전에 없는 말을 파싱하는 경우에는 더욱 어려워지는데요, 수많은 여러분들께서 리그 오브 레전드를 사랑해 주시지만, 인터넷이나 게임 커뮤니티에서 사용하는 단어는 아직 사전에 없는 경우가 많습니다. ‘정글러’가 사전에 올라가는 날이 올까요?

다행히 최근에 오픈소스 진영에서 한국어 처리에 대한 연구가 많이 이루어졌습니다. 저희는 Lucene 한국어 분석기를 활용하여 개발한 한국어 처리 플러그인을 통해 한국어 인덱싱 처리를 할 수 있었습니다. Lucene 한국어 분석기는 Lucene과 연동하기 위하여 만들어졌고, 덕분에 Lucene 기반으로 만들어진 Elasticsearch 에도 쉽게 연동할 수 있었습니다. 이 자리를 빌어 해당 프로젝트에 감사를 표하고 싶습니다.

한편, 이 기능은 추가적인 개선의 여지가 남아있습니다. 예를 들면 “소나”를 리그 오브 레전드 챔피언으로 인식하여 명사 하나로 해석할 수도 있고, “개나 소나~”라고 말할 때 처럼 “소”라는 명사와 “나”라는 접미사로 해석하는 편이 맞기도 하기 때문입니다. 이로 인하여 아직까지는 저희는 소고기라고 말할 때 나오는 “소”에 대해서 검색하고 싶은데 챔피언 “소나”에 관한 정보가 섞여 나오는 것을 감수하며 사용하고 있습니다. 문맥을 파악하는 알고리즘을 더한다면 이 문제를 완화할 수 있을 것이고, 훗날 여유가 된다면 직접 작업하여 보고 싶습니다.

 

인덱스 분할

Voyager에서 다루는 정보량이 많아질수록 검색 속도를 유지하기가 어렵기 때문에 인덱스 분할이 중요합니다. 인덱스는 Elasticsearch 클러스터 전체에 분산된 정보의 집합이기에, 개별 인덱스에 정보가 많아질수록 정보를 기록하고 검색하는 속도는 느려집니다. 시간이 지나면서 쌓이는 정보가 많아지면 자연히 속도가 느려질 수 밖에 없는 문제를 완화하기 위하여, 저희는 월별로 인덱스를 나누었습니다.

인덱스 분할은 곧 최근에 저장한 정보에 우선 순위를 부여함을 의미합니다. 마지막 한 달에 대한 검색은 더 빨라지지만 전체 기간에 대한 검색 속도는 느려집니다.

한편 매번 수작업으로 인덱스를 나누는 것은 굉장히 비효율적이기 때문에 Elasticsearch에서는 인덱스 템플릿 API를 제공합니다. Voyager에 다음과 같이 HTTP PUT request를 보내면 인덱스 분할을 자동적으로 진행합니다.

 
curl -XPUT 'http://voyager.riotgames.com/_template/ps_ticket_template' -d '
{
    "template": "ps-ticket-*",
    "settings": {
        "number_of_shards": 5,
        "number_of_replicas": 1,
        "analysis": {
            "analyzer": {
                "default": {
                    "type": "standard"
                },
                "ko_analyzer": {
                    "type": "custom",
                    "filter": ["lowercase", "arirang_filter"],
                    "tokenizer": "arirang_tokenizer",
                    "analyzer": "arirang_analyzer"
                }
            }
        }
    },
    "mappings": {
        "ps_ticket": {
            "_all": {"analyzer": "ko_analyzer"},
            "properties": {
                "_id": { "type": "integer" },
                "userID": { "type": "string", "index": "not_analyzed" },
                "kind": { "type": "string", "index": "not_analyzed" },
                "title": { "type": "string", "analyzer": "ko_analyzer" },
                "content": { "type": "string", "analyzer": "ko_analyzer" },
                "answered": { "type" : "boolean" },
                "deleted": { "type" : "boolean" },
                "regDate": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss Z", "include_in_all": false},
                "editDate": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss Z", "include_in_all": false},
                "answerDate": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss Z", "include_in_all": false},
                "ipAddress": { "type": "string", "index": "not_analyzed" }
            }
        }
    }
}
'

새 인덱스 이름을 ‘ps-ticket-*’으로 한다는 것을 ‘template’ 필드에서 확인할 수 있습니다. 또한 분석기와 tokenizer는 Lucene 한국어 분석기에 맞추어 개발한 플러그인 ‘arirang’으로 지정하였습니다.

‘mappings’에는 정보로 입력되는 각 필드별로 적용하여야 하는 분석기를 기록하였습니다. 모든 정보를 검색에 활용하는 것이 부적절한 경우가 있기 때문입니다. 예를 들어 ‘신지드’에 대하여 검색을 하는 경우, ‘신지드는 쫓지 말랬어’라는 필명을 가진 사용자가 작성한 티켓이 검색 결과에 나타나면 노이즈가 발생하여 검색 결과의 품질이 떨어질 것입니다. 그래서 ID나 IP주소는 데이터의 일부가 아닌 전체를 입력할때만 검색에 나타나도록 ‘index : not_analyzed’로 설정합니다. 한국어로 기록하는 핵심 정보가 있는 제목이나 본문은 제대로 나누어 기록하고 검색하도록 ‘ko_analyzer’로 설정하여 한국어 분석기를 적용합니다.  

 

결론

지금까지 Voyager의 인터페이스, 아키텍처, 유지보수 관련 사항을 살펴보았고, 실제로 한국에서 플레이어들이 겪는 라이브 이슈를 처리하는 데 어떻게 도움이 되었는지 실제 예도 보여드렸습니다.

보신 것처럼 Voyager의 가장 큰 장점은 수많은 한국 플레이어들이 서비스 이슈를 겪을 때 플레이어 모두의 ‘집합적인 목소리’를 직접 이해할 수 있게 해준다는 것입니다. 게임에 문제가 생겼을 때 이 문제를 겪는 플레이어 한 명은 (그게 설령 이 글을 쓰고 있는 저희였다 해도) 무엇이 문제인지 알지 못할 수도 있습니다. 그러나 Voyager를 활용하면 저희가 어떻게 대응해야 할지를 알 수 있습니다. 이미 이를 통해 서비스 이슈를 처리하는 대응 시간과 안정성이 향상되었으며, 소요되는 리소스도 줄일 수 있었습니다.

물론 Voyager에도 개선할 점이 있지만, 이 글이 비슷한 문제를 다루고 있는 분들에게 도움이 되었으면 좋겠습니다. 문제에 대한 해답은 의외로 문제와 가까운 곳에서 발견되는 경우도 있는데, 이 경우엔 저희가 서비스를 제공하는 플레이어들이 그 해답을 제공한다고 할 수 있겠습니다. 서비스 이슈에 가장 큰 영향을 받는 것은 플레이어들이기 때문입니다.

 
Posted by 김현강, 최종윤, 정수혁