0. 업무 중 불편사항

 

업무중 여러 설정 정보를 취합 하여 전달하는 API가 존재 한다.

해당 정보들은 RDB에 저장되어 있으며 일반적으로는 1개의 row가 존재하는 마스터 테이블이다.

 

해당 데이터들은 변경이 필요하지만 자주 변경은 되지않으므로 CRUD를 위하여 WEB과 API가 필요하다.
Ex) A 설정정보를 위해 A테이블에 대한 API CRUD와 WEB 개발 (데이터 1건)

B 설정정보를 위한 B 테이블에 대한 API CRUD와 WEB 개발 (데이터 1건)

C 설정정보를 위한 C 테이블에 대한 API CRUD와 WEB 개발 (데이터 1건)

... 등등등

단 해당 데이터들은 반드시 1건은 아니며 최대 5건 내외 정도 될 듯 하다 (3건 이상은 못 본듯 하지만 설계상 가능)

 

해당 건들을 조건에 따라 조합하여 1개의 종합 설정정보를 만들어 API로 전달한다.

 

위 데이터들을 위해 CRUD API와 WEB 개발 하는 것은 불필요해 보이며, 변동이 적고, 조건에 따른 조합이 필요했다.

 

이를 위해 WorkFlow를 도입하여 테스트 해보고자 했다

 


1. Node-Red 란 무엇인가.

Node-RED(노드 레드)는 하드웨어 장치들, API, 온라인 서비스를 사물인터넷의 일부로 와이어링(배선화)시키기 위해 본래 IBM이 개발한 시각 프로그래밍을 위한 플로 기반 개발 도구이다.

Node-RED는 브라우저 기반 플로 편집기를 제공하므로 자바스크립트 함수를 개발하는데 사용할 수 있다. 애플리케이션의 요소들은 재사용을 위해 저장하거나 공유할 수 있다. 런타임은 Node.js 위에서 개발되어 있다. Node-RED에서 만든 플로는 JSON을 사용하여 저장된다. 버전 0.14 이후 MQTT 노드들은 적절하게 구성된 TLS 연결을 만들 수 있다.

2016년에 IBM은 Node-RED를 오픈 소스 "JS Foundation" 프로젝트로 기여했다.

출처 위키백과

 


2. Node-Red 설치

2-1. 도커 설치

  • 추후 docker compose로 구성하여 volume 또한 잡아주어야 관리가 용이하다.
docker run -it -p 1880:1880 -v node_red_data:/data --name mynodered nodered/node-red

 

2-2. 로컬 설치

  • 주의 Node.js 설치가 필요하다.
npm install -g --unsafe-perm node-red

 

실행

node-red

 

 

접속

브라우저에서 http://localhost:1880

 


3. Node-Red에서 RestAPI 테스트

왼쪽의 팔레트에서 http in, function, http response를 드래그하여 플로우 화면에 이동 시킨 후 각 컴포넌트들을 연결한다.

 

 

 

http in을 더블클릭하여 수정

 

function을 클릭하여 수정

 

http response 수정 후 오른쪽 상단의 배포하기 클릭

 

 

http://localhost:1880/apiTest로 테스트


4. 추가 컴포넌트 설치

시간에 따른 분기처리하는 컴포넌트가 기본으로 제공 되지 않는다.

 

오른쪽의 = 메뉴 > 팔렛트 관리 > 설치 가능한 노드 > node-red-contrib-time-switch 검색 > 설치

 

 

기능 노드 > time switch가 생긴 것을 확인 할 수 있다.


5. 샘플 설정 API 구성해보기

5-1. 전체 구성

 

5-2. 구성 설명

  1. request로 codeValeu를 받는다.
  2. codeValue별 projectId를 다르게 세팅한다.(스위치 노드 사용하여 분기)
  3. target을 설정하는데 나중에 변경 할 수 있도록 A노드를 만들어두고 연결을 끊어둔다.(미사용 처리)
  4. 현재 시간에 따라 ment를 세팅한다. (타임 스위치 노드 사용하여 분기)

