<시작하며..>

apache mina에서 cpu 100% 치는 현상이 발생된 적이 있다. JDK 1.6.0_22 and mina 2.0.2에서는 더 이상 cpu가 치는 현상이 없었지만, old한 jdk 에서 동작하는 apache mina를 위해서 코드를 패치하여 해결했다고 한 상태이다. (2.0.3 패치)

NioProcessor 100% CPU usage on Linux (epoll selector bug)
https://issues.apache.org/jira/browse/DIRMINA-678

이 문제는 Jetty (http://jira.codehaus.org/browse/JETTY-937), jdk(http://bugs.sun.com/view_bug.do?bug_id=6693490, http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933) 와도 밀접한 연관이 있으므로 알아두면 좋을 것 같다.

참고

jdk 1.6.0_22 의 release note(http://www.oracle.com/technetwork/java/javase/6u22releasenotes-176121.html)를 봐도 특별한 내용은 없다.  그러나, 1.6.0_23의 버그 fix (http://www.oracle.com/technetwork/java/javase/2col/6u23bugfixes-191074.html) 의 내용 중 “6728542”은 관련 내용이 있다.  그 내용(http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6728542)은 다음과 같다. 아마도 이 부분을 수정한 것이 아닌가 싶다.

Epoll implementation of java.nio.channels.Selector
(EPollSelectorProvider) doesn't correctly packs
epoll_event struct for all platforms.

 

<내용>

jdk의 버그라 할지라도 문제를 해결했는지에 대한 것은 다른 이슈이다. 수많은 버그를 피해서 만들어둔 코드가 멋지기로 하다. (ibm jike 버그 회피 코드를 잘 짰었다.. ㅋ)

Mina 의 버그질라 및 Jetty, jdk 이슈는 아래 thread dump를 근거로 이야기를 하고 있다. select 하면서 loop에 빠지면서 cpu가 100%가 되는 현상이다. 버그질라에 나온 영어로는 spinning in tight loop 이다.

java.lang.Thread.State: RUNNABLE at sun.nio.ch.EPollArrayWrapper.epollWait(Native Method)
at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:215)
at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:65)
at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:69)
- locked <0x00002aaab3dd8760> (a sun.nio.ch.Util$1)
- locked <0x00002aaab3dd83a0> (a java.util.Collections$UnmodifiableSet)
- locked <0x00002aaab3dd82e8> (a sun.nio.ch.EPollSelectorImpl)
at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:80)
 
<자바 및 jvm 소스>
java.net에 있는 jdk 6 update 22 의 linux 소스를 바탕으로 작성한다. 
(다운로드 받을 수 있는 곳 : http://download.java.net/jdk6/6u10/archive/)
java.nio.channels.Selector클래스의 select() 메서드가 호출되는 tree를 쫓아가본다.
public abstract class Selector {
public abstract int select(long timeout)    throws IOException;
}
-> 

abstract class SelectorImpl   extends AbstractSelector {

    public int select(long timeout)  throws IOException
    {
        if (timeout < 0)
            throw new IllegalArgumentException("Negative timeout");
         return lockAndDoSelect((timeout == 0) ? -1 : timeout);
    }

    private int lockAndDoSelect(long timeout) throws IOException {
    synchronized (this) {
        if (!isOpen())
        throw new ClosedSelectorException();
        synchronized (publicKeys) {
        synchronized (publicSelectedKeys) {
            return doSelect(timeout);
        }
        }
        }
    }

   protected abstract int doSelect(long timeout) throws IOException;
}

-> 
class EPollSelectorImpl  extends SelectorImpl {
….

// The poll object
EPollArrayWrapper pollWrapper;


protected int doSelect(long timeout)  throws IOException
    {
        if (closed)
            throw new ClosedSelectorException();
        processDeregisterQueue();
        try {
            begin();
           pollWrapper.poll(timeout);
        } finally {
            end();
        }
        processDeregisterQueue();
        int numKeysUpdated = updateSelectedKeys();
        if (pollWrapper.interrupted()) {
            // Clear the wakeup pipe
            pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0);
            synchronized (interruptLock) {
                pollWrapper.clearInterrupted();
                IOUtil.drain(fd0);
                interruptTriggered = false;
            }
        }
        return numKeysUpdated;
    }

}

->

