Linux Memory Checker (java)


들어가며

가끔 회사 생활을 하다보면 이해할 수 없는 단순작업을 지시받는 경우가 있습니다.

이런 단순작업들을 귀찮음에 미쳐버릴거 같아서 꼼수를 생각해 냅니다.

잔머리는 언제나 옳습니다.

발단

한 지붕 ( 서버 ) 여러 구성원이 같이 사는 경우가 간혹 있습니다.

집이라는 녀석이 비싸서 구성원 수가 여러명이 될 수가 있죠.

이런 비싼 집에서 누군가가 매번 화장실을 독차지하고 있으면

다른 구성원들은 화장실을 못 가죠.

이 화장실을 독차지하는 얌체 구성원이 누군지 집에 CCTV 가 없는 이상 확인 할 방법이 없죠.

그래서 화장실 앞에서 누가 들어가는지 20분 마다 스냅샷을 찍어 놓으 랍니다.

20분마다 직접 체크를 하려니 좀이 쑤시고 내가 이걸 왜 하고 있는지 자괴감이 듭니다.

계획

  1. 화장실을 누가 많이 쓰는지 확인 할 수 있는 방법을 모색합니다.
  2. 이 방법을 정해 진 시간에 체크 합니다.
  3. 이렇게 쌓인 데이터로 누가누가 많이 썼나 찾을 겁니다.

확인 방법

  1. 서버에 OS 는 redhat 이라는 linux 입니다.
  2. ps 라는 명령어를 이용하여 현 시점에 차지하고 있는 메모리 점유율을 체크 합니다.

ps -Ao rss,pcpu,pmem,cmd –sort -rss

  1. 명령어
    1. ps : linux 프로세스 확인 명령어
    2. -A : 모든 프로세스 -e 와 동일한 옵션
    3. o : 유저가 정의한 규격으로 표기 -o, –foramt 과 동일한 옵션
    4. rss : resident set size 사용메모리용량
    5. pcpu : %cpu
    6. pmem : %memmory
    7. cmd : 명령어
    8. –sort -rss: 정렬 rss 기준으로

참조사항으로 가장 많이 쓰는 ps 옵션인 ps -ef 는 모든 프로세스를 표준규격으로 표기하라 입니다. 제 raspberryPI 로 테스트를 해 보면 jekyll 과 mysql 이 가장 많은 메모리를 사용하고 있습니다.

Java 코딩

  1. 확인방법에 사용한 명령어를 이용하여 데이터 베이스에 남길 겁니다.
  2. 프로세스는 다음과 같습니다.
    1. ssh 명령어를 전달 할 수 있는 라이브러리를 이용하여 명령어를 실행
    2. Link ethz.ssh2
    3. 결과 값을 가져옴
    4. 결과 값을 데이터베이스에 저장
  3. 소스는 매우 간단 합니다.
    1. 서버 접속할 ip, id, password 를 가지고 ethz.ssh2 Connection 을 사용하여 접속하고 Session 을 얻어 옵니다.
    2. 접속 된 Session에 메모리체크 명령어를 전달해 주고 결과 값을 받아 옵니다.
    3. 전달 받은 결과 값을 필터링 합니다. ( 머릿글 제거, 메모리 0 항목 제거)
    4. 결과 값을 VO List에 저장 해두고 완료 시 데이터 베이스에 insert 합니다.
    5. database insert CMD 데이터는 clob 으로 사용하였습니다.
  4. 아래와 같이 하나에 클래스에 간단하게 담아 냅니다.
package com;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.StreamGobbler;

public class MemoryChecker {

