JupyterHub에서 Spark 사용하기
[배경]
이전 포스팅(쿠버네티스(Kubernetes)에서 주피터허브(JupyterHub) 구성하기)에서 설명했듯이, 단순히 JupyterHub를 구성하면 끝나는 것이 아니라, JupyterHub에서 Spark를 사용해야 합니다.
기본적으로 쿠버네티스 환경의 JupyterHub Helm은 호스트 네트워크(Host Network, Pod이 올라간 노드의 네트워크)를 사용하지 않는 것을 전제로 하는데요.
그래서 새로운 계정이 접속해서 노트북 Pod(singleuser)을 만들 때마다 Port를 무조건 8888로 할당하게 설정되어 있습니다.
호스트 네트워크를 사용하지 않기 때문에 한 노드에 여러 개의 Pod이 생성되어 모두 8888 포트를 사용하더라도, 실제 노드의 8888 포트는 사용하지 않기 때문에 문제가 발생하지 않는거죠.
하지만 Spark는 Driver Node와 Worker Node의 통신을 위해 호스트 네트워크가 구성되어야 한다고 합니다. (사실 이게 맞는지 잘 모르겠어요. 혹시 더 좋은 방법이 있다면 공유 부탁드립니다.. ㅎㅎ)
여기서 문제인게 Spark를 위해 호스트 네트워크를 사용하도록 설정하면, 노트북 Pod은 실제 Kubernetes Node의 8888 포트를 사용하게 됩니다. 그래서 Kubernetes Node의 수보다 많은 노트북 Pod을 만들려고 하면 8888 포트를 모두 사용하고 있기 때문에 더 만들 수 없어집니다.
예를 들어 저희 팀은 분석용 노드의 수가 4개인데요. 팀원 4명이 노트북에 접속하고 나면, 이후로는 free port가 없다는 에러와 함께 접속을 할 수 없게 되는 문제가 발생합니다.
[트러블슈팅 과정]
사실 위 내용을 처음에는 제대로 이해하지 못 했습니다. 그래서 왜 이런 에러가 발생하는지도 몰랐고, 트러블슈팅에 시간을 많이 쏟았습니다.
찾다보니 노트북 Pod을 생성할 때 사용되는 KubeSpawner까지 타고 들어가, 관련 논의들을 찾기도 했는데요.
결론은 지원할 방법이 마땅치 않고 이상적인 해결 방법은 없는 것 같았습니다. Pod의 IP를 spark.driver.host로 지정하는 것도 제 경우엔 동작하지 않았습니다.
- https://github.com/jupyterhub/zero-to-jupyterhub-k8s/issues/1220
- https://github.com/jupyterhub/kubespawner/issues/299
[해결 방법]
여러 시도 중에 성공한 방법은 너무 나이브한 것 같긴하지만 KubeSpawner를 상속받는 클래스를 생성해서 Port를 랜덤으로 지정하는 방법이었습니다.
조금 더 자세히 설명하자면, 노트북 Pod을 생성할 때는 아래 과정을 거치게 됩니다.
Hub 접속
-> 계정 로그인
-> Spawner가 노트북 생성
-> 생성된 노트북 사용
여기서 Kubernetes에 사용되는 KubeSpawner class에는 init이 아래와 같이 정의되어 있습니다. (https://github.com/jupyterhub/kubespawner/blob/main/kubespawner/spawner.py)
class KubeSpawner(Spawner): """ A JupyterHub spawner that spawn pods in a Kubernetes Cluster. Each server spawned by a user will have its own KubeSpawner instance. """ def __init__(self, *args, **kwargs): _mock = kwargs.pop('_mock', False) super().__init__(*args, **kwargs) if _mock: # runs during test execution only if 'user' not in kwargs: user = MockObject() user.name = 'mock_name' user.id = 'mock_id' user.url = 'mock_url' self.user = user if 'hub' not in kwargs: hub = MockObject() hub.public_host = 'mock_public_host' hub.url = 'mock_url' hub.base_url = 'mock_base_url' hub.api_url = 'mock_api_url' self.hub = hub # We have to set the namespace (if user namespaces are enabled) # before we start the reflectors, so this must run before # watcher start in normal execution. We still want to get the # namespace right for test, though, so we need self.user to have # been set in order to do that. # By now, all the traitlets have been set, so we can use them to # compute other attributes if self.enable_user_namespaces: self.namespace = self._expand_user_properties(self.user_namespace_template) self.log.info(f"Using user namespace: {self.namespace}") self.pod_name = self._expand_user_properties(self.pod_name_template) self.dns_name = self.dns_name_template.format( namespace=self.namespace, name=self.pod_name ) self.secret_name = self._expand_user_properties(self.secret_name_template) self.pvc_name = self._expand_user_properties(self.pvc_name_template) if self.working_dir: self.working_dir = self._expand_user_properties(self.working_dir) if self.port == 0: # Our default port is 8888 self.port = 8888 # The attribute needs to exist, even though it is unset to start with self._start_future = None load_config(host=self.k8s_api_host, ssl_ca_cert=self.k8s_api_ssl_ca_cert) self.api = shared_client("CoreV1Api") self._start_watching_pods() if self.events_enabled: self._start_watching_events()
여기서 제일 중요한 부분은 port를 8888로 무조건 지정하게 되어있는 부분입니다.
if self.port == 0: # Our default port is 8888 self.port = 8888
그렇다면 이 부분을 새로 정의하면 해결되지 않을까 싶었습니다. 결과적으로는 잘 동작하지 않을 가능성도 있기 때문에 찝찝하지만 해결은 되었습니다.
[수정한 코드]
이전 포스팅에서 config.yaml을 정의했었는데, 그 부분에 하나 추가하면 됩니다.
hub: # Hub Control Panel 설정 networkPolicy: interNamespaceAccessLabels: accept extraConfig: hostNetwork: | c.KubeSpawner.extra_pod_config = { 'hostNetwork': True, 'dnsPolicy': 'ClusterFirstWithHostNet' } portConfig: | from kubespawner import KubeSpawner import random def random_port(): return random.choice([8888, 8889, 8890, 8891, 8892]) class RandomKubeSpawner(KubeSpawner): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.port = random_port() c.JupyterHub.spawner_class = RandomKubeSpawner httpTimeout: | c.KubeSpawner.http_timeout = 3000
hostNetwork 부분은 Spark 사용을 위해 hostNetwork를 사용하도록 설정한 것이구요.
portConfig 부분은 KubeSpawner를 상속 받은 RandomKubeSpawner를 정의해서, init 부분의 self.port를 변경한 뒤에 사용하는 Spawner를 RandomKubeSpawner로 변경한 내용입니다.
코드를 보시면 아시겠지만, 8888~8892 포트를 랜덤으로 지정하는 단순한 코드입니다.
언젠가는 문제가 발생할 소지가 있지만... 어떻게든 해결은 되어 Node 수보다 많은 Pod을 띄울 수 있게 되었습니다.. ㅎㅎ
'스터디 > DevOps' 카테고리의 다른 글
[Kubernetes] #4 ReplicaSet (0) | 2022.10.29 |
---|---|
[Kubernetes] #3 Pod (2) | 2022.10.08 |
[Kubernetes] #2 Kubernetes Architecture (0) | 2022.09.29 |
[Kubernetes] #1 Kubernetes(쿠버네티스) 개요 및 가상화 기술 (0) | 2022.09.29 |
[Kubernetes] 쿠버네티스(Kubernetes)에서 주피터허브(JupyterHub) 구성하기 (2) | 2022.04.17 |