소스 코드 까지 까보며 찾은 zeppelin 오류 해결법..

유알·2024년 7월 2일

3일 동안 zeppelin 가지고 씨름하면서 얻은 꿀팁 정리, 소스코드까지 까서 디버깅했다.

환경

환경이 다르면 아래 팁이 먹히지 않을 수 있다.
일단 hadoop yarn 클러스터(완전분산모드)로 구축을 하였고
spark을 spark on yarn으로 구축 하였다.

zeppelin에서는 이 spark으로 실행할 예정

팁 들어간다.

1. JAVA_HOME 이 모두 동일한지 확인하자

zeppelin-env.sh 파일에 정의된 JAVA_HOME이 모든 yarn 클러스터에 적용된다. 만약 JAVA_HOME이 컨테이너마다 다르다면, 커맨드를 실행하지 못한다면서 FAIL 이 날 것이다.
더 정확히 말하면, yarn에서 launch_container.sh 라는 쉘 파일이 실행되는데, 그 맨 마지막줄

exec /bin/bash -c "$JAVA_HOME/bin/java ... --class 'org.apache.zeppelin.interpreter.remote.RemoteInterpreterServer' --jar file:/opt/zeppelin-0.11.1/interpreter/spark/spark-interpreter-0.11.1.jar --arg '192.168.0.2' --arg '35379' --arg 'spark-shared_process' --arg ':' ...

이러한 라인에서 문제가 발생할 텐데 java를 못 찾아서 그건거다.

2. 인터프리터 설정을 확인하자

spark에 대한 기본적인 지식이 있다면, spark 실행에는 여러개의 모드가 있다는 것을 알 것이다.
만약 yarn으로 실행시킨다면, 인터프리터 설정에 들어가서 yarn, cluster 로 설정한 것을 체크하자

3. 제플린 설정 중 놓치기 쉬운 것들

만약 로컬에서 실행하는 것이 아니라 분산 모드로 실행한다면, 반 필수로 설정해줘야 하는 설정들이 있다.
우선 공식 문서에 따르면, zeppelin-env.sh 가 zeppelin-site.xml보다 우선순위가 높다.

JAVA_HOME

당연한 거지만,,, 중요한 것은 모든 컨테이너에서 동일한 경로를 가져야한다는거(나도 왜이렇게 설계했는지 모르겠다.)

ZEPPELIN_ADDR

외부에서 listen할 네트워크 인터페이스를 특정한다.
이게 왜 중요하냐면, Localhost로 두면 loop-back 인터페이스만 허용한다.
이렇게 되면 로컬에서 테스트 할 때는 상관이 없지만, 외부에서 접근하려고하면 접근이 안되는 불상사가 발생한다.

전체를 허용하려면 0.0.0.0 을 입력하면 된다.

export ZEPPELIN_ADDR=0.0.0.0                          # Bind address (default 127.0.0.1)

ZEPPELIN_LOCAL_IP

이게 상당히 중요하며, 이것 때문에 2일 정도를 할애 했다.

어 ? 이거 안해도 잘 되던데? 라는 사람이 있을 수 있다. 이따 코드에서 나온다.

export ZEPPELIN_PORT=8080                          # port number to listen (default 8080)
export ZEPPELIN_LOCAL_IP=192.168.0.2                      # Zeppelin's thrift server ip address, if not specified, one random IP address will be choosen.

이 값을 설정값으로 설정하면 무슨일이 벌어지냐면,

exec /bin/bash -c "$JAVA_HOME/bin/java ... --class 'org.apache.zeppelin.interpreter.remote.RemoteInterpreterServer' --jar file:/opt/zeppelin-0.11.1/interpreter/spark/spark-interpreter-0.11.1.jar --arg '192.168.0.2' --arg '35379' --arg 'spark-shared_process' --arg ':' ...

위에서 보는 저 arg값에 들어가게 된다. 이게 어디에 쓰이냐?

org.apache.zeppelin.interpreter.remote.RemoteInterpreterServer 의 main 함수이다.

바로 이렇게 필드값으로 설정하게 된다.

여기서는 중요하지는 않지만 저기 new 를 통해 만드는 RemoteInterpreterServer에서 중요하다.

저것은 쉽게 말하면 apache Thrift라는 서버를 감싸서 만든 객체인데, 여기서 서버를 실행하게 된다.

그니까 어떠한 구조냐면,

  public RemoteInterpreterServer(String intpEventServerHost,
                                 int intpEventServerPort,
                                 String portRange,
                                 String interpreterGroupId,
                                 boolean isTest) throws Exception {
    super("RemoteInterpreterServer-Thread");
    if (null != intpEventServerHost) {
      this.intpEventServerHost = intpEventServerHost;
      this.intpEventServerPort = intpEventServerPort;
      this.port = RemoteInterpreterUtils.findAvailablePort(portRange);
      this.host = RemoteInterpreterUtils.findAvailableHostAddress();
    } else {
      // DevInterpreter
      this.port = intpEventServerPort;
    }
    this.isTest = isTest;
    this.interpreterGroupId = interpreterGroupId;
  }
  • intpEventServerHost : 우리가 띄운 제플린 서버
  • this.host : 이 프로그램이 실행되는 서버
  • this.port : 이 프로그램이 실행되는 포트