/**
* Manipulates a native array of epoll_event structs on Linux:
*
* typedef union epoll_data {
*     void *ptr;
*     int fd;
*     __uint32_t u32;
*     __uint64_t u64;
*  } epoll_data_t;
*
* struct epoll_event {
*     __uint32_t events; 
*     epoll_data_t data;
* };
*/

class EPollArrayWrapper {

int poll(long timeout) throws IOException {
    updateRegistrations();
    updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
    for (int i=0; i<updated; i++) {
        if (getDescriptor(i) == incomingInterruptFD) {
            interruptedIndex = i;
            interrupted = true;
            break;
        }
    }
    return updated;
}

private native int epollWait(long pollAddress, int numfds, long timeout,
                             int epfd) throws IOException;
}

->

EPollArrayWrapper.c


/* epoll_wait(2) man page */

typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

struct epoll_event {
    __uint32_t events;  /* Epoll events */
    epoll_data_t data;  /* User data variable */
} __attribute__ ((__packed__));

JNIEXPORT jint JNICALL
Java_sun_nio_ch_EPollArrayWrapper_epollWait(JNIEnv *env, jobject this,
                                            jlong address, jint numfds,
                                            jlong timeout, jint epfd)
{
    struct epoll_event *events = jlong_to_ptr(address);
    int res;

    if (timeout <= 0) {           /* Indefinite or no wait */
        RESTARTABLE((*epoll_wait_func)(epfd, events, numfds, timeout), res);
    } else {                      /* Bounded wait; bounded restarts */
        res = iepoll(epfd, events, numfds, timeout);
    }
   
    if (res < 0) {
        JNU_ThrowIOExceptionWithLastError(env, "epoll_wait failed");
    }
    return res;
}

static int iepoll(int epfd, struct epoll_event *events, int numfds, jlong timeout)
{
    jlong start, now;
    int remaining = timeout;
    struct timeval t;
    int diff;

    gettimeofday(&t, NULL);
    start = t.tv_sec * 1000 + t.tv_usec / 1000;

    for (;;) {
        int res = (*epoll_wait_func)(epfd, events, numfds, timeout);
        if (res < 0 && errno == EINTR) {
            if (remaining >= 0) {
                gettimeofday(&t, NULL);
                now = t.tv_sec * 1000 + t.tv_usec / 1000;
                diff = now - start;
                remaining -= diff;
                if (diff < 0 || remaining <= 0) {
                    return 0;
                }
                start = now;
            }
        } else {
            return res;
        }
    }
}



<mina쪽 소스>

mina쪽에서 어떻게 해서 무한 루프가 되는 시나리오는 다음과 같다.

select(timeout)를 호출하지만, 바로 jvm 코드에 의해 selected가 리턴된다. isDisposing() 메서드는 종료여부를 알린다. selector와 바인드된 Channel 이 종료되면 disposing 값은 true가 된다.
jvm 내부의 버그로 종료되지 못한 channel 또는 처리해야 할 channel이 존재함으로서, select()에서는 이벤트가 계속 반복되는 현상이 일어나. 계속 무한 루프가 도는 현상이 발생하게 된다.

 

# Apache Mina 2.0.0-RC1 소스

