레이블이 DevOps인 게시물을 표시합니다. 모든 게시물 표시
레이블이 DevOps인 게시물을 표시합니다. 모든 게시물 표시

수요일

Redis maxmemory 설정 — OOM 에러와 eviction policy 완벽 가이드

증상 — 에러 메시지

OOM command not allowed when used memory > 'maxmemory'.
ERR command not allowed when used memory > 'maxmemory'

캐시 서버로 Redis를 운영하다 보면 어느 날 갑자기 쓰기 명령이 전부 실패하는 상황을 만납니다. OOM(Out of Memory) 에러는 Redis가 설정된 최대 메모리 한도에 도달했을 때 발생하며, 기본 정책(noeviction)에서는 새 데이터를 추가할 수 없습니다.

원인

Redis는 기본적으로 maxmemory 제한이 없어서 서버 물리 메모리를 모두 소모할 수 있습니다. 운영 환경에서 명시적으로 제한을 걸면, 그 한도를 넘는 순간 maxmemory-policy 설정에 따라 동작이 결정됩니다. 기본값인 noeviction은 메모리가 가득 차면 쓰기 에러를 반환합니다.

해결방법

1. maxmemory 및 정책 설정

# redis.conf 파일 수정
maxmemory 2gb
maxmemory-policy allkeys-lru

# 또는 런타임에 즉시 적용 (재시작 불필요)
redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru

# 설정 확인
redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET maxmemory-policy

2. Eviction Policy 종류와 용도

# noeviction (기본): 메모리 초과 시 쓰기 에러 반환
# allkeys-lru  : 모든 키 중 LRU 순서로 삭제 — 순수 캐시에 권장
# volatile-lru : TTL 있는 키 중 LRU 순서로 삭제 — 혼합 환경
# allkeys-lfu  : 사용 빈도 낮은 키 삭제 — Redis 4.0+
# volatile-ttl : TTL 짧은 키부터 삭제 — 세션 저장소
# allkeys-random: 무작위 삭제

3. 메모리 사용량 모니터링

redis-cli INFO memory
# 주요 항목: used_memory_human, maxmemory_human, mem_fragmentation_ratio

redis-cli DBSIZE                  # 전체 키 개수
redis-cli MEMORY USAGE mykey      # 특정 키 메모리 크기(bytes)

4. Spring Boot에서 Redis 메모리 효율화

@Bean
public RedisCacheConfiguration cacheConfiguration() {
    return RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(30))   // TTL 필수 지정
        .disableCachingNullValues()          // null 캐싱 방지
        .serializeValuesWith(
            RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer())
        );
}

정책 선택 가이드

사용 목적권장 정책이유
순수 캐시 (세션 없음)allkeys-lru모든 키를 대상으로 LRU 교체, 가장 일반적
캐시 + 영구 데이터 혼합volatile-lruTTL 있는 캐시 키만 삭제, 영구 키 보존
세션 저장소volatile-ttl만료 임박 세션 먼저 삭제
쓰기 보장 필수noeviction메모리 부족 시 에러 반환, 데이터 손실 없음
접근 빈도 편중 심함allkeys-lfu자주 쓰는 키 보존, Redis 4.0 이상

핵심 요약

  • maxmemory와 maxmemory-policy는 항상 함께 설정한다.
  • 캐시 전용 서버라면 allkeys-lru가 기본 선택지다.
  • Spring Boot에서는 entryTtl로 TTL을 반드시 지정해 키가 무한정 쌓이지 않게 한다.
  • mem_fragmentation_ratio가 1.5 이상이면 MEMORY PURGE 또는 재시작을 고려한다.

금요일

Nginx 502 Bad Gateway 완벽 해결 가이드 — upstream 연결 실패 원인과 해결

🔍 검색 키워드: Nginx 502 Bad Gateway 해결, Nginx upstream 502 에러, upstream connect() failed, Nginx 리버스 프록시 502, upstream timed out 해결

증상: 502 Bad Gateway 에러 발생

