[입 개발] Java.net.InetAddress 의 getLocalHost() 버그…

모 오픈소스를 실행시키다가 이상한 일이 생겨서, 버그인가 하고 보다가… 재미난 현상을 발견했습니다. Java.net.InetAddress 가 뭔가 이상한 결과를 넘겨주는 것입니다. 먼저… 간단한 소스를 보시죠. 테스트 프로그램은 다음과 같습니다.(결론부터 말하자면… 자바의 버그라고 할 수는 없습니다. ㅋㅋㅋ, DNS 변경으로 일단 원하는 결과가 나오는 ㅎㅎㅎ)

* 결론적으로는 U+등이 디지털 네임스랑 계약을 맺고, 분석이 되지 않는 도메인을 디지털네임스로 질의하고, 이를 디지털 네임스에서 키워드로 등록되었거나, 등록되지 않은 주소를 자신의 ip등으로 돌려줘서 발생하는 이슈로 추측되고 있습니다.)

import java.io.*;
import java.util.*;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class Test {
  public static void main(String [] args) {
    try {
      System.out.println(InetAddress.getLocalHost());
    } catch(UnknownHostException var1) {
      System.out.println("Exception : " + var1);
    }
  }
}

그런데 그 결과가 다음과 같습니다. -_-

charsyam ~/works/test $ java Test
charsyam.local/218.38.137.28
charsyam ~/works/test $ java Test
charsyam.local/218.38.137.28
charsyam ~/works/test $ java Test
charsyam.local/192.168.1.7
charsyam ~/works/test $ java Test
charsyam.local/192.168.1.7

네, getLocalHost()를 호출한 결과가 218.38.137.28 이거나 192.168.1.7이 나옵니다. 실제 저희 집의 네트웍은 공유기 밑에 접속이 되는 것이라, 192.168.1.7이 기대한 값입니다. 혹시나 외부 아이피인가 해서 확인해도 제 공유기가 가진 아이피도, 위의 218.38.137.28 값은 아니었습니다. 전혀 상관 없는 값이죠.

그런데 재미있는 것은 이 것은 단순히 자바의 문제는 아니라는 것입니다.
dig/nslookup 으로 해본 결과입니다.

nslookup 결과

charsyam ~ $ nslookup Macintosh-7.local
Server:		192.168.1.1
Address:	192.168.1.1#53

Name:	Macintosh-7.local
Address: 218.38.137.28

dig 결과

charsyam ~ $ dig Macintosh-7.local

; <<>> DiG 9.8.3-P1 <<>> Macintosh-7.local
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40380
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;Macintosh-7.local.		IN	A

;; ANSWER SECTION:
Macintosh-7.local.	3600	IN	A	218.38.137.28

;; Query time: 5 msec
;; SERVER: 192.168.1.1#53(192.168.1.1)
;; WHEN: Sun Dec 28 23:44:09 2014
;; MSG SIZE  rcvd: 51

네 DNS 결과가 이상합니다. 흑흑흑, 일단 네임서버가 뭔가 이상한 결과를 던져주는 건 일단 확실한데… 제가 보기엔 여기에 설정된 네임서버가 이상하고, 여기서 질의도 이상한데, 거기에 대해서 이상한 결과를 주는 듯 합니다.

그럼 왜 위의 getLocalHost()의 결과가 저럴까요? 이름만 보면 localhost를 줘야 할 것 같은데… 그게 아닙니다.
이제 Java 소스를 까보도록 하겠습니다. 저부분만…

