Upgrade to Pro — share decks privately, control downloads, hide ads and more …

대량의 광고 데이터 필터 기능 개발기: ES한테 뺨맞고 Citus한테 안긴 썰

kakao
December 08, 2022

대량의 광고 데이터 필터 기능 개발기: ES한테 뺨맞고 Citus한테 안긴 썰

#Citus

신규 키워드 광고 플랫폼을 오픈하고 나서, 대량의 광고데이터에 대해 다양한 필터 조건들을 적용하여 데이터를 보고 싶다는 요구사항이 확인되어 프로젝트를 진행하게 되었습니다.

데이터의 양이 많았기 때문에 단순히 애플리케이션에서 모든 것을 처리할 수 없었고, 필터 기능을 위해 사용할 데이터 저장소부터 필터 관련 로직 설계까지 다시 고민해야 했습니다.

이번 세션을 통해서 필터 기능을 제공하기 위해 Elasticsearch를 먼저 검토했으나 결국에는 citus라는 데이터 저장소를 사용하는 것으로 결정하게 된 과정을 공유하고자 합니다.

발표자 : genos.lee
카카오 crux개발2셀에서 서버 개발을 담당하고 있는 제노스라고 합니다.

kakao

December 08, 2022
Tweet

More Decks by kakao

Other Decks in Programming

