오늘은 어디로 갈까...

JMX를 이용한 daemon 모니터링 본문

낙서

JMX를 이용한 daemon 모니터링

剛宇 2009. 4. 21. 12:05

 daemon을 모니터링하는것은 중요한 일이다. 이놈이 살았는지 죽었는지 알 수 없다면 참 난감한 일일 것이다. 그렇다면 어떻게 모니터링 할 수 있을까? ps -ef 를 사용해서 단순히 프로세스가 살아있는지 확인을 할 수도 있지만, 살아 있어도 살아 있는게 아닐 수도 있어서 신용이 안간다. 그렇다고 살아있다~고 매초마다 파일로 출력하자니 모양새가 좋지 않고, 소켓을 통해 구현할려고 하면 귀차니즘이 발동하고... 어떻하면 좋을까? 결론부터 말하자면 걱정하지 말아라, 자바에서는 JMX란 아주 좋은(?) 관리 API를 제공하고 있다. (솔직히 JMX를 배우는게 더 힘들지 않을까 ^^;)

1. JMX(Java Management Extensions)
  - JMX는 말그대로 여러 자원을 감시(?) 관리 하기 위한 자바 API 이다.  JDK 1.5 이상부터는  기본적으로 지원하니, 별다른 노력없이도 사용할 수 있다. 관련 스펙은 JSR 3: Java management Extensions SpecificationJSR 160 : Java Management Extendsion (JMX) Remote API 을 참조 바란다.

2. JMX 원격접속 설정 파일
 - 로컬에서 직접 접근이 가능하지만, 여기서는 원격접속방법만을 다루도록 하겠다.
 - JMX 원격접속을 위해서는 jmxremote.access과 jmxremote.password 파일이 필요하다.
   (jmxremote.access는 사용자 권한이 정의되어있고, jmxremote.password에는 사용자 비밀번호가 정의되어있다.)
 - jmxremote.access는 $JAVA_HOME/jre/lib/management/ 폴더에 위치해 있다. 사용자 추가가 필요없을 경우는 그냥 사용하면 된다.
 - jmxremote.password는 $JAVA_HOME/jre/lib/management/에 템플릿파일(jmxremote.password.template)만 존재한다. jmxremote.password.template을 복사하여 jmxremote.password 파일을 만든후, 사용자를 추가해주면 된다.
   기본값으로 "QED" 비밀번호를 가진 monitorRole 사용자와, "R&D" 비밀번호를 가진 controlRole 사용자가 주석처리되어있다. 이 두 사용자는 jmxremote.access에 이미 정의되어 있으므로, 주석만 풀어주면 사용할 수 있다.

monitorRole  QED
controlRole   R&D


3. jmxremote.password 파일을 읽기 권한만 있도록 변경해준다.
  - 유닉스 환경이라면 퍼미션을 600으로 주면 된다.
  chmod 600 jmxremote.password

  - 윈도우 환경이라면 아래 명령어를 사용할 수 있다.
  cacls jmxremote.password /P [username]:R


4. 실행하는 자바에 JMX 옵션을 추가해주자.
  - 모든 프로그램에 적용하려면 $JAVA_HOME/jre/lib/management/management.properties 파일에 옵션을 추가해주면 된다.
com.sun.management.jmxremote
com.sun.management.jmxremote.port=8991
com.sun.management.jmxremote.ssl=false


  - 해당 프로그램에 적용하라면 -D 옵션을 줘서 추가해주면 된다.
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8991
-Dcom.sun.management.jmxremote.ssl=false


  - 만약 원격접속 설정 파일을 따로 적용하려면 아래와 같은 옵션을 추가해주면 된다.
com.sun.management.jmxremote.access.file=filepath (기본값은 $JRE/lib/management/jmxremote.access)
com.sun.management.jmxremote.password.file=filepath (기본값은 $JRE/lib/management/jmxremote.password)



  - 인증을 하지 않으려면 다음 옵션을 추가해주면 된다.
com.sun.management.jmxremote.authenticate=true


  - 옵션에 대한 설명은 management.properties 파일을 보면 잘 나와있다.


