일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 한글조사처리
- ORM
- PKCS#8
- date
- PKCS
- Freemaker
- Callable
- sha1
- 자바
- ACAP
- DAMO
- AES
- Runnable
- Executors
- 암호학
- RSA
- IPTV
- String
- Log4J
- mac
- xlet
- Java
- 이클립스 플러그인 개발
- 한글조사
- Executor
- Instrumentation
- 자바 암호화
- JCE
- StringUtils
- Postman
- Today
- Total
오늘은 어디로 갈까...
JMX를 이용한 daemon 모니터링 본문
daemon을 모니터링하는것은 중요한 일이다. 이놈이 살았는지 죽었는지 알 수 없다면 참 난감한 일일 것이다. 그렇다면 어떻게 모니터링 할 수 있을까? ps -ef 를 사용해서 단순히 프로세스가 살아있는지 확인을 할 수도 있지만, 살아 있어도 살아 있는게 아닐 수도 있어서 신용이 안간다. 그렇다고 살아있다~고 매초마다 파일로 출력하자니 모양새가 좋지 않고, 소켓을 통해 구현할려고 하면 귀차니즘이 발동하고... 어떻하면 좋을까? 결론부터 말하자면 걱정하지 말아라, 자바에서는 JMX란 아주 좋은(?) 관리 API를 제공하고 있다. (솔직히 JMX를 배우는게 더 힘들지 않을까 ^^;)
1. JMX(Java Management Extensions)
- JMX는 말그대로 여러 자원을 감시(?) 관리 하기 위한 자바 API 이다. JDK 1.5 이상부터는 기본적으로 지원하니, 별다른 노력없이도 사용할 수 있다. 관련 스펙은 JSR 3: Java management Extensions Specification과 JSR 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이란 접두사 인터페이스를 만들어서 구현하는법과
- 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답게 구현해보길 바란다.
- 오라클이 선을 잡아먹었으니, 자바의 앞날은 어떻게 될까나 ^^;