코드를 해석하면 처음에 LocalHostName을 가져옵니다. 저의 경우는 Macintosh-7.local 이겠죠.
그리고 위의 값이 localhost면 그냥 루프백 주소를 줍니다. 즉 이러면 127.0.0.1 이나 정상적인 값이 갈듯 합니다.
그리고 그 호스트네임을 이용해서 InetAddress.getAddressesFromNameService 을 이용해서 InetAddress를 가져오는데, 여기서 뭔가 해당 도메인 파싱이 잘못되고, 도메인 네임서버로 가서 이상한 결과가 오는게 아닌가 싶습니다.

    public static InetAddress getLocalHost() throws UnknownHostException {

        SecurityManager security = System.getSecurityManager();
        try {
            String local = impl.getLocalHostName();

            if (security != null) {
                security.checkConnect(local, -1);
            }

            if (local.equals("localhost")) {
                return impl.loopbackAddress();
            }

            InetAddress ret = null;
            synchronized (cacheLock) {
                long now = System.currentTimeMillis();
                if (cachedLocalHost != null) {
                    if ((now - cacheTime) < maxCacheTime) // Less than 5s old?
                        ret = cachedLocalHost;
                    else
                        cachedLocalHost = null;
                }

                // we are calling getAddressesFromNameService directly
                // to avoid getting localHost from cache
                if (ret == null) {
                    InetAddress[] localAddrs;
                    try {
                        localAddrs =
                            InetAddress.getAddressesFromNameService(local, null);
                    } catch (UnknownHostException uhe) {
                        // Rethrow with a more informative error message.
                        UnknownHostException uhe2 =
                            new UnknownHostException(local + ": " +
                                                     uhe.getMessage());
                        uhe2.initCause(uhe);
                        throw uhe2;
                    }
                    cachedLocalHost = localAddrs[0];
                    cacheTime = now;
                    ret = localAddrs[0];
                }
            }
            return ret;
        } catch (java.lang.SecurityException e) {
            return impl.loopbackAddress();
        }
    }

참고로 getAddressesFromNameService 는 다음과 같이 DNS 프로바인더를 이용해서 실제 DNS쿼리를 하게 됩니다.

    private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)
        throws UnknownHostException
    {
        InetAddress[] addresses = null;
        boolean success = false;
        UnknownHostException ex = null;

        // Check whether the host is in the lookupTable.
        // 1) If the host isn't in the lookupTable when
        //    checkLookupTable() is called, checkLookupTable()
        //    would add the host in the lookupTable and
        //    return null. So we will do the lookup.
        // 2) If the host is in the lookupTable when
        //    checkLookupTable() is called, the current thread
        //    would be blocked until the host is removed
        //    from the lookupTable. Then this thread
        //    should try to look up the addressCache.
        //     i) if it found the addresses in the
        //        addressCache, checkLookupTable()  would
        //        return the addresses.
        //     ii) if it didn't find the addresses in the
        //         addressCache for any reason,
        //         it should add the host in the
        //         lookupTable and return null so the
        //         following code would do  a lookup itself.
        if ((addresses = checkLookupTable(host)) == null) {
            try {
                // This is the first thread which looks up the addresses
                // this host or the cache entry for this host has been
                // expired so this thread should do the lookup.
                for (NameService nameService : nameServices) {
                    try {
                        /*
                         * Do not put the call to lookup() inside the
                         * constructor.  if you do you will still be
                         * allocating space when the lookup fails.
                         */

                        addresses = nameService.lookupAllHostAddr(host);
                        success = true;
                        break;
                    } catch (UnknownHostException uhe) {
                        if (host.equalsIgnoreCase("localhost")) {
                            InetAddress[] local = new InetAddress[] { impl.loopbackAddress() };
                            addresses = local;
                            success = true;
                            break;
                        }
                        else {
                            addresses = unknown_array;
                            success = false;
                            ex = uhe;
                        }
                    }
                }

                // More to do?
                if (reqAddr != null && addresses.length > 1 && !addresses[0].equals(reqAddr)) {
                    // Find it?
                    int i = 1;
                    for (; i < addresses.length; i++) {
                        if (addresses[i].equals(reqAddr)) {
                            break;
                        }
                    }
                    // Rotate
                    if (i < addresses.length) {
                        InetAddress tmp, tmp2 = reqAddr;
                        for (int j = 0; j < i; j++) {
                            tmp = addresses[j];
                            addresses[j] = tmp2;
                            tmp2 = tmp;
                        }
                        addresses[i] = tmp2;
                    }
                }
                // Cache the address.
                cacheAddresses(host, addresses, success);

                if (!success && ex != null)
                    throw ex;

            } finally {
                // Delete host from the lookupTable and notify
                // all threads waiting on the lookupTable monitor.
                updateLookupTable(host);
            }
        }

        return addresses;
    }

[BenchMark] ActiveMQ의 성능에 대해서

최근에 ActiveMQ를 써 볼일이 생겼는데, 기대한 것 보다 성능이 너무 안나와서