5. JMX 옵션 추가하여 PostMan 실행하기
 - 기본 postman.sh의 시작부분을 아래와 같이 수정한다.
 - -Dcom.sun.management.jmxremote 옵션이 추가된것을 알 수 있다.
	start)
    #
    # Start PostMan
    #
    $DAEMON_HOME/jsvc/jsvc \
    -user $DAEMON_USER \
    -home $JAVA_HOME \
    -Dcom.sun.management.jmxremote \
    -Dcom.sun.management.jmxremote.ssl=false \
    -Dcom.sun.management.jmxremote.port=8991 \
    -Dcom.sun.management.jmxremote.password.file=$CP_HOME/jmxremote.password \
    -wait 10 \
    -pidfile $PID_FILE \
    -outfile $POSTMAN_HOME/logs/postman.out \
    -errfile '&1' \
    -cp $CLASSPATH \
    kr.kangwoo.postman.daemon.PostManDaemon
    #
    # To get a verbose JVM
    #-verbose \
    # To get a debug of jsvc.
    #-debug \
    exit $?
    ;;



6. jconsole 실행하기
 - $JAVA_HOME/bin 에 보면 jconsole이 존재한다. 실행하면 다음과 같은 화면을 볼 수 있다.
   Local은 말그대로 로컬에서 작동하는 JVM을 보여주고 Remote와 Advanced는 원격접속해서 보여준다.
 - Remote를 선택한 후 접속정보를 넣어준다.(기본값을 그대로 사용했다면,Use Name : monitorRole, Password : QED)
 - Advanced를 선택했다면 JMX URL을 service:jmx:rmi:///jndi/rmi://localhost:8991/jmxrmi 형식으로 넣고 사용자 정보를 입력하면 된다.



