프로젝트에서 부하 테스트를 진행 하던 중 일부 구간에서 오동작하는 이슈가 발생하였다.

특이한 것은 적은양의 트래픽으로는 별 다른 이슈는 없었으나 예상 최대 부하를 진행하였을 때 일부의 로직이 잘못 동작하는 것으로 보였다.(로직들이 비동기가 많기 때문에 더욱 문제를 찾기 어려웠음)

결국 최적화 팀을 불러 어느 구간이 문제였는지 찾을 수 있었는데 최적화 팀에서 사용하는 툴을 찾아보니 오픈소스 툴 이었다.

최적화 팀에서 사용한 방법을 해보고자 한다.


Arthas 사이트

https://arthas.aliyun.com/en/doc/

 

Arthas

Alibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas - alibaba/arthas: Alibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas

arthas.aliyun.com

 

Arthas는 알리바바 미들웨어 팀에서 만든 오픈소스의 JAVA 진단 툴이라고한다.

 

특징

  • Class가 load 되어 있는지, 클레스가 어디로 부터 load 되었는지(jar 파일 충돌에 유용하다고 한다)
  • 예상되로 동작하는지 Decompile
  • 클레스로더의 통계(클래스 로더 수, 클래스 로더당 로드된 클래스 수, 클래스 로더 계층 구조, 가능한 클래스 로더 누수 등)
  • 메소드 호출 세부정보(메소드의 변수, 반환값 등)
  • 지정된 메소드의 stack 추적

등등

 


 

Arthas 테스트를 위한 설치

 

테스트 타겟이 될 JAVA 프로그램(math-game) arthas 다운로드

curl -O https://arthas.aliyun.com/math-game.jar
curl -O https://arthas.aliyun.com/arthas-boot.jar

 

math-game 실행해보기

(1초마다 난수를 생성하고 해당 숫자의 모든 소인수를 구하는 프로그램이라고 한다.)

java -jar math-game.jar

 

 

하나의 터미널을 더 띄워서 Arthas 실행

// math-game의 pid 조회
ps -ef| grep "math"

java -jar arthas-boot.jar ${pid}

 

 

help

여러 기능들을 확인 할수 있다. 

 


대시보드

대시보드를 몇 초주기로 snapshot할 것인가(i 옵션), 총 몇회 snapshot할 것인가(n옵션) 옵션을 사용할수 있으며 

타겟 jvm의 thread, memory, gc 등을 알 수 있다고한다.

 

 


thread

thread의 정보와 stack 정보(어디서부터 호출이 되었는지)

 

 

현재 메인스레드는 "대기상태"이며 Thread.sleep이 동작하고 있다.

 

스레드의 상태 (출처 :https://m.blog.naver.com/PostView.nhn?blogId=qbxlvnf11&logNo=220921178603&proxyReferer=https:%2F%2Fwww.google.com%2F)

 


jad

클래스를 디컴파일한다.

thrad에서 찾은 demo.MathGame 클래스를 디컴파일

 

 

       package demo;

       import java.util.ArrayList;
       import java.util.List;
       import java.util.Random;
       import java.util.concurrent.TimeUnit;

       public class MathGame {
           private static Random random = new Random();
           private int illegalArgumentCount = 0;

           public static void main(String[] args) throws InterruptedException {
               MathGame game = new MathGame();
               while (true) {
/*16*/             game.run();
/*17*/             TimeUnit.SECONDS.sleep(1L);
               }
           }

           public void run() throws InterruptedException {
               try {
/*23*/             int number = random.nextInt() / 10000;
/*24*/             List<Integer> primeFactors = this.primeFactors(number);
/*25*/             MathGame.print(number, primeFactors);
               }
               catch (Exception e) {
/*28*/             System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage());
               }
           }

           public static void print(int number, List<Integer> primeFactors) {
               StringBuffer sb = new StringBuffer(number + "=");
/*34*/         for (int factor : primeFactors) {
/*35*/             sb.append(factor).append('*');
               }
/*37*/         if (sb.charAt(sb.length() - 1) == '*') {
/*38*/             sb.deleteCharAt(sb.length() - 1);
               }
/*40*/         System.out.println(sb);
           }

           public List<Integer> primeFactors(int number) {
/*44*/         if (number < 2) {
/*45*/             ++this.illegalArgumentCount;
                   throw new IllegalArgumentException("number is: " + number + ", need >= 2");
               }
               ArrayList<Integer> result = new ArrayList<Integer>();
/*50*/         int i = 2;
/*51*/         while (i <= number) {
/*52*/             if (number % i == 0) {
/*53*/                 result.add(i);
/*54*/                 number /= i;
/*55*/                 i = 2;
                       continue;
                   }
/*57*/             ++i;
               }
/*61*/         return result;
           }
       }