좌절하고 있습니다.
일단 기본적인 Persistant 나 VMCursor 를 조절해도 속도가 변경될 여지를 안보이네요.
그래서 결국 간단한 프로그램을 만들어서 돌려봤는데
사양: vCore 8개
메모리: 16G(이중 JVM에는 1G 할당)
최초에 하나의 단일 Producer 로 동작할 때는(TSimpleServer)
초당 -_- 250 tps 정도
현재 앞단이 thrift 인데 이게 느려서 그럴 수도 있지만 -_- 그래도 성능이 T.T
다시 멀티 스레드로 ConnectionPool을 만들어서 테스트 하니(TNonblockingServer)
스레드 5개~20개로 늘려도
대략  초당 370 tps 거의 50% 성능 향상이지만, -_- 전혀 도움이 안되는 쿨럭…
물론, ActiveMQ 설정이 제대로 안된거고, 내가 성능을 못쓰는 형태로 구현한거
같긴한데 아직 확인해볼 부분이 많이 필요할듯 하다 -_-

zlib-1.2.3 에서의 libxml2 와 gzopen64 문제

 누군가에게는 쉬운 문제인지도 모르겠지만, CentOS 5.3에서 zlib-1.2.3 이 깔려있는 상태에서

libxml2 가 자꾸 gzopen64 가 undefined symbol 이라는 오류를 내면서 빌드가 되지 않았다.

 

 결론부터 말하자면, zlib-1.2.5를 누군가가 설치했고, 이에 따라서, include 와 zlib 의 library 가 꼬이면서

발생하는 문제다. zlib-1.2.3에는 gzopen64가 없는데, include 는 zlib-1.2.5를 가리키면서, 실제 gzopen

이 gzopen64로 매핑될려고 해서 발생한 것!!!

 

 정답은 zlib-1.2.5를 지우고 /etc/ld.so.conf 에서 제대로 위치를 지정해주면 된다. rpm을 깔면 zlib는

/usr/lib 에 설치된다.

 

 그런데 여기서 나는 .so 만 지우고 include 는 지우지 않는 실수를 -_-; 결국 이 빌드 오류를 찾는데만

하루종일 걸렸다. -_- 

실수를 줄이는 방법은?

아, 오늘도 또 대형사고를 쳐버렸습니다. 신규 서비스를 배포했는데, 바로 버그가 발견되서, 롤백해야 되는 아픈 순간이…

더더욱 문제는, 버그의 원인이, 어려운 것도 아니고, 아주 간단한 것을 실수해 버렸기 때문입니다. 그것도  두가지나!!!

첫번째는, 코드의 문제입니다. 아주 간단한 룰을 꺼꾸로 써버려놓고, 대충 테스트를 하고(딱 버그가 일어나기 직전까지만…) 아 문제없다라고 생각하고 넘어가버렸습니다. 반대로, 그 제대로 테스트를 했을 경우에는 해당 버그가 발생하지 않는 케이스 였다라는게 더더욱 문제였습니다.

QA 기간에 QA에서 버그를 발견하지 못하는 것은 QA의 책임은 아니라고 생각합니다. 개발자가 변경 사항에 대해서 더 자세히 알려주고, 테스트케이스에 대해서 고민했다면… 아마도 쉽게 발견했을지도 모르겠습니다. 일단, 저는 이번에 그러지 못했습니다.

두번째는, 배포직전에 백업을 조금 잘못해서, 롤백하는데 대략 1~20분 정도 시간이 걸렸습니다. 사실 이 부분도 큰 문제라고 생각합니다. 1번의 실수는 그래도 단지 실수일 뿐이지만… 두번째는 잘못된 행동이기 때문입니다.

물론, 어떻게 보면, 변명의 여지도 있습니다. 아주 바뻐서, 모두 다 제대로 챙기기에는 힘들었을 수도 있습니다. 하지만… 제대로 해야 하는 것들을 제대로 하지 못한 것을 단지 실수로 여기기 보다는, 여기서 개선점을 찾아내야 한다고 생각합니다.

그래서 “회고” 가 있는 것이겠죠.

이제부터는 기능을 구현하기 전에, 이것을 어떻게 테스트 해야 잘 할 수 있는가 부터, 고민을 해야겠습니다. 그리고,  할 수 있는한, 점검표를 만들어서 실제로 점검하면서 할 수 있도록 해야겠습니다.(단순 반복 작업)

그리고 귀찮다고 대충하는 것은 정말정말 피해야겠습니다.

“절차를 체계화 하기” 보다 좋은 “절차를 자동화하기!!!”

우리는 반복되는 작업의 실수를 줄이기 위해서 작업의 절차를 체계화하는 작업을 하게 됩니다.

항상 빌드시 마다, 리소스의 버전을 올린 다음 배포한다고 할 때, 그냥 담당자가 알아서 할 수

