기금넷 공식사이트 - 복권 조회 - 서비스 풀 GC 뒤의 메모리 누출을 기억하시나요? 정말 놀랍네요.
서비스 풀 GC 뒤의 메모리 누출을 기억하시나요? 정말 놀랍네요.
1. 비즈니스 로그를 확인하지만 관련 오류 로그를 찾을 수 없습니다.
2. nginx 액세스 로그를 확인하여 반환된 상태 코드가 모두 499 인지 확인합니다. Request_uri 를 검사하여 요청에 대한 것이 아니라 인터페이스 문제가 아니라 프로세스 수준 문제일 수 있음을 나타냅니다.
Upstream_addr 을 분류하면 문제가 기본적으로 한 시스템에 집중되어 있음을 알 수 있습니다.
3. 온라인 정보에 따르면 499 는 nginx 확장의 4xx 오류이며 클라이언트 요청이 아직 반환되지 않았을 때 클라이언트가 적극적으로 연결을 끊었음을 나타냅니다. 몇 가지 이유가 있지만 대부분 서버 업스트림 처리가 너무 느려서 사용자가 미리 연결을 닫게 될 수 있다고 합니다. 그런 다음 이 방향으로 가서 기계에 로그인하여 실제 access.log 를 확인합니다
업스트림 응답이 10s 이상 발견되었습니다. 이는 업스트림 서버가 10 초 동안 응답하지 않았기 때문에 nginx 가 링크를 미리 닫고 499 를 반환했음을 증명합니다.
4. 프로세스 응답이 이렇게 느린 이유는 무엇입니까? 10 초는 너무 변태적이다. 그 기간 동안 단 한 대의 기계만 문제가 발생했고 프로세스급 문제라는 점을 감안하면 가장 먼저 생각하는 것은 GC 다. 그래서 나는 다시 기계에 접속해 GC 일지를 확인했다. (데이비드 아셀, Northern Exposure (미국 TV 드라마), 컴퓨터명언) 전체 GC 가 발견되었고, 시점은 경보 시간과 일치합니다. FullGC 는 jdk8 의 기본 ParallelGC 를 사용하고 FullGC 기간 동안 전체 어플리케이션 실행을 중단하기 때문에 최대 19.07 초가 소요됩니다. 이것은 매우 무서운 일이다.
이에 따라 4xx 경보의 초기 원인은 풀GC 에 의해 파악된 것으로 보인다.
그렇다면 풀GC 는 도대체 왜 일어났을까요? 심도 있는 분석이 필요하다.
서비스 거버넌스 플랫폼의 JVM 모니터링을 통해 며칠 동안 관찰했습니다. 이 기간 동안 풀GC 도 서로 다른 기계에 따라 여러 번 발생했다. 감시에서 발견한 바에 따르면, 기본적으로 각 기계는 이틀마다 한 번씩 전체 GC 를 가지고 있다. 매번 풀GC 이후 구세대가 모은 쓰레기는 많지 않고 활용률이 매우 높다.
왜 기성세대가 그렇게 많은 공간을 차지합니까?
위의 전체 GC 로그를 계속 분석하고,
1. 전체 GC 발생 시 이전 세대의 메모리가 99.98% (1048397/1048576) 를 차지했다. 풀GC 는 기성세대가 꽉 찼기 때문에 촉발된 것 같다.
2. 전 GC 재활용 구세대 쓰레기 약 302M, 재활용 후 구세대가 70.4%(738282/ 1048576) 를 차지했다. 이 입주율은 여전히 비교적 높다.
1. 먼저 jmap 은 모든 개체에 대한 정보만 인쇄합니다. ClassPathList 및 classclasspath 를 발견한 객체 수는 10 만 개로 두 숫자가 같습니다. 나는 메모리 누출 냄새를 맡은 것 같다.
2. 객체 통계에만 의존하는 것만으로는 문제를 찾기에 충분하지 않으며, 전체 HeapDump 를 사용하여 MAT 을 통해 추가 분석이 필요합니다.
Jmap, 전체 힙을 덤프합니다.
일정 기간 후 jmap 를 계속하면 이번에는 생존자의 덤프만 취하게 됩니다 (실제 효과는 전체 GC 를 먼저 실행하는 것입니다).
GC 가 꽉 차면 ClassPathList 객체는 재활용되지 않지만 수량은 계속 증가하고 있음을 알 수 있습니다. 이 시점에서 ClassPathList 에 취약점이 있음을 기본적으로 확인할 수 있습니다.
그렇다면 누가 ClassPathList 를 인용하고 있어 재활용이 불가능할까요?
MAT 의 OQL 을 통해 이전 세대의 ClassPathList 객체를 필터링하여 객체의 종속성을 추가로 분석합니다.
먼저 이전 세대의 주소 범위를 알아야 합니다. VJ 도구를 사용할 수 있습니다.
Vjmap 의 address 명령을 사용하면 각 세대의 주소를 빠르게 인쇄할 수 있습니다.
OldGen 의 하한은 0x800000000 이고 상한은 0xc0000000 이라는 것을 알 수 있습니다 (OQL 을 사용할 때 숫자 앞의 0 을 빼도록 주의하세요).
OQL 실행 이전 세대의 ClassPathList 객체만 쿼리: OQL 을 실행하여 이전 세대의 ClassPathList 객체만 쿼리합니다.
객체 중 하나를 분석하여 이 ClassPathList 객체가 다른 ClassPathList 객체의 일련의 next 속성에 의해 참조된다는 것을 알 수 있습니다. 연결된 목록 구조처럼 보입니다.
GCRoot 를 보면 AppClassLoader, 즉 우리의 응용 프로그램 클래스 로더에 의해 참조된다는 것을 알 수 있습니다. GC 는 로더를 언로드하지 않는 한 ClassPathList 오브젝트를 제거하지 않습니다.
이곳의 분석은 점점 진실에 가까워지는 것 같다. 이 ClassPathList 는 프로젝트의 어느 곳에 사용됩니까?
앞의 분석을 통해 ClassPathList 의 전체 참조 체인을 알 수 있었습니다.
Appclass loader-& gt;; ClassPool 클래스의 DefaultPool 필드->; ClassPoolTail 클래스의 소스 필드->; 클래스 경로 목록 경로 목록 클래스
보시다시피 ClassPathList 에는 next 라는 두 가지 속성이 있습니다. 앞서 MAT 에 대한 분석과 함께 ClassPathList 는 확실히 연결된 목록 구조입니다. 시간이 지남에 따라 ClassPathList 가 증가하고, 연결된 목록이 커지고, 결국 메모리 사용량이 증가하게 됩니다.
기타 경로 필드는 클래스 경로 유형이며 클래스 경로는 인터페이스입니다. 구현 클래스를 살펴보면 익숙한 이름인 classpath 를 발견했습니다. 객체 통계를 분석하기 전에 ClassPathList 와 객체 수가 같은 또 다른 클래스인 ClassClassPath 가 있습니다. ClassPathList 를 추가할 때마다 해당 ClassPath 오브젝트가 추가되어 그 수가 동일한 이유를 설명합니다.
주석을 통해 이 classclasspath 의 역할은 ClassPool 이라는 객체로 호출하는 insertClassPath 메서드를 사용하여 classclasspath 객체를 추가하는 것일 가능성이 높다는 것을 알 수 있습니다. Head insertion 을 통해 ClassPath 메서드 classclasspath 를 ClassPathList 에 삽입하여 클래스 정보를 얻을 수 있는 검색 경로를 형성합니다.
그래서 프로젝트에서 insertClassPath 메서드를 호출하는 곳이 있는지 검색하려고 했습니다. 우연히 한 클래스를 발견했습니다.
이것은 우리 프로젝트가 매개 변수, 시간이 많이 걸리는 실행 및 보고 메트릭을 인쇄하는 데 사용하는 @AutoLog 의 구현 클래스가 아닙니까?
보시다시피 insertClassPath 는 getParams 메서드에서 호출되고 주석 @AutoLog 의 printParams 는 기본적으로 true 로 설정됩니다. 즉, 호출할 때마다 인쇄 메서드 매개 변수가 필요합니다. 인쇄하기 전에 getParams 를 호출하여 매개 변수 이름을 가져옵니다. 따라서 매번 insertClassPath 를 삽입할 때마다 ClassPathList 의 크기가 커집니다.
이 시점에서 메모리 누출의 주범이 이미 발견되었다. 해결 방법은 매우 간단하다.
목표는 메서드의 매개 변수 이름일 뿐 실제로 JoinPoint 를 통해 직접 얻을 수 있으므로 JoinPoint 가져오기 메서드로 변경할 수 있습니다.
비교를 위해 개조 전후에 스트레스 테스트를 했다. 스트레스 테스트 JVM 의 매개 변수는 온라인과 거의 일치합니다. 가능한 한 빨리 효과를 보기 위해 힙 크기는 줄어야 한다. -Xms200m -Xmx200m
ClassPathList 수가 증가하고 있습니다
기성세대가 한 번에 회수할 수 있는 쓰레기가 점점 줄어들고 있으며, 매번 회수할 때마다 남은 공간도 점점 작아지고 있다. 결국 옛 세대 전체가 가득 찼다.
아직 OOM 을 트리거하지는 않았지만 CPU 로드가 급증하고 있어 기본적으로 잦은 풀 GC 상태에 있습니다.
ClassPathList 가 삭제되었습니다.
풀 GC 도 일반화되는 경향이 있습니다. 매번 수집하는 쓰레기는 대체로 같다.
첫 번째 방법은 시작 매개 변수에 -XX:+PrintHeapAtGC 를 추가하여 GC 마다 주소를 인쇄하는 것입니다.
두 번째 방법은 vjmap 명령을 사용하는 것입니다. In -old, -sur, -address, 구간 주소가 인쇄됩니다.
세 번째 방법은 vjmap 의 address 명령을 사용하여 긴 일시 중지 없이 각 세대의 주소를 빠르게 인쇄하는 것입니다.
액세서리: Dell 서비스의 JVM 매개 변수