watch

메소드의 반환 오브젝트 확인

// demo.MathGame 클레스의 primeFactors method를 감시하며 감시결과의 Object는 2depth까지 확인
// (최대 4depth), defualt 감시항목은 {params, target, returnObj}
watch demo.MathGame primeFactors -x 2

 

 

첫 번째 결과는 parameter로 -122486이 들어왔으며 result는 null 이다.

두 번째 결과는 parameter로 1이 들어왔으며 resultsms [3,5,7,17,113] 이다

 

당시 Math-Game 로그

 

 

여기까지가 퀵스타트의 기술된 내용들이다. 

위의 내용 외에도 memory, monitor, trace 등 많은 옵션들이 있으니 시간 날 때 하나씩 확인해보도록 하자.


 

profile

최적화 팀에서 봤던 내용으로 jvm stack을 확인하며 동시에 어느 메소드에서 시간이 가장 오래 걸렸는지 확인해볼 수 있다.

 

다양한 옵션들이 있다.

 

어떠한 것을 감시할 것인가 list를 조회해볼수 있다.

최적화 팀에서는 wall 옵션을사용하였다.

 

https://github.com/async-profiler/async-profiler

 

GitHub - async-profiler/async-profiler: Sampling CPU and HEAP profiler for Java featuring AsyncGetCallTrace + perf_events

Sampling CPU and HEAP profiler for Java featuring AsyncGetCallTrace + perf_events - async-profiler/async-profiler

github.com

 

설명: 스레드 상태(실행 중, 휴면 중 또는 차단됨)에 관계없이 지정된 기간마다 모든 스레드를 동등하게 샘플링하도록 지시합니다.  ( 즉 어느 구간이 가장 오래 지연되는지 알 수 있는 것)

 

프로파일링 시작 event는 wall로

 

프로파일링 정지 결과 파일의 포멧은 html로

 

 

html을 열어보면

 

 

 

x축은 소요시간 y축은 하단부터 상단까지 JVM 호출 스택이다 즉 X축이 긴것이 소요시간이 오래 걸린것이다.

main함수를 찾아 클릭하면  Timeunit의 sleep을 사용한 것을 확인할수 있다.

 

그런데 MathGame Class의 run 메소드가 보이지 않는 이유를 모르겠다. 

기간에 대한 샘플링이기 때문에 sleep에 비해서 run의 동작시간이 너무 짧아서 보이지 않는 것일까.?

 

 

iframe으로 특정서버의 화면을 띄워야하는 일이 생겼다.

현재 운영되고 있는 서버에 추가적으로 batch Application을 올리고 iframe으로 호출하는 방식을 사용하기로 했다.

 


 

서버구성

 

 


문제1. batch 내부의 cookie 사용이 안되어 url에 jsessionId가 붙어감

예시로 /batch;jsessionid=~~~~~~~  형태로 잘못된 url이 생성되었다. 그로인데 Controller를 잘못찾아가는 문제가 발생

 

원인을 찾아보니 로컬에서 테스트 할 때 브라우저의 url을 localhost:8080~~~ 를 사용하였고 iframe의 주소는 
<iframe src="127.0.0.1:8099"..... 형식으로 사용하였기 때문에 발생

 

둘 다 같은 localhost로 변경하여 사용 하니 문제를 해결하였다.
다만 이로인해 운영에서 iframe으로 <iframe src="IP:PORT" ..... 구조를 사용 할수 없을 듯하였다.

 

운영에서는 <iframe src="도메인/batch" 형태로 사용하기로 한다.

 

 


문제2. nginx에 도메인/batch로 들어오면 2번 서버의 8099포트로 보내야한다.

 

배치서버는 1대지만 iframe 껍데기를 호출하는 서버는 2대 이다. 그렇다면 도메인/batch를 접속하게 된다면 LB를 통해
1번 서버 2번 서버 어느곳으로 갈지 알 수 없다.

 

그렇기 때문에 nginx에서 2번서버의 8099 포트로 이동 할수 있도록 proxy를 구성해야한다.

 

이를 위해 nginx의 default.conf를 수정

 

1번 서버, 2번서버 모두  location /batch/를 추가하여 어떤 서버로 요청이 오더라도 2번서버의 8099를 바라볼 수 있도록 구성

......

upstream backend{
	hash #remote_addr;
    server 1번서버아이피:8080;
    server 2번서버아이피:8080;
    
    keepalive 1024;
}