Nginx를 리버스 프록시로 사용할 때 클라이언트가 갑자기 502 Bad Gateway 에러를 마주치는 경우가 있습니다. 브라우저에는 아무 설명도 없고, Nginx 에러 로그를 보면 이런 메시지가 남습니다:

2026/09/18 09:12:34 [error] 12345#12345: *1 connect() failed (111: Connection refused) while connecting to upstream, client: 1.2.3.4, server: example.com, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8080/", host: "example.com"

2026/09/18 09:13:01 [error] 12345#12345: *2 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 1.2.3.4, upstream: "http://127.0.0.1:8080/"

원인: upstream 서버 연결 실패

Nginx 502 에러의 원인은 크게 세 가지입니다:

  • upstream 서버가 죽어 있음 — WAS(Spring Boot, Node.js 등)가 다운됨
  • upstream 응답 시간 초과 — 서버는 살아있지만 너무 느리게 응답
  • SELinux/방화벽 차단 — 네트워크 정책이 연결을 막음

먼저 어떤 케이스인지 확인해야 합니다:

# upstream 서버 포트 확인
ss -tlnp | grep 8080

# 서비스 상태 확인
systemctl status myapp

# Nginx 설정 문법 확인
nginx -t

해결 방법

케이스 1: upstream 서버가 꺼진 경우 → 재시작

# systemd 서비스라면
sudo systemctl restart myapp

# nohup으로 띄운 Spring Boot라면
nohup java -jar /opt/myapp/app.jar > /var/log/myapp.log 2>&1 &

케이스 2: 응답 시간 초과 → 타임아웃 설정 조정

Nginx의 기본 proxy_read_timeout은 60초입니다. 배치 처리나 파일 업로드처럼 오래 걸리는 요청은 이 값을 늘려야 합니다:

server {
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_connect_timeout 10s;
        proxy_send_timeout 300s;
        proxy_read_timeout 300s;
    }
}

설정 변경 후 반드시 reload:

sudo nginx -s reload

케이스 3: SELinux가 연결 차단 → 정책 허용

CentOS/RHEL 계열에서 SELinux가 활성화된 경우 Nginx가 upstream에 연결하지 못할 수 있습니다:

# SELinux 상태 확인
getenforce

# Nginx → upstream 네트워크 연결 허용
sudo setsebool -P httpd_can_network_connect on

케이스 4: upstream 이중화로 장애 대응

단일 upstream이 죽으면 502가 납니다. 백업 서버를 설정해두면 자동 전환됩니다:

upstream backend {
    server 127.0.0.1:8080;
    server 127.0.0.1:8081 backup;
}

server {
    location / {
        proxy_pass http://backend;
        proxy_next_upstream error timeout http_502;
    }
}

에러 유형별 빠른 참고표

에러 로그 키워드 원인 해결
Connection refused (111) upstream 프로세스 다운 서비스 재시작
upstream timed out (110) 응답 지연 / 타임아웃 proxy_read_timeout 증가
Permission denied (13) SELinux/방화벽 차단 setsebool 또는 방화벽 해제
no live upstreams while connecting upstream 전체 다운 백업 서버 추가

Nginx 502는 대부분 upstream 프로세스 상태 확인 → 타임아웃 튜닝 → SELinux 정책 순서로 해결됩니다. 에러 로그의 키워드를 보고 케이스를 특정하면 대부분 10분 안에 잡을 수 있습니다.

Nginx upstream 502 에러 완벽 해결 가이드

 🔍 검색 키워드: Nginx upstream 502, Nginx Bad Gateway, Nginx 리버스 프록시 에러, Nginx 서버 다운


Nginx upstream 502 에러 완벽 해결 가이드


증상: 502 Bad Gateway

브라우저에서 사이트에 접속했는데 갑자기 502 Bad Gateway 에러가 뜨나요?


502 Bad Gateway

nginx/1.24.0


이것이 upstream 502 에러입니다. Nginx가 백엔드 서버(upstream)로부터 유효한 응답을 받지 못했을 때 발생합니다.


원인 분석


원인 1: 백엔드 애플리케이션 서버 다운