Transcript

  1. 대량의 광고데이터 필터 기능 개발기 이동준 Genos.lee 카카오 Copyright 2022.

    Kakao Corp. All rights reserved. Redistribution or public display is not permitted without written permission from Kakao. if(kakao)2022 ES한테 뺨맞고 Citus한테 안긴 썰
  2. 키워드 광고 DSP (Demand Side Platform) 키워드 소재 비즈채널 DSP

    광고주1 광고주2 광고주3 광고주4 하나의 키워드에 대해 여러 명의 광고주들끼리 경쟁
  3. 키워드 광고 DSP 키워드 소재 비즈채널 DSP 광고가 노출될 기회를

    더 얻기 위해 다양한 키워드를 구매 -> 키워드 데이터가 기하급수적으로 늘어남
  4. 키워드 광고 DSP DSP 캠페인, 광고그룹을 통해 수많은 키워드, 소재를

    분류하고 노출 전략, 입찰 전략 등을 설정 키워드 캠페인 비즈채널 광고그룹 1 N 1 N 1 N 소재 1 N
  5. 키워드 광고 DSP MSA로 구성되어 있어서 다양한 서비스들과 정보를 주고

    받음 DSP Backend Account Service Review Service Report Service DSP front
  6. 키워드 광고 DSP 필터 기능 3. 임의의 기간에 집계된 보고서

    데이터를 기준으로 필터링(이상, 이하, 미만, 초과, 같음)
  7. 키워드 광고 DSP 필터 기능을 간단히 정리하면 1. 광고 도메인

    필드값 포함 또는 제외 2. 광고 도메인 필드값 일치 3. 보고서 데이터 범위 질의 4. 각각의 필터들은 AND 조건으로 합쳐져서 필터링되고 정렬과 페이징 제공
  8. 키워드 광고 DSP 필터 기능에 숨은 이슈들 1. 광고 도메인

    필드값 포함 또는 제외 • 키워드 검색 기능 2. 광고 도메인 필드값 일치 • 광고 도메인의 계층 구조를 고려한 상태값 일치 (ex. 운영상태) • 하나의 필드에 대해 선택된 옵션들은 OR 조건으로 합쳐져서 필터링 3. 보고서 데이터 범위 질의 • 최대 기간 2년 4. 각각의 필터들은 AND 조건으로 합쳐져서 필터링되고 정렬과 페이징 제공
  9. ElasticSearch(ES)에서 가능한지 검토가 필요한 기능들 1. 광고 도메인 필드값 포함

    또는 제외 • 키워드 검색 기능 2. 광고 도메인 필드값 일치 • 광고 도메인의 계층 구조를 고려한 상태값 일치 (ex. 운영상태) • 하나의 필드에 대해 선택된 옵션들은 OR 조건으로 합쳐져서 필터링 3. 보고서 데이터 범위 질의 • 최대 기간 2년 4. 각각의 필터들은 AND 조건으로 합쳐져서 필터링되고 정렬과 페이징 제공 -> 광고 도메인과 보고서 데이터를 ES에서 어떻게 사용할 것인가?
  10. 광고 도메인 데이터 + 보고서 데이터 키워드 캠페인 광고그룹 소재

    캠페인 보고서 광고그룹 보고서 소재 보고서 키워드 보고서
  11. ES에서 연관 관계를 사용하기 위한 다양한 방법들 1. Denormalize -

    각각의 보고서 document에 
 광고 도메인 데이터를 중복으로 가짐 
 (단점) 광고 도메인 데이터에 변경이 생겼을 때, 
 update 해야하는 document 수가 너무 많고, indexing도 자주 해야 함 { "date": "2022-09-01", "֢୹ࣻ": 100, "௿ܼࣻ": 2, "஬ಕੋݺ": "ifkakao-campaign", "ੌ৘࢑": 10000, "ON/OFF": "ON" } { "date": "2022-09-02", "֢୹ࣻ": 150, "௿ܼࣻ": 3, "஬ಕੋݺ": "ifkakao-campaign", "ੌ৘࢑": 10000, "ON/OFF": "ON" } { "date": "2022-09-03", "֢୹ࣻ": 130, "௿ܼࣻ": 2, "஬ಕੋݺ": "ifkakao-campaign", "ੌ৘࢑": 10000, "ON/OFF": "ON" }
  12. ES에서 연관 관계를 사용하기 위한 다양한 방법들 2. Nested -

    각각의 광고 도메인 document 안에 
 보고서 데이터를 list로 가지고 
 일 단위, 시간 단위로 변경 (단점) 광고 도메인, 보고서 데이터가 
 변경, 추가될 때마다 전체 document를 
 다시 indexing해야함 { "஬ಕੋݺ": "ifkakao-campaign", "ੌ৘࢑": 10000, "ON/OFF": "ON", "reports": [ { "date": "2022-09-01", "֢୹ࣻ": 100, "௿ܼࣻ": 2 }, { "date": "2022-09-02", "֢୹ࣻ": 150, "௿ܼࣻ": 3 }, { "date": "2022-09-03", "֢୹ࣻ": 130, "௿ܼࣻ": 2 } ] }
  13. ES에서 연관 관계를 사용하기 위한 다양한 방법들 3. Parent join

    - 특정 index 내에 연관 관계를 선언 - 해당 index 내에서 각각의 document는 연관 관계와 무관하게 업데이트 가능 
 -> 각각의 광고 도메인, 보고서 document가 서로 영향없이 변경, 추가 가능 PUT /keyword-ad-index { "mappings": { "properties": { "join_field": { "type": "join", "relations": { "campaign": ["adgroup", "campaign-report"], "adgroup": ["keyword", "creative", "adgroup-report"], "keyword": ["keyword-report"], "creative": ["creative-report"], } } } } } 키워드 캠페인 광고그룹 소재 캠페인 보고서 광고그룹 보고서 소재 보고서 키워드 보고서
  14. ES에서 연관 관계를 사용하기 위한 다양한 방법들 3. Parent join

    - 제약사항: 연관 관계로 묶인 document는 모두 하나의 shard(routing)에 있어야 함 - 단점: 많은 연산과 메모리 사용으로 인해 쿼리 성능이 나오지 않을 수 있음 PUT /keyword-ad-index/_doc/{campaign-report-doc-id}?routing={campaign-doc-id} { "campaign-report-doc-id": 3, "date": "2022-09-01", "֢୹ࣻ": 100, "௿ܼࣻ": 2 "join_field": { "name": "campaign-report", "parent": "1" //campaign-doc-id } }
  15. 광고 도메인 데이터 + 보고서 데이터 키워드 캠페인 광고그룹 캠페인

    1개당 최대 1000개 소재 캠페인 보고서 광고그룹 보고서 소재 보고서 키워드 보고서 광고그룹 1개당 최대 1000개 최대 2년치 광고그룹 1개당 최대 20개 최대 2년치 최대 2년치 최대 2년치 계정 계정당 최대 1000개
  16. 최대 데이터 규모 키워드 캠페인 광고그룹 캠페인 1개당 최대 1000개

    소재 캠페인 보고서 광고그룹 보고서 소재 보고서 키워드 보고서 광고그룹 1개당 최대 1000개 최대 2년치 광고그룹 1개당 최대 20개 최대 2년치 최대 2년치 최대 2년치 계정 1개당 2년치 보고서 데이터 row 수 
 = 2 x 365(일단위 데이터) + 24(당일 시간 단위 데이터) 
 = 754 계정 1개당 2년치 키워드 보고서 데이터 최대 row 수 
 = 754 x 1000 x 1000 x 1000 
 = 754,000,000,000 (~ 7500억) 전체 계정의 2년치 키워드 보고서 데이터 최대 row 수 = ??? 
 => 계정 2개만 되어도 1조 건이 넘어가는 규모 계정 계정당 최대 1000개
  17. 최대 데이터 규모 키워드 캠페인 광고그룹 캠페인 1개당 최대 1000개

    소재 캠페인 보고서 광고그룹 보고서 소재 보고서 키워드 보고서 광고그룹 1개당 최대 1000개 최대 2년치 광고그룹 1개당 최대 20개 최대 2년치 최대 2년치 최대 2년치 계정 1개당 2년치 보고서 데이터 row 수 
 = 2 x 365(일단위 데이터) + 24(당일 시간 단위 데이터) 
 = 754 계정 1개당 2년치 키워드 보고서 데이터 최대 row 수 
 = 754 x 1000 x 1000 x 1000 
 = 754,000,000,000 (~ 7500억) 전체 계정의 2년치 키워드 보고서 데이터 최대 row 수 = ??? 
 => 계정 2개만 되어도 1조 건이 넘어가는 규모 계정 계정당 최대 1000개
  18. 최대 데이터 규모 키워드 캠페인 광고그룹 캠페인 1개당 최대 1000개

    소재 캠페인 보고서 광고그룹 보고서 소재 보고서 키워드 보고서 광고그룹 1개당 최대 1000개 최대 2년치 광고그룹 1개당 최대 20개 최대 2년치 최대 2년치 최대 2년치 계정 1개당 2년치 보고서 데이터 row 수 
 = 2 x 365(일단위 데이터) + 24(당일 시간 단위 데이터) 
 = 754 계정 1개당 2년치 키워드 보고서 데이터 최대 row 수 
 = 754 x 1000 x 1000 x 1000 
 = 754,000,000,000 (~ 7500억) 전체 계정의 2년치 키워드 보고서 데이터 최대 row 수 = ??? 
 => 계정 2개만 되어도 1조 건이 넘어가는 규모 계정 계정당 최대 1000개
  19. ES index data sizing 1. 임의로 계정별 도메인별 index 사전

    정의 campaign-[adAccountId] report - daily-[campaignId]-[date] report - hourly-[campaignId]-[date] 광고계정별 캠페인 index adGroup-[adAccountId] report - daily-[adGroupId]-[date] report - hourly-[adGroupId]-[date] 광고계정별 광고그룹 index keyword-[adAccountId] report - daily-[keywordId]-[date] report - hourly-[keywordId]-[date] 광고계정별 키워드 index creative-[adAccountId] report - daily-[creativeId]-[date] report - hourly-[creativeId]-[date] 광고계정별 소재 index
  20. ES index data sizing 2. Document별 최대 사이즈 확인 -

    Document별로 필터 조건에 해당하는 필드 기준으로 schema를 생성하여 계산
  21. ES index data sizing 2. Document별 최대 사이즈 확인 -

    Document별로 필터 조건에 해당하는 필드 기준으로 schema를 생성하여 계산
  22. ES index data sizing 2. Document별 최대 사이즈 확인 -

    Document별로 필터 조건에 해당하는 필드 기준으로 schema를 생성하여 계산
  23. ES index data sizing 2. Document별 최대 사이즈 확인 -

    Document별로 필터 조건에 해당하는 필드 기준으로 schema를 생성하여 계산
  24. ES index data sizing 2. Document별 최대 사이즈 확인 -

    Document별로 필터 조건에 해당하는 필드 기준으로 schema를 생성하여 계산
  25. ES index data sizing 3. 실제 사용되는 데이터 건수와 스키마

    최대 사이즈를 고려한 결과에 1.5배하여 전체 사이즈 추정 - 가장 큰 계정별 도메인별 index size는 300GB 정도로 추정 - 전체 size는 2.4TB 정도로 추정 - 좀 넉넉하게 여유분을 가져간다고 했을 때 10TB 정도면 충분해 보임(replica 고려하면 20TB) => 마침 ssd 1.9TB 스펙의 장비 8대를 여유 장비로 쓸 수 있게 되어서 바로 테스트 진행
  26. 샤딩 정책도 바꿔보고 임의로 사전 정의했던 index 설계도 바꿔보고 기획

    부서랑 요구사항 협의도 하고 2년 보관주기를 위해 삭제 처리는 어떻게 할지 등 여러가지 고민할 거리들을 검토하고
  27. ES를 사용할 수 없게 된 이유 ES에서는 최대 bucket size(65,536개)만큼만

    데이터 집계가 가능함 키워드별로 임의의 기간에 대해 노출수 집계를 할 경우, 
 하나의 계정이 최대로 가질 수 있는 키워드 수는 10억 개이므로 정확한 키워드별 노출수 집계가 불가 
 (캠페인을 1개 필수 조건으로 선택해도 1000만 개까지는 지원해야 함) -> 노출수 기준으로 키워드 정렬, 페이징 불가 
 -> 실제로 max_buckets를 100,000으로 설정하고 쿼리를 수행했더니 데이터 노드 죽어버림. Ref: https:/ /www.elastic.co/guide/en/elasticsearch/reference/current/search - settings.html#search - settings - max - buckets
  28. 쿼리 몇 개 날려보고 다 될 것 같았는데 그 땐

    왜 이렇게 테스트해 볼 생각을 못했을까요?
  29. Citus - PostgreSQL을 확장하여 Distributed Database 제공 -> PostgreSQL의 SQL

    문법도 그대로 사용 가능 Ref: https:/ /docs.citusdata.com/en/v11.0/get_started/concepts.html
  30. 조회하는 대상 데이터 크기에 따라 달라지는 조회 방식 index cost값에

    따라 DB scan 방식이 달라짐(아래 예시는 default 설정 기준) 대상 데이터 row 수 500만 개 이상 500만 개 미만 DB scan 방식 Full scan Index scan 접근하는 Data Node 전체 DataNode 에서 쿼리 실행 후 Coordinator Node에서 쿼리 결과 집계해서 응답 index에 해당하는 data node에서 쿼리 실행 후 Coordinator Node에서 쿼리 결과 집계해서 응답
  31. 요구사항 재검토 1. 광고 도메인 필드값 포함 또는 제외 •

    키워드 검색 기능 2. 광고 도메인 필드값 일치 • 광고 도메인의 계층 구조를 고려한 상태값 일치 (ex. 운영상태) • 하나의 필드에 대해 선택된 옵션들은 OR 조건으로 합쳐져서 필터링 3. 보고서 데이터 범위 질의 • 최대 기간 2년 4. 각각의 필터들은 AND 조건으로 합쳐져서 필터링되고 정렬과 페이징 제공
  32. 요구사항 재검토 1. 광고 도메인 필드값 포함 또는 제외 •

    키워드 검색 기능 2. 광고 도메인 필드값 일치 • 광고 도메인의 계층 구조를 고려한 상태값 일치 (ex. 운영상태) • 하나의 필드에 대해 선택된 옵션들은 OR 조건으로 합쳐져서 필터링 3. 보고서 데이터 범위 질의 • 최대 기간 2년 4. 각각의 필터들은 AND 조건으로 합쳐져서 필터링되고 정렬과 페이징 제공
  33. 키워드 검색 기능을 위한 방식 검토 방식 매칭 방식 우려사항

    Trigram (pg_trgm) Ҋ, Ҋә, Ҋәݽ, әݽա, ݽա޷, ա޷ Locale 변경으로 인해 인덱스 사용 불가 Full Text Search 고급, 모나미 형태소 분석 후 조사(은, 는, 이,가 등)은 토큰에서 제외됨 N gram 고,고급,고급모,고급모나,고급모나미,급모나미,모나미,나미,미 순서가 유지된 채 일부 토큰만 검색 가능 Unigram 고, 급, 모, 나, 미 한 글자 단위로 검색 후, like 검색으로 필터링 해야함 Like 검색 특정 글자 포함 모두 검색 가능 속도가 느릴 것 같다?
  34. 키워드 검색 기능을 위한 방식 성능 비교 방식 쿼리 원하는

    결과 비율 응답시간 Like 검색 select count(*) from keyword where ad_account_id={id} and text like ‘%테스트%'; 100% 0.7초 Full Text Search select count(*) from keyword_fts where ad_account_id={id} and text_vector @@to_tsquery('korean', '테스트'); 95% 6분 34초 Unigram select count(*) from keyword_split where text_array @> ARRAY [‘테’,’스','트'] and ad_account_id={id} and text like ‘%테스트%'; 100% 6분 N gram select count(*) from keyword_ngram where text_array @> ARRAY ['테스트'] and ad_account_id={id}; 22% 2분 56초
  35. 요구사항 재검토 1. 광고 도메인 필드값 포함 또는 제외 •

    키워드 검색 기능 2. 광고 도메인 필드값 일치 • 광고 도메인의 계층 구조를 고려한 상태값 일치 (ex. 운영상태) • 하나의 필드에 대해 선택된 옵션들은 OR 조건으로 합쳐져서 필터링 3. 보고서 데이터 범위 질의 • 최대 기간 2년 4. 각각의 필터들은 AND 조건으로 합쳐져서 필터링되고 정렬과 페이징 제공
  36. 광고 도메인 간의 조인 기능 검토 조인하려는 shard가 같은 노드에서

    같은 sharding key로 접근 가능해야 함 [0A000] ERROR: complex joins are only supported when all distributed tables are co - located and joined on their distribution columns Sharding key 광고그룹-키워드 조인가능 여부 우려사항 광고그룹 ID 가능 하위 도메인이 상위 도메인의 ID 기준으로 분산되면 하위 도메인의 각 shard의 크기가 균일하지 않아서 조회되는 조건에 따라 성능 저하 가능 각 도메인 ID 불가 - [0A000] ERROR 발생 조인이 불가 -> 조인 없이 상위 도메인부터 필터 조건에 맞는 ID들을 찾아서 순차적으로 쿼리하자
  37. 요구사항 재검토 1. 광고 도메인 필드값 포함 또는 제외 •

    키워드 검색 기능 2. 광고 도메인 필드값 일치 • 광고 도메인의 계층 구조를 고려한 상태값 일치 (ex. 운영상태) • 하나의 필드에 대해 선택된 옵션들은 OR 조건으로 합쳐져서 필터링 3. 보고서 데이터 범위 질의 • 최대 기간 2년 4. 각각의 필터들은 AND 조건으로 합쳐져서 필터링되고 정렬과 페이징 제공
  38. 광고 도메인과 보고서 데이터 간의 조인 기능 검토 광고 도메인과

    보고서 데이터의 sharding key를 각 도메인 ID로 설정 - 예시) 다수의 키워드와 2년치 키워드 보고서 데이터를 조인하고, 노출수 기준으로 정렬, 페이징 select k.id, sum(r.viewable_impression_count) vimp from keyword k left outer join keyword_report r on (k.id=r.keyword_id and r.ad_account_id={ad_account_id} and (r.report_date between '2019-01-01' and '2021-11-16')) where k.ad_account_id={id} group by k.id order by vimp desc nulls last offset 0 limit 200; -> ES와 달리 데이터 노드들도 죽지 않고, 결과값도 나오지만 쿼리 응답시간이 3분 정도 소요
  39. 광고 도메인과 보고서 데이터 간의 조인 기능 검토 키워드가 특정

    개수 이상인 경우 캠페인 ID 조건을 필수로 사용하도록 요구사항 협의 - 예시) 다수의 키워드와 2년치 키워드 보고서 데이터를 조인하고, 노출수 기준으로 정렬, 페이징 
 + 캠페인 ID 조건 select k.id, sum(r.viewable_impression_count) vimp from keyword k left outer join keyword_report r on (k.id=r.keyword_id and r.ad_account_id = {ad_account_id} and (r.report_date between '2019-01-01' and '2021-11-16')) where k.ad_account_id={ad_account_id} and k.campaign_id = {campaign_id} group by k.id order by vimp desc nulls last offset 0 limit 200; -> 쿼리 응답시간: 20ms
  40. Filter Service Architecture DSP DB (PostgreSQL) Citus DSP Application Search

    Application Search Service AdManagement Service Air fl ow Flink Daily Report DB (Hadoop) Hourly Report (Kafka) DSP domain Event (Kafka)
  41. Filter Service Architecture DSP DB (PostgreSQL) Citus DSP Application Search

    Application Search Service AdManagement Service Air fl ow Flink Daily Report DB (Hadoop) Hourly Report (Kafka) DSP domain Event (Kafka) DSP 광고 도메인 데이터 동기화 - 전체 주의사항: citus deadlock detection 비용이 큼 도메인 id별 개별 트랜잭션으로 분리 필수!
  42. Filter Service Architecture DSP DB (PostgreSQL) Citus DSP Application Search

    Application Search Service AdManagement Service Air fl ow Flink Daily Report DB (Hadoop) Hourly Report (Kafka) DSP domain Event (Kafka) DSP 광고 도메인 데이터 동기화 - 변경분
  43. Filter Service Architecture DSP DB (PostgreSQL) Citus DSP Application Search

    Application Search Service AdManagement Service Air fl ow Flink Daily Report DB (Hadoop) Hourly Report (Kafka) DSP domain Event (Kafka) 일단위 보고서 데이터 동기화
  44. Filter Service Architecture DSP DB (PostgreSQL) Citus DSP Application Search

    Application Search Service AdManagement Service Air fl ow Flink Daily Report DB (Hadoop) Hourly Report (Kafka) DSP domain Event (Kafka) 당일 시간단위 보고서 데이터 동기화
  45. Filter Service Architecture DSP DB (PostgreSQL) Citus DSP Application Search

    Application Search Service AdManagement Service Air fl ow Flink Daily Report DB (Hadoop) Hourly Report (Kafka) DSP domain Event (Kafka) 필터 기능 조회
  46. 결론 - 처리해야 하는 데이터의 양이 많을 경우, 애플리케이션 로직으로

    모두 처리하면 하나의 요청에 대해 너무 많은 리 소스를 사용하게 되므로 적합한 저장소를 찾아서 적용하는 것이 효과적 - parent join을 통해 Elasticsearch에서도 연관관계를 구성할 수 있으나 성능상의 이슈로 권장하지 않음 - 약 65000개 이상의 집계 작업이 필요한 경우, Elasticsearch는 사용하기 어려움 - 결과적으로 사용하지 못하게 되었더라도 구현해야 하는 요구사항에 맞출 수 있는지 검토해보는 과정에서 해당 기 술을 더 잘 이해할 수 있음 - Citus의 like 검색은 생각보다 성능이 좋음. FTS를 꼭 써야 하는 요구사항이 아니라면 like 검색 사용 권장 - Citus에서도 각 도메인과 보고서 데이터 간의 조인은 사용했지만, 샤드 크기의 불균일화로 인한 성능 저하와 colocation 이슈로 인해 도메인 사이의 연관관계는 조인해서 사용하지 않음 - 요구사항과 기술적인 한계의 적절한 타협을 통해 기능 구현과 성능 두마리 토끼를 한꺼번에 잡을 수 있음