[발 번역] Handling the errors a ZooKeeper throws at you

해당 블로그는 KT UCloud의 지원을 받고 있습니다.

해당 글은 http://wiki.apache.org/hadoop/ZooKeeper/ErrorHandling 의 글을 발 번역한 것입니다. Zookeeper 를 처음 사용하는 사람들은 대부분, Zookeeper 장애로 인해서 시스템 장애를 경험하실 껍니다. 보통 일반적으로 우리가 보는 예제들이, 일반적으로는 잘 동작하지만, Zookeeper 관련해서장애가 있을 때, 이에 대한 에러 처리가 제대로 되지 않아서 나중에 Zookeeper 관련 코드가 예상대로 동작하지 않게 됩니다. 저 같은 경우도, Zookeeper 와 접속이 끊어졌는데, 이것을 다시 복구하지 않아서, 작업 처리가 안되었다든지 등의 여러가지 이슈들이 있었습니다.  그럼 어떤 경우에 Recoverable 한거고 어떤 경우가 아닌지 확인해 보도록 하겠습니다. 오역에 주의하세요.

Handling the errors a ZooKeeper throws at you

Zookeeper 를 다룰 때 잘못 될 수 있는 경우는 엄청나게 많습니다. 어플리케이션 코드와 라이브러리는 이것들을 꼭 처리해야합니다. 이 문서에서는 에러를 처리하는 베스트 프랙티스에 대해서 제안하고 있습니다.  라이브러리는 전체적인 그림을 그리기에는 여러가지 제약이 있으므로, 라이브러리 코드와 어플리케이션을 따로 보도록 하겠습니다.  예를 들어, 어플리케이션에서 한 라이브러리에서는 Lock 오브젝트를 다른 라이브러리에서는 KeptSet 오브젝트를 사용한다고 한다면, 어플리케이션은 Lock을 획득하고 있을 때에만, KeptSet 에 대해서 변경을 할 수 있다는 것을 알고 있습니다. ZooKeeper를 사용하는 데 치명적인 에러가 발생하면, 예를 들어서 세션이 만료된 경우, 오직 어플리케이션만 에러에서 복구되는 데 필요한 모든 절차를 알고 있습니다. Lock과 KeptSet 라이브러리는 복구를 시도해서도 안됩니다.

뭔가가 잘못될 때, 거기서 예외나 에러 코드를 전달합니다. 이 에러들은 다음과 같은 카테고리로 나눌 수 있습니다.

  • 정상 상태 예외: 이미 존재하는 znode를 다시 생성하려고 하거나, 존재하는 znode에 setData를 호출하거나, 기대되지 않는 버전의 znode에 조건부 쓰기 등등의 작업을 할 경우 발생
  • 복구 가능한 에러: 접속 종료 이벤트, 접속 타임 아웃, 연결을 잃어버리거나 등이 복구 가능한 에러입니다. 이 것들은 어떤 문제가 발생했다는 것을 알려줍니다. 그러나 Zookeeper 라이브러리가 ZooKeeper와 다시 연결을 하게되면, ZooKeeper 핸들은 여전히 정상적이고, 차후 발생하는 명령들도 성공적으로 수행될 것입니다.
  • 치명적인 에러: Zookeeper 핸들이 비정상입니다.  명시적으로 핸들이 닫혔거나, 인증 에러거나, 세션이 만료되었습니다.

어릎리케이션과 라이브러니는 정상 상태 예외가 발생했을 때 처리하게 됩니다. 일반적으로 그것들은 정상적인 예상되는 동작입니다. 예를 들어, 조건부 set 을 수행할 때, 프로그래머는 동시적으로 일어나는 작업에 의해서 같은 set 을 할 수 있고, 이걸 어떻게 다뤄야 할 지 알고 있습니다. 다음 두 종류의 에러는 좀 더 복잡합니다.

Recoverable errors

복구 가능한 에러들은 ZooKeeper가 직접 해당 에러들을 복구할 수 없기 때문에, 어플리케이션으로 해당 에러를 전달합니다.  ZooKeeper 라이브러리는 복구 가능한 에러에서 핸들이 닫히지 않았기 때문에 연결을 복구할려고 시도합니다. 그러나 어플리케이션에서는 반드시 명확한 에러로 처리해야 합니다. “그러나 잠시 기다려요” , “내가 getData()를 수행하는 중일 때 장애가 나면, 다시 실행시켜 주지 못한다구요?” 물론, ZooKeeper는 단지 getData만 호출하는 중이었다면 할 수 있습니다. 만약 create()나 delete() 조건 부 setData()를 하고 있었다면 어떻게 될까요? Zookeeper 클라이언트가 Zookeeper 서버와의 연결을 잃어버린다면 아마 몇개의 요청이 동작 중이었을 꺼고, 그 때 연결을 잃어 버리게 되면 어디서 해당 문제가 발생했는지 알 수 없습니다.

예를 들어, 연결을 일기 전에 create를 보냈다면, create는 네트웍 스택 밖으로 전달되지 못했거나 서버가 죽기 전에 ZooKeeper 서버에서 다른 서버들로 전달되었을 겁니다. 그래서 다시 연결이 되었을 때, create 가 실행되었는지, 아닌지 알 수 있는 좋은 방법이 없습니다.( 서버에서는 실제로 이 정보가 필요하지만, 해당 정보로 이익을 얻기 위해서는 엄청나게 많은 구현 작업을 필요로 합니다. 아이러니 하게도, 변경 요청을 다시 실행하게 되면, 읽기 요청이 문제가 생깁니다. ) 그래서  만약 create() 요청을 재실행하면, NodeExistsException 가 발생할 것입니다. ZooKeeper 클라이언트는 해당 예외가 이전 작업에 의해서 생성된 것인지, 다른 사람이 생성한 것인지 알지 못합니다.