가장 흔한 원인입니다. Node.js, PHP-FPM, Gunicorn 등 실제 애플리케이션 서버가 죽어있거나 재시작 중인 경우입니다.


원인 2: upstream 타임아웃

백엔드 서버 응답이 너무 느려서 Nginx가 설정된 타임아웃 시간 안에 응답을 받지 못하는 경우입니다.


원인 3: 소켓/포트 연결 실패

Nginx 설정의 upstream 주소(포트, 소켓 경로)가 실제 백엔드 서버와 일치하지 않는 경우입니다.


원인 4: 백엔드 서버의 버퍼 크기 초과

응답 헤더나 바디가 Nginx의 기본 버퍼 크기를 초과하면 502가 발생할 수 있습니다.


해결 방법


방법 1: 백엔드 서버 상태 확인

가장 먼저 애플리케이션 서버가 실제로 살아있는지 확인합니다.


systemctl status my-app

또는

pm2 status

또는

docker ps


프로세스가 죽어있다면 재시작하고, 재시작 후에도 계속 죽는다면 애플리케이션 로그를 확인해야 합니다.


방법 2: Nginx 에러 로그 확인

정확한 원인은 Nginx 에러 로그에서 확인할 수 있습니다.


tail -f /var/log/nginx/error.log


connect() failed (111: Connection refused) 메시지가 보이면 백엔드 서버 연결 자체가 안 되는 것이고, upstream timed out 메시지가 보이면 타임아웃 문제입니다.


방법 3: upstream 설정 확인 및 수정

nginx.conf 또는 사이트 설정 파일에서 upstream 블록을 확인합니다.


upstream backend {

    server 127.0.0.1:3000;

}


server {

    location / {

        proxy_pass http://backend;

    }

}


포트 번호가 실제 애플리케이션이 리스닝하는 포트와 일치하는지 반드시 확인합니다.


방법 4: 타임아웃 값 조정

백엔드 처리가 원래 느린 작업(대용량 파일 업로드, 무거운 연산)이라면 타임아웃을 늘려줍니다.


proxy_connect_timeout 60s;

proxy_send_timeout 60s;

proxy_read_timeout 60s;


단, 무작정 늘리기보다 애플리케이션 자체의 응답 속도를 개선하는 것이 근본적인 해결책입니다.


방법 5: 버퍼 크기 조정

헤더/바디 크기 초과 문제라면 버퍼 설정을 늘립니다.


proxy_buffer_size 16k;

proxy_buffers 4 32k;

proxy_busy_buffers_size 64k;


방법 6: 헬스체크 및 자동 재시작 설정

재발 방지를 위해 systemd나 pm2, docker의 헬스체크와 자동 재시작 설정을 구성해두면 서버 다운으로 인한 502를 최소화할 수 있습니다.


정리

- 502는 Nginx가 백엔드로부터 유효한 응답을 못 받았다는 의미

- 에러 로그(error.log)를 먼저 확인하는 것이 진단의 시작

- Connection refused는 서버 다운, timed out은 응답 지연 문제

- upstream 포트 설정 오타는 의외로 자주 발생하는 원인

- 헬스체크와 자동 재시작으로 재발 방지


Nginx는 프록시일 뿐이므로, 502 에러의 근본 원인은 대부분 백엔드 애플리케이션 쪽에 있다는 점을 기억해야 합니다.

목요일

Docker 멀티스테이지 빌드 캐시 최적화하기

🔍 검색 키워드: Docker 멀티스테이지 빌드, Dockerfile 캐시 성능, Docker 레이어 캐싱, 빌드 시간 단축

Docker 멀티스테이지 빌드 캐시 최적화하기

증상

Dockerfile로 이미지를 빌드할 때마다 다음과 같은 문제가 발생한다:

$ docker build -t myapp:latest .
  Step 3/10 : RUN npm install    # 매번 30초 이상 소요
  Step 4/10 : RUN npm run build  # 매번 60초 이상 소요
  ...
  real    2m 15s

