야후의 속도 개선(http://developer.yahoo.com/performance/rules.html) 은 워낙 유명하다.. 당연히 해야 하고..

그러나 이미지를 많이 쓰는 경우라면 별 짓을 해도 소용이 없다..

이외 더 있다면.. 이미지 캐쉬 서버를 멀티도메인으로 쓰는 것이다.

이미지 캐쉬 서버는 일반적으로  CDN 장비를 사용하고 있는데, 이를 여러개의 이미지 서버로 두게 한다. 수십대에서 수백대까지 있을 수 있는데, 트래픽 때문에 중간에 스위치없이 DNS를 사용한다. 디폴트는 가중치가 없으니. 골고루 ip를 받을 수 있다.

웹 브라우져는 connection을 여러 개를 한번에 병렬 처리를 할 수 있는데, 멀티 도메인을 사용하면 이미지나 css,js를 CDN으로부터 빨리 받음으로서 속도를 빨리 개선할 수 있다. (표준은 2개지만, 웹 브라우져마다 여러개의 socket 개수를 가지게 하고 있다.)

도메인을 제외한 full path를 기반으로 hashing key를 만들어서 1,2,3,4,5 등 cdn 서버 도메인을 가르키게 해서 빨리 이미지 로딩이 되게 하면 속도가 향상된다.

Posted by '김용환'
,

 

내가 쓴 아래 블로그에 대한 후속 공부들이다.

http://knight76.tistory.com/entry/Google%EC%9D%98-TCP-Fast-Open-paper
http://knight76.tistory.com/entry/%EA%B5%AC%EA%B8%80-TCP-Fast-Open-paper-TFO%EB%A5%BC-%EC%9D%BD%EA%B3%A0

 

Let’s make tcp faster(http://googlecode.blogspot.com/2012/01/lets-make-tcp-faster.html) 페이지에 보면 리눅스 커널에 패치된 글이 나온다.

image

1. 리눅스 2.6.39-rc1에 “TCP initial congestion window”을 10으로 수정했다.
2. 리눅스 3.1-rc1에 처음 재전송 timeout을 1초로 수정했다.
3. 리눅스 3.2-rc1에 Proportional Rate Reduction이 수정했다.
4. TCP Fast Open은 아직 리눅스 커널에 구현되지 않았다. 테스트를 진행중이다.

마치며…

구글의 리눅스 커널 패치하는 사람이 있어서 그런지 빨리 패치한다. 영향력이 대단하다. 사실 그동안 성능 튜닝하면서 TCP 커널 파라미터를 수정하려고 하면, 시스템 엔지니어들이 얼마나 반대했었다. 믿을 수 없다… 점점 구글이 관련 글을 쓰면서 디폴트값을 수정하는 것을 보면.. 진짜 멋지기도 하다. 리눅스 커널을 패치해야 시스템 엔지니어들도 따라올테니….

Posted by '김용환'
,


이 블로그는 내가 쓴 쓴글에 대한 후속이다. 

http://knight76.tistory.com/entry/Google%EC%9D%98-TCP-Fast-Open-paper 
 

 

http://static.googleusercontent.com/external_content/untrusted_dlcp/research.google.com/ko//pubs/archive/37517.pdf

 

예전에 RTOS의 통신 프로토콜, 방송용 전송 프로토콜(mpeg2, dab 프로토콜,)을 일부 구현하면서 transport layer에 대해서 관심이 많다. 이번에 구글에서 나온 TCP Fast Open을 보고, “TCP / IP Illustrated, Volume 1”을 다시 보았다. 그리고, “Blind TCP/IP hijacking is stall alive(TCP 하이재킹)” 블로그도 읽었다.

 

최근 웹 페이지 분석 자료.

(참조 : http://code.google.com/intl/ko-KR/speed/articles/web-metrics.html)

 

이 paper는 TCP 연결 기본과 관찰력으로 나왔다고 볼 수 있다.  파일을 읽고 쓰는 것보다 파일을 열고 닫는 것이 리소스가 많이 드는 것처럼 네트웍 소켓을 열고 닫는 부분에 대해서 고민하는 것과 비슷하다.  이 paper는 TCP 연결의 여는 부분에 대해서 고민했다.

크롬 브라우져와 구글 서버간의 통신 상태를 2011년에 28일 동안 분석해보았다. 33%정도가 Http persistent 연결이었다. 최신 브라우져는 서버당 연결 개수를 높여 빠르게 로딩하려고 하고 있다.
(TCP 3-way handshake 방식은 connection마다 이루어지기 때문에 서버와 클라이언트에 부하를 이룬다. 그러나 DDOS 나 트래픽이 너무 몰릴 때에는 안좋을 수 있어서 Keep Alive를 하지 않는 경우가 많다. Apache Http 서버의 기본 설정이 keep alive가 아닌 것으로 바뀐 것은 의미가 있다. 그렇지만 역시 keep alive 상태로 해서 http permanent connection인 경우에는 확실히 부하를 적게 할 뿐 아니라 브라우져의 병렬 처리 소켓 개수로 인해서 속도를 높일 수 있어서 점차 사용이 확대되고 있다. )

새로운 TCP 연결에 대한 요청은 Cold Request, 이미 연결된 TCP 연결에 대한 요청을 Warn request라고 정의했다. 새로운 TCP를 연결(Cold request) 하는데 Cost가 많이 들었다. Cold request는 8~28%의 Cost가 더 든다.

 

Cold request가 50% 정도 느리다.

 

TCP 3 way handshaking은 delay와 중복 syn 패킷을 처리하기 위해서 만들어졌다. Tcp Fast Open은  이 철학을 잘 이해하고 이 부분에 좀 더 보강을 했다. TCP 패킷은 security “Cookies” 값을 추가했다. 기존에 hijacking당할 수 있는 TCP에 보안적인 부분을 염두해서 보강했다. 클라이언트는 cookie 요청을 하지만, 실제로 cookie generation은 서버에서 하게 한다. 클라이언트는 cookie를 캐쉬해두고 사용한다. 서버는 이 쿠키를 validate한다.

TFO의 중요한 포인트는 client와 server간의 3 way handshaking 을 하면서 데이터가 전달되게 하는 것이다.  이렇게 함으로서 보다 많은 패킷을 전달할 수 있다.

 

 

그리고, TCP의 congestion control은 따로 수정할 필요가 없으며 sendto, sendmsg 와 같은 시스템콜을 수정하여 편하게 쓸 수 있게 했다.

 

좋아진 결과는 4~10% 정도였다. RTT가 늦어야 효과가 높았다. 떄로는 41%까지 좋아졌다.

현재는 IETF에 제안서(http://tools.ietf.org/html/draft-cheng-tcpm-fastopen-00)를 넣은 상태이다.

 
2012년 1월 까지 리눅스 커널에 패치되지는 않은 상태이다.
 

마치며..

response time이 긴 웹 서버의 경우의 서버, 국내용 서버라면 쓰기 어려운 부분이 있지만 세계를 배경으로 하는 웹 서버에 대해서는 쓸만한 대안이 될 수 있겠다는 생각이 들었다. 현재 필리핀이나 중국에서 한국 웹 서버를 접근하는데 시간이 오래 소요된다고 한다. TFO가 리눅스 커널에 적용된다고 여러 어플에 적용된다면 좋을 수도 있겠다는 생각이 든다.

Posted by '김용환'
,

 

notepad++은 ultraedit와 같은 강력한 기능은 없지만 왠만한 기능은 다 있다. 내가 자주 사용하는 괜찮은 팁을 정리한다.

 

1. 컬럼 단위의 Copy & Paste

alt 키 누르고 영역지정사용

image

 

2. 매크로 사용

매크로 메뉴 이용

image

 

3. unix/윈도우즈/mac 변환

image

 

4. 소스 비교

image

 

5. 분할 창 이동

vi처럼 창을 분할해서 보고 싶은 때 (2단만 되는 듯)

image

 

6. 정규 표현식 테스트 및 찾기

image

Posted by '김용환'
,

 

리눅스에서 디스크 용량을 체크하려면, df 나 du 명령어를 사용한다.

df는 파일시스텤 크기를 지정한다.

]# df -h
Filesystem            Size  Used Avail Use% Mounted on
/dev/sda1              28G  4.8G   23G  18% /
tmpfs                1014M     0 1014M   0% /dev/shm

]# du –h

엄청 많이 나온다.

 

그냥 du를 쓰기에는 조금 버겨울 수 있는데, 간단한 팁이 있다. 옵션 –hsc를 사용하여 해당 디렉토리를 넣어주면, 서브 디렉토리의 내용은 상세하게 보지 않아도 서브 디렉토리의 디스크 용량을 편리하게 찾아낼 수 있다.

 

]# cd /usr

]# du -hsc *
24K     X11R6
69M     bin
8.0K    etc
8.0K    games
60M     include
529M    lib
29M     libexec
346M    local
24M     sbin
528M    share
122M    src
4.0K    tmp
1.7G    합계

Posted by '김용환'
,

 

<시작하며..>

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 '김용환'
,