Загружаем каталог…
Загружаем каталог…
개발 환경에서 Java 프로세스가 반복적으로 종료됐다. API와 배치, Redis가 한 서버를 공유했고, 일부 자동화 작업은 별도 Java 프로세스와 브라우저도 실행했다. 처음에는 “작은 서버니까 메모리를 늘리면 되지 않을까?”라고 생각하기 쉬웠다. 하지만 증설을 결정하려면 먼저 어느 메모리 경계에서 실패했는지 구분해야 했다. 이 글은 해결 효과를 과장하는 성공담보다, 로그로 확인한 원인과 아직 확인하지 못한 범위를 정리한 기록이다. 내부 서버 주소와 식별자는 생략했다. 1. 메모리가 부족하다는 말은 세 가지를 섞고 있었다 경계 확인할 증거 바로 단정할 수 없는 것 JVM 힙 Java heap space 등 애플리케이션 오류 호스트 RAM 전체가 부족했는지 컨테이너 cgroup OOM, 사용량과 limit 호스트에 여유 메모리가 없었는지 호스트 시스템 전체 OOM과 당시 메모리 상태 특정 Java 힙만 늘리면 해결되는지 이번 커널 기록에서 결정적인 부분은 다음 형태였다. 원본 전체 대신 판정에 필요한 부분만 정리했다. memory: usage <한도에 도달한 값>, limit <설정한 한도> oom-kill:constraint=CONSTRAINT_MEMCG Memory cgroup out of memory: Killed process ... (java) 이 기록이 보여 준 직접 원인은 컨테이너 메모리 한도 도달이었다. 같은 컨테이너에서 Java 프로세스가 반복 종료된 사실도 확인했다. 여기서 “서버 전체 RAM을 모두 사용했다”까지 결론을 늘리면 증거보다 설명이 앞선다. Docker의 메모리 제한은 컨테이너에 적용된다. 서버를 증설해도 기존 제한이 유지되면 같은 경계에서 다시 실패할 수 있다. Docker 메모리 제한 문서 2. 로그가 안 보였던 이유부터 다시 확인했다 처음에는 날짜를 지정해 커널 로그를 검색했는데 결과가 없었다. 그때 “과거 기록은 없나 보다”라고 결론 내리기 쉬웠다. 그러나 재부팅 전 기록까지 검색하는지 확인해야 했다. # 보관된 부팅 이력 sudo journalctl --list-boots --no-pager # 현재 부팅으로 제한하지 않고 보관된 커널 로그 검색 # 날짜는 실제 장애 구간으로 변경한다. sudo journalctl _TRANSPORT=kernel \ --since "2026-10-01 00:00:00" \ --until "2026-10-02 00:00:00" \ --no-pager | grep -Ei -B 15 -A 25 \ 'out of memory|oom-kill|killed process|memory cgroup' 시간대도 확인해야 한다. 로그 화면의 시각과 업무 중 기억하는 시각을 비교하기 전에 서버와 조회 도구의 시간대부터 맞춰야 한다. 3. 점검 시점 사용량과 장애 순간 사용량을 분리했다 과거 점검 때 실행한 free와 docker stats 결과는 작업 기록에 남아 있었다. 이 자료로 그 순간의 사용량과 가용량은 확인할 수 있었다. 반면 배치가 가장 무거웠던 순간의 최대 사용량은 알 수 없었다. 증거 말할 수 있는 범위 ──────────────────────────────────────────── 커널 cgroup OOM 로그 → 컨테이너 한도로 Java가 종료됨 한 번 실행한 docker stats → 그 점검 순간의 사용량 CPU 사용률 그래프 → CPU 부하가 증가한 구간 미수집한 메모리 시계열 → 과거 최대값은 확정할 수 없음 CPU가 높았다는 사실을 메모리 부족의 증거로 바꾸거나, 점검 당시 사용량을 장애 당시 최대값으로 쓰지 않기로 했다. 증설 요청에서도 이 구분이 필요했다. 4. 선택지는 있었지만 각각 해결하는 범위가 달랐다 컨테이너 한도 조정 은 이번에 확인된 직접적인 경계를 다룬다. 다만 같은 호스트의 다른 프로세스까지 함께 고려해야 한다. JVM 힙 조정 은 별도로 발견한 Java Heap 오류를 다루는 선택지다. 컨테이너 안에는 힙 이외에도 메모리가 필요하고, 브라우저가 함께 실행된다면 JVM 밖의 사용량도 봐야 한다. 힙 한도와 컨테이너 한도를 같은 값으로 취급할 수 없었다. 서버 증설 은 전체 자원 여유를 늘리는 선택지다. 하지만 컨테이너 제한이나 대량 데이터 보관 방식이 그대로라면 모든 문제가 자동으로 해결되지는 않는다. 배치 분리 는 API와 무거운 작업의 자원 경쟁을 줄일 수 있지만, 서버 운영과 배포 구성이 늘어난다. 당장 분리가 어려운 조건에서는 우선 실행량을 제한하고 필요한 메모리를 측정하는 접근이 현실적이었다. 이 조건에서 선택한 방향은 한 가지 옵션을 만능 해결책으로 삼는 것이 아니라, 원인별 설정 조정과 배치의 메모리 보관량 감소를 함께 검토하는 것이었다. 별도 매크로 검증 작업에서는 전체 로딩을 100행 단위 순차 조회로 바꾸는 개선도 진행했다. 이 변경을 커널 OOM 사건과 동일 원인이라고 단정하지는 않았다. 5. 무엇을 해결했고 무엇이 남았나 확인한 성과는 반복 종료가 컨테이너 한도에 의해 발생했다는 사실을 로그로 좁힌 것이다. 증설 필요성을 설명할 때도 컨테이너 OOM, Java Heap 오류, 점검 시점 메모리 수치를 분리할 수 있게 됐다. 아직 필요한 검증은 개선된 배치를 실행하면서 호스트 가용 메모리, 컨테이너 사용량, Java와 브라우저의 사용량, API 응답 상태를 함께 기록하는 것이다. 이번 글에서는 증설 후 장애가 사라졌다거나 메모리를 몇 퍼센트 줄였다는 결과를 주장하지 않는다. 다음 장애에서는 “메모리가 부족했다”보다 “어느 경계에서, 어떤 작업 중에, 어떤 증거로 확인했는가”부터 설명하고 싶다. 참고 Docker: Resource constraints Linux Kernel: cgroup v2 2만 장의 PDF 레포트 생성 시간을 5시간→1시간대로 줄였다 : 다른 사례의 작업 규모·메모리 검증 전개를 참고했다. 해당 글의 수치와 해결 결과는 이 사례의 결과가 아니다. 6. 내가 맡은 일과 조사 순서 내가 맡은 범위는 반복 종료 증상을 서버 자원 부족이라는 표현으로 묶지 않고, 종료를 결정한 계층을 찾아 설정 변경의 근거를 만드는 일이었다. API와 배치가 한 호스트를 공유했기 때문에 한 프로세스의 한도를 늘리는 일도 다른 서비스의 여유와 연결됐다. 개발 서버였지만 검증이 끊기면 배치 결과를 확인하거나 변경 사항을 검증하는 작업도 함께 멈췄다. 조사 순서를 다음처럼 정리했다. 애플리케이션 예외로 종료됐는지, 외부에서 프로세스가 종료됐는지 확인한다. 커널 기록에서 종료된 프로세스와 컨테이너 cgroup을 연결한다. 해당 시점의 usage와 limit을 비교한다. 현재 실행 상태와 과거 장애 기록을 분리한다. 다른 배치의 Heap 오류가 같은 사건인지 별개의 사건인지 구분한다. 여기서 컨테이너 ID는 중요한 연결 키다. Java라는 이름만 보고 API와 배치 중 어느 쪽인지 결론 내리면 안 된다. 컨테이너가 남아 있다면 inspect로 이름과 제한을 확인할 수 있지만, 재생성됐다면 현재 설정이 과거 설정을 증명하지 않는다. 이전 배포 기록과 함께 읽어야 한다. 7. 왜 바로 Xmx부터 올리지 않았나 힙이 부족하면 Xmx를 늘리는 것이 자연스러운 선택이다. 그러나 이번 커널 기록의 기준은 JVM의 힙 사용량이 아니라 cgroup에 계상된 메모리였다. 힙 외에도 스레드 스택, 클래스 메타데이터, 네이티브 메모리 등이 필요하다. 별도 브라우저 프로세스의 사용량은 Java 힙으로 설명할 수도 없다. 구조를 단순화하면 다음과 같다. 실제 제한 적용은 프로세스가 어느 cgroup에 속하는지 확인해야 한다. 호스트 메모리 ├─ OS와 관리 프로세스 ├─ API 컨테이너 │ └─ JVM: 힙 + 메타데이터 + 스레드·네이티브 영역 ├─ 배치 컨테이너 │ ├─ JVM │ └─ 같은 컨테이너에서 실행한 브라우저·자식 프로세스 └─ Redis 컨테이너 그래서 “힙을 늘린다”와 “컨테이너 한도를 늘린다”를 따로 검토했다. Xmx만 올리면 힙에는 여유가 생겨도 컨테이너 전체의 여유는 줄 수 있다. 반대로 서버를 늘려도 컨테이너 한도가 그대로면 그 한도에서 다시 종료될 수 있다. 설정값 하나를 선택하기 전에 어떤 경계에 예산을 배분하는지 정해야 했다. 8. 전체 조회를 나눌 때도 지켜야 할 조건이 있었다 별도의 매크로 검증 개선에서는 전체 데이터를 한 번에 가져와 비교하던 흐름을 작은 단위로 읽는 방향으로 바꿨다. 이 선택의 목적은 처리한 행을 계속 보관하지 않는 것이었다. 전체 로딩: 전체 행 보관 → 비교 → 결과 도출 순차 처리: 일부 행 조회 → 필요한 정보 갱신 → 다음 일부 행 조회 다만 조회만 페이지로 나누고 결과를 하나의 리스트에 계속 추가하면 보관량은 다시 전체 데이터 크기까지 늘어난다. 작은 단위로 처리한다는 설명에는 반드시 무엇을 남기고 무엇을 버리는지가 포함돼야 한다. 이 방식에는 요청 횟수가 늘어날 수 있다는 비용이 있다. 조회 중 원본이 바뀌면 페이지 경계의 중복·누락도 검토해야 한다. 따라서 작은 페이지가 언제나 정답은 아니다. 이 작업에서는 처리 속도만 앞세우기보다 제한된 자원에서 검증을 끝낼 수 있는 형태를 우선했다. 실제 사용량 감소와 처리 시간 변화는 별도 측정이 필요하다. 9. 결과를 어디까지 말할 수 있는가 단계 이번 기록에서 말할 수 있는 것 원인 확인 반복 종료 중 확인한 사건은 컨테이너 한도 도달에 따른 Java 강제 종료였다 코드 개선 별도 매크로 검증의 데이터 조회를 작은 단위의 순차 처리로 변경했다 용량 판단 과거 한 시점의 측정만으로 동시 실행 최대 메모리를 확정할 수 없었다 남은 검증 개선된 작업의 최대 사용량·완료 여부·API 영향·재발 여부를 함께 측정해야 한다 후속 검증에서는 같은 데이터 범위와 동시 실행 조건을 기록한 뒤 변경 전후를 비교하려 한다. 배치가 완료됐다는 사실만으로 끝내지 않고, 그동안 API 지연이 커지지 않았는지와 브라우저가 종료 후 정리됐는지도 확인해야 한다. 이번 경험에서 바뀐 것은 증설 요청의 설명 방식이었다. 막연히 “서버가 작다”는 주장 대신, 확인한 종료 경계와 측정하지 못한 구간을 구분해서 제시할 수 있게 됐다. 인프라 변경과 코드 개선은 서로 대체하는 답이 아니라, 현재 제약 안에서 함께 검증해야 할 선택이었다.
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
서버를 늘리기 전에 확인한 것: Java OOM과 컨테이너 메모리 한도 구분하기. 개발 환경에서 Java 프로세스가 반복적으로 종료됐다. API와 배치, Redis가 한 서버를 공유했고, 일부 자동화 작업은 별도 Java 프로세스와 브라우저도 실행했다. 처음에는 “작은 서버니까 메모리를 늘리면 되지 않을까?”라고 생각하기 쉬웠다. 하지만 증설을 결정하려면 먼저 어느 메모리 경계에서 실패했는지 구분해야 했다. 이 글은 해결 효과를 과장하는 성공담보다, 로그로 확인한 원인과 아직 확인하지 못한 범위를 정리한 기록이다. 내부 서버 주소와 식별자는 생략했다. 1. 메모리가 부족하다는 말은 세 가지를 섞고 있었다 경계 확인할 증거 바로 단정할 수 없는 것 JVM 힙 Java heap space 등 애플리케이션 오류…
Открыть источник