있습니다. 그런데, 담당자가 차례를 잘못 기억할 수 도 있고, 담당자가 아닌, 다른 사람이

해당 작업을 수행해야 하는 경우도 생길 수 있습니다.

  그렇다면, 이런 과정에서 실수하게 되는 것들을 어떻게 하면, 줄이거나, 아예, 제로로

만들 수 있을까요?

첫번째는 해당 작업을 체계화 또는 순서화 시키는 겁니다.

예를 들어, 다음과 같은 간단한 체크 리스트를 만들 수도 있습니다.

순서 확인
1. 리소스의 버전 업 하기  
2. 빌드 하기  
3. 빌드 결과물 인증하기  
4. 인증한 결과물 인증 확인  
5. 결과물 설치 파일 만들기  
6. 서버에 결과물 배포  

이제 우리는 해당 작업을 할 때 마다, 위의 체크 리스트를 확인 해서, 실수를

줄일 수 있습니다.

그런데 이렇게 체계화 하는 것보다 더 좋은 방법이 있습니다.

그것은, 위의 절차를 자동화 시켜버리는 것입니다.

build 라고 치면, 자동적으로 리소스의 버전을 업하고, 빌드하고, 해당 결과에 인증서를

통해서 인증하고, 설치 파일을 만드는 것입니다. 이렇게 될 경우, 사람이 위의 체크리스트

기반으로 체크하는 것보다, 훨씬 빠르게 진행할 수 있습니다. 그리고 해당 시간에, 작업자는

배치 스크립트등만 가볍게 돌려주고, 자신은 다른 작업을 할 수 있습니다.

이렇게 자동화를 할 수 있는 것들에는 여러가지가 있을 수 있습니다.

1. 유닛테스트 실행하고, 결과 리포트 하기

2. 한방 빌드

3. LOC 나 Code Coverage 측정

이렇게 점점 자동화를 하고 나면 생기는 장점을 다시 정리하면

1. 작업자의 실수를 줄여준다. 품질이 올라간다.

2. 작업자의 시간을 아껴준다. 그 시간을 자기계발등으로 쓸 수 있다.

대박 실수 – 계약에 의한 프로그래밍의 필요성

메모리를 많이 쓰면 저 세상으로 황급히 가시는 프로그램때문에, 최대한 메모리를 아끼는

(그러기에는 이제는 너무 string 객체에 익숙해져 버렸지만…) 작업을 해야했다.

다행히도, 동시에 그 객체를 건드리는 일이 없었기 때문에, 아이디어가 나온게, 정적으로

한번만 할당하고, 계속 그 메모리를 이용하자였다.

그리고 이 메모리를 끊어서 줘야 하는 경우가 생겼는데, 이를 위해서 간단히 한 바이트를

기억해두고, 0으로 변경 후, 전송하고, 다시 원래대로 돌리는 작업을 하게 되었다. 그런데,

그러니 문제가 생겼다. –_-, 깜빡하고 마지막에 되돌리는 것을 잊은 것이다. –_-

 

중간에는 잘 나오니, 테스트도 쉽게 넘어가 버리고… 마지막에 혼자서 대박 버그를 만들어

버린것이었다.

 

이 때 배운 교훈,

1. 같은 흐름의 동작이라면, 나뉘기 보다는 하나로 구성해라.

뭐, 컴파일 시점에 속도를 조금이라도 더 올리기 위한것이 아니라면, 같은 조건이 들어가야

하는 함수들은 하나로 묶어두는 것이 좋다.(즉, 한군데만 고치고 다른곳에 적용이 안되는

케이스다 T.T, 로직이 다른 형태로라도 여러군데 있으면 안되고, 한군데서 처리해야 한다.)

2. 실수 할 수 있는 부분을 미리 정의하자.

이번 실수는 충분히 예상할 수 있던 부분이었다. 일이 바뻐서, 중간에 까먹기 때문에 생기는

문제라. 미리 완료해야 할 목록으로 만들어 두었다면 충분히 사전에 막을 수 있었다.

TDD를 한다든지나 Code Review를 한다든지, 이번 실수는 Code Review 와 Test 를

했음에도 불구하고, 대충하면 –_- 큰일난다라는 것의 교본이다. T.T

온라인 코드리뷰는 조금 힘든 듯, 작성자로 부터 설명을 듣고, 그 이후에 맞는 지 확인하는

것이 더 좋다라는 T.T