server {
	listen 443 lls;
    .... 
    기타 nginx 설정들, ssl 포함 
    
    ....
    
    location /batch/ {
    	proxy_pass http://2번서버 IP:8099;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    location / {
    	proxy_pass http://backend;
    }

 

 


문제3. https에서 http의 iframe을 사용 할 수 없음

브라우저에서 iframe으로 데이터를 불러올 때 데이터가 보이지 않아 console 을 확인해보니

 

에러로그가 발생

was loaded over an insecure connection. this file should be served over https

 

찾아보니 https 도메인을 사용하며 http를 iframe으로 사용할 수 없어서 발생하는 에러로그 

 

아마 배치 application에 

return "redirect:/" 와 같이 302 redirect를 발생시키며 생기는 문제로 보임.

 

nginx의 batch location에 강제로 http를 https로 변환해주도록 설정 추가

location /batch/ {
...
  proxy_redirect http:// https://
...
}

서버에 ssh로 접속 후 프로그램을 실행 후 ssh를 끊게 되면 해당 프로세스는 종료된다.

 

이때 사용 할 수 있는 명령어가 nohup이다

nohup java -jar -Dspring.profiles.active=dev test.jar 1> /dev/null 2>& &

 

nohup을 사용하면 nohup.out이라는 로그 파일이 생성되는데 이를 생성하지 않고자 한다면 

1> /dev/null 2>& 를 사용하도록 하자

let data = [
  {"id": "ROOT", "name" : "ROOT", "upperId": '', "level" : 0, "order": 0},

  {"id": "HOME",   "name" : "HOME",   "upperId": 'ROOT', "level" : 1, "order": 1},
  {"id": "MOBILE", "name" : "MOBILE", "upperId": 'ROOT', "level" : 1, "order": 2},
  {"id": "COMMON", "name" : "COMMON", "upperId": 'ROOT', "level" : 1, "order": 3},

  {"id": "cH1", "name" : "카테고리1", "upperId": 'HOME', "level" : 2, "order": 1},
  {"id": "cH2", "name" : "카테고리2", "upperId": 'HOME', "level" : 2, "order": 2},
  {"id": "cM3", "name" : "카테고리3", "upperId": 'MOBILE', "level" : 2, "order": 3},
  {"id": "cM4", "name" : "카테고리3", "upperId": 'MOBILE', "level" : 2, "order": 4},
  {"id": "cM5", "name" : "카테고리3", "upperId": 'MOBILE', "level" : 2, "order": 5},

  {"id": "c1s1", "name" : "시나리오1", "upperId": 'cH1', "level" : 3, "order": 2},
  {"id": "c1s2", "name" : "시나리오2", "upperId": 'cH1', "level" : 3, "order": 1},
  {"id": "c1s3", "name" : "시나리오3", "upperId": 'cH1', "level" : 3, "order": 3},
  {"id": "c2s4", "name" : "시나리오4", "upperId": 'cH2', "level" : 3, "order": 4},
  {"id": "c2s5", "name" : "시나리오5", "upperId": 'cH2', "level" : 3, "order": 6},
  {"id": "c2s6", "name" : "시나리오6", "upperId": 'cH2', "level" : 3, "order": 5},
  {"id": "c2s7", "name" : "시나리오7", "upperId": 'cH2', "level" : 3, "order": 7},
  {"id": "c3s8", "name" : "시나리오8", "upperId": 'cM3', "level" : 3, "order": 8},
]

function dataSort(data){
  return data.sort((a, b) => a.level - b.level || a.order - b.order )
}

function makeHierachy(data) {
  // root를 제외한 모든 id 별 포함되는데이터 추출(upperId를 사용)
  const dataMap = data.filter(x => x.level !== 0).reduce((acc, x) => {
    if (!acc[x.upperId]) {
      acc[x.upperId] = []
    }
    // order를 재정의
    x.order = acc[x.upperId].length + 1
    acc[x.upperId].push(x)

    return acc
  }, {})

  // 각 데이터들에 children에 데이터 할당
  for(const d of data) {
    if (dataMap[d.id]) {
      d.children = dataMap[d.id]
    } else {
      delete d.children
    }

  }

}

// 데이터 조회
function look(data) {
  console.log(String(data.name).padStart(data.level * 5)  )
  if(!data.children){
    return
  }
  for(const d of data.children){
    look(d)
  }
}

dataSort(data)
makeHierachy(data)
console.log(data)
// 루트부터 깊이 우선 탐색으로 조회
look(data[0])

 

 

+ Recent posts