코드는 조금만 바뀌었는데도 빌드 시간이 계속 길어진다. CI/CD 파이프라인에서 빌드만 몇 분씩 걸려서 배포 속도가 느리다. 특히 npm, pip 같은 의존성 설치 단계가 매번 전부 다시 실행된다.

원인

Docker는 각 RUN, COPY, ADD 단계를 하나의 레이어로 취급하고, 파일이 변경되면 그 이후의 모든 레이어를 다시 빌드한다. 예를 들어:

FROM node:18
  COPY . /app              # 레이어1: 모든 파일 복사
  WORKDIR /app
  RUN npm install          # 레이어2: 의존성 설치 (레이어1 변경되면 캐시 무효)
  RUN npm run build        # 레이어3: 빌드 (레이어2 변경되면 캐시 무효)

코드를 한 줄만 바꿔서 COPY . /app을 실행하면 npm install, npm run build가 모두 다시 실행된다. 즉, 캐시를 제대로 활용하지 못한다.

멀티스테이지 빌드를 사용하면서도 각 스테이지 간 의존성이 꼬여 있으면 캐시 효율이 떨어진다.

해결방법

1. 의존성 파일을 먼저 복사 (핵심!)

FROM node:18 AS builder
  
  WORKDIR /app
  
  # package.json, package-lock.json만 먼저 복사
  COPY package*.json ./
  
  # 의존성 설치 (코드 변경 시 캐시 유지)
  RUN npm ci
  
  # 이제 코드 복사
  COPY . .
  
  # 빌드
  RUN npm run build
  
  # 런타임 스테이지
  FROM node:18-alpine
  
  WORKDIR /app
  
  # 의존성만 복사 (빌드 아티팩트 불필요)
  COPY --from=builder /app/node_modules ./node_modules
  COPY --from=builder /app/dist ./dist
  COPY package*.json ./
  
  EXPOSE 3000
  CMD ["node", "dist/index.js"]

캐시 효과:

  • package.json 불변 → npm install 캐시 유지
  • 코드만 변경 → COPY . . 부터만 재실행
  • 빌드 시간 30초 → 5초

2. Docker Buildkit으로 고급 캐싱 활용

// syntax=docker/dockerfile:1.4
  
  FROM node:18 AS builder
  
  WORKDIR /app
  
  COPY package*.json ./
  
  // --mount=type=cache로 npm cache 디렉토리 보존
  RUN --mount=type=cache,target=/root/.npm \
      npm ci --prefer-offline
      
      COPY . .
      RUN npm run build
      
      FROM node:18-alpine
      
      WORKDIR /app
      
      COPY --from=builder /app/node_modules ./node_modules
      COPY --from=builder /app/dist ./dist
      COPY package*.json ./
      
      EXPOSE 3000
      CMD ["node", "dist/index.js"]

빌드 명령:

DOCKER_BUILDKIT=1 docker build -t myapp:latest .

효과:

  • npm cache가 빌드 간에 유지되어 더 빠름
  • 오프라인 설치 최적화

3. 불필요한 파일 제외 (.dockerignore)

.git
  .gitignore
  node_modules
  npm-debug.log
  .env
  .env.local
  dist
  build
  coverage
  .DS_Store
  README.md

효과:

  • COPY . . 시 변경되지 않은 파일이 많으면 해시 계산이 더 오래 걸림
  • .dockerignore로 제외하면 캐시 재계산 범위 축소

4. 여러 스테이지에서 캐시 공유

// syntax=docker/dockerfile:1.4
  
  FROM node:18 AS dependencies
  
  WORKDIR /app
  COPY package*.json ./
  
  RUN --mount=type=cache,target=/root/.npm \
      npm ci
      
      // 빌드 스테이지1
      FROM dependencies AS builder1
      
      COPY . .
      RUN npm run build
      
      // 빌드 스테이지2 (테스트)
      FROM dependencies AS tester
      
      COPY . .
      RUN npm test
      
      // 최종 스테이지
      FROM node:18-alpine
      
      WORKDIR /app
      
      COPY --from=builder1 /app/dist ./dist
      COPY --from=dependencies /app/node_modules ./node_modules
      COPY package*.json ./
      
      EXPOSE 3000
      CMD ["node", "dist/index.js"]