5-3. 테스트


6. 내보내기/가져오기

 

현재플로우/ 전체 플로우를 export, import가 가능하다. 해당 데이터는 json으로 만들어지며 파일/클립보드 형식으로 사용할 수있다.

 

테스트 샘플 json 공유

[
    {
        "id": "389108cf5e372918",
        "type": "tab",
        "label": "API 테스트",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "5c24f9ea6863049d",
        "type": "http in",
        "z": "389108cf5e372918",
        "name": "propertiesSetting",
        "url": "/apiTest",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 200,
        "y": 260,
        "wires": [
            [
                "691f00ddb0f0f4d1"
            ]
        ]
    },
    {
        "id": "691f00ddb0f0f4d1",
        "type": "switch",
        "z": "389108cf5e372918",
        "name": "code 별 분기처리",
        "property": "payload.codeValue",
        "propertyType": "msg",
        "rules": [
            {
                "t": "btwn",
                "v": "10001",
                "vt": "num",
                "v2": "10005",
                "v2t": "num"
            },
            {
                "t": "btwn",
                "v": "10006",
                "vt": "num",
                "v2": "10010",
                "v2t": "num"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 430,
        "y": 260,
        "wires": [
            [
                "dd2e6d4c3f91d1a1"
            ],
            [
                "8d56be140949ad6c"
            ]
        ]
    },
    {
        "id": "dd2e6d4c3f91d1a1",
        "type": "function",
        "z": "389108cf5e372918",
        "name": "projectNum01",
        "func": "msg.payload.projectId='projectNum01'\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 660,
        "y": 220,
        "wires": [
            [
                "23994dbc840a7ce6"
            ]
        ]
    },
    {
        "id": "411363cd9bd10a0c",
        "type": "comment",
        "z": "389108cf5e372918",
        "name": "A 설정 세팅(projectId)",
        "info": "",
        "x": 680,
        "y": 160,
        "wires": []
    },
    {
        "id": "8d56be140949ad6c",
        "type": "function",
        "z": "389108cf5e372918",
        "name": "projectNum02",
        "func": "msg.payload.projectId='projectNum02'\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 660,
        "y": 300,
        "wires": [
            [
                "bbf5e59569ef9c5e"
            ]
        ]
    },
    {
        "id": "23994dbc840a7ce6",
        "type": "function",
        "z": "389108cf5e372918",
        "name": "target(local)",
        "func": "msg.payload.targetUrl='localhost'\nmsg.payload.targetPort='8080'\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 970,
        "y": 220,
        "wires": [
            [
                "735dd47aa923edda"
            ]
        ]
    },
    {
        "id": "8db7557e970a9a9d",
        "type": "comment",
        "z": "389108cf5e372918",
        "name": "B 설정 세팅(target)",
        "info": "",
        "x": 950,
        "y": 160,
        "wires": []
    },
    {
        "id": "21a72f6b7be84d83",
        "type": "function",
        "z": "389108cf5e372918",
        "name": "target(serverA)",
        "func": "msg.payload.targetUrl='http:serverA.com'\nmsg.payload.targetPort='8081'\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1180,
        "y": 280,
        "wires": [
            [
                "735dd47aa923edda"
            ]
        ]
    },
    {
        "id": "bbf5e59569ef9c5e",
        "type": "function",
        "z": "389108cf5e372918",
        "name": "target(ServerB)",
        "func": "msg.payload.targetUrl = 'http:serverB.com'\nmsg.payload.targetPort='8082'\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 980,
        "y": 340,
        "wires": [
            [
                "735dd47aa923edda"
            ]
        ]
    },
    {
        "id": "b584ee422c183395",
        "type": "comment",
        "z": "389108cf5e372918",
        "name": "C 설정 세팅 분기 (ment)",
        "info": "",
        "x": 240,
        "y": 440,
        "wires": []
    },
    {
        "id": "735dd47aa923edda",
        "type": "time-switch",
        "z": "389108cf5e372918",
        "name": "근무시간 여부",
        "lat": "37.564214",
        "lon": "127.001699",
        "startTime": "09:00",
        "endTime": "18:00",
        "startOffset": "0",
        "endOffset": 0,
        "x": 140,
        "y": 500,
        "wires": [
            [
                "2117890feccac6aa"
            ],
            [
                "4e28e016aecb2cd3"
            ]
        ],
        "outputLabels": [
            "근무시간",
            "근무 외 시간"
        ]
    },
    {
        "id": "2117890feccac6aa",
        "type": "function",
        "z": "389108cf5e372918",
        "name": "근무시간",
        "func": "msg.payload.ment=\"근무시간 입니다.\"\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 440,
        "y": 480,
        "wires": [
            [
                "6b3c1c70b041e627"
            ]
        ]
    },
    {
        "id": "4e28e016aecb2cd3",
        "type": "function",
        "z": "389108cf5e372918",
        "name": "근무외 시간",
        "func": "msg.payload.ment=\"근무외 시간입니다.\"\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 450,
        "y": 540,
        "wires": [
            [
                "6b3c1c70b041e627"
            ]
        ]
    },
    {
        "id": "6b3c1c70b041e627",
        "type": "http response",
        "z": "389108cf5e372918",
        "name": "",
        "statusCode": "200",
        "headers": {},
        "x": 720,
        "y": 500,
        "wires": []
    },
    {
        "id": "cf30e87595d4e294",
        "type": "comment",
        "z": "389108cf5e372918",
        "name": "C 설정 세팅(ment)",
        "info": "",
        "x": 450,
        "y": 440,
        "wires": []
    }
]

Vue2 코드에서 Vue3로 변경하는 작업을 진행해야 하는 일이 생겼다.

변경 작업을하며 Node의 버전도 올려야 하는데 두 소스 모두 기동하면서 비교하면서 작업을 진행해야한다.

 

vscode에 .vscode 디렉토리 생성 후 

settings.json 생성

{ 
"terminal.integrated.env.windows" : { "PATH": {node 설치 경로}}

}

 

 

대용량 데이터를 생성하고 다운로드 하는 api가 있는데 데이터가 늘어나다보니 60초 이상이 걸리고 api에서는 timeout이 발생하여 결과를 받지 못하는 상황이 생겼다.

 

개발환경에서 테스트를 하기위해 임시 api를 만든다.

 

url로 /public/10 과같이 값을 입력 받아 Thread.sleep으로 강제 지연을 발생 시킨다.

@Controller
@RequestMapping(value = "/public")
public class HealthController {

	@GetMapping("/{time}")
    public ResponseEntity healthCheck(@PathVariable("time") long time) {
    	try {
        	Thread.sleep(time * 1000L)
        }catch (Exception e) {
        	e.printStackTrace();
        }
    }

}

 

 

nginx의 설정 추가

proxy_read_timeout은 nginx가 백엔드로부터 응답을 받기까지 대기하는 시간이다. 즉 client는 nginx로 요청을 보내고 nginx는 client의 요청을 다시 backend로 보내는데 nginx와 backend 통신이 proxy_read_timeout 기간내로 결과를 주지 못한다면 연결을 끊는다는 의미다.(default는 60초이다.)

location ^~ /public/ {
....
	proxy_read_timeout 120;
....
}

 

변경전에는 /public/61 으로 테스트 할 때 timeout이 발생하였으나, 변경 후 정상적으로 데이터를 받아온느 것을 확인하였다

 

추가적으로 axios(혹은 ajax), tomcat 등의 설정에서 nginx보다 더 작은 값의 timeout이 설정되어 있다면 가장 작은 값을 따라감으로 추가적인 확인이 필요하다.

 

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

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

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

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


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의 동작시간이 너무 짧아서 보이지 않는 것일까.?

 

 

+ Recent posts