[카테고리:] DB

  • 쿼리 라우팅 설정하기: ProxySQL 활용법

    쿼리 라우팅 설정하기: ProxySQL 활용법

    ⚙️ Version

    OS: rocky 9.5

    ProxySQL 3.0.2

    백앤드 DB: MySQL 8.4.X 버전

    서비스 API 에서 붙어야하는 백앤드 DB(master[Writer/Read], ReadReplica[Read])가 두개였다. 서비스 API 에서 보내는 쿼리 CRUD에 맞게 라우팅을 해야하는 상황이었다.

    🔖 사전정보

    • Admin 포트 6032 (기본 계정 admin/admin)
    • MySQL 프록시 포트 6033
    • ProxySQL 설정은 메모리(Runtime) ↔ 디스크(SQLite, /var/lib/proxysql/proxysql.db) 레이어로 관리하고, 필요시 config 파일도 사용. Admin에서 변경 후 LOAD … TO RUNTIME; SAVE … TO DISK;가 관례.

    ⚙️ 설치 (Rocky/Alma/RHEL 8/9 계열)

    📌 3.0.2 설치 & 기동

    # 최신 릴리스 페이지에서 버전 확인
    # https://github.com/sysown/proxysql/releases
    
    # 1) wget으로 rpm 다운로드
    wget https://github.com/sysown/proxysql/releases/download/v3.0.2/proxysql-3.0.2-1-centos8.x86_64.rpm
    
    # 2) 설치
    sudo dnf install -y ./proxysql-3.0.2-1-centos8.x86_64.rpm
    
    # 3) 서비스 등록/시작
    sudo systemctl enable --now proxysql
    
    # 4) 버전 확인
    proxysql --version
    # ProxySQL version 3.0.2 ... 나와야 정상

    📌 관리자 접속 & 기본 보안

    mysql -h 127.0.0.1 -P6032 -u admin -padmin --prompt='Admin> '

    📌 백엔드(MySQL) 서버 등록 (HG10: Writer, HG20: Reader) + SSL

    -- 백앤드 서버 등록 하는 법
    DELETE FROM mysql_servers;
    INSERT INTO mysql_servers (hostgroup_id, hostname, port, use_ssl, max_connections)
    VALUES
      (10, '0.0.0.0', 3306, 1, 2000),   -- Writer
      (20, '0.0.0.0' , 3306, 1, 2000);   -- Reader
    
    LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;
    
    -- (선택) 헬스체크 튜닝
    SET mysql-monitor_username = 'monuser';
    SET mysql-monitor_password = 'monpass!';
    LOAD MYSQL VARIABLES TO RUNTIME; SAVE MYSQL VARIABLES TO DISK;
    
    -- 모니터링 계정은 백엔드 MySQL에도 생성 필요(각 노드DB에서):
    -- CREATE USER 'monuser'@'%' IDENTIFIED BY 'monpass!';
    -- GRANT USAGE, REPLICATION CLIENT ON *.* TO 'monuser'@'%';
    
    -- 연결 확인
    SELECT hostgroup_id, hostname, port, status, use_ssl FROM runtime_mysql_servers ORDER BY hostgroup_id, hostname;

    📌 애플리케이션 계정(ProxySQL 프런트 사용자) 생성

    -- 앱이 ProxySQL:6033 으로 들어올 자격
    DELETE FROM mysql_users WHERE username='app_user';
    INSERT INTO mysql_users (username,password,active,default_hostgroup,transaction_persistent)
    VALUES ('app_user','StrongAppPW!',1,10,1);
    
    -- default_hostgroup=10(master), 트랜잭션 중엔 Writer로 붙게 룰이 조정됨
    -- (필요 시 'frontend SSL'도 설정 가능)
    
    LOAD MYSQL USERS TO RUNTIME;
    SAVE MYSQL USERS TO DISK;
    
    -- 앱 쪽 DSN 예:
    -- mysql -h 127.0.0.1 -P6033 -u app_user -pStrongAppPW!

    ⚠️ 백앤드 접근 유저 등록 체크하는 법

    -- admin 에서(6032포트)
    
    SELECT username, default_hostgroup, transaction_persistent, active
        -> FROM mysql_users
        -> ORDER BY username;

    📌 글로벌 변수 (정규식/다이제스트/멀티플렉싱)

    SET mysql-query_processor_regex = 1;
    SET mysql-query_digests         = true;
    SET mysql-multiplexing          = 1;
    
    LOAD MYSQL VARIABLES TO RUNTIME; SAVE MYSQL VARIABLES TO DISK;

    ⚙️ 쿼리 라우팅 룰

    • 읽기(Read 전용 DB)
    • 쓰기(Writer 전용 DB)
    • 트랜잭션 중 쓰기 작업 발생(Reader DB ➡️ Writer DB) 이후 트랜잭션이 끝날때 까지 이후 리드 작업(Writer DB)
    DELETE FROM mysql_query_rules;
    DELETE FROM mysql_query_rules_fast_routing;
    
    -- 10) WRITE류 → HG10, flag=2 + next_query_flagIN=2 (다음 쿼리에 플래그 확실히 전달)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,flagOUT,next_query_flagIN,comment,multiplex)
    VALUES
    (10,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(INSERT|UPDATE|DELETE|REPLACE|MERGE)','CASELESS',
     10,2,2,'WRITE → HG10, flag=2 + next_query_flagIN=2',0);
    
    -- 20/21) TX 시작/종료 → Writer (TX 중 SELECT 강제용 flag=1/0)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,flagOUT,comment,multiplex)
    VALUES
    (20,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(BEGIN|START[[:space:]]+TRANSACTION)','CASELESS',
     10,1,'BEGIN/START → HG10, flag=1',0),
    (21,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(COMMIT|ROLLBACK)','CASELESS',
     10,0,'COMMIT/ROLLBACK → HG10, flag reset',0);
    
    -- 30) SELECT ... FOR UPDATE/SHARE → Writer
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,comment,multiplex)
    VALUES
    (30,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(SELECT|WITH)(/\\*.*?\\*/|[[:space:]])*.*FOR[[:space:]]+(UPDATE|SHARE)','CASELESS',
     10,'SELECT/CTE ... FOR UPDATE|SHARE → HG10',0);
    
    -- 40) TX 중 READ(SELECT/WITH) → Writer (flagIN=1)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,flagIN,comment,multiplex)
    VALUES
    (40,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(SELECT|WITH)(/\\*.*?\\*/|[[:space:]])*','CASELESS',
     10,1,'flag=1(TX) READ → HG10',0);
    
    -- 41) 쓰기 직후 ‘첫 READ(SELECT/WITH)’ 1회 → Writer & reset (flagIN=2)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,flagIN,flagOUT,comment,multiplex)
    VALUES
    (41,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(SELECT|WITH)(/\\*.*?\\*/|[[:space:]])*','CASELESS',
     10,2,0,'flag=2 → next READ(SELECT/WITH) is HG10 & reset',0);
    
    -- 42) 쓰기 직후 SELECT 전에 끼는 임의 쿼리(SET/SHOW/USE 등) → Writer (flag 유지)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,flagIN,flagOUT,comment,multiplex)
    VALUES
    (42,1,1,'^','CASELESS',
     10,2,2,'flag=2 → non-SELECT goes HG10 (keep flag=2)',0);
    
    -- 50) 일반 READ(SELECT/WITH) → HG20 (공백/주석 유무 무관)
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,re_modifiers,
     destination_hostgroup,comment)
    VALUES
    (50,1,1,'^[[:space:]]*(/\\*.*?\\*/[[:space:]]*)*(SELECT|WITH)(/\\*.*?\\*/|[[:space:]])*','CASELESS',
     20,'READ(SELECT/CTE) → HG20');
    
    -- 90) 폴백 → HG10
    INSERT INTO mysql_query_rules
    (rule_id,active,apply,match_pattern,destination_hostgroup,comment)
    VALUES
    (90,1,1,'.*',10,'fallback → HG10');
    
    -- 적용/저장
    LOAD MYSQL QUERY RULES TO RUNTIME;
    SAVE MYSQL QUERY RULES TO DISK;
    
    -- (확인) 런타임 상태
    SELECT rule_id, active, apply, match_pattern, re_modifiers,
           flagIN, flagOUT, next_query_flagIN, multiplex, destination_hostgroup, comment
    FROM runtime_mysql_query_rules
    ORDER BY rule_id;

    🔖 검증 체크리스트(Port: 6033)

    -- 기본 Reader
    SELECT @@read_only AS ro0, @@hostname AS h0;
    
    -- 쓰기 → Writer (10)
    INSERT INTO test_rw_split VALUES (1) ON DUPLICATE KEY UPDATE id=id;
    
    -- 첫 READ → Writer & reset (41)
    SELECT@@read_only AS ro1, @@hostname AS h1;
    
    -- 다음 READ → Reader (50)
    SELECT@@read_only AS ro2, @@hostname AS h2;
    
    -- 트랜잭션 중 READ → Writer (40)
    BEGIN;
    SELECT@@read_only AS ro_tx, @@hostname AS h_tx;
    COMMIT;
    SELECT@@read_only AS ro_after, @@hostname AS h_after;

    관리자 포트에서 확인(6032):

    SELECT rule_id, hits FROM stats_mysql_query_rules ORDER BY rule_id;
    -- 기대: 10 → (중간 쿼리 있었으면 42) → 41 → 50,  그리고 TX 경로는 20 → 40 → 21 → 50

    🔗 운영 체크리스트

    • 전역 멀티플렉싱: 운영 기본은 1(ON) — 우리가 확인 끝낸 동작은 이 전제에서 정상 동작
    • HG20 리더의 ConnOK 증가 확인:
    SELECT hostgroup, srv_host, status, ConnOK, ConnERR
    FROM stats_mysql_connection_pool ORDER BY hostgroup, srv_host;
  • DB 실행계획 차이: 원인 및 해결책

    DB 실행계획 차이: 원인 및 해결책

    🚨 문제 상황 소개

    • 개발 DB와 운영 DB, 같은 버전/같은 테이블/같은 쿼리인데 실행계획이 달라졌다.
    • 한쪽은 인덱스를 타고, 다른 한쪽은 풀스캔 + filesort 발생
    • 원인 추적 과정 간단히 설명 (EXPLAIN 결과 비교)

    🧱 실행계획 차이 원인

    • 옵티마이저는 비용 기반(cost-based) 으로 플랜을 선택.
    • 이때 “선택도(selectivity)” 추정이 중요.
    • 그런데 추정 방식이 통계값만 보느냐, 실제 인덱스를 내려가 보느냐에 따라 결과가 달라짐.

    ⚠️ 인덱스 다이브(Index Dive)란?

    • 개념: 옵티마이저가 실제 인덱스 레벨에서 값 분포를 확인해 정확한 선택도를 계산하는 과정.
    • 장점: 더 정확한 실행계획.
    • 단점: 옵티마이저가 실행계획 계산에 시간이 조금 더 걸림.

    ⚙️ EQ_RANGE_INDEX_DIVE_LIMIT 파라미터

    • 역할: 인덱스 다이브를 몇 개 값까지 허용할지 결정.
    • 0 → 인덱스 다이브 안 함 → 통계 의존 → 잘못된 플랜 위험 ↑
    • 200(기본) → 200개 값까지 다이브 → 더 정확한 플랜 선택
    • 왜 운영 DB만 “0”으로 되어 있었는지, 확인 과정 설명.
    ### 해결방법
    
    -- 현재 값 확인
    SHOW VARIABLES LIKE 'eq_range_index_dive_limit';
    
    -- 세션/글로벌에서 조정
    SET SESSION eq_range_index_dive_limit = 200;
    SET GLOBAL eq_range_index_dive_limit = 200;
    
    -- 영구 적용(MySQL 8 이상, MariaDB 10.3.30 version)
    SET PERSIST eq_range_index_dive_limit = 200;

    🔖 정리

    • 실행계획이 달라질 수 있는 이유는 데이터 분포 차이만 있는 게 아니라, 옵티마이저 파라미터 값 차이도 있다.
    • 운영/개발 환경 비교할 때는 SHOW VARIABLES 로 변수 값부터 확인.
    • 실행계획 이슈 정리 체크리스트:
      1. EXPLAIN 플랜 비교
      2. SHOW INDEX로 카디널리티 확인
      3. 관련 옵티마이저 변수(eq_range_index_dive_limit, innodb_stats_*, optimizer_switch 등) 확인
  • MariaDB ➡️ MySQL Migration: mydumper & myloader 사용법 정리(스키마 제외, 데이터만)

    MariaDB ➡️ MySQL Migration: mydumper & myloader 사용법 정리(스키마 제외, 데이터만)

    📌 개요

    이 글에서는 MySQL에서 특정 데이터베이스의 데이터만 덤프하고, 다른 서버에 복원하는 과정을 mydumper / myloader 툴로 설명합니다.

    • ✅ 병렬 덤프 및 복원 지원
    • ✅ 데이터만 추출 및 복원
    • ✅ 병렬 덤프 및 복원 지원

    🧱 설치

    🧤 ubuntu

    sudo apt update
    sudo apt install mydumper -y

    🧤 데이터 덤프 (Source DB 에서)

    mydumper \
      -h [DB-IP] \
      -u [User] \
      -p ['password'] \
      -B [DataBase_name] \
      --no-schemas \
      --outputdir /home/ubuntu/[output_dir] \
      --threads 4

    🔖 옵션 설명

    옵션설명
    -hDB 서버 주소
    -u, -p접속 계정 정보
    -B백업할 데이터베이스 이름
    –no-schemas스키마 없이 데이터만 덤프
    –outputdir덤프 저장 디렉토리
    –threads병렬 처리할 스레드 수

    🧤 데이터복원 (Target DB에서)

    ⚙️ FK 제약조건 끄고 복원

    myloader \
      -h [DB-ip] \
      -u [User] \
      -p ['password'] \
      -B [DataBase_name] \
      --directory /home/ubuntu/[output_dir] \
      --disable-keys \
      --threads 4 \
      --verbose

    ⚙️ FK 제약조건 유지하며 복원

    myloader \
      -h [DB-ip] \
      -u [User] \
      -p ['password'] \
      -B [DataBase_name] \
      --directory /home/ubuntu/[output_dir] \
      --threads 4 \
      --verbose

    🔖 옵션 설명

    옵션설명
    –directory덤프 디렉토리 지정
    –disable-keysforeign key 체크 비활성화 (성능 향상)
    –verbose자세한 로그 출력

    📌 TIP

    • ❗ -v는 유효하지 않음 → –verbose 사용
    • ❗ –no-schemas를 빼먹으면 DDL까지 같이 덤프됨
    • ❗ mydumper/myloader 버전이 서로 다르면 에러 발생 가능


  • PostgreSQL DB Dump 방식으로 다운그레이드 가이드

    PostgreSQL DB Dump 방식으로 다운그레이드 가이드

    ✅ 1단계: PostgreSQL 17 클라이언트 설치

    sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm
    sudo dnf -qy module disable postgresql
    sudo dnf install -y postgresql17

    ✅ 2단계: 백업 (pg_dump 사용) – 17버전

    export PGPASSWORD='password'
    
    /usr/pgsql-17/bin/pg_dump \
      -h 192.168.3.72 \
      -p 5432 \
      -U DB-User \
      -d NineMemos_Dev_BC_01 \
      -Fc \
      -v \
      -f ~/NineMemos_Dev_BC_01_pg17.dump

    ✅ -Fc 뜻

    옵션설명
    -Fpg_dump의 output format(출력 형식) 지정 옵션
    c**custom format (커스텀 형식)**의 약자입니다.

    ✅ 왜 Fc를 써?


    커스텀 형식은 다음과 같은 장점이 있어서 실무에서 가장 많이 사용되는 방식입니다.

    장점설명
    ✅ pg_restore로 복원 가능백업 중 일부 테이블만 복원하거나 병렬 복원(–jobs) 가능
    ✅ 압축 지원기본적으로 압축된 상태로 저장됨
    ✅ 유연한 복원특정 스키마, 테이블, 함수만 선택적으로 복원 가능

    ✅ 3단계: PostgreSQL 14 클라이언트 설치

    sudo dnf install -y postgresql14

    ✅ 4단계: 복원 (pg_restore 사용)

    백업을 17버전으로 했다면, 복원도 17버전으로 하는게 좋다(권장)

    export PGPASSWORD='password'
    
    /usr/pgsql-14/bin/pg_restore \
      -h [14.17-RDS-엔드포인트] \
      -p 5432 \
      -U DB-User \
      -d NineMemos_Dev_BC_01 \
      ~/NineMemos_Dev_BC_01_pg17.dump
  • MariaDB Memory 관련 개념

    MariaDB Memory 관련 개념

    ■ MariaDB Memory 종류

    – MariaDB Memory는 두가지로 분류가된다. 모든 세션이 공유하고 사용하는 Global Memory 영역과 각각의 세션들별로 사용되는 Session Memory영역이 있다.

    ■ Global Memory 영역

    DB가 최초 기동되었을 때에는 메모리를 최소한만 사용하다가 설정된 값 까지 증가하며 증가한 이후에는 “메모리를 반환하지 않고” 설정 된 값 이내에서 계속 사용됩니다.

    (오라클의 경우 DB기동시 설정된 값 만큼 메모리를 할당 받고 올라가는 반면 Mariadb 는 기동시 설정된 메모리 값만큼 할당 받는것이 아닌 설정된 값 만큼 서서히 증가하게 됩니다)

    1) Innodb_buffer_pool_size

    – 디스크에서 데이터를 메모리에 캐싱함과 동시에 데이터의 변경을 버퍼링하는 역할을 수행한다. 일반적으로 전체 메모리의 50% ~ 80%까지 설정하며 낮은 값부터 조금씩 크기를 올려가며 적절한 값을 찾는 것이 것이 좋다.

    2) Key_buffer_size

    – MyISAM의 키 버퍼는 인덱스를 메모리에 저장하는 버퍼의 크기이다. 인덱스만 캐시하기 때문에 InnoDB의 버퍼 풀만큼 할당해서는 안된다. MyISAM 테이블을 전혀 사용하지 않는 경우 64K와 같이 매우 낮은 값으로 설정할 수 있습니다.

    3) innodb_log_buffer_size

    – InnoDB가 디스크의 로그 파일에 로그를 기록하는 데 사용하는 버퍼의 크기입니다. 이 값을 늘리면 커밋하기 전에 디스크 I/O를 수행할 필요 없이 더 큰 트랜잭션을 실행할 수 있습니다.

    4) tmp_table_size

    – 메모리에 생성되는 임시 테이블의 최대 크기를 설정한다.

    → Global 메모리 = Innodb_buffer_pool_size + Key_buffer_size + innodb_log_buffer_size + tmp_table_size

    ■ Session Memory 영역

    Mariadb session 별로 사용되어지는 메모리 공간으로 Max connection 설정 값과 관련이 있습니다.

    (커넥션을 맺고만 있을 경우에는 커넥션에 필요한 최소한의 메모리만 사용되어지며 조인, 정렬 등이 필요할 경우에는 아래 설정된 값만큼을 메모리에서 사용하게 됩니다.

    때문에 정확한 예측값은 동시에 Active한 세션 수 이지만 기본적으로는 Max connection 수 만큼 동시 사용 세션이 수행될 수 있기에 아래 공식으로 계산되어 집니다.)

    1) sort_buffer_size

    – 정렬을 수행하는 각 세션은 이 양의 메모리가 있는 버퍼를 할당합니다. 16k는 권장되는 최소값입니다.

    2) read_buffer_size

    – sequential scan를 수행하는 각 스레드는 스캔된 각 테이블에 대해 이 크기의 버퍼를 바이트 단위로 할당한다. ORDER BY 에서 사용된다.

    3) read_rnd_buffer_size

    – 키 정렬 후 정렬된 순서 로 MyISAM 테이블에서 행을 읽을 때 사용되는 버퍼의 크기(바이트)입니다

    4) join_buffer_size

    – 인덱스를 사용할 수 없고 대신 전체 테이블 스캔을 수행하는 쿼리에 사용되는 버퍼의 최소 크기(바이트)입니다. 조인은 항상 최소 크기를 할당하므로 메모리 문제를 인식하고 있지만 인덱스를 추가할 때 더 빠른 전체 조인을 얻기 위해 늘리는 것은 불가능합니다.

    5) thread_stack

    – 각 스레드의 스택 크기입니다. 너무 작게 설정하면 저장 프로시저의 재귀 깊이와 서버가 메모리에서 처리할 수 있는 SQL 문의 복잡성을 제한합니다

    6) binlog_cache_size

    – 바이너리 로그가 활성화되어 경우,이 변수가 트랜잭션 중에 바이너리 로그 변경 기록을 보유 캐시, 당 연결, 바이트 크기를 결정합니다.

    7) Max connection

    – 최대 동시 클라이언트 연결 수입니다

    → Session 메모리 = (sort_buffer_size + read_buffer_size + read_rnd_buffer_size + join_buffer_size + thread_stack + binlog_cache_size) x Max connection

    🔖 Reference

    https://yunhyeonglee.tistory.com/29

  • MongoDB에서 Oplog 사이즈 확인하는 법

    MongoDB에서 Oplog 사이즈 확인하는 법

    MongoDB에서 Oplog 사이즈 확인하는 법

    MongoDB의 oplog(Operation Log, 작업 로그)는 MongoDB 복제(replication)에서 사용되는 특별한 컬렉션으로, 변경 사항을 기록하는 역할.

    📦 oplog란?

    • oplog(rs.oplog)는 MongoDB 복제 세트(replica set)에서 Primary 노드의 데이터 변경 사항을 기록하는 컬렉션입니다.

    • Secondary 노드는 Primary의 oplog를 읽고 동일한 작업을 수행하여 데이터를 동기화함.

    • 컬렉션 이름: local.oplog.rs

    • Capped Collection(고정 크기 컬렉션)으로 동작하며, 일정 크기가 차면 가장 오래된 데이터부터 삭제!

    📦 oplog의 동작 방식

    1. Primary 노드에서 변경 발생

    • insert, update, delete 작업이 수행될 때, oplog에 기록됨.

    2. Secondary 노드가 oplog를 읽음

    • Secondary 노드는 Primary의 oplog를 주기적으로 읽어 적용함.

    3. 동기화 유지

    • Primary에서 발생한 변경 사항을 Secondary가 동일하게 수행하여 데이터 일관성을 유지.

    📦 oplog 확인 방법

    MongoDB에서 oplog를 직접 확인하려면 아래 명령을 실행

    use local
    db.oplog.rs.find().limit(10).pretty()
    • local 데이터베이스에 oplog.rs 컬렉션이 저장.
    • 위 명령을 실행하면 최근 10개의 oplog 항목을 확인가능.

    📦 oplog 항목 예시

    {
       "ts" : Timestamp(1712432874, 1),
       "t" : NumberLong(2),
       "h" : NumberLong("890273918273918273"),
       "v" : 2,
       "op" : "i",
       "ns" : "mydb.users",
       "o" : {
           "_id" : ObjectId("65f3b69ad2b3e6b5d3a6f6d1"),
           "name" : "Alice",
           "age" : 28
       }
    }
    • “op”: 작업 유형 (“i”: insert, “u”: update, “d”: delete)
    • “ns”: 네임스페이스(데이터베이스 및 컬렉션 정보)
    • “o”: 변경된 문서 내용.

    📦 oplog의 크기 확인 및 조정

    • oplog의 크기를 확인
    db.getReplicationInfo()
    • oplog 크기를 늘리려면 (예: 10GB)
    db.adminCommand({ replSetResizeOplog: 1, size: 10240 })

    🚀 정리.

    • oplog는 MongoDB 복제(replication)에서 Primary의 변경 사항을 기록하는 로그
    • Secondary 노드는 oplog를 읽고 동일한 작업을 수행하여 데이터 일관성을 유지
    • oplog 크기가 고정되어 있으며, 오래된 로그는 자동으로 삭제
  • MongoDB 설치한 OS 에서 “Too many open files” 에러 발생 시 해결 가이드

    MongoDB 설치한 OS 에서 “Too many open files” 에러 발생 시 해결 가이드

    Linux 서버(여기선 AWS EC2)를 운영하다보면 어제만해도 잘 돌아가던 서버 어플리케이션이 “Too many open files” 에러를 뱉고 죽는 현상이 발생함.

    이런 오류가 생기는 이유는 프로세스가 OS에 요청할수 있는 리소스의 개수/양 (여기서는 NOFILE 이라 불리는 최대 Open 가능한 파일 개수)에 Limit가 있고, 프로세스가 그 제한을 넘었기 때문이다.

    해결방법

    각 프로세스의 Limit는 그 프로세스가 실행되는 계정의 Limit를 바탕으로 만들어지므로 프로세스의 Limit를 올려준다고 해서 문제가 해결되진 않는다. 계정과 프로세스 모두 변경해 주어야 그 프로세스를 다시 시작했을때에도 같은 문제가 생기지 않음.

    $ ulimit -Hn
    4096
    $ ulimit -Sn
    1024

    Limit는 Soft와 Hard 2가지가 있는데 Soft Limit은 non-root 계정에서도 설정 가능하며 일시적으로 이를 넘어도 시스템 상에서 경고 이메일만 보낼뿐 큰 문제가 되지 않지만 Hard Limit는 root 계정에서만 세팅이 가능하고 절대로 넘을수 없는 Limit이다. Too many open files가 계속 발생하고 아예 기능이 동작하지 않을 경우 프로세스에 기본 할당된 Hard Limit를 넘었다고 볼수 있음.

    root 계정으로 /etc/security/limits.conf에 다음 내용을 추가.

    * hard nofile 500000
    * soft nofile 500000
    root hard nofile 500000
    root soft nofile 500000
    $ cat /proc/sys/fs/file-max

    계정에 할당된 Limit을 변경했다면 터미널을 로그아웃 했다가 다시 로그인 한 후에 앞에서 실행한 ulimit으로 값이 변경되어 있는지 확인해야 한다.

    이제 프로세스에 대한 Limit을 올려줄 차례다. 프로세스를 죽였다가 다시 살리면 되지만 운영환경에선 프로세스를 죽이면 안되는 때가 있음.

    방법

    $ ps -ef | grep foobar
    1234
    $ prlimit --nofile --output RESOURCE,SOFT,HARD --pid 1234
    $ prlimit --nofile=500000 --pid=1234

    이렇게 변경을 하면 Soft Limit과 Hard Limit이 동일하게 500000으로 맞춰진다. 다시 이전 command를 실행해서 더블체크.

  • MongoDB 에서 Replica Set Secondary Node 동기화 시간 확인하는 방법

    MongoDB 에서 Replica Set Secondary Node 동기화 시간 확인하는 방법

    MongoDB 에서 Replica Set Secondary Node 동기화 시간 확인하는 방법

    몽고디비 레플리카 셋으로 구축 되어 있을 때 secondary node가 primary node를 어느정도 동기화 하고 있는지, 알아 볼 수 있다.

    use admin
    db.auth({...})
    
    rs.printSecondaryReplicationInfo()

    나오는 시간이 Primary Node에 대해 동기화 하는 정도까지의 지연시간이다.

  • MongoDB DB 다운 시 재가동 방법

    MongoDB DB 다운 시 재가동 방법

    MongoDB DB 다운 시 재가동 방법

    mongoDB가 어떤 이유로 인해 DB 프로세스가 다운될 수 있다. 이유는 log를 살펴봐야한다. 보통 /var/log/mongodb/ 쪽에 기록되어 있다. 실제로 root로 접속 후, mongo가 죽었는지 확인해봐야 한다.

    mongo
    • 다시 살리는 code
    # root 권한으로.
    mongod --config /etc/mongod.conf

    만약에 다시 살리지 못하면, 백업 시켜둔 파일을 교체해서 다시 기동해야 한다.

    /var/lib/ 폴더를 봐야 한다.

    rm -rf mongo # 기존 파일 지우기.
    mv mongo.203.20240505 mongo # 파일 교체.

    그 다음, 재기동 해보자

    # root 권한으로.
    mongod --config /etc/mongod.conf

    그럼, 보통 재기동이 된다.

    이제, db단에서 레플리카셋 서버끼리 얼마나 동기화가 되었고, 지연이 얼마나 되는지 알아보자.

    ### 복제 지연 확인
    o 현재 어디까지 동기화를 진행했으며 얼마나 지연이 발생하는지 파악.
       PRIMARY> rs.printSecondaryReplicationInfo()
  • MongoDB 에서 wiredTiger engine 파일 찾는 법

    MongoDB 에서 wiredTiger engine 파일 찾는 법

    MongoDB 에서 wiredTiger engine 파일 찾는 법

    찾게 된 이유 : 몽고디비에서 CRUD를 할때 checksum이 match가 되지 않는 오류 발생 => 몽고디비 다운으로 이어졌다.

    log를 찾아보면, collection-218–4990451227493674325.wt 식의 파일 이름들이 보임.

    해당 wt파일이 어떤 database의 collection인지 찾아 볼 수 있음.

    from pymongo import MongoClient
    conn = MongoClient(host='host', port=port, username='username', password='password')
    databases = conn.list_database_names()
    # 몽고디비에서 wt파일 찾는 법.
    for db_name in databases:
        db = conn[db_name]
        collections = db.list_collection_names()
        for coll_name in collections:
            stats = db.command("collstats", coll_name)
            if 'wiredTiger' in stats and 'uri' in stats['wiredTiger']:
                uri = stats['wiredTiger']['uri']
                if "~~파일이름~~" in uri:
                    print(f"Found in {db_name}.{coll_name}")

    이렇게 하면 wt파일이 어느 database의 collection인지 찾아볼 수 있다.