5. 번들 사이즈 최소화 (캐시 영향 없지만 이미지 크기 감소)

FROM node:18 AS builder
  
  WORKDIR /app
  COPY package*.json ./
  RUN npm ci
  
  COPY . .
  RUN npm run build
  
  // 크기 최소화: dependencies만 복사, devDependencies 제외
  FROM node:18-alpine
  
  WORKDIR /app
  
  COPY --from=builder /app/dist ./dist
  
  // 본번 의존성만 설치 (devDependencies 없음)
  COPY package*.json ./
  RUN npm ci --omit=dev
  
  EXPOSE 3000
  CMD ["node", "dist/index.js"]

정리표

문제 원인 해결법
npm install 매번 재실행 COPY . . 후 RUN npm install package.json만 먼저 복사
캐시가 가끔만 작동 불필요 파일 포함 시 해시 변경 .dockerignore 작성
빌드 시간 여전히 길다 npm cache 미보존 Buildkit + --mount=cache 활용
이미지 크기 커짐 devDependencies 포함 npm ci --omit=dev
멀티스테이지 간 캐시 미공유 각 스테이지가 독립적 dependencies AS 공용 스테이지 생성

TIP: docker build --progress=plain으로 상세 로그 확인, docker image history myapp:latest로 각 레이어 크기 확인 가능. 캐시 강제 무효화는 docker build --no-cache 또는 COPY . . --chown=node:node 같은 타임스탐프 변경 명령으로.

PostgreSQL Connection Pool 고갈 해결하기

🔍 검색 키워드: PostgreSQL connection pool 에러, 커넥션 풀 소진, 데이터베이스 연결 오류, 최대 연결 초과

PostgreSQL Connection Pool 고갈 해결하기

증상

애플리케이션이 실행되다가 갑자기 다음과 같은 에러가 발생한다:

FATAL: sorry, too many clients already

또는:

Error: connect ECONNREFUSED - PostgreSQL connection failed

또는 connection pool 라이브러리에서:

Error: timeout acquiring a connection from the pool
  Error: Client has already been released to the pool

특히 트래픽이 많아지거나 장시간 실행되는 배치 작업 후에 자주 발생한다. 새로운 요청이 들어와도 DB 연결을 못 하고 응답 불가 상태가 된다.

원인

PostgreSQL은 최대 동시 연결 수 제한이 있고 (기본값 100), connection pool은 재사용 가능한 연결 수를 미리 정해둔다. 고갈 현상은 다음 경우에 발생:

  1. 연결을 반환 안 함: 쿼리 후 연결을 pool에 반환하지 않아 계속 증가
  2. 타임아웃 후 좀비 연결: 응답 없는 연결이 pool에 남아있음
  3. Pool 사이즈 설정 과소: 동시 사용자/요청량에 비해 pool이 너무 작음
  4. 쿼리 오래 걸림: 느린 쿼리가 연결을 오래 점유
  5. Connection leak: 예외 발생 시 연결을 close하지 않는 코드

해결방법

1. Connection Pool 설정 최적화

Node.js + pg (node-postgres):

const { Pool } = require('pg');
  
  const pool = new Pool({
    host: 'localhost',
      port: 5432,
        database: 'mydb',
          user: 'postgres',
            password: 'password',
              max: 20,                    // 최대 연결 수 (기본 10)
                idleTimeoutMillis: 30000,   // 30초 idle 후 닫기
                  connectionTimeoutMillis: 2000, // 연결 생성 타임아웃
                  });
                  
                  module.exports = pool;

Python + psycopg2:

import psycopg2.pool
  
  connection_pool = psycopg2.pool.SimpleConnectionPool(
      1,      // minimum connections
          20,     // maximum connections
              database="mydb",
                  user="postgres",
                      password="password",
                          host="localhost"
                          )
                          
                          // 사용
                          conn = connection_pool.getconn()
                          try:
                              // 쿼리 실행
                                  pass
                                  finally:
                                      connection_pool.putconn(conn)

