안녕하세요 깍돌이입니다.

 

기존 포스팅에 이어서 

 

바로 다음 포스팅에서는 최대워커 수만큼 호출시 와 지금 은 함수를 return new Promise로 해서 Promise.all 로 받았지만

사실 진짜 원하는건 멀티스레드아니였나 

A , B함수에서A가 계속 루프돌고 CPU 연산을 하면 B는 실행도 못하는게 실행되는게 아니였나? 

라는 저만의 질문에 답을 하기 위한 포스팅입니다.

일단 다시 한번 제가 하려고했던 기능을 정리하겠습니다.

A 함수

0.2초마다 A라는 배열을 채웁니다.

B 함수

0.2초마다 A라는 배열에 값이 차면 꺼내와서 테스트 플랫폼에 해당 내용으로 자동화를 요청합니다.

기존 포스팅에서 return new Promise로 했었기 때문에 사실 멀티스레드를 흉내내는지 궁금했습니다.

5개의 함수가 각각 CPU 연산을 할 경우 어떻게 되는가? 입니다.

 

그럼 실제 연산을 해야되니까 

 

워커 코드를 아래와같이 변경하였습니다.

Worker.js

'use strict'

// 테스트 용 
let memoryArray = [];

let worker ={
  today_resource_filtering,
  filtered_resource_request_automation,

  today_resource_filtering_node_fn,
  filtered_resource_request_automation_node_fn,

  dummy_fn,
}

let g_closeCondition =100000000;

function today_resource_filtering_node_fn(){
      let i=0;
      let calculatorNumber = 0;
      console.time('today_resource_filtering_node_fn');
      while(true && i < g_closeCondition){
        i++;
        calculatorNumber+=i;
      }
      console.timeEnd('today_resource_filtering_node_fn');
      console.warn(calculatorNumber);
}

function filtered_resource_request_automation_node_fn(){
      let i=0;
      let calculatorNumber = 0;
      console.time('filtered_resource_request_automation_node_fn');
      while(true && i < g_closeCondition){
        i++;
        calculatorNumber+=i;
      }
      console.timeEnd('filtered_resource_request_automation_node_fn');
      console.warn(calculatorNumber);
}

function dummy_fn(name){
  let increNumber=0;
  console.warn(`========Start ${name}`);
  for(let i=0;i<g_closeCondition*50;i++){
      increNumber+=i;
  }
  console.warn(`========End Dummy Fn ${name}`);

}





module.exports = worker;

 

Node.js ( basic ) 

단순 호출하는 Node.js 입니다. 

let worker = require('./filter_and_request.js');

console.time('using_basic_js');

// 1억 까지 더하는 함수 
console.warn('---------------------Start Today Resource node fn')
worker.today_resource_filtering_node_fn();
console.warn('\r\n');

// 1억 까지 더하는 함수 
console.warn('---------------------Start filtered_resource_request_automation node fn')
worker.filtered_resource_request_automation_node_fn();
console.warn('\r\n');

// 50억 까지 더하는 함수 5번 호출 
for(let i=0;i<5;i++){
  console.warn(`-------------Start dummy calculator fn ${i+1}`);
  worker.dummy_fn(i+1);
  console.warn('\r\n');
}

console.timeEnd('using_basic_js');

 

 

Piscina 사용

const path = require('path');
const Piscina = require('piscina');

const piscina = new Piscina({
  filename:path.resolve(__dirname,'filter_and_request.js')
});

console.time('using_Piscina_js');

// 1억 까지 더하는 함수 
console.warn('---------------------Start Today Resource node fn')
piscina.run({},{name:"today_resource_filtering_node_fn"});
console.warn('\r\n');

// 1억 까지 더하는 함수 
console.warn('---------------------Start filtered_resource_request_automation node fn')
piscina.run({},{name:"filtered_resource_request_automation_node_fn"})
console.warn('\r\n');

// 50억 까지 더하는 함수 5번 호출 
for(let i=0;i<5;i++){
  console.warn(`-------------Start dummy calculator fn ${i+1}`);
  piscina.run(i+1,{name:"dummy_fn"});
  console.warn('\r\n');
}

console.timeEnd('using_Piscina_js');

 

dummy_fn 의 시간을 재기 위해 console.time을 추가했습니다.

function dummy_fn(name){
  let increNumber=0;
  console.warn(`========Start ${name}`);
  console.time(`Check Time ${name}`)
  for(let i=0;i<g_closeCondition*50;i++){
      increNumber+=i;
  }
  console.warn(`========End Dummy Fn ${name}`);
  console.timeEnd(`Check Time ${name}`)

}

 

결과