	public static void main(String[] args) throws Exception {
		runCheck();
	}
	public static void runCheck() throws Exception {
		String serverIp, command, usernameString, password ;

		 // 서버 ip
		 serverIp = "127.0.0.1";
		 // id
		 usernameString = "ubuntu";
		 // password
		 password = "password";
		 
		 
		 // 메모리 체크 명령어
		 command = "ps -Ao rss,pcpu,pmem,cmd --sort -rss";
		 
		 // 접속 객체 선언
		 Connection conn = null;
		 // 세션 선언
		 ch.ethz.ssh2.Session sess = null;
		 // 결과값을 읽어들일 버퍼
		 BufferedReader br = null;
		 
		 System.out.println("inside the ssh function");
	     try
	     {
	    	 
	    	 // ip 에 접속
	         conn = new Connection(serverIp);
	         conn.connect();
	         // 권한획득
	         boolean isAuthenticated = conn.authenticateWithPassword(usernameString, password);
	         if (isAuthenticated == false)
	        	 // 권한 획득 실패시 오류 print 후 종료
	             throw new IOException("Authentication failed.");
	         // 권한 획득 시 세션 획득
	         sess = conn.openSession();
	         // 명령어 전달
	         sess.execCommand(command);
	         // 결과 값 가져오기
	         InputStream stdout = new StreamGobbler(sess.getStdout());
	         // 결과 값 버퍼에 저장
	         br = new BufferedReader(new InputStreamReader(stdout));
	         System.out.println("the output of the command is");
	         List<MemoryVO> pList = new ArrayList<MemoryVO>();
	         
	         // 결과 값 VO에 삽입
	         // 머리행 제거, 메모리 미사용 프로세스 제거
	         while (true)
	         {
	             String line = br.readLine();
	             
	             
	             if (line == null) {break;}
	             MemoryVO mVO = new MemoryVO();
	             mVO = printStr(line);
	             if(mVO.getCmd() != null && !mVO.getCmd().substring(0,1).equals("R") && !mVO.getCmd().substring(0,1).equals("0")) { 
	            	 // 명령어 문자열 trim 처리
	            	 pList.add(printStr(line));
	             }

	         }

	         // VO 에 삽입 된 데이터 insert 
	         insertDB(pList);
	         System.out.println("ExitCode: " + sess.getExitStatus());

	     }
	     catch (IOException e)
	     {
	         e.printStackTrace(System.err);

	     }	
	     finally {
	         if(br != null) br.close();
	         if(sess != null) sess.close();
	         if(conn != null) conn.close(); 
	     }
	}
	

    // 명령어 trim 처리 후 객체에 담아 return
	public static MemoryVO printStr(String param) {
		
		MemoryVO rtnVO = new MemoryVO();
		
		String line = "";
		line = param.trim();
		
		rtnVO.setCmd(line);
		
        return rtnVO;
	}
	
	public static void insertDB(List<MemoryVO> param) throws Exception{
		
		// sql connection 선언
		java.sql.Connection con = null;
		// 쿼리 전달을 위한 statement
		PreparedStatement pstmt = null;
		
		Statement stmt = null;
		
		String insertSql = "INSERT INTO COM_24_SERVER_MEM_CHECK(RNO, CMD, CREATE_TIME ) "
				+ "VALUES ((SELECT NVL(MAX(RNO),0) + 1 FROM COM_24_SERVER_MEM_CHECK) , ? , SYSDATE )";

		
		try {
			Class.forName("oracle.jdbc.driver.OracleDriver");
			con = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:mrstest","orcl","1234");
			stmt = con.createStatement();

			pstmt = con.prepareStatement(insertSql);
			
			int insertCnt = 0;
			for ( MemoryVO pVal : param) {
				
				pstmt.setString(1, pVal.getCmd());

				insertCnt ++;
				
				pstmt.addBatch();
				
				pstmt.clearParameters();
				
				if(  ( insertCnt % 10000) == 0  ) {
		    		pstmt.executeBatch();
		    		pstmt.clearBatch();
		    		con.commit();
		    	}
				
			}
			
			int[] result = pstmt.executeBatch();
			
			pstmt.clearBatch();
			con.commit();
			System.out.print(result.toString());
		} catch(Exception e) {
			e.printStackTrace();
		} finally {
			if(stmt != null ) { stmt.close(); }
			if(con  != null ) { con .close(); }
		}
		
	}
	
}

Jar 생성

이렇게 간단하게 만들어진 코드를 jar 파일료 export 시키고

명령어로 실행이 가능 합니다.

eclipse 를 사용 한다는 전제로 설명해 보겠습니다.

프로젝트 폴더에 우측 버튼을 누르면 아래와 같이 메뉴가 생성 되는데 여기서 export 를 선택 합니다.

Runnalbe JAR file 을 선택 해 줍니다.

