저희 와치덕스는 설치가 필요없는 트래픽 분석 서비스를 제공합니다. 캠프 구성원(서비스 제공자)들에게 제공되는 저희의 네임서버는 서비스 이용자 의해 시작된 DNS Query에 응답합니다. 네임서버는 도메인이 유효하다면(우리에게 등록된 것이라면) 우리의 프록시 서버 IP를 응답하는 간단한 역할을 수행하고 있습니다. 이때 프록시서버가 요청과 응답을 서비스 제공자와 이용자 사이에서 포워딩하며 트래픽을 영속화하는 것이죠.
계획된 트래픽, 응답 소요 시간, 응답 성공률에 추가로 어떤 어떤 지표를 보여줄지 고민하였습니다. 단순한 트래픽은 서비스의 실제 사용 빈도와 차이가 있으니, 하루에 실제로 얼마나 많은 사용자가 찾아오는지 DAU(Daily Active Users)를 확인하면 좋겠다 생각했습니다. 이를 위해 DNS Response의 TTL을 하루 단위로 조절하면 실제 DAU는 아니더라도 재미로 확인할 수 있는 한가지 지표가 될 것입니다.
아래 두가지 예시에서 볼 수 있듯, DNS Response 패킷은 TTL(Time to Live) 필드를 가집니다. 이는 OSI-7-Layer의 TCP/IP 계층에서 패킷이 실제로 얼마나 네트워크에서 라우터를 통과할 수 있는지 나타내는 TTL과는 다릅니다. DNS의 캐싱 시스템에서 해당 레코드가 얼만큼의 유효기간을 가지는지를 의미합니다.
출처: https://www.firewall.cx/networking/network-protocols/dns-protocol/protocols-dns-response.html
dig naver.com 실행 후 WireShark에서 확인되는 패킷
그런데 위 예시를 자세히 살펴보면 왼쪽은 TTL이 이틀인데 반해, 오른쪽은 TTL이 3분 정도밖에 되지 않습니다. 캐싱이 길수록 DNS Query 빈도가 줄어드니 효율은 좋겠지만, 최근에는 동적 IP를 사용하는 경우가 많기 때문에 이러한 결과가 나온 것입니다. 이때, 시간과 자원이 제한된 환경인 저희 부스트캠프에서 동적 IP가 사용될 경우는 사실상 없다 판단해 계획을 실행에 옮겼습니다.
네임 서버 동작에 수정의 거의 없습니다. dgram과 dns-packet 라이브러리를 사용한 기존 구현에 ttl을 86400(606024)초로 수정하면 끝입니다.
다만, 실제 DAU 값을 어떻게 영속화할지는 아직 정해지지 않았습니다.
저희 왓치덕스는 빠른 입력/메트릭 계산 성능을 위해 ClickHouse를 선택했습니다. Clickhouse 톺아보기
create table default.http_log
(
method String,
path String,
host String,
status_code UInt16,
elapsed_time UInt32,
timestamp DateTime,
response_date Date materialized toDate(timestamp),
is_error UInt8 materialized if(status_code >= 500, 1, 0)
)
engine = MergeTree PARTITION BY toYYYYMM(timestamp)
ORDER BY (timestamp, status_code)
SETTINGS index_granularity = 8192;
이전에 로그 정보를 저장하던 데이터베이스 DDL은 위와 같습니다. 테이블 엔진으로 MergeTree를 사용하며 pk별로 데이터를 정렬하여 저장하며, 삽입 작업의 경우 table parts를 생성하고 백그라운드에서 merge하는 방식으로 동작해 대규모 데이터 삽입 등에 높은 성능을 보입니다.
위 테이블은 각 로그를 저장하는 것이지, 날짜별로 프로젝트마다 하나의 행이 생성되고 갱신되는 DAU를 저장하기엔 적합하지 않습니다. 프로젝트 정보를 저장하던 MySQL 역시 잦은 입력에 오버헤드가 클 거라 예상하였습니다. 결국 ClickHouse DB에 새로운 DAU테이블을 생성하기로 결정했습니다.
그렇다면 DAU 테이블은 어떻게 정의하고 매 DNS 쿼리마다 갱신해야 할까요? 가장 먼저 떠오르는 쉬운 방법은 ALTER … UPDATE
문을 사용하는 것이겠죠. 하지만 ClickHouse의 UPDATE는 결코 좋은 선택지가 아닙니다.
ClickHouse의 UPDATE는 MergeTree가 그러하듯 백그라운드에서 비동기적으로 동작하며 table part의 새로운 변형된 버전을 생성하는 이른바, mutation 방식으로 동작합니다. 그때그때 많은 데이터를 새로 작성하고 대체해야 하며, 이때 원자성이 보장되지 않기에 이러한 변경 중에 발생한 읽기(SELECT문)는 아직 변경되지 않은 부분과 이미 변경된 부분을 함께 확인합니다.
자체적으로 데이터 정합성을 보장할지라도, 저장 및 처리를 위해 정말 많은 최적화 작업이 필요한 무거운 쿼리라는 것을 알 수 있습니다. 새로운 사용자로부터 DNS Query가 들어올 때마다 UPDATE를 하면 서버 자원이 많이 소모되겠죠.. 그렇다면 해결책은 무엇일까요?
저희의 경우에는 해답이 제법 명료했습니다. 바로 SummingMergeTree 엔진을 사용하는 것입니다. SummingMergeTree는 MergeTree를 상속받으면서도, 동일한 정렬키를 가진 행을 합친다는 차이점이 있습니다. 이를 사용하면 저희는 새로운 사용자가 들어올 때마다 access 값이 1인 행을 하나씩 INSERT만 해주면, 주기적으로 같은 도메인명과 날짜를 가진 행들이 병합되어 DAU 값을 얻을 수 있게 되는 것이죠.