Node.js 호출  ( 순차적으로 실행되며 -  57초 발생

밑에 57초라는게 결국 console.time에서 console.timeEnd가 만나는 시점에 결정되는건데 결국 앞단의 함수들이 CPU 를 집약적으로 사용하고있으면 뒤에 함수는 실행도 못하고 대기하고 있다는 뜻이 됩니다. 

100ms + 100ms + 5s + 5s + 15s + 15s + 15  = 대략 57000 = 57초  ( 동기식 ) 

Piscina 호출 

일단 코드 Blocking되는 부분이 없습니다. using_Piscna_js가 6ms입니다 ( 6s아니고 6s  0.06초입니다. )

이부분을 보면 테스트를 해야하는 코드는 바로 실행하고 넘어갔음을 알수 있습니다.

 

이로써 Piscina가 worker thread 를 정상적으로 잘 쓰고있는지 정말 worker thread pool 은 기존의 노드 코드를 Blocking하지 않는지 직접 확인해봤는데요 

 

마지막 포스팅은 제가 원래 하려고했던 작업 ( 하나의 배열을 한쪽은 계속 필터링해서 넣고 -> 한쪽은 배열 차면 꺼내서 테스트 자동화 플랫폼에 테스트 요청을 보내는 ) 에 대한 코드를 올리겠습니다.

 

 

 

 

안녕하세요 오랜만에 글을 쓰게 되는데요 

 

여기서는 궁금증을 다 풀어갈 예정입니다.

 

목적부터 말씀 드리면

 

A 함수

0.2초마다 A라는 배열을 채웁니다.

B 함수

0.2초마다 A라는 배열에 값이 차면 꺼내와서 테스트 플랫폼에 해당 내용으로 자동화를 요청합니다.

 

문제점

2개의 함수 모두 계속 loop을 돌면서 진행하게되는데 배열을 채우는 A함수는 지속적인 CPU I/O 가 발생하게 됩니다. 

Node.js에서는 기본적으로는 멀티스레드가 안되기 때문에 저번 포스팅에서 Piscina를 사용하는 예제를 보여드렸었습니다.

 

그래서 그에 대한 기본 샘플 파일을 작성 합니다.

 

Worker

'use strict'

// 테스트 용 
let memoryArray = [];

let worker ={
  today_resource_filtering,
  filtered_resource_request_automation
}
function today_resource_filtering(){
  return new Promise(async(resolve,reject)=>{
    try {
      
      let i=0;
      let closeCondition = 1000;
      while(true && i < closeCondition){
        console.warn(`today_resource_filtering : ${i}`);
        i++;
        // await wait(1000);
      }
      console.warn('Test Complete');

      resolve(true);
    } catch (error) {
      reject(error);
    }
  })
}
function filtered_resource_request_automation(){
  return new Promise(async(resolve,reject)=>{
    try {
      console.warn('Request Server Resource');


      let i=10;
      let closeCondition = 1000;
      while(true && i < closeCondition){
        console.warn(`resource_automation_request : ${i}`);
        i++;
      }
      console.warn('Test Complete');
      resolve(true);
      
    } catch (error) {
      reject(error);
    }
  })
}
// 테스트용 유틸 

function wait(time){
  return new Promise(async(resolve)=>{
    setTimeout(()=>{
      resolve(true);
    },time)
  })
}

module.exports = worker;

 

 

Using Piscina

const path = require('path');
const Piscina = require('piscina');

const piscina = new Piscina({
  filename:path.resolve(__dirname,'filter_and_request.js')
});

(async function(){
  const res = await Promise.all([
    piscina.run({},{name:"today_resource_filtering"}),
    piscina.run({},{name:"filtered_resource_request_automation"}),
  ]);

  console.warn(res);
})();

Not Using Piscina

let worker = require('./filter_and_request.js');




(async function(){
  const res = await Promise.all([
    worker.filtered_resource_request_automation(),
    worker.today_resource_filtering()
  ]);

  console.warn(res);
})();

 

Using Piscina 결과부터 보겠습니다.

 

엥? 멀티스레드라고 되어있었는데 한 작업이 끝나야 다른작업이 시작됩니다. 

 

기존의 worker의 내용을 바꿔보겠습니다.

 

function today_resource_filtering(){
  return new Promise(async(resolve,reject)=>{
    try {
      let i=0;
      let closeCondition = 1000000000;
      while(true && i < closeCondition){
        i++;
        // await wait(1000);
      }
      console.warn('Test Complete');

      resolve(i+'==today_resource_filtering');
    } catch (error) {
      reject(error);
    }
  })
}

closeCondition 값을 바꾸면서 반복을 하고 

 

(async function(){
  console.time('using_basic_js');
  const res = await Promise.all([
    piscina.run({},{name:"today_resource_filtering"}),
    piscina.run({},{name:"filtered_resource_request_automation"}),
  ]);
  console.warn(res);
  console.timeEnd('using_basic_js');
})();

각각의 실행자에서 이렇게 테스트해보겠습니다.

 

100000부터 0을 하나씩 추가하면 (워커가 2개니까 *2 하시면 될 것 같습니다.)

 

1. 1000000  (20만)     ( Piscina - 73ms   vs Node.js (Single Thread )  3ms )

오잉? 왜 피시나에서 더 걸리는거같죠? 

2. 10000000  ( 2000 만 ) ( Piscina - 74ms   vs Node.js (Single Thread )  13ms )

천만 정도 루프시 특이점이 노드는 10 ms 증가 피시나는 7 정도 증가 했습니다.

3. 100000000 ( 20억 ) ( Piscina - 545 ms   vs Node.js (Single Thread )  950ms )

둘의 차이가점점 좁혀집니다.

 

4. 10000000000 (200억) ( Piscina - 9002 ms   vs Node.js (Single Thread )  17767ms )

100억번의 루프 -> 워커가 2개기 때문에 어찌보면 200억 인데

여기서부터 차이가 나기 시작합니다. ( Piscina의 진가가 발휘됩니다. )  

 

 

5. 마지막으로 (2000억 번 순환 ++시 ) ( Piscina - 1m 33s   vs Node.js (Single Thread )  3m 05ss )

점점 늘어날수록 차이가 벌어집니다. 2000억 순환시 3배의 가량이 차이가 발생하게 됩니다. 

-> 기존의 10만 100만정도는 piscina의 인스턴스를 생성하기 위한 기본 세팅값에서 들어가는 작업에 대한 시간으로 보입니다.

 

1차 정리 

Piscina는 기본 세팅 시 70ms 정도가 소요 된다.

Promise의 2개의 워커를 사용시 루프시간에 따라 다르지만  3배까지도 차이가 발생

 

 

바로 다음 포스팅에서는 최대워커 수만큼 호출시 와 지금 은 함수를 return new Promise로 해서 Promise.all 로 받았지만

사실 진짜 원하는건 멀티스레드아니였나 

A , B함수에서A가 계속 루프돌고 CPU 연산을 하면 B는 실행도 못하는게 실행되는게 아니였나? 라는 의문에 대한 해결을 위한 포스팅을 하겠습니다.

 

 

안녕하세요 깍돌이 입니다. 오랜만에 인사드리네요 기존에 작업했던 내용들 ( UI ) 혹은 그 뒤에 작업했던 내용들이

 

사실 거의 다 대외비성이라 이게 .. 참 포스팅을 할수 없다는게 아쉽네요 

 

이번에도 시작은 이렇게 하고 또 대외비로 빠질진 모르겠지만..  나름의 큰 프로젝트를 하고있기도 하고 이번 UI는 있는거 에 한해서 이래저래 조합조합 해서 진행할 예정입니다.

 

PlayWright 입니다. 핫하기도하고 팀내에서 먼저 쓰고 있는 분들이 있기도하고 일단 간단하게 맛만 봤었을때 특이점이 하나 있어서 선택해보게 되었습니다.

 

물론 저는 셀레니움 버전 1 , 버전 2(RC타입) , 퍼펫티어도 어느정도 해봤으니 이래저래 팀내 프로젝트도 진행하고있고 여러 이유를 이용해서 이걸 시작해도 되지만 같이 해야 하는 인원이 있는 만큼  여러 가지 고민중 선택하게 된 계기는 

러닝커브가 매우 낮다 입니다.

 

셀레니움에서 고민해야될 부분 들이 100가지가 있다면 실제로 playwright 에서는 없다고 봐도 무방합니다. (기본적인 트러블 슈팅이 아닌 근본적인 고민 부분에 대한 이야기입니다. ) 

 

대표적으로 Find Element 에 대한 모듈 분리 및 다른 전략들이 필요로 하는데 이중에 예외처리 해야되는 것들 중 자동화 하시는 분들이 어려워하는게 Element 를 찾지 못하는 경우 입니다.  해당 Text 값같은 걸 찾는 경우 

Element에서 inner Text인지 textContents인지 value인지 data-property 인지 매번 예외처리를 해주었어야 했지만 playwright 에서는 해당 부분을 전부 자체 개발된 API 를 통해서 지원해주기 때문에 러닝커브가 매우 낮다고 판단하였습니다.

 

https://playwright.dev/docs/intro

 

Installation | Playwright

Playwright Test was created specifically to accommodate the needs of end-to-end testing. Playwright supports all modern rendering engines including Chromium, WebKit, and Firefox. Test on Windows, Linux, and macOS, locally or on CI, headless or headed with

playwright.dev

 

기본적으로 설치 하고 튜토리얼을 해보면서 Puppeteer와 차이점? 같은 부분을 적고 정리 하는 과정을 적어 보겠습니다.

 

( 솔직히 PlayWright 검색하면 다 누구껄 복붙했는지 다 같은 내용의 블로그 밖에 없네요 ;; 인생 좀 쉽게 쉽게 가고 싶었지만 한땀 한땀 다시 찾아서 포스팅을 시작.. 추후에는 또 어떻게 될지 모르겠지만 ) 

 

설치나 이런것들은 여기저기 많이 있으니까 제외 튜토리얼 대로 

npm init playwright@latest 아래와같이 설치 

타입스크립트 선택

E2E 테스트 파일을 어디에 넣을건가 -> tests

Github Actions  - Yes

수동 실행 브라우저 설치 - Yes

 

 

설치 후 나오는 설명을 보면서 하나씩 다 해보게 되면

npx playwright test 

e2e 테스트를 실행한다고 되어있는데 이게 무슨 기준일까 해서 실행해보니

프로젝트 - tests 폴더에 example.spec.ts 를 실행합니다. 

-> 실행 경로는 playwright.config.ts에서 바꿀수 있지만 이건 나중에 다시 적겠습니다.

 

npx playwright test --ui

UI 모드로 실행하는 명령어입니다.

폴더에 tests - example.spec.ts를 기준으로 나타나게 되네요

 

클릭하는 시점에 어딜 누르는 지도 보여줍니다.

 

로그도 있고 잘만들어진거 같습니다. 당장은 쓸일이 있나 모르겠네요 하면서 경험적인 내용들을 포스팅하겠습니다.

 

npx playwright test --project=chromium

플레이라이트 내부에서는 여러 프로젝트들이 있는데 그 부분들 중에 선택해서 진행할수 있게 하는 명령어입니다.

TS다보니 해당 리스트가 type으로 표현되어있는데요 

 

수많은 장치들이 있고 맨 밑에 저희가 대부분 써야 할 

데스크탑 앱이 있네요 저게 다 되는지 어떻게 차이가있는지 그냥 해상도로만 구분한건지는 좀더 확인해봐야 할거 같습니다.

 

npx playwright test example

파일을 선택하여 실행한다고 되어있는데 

도대체 example을 뭘로보고 실행하는건지 이해가 되지않았습니다.  파일명의 spec.ts를 무시하고 실행한다 해도

changeexample로 제가 바꿔놨기 때문입니다. 그래서 몇번의 테스트를 해보았는데

 

일단 npx playwright test 명령어를 치게되면 config의 testDir을 기준으로 바라 보게 됩니다.

그래서 1차적으로 해당 폴더의 위치를 하게 되고 파일명을 변경해가면서 테스트를 해보니 

 

demo-todo-app.spec.ts에서

 

demo 도 되고 todo 되고 app 되고 - 도 됩니다. 결론은 like 검색으로 포함되어있다면 해당 파일을 실행하는거같습니다.

둘다 demo를 넣고 실행하게되면  아래의 사진은 demo가 먼저 걸리는 파일을 실행하는거같은데 이 순서를 바꿔보겠습니다.

순서를 바꿔서 1_demo 로 앞에 오는 것을 기대하고 테스트를 해보았으나 기존과같이 밑에 demo-todo를 실행하게 됩니다.

1_demo ( 케이스 2개 )

demo-todo ( 24개 ) 

조금 이상함을 느껴서 npx playwright test로 전체 테스트를 돌렸으나

전체가 돌아가지 않음을 확인했습니다. 

그래서 다시 이상함을 느끼고 확인해보니

.spec 이 들어가야 같이 돌아가는걸로 보여 다시 재 테스트를 하였습니다.

 

다시 demo를 공통으로 넣으니

 

다 돌아가는거 보면 demo가 포함되는 모든 것들이 돌아가는걸로 보입니다.

 

마지막 테스트 ppap 로변경하고 실행

 

 

tc-seocnd.spec.ts로 변경하고 직접 실행

spec을 제외하고 직접 실행

 

결론 : spec이 들어가야 실행이 가능 

npx playwright test --debug

디버그로 실행하여 한땀한땀 실행하면서 내용을 확인할수 있게 됩니다.

Resume을 누르면 그냥 통으로 실행하고 다음 스텝으로 넘어갑니다.  Step over 는 해당 테스트 안에서 한스텝씩 넘어 갈수 있습니다.

 

npx playwright codegen

말 그대로 코드젠입니다. 내가 하는 액션들을 코드로 간단하게 먼저 뽑아주는 역할을 합니다.

 

npx playwright test

 

 

여기까지가 기본 OVER VIEW였습니다.

 

포스팅 시간이 짧아서 OVER VIEW 2탄으로 돌아오겠습니다.

 

2탄 내용은

 

playwright.config 옵션에 대해

 

github action 연결 

 

내부 test모듈 구조 

 

왜 이 틀을 구조가 강제화되어있는가

 

worekr 구조 

개인적인 궁금증 해결 입니다.

 

 

 

 

 

안녕하세요 깍돌이 입니다.

 

옛날에는 학습하면서 있던 트러블 슈팅들을 전부 포스팅 했었는데 솔직히 변명이지만 현재 너무 바쁘게 일상생활을 살아가고있어서 ( 노는거 아님 ) 

 

오늘은 테라폼(IAC) 를 이용하여서 NCP ( Naver Cloud Platform ) 에 인프라를 세팅 할수 있도록 적용해보려고 합니다.

IAC에 대해서는 구글에 검색하면 엄청 설명 잘되어있는 곳이 있기 때문에 생략 하겠습니다. 

우선 IAC에 대해서는 대략적으로는 알고 있는 상태이기 때문에 테라폼에 대해서 알아 보겠습니다.

What is Terraform?

이제 앞으로 작성되는 개요에서 설명은 공식 홈페이지를 참고합니다. 

https://developer.hashicorp.com/terraform/intro

내용들을 읽다 보면 "클라우드 및 온프레미스 리소스를 안전하고 효율적으로 빌드, 변경 , 버전화 할수 있는 인프라 도구" 라고 되어있습니다.  대표적으로는 AWS , Azure, GCP, OCP , Docker 등을 설명하고 있습니다.

 

테라폼 구조

기본적으로 테라폼은 PROVIDER를 통해서 벤더 클라우드사의 API(OPEN API ) 와의 연동을 통해서 진행되는 구조임을 알수 있습니다.

크게 3가지의 구조를 가진다고 합니다.

Write : 리소스를 정의 (VPC , Subnet, VM 등의 배포 구성)

Plan : 기존 인프라 및 구성을 기반으로 생성, 업데이트 , 삭제 할 인프라를 어떻게 할지에 대한 계획을 설정

Apply : apply 할 경우 모든 리소스 종속성을 고려하여 올바른 순서대로 작업을 수행 ( vpc, subnet, vm 이 있다면 vpc-> subnet- vm 으로 올바른 순서를 찾아서 수행 ) vpc를 수정하게될 경우 확장전의 vpc를 재생성 

 

위와같은 순서를 보게 될 경우 크게는

 

API -> Write -> Plan -> Apply 같은 순서로 진행됨을 알수 있습니다. 

API : NCP의 ACC,SEC 설정

Write : .tf 소스를 작성하는 구간으로 어떤식으로 자원을 구성할지에 대한 설정

Plan : Write 에 작성된 tf를 읽어서  생성이 되는지 오류가 없는지 어떻게 생성되는 등에 대한 내용 을 확인

Apply : 현재 작성된 내용으로 인프라를 적용

 

Terraform

설명은 대충 여기까지만 하고 Windows 에서 설치 및 동작을 확인해보겠습니다.

https://developer.hashicorp.com/terraform/downloads

스펙에 맞는 테라폼 다운로드 및 압축 해제 ( zip으로 되어있고 압축 해제하면 terraform.exe만 나옵니다 )

C:\terraform 폴더 생성 후 이동

 

Windows - 고급 시스템 설정 - 고급 - 환경 변수 ( N ) 에서 

path 에 편집으로 테라폼 경로 추가  및 terraform --version 테스트

1.3.7 Windows_386 확인

기본적인 샘플 파일 작성 및 동작 확인을 위해서 main.tf , outputs.tf , variables.tf, versions.tf 를 작성합니다.

main.tf

provider "ncloud" {
  access_key = var.access_key
  secret_key = var.secret_key
  region     = var.region
}

data "ncloud_regions" "regions" {
}

data "ncloud_server_images" "server_images" {
}

resource "ncloud_server" "server" {
  name                      = var.server_name
  server_image_product_code = var.server_image_product_code
  server_product_code       = var.server_product_code
}

 

variable.tf

variable "access_key" { 
  default = ""
}

variable "secret_key" { 
  default = ""
}

variable "region" {
  default = "KR"
}

variable "server_name" {
  default = "terraform-test"
}

variable "server_image_product_code" {
  default = "SPSW0LINUX000046"
}

variable "server_product_code" {
  default = "SPSVRHICPUSSD002" 
}

versions.tf

terraform {
  required_version = ">= 0.13"
  required_providers {
    ncloud = {
      source = "navercloudplatform/ncloud"
    }
  }
}

terraform plan 후 terraform apply 하면 서버를 생성합니다. 

그리고 server_name 을 변경시

위와같이 plan 이 나오고  terraform apply 시 변경이 시작됩니다.

 

** 주의사항 기본 샘플 파일로 이것저것 하다가 문제점은 아니지만 기대결과랑 다른 경우가 있습니다.

NCP에서 서버는 server_image_product_code ( OS 코드 ) 와 server_product_code ( OS에 따른 스펙코드 ) 쌍으로 이루어지게 됩니다.

 

테라폼에서 OS코드에 맞지 않는 스펙코드를 사용할경우 500이 발생하며 중지되는건 이해가 됩니다. 

예를들면 Windows는 100G만 지원하지만 windows 2016에 리눅스 스펙코드를 넣으면 (리눅스는 only 50g) 오류가 나는게 정상이니까요 

 

하지만 지원되는 스펙 코드지만 오류가 나는 케이스가 있습니다.

NCP의 경우 Classic과 VPC 2개의 플랫폼을 제공하고 있는데요 

server spec change에서 다른점이 하나 있습니다. VPC는 Standard -> HiCPU 로의 타입을 넘어선 스펙변경이 가능하다는 점이고 Classic은 불가능 하다는 점입니다.

Classic에서 변경이 되는 타입은 Compact, Standard 입니다. 그외에는 모두 같은 타입에서만 스펙 변경이 가능합니다.

 

VPC같은 경우는 모든 스펙 을 넘어선 변경이 지원되기 때문에 문제가 되지 않습니다. 

 

그래서 저는 Terraform 은 해당 인프라를 구성해주는 역할을 한다고 생각 했기에 hicpu 로 만들어진 Classic서버를 Standard로 하고 apply 하면 오류가 없을거라 생각 했습니다.

 

HICPU -> Standard 로 변경시

오류 발생 

Classic에서는 지원되지 않는게 정상이기 때문에 API 를 통해 벤더사와 연결하는 테라폼에서는 위와같은 에러를 정상적으로 받고 apply 를 종료하게 됩니다.  아무생각없이 이경우라면 저는 다시한번 재시도해서 정지 -> 반납 -> 원하는 스펙으로 생성 해주길 기대했는데 그렇진 않았습니다.

 

서버 이름 변경시에는 terminate -> create 지만

서버 스펙 변경 은   stop -> modify -> boot (또는 modify)같은 형태로 넘어가게 됩니다.  테라폼에서 말하는 멱등성에서도 벤더사의 케이스에 따라 오류가 나는 부분이 있을수 있기 때문에 학습 하는 과정에서 이런 예외처리등을 많이 확인해야 될거 같습니다.

 

* 추가 - 서버 설명 변경도 벤더사에서 지원해주는 API 가 없기 때문에 stop - terminate -> create(이때 설명을 새로 넣음) 

같은 형태로 넘어가게 됩니다. 별거 아닌거 수정한다고 건드리면 자주 지우고 만들거 같네요

 

다음 포스팅은 data, resource 및 파일 구조 terraform plan, terraform apply 시 어떤 동작이 되는지에 대해 작성하겠습니다.

 

 

 

 

안녕하세요 깍돌이입니다.

 

자동화 시스템 운영중 Network Traffic Automation System 에 관련된 내용을 하나 적으려고 합니다.

 

시스템 구조상 명령을 내리는 Command Server 와 명령을 받아 행동을 하는 (자동화 케이스의 기능 ) Agent Server 가 있습니다.

 

Agent는 ONE Source 로 되어있습니다.  기존에는 VM 1대 = Agent 1대 였습니다. 하지만 이렇게 사용하게되면 네트워크가 복잡함에 따라서 TC를 늘릴때 마다 VM이 늘어나는 일이 발생하였으며 이로 인해서 효율화를 요청 받았습니다.

 

그로 인해서 케이스를 복잡하게 만들게 되었고 기존에는 1개의 포트 ( 6500Port ) 를 사용하였다면 지금은 VM 1개에서 6500 7500 8500 ~ .. .등으로 사용하게 되어 훨씬 더 적은 VM 으로 많은 케이스를 사용하게 되었습니다.

 

해당 포트로 운영하다 발생한 이슈가 하나 있어 공유 드리려고 합니다. 

 

spawn tcpdump ENOENT

UDP의 트래픽을 캡처 및 검증 하기 위해서 Node.js에서 Child_process의 spawn 을 사용하고 있으며

해당 spawn 에서 tcpdump (OS 레벨) 에서 직접 사용하고 있습니다.

그리고 기존에 VM 1개에서 실행되던 Agent는 nohup형태로 실행되어야 하기 때문에 pm2를 사용하였는데요 ( private 한 환경이라 local pm2를 사용합니다. ) 

package.json 

자동화 테스트 실행시 외부에서 UDP를 발생하게 되는데 위의 spawn 을 통해서 아래와 같이 정상적으로 받아오고 UDP 패킷을 확인할수 있습니다.

하지만 6500 7500 8500 ... x500으로 포트를 나눠서 에이전트를 관리하게 될시

재부팅시 pm2의 오류로 인해서 spawn 시 ENOENT 오류가 발생합니다.

Agent가 있는 VM의 경우 해당 Agent가 죽지 않도록 crontab이 걸려있습니다.

retry.sh 의 경우 pm2 list했을시 6500 7500 8500 등의 서비스가 없을 경우 재시작 해주는 역할을 하는데 서버 재시작 혹은 재부팅 혹은 내서버이미지로 서버 생성하여 새로 생성시 등의 경우 ENOENT 이슈가 발생합니다.

 

일단 하나씩 테스트 해봅니다.  ( 모든 전제조건은 npm run delete로 stop and delete를 했다는 조건 입니다. ) 

 

1. 현재 root계정에서 npm run restart 

실패

2. npm run delete & npm run start 

성공  ( npm run delete 시 pm2 save  to synchronize 워닝 발생 ) 

3. crontab 에서 확인

npm run delete상태기 때문에 npm run start & npm run restart가 됩니다.

실패

4. crontab 확인 ( retry.sh 코드 수정 )

 

재 실행 확인

crotnab으로 동작이 되어야 하지만 동작이 되지 않습니다. 스택 오버 플로우를 찾아보다 보면

spawn 에서 shell:true로 넣으라는 이야기가있는데 그렇게 해볼 경우 exit code 127 이 발생하면서 권한 문제로 종료 되게 됩니다. ( 사실 ENOENT 오류 자체가  " 일부 디렉터리는 권한 문제로 인해 또는 존재하지 않기 때문에 액세스할 수 없습니다" 라는 뜻입니다. ) 

 

ENOENT보단 나은 에러 메시지 아닌가?  권한 문제로 생각하고 pm2 save를 이용해보려고 합니다.

 

5. retry.sh 코드 수정 

npm run save ( pm2 save --force를 추가 )

npm run start

npm run restart

실패 

 

pm2를 커맨드 라인에서 직접 실행할경우만 정상적으로 권한 문제가 발생하지않고 실행이 되는 현상이 발생합니다.

 

그럼 crontab 에서 실행이 문제라고 판단되고 crontab에서 테스트 한 내용입니다.

 

1. sudo crontab -e

루트 권한으로 실행하기 위해서 sudo 를 사용합니다. -> 실패 

 

직접 커맨드라인에서 실행한게 아니라면 restart를 커맨드라인에서 직접 해도 오류가 발생합니다.

 

 

결론

npm run save 부분을 sudo 로 실행해야 권한 이 꼬이지 않고 save가 됩니다. 

npm run save는 

 

pm2 save --force 입니다. ㅎㅎ

pm2에 구조에 대해서 이야기 를 할까 했지만 해결한 것 으로 만족 하겠습니다...

 

지금은 Docker 기반이 아니라 위와 같은 이슈가 발생하고 ( 서버의 지속적 운영을 위해서 ) 

Docker 로 애초에 쓰시고 계신 분들은 아마 이슈가 없을 거라고 생각 합니다. 

Docker에서는 Docker의 이슈가 있겠지.. 내년엔 다 Docker 로 바꿉니다. 

 

감사합니다.

 

 

안녕하세요 깍돌이 입니다. Timer Phase 에서 Worker ( 테스트 자동화 코드 ) 를 실행하면서

 

느꼈던 문제들 그리고 Node.js의 Event Loop 에 있는 Timer Phase 에 대한 생각 그리고 위의 Worker들을

 

Worker Thread Pool 로 해결이 될지 된다면 어떤방법인지에 대해 생각한 내용들입니다.

 

Node.js Timer Phase 

네 타이머 페이즈에서 해결할수 있었습니다. Event Loop 가 계속 돌면서 타이머 페이즈에 는 계속 등록된 함수들이 있게 되고 ( Worker ) Event Loop 가 돌면서 계속 실행됩니다.  하지만 테스트 플랫폼의 초기 단계이기도 하고 API 테스트의 구조는 비동기 형태로 단순 Request , Response 에 대한 결과를 검증 하기 때문에 큰 문제는 되지않았지만 ( 이것도 케이스가 수천건이 넘어가면 이슈가 생길듯 ) 

 

시나리오의 경우 UI, API 가 있고 현재는 API 를 우선하고있지만서도 스텝과 스텝 사이의 Polling 하는 구조가 많이 있다보니 Event loop 에서 Timer Phase가 점점 무거워집니다. 그렇기 때문에 결국 Timer Phase 도 뭔가 좋은 방법은 아니지 않을까? 라는 생각을 하게 되었습니다.  ( 이번 작업은 UI 를 하지 않았지만 UI 의 경우 Child_Process를 통하여 직접적으로 하나의 프로세스를 더 띄우는 방법을 생각 하고 있었습니다. ) 

 

그리고 테스트 해보고 싶었던 마음에 노드 관련 홈페이지를 기웃 기웃하다 보니 Node.js 공식 홈페이지에 아래와같은 말이 있었습니다. 

 

그리고 앞선 Worker Pool 에 대한 이야기 중 간과해야 될게 있습니다. 

 

현재 작성하고있는 테스트 플랫폼의 Test Case들은 CPU I/O 가 많지 않습니다.(그렇기 때문에 무조건 워커 스레드 풀을 써야하는게 아닌 Timer Phase 에서 처리되는 워커들이 워커 스레드 풀에선 좀 더 나을까?라는 내용에 대한 확인 을 하고 싶기 때문입니다. )   케이스 스텝 사이에 대기(Polling) 하는 텀이 길기 때문에 하나의 Task(작업) 이 일어날때 무거운 작업은 아니라는 점을 중간에 한번 적어보았습니다.  TC의 예시

 

A시나리오

 

               -> A작업이 완료가 됐는지 최대 10분간 확인 ( Polling) -> 10분이 지나도 완료가 안됐을 경우 -> 실패

A 작업 

            ->  10초마다 확인하여 10분(Test Timeout) 전에 작업이 완료되면 다시 다음 B작업 실행 

 

=> 위와같은 형태로 A -> B -> C -> D -> E -> F -> G -> H -> I -> J - > K -> L (실제 케이스는 이거보다 더많습니다. ) 

로 진행됩니다. 하나의 시나리오에 테스트 시간은 약 30분 정도가 됩니다.  Polling하는 시간이 많기 때문에 CPU I/O를 많이 쓰지 않는 케이스에 대한 자동화라는 점을 기억해 주시기 바랍니다.

Don't Block the Event Loop (or the Worker Pool)

https://nodejs.org/en/docs/guides/dont-block-the-event-loop/

이부분에서 핵심내용으로 생각되는 부분들에 대해서 간략하게 몇가지 적어 보자면  

    Node.js는 EventLoop ( Initialization and callbacks ) 에서  JavaScript 코드를 실행하고 파일 I/O 같은 값비싼 작업을 처리하기 위해서 작업자 풀 ( Worker Thread Pool ) 을 제공한다고 합니다.  

 

두가지 타입의 쓰레드가 존재 

첫번째는 Event Loop ( Main Loop, Main Thread , event Thread ) - 전 포스팅에 있던 노드 이벤트 루프

두번째는 Worker Pool ( Thread Pool ) 

Node.js의 경우 Event-Driven 구조이며 관리를 위한 Event Loop 와 비싼 작업을 하는 Worker Pool 이 있다.

 

그 밑에는 O(n) 의 복잡도를 가지는 콜백에 대해서 작은 n이라면 무엇보다 빠르지만 점점 높아질수록 문제가 생김에 대한 내용이 있고 Node.js는 V8 엔진을 사용하기 때문에 일반적인 조작은 매우고속이며 JSON 연산과 regexps에서는 예외가 될수 있음을 설명 합니다.  regexp는 입력 스트링에 따라서 o(2n) 이 가능하기 때문에 이 경우는 Event Loop를 차단합니다. 

그래서 regexp를 이용한 redos에 대한 샘플이 있지만 ( 테스트 자동화에서는 정규식을 많이 사용하지도 않으며.. 내부 백오피스 에서 사용하기 때문에 일단 제외 해도 될거 같습니다. ) 

결론은 regexp 를 조심해서 써야 한다 이런내용

 

Node.js의 Event Loop를 차단하는 코어 모듈 

Encryption

Compression

File System

Child Process ( 이건 추후에 쓸 예정인데 유심히 봐야될듯 ) 

 

위의 API 들은 상당한 계산을 잠재적으로 사용하기 때문에 많은 비용이 발생 하기 때문에 서버를 구성한다면 아래의 API 들을 사용하면 안된다. ( express를 예로 들면 express Router에서 쓰지 말라는 의미입니다. ) 

  • Encryption:
    • crypto.randomBytes (synchronous version)
    • crypto.randomFillSync
    • crypto.pbkdf2Sync
    • You should also be careful about providing large input to the encryption and decryption routines.
  • Compression:
    • zlib.inflateSync
    • zlib.deflateSync
  • File system:
    • Do not use the synchronous file system APIs. For example, if the file you access is in a distributed file system like NFS, access times can vary widely.
  • Child process:
    • child_process.spawnSync
    • child_process.execSync
    • child_process.execFileSync

API 자동화에서는 CryptoJS를 사용하는데 확인해봐야 될거 같습니다.  파일시스템의 NFS는 NAS같은 류를 이야기 하는거 같습니다.  대부분 동기적인 작업 및 CPU I/O가 순간적으로 많이 발생되는 API 들이라고 생각 됩니다.

 

해당 밑에는JSON DOS ( JSON 에 대한 이야기니 넘어 가겠습니다. ) 

Complex calculations without blocking the Event Loop

이제 뭔가 나왔습니다. 이벤트 루프를 막지 않고 복잡한 계산 에 대해서 파티셔닝과 오프로드 2가지 에 대해서 설명합니다.  이부분은 간략하게 적고 직접 테스트하면서 결과를 적어 보겠습니다.

 

첫번째

C++ 애드온을 개발하여 내장된 Node.js Worker Pool 을 직접 쓴다가 있습니다. 

node-webworekr-thread 를 이용하여 Node.js Worker Pool 에 액세스 하는 js를 사용합니다.

 

두번째

직접 Node.js I/O 테마 워커 풀 대신 계산 전용 워커 풀을 직접 만드는 방법 이다. 방법으로는 Child_Process or Cluster 를 이용한다. 

 

사실 Worker Thread 에 대한 내용을 읽어 보면 CPU I/O 에 대한 이야기가 많다.  왜냐면 해당 작업이 무거워지면 Event Loop 를 막는 경우가 생길거고 이로 인해서 Node.js 전체가 느려지는 현상이 발생하기 때문이다.  서버를 이용한다면 대용량 트래픽을 받을 경우 문제가 생길수 있다 그렇기 때문에 이와같은 형태의 설명들이 많았고 아마 지금 글을 쓰고 있는 저처럼 모든 시나리오의 스텝의 사이가 CPU I/O 가 아닌 대기(Polling) 로 이루어진 케이스가 있을 거라곤 생각을 못했을 것이다.

 

지금 포스팅을 하면서도 트리거 시스템을 이렇게 구성하는게 과연 맞는 것인가? 에 대한 의문이 들지만 나와 유사한 작업을 한 사람도 볼수 없었고 블로그 포스팅도 없었기 때문에 그냥 부딪혀 보면서 포스팅 하고 운영해보다가 더 좋은 개선될 내용이 있다면 다시 포스팅해서 Node.js로 완벽한 트리거 시스템을 구현해 보고자 한다.

 

서론이 쓸데 없이 길었던거 같긴한데 포스팅을 하게되면 이해없이는 쓸수 없기 때문에 서론을 쓰게되면 많은 이해가 된 상태로 작업을 할수 있게 됩니다.

 

해당 테스트는 많으면 2포스팅 적으면 1포스팅으로 하려고 하며 트리거 시스템의 기본 베이스 서버는 Express.js를 쓰기 때문에 Express.js를 가지고 작성하며 Express.js의 기본적인 내용은 다 알고 있다고 생각하고 자잘한 설명은 제외하겠습니다. ( Express.js는 블로그 검색하면 내용이 워낙 많으니까요 ) 

 

https://www.nearform.com/blog/learning-to-swim-with-piscina-the-node-js-worker-pool/

 

 

Express.js

const express = require('express')
const app = express()
const port = 8880

const jsWorker = require('./express_worker');

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.get('/latency',(req,res)=>{

  let st_time = performance.now();
  let ed_time = performance.now();

  let total_time = ed_time-st_time;
  res.send(`latency Time - ${total_time}`);
})

app.get('/express-polling-worker',(req,res)=>{

  let uuid = createStepUUID();
  setTimeout(async()=>{
    try {
      jsWorker(uuid);
    } catch (error) {
      console.error(error);
    }
  },0);
  res.send('Start Polling Worker');
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})


function createStepUUID() {
  let d = new Date().getTime();
  if (
    typeof performance !== 'undefined' &&
    typeof performance.now === 'function'
  ) {
    d += performance.now(); // use high -precision timer if available
  }
  return '깍돌이-exps-xxxx-4xxx-yxxx'.replace(/[xy]/g, function(c) {
    var r = (d + Math.random() * 16) % 16 | 0;
    d = Math.floor(d / 16);
    return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
  })
 }

 

Express_Worker.js

const { setTimeout } = require('timers/promises')

module.exports = (uuid)=>{
  return new Promise(async(resolve,reject)=>{
    try {
      console.warn(`Start UUID ${uuid}`);
      await setTimeout(1000000);
      console.warn(`Close UUID ${uuid}`);
      resolve(true);

    } catch (error) {
      reject('ERROR');
    }
  })
}

 

기본 베이스 코드입니다.

latency 는 해당 컨트롤러에서 코드를 읽고 res.send까지의 시간을 던져 줍니다.

express-polling-worker 는 Timer Phase 에 Timer 를 계속 추가합니다. 위의 코드 베이스로 테스트하겠습니다. 

시간초는 해당 컨트롤러에서 작업이 없기 때문에 보기 편하게 

 

let total_time = ((ed_time-st_time)*1000).toFixed(4);

으로 살짝 보정을 하겠습니다.  ( 사실 express 내에서 하는건 의미가없을 수도 있습니다. node.js의 블록킹이 걸리면 아래 GIF와 같이 코드가 실행되는시간이 오래 걸리는게 아니라 그냥 대기 ( 로딩 ) 시간이 생기기 때문입니다. )

express 내에서 찍어본건 요청이 왔을때 서버 내에서 시간을 한번 체크해보려고 했습니다.

 

Timer Phase 1만개 케이스 Polling

눈치 채셨을 수도 있지만 순식간에 1만개의 함수를 Timer Phase 에 넣었지만 넣고 있던 순간에 잠깐 느려질뿐 넣고 나서는 크게 느리지 않는걸 볼수 있습니다.  해당 이유는 워커에서 실제로 폴링을 한다면  상태 체크 -> 다음함수 또는 다시 상태 체크 의 경우를 해야되는데 하고있지 않기 때문입니다. 해당 코드를 추가하겠습니다.

 

module.exports = (uuid)=>{
  return new Promise(async(resolve,reject)=>{
    try {
      console.warn(`Start UUID ${uuid}`);

      let retry = 0 ;
      let loopCondition = 240;

      // 2400초 = 40분
      
      let closeCondition = true;
      while(loopCondition>=retry && closeCondition){

        let status = await getStatus(loopCondition);

        console.warn(`테스트 상태 체크 ${uuid}  = ${status}`);
        // true 면 반복문 종료 
        if(status==='Y'){
          closeCondition = false;
        }
        await setTimeout(5000);
        retry++;
      }

      console.warn(`Close UUID ${uuid}`);

      resolve(true);
    } catch (error) {
      reject('ERROR');
    }
  })
}

// 30분에 테스트 종료 
const getStatus = (loopCondition)=>{
  return new Promise(async(resolve,reject)=>{
    try {
      // 상태를 가져옵니다.
      let retrunValue='N';

      if(loopCondition===180){
        retrunValue='Y';
      }
      resolve(retrunValue);
    } catch (error) {
      reject(error);      
    }
  })
}

 

Timer Phase 1만개 케이스 Polling

서버에서 계속 동작을 하는 케이스로 추가하였습니다. 워커가 돌기 전 

 

폴링하면서 상태를 체크하는 코드에서 CPU가 과도하게 올라갑니다.

 

1만개의 워커를 사용시 최대 CPU 47%까지 사용하며 레이턴시 ( 서버내에서 실행할 경우 ) 는 큰 변화가 없었습니다.

아마 측정을 FE단에서 요청하고 돌려받는 시간을 재야 할거 같네요 마지막으로 위에 코드에 수정을 하나 하겠습니다.

 

서버쪽과 클라 쪽 입니다.

 

일반적으로는 FE에서는 9의 차이가 나타나지만

 

 

스레드 실행 후 1734 값이 나오는걸 알수 있습니다. 

=> 순간적으로 Timer Phase 에 꽉차기 때문에 병목 현상이 발생한다는 뜻입니다.   ( Event Loop 가 많은 양의 Fn을 Timer Phase 에 등록 하면서 생기는 현상 ) 

1. 케이스가 추가 될경우 병목 현상이 발생할수 있음

2. 추가된 케이스가 계속 수행되면서 CPU 사용량이 매우 높아짐 

 

이제 여기서 Node.js에서 말하는 Child_Process와 Cluster를 이용하여 처리해보려고합니다. 가장 유명한건 Cluster모듈이고 ( pm2 에서 많이 쓰고잇죠 )  해당 Cluster 모듈로 구현된 Node.js Piscina 라는 모듈을 사용해보려고 합니다.

 

Node.js with Piscina (feat. By James Snell

우선 관련 링크입니다.

https://www.nearform.com/blog/learning-to-swim-with-piscina-the-node-js-worker-pool/

해당 링크의 작성자를 유심히 봐야 하는데요 By James Snell 

 

https://github.com/piscinajs/piscina

해당 레포지토리의 메인 컨테이너가 쓴글인걸 알수 있습니다. 그리고 프로젝트들 보시면 알겠지만 Node.js의 개발자이기도 합니다.    해당 포스팅을 이용하여서 위의 테스트한 케이스들에 대해서 Piscina를 적용해보고 결과를 적어 보겠습니다.

해당 글이 2020년도 글이라 현재 많이 바뀐 점이 있습니다. 

runTask X  => run

 

단순하게 바로 테스트 해보겠습니다.

 

const piscina = new Piscina({
  filename: path.resolve(__dirname, 'express_worker')
});


app.get('/express-piscina-worker',async (req,res)=>{


  let loop=10000;
  for(let i=0;i<loop;i++){
      let uuid = createStepUUID();
      console.warn('ppap test - ' + i);
      const result = piscina.run(uuid+'-PPAP');
  }


  res.send('Piscina Worker Start ');
})

Timer Phase 1만개 케이스 Polling ( Node.js with Piscina )

위의 GIF ( without piscina ) 의 비교를 확실히 해보세요 !  같은 코드의 1만건의 테스트 런이 발생했을 때 실제 나의 PC의 CPU 와 웹 FE에서 latency 가 어떤지 비교하면 좋을거 같습니다.

 

1. 1만 TestCase  JS Timer Phase  Worker 실행 

1. 웹화면 지연

setTimer를 이용한 Timer Phase 에 TestCase Worker 를 1만개를 밀어 넣었을 경우 웹화면서 1862 ms 의 지연이 생깁니다. 

2. CPU 사용 률

케이스들의 폴링하면서 상태를 체크할 경우 최대 31% 까지 발생합니다.

* 참고로 제가 사용하는 cpu 입니다.

AMD Ryzen 7 5800X 8-Core Processor 3.80 GHz

 

 

 

1. 1만 TestCase  JS Worker Thread Pool 실행

 

1. 웹화면 지연

Worker Thread Pool 에 1만개의 TestCase를 밀어 넣었을 경우 800ms 가 나옵니다. 

Event Loop을 Blocking 하던 Timer Phase에서 2배의 효율이 발생합니다.

 

2. CPU 사용 률 

보시면 12%를 절대 넘지 않습니다.  

 

뭔가 결과만 보면 Piscina 에서 Worker Thread Pool 을 사용하면서 지연도 2배의 효율이고 CPU 도 오버해서 쓰지 않는거 같습니다.

 

하지만 자세히보면 다른점이 있습니다.  CPU 사용률 체크시 보면 Piscina의 경우 

 

24개의 워커만 실행되는걸 알수 있습니다.  1만대를 실행하였지만 실제로는 워커의 개수만큼인 24개의 워커만 실행되면서 진행한다는 점입니다. Piscina의 구조를 보면 알수 있습니다.

아마 Worker Pool 을 PC의 코어 수 * 1.5 로 만들어 놓고 실행합니다.

 

 

JS timerPhase로 밀어 넣는 경우는 서버의 상태등 아무것도 고려하지않고 밀어 넣기 때문에

 

10000개의 케이스를 전부 Timer Phase에 밀어 넣고 Event Loop 가 돌다가 다시 Timer Phase에 왔을때 실행이 되기 때문에 모든 케이스가 다 실행되며 CPU의 과부하 까지 걸리게 됩니다.

 

그럼 단순하게 여기서 피시나는 24개 뿐 ( Worker Thread )  -> 이것도 제 PC의 CPU 가 16코어이기 때문에 *1.5하여 24개의 스레드가 돌게 되는 겁니다.

 

만약에 스레드가 1초마다 실행하고 종료되는 케이스라면 적절하게 모든 케이스가 추가될건지 확인해보면

 

1만개의 TestCase가 추가된 후 적절하게 테스트 종료 및 대기하던 TestCase 가 다시 시작이 되는 걸 볼수 있습니다. 그런데 이렇게 24개의 워커만 사용해도 10%이상을 쓰지 않는데 좀더 많이 사용할수 없을까? 라는 고민으로 다음 포스팅은 위에서 piscina로 24개 Worker만 사용하는 PC 상태에서를 Core * 1.5배 만큼의 워커를 쓰지 않고 더 쓰는 방법이 있는지와 Piscina의 옵션 하나하나를 설명 하는 포스팅을 하도록 하겠습니다.

 

 

 

 

안녕하세요 깍돌이 입니다. 

 

해당 포스팅은 사실 QA가 릴리즈 또는 배포 모니터링시에 사용하게되는 툴 중에 자동화 스크립트가 있을수 있습니다.

하지만 해당 포스팅 시리즈는 테스트 플랫폼 ( Test Platform ) 에 관련된 이야기기 때문에

 

이번에 작성되는 내용은 worker 에 대한 내용을 적으려고합니다. 저는 QA하면서 필요한 기술적인 부분들은

전부 Node.js 하나로 처리하고 있습니다. 이부분에 대해서 간략하게 적고 넘어가자면 

Node.js 를 계속 써오고있어서 이기도 하지만 노드가 무조건 짱이다 이런 느낌도 아닙니다.

저연차 QA시절 (지금도 고연차는 아니지만) 에는 메인이 C++이였기 때문입니다.  ( UI 자동화를 IE , Chrome C++ Object 로 하던 시절.. 크흠 )

 

Node.js 하나로 필요한 부분들을 다 커버할수 있기도 하고 에 Node 위주로 사용하고 있으며 효율적으로 사용해야 하기 때문입니다.  ( 하지만 자바로 시작했어도 파이썬이나 노드로 넘어 왔을거 같긴합니다. ) 

 

그리고 궁극적인 목적은 높은 품질의 제품을 제공하기 위함입니다.  높은 품질의 S/W를 제공하기 위해서 자동화 테스트 를 넘어서 테스트 플랫폼이 필요하다면 만들면 됩니다. ( 자동화의 제일 중요한점은 이게 메인이 되선 안되고 QA활동에서 사용되는 Tool 로 써 작용되어야 하며 명확한 이유를 가지고 있어야 합니다. - 이건 대외비라 포스팅하지않습니다. )

 

JS Worker

JS Function 으로 작성된 함수에 대한 Worker 입니다. 

해당 Worker.js의 경우 5초 25초 50초를 대기 후 정상 종료 합니다. 

* 여기서부터 말하는 Worker는 실무에서는 작성될 독립된 자동화 테스트 코드 입니다. 

=> 제가 사용할 자동화 테스트 코드는 폴링하는 경우가 많기 때문에 시나리오 사이사이에 폴링을 유사하게 기대하는 loop 함수를 사용합니다.

 

실행시

순차적으로 실행이 되었습니다.  좀 더 자세히 보기 위해서 로그를 추가 하였습니다.

일반적으로 JS 함수를 통해서 Worker 를 진행시 

다같이 잘 돌아가는거 같지만 모든 함수들이 순차적으로 실행되고 있습니다.

모두 실행전에 결과를 주지않는 함수를 하나 만들어 놓겠습니다.

시나리오는 아래와 같습니다.

하나의 시나리오는 5초 15초 25초의 각각 의 폴링을 가지고 정상 종료 합니다.

2개의 시나리오를 실행시키며 1번째 시나리오에서는 5초 후 return 을 주지 않게됩니다.

 

이렇게만 보면 정상적으로 잘 동작하는 것 처럼 보입니다.

하지만 1에 suspend에서 어떠한 작업도 하지 않았기 때문입니다.  만약에 1에서 suspend 가 true일경우 에러가 발생했다면?

앞에 Worker 에서 발생한 서스펜드 에러로 인해서 뒤에서 실행되어야 모든 워커들이 실행되지 않는 현상이 발생합니다.

=> 해당 이유는 Node.js의 Event Loop 가 Single Thread 이기 때문인것 과 동시에 현재 실행되는 코드는 같은 Phase 에서 실행이 되고 있기 때문입니다. ( 이부분은 점차 수정하면서 설명 하도록 하겠습니다. ) 

 

이 뜻은 결국 각각의 Worker 들이 독립적으로 실행되지 않았다는 반증이 되기도 합니다. 

Node.js의 기본적인 Event Loop 를 같이 사용하다보니 위와같은 이슈가 발생합니다.

 

테스트 플랫폼을 이용하는 QA가 자기자신의 코드가 아닌 다른곳에서의 에러도 신경 써야 한다? 

=> 말이 안되는 이야기 입니다.

 

위의 경우로 인해서 일반적인 JS Function을 사용하는건 말이 안되는 상태입니다. 

Event Loop (Timer Phase)

Node.js의 Event Loop 에는 Timer Phase가 있습니다.

* 출처(Node.js 공식 홈페이지) : https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ 

 

The Node.js Event Loop, Timers, and process.nextTick() | Node.js

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org

 

필자의 자동화 시스템에서 Trigger 가 되는 주체는 Express.js로 구현된 서버가 되며 해당 Http 기반의 서버들은 기본적으로 Callback 이 등록되어있기 때문에 지속적으로 Event Loop 가 생성 하고 사라지지 않는 형태가 됩니다 .

그렇기 때문에 서버의 테스트 요청이 왔을 경우 ( 자동화 테스트 코드의 실행 요청 ) 해당 코드를 타이머로 묶어서 타이머 페이즈에 넣어 놓는 방법을 생각해 보았습니다.  

그 결과 코드는 아래와같이 변경 됩니다.   ( Express는 제외하고 샘플 코드를 작성하였습니다. - 실행할 자동화 테스트 코드를 타이머 페이즈로 밀어 넣습니다.)

 

 

위의 JS Worker 에서 오류가 났던 코드 그대로 사용시

뭔가 독립적으로 실행되는것처럼 보이게됩니다.

 

이상태에서 위의 JS Worker 를다시 보면 이거 try catch 를 잘써서 그런거 아닌가? 라는 의문이 다시 들수 있습니다.

setTimeout을 제거하고 Timer phase 로 들어가지않게 코드를 수정하게 되면

2개의 워커가 일단 실행은 되고 비동기 처럼? 돌아가는 것처럼 보이지만 같은 Stack 에서 돌아가고 있기 때문에

결국은 위와같은 현상이 발생하게 됩니다. 

 

이렇게만 보이면 독립적으로 작성된 자동화 테스트 코드를 Timer Phase 에 넣어서 돌아 가고 있는 것 같고 실제로  10개의 worker 를 작성하였을 경우 아래와 같이 (시뮬레이션이지만) 뭔가 잘 돌아가는거 같습니다.

 

하지만 여기에서는 문제가 하나있습니다.  이건 다음 포스팅에 작성 하도록 하겠습니다.

 

다음 포스팅 - 현재의 Timer Phase 에 Worker를 등록해서 사용할 경우 문제점

마지막 포스팅 - 해당 문제를 해결하기 위한 Node.js Piscina

 

안녕하세요 원래 기본적인 포스팅은 따로 안하려고 하는 편인데 자동화 플랫폼내에서는 필수적인 요소이기 때문에

 

기본부터 차근차근 넣어 놓으려고합니다.

 

실제 서버는 리눅스 환경이지만 현재는 윈도우즈에서 테스트를 하고있기 때문에 윈도우즈에서 우선 작성하도록 하겠습니다.

 

Node.js 가 설치된 상태로 PM2 를 글로벌 로 설치 후 테스트 시 아래와 같은 오류가 발생합니다.

 

npm i -g pm2
pm2 list

pm2 : 이 시스템에서 스크립트를 실행할 수 없으므로 C:\Users\GDL\AppData\Roaming\npm\pm2.ps1 파일을 로드할 수 없습니다. 자세한 내용은 about_Execution_Policies(https://go.microsoft.com/fwlink/?LinkID=135170)를 참조하십시오.

위치 줄: 1 문자 :1

pm2 list

     CategoryInfo : 보안 오류 : ( :) [], PSSecurityExeception

     FullyQualifiedErrorId : UnauthorizedAccess

 

인터넷에서 간단하게 구글링을 해보면 파워 쉘을 킨 후에 ExecutionPolicy Unrestricted 을 통해서 해제를 하라는 이야기들이 많이 있습니다.

 

 

Get-ExecutionPolicy : 'Scope' 매개 변수를 바인딩할 수 없습니다. 값 "Unrestricted"을(를) "Microsoft.PowerShell.Execution
PolicyScope" 유형으로 변환할 수 없습니다. 오류: "식별자 이름 Unrestricted을(를) 유효한 열거자 이름과 일치시킬 수 없습니
다. 다음 열거자 이름 중 하나를 지정한 후 다시 시도하십시오.
Process, CurrentUser, LocalMachine, UserPolicy, MachinePolicy"
위치 줄:1 문자:17
+ ExecutionPolicy Unrestricted
+                 ~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Get-ExecutionPolicy], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.PowerShell.Commands.GetExecutionPolicyCommand

 

실제 실행시에는 위처럼 발생하며 아래와 같은 방식을 사용해야 합니다.

 

이제 pm2가 준비 되었으니 기본적으로 pm2 cluster 를 기본적으로 해보도록 하겠습니다.

 

const express = require('express')
const app = express()

app.get('/', function (req, res) {
  res.send('Hello World  ')
})

app.listen(3000,()=>{
  console.log('Example Clustering App on Port 3000')
})

테스트에 사용할 express 기본 코드입니다.

 

npmjs.com 에서 pm2 의 cluster mode를 보면 아래를 참고하라고 되어있습니다.

https://pm2.keymetrics.io/docs/usage/cluster-mode/

npmjs 간단 설명입니다.

클러스터 모드는 Node.js 애플리케이션을 시작할 때의 특수 모드로, 여러 프로세스를 시작하고 이들 사이에서 HTTP/TCP/UDP 쿼리를 로드 밸런싱합니다. 이렇게 하면 전체 성능(16코어 시스템에서 10배)과 안정성(처리되지 않은 오류의 경우 더 빠른 소켓 재조정)이 향상됩니다.

 

const express = require('express')
const app = express()

let i=0;
function wait (time){
  return new Promise(async(resolve)=>{
    setTimeout(()=>{
      resolve(true)
    },time)
  })
}
app.get('/',async function (req, res) {
  try {
    const randomNumber =Math.floor( Math.random()*1000 );
    await wait(2000);

    console.log(`Count ${i}`);
    i++;
    res.send('test        '+ randomNumber)
  } catch (error) {
    console.error(error);
  }
})
app.listen(3000,()=>{
  console.log('Example Clustering App on Port 3000')
})

 

express  코드를 살짝 수정하였습니다.  / 의 Router에서 임의로 2000ms 의 시간을 두었습니다. 이렇게 되면 서버 1대로 돌릴 경우 웹에서 접속시 아래와같이 대기 시간이 생길수 있습니다.

해당 딜레이를 pm2 모니터로 확인시

 

 

PM2 Cluster 를 통하여서 해당 로딩이 없이 바로 바로 응답을 줄수 있도록 변경해보겠습니다.

해당 Cluster를 최대한으로 쓰기전에 현재 CPU 의 개수 먼저 확인해보니

const os = require('os')
const cpuCount = os.cpus().length

 

 

 

16개로 나오네요 자 이제 클러스터 모드를 사용하기 위해 공식홈페이지 Usage를 따라 세팅하도록 하겠습니다.

pm2 start app.js -i max

기본적인 로컬 PC의 CPU 개수만큼을 전부 사용 하는 모습입니다. 실제로 서버에서 요청도 다 받아 지는지 보겠습니다.

 

4번의 요청을 해보았을시 pm2 모니터를 통해 확인해보니 process 가 여러개 동작하는건 맞지만 실제로 제가 생각했던건 알아서 로드밸런싱을 해주는 형태였는데 해당 형태로는 절대 동작하지 않음을 알수 있습니다. 15번 app이 Idle타임을 가질경우 다른 서버가 대신 처리를 해줬어야 하는건데 말이죠 

 

해당 클러스터링 모드는 단순히 서버 한대가 돌아가다가 죽었을 경우 다음 서버가 일을 해주는 형태입니다.

 

여러 블로그들을 검색해서 찾아봤을 경우 대충 공식 홈페이지나 번역 하거나 의역하고 끝내는 경우가 많아서 좀더 찾아 보니 결국은 Node.js의 Cluster mode를 사용한 경우면 parent process에서 child_proces를 fork 로 여러대의 APP 을 띄워서 무중단 배포나 앞단에 서버가 죽었을 경우에 대한 처리를 이용하는 형태로 사용되며 결국 제가 원하는 로드밸런싱을 사용하기 위해서는 nginx의 로드밸런싱 기능을 같이 사용해야 합니다.

=> 오해가 있을수 있어 추가합니다.  해당 서버들의 CPU I/O 나 메모리의 사용률이 높아질 경우 알아서 클러스터링이 되지만 테스트 자동화의 경우 Idle타임을 사용하기 때문에 다른 클러스터를 사용해야한다고 인지 하지 못하여 클러스터링이 되지않는 것입니다.  ( 실제 클러스터링 되는 경우 하단의 테스트결과를 넣었습니다.)

 

module.exports = {
  apps : [
    {
      name      :"clusterTest",
      script    : "app.js",
      instances : "4",
      exec_mode : "cluster",
      wait_ready: true,
      listen_timeout: 50000,
      kill_timeout: 5000,
      watch: true, // 파일이 변경되었을 때 재시작 할지 선택
    }
  ]
}

 

마지막으로 테스트하였던 ecosystem.config.js 입니다. watch의 경우 파일 변경시 재시작인데 일반적으로는 쓰지 않는게 좋겠네요 

 

nginx 연동으로 LB처리는 실제로 쓰고있는 자동화 플랫폼 앞단에 처리하도록 하면서 다시 포스팅하겠습니다.

 

감사합니다.

 

 

--> 위에 로드밸런싱의경우 오해할수 있는 내용이 있어서 수정 하려고 합니다.

 

PM2 Cluster 를 사용할 경우 CPU , 메모리 등의 점유율에 따라 클러스터링을 해주고 있기 때문에 

테스트 자동화에 사용되는 Test Runner 들은  CPU , 메모리를 많이 쓴다기보단 Idle time 을 많이 사용하게 됩니다. 

그렇기 때문에 pm2 로 클러스터링을 늘려놔도 Test Runner 를 사용하기 위한 Idle time에서는 해당 클러스터링을 위한 조건에 부합하지 않기 때문에 동작되지 않는 것 처럼 보입니다. Round Robin 같은 방식이 아니기 때문에 그렇게 보이는 것이며

동작 예시를 아래에 추가 하였습니다.

 

pm2 monit

 

그리고 해당 cluster_app은 

새로 생성한 앱이라면 새로운 UUID를 response 로 주는 기본 서버입니다.

해당 코드로 비동기로 사용시 

안되는 것처럼 보입니다.

 

하지만 100이 아니라 5000번을 사용한다면 아마 200ms (0.2초안에 5000번이 접속시도)

 

최대 3개의 서버에서 돌아가면서 클러스터링 됨을 알수 있습니다. ㅎㅎ 좀더 직관적으로 보기 위해서 gif로 보여드리겠습니다.

 

 

정상적으로 클러스터링 되어 동작 함을 확인할수 있습니다 ㅎㅎ 

 

테스트 러너의 사용성에 살짝 부합하지 않을뿐 pm2 cluster 는 엄청 좋은 모듈입니다 

안녕하세요 깍돌이입니다.

오늘은 Express.js의 에러 핸들러 처리에 대해서 작성하려고 합니다.

사실 기존에는 관련 내용 작성이 안되어있었는데 사실 검색하면 워낙 많이들 나오는 내용이라 작성하지 않았습니다.

기본적인것들로 블로그 포스팅을 다 채우고 싶지 않았습니다. ( 조회수는 많이 늘어날텐데 ㅎㅎ;;)

초창기에 기본적인거 쓴건 히스토리용으로 냅두고 있습니다.

 

그외에는 작업하면서 경험한것들 잊어 버리지 않기 위해서 작성하고 있습니다.

 

그럼 금일은 뭐때문에 작성하게 되었냐면 Express.js의 ERROR 처리를 위한 Middleware처리는 2개의 방법이 있습니다.

 

ERROR Middleware 500 

기본적인 500 에러입니다. express.js의 서버에서 오류가 났을 경우 흔히들 볼수있는 Internal Server Error 오류입니다.

그래서 express.js에서는 해당 에러를 처리하기 위해서

Express Router Controller안에 있는 next라는 함수를 통해서 해당 next에 error객체를 넣어서 넘기고있습니다. 

Express의 error객체를 next에 넘겨 버리게 되면 기본적인 Middleware패턴에서 해당 에러 객체를 가진 함수로 넘어가게 됩니다.

 

 

app.ts 의 구현부는 아래와 같습니다.

 

 

ERROR Middleware 404 

기본적으로 Express는 middleware 패턴이라는 것을 알고 있다는 것을 감안하고 작성하겠습니다.

 

위의 Express Router 안에서 예외처리를 하는 catch 문에서 next(error) 를 사용을 했다는 것은 기본적으로 Express에서 선언된 Router가 매치가 됐다는 뜻입니다.

 

예를들어

http://localhost:5555/testRouter

http://localhost:5555/testNotExist

로 요청시에

 

Express에 router.get('/testRouter') 만 있다면 아래의 testNotExist는 매치가 되지 않습니다. 

그렇기 때문에 결국 Express에서 선언되는 Middleware중 매치되는 것을 찾지 못하고 가장 마지막 Middleware로 가게되는데요

 

 

그래서 가장 마지막 생성된 함수의 Middleware 함수로 넘어가게되고 해당 함수에서는 매치된 값을 찾지 못하였다는 에러 메시지를 res객체를 통해 반환하는 함수가 됩니다.

그래서 404 와 그 외 내부 예외처리를 통한 500 에러 크게 2가지로 나눌 수 있습니다.

 

이슈상황

그래서 이슈상황이 뭐냐! 네 위에 설명과 다르게 동작하는 경우 입니다.

 

네 맞습니다. next에 error 객체를 넣어서 넘겼을 경우 Middleware (500) 에러 함수(ERROR 객체가 선언된 함수 ) 로 넘어가져야 합니다. 

그런데 위의 상황에서는 404 ( 라우트가 매치 되지않았을 경우 ) 로 넘어가게 됩니다.

 

물론 둘다 오류의 상황은 맞기 때문에 상관없지 않나? 라고 생각 할수 있습니다.

 

하지만 개발하였을때 예상하지 못한 동작의 흐름에 생겼을 경우는 코드를 작성한 사람의 문제일 확률이 매우 높고 앞으로도 제대로 알지 못하는 케이스가 발생가능하기 때문에 해당 원인을 찾아보고자 했습니다.

 

 

Express.js 홈페이지

500 에러 함수 설명

라우트 맨 마지막에 선언 하여야 하며 -> OK

err 에 대한 인수가 있어야합니다. -> OK 

 

 

"next() 함수로 어떠한 내용을 전달하는 경우('route'라는 문자열 제외), Express는 현재의 요청에 오류가 있는 것으로 간주하며, 오류 처리와 관련되지 않은 나머지 라우팅 및 미들웨어 함수를 건너뜁니다. 이러한 오류를 어떻게든 처리하기 원하는 경우, 다음 섹션에 설명된 것과 같이 오류 처리 라우트를 작성해야 합니다."

 

-> 조금 잘못알고 있었네요 next 함수로 어떠한 내용 전달시 요청 오류로 간주하고 나머지 라우팅 건너 뜀 ( 여튼 OK )

 

제가 사용하고있는 형태는 기본 오류 핸들러 ( 404가 아닌 모든 오류를 받아서 처리함 ) 로 보이는데 특이사항은 없어 보입니다.

 

그럼 원인은 하나일거같습니다. next로 error를 넣지 않는건가?

 

에러가 발생한 로직입니다.

HTTP 요청 중 URL 오류가 발생해서 해당 axios.get  부분에서 catch 로 빠진 부분인데요  실제로 에러 객체를 reject하였고

 

하나씩 모든 에러를 따라가다보니

 

중간에 reject를 error가 아닌 error.message 

ERROR객체가 아닌 string으로 한 부분이 있었습니다... ㅜ

황급히 수정시  

 

역시 코드를 거짓말을 하지 않습니다...

 

사실 잠깐 그냥 봤을때는 원인이 잘 안보였는데 포스팅 을 하면서 한땀한땀 찾아가다보니 복병을 발견했네요.. 

 

다시 이런일 없기를 바라면서 히스토리로 남겨놓겠습니다.

 

감사합니다.

 

 

 

 

안녕하세요 깍돌이입니다. 

기존에 중단되었던 자동화 시스템 중 아직 정식시작이 된건 아니지만 앞에 부분들을 어느정도 다 갈아 엎고

다시 하고 있는 도중 상태관리에 대해서 중간 점검? 같은 형태를 해야될거같아서 정식 포스팅은 아니지만 중간 정리용으로 남기려고 합니다.

 

 

 

 

Recoil

이제까지 Redux 만 가지고 상태관리를 해오던 터라 요즘엔 Recoil 을 쓴다는 이야기가 있어 redux에서 recoil로 한번 넘어가보려고 써보게 되었는데 따로 사용법을 포스팅할거는 없을거같다는 생각이 들었습니다.

사용법이 생각보다 너무 간단해서 .. 하지만 hydraton 을 지원하는 next.js에서는 recoil 사용시 아래와 같은 warning이 뜨게 됩니다.  Duplicate atomkey '[key]' This is a FATAL ERROR in production. But it is safe to ignore this warning if it occurred because of hot module replacement. 

현재 2022-05-01 기준 Recoil 버전은 0.x 으로 베타 버전인데 해당 버전에 있는 알려진 이슈라고 하다보니 Redux에서 관리하던 상태를 전부 Recoil 로 가져오진 않았습니다. 

현재 Recoil 에서 사용되는 상태는 이제 FE 에서 사용되는 Nav의 상태 ( Open , Close ) 나 화면단의 설정 값 등으로만 사용하고 있습니다. BE에서 받는 비동기 처리에 대해서는 Recoil 을 사용하지 않는 상태입니다.

 

 

React-Query 

비동기 통신을 위한 데이터는 Recoil 을 사용하지 않기 때문에 비동기 통신 데이터는 React-Query 를 사용하고 있습니다.

 

처음에 사용할 때 고려 되었던 부분만 작성하도록 하겠습니다.

 

우선 React-Query 를 사용하게되면  QueryClientProvider 를 사용해야되는데 기존에 Recoil Root를 덮고 있어서

어떻게 써야되나 고민을 잠깐하게 되었는데 별건 없었습니다.  그냥 Recoil Root위에 덮어서 사용하면 됩니다.

 

 

GET

데이터를 가져와서 FE에 뿌려주는 형태로 사용시 아래와 같은 형태로 데이터를 요청해서 가져올수 있는데 

 

React-Query 의 경우 캐싱이 기본이기 때문에 위와같이 가져오게되면 

target_os가 A든 B든 돌아오는 값이 같다고 하여도 재 요청이 발생하지 않습니다. 

( retry 와 refetchOnWindowFocus를 Fasle로 놓았기 때문에 그런것도 있지만 ) 

의도하는 바람은 target_os가 가 달라질때만 요청을 하는게 의도되는 바이기 때문입니다. ( 불필요한 retry X ) 

 

첫번째 해결방법은 refetch  + useEffect 입니다.

 

아래와 같이 useEffect 와 refetch 를 하게되면 첫렌더링 후에 refetch 를 통하여 query 를 강제 재 발생합니다.

 

위와같에 하게되면 특이점이 

 

컴포넌트 렌더링시 

 

컴포넌트 렌더링 -> useQuery -> use Efffect  -> useQuery 로 재 렌더링 -> fetch Data ( 비동기 완료 ) -> 재 렌더링

발생합니다.

 

다른 방법은 useQuery의 인스턴스를 다시 초기화 하는 방법입니다.

위와같이 useQuery 의 인스턴스를 props를 통해서 지정하게 되면 다른 키로 인식을 하기 때문에 다시 fetch 가 발생합니다.

 

렌더링이 발생하는 건 똑같이 나타나네요 ㅎㅎ;;

 

새로 fetch을 할때만 렌더링을 하고싶은데 이건 오늘 처음 공부해본거라 좀더 찾아봐야될거 같습니다.

현재 사용 방식은 인스턴스를 재 초기화 하는 방식으로 사용 하려고합니다.

 

refetch 충분히 괜찮아 보이지만 렌더링시에 props를 통해서 새로 받는 부분이 더나을거 같고 그리고 추후에

Query Invalidation 의 경우 특정 한 쿼리 인스턴스를 통해서 해야 하기에 현재 받아온 쿼리 인스턴스를 위와같이 써야 특정 할수 있기에 위와같이 쓰도록 하겠습니다.

 

 

 

 

 

 

+ Recent posts