여기서 주의 할 점은 적어도 한번은 실행 해 본 클래스파일이여야 가능 합니다. 그리고 Library handling 에서 Package required libraries into generated JAR 를 선택 해 주어야 jar 파일 하나로 실행할 때 오류를 줄 일 수 있습니다. 라이브러리를 포하하지 않고 있다면 많은 오류를 야기시킬 수 있겠습니다.

이렇게 jar 파일을 생성 하면

java -jar LinuxCMD.jar

이와 같이 실행이 가능해 집니다.

스케쥴 생성

이제 이 생성 된 jar 파일을 20분마다 실행 해 주면

데이터 베이스에 쌓일 겁니다.

여기서는 linux OS를 이용해서 스케쥴을 걸어 보겠습니다.

작업용 PC는 윈도우 기반이지만 작업용 PC는 항상 켜있지 않으니

raspberryPI를 이용하여 해보겠습니다. 이녀석은 24시간 대기 중이니까요.

일단 파일 서버를 이용하여 jar 파일을 raspberryPI 로 옮겨 주고

raspberryPI 에는 java가 이미 설치되어 있습니다.

/home/ubuntu/jars 폴더에 놓겨 놓습니다.

리눅스 스케쥴러인 crontab 을 이용합니다.

crontab list 확인

crontab -l

crontab editor 모드 진입

crontab -e

이런 화면이 보일 텐데

최하위에 추가해서 넣습니다.

순서대로

분, 시간, 일, 월, 요일, 명령어 순으로 입력 해 주면 됩니다.

요일 : 0 = 일요일, 6 = 토요일

* 로 표기하면 전체

1-3 : 구간 정의 할 떄는 - 를 사용합니다.

재부팅시 사용하려면 : @reboot 명령어

ex)

@reboot sh /home/ubuntu/jars/execute.sh

메모리체크를 매월 4~말일 월요일 ~ 금요일 20분 마다 이렇게 지정을 한다고 하면

20 */3 4-31 * 1-5 java -jar /home/ubuntu/jars/LinuxCMD.jar

이렇게 설정하시면 됩니다.

추가 참조

이렇게 모아진 데이터 베이스에 정보를 가시적으로 보기 위해서 아래와 같이

쿼리를 작성해서 사용했습니다.

쿼리를 풀어 보자면

  1. cblob 데이터를 읽어 들일 수 있는 string 형태로 변환 합니다.
    1. dbms_lob 에 susbstr 함수를 이용하여 4천자씩 분리 시킵니다.
  2. 데이터 중 space 를 기준으로 첫번째 항목이 메모리 사이즈 이므로 보기좋게 GB로 변환 해 줍니다.
  3. pcpu, pmem 는 수치 그대로 보여줍니다.
  4. 날짜를 년월일 시간 으로 변환합니다.
  5. 메모리 점유율 상위 10 개를 필터링 합니다.
WITH TEMP AS (
SELECT
RNO,
LENGTH(CMD) LEN,
REGEXP_REPLACE(dbms_lob.substr(CMD , 4000, 1),'\s{2,}',' ') CMD,
CREATE_TIME
FROM COM_24_SERVER_MEM_CHECK
--WHERE RNO = 66
)
, MEM_DATA AS (
SELECT  
RNO,LEN,
ROUND(REGEXP_SUBSTR(CMD, '[^[:space:]]+',1,1)/1024/1024,2)  REMSIZE,
REGEXP_SUBSTR(CMD, '[^[:space:]]+',1,2) PCPU,
REGEXP_SUBSTR(CMD, '[^[:space:]]+',1,3) PMEM,
SUBSTR(CMD, REGEXP_INSTR(CMD,'[^[:space:]]+', 1,4),LEN) CMD,
TO_CHAR(CREATE_TIME,'YYYYMMDD HH24') CREATE_TIME
FROM TEMP
)
SELECT * FROM (
    SELECT
    RNO,
    REMSIZE,
    PCPU,
    PMEM,
    ATTR,
    CREATE_TIME,
    ROW_NUMBER() OVER (PARTITION BY RNO ORDER BY TO_NUMBER(REMSIZE) DESC) RNUM  
    FROM MEM_DATA
)
WHERE RNUM < 11
;