기존 멀티모달 검색 파이프라인은 음성 인식이나 이미지 캡셔닝 모델을 거쳐 텍스트로 바꾼 뒤 임베딩하는 우회로를 택해 왔습니다. 구글이 공개한 오픈 가중치 모델 EmbeddingGemma 2 는 텍스트, 코드, 이미지, 비디오, 오디오를 변환기 없이 단일 768차원 벡터 공간에 매핑해 이 문제를 정면으로 해결합니다. 1B 이하의 파라미터 크기로 온디바이스 및 모바일 환경에서도 효율적으로 동작하는 멀티모달 임베딩 아키텍처와 실제 구현 및 벤치마크 결과를 정리합니다. Embedding Gemma 2: On-Device Multimodal RAG Made Easy 멀티모달 RAG의 문제점과 단일 백본 모델의 등장 기존 검색 증강 생성( RAG ) 파이프라인은 대부분 텍스트 기반 검색에 의존합니다. 스마트폰에 저장된 사진, 화면 녹화본, 음성 메모, 프레젠테이션 슬라이드 같은 비정형 데이터는 내부에 텍스트가 명시적으로 없으면 텍스트 전용 시스템으로 찾아낼 수 없습니다. 이를 우회하기 위해 이미지에는 캡셔닝 모델을 붙이고 오디오에는 음성 인식( STT ) 모델을 연결한 뒤 텍스트 임베더로 넘기는 3단계 파이프라인 방식이 흔히 사용되었습니다. 그러나 이 방식은 캡셔닝 모델이나 STT 모델이 누락한 세부 정보가 검색 단계에서 영원히 소실된다는 구조적 한계가 있습니다. 예를 들어 강아지가 프리스비를 잡는 동영상에서 캡셔닝 모델이 단순히 '공원에 있는 개'로 요약해 버리면, 사용자가 '프리스비'라는 쿼리를 입력했을 때 해당 영상을 검색할 수 없습니다. 구글의 Gemini Embedding 2 논문에 따르면, 음성을 STT로 변환하여 임베딩했을 때의 검색 벤치마크 점수는 70.40이었으나, 오디오를 네이티브로 직접 임베딩했을 때는 73.99로 3.6점가량 더 높은 성능을 기록했습니다. 과거 OpenAI의 CLIP (2021)은 텍스트와 이미지 인코더 타워를 따로 두어 단일 모달리티 쌍만 처리했고, Meta의 ImageBind (2023)는 6개 모달리티를 이미지 중심으로 결합했으나 파이프라인이 복잡했습니다. ColPali (2024)는 페이지 스크린샷을 직접 벡터화해 OCR을 건너뛰었지만 페이지당 수많은 벡터를 생성하여 인덱스 크기가 급격히 커지는 문제가 있었습니다. 구글이 공개한 EmbeddingGemma 2 는 740M 크기의 단일 백본을 통해 텍스트, 코드, 이미지, 비디오, 오디오를 동일한 768차원 벡터 공간에 매핑합니다. 접근 방식 파이프라인 구성 한계점 기존 방식 (변환 후 검색) Captioner / STT $\rightarrow$ Text Embedder 변환 단계에서 세부 정보 누락 발생, 지연 시간 증가 CLIP / ImageBind 텍스트 타워 + 이미지 타워 (모달리티별 분리) 짧은 캡션 위주 학습, 단일 모달리티 쌍 중심 제약 ColPali VLM 기반 멀티 벡터 인덱싱 페이지당 과도한 벡터 수로 인해 인덱스 크기 급증 EmbeddingGemma 2 모듈형 인코더 $\rightarrow$ Gemma 4 백본 (단일 공간) 단일 768차원 출력, 온디바이스 동작 가능 (<1B) EmbeddingGemma 2 아키텍처와 경량화 배포 기법 EmbeddingGemma 2는 270M 파라미터 크기의 Gemma 4 기반 텍스트 모델을 공통 백본으로 사용합니다. 비텍스트 입력 처리를 위해 백본 전면에 170M 파라미터의 비전 인코더와 300M 파라미터의 오디오 인코더를 모듈식으로 배치했습니다. 비전 인코더는 이미지나 비디오 프레임을 토큰으로 변환하고, 오디오 인코더는 음향 신호를 토큰으로 변환한 뒤 텍스트 토큰과 동일한 단일 시퀀스로 백본에 주입합니다. 전체 모델의 컨텍스트 윈도우는 8,192 토큰이며, mean pooling 을 거쳐 최종 768차원 임베딩 벡터로 투영됩니다. 입력 데이터별 토큰 소비량은 고정되어 있습니다. 단일 이미지는 기본 280 토큰, 비디오 프레임은 140 토큰, 오디오는 1초당 약 25 토큰을 차지합니다. 따라서 8,192 토큰 컨텍스트 내에는 약 29장의 이미지, 58개의 비디오 프레임, 혹은 약 5.5분 분량의 오디오를 단일 입력으로 담을 수 있습니다. 상품 설명 텍스트, 제품 사진, 시연 비디오가 섞인 복합 멀티모달 입력도 하나의 벡터로 한 번에 임베딩할 수 있습니다. [텍스트 / 코드] ───────────────┐ [이미지 / 비디오] ──> Vision (170M) ┼──> Gemma 4 백본 (270M) ──> Mean Pooling ──> 768차원 벡터 [오디오 신호] ──> Audio (300M) ┘ 실제 프로덕션 배포를 위해 두 가지 최적화 기능이 포함되어 있습니다. 마트료시카 표현 학습(Matryoshka Representation Learning) : 768차원 벡터를 512, 256, 128차원으로 잘라내도 독립적인 임베딩으로 작동하도록 학습되었습니다. 128차원으로 축소 시 인덱스 용량은 6배(100만 벡터 기준 약 1.5GB에서 250MB) 줄어듭니다. 다만 텍스트 MTEB 점수는 61.36에서 57.89로 소폭 하락하는 반면 멀티모달 점수(MMEB)는 59.01에서 45.65로 크게 떨어지는 트레이드오프가 있습니다. 모듈식 로딩(Modular Loading) : 텍스트와 코드만 처리할 때는 270M 가중치만 불러오고, 비전 추가 시 440M, 오디오 추가 시 570M, 전체 로드 시 740M으로 선택적 로딩이 가능합니다. 양자화 적용 시 텍스트 전용 버전은 픽셀 11 프로 기준 약 191MB의 RAM만 사용합니다. Colab 핸즈온 검증: 이미지, 비디오, 오디오 검색 성능 분석 무료 Colab T4 GPU 환경에서 Flickr 이미지 1,000장, ESC-50 환경음 50개, 오픈 무비 'Big Buck Bunny' 비디오 5분 클립, 'Attention Is All You Need' PDF 논문을 사용해 모델 동작을 직접 검증할 수 있습니다. 구현 시 가장 주의해야 할 점은 데이터 타입입니다. T4 환경에서 float16 을 사용하면 내부 활성화 오버플로우가 발생해 에러 없이 NaN 값이나 잘못된 임베딩이 조용히 출력됩니다. 구형 GPU에서는 float32 를, Ampere 이상 GPU에서는 bfloat16 을 명시적으로 사용해야 합니다. # T4 GPU 환경에서의 올바른 로드 예시 (float16 금지) import torch from sentence_transformers import SentenceTransformer dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float32 model = SentenceTransformer( "google/embeddinggemma-2", model_kwargs={"torch_dtype": dtype} ) 이 모델의 실제 모달리티별 검색 특성은 다음과 같습니다. 텍스트 기반 사진 검색 : 1,000장 대상 검색에서 Top-1 정답 반환 확률은 86%, Top-5 기준으로는 97%의 정확도를 기록했습니다. 백본이 100개 이상의 언어를 지원하므로 독일어("Ein Hund, der im Schnee rennt")나 스페인어("una mujer tocando la guitarra") 쿼리로도 정확한 사진을 반환합니다. 이미지 및 음성 쿼리 : 텍스트 없이 서퍼 사진을 쿼리로 입력했을 때 단순한 파란색 배경 매칭에 그치지 않고 수상 레저(서핑, 웨이크보드) 액션을 이해하여 유사 사진을 찾아냈습니다. gTTS 로 생성한 음성 쿼리를 텍스트 변환 없이 모델에 직접 넣었을 때도 텍스트 쿼리와 완전히 동일한 상위 4개 사진을 검색했습니다. 환경음 검색의 한계 : ESC-50 데이터셋 환경음 검색에서는 Top-1 정확도가 24%에 불과했습니다. '웃음소리', '변기 물 내리는 소리'는 정답 클립을 찾았으나 '개 짖는 소리'는 까마귀 소리로, '빗소리'는 바람 소리로 오매핑되었습니다. 오디오 인코더가 사람의 음성(Speech) 위주로 강하게 정렬되어 있어 일반 환경 효과음 분별력은 떨어집니다. 비디오 및 문서 검색 : 비디오를 5초 단위(1 fps, 프레임당 140토큰, torchcodec 디코딩 활용)로 인덱싱했을 때 자막 없이도 "a purple butterfly", "a dark hole in the ground" 등의 쿼리로 정확한 타임스탬프를 찾아냈습니다. PyMuPDF(fitz) 로 렌더링한 PDF 논문 스캔본 역시 텍스트 추출 없이 "diagram of the transformer architecture" 쿼리로 아키텍처 다이어그램이 포함된 3페이지를 정확히 인덱싱했습니다. 정리 EmbeddingGemma 2는 STT와 캡셔닝으로 이어지던 복잡하고 정보 손실이 잦았던 기존 멀티모달 검색 파이프라인을 740M 단일 백본 모델로 단순화했습니다. 텍스트, 이미지, 비디오, 음성을 768차원 단일 벡터 공간으로 직접 매핑하면서도 마트료시카 표현 학습과 모듈식 로딩을 통해 온디바이스 환경에 맞는 경량성을 확보했습니다. 환경음 인식에는 한계가 있지만, 음성 검색, 비디오 타임스탬프 탐색, 도표 중심의 문서 인덱싱을 단일 오픈 가중치 모델로 구현할 수 있는 실용적인 표준을 제시합니다. 다룬 영상 Embedding Gemma 2: On-Device Multimodal RAG Made Easy — Prompt Engineering · 19분
위 화면은 아무 문제가 없어 보입니다. Alert도 없고, EDR 상태도 정상입니다. 그러나 보안 관제에서 가장 위험한 화면은 경고가 많은 화면이 아니라, 아무 이유 없이 조용한 화면입니다. Alert가 0건일 때 우리는 시스템이 안전하다고 생각하기 쉽습니다. 하지만 실제로는 Endpoint Agent가 오프라인이거나, 일부 센서가 제대로 동작하지 않거나, 수집 파이프라인의 처리가 지연된 상태일 수도 있습니다. 공격이 탐지된 이후에도 문제는 남습니다. 프로세스 실행, 외부 통신, 파일 변경과 DNS 기록이 서로 다른 화면과 저장소에 흩어져 있다면, 관제 담당자는 Alert가 어떤 Event에서 시작됐고 어떤 Rule을 거쳐 Incident가 됐는지 다시 조립해야 합니다. OWLBY는 이 두 문제에서 출발했습니다. 경보의 수가 아니라 볼 수 있는 상태인지를 먼저 판단하고, 탐지 결과를 흩어두지 않고 원본 Event까지 되짚을 수 있게. 실제 Windows·macOS Endpoint의 보안 이벤트를 수집하고, 위협 수준(Threat Level)과 수집 건전성(Collection Health)을 판단하며, 탐지 결과를 원본 Event까지 추적하는 EDR 플랫폼. 그게 OWLBY입니다. 우리의 EDR은 여기서부터 시작합니다. Github / 박소연 , 이주호 , 이혜령 , 황건하 핵심 기능 각각의 Event들이 어떻게 조사 가능한 하나의 Incident가 되는지, 하나의 알럿을 예로 들어 보여드립니다. 어느 단말에서 인코딩된 PowerShell이 실행됐고, OWLBY가 이를 알럿 으로 잡았습니다. 이 알럿 하나를, 담당자가 조사하는 순서 그대로 따라가 보겠습니다. — 수집 Agent “방금 그 알럿은 어디서 왔을까?” Windows(C++)와 macOS(Swift) 네이티브 Agent가 프로세스, 네트워크, 파일, DNS, L7 이벤트를 수집합니다. 그 PowerShell 실행도, 한 단말의 Agent가 프로세스 이벤트로 잡아 중앙으로 보낸 한 건이었습니다. — Overview “지금 우리 조직은 괜찮을까?” 담당자가 가장 먼저 보는 화면. 방금 그 알럿으로 위험도가 막 올라갔습니다. 조직 전체의 현재 상태를 Threat Level과 Collection Health 두 축으로 판단합니다. KPI, Detection Activity, Alert Severity 분포, Fleet 분포를 한 화면에서 확인합니다. Overview 사진 자료 — Alerts → Events “이 경고, 대체 뭐지?” 그 알럿을 엽니다. 규칙이 무엇을 보고 반응했는지 확인합니다. Alert 상세의 원본 이벤트까지 되짚고, Process Tree로 누가 이 PowerShell을 띄웠는지 부모 프로세스를 추적합니다. Alerts 사진 자료 Alerts-Events 사진 자료 Events 사진 자료 — Incidents “이게 다 연결된 하나의 사건인가?” 같은 단말에서 30분 안에 발생한 관련 Alert들이 하나의 Incident로 자동 연결됩니다. 이제 담당자는 흩어진 경고 여러 개가 아니라, 하나의 사건과 그 근거들을 봅니다. Attack Timeline과 Investigation Graph로 Event → Alert → Incident의 관계를 조사합니다. Incidents 사진 자료 Incidents 사진 자료 — Intelligence “그래서 이 공격, 밖으로 뭘 전송했을까?” MITRE ATT&CK tactic/technique 분포, top process/domain/IP, Endpoint egress Topology를 보여줍니다. IP·Domain을 correlate하면 현재 DNS와 과거 관찰을 구분해 함께 보여줍니다. Intelligence 사진 자료 Intelligence 사진 자료 — Operations “우리 수집이 놓친 건 없을까?” Ingest Health, Live Service/Pipeline Probe, Archive 상태로 수집 파이프라인 자체의 건강을 모니터링합니다. Operations 사진 자료 다시, 처음의 질문으로 돌아갑시다. “그래서 무슨 일이 일어난 거지?” 이제 담당자는 답할 수 있습니다. 누군가 인코딩된 PowerShell로 명령을 숨겨 실행했고, OWLBY는 그 한 줄에서 시작해 흩어진 신호들을 하나의 사건으로 엮어 보여줬습니다. 수십 개의 경고가 아니라, 하나의 근거. 탐지에서 끝나지 않는 조사. 그게 OWLBY입니다. 추가 기능 이외에도 사용자의 접근성과 편의를 위한 UI/UX적 기능을 도입했습니다. — 라이트/다크 테마 전환 사용자 환경에 맞춰 화면 테마를 선택할 수 있습니다. 오래 화면을 들여다보는 관제 담당자의 눈 피로를 줄입니다. — 한/영 언어 선택 한국어와 영어를 전환할 수 있습니다. 화면 언어와 함께 날짜 표기도 선택한 언어에 맞춰 바뀝니다. — Alerts 상태 변경 담당자가 Alert를 검토하며 상태를 직접 바꿀 수 있습니다. Submit & Next로 미해결 Alert 큐를 끊김 없이 넘겨 처리합니다. — 대시보드 커스텀 Overview 위젯을 드래그·리사이즈로 재배치해 자신에게 맞는 대시보드를 구성할 수 있습니다. 구성한 레이아웃은 사용자별로 저장됩니다. System Architecture ERD 데이터의 성격에 따라 저장소를 나눕니다. Agent 로컬 버퍼는 SQLite , 원본 이벤트 검색·집계는 ClickHouse , 사용자·단말·경고·사건은 PostgreSQL , 실패 원문과 장기 아카이브는 S3/Glacier 가 담당합니다. Frontend — React 19 React는 컴포넌트 기반 UI 라이브러리로, 복잡한 대시보드 화면을 재사용 가능한 단위로 구성할 수 있게 해줍니다. — TypeScript TypeScript는 정적 타입을 제공해 런타임 이전에 오류를 잡아줍니다. EDR_C에서는 백엔드 OpenAPI 명세로부터 타입을 자동 생성하여, API 응답 계약과 프론트 코드가 항상 일치하도록 보장했습니다. — Vite Vite는 초고속 빌드 및 개발 서버 환경을 제공하는 툴입니다. HMR덕분에 조사 화면을 개발하는 속도를 극대화할 수 있었습니다. Backend — FastAPI & Uvicorn FastAPI는 Python 기반의 고성능 웹 프레임워크로, Pydantic 모델을 통해 API 응답 계약을 엄격하게 강제합니다. EDR_C에서는 Agent용 Collector API와 Dashboard API를 FastAPI로 구축했습니다. — Nginx Nginx는 역방향 프록시로, HTTPS/mTLS 종료와 트래픽 관리를 담당합니다. EDR_C에서는 Nginx가 Agent의 client 인증서를 검증하고, 외부 신원 헤더를 제거한 뒤 검증된 인증서만 백엔드로 전달하도록 했습니다. — Apache Kafka Kafka는 대용량 이벤트 스트리밍 플랫폼입니다. EDR_C에서는 수집 경계와 탐지 경계를 분리하고, at-least-once 전달을 전제로 멱등성을 설계해 이벤트 중복과 Worker 장애를 견디도록 했습니다. — PostgreSQL PostgreSQL은 관계형 데이터베이스로, 정합성이 중요한 데이터를 저장합니다. EDR_C에서는 사용자, 단말, 인증키, Alert, Incident, 감사 로그를 PostgreSQL에 저장합니다. — ClickHouse ClickHouse는 대용량 이벤트 검색·집계에 특화된 컬럼형 데이터베이스입니다. EDR_C에서는 위험 여부와 관계없이 모든 유효한 원본 메타데이터 이벤트를 ClickHouse에 저장하여, 조사 시 언제든 원본을 빠르게 되짚을 수 있도록 했습니다. — AWS S3 & Glacier S3와 Glacier는 객체 저장소로, 장기 보관과 비용 효율을 담당합니다. EDR_C에서는 최근 이벤트는 ClickHouse에서 조회하고, 오래된 이벤트는 Parquet로 묶어 S3 Glacier에 아카이브했습니다. DevOps — Docker & Docker Compose EDR_C는 Docker와 Docker Compose로 전체 스택을 컨테이너화했습니다. 개발자는 한 번의 Compose로 전체 개발 환경을 번거로운 로컬 설정 없이 바로 실행할 수 있습니다. — AWS EC2 & Portainer (GitOps) Mac mini의 Portainer Server가 EC2 Agent를 원격 관리합니다. main에 push하면 GitHub Actions가 커밋 SHA로 이미지를 빌드하고, production 브랜치에 이미지 SHA를 고정하면, Portainer GitOps polling이 감지해 5분마다 자동 재배포합니다. — AWS S3 운영 환경에서 S3는 실패 이벤트 원문과 raw event 아카이브의 저장소입니다. Glacier에 raw event를 아카이빙 하고, 실패 이벤트는 90일 만료를 관리하여 저장 비용을 자동으로 최적화했습니다. Monitoring — Grafana Cloud Grafana Cloud는 Observability SaaS 플랫폼입니다. EDR_C에서는 메트릭, 로그, 알림을 Grafana Cloud에 모아, 별도 모니터링 서버를 직접 운영하지 않고도 시스템 상태를 실시간으로 파악할 수 있도록 했습니다. — Grafana Alloy Alloy는 Grafana의 통합 수집 에이전트입니다. EDR_C에서는 EC2에서 Alloy 하나가 호스트, 컨테이너, Kafka, 백엔드 지표와 Docker 로그를 30초 주기로 모아 Grafana Cloud로 전송합니다. — Grafana Mimir Mimir는 Grafana Cloud의 대규모 메트릭 저장 백엔드입니다. EDR_C의 모든 Prometheus 메트릭은 Alloy를 통해 Mimir에 저장되어 장기 추세 분석과 대시보드에 활용됩니다. — Grafana Loki Loki는 라벨 기반 로그 집계 시스템입니다. EDR_C에서는 Docker 소켓에서 컨테이너의 로그만 골라 Loki로 전송하여, 여러 서비스의 로그를 한 곳에서 검색·분석할 수 있도록 했습니다. Troubleshooting 시드 데이터를 248,000건 규모로 늘리자, 로컬에서 잘 돌던 화면들이 하나씩 무너지기 시작했습니다. 그 과정에서 발견하고 개선한 세 가지 사례를 공유합니다. — 이벤트 저장소를 PostgreSQL에서 ClickHouse로 옮긴 이유 문제 초기에는 Alert·incident 이외에도 원본 이벤트까지 전부 PostgreSQL에 저장했었습니다. 데이터 형식이 하나로 유지되어 개발이 빠르고, 조인도 편했습니다. 문제는 시드 데이터를 248,000건 규모로 늘리면서 드러났습니다. Dashboard Overview가 24시간 조회에 16.31초, 7일 조회는 20초(Timeout)를 넘겨 아예 응답하지 못했습니다. 원인 원본 이벤트 데이터는 쉴 새 없이 append 되어 양이 매우 방대합니다. 덧붙여 읽기는 GROUP BY(기간별 집계)로 이루어 지기에, PostgreSQL에서는 특정 컬럼 몇 개만 집계하고 싶어도 구조상 전체 행을 훑어야 했습니다. PostgreSQL을 계속 원본 이벤트 저장소로 사용했다면, 데이터 양이 늘어날수록 연산 시간은 끝도 없이 늘어날 수 밖에 없었습니다. 결과 따라서 원본 이벤트 저장소 용도로 ClickHouse를 도입했습니다. ClickHouse는 대량 append와 GROUP BY 연산에 최적화 된 DBMS입니다. 단지 DBMS 유형을 바꾼 것 뿐이지만, 매우 개선된 결과를 얻을 수 있었습니다. 아예 실패했던 7일 조회 화면은 0.7초대로 들어왔습니다. — 이벤트 아카이빙용 저장소로 RDS가 아닌 S3/Glacier을 선택한 이유 문제 이벤트는 끊임없이 쌓입니다. 이 모든 이벤트들을 RDS 또는 EC2 인스턴스 내에 저장하여, 계속 HOT Data로 처리하는 것은 비용 문제로 직결됩니다. 따라서 많은 데이터가 쌓이기 전에, 미리 오래된 이벤트는 다른 저장소에 아카이빙해 두자는 아이디어를 떠올려 봤습니다. 결과 실제 AWS 요금(us-east-1, 2026)으로 저장 단가를 비교하면 격차가 분명합니다. 예를 들어 아카이빙된 이벤트가 1TB까지 쌓였을 때, 각 저장소 마다의 1년 저장 비용을 계산해 봅시다. RDS cost = 1,000GB × $0.115 × 12M ≈ $1,380 / Y S3 Glacier cost = 1,000GB × $0.0036 × 12M ≈ $43 / Y 같은 데이터를 약 32배 저렴하게 보관할 수 있습니다. 이러한 계산을 통해 최근 데이터는 ClickHouse에 저장해 두되, 오래된 이벤트는 ZSTD로 묶어 보관이 저렴한 S3 Glacier에 저장하는 방식을 채택했습니다.
You have shipped a fast, clean, well-tested website, and the analytics dashboard stays almost empty. It is one of the most common frustrations in web development: the code is solid, the design is polished, yet search engines barely seem to notice. In most cases the cause is not content quality or bad luck. It is a handful of technical gaps that never show up in a code review. Teams that pair engineering with professional SEO and web development support tend to catch those gaps early, but developers can also close most of them on their own once they know where to look. This guide covers technical SEO for developers in practical terms: what crawlers need, where modern stacks go wrong, and how to build a workflow that keeps problems from coming back. Why Good Code Doesn't Automatically Mean Good Visibility Search engines do not experience a website the way users do. A person sees a rendered page, animations and a navigation menu. A crawler sees a stream of HTTP responses, HTML, links and signals, and it has to decide in a very short time whether a page is worth storing and ranking. That difference explains why technically impressive sites underperform. A single-page application may look perfect in the browser while delivering an almost empty HTML shell to a crawler. A staging configuration might leave a noindex directive in production. A redirect chain added during a migration may quietly waste crawl budget for months. Technical SEO is the discipline of removing those obstacles. It does not replace good content or links, but it decides whether those efforts can be recognised at all. For developers, the encouraging part is that most technical SEO work is deterministic. Issues are testable, reproducible and fixable, which makes them a natural fit for engineering habits such as code review, automated checks and monitoring. Crawlability and Indexing: The Foundation Before anything else, make sure search engines can reach your pages and are allowed to index them. Start with the robots.txt file. It should block only what genuinely needs blocking, such as internal search results, admin paths or endless filter combinations. A common and costly mistake is copying a restrictive robots.txt from a staging environment into production. Next, check meta robots tags and X-Robots-Tag headers. Noindex is useful for thank-you pages, internal tools and thin utility pages, but it should never appear on pages you want to rank. Add a simple automated test to your deployment pipeline that fails the build if key templates contain noindex. Provide an XML sitemap that lists canonical, indexable URLs only. Including redirected, blocked or error pages sends mixed signals and weakens the sitemap's value as a guide. Keep it updated automatically and reference it in robots.txt. Finally, use canonical tags deliberately. Every indexable page should point to its preferred version, and the canonical should match what is in the sitemap and internal links. When parameters, trailing slashes or uppercase variants produce duplicate URLs, canonicals consolidate them. Self-referencing canonicals are a sensible default for standard pages. Site Architecture, URLs and Internal Linking A clear architecture helps both users and crawlers understand how content relates. Aim for a shallow structure where important pages are reachable within a few clicks from the homepage. Pages buried deep, or reachable only through scripts and search boxes, tend to be crawled less often and valued less. URLs should be readable, stable and consistent. Use lowercase, hyphens between words, and descriptive slugs rather than database IDs. Decide on a trailing-slash convention and enforce it with redirects. When you must change a URL, implement a single 301 redirect to the new location and update internal links so they point directly to the final destination, rather than relying on redirects indefinitely. Internal links are one of the strongest tools you control. Link from high-authority pages to important deeper pages using descriptive anchor text, and avoid generic phrases such as "click here". Make sure navigation, breadcrumbs and related-content modules use real anchor elements with href attributes. Links that depend on click handlers alone may not be followed, which leaves pages orphaned even though users can reach them. Run a crawl periodically to find orphaned pages, redirect chains and broken links. These are easy to introduce during refactors and easy to fix once surfaced. Rendering and JavaScript Frameworks Modern frameworks bring real benefits, and also real SEO responsibilities. Search engines can render JavaScript, but rendering happens in a separate, resource-limited step, which can delay indexing and occasionally fail for complex pages. Relying entirely on client-side rendering for content that matters is a gamble. Server-side rendering, static generation or hybrid approaches reduce that risk by delivering meaningful HTML in the initial response. If your stack supports it, render critical content, titles, meta descriptions, headings and primary links on the server. Reserve client-side rendering for interactive elements that do not need to be indexed. Test what crawlers actually receive. View the raw HTML response with a command-line request, then compare it with the rendered DOM in a browser. If the key text, internal links or structured data appear only after scripts run, consider whether that content must be in the initial HTML. Also be careful with routing. Single-page apps should update the URL with the History API rather than hash fragments, return correct HTTP status codes for missing pages, and avoid serving a "not found" view with a 200 status. Soft 404s confuse crawlers and dilute index quality. Page Speed and Core Web Vitals Performance is both a user-experience issue and a ranking consideration. Google's Core Web Vitals focus on three aspects: loading speed of the main content, responsiveness to interaction, and visual stability during load. You do not need perfect scores, but consistently poor results make a site harder to use and harder to rank. The most dependable gains usually come from a short list of practices. Compress and properly size images, serve modern formats where appropriate, and lazy-load images that appear below the fold while keeping the main hero image eager. Reserve space for images, ads and embeds with explicit dimensions so layout does not shift as they load. Reduce JavaScript wherever you can. Split bundles, defer non-critical scripts, remove unused libraries and audit third-party tags, which are frequent sources of slow interaction. Use efficient caching headers, a content delivery network and HTTP compression to speed up repeat visits and distant users. Measure with both lab and field data. Lab tools help you debug a specific build, while real-user data shows how people actually experience the site across devices and connections. Set performance budgets and track them in your pipeline so regressions are caught before release rather than after rankings slip. Metadata, Headings and Structured Data On-page technical elements tell search engines what a page is about and how it might appear in results. Each indexable page needs a unique, descriptive title element and a meta description that summarises the content honestly. Neither should be generated carelessly from template defaults, since duplicate titles across hundreds of pages are a common indicator of weak site quality. Use a logical heading hierarchy: one H1 that reflects the main topic, followed by H2 and H3 elements that organise sections. Headings are structural signals as well as visual styling, so avoid choosing heading levels based purely on font size. Structured data adds explicit context. Using the Schema.org vocabulary in JSON-LD, you can describe articles, products, organisations, breadcrumbs, FAQs and more. Valid markup can make a page eligible for enhanced presentation in results, though eligibility is never a guarantee. Keep the markup consistent with visible content, since marking up information users cannot see violates guidelines and risks losing the benefit. For international or multi-region sites, implement hreflang annotations carefully, with reciprocal references and correct language and region codes. Errors here are common and easy to miss without a validator. Mobile Experience, Security and Accessibility Search engines primarily evaluate the mobile version of a site, which means anything missing there may effectively be missing from the index. Use responsive design, make sure content and structured data are equivalent across versions, and test tap targets, font sizes and viewport configuration on real devices. Serve every page over HTTPS with a valid certificate, redirect HTTP to HTTPS in a single step, and avoid mixed content that triggers browser warnings. Security headers do not directly lift rankings, but a compromised or untrusted site loses visibility quickly. Accessibility and SEO overlap more than most teams expect. Descriptive alt text, semantic HTML, clear link text, labelled form fields and sensible focus order all help assistive technologies and crawlers interpret content. Building accessibly is often the cheapest way to improve technical quality across the board. Building an SEO Workflow Into Development The best technical SEO is not a one-off audit but a habit. Add checks for status codes, canonical tags, titles, indexability and structured data to your continuous integration process. Review SEO impact during planning for migrations, redesigns and framework changes, because those are the moments when traffic is most easily lost. Maintain a redirect map before any URL changes, benchmark rankings and traffic before launch, and monitor Search Console and server logs afterwards. Logs reveal how crawlers actually behave, including wasted requests on parameters or error pages. Finally, document decisions. A short internal guide describing your canonic
Looking for Rugs for Sale in Albuquerque ? The right rug can instantly make a room feel warmer, more comfortable, and more stylish. Whether you are decorating a living room, bedroom, dining area, or office, Albuquerque offers a variety of rug styles, sizes, colors, and designs to match different interiors. Why Choose the Right Rug for Your Home? A rug is more than just a decorative piece. It can help define a space, add warmth, provide comfort underfoot, and bring different furniture pieces together. The right design can also become a focal point in your room and complement your existing décor. When shopping for a rug, consider the room's size, furniture arrangement, color scheme, and the amount of foot traffic the area receives. These factors can help you choose a rug that looks attractive while also being practical. Types of Rugs Available in Albuquerque When searching for rugs in Albuquerque, you can find many different styles and materials to suit your preferences. Handmade Rugs Handmade rugs are popular for their craftsmanship, unique patterns, and detailed designs. They can add character and an elegant appearance to living rooms, bedrooms, and other important areas of the home. Traditional Rugs Traditional rugs feature classic patterns, rich colors, and timeless designs. They work especially well with classic, vintage, and elegant interior styles. Modern Rugs Modern rugs are a great option for contemporary homes. They are available in clean patterns, geometric designs, neutral colors, and bold combinations that can complement modern furniture. Area Rugs Area rugs are available in different sizes and can be used to define specific sections of a room. They are especially useful for living rooms, dining rooms, bedrooms, and open-plan spaces. How to Choose the Right Rug Size Choosing the correct rug size is important because a rug that is too small or too large can affect the overall appearance of a room. Measure the available floor space before purchasing a rug. Consider the size and placement of your furniture. Choose a larger rug when you want to visually connect multiple pieces of furniture. Use smaller rugs or runners in hallways, entryways, and compact spaces. Leave enough visible flooring around the rug to create a balanced appearance. Consider the Material Before Buying Rug material can affect its appearance, comfort, durability, and maintenance requirements. Wool rugs are known for their softness and durability, while synthetic materials can provide practical and budget-friendly options. For areas with frequent foot traffic, consider a material that is easy to maintain and suitable for everyday use. For bedrooms or formal living spaces, you may prefer a softer and more decorative option. Find a Rug That Matches Your Interior Style Your rug should complement the furniture, walls, flooring, and overall design of your room. Neutral rugs can create a calm and sophisticated look, while colorful or patterned rugs can add personality and visual interest. If your furniture already has bold colors or patterns, a simpler rug may create a more balanced appearance. On the other hand, a patterned rug can bring energy to a room with neutral furniture and walls. Why Shop for Rugs Locally in Albuquerque? Shopping locally allows you to see the rug's actual colors, texture, size, and craftsmanship before making a purchase. You can also compare different designs and get assistance when selecting an option for your space. Albuquerque has a variety of rug retailers offering different styles, from affordable area rugs to handmade and luxury designs. Some local stores also provide rug cleaning, repair, restoration, and other care services. Tips for Buying a Rug Measure your room before shopping. Decide on your preferred rug style and color. Consider how much foot traffic the rug will receive. Check the material and maintenance requirements. Compare different sizes and designs. Choose a rug that complements your existing furniture and décor. Consider quality and durability along with price. Final Thoughts Finding the perfect rug can make a significant difference to the look and comfort of your home. Whether you prefer handmade, traditional, modern, or area rugs, exploring different options can help you find a design that fits your space and lifestyle. If you are searching for rugs for sale in Albuquerque, take time to compare sizes, materials, patterns, and prices before making your final decision. A well-chosen rug can add warmth, personality, and style to your home for years to come.
세 줄 요약 1분 Rollup이 원본(ClickHouse)과 어긋난 원인은 하나가 아니었다. 윈도우 경계, 워터마크, 재전송, 지연 도착이 동시에 숫자를 흔들었다. "MV로 바꾸면 되지 않을까?"는 더 나빴다. MV는 뷰가 아니라 INSERT 트리거 라서 과거·삭제·중복 제거를 보지 못한다. 해결의 핵심은 더하지 말고, 다시 세는 것 이다. 그리고 "돌고 있는가"가 아니라 "맞는가"를 감시하는 대사 지표가 필요하다. 0. 실험 환경 OWLBY(EDR 플랫폼)의 Dashboard Rollup 경로만 떼어 내 장애를 재현했다. 스키마는 운영 마이그레이션과 같은 컬럼·엔진·정렬 키를 쓴다. 구분 내용 원본 ClickHouse 24.8 · edr_events · ReplacingMergeTree(updated_at) · PARTITION BY toDate(occurred_at) Rollup PostgreSQL 16 · dashboard_event_rollups · PK (bucket_start_at, endpoint_id, event_type) 트래픽 endpoint 3대 × 5분 · Event 1,425건 (가상 시계로 돌려 매번 같은 결과) 비교 전략 naive_window · naive_watermark(>, >=) · mv(raw, sum) · mv_state · fixed 재현 코드 team-C labs/incident-lab ( python -m lab scenario s1 , s1 --matrix , s1-mv ) 목데이터에는 장애 요인을 일부러 섞었다. 경계 Event는 항상 넣고, 나머지는 --inject 로 하나씩 켜고 끈다. 요인 만드는 상황 실제로 생기는 이유 경계 Event HH:MM:59.999 · 00.000 · 00.001 에 20건씩 집계 윈도우 경계 resend 같은 event_id 를 새 ingested_at 으로 재적재 (5%) Kafka at-least-once 재전달 slow ingested_at 은 먼저 찍혔는데 Worker tick 이후에 커밋 Storage Worker 여러 대, 느린 insert same_ms 워터마크와 같은 ms로 찍힌 다른 배치가 tick 이후에 커밋 배치 단위 타임스탬프 late occurred_at 은 과거 분, ingested_at 은 3분 뒤 (45건) 오프라인 Agent 버퍼 flush skew ingested_at 이 10분 전으로 찍힌 배치 (30건) 시계가 틀어진 호스트 1. 대시보드 숫자가 원본과 다르다 OWLBY 대시보드는 기본으로 PostgreSQL의 1분 Rollup을 읽는다. 사용자가 Refresh를 누를 때만 ClickHouse 원본을 직접 센다(LIVE). 5분 동안 Event 1,425건을 흘리고 Refresh를 눌렀다. 숫자가 바뀌었다. 조회 Event 수 ROLLUP (기본) 1,290 LIVE (Refresh) 1,425 어떤 분은 부풀어 있고, 어떤 분은 100건 넘게 비어 있었다. 그런데 ClickHouse·PostgreSQL 에러는 0건이었다. Worker도 정상적으로 tick을 돌고 있었다. 2. 아무 지표도 울리지 않았다 이 사건은 지표로 잡히지 않았다. 잡힐 수가 없었다. /operations/health 검사 무엇을 보나 이번 사건에서 coverage 최근 24시간의 모든 분이 재집계됐는가 ✅ 통과 — 모든 분을 제때 돌았다 freshness 원본 최신 ingested_at 과 Rollup 차이가 300초 이내인가 ✅ 통과 — 최신 시각도 따라잡았다 정합성 Rollup 숫자가 원본과 같은가 — 검사 자체가 없었다 두 검사 모두 "집계가 돌았는가" 에 답한다. "집계가 맞는가" 에는 답하지 않는다. ⚠️ 최초 탐지는 사람이 Refresh를 누른 순간이었다. 사람이 먼저 발견했다는 것 자체가 탐지 실패다. 3. 무엇이 숫자를 틀리게 했나 3-1. 용의자 목록 지표가 범인을 가리켜 주지 않으니, 의심 가는 것을 다 꺼내 놓고 장애 요인을 하나씩만 켜서 지웠다. # 용의자 확인 방법 판정 1 ClickHouse·PostgreSQL 서버 이상 서버 상태, Worker 로그 ❌ 무죄 — 에러 0건 2 Storage Worker 적재 실패 SELECT uniqExact(event_id) FROM edr_events FINAL ❌ 무죄 — 원본은 정답 1,425건 3 Rollup 윈도우 경계 경계 Event만 넣고 재현 ✅ 유죄 — 분마다 정확히 +60 4 ingested_at 워터마크 늦은 커밋만 켜고 재현 ✅ 유죄 — −408 5 재전송 중복 재전송만 켜고 재현 ✅ 유죄 — count() 가 재전송분까지 센다 6 지연 도착 지연 도착만 켜고 재현 ✅ 유죄 — −45 범인이 넷이었다. 하나를 찾고 멈췄다면 나머지 셋을 놓쳤을 것이다. 3-2. 추측 전에 대사(對査)부터 원본을 직접 센 값과 Rollup을 분 단위로 나란히 놓는다. 분 (UTC) 원본 Rollup 차이 diff_pct 00:20 315 211 −104 −33.0% 00:21 300 270 −30 −10.0% 00:22 270 272 +2 +0.7% 00:23 270 279 +9 +3.3% 00:24 270 258 −12 −4.4% 00:22의 +2는 거의 맞아 보인다. 하지만 (분, endpoint, event_type) 키로 쪼개 보면 불일치 키가 67개 였다. +2는 맞은 게 아니라 중복과 누락이 상쇄된 값이다. 💡 총합이나 분 단위 합으로는 판정할 수 없다. 대사는 키 단위로 해야 한다. 3-3. 원인 1 — 분 경계마다 정확히 +60 -- 분이 닫히면 그 분을 세어 PostgreSQL에 '더한다' WHERE occurred_at BETWEEN '00:21:00.000' AND '00:22:00.000' -- 양끝 포함 -- 다음 tick WHERE occurred_at BETWEEN '00:22:00.000' AND '00:23:00.000' -- 00:22:00.000을 또 셈 BETWEEN 은 양끝을 포함한다. 정각 Event는 앞뒤 두 윈도우에 모두 잡혀 같은 bucket에 두 번 더해진다. 윈도우를 닫은 뒤 도착한 Event는 다시 볼 기회가 없다. 지연 도착 45건과 늦은 커밋 97건이 그렇게 빠졌다. 3-4. 원인 2 — 늦게 커밋된 배치가 사라진다 occurred_at 윈도우 대신 ingested_at 워터마크로 바꾸면 지연 도착은 잡힌다. 대신 비교 연산자에서 새 함정이 생긴다. 비교 깨지는 지점 실측 (그 요인만 켰을 때) ingested_at > 워터마크 먼저 찍혔거나 같은 ms로 찍힌 배치가 나중에 커밋되면 영원히 누락 늦은 커밋 −408, 같은 ms −408, 시계 오차 −30 ingested_at >= 워터마크 워터마크 시각의 행을 매 tick 다시 세어 더한다 요인 없이도 +1,500 💡 > 든 >= 든, "마지막으로 본 시각 이후만 가져와 더한다"는 구조에서는 누락과 중복 중 하나를 피할 수 없다. 문제는 연산자가 아니라 더한다는 것 자체다. 4. "Worker 대신 MV로 바꾸면?" — 세 가지 오해 여기서 떠오르는 대안이 ClickHouse Materialized View다. "집계는 ClickHouse가 알아서 하고, Worker는 결과만 옮기면 된다." count() 를 SummingMergeTree에 쌓는 MV를 붙여 같은 트래픽을 다시 흘렸다. 결과는 −273에서 −644로 더 나빴다. 오해 실제 실측 처방 1 MV는 원본 테이블을 집계한 뷰다 INSERT 블록에만 반응하는 트리거. 생성 이전 데이터는 모른다 원본 110건 중 MV에는 10건 MV를 먼저 만들고 과거는 백필 2 SummingMergeTree에는 합쳐진 한 행이 있다 백그라운드 머지 전까지 부분합 행이 여러 개다 같은 키에 7·5·3 세 행, sum() 은 15 읽을 때 항상 sum() + GROUP BY 3 원본을 지우면 집계에서도 빠진다 DELETE · DROP PARTITION은 MV로 전파되지 않는다 원본 80 → 0, 집계는 100 그대로 정정 삭제 시 집계도 재계산 오해 1 MV는 트리거다 insert> edr_events 100행 — MV 만들기 전 sql> CREATE MATERIALIZED VIEW events_per_minute_mv TO events_per_minute AS SELECT toStartOfMinute(occurred_at), endpoint_id, count() ... GROUP BY ... sql> SELECT sum(event_count) FROM events_per_minute → 0 insert> edr_events 10행 — MV 만든 뒤 sql> SELECT count() FROM edr_events → 110 sql> SELECT sum(event_count) FROM events_per_minute → 10 생성 이전 데이터는 "새로 들어온" 적이 없으니 MV는 본 적이 없다. 버그가 아니라 정의다. POPULATE 는 채우는 동안 들어온 INSERT를 놓칠 수 있어서 운영 중인 테이블에는 답이 아니다. 오해 2 읽는 방법이 틀렸다 insert> 배치 1 (7행), 배치 2 (5행), 배치 3 (3행) — 같은 1분, 같은 endpoint sql> SELECT bucket_start_at, endpoint_id, event_count FROM events_per_minute 2026-10-07 01:00:00 | 101 | 7 2026-10-07 01:00:00 | 101 | 5 2026-10-07 01:00:00 | 101 | 3 sql> SELECT sum(event_count) FROM events_per_minute → 15 이 행들을 키 기준으로 PostgreSQL에 덮어쓰면 마지막 부분합(3)만 남는다. 같은 트래픽인데 실행할 때마다 Rollup이 805건에서 1,093건 사이로 달랐다. 같은 입력에 다른 답 이 나오는 것 자체가 단서였다. 오해 3 삭제는 전파되지 않는다 insert> Endpoint 101 — 80행, Endpoint 999 — 20행 sql> ALTER TABLE edr_events DELETE WHERE endpoint_id = 999 SETTINGS mutations_sync = 2 sql> SELECT count() FROM edr_events → 80 sql> SELECT sum(event_count) FROM events_per_minute → 100 sql> ALTER TABLE edr_events DROP PARTITION '2026-10-07' sql> SELECT count() FROM edr_events → 0 sql> SELECT sum(event_count) FROM events_per_minute → 100 💡 OWLBY에서는 양날의 칼이다. Storage Lifecycle Worker는 S3로 아카이브한 날짜 파티션을 DROP PARTITION 으로 지운다. 이때 집계가 남는 건 원하는 동작 이다. 반대로 잘못 들어온 데이터를 지우는 정정 삭제라면 집계도 따로 재계산해야 한다. 추가로 알아야 할 MV의 성질 성질 오해하면 나는 사고 실측 소스가 ReplacingMergeTree여도 MV는 중복 제거 전 블록을 본다 원본은 재전송이 정리되는데 집계에는 그대로 더해진다 재전송 10건 → 원본 10, MV 20 (원본 머지 후에도 20) MV가 분 중간에 켜지면 그 분은 반쪽짜리 bucket이 된다 백필 범위를 잘못 잡으면 누락 또는 중복 정답 60 → 30 / 90 / 60 (아래) 00:59 50행, 01:00:10 30행 → 01:00:37 MV 생성 → 01:00:40 30행 [A] occurred_at < 01:00:00 백필 → 01:00 버킷 30 (누락) [B] occurred_at < 01:01:00 백필 → 01:00 버킷 90 (중복) [C] 01:00 버킷 삭제 → occurred_at < 01:01:00 백필 → 01:00 버킷 60 (정답) 경계 버킷 백필은 이 글의 출발점과 같은 문제다. 윈도우 경계를 한 번만 세는 게 어렵다. 5. 원인은 하나가 아니었다 장애 요인을 하나씩만 켜면 어떤 요인이 어떤 전략을 깨는지 갈라진다. 셀은 Rollup − 원본(+중복 / −누락)이다. 전략 경계만 resend slow same_ms late skew naive_window +240 +332 +2 / −97 +2 / −97 +240 / −45 +210 naive_watermark(>) ✅ +63 −408 −408 ✅ −30 naive_watermark(>=) +1,500 +162 +628 +1,092 +1,500 +1,470 mv(sum) −390 +40 / −378 −282 −282 −390 −390 mv_state ✅ ✅ ✅ ✅ ✅ ✅ fixed ✅ ✅ ✅ ✅ ✅ −30 → ✅ (reconcile 후) naive_window 의 skew는 +210으로 보이지만, 경계 중복 240건 안에 누락 30건이 숨어 있다. naive_watermark 는 지연 도착에는 강하지만, 늦은 커밋·같은 ms에서는 408건을 잃는다. mv(sum) 은 지연 도착·시계 오차는 잘 잡지만(INSERT 시점에 집계하므로), 설치 전 390건을 항상 잃는다. resend 열과 mv(raw) 는 ClickHouse 백그라운드 머지 타이밍에 따라 실행마다 조금씩 달라진다(예: naive_window resend +308~+332). 나머지 칸은 매번 같았다. 끝까지 맞은 두 방식의 공통점은 하나다. 더하지 않고, 다시 센다. 6. 해결 — 더하지 말고, 다시 센다 구분 조치 대가 응급 1 틀린 구간을 원본에서 강제 재집계 ( --backfill-only --backfill-hours N ) 그 구간만큼 ClickHouse 조회 부하 응급 2 (MV를 쓴 경우) 집계 테이블 조회에 GROUP BY + sum() 없다 응급 3 정정 삭제한 구간을 집계에서 지우고 재계산 재계산 동안 그 구간 조회가 흔들린다 근본 1 증분을 더하지 않는다. 바뀐 (endpoint, 분) 을 원본에서 통째로 다시 세어 교체 바뀐 bucket마다 원본 재조회 근본 2 FINAL + uniqExact(event_id) , 범위는 반열린 구간 [start, end) FINAL 비용 (dirty bucket만 읽어 완화) 근본 3 워터마크 이전 120초를 겹쳐 읽는 overlap + 주기적 reconcile reconcile 주기만큼은 틀린 값이 보일 수 있다 근본 4 ClickHouse 조회부터 PG 교체까지 advisory lock으로 직렬화 writer가 하나씩만 돈다 근본 5 MV가 꼭 필요하면 uniqExactState + "MV 생성 → 경계 버킷 삭제 → 백필" + -Merge 조회 원본의 77%를 추가 저장 OWLBY는 근본 1~4(실험의 fixed )를 택했다. overlap만으로 −30까지 줄었고, reconcile 한 번으로 0이 됐다. 남은 30건은 ingested_at 이 10분 늦게 찍힌, 시계가 틀어진 호스트의 Event였다. 6-1. 바뀐 bucket만 다시 센다 -- 1) 워터마크 - overlap 이후 들어온 행이 건드린 (endpoint, 분)을 찾는다 SELECT DISTINCT endpoint_id, toStartOfMinute(occurred_at) AS bucket FROM edr_events WHERE ingested_at >= {watermark} - INTERVAL 120 SECOND; -- 2) 그 bucket만 원본에서 통째로 다시 센다 SELECT toStartOfMinute(occurred_at) AS bucket, endpoint_id, event_type, uniqExact(event_id) AS event_count FROM edr_events FINAL WHERE occurred_at >= {lo} AND occurred_at < {hi} AND (endpoint_id, toStartOfMinute(occurred_at)) IN ({dirty_keys}) GROUP BY bucket, endpoint_id, event_type; -- 3) PostgreSQL: 한 트랜잭션 안에서 지우고 다시 넣는다 BEGIN; SELECT pg_advisory_xact_lock(hashtext('dashboard_rollup')); DELETE FROM dashboard_event_rollups r USING unnest(%s::timestamptz[], %s::bigint[]) AS k(bucket_start_at, endpoint_id) WHERE r.bucket_start_at = k.bucket_start_at AND r.endpoint_id = k.endpoint_id; INSERT INTO dashboard_event_rollups (...) VALUES (...) ON CONFLICT (bucket_start_at, endpoint_id, event_type) DO UPDATE SET event_count = EXCLUDED.event_count; -- += 가 아니다 COMMIT; 💡 event_count + EXCLUDED.event_count 가 아니라 EXCLUDED.event_count 다. 이 차이가 이 사건의 절반이다. 같은 bucket을 몇 번 다시 계산해도 결과가 같아진다(멱등). 6-2. MV를 꼭 써야 한다면 CREATE TABLE events_per_minute_state ( bucket_start_at DateTime('UTC'), endpoint_id UInt64, event_type LowCardinality(String), event_ids AggregateFunction(uniqExact, UUID) -- 개수가 아니라 ID 집합 ) ENGINE = AggregatingMergeTree ORDER BY (bucket_start_at, endpoint_id, event_type); CREATE MATERIALIZED VIEW events_per_minute_state_mv TO events_per_minute_state AS SELECT toStartOfMinute(occurred_at) AS bucket_start_at, endpoint_id, event_type, uniqExactState(event_id) AS event_ids FROM edr_events GROUP BY bucket_start_at, endpoint_id, event_type; -- MV를 먼저 만든 뒤 같은 SELECT로 백필한다 INSERT INTO events_per_minute_state SELECT ... FROM edr_events GROUP BY ...; -- 읽을 때는 항상 -Merge + GROUP BY SELECT bucket_start_at, endpoint_id, event_type, uniqExactMerge(event_ids) FROM events_per_minute_state GROUP BY bucket_start_at, endpoint_id, event_type; ID 집합이라 백필이 MV와 겹쳐도, 백필을 두 번 돌려도, 재전송이 와도 같은 답이 나온다. 실험에서 세 가지가 한꺼번에 일어났는데도 결과는 원본 고유 건수와 같은 110이었다. 6-3. 비용 비교 — 왜 OWLBY는 MV가 아니라 Worker인가 Event 100만 건(1시간, endpoint 50대), 로컬 ClickHouse 24.8, 조회 시간은 5회 중앙값이다. 방식 추가 저장 1시간 집계 조회 정확성 원본 재집계 ( FINAL + uniqExact , fixed) 없음 (원본 19.9 MiB) 77 ms ✅ 정확 uniqExactState MV (mv_state) 15.3 MiB (원본의 77%) 136 ms ✅ 정확 count() + SummingMergeTree MV 1.8 KiB 3 ms ❌ 틀림 (오해 1·3, 재전송) 정확한 MV는 ID 집합을 통째로 들고 있어서 원본만큼 무겁고, 이 규모에서는 읽기도 오히려 느렸다. 가벼운 MV는 틀린다. 그래서 OWLBY는 바뀐 bucket만 원본에서 다시 세는 Worker를 택했다. 7. 재발 방지 — "돌고 있다"가 아니라 "맞다"를 감시한다 7-1. 대사를 지표로 만든다 원인이 무엇이든, Rollup과 원본을 맞
Over the past several years, water resistance has quietly moved from a premium selling point to a line item on almost every mid-range spec sheet. IP ratings are printed on boxes, shown in keynote slides and referenced in reviews. Yet support forums tell a different story: people keep reporting muffled calls, crackling music and faint ringtones after a rainy commute or a poolside photo. The reason is that a rating protects the internals from damage, not the speaker grille from holding on to a few droplets. That gap has pushed speaker water ejection into the mainstream, from built-in features on some watches and phones to a browser-based speaker water ejector that plays tuned tones directly from a web page. This article explains how the technique works, where it helps, and where it stops being useful.  * Why "Water-Resistant" Doesn't Mean "Water-Free" * An IP68 or similar rating describes how well a device keeps water away from sensitive electronics under defined test conditions. It says nothing about how quickly the speaker opening dries afterwards. Speaker and microphone ports are deliberately open channels, because sound has to travel through them. Manufacturers cover them with fine mesh and sometimes a hydrophobic membrane, but that mesh still has openings measured in fractions of a millimetre. At that scale, water behaves differently than it does in a glass. Surface tension and capillary action allow a small droplet to bridge the gap across an opening and hold there, even if the phone is tilted or shaken. The droplet adds mass to the air column in front of the diaphragm, and if it touches the diaphragm itself, it damps the movement. The result is the sound users describe as underwater, fuzzy or distant. Several everyday situations produce this effect: a splash at the sink, a heavy rain shower, a steamy bathroom, sweat during a workout, or a quick dunk in a pool. In most of these cases the phone is perfectly healthy. It is simply carrying a bit of water in the one place it cannot easily drain on its own. * The Physics Behind Water Ejection * Speaker water ejection relies on a simple mechanical idea: if a droplet is stuck in an opening, move the surface behind it back and forth fast and hard enough, and the droplet gets pushed out. A speaker diaphragm is a lightweight membrane driven by an electromagnet. When it receives an audio signal, it vibrates at the frequency of that signal. Higher frequencies move it quickly through tiny distances; lower frequencies move it more slowly through larger distances. Water ejection tones tend to sit in the lower range, generally in the low hundreds of hertz, because large displacement is what generates the pressure needed to shove a droplet outward. Each push of the diaphragm creates a pressure pulse in the chamber behind the grille. The droplet, held in place by surface tension, is nudged forward with each cycle. Repeated many times per second, those nudges can overcome the adhesion between the water and the mesh. Small droplets are expelled, and larger ones can be broken up and pushed toward the edge of the opening, where they can evaporate or be wiped away. It is the same reason a tapping motion can free a droplet clinging to the side of a glass. The vibration reduces the effective grip of the surface, and the mechanical force tips the balance. Seen from an engineering perspective, it is a clever use of hardware that already exists in the device, with no new components required. * Hardware Features vs. Browser-Based Tools * Some devices ship with a dedicated water-eject function. Certain smartwatches, for instance, include a water lock mode that plays a tone when you finish a swim, and a handful of phones expose similar shortcuts in their settings or quick panels. These are convenient because they are tuned by the manufacturer for that specific speaker and housing. Not every phone has such a feature, however, and even among those that do, it may be hidden or limited to a particular model line. That is where web-based tools fill the gap. A browser tool uses the Web Audio API to generate a tone or frequency sweep directly through the phone's speaker, without any installation. It works on both Android and iOS, requires no permissions beyond audio playback, and can be launched in seconds. There are trade-offs. A built-in feature can control the amplifier output precisely, while a browser relies on system media volume, which varies from device to device. Browser tools also cannot run when the screen locks or the page is suspended, so you need to keep the tab active during a cycle. Still, for most users the practical difference is small, and the simplicity of opening a page and pressing play is hard to beat. Whichever route you choose, the underlying mechanism is the same. * How to Run a Water Ejection Cycle Safely * Used properly, a water ejection cycle is low risk. A few steps keep it that way. Start by wiping the exterior of the phone with a soft cloth, and remove the case so water is not trapped behind it. Hold the device with the speaker facing downward, angled slightly if possible, so gravity assists the droplets once they are loosened. Place a tissue or microfibre cloth beneath it to catch any water that comes out. Set the volume to a moderate level first. Many tools recommend maximum volume, but ramping up gradually lets you confirm that the speaker is not already damaged or producing a harsh buzz. If everything sounds normal, increase to the level the tool suggests and start the tone. Run the cycle for a short period, typically thirty seconds to a couple of minutes, then pause. Tap the phone gently against your palm with the speaker facing down, wipe the grille area, and check whether the sound has improved. Repeat in short rounds rather than one long session. Constant high-volume playback generates heat and unnecessary strain, and longer is not meaningfully better once the droplets have moved. Never use ejection tones on a phone that has been submerged in saltwater, seen significant liquid ingress, or shows other symptoms like a flickering screen or charging faults. Those situations call for powering off and professional inspection, as vibration cannot undo corrosion or short circuits. * What Sound Can't Fix * It helps to be honest about limits, because plenty of muffled speakers have nothing to do with water. Sound-based ejection moves liquid. It does not remove dust, lint, sand, pocket fluff or the greasy film that builds up on the grille over months of daily use. If you hear no change after a couple of cycles, solid debris is a more likely cause. For dry buildup, the safe approach is mechanical and gentle: a soft, dry brush, short light strokes, and a small piece of removable putty to lift particles from the surface. Avoid needles, SIM tools and compressed air, all of which can push debris further in or damage the mesh. A cloth only slightly damp with isopropyl alcohol can handle oily residue on the outside of the grille, as long as no liquid is allowed to run into the opening. Hardware faults are the third category. A worn diaphragm, a loosened connector or a failing amplifier circuit produces symptoms that mimic a clogged speaker but will not respond to either brushing or tones. If the sound is crackling at every volume level, completely silent, or accompanied by rattling, treat that as a repair issue rather than a cleaning one. Sorting the three categories — liquid, debris and hardware — is the quickest route to the right fix. * Drying Habits That Actually Help * Ejection tones are most effective when they are part of a broader drying routine. After any exposure to water, power the phone down if it behaves strangely, wipe it dry, and leave it somewhere with gentle airflow at room temperature. A small fan set to low, placed at a distance, is useful. Direct heat from hairdryers, radiators or sunny windowsills is not; high temperatures can weaken adhesives and seals designed to protect the device. The rice trick deserves a mention only to retire it. Uncooked rice does little to pull moisture from a sealed phone, and starch dust or small grains can find their way into ports. Silica gel packets, if you happen to have them, are a better option, though plain airflow and patience do most of the work. Avoid charging the phone until the charging port and speaker area feel dry. Moisture combined with current is a more serious risk than a muffled speaker. If your phone has a moisture warning on its charging port, treat it seriously and wait. Finally, build prevention into habit. Rinse off chlorinated or salty water with a quick dip in fresh water if your phone is rated for it, then dry it. Keep the phone out of steamy bathrooms when you can. And check the inside of your case now and then, since trapped moisture and grit often collect there first. * Troubleshooting Muffled Audio That Won't Go Away * If you have dried the phone, run several ejection cycles and cleaned the grille, but the sound is still dull, narrow the problem down methodically. First, test other outputs. Play audio through headphones or a Bluetooth speaker. If those sound fine, the issue is in the built-in speaker. Second, test different content: a call, a video and a music file. If only one app sounds bad, the cause may be software or a settings issue such as an equaliser profile or audio enhancement mode. Third, restart the phone and check the volume and sound settings, including any hearing-aid or mono audio features that could change output. Fourth, compare channels. Some phones have more than one speaker, usually a bottom speaker for media and a top earpiece for calls. If calls are muffled but music is fine, the earpiece mesh may be the problem and deserves the same careful brushing. If all of these checks point to hardware, contact an authorised repair provider. Check your warranty or any protection pla
이번에도 업데이트 소식으로 찾아왔어요. 일을 하다가도 '이거 추가하면 어떨까?', '이걸 이렇게 수정하면 더 낫나?', '아 맞다 그거 고쳐야 하는데' 등으로 고민하다 보면, 할 게 산더미처럼 쌓이네요. 그래서 하루하루가 쏜살같이 지나가요. 잠을 잊은 지 오래 이것만 하고 자야지, 이것만 고치고 자야지..만 백만번째. 매일 새벽에 자고 아침에 일어나면 매번 커피로 하루를 버팁니다. 그래도 너무 재밌어서 하게 돼요. 이번에는 어떤 것들이 추가되고, 개선 되었는지 설명 드릴게요. 본문 인용 댓글 이제 기록 본문의 텍스트를 선택하면, 그 부분을 댓글과 함께 남길 수 있어요. 좋은 문장이나 공감이 되는 부분들을 인용하여 쉽게 소통하실 수가 있습니다! 해당 내용을 가져와서 질문도 가능하겠네요. 😊 본문에 남긴 댓글은 본문 옆에 표시가 돼요. 또한 댓글 아이콘에 마우스를 올리면, 어떤 내용을 인용해서 남겼는지 확인하실 수 있도록 하이라이트 표시가 됩니다. 이런 본문 댓글은 전용 댓글 시트에서 모아 볼 수 있어요.(하단의 댓글 목록에서는 일반 댓글, 본문 댓글 모두 표시가 됩니다!) 하단 댓글 목록에서 댓글 속 인용문을 누르면 본문의 해당 위치로 이동해요. 꽤 공을 들여 만든 만큼 이 기능을 통해 더 많은 소통을 하고, 가입 계기 또한 되었으면 합니다~! 배지 2종 추가 새로운 배지 2종을 추가했어요. 시리즈 기능과 이번에 추가한 본문 댓글 관련 배지예요. 이번에 배지를 추가하면서, 전체적으로 배지 디자인을 한 번 더 일관성 있고 어색하지 않게 개선을 해봤어요. 또, 기존의 알림에서는 배지 획득 시 다음과 같이 표시가 됐는데요. 다른 알림 항목들과는 UI 디자인이 다소 차이가 있어서, 이 부분도 수정을 해봤어요. 이제 다른 알림 항목과 전반적으로 어색하지 않게 개선이 된 것 같아요. 라이트 모드에서는 이렇게 표시가 됩니다. 홈 피드 개편 이 부분도 굉장히 필요하다고 생각한 업데이트예요. 마음 한구석에 찝찝함이 계속 있었는데요. 드디어 마음에 드는 배치와 디자인을 찾게 되었어요. 예전 홈 화면부터 볼게요. 수정 전의 홈 화면은요 스프린트 카드와 급상승 토픽이 최상단에 위치해 있었어요. 사실상 히어로 섹션이 되어야할 핵심 화면 영역은 홈 피드였는데 말이죠. 특히, 모바일 기기같이 좁은 화면에서는 스크롤을 꽤 하고 나서야 기록이 보였죠. 현재는 홈 화면이 위 이미지처럼 표시가 됩니다. 스프린트 카드와 급상승 토픽을 오른쪽 섹션에 옮겨두었고, 핵심 콘텐츠인 기록들이 가장 먼저 눈에 띄어요. 모바일 기기에서도 이제 가장 먼저 기록이 보입니다. 소개 페이지 정비 소개 페이지 도 계속 수정 중이에요. 지금은 꽤 만족하고 있어요. 특히 시연 영상을 Remotion 까지 사용해서 다시 만들었어요. 개선점들 모바일에서 기록 이미지 업로드 이슈가 있어서 수정했어요. 작은 프로필 사진에 쓰이는 아바타의 용량을 줄였어요. 댓글의 업데이트 로직을 크게 개선했어요. 기록 에디터의 사용성을 개선했어요. 특히, 표 블록에 대한 사용자 경험을 크게 끌어올렸어요. 너무 피곤하지만 정말 재밌어요. 마음이 편하네요.