public abstract class AbstractPollingIoProcessor<T extends AbstractIoSession> implements IoProcessor<T> {

private static final long SELECT_TIMEOUT = 1000L;

private class Processor implements Runnable {
        public void run() {
            int nSessions = 0;
            lastIdleCheckTime = System.currentTimeMillis();

            for (;;) {
                try {
                    int selected = select(SELECT_TIMEOUT);

                    nSessions += handleNewSessions();
                    updateTrafficMask();

                     if (selected > 0) {
                        process();
                    }

                    long currentTime = System.currentTimeMillis();
                    flush(currentTime);
                    nSessions -= removeSessions();
                    notifyIdleSessions(currentTime);

                    if (nSessions == 0) {
                        synchronized (lock) {
                            if (newSessions.isEmpty() && isSelectorEmpty()) {
                                processor = null;
                                break;
                            }
                        }
                    }

                     if (isDisposing()) {
                        for (Iterator<T> i = allSessions(); i.hasNext();) {
                            scheduleRemove(i.next());
                        }
                        wakeup();
                     }
                } catch (Throwable t) {
                   …..

                }
            }

/**
* poll those sessions for the given timeout
* @param timeout milliseconds before the call timeout if no event appear
* @return The number of session ready for read or for write
* @throws Exception if some low level IO error occurs
*/
protected abstract int select(long timeout) throws Exception;

}

 

public final class NioProcessor extends AbstractPollingIoProcessor<NioSession> {
    private final Selector selector;

@Override
protected int select() throws Exception {
    return selector.select();
}


}

 
그래서 2011년 Apache Mina 2.0.3 에서는 old jvm을 위한 패치가 이루어졌다. 
Mina 버그질라 (https://issues.apache.org/jira/browse/DIRMINA-678)의 Attachment에 있는
diff 파일을 참조할 수 있다.
image
핵심 코드만 따로 보면, 아래 소스를 보면 된다. 
Selector 안에 Channel이 문제가 있는 것(connection이 broken)일 때는 정리하면 되고, 
이미 연결되어 있어 있는 Channel에 대해서는 selector를 새거로 바꾸고 예전꺼는 정리하는 구조
(NioProcessor.registerNewSelector())로 바꿔놨다.
특별히 이 버그에 대해서 또다른 내용이 등록되어 있지 않아서 잘 동작되는 것 같다. 
# Apache Mina 2.0.4 (수정된 패치 내역이 있는 부분)

public abstract class AbstractPollingIoProcessor<T extends AbstractIoSession> implements IoProcessor<T> {

 protected AtomicBoolean wakeupCalled = new AtomicBoolean(false);

private class Processor implements Runnable {
        public void run() {
            assert (processorRef.get() == this);

            int nSessions = 0;
            lastIdleCheckTime = System.currentTimeMillis();

            for (;;) {
                try {
                    // This select has a timeout so that we can manage
                    // idle session when we get out of the select every
                    // second. (note : this is a hack to avoid creating
                    // a dedicated thread).
                   long t0 = System.currentTimeMillis();
                    int selected = select(SELECT_TIMEOUT);
                    long t1 = System.currentTimeMillis();
                    long delta = (t1 - t0);

                    if ((selected == 0) && !wakeupCalled.get() && (delta < 100)) {
                        // Last chance : the select() may have been
                        // interrupted because we have had an closed channel.
                        if (isBrokenConnection()) {
                            LOG.warn("Broken connection");

                            // we can reselect immediately
                            // set back the flag to false
                            wakeupCalled.getAndSet(false);

                            continue;
                        } else {
                            LOG.warn("Create a new selector. Selected is 0, delta = "
                                            + (t1 - t0));
                            // Ok, we are hit by the nasty epoll
                            // spinning.
                            // Basically, there is a race condition
                            // which causes a closing file descriptor not to be
                            // considered as available as a selected channel, but
                            // it stopped the select. The next time we will
                            // call select(), it will exit immediately for the same
                            // reason, and do so forever, consuming 100%
                            // CPU.
                            // We have to destroy the selector, and
                            // register all the socket on a new one.
                           registerNewSelector();
                        }

                       // Set back the flag to false
                        wakeupCalled.getAndSet(false);

                        // and continue the loop
                        continue;
                    }

                    // Manage newly created session first
                    nSessions += handleNewSessions();

                    updateTrafficMask();

                    // Now, if we have had some incoming or outgoing events,
                    // deal with them
                    if (selected > 0) {
                        //LOG.debug("Processing ..."); // This log hurts one of the MDCFilter test...
                        process();
                    }

                    // Write the pending requests
                    long currentTime = System.currentTimeMillis();
                    flush(currentTime);

                    // And manage removed sessions
                    nSessions -= removeSessions();

                    // Last, not least, send Idle events to the idle sessions
                    notifyIdleSessions(currentTime);

                    // Get a chance to exit the infinite loop if there are no
                    // more sessions on this Processor
                    if (nSessions == 0) {
                        processorRef.set(null);
                       
                        if (newSessions.isEmpty() && isSelectorEmpty()) {
                            // newSessions.add() precedes startupProcessor
                            assert (processorRef.get() != this);
                            break;
                        }
                       
                        assert (processorRef.get() != this);
                       
                        if (!processorRef.compareAndSet(null, this)) {
                            // startupProcessor won race, so must exit processor
                            assert (processorRef.get() != this);
                            break;
                        }
                       
                        assert (processorRef.get() == this);
                    }

                    // Disconnect all sessions immediately if disposal has been
                    // requested so that we exit this loop eventually.
                    if (isDisposing()) {
                        for (Iterator<S> i = allSessions(); i.hasNext();) {
                            scheduleRemove(i.next());
                        }

                        wakeup();
                    }
                } catch (ClosedSelectorException cse) {
                    // If the selector has been closed, we can exit the loop
                    break;
                } catch (Throwable t) {
                    ExceptionMonitor.getInstance().exceptionCaught(t);

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e1) {
                        ExceptionMonitor.getInstance().exceptionCaught(e1);
                    }
                }
            }

 

public final class NioProcessor extends AbstractPollingIoProcessor<NioSession> {
    /** The selector associated with this processor */
    private Selector selector;

@Override
protected void registerNewSelector() throws IOException {
    synchronized (selector) {
        Set<SelectionKey> keys = selector.keys();

        // Open a new selector
        Selector newSelector = Selector.open();

        // Loop on all the registered keys, and register them on the new selector
        for (SelectionKey key : keys) {
            SelectableChannel ch = key.channel();

            // Don't forget to attache the session, and back !
            NioSession session = (NioSession)key.attachment();
            SelectionKey newKey = ch.register(newSelector, key.interestOps(), session);
            session.setSelectionKey( newKey );
        }

        // Now we can close the old selector and switch it
        selector.close();
        selector = newSelector;
    }
}

 

 

마치며..

jvm의 버그로 인해서 회피하는 Apache mina 코드와 연관된 jvm 소스를 살펴보았다. 원천적으로 jvm 코드의 수정이 됨으로서 문제가 해결되었지만, jvm 버그가 언제 고쳐질지 모르거나 old jvm을 쓰면서 이슈가 생기는 것을 막기 위해서 회피하는 코드를 만들었다. 책임감이 강한 멋쟁이 mina commitor.. 잘 되기를 바란다.

Posted by '김용환'
,

 

3년전 (2009년) MBean을 가지고 모니터링 솔루션을 만들다가 JMX에서 memory leak이 있는 것을 발견했었다. 이것을 영어로 어떻게 설명하나, 누구에게 얘기해야 하나  있었는데. 1.6.0_22에서 패치되었다. 굿!

이젠 이런 그림 안봐도 되겠군~

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6957378

Posted by '김용환'
,

 

2009년 6월, 주키퍼(zookeeper)에서 cpu 튀는 현상이 발생했다는 버그가 리포트되었다.

ZooKeeper server unexpectedly high CPU utilisation
https://issues.apache.org/jira/browse/ZOOKEEPER-427

(현상)

5개 노드의 zookeeper를 돌리는데 cpu가 95%까지 튀는 현상이 발견되었다.

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
6883 infact 22 0 725m 41m 4188 S 95 0.5 5671:54 java

(결론)

다들 jvm을 의심을 했지만. 드라마틱하게 문제는 zookeeper 자체 코드였다.

 

zookeeper의 org/apache/zookeeper/server/quorum/QuorumCnxManager.java 소스의 Thread 클래스를 상속받은 RecvWorker 내부 클래스의 run() 메서드에서 받아야 할 메시지를 처리를 잘 못하면서 cpu가 튀었다.

처음 메시지를 받는 4byte를 읽으려고 하는데, 그냥 읽기만 했지 잘못된 메세지를

 

class RecvWorker extends Thread {

    Long sid;

    SocketChannel channel;

    volatile boolean running = true;

     final SendWorker sw;

 

    RecvWorker(SocketChannel channel, Long sid, SendWorker sw) {

        this.sid = sid;

        this.channel = channel;

        this.sw = sw;

    }

 

    public void run() {

        threadCnt.incrementAndGet();

        try {

        byte[] size = new byte[4];

        ByteBuffer msgLength = ByteBuffer.wrap(size);

        while (running && !shutdown && channel != null) {

            while (msgLength.hasRemaining()) {

-                channel.read(msgLength);  // 기존 코드

+                if (channel.read(msgLength) < 0) { // 수정된 코드

+                      throw new IOException("Channel eof");

+                }

       }

       msgLength.position(0);

      int length = msgLength.getInt();

      if(length <= 0) {

         throw new IOException("Invalid packet length:" + length);

      }

}

 

원인은 다음과 같다.


서버가 동작되고 (running=true,  shutdown=false), 채널은 연결되고 (channel instance not null), 4byte를 읽으려고 하는데, 다른 zookeeper의 인스턴스에서 stop하고 –1 을 리턴할 때 이에 대한 처리가 없었다. (channel.read(msgLength) = 1) 그래서 계속 while 문이 반복되면서 cpu ratio가 95%까지 올라갔다.

 

패치는 channel의 첫 바이트를 읽어 문제가 될 때는 IOException을 던져 개발자가 처리할 수 있게 했다.

 

통신프로그램에서는 (nio이든, io이든) read() 메서드에 대해서는 항상 체크하는 습관이 필요하다.

Posted by '김용환'
,

 

(1년 전쯤에 리눅스 서버에서의 JDK/Web/WAS 표준화를 마무리했던 것을 작성해보려고 한다. 함께 같이 고민했던 분들이 기억이 난다. 서로 배려해주고, 이해해주고 회사의 장래에 대한 깊은 고민을 같이 했던 분들을 그 때 많이 만난 것 같다. 정말 고마운 분들 ^^)

표준화를 진행하는 것은 상당히 부담스럽다. 무엇이 표준인가? 라는 답에 대답하기가 쉽지 않다. (표준어가 교양 있는 서울 말인가? 하는 고민과 비슷할 수도 있다. )

네이버에 따르면, 표준은 두가지 의미가 있다. 기준 또는 평균 (영어로도 standard, average, norm이다 )이다.

image

(출처 : http://krdic.naver.com/detail.nhn?docid=40783400)

 

나는 단어 사전의 의미보다는 사회과학의 ‘표준’의 정의가 마음에 들었다. 단순화와 통일화를 도모하는 방법으로 배치,절차, 방법에 대해서 설정된 기준 정도로 했다.

image

(출처 : (http://terms.naver.com/entry.nhn?docId=473521))

 

자유롭게 하지 않고 왜 표준화냐? 에 대한 부분은 공감이 필요할 것 같다.

만약 팀 단위에서 모든 일이 해결한다고 가정하면 이런 표준에 대한 의미가 없을 수 있다. 그러나 점점 버그와 보안 문제로 인한 오픈 소스 거버넌스와 문제, 트러블 슈팅과 같은 기술지원이 빈번해지면서 필요성을 느낄 수 밖에 없었다. 특히 나는 기술지원이나 트러블 슈팅해주다 보니 , 같은 디렉토리를 서로 다른 디렉토리를 쓰다 보니 늘 혼란스러웠다.

어떤 사람은 소스로 설치하고 어떤 사람은 자동 배포(apt-get, yum)으로 하고. 서로 다른 것들로 싸여 있었다. 그렇게 때문에 naming 때문에 고민하기도 했다. 많은 개발자들이 직감적으로 이해할 수 있게 하려고 했다.

웹 서비스가 처음 개발하게 되면 그 상태로 계속 가능 경향이 있었다. 특히 리눅스를 잘 다루는 개발자가 리눅스에서 jdk/web/was를 셋팅한다. 몇 개월 또는 몇 년이 지나 그 환경을 인수인계 받은 사람은 대부분 신입들이 그 자리를 차지한다. 리눅스를 잘 다루면서 코딩까지 자유롭게 하는 사람은 그리 많지 않다. 문제가 생기는데 해결하는 것이 어려워지면서 기술지원팀에 요청하게 되면서 점차 요구사항이 커졌던 것 같다.

코딩은 자유롭게 하되, 리눅스의 환경을 동일하게 맞춘다면 개발자나 시스템 엔지니어가 어떤 팀으로 이동해도 학습 비용은 절감될 수 있을 것이라 믿었다. 또한 root 계정에 쉽게 사용하지 않게 함으로서 보안을 높일 수 있는 믿음이 있었다. 우리가 하향 평준화가 아닌 상향 평준화가 되면 좋겠다는 믿음이 있었다. (잘하는 사람에게는 상당한 부담이 될 수 밖에 없었다. )  마지막으로 일반적인 개발자 입장에서는 표준화된 환경에서 대부분 코드에 집중함으로서 생산성을 높일 수 있는 시간을 줄 수 있도록 하고 싶었다. 그래서 pc나, 리눅스 개발환경의 표준은 일부러 정하지 않고 배포된 서버(release)중심으로 정했다.

 

<배경>

1. 소프트웨어 버전,설치 방법은 개인역량에 따라서 달라졌다. 웹 서비스 자체를 운영 한다면 다양한 스크립트가 양산되는데, 사람의 역량에 너무 의존한 부분이 있었다. 누구는 perl, csh, bash, python, ruby…. 개념은 같고 먼가 복잡성이 거의 없다. 사실 한번 잘 만들면 더 이상 수정하지 않는 특성이 있다.

2. 부서 이동이 많아지면서 웹 서비스를 지탱하는 소프트웨어나  버전을 정확히 답변하는 사람이 드물었다. 기술 지원하려면 시간이 불필요하게 소요되지 않게 하는 표준화가 필요했다.

3. 일반적인 웹 서비스 개발자는 리눅스 자체가 두려움이 대상이 되어 잘 모르면 다칠 수 있다는 것이 있다. 많이 리눅스를 접하고 리눅스 환경에서 익혀본 개발자가 아닌 어플 개발자의 입장에서는 상당히 곤혹스러울 수 있다. jdk, web, was 설치 자체가 번거롭다.

4. 처음부터 오픈 소스 거버넌스(보안, 버그)로 검증된 표준화된  jdk/web/was가 설치된 상태로 서버가 개발자에게 전달하면 바로 웹 어플리케이션만 배포하면 되니. 속도가 빨라질 수 있다.

5. 처음 웹 서비스 개발하는 사람에게는 리눅스 환경을 전혀 알 수가 없다. 따라서 비슷하게 작업하는 공통의 스크립트과 환경 설정을 모아서 물리 서버에 배포하면 웹 서비스를 처음 하는 사람에게 쉽게 노하우를 전파받을 수 있을 것이다.

 

<진행>

초안(워드)를 작성해서 같은 TF사람들/ 웹 어플 배포팀 / 보안 팀 / 시스템 엔지니어 와 함께 협의 하며 진행했다. 300페이지가 넘는 ‘설치가이드’,’노하우가 담긴 설정 가이드’,’예제/체크가이드’ 문서를 작성했다. 우리 모두를 위한 서비스를 위한 jdk/web/was 환경이 담겨있었다.

기본적으로 root 계정이 아닌 임의의 계정으로 통일하고, 기본 디렉토리, 좋은 스크립트, 불필요한 설정 제거, history/psacct, jdk 이슈등에 대한 정보와 web 서버인 apache http와 모듈 정보, was인 tomcat, mod_jk에 대한 상세 설정을 작성했다.

내가 가지고 있는 지식과 직접 소스를 파며 얻었던 지식들을 많이 넣었다. (많이 부족한 사람이 쓴 거라 누군가가 더 좋은 글로 탄생해줄 것이라 믿는다.)

책에는 없는 설명들, 운영하면서 중요하게 생각한 팩터들, 장애 및 보안에 대한 깊은 이해와 노력을 넣었다. 기본적인 트러블 슈팅에 대한 정보를 넣었다. 단편적인 인터넷 문서나 너무 많은 jdk/web/was 매뉴얼의 단점을 보완하고 꼭 필요한 내용으로 작성한 문서였다.

PPT를 작성해서 한 눈에 보면 디렉토리 구조와 심볼링 링크를 따라갈 수 있게 했고, 전체적으로 빨리 볼 수 있게 했다.

 

<마치며…>

저말 개발자에게 도움이 되는 좋은 표준안이 되기를 진심으로 바랬던 마음으로 쓰려고 했다. 한편 다른 마음으로는 그 시간에 코딩을 하고 싶었다. 지금 돌아보니 나의 욕심 대신 개발자를 섬긴 시간들이 소중했던 것 같다. 겸손함과 진지함, 상대방에 대한 존중을 배웠던 것 같다. 

Posted by '김용환'
,

 

http://www.usenix.org/events/hotos03/tech/full_papers/vonbehren/vonbehren.pdf

이 paper를 읽으니, 재미가 있었다. 기억에 남는 것은 thread 방식의 문제를 compiler가 고치면 된다고 했다. dynamic stack growth, live state management, synchronization 등의 문제에 대해서 compiler의 도움을 얻는다면 성능이 잘 나올 수 것 같다고 작성했다.

------

내가 다니고 있는 회사의 웹 서버에서 사용하는 수많은 서버들의  web 서버들은 여전히 apache http가 가장 편하고 안정적이기 때문에 이를 사용하고 있다. 또한 nginx를 일부 사용하고 있다. was인 tomcat과 연동해서 사용할 때는 대부분 apache http 서버를 사용하고 있고, 주로 static 서버로 사용할 때는 nginx 를 사용하고 있다. 물론 nginx는 tomcat과 연동해서 쓰고 있다.

event-driven이든, fork/thread 방식이든 상관없이 이미 안전하고 운영하기 편리한 것으로 검증 받는 것으로 가는 것은 사람의 기본 심리인 것 같다.

2003년도에 thread나 event 방식의 개발 중에 어느 것이 더 좋은 것인지 학자들끼리 토론하는 것이 있었던 것 같다. 나는 그 때 머했나? 음. 당시에 다니던 전직 회사에서는 thread에 대한 부담감이 많이 있었다. 미들웨어 개발 당시에 thread의 남발로 인한 부작용이 심각했다. 너무 많은 thread들이 돌아다니면서 conditional 코드들이 생기고 코드는 점점 복잡해져 갔고, 무거워져 갔다. 성능이 느려지기 시작했다. 또한 코드 또한 복잡해졌다. 당시에 쓰레드를 남발하고 synchronized 블럭을 잘 써서 개발자라면 능력 있는 개발자로 여기는 행태도 있었다.

운영체제에서 쓰는 one thread에서 동작하는 polling 방식의 event 방식으로 구현하는 것은 어떨까 고민하고 구현하던 중이던 시점인 것 같다. event driven 구현 방식도 api를 제공이나 내부 구현의 복잡성, 속도에 대해서 한계가 존재했다.

구현 후 느낀 점은.. 어떻게 구현해도 만족시키지 못했던 것 같다. 각각 장/단점이 있었던 것 같다. 그 틀에서 어떻게 좀더 성능을 높인다는 것은 뼈를 깎는 고통인 것으로 기억한다. 나의 무식과 무지가 많이 드러나서 힘들었다.  웹 환경에서는 장비를 늘리고, scalabity를 지원하는 오픈소스를 사용하는 지금의 모습과는 많이 다른 것 같다.

Posted by '김용환'
,

 

다른 개발자가 개발한 파일업로드용 웹 서버의 코드를 보면서 든 예전에 이런 부분에 대해서 간단히 노트해 본다.

수많은 개발자들이 “당연히 nio가 io보다 성능이 좋다”라는 인식을 많이 하고 있다. 나도 그렇게 생각해오긴 했었다. 그러나 그런 것만은 아니다. 환경에 따라서 io가 nio보다 성능이 더 좋을 수 있다.  내가 보았던 테스트중의 일부는 nio보다 io가 더 빨랐다. (진짜!. io가 nio보다 10~20% 정도 더 나왔다)

한 블로그(http://geekomatic.ch/2009/01/16/1232134440000.html)의 글에서는 nio가 io에 비해서 판정승으로 성능이 좋다.

java.io versus java.nio

 

 

다른 블로그(참조 : http://geekomatic.ch/2008/09/06/1220730740479.html) 의 글을 참조해 본다. 역시 nio가 io보다 성능이 좀 더 좋다.

x축은 크기, y축은 속도를 의미하며, 파란색은 nio이고, 빨간색은 io 테스트이다. 용량이 작을 때는 오히려 io가 더 속도가 높을 때(file size가 10mb)가 있다. 그러나 큰 용량으로 갈 수록 nio가 확실히 속도가 빨라진다.

java.io versus java.nio

 

구글의 한 개발자(http://www.mailinator.com/tymaPaulMultithreaded.pdf)의 발표 자료을 보면, nio보다 io가 성능이 더 높게 나오게 했다. (do not compare charts 를 하지 말라고 했지만. 해버렸다.)

image

또다른 분의 글(http://www.thebuzzmedia.com/java-io-faster-than-nio-old-is-new-again/)을 참조하면 위의 ppt에 사족을 다신 분이 있다. 이 분의 글을 잠시 빌려온다면 다음과 같다.

NIO가 빠른 이유는 asynchrous와 non-blocking 때문에 빠르다는 것이다.

 

 

누가 더 성능이 좋냐. 참 성능 이라는 지표가 io/net, 코드, vm, OS에 depedent가 있기 때문에 상당히 애매모호하다.

stackoverflow(http://stackoverflow.com/questions/1605332/java-nio-filechannel-versus-fileoutputstream-performance-usefulness)에 좋은 자료가 있다.

아래 코드를 가지고 테스트했는데, nio보다 io의 성능이 더 좋냐는 질문이었다.

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class JavaNIOTest {
    public static void main(String[] args) throws Exception {
        useNormalIO();
        useFileChannel();
    }

    private static void useNormalIO() throws Exception {
        File file = new File("/home/developer/test.iso");
        File oFile = new File("/home/developer/test2");

        long time1 = System.currentTimeMillis();
        InputStream is = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(oFile);
        byte[] buf = new byte[64 * 1024];
        int len = 0;
        while((len = is.read(buf)) != -1) {
                fos.write(buf, 0, len);
        }
        fos.flush();
        fos.close();
        is.close();
        long time2 = System.currentTimeMillis();
        System.out.println("Time taken: "+(time2-time1)+" ms");
    }

    private static void useFileChannel() throws Exception {
        File file = new File("/home/developer/test.iso");
        File oFile = new File("/home/developer/test2");

        long time1 = System.currentTimeMillis();
        FileInputStream is = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(oFile);
        FileChannel f = is.getChannel();
        FileChannel f2 = fos.getChannel();

        ByteBuffer buf = ByteBuffer.allocateDirect(64 * 1024);
        long len = 0;
        while((len = f.read(buf)) != -1) {
                buf.flip();
                f2.write(buf);
                buf.clear();
        }

        f2.close();
        f.close();

        long time2 = System.currentTimeMillis();
        System.out.println("Time taken: "+(time2-time1)+" ms");
    }
}

 

좋은 답이라 투표를 받은 글의 전문은 다음과 같다.  너무 괜찮아서 번역 없이 그냥 작성한다.  결론은 nio 코딩할 때는 nio 코딩의 묘미에 맞게 코딩해야 제대로 성능이 나온다는 것이다.

My experience with larger files sizes has been that java.nio is faster than java.io. Solidly faster.Like in the >250% range. That said, I am eliminating obvious bottlenecks, which I suggest your micro-benchmark might suffer from. Potential areas for investigating:

The buffer size. The algorithm you basically have is

  • copy from disk to buffer
  • copy from buffer to disk

My own experience has been that this buffer size is ripe for tuning. I've settled on 4KB for one part of my application, 256KB for another. I suspect your code is suffering with such a large buffer. Run some benchmarks with buffers of 1KB, 2KB, 4KB, 8KB, 16KB, 32KB and 64KB to prove it to yourself.

Don't perform java benchmarks that read and write to the same disk.

If you do, then you are really benchmarking the disk, and not Java. I would also suggest that if your CPU is not busy, then you are probably experience some other bottle neck.

Don't use a buffer if you don't need to.

Why copy to memory if your target is another disk or a NIC? With larger files, the latency incured is non-trivial.

Like other have said, use FileChannel.transferTo() or FileChannel.transferFrom(). The key advantage here is that the JVM uses the OS's access to DMA (Direct Memory Access), if present.(This is implementation dependent, but modern Sun and IBM versions on general purpose CPUs are good to go.) What happens is the data goes straight to/from disc, to the bus, and then to the destination...by passing any circuit through RAM or the CPU.

The web app I spent my days and night working on is very IO heavy. I've done micro benchmarks and real-world benchmarks to. And the results are up on my blog, have a look-see:

Use production data and environments

Micro-benchmarks are prone to distortion. If you can, make the effort to gather data from exactly what you plan to do, with the load you expect, on the hardware you expect.

My benchmarks are solid and reliable because they took place on a production system, a beefy system, a system under load, gathered in logs. Not my notebook's 7200 RPM 2.5" SATA drive while I watched intensely as the JVM work my hard disc.

What are you running on? It matters.

 

 

마치며..

nio 가 io 보다 성능이 낮게 나온다면 nio가 성능이 나오도록 제대로 코딩했는지 살펴볼 필요가 있다. bottleneck이 있는지, 버퍼를 너무 크게 잡았는지, api를 잘 사용했는지, 상황에 맞게 코딩했는지 살펴보면서 테스트하면 좋은 결과가 있을 것 같다.

Posted by '김용환'
,

 

전 직장에서는 awk가 궁금하다 싶으면, 그냥 man awk 보면 된다고 했다. 그러나 현실은 man으로 할 수 없다. 그냥 매뉴얼일 뿐..

awk를 처음하거나, 오랜만에 다뤄보는 사람들을 위한 글을 소개한다.

순서대로 보는게 좋다.

 

http://www.kingcomputerservices.com/unix_101/processing_files_with_awk.htm

http://gregable.com/2010/09/why-you-should-know-just-little-awk.html

http://www.askapache.com/linux/awk-tutorial.html

http://www.grymoire.com/Unix/Awk.html

Posted by '김용환'
,