안녕하세요. GameUniv프로젝트의 백엔드 개발을 맡은 민경찬입니다.
GameUniv는 대학생을 대상으로 테트리스와 2048 게임의 점수로 경쟁하는 웹사이트입니다.
웹 게임이라는 특성상, 게임의 점수를 API를 무단으로 호출하여 조작할 가능성이 존재합니다. 이러한 문제를 해결하기 위해, GameUniv에서는 게임 점수 모니터링 시스템을 추가적으로 개발하기로 했습니다.
해당 글에서는 Elasticsearch에 대한 Index Mapping 정보와 Logstash의 grok 설정 등의 내용은 제외하였습니다.
가장 먼저 생각한 방법은 이전 점수를 일시적으로 저장하고 다음 기록 갱신에서 점수의 차이를 계산하여 해당 사용자가 무단으로 API를 호출하여 사용하는 사용자인지 확인하는 방법이었습니다.
이를 구현하기 위하여 매 순간 점수가 올라갈 때마다 점수의 변동을 기록하는 API를 만들었습니다.
최종적으로 점수를 갱신하는 순간 이전 기록들과의 시간 차이와 점수 변동 폭을 확인하는 로직을 추가하였습니다. 로직에서 시스템에 설정한 이전 기록 수치와 시간의 차이가 벌어졌을 경우 해당 사용자를 악성 사용자로 인식하였습니다.
해당 방식에는 큰 문제점이 있습니다. 이전 기록과의 시간 차이까지 계산하게 된다면 자칫 억울하게 정지되는 사용자가 발생할 수 있습니다.
다음의 사용 사례를 예시로 들어볼 수 있습니다.
해당 사용 사례에서 40점을 기록했을 당시 26점을 기록했다는 기록이 특정 시간 차이를 넘어가버려 시스템에서 악성 사용자로 판별해버린다는 문제점이 발생합니다.
이를 해결하고자 의심 사용자 테이블을 만들어 별도로 의심 사용자를 관리하였고 운영자를 거쳐 해당 사용자를 정지할 수 있도록 하였습니다.
기록 간 시간 차이를 계산해야하는 이유: 게임 특성상 빠른 속도로 점수가 변동될 수 있습니다. 예를 들어, 0.1초 내로 점수가 변동되는 경우가 매우 흔하게 발생합니다. Gameuniv에서는 이를 효율적으로 관리해야할 필요가 있었습니다.
초기에 직전 기록을 시간 차이 없이 계산하기 위해 RDB에 모든 기록을 저장하였습니다. 그러나 동시 플레이어 수가 늘어날수록 INSERT 부하가 너무 심하게 발생하였고 이를 해결하기 위하여 메모리 관리 방식을 도입하였습니다. 그러나 메모리에서 데이터를 관리하려면 기록 데이터의 만료 시간을 적절히 고려해야만 하였고 결론적으로 이전 기록과의 시간 차이를 고려해야만 하는 결과가 발생하였습니다.
그렇다면 의심 사용자로 분류된 사용자를 어떻게 판단해야할까요? GameUniv에서는 이를 모니터링 시스템을 두어 해결하였습니다.
의심 사용자의 점수 상승폭을 시각화하여 볼 수 있는 시스템을 개발하여 운영자가 의심 사용자의 점수 변동을 직접 확인하고 정지할 수 있도록 구현하였습니다.
-> 모니터링 시스템을 개발하기 위해서 ELK 스택을 사용하였습니다.
모니터링 시스템을 개발할 때 데이터 집계를 위한 Elasticsearch와 데이터를 시각화 할 수 있는 Kibana는 필수라고 판단했습니다. 그러나 데이터를 가공하는 파이프라인으로써 Logstash의 필요성에 대해서는 고민이 필요했습니다.
서버에서 직접 Elasticsearch에 데이터를 넣는 방식은 간단하게 구현할 수 있습니다. 중간 단계가 없기 때문이죠. 그러나 이 방식에는 몇 가지 단점을 예상해볼 수 있습니다.
그렇다면 Logstash라는 파이프라인을 두는 방식은 어떨까요? 해당 방식에서는 API 서버에서 단순히 Logstash로 특정 포맷을 가진 문자열만을 던져주면 됩니다. 이 방식은 결론적으로 초기 설정에 대한 복잡성을 증가시킨다는 단점이 있습니다. 그러나 저희 GameUniv에서는 다음의 장점에 초점을 맞추었습니다.
결론적으로 2번 방식인 Logstash를 거쳐 데이터를 Elasticsearch에 넣는 방식이 가장 좋은 방식이라고 생각했습니다.
(Logstash에 대한 초기 설정 복잡성 증가는 적절한 문서화를 통해 해결하였습니다.)
API 서버에서는 아래의 간단한 코드를 통해 로그 문자열을 Logstash로 전달하기만 하면 됩니다.
const user = verifyToken(req.cookies.token).data;
const fullUrl = req.originalUrl;
const userAgent = req.get('User-Agent');
const requestTime = new Date().getTime() - new Date(req.date).getTime();
const logString = `${req.ip} ${user?.email || '-'} ${req.date} ${fullUrl} ${
req.method
} "${userAgent}" ${requestTime} ${res.statusCode} ${req.score || ''} ${
process.env.LOGSTASH_SECRET
}`;
try {
await axios.post(`http://${process.env.LOGSTASH_IP}:${process.env.LOGSTASH_PORT}`, {
message: logString,
});
} catch (err) {
// Exception handling
}
그럼 미리 설정해놓은 Logstash에서 문자열을 확인하여 읽어 Elastcisearch에 넣을 수 있습니다.
이로써 모니터링 시스템과 API 서버의 독립적인 시스템을 개발할 수 있었습니다.
이렇게하여 의심 사용자를 필터링하여 해당 시간에 사용자의 점수 상승 추이를 확인할 수 있는 모니터링 시스템을 구현하였습니다.
읽어주셔서 감사합니다.