복구 가능한 에러를 다루기 위해서, 개발자는 두 종류의 요청이 있다는 것을 알아야 합니다. idempotent 와 non-idempotent 요청입니다. 읽기 요청이이나, 조건 없는 set, delete의 경우는 idempotent 요청의 예입니다. 이 작업들은 다시 재 수행되더라도 같은 결과를 줍니다.( 비록 delete의 경우는 NoNodeException 을 던지겠지만, 다시 실행이 되더라도 실제 ZooKeeper 의 상태는 그대로 동일합니다.) Non-idempotent 요청은 특별한 처리가 필요합니다. 어플리케이션과 라이브러리 작성자는 재시도 된 것을 감지하기 위해서 znode 의 이름이나 데이터의 정보를 인코딩할 필요가 있을 수도 있다는 것을 기억해야 합니다. 간단한 예는 sequence flag를 만들어서 사용하는 것입니다. 만약 프로세스가 create(“/x-“, …, SEQUENCE)  을 수행하고, 연결을 잃어버리게 되어 예외가 발생하면, 해당 프로세스는 다시 create(“/x-“, …, SEQUENCE) 를 재수행 할 것입니다. 그리고 x-111을 얻게 될 것입니다. 프로세스가 getChildren(“/”) 를 수행하면, 극서은 x-1, x-30, x-109, x-110, x-111을 보게 됩니다. 그리고 x-109는 이전 create의 결과일 수 도 있습니다. 해당 프로세스는 확실히 x-109와 x-111 두개를 소유하고 있을 것입니다. 쉬운 해결책은 “x-process id-” 를 create를 수행할 때 사용하는 것입니다. process id 가 352를 사용한다면, create를 재 실행했다면, getChildren(“/”) 를 실행하면 “x-222-1″, x-542-30”, “x-352-109”, “x-333-110” 을 보게 될 것입니다. 해당 프로세스는 원본 create가 성공했다는 것과 해당 znode가 “x-352-109” 라는 것을 알게될 것입니다.

요컨대 ZooKeeper는 게으르지 않습니다. 단순히 재시도가 귀찮다라는 이유로 문제가 있음에도 그냥 손을 놓고 있는 것이 아닙니다. 복구를 필요로 하는 어플리케이션의 특별한 로직이기 때문입니다. ZooKeeper는  클라이언트에 제공되는 보장을 깨트리므로, non-idempotent 한 요청을 재시도 하지 않습니다. 그래서 어플리케이션 프로그램에서는 이러한 종류의 에러가 발생했을 때, 간단하게 요청을 재요청 하는 레이어를 ZooKeeper 상단에 구축하는 것에 대해서 회의적이어야 합니다.

Unrecoverable errors

ZooKeeper 핸들은 명시적으로 닫히거나 세션 만료 등의 치명적인 에러로 인해서 유효하지 않게 됩니다. 어떤 상황이든 ZooKeeper 핸들과 연관된 어떤 임시노드라도 사라지게 될 것입니다. 어플리케이션은 유효하지 핸들을 다룰 수 있지만, 라이브러리에서는 가능하지 않습니다. 어플리케이션은 적절하게 해당 작업들을 셋업할 수 있는 단계를 알고 있고, 이 스텝을 재실행 할 수 있습니다. 그러나 라이브러리는 전체를 볼 수 없기 때문에 이런 단계의 일부분만 알고 잇습니다. 이런 이유로, 복구 불가능한 에러가 발생했을 때, 라이브러리에서 복구 시도를 하지 않는 것은 중요합니다. ZooKeeper 와같은 라이브러리에서는 전체 구조를 알 수 없고, 자동적으로 복구하기 위한 어플리케이션에 대한 충분한 지식이 없습니다.

라이브러리가 복구불가능한 에러가 발생했을 때, 가능한 안정적으로 종료해야만 합니다. 명확하게 그것은 ZooKeeper 와 더 이상 통신을 할 수 없을 것입니다. 그러나, 내부적인 데이터 구조를 정리하고, 현재 상태를 유효하지 않다고 기록하는 것은 가능합니다.

어플리케이션에서 복구 불가능한 에러를 받았을 때는 이전에 사용하던 ZooKeeper 핸들을 적절히 종료하고, 다시 핸들을 가져와서 수행하는 절차에 대해서 잘 알아야 합니다. 이 작업을 원할히 수행하기 위해서 라이브러니는 반드시 내부에서 특별한 작업을 처리하면 안됩니다. 최초의 예제로 돌아가서 KeptSet 은 Lock에 의해서 보호되고 있습니다. KeptSet 구현자가 이렇게 생각할 수 있습니다. “임시 노드를 사용하지 않으니깐, 세션이 만료되었을 때, 그냥 ZooKeeper 오브젝트를 새로 만들어서 복구를 하면 되겠군.” 이것은 라이브러리에서 보면 사실입니다. 그러나, 어플리케이션에서는 고쳐져야 합니다. 요새의 대부분의 어플리케이션은 멀티 스레드로 구현되어 있습니다. 어플리케이션이 Lock을 가지고 스레드가 KeptSet에 접근하면, 세션 만료 때문에 Lock이 사라진다면, KeptSet은 뭔가 잘못된 결과를 만들어 낼 것입니다. Lock 을 잃어버리면 스레드가 종료되야 한다고 말할 수도 있지만, 그러나 세션 만료를 받아서 스레드를 종료하는 것과, KeptSet이 자동으로 복구하는 사이에 Race Condition이 발생하게 됩니다.

결론적으로, 복구 불가능한 오류를 복구하려고 하는 것은 극도로 주의하거나, 전혀 사용하지 않아야 한다는 것입니다.