이 프로그램이 yarn 컨테이너에서 실행이 되면, 임의의 포트로 서버를 띄우고, 제플린 서버에 등록을 한다.(나 여기있어요)

중요한 점은 무엇이냐면, 우리가 제플린 서버 측에 설정한 ZEPPELIN_LOCAL_IP 는 intpEventServerHost 변수로 배정된다는 것이다.

그러면 host와 port는 어떻게 결정되는지 보자.

포트의 경우 가능한 포트 범위 (기본값은 ':'로 모두 허용)에서 랜덤하게 배정한다.
중요한 점은 호스트인데

  public static String findAvailableHostAddress() throws UnknownHostException, SocketException {
    String zeppelinServerIP = System.getenv("ZEPPELIN_LOCAL_IP");
    if (zeppelinServerIP != null) {
      return zeppelinServerIP;
    }

    InetAddress address = InetAddress.getLocalHost();
    if (address.isLoopbackAddress()) {
      for (NetworkInterface networkInterface : Collections
          .list(NetworkInterface.getNetworkInterfaces())) {
        if (!networkInterface.isLoopback()) {
          for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
            InetAddress a = interfaceAddress.getAddress();
            if (a instanceof Inet4Address) {
              return a.getHostAddress();
            }
          }
        }
      }
    }
    return address.getHostAddress();
  }

위 코드를 보면 이해가 빠를 것이다.

  1. ZEPPELIN_LOCAL_IP 값이 있으면 그걸 사용한다.

중요한 것은 제플린 서버측에 설정한 그 값이 아니라, 컨테이너 측의 환경변수를 의미한다는 것이다.
더 중요한 것은 yarn 컨테이너는 노드(서버)의 환경 변수를 공유하지 않는다는 것이다.

그러면 어떻게 설정하냐는 것이다.
우선 설정을 하지 않았을 때(기본값)를 기준으로 보자.

loop-back (로컬호스트)를 제외한 인터페이스 중 아무거나 선택해서 배정한다.

나의 경우 왜 에러가 떳는지 한번 보자

$ ip add
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether dc:a6:32:f4:98:c0 brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.8/24 metric 100 brd 192.168.0.255 scope global dynamic eth0
       valid_lft 4622sec preferred_lft 4622sec
    inet6 fe80::dea6:32ff:fef4:98c0/64 scope link
       valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:d5:f6:ca:b2 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever

일단 1번은 루프백이니까 건너 뛰고
4번이 자꾸 배정되는 상황이었다.

4번은 도커가 차지하고 있는데, 자기 자신을 172.17.0.1 로 소개하며 제플린 서버에 등록해 보았자, 제플린 서버에서는 이 서버로 요청을 보낼 수가 없다.

(이걸 찾는데 꼬박 48시간이 걸렸다)

따라서 인터페이스 사정이 바뀔 때마다 어느 인터페이스로 등록을 할지 모르는 러시안룰렛과 같은 설정인 것이다.

그래서!! 어떻게 설정을 하냐하면 각 컨테이너마다 서버 호스트를 인식해서 등록을 해야하는데 나의 경우는 yarn-site를 고쳐서 이를 해결하였다.

    <property>
        <name>yarn.nodemanager.env-whitelist</name>
        <value>ZEPPELIN_LOCAL_IP</value>
    </property>

이러한 식으로 설정하게 되면, 컨테이너가 실행하는 환경에서 불러올 환경변수를 설정할 수 있다.

그리고 나서 각 서버 별로 ZEPPELIN_LOCAL_IP를 등록해 놓았다.

결과적으로 어떤 컨테이너에서 실행이 되던 각 서버 호스트로 등록을 하게 되고, 제플린 서버에서 이를 인식할 수 있게되었다.

노트북 모드 파이썬

Pysaprk나 python을 대화형을 설정하고 싶으면, 아래 링크를 참고해서 설정하자

https://zeppelin.apache.org/docs/latest/interpreter/python.html#ipython-interpreter-pythonipython-recommended

부족하지만, 추가적인 팁

소스코드를 까보고, yarn 로그를 수십번 뒤져가며 알게된 사실은 yarn을 통해 작업을 실행할 때, hdfs의 tmp 를 통해 전체 필요한파일(jar 같은거)를 전송한다는 점이었다.

또 컨테이너 로컬에 tmp 하위 디렉토리에 캐싱도 해놓는다는 것이다.

즉 같은 작업을 여러번 실행시킨다면, 이 캐시를 활용하고, ln -s 로 심볼릭 링크를 만들어 사용한다.

이는 다양한 시사점을 주는데, 다양한 작업을 다양한 컨테이너에서 번갈아 실행하는 것이 다소 비효율적이라는 점과, 만약 tmpfs 가 인메모리로 구현되어 있을 경우 컨테이너 노드에 성능 저하를 일으킬 수 있다는 점이다.

profile
더 좋은 구조를 고민하는 개발자 입니다

0개의 댓글