블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

 node.js에서 Heapdump를 이용한 메모리 누수 추적하기


조대협 (http://bcho.tistory.com)
 

대부분의 애플리케이션 서버들에서 고질적인 문제점중의 하나가 메모리 누수 현상이다. 비단 애플리케이션 서버에만 해당하는 문제는 아니지만 특히나 동시에 여러개의 요청을 반복적으로 받는 애플리케이션 서버의 경우에는 이 메모리 누수 현상이 훨씬 더 많이 발생한다.

 

node.js 의 경우, 엔진의 근간이 되는 구글 크롬 V8 자바스크립트 엔진 자체가 많은 메모리 누수 버그를 가지고 있지만, 애플리케이션에서 발생하는 메모리 누수는 발생하는 양도 크거니와 더 큰 문제를 야기할 수 있다. 이러한 애플리케이션에서의 메모리 누수를 추적하기 위한 방법을 알아보자

 

node.js의 확장 모듈인 heapdump는 기동중인 node.js 서버의 메모리 스냅샷을 추출할 수 있는 기능을 제공한다. 이 메모리 스냅샷을 힙덤프라고 하는데, 힙덤프에서 어떤 객체들이 메모리를 반복적으로 많이 점유하는 지를 찾아내면 메모리가 누수 되는 지점을 파악할 수 있다.

 

예제를 통해서 살펴보자

먼저 heapdump 모듈을 설치해야 한다. https://www.npmjs.com/package/heapdump

%npm install heapdump

명령어를 이용하여 heapdump 모듈을 설치한다.

 

Express로 간단한 웹 애플리케이션을 생성하자

package.jsonheapdump 모듈에 대한 의존성을 다음과 같이 추가한다.

 

{

  "name": "MemoryLeak",

  "version": "0.0.0",

  "private": true,

  "scripts": {

    "start": "node ./bin/www"

  },

  "dependencies": {

    "body-parser": "~1.13.2",

    "cookie-parser": "~1.3.5",

    "debug": "~2.2.0",

    "express": "~4.13.1",

    "jade": "~1.11.0",

    "morgan": "~1.6.1",

    "serve-favicon": "~2.3.0",

    "heapdump":"~0.3.7"

  }

}

 

Figure 1 package.jsonheapdump 모듈 의존성을 추가

 

다음 express 프로젝트의 app.js에 아래와 같은 코드를 추가한다.

app.use('/', routes); // 기존에 자동으로 생성된 코드

app.use('/users', users); // 기존에 자동으로 생성된 코드

 

var heapdump = require('heapdump');

var memoryLeak = [];

function LeakedObject(){ };

 

app.use('/leak',function(req,res,next){

     

      for(var i=0;i<1000;i++){

           memoryLeak.push(new LeakedObject());

      }

      res.send('making memory leak. Current memory usage :'

                 +(process.memoryUsage().rss / 1024 / 1024) + 'MB');

});

 

app.use('/heapdump',function(req,res,next){

      var filename = '/Users/terry/heapdump' + Date.now() + '.heapsnapshot';

      heapdump.writeSnapshot(filename);

      res.send('Heapdump has been generated in '+filename);

});

 

 

Figure 2 메모리 누수를 유발하는 코드 및 힙덤프를 추출하는 코드 추가

 

/leak URL을 처리하는 부분은 인위적으로 메모리 누수를 만들어낸다. for 루프를 이용하여 LeakedObject 1000개씩 배열에 추가하여 메모리 누수를 유발하고, 화면에 현재 메모리 사용량을 리턴하도록 하였다.

 

다음 /heapdump라는 URL에서는 실제로 heapdump를 생성한다. heapdump.writeSnapshot(파일명) 이라는 메서드를 사용하는데, 이 파일명 위치에 힙덤프를 저장한다.

 

또는 명령어를 사용하지 않더라도 프롬프트 상에서 힘덤프를 생성할 수 있다.

% kill –USR2 {node.js 프로세스 ID}

명령을 이용하면, node.js를 실행한 위치에 heapdumpxxx.heapsnapshot 이라는 이름으로 힙덤프를 생성해준다.

 

코드 작성이 끝났으면 애플리케이션을 실행해보자. http://localhost:3000/leak 로 들어가면 메모리 누수를 유발하고, 현재 사용중인 메모리 양을 볼 수 있다.

 



Figure 3 메모리 누수를 유발하는 URL에 접속

 

많은 메모리 누수를 유발해보기 위해서 ab (Apache benchmark : 아파치 웹서버를 설치하면 같이 설치되는 간단한 부하 테스트 툴이다. http://httpd.apache.org/) 를 이용해서 반복적으로 http://localhost:3000/leak 를 호출해보자

%ab -n 5000 http://localhost:3000/leak

명령어를 이용하면 http://localhost:3000/leak 5000번 호출한다.



Figure 4 아파치 ab툴을 이용하여 메모리 누수를 유도하기 위해서 5000번 부하를 줌

 

다시 http://localhost:3000/leak 를 접속해보면 사용중인 메모리 양이 늘어난것을 볼 수 있다.

 



Figure 5 node.js 의 메모리 사용량이 늘어난것을 확인

 

이제 힙덤프를 추출해보자. http://localhost:3000/heapdump 에 접속하면 자동으로 힙 덤프가 생성된다.

이 힙덤프는 현재 node.js가 사용중인 메모리 양이 클수록 추출하는 속도가 느려진다.

 



Figure 6 힙덤프 추출

 

힙덤프 파일이 추출되었으면, 이 힙덤프를 분석하기 위해서 구글의 크롬 브라우져에서 개발자 도구를 실행해보자

개발자 도구에서 “Profiles” 탭에서 Load 버튼을 눌러서 앞에서 추출한 힙덤프 파일을 로드한다.




Figure 7 크롬 브라우져 개발자 모드에서 힙덤프 파일을 로드

 

힙덤프를 보면, LeakedObject라는 객체가 전체 메모리의 66%, 120MB를 점유함을 확인할 수 있다.

이 객체를 열어보면, 같은 객체가 수도 없이 반복됨을 확인할 수 있다.

 



Figure 8 메모리 누수를 유발한 LeakedObject를 발견

 

이러한 방식으로 어떤 객체들이 메모리를 많이 점유해서 메모리 누수를 유발하는지 찾아낼 수 있다.

예제 소스 코드는 https://github.com/bwcho75/nodejs_tutorial/tree/master/MemoryLeak 를 참고하기 바란다.

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

Plug in 을 통한 maven 빌드 확장


앞글에서 http://bcho.tistory.com/739 maven을 이용한 가장 기본적인 빌드 시나리오에 대해서 살펴보았다.

maven plug in이라는 기능을 통해서, 여러 기능 들을 추가로 빌드 프로세스내에 추가할 수 있는데, 여기서는 빌드에서 확장하여, 배포 환경 별로 패키징 하는 시나리오를 추가해보기로 한다.

 

다음과 같은 시나리오를 생각해보자, 빌드에 의해서 생성된 웹 애플리케이션은 내 local PC에서도 돌아가야 하며, 다른 개발자와 협업하는 서버의 개발환경에서도 돌아야 하고, 개발 주기마다 주기적으로 QA환경에서 테스트를 받은 후, staging 환경을 거쳐서 최종적으로 production 환경에 배포 되어야 한다고 가정하자.

 

Java Version 지정하기

먼저 대상 환경에 맞는 컴파일 run time시에 소스 코드에 대한 java 버전과, target compiler java 버전을 지정할 있다. 아주 기본적인 내용인것 같지만, 실제 빌드 과정에서 java 버전을 명시적으로 지정하지 않아서 run time등에서 문제가 생기는 경우가 의외로 많다. 예를 들어 runtime jvm 버전이 1.5인데, 최신 버전으로만 컴파일을 하는 것을 정책으로 잡아놓고 1.7로만 컴파일이 되게 하면, 빌드 스크립트에 의해서 컴파일된 바이너리는 run time에서 작동하지 않는다.

 

             <!--  compiler plug in -->

             <plugin>

                <artifactId>maven-compiler-plugin</artifactId>

                       <version>3.0</version>

                <configuration>

                    <source>1.5</source>

                    <target>1.5</target>

                </configuration>

              </plugin>

 

개발환경 별로 컴파일 하기

이를 지원하는 배포 프로세스는 뒤에서 다시 설명하기로 하고, 여기서는 이러한 다중 환경을 지원하기 위해서 같은 소스 코드로 어떻게 다르게 packaging을 할 것인가에 대해서 고민해보자, 각 환경이 다르면, dbms를 접근하기 위한 db url, id,password도 다를 것이며, 기타 디렉토리 구조나, 다른 서버의 ip등이 모두 다를 것이다.

즉 배포 target에 따라서 다른 빌드 프로세스를 타야 한다는 것이다.

 


이를 maven에서는 profile이라는 것으로 지원할 수 있다.

지금 소개하는 Sample 시나리오는 이러한 설정 정보를 WEB-INF/config/config.properties에 정의했다고 하고, 빌드 target에 따라 각각 다른 config.properties 파일을 패키징 하는 시나리오이다.

각각 다른 환경의 config 파일을 저장하기 위해서 ${basedir} 아래에 다음과 같은 이름으로 디렉토리를 만든다.

 

${basedir}/resource-local     : PC 용 설정 정보

${basedir}/resource-dev               : 공용 개발 서버용 설정 정보

${basedir}/resource-qa        : QA 환경용 설정 정보

${basedir}/resource-stage     : Staging 환경용 설정 정보

그리고 각 디렉토리 아래 WEB-INF/config/config.property를 각 환경에 맞게 정의한다.
다음 예제는 dev 환경용 설정 파일이다.

mybatis.jdbc.driverclass=com.mysql.jdbc.Driver

mybatis.jdbc.url=jdbc:mysql://localhost:3306/development

mybatis.jdbc.username=developer

mybatis.jdbc.password=developer

 

s3.url=developer_s3

이렇게 설정한 디렉토리는 다음과 같은 구조를 가지게 된다.

 


 

다음으로, maven pom.xml에서 환경에 맞게 위에 지정한 파일들을 포함해서 패키징 하게 해야 한다. 이를 maven에서는 profile이라고 하고, 다음과 같이 정의한다. 각 빌드 환경마다 여기서는 <environment> 라는 환경 변수를 지정하게 하였다.

<!--  profile definition -->

    <profiles>

        <profile>

               <id>local</id>

               <properties>

                       <environment>local</environment>

               </properties>

        </profile>

        <profile>

               <id>dev</id>

               <properties>

                       <environment>dev</environment>

               </properties>

        </profile>

        <profile>

               <id>qa</id>

               <properties>

                       <environment>qa</environment>

               </properties>

        </profile>

        <profile>

               <id>stage</id>

               <properties>

                       <environment>stage</environment>

               </properties>

        </profile>

  </profiles>

 

그리고 빌드시 ${basedir}/resource-{environment} 디렉토리를  webapp/ 아래에 복사하도록 하고자 한다. 이를 위해서는 war를 패키징 하는 war 플러그인의 속성세 web-resource (WEB-INF 디렉토리)를 빌드 타겟에 맞게 선택이 되도록 다음과 같이 지정한다.

환경에 맞게 정의한다.
다음 예제는 dev 환경용 설정 파일이다.

             <!--  war plug in  -->

             <plugin>

                <groupId>org.apache.maven.plugins</groupId>

                       <artifactId>maven-war-plugin</artifactId>

                       <version>2.3</version>

                       <configuration>

                               <warSourceDirectory>${basedir}/src/main/webapp</warSourceDirectory>

                              <webResources>

                                      <webResource>

                                                     <directory>${basedir}/src/main/resources-${environment}</directory>

                                      </webResource>

                              </webResources>

                       </configuration>

               </plugin>

 

여기 까지 진행한 후에, 각 환경에 맞게 packaging을 하기 위해서는 mvn에서 -P{environment} 이름을 적어주면 된다.

예를 들어 qa환경용으로 war 파일을 만들기 위해서는

 

% mvn -Pqa package

 

와 같이 명령을 실행하면 된다.

 

RPM으로 패키징 및 배포 하기

maven의 빌드 패키징의 문제가 java에 관련된 jar, ear 또는 war 파일만 딱 생성하고 패키징을 한다. 그러나 서버를 배포하기 위해서는 이런 jar형태의 파일 뿐만 아니라 애플리케이션 들이 참고하는 각종 설정 파일들을 함께 배포해야 할 필요가 있다. 운이 좋게 모든 파일들이 jar,ear,war등에 함께 패키징 되면 좋겠지만, 애플리케이션이 어느정도 규모가 되면 함께 패키징이 되지 않는 파일들이 발생한다.

이런 문제를 해결하기 좋은 방법으로는 linux에서 제공하는 rpm 패키징을 이용하는 방법이 있다.

maven에서는 이 rpm 패키징을 플러그인으로 제공한다.

(http://mojo.codehaus.org/rpm-maven-plugin/ )

사용법도 상당히 간단하기 때문에 추천한다. 플러그인은 linux 명령어인 rpmbuild 내부적으로 수행하기 때문에, 반드시 windows 아닌 linux machine에서만 사용이 가능하다.

 

rpm package 여러가지 기능을 수행할 있지만, 여기서 설명하는 것은 기본적으로 file copy 이다.

 

              <!--  rpm plug in -->

              <plugin>

                <groupId>org.codehaus.mojo</groupId>

                <artifactId>rpm-maven-plugin</artifactId>

                <version>2.0-beta-2</version>

                <configuration>

                        <copyright>2013 - Terry Cho</copyright>

                        <group>terry/example</group>

                        <mappings>

                        <mapping>

                               <directoryIncluded>false</directoryIncluded>

                                 <directory>${rpm.install.webapps}</directory>

                                <username>bwcho</username>

                                <groupname>bwcho</groupname>                            

                                 <sources>

                                         <source>

                                                 <location>${basedir}/target/${artifactId}.war</location>

                                         </source>

                                 </sources>

                        </mapping>

                        </mappings>

                    </configuration>

              </plugin>

 

설정을 제공하는 <configuration> 부분을 보자

rpm 패키지에 대한 관리를 위해서 rpm 그룹핑을 제공하는데, <group> rpm 패키지의 그룹을 정의한다. group 여러개의 컴포넌트를 함께 배포할때 매우 유용하게 사용될 있다. 예를 들어 비디오 인코딩 애플리케이션이 있고, 애플리케이션이 upload, download,encoding 컴포넌트 3개로 구성되어 있고, 각각 배포 되어야 한다면 그룹명을 videoencoding/upload, videoencoding/download, videoencoding/encoding 등으로 지정하여 구별할 있다.

다음으로 <mappings> 엘리먼트에서는 복사할 파일 리스트를 지정할 있다. 하위 엘리먼트로 <mapping>이라는 엘리먼트에서 개개별의 디렉토리나 파일을 지정하면 된다.

<sources><source> 엘리먼트에서 복사할 원본 파일 리스트를 지정하고,

<directory> 엘리먼트에서 복사될 타겟 디렉토리를 지정한다.

그리고 <username> <groupname> 파일이 복사될때 (생성되는 파일의) user id group (unix) 정의한다.

위의 예제는 war 파일을 ${rpm.install.webapps} 복사하는 rpm 파일을 생성하는 스크립트이다.

이렇게 생성된 스크립트는

 

% mvn package rpm:rpm  으로 수행될 있다.

앞서 설명한, profile 함께 사용하면, dev rpm 패키지 생성은

% man -Pdev package rpm:rpm

으로 하면된다.

 

이렇게 생성된 rpm 파일들은 대상 시스템으로 복사되서 rpm 수행하는 것만으로도, 모든 의존성을 가진 파일들을 함께 설치할 있다.

아울러 rpm 가지고 있는 고유한 기능으로, 특정 버전으로의 roll back등이 가능하다.

rpm 파일들을 대상 시스템으로 자동으로 복사 실행 하는 부분에 대해서는 뒤에서 다시 살펴 보도록 하자

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. jake 2018.06.27 13:39  댓글주소  수정/삭제  댓글쓰기

    안녕하세요 혹시 ${environment} 값을 java 땅에서 사용할수있나요?