7. MBean(Managed Bean)
 - JDK 1.5에는 9개의 MBean이 정의되어 있다.(http://java.sun.com/j2se/1.5.0/docs/api/java/lang/management/package-summary.html)

CompilationMXBean 컴파일러
GarbageCollectorMXBean 가비지 컬렉터
MemoryMXBean 메모리
MemoryManagerMXBean 메모리 관리자
ThreadMXBean 쓰레드
OperatingSystemMXBean 운영체제
RuntimeMXBean 런타임
ClassLoadingMXBean 클래스 로더
MemoryPoolMXBean 메모리 풀

 - 물론 사용자가 MBean을 만들어서 추가해줄 수도 있다.




8. JMXClient 예제

- jconsole 처럼 클라이언트를 직접 구현할 수 있다. 단순히 이런게 있다고 참고용으로만 설명하는것이나, 구경만 하시길 바란다.(예제에는 없지만, NotificationBroadcaster를 구현한 MBean일 경우는 NotificationListener를 통해서 변경된 정보를 통보 받을 수도 있다.)
package kr.kangwoo.postman.jmx;

import java.io.IOException;
import java.lang.management.MemoryUsage;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.openmbean.CompositeData;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

public class JMXClient {

	public static void main(String[] args) throws Exception {
	    String user = "monitorRole";
	    String password = "QED";
	    
	    String[] credentials = new String[] { user, password };
   
	    Map<String, String[]> props = new HashMap<String, String[]>();
	    props.put("jmx.remote.credentials", credentials);

	    JMXServiceURL address = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:8991/jmxrmi");
	    JMXConnector connector = null;
	    try {
		    connector = JMXConnectorFactory.connect(address, props);
		    
		    MBeanServerConnection mbs = connector.getMBeanServerConnection();
		    ObjectName name = null;
		    
		    name = new ObjectName("java.lang:type=ClassLoading");
		    System.out.println("* " + name);
		    MBeanInfo mBeanInfo = mbs.getMBeanInfo(name);
		    MBeanAttributeInfo[] attrInfos = mBeanInfo.getAttributes();
		    for (MBeanAttributeInfo attrInfo : attrInfos) {
		    	System.out.println(attrInfo.getName() + " = " + mbs.getAttribute(name, attrInfo.getName()));
		    }
		    
		    name = new ObjectName("java.lang:type=MemoryPool,*");
		    Set<ObjectName> pools = mbs.queryNames(null, name);
		    for (ObjectName on : pools) {
		    	System.out.println("* " + on);
		    	mBeanInfo = mbs.getMBeanInfo(on);
		    	MemoryUsage usage = MemoryUsage.from((CompositeData)mbs.getAttribute(on, "Usage"));
			    System.out.println(usage);
		    }
		    
	    } finally {
	    	if (connector != null) try { connector.close(); } catch(IOException ie) {}
	    }

	}
}


9. MBean 만들어보기
 - PostMan에 알맞는(?) Mbean을 만들어보자. MBean은 XxxMBean이란 접두사 인터페이스를 만들어서 구현하는법과
DynamicMBean을 상속받아 구현하는 법이 있다. 여기서는 간단한 인터페이스를 만들어서 구현해보자.
 - reloadTemplate() 메소드를 만들어서 템플릿 정보를 요청이 들어올때만 읽어오게 처리하고, futureList의 크기를 반환하는 간단한 MBean을 만들겠다.
package kr.kangwoo.postman.jmx;

public interface MonMBean {

	public void reloadTemplate();
	
	public int getFutureListSize();

}

package kr.kangwoo.postman.jmx;

public class Mon implements MonMBean {

	private int futureListSize;
	private boolean reloadTemplate = false;

	public void setFutureListSize(int futureListSize) {
		this.futureListSize = futureListSize;
	}

	public int getFutureListSize() {
		return futureListSize;
	}

	public void reloadTemplate() {
		reloadTemplate = true;
	}

	public boolean isReloadTemplate() {
		return reloadTemplate;
	}

	public void setReloadTemplate(boolean reloadFlag) {
		this.reloadTemplate = reloadFlag;
	}
}

package kr.kangwoo.postman.jmx;

public class DummyMon extends Mon {

}
 - DummyMon은 전혀 필요 없는 물건이긴 하다. 단순히 MBean을 사용안할때 null 체크하기가 귀찮아서 만든 유령 클래스일 뿐이다.

 - 자 그럼, PostManDaemon 클래스와 PostManFastJob. 클래스를 수정해보자.   - 현재 PostMan이 spring 기반이라서 JMX 처리 부분도 Spring-JMX를 이용하는게 도리이기는 하나, 복잡성만 가중시킬수 있기에, 순수한 자바 형식으로 하기에 조금 므훗한(?) 구조가 되어버렸다. 중요한 것은 손가락이 아니라 달인게 아닌가? 하.하.하. ^^;
package kr.kangwoo.postman.daemon;

import java.lang.management.ManagementFactory;

import javax.management.MBeanServer;
import javax.management.ObjectName;

import kr.kangwoo.postman.core.PostManFastJob;
import kr.kangwoo.postman.jmx.Mon;

import org.apache.commons.daemon.Daemon;
import org.apache.commons.daemon.DaemonContext;
import org.quartz.SchedulerException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

public class PostManDaemon implements Daemon {

	private ApplicationContext applicationContext;

	public void init(DaemonContext context) throws Exception {
		System.err.println("PostManDaemon: instance "+ this.hashCode() + " created");
//		String[] args = context.getArguments();
//		if (args != null) {
//			for (String arg : args) {
//				System.out.println(arg);
//			}
//		}
	}

	public void start() throws Exception {
		System.err.println("PostManDaemon: instance "+ this.hashCode() + " start");
		applicationContext = new ClassPathXmlApplicationContext(new String[] {"post-man-scheduler.xml"});
		
		
		MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();

		ObjectName name = new ObjectName("kr.kangwoo:type=PostMan");
		Mon mBean = new Mon();
		mbs.registerMBean(mBean, name);
		
		PostManFastJob postManJob = (PostManFastJob)applicationContext.getBean("postManJob", PostManFastJob.class);
		postManJob.setPostManMBean(mBean);
		
	}

	public void stop() throws Exception {
		System.err.println("PostManDaemon: instance "+ this.hashCode() + " stop");
		if (applicationContext != null) {
			SchedulerFactoryBean scheduler = (SchedulerFactoryBean)applicationContext.getBean("scheduler", SchedulerFactoryBean.class);
			if (scheduler != null) {
				scheduler.stop();
			}
		}
	}

	public void destroy() {
		System.err.println("PostManDaemon: instance "+ this.hashCode() + " destroy");
		if (applicationContext != null) {
			SchedulerFactoryBean scheduler = (SchedulerFactoryBean)applicationContext.getBean("scheduler", SchedulerFactoryBean.class);
			if (scheduler != null) {
				try {
					scheduler.destroy();
				} catch (SchedulerException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	public static void main(String[] args) throws Exception {
		PostManDaemon daemon = new PostManDaemon();
		daemon.init(null);
		daemon.start();
	}
}


- MBean의 reload 플래그 값을 읽어와서 템플릿을 다시 읽어오게 하는 부분과, futureList의 크기 정보를 저장하는 부분이 추가되었다.
package kr.kangwoo.postman.core;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import kr.kangwoo.postman.domain.Mail;
import kr.kangwoo.postman.jmx.DummyMon;
import kr.kangwoo.postman.jmx.Mon;
import kr.kangwoo.postman.service.MailManager;
import kr.kangwoo.postman.service.MailSendManager;
import kr.kangwoo.postman.service.MailTemplateManager;
import kr.kangwoo.util.StopWatch;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PostManFastJob {

	private Logger logger = LoggerFactory.getLogger(PostManFastJob.class);
	
	private String daemonName = getClass().getName();
	
	private MailManager mailManager;
	private MailTemplateManager mailTemplateManager;
	private MailSendManager mailSendManager;
	
	private int threadPoolSize = 10;
	private int listSize = 100;	// 데이터 베이스에서 한번에 가져올 목록 크기
	private int futureMaxSize = 1000; //ThreadPool에 적재할 작업들의 최대크기
	
	private Mon postManMBean = new DummyMon();
	
	private boolean isFirst = true;
	
	public void setDaemonName(String daemonName) {
		this.daemonName = daemonName;
	}

	public void setMailManager(MailManager mailManager) {
		this.mailManager = mailManager;
	}

	public void setMailTemplateManager(MailTemplateManager mailTemplateManager) {
		this.mailTemplateManager = mailTemplateManager;
	}

	public void setMailSendManager(MailSendManager mailSendManager) {
		this.mailSendManager = mailSendManager;
	}
	
	public void setThreadPoolSize(int threadPoolSize) {
		this.threadPoolSize = threadPoolSize;
	}

	public void setListSize(int listSize) {
		this.listSize = listSize;
	}

	public void setFutureMaxSize(int futureMaxSize) {
		this.futureMaxSize = futureMaxSize;
	}

	public void setPostManMBean(Mon postManMBean) {
		this.postManMBean = postManMBean;
	}

	public void run() {
		
		StopWatch stopWath = new StopWatch();
		ExecutorService executorService = null;
		try {
			if (isFirst) {
				// 처음 실행할때는 템플릿 정보를 읽어오게 한다.
				isFirst = false;
				postManMBean.setReloadTemplate(true);
			}
			
			if (postManMBean.isReloadTemplate()) {
				postManMBean.setReloadTemplate(false);
				
				logger.info("메일 템플릿 정보 적재 시작");
				mailTemplateManager.reload();
				logger.info("메일 템플릿 정보 적재 완료");
				
				if (logger.isDebugEnabled()) {
					logger.debug("메일 템플릿 적재 소요 시간 -> " + stopWath.getElapsedTimeString());	
				}
			}
			
			List<Mail> sendList = null;
			List<Future<Mail>> futureList = new ArrayList<Future<Mail>>();
			
			postManMBean.setFutureListSize(0);
			
			while (!Thread.currentThread().isInterrupted() 
					&& ((sendList = mailManager.getSendList(daemonName, listSize)) != null) 
					&& sendList.size() > 0) {
				if (logger.isDebugEnabled()) {
					logger.debug("{}개의 메일을 가져왔습니다.({})", sendList != null ? sendList.size() : 0, stopWath.getElapsedTimeString());	
				}
				
				if (executorService == null) {
					executorService = Executors.newFixedThreadPool(threadPoolSize);
				}

				String subject = null;
				String content = null;
				for (Mail mail : sendList) {
					subject = mailTemplateManager.getSubject(mail);
					content = mailTemplateManager.getContent(mail);
					mail.setStatusCode(MailStatusCode.SEND_READY);
					
					futureList.add(executorService.submit(new MailSendWorker(mailSendManager, mail, subject, content)));
				}
				
				postManMBean.setFutureListSize(futureList.size());
				
				// 완료된 작업 정리하기.
				int futureListSize = futureList.size();
				List<Future<Mail>> doneList = new ArrayList<Future<Mail>>();
				Date updatedDate = new Date();
				for (Future<Mail> future : futureList) {
					if (future.isDone()) {
						doneList.add(future);
						
						try {
							Mail mail = future.get();
							mail.setUpdatedBy(daemonName);
							mail.setUpdatedDate(updatedDate);
							mailManager.updateMail(mail);
						} catch (Exception e) {
							logger.warn("메일 발송 중 에러가 발생했습니다.", e);
						}
					}
				}
				if (doneList.size() > 0) {
					for (Future<Mail> future : doneList) {
						futureList.remove(future);
					}
					if (logger.isDebugEnabled()) {
						logger.debug("FutureList ({}->{})", futureListSize, futureList.size());	
					}
					futureListSize = futureList.size();
				}
				
				if (futureListSize > futureMaxSize) {
					if (logger.isDebugEnabled()) {
						logger.debug("FutureList가 최대크기를 초과하여 정리합니다.({}/{})", futureList.size(), futureMaxSize);	
					}
					for (Future<Mail> future : futureList) {
						try {
							Mail mail = future.get();
							mail.setUpdatedBy(daemonName);
							mail.setUpdatedDate(updatedDate);
							mailManager.updateMail(mail);
						} catch (Exception e) {
							logger.warn("메일 발송 중 에러가 발생했습니다.", e);
						}
					}
					futureList.clear();
					if (logger.isDebugEnabled()) {
						logger.debug("FutureList ({}->{})", futureListSize, futureList.size());	
					}
				}
				
			}
			
			postManMBean.setFutureListSize(futureList.size());
			
			// 작업 완료될때까지 대기
			Date updatedDate = new Date();
			for (Future<Mail> future : futureList) {
				try {
					Mail mail = future.get();
					mail.setUpdatedBy(daemonName);
					mail.setUpdatedDate(updatedDate);
					mailManager.updateMail(mail);
				} catch (Exception e) {
					logger.warn("메일 발송 중 에러가 발생했습니다.", e);
				}
			}
			
			postManMBean.setFutureListSize(0);
			
			if (logger.isDebugEnabled()) {
				logger.debug("메일 발송 소요 시간 -> " + stopWath.getElapsedTimeString());	
			}
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		} finally {
			if (executorService != null) {
				executorService.shutdown();
			}
		}
	}
}

- jconolse을 실행해보자. MBean 탭에 보면 kr.kangwoo가 추가되어 있을것이다. attributes를 통해 fururelistsize를 알 수 있고(데이터를 넣고 refresh를 겁나게~ 누르면 변하는것을 볼 수 있다. ^^;), operations을 통해 reloadTemplate()메소드를 실행할 수도 있다.

참고 : http://java.sun.com/docs/books/tutorial/jmx/mbeans/standard.html


10. 넋두리.
  - 이런 기본 원리(?)를 이용하면 멋진 모니터링 프로그램을 만들 수 있을지도 모른다.
  - 사실 MC4J(http://www.mc4j.org/)를 이용하면 futureListSize를 그래프로 볼 수 있다. --;
  - 주기적으로 값을 가져오는 방법도 있지만, NotificationBroadcaster를 통해서 통보(?)받는 방법도 존재한다.
  - Spring에서도 JMX를 지원하니, 게으른 본인처럼 저렇게 구현하지 말고, Spring답게 구현해보길 바란다.
  - 오라클이 선을 잡아먹었으니, 자바의 앞날은 어떻게 될까나 ^^;