Java + HikariCP:

HikariConfig config = new HikariConfig();
  config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
  config.setUsername("postgres");
  config.setPassword("password");
  config.setMaximumPoolSize(20);          // 최대 연결
  config.setMinimumIdle(5);               // 최소 유휴 연결
  config.setConnectionTimeout(2000);       // 2초 타임아웃
  config.setIdleTimeout(600000);          // 10분 idle 타임아웃
  config.setMaxLifetime(1800000);         // 30분 최대 수명
  
  HikariDataSource dataSource = new HikariDataSource(config);

2. 연결이 제대로 반환되는지 확인

Node.js에서 연결 누수 확인:

const pool = new Pool(config);
  
  pool.on('error', (err, client) => {
    console.error('Unexpected error on idle client', err);
      process.exit(-1);
      });
      
      pool.on('connect', () => {
        console.log('New connection created');
        });
        
        // 항상 try-finally로 연결 반환 보장
        const client = await pool.connect();
        try {
          const result = await client.query('SELECT * FROM users WHERE id = $1', [1]);
            return result.rows;
            } finally {
              client.release();  // 반드시 실행
              }

또는 with 문 활용 (Python):

from contextlib import contextmanager
  
  @contextmanager
  def get_db_connection():
      conn = connection_pool.getconn()
          try:
                  yield conn
                      finally:
                              connection_pool.putconn(conn)
                              
                              // 사용
                              with get_db_connection() as conn:
                                  cursor = conn.cursor()
                                      cursor.execute("SELECT * FROM users WHERE id = %s", (1,))
                                          // 예외 발생해도 자동으로 반환됨

3. 느린 쿼리 최적화

-- 인덱스 추가
  CREATE INDEX idx_users_id ON users(id);
  
  -- 실행계획 확인
  EXPLAIN ANALYZE SELECT * FROM users WHERE status = 'active';
  
  -- 필요 없는 조인 제거, WHERE 조건 최적화
  SELECT u.id, u.name
  FROM users u
  WHERE u.created_at > NOW() - INTERVAL '7 days'
    AND u.status = 'active'
    LIMIT 100;

4. PostgreSQL 서버 설정 확인

# PostgreSQL 최대 연결 수 조회
  psql -U postgres -c "SHOW max_connections;"
  
  # 현재 연결 수 조회
  psql -U postgres -c "SELECT count(*) FROM pg_stat_activity;"
  
  # 연결 상세 정보
  psql -U postgres -c "SELECT datname, count(*) FROM pg_stat_activity GROUP BY datname;"

필요하면 postgresql.conf에서:

max_connections = 200   # 기본값 100에서 증가

5. 좀비 연결 정리

-- 유휴 연결 종료
  SELECT pg_terminate_backend(pid)
  FROM pg_stat_activity
  WHERE datname = 'mydb'
    AND state = 'idle'
      AND query_start < now() - interval '30 minutes';
      
      -- 슬로우 쿼리 강제 종료 (주의)
      SELECT pg_terminate_backend(pid)
      FROM pg_stat_activity
      WHERE query_start < now() - interval '5 minutes'
        AND state != 'idle';

정리표

상황 원인 해결법
연결 고갈 후 다시 증가 연결을 close하지 않음 try-finally 또는 context manager로 보장
Pool 타임아웃 pool이 너무 작음 max 크기 증가, idleTimeoutMillis 조정
"too many clients" 에러 PostgreSQL 최대 연결 도달 max_connections 증가 또는 app pool 감소
간헐적 연결 오류 connectionTimeoutMillis 너무 짧음 타임아웃 값 증가 또는 네트워크 확인
메모리 누수 좀비 연결이 메모리 점유 주기적으로 idle 연결 정리 (pg_terminate_backend)

TIP: 프로덕션에서는 반드시 connection pool 크기, 타임아웃, idle 설정을 검토하고, 정기적으로 pg_stat_activity로 모니터링하자.