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답게 구현해보길 바란다.
- 오라클이 선을 잡아먹었으니, 자바의 앞날은 어떻게 될까나 ^^;