diff --git a/10/index.html b/10/index.html index 9c6df3e..1b4c7cd 100644 --- a/10/index.html +++ b/10/index.html @@ -68,6 +68,7 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
Skip to content
10 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/11/index.html b/11/index.html index 13c77f7..c35a046 100644 --- a/11/index.html +++ b/11/index.html @@ -1,4 +1,4 @@ -Swalloow Blog
Skip to content
11 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file +} catch (e) {} })();
Skip to content
11 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/12/index.html b/12/index.html index 13fb953..50eea5b 100644 --- a/12/index.html +++ b/12/index.html @@ -1,4 +1,4 @@ -Swalloow Blog
Skip to content
12 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/13/index.html b/13/index.html index 62b25bf..f958b6b 100644 --- a/13/index.html +++ b/13/index.html @@ -68,9 +68,8 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
Skip to content
13 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/14/index.html b/14/index.html index 5a19648..999ee31 100644 --- a/14/index.html +++ b/14/index.html @@ -1,4 +1,4 @@ -Swalloow Blog
Skip to content
14 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/15/index.html b/15/index.html index 024ba63..4a26c0b 100644 --- a/15/index.html +++ b/15/index.html @@ -1,4 +1,4 @@ -Swalloow Blog
Skip to content
15 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file +} catch (e) {} })();
Skip to content
15 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/16/index.html b/16/index.html index b808605..9de0880 100644 --- a/16/index.html +++ b/16/index.html @@ -1,4 +1,4 @@ -Swalloow Blog
Skip to content
\ No newline at end of file +} catch (e) {} })();
Skip to content
2 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/3/index.html b/3/index.html index 9e77651..584374c 100644 --- a/3/index.html +++ b/3/index.html @@ -68,5 +68,6 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
Skip to content
3 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file +} catch (e) {} })();
Skip to content
3 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/4/index.html b/4/index.html index b328b02..4773786 100644 --- a/4/index.html +++ b/4/index.html @@ -1,4 +1,4 @@ -Swalloow Blog
Skip to content
4 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file +} catch (e) {} })();
Skip to content
4 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/5/index.html b/5/index.html index dcded01..0b282c3 100644 --- a/5/index.html +++ b/5/index.html @@ -68,8 +68,9 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
Skip to content
5 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/6/index.html b/6/index.html index 42b4b3c..af3fdd7 100644 --- a/6/index.html +++ b/6/index.html @@ -68,6 +68,5 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
Skip to content
6 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file +} catch (e) {} })();
Skip to content
6 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/7/index.html b/7/index.html index a8702b0..d5d6ead 100644 --- a/7/index.html +++ b/7/index.html @@ -68,7 +68,8 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
Skip to content
7 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/8/index.html b/8/index.html index 5b74896..befecbe 100644 --- a/8/index.html +++ b/8/index.html @@ -68,5 +68,5 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
Skip to content
8 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file +} catch (e) {} })();
Skip to content
8 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/9/index.html b/9/index.html index 7351953..d24cea4 100644 --- a/9/index.html +++ b/9/index.html @@ -68,8 +68,7 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
Skip to content
9 / 16
PrevNext
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/feed.xml b/feed.xml index 813f284..962f0f7 100644 --- a/feed.xml +++ b/feed.xml @@ -1,4 +1,5 @@ -<![CDATA[Swalloow Blog]]>https://swalloow.github.ioGatsbyJSSat, 20 Jan 2024 13:51:54 GMT<![CDATA[Pandas 2.0의 Copy-on-Write에 대하여]]>https://swalloow.github.io/pandas-2-0-copy-on-writehttps://swalloow.github.io/pandas-2-0-copy-on-writeSun, 24 Dec 2023 00:00:00 GMT<![CDATA[Spark on Kubernetes: 커스텀 스케줄러 (2)]]>https://swalloow.github.io/spark-on-kubernetes-scheduler-2https://swalloow.github.io/spark-on-kubernetes-scheduler-2Sun, 10 Dec 2023 00:00:00 GMT<![CDATA[Spark on Kubernetes: 커스텀 스케줄러 (1)]]>https://swalloow.github.io/spark-on-kubernetes-schedulerhttps://swalloow.github.io/spark-on-kubernetes-schedulerThu, 08 Jun 2023 00:00:00 GMT<![CDATA[베를린에서 2개월 살아남기]]>https://swalloow.github.io/berlinhttps://swalloow.github.io/berlinWed, 10 May 2023 00:00:00 GMT<![CDATA[MLOps 관련 책, 강의 리뷰 (DMLS, FSDL)]]>https://swalloow.github.io/mlops-dmls-fsdlhttps://swalloow.github.io/mlops-dmls-fsdlTue, 13 Sep 2022 00:00:00 GMT<![CDATA[Spark on Kubernetes: 스팟 인스턴스 사용을 위한 기능들]]>https://swalloow.github.io/spark-on-kubernetes-spot-instancehttps://swalloow.github.io/spark-on-kubernetes-spot-instanceSat, 23 Jul 2022 00:00:00 GMT<![CDATA[쿠버네티스에서 GPU 리소스를 효율적으로 활용하는 방법]]>https://swalloow.github.io/gpu-utilizationhttps://swalloow.github.io/gpu-utilizationFri, 08 Jul 2022 00:00:00 GMT<![CDATA[Airflow worker에 KEDA AutoScaler 적용한 후기]]>https://swalloow.github.io/airflow-worker-keda-autoscalerhttps://swalloow.github.io/airflow-worker-keda-autoscalerFri, 24 Jun 2022 00:00:00 GMT<![CDATA[컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)]]>https://swalloow.github.io/container-tini-dumb-inithttps://swalloow.github.io/container-tini-dumb-initFri, 27 May 2022 00:00:00 GMT<![CDATA[EKS Karpenter를 활용한 Groupless AutoScaling]]>https://swalloow.github.io/eks-karpenter-groupless-autoscalinghttps://swalloow.github.io/eks-karpenter-groupless-autoscalingFri, 13 May 2022 00:00:00 GMT<![CDATA[개발자가 의사결정을 기록하는 방법 (feat. ADR)]]>https://swalloow.github.io/feat-adrhttps://swalloow.github.io/feat-adrSat, 04 Dec 2021 00:00:00 GMT<![CDATA[JupyterHub에 Tensorboard 연동하기]]>https://swalloow.github.io/jupyterhub-tensorboardhttps://swalloow.github.io/jupyterhub-tensorboardSat, 23 Oct 2021 00:00:00 GMT<![CDATA[JupyterHub on Kubernetes]]><![CDATA[Swalloow Blog]]>https://swalloow.github.ioGatsbyJSSun, 21 Jan 2024 08:22:35 GMT<![CDATA[AI를 통해 진화하는 데이터플랫폼 근황]]>https://swalloow.github.io/llm-dataplatformhttps://swalloow.github.io/llm-dataplatformSun, 21 Jan 2024 00:00:00 GMT<![CDATA[Pandas 2.0의 Copy-on-Write에 대하여]]>https://swalloow.github.io/pandas-2-0-copy-on-writehttps://swalloow.github.io/pandas-2-0-copy-on-writeSun, 24 Dec 2023 00:00:00 GMT<![CDATA[Spark on Kubernetes: 커스텀 스케줄러 (2)]]>https://swalloow.github.io/spark-on-kubernetes-scheduler-2https://swalloow.github.io/spark-on-kubernetes-scheduler-2Sun, 10 Dec 2023 00:00:00 GMT<![CDATA[Spark on Kubernetes: 커스텀 스케줄러 (1)]]>https://swalloow.github.io/spark-on-kubernetes-schedulerhttps://swalloow.github.io/spark-on-kubernetes-schedulerThu, 08 Jun 2023 00:00:00 GMT<![CDATA[베를린에서 2개월 살아남기]]>https://swalloow.github.io/berlinhttps://swalloow.github.io/berlinWed, 10 May 2023 00:00:00 GMT<![CDATA[MLOps 관련 책, 강의 리뷰 (DMLS, FSDL)]]>https://swalloow.github.io/mlops-dmls-fsdlhttps://swalloow.github.io/mlops-dmls-fsdlTue, 13 Sep 2022 00:00:00 GMT<![CDATA[Spark on Kubernetes: 스팟 인스턴스 사용을 위한 기능들]]>https://swalloow.github.io/spark-on-kubernetes-spot-instancehttps://swalloow.github.io/spark-on-kubernetes-spot-instanceSat, 23 Jul 2022 00:00:00 GMT<![CDATA[쿠버네티스에서 GPU 리소스를 효율적으로 활용하는 방법]]>https://swalloow.github.io/gpu-utilizationhttps://swalloow.github.io/gpu-utilizationFri, 08 Jul 2022 00:00:00 GMT<![CDATA[Airflow worker에 KEDA AutoScaler 적용한 후기]]>https://swalloow.github.io/airflow-worker-keda-autoscalerhttps://swalloow.github.io/airflow-worker-keda-autoscalerFri, 24 Jun 2022 00:00:00 GMT<![CDATA[컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)]]>https://swalloow.github.io/container-tini-dumb-inithttps://swalloow.github.io/container-tini-dumb-initFri, 27 May 2022 00:00:00 GMT<![CDATA[EKS Karpenter를 활용한 Groupless AutoScaling]]>https://swalloow.github.io/eks-karpenter-groupless-autoscalinghttps://swalloow.github.io/eks-karpenter-groupless-autoscalingFri, 13 May 2022 00:00:00 GMT<![CDATA[개발자가 의사결정을 기록하는 방법 (feat. ADR)]]>https://swalloow.github.io/feat-adrhttps://swalloow.github.io/feat-adrSat, 04 Dec 2021 00:00:00 GMT<![CDATA[JupyterHub에 Tensorboard 연동하기]]>https://swalloow.github.io/jupyterhub-tensorboardhttps://swalloow.github.io/jupyterhub-tensorboardSat, 23 Oct 2021 00:00:00 GMT<![CDATA[JupyterHub on Kubernetes]]>https://swalloow.github.io/jupyterhub-on-kuberneteshttps://swalloow.github.io/jupyterhub-on-kubernetesSat, 23 Oct 2021 00:00:00 GMT<![CDATA[Data Mesh 아키텍쳐의 네 가지 원칙]]>https://swalloow.github.io/data-mesh-principlehttps://swalloow.github.io/data-mesh-principleSat, 25 Sep 2021 00:00:00 GMT<![CDATA[Spark on Kubernetes: 성능 최적화 방법들]]>https://swalloow.github.io/spark-on-kubernetes-perfhttps://swalloow.github.io/spark-on-kubernetes-perfSat, 11 Sep 2021 00:00:00 GMT<![CDATA[여러 조직이 함께 사용하는 Airflow 만들기]]>https://swalloow.github.io/airflow-multi-tenent-1https://swalloow.github.io/airflow-multi-tenent-1Sun, 15 Aug 2021 00:00:00 GMT<![CDATA[사이드카 컨테이너로 Airflow 기능 확장하기]]>https://swalloow.github.io/airflow-sidecarhttps://swalloow.github.io/airflow-sidecarSun, 01 Aug 2021 00:00:00 GMT<![CDATA[Airflow on Kubernetes (3)]]>https://swalloow.github.io/airflow-on-kubernetes-3https://swalloow.github.io/airflow-on-kubernetes-3Fri, 05 Feb 2021 00:00:00 GMT<![CDATA[Airflow on Kubernetes (2)]]>https://swalloow.github.io/airflow-on-kubernetes-2https://swalloow.github.io/airflow-on-kubernetes-2Sun, 12 Jul 2020 00:00:00 GMT<![CDATA[K8S 클러스터 초기 설정을 위한 Helm Chart 만들기]]>https://swalloow.github.io/umbrella-helm-charthttps://swalloow.github.io/umbrella-helm-chartSat, 20 Jun 2020 00:00:00 GMT<![CDATA[Airflow on Kubernetes (1)]]>https://swalloow.github.io/airflow-on-kubernetes-1https://swalloow.github.io/airflow-on-kubernetes-1Fri, 05 Jun 2020 00:00:00 GMT<![CDATA[Gatsby와 Contentful로 블로그 이전한 후기]]>https://swalloow.github.io/gatsby-contentfulhttps://swalloow.github.io/gatsby-contentfulSat, 25 Apr 2020 00:00:00 GMT<![CDATA[EKS 클러스터에 VPC CIDR 추가하기]]>https://swalloow.github.io/eks-cidrhttps://swalloow.github.io/eks-cidrSat, 14 Mar 2020 00:00:00 GMT<![CDATA[AWS Solutions Architect Associate 취득 후기]]>https://swalloow.github.io/aws-certhttps://swalloow.github.io/aws-certSat, 30 Nov 2019 00:00:00 GMT<![CDATA[EKS의 AutoScaling 이해하기]]>
Skip to content
1 / 16
Next
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file +} catch (e) {} })();
Skip to content
1 / 16
Next
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/llm-dataplatform/index.html b/llm-dataplatform/index.html new file mode 100644 index 0000000..f5ccd50 --- /dev/null +++ b/llm-dataplatform/index.html @@ -0,0 +1,265 @@ +AI를 통해 진화하는 데이터플랫폼 근황 | Swalloow Blog
Skip to content
cover-dataengineering

AI를 통해 진화하는 데이터플랫폼 근황

  • DataEngineering

📅 January 21, 2024

⏱️4 min read

생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다.
+오늘은 데이터플랫폼 영역에서 AI를 통해 어떤 변화가 나타나고 있는지 정리해보려 합니다.

+
+

자연어를 SQL로 변환 (Text2SQL, SQL2Text)

+

지난 수 년간 클라우드 마이그레이션이 늘어남에 따라 Databrics, Snowflake와 같은 Managed DW 서비스도 함께 성장해왔습니다. Managed DW 서비스가 23년 Summit에 내세운 키워드는 생성형 AI 였습니다. 다양한 기능을 공개했지만 핵심은 Text2SQL, SQL2Text 기술이라고 볼 수 있습니다.

+

Text2SQL이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다. 쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다. +데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 "자연어" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

+
+

검색 UI 연동

+

+ + databricks1 + +

+

그 중 첫 번째는 검색 UI를 연동하는 방식입니다. +사이드에 어시스턴트를 추가함으로써 ChatGPT 서비스와 유사한 환경을 제공합니다. +검색 UI는 쿼리문을 입력하는 쿼리 에디터 뿐만 아니라 노트북, 카탈로그 등 다양한 기능에 연결되어 있습니다.

+
+

SQL 함수, 자연어 SDK 추가

+
SELECT ai_query(
+    'my-external-model-openai-chat',
+    'Describe Databricks SQL in 30 words.'
+  ) AS summary
+
+

두 번째는 SQL 함수나 자연어 SDK를 추가하는 방식입니다. +이를 통해 사용자는 개발 과정에도 자연어를 활용할 수 있습니다. +검색 UI와 달리 사용자의 검증을 거치지 않고 사용할 수 있지만, 일관된 답변을 보장 할 수 없는 관계로 운영 시스템에 직접 연동은 아직 어려울 것 같습니다.

+

이처럼 다양한 방식을 지원함으로써 사용자는 AI에 쉽게 접근하고 일관된 개발 경험을 가질 수 있습니다.

+



+

기술 문서 검색

+

개발자는 개발 과정에서 문서 검색에 많은 시간을 할애합니다. +stackoverflow를 통해 검색하는 경우, 내가 사용하고 있는 프레임워크와 버전에 정확히 일치하는 문서를 찾지 못하는 경우도 많았습니다. 이제 데이터플랫폼 내에서 기술 문서와 코드를 기반으로 AI에게 질의할 수 있게 되었습니다.

+
+

AWS Amazon Q Assistant

+

+ + amazon-q + +

+

Amazon Q는 AWS에서 출시한 생성형 AI 어시스턴트입니다. +AWS 콘솔 우측에 추가되어 AWS 클라우드와 관련된 다양한 질의를 수행할 수 있습니다.

+
+

GitHub Dosu

+

+ + ai-dosu + +

+

오픈소스 영역에서도 생성형 AI를 통해 Issue, Discussion 문의 대응하는 사례가 생기고 있습니다. 위 그림은 LLM 프레임워크인 LangChain에서 사용하는 Dosu 봇 입니다. +출시 예정인 GitHub Copilot도 이와 유사한 기능을 지원합니다. +이러한 기능을 통해 사용자는 빠르게 문제를 해결하고 메인테이너는 중요한 의사결정에 집중할 수 있습니다.

+



+

데이터 거버넌스 도구

+

데이터 거버넌스는 정책을 만드는 일보다 운영하는데 더 많은 노력이 들어갑니다. +거버넌스 내에는 다양한 영역이 있지만 그 중 데이터 디스커버리와 메타데이터 관리에 AI가 활용되고 있습니다.

+

데이터 디스커버리 영역의 경우, 기존 UI 기반 검색 엔진에 자연어 질의가 추가됩니다. +이를 통해 앞서 언급한 Text2SQL과 유사한 경험을 제공할 수 있습니다.

+

다음은 메타데이터 관리 영역입니다. 메타데이터 관리는 데이터 신뢰도를 위해 데이터 생산자와 소비자 모두에게 중요합니다. 하지만 거버넌스 정책이 새로 추가되거나 변경되면 데이터에 대한 오너십을 가지는 도메인 전문가는 이를 항상 인지하기 어렵습니다. 만약 불일치가 발생하면 거버넌스 담당자가 보정하는 작업을 수행하는 경우도 있습니다. 메타데이터 영역의 AI는 거버넌스 정책을 유지하고 메타데이터 입력을 도와주는 역할을 합니다.

+

+ + aws-datazone + +

+

위 예시는 AWS DataZone 입니다. AI를 통해 입력된 메타데이터를 리뷰하여 올바른 내용으로 교정할 수 있습니다.

+

+ + grab-ai + +

+

Grab의 경우, LLM이 데이터 분류를 위한 태그를 생성하고 거버넌스 담당자가 확인 후 승인하는 프로세스를 개발했습니다. 이를 통해 민감도 분류, 개인정보 컬럼에 PII 태그를 붙이는 등의 거버넌스 정책을 20,000개 이상 데이터에 일관되게 적용할 수 있었습니다.

+



+

플랫폼에 AI를 사용하는 이유

+

+ + github-copilot + +

+

GitHub Copilot Research에 따르면 Copilot 사용 시 55%의 생산성 증가 효과가 나타난다고 합니다. 플랫폼에 AI를 도입함으로써 사용자는 개발 생산성을 얻을 수 있고 기업은 운영 비용을 절감할 수 있습니다.

+



+

Reference

+

실제 기능에 대한 자세한 내용은 아래 링크를 통해 확인하실 수 있습니다.

+
Next →
  • Powered by Contentful
  • COPYRIGHT © 2020 by @swalloow
\ No newline at end of file diff --git a/page-data/10/page-data.json b/page-data/10/page-data.json index eebde03..734a976 100644 --- a/page-data/10/page-data.json +++ b/page-data/10/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-posts-js","path":"/10","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Spark의 Random Sampling에 대하여","id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","slug":"spark-sampling","publishDate":"June 20, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}}},{"node":{"title":"Spark의 Temporary View에 대하여","id":"a6401b01-05d6-5899-8c9b-4984720e0f66","slug":"spark-temp-view","publishDate":"June 16, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}}},{"node":{"title":"넷플릭스 본사 방문 후기","id":"5a52a186-c4a6-5232-b374-999e5613c51f","slug":"netflix","publishDate":"June 05, 2017","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

\n \n \n \n

\n

회사 내부는 마치 영화관처럼 꾸며져 있었다.\n실제로 넷플릭스 내에는 영화를 볼 수 있는 영화관도 있다.\n계단에는 카페트가 깔려있고 벽면에는 영화 포스터들이 붙어있었다.\n각 층마다 회의실, 세미나실이 정말 많았는데, 모두 영화 이름이 붙어 있었다.\n예를 들면, 회의실의 이름이 닥터 지바고, 어벤져스 이런 식이다.

\n

\n \n \n \n

\n

넷플릭스의 추천 시스템은 실시간, 전 세계 사용자를 대상으로 돌아간다.\n셀제로 각 국가 별 실시간 인기 영화 순위를 보여주는 대시보드를 볼 수 있었다.\n넷플릭스의 추천 시스템은 정말 유명하지만 여전히 고민하는 부분이 많다고 한다.\n(비인기지역의 데이터를 어떻게 처리할 것인지, Cold start problem을 어떻게 해결할 것인지 등)\n최근에는 딥러닝을 이용한 추천 시스템도 개발 중이라고 한다.

\n

데이터 관련 팀은 데이터 분석 팀과 데이터 엔지니어링 팀이 나뉘어 있고 추천 팀은 그 사이에 존재한다.\n데이터 분석 팀은 주로 머신러닝 쪽 일을 한다. (Model training, Parameter tuning)\n중요한 것은 연구를 한다기보다 기존에 나온 여러 연구들 중에\n넷플릭스에 어울리는 것을 찾아 빠르게 적용해보고 성능 테스트를 하는 것이다.

\n

그리고 데이터 엔지니어링 팀은 사용자들의 다양한 로그 데이터를 수집하고 이를 빠르게 프로세싱한다.\n흔히 말하는 데이터 파이프라인에 관련된 일을 한다.\n그리고 또 한 가지 일은 실제 어플리케이션에 모델이 잘 동작하도록 만드는 일이다.\n데이터 엔지니어링 팀에서 주로 사용하는 것은 Apache Spark 그리고 Scala 언어다.\n미국에서도 Spark의 인기는 많았다. 날짜, 시간 단위로 데이터를 나누어 저장하고 이를 불러와서 사용한다.

\n

추천이 중요한 이유는 명확하다.\n넷플릭스의 유저가 전 세계 몇 억명인데 이 중에 0.5% 리텐션이 늘게 되었을 때,\n얻을 수 있는 매출 효과가 어마어마하기 때문이다.

\n
","excerpt":"…"}}}},{"node":{"title":"실리콘밸리 여행 후기","id":"bb5a3ae0-a1e2-52e1-b8a5-f146be7dbfa8","slug":"sanfran-travel","publishDate":"June 05, 2017","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

실리콘밸리는 미국 캘리포니아 주 샌프란시스코 만 지역 남부에 위치한다.\n우연히 좋은 기회를 얻게 되어 여러 회사에 들어가볼 수 있었다.

\n

먼저 여행 계획 단계부터 대충 정리하자면,\n어떤 IT 회사든지 방문증이 없으면 사내에 들어갈 수 없다.\n나 같은 경우, 고민 끝에 링크드인에 있는 지인들을 수소문하거나\n컨퍼런스에서 처음 만난 분에게 부탁해서 들어갈 수 있었다.

\n

미국에서는 흔히 CS과 대학생들이 실리콘밸리 회사 투어를 하기도 하는데\n누군가가 멋진 구글 지도를 만들어 주어서 편하게 다닐 수 있었다.

\n

https://www.google.com/maps/d/viewer?mid=1Inhyh5iUl-incqCMwzdXirUbWUk&ll=37.54682624469536%2C-122.17239440000003&z=9

\n

간단히 살펴보면, 샌프란 위쪽에 있는 BayArea 지역에 몰려있고\n다음으로 Palo Alto, SunneyVale, SanJose 까지 널려있다.\n보통 시내에서 떨어진 지역에 위치한 회사들은 대부분 캠퍼스처럼 자리잡고 있다.

\n

\n \n

\n \n \n \n \n\n \n \n \n

\n

친절한 GitHub Help 직원이 스티커를 계속 가져가라고 해서 챙겨왔다.\n그리고 로비에는 '생각하는 옥토캣' 조각상이 있다.

\n

\n \n \n \n \n\n \n \n \n \n\n \n \n \n

\n

파이어폭스와 구글 오피스는 바다 바로 앞에 위치한다.\n구글은 오직 직원에게만 출입이 허용된다.

\n
","excerpt":"…"}}}},{"node":{"title":"influxDB와 Grafana로 실시간 서버 모니터링 구축하기(1)","id":"d40af3ba-ae50-5de9-94c2-90a9fc861c0d","slug":"influx-grafana1","publishDate":"April 05, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

요즘 실시간 로그 수집 및 분석 도구로 ELK (Elastic Search) 를 많이 사용하지만,\n간단한 서버 모니터링이나 시계열 데이터 분석도구를 찾으신다면, influxDB-Grafana 도 좋습니다.\n이 포스팅에서는 간단한 예제를 통해 influxDB와 Telegraf, Grafana에 대해 알아보겠습니다.

\n
\n

influxDB

\n

\n \n \n \n

\n

쿼리를 잘 모르더라도 좌측의 Query Templates를 통해 쉽게 입력할 수 있습니다.\n스키마는 기존의 데이터베이스와 비슷하면서도 조금 다릅니다.\nHTTP로 데이터를 핸들링 할 수 있으며, 이외에도 여러 가지 특징이 있지만\n여기서 설명하기보다는 공식 래퍼런스를 참조하는 편이 나을 것 같습니다.

\n
\n

Telegraf

\n

이제 Telegraf로 실시간 시스템 지표를 influxDB에 넣어보겠습니다.\nTelegraf는 다양한 데이터 소스에서 plugin을 통해 데이터를 수집하고 저장합니다.\n제공하는 input plugin은 다음 페이지에서 확인하실 수 있습니다.

\n
$ brew update\n$ brew install telegraf
\n

위와 같이 설치한 다음, system plugin을 사용해보도록 하겠습니다.\n--sample-config를 통해 .conf 파일을 생성할 수 있습니다.\ninput filter는 cpu와 memory 지표로, output은 influxDB에 저장됩니다.\n.conf 파일을 열어 output host url을 본인의 호스트에 맞게 변경해주어야 합니다.

\n
$ telegraf --sample-config  --input-filter cpu:mem --output-filter influxdb > telegraf.conf\n$ telegraf -config telegraf.conf
\n

이제 influxDB로 돌아와서 보시면, telegraf 라는 데이터베이스가 생성되어 있습니다.\nmeasurement를 확인해보시거나 쿼리를 날려 데이터를 확인할 수 있습니다.

\n

\n \n \n \n

\n

influxDB는 언젠가 디스크가 찰 수 있어서, 데이터 보관 또는 삭제에 대한 정책이 필요합니다.\n이에 대해서는 Retention Policy를 찾아보시면 됩니다.

\n
","excerpt":"요즘 실시간 로그 수집 및 분석 도구로 ELK (Elastic Search…"}}}},{"node":{"title":"influxDB와 Grafana로 실시간 서버 모니터링 구축하기(2)","id":"8ee50eaf-1f25-596a-8519-125bb05ded98","slug":"influx-grafana2","publishDate":"April 05, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

지난 포스팅에 이어서 Grafana를 연동해보도록 하겠습니다.

\n
\n

Grafana

\n

\n \n \n \n

\n

지난 번에 설치했던 Grafana 도커 이미지를 컨테이너로 실행하면 위와 같이 로그인 화면이 나타납니다.\n아이디는 admin, 비밀번호는 admin으로 접속하시면 됩니다.\n이제 아까 만들었던 database를 Grafana의 data source에 등록할 차례입니다.

\n

\n \n \n \n

\n

Type을 InfluxDB로 맞추고, Url과 Database만 잘 설정해주시면 됩니다.\n이제 새로운 대시보드를 생성하고 Add Graph로 그래프를 추가합니다.

\n

\n \n \n \n

\n

위와 같이 cpu, memory 지표 외에도 다양한 지표를 쉽게 추가할 수 있습니다.

\n

\n

\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}}},{"node":{"title":"Spark의 Random Sampling에 대하여","id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","slug":"spark-sampling","publishDate":"June 20, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}}},{"node":{"title":"Spark의 Temporary View에 대하여","id":"a6401b01-05d6-5899-8c9b-4984720e0f66","slug":"spark-temp-view","publishDate":"June 16, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}}},{"node":{"title":"넷플릭스 본사 방문 후기","id":"5a52a186-c4a6-5232-b374-999e5613c51f","slug":"netflix","publishDate":"June 05, 2017","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

\n \n \n \n

\n

회사 내부는 마치 영화관처럼 꾸며져 있었다.\n실제로 넷플릭스 내에는 영화를 볼 수 있는 영화관도 있다.\n계단에는 카페트가 깔려있고 벽면에는 영화 포스터들이 붙어있었다.\n각 층마다 회의실, 세미나실이 정말 많았는데, 모두 영화 이름이 붙어 있었다.\n예를 들면, 회의실의 이름이 닥터 지바고, 어벤져스 이런 식이다.

\n

\n \n \n \n

\n

넷플릭스의 추천 시스템은 실시간, 전 세계 사용자를 대상으로 돌아간다.\n셀제로 각 국가 별 실시간 인기 영화 순위를 보여주는 대시보드를 볼 수 있었다.\n넷플릭스의 추천 시스템은 정말 유명하지만 여전히 고민하는 부분이 많다고 한다.\n(비인기지역의 데이터를 어떻게 처리할 것인지, Cold start problem을 어떻게 해결할 것인지 등)\n최근에는 딥러닝을 이용한 추천 시스템도 개발 중이라고 한다.

\n

데이터 관련 팀은 데이터 분석 팀과 데이터 엔지니어링 팀이 나뉘어 있고 추천 팀은 그 사이에 존재한다.\n데이터 분석 팀은 주로 머신러닝 쪽 일을 한다. (Model training, Parameter tuning)\n중요한 것은 연구를 한다기보다 기존에 나온 여러 연구들 중에\n넷플릭스에 어울리는 것을 찾아 빠르게 적용해보고 성능 테스트를 하는 것이다.

\n

그리고 데이터 엔지니어링 팀은 사용자들의 다양한 로그 데이터를 수집하고 이를 빠르게 프로세싱한다.\n흔히 말하는 데이터 파이프라인에 관련된 일을 한다.\n그리고 또 한 가지 일은 실제 어플리케이션에 모델이 잘 동작하도록 만드는 일이다.\n데이터 엔지니어링 팀에서 주로 사용하는 것은 Apache Spark 그리고 Scala 언어다.\n미국에서도 Spark의 인기는 많았다. 날짜, 시간 단위로 데이터를 나누어 저장하고 이를 불러와서 사용한다.

\n

추천이 중요한 이유는 명확하다.\n넷플릭스의 유저가 전 세계 몇 억명인데 이 중에 0.5% 리텐션이 늘게 되었을 때,\n얻을 수 있는 매출 효과가 어마어마하기 때문이다.

\n
","excerpt":"…"}}}},{"node":{"title":"실리콘밸리 여행 후기","id":"bb5a3ae0-a1e2-52e1-b8a5-f146be7dbfa8","slug":"sanfran-travel","publishDate":"June 05, 2017","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

실리콘밸리는 미국 캘리포니아 주 샌프란시스코 만 지역 남부에 위치한다.\n우연히 좋은 기회를 얻게 되어 여러 회사에 들어가볼 수 있었다.

\n

먼저 여행 계획 단계부터 대충 정리하자면,\n어떤 IT 회사든지 방문증이 없으면 사내에 들어갈 수 없다.\n나 같은 경우, 고민 끝에 링크드인에 있는 지인들을 수소문하거나\n컨퍼런스에서 처음 만난 분에게 부탁해서 들어갈 수 있었다.

\n

미국에서는 흔히 CS과 대학생들이 실리콘밸리 회사 투어를 하기도 하는데\n누군가가 멋진 구글 지도를 만들어 주어서 편하게 다닐 수 있었다.

\n

https://www.google.com/maps/d/viewer?mid=1Inhyh5iUl-incqCMwzdXirUbWUk&ll=37.54682624469536%2C-122.17239440000003&z=9

\n

간단히 살펴보면, 샌프란 위쪽에 있는 BayArea 지역에 몰려있고\n다음으로 Palo Alto, SunneyVale, SanJose 까지 널려있다.\n보통 시내에서 떨어진 지역에 위치한 회사들은 대부분 캠퍼스처럼 자리잡고 있다.

\n

\n \n

\n \n \n \n \n\n \n \n \n

\n

친절한 GitHub Help 직원이 스티커를 계속 가져가라고 해서 챙겨왔다.\n그리고 로비에는 '생각하는 옥토캣' 조각상이 있다.

\n

\n \n \n \n \n\n \n \n \n \n\n \n \n \n

\n

파이어폭스와 구글 오피스는 바다 바로 앞에 위치한다.\n구글은 오직 직원에게만 출입이 허용된다.

\n
","excerpt":"…"}}}},{"node":{"title":"influxDB와 Grafana로 실시간 서버 모니터링 구축하기(1)","id":"d40af3ba-ae50-5de9-94c2-90a9fc861c0d","slug":"influx-grafana1","publishDate":"April 05, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

요즘 실시간 로그 수집 및 분석 도구로 ELK (Elastic Search) 를 많이 사용하지만,\n간단한 서버 모니터링이나 시계열 데이터 분석도구를 찾으신다면, influxDB-Grafana 도 좋습니다.\n이 포스팅에서는 간단한 예제를 통해 influxDB와 Telegraf, Grafana에 대해 알아보겠습니다.

\n
\n

influxDB

\n

\n \n \n \n

\n

쿼리를 잘 모르더라도 좌측의 Query Templates를 통해 쉽게 입력할 수 있습니다.\n스키마는 기존의 데이터베이스와 비슷하면서도 조금 다릅니다.\nHTTP로 데이터를 핸들링 할 수 있으며, 이외에도 여러 가지 특징이 있지만\n여기서 설명하기보다는 공식 래퍼런스를 참조하는 편이 나을 것 같습니다.

\n
\n

Telegraf

\n

이제 Telegraf로 실시간 시스템 지표를 influxDB에 넣어보겠습니다.\nTelegraf는 다양한 데이터 소스에서 plugin을 통해 데이터를 수집하고 저장합니다.\n제공하는 input plugin은 다음 페이지에서 확인하실 수 있습니다.

\n
$ brew update\n$ brew install telegraf
\n

위와 같이 설치한 다음, system plugin을 사용해보도록 하겠습니다.\n--sample-config를 통해 .conf 파일을 생성할 수 있습니다.\ninput filter는 cpu와 memory 지표로, output은 influxDB에 저장됩니다.\n.conf 파일을 열어 output host url을 본인의 호스트에 맞게 변경해주어야 합니다.

\n
$ telegraf --sample-config  --input-filter cpu:mem --output-filter influxdb > telegraf.conf\n$ telegraf -config telegraf.conf
\n

이제 influxDB로 돌아와서 보시면, telegraf 라는 데이터베이스가 생성되어 있습니다.\nmeasurement를 확인해보시거나 쿼리를 날려 데이터를 확인할 수 있습니다.

\n

\n \n \n \n

\n

influxDB는 언젠가 디스크가 찰 수 있어서, 데이터 보관 또는 삭제에 대한 정책이 필요합니다.\n이에 대해서는 Retention Policy를 찾아보시면 됩니다.

\n
","excerpt":"요즘 실시간 로그 수집 및 분석 도구로 ELK (Elastic Search…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":9,"humanPageNumber":10,"skip":55,"limit":6,"numberOfPages":16,"previousPagePath":"/9","nextPagePath":"/11"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/11/page-data.json b/page-data/11/page-data.json index c4bbe0a..61eff3b 100644 --- a/page-data/11/page-data.json +++ b/page-data/11/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-posts-js","path":"/11","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Spring JPA, MyBatis","id":"64d1ae38-b6bd-56d0-8f9e-d96d3fc09387","slug":"spring-boot-jpa","publishDate":"April 05, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

최근 스프링 부트를 공부하면서 이것저것 정리하는 중 입니다.

\n
\n

JPA, Hibernate

\n

스프링 JPA와 MyBatis 모두 Persistance API 입니다.\nJPA는 기존의 EJB와 다른 POJO 기반의 ORM 모델을 제공하며,\n대표적으로 Hibernate 프레임워크 구현체가 있습니다.

\n

먼저, ORM이라는 개념에 대해 알아야 합니다.\nORM(Object Relational Mapper) 는 말 그대로 자바 객체와\n데이터베이스의 Entity를 그대로 연결하는 것을 말합니다.\n따라서, 모든 SQL문은 Hibernate의 HSQL을 통해 이루어집니다.

\n

이런 개념이 생겨나게 된 이유를 생각해보면\n객체지향적으로 데이터를 관리할 수 있기 때문에 비즈니스 로직에 집중 할 수 있으며,\n객체지향 개발이 가능하다. 또한, 로직을 쿼리보다 객체에 집중할 수 있다.\n따라서, 더 빠른 개발이 가능합니다.

\n
\n

MyBatis

\n

MyBatis는 SQL Mapper 입니다.\n기존에 JDBC를 사용할 때는 DB와 관련된 여러 복잡한 설정(Connection) 들을 다루어야 했습니다.\nSQL Mapper는 자바 객체를 실제 SQL문에 연결함으로써,\n빠른 개발과 편리한 테스트 환경을 제공해주었습니다.

\n

MyBatis 프로젝트는 원래 Apache Foundation의 iBatis 였으나,\n생산성, 개발 프로세스, 커뮤니티 등의 이유로 Google Code로 이전되면서 이름이 바뀌었습니다.\n마이그레이션이 되면서 바뀐 차이점은 아래와 같습니다.

\n\n
\n

정리하면서

\n

전 세계적으로 다양한 언어와 프레임워크가 ORM을 지원하는 방향으로\n움직이고 있으며, 많은 회사에서 생산성이 높다는 사실을 입증했습니다.

\n

하지만, MyBatis를 쓰더라도 본인이 쿼리 작성 능력이 뛰어나고,\n쿼리 최적화에 자신이 있다면, 더 생산성이 높을 수 있습니다.

\n

반대로, 데이터베이스 모델링에 대한 개념이 없다면,\nHibernate를 쓰더라도 성능 문제와 데이터 손실이 생길 수 있습니다.\nORM도 결국 SQL을 사용한 기술입니다.\n따라서 ORM을 사용하더라도 어떤 SQL이 실행될 지 알아야 할 필요가 있습니다.\n결국 상황을 잘 고려해서 본인에게 생산성이 더 높은 프레임워크를 선택하는 것이 옳은 것 같습니다.

\n
","excerpt":"최근 스프링 부트를 공부하면서 이것저것 정리하는 중 입니다. JPA, Hibernate 스프링 JPA와 MyBatis 모두 Persistance…"}}}},{"node":{"title":"Docker와 Gitlab CI를 활용한 빌드, 테스트 자동화","id":"b185d4b2-a18e-5d7b-aa82-f37f36d0359f","slug":"gitlabci-docker","publishDate":"March 31, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAAABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAADTAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJDAAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAEFBP/EACIQAAICAgIABwAAAAAAAAAAAAECABEDBBIhQVFSYqHB0f/EABUBAQEAAAAAAAAAAAAAAAAAAAIE/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oADAMBAAIRAxEAPwBnZyBeRZVPlzr6mc7mVspLMle7LY+KkobTN2QOuoxkJBYEihdeEoTbVlN4KKLoTfrP7CQTvOOioMIdhZX/2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Gitlab은 설치형 GitHub이라고 이해하시면 편합니다.\n무료로 private repository와 CI 서버를 제공해줍니다.\n심지어 Docker Registry도 무료로 제공하고 있습니다.\n아직 많은 분들이 Gitlab CI의 여러 장점들을 잘 모르시는 것 같아 정리해보았습니다.

\n
\n

Gitlab CI

\n

Gitlab CI는 Gitlab에서 무료로 제공하는 CI 툴 입니다.\nGitlab과 완벽하게 연동되며 CI를 위해 CI linter, pipeline, cycle analytics 등 다양한 서비스를 제공합니다.

\n

\n \n \n \n

\n
    \n
  1. 사용자가 Gitlab 저장소에 push를 하면, Gitlab CI Runner로 전달됩니다.
  2. \n
  3. Gitlab CI는 Gitlab Registry로부터 Docker 이미지를 받아옵니다. Docker 이미지에는 어플리케이션 환경이 설정되어 있습니다.
  4. \n
  5. Docker 컨테이너가 실행되면 첫번째 job에 정의된 대로 필요한 패키지를 설치하고 빌드를 수행합니다.
  6. \n
  7. 빌드가 통과되면 두번째 job에 정의된 대로 테스트를 수행합니다.
  8. \n
  9. 테스트가 통과되면 세번째 job에 정의된 대로 배포 과정을 수행합니다.
  10. \n
  11. 각 과정은 모두 Slack 알림으로 확인할 수 있습니다.
  12. \n
\n
\n

\n \n \n \n

\n

위와 같이 모든 과정을 Gitlab Pipeline을 통해 확인하실 수 있습니다.

\n

Gitlab의 단점이라면 Community 버전의 서버가 조금 불안정하다는 점입니다.\n물론 설치형 Gitlab을 사용하신다면 이런 단점마저 존재하지 않습니다.\n소규모의 팀이라면 충분히 도입을 검토해볼만 하다고 생각합니다.

\n
","excerpt":"Gitlab은 설치형 GitHub이라고 이해하시면 편합니다.\n무료로 private repository와 CI…"}}}},{"node":{"title":"올바른 Dockerfile 작성을 위한 가이드라인","id":"6fa724a3-3395-5afb-bc3f-f17a9116bc81","slug":"dockerfile-ignore","publishDate":"March 28, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAAABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAADTAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJDAAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAEFBP/EACIQAAICAgIABwAAAAAAAAAAAAECABEDBBIhQVFSYqHB0f/EABUBAQEAAAAAAAAAAAAAAAAAAAIE/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oADAMBAAIRAxEAPwBnZyBeRZVPlzr6mc7mVspLMle7LY+KkobTN2QOuoxkJBYEihdeEoTbVlN4KKLoTfrP7CQTvOOioMIdhZX/2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Docker가 처음이라면, 이전 포스팅을 참고하시기 바랍니다.

\n\n
\n

Dockerfile

\n

Dockerfile은 일종의 이미지 설정파일입니다.\n생긴 모양새는 쉘 스크립트와 유사하지만 자체의 문법을 가지고 있습니다.\n이렇게 작성된 Dockerfile은 build 명령어를 통해 이미지를 생성할 수 있습니다.

\n

이 포스팅에서는 Dockerfile 레퍼런스에 나와 있는 가이드라인을 정리해보도록 하겠습니다.\nhttps://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/에 자세한 내용이 설명되어 있습니다.

\n
\n

컨테이너는 일시적이어야 한다

\n

일시적이라는 말은 가능한 최소한의 설정 및 구성으로 이루어져있어야 한다는 것을 의미합니다.\n이에 대한 내용은 Twelve Factors Application을 참고하시면 좋습니다.

\n
\n

.dockerignore을 활용하자

\n

대부분의 경우 각 Docker 파일을 빈 디렉토리에 저장하는 것이 가장 좋습니다.\n그런 다음 Dockerfile을 빌드하는 데 필요한 파일만 해당 디렉토리에 추가하시면 됩니다.\n빌드의 성능을 높이려면 해당 디렉토리에 .dockerignore 파일을 추가하여 파일 및 디렉토리를 제외 할 수 있습니다.\n.dockerignore 파일은 .gitignore 파일과 유사하게 동작한다고 보시면 됩니다.

\n
*.md\n!README.md
\n

위와 같은 .dockerignore 파일은 README.md 파일을 제외한 모든 마크다운 파일을 제외시킵니다. 이런식으로 원하지 않는 파일 및 디렉토리를 제외시켜 이미지의 용량을 줄일 수 있습니다.

\n
\n

불필요한 패키지를 설치하지 말자

\n

복잡성, 의존성, 파일 크기 및 빌드 시간을 줄이기 위해서는 불필요한 패키지를 설치하지 말아야 합니다.\n예를 들어, 데이터베이스 이미지에 텍스트 편집기를 포함시킨다거나 하는 일은 없어야 합니다.

\n
\n

컨테이너는 오직 하나의 관심사만 갖는다

\n

애플리케이션을 여러 컨테이너로 분리하면 컨테이너를 확장하고 재사용하는 것이 훨씬 쉬워집니다.\n예를 들어, 일반적인 어플리케이션은 웹 어플리케이션, 데이터베이스, 인메모리-캐시와 같이 세 개의 컨테이너로 구성 될 수 있습니다.

\n

컨테이너 당 하나의 프로세스 가 있어야한다는 말을 들어 보셨을 겁니다.\n하지만, 언제나 컨테이너 당 하나의 운영 체제 프로세스만 있어야 하는 것은 아닙니다.\n컨테이너가 init 프로세스로 생성 될 수 있다는 사실 외에도 일부 프로그램은 자체적으로 추가 프로세스를 생성 할 수 있습니다.\n예를 들어 Celery는 여러 작업자 프로세스를 생성하거나 Apache 스스로 요청에 따른 프로세스를 생성 할 수 있습니다.\n컨테이너를 깔끔한 모듈 형식으로 유지하기 위해 신중히 선택해야 합니다.\n컨테이너에 서로 의존성이 생기는 경우 Docker 컨테이너 네트워크를 사용하여 서로 통신 할 수 있습니다.

\n
\n

레이어의 수를 최소화하자

\n

사용하는 레이어의 수에 대해 전략적이고 신중해야합니다.\n장기적인 관점에서 보았을 때 유지보수를 위해서는 레이어의 수를 최소화하는 것이 현명한 선택이 될 수 있습니다.

\n
\n

줄바꿈을 사용하여 정렬하자

\n
RUN apt-get update && apt-get install -y \\\n  bzr \\\n  cvs \\\n  git
\n

위와 같이 줄바꿈을 사용하면, 패키지의 중복을 피하고 목록을 훨씬 쉽게 업데이트 할 수 있습니다.\n백 슬래시 (\\) 앞에 공백을 추가하면 가독성을 높이는 데에 도움이됩니다.

\n
\n

캐시를 활용하여 빌드하자

\n

이미지를 작성하는 과정에서 Docker는 지정한 순서대로 Dockerfile을 단계 별로 실행합니다.\n각 명령을 실행할 때 Docker는 매번 새로운 이미지를 만드는 대신 캐시에서 기존 이미지를 찾아 재사용 할 수 있습니다.\n캐시를 전혀 사용하지 않으려는 경우 docker 빌드 명령에서 --no-cache = true 옵션을 사용하시면 됩니다.

\n

Docker가 캐시를 사용하게하려면 일치하는 이미지를 찾을 때와 그렇지 않을 때를 이해하는 것이 매우 중요합니다.\nDocker cache의 기본 규칙은 다음과 같습니다.

\n\n
","excerpt":"Docker가 처음이라면, 이전 포스팅을 참고하시기 바랍니다. Docker 간편한 설치부터 실행까지 Docker, DockerHub…"}}}},{"node":{"title":"파이썬을 위한 Dockerfile 작성하기","id":"4ba084f5-7e76-574b-8c1a-db0091539cf1","slug":"dockerfile","publishDate":"March 27, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Docker가 처음이라면, 이전 포스팅을 참고하시기 바랍니다.

\n\n
\n

Flask Application

\n

Dockerfile은 일종의 이미지 설정파일입니다. build 명령어를 통해 이미지를 생성할 수 있습니다.\n파이썬 웹 어플리케이션을 Docker로 실행시키는 예제를 통해 천천히 정리해보겠습니다.

\n
From flask import Flask\napp = Flask(__name__)\n\n@app.route('/')\ndef hello_world():\n    return 'Hello, world!'\n\nif __name__ == '__main__':\n    app.run(debug=True,host='0.0.0.0')
\n

먼저 위와 같이 간단한 플라스크 웹 어플리케이션을 작성합니다.\n필요한 패키지는 requirements.txt로 관리합니다.\npip freeze > requirements.txt 명령어를 통해 파일을 생성할 수 있습니다.

\n
Flask==0.12
\n
\n

Dockerfile 작성하기

\n
FROM ubuntu:latest\nMAINTAINER your_name \"email@gmail.com\"\nRUN apt-get update -y\nRUN apt-get install -y python-pip python-dev build-essential\nCOPY . /app\nWORKDIR /app\nRUN pip install -r requirements.txt\nENTRYPOINT [\"python\"]\nCMD [\"app.py\"]
\n

위와 같이 Dockerfile을 작성하시면 됩니다.\n간단히 설명하자면, ubuntu 이미지를 받아와서 파이썬 환경설정을 하고\n현재 경로에 있는 폴더를 복사해서 파이썬 패키지를 설치하고 앱을 실행시키는 이미지입니다.

\n
\n

Dockerfile 빌드 및 실행하기

\n
$ docker build -t flask-application:latest .\n$ docker run -d -p 5000:5000 flask-application
\n

docker build [name] 명령어를 통해 이미지를 빌드합니다.\n그리고 docker run [image] 명령어를 통해 컨테이너를 실행시킵니다.\n-p 옵션은 포트를 지정하며, -d 옵션은 백그라운드로 실행시키는 옵션입니다.\n5000번 포트를 확인해보면 플라스크 어플리케이션이 실행된 것을 확인할 수 있습니다.

\n
","excerpt":"Docker가 처음이라면, 이전 포스팅을 참고하시기 바랍니다. Docker 간편한 설치부터 실행까지 Docker, DockerHub…"}}}},{"node":{"title":"폴리글랏 프로그래밍 후기","id":"2f49d83d-d958-5727-9eb3-4a201adc5566","slug":"polyglot-programming","publishDate":"March 25, 2017","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

임백준 작가님의 폴리글랏 프로그래밍 을 읽고 핵심내용만 간략하게 정리해보려고 한다.\n시대의 흐름에 따라 재미있게 설명해주기 때문에, 킬링타임 용으로 가볍게 읽기 좋은 책인 것 같다.

\n\n

앞으로 프로그래머는 어느 하나의 언어에 안주할 수 없다.\n폴리그랏 프로그래밍 시대에는 패러다임을 달리하는 여러 개의 언어를 자유롭게 구사하지 않으면 살아남기 힘들다.\n따라서 앞으로 필요에 따라 언어를 빨리 습득하는 능력 또한 중요하다.

\n
","excerpt":"…"}}}},{"node":{"title":"리눅스 시스템 모니터링 명령어 정리","id":"37b880e0-b8ba-5432-acda-d064142e9195","slug":"system-monitoring","publishDate":"March 24, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

리눅스 시스템 모니터링을 위한 명령어에 대해 정리해보았습니다.

\n
\n

프로세스 모니터링 명령어 - top

\n

\n \n \n \n

\n

top 명령어는 커널을 통하여 관리되는 프로세스들의 정보(메모리 사용률, CPU 사용률, 상태정보 등)를 확인할 수 있는 명령어입니다.\n응용프로그램을 강제로 종료시키고 싶을 때, 실행중인 프로세스를 찾아 kill 명령어를 통해 강제종료시킬 수도 있습니다.

\n

OS X에서는 -o 옵션을 통해, 리눅스에서는 shift + f 명령어를 통해 프로세스를 key에 따라 정렬할 수 있습니다.

\n
\n

시스템 리소스 정보 - vmstat, iostat, sar

\n

\n \n \n \n

\n

vmstat 명령어는 virtual memory statistics 의 줄임말로 가상메모리 등 다양한 리소스 정보를 제공합니다.\nOS X에서는 vm_stat 명령어로, 리눅스에서는 vmstat 명령어로 확인하실 수 있습니다.

\n

\n \n \n \n

\n

iostat 명령어는 sysstat에서 가장 기본적인 명령어로 CPU 및 디스크 입출력에 대한 기본정보를 제공합니다.

\n

\n \n \n \n

\n

sar 명령어는 시스템 활동 모니터링에 유용합니다.\n특히 -r, -f 옵션을 통해 CPU, 메모리 사용률을 날짜, 시간 대 별로 확인할 수 있습니다.

\n
\n

Linux sysstat 패키지 설치

\n

CentOS, Ubuntu에서는 앞서 말씀드린 sar, vmstat 등의 명령어를 사용하기 위해서 sysstat 패키지를 설치해야 합니다.\n아래의 명령어를 통해 설치할 수 있습니다.

\n
// CentOS, Ubuntu\n$ yum install sysstat -y\n$ apt install sysstat -y
\n

만일 권한 오류나 명령어를 찾을 수 없다는 오류가 나타난다면 아래의 설정을 통해 해결할 수 있습니다.

\n
$ sudo vi /etc/default/sysstat\n$ ENABLED=”true”
\n
","excerpt":"리눅스 시스템 모니터링을 위한 명령어에 대해 정리해보았습니다. 프로세스 모니터링 명령어 - top top…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":10,"humanPageNumber":11,"skip":61,"limit":6,"numberOfPages":16,"previousPagePath":"/10","nextPagePath":"/12"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/11","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"influxDB와 Grafana로 실시간 서버 모니터링 구축하기(2)","id":"8ee50eaf-1f25-596a-8519-125bb05ded98","slug":"influx-grafana2","publishDate":"April 05, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

지난 포스팅에 이어서 Grafana를 연동해보도록 하겠습니다.

\n
\n

Grafana

\n

\n \n \n \n

\n

지난 번에 설치했던 Grafana 도커 이미지를 컨테이너로 실행하면 위와 같이 로그인 화면이 나타납니다.\n아이디는 admin, 비밀번호는 admin으로 접속하시면 됩니다.\n이제 아까 만들었던 database를 Grafana의 data source에 등록할 차례입니다.

\n

\n \n \n \n

\n

Type을 InfluxDB로 맞추고, Url과 Database만 잘 설정해주시면 됩니다.\n이제 새로운 대시보드를 생성하고 Add Graph로 그래프를 추가합니다.

\n

\n \n \n \n

\n

위와 같이 cpu, memory 지표 외에도 다양한 지표를 쉽게 추가할 수 있습니다.

\n

\n \n \n \n

\n
    \n
  1. 사용자가 Gitlab 저장소에 push를 하면, Gitlab CI Runner로 전달됩니다.
  2. \n
  3. Gitlab CI는 Gitlab Registry로부터 Docker 이미지를 받아옵니다. Docker 이미지에는 어플리케이션 환경이 설정되어 있습니다.
  4. \n
  5. Docker 컨테이너가 실행되면 첫번째 job에 정의된 대로 필요한 패키지를 설치하고 빌드를 수행합니다.
  6. \n
  7. 빌드가 통과되면 두번째 job에 정의된 대로 테스트를 수행합니다.
  8. \n
  9. 테스트가 통과되면 세번째 job에 정의된 대로 배포 과정을 수행합니다.
  10. \n
  11. 각 과정은 모두 Slack 알림으로 확인할 수 있습니다.
  12. \n
\n
\n

\n \n \n \n

\n

위와 같이 모든 과정을 Gitlab Pipeline을 통해 확인하실 수 있습니다.

\n

Gitlab의 단점이라면 Community 버전의 서버가 조금 불안정하다는 점입니다.\n물론 설치형 Gitlab을 사용하신다면 이런 단점마저 존재하지 않습니다.\n소규모의 팀이라면 충분히 도입을 검토해볼만 하다고 생각합니다.

\n
","excerpt":"Gitlab은 설치형 GitHub이라고 이해하시면 편합니다.\n무료로 private repository와 CI…"}}}},{"node":{"title":"올바른 Dockerfile 작성을 위한 가이드라인","id":"6fa724a3-3395-5afb-bc3f-f17a9116bc81","slug":"dockerfile-ignore","publishDate":"March 28, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Docker가 처음이라면, 이전 포스팅을 참고하시기 바랍니다.

\n\n
\n

Dockerfile

\n

Dockerfile은 일종의 이미지 설정파일입니다.\n생긴 모양새는 쉘 스크립트와 유사하지만 자체의 문법을 가지고 있습니다.\n이렇게 작성된 Dockerfile은 build 명령어를 통해 이미지를 생성할 수 있습니다.

\n

이 포스팅에서는 Dockerfile 레퍼런스에 나와 있는 가이드라인을 정리해보도록 하겠습니다.\nhttps://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/에 자세한 내용이 설명되어 있습니다.

\n
\n

컨테이너는 일시적이어야 한다

\n

일시적이라는 말은 가능한 최소한의 설정 및 구성으로 이루어져있어야 한다는 것을 의미합니다.\n이에 대한 내용은 Twelve Factors Application을 참고하시면 좋습니다.

\n
\n

.dockerignore을 활용하자

\n

대부분의 경우 각 Docker 파일을 빈 디렉토리에 저장하는 것이 가장 좋습니다.\n그런 다음 Dockerfile을 빌드하는 데 필요한 파일만 해당 디렉토리에 추가하시면 됩니다.\n빌드의 성능을 높이려면 해당 디렉토리에 .dockerignore 파일을 추가하여 파일 및 디렉토리를 제외 할 수 있습니다.\n.dockerignore 파일은 .gitignore 파일과 유사하게 동작한다고 보시면 됩니다.

\n
*.md\n!README.md
\n

위와 같은 .dockerignore 파일은 README.md 파일을 제외한 모든 마크다운 파일을 제외시킵니다. 이런식으로 원하지 않는 파일 및 디렉토리를 제외시켜 이미지의 용량을 줄일 수 있습니다.

\n
\n

불필요한 패키지를 설치하지 말자

\n

복잡성, 의존성, 파일 크기 및 빌드 시간을 줄이기 위해서는 불필요한 패키지를 설치하지 말아야 합니다.\n예를 들어, 데이터베이스 이미지에 텍스트 편집기를 포함시킨다거나 하는 일은 없어야 합니다.

\n
\n

컨테이너는 오직 하나의 관심사만 갖는다

\n

애플리케이션을 여러 컨테이너로 분리하면 컨테이너를 확장하고 재사용하는 것이 훨씬 쉬워집니다.\n예를 들어, 일반적인 어플리케이션은 웹 어플리케이션, 데이터베이스, 인메모리-캐시와 같이 세 개의 컨테이너로 구성 될 수 있습니다.

\n

컨테이너 당 하나의 프로세스 가 있어야한다는 말을 들어 보셨을 겁니다.\n하지만, 언제나 컨테이너 당 하나의 운영 체제 프로세스만 있어야 하는 것은 아닙니다.\n컨테이너가 init 프로세스로 생성 될 수 있다는 사실 외에도 일부 프로그램은 자체적으로 추가 프로세스를 생성 할 수 있습니다.\n예를 들어 Celery는 여러 작업자 프로세스를 생성하거나 Apache 스스로 요청에 따른 프로세스를 생성 할 수 있습니다.\n컨테이너를 깔끔한 모듈 형식으로 유지하기 위해 신중히 선택해야 합니다.\n컨테이너에 서로 의존성이 생기는 경우 Docker 컨테이너 네트워크를 사용하여 서로 통신 할 수 있습니다.

\n
\n

레이어의 수를 최소화하자

\n

사용하는 레이어의 수에 대해 전략적이고 신중해야합니다.\n장기적인 관점에서 보았을 때 유지보수를 위해서는 레이어의 수를 최소화하는 것이 현명한 선택이 될 수 있습니다.

\n
\n

줄바꿈을 사용하여 정렬하자

\n
RUN apt-get update && apt-get install -y \\\n  bzr \\\n  cvs \\\n  git
\n

위와 같이 줄바꿈을 사용하면, 패키지의 중복을 피하고 목록을 훨씬 쉽게 업데이트 할 수 있습니다.\n백 슬래시 (\\) 앞에 공백을 추가하면 가독성을 높이는 데에 도움이됩니다.

\n
\n

캐시를 활용하여 빌드하자

\n

이미지를 작성하는 과정에서 Docker는 지정한 순서대로 Dockerfile을 단계 별로 실행합니다.\n각 명령을 실행할 때 Docker는 매번 새로운 이미지를 만드는 대신 캐시에서 기존 이미지를 찾아 재사용 할 수 있습니다.\n캐시를 전혀 사용하지 않으려는 경우 docker 빌드 명령에서 --no-cache = true 옵션을 사용하시면 됩니다.

\n

Docker가 캐시를 사용하게하려면 일치하는 이미지를 찾을 때와 그렇지 않을 때를 이해하는 것이 매우 중요합니다.\nDocker cache의 기본 규칙은 다음과 같습니다.

\n\n
","excerpt":"Docker가 처음이라면, 이전 포스팅을 참고하시기 바랍니다. Docker 간편한 설치부터 실행까지 Docker, DockerHub…"}}}},{"node":{"title":"파이썬을 위한 Dockerfile 작성하기","id":"4ba084f5-7e76-574b-8c1a-db0091539cf1","slug":"dockerfile","publishDate":"March 27, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Docker가 처음이라면, 이전 포스팅을 참고하시기 바랍니다.

\n\n
\n

Flask Application

\n

Dockerfile은 일종의 이미지 설정파일입니다. build 명령어를 통해 이미지를 생성할 수 있습니다.\n파이썬 웹 어플리케이션을 Docker로 실행시키는 예제를 통해 천천히 정리해보겠습니다.

\n
From flask import Flask\napp = Flask(__name__)\n\n@app.route('/')\ndef hello_world():\n    return 'Hello, world!'\n\nif __name__ == '__main__':\n    app.run(debug=True,host='0.0.0.0')
\n

먼저 위와 같이 간단한 플라스크 웹 어플리케이션을 작성합니다.\n필요한 패키지는 requirements.txt로 관리합니다.\npip freeze > requirements.txt 명령어를 통해 파일을 생성할 수 있습니다.

\n
Flask==0.12
\n
\n

Dockerfile 작성하기

\n
FROM ubuntu:latest\nMAINTAINER your_name \"email@gmail.com\"\nRUN apt-get update -y\nRUN apt-get install -y python-pip python-dev build-essential\nCOPY . /app\nWORKDIR /app\nRUN pip install -r requirements.txt\nENTRYPOINT [\"python\"]\nCMD [\"app.py\"]
\n

위와 같이 Dockerfile을 작성하시면 됩니다.\n간단히 설명하자면, ubuntu 이미지를 받아와서 파이썬 환경설정을 하고\n현재 경로에 있는 폴더를 복사해서 파이썬 패키지를 설치하고 앱을 실행시키는 이미지입니다.

\n
\n

Dockerfile 빌드 및 실행하기

\n
$ docker build -t flask-application:latest .\n$ docker run -d -p 5000:5000 flask-application
\n

docker build [name] 명령어를 통해 이미지를 빌드합니다.\n그리고 docker run [image] 명령어를 통해 컨테이너를 실행시킵니다.\n-p 옵션은 포트를 지정하며, -d 옵션은 백그라운드로 실행시키는 옵션입니다.\n5000번 포트를 확인해보면 플라스크 어플리케이션이 실행된 것을 확인할 수 있습니다.

\n
","excerpt":"Docker가 처음이라면, 이전 포스팅을 참고하시기 바랍니다. Docker 간편한 설치부터 실행까지 Docker, DockerHub…"}}}},{"node":{"title":"폴리글랏 프로그래밍 후기","id":"2f49d83d-d958-5727-9eb3-4a201adc5566","slug":"polyglot-programming","publishDate":"March 25, 2017","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

임백준 작가님의 폴리글랏 프로그래밍 을 읽고 핵심내용만 간략하게 정리해보려고 한다.\n시대의 흐름에 따라 재미있게 설명해주기 때문에, 킬링타임 용으로 가볍게 읽기 좋은 책인 것 같다.

\n\n

앞으로 프로그래머는 어느 하나의 언어에 안주할 수 없다.\n폴리그랏 프로그래밍 시대에는 패러다임을 달리하는 여러 개의 언어를 자유롭게 구사하지 않으면 살아남기 힘들다.\n따라서 앞으로 필요에 따라 언어를 빨리 습득하는 능력 또한 중요하다.

\n
","excerpt":"…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":10,"humanPageNumber":11,"skip":61,"limit":6,"numberOfPages":16,"previousPagePath":"/10","nextPagePath":"/12"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/12/page-data.json b/page-data/12/page-data.json index ac1043c..d469599 100644 --- a/page-data/12/page-data.json +++ b/page-data/12/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-posts-js","path":"/12","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Jupyter에서 Scala로 Spark 사용하는 방법","id":"b68b3f15-e560-5485-9b60-204947689edd","slug":"jupyter-spark","publishDate":"March 22, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n
$ ssh -R port1:host_name:port2 server_name
\n

이번에는 로컬에서 파이썬 웹 애플리케이션을 개발 중인데 친구에게 보여주고 싶다고 가정 해보겠습니다.\n아직 공개 IP 주소를 제공하지 않기 때문에 인터넷을 통해 직접 기기에 연결할 수 없을 겁니다.\n라우터에서 NAT를 구성하여 해결할 수 있지만 라우터의 구성을 변경해야하므로 번거롭습니다.\n이럴때 Remote port forwarding을 통해 쉽게 해결할 수 있습니다.

\n

먼저 port1의 서버에서 port2로 로컬 트래픽을 전달하는 SSH 터널을 생성합니다.\n이후 로컬에서 port2의 서버에 연결하면 실제로 SSH 터널을 통해 데이터를 요청하는 것을 확인할 수 있습니다.

\n

OSI 7계층에서 생각해보면 SSH는 Application - Transport - Network 계층에 걸쳐있습니다.\nApplication 계층에서 포트를 연결하면 Transport 계층의 TCP 통신을 통해 전달되고,\nNetwork 계층을 통해 목적지로 이동하게 됩니다.

\n
","excerpt":"지금까지 아무 생각없이 SSH를 사용하다가 한번 정리해보았습니다. SSH Protocol SSH는 Secure Shell의 약자입니다. SSH…"}}}},{"node":{"title":"빅데이터 처리에 Scala가 필요한 이유","id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","slug":"scala-for-bigdata","publishDate":"March 17, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}}},{"node":{"title":"AWS EC2 인스턴스 SSH 접속을 위한 초기설정 그리고 주의사항","id":"580383a5-b9d8-5ed6-b06d-0e128a0d1712","slug":"aws-ec2","publishDate":"March 10, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 포스팅의 모든 내용은 OS X에 최적화되어 있습니다.\n그리고, 정리한 내용은 AWS 공식문서에 아주 잘 소개되어 있습니다.

\n
\n

AWS CLI

\n

AWS CLI는 여러 AWS 서비스를 명령줄에서 제어하고 스크립트를 통해 자동화할 수 있는 커멘드라인 인터페이스입니다.\n이를 사용하기 전에 먼저, brew를 통해 awscli를 설치해야 합니다.

\n
$ brew install awscli
\n

설치하고 나면 이제 aws 명령어를 사용할 수 있습니다.\n가장 먼저 configure 명령어를 통해 Access key를 입력해야합니다.

\n
$ aws configure\nAWS Access Key ID [None]: AAAAIOSFODNN7EXAMPLE\nAWS Secret Access Key [None]: wwwwwXUtnFEMI/K7MDENG/bPxRfiCYEXAMKEYKEY\nDefault region name [None]: ap-northeast-2\nDefault output format [None]: json
\n

여기에서 Access Key ID는 [IAM - Security credentials]에서 확인할 수 있습니다.

\n
\n

AWS EC2 접속

\n

EC2 인스턴스를 만들면 .pem이라는 파일을 발급받게 됩니다.\n이 파일은 절대 외부로 유출되면 안되기 때문에 조심해야합니다.\n.pem 파일이 있는 경로로 이동한 다음 아래의 명령어를 통해 접속하면 됩니다.

\n
$ chmod 400 /path/my-key-pair.pem\n$ ssh -i /path/my-key-pair.pem ec2-user@ec2-198-51-100-1.compute-1.amazonaws.com
\n

\"Permission denied\" 에러가 발생하면 -vvv 옵션을 통해 디버깅 할 수 있습니다.\n생성한 인스턴스 OS에 해당하는 유저아이디로 변경해주어야 합니다.\n예를 들어 ubuntu 인스턴스인 경우, ec2-user 대신 ubuntu가 들어갑니다.

\n
\n

AWS를 사용하면서 조심해야할 사항

\n

1.인스턴스 관리

\n

프리티어를 사용하는 경우, 다 사용하고 나서 인스턴스를 항상 꺼주는 습관을 들여 과도한 요금이 과금되지 않도록 해야합니다. 특히 여러 개의 인스턴스를 돌리는 경우 순식간에 청구서가 날아올 수 있습니다.

\n

2. ROOT 계정 사용 자제

\n

많은 경우에 ROOT 계정의 키가 털려서 과금이 발생됩니다. IAM을 통해 별도의 계정을 만들어서 사용하고, GitHub 같은 곳에 설정파일을 올리지 말아야합니다.

\n

3. CloudWatch로 요금 확인

\n

CloudWatch를 통해 Billing Cost가 일정 금액을 넘어가면 메일이나 Slack 메세지로 보내도록 설정해두면 편합니다.

\n

4. 네트워크 확인

\n

네트워크에서 모든 포트를 여는 것도 위험합니다. 이렇게 되면 다양한 공격을 받을 위험이 있습니다.

\n
\n

참고링크

\n\n
","excerpt":"이 포스팅의 모든 내용은 OS X에 최적화되어 있습니다.\n그리고, 정리한 내용은 AWS 공식문서에 아주 잘 소개되어 있습니다. AWS CLI…"}}}},{"node":{"title":"파이썬 웹 어플리케이션 보안 점검 가이드","id":"51fa9f19-22b3-5955-a571-ee02d8251a78","slug":"flask-security","publishDate":"March 08, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

이 포스팅은 Jacob Kaplan-Moss가 2013년 호주 pycon에서 발표한 자료를 바탕으로 하며, OYT님이 최신화하여 정리해주신 자료를 참고하였습니다.

\n
\n

OWASP Top 10

\n

먼저, OWASP(The Open Web Application Security Project)는 오픈소스 웹 애플리케이션 보안 프로젝트로, 주로 웹에 관한 정보노출, 악성 파일 및 스크립트, 보안 취약점 등을 연구하며,\n웹 애플리케이션의 취약점 중에서 빈도가 많이 발생하고, 보안상 영향을 크게 줄 수 있는 것들의 10대 취약점들을 발표합니다.\n보통 3년을 주기로 Top 10 리스트를 발표하는데 2017년에도 발표할 예정이라고 합니다.\nTop 10 항목들은 다음과 같습니다.

\n
    \n
  1. injection
  2. \n
  3. Broken auth and session managment
  4. \n
  5. XSS(cross site scripting)
  6. \n
  7. Insecure direct object reference(bad url)
  8. \n
  9. Security misconfiguration(read official secret guideline)
  10. \n
  11. Sensitive data exposure
  12. \n
  13. Missing function-level access control(decorator)
  14. \n
  15. CSRF(Cross site request forgery)
  16. \n
  17. Components with known vulnerabilities(version check!)
  18. \n
  19. Unvalidated redirects
  20. \n
\n

이제 파이썬 웹 어플리케이션에서 이를 어떻게 대응할 수 있는지 알아보겠습니다.

\n
\n

1. SQL injection

\n

SQL 인젝션이란, 사용자가 입력한 값이 개발자가 의도치 않은 db query 결과를 초래하는 것, 또는 그것을 이용한 공격을 말합니다.\n예를 들면, 아래와 같이 user_id를 string format 쿼리로 넣으면 이러한 공격에 취약할 수 있습니다.

\n
@app.route(\"/user/<user_id>\")\ndef show_user(user_id):\n    cur = db.cursor()\n    query = \"SELECT * FROM user_table where user = %s\"%user_id\n    c.execute(query)\n    return c.fetchall()
\n\n
\n

2. Session Management

\n

세션 데이터는 항상 안전하지 않다고 생각해야 합니다. db에 저장되더라도 안전하지 않은건 마찬가지입니다.\n특히 예기치 않은 공격에 대비하기 위해 서버 측에서 세션을 관리 하는 것이 필요합니다.

\n

Flask에서는 Flask-Session이라는 확장 패키지를 통해 이를 쉽게 구현할 수 있습니다.\n특히 PERMANENT_SESSION_LIFETIME 이라는 변수를 통해 일정 시간이 지나면 세션을 자동 파기할 수 있습니다.

\n\n
\n

3. XSS (Cross-site-scripting)

\n

XSS란, 웹페이지에 관리자가 의도하지 않는 스크립트(주로 javascript)를 사용자가 넣을 수 있는 상황을 말합니다.\n예를 들면 비정상적인 페이지가 보이게하여 타 사용자의 사용을 방해하거나 쿠키 및 기타 개인정보를 특정 사이트로 전송하는 등의 문제가 이에 해당합니다.

\n
@app.route('/hi/<user>')\ndef hi(user):\n    return \"<h1>hello, %s!</h1>\"%user\n\n# 위와 같은 간단한 라우팅에서 아래와 같이 공격할 수 있습니다.\n\n# GET /hi/alert(\"hacked!\")\n# <h1> hello, alert(\"hacked!\") </h1>\n# 이걸 본 유저는 javascript alert창이 나타난다
\n\n
\n

4. Insecure Direct Object References

\n

일명 직접 객체 참조, 또는 Bad url은 개발자가 파일, 디렉토리, DB 키와 같은 내부 구현 객체를 참조하는 것을 노출시킬 때 발생합니다.\n아래의 코드를 통해 예를 들어보겠습니다.

\n
# GET /jobs/application/6337\n@app.route(/jobs/application/<job_id>)\ndef find_job(job_id):\n    SELECT * FROM job where id = job_id ...\n\n# 대응방안으로는 flask-login 등을 사용하여 간접 참조하는 방법이 있습니다.\n\nfrom flask.ext.login import login_required, current_user\n\n@app.route(\"/mypage/<id>\")\n@login_required\ndef mypage(id):\n    ...
\n\n
\n

5. Security Misconfiguration

\n

기본으로 제공되는 값은 종종 안전하지 않기 때문에 보안 설정은 정의, 구현 및 유지되어야 합니다.\n대표적으로 코드 난독화 가 이에 해당합니다.

\n

파이썬에서는 Base64 패키지를 통해 Encoding/Decoding 하는 방법이 있습니다.\n더 나아가 pycrypto 패키지를 사용하면 Crypto 모듈을 통해 AES 알고리즘으로 암호화할 수 있습니다.

\n\n
\n

6. Sensitive data exposure

\n

많은 웹 애플리케이션들이 신용카드, 개인 식별 정보 및 인증 정보와 같은 중요한 데이터를 제대로 보호하지 않습니다.\n공격자는 신용카드 사기, 신분 도용 또는 다른 범죄를 수행하는 등 약하게 보호된 데이터를 훔치거나 변경할 수 있습니다.\n따라서, 중요 데이터가 저장 또는 전송 중이거나 브라우저와 교환하는 경우 특별히 주의하여야 하며, 암호화해야 합니다.

\n

REST API에서는 JSON으로 데이터를 통해 통신하기 때문에 JWE (JSON Web Encryption) 를 통해 JSON을 암호화해주는 방법이 있습니다.\n파이썬의 python-jose 또는 PyJWE를 참고하시면 쉽게 구현할 수 있습니다.

\n\n
\n

7. Missing function-level access control

\n

요청에 대해 적절히 확인하지 않을 경우 공격자는 적절한 권한 없이 기능에 접근하기 위한 요청을 위조할 수 있게 됩니다.\n따라서, 관리자 페이지 등에 대하여 유저 권한을 클라이언트가 아닌 서버에서 판단 해야 하며\n역할에 기반한 별도의 인증절차를 요구하도록 만들어야 하고 권한이 없는 유저들은 접근 불가하게 코딩해야 합니다.

\n
@app.route(\"/mypage/<id>\")\n@jwt_required(scope='admin')\ndef mypage(id):\n    ...
\n\n
\n

8. CSRF(Cross site request forgery)

\n

CSRF는 로그인 된 사용자의 웹 애플리케이션에 세션 쿠키와 기타 다른 인증정보를 자동으로 포함하여 위조된 HTTP 요청을 강제로 보내도록 하는 것입니다.\n공격자는 주로 상태(DB, 세션 등)를 변경하는 공격을 합니다. XSS를 방지하면 어느정도 커버되기도 합니다.

\n
<!-- 버튼이나 input을 제출하면 주식이 팔린다! -->\n<form action='/stock/sell' method='get'>\n    <input type=submit value=sell_stock>\n</form>\n<a href=\"/stock/sell/\"> click me!</a>\n<form action='/stock/sell' method='post'>\n    <input type=submit value=sell_stock>\n</form>\n\n<!-- 플라스크에서는 wtf 패키지를 통해 입력 폼을 검증하고 CSRF를 방지가능 -->\n\n<form method=\"POST\" action=\"/\">\n    {{ form.csrf_token }}\n    {{ form.name.label }} {{ form.name(size=20) }}\n    <input type=\"submit\" value=\"Go\">\n</form>
\n\n
\n

9. Components with known vulnerabilities

\n

컴포넌트, 라이브러리, 프레임워크 및 다른 소프트웨어 모듈은 대부분 항상 전체 권한으로 실행되어\n취약한 컴포넌트를 악용하여 공격하는 경우 심각한 데이터 손실이 발생하거나 서버가 장악될 수 있습니다.

\n\n
\n

10. Unvalidated redirects

\n

웹에서 종종 사용자들을 다른 페이지로 리다이렉트 하거나 포워드하기 위해 신뢰할 수 없는 데이터를 사용하는 경우가 많습니다.\n적절한 검증 절차가 없으면 공격자는 피해자를 피싱 또는 악성코드 사이트로 리다이렉트 될 수 있기 때문에 주의해야 합니다.

\n\n
\n

정리 및 관련 링크

\n

저도 그렇고 학생 때는 대부분 보안을 고려하지 않은 웹 어플리케이션을 개발하는 경우가 많은데,\n이 자료가 많은 도움이 되었으면 좋겠습니다!

\n","excerpt":"이 포스팅은 Jacob Kaplan-Moss가 2013년 호주 pycon에서 발표한 자료를 바탕으로 하며, OYT…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":11,"humanPageNumber":12,"skip":67,"limit":6,"numberOfPages":16,"previousPagePath":"/11","nextPagePath":"/13"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/12","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"리눅스 시스템 모니터링 명령어 정리","id":"37b880e0-b8ba-5432-acda-d064142e9195","slug":"system-monitoring","publishDate":"March 24, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

리눅스 시스템 모니터링을 위한 명령어에 대해 정리해보았습니다.

\n
\n

프로세스 모니터링 명령어 - top

\n

\n \n \n \n

\n

top 명령어는 커널을 통하여 관리되는 프로세스들의 정보(메모리 사용률, CPU 사용률, 상태정보 등)를 확인할 수 있는 명령어입니다.\n응용프로그램을 강제로 종료시키고 싶을 때, 실행중인 프로세스를 찾아 kill 명령어를 통해 강제종료시킬 수도 있습니다.

\n

OS X에서는 -o 옵션을 통해, 리눅스에서는 shift + f 명령어를 통해 프로세스를 key에 따라 정렬할 수 있습니다.

\n
\n

시스템 리소스 정보 - vmstat, iostat, sar

\n

\n \n \n \n

\n

vmstat 명령어는 virtual memory statistics 의 줄임말로 가상메모리 등 다양한 리소스 정보를 제공합니다.\nOS X에서는 vm_stat 명령어로, 리눅스에서는 vmstat 명령어로 확인하실 수 있습니다.

\n

\n \n \n \n

\n

iostat 명령어는 sysstat에서 가장 기본적인 명령어로 CPU 및 디스크 입출력에 대한 기본정보를 제공합니다.

\n

\n \n \n \n

\n

sar 명령어는 시스템 활동 모니터링에 유용합니다.\n특히 -r, -f 옵션을 통해 CPU, 메모리 사용률을 날짜, 시간 대 별로 확인할 수 있습니다.

\n
\n

Linux sysstat 패키지 설치

\n

CentOS, Ubuntu에서는 앞서 말씀드린 sar, vmstat 등의 명령어를 사용하기 위해서 sysstat 패키지를 설치해야 합니다.\n아래의 명령어를 통해 설치할 수 있습니다.

\n
// CentOS, Ubuntu\n$ yum install sysstat -y\n$ apt install sysstat -y
\n

만일 권한 오류나 명령어를 찾을 수 없다는 오류가 나타난다면 아래의 설정을 통해 해결할 수 있습니다.

\n
$ sudo vi /etc/default/sysstat\n$ ENABLED=”true”
\n
","excerpt":"리눅스 시스템 모니터링을 위한 명령어에 대해 정리해보았습니다. 프로세스 모니터링 명령어 - top top…"}}}},{"node":{"title":"Jupyter에서 Scala로 Spark 사용하는 방법","id":"b68b3f15-e560-5485-9b60-204947689edd","slug":"jupyter-spark","publishDate":"March 22, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n
$ ssh -R port1:host_name:port2 server_name
\n

이번에는 로컬에서 파이썬 웹 애플리케이션을 개발 중인데 친구에게 보여주고 싶다고 가정 해보겠습니다.\n아직 공개 IP 주소를 제공하지 않기 때문에 인터넷을 통해 직접 기기에 연결할 수 없을 겁니다.\n라우터에서 NAT를 구성하여 해결할 수 있지만 라우터의 구성을 변경해야하므로 번거롭습니다.\n이럴때 Remote port forwarding을 통해 쉽게 해결할 수 있습니다.

\n

먼저 port1의 서버에서 port2로 로컬 트래픽을 전달하는 SSH 터널을 생성합니다.\n이후 로컬에서 port2의 서버에 연결하면 실제로 SSH 터널을 통해 데이터를 요청하는 것을 확인할 수 있습니다.

\n

OSI 7계층에서 생각해보면 SSH는 Application - Transport - Network 계층에 걸쳐있습니다.\nApplication 계층에서 포트를 연결하면 Transport 계층의 TCP 통신을 통해 전달되고,\nNetwork 계층을 통해 목적지로 이동하게 됩니다.

\n
","excerpt":"지금까지 아무 생각없이 SSH를 사용하다가 한번 정리해보았습니다. SSH Protocol SSH는 Secure Shell의 약자입니다. SSH…"}}}},{"node":{"title":"빅데이터 처리에 Scala가 필요한 이유","id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","slug":"scala-for-bigdata","publishDate":"March 17, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}}},{"node":{"title":"AWS EC2 인스턴스 SSH 접속을 위한 초기설정 그리고 주의사항","id":"580383a5-b9d8-5ed6-b06d-0e128a0d1712","slug":"aws-ec2","publishDate":"March 10, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 포스팅의 모든 내용은 OS X에 최적화되어 있습니다.\n그리고, 정리한 내용은 AWS 공식문서에 아주 잘 소개되어 있습니다.

\n
\n

AWS CLI

\n

AWS CLI는 여러 AWS 서비스를 명령줄에서 제어하고 스크립트를 통해 자동화할 수 있는 커멘드라인 인터페이스입니다.\n이를 사용하기 전에 먼저, brew를 통해 awscli를 설치해야 합니다.

\n
$ brew install awscli
\n

설치하고 나면 이제 aws 명령어를 사용할 수 있습니다.\n가장 먼저 configure 명령어를 통해 Access key를 입력해야합니다.

\n
$ aws configure\nAWS Access Key ID [None]: AAAAIOSFODNN7EXAMPLE\nAWS Secret Access Key [None]: wwwwwXUtnFEMI/K7MDENG/bPxRfiCYEXAMKEYKEY\nDefault region name [None]: ap-northeast-2\nDefault output format [None]: json
\n

여기에서 Access Key ID는 [IAM - Security credentials]에서 확인할 수 있습니다.

\n
\n

AWS EC2 접속

\n

EC2 인스턴스를 만들면 .pem이라는 파일을 발급받게 됩니다.\n이 파일은 절대 외부로 유출되면 안되기 때문에 조심해야합니다.\n.pem 파일이 있는 경로로 이동한 다음 아래의 명령어를 통해 접속하면 됩니다.

\n
$ chmod 400 /path/my-key-pair.pem\n$ ssh -i /path/my-key-pair.pem ec2-user@ec2-198-51-100-1.compute-1.amazonaws.com
\n

\"Permission denied\" 에러가 발생하면 -vvv 옵션을 통해 디버깅 할 수 있습니다.\n생성한 인스턴스 OS에 해당하는 유저아이디로 변경해주어야 합니다.\n예를 들어 ubuntu 인스턴스인 경우, ec2-user 대신 ubuntu가 들어갑니다.

\n
\n

AWS를 사용하면서 조심해야할 사항

\n

1.인스턴스 관리

\n

프리티어를 사용하는 경우, 다 사용하고 나서 인스턴스를 항상 꺼주는 습관을 들여 과도한 요금이 과금되지 않도록 해야합니다. 특히 여러 개의 인스턴스를 돌리는 경우 순식간에 청구서가 날아올 수 있습니다.

\n

2. ROOT 계정 사용 자제

\n

많은 경우에 ROOT 계정의 키가 털려서 과금이 발생됩니다. IAM을 통해 별도의 계정을 만들어서 사용하고, GitHub 같은 곳에 설정파일을 올리지 말아야합니다.

\n

3. CloudWatch로 요금 확인

\n

CloudWatch를 통해 Billing Cost가 일정 금액을 넘어가면 메일이나 Slack 메세지로 보내도록 설정해두면 편합니다.

\n

4. 네트워크 확인

\n

네트워크에서 모든 포트를 여는 것도 위험합니다. 이렇게 되면 다양한 공격을 받을 위험이 있습니다.

\n
\n

참고링크

\n\n
","excerpt":"이 포스팅의 모든 내용은 OS X에 최적화되어 있습니다.\n그리고, 정리한 내용은 AWS 공식문서에 아주 잘 소개되어 있습니다. AWS CLI…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":11,"humanPageNumber":12,"skip":67,"limit":6,"numberOfPages":16,"previousPagePath":"/11","nextPagePath":"/13"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/13/page-data.json b/page-data/13/page-data.json index 6429706..e77e66a 100644 --- a/page-data/13/page-data.json +++ b/page-data/13/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-posts-js","path":"/13","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"JWT를 구현하면서 마주치게 되는 고민들","id":"b133bf5e-0c00-5c91-aad3-d3952d26a61c","slug":"implement-jwt","publishDate":"March 03, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

최근 모바일, 웹 등 다양한 환경에서 서버와 통신하면서 많은 사람들이 JWT 토큰 인증 방식을 추천합니다.\n이 포스팅에서는 JWT를 이해하고 구현하면서 마주치게 되는 고민들에 대해 정리해보려 합니다.

\n
\n

JSON Web Token

\n

\"JWT-Token\"

\n

JWT에 대한 소개는 생략하고 Token이 어떻게 구성되어 있는지 간략하게 알아보겠습니다.\nJSON Web Token은 세 파트로 나뉘어지며, 각 파트는 점(.)에 의해 구분됩니다.\n이를 테면 xxxxx.yyyyy.zzzzz 이런식입니다.

\n\n
\n

JWT Process

\n

\"JWT-Diagram\"

\n

일반적으로 JWT 토큰 기반의 인증 시스템은 위와 같은 프로세스로 이루어집니다.\n처음 사용자를 등록할 때 Access token과 Refresh token이 모두 발급되어야 합니다.

\n
    \n
  1. \n

    먼저 사용자가 id와 password를 입력하여 로그인을 시도합니다.

    \n
  2. \n
  3. \n

    서버는 요청을 확인하고 secret key를 통해 Access token을 발급합니다.

    \n
  4. \n
  5. \n

    이후 JWT가 요구되는 API를 요청할 때는\n클라이언트가 Authorization header에 Access token을 담아서 보냅니다.

    \n
  6. \n
  7. \n

    서버는 JWT Signature를 체크하고 Payload로부터 user 정보를 확인해 데이터를 리턴합니다.

    \n
  8. \n
\n
\n

JWT와 기존의 OAuth는 서로 어떤 관계가 있을까?

\n

토큰 기반의 인증 시스템을 처음 접한 사람이라면 저 두 가지 개념이 헷갈릴 수 있습니다.\n먼저, 정답부터 말하자면 JWT는 토큰 유형이고 OAuth는 토큰을 발급하고 인증하는 방법을 설명하는 일종의 프레임워크입니다.\n기존의 /outh/token endpoint에 의해 발급되는 모든 토큰은 일종의 OAuth 프레임워크에 의해 관리된다고 볼 수 있습니다.

\n
{\n\t\"token_type\":\"bearer\",\n\t\"access_token\":\"eyJ0eXAiOiJKV1QiLCJh\",\n\t\"expires_in\":20,\n\t\"refresh_token\":\"fdb8fdbecf1d03ce5e6125c067733c0d51de209c\"\n}
\n

위의 토큰이 기존 OAuth에서 주로 사용하는 bearer 기반의 토큰 방식입니다.\n다만 JWT는 토큰 자체에 유저 정보를 담아서 HTTP 헤더로 전달하기 때문에\n유저 세션을 유지할 필요가 없고 가볍게 데이터를 주고받을 수 있다는 장점이 있습니다.

\n
\n

Access Token과 Refresh Token을 어디에 저장해야 할까?

\n

앞서 말한것처럼 기본적으로 두 가지 토큰을 사용합니다.\nAPI 요청을 허가하는데 Access Token을 사용하고, 액세스 토큰이 만료된 후 새로운 액세스 토큰을 얻기 위해 Refresh Token을 사용합니다.

\n

![access_token](/assets/images/access token.png)

\n

Access Token은 리소스에 직접 접근할 수 있도록 해주는 정보만을 가지고 있습니다. 즉, 클라이언트는 Access Token이 있어야 서버 자원에 접근할 수 있습니다. Access Token은 짧은 수명을 가지며, 만료기간을 갖습니다. 주로 세션 에 담아서 관리합니다.

\n

![refresh_token](/assets/images/refresh token.png)

\n

Refresh Token은 새로운 Access Token을 발급받기 위한 정보를 갖습니다. 즉, 클라이언트가 Access Token이 없거나 만료되었다면 Refresh Token을 통해 Auth Server에 요청해서 발급받을 수 있습니다. Refresh Token 또한 만료기간이 있지만 깁니다. Refresh Token은 중요하기 때문에 외부에 노출되지 않도록 엄격하게 관리해야 하므로 주로 데이터베이스 에 저장합니다.

\n

토큰이 안전하게 관리되는지 여부는 어떻게 구현하느냐에 달려있습니다. 보통은 Access Token에 대해 직접적으로 인증(direct authorization) 체크합니다. 무슨 말이냐 하면, Access Token이 서버 자원에 접근하려고 하면 서버가 토큰에 있는 정보를 읽어 스스로 인증여부를 결정합니다. 반면에, Refresh Token은 Auth Server에 대한 체크가 필요합니다. 이때 다음과 같은 세 가지 사항을 고려해야 합니다.

\n\n
\n

Sliding sessions

\n

Sliding sessions은 일정 기간 사용하지 않으면 만료되는 세션입니다. 이 세션은 refresh token과 access token을 통해 구현할 수 있습니다. 먼저, 사용자가 작업을 수행하려하면 새 access token이 발급됩니다. 반면, 사용자가 만료된 access token을 사용하려 하면 세션은 비활성화되며 새 access token을 요청합니다. 이 상황에서 refresh token을 통해 새로운 토큰을 발급받을 지는 개발 팀이 결정하기에 따라 다릅니다.

\n
\n

토큰 재발급 로직에 대한 고민

\n

JWT를 쓴다면 만료시간을 꼭 명시적으로 두도록 하고 중간마다 토큰을 재발행하도록 권장하는 것이 대부분입니다.\n리프레시 관련해서 정확한 내용이 정해져 있는 것은 아니지만 일반적인 API를 보면\n최초 발급시 Access Token과 Refresh Token 2개를 발급하고 Access Token으로 API를 사용하다가\n만료시간이 지나면 만료시간을 길게 준 Refresh Token을 이용해서 Access Token을 다시 발급합니다.

\n

가장 일반적인 방법은 클라이언트가 토큰의 만료시간을 알 수 있기 때문에, 클라이언트에서 판단해서 만료시간이 넘었으면 토큰 재발급을 요청하는 방법입니다.\n좀 더 쉽게 구현하는 방법은 TokenExpiredError가 발생했을 때 재발급을 해주는 것입니다.

\n

만료된 토큰을 통해 접근하려고 하면 다음과 같은 오류가 나타나게 합니다.

\n
{\n\t\"code\":401,\n\t\"error\":\"invalid_token\",\n\t\"error_description\":\"The access token provided has expired.\"\n}
\n
\n

기타, 참고자료

\n

Flask에는 Flask-JWT라는 패키지가 있지만 Scope나 Refresh Token에 대한 구현이 부족하기 때문에\npyjwt를 통해 직접 구현하는 방법을 추천합니다.

\n

아래는 JWT에 대하여 가장 많이 도움이 되는 페이지입니다.\n들어가서 가입하면 JWT Handbook 이라는 EBook도 무료로 제공합니다. https://jwt.io/

\n","excerpt":"최근 모바일, 웹 등 다양한 환경에서 서버와 통신하면서 많은 사람들이 JWT 토큰 인증 방식을 추천합니다.\n이 포스팅에서는 JWT…"}}}},{"node":{"title":"HTTPS와 SSL 인증서, 그리고 SHA1 알고리즘","id":"d28c5614-a493-58ce-8f25-1193f44511db","slug":"https-ssl","publishDate":"March 01, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

크롬 업데이트 이후 몇몇 웹 페이지는 브라우저 주소 창에 안전하지 않음이 나타나고,\n아니면 자물쇠 모양에 안전함이 표시되는 것을 볼 수 있습니다.\n오늘은 그 두 가지 방식에 어떤 차이가 있는지, HTTPS와 SSL 인증서에 대해 알아보고\n최근 구글과 리누스 토발즈의 언급으로 이슈화 되고 있는 SHA1 암호화 알고리즘에 대해 정리해보려 합니다.

\n
\n

HTTP와 HTTPS

\n

우선 간단히 웹 통신규약에 대해 설명하자면, HTTP는 Hypertext 인 HTML을 전송하기 위한 통신규약 을 의미합니다.\nHTTP는 암호화되지 않은 방법으로 데이터를 전송하기 때문에 서버와 클라이언트가 주고 받는 메시지를 확인하는 것이 매우 쉽습니다.\n심각한 예를 들면, 로그인을 위해서 서버로 비밀번호를 전송하거나 중요한 기밀 문서를 열람하는 과정에서 악의적인 감청이나 데이터의 위변조 등이 일어날 수 있습니다.

\n

이를 보완하기 위해 나온 것이 보안이 강화된 HTTPS 입니다.\nHTTPS는 보안을 강화하기 위해 통신에서 일반 텍스트를 이용하지 않고 SSL이나 TLS 프로토콜 을 통해 세션 데이터를 암호화합니다.\n이를 통해 데이터의 보안을 더 강화할 수 있지만 전적으로 웹 브라우저에서의 구현 정확도와 서버 소프트웨어, 지원하는 암호화 알고리즘에 달려있습니다.

\n

결국 크롬에 안전하지 않음으로 표기되는 페이지는 보안 인증서가 없는 페이지입니다.\n반면에, 안전함으로 표기되는 페이지는 기관으로부터 인증서를 받은 페이지입니다.\n실제로 인터넷 익스플로러의 인터넷 옵션을 확인해보면 CA 라는 탭에서 인증서를 확인하실 수 있습니다.

\n
\n

SSL 통신 과정

\n

그렇다면 HTTPS에 포함된 SSL 프로토콜이 어떤 과정을 통해 동작하는지 간단히 알아보겠습니다.\n다시 설명하자면, SSL 인증서는 클라이언트와 서버간의 통신을 제3자가 보증해주는 전자화된 문서 라고 보시면 됩니다.\n처음 클라이언트와 서버가 데이터를 주고 받기 위해서는 준비과정이 필요합니다. 이를 핸드쉐이크(HandShake) 라고 부릅니다.

\n

\n 라는 방법을 선보이면서,\nSHA1에 의존하는 기술은 믿음직하지 않다는 점을 보여줬습니다.

\n

여기서 섀터드 기법을 소개한 연구자들은 Git이 SHA1에 의존하기 때문에 안전하지 않다 고 말했습니다.\n이에 리누스 토발즈는 \"깃은 데이터를 해시하기만 하는 게 아니라, 거기에 타입과 길이 필드를 측량한다\" 고 말하면서\n당장은 하늘이 무너지는게 아니니 상관없다고 답변했습니다.

\n

물론 당장은 아니지만 불안함에 많은 기관들이 SHA1 방식에서 SHA2 또는 SHA256 으로 옮겨가고 있습니다.\n대표적으로 크롬, 파이어폭스 등의 브라우저들이 SHA1 인증서 퇴출에 노력하고 있고, 인터넷뱅킹이나 비트코인 같은 경우 SHA256 방식을 사용합니다.

\n

리누스 토발즈의 글 : https://plus.google.com/+LinusTorvalds/posts/7tp2gYWQugL

","excerpt":"…"}}}},{"node":{"title":"Pandas DataFrame을 병렬처리 하는 방법","id":"99925524-39d0-5943-982f-79148d6dbe29","slug":"pandas-parallel","publishDate":"February 27, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}}},{"node":{"title":"Pandas DataFrame을 MySQL에 저장하는 방법","id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}}},{"node":{"title":"Swagger로 API 문서화하기","id":"c6a4715e-1458-5370-bd23-1fcf178bc237","slug":"swagger-api-doc","publishDate":"February 25, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

개발에 있어서 API 문서화는 아주 중요하지만 번거로운 일 중에 하나입니다.\n특히 pdf 또는 한글 파일로 관리하고 있다면 갱신할때마다 아주 번거롭습니다.

\n

이를 해결하기 위해 자칭 세계에서 가장 유명한 API 프레임워크인 Swagger 를 사용해보겠습니다.\nSwagger 공식 홈페이지를 보면 많은 정보가 있습니다.

\n
\n

Swagger UI

\n

Swagger UI를 사용하면 웹 브라우저를 통해 Swagger에 정의된 REST API를 시각화하고 테스트할 수 있습니다.\n내장된 테스트 기능을 사용하면 GUI로 API를 테스트하며 결과를 확인할 수 있습니다.\n자세한 정보는 GitHub: Swagger UI 를 참고하시면 됩니다.

\n
\n

Swagger Codegen

\n

Swagger Codegen을 사용하면 REST API용 Swagger 문서를 통해 다양한 언어로 SDK를 생성할 수 있습니다.\n생성된 SDK를 사용하여 API를 완전히 구현하기 전에 생성된 샘플 서버 구현에서 실시간으로 API를 테스트할 수 있습니다.\n자세한 정보는 GitHub: Swagger Codegen 를 참조하시면 됩니다.

\n
\n

Swagger Editor

\n

editor.swagger.io 에서 Swagger Editor, Swagger UI 및 Swagger Codegen을 제공합니다.\n최대한 간편한 용도로 사용하시겠다면 yaml 이나 json 파일을 이곳에 import 해서 사용하셔도 됩니다.

\n

이외에도 http://bigstickcarpet.com/swagger-parser/www/index.html\nSwagger Validator와 Parser가 있습니다.

\n
\n

Docker로 Swagger UI를 적용해보자

\n

앞서 설명하자면 Docker로 실행하는 이유는 \"아직 맥북에 npm 환경구축이 안되어서\" 입니다.\nnpm install 을 통해 빌드하셔도 됩니다.

\n

1.

\n

먼저 로컬에 Swagger UI GitHub 저장소를 clone 해줍니다.

\n

2.

\n

그 다음 Docker를 실행하고 아래의 명령어를 통해 Dockerfile을 빌드합니다.\nlocalhost:80 에 접속하면 Swagger-UI가 실행된 것을 볼 수 있습니다.

\n
docker build -t swagger-ui-builder .\ndocker run -p 80:8080 swagger-ui-builder
\n

3.

\n

이제 Swagger Editor로 문서를 작성하고 yaml 형식으로 다운로드 받고, 파일을 Swagger/dist/로 이동합니다.\n그리고 Swagger/dist/index.html 파일의 url을 swagger.yaml로 수정합니다.

\n
$(function () {\n   var url = window.location.search.match(/url=([^&]+)/);\n   if (url && url.length > 1) {\n     url = decodeURIComponent(url[1]);\n   } else {\n     url = \"swagger.yaml\";\n   }
\n

이제 웹 서버에 올리기만 하면 API 문서를 쉽게 확인할 수 있습니다!

\n
","excerpt":"개발에 있어서 API 문서화는 아주 중요하지만 번거로운 일 중에 하나입니다.\n특히 pdf…"}}}},{"node":{"title":"충돌을 해결하기 위한 git stash 명령어","id":"1e87f085-c0c1-515e-9204-1768ba7dda62","slug":"git-stash","publishDate":"February 23, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

git을 사용하다보면 여러 변경내역이 생기게 됩니다.\n예를 들면 내 로컬에서 변경된 내역을 아직 commit을 하지 않은 상태로 pull을 하게 되면,\n충돌이 발생하게 되어 초보자에게는 난감한 상황이 됩니다.\n이런 경우에 git stash 명령어를 사용하시면 편리합니다.

\n

git stash 명령어는 unstaged 상태인 변경사항을 일시적으로 백업하고 워킹디렉토리를 깨끗한 상태로 유지합니다.\n즉, 일종의 책갈피 역할을 한다고 보시면 됩니다.

\n
\n

1. git stash

\n
$ git stash\nSaved working directory and index state \\\n  \"WIP on master: 049d078 added the index file\"\nHEAD is now at 049d078 added the index file\n(To restore them type \"git stash apply\")
\n

git stash 명령어를 실행하면 작업 중인 파일을 새로운 Stash에 저장합니다.

\n
\n

2. git stash list

\n
$ git stash list\nstash@{0}: WIP on master: 049d078 added the index file\nstash@{1}: WIP on master: c264051 Revert \"added file_size\"\nstash@{2}: WIP on master: 21d80a5 added number to log
\n

git stash list 명령어를 통해 저장된 책갈피들의 리스트를 볼 수 있습니다.

\n
\n

3. git stash apply

\n
$ git stash apply\n# On branch master\n# Changes not staged for commit:\n#   (use \"git add <file>...\" to update what will be committed)\n#\n#      modified:   index.html\n#      modified:   lib/simplegit.rb\n#
\n

git stash apply 명령어를 사용하면 저장된 stash를 적용할 수 있습니다.

\n
\n

4. git stash drop

\n
$ git stash drop stash@{0}\nDropped stash@{0} (364e91f3f268f0900bc3ee613f9f733e82aaed43)
\n

apply 명령어로 stash를 적용한다고 해서 스택에서 사라지는게 아닙니다.\ngit stash drop 명령어를 통해 스택에서 삭제할 수 있습니다.

\n
","excerpt":"git을 사용하다보면 여러 변경내역이 생기게 됩니다.\n예를 들면 내 로컬에서 변경된 내역을 아직 commit을 하지 않은 상태로 pull…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":12,"humanPageNumber":13,"skip":73,"limit":6,"numberOfPages":16,"previousPagePath":"/12","nextPagePath":"/14"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/13","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"파이썬 웹 어플리케이션 보안 점검 가이드","id":"51fa9f19-22b3-5955-a571-ee02d8251a78","slug":"flask-security","publishDate":"March 08, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

이 포스팅은 Jacob Kaplan-Moss가 2013년 호주 pycon에서 발표한 자료를 바탕으로 하며, OYT님이 최신화하여 정리해주신 자료를 참고하였습니다.

\n
\n

OWASP Top 10

\n

먼저, OWASP(The Open Web Application Security Project)는 오픈소스 웹 애플리케이션 보안 프로젝트로, 주로 웹에 관한 정보노출, 악성 파일 및 스크립트, 보안 취약점 등을 연구하며,\n웹 애플리케이션의 취약점 중에서 빈도가 많이 발생하고, 보안상 영향을 크게 줄 수 있는 것들의 10대 취약점들을 발표합니다.\n보통 3년을 주기로 Top 10 리스트를 발표하는데 2017년에도 발표할 예정이라고 합니다.\nTop 10 항목들은 다음과 같습니다.

\n
    \n
  1. injection
  2. \n
  3. Broken auth and session managment
  4. \n
  5. XSS(cross site scripting)
  6. \n
  7. Insecure direct object reference(bad url)
  8. \n
  9. Security misconfiguration(read official secret guideline)
  10. \n
  11. Sensitive data exposure
  12. \n
  13. Missing function-level access control(decorator)
  14. \n
  15. CSRF(Cross site request forgery)
  16. \n
  17. Components with known vulnerabilities(version check!)
  18. \n
  19. Unvalidated redirects
  20. \n
\n

이제 파이썬 웹 어플리케이션에서 이를 어떻게 대응할 수 있는지 알아보겠습니다.

\n
\n

1. SQL injection

\n

SQL 인젝션이란, 사용자가 입력한 값이 개발자가 의도치 않은 db query 결과를 초래하는 것, 또는 그것을 이용한 공격을 말합니다.\n예를 들면, 아래와 같이 user_id를 string format 쿼리로 넣으면 이러한 공격에 취약할 수 있습니다.

\n
@app.route(\"/user/<user_id>\")\ndef show_user(user_id):\n    cur = db.cursor()\n    query = \"SELECT * FROM user_table where user = %s\"%user_id\n    c.execute(query)\n    return c.fetchall()
\n\n
\n

2. Session Management

\n

세션 데이터는 항상 안전하지 않다고 생각해야 합니다. db에 저장되더라도 안전하지 않은건 마찬가지입니다.\n특히 예기치 않은 공격에 대비하기 위해 서버 측에서 세션을 관리 하는 것이 필요합니다.

\n

Flask에서는 Flask-Session이라는 확장 패키지를 통해 이를 쉽게 구현할 수 있습니다.\n특히 PERMANENT_SESSION_LIFETIME 이라는 변수를 통해 일정 시간이 지나면 세션을 자동 파기할 수 있습니다.

\n\n
\n

3. XSS (Cross-site-scripting)

\n

XSS란, 웹페이지에 관리자가 의도하지 않는 스크립트(주로 javascript)를 사용자가 넣을 수 있는 상황을 말합니다.\n예를 들면 비정상적인 페이지가 보이게하여 타 사용자의 사용을 방해하거나 쿠키 및 기타 개인정보를 특정 사이트로 전송하는 등의 문제가 이에 해당합니다.

\n
@app.route('/hi/<user>')\ndef hi(user):\n    return \"<h1>hello, %s!</h1>\"%user\n\n# 위와 같은 간단한 라우팅에서 아래와 같이 공격할 수 있습니다.\n\n# GET /hi/alert(\"hacked!\")\n# <h1> hello, alert(\"hacked!\") </h1>\n# 이걸 본 유저는 javascript alert창이 나타난다
\n\n
\n

4. Insecure Direct Object References

\n

일명 직접 객체 참조, 또는 Bad url은 개발자가 파일, 디렉토리, DB 키와 같은 내부 구현 객체를 참조하는 것을 노출시킬 때 발생합니다.\n아래의 코드를 통해 예를 들어보겠습니다.

\n
# GET /jobs/application/6337\n@app.route(/jobs/application/<job_id>)\ndef find_job(job_id):\n    SELECT * FROM job where id = job_id ...\n\n# 대응방안으로는 flask-login 등을 사용하여 간접 참조하는 방법이 있습니다.\n\nfrom flask.ext.login import login_required, current_user\n\n@app.route(\"/mypage/<id>\")\n@login_required\ndef mypage(id):\n    ...
\n\n
\n

5. Security Misconfiguration

\n

기본으로 제공되는 값은 종종 안전하지 않기 때문에 보안 설정은 정의, 구현 및 유지되어야 합니다.\n대표적으로 코드 난독화 가 이에 해당합니다.

\n

파이썬에서는 Base64 패키지를 통해 Encoding/Decoding 하는 방법이 있습니다.\n더 나아가 pycrypto 패키지를 사용하면 Crypto 모듈을 통해 AES 알고리즘으로 암호화할 수 있습니다.

\n\n
\n

6. Sensitive data exposure

\n

많은 웹 애플리케이션들이 신용카드, 개인 식별 정보 및 인증 정보와 같은 중요한 데이터를 제대로 보호하지 않습니다.\n공격자는 신용카드 사기, 신분 도용 또는 다른 범죄를 수행하는 등 약하게 보호된 데이터를 훔치거나 변경할 수 있습니다.\n따라서, 중요 데이터가 저장 또는 전송 중이거나 브라우저와 교환하는 경우 특별히 주의하여야 하며, 암호화해야 합니다.

\n

REST API에서는 JSON으로 데이터를 통해 통신하기 때문에 JWE (JSON Web Encryption) 를 통해 JSON을 암호화해주는 방법이 있습니다.\n파이썬의 python-jose 또는 PyJWE를 참고하시면 쉽게 구현할 수 있습니다.

\n\n
\n

7. Missing function-level access control

\n

요청에 대해 적절히 확인하지 않을 경우 공격자는 적절한 권한 없이 기능에 접근하기 위한 요청을 위조할 수 있게 됩니다.\n따라서, 관리자 페이지 등에 대하여 유저 권한을 클라이언트가 아닌 서버에서 판단 해야 하며\n역할에 기반한 별도의 인증절차를 요구하도록 만들어야 하고 권한이 없는 유저들은 접근 불가하게 코딩해야 합니다.

\n
@app.route(\"/mypage/<id>\")\n@jwt_required(scope='admin')\ndef mypage(id):\n    ...
\n\n
\n

8. CSRF(Cross site request forgery)

\n

CSRF는 로그인 된 사용자의 웹 애플리케이션에 세션 쿠키와 기타 다른 인증정보를 자동으로 포함하여 위조된 HTTP 요청을 강제로 보내도록 하는 것입니다.\n공격자는 주로 상태(DB, 세션 등)를 변경하는 공격을 합니다. XSS를 방지하면 어느정도 커버되기도 합니다.

\n
<!-- 버튼이나 input을 제출하면 주식이 팔린다! -->\n<form action='/stock/sell' method='get'>\n    <input type=submit value=sell_stock>\n</form>\n<a href=\"/stock/sell/\"> click me!</a>\n<form action='/stock/sell' method='post'>\n    <input type=submit value=sell_stock>\n</form>\n\n<!-- 플라스크에서는 wtf 패키지를 통해 입력 폼을 검증하고 CSRF를 방지가능 -->\n\n<form method=\"POST\" action=\"/\">\n    {{ form.csrf_token }}\n    {{ form.name.label }} {{ form.name(size=20) }}\n    <input type=\"submit\" value=\"Go\">\n</form>
\n\n
\n

9. Components with known vulnerabilities

\n

컴포넌트, 라이브러리, 프레임워크 및 다른 소프트웨어 모듈은 대부분 항상 전체 권한으로 실행되어\n취약한 컴포넌트를 악용하여 공격하는 경우 심각한 데이터 손실이 발생하거나 서버가 장악될 수 있습니다.

\n\n
\n

10. Unvalidated redirects

\n

웹에서 종종 사용자들을 다른 페이지로 리다이렉트 하거나 포워드하기 위해 신뢰할 수 없는 데이터를 사용하는 경우가 많습니다.\n적절한 검증 절차가 없으면 공격자는 피해자를 피싱 또는 악성코드 사이트로 리다이렉트 될 수 있기 때문에 주의해야 합니다.

\n\n
\n

정리 및 관련 링크

\n

저도 그렇고 학생 때는 대부분 보안을 고려하지 않은 웹 어플리케이션을 개발하는 경우가 많은데,\n이 자료가 많은 도움이 되었으면 좋겠습니다!

\n","excerpt":"이 포스팅은 Jacob Kaplan-Moss가 2013년 호주 pycon에서 발표한 자료를 바탕으로 하며, OYT…"}}}},{"node":{"title":"JWT를 구현하면서 마주치게 되는 고민들","id":"b133bf5e-0c00-5c91-aad3-d3952d26a61c","slug":"implement-jwt","publishDate":"March 03, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

최근 모바일, 웹 등 다양한 환경에서 서버와 통신하면서 많은 사람들이 JWT 토큰 인증 방식을 추천합니다.\n이 포스팅에서는 JWT를 이해하고 구현하면서 마주치게 되는 고민들에 대해 정리해보려 합니다.

\n
\n

JSON Web Token

\n

\"JWT-Token\"

\n

JWT에 대한 소개는 생략하고 Token이 어떻게 구성되어 있는지 간략하게 알아보겠습니다.\nJSON Web Token은 세 파트로 나뉘어지며, 각 파트는 점(.)에 의해 구분됩니다.\n이를 테면 xxxxx.yyyyy.zzzzz 이런식입니다.

\n\n
\n

JWT Process

\n

\"JWT-Diagram\"

\n

일반적으로 JWT 토큰 기반의 인증 시스템은 위와 같은 프로세스로 이루어집니다.\n처음 사용자를 등록할 때 Access token과 Refresh token이 모두 발급되어야 합니다.

\n
    \n
  1. \n

    먼저 사용자가 id와 password를 입력하여 로그인을 시도합니다.

    \n
  2. \n
  3. \n

    서버는 요청을 확인하고 secret key를 통해 Access token을 발급합니다.

    \n
  4. \n
  5. \n

    이후 JWT가 요구되는 API를 요청할 때는\n클라이언트가 Authorization header에 Access token을 담아서 보냅니다.

    \n
  6. \n
  7. \n

    서버는 JWT Signature를 체크하고 Payload로부터 user 정보를 확인해 데이터를 리턴합니다.

    \n
  8. \n
\n
\n

JWT와 기존의 OAuth는 서로 어떤 관계가 있을까?

\n

토큰 기반의 인증 시스템을 처음 접한 사람이라면 저 두 가지 개념이 헷갈릴 수 있습니다.\n먼저, 정답부터 말하자면 JWT는 토큰 유형이고 OAuth는 토큰을 발급하고 인증하는 방법을 설명하는 일종의 프레임워크입니다.\n기존의 /outh/token endpoint에 의해 발급되는 모든 토큰은 일종의 OAuth 프레임워크에 의해 관리된다고 볼 수 있습니다.

\n
{\n\t\"token_type\":\"bearer\",\n\t\"access_token\":\"eyJ0eXAiOiJKV1QiLCJh\",\n\t\"expires_in\":20,\n\t\"refresh_token\":\"fdb8fdbecf1d03ce5e6125c067733c0d51de209c\"\n}
\n

위의 토큰이 기존 OAuth에서 주로 사용하는 bearer 기반의 토큰 방식입니다.\n다만 JWT는 토큰 자체에 유저 정보를 담아서 HTTP 헤더로 전달하기 때문에\n유저 세션을 유지할 필요가 없고 가볍게 데이터를 주고받을 수 있다는 장점이 있습니다.

\n
\n

Access Token과 Refresh Token을 어디에 저장해야 할까?

\n

앞서 말한것처럼 기본적으로 두 가지 토큰을 사용합니다.\nAPI 요청을 허가하는데 Access Token을 사용하고, 액세스 토큰이 만료된 후 새로운 액세스 토큰을 얻기 위해 Refresh Token을 사용합니다.

\n

![access_token](/assets/images/access token.png)

\n

Access Token은 리소스에 직접 접근할 수 있도록 해주는 정보만을 가지고 있습니다. 즉, 클라이언트는 Access Token이 있어야 서버 자원에 접근할 수 있습니다. Access Token은 짧은 수명을 가지며, 만료기간을 갖습니다. 주로 세션 에 담아서 관리합니다.

\n

![refresh_token](/assets/images/refresh token.png)

\n

Refresh Token은 새로운 Access Token을 발급받기 위한 정보를 갖습니다. 즉, 클라이언트가 Access Token이 없거나 만료되었다면 Refresh Token을 통해 Auth Server에 요청해서 발급받을 수 있습니다. Refresh Token 또한 만료기간이 있지만 깁니다. Refresh Token은 중요하기 때문에 외부에 노출되지 않도록 엄격하게 관리해야 하므로 주로 데이터베이스 에 저장합니다.

\n

토큰이 안전하게 관리되는지 여부는 어떻게 구현하느냐에 달려있습니다. 보통은 Access Token에 대해 직접적으로 인증(direct authorization) 체크합니다. 무슨 말이냐 하면, Access Token이 서버 자원에 접근하려고 하면 서버가 토큰에 있는 정보를 읽어 스스로 인증여부를 결정합니다. 반면에, Refresh Token은 Auth Server에 대한 체크가 필요합니다. 이때 다음과 같은 세 가지 사항을 고려해야 합니다.

\n\n
\n

Sliding sessions

\n

Sliding sessions은 일정 기간 사용하지 않으면 만료되는 세션입니다. 이 세션은 refresh token과 access token을 통해 구현할 수 있습니다. 먼저, 사용자가 작업을 수행하려하면 새 access token이 발급됩니다. 반면, 사용자가 만료된 access token을 사용하려 하면 세션은 비활성화되며 새 access token을 요청합니다. 이 상황에서 refresh token을 통해 새로운 토큰을 발급받을 지는 개발 팀이 결정하기에 따라 다릅니다.

\n
\n

토큰 재발급 로직에 대한 고민

\n

JWT를 쓴다면 만료시간을 꼭 명시적으로 두도록 하고 중간마다 토큰을 재발행하도록 권장하는 것이 대부분입니다.\n리프레시 관련해서 정확한 내용이 정해져 있는 것은 아니지만 일반적인 API를 보면\n최초 발급시 Access Token과 Refresh Token 2개를 발급하고 Access Token으로 API를 사용하다가\n만료시간이 지나면 만료시간을 길게 준 Refresh Token을 이용해서 Access Token을 다시 발급합니다.

\n

가장 일반적인 방법은 클라이언트가 토큰의 만료시간을 알 수 있기 때문에, 클라이언트에서 판단해서 만료시간이 넘었으면 토큰 재발급을 요청하는 방법입니다.\n좀 더 쉽게 구현하는 방법은 TokenExpiredError가 발생했을 때 재발급을 해주는 것입니다.

\n

만료된 토큰을 통해 접근하려고 하면 다음과 같은 오류가 나타나게 합니다.

\n
{\n\t\"code\":401,\n\t\"error\":\"invalid_token\",\n\t\"error_description\":\"The access token provided has expired.\"\n}
\n
\n

기타, 참고자료

\n

Flask에는 Flask-JWT라는 패키지가 있지만 Scope나 Refresh Token에 대한 구현이 부족하기 때문에\npyjwt를 통해 직접 구현하는 방법을 추천합니다.

\n

아래는 JWT에 대하여 가장 많이 도움이 되는 페이지입니다.\n들어가서 가입하면 JWT Handbook 이라는 EBook도 무료로 제공합니다. https://jwt.io/

\n","excerpt":"최근 모바일, 웹 등 다양한 환경에서 서버와 통신하면서 많은 사람들이 JWT 토큰 인증 방식을 추천합니다.\n이 포스팅에서는 JWT…"}}}},{"node":{"title":"HTTPS와 SSL 인증서, 그리고 SHA1 알고리즘","id":"d28c5614-a493-58ce-8f25-1193f44511db","slug":"https-ssl","publishDate":"March 01, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

크롬 업데이트 이후 몇몇 웹 페이지는 브라우저 주소 창에 안전하지 않음이 나타나고,\n아니면 자물쇠 모양에 안전함이 표시되는 것을 볼 수 있습니다.\n오늘은 그 두 가지 방식에 어떤 차이가 있는지, HTTPS와 SSL 인증서에 대해 알아보고\n최근 구글과 리누스 토발즈의 언급으로 이슈화 되고 있는 SHA1 암호화 알고리즘에 대해 정리해보려 합니다.

\n
\n

HTTP와 HTTPS

\n

우선 간단히 웹 통신규약에 대해 설명하자면, HTTP는 Hypertext 인 HTML을 전송하기 위한 통신규약 을 의미합니다.\nHTTP는 암호화되지 않은 방법으로 데이터를 전송하기 때문에 서버와 클라이언트가 주고 받는 메시지를 확인하는 것이 매우 쉽습니다.\n심각한 예를 들면, 로그인을 위해서 서버로 비밀번호를 전송하거나 중요한 기밀 문서를 열람하는 과정에서 악의적인 감청이나 데이터의 위변조 등이 일어날 수 있습니다.

\n

이를 보완하기 위해 나온 것이 보안이 강화된 HTTPS 입니다.\nHTTPS는 보안을 강화하기 위해 통신에서 일반 텍스트를 이용하지 않고 SSL이나 TLS 프로토콜 을 통해 세션 데이터를 암호화합니다.\n이를 통해 데이터의 보안을 더 강화할 수 있지만 전적으로 웹 브라우저에서의 구현 정확도와 서버 소프트웨어, 지원하는 암호화 알고리즘에 달려있습니다.

\n

결국 크롬에 안전하지 않음으로 표기되는 페이지는 보안 인증서가 없는 페이지입니다.\n반면에, 안전함으로 표기되는 페이지는 기관으로부터 인증서를 받은 페이지입니다.\n실제로 인터넷 익스플로러의 인터넷 옵션을 확인해보면 CA 라는 탭에서 인증서를 확인하실 수 있습니다.

\n
\n

SSL 통신 과정

\n

그렇다면 HTTPS에 포함된 SSL 프로토콜이 어떤 과정을 통해 동작하는지 간단히 알아보겠습니다.\n다시 설명하자면, SSL 인증서는 클라이언트와 서버간의 통신을 제3자가 보증해주는 전자화된 문서 라고 보시면 됩니다.\n처음 클라이언트와 서버가 데이터를 주고 받기 위해서는 준비과정이 필요합니다. 이를 핸드쉐이크(HandShake) 라고 부릅니다.

\n

\n 라는 방법을 선보이면서,\nSHA1에 의존하는 기술은 믿음직하지 않다는 점을 보여줬습니다.

\n

여기서 섀터드 기법을 소개한 연구자들은 Git이 SHA1에 의존하기 때문에 안전하지 않다 고 말했습니다.\n이에 리누스 토발즈는 \"깃은 데이터를 해시하기만 하는 게 아니라, 거기에 타입과 길이 필드를 측량한다\" 고 말하면서\n당장은 하늘이 무너지는게 아니니 상관없다고 답변했습니다.

\n

물론 당장은 아니지만 불안함에 많은 기관들이 SHA1 방식에서 SHA2 또는 SHA256 으로 옮겨가고 있습니다.\n대표적으로 크롬, 파이어폭스 등의 브라우저들이 SHA1 인증서 퇴출에 노력하고 있고, 인터넷뱅킹이나 비트코인 같은 경우 SHA256 방식을 사용합니다.

\n

리누스 토발즈의 글 : https://plus.google.com/+LinusTorvalds/posts/7tp2gYWQugL

","excerpt":"…"}}}},{"node":{"title":"Pandas DataFrame을 병렬처리 하는 방법","id":"99925524-39d0-5943-982f-79148d6dbe29","slug":"pandas-parallel","publishDate":"February 27, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}}},{"node":{"title":"Pandas DataFrame을 MySQL에 저장하는 방법","id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}}},{"node":{"title":"Swagger로 API 문서화하기","id":"c6a4715e-1458-5370-bd23-1fcf178bc237","slug":"swagger-api-doc","publishDate":"February 25, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

개발에 있어서 API 문서화는 아주 중요하지만 번거로운 일 중에 하나입니다.\n특히 pdf 또는 한글 파일로 관리하고 있다면 갱신할때마다 아주 번거롭습니다.

\n

이를 해결하기 위해 자칭 세계에서 가장 유명한 API 프레임워크인 Swagger 를 사용해보겠습니다.\nSwagger 공식 홈페이지를 보면 많은 정보가 있습니다.

\n
\n

Swagger UI

\n

Swagger UI를 사용하면 웹 브라우저를 통해 Swagger에 정의된 REST API를 시각화하고 테스트할 수 있습니다.\n내장된 테스트 기능을 사용하면 GUI로 API를 테스트하며 결과를 확인할 수 있습니다.\n자세한 정보는 GitHub: Swagger UI 를 참고하시면 됩니다.

\n
\n

Swagger Codegen

\n

Swagger Codegen을 사용하면 REST API용 Swagger 문서를 통해 다양한 언어로 SDK를 생성할 수 있습니다.\n생성된 SDK를 사용하여 API를 완전히 구현하기 전에 생성된 샘플 서버 구현에서 실시간으로 API를 테스트할 수 있습니다.\n자세한 정보는 GitHub: Swagger Codegen 를 참조하시면 됩니다.

\n
\n

Swagger Editor

\n

editor.swagger.io 에서 Swagger Editor, Swagger UI 및 Swagger Codegen을 제공합니다.\n최대한 간편한 용도로 사용하시겠다면 yaml 이나 json 파일을 이곳에 import 해서 사용하셔도 됩니다.

\n

이외에도 http://bigstickcarpet.com/swagger-parser/www/index.html\nSwagger Validator와 Parser가 있습니다.

\n
\n

Docker로 Swagger UI를 적용해보자

\n

앞서 설명하자면 Docker로 실행하는 이유는 \"아직 맥북에 npm 환경구축이 안되어서\" 입니다.\nnpm install 을 통해 빌드하셔도 됩니다.

\n

1.

\n

먼저 로컬에 Swagger UI GitHub 저장소를 clone 해줍니다.

\n

2.

\n

그 다음 Docker를 실행하고 아래의 명령어를 통해 Dockerfile을 빌드합니다.\nlocalhost:80 에 접속하면 Swagger-UI가 실행된 것을 볼 수 있습니다.

\n
docker build -t swagger-ui-builder .\ndocker run -p 80:8080 swagger-ui-builder
\n

3.

\n

이제 Swagger Editor로 문서를 작성하고 yaml 형식으로 다운로드 받고, 파일을 Swagger/dist/로 이동합니다.\n그리고 Swagger/dist/index.html 파일의 url을 swagger.yaml로 수정합니다.

\n
$(function () {\n   var url = window.location.search.match(/url=([^&]+)/);\n   if (url && url.length > 1) {\n     url = decodeURIComponent(url[1]);\n   } else {\n     url = \"swagger.yaml\";\n   }
\n

이제 웹 서버에 올리기만 하면 API 문서를 쉽게 확인할 수 있습니다!

\n
","excerpt":"개발에 있어서 API 문서화는 아주 중요하지만 번거로운 일 중에 하나입니다.\n특히 pdf…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":12,"humanPageNumber":13,"skip":73,"limit":6,"numberOfPages":16,"previousPagePath":"/12","nextPagePath":"/14"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/14/page-data.json b/page-data/14/page-data.json index 75561f7..178d69a 100644 --- a/page-data/14/page-data.json +++ b/page-data/14/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-posts-js","path":"/14","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Docker, DockerHub 명령어 정리","id":"e8250779-02a0-5f91-b5a2-48d573ee9c63","slug":"docker-command","publishDate":"February 22, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

\n \n \n \n

\n

명령어를 정리하기 전에 Docker 시스템 아키텍처를 보면 이해하기 쉽습니다.\nDocker는 크게 클라이언트-서버 아키텍처 로 구성되어 있습니다.\n그림처럼 우리가 명령어를 입력하면 클라이언트는 데몬과 통신하고 데몬이 컨테이너를 빌드, 실행합니다.

\n

DockerHub는 도커 컨테이너를 관리하고 공유하기 위한 SAAS로\nGitHub 처럼 이미지를 올려서 공유하거나 내려받을 수 있습니다.

\n

지난 번 포스팅에 이어서\n이제 Docker 명령어를 정리하고, 더 나아가 DockerHub에 이미지를 올리는 방법까지 알아보겠습니다.

\n
\n

상태 확인하기

\n
docker ps\ndocker images
\n

docker ps는 실행 중인 컨테이너 목록을 확인할 때 사용합니다.\n-a 옵션을 사용하면 전체 목록을 확인할 수 있습니다.\ndocker images는 설치된 이미지 목록을 확인할 때 사용합니다.

\n
\n

이미지 받아오기

\n
docker search nginx\ndocker pull nginx
\n

다음은 Dockerhub로 부터 이미지를 받아오기 위한 명령어입니다.\ndocker search [image]로 이미지를 검색할 수 있습니다.\ndocker pull [image]을 사용하여 이미지를 받아올 수 있습니다.

\n
\n

컨테이너 실행하기

\n
docker run -d -p 80:80 --name webserver nginx
\n

docker run [image] 명령어를 통해 컨테이너를 실행할 수 있습니다.\n-p 옵션을 통해 포트를 지정할 수 있고, -d 옵션을 통해 백그라운드로 실행시킬 수 있습니다.\n그리고 --name을 통해 컨테이너 이름을 지정할 수 있습니다.

\n
\n

컨테이너 중지/재시작하기

\n
docker stop webserver\ndocker restart webserver\ndocker start webserver
\n

docker stop/restart/start [container] 명령어를 통해 컨테이너를 중지/재시작/시작할 수 있습니다.

\n
\n

컨테이너/이미지 삭제하기

\n
docker rm -f webserver\ndocker rmi webserver
\n

docker rm -f [container] 명령어를 통해 컨테이너를 삭제할 수 있습니다.\ndocker rmi [image] 명령어를 통해 이미지를 삭제할 수 있습니다.

\n
\n

DockerHub 관련 명령어

\n
docker build [PATH]\ndocker commit\ndocker push
\n

DockerHub를 사용하기 위해서는 먼저 로그인이 되어 있어야 합니다.\n아이디가 없다면, https://hub.docker.com/에서 가입하시면 됩니다.\n이후 docker login 명령어를 통해 연결할 수 있습니다.

\n

docker build [PATH]는 지정된 경로에 Dockerfile로 이미지를 만드는 명령어 입니다.\ndocker commit 명령어를 통해 변경사항을 저장할 수 있습니다.\ndocker push 명령어를 통해 DockerHub 저장소에 이미지를 올릴 수 있습니다.

\n
\n

이외에도 자주 사용하는 명령어

\n
docker history\ndocker inspect\ndocker cp [PATH]
\n

docker history [container/image] 명령어를 통해 히스토리를 확인할 수 있습니다.\ndocker inspect [container/image] 명령어를 통해 상세정보를 확인할 수 있습니다.\ndocker cp [PATH] 명령어를 통해 파일을 지정한 경로로 꺼내올 수 있습니다.

","excerpt":"명령어를 정리하기 전에 Docker 시스템 아키텍처를 보면 이해하기 쉽습니다.\nDocker…"}}}},{"node":{"title":"Docker 간편한 설치부터 실행까지","id":"582e6123-2ca0-54f7-95d6-81d9875a0e5d","slug":"docker-install","publishDate":"February 21, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Docker는 오픈소스 컨테이너입니다. 기존의 가상머신과 비슷하면서도 훨씬 가벼운 형태라고 볼 수 있습니다. 그렇다면 VMWare, VirtualBox와 같은 기존의 가상머신과 Docker Container가 어떻게 다른지 살펴보겠습니다.

\n

\n \n \n \n

\n

Docker Toolbox를 사용하는 경우, 위와 같이 /usr/local/bin 폴더에 docker, docker-compose, docker-machine이 설치됩니다. 그리고 가상화는 VirtualBox를 통해 이루어지게 됩니다.

\n
\n

\n \n \n \n

\n

어플리케이션 데몬을 실행시키면 이제 docker 명령어를 사용할 수 있게 됩니다.

\n
docker version\ndocker info
\n

이제 한번 테스트 해볼 시간입니다.\n아래의 명령어를 통해 nginx 이미지를 만들고 80번 포트에 웹 서버를 띄워 보겠습니다.

\n
docker run -d -p 80:80 --name webserver nginx
\n

\n \n \n \n

\n

실행되고 있는 웹 서버를 중지하고 컨테이너를 삭제해보겠습니다.

\n
docker ps\ndocker stop webserver\ndocker rm -f webserver
\n

아직 이미지는 남아있는 상태입니다. 이미지까지 삭제해줍니다.

\n
docker images\ndocker rmi nginx
\n

자세한 Docker 사용법이나 명령어는 다음에 정리하도록 하겠습니다.

","excerpt":"Docker는 오픈소스 컨테이너입니다. 기존의 가상머신과 비슷하면서도 훨씬 가벼운 형태라고 볼 수 있습니다. 그렇다면 VMWare…"}}}},{"node":{"title":"자주 사용하는 리눅스 명령어 정리 (3) - Screen","id":"d22345cb-f866-5727-b127-bab4dda44c2b","slug":"linux3","publishDate":"February 18, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Screen은 여러 프로세스 간에 물리적 콘솔을 다중화하는데 사용할 수있는 전체 화면 소프트웨어 프로그램이다.\n하나의 단일 터미널 창 관리자에서 여러 개의 개별 터미널 인스턴스를 열 수 있는 사용자를 제공한다.

\n

사실 다중 터미널이 필요한거라면 tmux나 iTerm이 더 편하다고 생각한다.\n하지만, 스크린은 서버에서 백드라운드 데몬을 돌려야 할 때 아주 유용하다.

\n
screen -S pingsession -d -m -L ping localhost
\n

이 명령은 화면에 새로운 세션 (-m)을 만들고, 출력 (-L)을 기록하고, 즉시 분리 (-d) 명령이 실행되도록 지시한다.\n로그는 현재 디렉토리의 screenlog.n 에 기록된다.

\n

여기서 n은 화면 세션의 \"창\" 번호이다. 로깅은 정기적으로 버퍼링되고 플러시되며 로그 파일을 기록 할 수 있다.\n화면 세션은 프로세스 제어, 즉 실행중인 데몬 중지 등을 지원한다.\n이를 수행하기 위해 화면 세션은 세션 이름 (-S 세션 이름)으로 시작되어야하며 나중에 이름과 함께 종료 될 수 있다.

\n
\n

Screen 명령어 정리

\n","excerpt":"Screen…"}}}},{"node":{"title":"자주 사용하는 리눅스 명령어 정리 (2) - 쉘 스크립트","id":"d77889e4-2d46-52ff-9be0-629112a3a33e","slug":"linux2","publishDate":"February 16, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

맥북을 사용하면서 가장 좋았던 점은 기본 운영체제가 유닉스 계열이다보니,\n모든 것이 커멘드라인으로 해결된다는 점이었다. 특히 쉘 스크립트를 활용하면 간단한 자동화도 구현할 수 있다.\n따라서, 이번 포스팅에서는 자동화를 위한 쉘 스크립트 문법을 정리해보려 한다.

\n
\n

쉘 스크립트란?

\n

문법에 대해 알기 이전에 쉘 스크립트가 어떤 역할을 하는지 알아야 한다.\n기본적으로 우리가 사용하는 운영체제는 하드웨어 제어, CPU 스케줄링 등 많은 역할을 수행한다.\n쉘은 운영체제 위에서 다양한 운영 체제 기능과 서비스를 구현하는 인터페이스를 제공하는 프로그램이다.\n즉, 사용자와 맞닿아 있기 때문에 우리는 쉘의 명령어를 통해 직접 조작할 수 있는 것이다.

\n

아래는 쉘 스크립트와 관련된 기본 명령어이다.

\n\n
\n

변수의 기본

\n\n
\n

연산자

\n\n
\n

if-else 문

\n
if [ case ]; then\n  true\nelse\n  false\nfi
\n
\n

case 문

\n
case $answer in\n  yes)\n  \techo \"yes\"\n  no)\n  \techo \"no\"\nesac
\n
\n

for-in 문

\n
for fname in $(ls .sh); do\n  echo \"fname\"\ndone
\n
\n

while 문

\n
while [ 1 ]; do\n  echo \"fname\"\ndone
\n
","excerpt":"…"}}}},{"node":{"title":"자주 사용하는 리눅스 명령어 정리 (1) - 기본 명령어","id":"2eeccb84-3a8f-56e9-b5e4-7130159ab8a0","slug":"linux1","publishDate":"February 15, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAAABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAADTAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJDAAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAEFBP/EACIQAAICAgIABwAAAAAAAAAAAAECABEDBBIhQVFSYqHB0f/EABUBAQEAAAAAAAAAAAAAAAAAAAIE/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oADAMBAAIRAxEAPwBnZyBeRZVPlzr6mc7mVspLMle7LY+KkobTN2QOuoxkJBYEihdeEoTbVlN4KKLoTfrP7CQTvOOioMIdhZX/2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

리눅스는 리누스 토발즈가 1991년 처음 개발을 시작한 오픈소스 소프트웨어이다.\n보통 윈도우를 오래 사용하다보면 터미널보다 GUI에 익숙해지기 마련이다.\n하지만 최근 맥북으로 갈아타면서 커멘드라인이 편하다는 걸 알게 되었고,\n앞으로 좀 더 생산성을 높이기 위해 몇 가지 유용한 명령어들을 정리해보려 한다.

\n

생활코딩에 리눅스에 대해 잘 정리한 강의가 있어 참고하면 좋다.\nhttps://opentutorials.org/course/2598

\n
\n

패키지 매니저

\n

리눅스는 패키지 매니저를 통해 설치되어 있지 않은 프로그램을 설치한다.\n맥에서 사용하는 Brew를 떠올리면 이해하기 쉽다.

\n\n

위와 같이 리눅스 배포판에 따라 패키지 매니저가 조금씩 다르지만, 사용법은 대체로 비슷한 편이다.\n예를 들어 패키지를 설치할 때는 apt-get install \"package name\" 이런 식이다.\n모든 패키지 매니저가 설치/업데이트/삭제 명령어를 가지고 있으며,\n설치된 패키지를 관리하기 위한 명령어도 존재한다.

\n
\n

alias 명령어

\n

한번 설정해놓으면 이것만큼 편한게 없다.\n바로 예시를 드는게 더 이해하기 편할거 같다.

\n

예를 들어, 서버의 원격주소로 매일 접속해야 하는 상황이라고 가정해보자.\n보통은 매번 ssh username@address -p port 이런식으로 입력해야 할 것이다.\n하지만, alias를 설정해놓으면 커스텀 명령어로 지정하여 간단히 접속할 수 있다.

\n
    \n
  1. 먼저 ~/.bashrc로 들어간다. (zsh를 사용한다면, ~/.zshrc로 들어가자)
  2. \n
  3. alias login = 'ssh username@address -p port' 한 줄을 추가한다.
  4. \n
  5. source ~/.bashrc로 업데이트 해준다.
  6. \n
\n

이후에는 접속할 때 login 이라는 명령어만 입력하면 된다.

\n
\n

명령어 순차실행과 파이프라인

\n

사용하다보면 여러 명령어를 연속적으로 실행해야하는 경우가 많다.

\n

이럴 때는 Sequence와 Pipeline 개념을 알아두면 편하다.\n예를 들어, commit과 push 명령어를 연속적으로 실행하고 싶다고 가정해보자.

\n
git add -A;git push
\n

위와 같이 중간에 세미콜론만 추가하면 된다.

\n

이번에는 실행중인 특정 프로세스 번호를 찾아야 한다고 가정해보자.\n처음이라면 ps -ef 로 프로세스를 직접 확인할 것이다.\n하지만 파이프라인과 grep 명령어를 사용한다면 다음과 같이 한줄로 끝난다.

\n
ps -ef | grep process_name
\n
\n

백그라운드 실행 - nohup

\n

어떤 작업을 백그라운드로 실행을 하면 별도의 창으로 켜놓지 않아도\n하나의 프로세스로 계속 돌아가는 것을 확인할 수 있다.

\n
nohup name &
\n

리눅스에서는 nohup 이라는 명령어로 실행할 수 있다.\n실행하고 나면 nohup.out 이라는 파일이 생기는데\ncat 명령어로 확인해보면 로그가 찍혀있는 것을 볼 수 있다.\n실행중지 시킬 때는 kill 명령어로 프로세스를 죽이면 된다.

\n
\n

스케줄링을 통한 주기적인 실행 - cron, crontab

\n

crontab은 일종의 리눅스 작업 스케줄러이다.\n이 명령어를 사용하면 특정 시간에 내가 원하는 특정 명령어나 스크립트를 실행시킬 수 있다.\n보통 주기적인 크롤링에 사용하기도 한다.

\n
* * * * * /root/script.sh
\n

이렇게 설정하면 1분마다 script.sh를 실행한다.\n앞의 별 다섯개는 순서대로 \"분,시,일,월,요일\"을 뜻한다.\n내가 실행중인 스케줄러를 관리하기 위한 명령어는 다음과 같다.

\n","excerpt":"리눅스는 리누스 토발즈가 1991년 처음 개발을 시작한 오픈소스 소프트웨어이다.\n보통 윈도우를 오래 사용하다보면 터미널보다 GUI…"}}}},{"node":{"title":"Jupyter Notebook 외부접속 설정하기","id":"79c1215f-bb79-5e21-b334-04fb090a7956","slug":"jupyter-config","publishDate":"February 12, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":13,"humanPageNumber":14,"skip":79,"limit":6,"numberOfPages":16,"previousPagePath":"/13","nextPagePath":"/15"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/14","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"충돌을 해결하기 위한 git stash 명령어","id":"1e87f085-c0c1-515e-9204-1768ba7dda62","slug":"git-stash","publishDate":"February 23, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

git을 사용하다보면 여러 변경내역이 생기게 됩니다.\n예를 들면 내 로컬에서 변경된 내역을 아직 commit을 하지 않은 상태로 pull을 하게 되면,\n충돌이 발생하게 되어 초보자에게는 난감한 상황이 됩니다.\n이런 경우에 git stash 명령어를 사용하시면 편리합니다.

\n

git stash 명령어는 unstaged 상태인 변경사항을 일시적으로 백업하고 워킹디렉토리를 깨끗한 상태로 유지합니다.\n즉, 일종의 책갈피 역할을 한다고 보시면 됩니다.

\n
\n

1. git stash

\n
$ git stash\nSaved working directory and index state \\\n  \"WIP on master: 049d078 added the index file\"\nHEAD is now at 049d078 added the index file\n(To restore them type \"git stash apply\")
\n

git stash 명령어를 실행하면 작업 중인 파일을 새로운 Stash에 저장합니다.

\n
\n

2. git stash list

\n
$ git stash list\nstash@{0}: WIP on master: 049d078 added the index file\nstash@{1}: WIP on master: c264051 Revert \"added file_size\"\nstash@{2}: WIP on master: 21d80a5 added number to log
\n

git stash list 명령어를 통해 저장된 책갈피들의 리스트를 볼 수 있습니다.

\n
\n

3. git stash apply

\n
$ git stash apply\n# On branch master\n# Changes not staged for commit:\n#   (use \"git add <file>...\" to update what will be committed)\n#\n#      modified:   index.html\n#      modified:   lib/simplegit.rb\n#
\n

git stash apply 명령어를 사용하면 저장된 stash를 적용할 수 있습니다.

\n
\n

4. git stash drop

\n
$ git stash drop stash@{0}\nDropped stash@{0} (364e91f3f268f0900bc3ee613f9f733e82aaed43)
\n

apply 명령어로 stash를 적용한다고 해서 스택에서 사라지는게 아닙니다.\ngit stash drop 명령어를 통해 스택에서 삭제할 수 있습니다.

\n
","excerpt":"git을 사용하다보면 여러 변경내역이 생기게 됩니다.\n예를 들면 내 로컬에서 변경된 내역을 아직 commit을 하지 않은 상태로 pull…"}}}},{"node":{"title":"Docker, DockerHub 명령어 정리","id":"e8250779-02a0-5f91-b5a2-48d573ee9c63","slug":"docker-command","publishDate":"February 22, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

\n \n \n \n

\n

명령어를 정리하기 전에 Docker 시스템 아키텍처를 보면 이해하기 쉽습니다.\nDocker는 크게 클라이언트-서버 아키텍처 로 구성되어 있습니다.\n그림처럼 우리가 명령어를 입력하면 클라이언트는 데몬과 통신하고 데몬이 컨테이너를 빌드, 실행합니다.

\n

DockerHub는 도커 컨테이너를 관리하고 공유하기 위한 SAAS로\nGitHub 처럼 이미지를 올려서 공유하거나 내려받을 수 있습니다.

\n

지난 번 포스팅에 이어서\n이제 Docker 명령어를 정리하고, 더 나아가 DockerHub에 이미지를 올리는 방법까지 알아보겠습니다.

\n
\n

상태 확인하기

\n
docker ps\ndocker images
\n

docker ps는 실행 중인 컨테이너 목록을 확인할 때 사용합니다.\n-a 옵션을 사용하면 전체 목록을 확인할 수 있습니다.\ndocker images는 설치된 이미지 목록을 확인할 때 사용합니다.

\n
\n

이미지 받아오기

\n
docker search nginx\ndocker pull nginx
\n

다음은 Dockerhub로 부터 이미지를 받아오기 위한 명령어입니다.\ndocker search [image]로 이미지를 검색할 수 있습니다.\ndocker pull [image]을 사용하여 이미지를 받아올 수 있습니다.

\n
\n

컨테이너 실행하기

\n
docker run -d -p 80:80 --name webserver nginx
\n

docker run [image] 명령어를 통해 컨테이너를 실행할 수 있습니다.\n-p 옵션을 통해 포트를 지정할 수 있고, -d 옵션을 통해 백그라운드로 실행시킬 수 있습니다.\n그리고 --name을 통해 컨테이너 이름을 지정할 수 있습니다.

\n
\n

컨테이너 중지/재시작하기

\n
docker stop webserver\ndocker restart webserver\ndocker start webserver
\n

docker stop/restart/start [container] 명령어를 통해 컨테이너를 중지/재시작/시작할 수 있습니다.

\n
\n

컨테이너/이미지 삭제하기

\n
docker rm -f webserver\ndocker rmi webserver
\n

docker rm -f [container] 명령어를 통해 컨테이너를 삭제할 수 있습니다.\ndocker rmi [image] 명령어를 통해 이미지를 삭제할 수 있습니다.

\n
\n

DockerHub 관련 명령어

\n
docker build [PATH]\ndocker commit\ndocker push
\n

DockerHub를 사용하기 위해서는 먼저 로그인이 되어 있어야 합니다.\n아이디가 없다면, https://hub.docker.com/에서 가입하시면 됩니다.\n이후 docker login 명령어를 통해 연결할 수 있습니다.

\n

docker build [PATH]는 지정된 경로에 Dockerfile로 이미지를 만드는 명령어 입니다.\ndocker commit 명령어를 통해 변경사항을 저장할 수 있습니다.\ndocker push 명령어를 통해 DockerHub 저장소에 이미지를 올릴 수 있습니다.

\n
\n

이외에도 자주 사용하는 명령어

\n
docker history\ndocker inspect\ndocker cp [PATH]
\n

docker history [container/image] 명령어를 통해 히스토리를 확인할 수 있습니다.\ndocker inspect [container/image] 명령어를 통해 상세정보를 확인할 수 있습니다.\ndocker cp [PATH] 명령어를 통해 파일을 지정한 경로로 꺼내올 수 있습니다.

","excerpt":"명령어를 정리하기 전에 Docker 시스템 아키텍처를 보면 이해하기 쉽습니다.\nDocker…"}}}},{"node":{"title":"Docker 간편한 설치부터 실행까지","id":"582e6123-2ca0-54f7-95d6-81d9875a0e5d","slug":"docker-install","publishDate":"February 21, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAAABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAADTAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJDAAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAEFBP/EACIQAAICAgIABwAAAAAAAAAAAAECABEDBBIhQVFSYqHB0f/EABUBAQEAAAAAAAAAAAAAAAAAAAIE/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oADAMBAAIRAxEAPwBnZyBeRZVPlzr6mc7mVspLMle7LY+KkobTN2QOuoxkJBYEihdeEoTbVlN4KKLoTfrP7CQTvOOioMIdhZX/2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Docker는 오픈소스 컨테이너입니다. 기존의 가상머신과 비슷하면서도 훨씬 가벼운 형태라고 볼 수 있습니다. 그렇다면 VMWare, VirtualBox와 같은 기존의 가상머신과 Docker Container가 어떻게 다른지 살펴보겠습니다.

\n

\n \n \n \n

\n

Docker Toolbox를 사용하는 경우, 위와 같이 /usr/local/bin 폴더에 docker, docker-compose, docker-machine이 설치됩니다. 그리고 가상화는 VirtualBox를 통해 이루어지게 됩니다.

\n
\n

\n \n \n \n

\n

어플리케이션 데몬을 실행시키면 이제 docker 명령어를 사용할 수 있게 됩니다.

\n
docker version\ndocker info
\n

이제 한번 테스트 해볼 시간입니다.\n아래의 명령어를 통해 nginx 이미지를 만들고 80번 포트에 웹 서버를 띄워 보겠습니다.

\n
docker run -d -p 80:80 --name webserver nginx
\n

\n \n \n \n

\n

실행되고 있는 웹 서버를 중지하고 컨테이너를 삭제해보겠습니다.

\n
docker ps\ndocker stop webserver\ndocker rm -f webserver
\n

아직 이미지는 남아있는 상태입니다. 이미지까지 삭제해줍니다.

\n
docker images\ndocker rmi nginx
\n

자세한 Docker 사용법이나 명령어는 다음에 정리하도록 하겠습니다.

","excerpt":"Docker는 오픈소스 컨테이너입니다. 기존의 가상머신과 비슷하면서도 훨씬 가벼운 형태라고 볼 수 있습니다. 그렇다면 VMWare…"}}}},{"node":{"title":"자주 사용하는 리눅스 명령어 정리 (3) - Screen","id":"d22345cb-f866-5727-b127-bab4dda44c2b","slug":"linux3","publishDate":"February 18, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Screen은 여러 프로세스 간에 물리적 콘솔을 다중화하는데 사용할 수있는 전체 화면 소프트웨어 프로그램이다.\n하나의 단일 터미널 창 관리자에서 여러 개의 개별 터미널 인스턴스를 열 수 있는 사용자를 제공한다.

\n

사실 다중 터미널이 필요한거라면 tmux나 iTerm이 더 편하다고 생각한다.\n하지만, 스크린은 서버에서 백드라운드 데몬을 돌려야 할 때 아주 유용하다.

\n
screen -S pingsession -d -m -L ping localhost
\n

이 명령은 화면에 새로운 세션 (-m)을 만들고, 출력 (-L)을 기록하고, 즉시 분리 (-d) 명령이 실행되도록 지시한다.\n로그는 현재 디렉토리의 screenlog.n 에 기록된다.

\n

여기서 n은 화면 세션의 \"창\" 번호이다. 로깅은 정기적으로 버퍼링되고 플러시되며 로그 파일을 기록 할 수 있다.\n화면 세션은 프로세스 제어, 즉 실행중인 데몬 중지 등을 지원한다.\n이를 수행하기 위해 화면 세션은 세션 이름 (-S 세션 이름)으로 시작되어야하며 나중에 이름과 함께 종료 될 수 있다.

\n
\n

Screen 명령어 정리

\n","excerpt":"Screen…"}}}},{"node":{"title":"자주 사용하는 리눅스 명령어 정리 (2) - 쉘 스크립트","id":"d77889e4-2d46-52ff-9be0-629112a3a33e","slug":"linux2","publishDate":"February 16, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

맥북을 사용하면서 가장 좋았던 점은 기본 운영체제가 유닉스 계열이다보니,\n모든 것이 커멘드라인으로 해결된다는 점이었다. 특히 쉘 스크립트를 활용하면 간단한 자동화도 구현할 수 있다.\n따라서, 이번 포스팅에서는 자동화를 위한 쉘 스크립트 문법을 정리해보려 한다.

\n
\n

쉘 스크립트란?

\n

문법에 대해 알기 이전에 쉘 스크립트가 어떤 역할을 하는지 알아야 한다.\n기본적으로 우리가 사용하는 운영체제는 하드웨어 제어, CPU 스케줄링 등 많은 역할을 수행한다.\n쉘은 운영체제 위에서 다양한 운영 체제 기능과 서비스를 구현하는 인터페이스를 제공하는 프로그램이다.\n즉, 사용자와 맞닿아 있기 때문에 우리는 쉘의 명령어를 통해 직접 조작할 수 있는 것이다.

\n

아래는 쉘 스크립트와 관련된 기본 명령어이다.

\n\n
\n

변수의 기본

\n\n
\n

연산자

\n\n
\n

if-else 문

\n
if [ case ]; then\n  true\nelse\n  false\nfi
\n
\n

case 문

\n
case $answer in\n  yes)\n  \techo \"yes\"\n  no)\n  \techo \"no\"\nesac
\n
\n

for-in 문

\n
for fname in $(ls .sh); do\n  echo \"fname\"\ndone
\n
\n

while 문

\n
while [ 1 ]; do\n  echo \"fname\"\ndone
\n
","excerpt":"…"}}}},{"node":{"title":"자주 사용하는 리눅스 명령어 정리 (1) - 기본 명령어","id":"2eeccb84-3a8f-56e9-b5e4-7130159ab8a0","slug":"linux1","publishDate":"February 15, 2017","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

리눅스는 리누스 토발즈가 1991년 처음 개발을 시작한 오픈소스 소프트웨어이다.\n보통 윈도우를 오래 사용하다보면 터미널보다 GUI에 익숙해지기 마련이다.\n하지만 최근 맥북으로 갈아타면서 커멘드라인이 편하다는 걸 알게 되었고,\n앞으로 좀 더 생산성을 높이기 위해 몇 가지 유용한 명령어들을 정리해보려 한다.

\n

생활코딩에 리눅스에 대해 잘 정리한 강의가 있어 참고하면 좋다.\nhttps://opentutorials.org/course/2598

\n
\n

패키지 매니저

\n

리눅스는 패키지 매니저를 통해 설치되어 있지 않은 프로그램을 설치한다.\n맥에서 사용하는 Brew를 떠올리면 이해하기 쉽다.

\n\n

위와 같이 리눅스 배포판에 따라 패키지 매니저가 조금씩 다르지만, 사용법은 대체로 비슷한 편이다.\n예를 들어 패키지를 설치할 때는 apt-get install \"package name\" 이런 식이다.\n모든 패키지 매니저가 설치/업데이트/삭제 명령어를 가지고 있으며,\n설치된 패키지를 관리하기 위한 명령어도 존재한다.

\n
\n

alias 명령어

\n

한번 설정해놓으면 이것만큼 편한게 없다.\n바로 예시를 드는게 더 이해하기 편할거 같다.

\n

예를 들어, 서버의 원격주소로 매일 접속해야 하는 상황이라고 가정해보자.\n보통은 매번 ssh username@address -p port 이런식으로 입력해야 할 것이다.\n하지만, alias를 설정해놓으면 커스텀 명령어로 지정하여 간단히 접속할 수 있다.

\n
    \n
  1. 먼저 ~/.bashrc로 들어간다. (zsh를 사용한다면, ~/.zshrc로 들어가자)
  2. \n
  3. alias login = 'ssh username@address -p port' 한 줄을 추가한다.
  4. \n
  5. source ~/.bashrc로 업데이트 해준다.
  6. \n
\n

이후에는 접속할 때 login 이라는 명령어만 입력하면 된다.

\n
\n

명령어 순차실행과 파이프라인

\n

사용하다보면 여러 명령어를 연속적으로 실행해야하는 경우가 많다.

\n

이럴 때는 Sequence와 Pipeline 개념을 알아두면 편하다.\n예를 들어, commit과 push 명령어를 연속적으로 실행하고 싶다고 가정해보자.

\n
git add -A;git push
\n

위와 같이 중간에 세미콜론만 추가하면 된다.

\n

이번에는 실행중인 특정 프로세스 번호를 찾아야 한다고 가정해보자.\n처음이라면 ps -ef 로 프로세스를 직접 확인할 것이다.\n하지만 파이프라인과 grep 명령어를 사용한다면 다음과 같이 한줄로 끝난다.

\n
ps -ef | grep process_name
\n
\n

백그라운드 실행 - nohup

\n

어떤 작업을 백그라운드로 실행을 하면 별도의 창으로 켜놓지 않아도\n하나의 프로세스로 계속 돌아가는 것을 확인할 수 있다.

\n
nohup name &
\n

리눅스에서는 nohup 이라는 명령어로 실행할 수 있다.\n실행하고 나면 nohup.out 이라는 파일이 생기는데\ncat 명령어로 확인해보면 로그가 찍혀있는 것을 볼 수 있다.\n실행중지 시킬 때는 kill 명령어로 프로세스를 죽이면 된다.

\n
\n

스케줄링을 통한 주기적인 실행 - cron, crontab

\n

crontab은 일종의 리눅스 작업 스케줄러이다.\n이 명령어를 사용하면 특정 시간에 내가 원하는 특정 명령어나 스크립트를 실행시킬 수 있다.\n보통 주기적인 크롤링에 사용하기도 한다.

\n
* * * * * /root/script.sh
\n

이렇게 설정하면 1분마다 script.sh를 실행한다.\n앞의 별 다섯개는 순서대로 \"분,시,일,월,요일\"을 뜻한다.\n내가 실행중인 스케줄러를 관리하기 위한 명령어는 다음과 같다.

\n","excerpt":"리눅스는 리누스 토발즈가 1991년 처음 개발을 시작한 오픈소스 소프트웨어이다.\n보통 윈도우를 오래 사용하다보면 터미널보다 GUI…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":13,"humanPageNumber":14,"skip":79,"limit":6,"numberOfPages":16,"previousPagePath":"/13","nextPagePath":"/15"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/15/page-data.json b/page-data/15/page-data.json index 7ab9470..ca3890f 100644 --- a/page-data/15/page-data.json +++ b/page-data/15/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-posts-js","path":"/15","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"DecisionTree와 RandomForest에 대하여","id":"06ab8a86-5c79-565e-8a20-7665fe5adee3","slug":"decision-randomforest","publishDate":"February 10, 2017","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

의사결정트리 (DecisionTree)

\n

의사결정나무는 다양한 의사결정 경로와 결과를 나타내는데 트리 구조를 사용합니다. 보통 어렸을 때의 스무고개 놀이를 예로 드는 경우가 많습니다.

\n

\"img\"

\n
\n

위의 그림은 타이타닉 생존자를 찾는 의사결정트리 모델입니다. 첫번째 뿌리 노드를 보면 성별 <= 0.5 라고 되어있는데 이는 남자냐? 여자냐? 라고 질문하는 것과 같습니다.

\n

최종적으로, 모든 승객에 대한 분류(Classification)를 통해 생존확률을 예측할 수 있게 됩니다.

\n

이처럼, 숫자형 결과를 반환하는 것을 회귀나무(Regression Tree) 라고 하며, 범주형 결과를 반환하는 것을 분류나무(Classification Tree) 라고 합니다. 의사결정트리를 만들기 위해서는 먼저 어떤 질문을 할 것인지, 어떤 순서로 질문을 할 것인지 정해야 합니다.

\n

가장 좋은 방법은 예측하려는 대상에 대해 가장 많은 정보를 담고 있는 질문을 고르는 것이 좋습니다. 이러한 '얼마만큼의 정보를 담고 있는가'를 엔트로피(entropy) 라고 합니다. 엔트로피는 보통 데이터의 불확실성(?)을 나타내며, 결국 엔트로피가 클 수록 데이터 정보가 잘 분포되어 있기 때문에 좋은 지표라고 예상할 수 있습니다.

\n

그림과 같이 의사결정트리는 이해하고 해석하기 쉽다는 장점이 있습니다. 또한 예측할 때 사용하는 프로세스가 명백하며, 숫자형 / 범주형 데이터를 동시에 다룰 수 있습니다. 그리고 특정 변수의 값이 누락되어도 사용할 수 있습니다.

\n

하지만 최적의 의사결정트리를 찾는 것이 정말 어려운 문제입니다. (어떤 것들을 조건(Feature)으로 넣어야 할지, 깊이(Depth)는 얼마로 정해야 할지…) 그리고 의사결정트리의 단점은 새로운 데이터에 대한 일반화 성능이 좋지 않게 오버피팅(Overfitting)되기 쉽다는 것입니다.

\n

잠깐 오버피팅에 대해 설명하자면, 오버피팅이란 Supervised Learning에서 과거의 학습한 데이터에 대해서는 잘 예측하지만 새로 들어온 데이터에 대해서 성능이 떨어지는 경우를 말합니다. 즉, 학습 데이터에 지나치게 최적화되어 일반화가 어렵다는 말입니다. 이러한 오버피팅을 방지할 수 있는 대표적인 방법 중 하나가 바로 앙상블 기법을 적용한 랜덤포레스트(Random Forest) 입니다.

\n
\n

랜덤포레스트 (RandomForest)

\n

랜덤포레스트는 위에서 말한 것과 같이 의사결정트리를 이용해 만들어진 알고리즘입니다.

\n
\n

랜덤포레스트는 분류, 회귀 분석 등에 사용되는 앙상블 학습 방법의 일종으로, 

\n

훈련 과정에서 구성한 다수의 결정 트리로부터 분류 또는 평균 예측치를 출력함으로써 동작한다.

\n
\n
\n

즉, 랜덤포레스트란 여러 개의 의사결정트리를 만들고, 투표를 시켜 다수결로 결과를 결정하는 방법입니다.

\n
\n

\"img\"

\n

위의 그림은 고작 몇 십개의 트리노드가 있지만 실제로는 수 백개에서 수 만개까지 노드가 생성될 수 있습니다. 이렇게 여러 개의 트리를 통해 투표를 해서 오버피팅이 생길 경우에 대비할 수 있습니다.

\n

그런데 보통 구축한 트리에는 랜덤성이 없는데 어떻게하면 랜덤하게 트리를 얻을 수 있나? 라는 의문이 듭니다. 랜덤포레스트에서는 데이터를 bootstrap 해서 포레스트를 구성합니다.

\n

bootstrap aggregating 또는 begging 이라고 하는데, 전체 데이터를 전부 이용해서 학습시키는 것이 아니라 샘플의 결과물을 각 트리의 입력 값으로 넣어 학습하는 방식입니다. 이렇게 하면 각 트리가 서로 다른 데이터로 구축되기 때문에 랜덤성이 생기게 됩니다. 그리고 파티션을 나눌 때 변수에 랜덤성을 부여합니다. 즉, 남아있는 모든 변수 중에서 최적의 변수를 선택하는 것이 아니라 변수 중 일부만 선택하고 그 일부 중에서 최적의 변수를 선택하는 것입니다.

\n

이러한 방식을 앙상블 기법(ensemble learning) 이라고 합니다. 랜덤포레스트는 아주 인기가 많고 자주 사용되는 알고리즘 중 하나입니다. 샘플링되지 않은 데이터를 테스트 데이터로 이용할 수 있기 때문에 데이터 전체를 학습에 사용할 수 있으며, 의사결정트리에 비해 일반화도 적용될 수 있습니다.

\n

하지만 실제로 테스트 해보면 꼭 모든 경우에 뛰어나다고 할 수는 없습니다. (예를 들면 데이터 셋이 비교적 적은 경우)

\n
\n

실제로 사용해보자

\n

이렇게 이론을 공부하고 나면 실제로 적용해보는 예측모델을 만들고 싶어집니다.\n정말 고맙게도 파이썬의 scikit-learn에 다양한 트리 모델이 구현되어 있습니다.\n다른 앙상블 모델 뿐만 아니라 RandomForest까지 제공합니다.

\n

공식 레퍼런스는 아래의 링크를 참조

\n

http://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html

\n
\n

\n \n \n \n

\n
\n

Preprocessing

\n

전처리 과정은 Preprocessing 또는 Data Cleaning / Wrangling / Munging 과 같이 다양한 용어로 불립니다. 이 과정에서 어떤 일을 하는지 한마디로 요약하자면 비정형 데이터를 정형화시키는 작업을 합니다. 대부분의 우리가 수집한 데이터는 쓰레기의 집합에 가깝습니다. 따라서, 원하는 형태에 맞게 수정 / 변환해주는 작업이 필요한 것입니다.

\n

비정형 데이터를 가치있는 데이터로 변환하기 이전에, 먼저 어떻게 생겼는지 확인해보는 것이 좋습니다. 실제 Kaggle에 가보면 많은 사이언티스트들이 데이터를 시각화해놓은 스크립트를 많이 볼 수 있습니다. 이러한 과정을 통해 모델링 이전에 인사이트를 얻게 된다면, 이후에 많은 시간을 절약할 수 있게 됩니다.

\n

대충 확인했다면 이제 데이터를 정형화시키는 작업을 진행합니다. 이 단계는 데이터에 따라 다르겠지만 약간 노가다 작업이 많이 들어갑니다. 텍스트 날짜 데이터를 datetime 객체로 변환하고 날짜별로 정리한다던지, Null 데이터를 적절한 값으로 채우거나 버리는 작업이 이에 해당합니다.

\n

만일 텍스트 데이터를 분석해야 한다면, 상황에 따라 다음과 같은 과정이 필요할 것입니다. 이 부분에 대해서는 나중에 자세히 다루겠습니다.

\n\n

데이터가 어느정도 깨끗해지고 나면 Training Dataset과 Test Dataset을 나누어 줘야 합니다. Training Dataset으로 학습하고 나면, 모델이 그 데이터에 최적화 되어 있기 때문에 학습에 사용하지 않은 데이터로 나누어주는 것이 좋습니다. Coursera 강의를 보면 일반적으로 Training Dataset을 70%, Test Dataset을 30%로 나누는게 적당하다고 말합니다.

\n
\n

Modeling

\n

모델링 과정은 적절한 Learning Algorithm을 선택하여 Training Dataset을 넣고 학습하는 과정입니다. 먼저 Model Selection 같은 경우, 분석하고자 하는 과정이 Classification인지, Regression인지 등 여러 경우와 데이터에 따라 적절한 모델을 선택하면 됩니다. 이 과정에서 통계적 지식이 조금 요구됩니다.

\n

이렇게 모델을 만들고 나서 바로 Test Dataset 과 비교하는 것이 아니라, Cross-Validation 과정을 거치게 됩니다. Cross-Validation은 학습에 사용된 Training Dataset 중 일부분을 나누어 검증하는 데 사용하는 것을 말합니다. 왜 중간에 검증 과정이 필요한가 하면, 우리가 가지고 있는 데이터는 전체 데이터 중 일부를 샘플링 한 데이터이기 때문에 신뢰도가 100%일 수 없습니다. 따라서, Cross-Validation은 통계적으로 신뢰도를 높이기 위한 과정입니다. 대표적인 방법으로 K-FoldBootstrap이 있습니다.

\n

그리고 모델의 성능을 높이기 위한 방법으로 Hyperparameter Optimization 이 있습니다. Parameter Tuning 이라고 부르기도 합니다. 간단히 말해서 모델에 들어가는 파라메터들을 최적의 값으로 튜닝하는 것입니다. 딥러닝의 경우 뉴럴넷을 통해 알아서 최적화 되지만, 머신러닝 알고리즘의 경우 결과 값을 확인하여 최적화하는 경우가 많습니다. 대표적으로 Grid Search, Random Search 등의 방법이 있습니다.

\n
\n

Evaluation & Prediction

\n

마지막은 모델을 평가하고 결과를 예측하는 과정입니다. 모델을 평가한다는 것은 말 그대로 이 모델이 좋은지 별로인지, 너무 Training Dataset 에만 최적화 된 것은 아닌지 등을 평가하는 작업을 말합니다. 일반적으로 모델 평가에는 performance matrixloss function 이 사용됩니다. Log Loss, F1 Score, RMSE 등의 값들이 이에 해당합니다.

\n
\n

Python Library

\n

다행히 파이썬에는 일련의 과정에 필요한 패키지들이 다양하게 제공됩니다. 그 중에서 제가 자주 쓰는 라이브러리를 간단히 정리해보았습니다.

\n

Data Analysis

\n\n

Data Visualization

\n","excerpt":"…"}}}},{"node":{"title":"머신러닝을 시작하기 위한 기초 지식 (1)","id":"8428f581-63b4-5beb-a64e-19bd4128b369","slug":"pyml-intro1","publishDate":"February 05, 2017","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

이 글의 목차나 그림은 Sebastian Rashka - Python Machine Learning 을 참고하였습니다.

\n

사실 기계학습, 인공지능에 대한 연구는 예전부터 존재했지만 발전이 없었으며 소수에 연구원들에 의한 주제였기에 대중화 될 수 없었습니다. 하지만 풍부한 데이터의 확보, 컴퓨팅 성능향상, 오픈소스 라이브러리로 인해 많은 개발자들이 인공지능 연구에 참여하게 되었습니다.

\n

이 글에서는 기계학습에 대한 간략한 소개와 데이터 분석 시스템을 어떻게 디자인해야 되는지, 마지막으로 파이썬을 이용한 데이터 분석에 대해 소개해드리겠습니다.

\n
\n

\n \n \n \n

\n
\n

Classification

\n

지도학습은 Classification과 Regression으로 나누어집니다. 먼저, Classification은 주어진 데이터를 정해진 카테고리에 따라 분류하는 문제를 말합니다. 최근에 많이 사용되는 이미지 분류도 Classification 문제 중에 하나입니다.

\n

예를 들어, 이메일이 스팸메일인지 아닌지를 예측한다고 하면 이메일은 스팸메일 / 정상적인 메일로 라벨링 될 수 있을 것입니다. 비슷한 예시로 암을 예측한다고 가정했을 때 이 종양이 악성종양인지 / 아닌지로 구분할 수 있습니다. 이처럼 맞다 / 아니다로 구분되는 문제를 Binary Classification 이라고 부릅니다.

\n

분류 문제가 모두 맞다 / 아니다로 구분되지는 않습니다. 예를 들어, 공부시간에 따른 전공 Pass / Fail 을 예측한다고 하면 이는 Binary Classifiaction 으로 볼 수 있습니다. 반면에, 수능 공부시간에 따른 전공 학점을 A / B / C / D / F 으로 예측하는 경우도 있습니다. 이러한 분류를 Multi-label Classification 이라고 합니다.

\n
\n

Regression

\n

다음으로 Regression은 연속된 값을 예측하는 문제를 말합니다. 주로 어떤 패턴이나 트렌드, 경향을 예측할 때 사용됩니다. Coursera에서는 Regression을 설명할 때 항상 집의 크기에 따른 매매가격을 예로 듭니다. 아까와 유사한 예를 들자면, 공부시간에 따른 전공 시험 점수를 예측하는 문제를 예로 들 수 있습니다.

\n
\n

Unsupervised Learning

\n

비지도학습은 앞에서 언급한 것 처럼 라벨링이 되어 있지 않은 데이터로부터 미래를 예측하는 학습방법입니다. 평가되어 있지 않은 데이터로부터 숨어있는 패턴이나 형태를 찾아야 하기 때문에 당연히 더 어렵습니다. 비지도학습도 데이터가 분리되어 있는지 (Categorial data) 연속적인지 (Continuous data)로 나누어 생각할 수 있습니다.

\n

대표적으로 클러스터링 (Clustering) 이 있습니다. 실제로는 그 데이터의 label이나 category가 무엇인지 알 수 없는 경우가 많기 때문에 이러한 방법이 중요하다고 볼 수 있습니다. 이외에도 차원축소(Dimentionality Reduction), Hidden Markov Model 등이 있습니다.

\n
\n

Reinforcement Learning

\n

\n \n \n \n

\n

마지막으로 강화학습은 앞서 말했던 학습방법과는 조금 다른 개념입니다. 데이터가 정답이 있는 것도 아니며, 심지어 주어진 데이터가 없을 수도 있습니다. 강화학습이란, 자신이 한 행동에 대한 \"보상\"을 알 수 있어서 그로부터 학습하는 것을 말합니다.

\n

예를 들면, 아이가 걷는 것을 배우는 것처럼 어떻게 행동할 줄 모르지만 환경과 상호작용하면서 걷는 법을 알아가는 것과 같은 학습 방법을 강화학습이라고 합니다.

\n

\"atari\"

\n

보통 아타리 게임 인공지능을 많이 예시로 드는데, 여기에서 학습 대상 (agent) 은 움직이면서 적을 물리치는 존재입니다. 이 학습 대상은 움직이면서 적을 물리치면 보상 (reward) 을 받게 됩니다. 이러한 과정을 스스로 반복 학습 (Trial and Error) 하면서 점수를 최대화하는 것이 목표입니다.

\n
\n

How?

\n

처음에는 공부를 시작하기에 막막한데 다행히 아주 좋은 강의와 자료들이 많이 있습니다.

\n

Machine Learning / Deep Learning

\n\n
\n

Reinforcement Learning

\n\n
","excerpt":"이 글의 목차나 그림은 Sebastian Rashka - Python Machine Learning…"}}}},{"node":{"title":"다양한 소셜 API를 연동하기 전에 고려할 것들 (AWS Cognito)","id":"5edbcc11-9ccb-5dfd-982e-b6b87ecd1477","slug":"social-api-cognito","publishDate":"January 28, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근에 대부분의 웹, 모바일 어플리케이션에서 카카오, 네이버 등 다양한 소셜 로그인 기능을 제공하고 있다.\n만약 우리가 만들어야 할 어플리케이션이 다양한 소셜 로그인 API와 연동하여 사용자를 관리해야한다면, OAuth 인증, 보안 등 개발할 때 고려해야할 요소가 많을 것이다.

\n

따라서, 이 글을 통해 최근 유행하는 클라우드 기반 웹 어플리케이션 설계 방식을 아주 간단히 보고 적합한 설계 방식을 선택하는데 도움이 되었으면 좋겠다.

\n

OAuth2.0에 대해서는 이전에 쓴 글을 참조하길 바란다. http://swalloow.github.io/about-oauth2/

\n
\n

1. OAuth 2.0 Grant Flow

\n

\n \n \n \n

\n

이 방식은 직접 ID, PW 보내는 방식으로 파트너나 자사 시스템에 사용한다.\n기존의 HTTP 방식을 그대로 사용하기 용이하다.

\n

위와 같은 방식을 사용했을 때의 장점은 OAuth 2.0을 몸소 체험할 수 있다는 것이다.\n반면에, 단점은 다음과 같다.

\n\n
\n

2. AWS EC2 + Cognito (BaaS)

\n

\n \n \n \n

\n

사용자 로그인, 인증 처리에 대해 AWS Cognito를 사용한 방법이다.\n기본적인 EC2 인스턴스에 Cognito만 추가해서 사용하면 된다.

\n

이러한 방법을 적용했을 때의 단점은 일단 클라우드에 요금을 내야 한다는 것이다.\n또한, AWS Cognito에서 지원하지 않는 카카오 로그인 같은 경우 복잡한 과정이 필요하다.\n반면에 장점은 다음과 같다.

\n\n
\n

3. AWS Serverless Architecture (BaaS + FaaS)

\n

\n \n \n \n

\n

AWS API Gateway와 Lambda를 통한 서버리스 아키텍쳐에 대해서는 아래 링크를 참고하자.\n서버리스 아키텍쳐는 서버를 관리할 필요 없이 특정 이벤트에 반응하는 함수를 등록하고, 해당 이벤트가 발생하면 함수가 실행되는 구조이다.\n장점은 다음과 같다.

\n\n

반면에 단점은 다음과 같다.

\n\n
\n

결론

\n

최근에 유행하는 서버리스 아키텍쳐나 마이크로 아키텍쳐를 무조건 도입해야하는 것은 절대 아니다.\n각자 프로젝트의 상황에 맞는 방법을 선택하는게 답인듯하다.

\n
\n

참고하면 좋은 문서들

\n","excerpt":"…"}}}},{"node":{"title":"Jupyter Notebook 다중커널 설정하기","id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}},{"node":{"title":"Open API를 설계할 때 알아야 하는 것들","id":"846abcd6-b4b7-567b-85f5-635c8cf30678","slug":"open-api-guide","publishDate":"January 25, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":9,"html":"

오픈 API를 사용하다보면 공통의 패턴을 발견할 수 있을 것이다.\n이처럼, API를 설계할 때도 개발자들이 쉽게 사용할 수 있도록 만든 규칙이라는게 존재한다.\n오늘은 RESTful한 Open API를 설계하기 위해 알아야 하는 것들에 대해 정리해보았다.

\n
\n

Open API 디자인

\n
\n

API 란?

\n
\n

운영체제, 시스템, 애플리케이션, 라이브러리 등을 개발자들이 프로그래밍 작업을 통해 응용 프로그램을 작성할 수 있는 다양한 인터페이스들을 총칭한다. (예: Window API, Java API, HTML5 API, Android API…) - 네이버 개발자센터 인용

\n
\n
\n

오픈 API 란?

\n
\n

API 중에서 플랫폼의 기능 또는 컨텐츠를 외부에서 쓸 수 있도록 웹 프로토콜(HTTP)로 호출할 수 있도록 개방(open)한 API를 의미한다. 네이버 개발자센터에서 제공하고 있는 지도, 검색을 비롯 기계번역, 캡차, 단축 URL 등 대부분 API 들은 HTTP로 호출할 수 있는 오픈 API에 해당한다. - 네이버 개발자센터 인용

\n
\n
\n

이제 기업 또는 사용자에게 제공할 RESTful Open API를 어떻게 설계할지 고민해보자.\n기업에게 전문적으로 API를 공급하는 Apigee 사의 Web API Design 을 레퍼런스로 삼았다.

\n
\n

Best Web API Design Rules

\n
\n

1. 기본 URL에는 동사가 아닌 명사를 사용, 리소스마다 2개의 기본 URL을 유지하자.

\n
/dogs (Collection), /dogs/1234 (Element)
\n
\n

2. 올바른 HTTP 메서드(POST, GET, PUT, DELETE)를 사용하자.

\n
POST(create), GET(read), PUT(update), DELETE(delete)
\n
\n

3. 복수형 명사와 구체적인 이름을 사용하자.

\n
/animals, /dogs
\n
\n

4. 자원 간의 관계를 간단히 하여 URL 계층이 깊어지는 것을 피하자.

\n
GET\t/owners/5678/dogs?color=red
\n
\n

5. 오류 처리를 명확하게 하고 에러 스택은 절대 비공개 해야 한다.

\n
200 - OK\n400 - Bad Request\n500 - Internal Server Error\n201 - Created\n304 - Not Modified\n404 - Not Found\n401 - Unauthorized\n403 - Forbidden
\n
\n

6. 접두사 \"v\"로 버전을 지정하고 지속적인 버전 관리를 하자.

\n
GET\t/v1/dogs
\n
\n

7. 데이터베이스에 없는 자원에 대한 응답일 경우 동사를 사용하자.

\n
ex) Caculate, Translate, Convert ...
\n
\n

8. 속성의 네이밍은 Javascript의 관습을 따르고 카멜 케이스를 사용하자.

\n
\"createdAt\": 123415125
\n
\n

9. 하위 도메인의 독립적인 API 요청 처리는 통일하자.

\n
company.com\napi.company.com\t(if not exists, redirect)\ndevelopers.company.com
\n
\n

10. 기타

\n\n
\n

NAVER Open API

\n

실제 네이버와 카카오의 오픈 API는 어떻게 디자인되어 있는지 간단히 살펴보자.\nhttps://developers.naver.com 참조

\n
\n

이전(2015년 쯤)에는 네이버에서 제공하는 API를 사용하기 위해 'API 키'라는 유니크한 텍스트 문자열을 발급받고, 이를 API 호출시 같이 API 게이트웨이 서버로 전송함으로써 인증된 사용자임을 입증했다. 새로운 개발자센터에서는 API 키 방식은 더 이상 사용하지 않고 애플리케이션마다 일종의 유니크한 아이디와 비밀번호(클라이언트 아이디, 시크릿)값을 이용해서 인증하고 있다.

\n
\n
\n

1. API 호출 URL과 요청 변수

\n\n
\n

2. 에러 코드 정의 - HTTP status code

\n\n
\n

Kakao REST API (카카오톡, 카카오페이)

\n
\n

1. 먼저, 카카오 로그인 후에 사용자 토큰을 받아온다.

\n

2. 사용자 토큰을 헤더에 담아 GET으로 요청한다.

\n
GET /v1/api/talk/profile HTTP/1.1\nHost: kapi.kakao.com\nAuthorization: Bearer {access_token}
\n
\n

3. 응답은 JSON 형태로 다음과 같은 정보를 포함한다.

\n
{\n \"nickName\":\"홍길동\",\n \"profileImageURL\":\"http://xxx.kakao.co.kr/.../aaa.jpg\",\n \"thumbnailURL\":\"http://xxx.kakao.co.kr/.../bbb.jpg\",\n \"countryISO\":\"KR\"\n}
\n
\n

Response Code Example

\n
{\n  \"meta\": {\n    \"code\": 200,\n    \"response_time\": {\n      \"time\": 0,\n      \"measure\": \"seconds\"\n    }\n  },\n  \"notifications\": {},\n  \"response\": {}\n}
\n
\n

Error Code Example

\n
{\n  \"meta\": {\n    \"code\": 500,\n    \"error_detail\": \"The user has not authorized or the token is invalid.\",\n    \"error_type\": \"invalid_auth\",\n    \"developer_friendly\": \"The user has not authorized or the token is invalid.\",\n    \"response_time\": {\n      \"time\": 0,\n      \"measure\": \"seconds\"\n    }\n  }\n}
\n
\n

참고자료

\n","excerpt":"오픈 API를 사용하다보면 공통의 패턴을 발견할 수 있을 것이다.\n이처럼, API…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":14,"humanPageNumber":15,"skip":85,"limit":6,"numberOfPages":16,"previousPagePath":"/14","nextPagePath":"/16"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/15","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Jupyter Notebook 외부접속 설정하기","id":"79c1215f-bb79-5e21-b334-04fb090a7956","slug":"jupyter-config","publishDate":"February 12, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}}},{"node":{"title":"DecisionTree와 RandomForest에 대하여","id":"06ab8a86-5c79-565e-8a20-7665fe5adee3","slug":"decision-randomforest","publishDate":"February 10, 2017","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

의사결정트리 (DecisionTree)

\n

의사결정나무는 다양한 의사결정 경로와 결과를 나타내는데 트리 구조를 사용합니다. 보통 어렸을 때의 스무고개 놀이를 예로 드는 경우가 많습니다.

\n

\"img\"

\n
\n

위의 그림은 타이타닉 생존자를 찾는 의사결정트리 모델입니다. 첫번째 뿌리 노드를 보면 성별 <= 0.5 라고 되어있는데 이는 남자냐? 여자냐? 라고 질문하는 것과 같습니다.

\n

최종적으로, 모든 승객에 대한 분류(Classification)를 통해 생존확률을 예측할 수 있게 됩니다.

\n

이처럼, 숫자형 결과를 반환하는 것을 회귀나무(Regression Tree) 라고 하며, 범주형 결과를 반환하는 것을 분류나무(Classification Tree) 라고 합니다. 의사결정트리를 만들기 위해서는 먼저 어떤 질문을 할 것인지, 어떤 순서로 질문을 할 것인지 정해야 합니다.

\n

가장 좋은 방법은 예측하려는 대상에 대해 가장 많은 정보를 담고 있는 질문을 고르는 것이 좋습니다. 이러한 '얼마만큼의 정보를 담고 있는가'를 엔트로피(entropy) 라고 합니다. 엔트로피는 보통 데이터의 불확실성(?)을 나타내며, 결국 엔트로피가 클 수록 데이터 정보가 잘 분포되어 있기 때문에 좋은 지표라고 예상할 수 있습니다.

\n

그림과 같이 의사결정트리는 이해하고 해석하기 쉽다는 장점이 있습니다. 또한 예측할 때 사용하는 프로세스가 명백하며, 숫자형 / 범주형 데이터를 동시에 다룰 수 있습니다. 그리고 특정 변수의 값이 누락되어도 사용할 수 있습니다.

\n

하지만 최적의 의사결정트리를 찾는 것이 정말 어려운 문제입니다. (어떤 것들을 조건(Feature)으로 넣어야 할지, 깊이(Depth)는 얼마로 정해야 할지…) 그리고 의사결정트리의 단점은 새로운 데이터에 대한 일반화 성능이 좋지 않게 오버피팅(Overfitting)되기 쉽다는 것입니다.

\n

잠깐 오버피팅에 대해 설명하자면, 오버피팅이란 Supervised Learning에서 과거의 학습한 데이터에 대해서는 잘 예측하지만 새로 들어온 데이터에 대해서 성능이 떨어지는 경우를 말합니다. 즉, 학습 데이터에 지나치게 최적화되어 일반화가 어렵다는 말입니다. 이러한 오버피팅을 방지할 수 있는 대표적인 방법 중 하나가 바로 앙상블 기법을 적용한 랜덤포레스트(Random Forest) 입니다.

\n
\n

랜덤포레스트 (RandomForest)

\n

랜덤포레스트는 위에서 말한 것과 같이 의사결정트리를 이용해 만들어진 알고리즘입니다.

\n
\n

랜덤포레스트는 분류, 회귀 분석 등에 사용되는 앙상블 학습 방법의 일종으로, 

\n

훈련 과정에서 구성한 다수의 결정 트리로부터 분류 또는 평균 예측치를 출력함으로써 동작한다.

\n
\n
\n

즉, 랜덤포레스트란 여러 개의 의사결정트리를 만들고, 투표를 시켜 다수결로 결과를 결정하는 방법입니다.

\n
\n

\"img\"

\n

위의 그림은 고작 몇 십개의 트리노드가 있지만 실제로는 수 백개에서 수 만개까지 노드가 생성될 수 있습니다. 이렇게 여러 개의 트리를 통해 투표를 해서 오버피팅이 생길 경우에 대비할 수 있습니다.

\n

그런데 보통 구축한 트리에는 랜덤성이 없는데 어떻게하면 랜덤하게 트리를 얻을 수 있나? 라는 의문이 듭니다. 랜덤포레스트에서는 데이터를 bootstrap 해서 포레스트를 구성합니다.

\n

bootstrap aggregating 또는 begging 이라고 하는데, 전체 데이터를 전부 이용해서 학습시키는 것이 아니라 샘플의 결과물을 각 트리의 입력 값으로 넣어 학습하는 방식입니다. 이렇게 하면 각 트리가 서로 다른 데이터로 구축되기 때문에 랜덤성이 생기게 됩니다. 그리고 파티션을 나눌 때 변수에 랜덤성을 부여합니다. 즉, 남아있는 모든 변수 중에서 최적의 변수를 선택하는 것이 아니라 변수 중 일부만 선택하고 그 일부 중에서 최적의 변수를 선택하는 것입니다.

\n

이러한 방식을 앙상블 기법(ensemble learning) 이라고 합니다. 랜덤포레스트는 아주 인기가 많고 자주 사용되는 알고리즘 중 하나입니다. 샘플링되지 않은 데이터를 테스트 데이터로 이용할 수 있기 때문에 데이터 전체를 학습에 사용할 수 있으며, 의사결정트리에 비해 일반화도 적용될 수 있습니다.

\n

하지만 실제로 테스트 해보면 꼭 모든 경우에 뛰어나다고 할 수는 없습니다. (예를 들면 데이터 셋이 비교적 적은 경우)

\n
\n

실제로 사용해보자

\n

이렇게 이론을 공부하고 나면 실제로 적용해보는 예측모델을 만들고 싶어집니다.\n정말 고맙게도 파이썬의 scikit-learn에 다양한 트리 모델이 구현되어 있습니다.\n다른 앙상블 모델 뿐만 아니라 RandomForest까지 제공합니다.

\n

공식 레퍼런스는 아래의 링크를 참조

\n

http://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html

\n
\n

\n \n \n \n

\n
\n

Preprocessing

\n

전처리 과정은 Preprocessing 또는 Data Cleaning / Wrangling / Munging 과 같이 다양한 용어로 불립니다. 이 과정에서 어떤 일을 하는지 한마디로 요약하자면 비정형 데이터를 정형화시키는 작업을 합니다. 대부분의 우리가 수집한 데이터는 쓰레기의 집합에 가깝습니다. 따라서, 원하는 형태에 맞게 수정 / 변환해주는 작업이 필요한 것입니다.

\n

비정형 데이터를 가치있는 데이터로 변환하기 이전에, 먼저 어떻게 생겼는지 확인해보는 것이 좋습니다. 실제 Kaggle에 가보면 많은 사이언티스트들이 데이터를 시각화해놓은 스크립트를 많이 볼 수 있습니다. 이러한 과정을 통해 모델링 이전에 인사이트를 얻게 된다면, 이후에 많은 시간을 절약할 수 있게 됩니다.

\n

대충 확인했다면 이제 데이터를 정형화시키는 작업을 진행합니다. 이 단계는 데이터에 따라 다르겠지만 약간 노가다 작업이 많이 들어갑니다. 텍스트 날짜 데이터를 datetime 객체로 변환하고 날짜별로 정리한다던지, Null 데이터를 적절한 값으로 채우거나 버리는 작업이 이에 해당합니다.

\n

만일 텍스트 데이터를 분석해야 한다면, 상황에 따라 다음과 같은 과정이 필요할 것입니다. 이 부분에 대해서는 나중에 자세히 다루겠습니다.

\n\n

데이터가 어느정도 깨끗해지고 나면 Training Dataset과 Test Dataset을 나누어 줘야 합니다. Training Dataset으로 학습하고 나면, 모델이 그 데이터에 최적화 되어 있기 때문에 학습에 사용하지 않은 데이터로 나누어주는 것이 좋습니다. Coursera 강의를 보면 일반적으로 Training Dataset을 70%, Test Dataset을 30%로 나누는게 적당하다고 말합니다.

\n
\n

Modeling

\n

모델링 과정은 적절한 Learning Algorithm을 선택하여 Training Dataset을 넣고 학습하는 과정입니다. 먼저 Model Selection 같은 경우, 분석하고자 하는 과정이 Classification인지, Regression인지 등 여러 경우와 데이터에 따라 적절한 모델을 선택하면 됩니다. 이 과정에서 통계적 지식이 조금 요구됩니다.

\n

이렇게 모델을 만들고 나서 바로 Test Dataset 과 비교하는 것이 아니라, Cross-Validation 과정을 거치게 됩니다. Cross-Validation은 학습에 사용된 Training Dataset 중 일부분을 나누어 검증하는 데 사용하는 것을 말합니다. 왜 중간에 검증 과정이 필요한가 하면, 우리가 가지고 있는 데이터는 전체 데이터 중 일부를 샘플링 한 데이터이기 때문에 신뢰도가 100%일 수 없습니다. 따라서, Cross-Validation은 통계적으로 신뢰도를 높이기 위한 과정입니다. 대표적인 방법으로 K-FoldBootstrap이 있습니다.

\n

그리고 모델의 성능을 높이기 위한 방법으로 Hyperparameter Optimization 이 있습니다. Parameter Tuning 이라고 부르기도 합니다. 간단히 말해서 모델에 들어가는 파라메터들을 최적의 값으로 튜닝하는 것입니다. 딥러닝의 경우 뉴럴넷을 통해 알아서 최적화 되지만, 머신러닝 알고리즘의 경우 결과 값을 확인하여 최적화하는 경우가 많습니다. 대표적으로 Grid Search, Random Search 등의 방법이 있습니다.

\n
\n

Evaluation & Prediction

\n

마지막은 모델을 평가하고 결과를 예측하는 과정입니다. 모델을 평가한다는 것은 말 그대로 이 모델이 좋은지 별로인지, 너무 Training Dataset 에만 최적화 된 것은 아닌지 등을 평가하는 작업을 말합니다. 일반적으로 모델 평가에는 performance matrixloss function 이 사용됩니다. Log Loss, F1 Score, RMSE 등의 값들이 이에 해당합니다.

\n
\n

Python Library

\n

다행히 파이썬에는 일련의 과정에 필요한 패키지들이 다양하게 제공됩니다. 그 중에서 제가 자주 쓰는 라이브러리를 간단히 정리해보았습니다.

\n

Data Analysis

\n\n

Data Visualization

\n","excerpt":"…"}}}},{"node":{"title":"머신러닝을 시작하기 위한 기초 지식 (1)","id":"8428f581-63b4-5beb-a64e-19bd4128b369","slug":"pyml-intro1","publishDate":"February 05, 2017","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

이 글의 목차나 그림은 Sebastian Rashka - Python Machine Learning 을 참고하였습니다.

\n

사실 기계학습, 인공지능에 대한 연구는 예전부터 존재했지만 발전이 없었으며 소수에 연구원들에 의한 주제였기에 대중화 될 수 없었습니다. 하지만 풍부한 데이터의 확보, 컴퓨팅 성능향상, 오픈소스 라이브러리로 인해 많은 개발자들이 인공지능 연구에 참여하게 되었습니다.

\n

이 글에서는 기계학습에 대한 간략한 소개와 데이터 분석 시스템을 어떻게 디자인해야 되는지, 마지막으로 파이썬을 이용한 데이터 분석에 대해 소개해드리겠습니다.

\n
\n

\n \n \n \n

\n
\n

Classification

\n

지도학습은 Classification과 Regression으로 나누어집니다. 먼저, Classification은 주어진 데이터를 정해진 카테고리에 따라 분류하는 문제를 말합니다. 최근에 많이 사용되는 이미지 분류도 Classification 문제 중에 하나입니다.

\n

예를 들어, 이메일이 스팸메일인지 아닌지를 예측한다고 하면 이메일은 스팸메일 / 정상적인 메일로 라벨링 될 수 있을 것입니다. 비슷한 예시로 암을 예측한다고 가정했을 때 이 종양이 악성종양인지 / 아닌지로 구분할 수 있습니다. 이처럼 맞다 / 아니다로 구분되는 문제를 Binary Classification 이라고 부릅니다.

\n

분류 문제가 모두 맞다 / 아니다로 구분되지는 않습니다. 예를 들어, 공부시간에 따른 전공 Pass / Fail 을 예측한다고 하면 이는 Binary Classifiaction 으로 볼 수 있습니다. 반면에, 수능 공부시간에 따른 전공 학점을 A / B / C / D / F 으로 예측하는 경우도 있습니다. 이러한 분류를 Multi-label Classification 이라고 합니다.

\n
\n

Regression

\n

다음으로 Regression은 연속된 값을 예측하는 문제를 말합니다. 주로 어떤 패턴이나 트렌드, 경향을 예측할 때 사용됩니다. Coursera에서는 Regression을 설명할 때 항상 집의 크기에 따른 매매가격을 예로 듭니다. 아까와 유사한 예를 들자면, 공부시간에 따른 전공 시험 점수를 예측하는 문제를 예로 들 수 있습니다.

\n
\n

Unsupervised Learning

\n

비지도학습은 앞에서 언급한 것 처럼 라벨링이 되어 있지 않은 데이터로부터 미래를 예측하는 학습방법입니다. 평가되어 있지 않은 데이터로부터 숨어있는 패턴이나 형태를 찾아야 하기 때문에 당연히 더 어렵습니다. 비지도학습도 데이터가 분리되어 있는지 (Categorial data) 연속적인지 (Continuous data)로 나누어 생각할 수 있습니다.

\n

대표적으로 클러스터링 (Clustering) 이 있습니다. 실제로는 그 데이터의 label이나 category가 무엇인지 알 수 없는 경우가 많기 때문에 이러한 방법이 중요하다고 볼 수 있습니다. 이외에도 차원축소(Dimentionality Reduction), Hidden Markov Model 등이 있습니다.

\n
\n

Reinforcement Learning

\n

\n \n \n \n

\n

마지막으로 강화학습은 앞서 말했던 학습방법과는 조금 다른 개념입니다. 데이터가 정답이 있는 것도 아니며, 심지어 주어진 데이터가 없을 수도 있습니다. 강화학습이란, 자신이 한 행동에 대한 \"보상\"을 알 수 있어서 그로부터 학습하는 것을 말합니다.

\n

예를 들면, 아이가 걷는 것을 배우는 것처럼 어떻게 행동할 줄 모르지만 환경과 상호작용하면서 걷는 법을 알아가는 것과 같은 학습 방법을 강화학습이라고 합니다.

\n

\"atari\"

\n

보통 아타리 게임 인공지능을 많이 예시로 드는데, 여기에서 학습 대상 (agent) 은 움직이면서 적을 물리치는 존재입니다. 이 학습 대상은 움직이면서 적을 물리치면 보상 (reward) 을 받게 됩니다. 이러한 과정을 스스로 반복 학습 (Trial and Error) 하면서 점수를 최대화하는 것이 목표입니다.

\n
\n

How?

\n

처음에는 공부를 시작하기에 막막한데 다행히 아주 좋은 강의와 자료들이 많이 있습니다.

\n

Machine Learning / Deep Learning

\n\n
\n

Reinforcement Learning

\n\n
","excerpt":"이 글의 목차나 그림은 Sebastian Rashka - Python Machine Learning…"}}}},{"node":{"title":"다양한 소셜 API를 연동하기 전에 고려할 것들 (AWS Cognito)","id":"5edbcc11-9ccb-5dfd-982e-b6b87ecd1477","slug":"social-api-cognito","publishDate":"January 28, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근에 대부분의 웹, 모바일 어플리케이션에서 카카오, 네이버 등 다양한 소셜 로그인 기능을 제공하고 있다.\n만약 우리가 만들어야 할 어플리케이션이 다양한 소셜 로그인 API와 연동하여 사용자를 관리해야한다면, OAuth 인증, 보안 등 개발할 때 고려해야할 요소가 많을 것이다.

\n

따라서, 이 글을 통해 최근 유행하는 클라우드 기반 웹 어플리케이션 설계 방식을 아주 간단히 보고 적합한 설계 방식을 선택하는데 도움이 되었으면 좋겠다.

\n

OAuth2.0에 대해서는 이전에 쓴 글을 참조하길 바란다. http://swalloow.github.io/about-oauth2/

\n
\n

1. OAuth 2.0 Grant Flow

\n

\n \n \n \n

\n

이 방식은 직접 ID, PW 보내는 방식으로 파트너나 자사 시스템에 사용한다.\n기존의 HTTP 방식을 그대로 사용하기 용이하다.

\n

위와 같은 방식을 사용했을 때의 장점은 OAuth 2.0을 몸소 체험할 수 있다는 것이다.\n반면에, 단점은 다음과 같다.

\n\n
\n

2. AWS EC2 + Cognito (BaaS)

\n

\n \n \n \n

\n

사용자 로그인, 인증 처리에 대해 AWS Cognito를 사용한 방법이다.\n기본적인 EC2 인스턴스에 Cognito만 추가해서 사용하면 된다.

\n

이러한 방법을 적용했을 때의 단점은 일단 클라우드에 요금을 내야 한다는 것이다.\n또한, AWS Cognito에서 지원하지 않는 카카오 로그인 같은 경우 복잡한 과정이 필요하다.\n반면에 장점은 다음과 같다.

\n\n
\n

3. AWS Serverless Architecture (BaaS + FaaS)

\n

\n \n \n \n

\n

AWS API Gateway와 Lambda를 통한 서버리스 아키텍쳐에 대해서는 아래 링크를 참고하자.\n서버리스 아키텍쳐는 서버를 관리할 필요 없이 특정 이벤트에 반응하는 함수를 등록하고, 해당 이벤트가 발생하면 함수가 실행되는 구조이다.\n장점은 다음과 같다.

\n\n

반면에 단점은 다음과 같다.

\n\n
\n

결론

\n

최근에 유행하는 서버리스 아키텍쳐나 마이크로 아키텍쳐를 무조건 도입해야하는 것은 절대 아니다.\n각자 프로젝트의 상황에 맞는 방법을 선택하는게 답인듯하다.

\n
\n

참고하면 좋은 문서들

\n","excerpt":"…"}}}},{"node":{"title":"Jupyter Notebook 다중커널 설정하기","id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":14,"humanPageNumber":15,"skip":85,"limit":6,"numberOfPages":16,"previousPagePath":"/14","nextPagePath":"/16"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/16/page-data.json b/page-data/16/page-data.json index 27c5d90..cfbced0 100644 --- a/page-data/16/page-data.json +++ b/page-data/16/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-posts-js","path":"/16","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"DB 테이블을 DataFrame으로 읽어오는 방법","id":"ea6cffe1-0590-587f-975e-f196ce841ed7","slug":"db-to-dataframe","publishDate":"January 14, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}}},{"node":{"title":"맥북프로 터치바 모델 초기설정","id":"e27d1aed-a171-5885-8d97-8fc290bd164e","slug":"macbook-setting","publishDate":"January 08, 2017","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

\n \n \n \n

\n

1년을 기다린 결과 드디어 맥북프로 터치바 모델을 구매했다!\n맥을 처음 사용하는 입장에서 초기에 어떤 것들을 설정해야 하는지 정리해보았다.

\n
\n

프로그램 관련

\n\n
\n

문서작성

\n\n
\n

커멘드 및 기타 설정

\n\n
\n

후기

\n

윈도우에서는 초기에 환경구축하려면 온갖 삽질을 해야 했는데,\n맥은 금방 설치되는 점이 가장 좋았다.\n터치바를 커스터마이징 하는 것은 나중에 따로 정리해봐야겠다.

","excerpt":"…"}}}},{"node":{"title":"OAuth2에 대해 알아보자","id":"0f396ba5-9160-5a8a-9736-893fdda81cbe","slug":"about-oauth2","publishDate":"January 05, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

먼저 OAuth 인증을 이해하기 위해 필요한 몇 가지 개념들에 대해 알아보자. OAuth 인증을 진행할 때 해당 서비스 제공자는 '제 3자가 어떤 정보나 서비스에 사용자의 권한으로 접근하려 하는데 허용하겠느냐'라는 안내 메시지를 보여 주는 것이다.

\n
\n

인증과 허가

\n\n
\n

일반 로그인은 사원이 63빌딩에 출입하는 것이라면, (사원증이 있어야 출입가능)

\n

OAuth는 1층에서 방문증을 수령한 후 63빌딩에 출입하는 것이다. (방문증만 있어도 출입가능)

\n
\n

OAuth 1.0의 특징

\n

기존의 다른 인증방식(OpenID)과 구분되는 특징은 크게 두 가지이다.

\n
    \n
  1. API 인증 시, 써드파티 어플리케이션에게 사용자의 비번을 노출하지 않고 인증할 수 있다는 점
  2. \n
  3. 인증(Authentication)과 API 권한(Authorization) 부여를 동시에 할 수 있다는 점
  4. \n
\n
\n

OAuth 1.0의 동작방식

\n

OAuth 1.0은 기본적으로 user / consumer / service provider가 있어야 한다.

\n

OAuth 1.0 인증을 3-legged OAuth 라고도 하는데 결국 주체가 셋 이라는 말이다.

\n

\n \n \n \n

\n

우리의 서비스에서 트위터 로그인을 연동한다고 가정해보자. 사용자 입장에서는 아이디 / 비밀번호를 통해 가입하면 그 정보를 이용해서 무슨 짓을 할지 모르기 때문에 꺼려한다. OAuth 1.0은 우리의 서비스(Consumer)에게 인증토큰 (Access Token)만을 전달하고 서비스에서 인증토큰으로 트위터 API(Service Provider)를 사용할 수 있도록 해준다.

\n
\n

Outh 1.0 프로세스

\n
    \n
  1. 사용자(User)가 트위터 로그인 요청
  2. \n
  3. 사용자를 트위터(Service Provider) 로그인 화면으로 리다이렉트
  4. \n
  5. 트위터 로그인 진행
  6. \n
  7. 서비스(Consumer)로 인증토큰(Access Token)이 전달
  8. \n
\n
\n

인증토큰의 장점

\n\n
\n

OAuth 2.0의 개선사항

\n

일단 OAuth 2.0은 1.0과 호환되지 않으며 용어부터 많은 것이 다르다. 모바일에서의 사용성 문제나 서명과 같은 개발이 복잡하고 기능과 규모의 확장성 등을 지원하기 위해 만들어진 표준이다. 표준이 매우 크고 복잡해서 이름도 \"OAuth 인증 프레임워크(OAuth 2.0 Authorization Framework)\" 이다. http://tools.ietf.org/wg/oauth/ 에서 확인 가능

\n
\n

OAuth 1.0에서 개선된 사항

\n
    \n
  1. \n

    용어 변경

    \n
      \n
    • Resource Owner : 사용자
    • \n
    • Resource Server : REST API 서버
    • \n
    • Authorization Server : 인증서버 (API 서버와 같을 수도 있음)
    • \n
    • Client : 써드파티 어플리케이션 (서비스)
    • \n
    \n

    \n
  2. \n
  3. \n

    간단하고 직관적

    \n
      \n
    • OAuth 1.0에서는 HTTPS가 필수
    • \n
    • Signature 없이 생성, 호출 가능
    • \n
    • URL 인코딩이 필요없음
    • \n
    \n
  4. \n
\n
\n
    \n
  1. \n

    더 많은 인증 방법을 지원

    \n
      \n
    • 이전에는 HMAC을 이용한 암호화 인증만 지원
    • \n
    • OAuth 2.0은 여러 인증 방식을 통해 웹 / 모바일 등 다양한 시나리오에 대응 가능
    • \n
    • Access Token의 Life-time을 지정하여 만료일 설정 가능
    • \n
    \n
  2. \n
\n
\n
    \n
  1. \n

    대형 서비스로의 확장성 지원

    \n
      \n
    • 커다란 서비스는 인증 서버를 분리하거나 다중화 할 수 있어야 함
    • \n
    • Authorization Server의 역할을 명확히 하여 이에 대한 고려가 되었음
    • \n
    \n
  2. \n
\n
\n

OAuth 2.0 사용 서비스

\n

2013년까지만 해도 1.0만 지원하거나 2.0으로 개선하는 인터넷 서비스 기업이 많았지만,

\n

현재는 대부분 2.0만 지원한다고 봐도 무방하다. (1.0은 자체 로그인에만 사용하는 기업이 많음)

\n\n
\n

참고하면 좋은 자료

\n","excerpt":"먼저 OAuth 인증을 이해하기 위해 필요한 몇 가지 개념들에 대해 알아보자. OAuth 인증을 진행할 때 해당 서비스 제공자는 '제…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":15,"humanPageNumber":16,"skip":91,"limit":6,"numberOfPages":16,"previousPagePath":"/15","nextPagePath":""}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/16","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Open API를 설계할 때 알아야 하는 것들","id":"846abcd6-b4b7-567b-85f5-635c8cf30678","slug":"open-api-guide","publishDate":"January 25, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":9,"html":"

오픈 API를 사용하다보면 공통의 패턴을 발견할 수 있을 것이다.\n이처럼, API를 설계할 때도 개발자들이 쉽게 사용할 수 있도록 만든 규칙이라는게 존재한다.\n오늘은 RESTful한 Open API를 설계하기 위해 알아야 하는 것들에 대해 정리해보았다.

\n
\n

Open API 디자인

\n
\n

API 란?

\n
\n

운영체제, 시스템, 애플리케이션, 라이브러리 등을 개발자들이 프로그래밍 작업을 통해 응용 프로그램을 작성할 수 있는 다양한 인터페이스들을 총칭한다. (예: Window API, Java API, HTML5 API, Android API…) - 네이버 개발자센터 인용

\n
\n
\n

오픈 API 란?

\n
\n

API 중에서 플랫폼의 기능 또는 컨텐츠를 외부에서 쓸 수 있도록 웹 프로토콜(HTTP)로 호출할 수 있도록 개방(open)한 API를 의미한다. 네이버 개발자센터에서 제공하고 있는 지도, 검색을 비롯 기계번역, 캡차, 단축 URL 등 대부분 API 들은 HTTP로 호출할 수 있는 오픈 API에 해당한다. - 네이버 개발자센터 인용

\n
\n
\n

이제 기업 또는 사용자에게 제공할 RESTful Open API를 어떻게 설계할지 고민해보자.\n기업에게 전문적으로 API를 공급하는 Apigee 사의 Web API Design 을 레퍼런스로 삼았다.

\n
\n

Best Web API Design Rules

\n
\n

1. 기본 URL에는 동사가 아닌 명사를 사용, 리소스마다 2개의 기본 URL을 유지하자.

\n
/dogs (Collection), /dogs/1234 (Element)
\n
\n

2. 올바른 HTTP 메서드(POST, GET, PUT, DELETE)를 사용하자.

\n
POST(create), GET(read), PUT(update), DELETE(delete)
\n
\n

3. 복수형 명사와 구체적인 이름을 사용하자.

\n
/animals, /dogs
\n
\n

4. 자원 간의 관계를 간단히 하여 URL 계층이 깊어지는 것을 피하자.

\n
GET\t/owners/5678/dogs?color=red
\n
\n

5. 오류 처리를 명확하게 하고 에러 스택은 절대 비공개 해야 한다.

\n
200 - OK\n400 - Bad Request\n500 - Internal Server Error\n201 - Created\n304 - Not Modified\n404 - Not Found\n401 - Unauthorized\n403 - Forbidden
\n
\n

6. 접두사 \"v\"로 버전을 지정하고 지속적인 버전 관리를 하자.

\n
GET\t/v1/dogs
\n
\n

7. 데이터베이스에 없는 자원에 대한 응답일 경우 동사를 사용하자.

\n
ex) Caculate, Translate, Convert ...
\n
\n

8. 속성의 네이밍은 Javascript의 관습을 따르고 카멜 케이스를 사용하자.

\n
\"createdAt\": 123415125
\n
\n

9. 하위 도메인의 독립적인 API 요청 처리는 통일하자.

\n
company.com\napi.company.com\t(if not exists, redirect)\ndevelopers.company.com
\n
\n

10. 기타

\n\n
\n

NAVER Open API

\n

실제 네이버와 카카오의 오픈 API는 어떻게 디자인되어 있는지 간단히 살펴보자.\nhttps://developers.naver.com 참조

\n
\n

이전(2015년 쯤)에는 네이버에서 제공하는 API를 사용하기 위해 'API 키'라는 유니크한 텍스트 문자열을 발급받고, 이를 API 호출시 같이 API 게이트웨이 서버로 전송함으로써 인증된 사용자임을 입증했다. 새로운 개발자센터에서는 API 키 방식은 더 이상 사용하지 않고 애플리케이션마다 일종의 유니크한 아이디와 비밀번호(클라이언트 아이디, 시크릿)값을 이용해서 인증하고 있다.

\n
\n
\n

1. API 호출 URL과 요청 변수

\n\n
\n

2. 에러 코드 정의 - HTTP status code

\n\n
\n

Kakao REST API (카카오톡, 카카오페이)

\n
\n

1. 먼저, 카카오 로그인 후에 사용자 토큰을 받아온다.

\n

2. 사용자 토큰을 헤더에 담아 GET으로 요청한다.

\n
GET /v1/api/talk/profile HTTP/1.1\nHost: kapi.kakao.com\nAuthorization: Bearer {access_token}
\n
\n

3. 응답은 JSON 형태로 다음과 같은 정보를 포함한다.

\n
{\n \"nickName\":\"홍길동\",\n \"profileImageURL\":\"http://xxx.kakao.co.kr/.../aaa.jpg\",\n \"thumbnailURL\":\"http://xxx.kakao.co.kr/.../bbb.jpg\",\n \"countryISO\":\"KR\"\n}
\n
\n

Response Code Example

\n
{\n  \"meta\": {\n    \"code\": 200,\n    \"response_time\": {\n      \"time\": 0,\n      \"measure\": \"seconds\"\n    }\n  },\n  \"notifications\": {},\n  \"response\": {}\n}
\n
\n

Error Code Example

\n
{\n  \"meta\": {\n    \"code\": 500,\n    \"error_detail\": \"The user has not authorized or the token is invalid.\",\n    \"error_type\": \"invalid_auth\",\n    \"developer_friendly\": \"The user has not authorized or the token is invalid.\",\n    \"response_time\": {\n      \"time\": 0,\n      \"measure\": \"seconds\"\n    }\n  }\n}
\n
\n

참고자료

\n","excerpt":"오픈 API를 사용하다보면 공통의 패턴을 발견할 수 있을 것이다.\n이처럼, API…"}}}},{"node":{"title":"DB 테이블을 DataFrame으로 읽어오는 방법","id":"ea6cffe1-0590-587f-975e-f196ce841ed7","slug":"db-to-dataframe","publishDate":"January 14, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}}},{"node":{"title":"맥북프로 터치바 모델 초기설정","id":"e27d1aed-a171-5885-8d97-8fc290bd164e","slug":"macbook-setting","publishDate":"January 08, 2017","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

\n \n \n \n

\n

1년을 기다린 결과 드디어 맥북프로 터치바 모델을 구매했다!\n맥을 처음 사용하는 입장에서 초기에 어떤 것들을 설정해야 하는지 정리해보았다.

\n
\n

프로그램 관련

\n\n
\n

문서작성

\n\n
\n

커멘드 및 기타 설정

\n\n
\n

후기

\n

윈도우에서는 초기에 환경구축하려면 온갖 삽질을 해야 했는데,\n맥은 금방 설치되는 점이 가장 좋았다.\n터치바를 커스터마이징 하는 것은 나중에 따로 정리해봐야겠다.

","excerpt":"…"}}}},{"node":{"title":"OAuth2에 대해 알아보자","id":"0f396ba5-9160-5a8a-9736-893fdda81cbe","slug":"about-oauth2","publishDate":"January 05, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

먼저 OAuth 인증을 이해하기 위해 필요한 몇 가지 개념들에 대해 알아보자. OAuth 인증을 진행할 때 해당 서비스 제공자는 '제 3자가 어떤 정보나 서비스에 사용자의 권한으로 접근하려 하는데 허용하겠느냐'라는 안내 메시지를 보여 주는 것이다.

\n
\n

인증과 허가

\n\n
\n

일반 로그인은 사원이 63빌딩에 출입하는 것이라면, (사원증이 있어야 출입가능)

\n

OAuth는 1층에서 방문증을 수령한 후 63빌딩에 출입하는 것이다. (방문증만 있어도 출입가능)

\n
\n

OAuth 1.0의 특징

\n

기존의 다른 인증방식(OpenID)과 구분되는 특징은 크게 두 가지이다.

\n
    \n
  1. API 인증 시, 써드파티 어플리케이션에게 사용자의 비번을 노출하지 않고 인증할 수 있다는 점
  2. \n
  3. 인증(Authentication)과 API 권한(Authorization) 부여를 동시에 할 수 있다는 점
  4. \n
\n
\n

OAuth 1.0의 동작방식

\n

OAuth 1.0은 기본적으로 user / consumer / service provider가 있어야 한다.

\n

OAuth 1.0 인증을 3-legged OAuth 라고도 하는데 결국 주체가 셋 이라는 말이다.

\n

\n \n \n \n

\n

우리의 서비스에서 트위터 로그인을 연동한다고 가정해보자. 사용자 입장에서는 아이디 / 비밀번호를 통해 가입하면 그 정보를 이용해서 무슨 짓을 할지 모르기 때문에 꺼려한다. OAuth 1.0은 우리의 서비스(Consumer)에게 인증토큰 (Access Token)만을 전달하고 서비스에서 인증토큰으로 트위터 API(Service Provider)를 사용할 수 있도록 해준다.

\n
\n

Outh 1.0 프로세스

\n
    \n
  1. 사용자(User)가 트위터 로그인 요청
  2. \n
  3. 사용자를 트위터(Service Provider) 로그인 화면으로 리다이렉트
  4. \n
  5. 트위터 로그인 진행
  6. \n
  7. 서비스(Consumer)로 인증토큰(Access Token)이 전달
  8. \n
\n
\n

인증토큰의 장점

\n\n
\n

OAuth 2.0의 개선사항

\n

일단 OAuth 2.0은 1.0과 호환되지 않으며 용어부터 많은 것이 다르다. 모바일에서의 사용성 문제나 서명과 같은 개발이 복잡하고 기능과 규모의 확장성 등을 지원하기 위해 만들어진 표준이다. 표준이 매우 크고 복잡해서 이름도 \"OAuth 인증 프레임워크(OAuth 2.0 Authorization Framework)\" 이다. http://tools.ietf.org/wg/oauth/ 에서 확인 가능

\n
\n

OAuth 1.0에서 개선된 사항

\n
    \n
  1. \n

    용어 변경

    \n
      \n
    • Resource Owner : 사용자
    • \n
    • Resource Server : REST API 서버
    • \n
    • Authorization Server : 인증서버 (API 서버와 같을 수도 있음)
    • \n
    • Client : 써드파티 어플리케이션 (서비스)
    • \n
    \n

    \n
  2. \n
  3. \n

    간단하고 직관적

    \n
      \n
    • OAuth 1.0에서는 HTTPS가 필수
    • \n
    • Signature 없이 생성, 호출 가능
    • \n
    • URL 인코딩이 필요없음
    • \n
    \n
  4. \n
\n
\n
    \n
  1. \n

    더 많은 인증 방법을 지원

    \n
      \n
    • 이전에는 HMAC을 이용한 암호화 인증만 지원
    • \n
    • OAuth 2.0은 여러 인증 방식을 통해 웹 / 모바일 등 다양한 시나리오에 대응 가능
    • \n
    • Access Token의 Life-time을 지정하여 만료일 설정 가능
    • \n
    \n
  2. \n
\n
\n
    \n
  1. \n

    대형 서비스로의 확장성 지원

    \n
      \n
    • 커다란 서비스는 인증 서버를 분리하거나 다중화 할 수 있어야 함
    • \n
    • Authorization Server의 역할을 명확히 하여 이에 대한 고려가 되었음
    • \n
    \n
  2. \n
\n
\n

OAuth 2.0 사용 서비스

\n

2013년까지만 해도 1.0만 지원하거나 2.0으로 개선하는 인터넷 서비스 기업이 많았지만,

\n

현재는 대부분 2.0만 지원한다고 봐도 무방하다. (1.0은 자체 로그인에만 사용하는 기업이 많음)

\n\n
\n

참고하면 좋은 자료

\n","excerpt":"먼저 OAuth 인증을 이해하기 위해 필요한 몇 가지 개념들에 대해 알아보자. OAuth 인증을 진행할 때 해당 서비스 제공자는 '제…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":15,"humanPageNumber":16,"skip":91,"limit":6,"numberOfPages":16,"previousPagePath":"/15","nextPagePath":""}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/2/page-data.json b/page-data/2/page-data.json index b97ec9d..4b26c52 100644 --- a/page-data/2/page-data.json +++ b/page-data/2/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-posts-js","path":"/2","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Airflow worker에 KEDA AutoScaler 적용한 후기","id":"641c0253-f45e-5b70-90a2-43300aece54b","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}}},{"node":{"title":"EKS Karpenter를 활용한 Groupless AutoScaling","id":"bb361222-a98b-57f3-851e-02cf8bd63fc0","slug":"eks-karpenter-groupless-autoscaling","publishDate":"May 13, 2022","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

21년 12월 EKS에서 새로운 쿠버네티스 클러스터 오토스케일러인 Karpenter를 발표했습니다.
\n이후로 많은 사용자들이 오픈소스에 참여하면서 버전도 많이 올라갔고 안정적으로 사용하고 있습니다. 이 글에서는 Karpenter와 기존에 사용하던 Cluster AutoScaler를 비교하고 이관할 때 알아두면 좋은 내용에 대해 정리해보려 합니다.

\n
\n

Cluster AutoScaler가 가진 한계점

\n

\n \n \n \n

\n

그 동안 EKS의 Cluster AutoScaler는 AWS의 AutoScaling Group(ASG) 을 활용하고 있었습니다. ASG는 주기적으로 현재 상태를 확인하고 Desired State로 변화하는 방식으로 동작합니다. 사용자는 목적에 맞게 노드 그룹을 나누고 ASG의 Min, Max 설정을 통해 클러스터 노드 수를 제한할 수 있습니다. 이를 통해 기존 AWS 사용자가 직관적인 구조를 그대로 활용할 수 있었습니다. 하지만 클러스터의 규모가 커질수록 ASG 활용으로 인해 불편한 점이 존재했습니다.

\n

1. 번거로운 ASG 노드 그룹 관리
\nK8S 클러스터는 여러 조직이 함께 사용할 수 있는 멀티테넌트 구조를 지원합니다. 두 조직이 서비스의 안정적인 운영을 위해 노드 그룹을 격리해야 하는 요구사항이 생기면 EKS 운영자는 새로운 ASG 노드 그룹을 생성하고 관리해주어야 합니다. 많은 운영자가 EKS의 IaC 구현을 위해 terraform-aws-eks 모듈을 사용하는데 여기에 매번 설정을 업데이트하고 반영하는 일은 번거롭고 각 조직에게 역할을 위임하기도 애매합니다.

\n

또 다른 예시는 리소스 활용 목적에 따라 노드 그룹을 분리할 때 입니다. 많은 CPU가 필요한 워크로드는 컴퓨팅 최적화 인스턴스 유형을 사용하고 메모리가 필요한 워크로드는 메모리 최적화 인스턴스 유형을 사용하는 것이 효율적입니다. 그리고 비용 최적화를 위해 spot 인스턴스 유형을 사용할 수도 있습니다. 이를 구현하기 위해 ASG에서는 c타입, r타입, spot 인스턴스를 가지는 각 노드 그룹을 만들어주어야 합니다.

\n

2. ASG로 인한 노드 프로비저닝 시간 지연
\nEKS Cluster AutoScaler는 K8S의 Cluster AutoScaler에 ASG를 활용하여 AWS cloud provider를 구현한 형태입니다. 클러스터 내에서 어플리케이션 로드를 감지한 이후, 중간에 AWS 리소스 요청을 거치기 때문에 즉시 처리되기가 어렵습니다.

\n
\n

Karpenter 소개

\n

\n \n \n \n

\n

Karpenter는 다음과 같이 세 가지 컴포넌트로 구성되어 있습니다.

\n\n

Karpenter Helm Chart를 통해 설치하면 controller와 webhook pod가 생성됩니다. 이후에 provisioner CRD를 정의하고 클러스터에 배포하면 사용할 수 있습니다. provisioner는 ASG 노드 그룹과 유사한 개념입니다. 따라서 default를 사용하는게 아니라 기존에 사용하던 설정에 맞게 새로 만들어야 합니다. Scale In/Out 관련된 내용은 다음과 같습니다.

\n

Scale Out 기준

\n\n

Scale In 기준

\n\n
\n

Karpenter vs AutoScaler

\n

앞서 언급했던 Cluster AutoScaler와 Karpenter를 비교해보면 다음과 같습니다.

\n

1. Provisioner API를 통해 간편한 노드 관리

\n
requirements:\n  - key: \"node.kubernetes.io/instance-type\"\n    operator: In\n    values: [\"m5.large\", \"m5.2xlarge\"]\n  - key: \"topology.kubernetes.io/zone\"\n    operator: In\n    values: [\"ap-northeast-2a\", \"ap-northeast-2c\"]\n  - key: \"karpenter.sh/capacity-type\"\n    operator: In\n    values: [\"spot\", \"on-demand\"]
\n

Karpenter는 노드 프로비저닝을 위해 ASG 노드 그룹을 생성할 필요가 없습니다. 대신 yaml을 통해 Provisioner CRD만 생성하면 됩니다. 현재 노드 프로비저닝을 위한 instance type, subnet, volume, SG 등 대부분의 설정을 지원하고 있습니다.

\n

2. 수 많은 인스턴스 유형에 대해 유연하게 처리
\nKarpenter는 노드 프로비저닝을 위해 EC2 Fleet API를 사용합니다. 사용자는\n여러 유형의 인스턴스를 지정할 수 있으며 어떤 유형의 인스턴스를 생성할지는 Karpenter가 결정합니다. 예를 들어 pending 상태의 pod가 1CPU, 4GB 리소스를 요청한다면 m5.large 인스턴스를 생성합니다. spot 인스턴스의 경우, Fleet API의 최저 입찰 경쟁에 따라 저렴한 비용으로 사용할 수 있습니다.

\n

3. 노드 프로비저닝 시간 단축
\nKarpenter는 Cluster AutoScaler와 동일한 역할을 하지만 자체 구현된 오픈소스로 JIT(Just-In-Time)을 지원합니다. 적용한 이후 실제로 약 2배 정도 프로비저닝 시간이 단축되었습니다. Karpenter를 통해 생성된 노드는 pre-pulling을 통해 이미지를 미리 받아올 수 있으며 빠른 컨테이너 런타임 준비를 통해 pod를 즉시 바인딩할 수 있습니다.

\n

두 가지 AutoScaler는 여러 장단점이 존재하기 때문에 적절하게 선택할 필요가 있습니다. 데이터 영역에서 활용하는 클러스터는 다양한 인스턴스 유형을 사용하고 빈번하게 스케일 조정이 일어나는 경우가 많습니다. 따라서 Karpenter가 가지는 장점을 최대로 활용할 수 있습니다.

\n
\n

Karpenter 이관 가이드

\n

최근에 공식 이관 가이드가 나와서 제가 사용했던 이관 방법들과 주의사항 위주로 정리해보았습니다.

\n

이관 방법

\n\n
\n

Karpenter가 가지는 제한 사항

\n\n
\n

Reference

\n","excerpt":"21년 12월 EKS에서 새로운 쿠버네티스 클러스터 오토스케일러인 Karpenter…"}}}},{"node":{"title":"개발자가 의사결정을 기록하는 방법 (feat. ADR)","id":"89d11bbc-7231-5fe4-b919-ca586e517cb8","slug":"feat-adr","publishDate":"December 04, 2021","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

개발자들에게는 항상 다양한 선택지 중에 하나를 골라야 하는 상황이 주어집니다.
\n가장 간단한 예시로는 어떤 언어/프레임워크를 사용할지, 어느 버전을 사용할지에 대한 결정입니다.
\n오늘은 위와 같은 의사결정을 기록하기 위한 ADR에 대해 소개하려고 합니다.

\n
\n

ADR이란?

\n

ADR은 Architectural Decision Records의 약자로 아키텍쳐와 관련된 결정을 내렸을 때 그 과정을 기록해 두는 문서를 말합니다. 아마 ADR이라는 단어를 몰라도 큰 규모의 오픈소스를 사용하다보면 많이 접해보셨을거라 생각합니다. 예를 들어 Kubernetes 프로젝트에서는 개선 과제를 제안할 때 KEP 템플릿을 사용하여 문서를 작성하도록 가이드하고 있습니다.

\n

ADR은 간단한 양식을 통해 마크다운 형식으로 작성되며 문제 정의, 결정에 영향을 주는 기본 요구사항, 설계 결정 등의 내용이 포함되어 있습니다. GitHub, Spotify, Google 등 다양한 tech 기업들이 ADR형식을 사용하고 있습니다.

\n
\n

ADR이 필요한 이유

\n

우리는 어떤 방식으로든 팀원들과 함께 의사결정하고 공유하게 됩니다. 그런데 새로운 팀원이 들어오고 히스토리에 대해 묻는다면 기억을 더듬어 당시에 왜 그렇게 했는지 설명합니다. 이러한 과정을 반복하며 시간을 낭비하고 있다면 ADR을 통해 해결할 수 있습니다. 많은 사람들이 말하는 ADR 도입의 장점은 아래와 같습니다.

\n

명확하고 합리적인 의사결정을 내릴 수 있습니다
\n정의된 ADR 템플릿에 따라 문서화하면 일관된 방식으로 의사결정할 수 있습니다.\n저자는 문서를 작성하는 과정에서 더 합리적인 결론을 도출해낼 수 있으며 독자는 문제에 대해 쉽게 이해할 수 있습니다.

\n

새로운 팀원이 적응하는데 많은 도움이 됩니다
\n새로운 팀원이 들어오면 새로운 개발환경, 아키텍쳐를 이해하기까지 많은 시간을 할애합니다.\n만약 ADR을 통해 과거 의사결정 과정까지 알게 된다면 더 쉽게 이해할 수 있습니다.

\n
\n

ADR template

\n

ADR 형식은 정하기 나름이지만 이 글에서는 가장 알려진 템플릿을 기준으로 설명하겠습니다.\narchitecture-decision-record GitHub에서 더 다양한 템플릿과 예시 문서를 확인할 수 있습니다.

\n

1. Status
\n\n \n \n \n

\n

먼저 status는 위와 같은 상태 다이어그램으로 표현되며 현재 문서의 상태를 나타냅니다.

\n

2. Context
\nContext는 해결하고자 하는 문제를 정의하는 목차입니다.

\n

3. Decision
\n\n \n \n \n

\n

Decision에서는 제안하고자 하는 내용 및 해당 결정의 이유에 대해 설명합니다.
\n의사결정 과정에서 고려했던 대안들과 장단점에 대한 내용도 포함됩니다.
\n위와 같이 간단히 비교하는 표를 추가한다면 읽는 사람들이 더 쉽게 이해할 수 있습니다.

\n

4. Consequences
\nConsequences에서는 결정을 통해 사용자가 받는 영향에 대해 정의합니다.\n예를 들어 이 결정이 도입된다면 어떤 효과가 나타날 수 있는지, 마이그레이션 과제라면 다운타임이 발생하는지, 사용자들의 코드 변경이 필요한지 등을 작성합니다.

\n
\n

새로 도입할 때는 ADR이 부담스러운 업무가 되지 않도록 가능한 가볍게 유지할 수 있어야 합니다.
\nADR은 나를 위한 것이 아니라 현재 그리고 미래의 팀원들을 위한 것이라고 합니다.
\n그 동안 문서로 작성 안했다면 아주 간단하게 시작해보는건 어떨까요?

\n
\n

참고 자료

\n","excerpt":"…"}}}},{"node":{"title":"JupyterHub에 Tensorboard 연동하기","id":"ab488772-c7d7-5f01-8241-ce087829c842","slug":"jupyterhub-tensorboard","publishDate":"October 23, 2021","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이 글에서는 JupyterHub 사용자 환경에 tensorboard를 proxy 형태로 연동하는 방법에 대해 정리해보려고 합니다. 연동 과정으로 jupyter-server-proxy 라는 extension을 사용합니다.

\n
\n

기존 연동 방식

\n

Jupyter Notebook에 Tensorboard를 연동하는 가장 쉬운 방법은 공식문서에 나와있는 %tensorboard 를 사용하는 방법입니다.

\n
%load_ext tensorboard\n%tensorboard --logdir logs
\n

이 방법은 간단하지만 노트북 내에서 접근하거나 IP주소:포트번호를 통해 접근하게 됩니다.

\n

따라서 JupyterHub와 같이 여러 사용자가 쓰는 환경이라면 나의 Tensorboard 프로세스에 어떤 주소를 통해 접근해야 하는지 매번 찾아야 합니다.\n또한 JupyterHub는 인증 과정을 거치는 반면 프로세스로 직접 띄우는 텐서보드는 인증 없이 접근이 가능해집니다.

\n
\n

jupyter-server-proxy

\n

jupyter-server-proxy는 외부 웹 서비스의 프록시를 지원하는 extension 입니다.\njupyter-server-proxy를 통해 연동하면 다음과 같은 이점을 가질 수 있습니다.

\n\n
\n

jupyter-tensorboard-proxy 설치

\n
# pip install jupyter-tensorboard-proxy\n\n# log path\nlog_dir = \"/home/jovyan/logs/\" + datetime.datetime.now().strftime(\"%Y%m%d-%H%M\")\ntensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1)
\n

설치 방법은 아주 간단합니다. singleuser profile 이미지에 위의 패키지만 설치해주면 됩니다. 기본으로 바라보는 로그 경로는 $HOME/logs 입니다. 따라서 tensorflow 코드에서 로그 경로를 연결해주어야 합니다.

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":1,"humanPageNumber":2,"skip":7,"limit":6,"numberOfPages":16,"previousPagePath":"/","nextPagePath":"/3"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/2","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"쿠버네티스에서 GPU 리소스를 효율적으로 활용하는 방법","id":"c5510818-773d-5ec0-8047-6f2c4c31d67f","slug":"gpu-utilization","publishDate":"July 08, 2022","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

GPU는 강력한 연산 기능을 제공하지만 비용이 많이 들기 때문에 제한된 리소스를 효율적으로 활용하는 것이 중요합니다. 이번 글에서는 NVIDIA GPU의 리소스 공유를 지원하기 위한 방법으로 Time SlicingMIG에 대해 정리해보려 합니다.

\n
\n

GPU 리소스가 낭비되고 있다?

\n

\n \n \n \n

\n

여러 아키텍쳐(암페어, 파스칼 등)로 구성된 GPU들을 모아 쿠버네티스 노드 풀을 구성하고 사용자들은 GPU 리소스를 할당받아 사용하는 환경이라고 가정해보겠습니다. 사용자들은 GPU 할당을 못 받는 상황임에도 실제 GPU 사용량을 측정해보면 생각보다 낮게 유지되고 있는 경우가 있습니다. 워크로드에 따라 필요한 리소스가 다르기 때문입니다.

\n

노트북 환경은 항상 개발을 하는게 아니기 때문에 idle 상태로 대기하는 시간이 많습니다. 작은 배치 사이즈로 운영되는 인퍼런스의 경우, 트래픽에 따라 사용량이 달라질 수 있습니다.\n따라서 이런 상황에서는 항상 리소스를 점유하기 보다 필요할 때 bursting 가능한 방식으로 운영하는 것이 효율적입니다.

\n
apiVersion: v1\nkind: Pod\nmetadata:\n  name: cuda-vector-add\nspec:\n  restartPolicy: OnFailure\n  containers:\n    - name: cuda-vector-add\n      image: \"k8s.gcr.io/cuda-vector-add:v0.1\"\n      resources:\n        limits:\n          nvidia.com/gpu: 1 # GPU 1개 요청하기
\n

쿠버네티스에서는 디바이스 플러그인을 통해 Pod가 GPU 리소스를 요청할 수 있습니다.\n하지만 Pod는 하나 이상의 GPU만 요청할 수 있으며 CPU와 달리 GPU의 일부(fraction)를 요청하는 것은 불가능합니다. 예를 들어 간단한 실험에 최신 버전의 고성능 GPU 1개를 온전히 할당 받는 것은 낭비입니다. NVIDIA 문서에서는 SW/HW 관점에서 GPU 리소스를 효율적으로 사용하기 위해 다양한 방법을 소개합니다. 그 중 Time SlicingMIG에 대해 알아보겠습니다.

\n
\n

Time Slicing

\n

Time Slicing은 GPU의 시간 분할 스케줄러입니다.\n파스칼 아키텍쳐부터 지원하는 compute preemption 기능을 활용한 방법입니다.\n각 컨테이너는 공평하게 timeslice를 할당받게 되지만 전환할 때 context switching 비용이 발생합니다.

\n
kind: ConfigMap\nmetadata:\n  name: time-slicing-config\n  namespace: gpu-operator\ndata:\n  a100-40gb: |-\n    version: v1\n    sharing:\n      timeSlicing:\n        resources:\n        - name: nvidia.com/gpu\n          replicas: 8\n        - name: nvidia.com/mig-1g.5gb\n          replicas: 1\n  tesla-t4: |-\n    version: v1\n    sharing:\n      timeSlicing:\n        resources:\n        - name: nvidia.com/gpu\n          replicas: 4
\n

NVIDIA GPU Operator에서는 위와 같이 ConfigMap을 사용하거나 node label을 통해 설정할 수 있습니다. 설정한 이후에 노드를 확인해보면 아래와 같이 리소스에 값이 추가된 것을 확인할 수 있습니다.

\n
$ kubectl describe node $NODE\n\nstatus:\n  capacity:\n    nvidia.com/gpu: 8\n  allocatable:\n    nvidia.com/gpu: 8
\n

최대 8개 컨테이너까지 timeslice 방식으로 shared GPU를 사용할 수 있다는 것을 의미합니다. 이 방법은 GPU 메모리 limit 설정을 강제하는 것이 아니기 때문에 OOM이 발생할 수도 있습니다. 이를 방지하려면 GPU를 사용하는 컨테이너 수를 모니터링하고 TensorflowPyTorch 같은 프레임워크에서 총 GPU 메모리 제한 설정이 필요합니다.

\n
\n

Multi instance GPU (MIG)

\n

\n \n \n \n

\n

MIG는 A100과 같은 암페어 아키텍처 기반 GPU를 최대 7개의 개별 GPU 인스턴스로 분할해서 사용할 수 있는 기능입니다.\n분할된 인스턴스를 파티션이라고 부르는데, 각 파티션은 물리적으로 격리되어 있기 때문에 안전하게 병렬로 사용할 수 있습니다.

\n

\n \n \n \n

\n

두 방식을 비교해보면 위의 표와 같습니다.\nTime Slicing 방식은 7개 이상의 컨테이너를 사용할 수 있습니다. 따라서 bursting 워크로드에 적합한 방식이라고 볼 수 있습니다. 반면 MIG는 적은 양의 고정된 사용량을 가지는 워크로드에 적합합니다.\nA100은 MIG를 통해 분할하고 그 외의 GPU는 Time Slicing을 사용하는 방식으로 함께 사용할 수 있으니 워크로드에 맞는 방식을 선택하는 것이 중요합니다.

\n
\n

Reference

\n","excerpt":"GPU는 강력한 연산 기능을 제공하지만 비용이 많이 들기 때문에 제한된 리소스를 효율적으로 활용하는 것이 중요합니다. 이번 글에서는 NVIDIA…"}}}},{"node":{"title":"Airflow worker에 KEDA AutoScaler 적용한 후기","id":"641c0253-f45e-5b70-90a2-43300aece54b","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}}},{"node":{"title":"EKS Karpenter를 활용한 Groupless AutoScaling","id":"bb361222-a98b-57f3-851e-02cf8bd63fc0","slug":"eks-karpenter-groupless-autoscaling","publishDate":"May 13, 2022","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAAABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAADTAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJDAAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAEFBP/EACIQAAICAgIABwAAAAAAAAAAAAECABEDBBIhQVFSYqHB0f/EABUBAQEAAAAAAAAAAAAAAAAAAAIE/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oADAMBAAIRAxEAPwBnZyBeRZVPlzr6mc7mVspLMle7LY+KkobTN2QOuoxkJBYEihdeEoTbVlN4KKLoTfrP7CQTvOOioMIdhZX/2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

21년 12월 EKS에서 새로운 쿠버네티스 클러스터 오토스케일러인 Karpenter를 발표했습니다.
\n이후로 많은 사용자들이 오픈소스에 참여하면서 버전도 많이 올라갔고 안정적으로 사용하고 있습니다. 이 글에서는 Karpenter와 기존에 사용하던 Cluster AutoScaler를 비교하고 이관할 때 알아두면 좋은 내용에 대해 정리해보려 합니다.

\n
\n

Cluster AutoScaler가 가진 한계점

\n

\n \n \n \n

\n

그 동안 EKS의 Cluster AutoScaler는 AWS의 AutoScaling Group(ASG) 을 활용하고 있었습니다. ASG는 주기적으로 현재 상태를 확인하고 Desired State로 변화하는 방식으로 동작합니다. 사용자는 목적에 맞게 노드 그룹을 나누고 ASG의 Min, Max 설정을 통해 클러스터 노드 수를 제한할 수 있습니다. 이를 통해 기존 AWS 사용자가 직관적인 구조를 그대로 활용할 수 있었습니다. 하지만 클러스터의 규모가 커질수록 ASG 활용으로 인해 불편한 점이 존재했습니다.

\n

1. 번거로운 ASG 노드 그룹 관리
\nK8S 클러스터는 여러 조직이 함께 사용할 수 있는 멀티테넌트 구조를 지원합니다. 두 조직이 서비스의 안정적인 운영을 위해 노드 그룹을 격리해야 하는 요구사항이 생기면 EKS 운영자는 새로운 ASG 노드 그룹을 생성하고 관리해주어야 합니다. 많은 운영자가 EKS의 IaC 구현을 위해 terraform-aws-eks 모듈을 사용하는데 여기에 매번 설정을 업데이트하고 반영하는 일은 번거롭고 각 조직에게 역할을 위임하기도 애매합니다.

\n

또 다른 예시는 리소스 활용 목적에 따라 노드 그룹을 분리할 때 입니다. 많은 CPU가 필요한 워크로드는 컴퓨팅 최적화 인스턴스 유형을 사용하고 메모리가 필요한 워크로드는 메모리 최적화 인스턴스 유형을 사용하는 것이 효율적입니다. 그리고 비용 최적화를 위해 spot 인스턴스 유형을 사용할 수도 있습니다. 이를 구현하기 위해 ASG에서는 c타입, r타입, spot 인스턴스를 가지는 각 노드 그룹을 만들어주어야 합니다.

\n

2. ASG로 인한 노드 프로비저닝 시간 지연
\nEKS Cluster AutoScaler는 K8S의 Cluster AutoScaler에 ASG를 활용하여 AWS cloud provider를 구현한 형태입니다. 클러스터 내에서 어플리케이션 로드를 감지한 이후, 중간에 AWS 리소스 요청을 거치기 때문에 즉시 처리되기가 어렵습니다.

\n
\n

Karpenter 소개

\n

\n \n \n \n

\n

Karpenter는 다음과 같이 세 가지 컴포넌트로 구성되어 있습니다.

\n\n

Karpenter Helm Chart를 통해 설치하면 controller와 webhook pod가 생성됩니다. 이후에 provisioner CRD를 정의하고 클러스터에 배포하면 사용할 수 있습니다. provisioner는 ASG 노드 그룹과 유사한 개념입니다. 따라서 default를 사용하는게 아니라 기존에 사용하던 설정에 맞게 새로 만들어야 합니다. Scale In/Out 관련된 내용은 다음과 같습니다.

\n

Scale Out 기준

\n\n

Scale In 기준

\n\n
\n

Karpenter vs AutoScaler

\n

앞서 언급했던 Cluster AutoScaler와 Karpenter를 비교해보면 다음과 같습니다.

\n

1. Provisioner API를 통해 간편한 노드 관리

\n
requirements:\n  - key: \"node.kubernetes.io/instance-type\"\n    operator: In\n    values: [\"m5.large\", \"m5.2xlarge\"]\n  - key: \"topology.kubernetes.io/zone\"\n    operator: In\n    values: [\"ap-northeast-2a\", \"ap-northeast-2c\"]\n  - key: \"karpenter.sh/capacity-type\"\n    operator: In\n    values: [\"spot\", \"on-demand\"]
\n

Karpenter는 노드 프로비저닝을 위해 ASG 노드 그룹을 생성할 필요가 없습니다. 대신 yaml을 통해 Provisioner CRD만 생성하면 됩니다. 현재 노드 프로비저닝을 위한 instance type, subnet, volume, SG 등 대부분의 설정을 지원하고 있습니다.

\n

2. 수 많은 인스턴스 유형에 대해 유연하게 처리
\nKarpenter는 노드 프로비저닝을 위해 EC2 Fleet API를 사용합니다. 사용자는\n여러 유형의 인스턴스를 지정할 수 있으며 어떤 유형의 인스턴스를 생성할지는 Karpenter가 결정합니다. 예를 들어 pending 상태의 pod가 1CPU, 4GB 리소스를 요청한다면 m5.large 인스턴스를 생성합니다. spot 인스턴스의 경우, Fleet API의 최저 입찰 경쟁에 따라 저렴한 비용으로 사용할 수 있습니다.

\n

3. 노드 프로비저닝 시간 단축
\nKarpenter는 Cluster AutoScaler와 동일한 역할을 하지만 자체 구현된 오픈소스로 JIT(Just-In-Time)을 지원합니다. 적용한 이후 실제로 약 2배 정도 프로비저닝 시간이 단축되었습니다. Karpenter를 통해 생성된 노드는 pre-pulling을 통해 이미지를 미리 받아올 수 있으며 빠른 컨테이너 런타임 준비를 통해 pod를 즉시 바인딩할 수 있습니다.

\n

두 가지 AutoScaler는 여러 장단점이 존재하기 때문에 적절하게 선택할 필요가 있습니다. 데이터 영역에서 활용하는 클러스터는 다양한 인스턴스 유형을 사용하고 빈번하게 스케일 조정이 일어나는 경우가 많습니다. 따라서 Karpenter가 가지는 장점을 최대로 활용할 수 있습니다.

\n
\n

Karpenter 이관 가이드

\n

최근에 공식 이관 가이드가 나와서 제가 사용했던 이관 방법들과 주의사항 위주로 정리해보았습니다.

\n

이관 방법

\n\n
\n

Karpenter가 가지는 제한 사항

\n\n
\n

Reference

\n","excerpt":"21년 12월 EKS에서 새로운 쿠버네티스 클러스터 오토스케일러인 Karpenter…"}}}},{"node":{"title":"개발자가 의사결정을 기록하는 방법 (feat. ADR)","id":"89d11bbc-7231-5fe4-b919-ca586e517cb8","slug":"feat-adr","publishDate":"December 04, 2021","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

개발자들에게는 항상 다양한 선택지 중에 하나를 골라야 하는 상황이 주어집니다.
\n가장 간단한 예시로는 어떤 언어/프레임워크를 사용할지, 어느 버전을 사용할지에 대한 결정입니다.
\n오늘은 위와 같은 의사결정을 기록하기 위한 ADR에 대해 소개하려고 합니다.

\n
\n

ADR이란?

\n

ADR은 Architectural Decision Records의 약자로 아키텍쳐와 관련된 결정을 내렸을 때 그 과정을 기록해 두는 문서를 말합니다. 아마 ADR이라는 단어를 몰라도 큰 규모의 오픈소스를 사용하다보면 많이 접해보셨을거라 생각합니다. 예를 들어 Kubernetes 프로젝트에서는 개선 과제를 제안할 때 KEP 템플릿을 사용하여 문서를 작성하도록 가이드하고 있습니다.

\n

ADR은 간단한 양식을 통해 마크다운 형식으로 작성되며 문제 정의, 결정에 영향을 주는 기본 요구사항, 설계 결정 등의 내용이 포함되어 있습니다. GitHub, Spotify, Google 등 다양한 tech 기업들이 ADR형식을 사용하고 있습니다.

\n
\n

ADR이 필요한 이유

\n

우리는 어떤 방식으로든 팀원들과 함께 의사결정하고 공유하게 됩니다. 그런데 새로운 팀원이 들어오고 히스토리에 대해 묻는다면 기억을 더듬어 당시에 왜 그렇게 했는지 설명합니다. 이러한 과정을 반복하며 시간을 낭비하고 있다면 ADR을 통해 해결할 수 있습니다. 많은 사람들이 말하는 ADR 도입의 장점은 아래와 같습니다.

\n

명확하고 합리적인 의사결정을 내릴 수 있습니다
\n정의된 ADR 템플릿에 따라 문서화하면 일관된 방식으로 의사결정할 수 있습니다.\n저자는 문서를 작성하는 과정에서 더 합리적인 결론을 도출해낼 수 있으며 독자는 문제에 대해 쉽게 이해할 수 있습니다.

\n

새로운 팀원이 적응하는데 많은 도움이 됩니다
\n새로운 팀원이 들어오면 새로운 개발환경, 아키텍쳐를 이해하기까지 많은 시간을 할애합니다.\n만약 ADR을 통해 과거 의사결정 과정까지 알게 된다면 더 쉽게 이해할 수 있습니다.

\n
\n

ADR template

\n

ADR 형식은 정하기 나름이지만 이 글에서는 가장 알려진 템플릿을 기준으로 설명하겠습니다.\narchitecture-decision-record GitHub에서 더 다양한 템플릿과 예시 문서를 확인할 수 있습니다.

\n

1. Status
\n\n \n \n \n

\n

먼저 status는 위와 같은 상태 다이어그램으로 표현되며 현재 문서의 상태를 나타냅니다.

\n

2. Context
\nContext는 해결하고자 하는 문제를 정의하는 목차입니다.

\n

3. Decision
\n\n \n \n \n

\n

Decision에서는 제안하고자 하는 내용 및 해당 결정의 이유에 대해 설명합니다.
\n의사결정 과정에서 고려했던 대안들과 장단점에 대한 내용도 포함됩니다.
\n위와 같이 간단히 비교하는 표를 추가한다면 읽는 사람들이 더 쉽게 이해할 수 있습니다.

\n

4. Consequences
\nConsequences에서는 결정을 통해 사용자가 받는 영향에 대해 정의합니다.\n예를 들어 이 결정이 도입된다면 어떤 효과가 나타날 수 있는지, 마이그레이션 과제라면 다운타임이 발생하는지, 사용자들의 코드 변경이 필요한지 등을 작성합니다.

\n
\n

새로 도입할 때는 ADR이 부담스러운 업무가 되지 않도록 가능한 가볍게 유지할 수 있어야 합니다.
\nADR은 나를 위한 것이 아니라 현재 그리고 미래의 팀원들을 위한 것이라고 합니다.
\n그 동안 문서로 작성 안했다면 아주 간단하게 시작해보는건 어떨까요?

\n
\n

참고 자료

\n","excerpt":"…"}}}},{"node":{"title":"JupyterHub에 Tensorboard 연동하기","id":"ab488772-c7d7-5f01-8241-ce087829c842","slug":"jupyterhub-tensorboard","publishDate":"October 23, 2021","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이 글에서는 JupyterHub 사용자 환경에 tensorboard를 proxy 형태로 연동하는 방법에 대해 정리해보려고 합니다. 연동 과정으로 jupyter-server-proxy 라는 extension을 사용합니다.

\n
\n

기존 연동 방식

\n

Jupyter Notebook에 Tensorboard를 연동하는 가장 쉬운 방법은 공식문서에 나와있는 %tensorboard 를 사용하는 방법입니다.

\n
%load_ext tensorboard\n%tensorboard --logdir logs
\n

이 방법은 간단하지만 노트북 내에서 접근하거나 IP주소:포트번호를 통해 접근하게 됩니다.

\n

따라서 JupyterHub와 같이 여러 사용자가 쓰는 환경이라면 나의 Tensorboard 프로세스에 어떤 주소를 통해 접근해야 하는지 매번 찾아야 합니다.\n또한 JupyterHub는 인증 과정을 거치는 반면 프로세스로 직접 띄우는 텐서보드는 인증 없이 접근이 가능해집니다.

\n
\n

jupyter-server-proxy

\n

jupyter-server-proxy는 외부 웹 서비스의 프록시를 지원하는 extension 입니다.\njupyter-server-proxy를 통해 연동하면 다음과 같은 이점을 가질 수 있습니다.

\n\n
\n

jupyter-tensorboard-proxy 설치

\n
# pip install jupyter-tensorboard-proxy\n\n# log path\nlog_dir = \"/home/jovyan/logs/\" + datetime.datetime.now().strftime(\"%Y%m%d-%H%M\")\ntensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1)
\n

설치 방법은 아주 간단합니다. singleuser profile 이미지에 위의 패키지만 설치해주면 됩니다. 기본으로 바라보는 로그 경로는 $HOME/logs 입니다. 따라서 tensorflow 코드에서 로그 경로를 연결해주어야 합니다.

\n

\n '

\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}}},{"node":{"title":"여러 조직이 함께 사용하는 Airflow 만들기","id":"0d51ef05-306f-56ae-b726-ab2712215dec","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}}},{"node":{"title":"사이드카 컨테이너로 Airflow 기능 확장하기","id":"381770e9-3117-58b1-979e-b4b146f5a7b3","slug":"airflow-sidecar","publishDate":"August 01, 2021","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow 2.1 버전부터 공식 Helm Chart가 정식 릴리즈 되었습니다.\n오늘은 공식 차트에서 사용할 수 있는 기능 중 extraContainers 옵션을 활용하는 방법을 3가지 예시를 통해 소개해보려 합니다.

\n\n
\n

Sidecar Container

\n

분산 컨테이너 환경에서 사이드카 패턴이란 Pod 안에서 두 개 이상의 컨테이너로 구성되어 있는 형태를 말합니다. 컨테이너들은 서로 네트워크 또는 볼륨을 공유할 수 있습니다. 사이드카 컨테이너를 활용하면 다음과 장점을 가져갈 수 있습니다.

\n

기존 로직의 변경 없이 새로운 기능 추가:\n가끔 일부 기능 추가를 위해 Airflow 저장소 코드를 수정하는 경우가 생길 수 있습니다.\n하지만 이렇게 한번 수정하고 나면 이후에 버전 업데이트할 때마다 새로운 버전 브랜치와 병합해야 하는 번거로움이 생깁니다. 만약 원하는 기능이 사이드카 컨테이너를 활용할 수 있다면 기존 저장소의 변경 없이 새로운 기능을 추가할 수 있습니다.

\n

컨테이너 재사용:\n사내에서 개발 환경에 따라 또는 접근 권한에 따라 Airflow 인스턴스를 여러 개 구성하고 운영하는 경우가 많습니다. 사이드카 컨테이너로 구성한 기능은 재사용이 가능하기 때문에 새로 배포한 Airflow 인스턴스에 쉽게 적용할 수 있습니다.

\n
\n

Airflow extraContainers

\n

Airflow Helm Chart에서는 extraContainers 옵션을 통해 사이드카 컨테이너를 scheduler, webserver, worker에 정의할 수 있습니다. 제가 기여한 옵션입니다! (https://github.com/apache/airflow/pull/13735)

\n

이제 몇 가지 예시를 통해 어떻게 활용할 수 있는지 알아보겠습니다.

\n
\n

1. S3 Sync Container

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":2,"humanPageNumber":3,"skip":13,"limit":6,"numberOfPages":16,"previousPagePath":"/2","nextPagePath":"/4"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/3","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"JupyterHub on Kubernetes","id":"87397863-28d6-5e79-898e-aeccb9f21920","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}}},{"node":{"title":"Data Mesh 아키텍쳐의 네 가지 원칙","id":"37bd75cd-1b56-5ac3-be4b-f45a76e99e36","slug":"data-mesh-principle","publishDate":"September 25, 2021","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

이 글은 martinfowler.com의 Data Mesh Principles and Logical Architecture 원문을 정리한 내용입니다. Data Mesh 아키텍쳐의 네 가지 원칙에 대한 내용은 How to Move Beyond a Monolithic Data Lake to a Distributed Data Mesh의 후속글 입니다.

\n
\n

The great divide of data

\n

오늘 날의 데이터 환경은 운영 데이터 영역분석 데이터 영역으로 나누어 볼 수 있습니다. 운영 데이터는 주로 마이크로서비스에서 사용하는 데이터베이스에 해당하며 트랜잭션과 비즈니스 요구사항을 담고 있습니다. 분석 데이터는 특정 시간 경과에 따라 집계된 비즈니스 데이터이며 주로 BI / 분석 리포트나 ML 모델링에 사용됩니다.

\n

\"data-planes\"

\n

데이터 아키텍쳐와 조직 구조 또한 두 가지 데이터 영역을 반영합니다.\n운영 환경으로부터 데이터를 가져오고 ETL 프로세스를 거쳐 분석 데이터를 생성합니다.\n그리고 분석 데이터를 또 다시 운영 환경에 활용하는 경우가 많습니다.\n이러한 데이터 흐름은 빈번한 ETL 프로세스의 실패와 복잡한 파이프라인으로 이어졌습니다.

\n

\"dw\"

\n

분석 데이터 영역은 데이터 레이크와 데이터 웨어하우스라는 아키텍쳐로 나누어집니다.\n데이터 레이크는 데이터 사이언스 환경을 지원하며 데이터 웨어하우스는 분석 리포트 및 BI 도구를 지원합니다.

\n

\"datalake\"

\n

Data Mesh에서는 분석 데이터 영역에 중점을 두고 두 가지 데이터 영역을 연결하고 합니다.\n두 가지 영역의 데이터를 관리하기 위해 기술 스택을 나누고 조직과 팀을 분리하면 안 됩니다.\n마이크로서비스 아키텍쳐로 인해 운영 데이터도 과거에 비해 많이 성숙해졌으며 데이터는 각 마이크로서비스의 API를 통해 제어됩니다. 하지만 분석 데이터에 대한 관리 및 접근 제어는 여전히 어려운 과제로 남아있습니다. Data Mesh는 이 부분을 중점적으로 해결하고 합니다.

\n

Data Mesh의 목표는 분석 데이터와 히스토리로부터 가치를 얻기 위한 기반을 만드는 것 입니다.\n데이터 환경의 지속적인 변화에도 대응하고 데이터의 품질과 무결성을 제공하면서 데이터 사용에 대한 다양한 요구사항을 지원할 수 있어야 합니다. 이 글에서는 이를 달성하기 위한 네 가지 원칙을 제안합니다.

\n
\n

Domain Ownership

\n

Data Mesh는 지속적인 변화와 확장성을 지원하기 위해 데이터를 가장 잘 이해하는 사람들에게 책임을 분산하고 탈중앙화하는데 기반을 두고 있습니다. 여기서 분석 데이터, 메타 데이터에 대한 소유권을 어떻게 나누어야 하는지에 대한 의문이 생기게 됩니다.

\n

요즘 조직 구조는 비즈니스 도메인을 기준으로 나누어집니다. 이러한 구조를 통해 도메인 경계에 따라 지속적인 발전을 할 수 있게 만듭니다. 따라서 비즈니스 도메인의 경계(Bounded Context)를 기준으로 나누는 것이 적절하다고 볼 수 있습니다.

\n

이러한 기준을 가지고 분리하려면 분석 데이터를 도메인 별로 나누는 아키텍쳐를 모델링해야 합니다. 이 아키텍처에서 도메인의 인터페이스에는 운영 데이터 뿐만 아니라 도메인이 제공하는 분석 데이터도 포함됩니다.

\n

\"domain-not\"

\n

각 도메인은 하나 이상의 운영 API와 하나 이상의 분석 데이터를 제공합니다.\n또한 각 도메인은 다른 도메인의 운영 및 분석 데이터와 의존 관계를 가질 수도 있습니다.

\n

\"domains\"

\n

위의 예시와 같이 Podcasts 도메인은 Users 도메인의 데이터를 통해 Podcast 청취자들의 정보를 데이터화 할 수 있습니다.

\n
\n

Data as a product

\n

기존 데이터 분석 아키텍쳐에서 어떤 데이터가 있는지 탐색하고 이해하고 데이터 품질을 유지하는 것이 큰 과제로 남아있었습니다. 이를 해결하지 않으면 Data Mesh 아키텍쳐에서 더 큰 문제로 다가올 수 있습니다. 탈중앙화 원칙에 따라 데이터를 제공하는 곳과 팀의 수가 늘어나기 때문입니다.

\n

Data as a product 원칙은 데이터 사일로와 데이터 품질 문제를 해결하기 위한 방법입니다.\n도메인에서 제공하는 분석 데이터는 product로 취급되어야 하며 데이터의 소비자는 고객으로 받아들여야 합니다.

\n

조직에서는 도메인 데이터에 대한 PO(Product Owner)를 지정해야 하며 PO는 데이터가 프로덕트로써 전달되기 위한 여러 역할을 담당합니다. PO는 데이터 사용자가 누구인지, 어떻게 사용하는지 정의하고 데이터에 대해 깊이 이해하고 있어야 합니다. 데이터 품질, 데이터 사용 만족도를 측정하고 데이터에는 이를 지원하기 위한 표준 인터페이스가 개발되어야 합니다. 데이터 사용자와 PO는 꾸준히 커뮤니케이션을 통해 data product를 발전시킬 수 있습니다.

\n

각 도메인에는 도메인의 data product를 구축하고 운영 및 제공하는 데이터 개발자 역할도 있어야 합니다. 각 도메인 팀은 하나 이상의 data product를 제공할 수 있습니다.

\n

\"dataproduct\"

\n

data product는 위와 같이 세 가지 구성 요소로 이루어져 있습니다.

\n

1. Code

\n\n
\n

2. Data and Metadata

\n\n
\n

3. Infrastructure

\n\n
\n

\"notation\"

\n

이를 다이어그램으로 표현하면 위와 같습니다.

\n
\n

Self-serve data platform

\n

위와 같이 data product를 구축, 배포, 실행 및 모니터링하려면 이를 위해 많은 인프라가 필요합니다. 이를 구성하는데 필요한 기술은 전문적인 영역이라 각 도메인에서 운영하기 어렵습니다. 각 팀이 data product를 자율적으로 개발하고 운영하기 위해 제품의 수명 주기를 프로비저닝하고 관리할 수 있는 추상화된 인프라가 필요합니다. Self-serve data platform 원칙은 도메인 자율성을 가능하도록 지원하는 플랫폼을 말합니다.

\n

셀프 서비스 데이터 플랫폼은 데이터 개발자의 워크플로우를 지원할 수 있어야 합니다.\n데이터 제품을 생성하기 위해 필요한 비용과 진입장벽을 낮추고 스키마, 파이프라인 개발, 데이터 리니지, 컴퓨팅 클러스터 등을 지원해야 합니다.

\n

\"platform\"

\n
\n

셀프 서비스 플랫폼에는 위와 같이 여러 기능을 제공하는 영역이 존재합니다.\n위 그림에서는 아래와 같이 세 가지 영역으로 나누고 있습니다.

\n

1. Data infrastructure provisioning plane

\n\n

2. Data product developer experience plane

\n\n

3. Data mesh supervision plane

\n\n
\n

Federated computational governance

\n

지금까지 정의한 내용과 같이 Data Mesh 모델은 분산 아키텍쳐 형태를 가지고 있습니다.\n독립적인 data product를 가지며 각 팀이 구축하고 배포합니다.\n그러나 ML 영역과 같은 곳에서 가치를 얻으려면 각 data product가 상호적으로 운용되어야 합니다. 이러한 상호 운용을 위해 플랫폼에 의한 의사 결정을 자동화하기 위한 거버넌스 모델이 필요합니다. 이를 Federated computational governance 원칙이라고 합니다.\n데이터 PO와 데이터 플랫폼 PO가 함께 주도하는 의사 결정 모델은 도메인 의사 결정 권한을 가지며 여러 규칙을 만들고 준수합니다. 이러한 거버넌스를 통해 중앙 집중화와 분산화 사이의 균형을 유지할 수 있습니다.

\n

\"governance\"

\n

거버넌스 모델을 구현하기 위해 참여해야 하는 조직과 인센티브 모델을 정의해야 합니다.\n데이터 플랫폼은 거버넌스로부터 정의된 정책을 자동으로 적용하기 위한 기능을 제공해야 합니다.

\n
\n

Principles Summary

\n

Domain Ownership을 통해 데이터 생성과 사용자 수의 증가, 데이터 접근 정책의 다양성과 데이터의 확장에 대응할 수 있습니다.

\n

Data as a product를 통해 데이터 사용자가 데이터를 쉽게 검색이 가능하고 품질이 보장된 데이터를 사용하며 데이터에 대한 이해도가 올라가고 안전하게 사용할 수 있습니다.

\n

Self-serve data platform을 통해 각 도메인 팀이 자율적으로 제품을 만들고 사용할 수 있도록 하며 data product를 쉽게 구축, 실행 및 운영할 수 있습니다.

\n

Federated computational governance를 통해 데이터 사용자가 상호 운용을 위한 표준을 따르는 생태계로 운영할 수 있습니다. 이러한 표준 정책은 플랫폼에 반영됩니다.

\n
","excerpt":"이 글은 martinfowler.com의 Data Mesh Principles and Logical Architecture…"}}}},{"node":{"title":"Spark on Kubernetes: 성능 최적화 방법들","id":"29b26dc7-c49b-5fb3-99cd-814af4eae2cd","slug":"spark-on-kubernetes-perf","publishDate":"September 11, 2021","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN 만큼의 성능을 내기 위해서 필요한 설정들에 대해 알아보겠습니다.

\n
\n

교차 AZ 전송 지연 개선

\n

대부분 사용자들은 가용성을 우려하여 Multi-AZ 사용을 선호합니다.\n하지만 driver, executor pod가 여러 AZ에 분산되어 있는 어플리케이션은 AZ 간 추가 데이터 전송 비용이 발생할 수 있습니다. 특히 spark shuffle은 disk IO, network IO에 대한 비용이 많이 드는 연산이므로 latency가 낮은 단일 AZ가 좋은 성능을 보일 수 있습니다.

\n
--conf spark.kubernetes.node.selector.zone='<availability zone>'
\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}}},{"node":{"title":"여러 조직이 함께 사용하는 Airflow 만들기","id":"0d51ef05-306f-56ae-b726-ab2712215dec","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}}},{"node":{"title":"사이드카 컨테이너로 Airflow 기능 확장하기","id":"381770e9-3117-58b1-979e-b4b146f5a7b3","slug":"airflow-sidecar","publishDate":"August 01, 2021","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow 2.1 버전부터 공식 Helm Chart가 정식 릴리즈 되었습니다.\n오늘은 공식 차트에서 사용할 수 있는 기능 중 extraContainers 옵션을 활용하는 방법을 3가지 예시를 통해 소개해보려 합니다.

\n\n
\n

Sidecar Container

\n

분산 컨테이너 환경에서 사이드카 패턴이란 Pod 안에서 두 개 이상의 컨테이너로 구성되어 있는 형태를 말합니다. 컨테이너들은 서로 네트워크 또는 볼륨을 공유할 수 있습니다. 사이드카 컨테이너를 활용하면 다음과 장점을 가져갈 수 있습니다.

\n

기존 로직의 변경 없이 새로운 기능 추가:\n가끔 일부 기능 추가를 위해 Airflow 저장소 코드를 수정하는 경우가 생길 수 있습니다.\n하지만 이렇게 한번 수정하고 나면 이후에 버전 업데이트할 때마다 새로운 버전 브랜치와 병합해야 하는 번거로움이 생깁니다. 만약 원하는 기능이 사이드카 컨테이너를 활용할 수 있다면 기존 저장소의 변경 없이 새로운 기능을 추가할 수 있습니다.

\n

컨테이너 재사용:\n사내에서 개발 환경에 따라 또는 접근 권한에 따라 Airflow 인스턴스를 여러 개 구성하고 운영하는 경우가 많습니다. 사이드카 컨테이너로 구성한 기능은 재사용이 가능하기 때문에 새로 배포한 Airflow 인스턴스에 쉽게 적용할 수 있습니다.

\n
\n

Airflow extraContainers

\n

Airflow Helm Chart에서는 extraContainers 옵션을 통해 사이드카 컨테이너를 scheduler, webserver, worker에 정의할 수 있습니다. 제가 기여한 옵션입니다! (https://github.com/apache/airflow/pull/13735)

\n

이제 몇 가지 예시를 통해 어떻게 활용할 수 있는지 알아보겠습니다.

\n
\n

1. S3 Sync Container

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n

Umbrella Helm Chart란 여러 개의 Helm Chart들이 모여있는 집합을 의미합니다. 어디에서부터 시작된 용어인지 모르겠으나 저는 2019 Kubecon - Scaling to Thousands of Nodes (Airbnb) 발표에서 처음 접하게 되었습니다. 사내에 K8S에 대한 요구사항이 빠르게 늘어나던 Airbnb는 멀티 클러스터 배포 전략을 통해 Node Hard Limit 이슈를 해결하게 되었고 클러스터마다 배포를 위해 Umbrella Chart를 만들어 활용했다고 합니다.

\n
\n

Helm Chart 만들기

\n

말로하면 이해가 어려우니 클러스터를 새로 설정한다고 가정하고 예시를 통해 Helm Chart를 만들어보겠습니다. 예시의 클러스터에는 아래와 같은 의존성이 존재합니다. 예시는 Helm 2.16.7 버전을 사용했습니다.

\n\n
\n

먼저 Helm Chart 생성을 위한 폴더 구조를 생성해줍니다.

\n
$ helm create umbrella\n$ cd umbrella\n$ tree\n.\n├── Chart.yaml\n├── charts\n├── templates\n│   ├── NOTES.txt\n│   ├── _helpers.tpl\n│   ├── deployment.yaml\n│   ├── ingress.yaml\n│   ├── service.yaml\n│   └── tests\n│       └── test-connection.yaml\n├── requirements.yaml\n└── values.yaml
\n
\n

기본 생성되는 폴더 구조에서 requirements.yaml 파일은 추가로 생성해주셔야 합니다.\n폴더 구조에서 알아두어야 할 경로는 아래와 같습니다.

\n\n
\n

Chart Metadata

\n

먼저 Chart.yaml 파일을 간단히 수정해줍니다.\n이름과 버전, 설명 등의 정보를 본인에 맞게 작성해주시면 됩니다.

\n
apiVersion: v1\nappVersion: \"1.0\"\ndescription: A Helm chart for Cluster Setup\nname: umbrella\nversion: 0.1.0
\n
\n

Chart Dependencies

\n

이제 앞서 언급한 의존성 중에 Helm Chart 형태로 배포할 컴포넌트를 requirements.yaml 파일에 정의하겠습니다. 각 차트는 이름, 저장소, 버전, 컨디션 값을 지정할 수 있습니다. condition: prometheus.enabled의 의미는 values.yamlprometheus.enabled 값이 true 일 경우에만 해당 차트를 설정하겠다는 뜻 입니다.

\n
dependencies:\n  - name: prometheus\n    repository: https://kubernetes-charts.storage.googleapis.com\n    version: 9.7.3\n    condition: prometheus.enabled\n\n  - name: grafana\n    repository: https://kubernetes-charts.storage.googleapis.com\n    version: 4.2.2\n    condition: grafana.enabled\n\n  - name: cluster-autoscaler\n    repository: https://kubernetes-charts.storage.googleapis.com\n    version: 6.3.0
\n
\n

이제 helm dep up 명령어를 통해 의존성 차트를 갱신할 수 있습니다.\n이를 통해 charts/ 하위에 차트 파일들이 설치됩니다.

\n
$ helm dep up\nHang tight while we grab the latest from your chart repositories...\nUpdate Complete.\nSaving 3 charts\nDownloading prometheus from repo https://kubernetes-charts.storage.googleapis.com\nDownloading grafana from repo https://kubernetes-charts.storage.googleapis.com\nDownloading cluster-autoscaler from repo https://kubernetes-charts.storage.googleapis.com\nDeleting outdated charts
\n
\n

의존성 차트에 적용할 설정 값들은 values.yaml에 정의할 수 있습니다.\ngrafana를 예시로 들면 아래와 같습니다.

\n
# values.yaml\ngrafana:\n  enabled: true\n  adminPassword: mypassword\n  persistence:\n    enabled: true\n    storageClass: \"gp2\"\n    type: pvc\n    size: 5Gi\n  service:\n    type: ClusterIP
\n
\n

Templates

\n

의존성 패키지가 공식 Helm Chart를 지원한다면 dependencies 부분에 정의할 수 있겠지만 yaml 파일만 제공하는 패키지도 많이 존재합니다. 이 경우, templates/ 하위에 yaml 파일을 추가해주시면 됩니다. 외부 설정으로 내보내고 싶은 항목들은 template function을 활용해서 수정할 수 있습니다. 여기에서는 fluentbit만 예시로 추가하겠습니다.

\n
$ cd templates\n$ curl -O https://raw.githubusercontent.com/fluent/fluent-bit-kubernetes-logging/master/fluent-bit-service-account.yaml\n$ curl -O https://raw.githubusercontent.com/fluent/fluent-bit-kubernetes-logging/master/fluent-bit-role.yaml\n$ curl -O https://raw.githubusercontent.com/fluent/fluent-bit-kubernetes-logging/master/fluent-bit-role-binding.yaml\n$ curl -O https://raw.githubusercontent.com/fluent/fluent-bit-kubernetes-logging/master/output/elasticsearch/fluent-bit-configmap.yaml
\n
\n

Deploy

\n

이제 예시 차트를 클러스터에 배포할 차례입니다. 배포 후 차트 내 컴포넌트 버전 업데이트가 발생하더라도 helm upgrade 명령어를 통해 쉽게 관리할 수 있습니다.

\n
# install\n$ helm install . --name umbrella --namespace umbrella\n\n# check status\n$ helm status umbrella\n$ helm get umbrella\n\n# upgrade\n$ helm upgrade -f values.yaml umbrella .\n\n# delete\nhelm del umbrella --purge
\n
\n

만일 어플리케이션들이 모두 별도의 Helm Chart로 관리되고 있다면 Umbrella Chart에서 어플리케이션 차트까지 의존성으로 추가하는 방식으로 운영하는 방법도 있습니다. 요즘에는 Helm 이외에도 kustomize 등 다양한 도구들이 있으니 비교해보고 적절한 방식을 선택하시면 좋습니다.

\n
","excerpt":"K8S 클러스터를 설정하고 운영하다보면 버전 업데이트, 컴포넌트 추가 설치 등 다양한 변경에 대응할 수 있어야 합니다. 또한 Develop…"}}}},{"node":{"title":"Airflow on Kubernetes (1)","id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}}},{"node":{"title":"Gatsby와 Contentful로 블로그 이전한 후기","id":"4ebcfc23-f315-530e-899e-c3fb7cf499bc","slug":"gatsby-contentful","publishDate":"April 25, 2020","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

3년 정도 이어왔던 Jekyll 블로그를 Gatsby와 Contentful로 옮긴 이유와\n이에 따라 생긴 변화들에 대해 정리해보려 합니다.

\n
\n

왜 옮기게 되었을까

\n

블로그를 옮기고 싶었던 가장 큰 이유는 컨텐츠 관리가 불편하다는 점이었습니다.\n이전에 사용하던 테마는 로컬에서 posts 하위의 md 파일을 통해 컨텐츠를 작성하고 이를 빌드 후 github.io 저장소에 배포하는 형태였습니다. 만일 개인적으로 사용하고 있는 노트북과 데스크탑이 여러 개라면 모든 기기에 블로그 개발환경을 구축해야만 합니다. md 기반의 글들은 테마를 옮겨가려고 해도 기존에 사용하던 방식과 맞지 않다면 많은 부분을 수정해야하는 불편함이 있습니다.

\n

또한 요즘 잘 구축되어 있는 블로그 플랫폼(brunch, velog, medium)들을 보면 글 작성을 위한 에디터가 정말 편리하게 구성되어 있습니다. 저 또한 이러한 장점을 사용하고 싶었지만 기존에 작성해둔 글을 옮기기에 무리가 있고, 글에 대한 링크가 변경된다는 점 때문에 옮길 수는 없다고 판단했습니다.

\n

두번째 이유는 사용하고 있던 Jekyll 테마 자체에 대한 불만이었습니다.\n특히 코드 스타일링 플러그인의 사용 방식과 UI가 마음에 들지 않아 gist 링크를 사용하곤 했는데 옮겨다니면서 편집하려다보니 시간이 더 오래 걸렸습니다. 루비와 템플릿 엔진 방식의 테마는 커스터마이징하기 불편했고 이를 위해 사용하지도 않을 언어를 공부하고 싶지도 않았습니다.\n이러한 이유로 Gatsby를 선택하게 되었고 다양한 플러그인과 쉬운 커스터마이징에 만족하고 있습니다.

\n
\n

Contentful

\n

제가 컨텐츠 관리를 더 편하게 하기 위해 선택한 플랫폼은 Contentful 입니다.\nHeadless CMS라 불리기도 하는 Contentful은 컨텐츠를 관리하기 위한 모든 역할을 수행할 수 있습니다. 실제로 사용해보면서 느낀 장점은 아래와 같습니다.

\n\n

이러한 장점들은 기존에 가지고 있던 불편함을 해소하기에 충분했고 글을 전부 마이그레이션하기에도 불편함이 없었습니다. 특히 slug 지정을 통해 기존 경로를 그대로 유지할 수 있었습니다.

\n
\n

\n \n \n \n

\n
\n

https://swalloow.github.io/contents-url 경로를 예를 들면 하위에 오는 contents-url이 위 그림에서 slug 값에 해당합니다. 이런식으로 Post 마다 태그 등의 다양한 메타데이터를 설정할 수 있습니다. 모든 포스트는 Draft, Publish, Archive 단계의 상태를 가지게 됩니다. 아직 글을 작성 중이라면 Draft 상태로 남겨두고 완성 후 Pulish를 수행하는 식으로 활용할 수 있습니다. Publish 할때마다 자동으로 버전이 추가되기 때문에 이전으로 롤백하는 것도 쉽게 가능합니다.

\n
\n

\n \n \n \n

\n

master 브랜치로 배포하는 step은 JamesIves/github-pages-deploy-action@releases/v3를 사용했습니다. 대상 폴더와 브랜치를 지정해주면 위 그림과 같은 형태로 push가 됩니다.

\n
\n

\n \n \n \n

\n

배포 결과는 위와 같이 Actions 메뉴에서 확인할 수 있습니다.

\n
\n

Gatsby

\n

블로그 테마를 옮기면서 추가한 주요 플러그인들은 아래와 같습니다.

\n\n
\n

블로그 댓글 기능도 기존 disqus에서 utterances로 변경하면서 댓글 확인도 편리해졌습니다. 이제 GitHub 모바일 앱이 있기 때문에 댓글이 달리면 휴대폰으로 확인하고 답글을 달 수 있게 되었습니다. 미세하게 utterances 렌더링 속도가 더 빠르기도 합니다.

\n
\n

\n \n \n \n

\n

먼저 AutoScaler를 설정하면 대기 상태의 Pod을 주기적으로 확인합니다.\n클러스터 리소스가 부족하면서 사용자가 정의한 최대 노드 수에 도달하지 않은 경우 노드 프로비저닝을 요청합니다.\n노드가 추가되면 스케줄러에 의해 대기 상태의 Pod들이 새로운 노드로 할당됩니다.

\n

노드를 축소하는 프로세스는 사용자가 정의한 메트릭에 의해 시작됩니다.\n예를 들어 CPU Utilization이 50% 이하로 설정했다고 가정해보겠습니다.\nCluster AutoScaler는 삭제할 노드에서 실행 중인 Pod를 다른 노드로 안전하게 이동시킬 수 있는지 확인합니다.\n이때 Pod가 로컬 스토리지를 사용하고 있었다면 데이터 유실이 발생할 수 있으니 PV 사용을 권장합니다.\n이러한 확인 프로세스를 노드 또는 Pod 단위로 수행하고 Pod이 모두 이동하게 되면 노드를 제거합니다.

\n
\n

EKS AutoScaler

\n

EKS의 AutoScaler는 AWS의 Auto Scaling Group을 활용하고 있습니다.\nASG는 주기적으로 현재 상태를 확인하고 Desired State로 변화하는 방식으로 동작합니다.\n사용자는 클러스터 노드 수를 제한하는 Min, Max 값을 지정할 수 있습니다.

\n

\n \n \n \n

\n

위와 같이 목적에 따라 여러 종류의 ASG를 설정하고 서로 다른 AutoScaling Policy를 적용할 수 있습니다.\nSpot Instance Group을 설정하면 저렴하지만 입찰 가격에 의해 언제든지 인스턴스가 내려갈 수 있습니다.\n하지만 EKS의 Spot Interrupt Handler (DeamonSet) 에 의해 정상적으로 실행 중인 Pod들을 재배치할 수 있습니다.

\n

\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}}},{"node":{"title":"K8S 클러스터 초기 설정을 위한 Helm Chart 만들기","id":"d5872346-83a6-5ccf-a333-059ff856bd08","slug":"umbrella-helm-chart","publishDate":"June 20, 2020","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

K8S 클러스터를 설정하고 운영하다보면 버전 업데이트, 컴포넌트 추가 설치 등 다양한 변경에 대응할 수 있어야 합니다. 또한 Develop, Production, Staging 등 목적에 따라 다양한 클러스터를 추가로 설정하고 배포해야 하는 경우도 생깁니다. 오늘은 이러한 어려움을 해결할 수 있는 Umbrella Helm Chart에 대해 정리해보려고 합니다. Helm을 처음 접하신다면 공식문서를 먼저 보는 방법을 추천드립니다.

\n
\n

Umbrella Helm Chart

\n

\n \n \n \n

\n

Umbrella Helm Chart란 여러 개의 Helm Chart들이 모여있는 집합을 의미합니다. 어디에서부터 시작된 용어인지 모르겠으나 저는 2019 Kubecon - Scaling to Thousands of Nodes (Airbnb) 발표에서 처음 접하게 되었습니다. 사내에 K8S에 대한 요구사항이 빠르게 늘어나던 Airbnb는 멀티 클러스터 배포 전략을 통해 Node Hard Limit 이슈를 해결하게 되었고 클러스터마다 배포를 위해 Umbrella Chart를 만들어 활용했다고 합니다.

\n
\n

Helm Chart 만들기

\n

말로하면 이해가 어려우니 클러스터를 새로 설정한다고 가정하고 예시를 통해 Helm Chart를 만들어보겠습니다. 예시의 클러스터에는 아래와 같은 의존성이 존재합니다. 예시는 Helm 2.16.7 버전을 사용했습니다.

\n\n
\n

먼저 Helm Chart 생성을 위한 폴더 구조를 생성해줍니다.

\n
$ helm create umbrella\n$ cd umbrella\n$ tree\n.\n├── Chart.yaml\n├── charts\n├── templates\n│   ├── NOTES.txt\n│   ├── _helpers.tpl\n│   ├── deployment.yaml\n│   ├── ingress.yaml\n│   ├── service.yaml\n│   └── tests\n│       └── test-connection.yaml\n├── requirements.yaml\n└── values.yaml
\n
\n

기본 생성되는 폴더 구조에서 requirements.yaml 파일은 추가로 생성해주셔야 합니다.\n폴더 구조에서 알아두어야 할 경로는 아래와 같습니다.

\n\n
\n

Chart Metadata

\n

먼저 Chart.yaml 파일을 간단히 수정해줍니다.\n이름과 버전, 설명 등의 정보를 본인에 맞게 작성해주시면 됩니다.

\n
apiVersion: v1\nappVersion: \"1.0\"\ndescription: A Helm chart for Cluster Setup\nname: umbrella\nversion: 0.1.0
\n
\n

Chart Dependencies

\n

이제 앞서 언급한 의존성 중에 Helm Chart 형태로 배포할 컴포넌트를 requirements.yaml 파일에 정의하겠습니다. 각 차트는 이름, 저장소, 버전, 컨디션 값을 지정할 수 있습니다. condition: prometheus.enabled의 의미는 values.yamlprometheus.enabled 값이 true 일 경우에만 해당 차트를 설정하겠다는 뜻 입니다.

\n
dependencies:\n  - name: prometheus\n    repository: https://kubernetes-charts.storage.googleapis.com\n    version: 9.7.3\n    condition: prometheus.enabled\n\n  - name: grafana\n    repository: https://kubernetes-charts.storage.googleapis.com\n    version: 4.2.2\n    condition: grafana.enabled\n\n  - name: cluster-autoscaler\n    repository: https://kubernetes-charts.storage.googleapis.com\n    version: 6.3.0
\n
\n

이제 helm dep up 명령어를 통해 의존성 차트를 갱신할 수 있습니다.\n이를 통해 charts/ 하위에 차트 파일들이 설치됩니다.

\n
$ helm dep up\nHang tight while we grab the latest from your chart repositories...\nUpdate Complete.\nSaving 3 charts\nDownloading prometheus from repo https://kubernetes-charts.storage.googleapis.com\nDownloading grafana from repo https://kubernetes-charts.storage.googleapis.com\nDownloading cluster-autoscaler from repo https://kubernetes-charts.storage.googleapis.com\nDeleting outdated charts
\n
\n

의존성 차트에 적용할 설정 값들은 values.yaml에 정의할 수 있습니다.\ngrafana를 예시로 들면 아래와 같습니다.

\n
# values.yaml\ngrafana:\n  enabled: true\n  adminPassword: mypassword\n  persistence:\n    enabled: true\n    storageClass: \"gp2\"\n    type: pvc\n    size: 5Gi\n  service:\n    type: ClusterIP
\n
\n

Templates

\n

의존성 패키지가 공식 Helm Chart를 지원한다면 dependencies 부분에 정의할 수 있겠지만 yaml 파일만 제공하는 패키지도 많이 존재합니다. 이 경우, templates/ 하위에 yaml 파일을 추가해주시면 됩니다. 외부 설정으로 내보내고 싶은 항목들은 template function을 활용해서 수정할 수 있습니다. 여기에서는 fluentbit만 예시로 추가하겠습니다.

\n
$ cd templates\n$ curl -O https://raw.githubusercontent.com/fluent/fluent-bit-kubernetes-logging/master/fluent-bit-service-account.yaml\n$ curl -O https://raw.githubusercontent.com/fluent/fluent-bit-kubernetes-logging/master/fluent-bit-role.yaml\n$ curl -O https://raw.githubusercontent.com/fluent/fluent-bit-kubernetes-logging/master/fluent-bit-role-binding.yaml\n$ curl -O https://raw.githubusercontent.com/fluent/fluent-bit-kubernetes-logging/master/output/elasticsearch/fluent-bit-configmap.yaml
\n
\n

Deploy

\n

이제 예시 차트를 클러스터에 배포할 차례입니다. 배포 후 차트 내 컴포넌트 버전 업데이트가 발생하더라도 helm upgrade 명령어를 통해 쉽게 관리할 수 있습니다.

\n
# install\n$ helm install . --name umbrella --namespace umbrella\n\n# check status\n$ helm status umbrella\n$ helm get umbrella\n\n# upgrade\n$ helm upgrade -f values.yaml umbrella .\n\n# delete\nhelm del umbrella --purge
\n
\n

만일 어플리케이션들이 모두 별도의 Helm Chart로 관리되고 있다면 Umbrella Chart에서 어플리케이션 차트까지 의존성으로 추가하는 방식으로 운영하는 방법도 있습니다. 요즘에는 Helm 이외에도 kustomize 등 다양한 도구들이 있으니 비교해보고 적절한 방식을 선택하시면 좋습니다.

\n
","excerpt":"K8S 클러스터를 설정하고 운영하다보면 버전 업데이트, 컴포넌트 추가 설치 등 다양한 변경에 대응할 수 있어야 합니다. 또한 Develop…"}}}},{"node":{"title":"Airflow on Kubernetes (1)","id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}}},{"node":{"title":"Gatsby와 Contentful로 블로그 이전한 후기","id":"4ebcfc23-f315-530e-899e-c3fb7cf499bc","slug":"gatsby-contentful","publishDate":"April 25, 2020","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

3년 정도 이어왔던 Jekyll 블로그를 Gatsby와 Contentful로 옮긴 이유와\n이에 따라 생긴 변화들에 대해 정리해보려 합니다.

\n
\n

왜 옮기게 되었을까

\n

블로그를 옮기고 싶었던 가장 큰 이유는 컨텐츠 관리가 불편하다는 점이었습니다.\n이전에 사용하던 테마는 로컬에서 posts 하위의 md 파일을 통해 컨텐츠를 작성하고 이를 빌드 후 github.io 저장소에 배포하는 형태였습니다. 만일 개인적으로 사용하고 있는 노트북과 데스크탑이 여러 개라면 모든 기기에 블로그 개발환경을 구축해야만 합니다. md 기반의 글들은 테마를 옮겨가려고 해도 기존에 사용하던 방식과 맞지 않다면 많은 부분을 수정해야하는 불편함이 있습니다.

\n

또한 요즘 잘 구축되어 있는 블로그 플랫폼(brunch, velog, medium)들을 보면 글 작성을 위한 에디터가 정말 편리하게 구성되어 있습니다. 저 또한 이러한 장점을 사용하고 싶었지만 기존에 작성해둔 글을 옮기기에 무리가 있고, 글에 대한 링크가 변경된다는 점 때문에 옮길 수는 없다고 판단했습니다.

\n

두번째 이유는 사용하고 있던 Jekyll 테마 자체에 대한 불만이었습니다.\n특히 코드 스타일링 플러그인의 사용 방식과 UI가 마음에 들지 않아 gist 링크를 사용하곤 했는데 옮겨다니면서 편집하려다보니 시간이 더 오래 걸렸습니다. 루비와 템플릿 엔진 방식의 테마는 커스터마이징하기 불편했고 이를 위해 사용하지도 않을 언어를 공부하고 싶지도 않았습니다.\n이러한 이유로 Gatsby를 선택하게 되었고 다양한 플러그인과 쉬운 커스터마이징에 만족하고 있습니다.

\n
\n

Contentful

\n

제가 컨텐츠 관리를 더 편하게 하기 위해 선택한 플랫폼은 Contentful 입니다.\nHeadless CMS라 불리기도 하는 Contentful은 컨텐츠를 관리하기 위한 모든 역할을 수행할 수 있습니다. 실제로 사용해보면서 느낀 장점은 아래와 같습니다.

\n\n

이러한 장점들은 기존에 가지고 있던 불편함을 해소하기에 충분했고 글을 전부 마이그레이션하기에도 불편함이 없었습니다. 특히 slug 지정을 통해 기존 경로를 그대로 유지할 수 있었습니다.

\n
\n

\n \n \n \n

\n
\n

https://swalloow.github.io/contents-url 경로를 예를 들면 하위에 오는 contents-url이 위 그림에서 slug 값에 해당합니다. 이런식으로 Post 마다 태그 등의 다양한 메타데이터를 설정할 수 있습니다. 모든 포스트는 Draft, Publish, Archive 단계의 상태를 가지게 됩니다. 아직 글을 작성 중이라면 Draft 상태로 남겨두고 완성 후 Pulish를 수행하는 식으로 활용할 수 있습니다. Publish 할때마다 자동으로 버전이 추가되기 때문에 이전으로 롤백하는 것도 쉽게 가능합니다.

\n
\n

\n \n \n \n

\n

master 브랜치로 배포하는 step은 JamesIves/github-pages-deploy-action@releases/v3를 사용했습니다. 대상 폴더와 브랜치를 지정해주면 위 그림과 같은 형태로 push가 됩니다.

\n
\n

\n \n \n \n

\n

배포 결과는 위와 같이 Actions 메뉴에서 확인할 수 있습니다.

\n
\n

Gatsby

\n

블로그 테마를 옮기면서 추가한 주요 플러그인들은 아래와 같습니다.

\n\n
\n

블로그 댓글 기능도 기존 disqus에서 utterances로 변경하면서 댓글 확인도 편리해졌습니다. 이제 GitHub 모바일 앱이 있기 때문에 댓글이 달리면 휴대폰으로 확인하고 답글을 달 수 있게 되었습니다. 미세하게 utterances 렌더링 속도가 더 빠르기도 합니다.

\n
\n

\n \n \n \n

\n

아마 AWS를 production 환경에서 사용하고 있다면 VPC 구성은 이미 잘 이해하고 계시리라 생각합니다.\nVPC 내에 생성한 인스턴스는 eth0이라는 기본 네트워크 인터페이스를 가지게 됩니다.\n그리고 네트워크 인터페이스에 하나 이상의 IPv4 또는 IPv6 주소를 할당할 수 있습니다.\n또한 각 Subnet에 존재하는 인스턴스는 Route Table을 통해 통신을 할 수 있습니다.\n여기까지가 우리가 알고 있는 VPC 내의 Host 간 통신입니다.

\n

그렇다면 EKS는 어떤 점이 다를까요?\n쿠버네티스의 Pod은 한 개 이상의 컨테이너를 구성하고 같은 Host와 Network 스택을 공유합니다.\n그리고 여러 Host에 사이에 걸쳐 생성된 Pod은 Overlay Network를 통해 서로 통신하게 됩니다.\n기존 VPC 환경에서는 Pod 네트워크 통신을 기존 방식처럼 지원하기 어려웠습니다.

\n

하지만 대부분의 사용자들이 VPC 기반의 인프라를 구성하고 있었기 때문에\nEKS는 VPC를 지원할 수 있어야 했습니다.\n예를 들어 사용자는 Security Group, VPC Flow 로그 등의 기능을 그대로 사용하면서,\nPrivateLink를 통해 다른 AWS 서비스와 통신할 수 있어야 합니다.\n이 문제를 해결하기 위해 AWS는 CNI 라는 네트워크 플러그인을 지원하기 시작했습니다.

\n
\n

EKS CNI

\n

\n

\n

처음 Worker Node가 추가되면 하나의 ENI 가 인스턴스에 할당됩니다.\n하지만 실행되는 Pod의 수가 단일 ENI 에서 허용하는 주소를 초과하면 CNI는 노드에 새로운 ENI 를 추가합니다.\nENI 에 secondary IP 할당과 Pod에 할당할 노드의 IP 주소 풀 관리는 L-IPAM 데몬을 통해 이루어집니다.\nL-IPAM 데몬은 모든 노드에 DeamonSet으로 배포되며 gRPC를 통해 CNI 플러그인과 통신합니다.

\n

사용하고 있는 인스턴스 유형이 m5.xlarge라고 가정하고 예시를 들어보겠습니다.\n우선 m5.xlarge 유형은 4 ENI 와 ENI 당 15 개의 IP 주소를 가질 수 있습니다.\n배포된 Pod의 수가 0에서 14 사이라면 IPAM 데몬은 2개의 Warm Pool을 유지하기 위해 ENI를 하나 더 할당합니다.\n이때 사용가능한 IP 수는 2 * (15 - 1) = 28 개가 됩니다.\n이런식으로 Warm Pool을 늘려가면서 최대 4 * (15 - 1) = 56 개의 IP를 가질 수 있습니다.\n물론 이 부분은 WARM_ENI_TARGET 과 같은 CNI 옵션을 통해 수정할 수 있습니다.

\n
\n

\n \n \n \n

\n

구체적으로 CNI를 통해 Pod1 과 Pod2가 어떻게 통신하는지 다이어그램으로 표현하면 위와 같습니다.\n각 Pod의 eth0에는 secondary IP address가 할당되며 Pod Side Route Table를 가지고 있습니다.\n노드의 네트워크 인터페이스까지 도달한 패킷은 EC2-VPC fabric에 의해 포워딩 됩니다.

\n

따라서 EKS 노드를 결정할 때 ENI 제한 관련 부분도 중요하게 생각하셔야 합니다.\n노드 당 ENI, IP 주소 제한은 해당 공식 문서에서 확인하실 수 있습니다.\n물론 CNI를 사용하지 않고 기존의 Calico와 같은 Overlay Network를 사용할 수도 있습니다.\n하지만 이를 사용하게 되면 네트워크까지 관리해야하며 새로운 장애 포인트로 이어질 수 있습니다.

\n


\n

Reference

\n\n
","excerpt":"모든 Kubernetes as a Service가 그렇듯 EKS 역시 빠르게 변화하고 있습니다.\n오늘의 주제는 EKS의 VPC…"}}}},{"node":{"title":"AWS MFA CLI 설정 변경 자동화하기","id":"0fb3ff35-6e59-55ed-a84f-41ea9ef99d84","slug":"aws-cli-mfa","publishDate":"October 03, 2019","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAAABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAADTAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJDAAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAEFBP/EACIQAAICAgIABwAAAAAAAAAAAAECABEDBBIhQVFSYqHB0f/EABUBAQEAAAAAAAAAAAAAAAAAAAIE/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oADAMBAAIRAxEAPwBnZyBeRZVPlzr6mc7mVspLMle7LY+KkobTN2QOuoxkJBYEihdeEoTbVlN4KKLoTfrP7CQTvOOioMIdhZX/2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

클라우드 인프라를 관리하는 경우 여러 계정에 걸친 CLI를 사용하는 경우가 빈번합니다.\n만일 CLI 사용 시 MFA 인증을 요구하는 계정과 아닌 계정이 혼재되어 있다면 설정이 정말 귀찮아집니다.\n이 글에서는 간단한 스크립트를 통해 AWS CLI 설정 변경을 자동화해보려 합니다.

\n
\n

AWS Credential 설정

\n

먼저 사용하는 프로필의 credential 정보를 설정해줍니다.\nMFA인 프로필과 MFA가 아닌 프로필을 구분하기 위해 아래와 같은 구조로 저장하겠습니다.\n-default가 붙은 프로필이 MFA를 사용하는 프로필입니다.\n스크립트 실행을 통해 autogen 값이 자동생성됩니다.

\n
[test]\naws_access_key_id = mykey\naws_secret_access_key = mykey\n \n[dev-default]\naws_access_key_id = mykey\naws_secret_access_key = mykey\n \n[prod-default]\naws_access_key_id = mykey\naws_secret_access_key = mykey\n \n[dev]\naws_arn_mfa = arn:aws:iam::myaccount:mfa/myaccount\naws_access_key_id = autogen\naws_secret_access_key = autogen\naws_session_token = autogen\n \n[prod]\naws_arn_mfa = arn:aws:iam::myaccount:mfa/myaccount\naws_access_key_id = autogen\naws_secret_access_key = autogen\naws_session_token = autogen
\n\n
\n

MFA 설정 스크립트

\n

다음으로 아래의 스크립트를 각 awsp.sh, mfa.py 이름으로 ~/.aws/ 경로에 추가해줍니다.\n이후에 ~/.zshrc 또는 ~/.bashrc 경로에 source ~/.aws/awsp.sh를 추가해줍니다.

\n
#! /bin/bash\n\nsetProfile() {\n  export AWS_PROFILE=$1\n  export AWS_DEFAULT_PROFILE=$1\n\n  python ~/.aws/mfa.py --profile $1 $2\n}\nalias awsp=setProfile
\n
\n
import os\nimport json\nimport sys\nimport argparse\nimport subprocess\nimport configparser\n\nparser = argparse.ArgumentParser(description='Update your AWS CLI Token')\nparser.add_argument('token', help='token from your MFA device')\nparser.add_argument('--profile', help='aws profile to store the session token', default=os.getenv('AWS_PROFILE'))\nparser.add_argument('--arn', help='AWS ARN from the IAM console (Security credentials -> Assigned MFA device). This is saved to your .aws/credentials file')\nparser.add_argument('--credential-path', help='path to the aws credentials file', default=os.path.expanduser('~/.aws/credentials'))\n\nargs = parser.parse_args()\n\nif args.profile is None:\n    parser.error('Expecting --profile or profile set in environment AWS_PROFILE. e.g. \"stage\"')\n\nconfig = configparser.ConfigParser()\nconfig.read(args.credential_path)\n\nif args.profile not in config.sections():\n    parser.error('Invalid profile. Section not found in ~/.aws/credentails')\n\nif args.arn is None:\n    if 'aws_arn_mfa' not in config[args.profile]:\n        sys.exit(0)\n# parser.error('ARN is not provided. Specify via --arn')\n\n    args.arn = config[args.profile]['aws_arn_mfa']\nelse:\n    # Update the arn with user supplied one\n    config[args.profile]['aws_arn_mfa'] = args.arn\n\n# Generate the session token from the profile\nresult = subprocess.run(['aws', 'sts', 'get-session-token', '--profile', args.profile + '-default', '--serial-number', args.arn, '--token-code', args.token], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\nif result.returncode != 0:\n    parser.error(result.stderr.decode('utf-8').strip('\\n'))\n\ncredentials = json.loads(result.stdout.decode('utf-8'))['Credentials']\n\nconfig[args.profile]['aws_access_key_id'] = credentials['AccessKeyId']\nconfig[args.profile]['aws_secret_access_key'] = credentials['SecretAccessKey']\nconfig[args.profile]['aws_session_token'] = credentials['SessionToken']\n\n# Save the changes back to the file\nwith open(args.credential_path, 'w') as configFile:\n    config.write(configFile)\n\nprint('Saved {} credentials to {}'.format(args.profile, args.credential_path))
\n
\n

이제 awsp [프로필명] [mfa code]과 같은 명령어로 사용하실 수 있습니다.\n예를 들어 dev 프로필의 경우 awsp dev 012345와 같이 실행합니다.\nMFA 조건이 없는 프로필의 경우 awsp test 0과 같이 실행하시면 됩니다.

\n
","excerpt":"클라우드 인프라를 관리하는 경우 여러 계정에 걸친 CLI를 사용하는 경우가 빈번합니다.\n만일 CLI 사용 시 MFA…"}}}},{"node":{"title":"Terraform 입문자를 위한 미세 팁","id":"04c97999-9e82-53a0-bca8-0ac8c6db3757","slug":"tf-tips","publishDate":"September 20, 2019","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

클라우드를 활용하는 경우, 인프라 구성 관리 도구로 테라폼을 많이 사용합니다.\n오늘은 처음 테라폼을 도입하려고 할때 알아두면 좋은 점들에 대해 정리해보려 합니다.

\n
\n

Procedural vs Declarative

\n
# Ansible\n- ec2:\n  count: 10\n  image: ami-v1\n  instance_type: t2.micro\n\n# Terraform\nresource \"aws_instance\" \"example\" {\n  count = 10\n  ami = \"ami-v1\"\n  instance_type = \"t2.micro\"\n}
\n

위에 나와있는 코드는 Ansible과 Terraform으로 EC2 인스턴스를 구성하는 코드입니다.\n만일 여기서 둘의 count 값을 15로 변경한다면 어떻게 변할까요?

\n

먼저 Ansible의 경우, procedural이며 mutable infrastructure를 지향합니다.\n따라서 이미 생성된 10개의 인스턴스에 15개의 인스턴스가 추가로 생성되어 총 25개의 인스턴스가 떠있게 됩니다.\n반면에 Terraform의 경우, declarative이며 immutable infrastructure를 지향합니다.\ncount를 15로 선언했기 때문에 Terraform은 이전 상태와 비교한다음, 5만큼의 변경에 대해 교체를 수행합니다.\n결과적으로 총 15개의 인스턴스가 떠있게 됩니다.

\n

서로 지향하는 성격이 다르다보니 적절한 상황에 사용하거나 함께 사용하면 좋습니다.\n예를 들어 Provisioning 단계에서 Terraform을 사용하고\nConfiguration, Dependency 설정 단계에서 Ansible을 사용하실 수 있습니다.

\n
\n

Terraform vs CloudFormation

\n

AWS를 사용하는 경우, 클라우드 내에서 CloudFormation이라는 서비스를 제공합니다.\nCloudFormation 역시 Terraform과 같은 기능을 제공하다보니 도입하기 전에 비교를 많이 합니다.\n우선 모듈화, 개발, 문서 측면에서는 Terraform이 더 편했습니다.\n이외의 큰 차이를 정리하자면 아래와 같습니다.

\n

CloudFormation은 AWS 지원이 빠릅니다.\n신규 릴리즈된 서비스나 설정들은 Terraform AWS 모듈에 반영되기까지 시간이 좀 걸립니다.\n반면에 CloudFormation은 대부분 바로 지원해주다보니 더 편할 수 있습니다.

\n

Terraform은 다른 클라우드 서비스도 지원합니다(Azure, Google Cloud).\n만일 멀티클라우드 이슈에 대한 대응까지 고려하고 있다면 Terraform을 추천드립니다.

\n
\n

Terraform Remote Backend

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

배치 프로세싱의 가장 간단한 형태가 바로 Work Queue System 이다.\nQueue 형태의 작업 대기열이 있고 이를 관리해주는 컨테이너가 Worker에 작업을 분배해주는 형태이다.\n일반적으로 작업 대기열에서 각 작업은 일정 시간 내에 수행되어야 하며 처리량에 따라 Worker는 Scale-out 할 수 있어야 한다.\n작업이 지속적으로 지연된다면 큐에 작업이 계속 쌓이게 되고 이는 장애로 이어질 수 있다.

\n
\n

Source, Worker Container Interface

\n\n
\n

Event-Driven Batch Processing

\n

앞서 설명한 작업 대기열 처리는 하나의 입력을 하나의 출력으로 변환할 때 많이 사용한다.\n하지만 단일 작업 이상을 처리해야 한다거나 단일 입력에서 여러 출력을 생성해야 하는 경우도 있다.\n이러한 경우 여러 작업 대기열을 연결하는 이벤트 스트림 방식을 통해 작업을 수행한다.\n이전 단계의 작업이 완료되면 이벤트가 발생하고 이벤트를 통해 다음 단계의 대기열로 이동하는 형태이다.

\n

\n \n \n \n

\n

스트림 처리에 대한 여러 패턴이 존재하지만 위 그림은 그 중 하나인 Sharder에 대한 내용이다.\nSharder는 작업 큐 2개를 두고 만일 두 개의 큐가 모두 healthy 하다면, id 기준으로 작업을 분배한다.\n만일 어느 하나가 unhealthy 하다면, 새로운 큐를 생성해서 다른 한쪽으로 작업을 분배한다.\n이를 통해 단일 큐를 사용하는 것보다 안정적이고 작업을 분산처리할 수 있다.

\n

이외에도 Publisher/Subscriber, Merger, Splitter 등의 패턴이 있다.\nSpark의 DAG와 같은 그림을 떠올린다면 어떤 내용인지 바로 이해할 수 있을 것이다.\n처리 시간이 비교적 짧은 이벤트 기반의 배치 프로세싱은 앞서 소개했던 FaaS 패턴으로 구현할 수도 있다.\nAWS의 Lambda, Stream, Kinesis Firehose를 떠올리면 된다.

\n
\n

Coordinated Batch Processing

\n

\n \n \n \n

\n

처음에는 단일 대기열로 처리했지만 더 복잡한 배치 작업을 처리하기 위해 대기열을 분할하고 연결했다.\n하지만 최종 단계에서는 결국 원하는 결과를 생성하기 위해 여러 출력을 하나로 합쳐야 한다.\n합치는 작업은 여러 개의 작업 큐가 모두 종료되고 난 이후에 수행되어야 한다.\n이와 관련된 패턴으로 Join과 Reduce가 있다.\nJoin과 달리 Reducer의 경우 parallel 하게 시작할 수 있다는 차이가 있다.\ncount, sum, histogram을 추출하는 작업이 이에 해당한다.

\n
\n

Reference

\n

책을 마무리할 무렵 Kubernetes Korea Group 세미나에서 책 저자의 발표를 들을 수 있었다.\n다른 컨퍼런스에서 진행했던 발표들과 내용이 유사했고 Azure Kuberentes Service에 대한 홍보가 짙었지만\n클라우드 네이티브와 최근 개발 환경의 변화에 대한 생각을 들을 수 있는 시간이었다.

\n

그동안 개발할 때 정형화 된 패턴을 공부하지 않았음에도 비슷한 형태로 설계된 경우를 많이 볼 수 있었다.\n하지만 패턴을 한번 정리하고 나면 더 명확하게 이해되고 현재 상황에 맞는 패턴을 적용할 수 있게 되는 것 같다.\n마틴 파울러의 리팩토링 책도 그렇고 이번 책 또한 그런 부분을 느낄 수 있었다.

\n\n
","excerpt":"구글 클라우드 팀이 Kubernetes와 같은 Container Orchestration…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":4,"humanPageNumber":5,"skip":25,"limit":6,"numberOfPages":16,"previousPagePath":"/4","nextPagePath":"/6"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/5","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"EKS의 AutoScaling 이해하기","id":"083eb498-8c8a-5a49-817f-dc19254b7979","slug":"eks-autoscale","publishDate":"November 23, 2019","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

오늘은 Kubernetes의 Cluster AutoScaling에 대해 정리해보려 합니다.\n그 다음 EKS에서는 어떻게 적용할 수 있는지, 어떤 효과를 볼 수 있는지 알아보겠습니다.

\n
\n

Kubernetes Cluster AutoScaling

\n

Kubernetes는 Cluster AutoScaler를 통해 동적으로 인프라를 확장할 수 있습니다.\nCluster AutoScaler는 Pod의 리소스 요청에 따라 클러스터의 노드를 추가하거나 제거합니다.\n만약 리소스 부족으로 인해 스케줄링 대기 상태의 Pod가 존재하는 경우 Cluster AutoScaler가 노드를 추가합니다.\n추가 시 설정한 Min, Max 값을 넘어가지 않도록 구성 할 수 있습니다.

\n

\n \n \n \n

\n

먼저 AutoScaler를 설정하면 대기 상태의 Pod을 주기적으로 확인합니다.\n클러스터 리소스가 부족하면서 사용자가 정의한 최대 노드 수에 도달하지 않은 경우 노드 프로비저닝을 요청합니다.\n노드가 추가되면 스케줄러에 의해 대기 상태의 Pod들이 새로운 노드로 할당됩니다.

\n

노드를 축소하는 프로세스는 사용자가 정의한 메트릭에 의해 시작됩니다.\n예를 들어 CPU Utilization이 50% 이하로 설정했다고 가정해보겠습니다.\nCluster AutoScaler는 삭제할 노드에서 실행 중인 Pod를 다른 노드로 안전하게 이동시킬 수 있는지 확인합니다.\n이때 Pod가 로컬 스토리지를 사용하고 있었다면 데이터 유실이 발생할 수 있으니 PV 사용을 권장합니다.\n이러한 확인 프로세스를 노드 또는 Pod 단위로 수행하고 Pod이 모두 이동하게 되면 노드를 제거합니다.

\n
\n

EKS AutoScaler

\n

EKS의 AutoScaler는 AWS의 Auto Scaling Group을 활용하고 있습니다.\nASG는 주기적으로 현재 상태를 확인하고 Desired State로 변화하는 방식으로 동작합니다.\n사용자는 클러스터 노드 수를 제한하는 Min, Max 값을 지정할 수 있습니다.

\n

\n \n \n \n

\n

위와 같이 목적에 따라 여러 종류의 ASG를 설정하고 서로 다른 AutoScaling Policy를 적용할 수 있습니다.\nSpot Instance Group을 설정하면 저렴하지만 입찰 가격에 의해 언제든지 인스턴스가 내려갈 수 있습니다.\n하지만 EKS의 Spot Interrupt Handler (DeamonSet) 에 의해 정상적으로 실행 중인 Pod들을 재배치할 수 있습니다.

\n

\n \n \n \n

\n

아마 AWS를 production 환경에서 사용하고 있다면 VPC 구성은 이미 잘 이해하고 계시리라 생각합니다.\nVPC 내에 생성한 인스턴스는 eth0이라는 기본 네트워크 인터페이스를 가지게 됩니다.\n그리고 네트워크 인터페이스에 하나 이상의 IPv4 또는 IPv6 주소를 할당할 수 있습니다.\n또한 각 Subnet에 존재하는 인스턴스는 Route Table을 통해 통신을 할 수 있습니다.\n여기까지가 우리가 알고 있는 VPC 내의 Host 간 통신입니다.

\n

그렇다면 EKS는 어떤 점이 다를까요?\n쿠버네티스의 Pod은 한 개 이상의 컨테이너를 구성하고 같은 Host와 Network 스택을 공유합니다.\n그리고 여러 Host에 사이에 걸쳐 생성된 Pod은 Overlay Network를 통해 서로 통신하게 됩니다.\n기존 VPC 환경에서는 Pod 네트워크 통신을 기존 방식처럼 지원하기 어려웠습니다.

\n

하지만 대부분의 사용자들이 VPC 기반의 인프라를 구성하고 있었기 때문에\nEKS는 VPC를 지원할 수 있어야 했습니다.\n예를 들어 사용자는 Security Group, VPC Flow 로그 등의 기능을 그대로 사용하면서,\nPrivateLink를 통해 다른 AWS 서비스와 통신할 수 있어야 합니다.\n이 문제를 해결하기 위해 AWS는 CNI 라는 네트워크 플러그인을 지원하기 시작했습니다.

\n
\n

EKS CNI

\n

\n

\n

처음 Worker Node가 추가되면 하나의 ENI 가 인스턴스에 할당됩니다.\n하지만 실행되는 Pod의 수가 단일 ENI 에서 허용하는 주소를 초과하면 CNI는 노드에 새로운 ENI 를 추가합니다.\nENI 에 secondary IP 할당과 Pod에 할당할 노드의 IP 주소 풀 관리는 L-IPAM 데몬을 통해 이루어집니다.\nL-IPAM 데몬은 모든 노드에 DeamonSet으로 배포되며 gRPC를 통해 CNI 플러그인과 통신합니다.

\n

사용하고 있는 인스턴스 유형이 m5.xlarge라고 가정하고 예시를 들어보겠습니다.\n우선 m5.xlarge 유형은 4 ENI 와 ENI 당 15 개의 IP 주소를 가질 수 있습니다.\n배포된 Pod의 수가 0에서 14 사이라면 IPAM 데몬은 2개의 Warm Pool을 유지하기 위해 ENI를 하나 더 할당합니다.\n이때 사용가능한 IP 수는 2 * (15 - 1) = 28 개가 됩니다.\n이런식으로 Warm Pool을 늘려가면서 최대 4 * (15 - 1) = 56 개의 IP를 가질 수 있습니다.\n물론 이 부분은 WARM_ENI_TARGET 과 같은 CNI 옵션을 통해 수정할 수 있습니다.

\n
\n

\n \n \n \n

\n

구체적으로 CNI를 통해 Pod1 과 Pod2가 어떻게 통신하는지 다이어그램으로 표현하면 위와 같습니다.\n각 Pod의 eth0에는 secondary IP address가 할당되며 Pod Side Route Table를 가지고 있습니다.\n노드의 네트워크 인터페이스까지 도달한 패킷은 EC2-VPC fabric에 의해 포워딩 됩니다.

\n

따라서 EKS 노드를 결정할 때 ENI 제한 관련 부분도 중요하게 생각하셔야 합니다.\n노드 당 ENI, IP 주소 제한은 해당 공식 문서에서 확인하실 수 있습니다.\n물론 CNI를 사용하지 않고 기존의 Calico와 같은 Overlay Network를 사용할 수도 있습니다.\n하지만 이를 사용하게 되면 네트워크까지 관리해야하며 새로운 장애 포인트로 이어질 수 있습니다.

\n


\n

Reference

\n\n
","excerpt":"모든 Kubernetes as a Service가 그렇듯 EKS 역시 빠르게 변화하고 있습니다.\n오늘의 주제는 EKS의 VPC…"}}}},{"node":{"title":"AWS MFA CLI 설정 변경 자동화하기","id":"0fb3ff35-6e59-55ed-a84f-41ea9ef99d84","slug":"aws-cli-mfa","publishDate":"October 03, 2019","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

클라우드 인프라를 관리하는 경우 여러 계정에 걸친 CLI를 사용하는 경우가 빈번합니다.\n만일 CLI 사용 시 MFA 인증을 요구하는 계정과 아닌 계정이 혼재되어 있다면 설정이 정말 귀찮아집니다.\n이 글에서는 간단한 스크립트를 통해 AWS CLI 설정 변경을 자동화해보려 합니다.

\n
\n

AWS Credential 설정

\n

먼저 사용하는 프로필의 credential 정보를 설정해줍니다.\nMFA인 프로필과 MFA가 아닌 프로필을 구분하기 위해 아래와 같은 구조로 저장하겠습니다.\n-default가 붙은 프로필이 MFA를 사용하는 프로필입니다.\n스크립트 실행을 통해 autogen 값이 자동생성됩니다.

\n
[test]\naws_access_key_id = mykey\naws_secret_access_key = mykey\n \n[dev-default]\naws_access_key_id = mykey\naws_secret_access_key = mykey\n \n[prod-default]\naws_access_key_id = mykey\naws_secret_access_key = mykey\n \n[dev]\naws_arn_mfa = arn:aws:iam::myaccount:mfa/myaccount\naws_access_key_id = autogen\naws_secret_access_key = autogen\naws_session_token = autogen\n \n[prod]\naws_arn_mfa = arn:aws:iam::myaccount:mfa/myaccount\naws_access_key_id = autogen\naws_secret_access_key = autogen\naws_session_token = autogen
\n\n
\n

MFA 설정 스크립트

\n

다음으로 아래의 스크립트를 각 awsp.sh, mfa.py 이름으로 ~/.aws/ 경로에 추가해줍니다.\n이후에 ~/.zshrc 또는 ~/.bashrc 경로에 source ~/.aws/awsp.sh를 추가해줍니다.

\n
#! /bin/bash\n\nsetProfile() {\n  export AWS_PROFILE=$1\n  export AWS_DEFAULT_PROFILE=$1\n\n  python ~/.aws/mfa.py --profile $1 $2\n}\nalias awsp=setProfile
\n
\n
import os\nimport json\nimport sys\nimport argparse\nimport subprocess\nimport configparser\n\nparser = argparse.ArgumentParser(description='Update your AWS CLI Token')\nparser.add_argument('token', help='token from your MFA device')\nparser.add_argument('--profile', help='aws profile to store the session token', default=os.getenv('AWS_PROFILE'))\nparser.add_argument('--arn', help='AWS ARN from the IAM console (Security credentials -> Assigned MFA device). This is saved to your .aws/credentials file')\nparser.add_argument('--credential-path', help='path to the aws credentials file', default=os.path.expanduser('~/.aws/credentials'))\n\nargs = parser.parse_args()\n\nif args.profile is None:\n    parser.error('Expecting --profile or profile set in environment AWS_PROFILE. e.g. \"stage\"')\n\nconfig = configparser.ConfigParser()\nconfig.read(args.credential_path)\n\nif args.profile not in config.sections():\n    parser.error('Invalid profile. Section not found in ~/.aws/credentails')\n\nif args.arn is None:\n    if 'aws_arn_mfa' not in config[args.profile]:\n        sys.exit(0)\n# parser.error('ARN is not provided. Specify via --arn')\n\n    args.arn = config[args.profile]['aws_arn_mfa']\nelse:\n    # Update the arn with user supplied one\n    config[args.profile]['aws_arn_mfa'] = args.arn\n\n# Generate the session token from the profile\nresult = subprocess.run(['aws', 'sts', 'get-session-token', '--profile', args.profile + '-default', '--serial-number', args.arn, '--token-code', args.token], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\nif result.returncode != 0:\n    parser.error(result.stderr.decode('utf-8').strip('\\n'))\n\ncredentials = json.loads(result.stdout.decode('utf-8'))['Credentials']\n\nconfig[args.profile]['aws_access_key_id'] = credentials['AccessKeyId']\nconfig[args.profile]['aws_secret_access_key'] = credentials['SecretAccessKey']\nconfig[args.profile]['aws_session_token'] = credentials['SessionToken']\n\n# Save the changes back to the file\nwith open(args.credential_path, 'w') as configFile:\n    config.write(configFile)\n\nprint('Saved {} credentials to {}'.format(args.profile, args.credential_path))
\n
\n

이제 awsp [프로필명] [mfa code]과 같은 명령어로 사용하실 수 있습니다.\n예를 들어 dev 프로필의 경우 awsp dev 012345와 같이 실행합니다.\nMFA 조건이 없는 프로필의 경우 awsp test 0과 같이 실행하시면 됩니다.

\n
","excerpt":"클라우드 인프라를 관리하는 경우 여러 계정에 걸친 CLI를 사용하는 경우가 빈번합니다.\n만일 CLI 사용 시 MFA…"}}}},{"node":{"title":"Terraform 입문자를 위한 미세 팁","id":"04c97999-9e82-53a0-bca8-0ac8c6db3757","slug":"tf-tips","publishDate":"September 20, 2019","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

클라우드를 활용하는 경우, 인프라 구성 관리 도구로 테라폼을 많이 사용합니다.\n오늘은 처음 테라폼을 도입하려고 할때 알아두면 좋은 점들에 대해 정리해보려 합니다.

\n
\n

Procedural vs Declarative

\n
# Ansible\n- ec2:\n  count: 10\n  image: ami-v1\n  instance_type: t2.micro\n\n# Terraform\nresource \"aws_instance\" \"example\" {\n  count = 10\n  ami = \"ami-v1\"\n  instance_type = \"t2.micro\"\n}
\n

위에 나와있는 코드는 Ansible과 Terraform으로 EC2 인스턴스를 구성하는 코드입니다.\n만일 여기서 둘의 count 값을 15로 변경한다면 어떻게 변할까요?

\n

먼저 Ansible의 경우, procedural이며 mutable infrastructure를 지향합니다.\n따라서 이미 생성된 10개의 인스턴스에 15개의 인스턴스가 추가로 생성되어 총 25개의 인스턴스가 떠있게 됩니다.\n반면에 Terraform의 경우, declarative이며 immutable infrastructure를 지향합니다.\ncount를 15로 선언했기 때문에 Terraform은 이전 상태와 비교한다음, 5만큼의 변경에 대해 교체를 수행합니다.\n결과적으로 총 15개의 인스턴스가 떠있게 됩니다.

\n

서로 지향하는 성격이 다르다보니 적절한 상황에 사용하거나 함께 사용하면 좋습니다.\n예를 들어 Provisioning 단계에서 Terraform을 사용하고\nConfiguration, Dependency 설정 단계에서 Ansible을 사용하실 수 있습니다.

\n
\n

Terraform vs CloudFormation

\n

AWS를 사용하는 경우, 클라우드 내에서 CloudFormation이라는 서비스를 제공합니다.\nCloudFormation 역시 Terraform과 같은 기능을 제공하다보니 도입하기 전에 비교를 많이 합니다.\n우선 모듈화, 개발, 문서 측면에서는 Terraform이 더 편했습니다.\n이외의 큰 차이를 정리하자면 아래와 같습니다.

\n

CloudFormation은 AWS 지원이 빠릅니다.\n신규 릴리즈된 서비스나 설정들은 Terraform AWS 모듈에 반영되기까지 시간이 좀 걸립니다.\n반면에 CloudFormation은 대부분 바로 지원해주다보니 더 편할 수 있습니다.

\n

Terraform은 다른 클라우드 서비스도 지원합니다(Azure, Google Cloud).\n만일 멀티클라우드 이슈에 대한 대응까지 고려하고 있다면 Terraform을 추천드립니다.

\n
\n

Terraform Remote Backend

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

가장 간단하게 웹 서버와 캐시 서버를 구성하는 방법은 위 그림과 같이 앞서 배웠던 Sidecar를 활용하는 방법.\n이 방법은 간단하게 구현할 수 있지만 내 웹 서버와 동일한 레벨에 있기 때문에 각자 스케일링하기 어렵다.\nKubernetes의 Deployments와 Service로 동일한 환경을 쉽게 구성해볼 수 있다.

\n
\n

Shared Services

\n

\n \n \n \n

\n

scatter, getter 패턴은 스케일링, 분산처리에 유용한 패턴 중 하나이다.\n위에 언급했던 패턴들처럼 트리 구조로 되어 있고,\nroot 서버가 요청을 분산시키거나 여러 서버로부터 받아오는 구조.\n이 패턴은 분산처리에서 embarassingly parallel 문제를 해결하는 것과 동일하다.

\n

\n \n \n \n

\n

Hands On: Distributed Document Search

\n

모든 문서 파일로부터 cat, dog 단어가 모두 포함되어 있는지 검색하는 문제를 풀어야 한다고 가정해보자.\n왼쪽 그림과 같이 각 노드로부터 서로 다른 단어를 검색하고 결과를 내는 방법이 있다.\n하지만 document 하나의 사이즈가 아주 크다면? 하나의 노드에서 처리는 불가능하다.\n이 경우, 단어 기준이 아니라 document 기준으로 분산처리해야 한다. (MapReduce의 WordCount 예제와 동일)

\n

이 때 적절한 노드 개수를 선정하는 것이 중요하다. (컴퓨팅과 비용 사이의 문제)\n노드가 많아질수록 요청을 분산하고 수집하는데 오버헤드가 발생할 수 있다.\n위 경우 처리가 가장 오래걸리는 노드의 시간이 전체 처리 시간을 결정할 것이다. (straggler problem)\n이를 해결하기 위해 Erasure Coding을 통해 연산을 추정하는 방법도 있다.

\n

\n \n \n \n

\n

Scaling Scatter/Gather for Reliability and Scale

\n

항상 실패에 대한 대비가 되어 있어야 한다. (Fault Tolerance)\n앞서 설명한대로 단순히 샤드의 수를 늘리는 것은 적절하지 못한 방법이다.\n각 Shard가 Replica로 구성되어 있기 때문에 동시에 업그레이드도 가능할 수 있어야 한다.

\n
\n

Functions and Event-Driven Processing

\n

방금 전까지는 long-running computation에 대한 패턴에 대해 소개했다면,\n이번에는 FaaS 기반의 일시적인 서비스에 대한 패턴에 대한 내용이다. (serverless computing)\n서버리스는 좋은 패턴이자 도구이지만 모든 어플리케이션에 무조건 적용하기 보다는\n장점과 단점을 잘 이해하고 적절한 경우에 사용하는 것이 좋다.

\n\n

\n \n \n \n

\n

FaaS Decorator Pattern: Request or Response Transformation

\n

Input이 들어왔을 때, transform 해서 output을 내는 패턴이다.\n이번에는 Kubernetes 기반의 serverless 플랫폼인 kubeless로 간단한 예제를 진행해보았다.\n설치와 실행은 아래와 같이 진행하면 된다.\n예제 코드와 이에 대한 자세한 설명은 https://github.com/Swalloow/KubeStudy/tree/master/faas 에서 확인.

\n
# install kubeless, cli\nkubectl create ns kubeless\nkubectl create -f https://github.com/kubeless/kubeless/releases/download/v1.0.3/kubeless-v1.0.3.yaml\n\ncurl -OL https://github.com/kubeless/kubeless/releases/download/v1.0.3/kubeless_darwin-amd64.zip\nunzip kubeless_darwin-amd64.zip\nsudo mv bundles/kubeless_darwin-amd64/kubeless /usr/local/bin/\n\n# install kubeless UI\nkubectl create -f https://raw.githubusercontent.com/kubeless/kubeless-ui/master/k8s.yaml\nkubectl get svc ui -n kubeless\nkubectl port-forward svc/ui -n kubeless 3000:3000
\n

Hands On: Implementing Two-Factor Authentication

\n\n

Hands On: Implementing a Pipeline for New-User Signup

\n\n
\n

9. Ownership Election

\n

\n \n \n \n

\n

분산 시스템에서는 Ownership을 결정하는 것이 중요하다. 리더가 여러 책임을 가지고 있다.\n마스터 노드에 장애가 발생하더라도 Election에 의해 새로운 마스터 노드를 선정할 수 있어야 한다.

\n

Determining If You Even Need Master Election

\n\n

The Basics of Master Election

\n

가장 잘 알려진 Leader Election 알고리즘으로 Paxos와 Raft가 있다.\n자세한 내용은 이전에 작성한 Raft Consensus 글을 참고.

\n
\n

Reference

\n\n
","excerpt":"구글 클라우드 팀이 Kubernetes와 같은 Container Orchestration…"}}}},{"node":{"title":"Amazon EKS에 Kubeflow 구축하기","id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","slug":"eks-kubeflow","publishDate":"March 10, 2019","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}}},{"node":{"title":"KOPS로 AWS에 Kubernetes 클러스터 구축하기","id":"1b7724e0-f57a-512f-aac4-9a43284c7e5f","slug":"aws-kops","publishDate":"February 10, 2019","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kubernetes 클러스터를 구성하는 방법은 여러 가지가 있습니다.\n그 중에서 kubeadam은 온프레미스 환경에서 많이 사용하고 kops는 클라우드 환경에서 많이 사용하고 있습니다. 이번 글에서는 kops로 AWS EC2에 Kubernetes 클러스터 구축하는 방법에 대해 정리해보겠습니다.

\n
\n

kops, kubectl, awscli 설치 (Linux)

\n
# kops 설치\nwget -O kops https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '\"' -f 4)/kops-linux-amd64\nchmod +x ./kops\nsudo mv ./kops /usr/local/bin/\n\n# kubectl 설치\nwget -O kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl\nchmod +x ./kubectl\nsudo mv ./kubectl /usr/local/bin/kubectl\n\n# aws-cli 설치 (amazon linux라면 불필요)\npip install awscli
\n
\n

IAM User 설정

\n
# 아래의 권한이 필요\nAmazonEC2FullAccess\nAmazonRoute53FullAccess\nAmazonS3FullAccess\nIAMFullAccess\nAmazonVPCFullAccess
\n
\n

aws-cli로 IAM 계정 생성

\n
aws iam create-group --group-name kops\n\naws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess --group-name kops\naws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonRoute53FullAccess --group-name kops\naws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess --group-name kops\naws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/IAMFullAccess --group-name kops\naws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonVPCFullAccess --group-name kops\n\naws iam create-user --user-name kops\naws iam add-user-to-group --user-name kops --group-name kops\naws iam create-access-key --user-name kops\n\naws configure   # AccessKeyID와 SecretAccessKey 등록
\n
\n

DNS, Cluster State storage 설정

\n\n
# Create Bucket\naws s3api create-bucket \\\n    --bucket prefix-example-com-state-store \\\n    --region ap-northeast-2\n\n# S3 versioning\naws s3api put-bucket-versioning \\\n    --bucket prefix-example-com-state-store \\\n    --versioning-configuration Status=Enabled
\n
\n

Kubernetes Cluster 생성

\n\n
# Environment\nexport NAME=myfirstcluster.example.com  # DNS가 설정되어 있는 경우\nexport NAME=myfirstcluster.k8s.local    # DNS가 설정되어 있지 않은 경우\nexport KOPS_STATE_STORE=s3://prefix-example-com-state-store\n\n# Seoul region\naws ec2 describe-availability-zones --region ap-northeast-2\nkops create cluster --zones ap-northeast-2 ${NAME}\nkops edit cluster ${NAME}\nkops update cluster ${NAME} --yes\nkops validate cluster\n\n# Kubectl\nkubectl get nodes\nkubectl cluster-info\nkubectl -n kube-system get po   # system pod\n\n# Dashboard\nkops get secrets admin -oplaintext\nkubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml\n\n# Access https://<kubernetes-master-hostname>/ui\nkops get secrets admin --type secret -oplaintext\n\n# Stop cluster\n# Change minSize, MaxSize to 0\nkops get ig\nkops edit ig nodes\nkops edit ig master
\n
\n

Advanced

\n\n
# SSH Key\nssh-keygen -t rsa -f $NAME.key -N ''\nexport PUBKEY=\"$NAME.key.pub\"\n\n# CoreOS Image\nexport IMAGE=$(curl -s https://coreos.com/dist/aws/aws-stable.json|sed 's/-/_/g'|jq '.'$REGION'.hvm'|sed 's/_/-/g' | sed 's/\\\"//g')\n\n# Create Cluster\nkops create cluster --kubernetes-version=1.12.1 \\\n    --ssh-public-key $PUBKEY \\\n    --networking flannel \\\n    --api-loadbalancer-type public \\\n    --admin-access 0.0.0.0/0 \\\n    --authorization RBAC \\\n    --zones ap-northeast-2 \\\n    --master-zones ap-northeast-2 \\\n    --master-size t2.medium \\\n    --node-size t2.medium \\\n    --image $IMAGE \\\n    --node-count 3 \\\n    --cloud aws \\\n    --bastion \\\n    --name $NAME \\\n    --yes
\n
\n

Reference

\n\n
","excerpt":"Kubernetes 클러스터를 구성하는 방법은 여러 가지가 있습니다.\n그 중에서 kubeadam은 온프레미스 환경에서 많이 사용하고 kops…"}}}},{"node":{"title":"분산 컨테이너 환경에서의 디자인 패턴 (1)","id":"b5808ba3-22f9-5847-b6ba-e8960369c054","slug":"container-patterns","publishDate":"January 26, 2019","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

구글 클라우드 팀이 Kubernetes와 같은 Container Orchestration 기술을 개발하면서 겪은\n분산 컨테이너 환경에서의 디자인 패턴에 대해 정리한 내용입니다.\n분산 어플리케이션을 컨테이너 환경으로 옮기려는 분들에게 많은 도움이 될 듯 합니다.

\n\n
\n

Modular Container Design

\n

최근 많은 개발 환경이 Docker 기반의 컨테이너 환경으로 옮겨가고 있습니다.\n그 중에서는 복잡한 의존성에서 벗어나 독립적으로 운영하고 싶어서 옮기는 경우가 많습니다.

\n

하지만 분산 컨테이너 환경은 아주 복잡하게 연결되어 있어서 운영하기 힘듭니다.\n복잡한 연결을 쉽게 이해하려면 이 문제가 어디에 해당하는지 경계를 잘 정의하는 것이 중요합니다.\n이렇게 경계를 정의하고 분류하는 것을 모듈화라고 부릅니다.

\n

우리는 과거부터 효율적인 코드를 작성하기 위해 절차지향 프로그래밍에서 객체지향 프로그래밍으로 변화해왔습니다.\n객체지향 프로그래밍의 클래스는 경계를 정의한다는 측면에서 컨테이너 환경과 유사한 면이 있습니다.

\n

\n \n \n \n

\n

과거 모놀리틱 아키텍쳐에서는 왼쪽 그림과 같은 구조로 설계되어 왔습니다.\n우리가 기여하거나 자주 수정하는 코드가 있는 반면, 코어 모듈이나 공통에 해당하는 부분은 잘 변하지 않습니다.

\n

컨테이너 환경은 이와 조금 다릅니다.\n우선 컨테이너 환경에서 컨테이너는 하나의 어플리케이션이 아니라는 점을 이해하고 있어야 합니다.\n하나의 컨테이너는 객체지향 언어의 클래스 또는 함수와 유사합니다.\n오른쪽 그림처럼 작은 모듈 조각을 모으고 조립해서 다음 어플리케이션을 설계하는 형태가 되어야 합니다.

\n
\n

Benefit

\n

위와 같은 컨테이너 환경을 구성했을 때 가지는 장점은 아래와 같습니다.

\n\n
\n

Requirements

\n

위와 같은 컨테이너 환경을 구성하기 위해 필요한 요소는 아래와 같습니다.

\n\n

특히 여러 컨테이너에서 공통으로 사용하는 라이브러리의 경우 필요

\n
\n

Sidecar Pattern

\n

\n \n \n \n

\n

이제부터 자주 사용되는 세 가지 디자인 패턴을 소개드리려고 합니다.\n먼저 첫 번째는 사이드 카 패턴입니다.\n사이드 카 패턴은 이전에 사용되던 컨테이너의 기능을 확장시키고 싶을 때 유용하게 사용됩니다.\n여기서 이전에 사용되던 컨테이너란 잘 변하지 않으며 같은 작업을 반복하는 어플리케이션을 말합니다.

\n

위 그림의 예시에서 이전에 사용되던 컨테이너는 왼쪽의 node.js 어플리케이션 입니다.\nnode.js 어플리케이션은 단순히 파일 시스템에 접근하여 어떤 작업을 수행하는 일만 합니다.\n만일 파일 시스템에 대해 git 동기화 기능을 추가하고 싶다면, 오른쪽 컨테이너처럼 확장시킬 수 있습니다.\nnode.js 어플리케이션은 사이드 카 컨테이너가 어떤 작업을 수행하는지 고려할 필요가 없습니다.\n사이드 카 컨테이너 역시 어떤 어플리케이션이 이 파일을 서빙하는지 고려할 필요가 없습니다.\n앞서 말한 것처럼 관심사의 분리를 만족시키며, 컨테이너를 관리하는 팀을 분리시킬 수 있습니다.\n또한 사이드 카 컨테이너를 다른 어플리케이션에서 재사용할 수 있고, 더 다양한 기능으로 확장시킬 수 있습니다.

\n
\n

Ambassador Pattern

\n

\n \n \n \n

\n

다음은 엠베서더 패턴입니다.\n엠베서더 패턴은 어플리케이션을 대신하여 외부의 네트워크 또는 요청을 처리해야할 때 유용하게 사용됩니다.

\n

위 그림의 예시에서 어플리케이션은 PHP 앱이고 Memcache를 사용한 지속적인 해싱이 필요하다고 가정해보겠습니다.\n그리고 Memcache 사용을 위해 twemproxy라는 라이브러리를 가져와야 합니다.\n위 그림처럼 어플리케이션과 twemproxy 컨테이너를 분리시킨다면,\ntwemproxy 컨테이너는 외부에 있는 Memcache 샤드를 관리하고 통신하는 역할을 수행할 수 있습니다.\n기존에 있던 어플리케이션 컨테이너는 twemproxy 컨테이너와 같은 네임스페이스에 존재하지만, 외부의 통신에 대해서는 관여할 필요가 없습니다.\n역시 마찬가지로 관심사의 분리를 만족시키며, 재사용될 수 있고, 다양한 기능으로 확장시킬 수 있습니다.

\n
\n

Adapter Pattern

\n

\n \n \n \n

\n

많이 사용하는 prometheus exporter도 이와 같은 패턴으로 설계되어 있습니다.\nexporter는 모니터링 시스템과 쉽게 결합할 수 있습니다.\n그리고 exporter와 memcache, redis는 결합해서 하나의 컨테이너로 배포하는 형태입니다.\nredis-exporter에 코드 변경이 이루어지더라도 memcache-exporter는 변경될 필요가 없습니다.

\n
\n

Reference

\n

이외에도 Replication, Micro service Load Balancer에 사용되는 다양한 패턴이 존재합니다.\n분산 컨테이너 환경에서 어플리케이션을 개발하는 일은 레고 블럭을 조립하는 것과 비슷합니다.\n더 자세한 내용이 궁금하신 분은 아래 링크를 확인하시면 됩니다.

\n\n
","excerpt":"구글 클라우드 팀이 Kubernetes와 같은 Container Orchestration…"}}}},{"node":{"title":"Apache Airflow에 기여하면서 배운 점들","id":"a393498e-de9e-5231-bc9f-fd1df0495f45","slug":"airflow-contrib","publishDate":"December 08, 2018","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":5,"humanPageNumber":6,"skip":31,"limit":6,"numberOfPages":16,"previousPagePath":"/5","nextPagePath":"/7"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/6","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"분산 컨테이너 환경에서의 디자인 패턴 (3)","id":"5973ce53-a8c6-5165-af2f-eb64b920f602","slug":"container-patterns3","publishDate":"April 06, 2019","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

구글 클라우드 팀이 Kubernetes와 같은 Container Orchestration 기술을 개발하면서 겪은\n분산 컨테이너 환경에서의 디자인 패턴에 대해 정리한 내용입니다.\n지난 글에 이어서 배치 작업에 관련된 디자인 패턴에 대한 내용입니다.

\n\n
\n

Batch Computational Patterns

\n

앞서 설명했던 long-running computation 패턴과 달리 이번에는 일시적으로 돌아가는 Batch Computational 패턴에 대한 내용이다.\n배치 프로세스는 사용자 로그 데이터의 수집, 데이터 분석 또는 미디어 파일의 변환 등에 자주 사용된다.\n일반적으로 대용량 데이터의 처리 속도를 높이기 위해 병렬 처리를 사용한다는 특징이 있다.\n이에 대해 가장 유명한 패턴은 MapReduce 패턴이며 이미 많이 사용하고 있다.

\n
\n

Work Queue Systems

\n

\n \n \n \n

\n

배치 프로세싱의 가장 간단한 형태가 바로 Work Queue System 이다.\nQueue 형태의 작업 대기열이 있고 이를 관리해주는 컨테이너가 Worker에 작업을 분배해주는 형태이다.\n일반적으로 작업 대기열에서 각 작업은 일정 시간 내에 수행되어야 하며 처리량에 따라 Worker는 Scale-out 할 수 있어야 한다.\n작업이 지속적으로 지연된다면 큐에 작업이 계속 쌓이게 되고 이는 장애로 이어질 수 있다.

\n
\n

Source, Worker Container Interface

\n\n
\n

Event-Driven Batch Processing

\n

앞서 설명한 작업 대기열 처리는 하나의 입력을 하나의 출력으로 변환할 때 많이 사용한다.\n하지만 단일 작업 이상을 처리해야 한다거나 단일 입력에서 여러 출력을 생성해야 하는 경우도 있다.\n이러한 경우 여러 작업 대기열을 연결하는 이벤트 스트림 방식을 통해 작업을 수행한다.\n이전 단계의 작업이 완료되면 이벤트가 발생하고 이벤트를 통해 다음 단계의 대기열로 이동하는 형태이다.

\n

\n \n \n \n

\n

스트림 처리에 대한 여러 패턴이 존재하지만 위 그림은 그 중 하나인 Sharder에 대한 내용이다.\nSharder는 작업 큐 2개를 두고 만일 두 개의 큐가 모두 healthy 하다면, id 기준으로 작업을 분배한다.\n만일 어느 하나가 unhealthy 하다면, 새로운 큐를 생성해서 다른 한쪽으로 작업을 분배한다.\n이를 통해 단일 큐를 사용하는 것보다 안정적이고 작업을 분산처리할 수 있다.

\n

이외에도 Publisher/Subscriber, Merger, Splitter 등의 패턴이 있다.\nSpark의 DAG와 같은 그림을 떠올린다면 어떤 내용인지 바로 이해할 수 있을 것이다.\n처리 시간이 비교적 짧은 이벤트 기반의 배치 프로세싱은 앞서 소개했던 FaaS 패턴으로 구현할 수도 있다.\nAWS의 Lambda, Stream, Kinesis Firehose를 떠올리면 된다.

\n
\n

Coordinated Batch Processing

\n

\n \n \n \n

\n

처음에는 단일 대기열로 처리했지만 더 복잡한 배치 작업을 처리하기 위해 대기열을 분할하고 연결했다.\n하지만 최종 단계에서는 결국 원하는 결과를 생성하기 위해 여러 출력을 하나로 합쳐야 한다.\n합치는 작업은 여러 개의 작업 큐가 모두 종료되고 난 이후에 수행되어야 한다.\n이와 관련된 패턴으로 Join과 Reduce가 있다.\nJoin과 달리 Reducer의 경우 parallel 하게 시작할 수 있다는 차이가 있다.\ncount, sum, histogram을 추출하는 작업이 이에 해당한다.

\n
\n

Reference

\n

책을 마무리할 무렵 Kubernetes Korea Group 세미나에서 책 저자의 발표를 들을 수 있었다.\n다른 컨퍼런스에서 진행했던 발표들과 내용이 유사했고 Azure Kuberentes Service에 대한 홍보가 짙었지만\n클라우드 네이티브와 최근 개발 환경의 변화에 대한 생각을 들을 수 있는 시간이었다.

\n

그동안 개발할 때 정형화 된 패턴을 공부하지 않았음에도 비슷한 형태로 설계된 경우를 많이 볼 수 있었다.\n하지만 패턴을 한번 정리하고 나면 더 명확하게 이해되고 현재 상황에 맞는 패턴을 적용할 수 있게 되는 것 같다.\n마틴 파울러의 리팩토링 책도 그렇고 이번 책 또한 그런 부분을 느낄 수 있었다.

\n\n
","excerpt":"구글 클라우드 팀이 Kubernetes와 같은 Container Orchestration…"}}}},{"node":{"title":"분산 컨테이너 환경에서의 디자인 패턴 (2)","id":"e70f1f8d-c9d5-550e-bc7d-0b3004a22a98","slug":"container-patterns2","publishDate":"March 23, 2019","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

구글 클라우드 팀이 Kubernetes와 같은 Container Orchestration 기술을 개발하면서 겪은\n분산 컨테이너 환경에서의 디자인 패턴에 대해 정리한 내용입니다.\n지난 글에 이어서 멀티 노드에 관련된 디자인 패턴에 대한 내용입니다.

\n\n
\n

Multi-Node Serving Pattern

\n

지난 번에 소개했던 싱글 노드 패턴들은 컨테이너 간에 커플링이 강하다는 특징이 있다.\n반면 이번에 소개할 멀티 노드 패턴들은 컨테이너 간에 커플링이 약하다.\n서로 다른 노드 간에 네트워크 호출을 통해 통신이 이루어지며\nParallel, Coordinate via loose synchronization에 대한 이슈가 존재한다.\n특히 어플리케이션이 MSA 아키텍쳐로 구성된 경우, 컨테이너 간의 커플링이 낮다보니 스케일링에 이점이 있다.\n하지만 여러 노드에서 복잡한 구조로 동작하다보니 디버깅이 어려울 수 있다.

\n
\n

Replicated Load-Balanced Services

\n

LoadBalancer가 트래픽을 관리하는 패턴이다. 가장 간단하면서 잘 알려져 있다.\nstateless한 서비스의 경우, LoadBalancer + Replica 형태로 쉽게 구성할 수 있다.\n처리해야 하는 요청에 따라 Scale-Up, Down을 쉽게 구성할 수 있어야 한다.

\n\n

\n \n \n \n

\n

가장 간단하게 웹 서버와 캐시 서버를 구성하는 방법은 위 그림과 같이 앞서 배웠던 Sidecar를 활용하는 방법.\n이 방법은 간단하게 구현할 수 있지만 내 웹 서버와 동일한 레벨에 있기 때문에 각자 스케일링하기 어렵다.\nKubernetes의 Deployments와 Service로 동일한 환경을 쉽게 구성해볼 수 있다.

\n
\n

Shared Services

\n

\n \n \n \n

\n

scatter, getter 패턴은 스케일링, 분산처리에 유용한 패턴 중 하나이다.\n위에 언급했던 패턴들처럼 트리 구조로 되어 있고,\nroot 서버가 요청을 분산시키거나 여러 서버로부터 받아오는 구조.\n이 패턴은 분산처리에서 embarassingly parallel 문제를 해결하는 것과 동일하다.

\n

\n \n \n \n

\n

Hands On: Distributed Document Search

\n

모든 문서 파일로부터 cat, dog 단어가 모두 포함되어 있는지 검색하는 문제를 풀어야 한다고 가정해보자.\n왼쪽 그림과 같이 각 노드로부터 서로 다른 단어를 검색하고 결과를 내는 방법이 있다.\n하지만 document 하나의 사이즈가 아주 크다면? 하나의 노드에서 처리는 불가능하다.\n이 경우, 단어 기준이 아니라 document 기준으로 분산처리해야 한다. (MapReduce의 WordCount 예제와 동일)

\n

이 때 적절한 노드 개수를 선정하는 것이 중요하다. (컴퓨팅과 비용 사이의 문제)\n노드가 많아질수록 요청을 분산하고 수집하는데 오버헤드가 발생할 수 있다.\n위 경우 처리가 가장 오래걸리는 노드의 시간이 전체 처리 시간을 결정할 것이다. (straggler problem)\n이를 해결하기 위해 Erasure Coding을 통해 연산을 추정하는 방법도 있다.

\n

\n \n \n \n

\n

Scaling Scatter/Gather for Reliability and Scale

\n

항상 실패에 대한 대비가 되어 있어야 한다. (Fault Tolerance)\n앞서 설명한대로 단순히 샤드의 수를 늘리는 것은 적절하지 못한 방법이다.\n각 Shard가 Replica로 구성되어 있기 때문에 동시에 업그레이드도 가능할 수 있어야 한다.

\n
\n

Functions and Event-Driven Processing

\n

방금 전까지는 long-running computation에 대한 패턴에 대해 소개했다면,\n이번에는 FaaS 기반의 일시적인 서비스에 대한 패턴에 대한 내용이다. (serverless computing)\n서버리스는 좋은 패턴이자 도구이지만 모든 어플리케이션에 무조건 적용하기 보다는\n장점과 단점을 잘 이해하고 적절한 경우에 사용하는 것이 좋다.

\n\n

\n \n \n \n

\n

FaaS Decorator Pattern: Request or Response Transformation

\n

Input이 들어왔을 때, transform 해서 output을 내는 패턴이다.\n이번에는 Kubernetes 기반의 serverless 플랫폼인 kubeless로 간단한 예제를 진행해보았다.\n설치와 실행은 아래와 같이 진행하면 된다.\n예제 코드와 이에 대한 자세한 설명은 https://github.com/Swalloow/KubeStudy/tree/master/faas 에서 확인.

\n
# install kubeless, cli\nkubectl create ns kubeless\nkubectl create -f https://github.com/kubeless/kubeless/releases/download/v1.0.3/kubeless-v1.0.3.yaml\n\ncurl -OL https://github.com/kubeless/kubeless/releases/download/v1.0.3/kubeless_darwin-amd64.zip\nunzip kubeless_darwin-amd64.zip\nsudo mv bundles/kubeless_darwin-amd64/kubeless /usr/local/bin/\n\n# install kubeless UI\nkubectl create -f https://raw.githubusercontent.com/kubeless/kubeless-ui/master/k8s.yaml\nkubectl get svc ui -n kubeless\nkubectl port-forward svc/ui -n kubeless 3000:3000
\n

Hands On: Implementing Two-Factor Authentication

\n\n

Hands On: Implementing a Pipeline for New-User Signup

\n\n
\n

9. Ownership Election

\n

\n \n \n \n

\n

분산 시스템에서는 Ownership을 결정하는 것이 중요하다. 리더가 여러 책임을 가지고 있다.\n마스터 노드에 장애가 발생하더라도 Election에 의해 새로운 마스터 노드를 선정할 수 있어야 한다.

\n

Determining If You Even Need Master Election

\n\n

The Basics of Master Election

\n

가장 잘 알려진 Leader Election 알고리즘으로 Paxos와 Raft가 있다.\n자세한 내용은 이전에 작성한 Raft Consensus 글을 참고.

\n
\n

Reference

\n\n
","excerpt":"구글 클라우드 팀이 Kubernetes와 같은 Container Orchestration…"}}}},{"node":{"title":"Amazon EKS에 Kubeflow 구축하기","id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","slug":"eks-kubeflow","publishDate":"March 10, 2019","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}}},{"node":{"title":"KOPS로 AWS에 Kubernetes 클러스터 구축하기","id":"1b7724e0-f57a-512f-aac4-9a43284c7e5f","slug":"aws-kops","publishDate":"February 10, 2019","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kubernetes 클러스터를 구성하는 방법은 여러 가지가 있습니다.\n그 중에서 kubeadam은 온프레미스 환경에서 많이 사용하고 kops는 클라우드 환경에서 많이 사용하고 있습니다. 이번 글에서는 kops로 AWS EC2에 Kubernetes 클러스터 구축하는 방법에 대해 정리해보겠습니다.

\n
\n

kops, kubectl, awscli 설치 (Linux)

\n
# kops 설치\nwget -O kops https://github.com/kubernetes/kops/releases/download/$(curl -s https://api.github.com/repos/kubernetes/kops/releases/latest | grep tag_name | cut -d '\"' -f 4)/kops-linux-amd64\nchmod +x ./kops\nsudo mv ./kops /usr/local/bin/\n\n# kubectl 설치\nwget -O kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl\nchmod +x ./kubectl\nsudo mv ./kubectl /usr/local/bin/kubectl\n\n# aws-cli 설치 (amazon linux라면 불필요)\npip install awscli
\n
\n

IAM User 설정

\n
# 아래의 권한이 필요\nAmazonEC2FullAccess\nAmazonRoute53FullAccess\nAmazonS3FullAccess\nIAMFullAccess\nAmazonVPCFullAccess
\n
\n

aws-cli로 IAM 계정 생성

\n
aws iam create-group --group-name kops\n\naws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess --group-name kops\naws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonRoute53FullAccess --group-name kops\naws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess --group-name kops\naws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/IAMFullAccess --group-name kops\naws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonVPCFullAccess --group-name kops\n\naws iam create-user --user-name kops\naws iam add-user-to-group --user-name kops --group-name kops\naws iam create-access-key --user-name kops\n\naws configure   # AccessKeyID와 SecretAccessKey 등록
\n
\n

DNS, Cluster State storage 설정

\n\n
# Create Bucket\naws s3api create-bucket \\\n    --bucket prefix-example-com-state-store \\\n    --region ap-northeast-2\n\n# S3 versioning\naws s3api put-bucket-versioning \\\n    --bucket prefix-example-com-state-store \\\n    --versioning-configuration Status=Enabled
\n
\n

Kubernetes Cluster 생성

\n\n
# Environment\nexport NAME=myfirstcluster.example.com  # DNS가 설정되어 있는 경우\nexport NAME=myfirstcluster.k8s.local    # DNS가 설정되어 있지 않은 경우\nexport KOPS_STATE_STORE=s3://prefix-example-com-state-store\n\n# Seoul region\naws ec2 describe-availability-zones --region ap-northeast-2\nkops create cluster --zones ap-northeast-2 ${NAME}\nkops edit cluster ${NAME}\nkops update cluster ${NAME} --yes\nkops validate cluster\n\n# Kubectl\nkubectl get nodes\nkubectl cluster-info\nkubectl -n kube-system get po   # system pod\n\n# Dashboard\nkops get secrets admin -oplaintext\nkubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml\n\n# Access https://<kubernetes-master-hostname>/ui\nkops get secrets admin --type secret -oplaintext\n\n# Stop cluster\n# Change minSize, MaxSize to 0\nkops get ig\nkops edit ig nodes\nkops edit ig master
\n
\n

Advanced

\n\n
# SSH Key\nssh-keygen -t rsa -f $NAME.key -N ''\nexport PUBKEY=\"$NAME.key.pub\"\n\n# CoreOS Image\nexport IMAGE=$(curl -s https://coreos.com/dist/aws/aws-stable.json|sed 's/-/_/g'|jq '.'$REGION'.hvm'|sed 's/_/-/g' | sed 's/\\\"//g')\n\n# Create Cluster\nkops create cluster --kubernetes-version=1.12.1 \\\n    --ssh-public-key $PUBKEY \\\n    --networking flannel \\\n    --api-loadbalancer-type public \\\n    --admin-access 0.0.0.0/0 \\\n    --authorization RBAC \\\n    --zones ap-northeast-2 \\\n    --master-zones ap-northeast-2 \\\n    --master-size t2.medium \\\n    --node-size t2.medium \\\n    --image $IMAGE \\\n    --node-count 3 \\\n    --cloud aws \\\n    --bastion \\\n    --name $NAME \\\n    --yes
\n
\n

Reference

\n\n
","excerpt":"Kubernetes 클러스터를 구성하는 방법은 여러 가지가 있습니다.\n그 중에서 kubeadam은 온프레미스 환경에서 많이 사용하고 kops…"}}}},{"node":{"title":"분산 컨테이너 환경에서의 디자인 패턴 (1)","id":"b5808ba3-22f9-5847-b6ba-e8960369c054","slug":"container-patterns","publishDate":"January 26, 2019","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

구글 클라우드 팀이 Kubernetes와 같은 Container Orchestration 기술을 개발하면서 겪은\n분산 컨테이너 환경에서의 디자인 패턴에 대해 정리한 내용입니다.\n분산 어플리케이션을 컨테이너 환경으로 옮기려는 분들에게 많은 도움이 될 듯 합니다.

\n\n
\n

Modular Container Design

\n

최근 많은 개발 환경이 Docker 기반의 컨테이너 환경으로 옮겨가고 있습니다.\n그 중에서는 복잡한 의존성에서 벗어나 독립적으로 운영하고 싶어서 옮기는 경우가 많습니다.

\n

하지만 분산 컨테이너 환경은 아주 복잡하게 연결되어 있어서 운영하기 힘듭니다.\n복잡한 연결을 쉽게 이해하려면 이 문제가 어디에 해당하는지 경계를 잘 정의하는 것이 중요합니다.\n이렇게 경계를 정의하고 분류하는 것을 모듈화라고 부릅니다.

\n

우리는 과거부터 효율적인 코드를 작성하기 위해 절차지향 프로그래밍에서 객체지향 프로그래밍으로 변화해왔습니다.\n객체지향 프로그래밍의 클래스는 경계를 정의한다는 측면에서 컨테이너 환경과 유사한 면이 있습니다.

\n

\n \n \n \n

\n

과거 모놀리틱 아키텍쳐에서는 왼쪽 그림과 같은 구조로 설계되어 왔습니다.\n우리가 기여하거나 자주 수정하는 코드가 있는 반면, 코어 모듈이나 공통에 해당하는 부분은 잘 변하지 않습니다.

\n

컨테이너 환경은 이와 조금 다릅니다.\n우선 컨테이너 환경에서 컨테이너는 하나의 어플리케이션이 아니라는 점을 이해하고 있어야 합니다.\n하나의 컨테이너는 객체지향 언어의 클래스 또는 함수와 유사합니다.\n오른쪽 그림처럼 작은 모듈 조각을 모으고 조립해서 다음 어플리케이션을 설계하는 형태가 되어야 합니다.

\n
\n

Benefit

\n

위와 같은 컨테이너 환경을 구성했을 때 가지는 장점은 아래와 같습니다.

\n\n
\n

Requirements

\n

위와 같은 컨테이너 환경을 구성하기 위해 필요한 요소는 아래와 같습니다.

\n\n

특히 여러 컨테이너에서 공통으로 사용하는 라이브러리의 경우 필요

\n
\n

Sidecar Pattern

\n

\n \n \n \n

\n

이제부터 자주 사용되는 세 가지 디자인 패턴을 소개드리려고 합니다.\n먼저 첫 번째는 사이드 카 패턴입니다.\n사이드 카 패턴은 이전에 사용되던 컨테이너의 기능을 확장시키고 싶을 때 유용하게 사용됩니다.\n여기서 이전에 사용되던 컨테이너란 잘 변하지 않으며 같은 작업을 반복하는 어플리케이션을 말합니다.

\n

위 그림의 예시에서 이전에 사용되던 컨테이너는 왼쪽의 node.js 어플리케이션 입니다.\nnode.js 어플리케이션은 단순히 파일 시스템에 접근하여 어떤 작업을 수행하는 일만 합니다.\n만일 파일 시스템에 대해 git 동기화 기능을 추가하고 싶다면, 오른쪽 컨테이너처럼 확장시킬 수 있습니다.\nnode.js 어플리케이션은 사이드 카 컨테이너가 어떤 작업을 수행하는지 고려할 필요가 없습니다.\n사이드 카 컨테이너 역시 어떤 어플리케이션이 이 파일을 서빙하는지 고려할 필요가 없습니다.\n앞서 말한 것처럼 관심사의 분리를 만족시키며, 컨테이너를 관리하는 팀을 분리시킬 수 있습니다.\n또한 사이드 카 컨테이너를 다른 어플리케이션에서 재사용할 수 있고, 더 다양한 기능으로 확장시킬 수 있습니다.

\n
\n

Ambassador Pattern

\n

\n \n \n \n

\n

다음은 엠베서더 패턴입니다.\n엠베서더 패턴은 어플리케이션을 대신하여 외부의 네트워크 또는 요청을 처리해야할 때 유용하게 사용됩니다.

\n

위 그림의 예시에서 어플리케이션은 PHP 앱이고 Memcache를 사용한 지속적인 해싱이 필요하다고 가정해보겠습니다.\n그리고 Memcache 사용을 위해 twemproxy라는 라이브러리를 가져와야 합니다.\n위 그림처럼 어플리케이션과 twemproxy 컨테이너를 분리시킨다면,\ntwemproxy 컨테이너는 외부에 있는 Memcache 샤드를 관리하고 통신하는 역할을 수행할 수 있습니다.\n기존에 있던 어플리케이션 컨테이너는 twemproxy 컨테이너와 같은 네임스페이스에 존재하지만, 외부의 통신에 대해서는 관여할 필요가 없습니다.\n역시 마찬가지로 관심사의 분리를 만족시키며, 재사용될 수 있고, 다양한 기능으로 확장시킬 수 있습니다.

\n
\n

Adapter Pattern

\n

\n \n \n \n

\n

많이 사용하는 prometheus exporter도 이와 같은 패턴으로 설계되어 있습니다.\nexporter는 모니터링 시스템과 쉽게 결합할 수 있습니다.\n그리고 exporter와 memcache, redis는 결합해서 하나의 컨테이너로 배포하는 형태입니다.\nredis-exporter에 코드 변경이 이루어지더라도 memcache-exporter는 변경될 필요가 없습니다.

\n
\n

Reference

\n

이외에도 Replication, Micro service Load Balancer에 사용되는 다양한 패턴이 존재합니다.\n분산 컨테이너 환경에서 어플리케이션을 개발하는 일은 레고 블럭을 조립하는 것과 비슷합니다.\n더 자세한 내용이 궁금하신 분은 아래 링크를 확인하시면 됩니다.

\n\n
","excerpt":"구글 클라우드 팀이 Kubernetes와 같은 Container Orchestration…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":5,"humanPageNumber":6,"skip":31,"limit":6,"numberOfPages":16,"previousPagePath":"/5","nextPagePath":"/7"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/7/page-data.json b/page-data/7/page-data.json index 0e5664b..24d8031 100644 --- a/page-data/7/page-data.json +++ b/page-data/7/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-posts-js","path":"/7","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Kafka Connect로 S3에 데이터를 저장해보자","id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","slug":"kafka-connect","publishDate":"November 16, 2018","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}}},{"node":{"title":"17-18년 블로그 회고 및 다짐","id":"50dcd8b5-0f2f-5e1a-94fd-0ed818d8a839","slug":"start","publishDate":"November 09, 2018","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

지킬 블로그로 이사한지도 벌써 2년이 다 되어 간다.\n항상 이 맘때쯤이면 글쓰는 횟수가 줄어들고 블로그를 또 이사하고 싶은 욕구가 생긴다.\n하지만 지금 운영하고 있는 블로그도 전혀 문제가 없기 때문에 올해까지는 참기로 결정했다.

\n

\n \n \n \n

\n

18년 들어서 글을 정말 안쓰기 시작했는데 이상하게도 방문자 수는 계속 증가했다.\n특히 최근 몇달 간 방문자 수가 6천 - 8천 사이에만 있는 걸 보니 다시 글을 써야겠다는 생각이 들었다.\n가장 많이 방문한 페이지 리스트를 보면 어려운 주제에 대한 글보다\n간단한 주제이면서 잊지 않기 위해 기록해 둔 글들이 더 인기가 많았다.

\n

다시 2주에 1번 정도 글을 써보려 한다.\n주로 백엔드, 데이터 엔지니어링에 대한 내용이겠지만, 가끔씩 금융 도메인에 대한 내용도 올릴 예정이다.

\n
","excerpt":"지킬 블로그로 이사한지도 벌써…"}}}},{"node":{"title":"Raft consensus algorithm","id":"4324a369-91a5-5afa-bd6c-f65af537b7d9","slug":"raft-consensus","publishDate":"September 01, 2018","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos가 있고, Zookeeper에서 사용하는 Zab이 있습니다.\nRaft는 이해하기 어려운 기존의 알고리즘과 달리 쉽게 이해하고 구현하기 위해 설계되었습니다.\n(PS. 이 글은 블록체인에서의 Consensus 알고리즘을 말하는 것이 아닙니다)

\n
\n

What is consensus problem

\n

하나의 클라이언트와 서버가 있고 클라이언트가 서버에게 데이터를 전달한다고 가정해보겠습니다.\n서버는 하나의 노드로 이루어져있기 때문에 합의가 이루어지는건 아주 쉬운 문제입니다.\n(여기에서 말하는 합의는 공유된 상태라고 이해하시면 됩니다)

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}}},{"node":{"title":"AWS에 Hadoop MR 어플리케이션 환경 구축하기","id":"3d5aacf4-f336-5c17-a880-4efb995c9b99","slug":"aws-hadoop","publishDate":"June 13, 2018","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

이번 학기에 하둡 프로그래밍 강의를 들으면서 정말 실습 환경의 개선이 필요하다는 생각이 들었습니다...\n나약한 실습 환경속에서 과제와 기말 프로젝트를 제출해야하는 후배들을 위해 AWS를 추천합니다!

\n
\n

EC2 Amazon Linux2에 기본 환경 구축

\n

AWS에는 EMR이라는 클러스터 서비스가 있지만, 스터디 목적이라면 비용을 생각해서 사용하지 않겠습니다.\nAmazon Linux AMI는 EC2에서 편하게 사용할 수 있도록 지원하고 관리하는 리눅스 이미지입니다.\n만일 학생용 크레딧이 있다면 t2.medium 인스턴스를 추천합니다.

\n

먼저, JAVA JDK와 Hadoop 파일을 받겠습니다. 실습 환경은 자바 7, 하둡 1.2 버전입니다.

\n
$ sudo yum update -y\n$ sudo yum install -y java-1.7.0-openjdk-devel\n$ wget https://archive.apache.org/dist/hadoop/core/hadoop-1.2.1/hadoop-1.2.1.tar.gz\n$ tar xvfz hadoop-1.2.1
\n

그리고 자바 프로젝트를 위해 Maven도 설치해줍니다.

\n
$ wget http://mirror.navercorp.com/apache/maven/maven-3/3.5.3/binaries/apache-maven-3.5.3-bin.tar.gz\n$ tar xvfs apache-maven-3.5.3-bin.tar.gz\n$ mv apache-maven-3.5.3/ apache-maven\n$ sudo vi /etc/profile.d/maven.sh\n\n# Apache Maven Environment Variables\n# MAVEN_HOME for Maven 1 - M2_HOME for Maven 2\n$ export M2_HOME=/home/ec2-user/apache-maven\n$ export PATH=${M2_HOME}/bin:${PATH}\n\n$ chmod +x maven.sh\n$ source /etc/profile.d/maven.sh
\n

정상적으로 설치가 되었다면 아래의 명령어에 대한 결과가 나옵니다.

\n
$ java --version\n$ mvn --version
\n
\n

Hadoop 환경 구축

\n

실습환경은 Pseudo-Distibuted 모드로 진행합니다.\n먼저 Password less SSH Login을 설정해주어야 합니다.\n그리고 편의를 위해 hadoop-1.2.1 폴더에 Symbolic link를 생성하겠습니다.

\n
# ssh login setting\n$ ssh-keygen -t rsa -P \"\"\n$ cat /home/ec2-user/.ssh/id_rsa.pub >> /home/ec2-user/.ssh/authorized_keys\n\n# symbolic link\n$ ln -s hadoop-1.2.1 hadoop
\n

이제 HDFS와 MR 실행을 위해 설정파일을 수정해줍니다.\n먼저 hadoop-env.sh을 열어 JAVA_HOME 환경변수를 지정해줍니다.\n가상분산모드에서는 masters, slaves 파일을 수정할 필요가 없습니다.

\n
$ cd hadoop\n$ vi conf/hadoop-env.sh\n\n# set JAVA_HOME in this file, so that it is correctly defined on\n# remote nodes.\n\n# The java implementation to use. Required.\nexport JAVA_HOME=/usr/lib/jvm/java-1.7.0\n\n# Extra Java CLASSPATH elements.  Optional.\n# export HADOOP_CLASSPATH=
\n

이제 core-site.xml 파일을 아래와 같이 수정해줍니다.\nHDFS 데이터 파일들은 홈 디렉토리의 hadoop-data 폴더에 저장하겠습니다.

\n
$ vi conf/core-site.xml\n\n<configuration>\n    <property>\n        <name>fs.default.name</name>\n        <value>hdfs://localhost:9000</value>\n    </property>\n    <property>\n        <name>hadoop.tmp.dir</name>\n        <value>/home/ec2-user/hadoop-data/</value>\n    </property>\n</configuration>
\n

hdfs-site.xml 파일도 수정해줍니다.\ndfs.replication 프로퍼티는 복제 개수를 의미합니다.\n일반적으로 복제 개수를 3으로 두는 것을 권장하지만,\n실습에서는 Fully-Distributed 모드가 아니기 때문에 1로 설정하겠습니다.

\n
$ vi conf/hdfs-site.xml\n\n<configuration>\n    <property>\n        <name>dfs.replication</name>\n        <value>1</value>\n    </property>\n</configuration>
\n

mapred-site.xml 파일도 수정해줍니다.\nmapred.job.tracker 프로퍼티는 job tracker가 동작하는 서버를 말합니다.

\n
$ vi conf/mapred-site.xml\n\n<configuration>\n    <property>\n        <name>mapred.job.tracker</name>\n        <value>localhost:9001</value>\n    </property>\n</configuration>
\n
\n

Hadoop MR

\n

이제 NameNode를 초기화하고 하둡과 관련된 모든 데몬을 실행합니다.

\n
./bin/hadoop namenode-format\n./bin/start-all.sh
\n

jps를 통해 자바 프로세스가 제대로 실행되었는지 확인할 수 있습니다.

\n
$ jps\n3368 TaskTracker\n2991 DataNode\n3241 JobTracker\n3480 Jps\n2872 NameNode\n3139 SecondaryNameNode
\n

HDFS 웹 인터페이스 주소는 http://localhost:50070 이며,\nMapReduce 웹 인터페이스 주소는 http://localhost:50030 입니다.\n들어가시면 아래와 같은 화면이 나타납니다.

\n

\n \n \n \n

\n
\n

포트폴리오의 기대수익률과 변동성 계산

\n

처음 설명한 자산 배분 문제는 결국 아래와 같은 과정을 거치게 됩니다.

\n
    \n
  1. 투자할 자산군을 결정
  2. \n
  3. 결정한 자산 별 수익률, 변동성 및 상관관계를 계산
  4. \n
  5. 변동성 대비 수익률이 가장 높은 포트폴리오를 구성
  6. \n
\n

w=portfolio=[w1,w2,...,wN]T,wherei=1Nwi=1w = portfolio = [w_1, w_2, ... , w_N]^T, where \\sum_{i=1}^{N}w_i = 1w=portfolio=[w1,w2,...,wN]T,wherei=1Nwi=1

\n

포트폴리오의 경우 앞서 설명한 단일 자산과는 달리 여러 자산으로 구성됩니다.\n따라서, 포트폴리오의 기대수익률과 변동성을 계산하려면 주어진 예산에서의 자산 별 투자 비중이 정해져야 합니다.

\n

μp=portfolio×expectation=[w1,w2,...,wN][R1,R2,...,RN]T\\mu_p = portfolio \\times expectation = [w_1, w_2, ... , w_N][R_1, R_2, ... , R_N]^Tμp=portfolio×expectation=[w1,w2,...,wN][R1,R2,...,RN]T

\n

포트폴리오의 기대수익률은 개별 자산의 기대수익률과 포트폴리오의 비중을 곱해서 합산하는 방법으로 계산합니다.

\n
\n

Cov(R1,R2)=E[(R1R1ˉ)(R2R2ˉ)]=1N1i=1N(R1R1ˉ)(R2R2ˉ)Cov(R^1, R^2) = E[(R^1-\\bar{R^1})(R^2-\\bar{R^2})] = \\frac{1}{N-1}\\sum_{i=1}^{N}(R^1-\\bar{R^1})(R^2-\\bar{R^2})Cov(R1,R2)=E[(R1R1ˉ)(R2R2ˉ)]=N11i=1N(R1R1ˉ)(R2R2ˉ)

\n

포트폴리오의 변동성은 **공분산(Covarience)**을 이용해서 계산할 수 있습니다.\n공분산은 확률변수가 2개 이상일 때 각 확률변수들이 얼마나 퍼져있는지를 나타내는 값을 알려주는 지표입니다.

\n
ρ=Cov(X,Y)Std(X)Std(Y),(1ρ1)\\rho = \\cfrac{Cov(X,Y)}{Std(X)Std(Y)}, (-1\\leq\\rho\\leq1)ρ=Std(X)Std(Y)Cov(X,Y),(1ρ1)
\n
\n

**상관관계(Correlation Coefficient)**는 확률변수의 절대적 크기에 영향을 받지 않도록 0과 1사이로 단위화시킨 값이라고 보시면 됩니다.\n100만원 대의 주식 2개와 10만원 대의 주식 2개의 각 공분산을 계산해보면 100만원 대 주식의 공분산에서 더 큰 값이 나오게 됩니다.\n이러한 크기차이에 영향을 받지 않도록 분산의 크기만큼 나눈 것 입니다.

\n
[σ11σ12σ1nσn1σn2σn2][w1w2wN] \\begin{bmatrix}\n \\sigma_{11} & \\sigma_{12} & \\cdots & \\sigma_{1n} \\\\\n \\vdots & \\vdots & \\ddots & \\vdots \\\\\n \\sigma_{n1} & \\sigma_{n2} & \\cdots & \\sigma_{n^2} \n \\end{bmatrix}\n \\begin{bmatrix}\n w_{1} \\\\\n w_{2} \\\\\n \\vdots \\\\\n w_{N} \\\\\n\\end{bmatrix}σ11σn1σ12σn2σ1nσn2w1w2wN
\n
\n

결국 포트폴리오의 변동성은 포트폴리오의 비중, 투자 자산들 간의 공분산 행렬, 다시 포트폴리오 비중을 곱해 계산할 수 있습니다.\n아래는 파이썬의 numpy, pandas, matplotlib을 이용해서 계산하는 노트북 링크입니다.\n다음에는 포트폴리오 이론의 시초이자 최적의 mean-varience를 계산하는 markowitz 모델에 대해 정리해보겠습니다.

\n

http://nbviewer.jupyter.org/github/Swalloow/pytrading/blob/master/notebook/portfolio-optimization.ipynb

\n
","excerpt":"…"}}}},{"node":{"title":"Structuring Your TensorFlow Models","id":"976a40c0-463b-5a5b-918a-bb7adeb7a48e","slug":"structuring-tf","publishDate":"April 20, 2018","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

이 글은 저자의 허락을 받아 번역한 글 입니다. 원문 링크

\n

TensorFlow에서 모델을 정의하다보면 어느새 많은 양의 코드가 생성된 경험이 있을 것 입니다. 어떻게 하면 가독성과 재사용성이 높은 코드로 구성할 수 있을까요? 여기에서 실제 동작하는 예시 코드를 확인하실 수 있습니다. Gist Link

\n
\n

Defining the Compute Graph

\n

모델 당 하나의 클래스부터 시작하는 것이 좋습니다. 그 클래스의 인터페이스는 무엇인가요? 일반적으로 모델은 input data와 target placeholder를 연결하며 training, evaluation 그리고 inference 관련 함수를 제공합니다.

\n\n

위의 코드가 기본적으로 TensorFlow codebase에서 모델이 정의되는 방식입니다. 그러나 여기에도 몇 가지 문제가 있습니다. 가장 중요한 문제는 전체 그래프가 단일 함수 생성자로 정의된다는 점 입니다. 이렇게 되면 가독성이 떨어지며 재사용이 어렵습니다.

\n
\n

Using Properties

\n

함수가 호출 될 때마다 그래프는 확장되기 때문에 함수로 분할하는 것만으로는 부족합니다. 따라서 함수를 처음 호출하는 시점에 operation들이 그래프에 추가되도록 해야합니다. 이러한 방식을 기본적으로 lazy-loading이라고 합니다.

\n
class Model:\n\n    def __init__(self, data, target):\n        data_size = int(data.get_shape()[1])\n        target_size = int(target.get_shape()[1])\n        weight = tf.Variable(tf.truncated_normal([data_size, target_size]))\n        bias = tf.Variable(tf.constant(0.1, shape=[target_size]))\n        incoming = tf.matmul(data, weight) + bias\n        self._prediction = tf.nn.softmax(incoming)\n        cross_entropy = -tf.reduce_sum(target, tf.log(self._prediction))\n        self._optimize = tf.train.RMSPropOptimizer(0.03).minimize(cross_entropy)\n        mistakes = tf.not_equal(\n            tf.argmax(target, 1), tf.argmax(self._prediction, 1))\n        self._error = tf.reduce_mean(tf.cast(mistakes, tf.float32))\n\n    @property\n    def prediction(self):\n        return self._prediction\n\n    @property\n    def optimize(self):\n        return self._optimize\n\n    @property\n    def error(self):\n        return self._error
\n

위의 방식이 첫 번째 예제보다 훨씬 좋습니다. 이제 코드는 독립적인 함수로 구성되어 있습니다. 그러나 아직 코드는 lazy-loading으로 인해 약간 복잡해보입니다. 이를 어떻게 개선 할 수 있는지 보도록 하겠습니다.

\n
\n

Lazy Property Decorator

\n

파이썬은 아주 유연한 언어입니다. 이제 마지막 예제에서 중복 코드를 제거하는 방법을 보여드리겠습니다. 우리는 @property처럼 동작하지만 한번만 함수를 평가하는 decorator를 사용할 것입니다. decorator는 함수(접두사를 앞에 붙임)의 이름을 따서 멤버에 결과를 저장하고 나중에 호출되는 시점에 해당 값을 반환합니다. custom decorator를 아직 사용해본적이 없다면, 이 가이드를 참고하시면 됩니다.

\n
class Model:\n\n    def __init__(self, data, target):\n        self.data = data\n        self.target = target\n        self._prediction = None\n        self._optimize = None\n        self._error = None\n\n    @property\n    def prediction(self):\n        if not self._prediction:\n            data_size = int(self.data.get_shape()[1])\n            target_size = int(self.target.get_shape()[1])\n            weight = tf.Variable(tf.truncated_normal([data_size, target_size]))\n            bias = tf.Variable(tf.constant(0.1, shape=[target_size]))\n            incoming = tf.matmul(self.data, weight) + bias\n            self._prediction = tf.nn.softmax(incoming)\n        return self._prediction\n\n    @property\n    def optimize(self):\n        if not self._optimize:\n            cross_entropy = -tf.reduce_sum(self.target, tf.log(self.prediction))\n            optimizer = tf.train.RMSPropOptimizer(0.03)\n            self._optimize = optimizer.minimize(cross_entropy)\n        return self._optimize\n\n    @property\n    def error(self):\n        if not self._error:\n            mistakes = tf.not_equal(\n                tf.argmax(self.target, 1), tf.argmax(self.prediction, 1))\n            self._error = tf.reduce_mean(tf.cast(mistakes, tf.float32))\n        return self._error
\n

위의 decorator를 사용해서 예시 코드는 아래와 같이 간결해졌습니다.

\n
import functools\n\ndef lazy_property(function):\n    attribute = '_cache_' + function.__name__\n\n    @property\n    @functools.wraps(function)\n    def decorator(self):\n        if not hasattr(self, attribute):\n            setattr(self, attribute, function(self))\n        return getattr(self, attribute)\n\n    return decorator
\n

생성자에서 property를 언급했다는 부분이 중요합니다. 이렇게 구성한다면 tf.initialize_variables()를 실행할 때 전체 그래프가 정의됩니다.

\n
\n

Organizing the Graph with Scopes

\n

이제 코드에서 모델을 정의하는 부분은 깔끔해졌지만, 그래프의 연산 부분은 여전히 복잡합니다. 만일 그래프를 시각화한다면, 서로 연결되어 있는 노드가 많이 나타날 것 입니다. 이를 해결하기 위한 방법은 tf.name_scope('name') 또는 tf.variable_scope('name')을 사용하여 각 함수의 내용을 래핑하는 것 입니다. 이렇게 하면 노드들은 그래프 상에서 그룹화되어 있을 것 입니다. 우리는 이전에 만들었던 decorator를 이용하여 이를 자동으로 적용시켜보겠습니다.

\n
class Model:\n\n    def __init__(self, data, target):\n        self.data = data\n        self.target = target\n        self.prediction\n        self.optimize\n        self.error\n\n    @lazy_property\n    def prediction(self):\n        data_size = int(self.data.get_shape()[1])\n        target_size = int(self.target.get_shape()[1])\n        weight = tf.Variable(tf.truncated_normal([data_size, target_size]))\n        bias = tf.Variable(tf.constant(0.1, shape=[target_size]))\n        incoming = tf.matmul(self.data, weight) + bias\n        return tf.nn.softmax(incoming)\n\n    @lazy_property\n    def optimize(self):\n        cross_entropy = -tf.reduce_sum(self.target, tf.log(self.prediction))\n        optimizer = tf.train.RMSPropOptimizer(0.03)\n        return optimizer.minimize(cross_entropy)\n\n    @lazy_property\n    def error(self):\n        mistakes = tf.not_equal(\n            tf.argmax(self.target, 1), tf.argmax(self.prediction, 1))\n        return tf.reduce_mean(tf.cast(mistakes, tf.float32))
\n

lazy caching 이외에도 TensorFlow의 기능을 포함시키므로 decorator에 새로운 이름을 지정했습니다. 그 외의 나머지 부분은 이전과 동일합니다.

\n

이제 @define_scope decorator를 통해 tf.variable_scope()에 인자를 전달할 수 있습니다. 예를 들어 해당 scope에 default initializer를 정의할 수 있습니다. 이 부분이 더 궁금하다면 전체 예제 코드를 확인해보시면 됩니다.

\n
\n

Reference

\n

https://danijar.com/structuring-your-tensorflow-models/

\n
","excerpt":"이 글은 저자의 허락을 받아 번역한 글 입니다. 원문 링크 TensorFlow…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":6,"humanPageNumber":7,"skip":37,"limit":6,"numberOfPages":16,"previousPagePath":"/6","nextPagePath":"/8"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/7","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Apache Airflow에 기여하면서 배운 점들","id":"a393498e-de9e-5231-bc9f-fd1df0495f45","slug":"airflow-contrib","publishDate":"December 08, 2018","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}}},{"node":{"title":"Kafka Connect로 S3에 데이터를 저장해보자","id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","slug":"kafka-connect","publishDate":"November 16, 2018","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}}},{"node":{"title":"17-18년 블로그 회고 및 다짐","id":"50dcd8b5-0f2f-5e1a-94fd-0ed818d8a839","slug":"start","publishDate":"November 09, 2018","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

지킬 블로그로 이사한지도 벌써 2년이 다 되어 간다.\n항상 이 맘때쯤이면 글쓰는 횟수가 줄어들고 블로그를 또 이사하고 싶은 욕구가 생긴다.\n하지만 지금 운영하고 있는 블로그도 전혀 문제가 없기 때문에 올해까지는 참기로 결정했다.

\n

\n \n \n \n

\n

18년 들어서 글을 정말 안쓰기 시작했는데 이상하게도 방문자 수는 계속 증가했다.\n특히 최근 몇달 간 방문자 수가 6천 - 8천 사이에만 있는 걸 보니 다시 글을 써야겠다는 생각이 들었다.\n가장 많이 방문한 페이지 리스트를 보면 어려운 주제에 대한 글보다\n간단한 주제이면서 잊지 않기 위해 기록해 둔 글들이 더 인기가 많았다.

\n

다시 2주에 1번 정도 글을 써보려 한다.\n주로 백엔드, 데이터 엔지니어링에 대한 내용이겠지만, 가끔씩 금융 도메인에 대한 내용도 올릴 예정이다.

\n
","excerpt":"지킬 블로그로 이사한지도 벌써…"}}}},{"node":{"title":"Raft consensus algorithm","id":"4324a369-91a5-5afa-bd6c-f65af537b7d9","slug":"raft-consensus","publishDate":"September 01, 2018","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos가 있고, Zookeeper에서 사용하는 Zab이 있습니다.\nRaft는 이해하기 어려운 기존의 알고리즘과 달리 쉽게 이해하고 구현하기 위해 설계되었습니다.\n(PS. 이 글은 블록체인에서의 Consensus 알고리즘을 말하는 것이 아닙니다)

\n
\n

What is consensus problem

\n

하나의 클라이언트와 서버가 있고 클라이언트가 서버에게 데이터를 전달한다고 가정해보겠습니다.\n서버는 하나의 노드로 이루어져있기 때문에 합의가 이루어지는건 아주 쉬운 문제입니다.\n(여기에서 말하는 합의는 공유된 상태라고 이해하시면 됩니다)

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}}},{"node":{"title":"AWS에 Hadoop MR 어플리케이션 환경 구축하기","id":"3d5aacf4-f336-5c17-a880-4efb995c9b99","slug":"aws-hadoop","publishDate":"June 13, 2018","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

이번 학기에 하둡 프로그래밍 강의를 들으면서 정말 실습 환경의 개선이 필요하다는 생각이 들었습니다...\n나약한 실습 환경속에서 과제와 기말 프로젝트를 제출해야하는 후배들을 위해 AWS를 추천합니다!

\n
\n

EC2 Amazon Linux2에 기본 환경 구축

\n

AWS에는 EMR이라는 클러스터 서비스가 있지만, 스터디 목적이라면 비용을 생각해서 사용하지 않겠습니다.\nAmazon Linux AMI는 EC2에서 편하게 사용할 수 있도록 지원하고 관리하는 리눅스 이미지입니다.\n만일 학생용 크레딧이 있다면 t2.medium 인스턴스를 추천합니다.

\n

먼저, JAVA JDK와 Hadoop 파일을 받겠습니다. 실습 환경은 자바 7, 하둡 1.2 버전입니다.

\n
$ sudo yum update -y\n$ sudo yum install -y java-1.7.0-openjdk-devel\n$ wget https://archive.apache.org/dist/hadoop/core/hadoop-1.2.1/hadoop-1.2.1.tar.gz\n$ tar xvfz hadoop-1.2.1
\n

그리고 자바 프로젝트를 위해 Maven도 설치해줍니다.

\n
$ wget http://mirror.navercorp.com/apache/maven/maven-3/3.5.3/binaries/apache-maven-3.5.3-bin.tar.gz\n$ tar xvfs apache-maven-3.5.3-bin.tar.gz\n$ mv apache-maven-3.5.3/ apache-maven\n$ sudo vi /etc/profile.d/maven.sh\n\n# Apache Maven Environment Variables\n# MAVEN_HOME for Maven 1 - M2_HOME for Maven 2\n$ export M2_HOME=/home/ec2-user/apache-maven\n$ export PATH=${M2_HOME}/bin:${PATH}\n\n$ chmod +x maven.sh\n$ source /etc/profile.d/maven.sh
\n

정상적으로 설치가 되었다면 아래의 명령어에 대한 결과가 나옵니다.

\n
$ java --version\n$ mvn --version
\n
\n

Hadoop 환경 구축

\n

실습환경은 Pseudo-Distibuted 모드로 진행합니다.\n먼저 Password less SSH Login을 설정해주어야 합니다.\n그리고 편의를 위해 hadoop-1.2.1 폴더에 Symbolic link를 생성하겠습니다.

\n
# ssh login setting\n$ ssh-keygen -t rsa -P \"\"\n$ cat /home/ec2-user/.ssh/id_rsa.pub >> /home/ec2-user/.ssh/authorized_keys\n\n# symbolic link\n$ ln -s hadoop-1.2.1 hadoop
\n

이제 HDFS와 MR 실행을 위해 설정파일을 수정해줍니다.\n먼저 hadoop-env.sh을 열어 JAVA_HOME 환경변수를 지정해줍니다.\n가상분산모드에서는 masters, slaves 파일을 수정할 필요가 없습니다.

\n
$ cd hadoop\n$ vi conf/hadoop-env.sh\n\n# set JAVA_HOME in this file, so that it is correctly defined on\n# remote nodes.\n\n# The java implementation to use. Required.\nexport JAVA_HOME=/usr/lib/jvm/java-1.7.0\n\n# Extra Java CLASSPATH elements.  Optional.\n# export HADOOP_CLASSPATH=
\n

이제 core-site.xml 파일을 아래와 같이 수정해줍니다.\nHDFS 데이터 파일들은 홈 디렉토리의 hadoop-data 폴더에 저장하겠습니다.

\n
$ vi conf/core-site.xml\n\n<configuration>\n    <property>\n        <name>fs.default.name</name>\n        <value>hdfs://localhost:9000</value>\n    </property>\n    <property>\n        <name>hadoop.tmp.dir</name>\n        <value>/home/ec2-user/hadoop-data/</value>\n    </property>\n</configuration>
\n

hdfs-site.xml 파일도 수정해줍니다.\ndfs.replication 프로퍼티는 복제 개수를 의미합니다.\n일반적으로 복제 개수를 3으로 두는 것을 권장하지만,\n실습에서는 Fully-Distributed 모드가 아니기 때문에 1로 설정하겠습니다.

\n
$ vi conf/hdfs-site.xml\n\n<configuration>\n    <property>\n        <name>dfs.replication</name>\n        <value>1</value>\n    </property>\n</configuration>
\n

mapred-site.xml 파일도 수정해줍니다.\nmapred.job.tracker 프로퍼티는 job tracker가 동작하는 서버를 말합니다.

\n
$ vi conf/mapred-site.xml\n\n<configuration>\n    <property>\n        <name>mapred.job.tracker</name>\n        <value>localhost:9001</value>\n    </property>\n</configuration>
\n
\n

Hadoop MR

\n

이제 NameNode를 초기화하고 하둡과 관련된 모든 데몬을 실행합니다.

\n
./bin/hadoop namenode-format\n./bin/start-all.sh
\n

jps를 통해 자바 프로세스가 제대로 실행되었는지 확인할 수 있습니다.

\n
$ jps\n3368 TaskTracker\n2991 DataNode\n3241 JobTracker\n3480 Jps\n2872 NameNode\n3139 SecondaryNameNode
\n

HDFS 웹 인터페이스 주소는 http://localhost:50070 이며,\nMapReduce 웹 인터페이스 주소는 http://localhost:50030 입니다.\n들어가시면 아래와 같은 화면이 나타납니다.

\n

\n \n \n \n

\n
\n

포트폴리오의 기대수익률과 변동성 계산

\n

처음 설명한 자산 배분 문제는 결국 아래와 같은 과정을 거치게 됩니다.

\n
    \n
  1. 투자할 자산군을 결정
  2. \n
  3. 결정한 자산 별 수익률, 변동성 및 상관관계를 계산
  4. \n
  5. 변동성 대비 수익률이 가장 높은 포트폴리오를 구성
  6. \n
\n

w=portfolio=[w1,w2,...,wN]T,wherei=1Nwi=1w = portfolio = [w_1, w_2, ... , w_N]^T, where \\sum_{i=1}^{N}w_i = 1w=portfolio=[w1,w2,...,wN]T,wherei=1Nwi=1

\n

포트폴리오의 경우 앞서 설명한 단일 자산과는 달리 여러 자산으로 구성됩니다.\n따라서, 포트폴리오의 기대수익률과 변동성을 계산하려면 주어진 예산에서의 자산 별 투자 비중이 정해져야 합니다.

\n

μp=portfolio×expectation=[w1,w2,...,wN][R1,R2,...,RN]T\\mu_p = portfolio \\times expectation = [w_1, w_2, ... , w_N][R_1, R_2, ... , R_N]^Tμp=portfolio×expectation=[w1,w2,...,wN][R1,R2,...,RN]T

\n

포트폴리오의 기대수익률은 개별 자산의 기대수익률과 포트폴리오의 비중을 곱해서 합산하는 방법으로 계산합니다.

\n
\n

Cov(R1,R2)=E[(R1R1ˉ)(R2R2ˉ)]=1N1i=1N(R1R1ˉ)(R2R2ˉ)Cov(R^1, R^2) = E[(R^1-\\bar{R^1})(R^2-\\bar{R^2})] = \\frac{1}{N-1}\\sum_{i=1}^{N}(R^1-\\bar{R^1})(R^2-\\bar{R^2})Cov(R1,R2)=E[(R1R1ˉ)(R2R2ˉ)]=N11i=1N(R1R1ˉ)(R2R2ˉ)

\n

포트폴리오의 변동성은 **공분산(Covarience)**을 이용해서 계산할 수 있습니다.\n공분산은 확률변수가 2개 이상일 때 각 확률변수들이 얼마나 퍼져있는지를 나타내는 값을 알려주는 지표입니다.

\n
ρ=Cov(X,Y)Std(X)Std(Y),(1ρ1)\\rho = \\cfrac{Cov(X,Y)}{Std(X)Std(Y)}, (-1\\leq\\rho\\leq1)ρ=Std(X)Std(Y)Cov(X,Y),(1ρ1)
\n
\n

**상관관계(Correlation Coefficient)**는 확률변수의 절대적 크기에 영향을 받지 않도록 0과 1사이로 단위화시킨 값이라고 보시면 됩니다.\n100만원 대의 주식 2개와 10만원 대의 주식 2개의 각 공분산을 계산해보면 100만원 대 주식의 공분산에서 더 큰 값이 나오게 됩니다.\n이러한 크기차이에 영향을 받지 않도록 분산의 크기만큼 나눈 것 입니다.

\n
[σ11σ12σ1nσn1σn2σn2][w1w2wN] \\begin{bmatrix}\n \\sigma_{11} & \\sigma_{12} & \\cdots & \\sigma_{1n} \\\\\n \\vdots & \\vdots & \\ddots & \\vdots \\\\\n \\sigma_{n1} & \\sigma_{n2} & \\cdots & \\sigma_{n^2} \n \\end{bmatrix}\n \\begin{bmatrix}\n w_{1} \\\\\n w_{2} \\\\\n \\vdots \\\\\n w_{N} \\\\\n\\end{bmatrix}σ11σn1σ12σn2σ1nσn2w1w2wN
\n
\n

결국 포트폴리오의 변동성은 포트폴리오의 비중, 투자 자산들 간의 공분산 행렬, 다시 포트폴리오 비중을 곱해 계산할 수 있습니다.\n아래는 파이썬의 numpy, pandas, matplotlib을 이용해서 계산하는 노트북 링크입니다.\n다음에는 포트폴리오 이론의 시초이자 최적의 mean-varience를 계산하는 markowitz 모델에 대해 정리해보겠습니다.

\n

http://nbviewer.jupyter.org/github/Swalloow/pytrading/blob/master/notebook/portfolio-optimization.ipynb

\n
","excerpt":"…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":6,"humanPageNumber":7,"skip":37,"limit":6,"numberOfPages":16,"previousPagePath":"/6","nextPagePath":"/8"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/8/page-data.json b/page-data/8/page-data.json index a5ddcda..7fdb51a 100644 --- a/page-data/8/page-data.json +++ b/page-data/8/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-posts-js","path":"/8","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Data Science inconvenient truth","id":"94eeb309-1ab1-58ad-af4b-9e354444e47b","slug":"data-science-inconvenient-truth","publishDate":"April 01, 2018","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터과학의 불편한 진실

\n\n
\n

Ref: https://www.kdnuggets.com/2015/05/data-science-inconvenient-truth.html

","excerpt":"데이터과학의 불편한 진실 Data is never clean (데이터는 절대 깨끗하지 않다) You will spend most of your…"}}}},{"node":{"title":"Deep Learning Programming Style: Symbolic, Imperative","id":"9160ad92-4b57-5842-ab2e-5ee3b2c9070c","slug":"deep-learning-style","publishDate":"January 05, 2018","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

TensorFlow 1.5 버전부터 Eager Execution 이라는 기능이 추가되었습니다.\n다시 말해서 imperative programming style을 지원한다고 적혀있는데, 기존의 방식과 어떤 차이가 있는지 알아보겠습니다.\nMXNet의 Deep Learning Programming Style 문서를 번역한 내용입니다.

\n
\n

Deep Learning Programming Style

\n

우리는 항상 성능과 최적화에 대한 고민을 합니다. 하지만 그 이전에 잘 동작하는 코드인지 여부가 중요합니다. 이제는 다양한 딥러닝 라이브러리들이 존재하지만 각자 프로그래밍 방식에 대해 다른 접근 방식을 가지고 있기 때문에 학습하는 것도 힘들며, 이를 이용하여 명확하고 직관적인 deep learning 코드를 작성하는 것도 어렵습니다.

\n

이 문서에서는 가장 중요한 두 가지 디자인 패턴에 집중하려고 합니다.

\n
    \n
  1. \n

    Whether to embrace the symbolic or imperative paradigm for mathematical computation.

    \n
  2. \n
  3. \n

    Whether to build networks with bigger (more abstract) or more atomic operations.

    \n
  4. \n
\n
\n

Symbolic vs. Imperative Programs

\n

만일 당신이 파이썬 또는 C++ 개발자라면, 이미 Imperative program과 친숙할 것 입니다.\nImperative style program들은 바로 연산을 수행합니다. 대부분의 파이썬 코드들이 imperative 한 형태를 보여주는데, 예를 들면 아래와 같은 Numpy 코드를 말합니다.

\n
import numpy as np\na = np.ones(10)\nb = np.ones(10) * 2\nc = b * a\nd = c + 1
\n

프로그램이 c = b * a를 수행하도록 명령을 내리면, 실제로 연산이 실행됩니다.

\n

반면에 Symbolic program은 조금 다릅니다. Symbolic-style program에서는 먼저 function (potentially complex) 을 정의합니다. function을 정의했다고 해서 실제 연산이 수행되는 것은 아닙니다. 우리는 그저 placeholder 값에 function을 정의한 것 뿐 입니다. 이 과정 이후에 function을 컴파일 할 수 있으며, 실제 입력 값을 통해 이를 평가하게 됩니다. 아래는 위에서 언급했던 imperative 코드를 symbolic style로 변환한 예제입니다.

\n
A = Variable('A')\nB = Variable('B')\nC = B * A\nD = C + Constant(1)\n# compiles the function\nf = compile(D)\nd = f(A=np.ones(10), B=np.ones(10)*2)
\n

보시다시피 symbolic 버전에서는 C = B * A가 수행되는 시점에 실제로 연산이 일어나지 않습니다. 대신에 이 operation은 연산 과정을 표현하는 computation graph (aka. symbolic graph) 를 생성합니다. 예를 들면, D의 연산을 위해 아래와 같은 computation graph가 생성됩니다.

\n

\n \n \n \n

\n

대부분의 symbolic-style 프로그램들은 명시적으로든 암시적으로든 컴파일 단계를 포함합니다. 이를 통해 computation graph를 언제든 호출할 수 있는 함수로 변환시켜줍니다. 위의 예제에서도 실제 연산은 코드의 마지막 줄에서만 수행됩니다. 이를 통해 얻을 수 있는 점은 computation graph를 작성하는 단계와 실행하는 단계를 명확히 분리할 수 있다는 것 입니다. Neural Network에서도 우리는 전체 모델을 단일 computation graph로 정의합니다.

\n

Torch, Chiner 그리고 Minerva와 같은 딥러닝 라이브러리들은 imperative style을 사용하고 있습니다. symbolic-style을 사용하는 딥러닝 라이브러리로는 Theano, CGT 그리고 TensorFlow가 있습니다. 그리고 CXXNet 이나 Caffe와 같은 라이브러리들은 설정파일에 의존하는 방식으로 symbolic style을 지원합니다. (ex. Caffe의 prototxt)\n이제 두 가지 딥러닝 프로그래밍 방식에 대해 이해했으니, 각 방식의 장점에 대해 알아보겠습니다.

\n
\n

Imperative Programs Tend to be More Flexible

\n

imperative program은 프로그래밍 언어의 flow와 상당히 잘 맞아들어가며 유연하게 동작하는 것 처럼 보입니다. 그렇다면 왜 수 많은 딥러닝 라이브러리들이 symbolic 패러다임을 선택할까요? 가장 큰 이유는 메모리 사용량과 속도 측면에서의 효율성 때문입니다. 위에서 언급했던 예제로 돌아가 천천히 설명드리겠습니다.

\n
a = np.ones(10)\nb = np.ones(10) * 2\nc = b * a\nd = c + 1
\n

\n \n \n \n

\n

주어진 array의 각 셀이 8 바이트의 메모리를 소모한다고 가정해보겠습니다. 콘솔에서 위의 프로그램을 실행하면 메모리가 얼마나 소모될까요?

\n

imperative program에서는 각 라인마다 메모리 할당이 요구됩니다. 사이즈가 10인 array가 4개 할당되므로 4 * 10 * 8 = 320 bytes의 메모리가 요구됩니다.\n반면 computation graph에서는 궁극적으로 d가 필요하다는 것을 알고 있기 때문에, 즉시 값을 메모리에 할당하는 대신에 메모리를 재사용할 수 있습니다. 예를 들어 b를 위해 할당된 공간에 c를 저장하도록 재사용하고, c를 위해 할당된 공간에 다시 d를 저장하도록 한다면 결국 요구되는 메모리는 2 * 10 * 8 = 160 bytes 절반으로 줄어들게 됩니다.

\n

\n )\nb = array(2, 'b')\nc = b * a\nd = c + 1\nprint d.value\nprint d.grad(1)\n# Results\n# 3\n# {'a': 2, 'b': 1}\n


\n

Model Checkpoints

\n

모델을 저장하고 다시 불러오는 일 또한 중요합니다. 보통 Neural Network 모델을 저장한다는 것은 네트워크의 구조, 설정 값 그리고 weight 값의 저장을 의미합니다.

\n
A = Variable('A')\nB = Variable('B')\nC = B * A\nD = C + Constant(1)\n\nD.save('mygraph')\nD2 = load('mygraph')\nf = compile([D2])\n# more operations\n...
\n

설정 값을 체크하는 일은 symbolic program이 더 유리 합니다. symbolic 구조에서는 실제 연산을 수행할 필요가 없기 때문에 computation graph를 그대로 serialize 하면 됩니다.\n반면에 Imperative program은 연산 할 때 실행되기 때문에 코드 자체를 설정 파일로 저장하거나 그 위에 또 다른 레이어를 구성해야합니다.

\n
\n

Parameter Updates

\n

computation graph의 경우 연산과정은 쉽게 설명할 수 있지만 parameter 업데이트에 대해서는 명확하지 못합니다. parameter update는 기본적으로 값의 변경(mutation)을 요구하기 때문에 computation graph의 개념과 맞지 않습니다. 따라서 대부분의 symbolic program들은 persistent state를 갱신하기 위해 special update 구문을 사용하고 있습니다.

\n

반면에 imperative style에서는 parameter 업데이트를 작성하는 것이 쉽습니다. 특히 서로 연관된 여러 업데이트가 필요할 때 더욱 그렇습니다. Symbolic program의 경우 업데이트 문은 사용자가 호출 할 때 실행됩니다. 이런 점에서 대부분의 symbolic deep learning 라이브러리는 parameter 업데이트에 대해 gradient 연산을 수행하면서 업데이트를 수행하는 imperative style로 다시 돌아갑니다.

\n
\n

There Is No Strict Boundary

\n

두 가지 프로그래밍 스타일을 비교해서, 하나만 사용하라는 말이 아닙니다. 명령형 프로그램을 상징형 프로그램처럼 만들거나 그 반대도 가능합니다. 예를 들면, Python으로 JIT (just-in-time) 컴파일러를 작성하여 명령형 Python 프로그램을 컴파일 할 수 있습니다. 하지만 두 가지 아키텍쳐를 이해하는 것은 수 많은 딥러닝 라이브러리의 추상화와 그 차이를 이해하는데 도움이 됩니다. 결국, 우리는 프로그래밍 스타일 간에 명확한 경계선이 없다고 결론을 내릴 수 있습니다.

\n
","excerpt":"TensorFlow 1.5 버전부터 Eager Execution…"}}}},{"node":{"title":"제플린 노트북 자동 실행 스크립트 만들기","id":"cd93f129-9906-59b5-84e8-6a1fff1e7e00","slug":"zeppelin-bootstrap","publishDate":"September 13, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf 등록을 위해 처음 실행시켜야 하는 노트북이 있다면 참 번거롭습니다.\n하지만 Zeppelin Notebook API 사용한다면 이를 쉽게 자동화 할 수 있습니다.

\n
\n

Zeppelin Notebook API

\n

제플린은 노트북 자동실행을 위한 REST API를 제공합니다.\n하지만 제플린에 인증이 걸려있다면, 인증을 거쳐야만 API를 사용할 수 있습니다.\n따라서, 먼저 curl로 세션 값을 받고 해당 노트북 아이디를 호출하시면 됩니다.

\n

노트북 아이디는 해당 노트 URL의 가장 마지막 값 입니다. (ex 2AZPHY918)\n아래의 스크립트는 아이디가 user, 패스워드가 1234인 경우를 예시로 들었습니다.

\n
#!/bin/sh\nsudo /usr/lib/zeppelin/bin/zeppelin-daemon.sh stop\nsleep 3\nsudo /usr/lib/zeppelin/bin/zeppelin-daemon.sh start\n\nsleep 15\n\nSESSION=\"`curl -i --data 'userName=user&password=1234)' -X POST http://zeppelin-url.com:8890/api/login | grep 'Set-Cookie: JSESSIONID=' | cut -d ':' -f2 |  tail -1 | cut -d ';' -f1`\"\necho $SESSION\ncurl -i -b ${SESSION} -X POST http://zeppelin-url.com:8890/api/notebook/job/NOTEBOOK_ID
\n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}}},{"node":{"title":"AWS EMR에서 S3 사용 시 주의사항","id":"990a6e60-c773-50b0-a6c0-a9c79431c620","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}}},{"node":{"title":"Spark의 Shuffling 이해하기","id":"93e10410-a3f1-5148-a79f-32f260c5b90d","slug":"spark-shuffling","publishDate":"August 25, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":7,"humanPageNumber":8,"skip":43,"limit":6,"numberOfPages":16,"previousPagePath":"/7","nextPagePath":"/9"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/8","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Structuring Your TensorFlow Models","id":"976a40c0-463b-5a5b-918a-bb7adeb7a48e","slug":"structuring-tf","publishDate":"April 20, 2018","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

이 글은 저자의 허락을 받아 번역한 글 입니다. 원문 링크

\n

TensorFlow에서 모델을 정의하다보면 어느새 많은 양의 코드가 생성된 경험이 있을 것 입니다. 어떻게 하면 가독성과 재사용성이 높은 코드로 구성할 수 있을까요? 여기에서 실제 동작하는 예시 코드를 확인하실 수 있습니다. Gist Link

\n
\n

Defining the Compute Graph

\n

모델 당 하나의 클래스부터 시작하는 것이 좋습니다. 그 클래스의 인터페이스는 무엇인가요? 일반적으로 모델은 input data와 target placeholder를 연결하며 training, evaluation 그리고 inference 관련 함수를 제공합니다.

\n\n

위의 코드가 기본적으로 TensorFlow codebase에서 모델이 정의되는 방식입니다. 그러나 여기에도 몇 가지 문제가 있습니다. 가장 중요한 문제는 전체 그래프가 단일 함수 생성자로 정의된다는 점 입니다. 이렇게 되면 가독성이 떨어지며 재사용이 어렵습니다.

\n
\n

Using Properties

\n

함수가 호출 될 때마다 그래프는 확장되기 때문에 함수로 분할하는 것만으로는 부족합니다. 따라서 함수를 처음 호출하는 시점에 operation들이 그래프에 추가되도록 해야합니다. 이러한 방식을 기본적으로 lazy-loading이라고 합니다.

\n
class Model:\n\n    def __init__(self, data, target):\n        data_size = int(data.get_shape()[1])\n        target_size = int(target.get_shape()[1])\n        weight = tf.Variable(tf.truncated_normal([data_size, target_size]))\n        bias = tf.Variable(tf.constant(0.1, shape=[target_size]))\n        incoming = tf.matmul(data, weight) + bias\n        self._prediction = tf.nn.softmax(incoming)\n        cross_entropy = -tf.reduce_sum(target, tf.log(self._prediction))\n        self._optimize = tf.train.RMSPropOptimizer(0.03).minimize(cross_entropy)\n        mistakes = tf.not_equal(\n            tf.argmax(target, 1), tf.argmax(self._prediction, 1))\n        self._error = tf.reduce_mean(tf.cast(mistakes, tf.float32))\n\n    @property\n    def prediction(self):\n        return self._prediction\n\n    @property\n    def optimize(self):\n        return self._optimize\n\n    @property\n    def error(self):\n        return self._error
\n

위의 방식이 첫 번째 예제보다 훨씬 좋습니다. 이제 코드는 독립적인 함수로 구성되어 있습니다. 그러나 아직 코드는 lazy-loading으로 인해 약간 복잡해보입니다. 이를 어떻게 개선 할 수 있는지 보도록 하겠습니다.

\n
\n

Lazy Property Decorator

\n

파이썬은 아주 유연한 언어입니다. 이제 마지막 예제에서 중복 코드를 제거하는 방법을 보여드리겠습니다. 우리는 @property처럼 동작하지만 한번만 함수를 평가하는 decorator를 사용할 것입니다. decorator는 함수(접두사를 앞에 붙임)의 이름을 따서 멤버에 결과를 저장하고 나중에 호출되는 시점에 해당 값을 반환합니다. custom decorator를 아직 사용해본적이 없다면, 이 가이드를 참고하시면 됩니다.

\n
class Model:\n\n    def __init__(self, data, target):\n        self.data = data\n        self.target = target\n        self._prediction = None\n        self._optimize = None\n        self._error = None\n\n    @property\n    def prediction(self):\n        if not self._prediction:\n            data_size = int(self.data.get_shape()[1])\n            target_size = int(self.target.get_shape()[1])\n            weight = tf.Variable(tf.truncated_normal([data_size, target_size]))\n            bias = tf.Variable(tf.constant(0.1, shape=[target_size]))\n            incoming = tf.matmul(self.data, weight) + bias\n            self._prediction = tf.nn.softmax(incoming)\n        return self._prediction\n\n    @property\n    def optimize(self):\n        if not self._optimize:\n            cross_entropy = -tf.reduce_sum(self.target, tf.log(self.prediction))\n            optimizer = tf.train.RMSPropOptimizer(0.03)\n            self._optimize = optimizer.minimize(cross_entropy)\n        return self._optimize\n\n    @property\n    def error(self):\n        if not self._error:\n            mistakes = tf.not_equal(\n                tf.argmax(self.target, 1), tf.argmax(self.prediction, 1))\n            self._error = tf.reduce_mean(tf.cast(mistakes, tf.float32))\n        return self._error
\n

위의 decorator를 사용해서 예시 코드는 아래와 같이 간결해졌습니다.

\n
import functools\n\ndef lazy_property(function):\n    attribute = '_cache_' + function.__name__\n\n    @property\n    @functools.wraps(function)\n    def decorator(self):\n        if not hasattr(self, attribute):\n            setattr(self, attribute, function(self))\n        return getattr(self, attribute)\n\n    return decorator
\n

생성자에서 property를 언급했다는 부분이 중요합니다. 이렇게 구성한다면 tf.initialize_variables()를 실행할 때 전체 그래프가 정의됩니다.

\n
\n

Organizing the Graph with Scopes

\n

이제 코드에서 모델을 정의하는 부분은 깔끔해졌지만, 그래프의 연산 부분은 여전히 복잡합니다. 만일 그래프를 시각화한다면, 서로 연결되어 있는 노드가 많이 나타날 것 입니다. 이를 해결하기 위한 방법은 tf.name_scope('name') 또는 tf.variable_scope('name')을 사용하여 각 함수의 내용을 래핑하는 것 입니다. 이렇게 하면 노드들은 그래프 상에서 그룹화되어 있을 것 입니다. 우리는 이전에 만들었던 decorator를 이용하여 이를 자동으로 적용시켜보겠습니다.

\n
class Model:\n\n    def __init__(self, data, target):\n        self.data = data\n        self.target = target\n        self.prediction\n        self.optimize\n        self.error\n\n    @lazy_property\n    def prediction(self):\n        data_size = int(self.data.get_shape()[1])\n        target_size = int(self.target.get_shape()[1])\n        weight = tf.Variable(tf.truncated_normal([data_size, target_size]))\n        bias = tf.Variable(tf.constant(0.1, shape=[target_size]))\n        incoming = tf.matmul(self.data, weight) + bias\n        return tf.nn.softmax(incoming)\n\n    @lazy_property\n    def optimize(self):\n        cross_entropy = -tf.reduce_sum(self.target, tf.log(self.prediction))\n        optimizer = tf.train.RMSPropOptimizer(0.03)\n        return optimizer.minimize(cross_entropy)\n\n    @lazy_property\n    def error(self):\n        mistakes = tf.not_equal(\n            tf.argmax(self.target, 1), tf.argmax(self.prediction, 1))\n        return tf.reduce_mean(tf.cast(mistakes, tf.float32))
\n

lazy caching 이외에도 TensorFlow의 기능을 포함시키므로 decorator에 새로운 이름을 지정했습니다. 그 외의 나머지 부분은 이전과 동일합니다.

\n

이제 @define_scope decorator를 통해 tf.variable_scope()에 인자를 전달할 수 있습니다. 예를 들어 해당 scope에 default initializer를 정의할 수 있습니다. 이 부분이 더 궁금하다면 전체 예제 코드를 확인해보시면 됩니다.

\n
\n

Reference

\n

https://danijar.com/structuring-your-tensorflow-models/

\n
","excerpt":"이 글은 저자의 허락을 받아 번역한 글 입니다. 원문 링크 TensorFlow…"}}}},{"node":{"title":"Data Science inconvenient truth","id":"94eeb309-1ab1-58ad-af4b-9e354444e47b","slug":"data-science-inconvenient-truth","publishDate":"April 01, 2018","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터과학의 불편한 진실

\n\n
\n

Ref: https://www.kdnuggets.com/2015/05/data-science-inconvenient-truth.html

","excerpt":"데이터과학의 불편한 진실 Data is never clean (데이터는 절대 깨끗하지 않다) You will spend most of your…"}}}},{"node":{"title":"Deep Learning Programming Style: Symbolic, Imperative","id":"9160ad92-4b57-5842-ab2e-5ee3b2c9070c","slug":"deep-learning-style","publishDate":"January 05, 2018","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

TensorFlow 1.5 버전부터 Eager Execution 이라는 기능이 추가되었습니다.\n다시 말해서 imperative programming style을 지원한다고 적혀있는데, 기존의 방식과 어떤 차이가 있는지 알아보겠습니다.\nMXNet의 Deep Learning Programming Style 문서를 번역한 내용입니다.

\n
\n

Deep Learning Programming Style

\n

우리는 항상 성능과 최적화에 대한 고민을 합니다. 하지만 그 이전에 잘 동작하는 코드인지 여부가 중요합니다. 이제는 다양한 딥러닝 라이브러리들이 존재하지만 각자 프로그래밍 방식에 대해 다른 접근 방식을 가지고 있기 때문에 학습하는 것도 힘들며, 이를 이용하여 명확하고 직관적인 deep learning 코드를 작성하는 것도 어렵습니다.

\n

이 문서에서는 가장 중요한 두 가지 디자인 패턴에 집중하려고 합니다.

\n
    \n
  1. \n

    Whether to embrace the symbolic or imperative paradigm for mathematical computation.

    \n
  2. \n
  3. \n

    Whether to build networks with bigger (more abstract) or more atomic operations.

    \n
  4. \n
\n
\n

Symbolic vs. Imperative Programs

\n

만일 당신이 파이썬 또는 C++ 개발자라면, 이미 Imperative program과 친숙할 것 입니다.\nImperative style program들은 바로 연산을 수행합니다. 대부분의 파이썬 코드들이 imperative 한 형태를 보여주는데, 예를 들면 아래와 같은 Numpy 코드를 말합니다.

\n
import numpy as np\na = np.ones(10)\nb = np.ones(10) * 2\nc = b * a\nd = c + 1
\n

프로그램이 c = b * a를 수행하도록 명령을 내리면, 실제로 연산이 실행됩니다.

\n

반면에 Symbolic program은 조금 다릅니다. Symbolic-style program에서는 먼저 function (potentially complex) 을 정의합니다. function을 정의했다고 해서 실제 연산이 수행되는 것은 아닙니다. 우리는 그저 placeholder 값에 function을 정의한 것 뿐 입니다. 이 과정 이후에 function을 컴파일 할 수 있으며, 실제 입력 값을 통해 이를 평가하게 됩니다. 아래는 위에서 언급했던 imperative 코드를 symbolic style로 변환한 예제입니다.

\n
A = Variable('A')\nB = Variable('B')\nC = B * A\nD = C + Constant(1)\n# compiles the function\nf = compile(D)\nd = f(A=np.ones(10), B=np.ones(10)*2)
\n

보시다시피 symbolic 버전에서는 C = B * A가 수행되는 시점에 실제로 연산이 일어나지 않습니다. 대신에 이 operation은 연산 과정을 표현하는 computation graph (aka. symbolic graph) 를 생성합니다. 예를 들면, D의 연산을 위해 아래와 같은 computation graph가 생성됩니다.

\n

\n \n \n \n

\n

대부분의 symbolic-style 프로그램들은 명시적으로든 암시적으로든 컴파일 단계를 포함합니다. 이를 통해 computation graph를 언제든 호출할 수 있는 함수로 변환시켜줍니다. 위의 예제에서도 실제 연산은 코드의 마지막 줄에서만 수행됩니다. 이를 통해 얻을 수 있는 점은 computation graph를 작성하는 단계와 실행하는 단계를 명확히 분리할 수 있다는 것 입니다. Neural Network에서도 우리는 전체 모델을 단일 computation graph로 정의합니다.

\n

Torch, Chiner 그리고 Minerva와 같은 딥러닝 라이브러리들은 imperative style을 사용하고 있습니다. symbolic-style을 사용하는 딥러닝 라이브러리로는 Theano, CGT 그리고 TensorFlow가 있습니다. 그리고 CXXNet 이나 Caffe와 같은 라이브러리들은 설정파일에 의존하는 방식으로 symbolic style을 지원합니다. (ex. Caffe의 prototxt)\n이제 두 가지 딥러닝 프로그래밍 방식에 대해 이해했으니, 각 방식의 장점에 대해 알아보겠습니다.

\n
\n

Imperative Programs Tend to be More Flexible

\n

imperative program은 프로그래밍 언어의 flow와 상당히 잘 맞아들어가며 유연하게 동작하는 것 처럼 보입니다. 그렇다면 왜 수 많은 딥러닝 라이브러리들이 symbolic 패러다임을 선택할까요? 가장 큰 이유는 메모리 사용량과 속도 측면에서의 효율성 때문입니다. 위에서 언급했던 예제로 돌아가 천천히 설명드리겠습니다.

\n
a = np.ones(10)\nb = np.ones(10) * 2\nc = b * a\nd = c + 1
\n

\n \n \n \n

\n

주어진 array의 각 셀이 8 바이트의 메모리를 소모한다고 가정해보겠습니다. 콘솔에서 위의 프로그램을 실행하면 메모리가 얼마나 소모될까요?

\n

imperative program에서는 각 라인마다 메모리 할당이 요구됩니다. 사이즈가 10인 array가 4개 할당되므로 4 * 10 * 8 = 320 bytes의 메모리가 요구됩니다.\n반면 computation graph에서는 궁극적으로 d가 필요하다는 것을 알고 있기 때문에, 즉시 값을 메모리에 할당하는 대신에 메모리를 재사용할 수 있습니다. 예를 들어 b를 위해 할당된 공간에 c를 저장하도록 재사용하고, c를 위해 할당된 공간에 다시 d를 저장하도록 한다면 결국 요구되는 메모리는 2 * 10 * 8 = 160 bytes 절반으로 줄어들게 됩니다.

\n

\n )\nb = array(2, 'b')\nc = b * a\nd = c + 1\nprint d.value\nprint d.grad(1)\n# Results\n# 3\n# {'a': 2, 'b': 1}\n


\n

Model Checkpoints

\n

모델을 저장하고 다시 불러오는 일 또한 중요합니다. 보통 Neural Network 모델을 저장한다는 것은 네트워크의 구조, 설정 값 그리고 weight 값의 저장을 의미합니다.

\n
A = Variable('A')\nB = Variable('B')\nC = B * A\nD = C + Constant(1)\n\nD.save('mygraph')\nD2 = load('mygraph')\nf = compile([D2])\n# more operations\n...
\n

설정 값을 체크하는 일은 symbolic program이 더 유리 합니다. symbolic 구조에서는 실제 연산을 수행할 필요가 없기 때문에 computation graph를 그대로 serialize 하면 됩니다.\n반면에 Imperative program은 연산 할 때 실행되기 때문에 코드 자체를 설정 파일로 저장하거나 그 위에 또 다른 레이어를 구성해야합니다.

\n
\n

Parameter Updates

\n

computation graph의 경우 연산과정은 쉽게 설명할 수 있지만 parameter 업데이트에 대해서는 명확하지 못합니다. parameter update는 기본적으로 값의 변경(mutation)을 요구하기 때문에 computation graph의 개념과 맞지 않습니다. 따라서 대부분의 symbolic program들은 persistent state를 갱신하기 위해 special update 구문을 사용하고 있습니다.

\n

반면에 imperative style에서는 parameter 업데이트를 작성하는 것이 쉽습니다. 특히 서로 연관된 여러 업데이트가 필요할 때 더욱 그렇습니다. Symbolic program의 경우 업데이트 문은 사용자가 호출 할 때 실행됩니다. 이런 점에서 대부분의 symbolic deep learning 라이브러리는 parameter 업데이트에 대해 gradient 연산을 수행하면서 업데이트를 수행하는 imperative style로 다시 돌아갑니다.

\n
\n

There Is No Strict Boundary

\n

두 가지 프로그래밍 스타일을 비교해서, 하나만 사용하라는 말이 아닙니다. 명령형 프로그램을 상징형 프로그램처럼 만들거나 그 반대도 가능합니다. 예를 들면, Python으로 JIT (just-in-time) 컴파일러를 작성하여 명령형 Python 프로그램을 컴파일 할 수 있습니다. 하지만 두 가지 아키텍쳐를 이해하는 것은 수 많은 딥러닝 라이브러리의 추상화와 그 차이를 이해하는데 도움이 됩니다. 결국, 우리는 프로그래밍 스타일 간에 명확한 경계선이 없다고 결론을 내릴 수 있습니다.

\n
","excerpt":"TensorFlow 1.5 버전부터 Eager Execution…"}}}},{"node":{"title":"제플린 노트북 자동 실행 스크립트 만들기","id":"cd93f129-9906-59b5-84e8-6a1fff1e7e00","slug":"zeppelin-bootstrap","publishDate":"September 13, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf 등록을 위해 처음 실행시켜야 하는 노트북이 있다면 참 번거롭습니다.\n하지만 Zeppelin Notebook API 사용한다면 이를 쉽게 자동화 할 수 있습니다.

\n
\n

Zeppelin Notebook API

\n

제플린은 노트북 자동실행을 위한 REST API를 제공합니다.\n하지만 제플린에 인증이 걸려있다면, 인증을 거쳐야만 API를 사용할 수 있습니다.\n따라서, 먼저 curl로 세션 값을 받고 해당 노트북 아이디를 호출하시면 됩니다.

\n

노트북 아이디는 해당 노트 URL의 가장 마지막 값 입니다. (ex 2AZPHY918)\n아래의 스크립트는 아이디가 user, 패스워드가 1234인 경우를 예시로 들었습니다.

\n
#!/bin/sh\nsudo /usr/lib/zeppelin/bin/zeppelin-daemon.sh stop\nsleep 3\nsudo /usr/lib/zeppelin/bin/zeppelin-daemon.sh start\n\nsleep 15\n\nSESSION=\"`curl -i --data 'userName=user&password=1234)' -X POST http://zeppelin-url.com:8890/api/login | grep 'Set-Cookie: JSESSIONID=' | cut -d ':' -f2 |  tail -1 | cut -d ';' -f1`\"\necho $SESSION\ncurl -i -b ${SESSION} -X POST http://zeppelin-url.com:8890/api/notebook/job/NOTEBOOK_ID
\n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}}},{"node":{"title":"AWS EMR에서 S3 사용 시 주의사항","id":"990a6e60-c773-50b0-a6c0-a9c79431c620","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}}},{"node":{"title":"Spark의 Shuffling 이해하기","id":"93e10410-a3f1-5148-a79f-32f260c5b90d","slug":"spark-shuffling","publishDate":"August 25, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n ;\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}}},{"node":{"title":"Bagging과 Boosting 그리고 Stacking","id":"b6f97b31-bd09-5915-81f9-4ad527909181","slug":"bagging-boosting","publishDate":"July 19, 2017","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

오늘은 머신러닝 성능을 최대로 끌어올릴 수 있는 앙상블 기법에 대해 정리해보았습니다.

\n
\n

Ensemble, Hybrid Method

\n

앙상블 기법은 동일한 학습 알고리즘을 사용해서 여러 모델을 학습하는 개념입니다.\nWeak learner를 결합한다면, Single learner보다 더 나은 성능을 얻을 수 있다는 아이디어입니다.\nBaggingBoosting 이 이에 해당합니다.

\n

동일한 학습 알고리즘을 사용하는 방법을 앙상블이라고 한다면,\n서로 다른 모델을 결합하여 새로운 모델을 만들어내는 방법도 있습니다.\n대표적으로 Stacking 이 있으며, 최근 Kaggle 에서 많이 소개된 바 있습니다.

\n
\n

Bagging

\n

Bagging은 샘플을 여러 번 뽑아 각 모델을 학습시켜 결과를 집계(Aggregating) 하는 방법입니다. 아래의 그림을 통해 자세히 알아보겠습니다.

\n

\n \n \n \n

\n
\n

Boosting

\n

Bagging이 일반적인 모델을 만드는데 집중되어있다면,\nBoosting은 맞추기 어려운 문제를 맞추는데 초점이 맞춰져 있습니다.

\n

수학 문제를 푸는데 9번 문제가 엄청 어려워서 계속 틀렸다고 가정해보겠습니다.\nBoosting 방식은 9번 문제에 가중치를 부여해서 9번 문제를 잘 맞춘 모델을 최종 모델로 선정합니다.\n아래 그림을 통해 자세히 알아보겠습니다.

\n

\"\"

\n

Boosting도 Bagging과 동일하게 복원 랜덤 샘플링을 하지만, 가중치를 부여한다는 차이점이 있습니다.\nBagging이 병렬로 학습하는 반면, Boosting은 순차적으로 학습시킵니다.\n학습이 끝나면 나온 결과에 따라 가중치가 재분배됩니다.

\n

오답에 대해 높은 가중치를 부여하고, 정답에 대해 낮은 가중치를 부여하기 때문에\n오답에 더욱 집중할 수 있게 되는 것 입니다.\nBoosting 기법의 경우, 정확도가 높게 나타납니다.\n하지만, 그만큼 Outlier에 취약하기도 합니다.

\n

AdaBoost, XGBoost, GradientBoost 등 다양한 모델이 있습니다.\n그 중에서도 XGBoost 모델은 강력한 성능을 보여줍니다. 최근 대부분의 Kaggle 대회 우승 알고리즘이기도 합니다.

\n
\n

Stacking

\n

Meta Modeling 이라고 불리기도 하는 이 방법은 위의 2가지 방식과는 조금 다릅니다.\n“Two heads are better than one” 이라는 아이디어에서 출발합니다.

\n

Stacking은 서로 다른 모델들을 조합해서 최고의 성능을 내는 모델을 생성합니다.\n여기에서 사용되는 모델은 SVM, RandomForest, KNN 등 다양한 알고리즘을 사용할 수 있습니다.\n이러한 조합을 통해 서로의 장점은 취하고 약점을 보완할 수 있게 되는 것 입니다.

\n

Stacking은 이미 느끼셨겠지만 필요한 연산량이 어마어마합니다.\n적용해보고 싶다면 아래의 StackNet을 사용하는 방법을 추천합니다.

\n

https://github.com/kaz-Anova/StackNet

\n

문제에 따라 정확도를 요구하기도 하지만, 안정성을 요구하기도 합니다.\n따라서, 주어진 문제에 적절한 모델을 선택하는 것이 중요합니다.

\n
","excerpt":"오늘은 머신러닝 성능을 최대로 끌어올릴 수 있는 앙상블 기법에 대해 정리해보았습니다. Ensemble, Hybrid Method…"}}}},{"node":{"title":"Spark DataFrame을 MySQL에 저장하는 방법","id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","slug":"spark-df-mysql","publishDate":"July 17, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}}},{"node":{"title":"Spark 2.2.0 릴리즈 업데이트 정리","id":"685d6694-ca41-5c2f-89a2-86556223c62c","slug":"spark22","publishDate":"July 14, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}}},{"node":{"title":"Scala의 빌드 도구 SBT","id":"6f46a630-8289-50b6-b037-f06b3d58bfb4","slug":"scala-sbt","publishDate":"July 08, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scala에는 SBT라는 빌드 도구가 있습니다.\nSBT는 의존성 관리에 Apache ivy를 사용합니다.

\n
\n

SBT

\n

SBT로 생성한 프로젝트의 기본 디렉토리를 보면 build.sbt가 있습니다.\nsbt라는 명령어를 통해 sbt-shell로 이동할 수 있습니다.

\n
\n

자주 사용하는 SBT 명령어

\n\n
\n

Reference

\n

http://www.scala-sbt.org/0.13/docs/index.html

\n
","excerpt":"Scala에는 SBT라는 빌드 도구가 있습니다.\nSBT는 의존성 관리에 Apache ivy를 사용합니다. SBT SBT…"}}}},{"node":{"title":"AWS EMR step을 이용한 Spark Batch 작업","id":"c78e09d9-7707-54ec-863b-69e21551e3b0","slug":"emr-step","publishDate":"July 02, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":8,"humanPageNumber":9,"skip":49,"limit":6,"numberOfPages":16,"previousPagePath":"/8","nextPagePath":"/10"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/9","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Spark groupByKey vs reduceByKey","id":"f4923e82-cd6e-5ba2-8897-c378854708c3","slug":"spark-reduceByKey-groupByKey","publishDate":"August 22, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark Application 성능 개선을 위한 groupByKey, reduceBykey에 대해 알아보겠습니다.

\n
\n

groupByKey vs reduceBykey

\n
# reduceByKey\nspark.textFile(\"hdfs://...\")\n .flatMap(lambda line: line.split())\n .map(lambda word: (word, 1))\n .reduceByKey(lambda a, b: a + b)\n\n# groupByKey\nspark.textFile(\"hdfs://...\")\n .flatMap(lambda line: line.split())\n .map(lambda word: (word, 1))\n .groupByKey()\n .map(lambda (w, counts): (w, sum(counts)))
\n

가장 흔히 알고 있는 word count 예제를 예로 들어보겠습니다.\n위의 예시는 reduceByKey를 사용했으며, 아래의 예시는 groupByKey를 사용했습니다.\n둘의 결과는 같지만 성능은 확인히 차이가 납니다.

\n

먼저 위의 코드에서 flatMap, map 까지는 동일한 노드에서 실행이 됩니다.\n하지만 reducer 부분에서는 모든 동일한 단어 쌍을 같은 노드로 이동시켜야 하기 때문에 Shuffle 이 발생합니다.

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}}},{"node":{"title":"Hive Metastore 구축 관련 문제와 해결과정","id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","slug":"hive-metastore-issue","publishDate":"August 11, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}}},{"node":{"title":"Bagging과 Boosting 그리고 Stacking","id":"b6f97b31-bd09-5915-81f9-4ad527909181","slug":"bagging-boosting","publishDate":"July 19, 2017","heroImage":{"title":"cover-datascience","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUG/8QAHxAAAgEEAgMAAAAAAAAAAAAAAQIAAwQRIQVCExSR/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oADAMBAAIRAxEAPwCthE7ioFIBOznEy05C8uWI8wpjHVZCF3uFSpUd99m18kpIXSYyTqES96ouiFMJnScb/9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/5l0PQJpz5C5IDFjHYigWJI/389fe4852b9cb39e9ada4938db33e6ca/cover_datascience.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

오늘은 머신러닝 성능을 최대로 끌어올릴 수 있는 앙상블 기법에 대해 정리해보았습니다.

\n
\n

Ensemble, Hybrid Method

\n

앙상블 기법은 동일한 학습 알고리즘을 사용해서 여러 모델을 학습하는 개념입니다.\nWeak learner를 결합한다면, Single learner보다 더 나은 성능을 얻을 수 있다는 아이디어입니다.\nBaggingBoosting 이 이에 해당합니다.

\n

동일한 학습 알고리즘을 사용하는 방법을 앙상블이라고 한다면,\n서로 다른 모델을 결합하여 새로운 모델을 만들어내는 방법도 있습니다.\n대표적으로 Stacking 이 있으며, 최근 Kaggle 에서 많이 소개된 바 있습니다.

\n
\n

Bagging

\n

Bagging은 샘플을 여러 번 뽑아 각 모델을 학습시켜 결과를 집계(Aggregating) 하는 방법입니다. 아래의 그림을 통해 자세히 알아보겠습니다.

\n

\n \n \n \n

\n
\n

Boosting

\n

Bagging이 일반적인 모델을 만드는데 집중되어있다면,\nBoosting은 맞추기 어려운 문제를 맞추는데 초점이 맞춰져 있습니다.

\n

수학 문제를 푸는데 9번 문제가 엄청 어려워서 계속 틀렸다고 가정해보겠습니다.\nBoosting 방식은 9번 문제에 가중치를 부여해서 9번 문제를 잘 맞춘 모델을 최종 모델로 선정합니다.\n아래 그림을 통해 자세히 알아보겠습니다.

\n

\"\"

\n

Boosting도 Bagging과 동일하게 복원 랜덤 샘플링을 하지만, 가중치를 부여한다는 차이점이 있습니다.\nBagging이 병렬로 학습하는 반면, Boosting은 순차적으로 학습시킵니다.\n학습이 끝나면 나온 결과에 따라 가중치가 재분배됩니다.

\n

오답에 대해 높은 가중치를 부여하고, 정답에 대해 낮은 가중치를 부여하기 때문에\n오답에 더욱 집중할 수 있게 되는 것 입니다.\nBoosting 기법의 경우, 정확도가 높게 나타납니다.\n하지만, 그만큼 Outlier에 취약하기도 합니다.

\n

AdaBoost, XGBoost, GradientBoost 등 다양한 모델이 있습니다.\n그 중에서도 XGBoost 모델은 강력한 성능을 보여줍니다. 최근 대부분의 Kaggle 대회 우승 알고리즘이기도 합니다.

\n
\n

Stacking

\n

Meta Modeling 이라고 불리기도 하는 이 방법은 위의 2가지 방식과는 조금 다릅니다.\n“Two heads are better than one” 이라는 아이디어에서 출발합니다.

\n

Stacking은 서로 다른 모델들을 조합해서 최고의 성능을 내는 모델을 생성합니다.\n여기에서 사용되는 모델은 SVM, RandomForest, KNN 등 다양한 알고리즘을 사용할 수 있습니다.\n이러한 조합을 통해 서로의 장점은 취하고 약점을 보완할 수 있게 되는 것 입니다.

\n

Stacking은 이미 느끼셨겠지만 필요한 연산량이 어마어마합니다.\n적용해보고 싶다면 아래의 StackNet을 사용하는 방법을 추천합니다.

\n

https://github.com/kaz-Anova/StackNet

\n

문제에 따라 정확도를 요구하기도 하지만, 안정성을 요구하기도 합니다.\n따라서, 주어진 문제에 적절한 모델을 선택하는 것이 중요합니다.

\n
","excerpt":"오늘은 머신러닝 성능을 최대로 끌어올릴 수 있는 앙상블 기법에 대해 정리해보았습니다. Ensemble, Hybrid Method…"}}}},{"node":{"title":"Spark DataFrame을 MySQL에 저장하는 방법","id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","slug":"spark-df-mysql","publishDate":"July 17, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}}},{"node":{"title":"Spark 2.2.0 릴리즈 업데이트 정리","id":"685d6694-ca41-5c2f-89a2-86556223c62c","slug":"spark22","publishDate":"July 14, 2017","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}}},{"node":{"title":"Scala의 빌드 도구 SBT","id":"6f46a630-8289-50b6-b037-f06b3d58bfb4","slug":"scala-sbt","publishDate":"July 08, 2017","heroImage":{"title":"cover-develop","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&q=50&fm=webp 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&q=50&fm=webp 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&q=50&fm=webp 1800w","sizes":"(min-width: 1800px) 1800px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=450&h=300&fl=progressive&q=50&fm=jpg 450w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=900&h=600&fl=progressive&q=50&fm=jpg 900w,\nhttps://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&h=1200&fl=progressive&q=50&fm=jpg 1800w","sizes":"(min-width: 1800px) 1800px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gJASUNDX1BST0ZJTEUAAQEAAAIwQURCRQIQAABtbnRyUkdCIFhZWiAHzwAGAAMAAAAAAABhY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAADJkZXNjAAABMAAAAGt3dHB0AAABnAAAABRia3B0AAABsAAAABRyVFJDAAABxAAAAA5nVFJDAAAB1AAAAA5iVFJDAAAB5AAAAA5yWFlaAAAB9AAAABRnWFlaAAACCAAAABRiWFlaAAACHAAAABR0ZXh0AAAAAENvcHlyaWdodCAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jb3Jwb3JhdGVkAAAAZGVzYwAAAAAAAAARQWRvYmUgUkdCICgxOTk4KQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAGN1cnYAAAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAY3VydgAAAAAAAAABAjMAAFhZWiAAAAAAAACcGAAAT6UAAAT8WFlaIAAAAAAAADSNAACgLAAAD5VYWVogAAAAAAAAJjEAABAvAAC+nP/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//AABEIAA0AFAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAEAAEF/8QAIhAAAgIDAAAHAQAAAAAAAAAAAQIDEQAEEhMUISIxQaGx/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEQEh/9oADAMBAAIRAxEAPwDgvHMddmjWmr2mhX7hNfzgkucjwwLNcn+Y2LaPKB0VlArn4vNm2YxGyrrqCfu8nkhdFkChz65YR5umvkDLCT//2Q=="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/4W9SzEIJpHuwsUBnxSSypH/3a18765095ea5756c742b7adb83a0518/cover_develop.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scala에는 SBT라는 빌드 도구가 있습니다.\nSBT는 의존성 관리에 Apache ivy를 사용합니다.

\n
\n

SBT

\n

SBT로 생성한 프로젝트의 기본 디렉토리를 보면 build.sbt가 있습니다.\nsbt라는 명령어를 통해 sbt-shell로 이동할 수 있습니다.

\n
\n

자주 사용하는 SBT 명령어

\n\n
\n

Reference

\n

http://www.scala-sbt.org/0.13/docs/index.html

\n
","excerpt":"Scala에는 SBT라는 빌드 도구가 있습니다.\nSBT는 의존성 관리에 Apache ivy를 사용합니다. SBT SBT…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":8,"humanPageNumber":9,"skip":49,"limit":6,"numberOfPages":16,"previousPagePath":"/8","nextPagePath":"/10"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/index/page-data.json b/page-data/index/page-data.json index f13bb6f..ecb3c0f 100644 --- a/page-data/index/page-data.json +++ b/page-data/index/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-posts-js","path":"/","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"Pandas 2.0의 Copy-on-Write에 대하여","id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}}},{"node":{"title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}}},{"node":{"title":"베를린에서 2개월 살아남기","id":"ab300765-6809-53b6-a6cd-952c7dd3c976","slug":"berlin","publishDate":"May 10, 2023","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

우연히 회사에서 좋은 기회를 얻게 되어 독일에서 2개월 근무한 후기
\n베를린 생활부터 유럽의 개발 문화까지 그 동안 겪은 경험을 정리해보려 합니다.

\n
\n

베를린 아파트

\n

숙소가 정말 평이 좋고 실제로 시설도 좋은 아파트인데 엘리베이터가 없었다.\n여기는 그라운드 개념이 있어서 5층이 한국으로 치면 6층인데 다들 아무렇지 않게 걸어올라간다.\n빨래, 건조를 하려면 6층을 왕복 3번 다녀야 하는데 이게 제일 힘들었다.

\n

그리고 모든 문을 열쇠로 열어야 하는데 이게 정말 난감하다.\n최근에 열쇠를 들고 다녀본 기억이 없다보니 두고 다닐 수 있는데\n열쇠를 두고 오면 한화 약 20만원 정도의 벌금을 내야한다.\n열쇠 분실로 인한 사고 방지를 위해 문을 교체해버리기 때문에 저런 비용이 발생한다.\n개인적인 생각으로는 그냥 도어락을 도입하는게 나아보였다 🙏🏻

\n
\n

베를린 교통과 생활

\n

처음 도착하자마자 놀란건 일요일에 모든 마트, 식료품가게가 문을 닫는다는 것이다.\n만약을 위해 베를린 전체에서 세 군데 정도 대형마트만 문을 연다.\n결국 생활용품을 사기 위해 베를린 중앙역까지 갔는데 뉴스에서만 보던 그림을 볼 수 있었다.\n재난상황마냥 줄이 끝까지 이어져있고 마트 내부 물품은 다 털려있었다.\n그래도 독일은 국가가 통제하기 때문에 마트 물가가 정말 저렴하다.

\n

베를린 대중교통으로는 트램, 지하철, 버스를 제일 많이 탄다.\n종일권을 끊으면 모든 교통수단을 무제한으로 탈 수 있다.\n근데 이 보다 더 좋은 교통수단은 자전거다.\n자전거 도로가 너무 잘되어 있어서 대중교통 이용하는 것보다 시간이 빠를 때가 많다.\n도시 간 이동으로는 flixbus와 ICE 고속열차를 많이 이용한다.\n독일의 고속철은 워낙 악명 높아서 취소되는 일이 빈번하다 들었는데\n역시나 당일 오전 출발 5분 전에 ICE 열차 취소를 겪었다.

\n

\n \n \n \n

\n

베를린은 너무 다양한 인종이 살고 있어 전 세계 음식을 다 먹어볼 수 있다.
\n그 중에서 독일 음식은 흰색 소세지와 커리부어스트만 먹어보면 된다. 나머지는 별로였다.\n햄버거의 어원이 함부르크에서 나온 만큼 수제버거도 맛있는 곳이 많다.

\n

\n \n \n \n

\n

때마침 베를린 빛 축제가 진행 중이어서 관광지에서 다양한 야경을 볼 수 있었다.

\n
\n

베를린 개발 문화, 스타트업

\n

유럽도 일하는 방식은 비슷했으나 한국과 비교했을 때 일을 디테일하게 한다고 느꼈다.\n예를 들면 RFC 문서를 정말 자세히 작성하고 오픈소스와 같은 플랫폼 운영 방식을 가지고 있었다.

\n

그리고 정서 상 한국은 정말 겸손한 반면 여기는 잘한 일이 생기면 자랑하고 모두가 축하하는 분위기였다.\n해외 감성으로 네트워킹, 파티도 자주 열린다.\n독일은 아프면 바로 병가를 15일까지 낼 수 있고 휴가가 25일이다.

\n

금요일에는 모두 일찍 퇴근하고 앞에서 맥주를 마신다.\n모든 과정을 경험해보니 절대 한국만큼의 개발 속도가 나올 수는 없겠다는 생각이 들었지만\n반대로 유럽에서 여유롭게 사는 법을 배운 것 같다.

\n

데이터 분야에서는 특별한 차이가 있는데 개인정보에 대한 사람들의 인식이다.\n한국에 있을 때도 많이 들어봤던 GDPR이라는 규정에 대해서도 알게 되었다.\n대부분 사용자들은 개인정보를 절대 서비스에 넘기려하지 않는다.

\n

그러다보니 데이터 기반의 서비스를 만드는 사람들은 정말 난감할 때가 많은데 개인화 추천이 가장 대표적이다.\n일단 사용자를 식별해야 개인화를 할텐데 여기는 개인을 정의하는 것부터가 어려운 문제다.

\n

\n \n \n \n

\n

베를린의 옛 건물을 내부만 리모델링해서 사용한 스타트업 공유 오피스도 방문했었다.
\n자동차 회사가 많은 나라답게 다양한 모빌리티 스타트업을 만날 수 있었다.
\n베를린에는 유럽 내의 스타트업이 많은 편인데 이 도시가 유럽에서 가장 글로벌하기 때문이다.
\n시골로 내려가면 기술에 대한 거부반응을 가진 사람이 많은 반면 베를린은 해외에서 온 이민자가 많다.\n그래서 독일어가 있음에도 영어를 정말 많이 사용한다.

\n

개발자 한정 2년만 근무하면 시민권도 쉽게 얻을 수 있다.\n대신 세금으로 절반을 가져간다 💸
\n이 말을 듣고 한국에서 살아야겠다는 생각이 들었다.

\n

인터넷이 정말 느려서 불편하다고 생각했는데 이 정도면 유럽에서 엄청 빠른 편이라고 한다.
\n하지만 스웨덴 스톡홀름에 가보니 독일 인터넷이 느리다는걸 확신할 수 있었다.

\n
\n

어쨋든 무사히 돌아와서 다행이라는 생각이 들었다.\n영어로 일하고 회의하는 것은 정말 많은 노력이 필요했다.
\n대신 유럽에서 근무하면 주말 동안 주변 국가 여행할 수 있다는 점이 큰 장점이다.
\n요즘 재택이 많아지다보니 해외에서 근무 가능한 회사들도 많이 생기고 있는데
\n만약 유럽에서 살고 싶다면 베를린에서 살아보는 것도 괜찮은 것 같다.

\n
","excerpt":"우연히 회사에서 좋은 기회를 얻게 되어 독일에서…"}}}},{"node":{"title":"MLOps 관련 책, 강의 리뷰 (DMLS, FSDL)","id":"8a70bf4b-6469-516d-9b3f-43f3fa109774","slug":"mlops-dmls-fsdl","publishDate":"September 13, 2022","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

MLOps는 다양한 지식과 컴포넌트를 다루고 있어 따로 공부하다보면 중요한 부분을 놓치고 도구에만 집착하게 되는 경우도 많습니다. 반면 알려진 책이나 강의를 들으니 퍼즐 조각들이 맞춰지는 것처럼 흩어져 있는 지식들이 하나로 정리되는 느낌을 받을 수 있었습니다.

\n

이 글에서는 MLOps 관련 자료 중 유명한 Full Stack Deep Learning 강의와 Designing Machine Learning Systems 책을 리뷰해보려 합니다.\nMLOps에 대해 관심있거나 시작하기 위해 자료를 찾는 분들에게 도움이 될 수 있을 것 같습니다.

\n
\n

Full Stack Deep Learning

\n

\n \n \n \n

\n

FSDL 강의는 MLOps 전반적인 주제를 모두 다루는 온라인 강의입니다.
\n아래 사이트 또는 유튜브에서 최신 강의를 볼 수 있습니다.
\n링크: https://fullstackdeeplearning.com/

\n

좋았던 점
\n매년 강의의 내용이 최신 트렌드를 최대한 반영하기 위해 업데이트 되고 있으며 Lab이라는 실습 과정이 준비되어 있습니다. CoLab 환경에서 실습할 수 있도록 자료가 준비되어 있는데 특히 ML 테스트 챕터의 자료가 좋았습니다.
\n중간에 다양한 오픈소스나 도구들을 소개해주는데 직접 구축하는 경우에도 아이디어를 얻을 수 있어 유용했습니다.

\n

아쉬운 점
\n많은 내용을 다루다보니 특정 주제는 간단하게 이미지나 링크만 공유하고 넘어가는 경우가 있어 설명이 부족한 경우가 있습니다. 제대로 이해하려면 제공되는 학습 자료들을 모두 찾아서 봐야 했습니다.
\n절대 사용할 일이 없을 법한 초기 스타트업들의 SaaS를 소개할때가 있는데 광고를 받은게 아닌가 싶은 생각이 들었습니다.

\n
\n

Designing Machine Learning Systems

\n

\n \n \n \n

\n

DMLS 책은 스탠포드 MLOps 강의로 유명한 Chip Huyen 교수님이 최근에 출판한 책입니다.
\n아직 한글판은 없어 Oreilly Learning 또는 Amazon에서 받아볼 수 있습니다.

\n

좋았던 점
\nFSDL보다 더 구체적인 사례를 들어 전반적인 내용을 이해하기 쉽게 설명한다고 느꼈습니다. 특히 구조적으로 설명해주고 바로 실무에 적용할 수 있도록 여러 가이드라인을 제시해주는 부분이 많습니다.
\n여러 오픈소스나 도구에 대해서도 기능을 구체적으로 다루기보다 어떤 기준으로 선택해야 하는지를 설명합니다. 실제 프로덕션 환경에서 마주치는 문제들을 소개하고 어떻게 해결하는지에 대한 내용을 미국 빅테크 기업들의 사례를 통해 설명하는 부분이 좋았습니다.

\n

아쉬운 점
\nMLOps와 데이터플랫폼의 역할을 완전히 나누어 두고 이건 우리의 역할이 아니라고 단정 짓는 부분들이 있습니다. 맞는 말이지만 어느 정도 같이 보고 싶은 분들에게는 아쉬울 수 있을 것 같습니다.

\n
\n

처음 시작한다면 접근성이 좋은 FSDL 강의를 보고 이후에 DMLS 책을 보는걸 추천드립니다.
\n특히 Data Distribution Shifts and Monitoring 목차는 FSDL을 먼저 확인한 다음 책을 보는게 이해하는데 도움이 되었습니다.

","excerpt":"MLOps…"}}}},{"node":{"title":"Spark on Kubernetes: 스팟 인스턴스 사용을 위한 기능들","id":"1b2de017-d945-522e-be73-569bf48aea40","slug":"spark-on-kubernetes-spot-instance","publishDate":"July 23, 2022","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~90%의 비용을 절감할 수 있습니다.\n하지만 스팟 인스턴스는 가격 입찰, 가용성 등 여러 이유로 중단될 수 있습니다.\n따라서 스팟 인스턴스를 사용한다면 노드가 중단되는 상황에 대비할 수 있어야 합니다.\n이 글에서는 Spark on Kubernetes를 스팟 인스턴스 위에서 안정적으로 운영하기 위해 필요한 설정들을 정리해보려 합니다.

\n



\n

driver는 on-demand에 할당하기

\n

중단된 노드에 있던 driver pod가 종료되는 경우, Spark 작업은 실패하게 됩니다. executor pod가 종료되는 경우, 캐시된 데이터 또는 셔플 파일을 잃게 되지만 새로운 executor를 통해 이를 다시 계산하기 때문에 전체 작업이 실패하지는 않습니다.

\n

위와 같은 이유로 driver는 온디맨드 인스턴스에 할당하는 것이 안전합니다.\n노드 그룹을 분리하고 nodeSelector를 활용한다면 driver는 온디맨드에서, executor는 스팟에서 실행하도록 설정할 수 있습니다.

\n



\n

적절한 인스턴스 유형 선택하기

\n

일부 인스턴스 유형은 해당 시점의 spot market 상황에 따라 안정적으로 확보하지 못할 수도 있습니다. 확보를 못하게 되면 executor는 계속 pending 상태에 머무르게 되고 전체 수행시간도 지연됩니다.

\n

사용량에 비해 크기가 큰 인스턴스 유형을 선택했다면, 여러 executor pod가 하나의 노드에 할당됩니다. 이 때 해당 노드가 중단된다면 여러 executor가 종료되므로 재계산에 더 많은 시간이 소요됩니다.

\n

위와 같은 이유로 적절한 인스턴스 유형을 선택하는 것이 spot kill을 줄이는데 도움이 됩니다.\nKarpenter를 사용한다면, 여러 인스턴스 유형을 지정하여 Pod의 리소스 요청량에 가장 적합한 노드를 프로비저닝 할 수 있습니다. 또한 Instance FleetAllocation Strategy에 따라 가장 안정적으로 확보 가능한 인스턴스 유형을 선택할 수 있습니다.

\n



\n

Spark 3.1: Graceful Executor Decommissioning

\n

Graceful Executor Decommissioning은 Spark 3.1 버전에 추가된 기능입니다.\n이 기능을 통해 노드가 중단되더라도 최소한의 손실로 Spark 작업이 지속되도록 설정할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Node Termination Handler가 설치되어 있어야 합니다. Node Termination Handler는 클라우드에 따라 다르게 설치할 수 있도록 지원하고 있습니다.

\n

이제 노드가 중단되었을 때 과정을 아래 그림을 통해 확인해보겠습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}}},{"node":{"title":"쿠버네티스에서 GPU 리소스를 효율적으로 활용하는 방법","id":"c5510818-773d-5ec0-8047-6f2c4c31d67f","slug":"gpu-utilization","publishDate":"July 08, 2022","heroImage":{"title":"cover-devops","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&q=50&fm=webp 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&q=50&fm=webp 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&q=50&fm=webp 1080w","sizes":"(min-width: 1080px) 1080px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=270&h=180&fl=progressive&q=50&fm=jpg 270w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=540&h=360&fl=progressive&q=50&fm=jpg 540w,\nhttps://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1080&h=720&fl=progressive&q=50&fm=jpg 1080w","sizes":"(min-width: 1080px) 1080px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,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"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7KaSTt3mdmrYq2ZK1RiJku/dafd981ff3686217ac151b562e8b1412/cover_devops.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

GPU는 강력한 연산 기능을 제공하지만 비용이 많이 들기 때문에 제한된 리소스를 효율적으로 활용하는 것이 중요합니다. 이번 글에서는 NVIDIA GPU의 리소스 공유를 지원하기 위한 방법으로 Time SlicingMIG에 대해 정리해보려 합니다.

\n
\n

GPU 리소스가 낭비되고 있다?

\n

\n \n \n \n

\n

여러 아키텍쳐(암페어, 파스칼 등)로 구성된 GPU들을 모아 쿠버네티스 노드 풀을 구성하고 사용자들은 GPU 리소스를 할당받아 사용하는 환경이라고 가정해보겠습니다. 사용자들은 GPU 할당을 못 받는 상황임에도 실제 GPU 사용량을 측정해보면 생각보다 낮게 유지되고 있는 경우가 있습니다. 워크로드에 따라 필요한 리소스가 다르기 때문입니다.

\n

노트북 환경은 항상 개발을 하는게 아니기 때문에 idle 상태로 대기하는 시간이 많습니다. 작은 배치 사이즈로 운영되는 인퍼런스의 경우, 트래픽에 따라 사용량이 달라질 수 있습니다.\n따라서 이런 상황에서는 항상 리소스를 점유하기 보다 필요할 때 bursting 가능한 방식으로 운영하는 것이 효율적입니다.

\n
apiVersion: v1\nkind: Pod\nmetadata:\n  name: cuda-vector-add\nspec:\n  restartPolicy: OnFailure\n  containers:\n    - name: cuda-vector-add\n      image: \"k8s.gcr.io/cuda-vector-add:v0.1\"\n      resources:\n        limits:\n          nvidia.com/gpu: 1 # GPU 1개 요청하기
\n

쿠버네티스에서는 디바이스 플러그인을 통해 Pod가 GPU 리소스를 요청할 수 있습니다.\n하지만 Pod는 하나 이상의 GPU만 요청할 수 있으며 CPU와 달리 GPU의 일부(fraction)를 요청하는 것은 불가능합니다. 예를 들어 간단한 실험에 최신 버전의 고성능 GPU 1개를 온전히 할당 받는 것은 낭비입니다. NVIDIA 문서에서는 SW/HW 관점에서 GPU 리소스를 효율적으로 사용하기 위해 다양한 방법을 소개합니다. 그 중 Time SlicingMIG에 대해 알아보겠습니다.

\n
\n

Time Slicing

\n

Time Slicing은 GPU의 시간 분할 스케줄러입니다.\n파스칼 아키텍쳐부터 지원하는 compute preemption 기능을 활용한 방법입니다.\n각 컨테이너는 공평하게 timeslice를 할당받게 되지만 전환할 때 context switching 비용이 발생합니다.

\n
kind: ConfigMap\nmetadata:\n  name: time-slicing-config\n  namespace: gpu-operator\ndata:\n  a100-40gb: |-\n    version: v1\n    sharing:\n      timeSlicing:\n        resources:\n        - name: nvidia.com/gpu\n          replicas: 8\n        - name: nvidia.com/mig-1g.5gb\n          replicas: 1\n  tesla-t4: |-\n    version: v1\n    sharing:\n      timeSlicing:\n        resources:\n        - name: nvidia.com/gpu\n          replicas: 4
\n

NVIDIA GPU Operator에서는 위와 같이 ConfigMap을 사용하거나 node label을 통해 설정할 수 있습니다. 설정한 이후에 노드를 확인해보면 아래와 같이 리소스에 값이 추가된 것을 확인할 수 있습니다.

\n
$ kubectl describe node $NODE\n\nstatus:\n  capacity:\n    nvidia.com/gpu: 8\n  allocatable:\n    nvidia.com/gpu: 8
\n

최대 8개 컨테이너까지 timeslice 방식으로 shared GPU를 사용할 수 있다는 것을 의미합니다. 이 방법은 GPU 메모리 limit 설정을 강제하는 것이 아니기 때문에 OOM이 발생할 수도 있습니다. 이를 방지하려면 GPU를 사용하는 컨테이너 수를 모니터링하고 TensorflowPyTorch 같은 프레임워크에서 총 GPU 메모리 제한 설정이 필요합니다.

\n
\n

Multi instance GPU (MIG)

\n

\n \n \n \n

\n

MIG는 A100과 같은 암페어 아키텍처 기반 GPU를 최대 7개의 개별 GPU 인스턴스로 분할해서 사용할 수 있는 기능입니다.\n분할된 인스턴스를 파티션이라고 부르는데, 각 파티션은 물리적으로 격리되어 있기 때문에 안전하게 병렬로 사용할 수 있습니다.

\n

\n \n \n \n

\n

두 방식을 비교해보면 위의 표와 같습니다.\nTime Slicing 방식은 7개 이상의 컨테이너를 사용할 수 있습니다. 따라서 bursting 워크로드에 적합한 방식이라고 볼 수 있습니다. 반면 MIG는 적은 양의 고정된 사용량을 가지는 워크로드에 적합합니다.\nA100은 MIG를 통해 분할하고 그 외의 GPU는 Time Slicing을 사용하는 방식으로 함께 사용할 수 있으니 워크로드에 맞는 방식을 선택하는 것이 중요합니다.

\n
\n

Reference

\n","excerpt":"GPU는 강력한 연산 기능을 제공하지만 비용이 많이 들기 때문에 제한된 리소스를 효율적으로 활용하는 것이 중요합니다. 이번 글에서는 NVIDIA…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":0,"humanPageNumber":1,"skip":0,"limit":7,"numberOfPages":16,"previousPagePath":"","nextPagePath":"/2"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-posts-js","path":"/","result":{"data":{"allContentfulPost":{"edges":[{"node":{"title":"AI를 통해 진화하는 데이터플랫폼 근황","id":"3bc2c838-2281-5852-899f-ba16e366f41b","slug":"llm-dataplatform","publishDate":"January 21, 2024","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다.
\n오늘은 데이터플랫폼 영역에서 AI를 통해 어떤 변화가 나타나고 있는지 정리해보려 합니다.

\n
\n

자연어를 SQL로 변환 (Text2SQL, SQL2Text)

\n

지난 수 년간 클라우드 마이그레이션이 늘어남에 따라 Databrics, Snowflake와 같은 Managed DW 서비스도 함께 성장해왔습니다. Managed DW 서비스가 23년 Summit에 내세운 키워드는 생성형 AI 였습니다. 다양한 기능을 공개했지만 핵심은 Text2SQL, SQL2Text 기술이라고 볼 수 있습니다.

\n

Text2SQL이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다. 쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n

두 번째는 SQL 함수나 자연어 SDK를 추가하는 방식입니다.\n이를 통해 사용자는 개발 과정에도 자연어를 활용할 수 있습니다.\n검색 UI와 달리 사용자의 검증을 거치지 않고 사용할 수 있지만, 일관된 답변을 보장 할 수 없는 관계로 운영 시스템에 직접 연동은 아직 어려울 것 같습니다.

\n

이처럼 다양한 방식을 지원함으로써 사용자는 AI에 쉽게 접근하고 일관된 개발 경험을 가질 수 있습니다.

\n



\n

기술 문서 검색

\n

개발자는 개발 과정에서 문서 검색에 많은 시간을 할애합니다.\nstackoverflow를 통해 검색하는 경우, 내가 사용하고 있는 프레임워크와 버전에 정확히 일치하는 문서를 찾지 못하는 경우도 많았습니다. 이제 데이터플랫폼 내에서 기술 문서와 코드를 기반으로 AI에게 질의할 수 있게 되었습니다.

\n
\n

AWS Amazon Q Assistant

\n

\n \n \n \n

\n

Amazon Q는 AWS에서 출시한 생성형 AI 어시스턴트입니다.\nAWS 콘솔 우측에 추가되어 AWS 클라우드와 관련된 다양한 질의를 수행할 수 있습니다.

\n
\n

GitHub Dosu

\n

\n \n \n \n

\n

오픈소스 영역에서도 생성형 AI를 통해 Issue, Discussion 문의 대응하는 사례가 생기고 있습니다. 위 그림은 LLM 프레임워크인 LangChain에서 사용하는 Dosu 봇 입니다.\n출시 예정인 GitHub Copilot도 이와 유사한 기능을 지원합니다.\n이러한 기능을 통해 사용자는 빠르게 문제를 해결하고 메인테이너는 중요한 의사결정에 집중할 수 있습니다.

\n



\n

데이터 거버넌스 도구

\n

데이터 거버넌스는 정책을 만드는 일보다 운영하는데 더 많은 노력이 들어갑니다.\n거버넌스 내에는 다양한 영역이 있지만 그 중 데이터 디스커버리와 메타데이터 관리에 AI가 활용되고 있습니다.

\n

데이터 디스커버리 영역의 경우, 기존 UI 기반 검색 엔진에 자연어 질의가 추가됩니다.\n이를 통해 앞서 언급한 Text2SQL과 유사한 경험을 제공할 수 있습니다.

\n

다음은 메타데이터 관리 영역입니다. 메타데이터 관리는 데이터 신뢰도를 위해 데이터 생산자와 소비자 모두에게 중요합니다. 하지만 거버넌스 정책이 새로 추가되거나 변경되면 데이터에 대한 오너십을 가지는 도메인 전문가는 이를 항상 인지하기 어렵습니다. 만약 불일치가 발생하면 거버넌스 담당자가 보정하는 작업을 수행하는 경우도 있습니다. 메타데이터 영역의 AI는 거버넌스 정책을 유지하고 메타데이터 입력을 도와주는 역할을 합니다.

\n

\n \n \n \n

\n

Grab의 경우, LLM이 데이터 분류를 위한 태그를 생성하고 거버넌스 담당자가 확인 후 승인하는 프로세스를 개발했습니다. 이를 통해 민감도 분류, 개인정보 컬럼에 PII 태그를 붙이는 등의 거버넌스 정책을 20,000개 이상 데이터에 일관되게 적용할 수 있었습니다.

\n



\n

플랫폼에 AI를 사용하는 이유

\n

\n , 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}}},{"node":{"title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}}},{"node":{"title":"베를린에서 2개월 살아남기","id":"ab300765-6809-53b6-a6cd-952c7dd3c976","slug":"berlin","publishDate":"May 10, 2023","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

우연히 회사에서 좋은 기회를 얻게 되어 독일에서 2개월 근무한 후기
\n베를린 생활부터 유럽의 개발 문화까지 그 동안 겪은 경험을 정리해보려 합니다.

\n
\n

베를린 아파트

\n

숙소가 정말 평이 좋고 실제로 시설도 좋은 아파트인데 엘리베이터가 없었다.\n여기는 그라운드 개념이 있어서 5층이 한국으로 치면 6층인데 다들 아무렇지 않게 걸어올라간다.\n빨래, 건조를 하려면 6층을 왕복 3번 다녀야 하는데 이게 제일 힘들었다.

\n

그리고 모든 문을 열쇠로 열어야 하는데 이게 정말 난감하다.\n최근에 열쇠를 들고 다녀본 기억이 없다보니 두고 다닐 수 있는데\n열쇠를 두고 오면 한화 약 20만원 정도의 벌금을 내야한다.\n열쇠 분실로 인한 사고 방지를 위해 문을 교체해버리기 때문에 저런 비용이 발생한다.\n개인적인 생각으로는 그냥 도어락을 도입하는게 나아보였다 🙏🏻

\n
\n

베를린 교통과 생활

\n

처음 도착하자마자 놀란건 일요일에 모든 마트, 식료품가게가 문을 닫는다는 것이다.\n만약을 위해 베를린 전체에서 세 군데 정도 대형마트만 문을 연다.\n결국 생활용품을 사기 위해 베를린 중앙역까지 갔는데 뉴스에서만 보던 그림을 볼 수 있었다.\n재난상황마냥 줄이 끝까지 이어져있고 마트 내부 물품은 다 털려있었다.\n그래도 독일은 국가가 통제하기 때문에 마트 물가가 정말 저렴하다.

\n

베를린 대중교통으로는 트램, 지하철, 버스를 제일 많이 탄다.\n종일권을 끊으면 모든 교통수단을 무제한으로 탈 수 있다.\n근데 이 보다 더 좋은 교통수단은 자전거다.\n자전거 도로가 너무 잘되어 있어서 대중교통 이용하는 것보다 시간이 빠를 때가 많다.\n도시 간 이동으로는 flixbus와 ICE 고속열차를 많이 이용한다.\n독일의 고속철은 워낙 악명 높아서 취소되는 일이 빈번하다 들었는데\n역시나 당일 오전 출발 5분 전에 ICE 열차 취소를 겪었다.

\n

\n \n \n \n

\n

베를린은 너무 다양한 인종이 살고 있어 전 세계 음식을 다 먹어볼 수 있다.
\n그 중에서 독일 음식은 흰색 소세지와 커리부어스트만 먹어보면 된다. 나머지는 별로였다.\n햄버거의 어원이 함부르크에서 나온 만큼 수제버거도 맛있는 곳이 많다.

\n

\n \n \n \n

\n

때마침 베를린 빛 축제가 진행 중이어서 관광지에서 다양한 야경을 볼 수 있었다.

\n
\n

베를린 개발 문화, 스타트업

\n

유럽도 일하는 방식은 비슷했으나 한국과 비교했을 때 일을 디테일하게 한다고 느꼈다.\n예를 들면 RFC 문서를 정말 자세히 작성하고 오픈소스와 같은 플랫폼 운영 방식을 가지고 있었다.

\n

그리고 정서 상 한국은 정말 겸손한 반면 여기는 잘한 일이 생기면 자랑하고 모두가 축하하는 분위기였다.\n해외 감성으로 네트워킹, 파티도 자주 열린다.\n독일은 아프면 바로 병가를 15일까지 낼 수 있고 휴가가 25일이다.

\n

금요일에는 모두 일찍 퇴근하고 앞에서 맥주를 마신다.\n모든 과정을 경험해보니 절대 한국만큼의 개발 속도가 나올 수는 없겠다는 생각이 들었지만\n반대로 유럽에서 여유롭게 사는 법을 배운 것 같다.

\n

데이터 분야에서는 특별한 차이가 있는데 개인정보에 대한 사람들의 인식이다.\n한국에 있을 때도 많이 들어봤던 GDPR이라는 규정에 대해서도 알게 되었다.\n대부분 사용자들은 개인정보를 절대 서비스에 넘기려하지 않는다.

\n

그러다보니 데이터 기반의 서비스를 만드는 사람들은 정말 난감할 때가 많은데 개인화 추천이 가장 대표적이다.\n일단 사용자를 식별해야 개인화를 할텐데 여기는 개인을 정의하는 것부터가 어려운 문제다.

\n

\n \n \n \n

\n

베를린의 옛 건물을 내부만 리모델링해서 사용한 스타트업 공유 오피스도 방문했었다.
\n자동차 회사가 많은 나라답게 다양한 모빌리티 스타트업을 만날 수 있었다.
\n베를린에는 유럽 내의 스타트업이 많은 편인데 이 도시가 유럽에서 가장 글로벌하기 때문이다.
\n시골로 내려가면 기술에 대한 거부반응을 가진 사람이 많은 반면 베를린은 해외에서 온 이민자가 많다.\n그래서 독일어가 있음에도 영어를 정말 많이 사용한다.

\n

개발자 한정 2년만 근무하면 시민권도 쉽게 얻을 수 있다.\n대신 세금으로 절반을 가져간다 💸
\n이 말을 듣고 한국에서 살아야겠다는 생각이 들었다.

\n

인터넷이 정말 느려서 불편하다고 생각했는데 이 정도면 유럽에서 엄청 빠른 편이라고 한다.
\n하지만 스웨덴 스톡홀름에 가보니 독일 인터넷이 느리다는걸 확신할 수 있었다.

\n
\n

어쨋든 무사히 돌아와서 다행이라는 생각이 들었다.\n영어로 일하고 회의하는 것은 정말 많은 노력이 필요했다.
\n대신 유럽에서 근무하면 주말 동안 주변 국가 여행할 수 있다는 점이 큰 장점이다.
\n요즘 재택이 많아지다보니 해외에서 근무 가능한 회사들도 많이 생기고 있는데
\n만약 유럽에서 살고 싶다면 베를린에서 살아보는 것도 괜찮은 것 같다.

\n
","excerpt":"우연히 회사에서 좋은 기회를 얻게 되어 독일에서…"}}}},{"node":{"title":"MLOps 관련 책, 강의 리뷰 (DMLS, FSDL)","id":"8a70bf4b-6469-516d-9b3f-43f3fa109774","slug":"mlops-dmls-fsdl","publishDate":"September 13, 2022","heroImage":{"title":"cover-personal","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&q=50&fm=webp 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&q=50&fm=webp 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&q=50&fm=webp 1400w","sizes":"(min-width: 1400px) 1400px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=350&h=207&fl=progressive&q=50&fm=jpg 350w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=700&h=413&fl=progressive&q=50&fm=jpg 700w,\nhttps://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1400&h=826&fl=progressive&q=50&fm=jpg 1400w","sizes":"(min-width: 1400px) 1400px, 100vw"}},"layout":"constrained","width":1800,"height":1062,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBv/EABoQAAIDAQEAAAAAAAAAAAAAAAMRAAECEiH/xAAVAQEBAAAAAAAAAAAAAAAAAAABA//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDlAjvdNSiD5p35M8m2PbxaVKSQuy296alalC1EiIF//9k="}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/3ltdJp06NzCExAWz9OF8Ak/d8ca530c80e7c79a7bd7e4c396c0ae00/cover_personal.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

MLOps는 다양한 지식과 컴포넌트를 다루고 있어 따로 공부하다보면 중요한 부분을 놓치고 도구에만 집착하게 되는 경우도 많습니다. 반면 알려진 책이나 강의를 들으니 퍼즐 조각들이 맞춰지는 것처럼 흩어져 있는 지식들이 하나로 정리되는 느낌을 받을 수 있었습니다.

\n

이 글에서는 MLOps 관련 자료 중 유명한 Full Stack Deep Learning 강의와 Designing Machine Learning Systems 책을 리뷰해보려 합니다.\nMLOps에 대해 관심있거나 시작하기 위해 자료를 찾는 분들에게 도움이 될 수 있을 것 같습니다.

\n
\n

Full Stack Deep Learning

\n

\n \n \n \n

\n

FSDL 강의는 MLOps 전반적인 주제를 모두 다루는 온라인 강의입니다.
\n아래 사이트 또는 유튜브에서 최신 강의를 볼 수 있습니다.
\n링크: https://fullstackdeeplearning.com/

\n

좋았던 점
\n매년 강의의 내용이 최신 트렌드를 최대한 반영하기 위해 업데이트 되고 있으며 Lab이라는 실습 과정이 준비되어 있습니다. CoLab 환경에서 실습할 수 있도록 자료가 준비되어 있는데 특히 ML 테스트 챕터의 자료가 좋았습니다.
\n중간에 다양한 오픈소스나 도구들을 소개해주는데 직접 구축하는 경우에도 아이디어를 얻을 수 있어 유용했습니다.

\n

아쉬운 점
\n많은 내용을 다루다보니 특정 주제는 간단하게 이미지나 링크만 공유하고 넘어가는 경우가 있어 설명이 부족한 경우가 있습니다. 제대로 이해하려면 제공되는 학습 자료들을 모두 찾아서 봐야 했습니다.
\n절대 사용할 일이 없을 법한 초기 스타트업들의 SaaS를 소개할때가 있는데 광고를 받은게 아닌가 싶은 생각이 들었습니다.

\n
\n

Designing Machine Learning Systems

\n

\n \n \n \n

\n

DMLS 책은 스탠포드 MLOps 강의로 유명한 Chip Huyen 교수님이 최근에 출판한 책입니다.
\n아직 한글판은 없어 Oreilly Learning 또는 Amazon에서 받아볼 수 있습니다.

\n

좋았던 점
\nFSDL보다 더 구체적인 사례를 들어 전반적인 내용을 이해하기 쉽게 설명한다고 느꼈습니다. 특히 구조적으로 설명해주고 바로 실무에 적용할 수 있도록 여러 가이드라인을 제시해주는 부분이 많습니다.
\n여러 오픈소스나 도구에 대해서도 기능을 구체적으로 다루기보다 어떤 기준으로 선택해야 하는지를 설명합니다. 실제 프로덕션 환경에서 마주치는 문제들을 소개하고 어떻게 해결하는지에 대한 내용을 미국 빅테크 기업들의 사례를 통해 설명하는 부분이 좋았습니다.

\n

아쉬운 점
\nMLOps와 데이터플랫폼의 역할을 완전히 나누어 두고 이건 우리의 역할이 아니라고 단정 짓는 부분들이 있습니다. 맞는 말이지만 어느 정도 같이 보고 싶은 분들에게는 아쉬울 수 있을 것 같습니다.

\n
\n

처음 시작한다면 접근성이 좋은 FSDL 강의를 보고 이후에 DMLS 책을 보는걸 추천드립니다.
\n특히 Data Distribution Shifts and Monitoring 목차는 FSDL을 먼저 확인한 다음 책을 보는게 이해하는데 도움이 되었습니다.

","excerpt":"MLOps…"}}}},{"node":{"title":"Spark on Kubernetes: 스팟 인스턴스 사용을 위한 기능들","id":"1b2de017-d945-522e-be73-569bf48aea40","slug":"spark-on-kubernetes-spot-instance","publishDate":"July 23, 2022","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~90%의 비용을 절감할 수 있습니다.\n하지만 스팟 인스턴스는 가격 입찰, 가용성 등 여러 이유로 중단될 수 있습니다.\n따라서 스팟 인스턴스를 사용한다면 노드가 중단되는 상황에 대비할 수 있어야 합니다.\n이 글에서는 Spark on Kubernetes를 스팟 인스턴스 위에서 안정적으로 운영하기 위해 필요한 설정들을 정리해보려 합니다.

\n



\n

driver는 on-demand에 할당하기

\n

중단된 노드에 있던 driver pod가 종료되는 경우, Spark 작업은 실패하게 됩니다. executor pod가 종료되는 경우, 캐시된 데이터 또는 셔플 파일을 잃게 되지만 새로운 executor를 통해 이를 다시 계산하기 때문에 전체 작업이 실패하지는 않습니다.

\n

위와 같은 이유로 driver는 온디맨드 인스턴스에 할당하는 것이 안전합니다.\n노드 그룹을 분리하고 nodeSelector를 활용한다면 driver는 온디맨드에서, executor는 스팟에서 실행하도록 설정할 수 있습니다.

\n



\n

적절한 인스턴스 유형 선택하기

\n

일부 인스턴스 유형은 해당 시점의 spot market 상황에 따라 안정적으로 확보하지 못할 수도 있습니다. 확보를 못하게 되면 executor는 계속 pending 상태에 머무르게 되고 전체 수행시간도 지연됩니다.

\n

사용량에 비해 크기가 큰 인스턴스 유형을 선택했다면, 여러 executor pod가 하나의 노드에 할당됩니다. 이 때 해당 노드가 중단된다면 여러 executor가 종료되므로 재계산에 더 많은 시간이 소요됩니다.

\n

위와 같은 이유로 적절한 인스턴스 유형을 선택하는 것이 spot kill을 줄이는데 도움이 됩니다.\nKarpenter를 사용한다면, 여러 인스턴스 유형을 지정하여 Pod의 리소스 요청량에 가장 적합한 노드를 프로비저닝 할 수 있습니다. 또한 Instance FleetAllocation Strategy에 따라 가장 안정적으로 확보 가능한 인스턴스 유형을 선택할 수 있습니다.

\n



\n

Spark 3.1: Graceful Executor Decommissioning

\n

Graceful Executor Decommissioning은 Spark 3.1 버전에 추가된 기능입니다.\n이 기능을 통해 노드가 중단되더라도 최소한의 손실로 Spark 작업이 지속되도록 설정할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Node Termination Handler가 설치되어 있어야 합니다. Node Termination Handler는 클라우드에 따라 다르게 설치할 수 있도록 지원하고 있습니다.

\n

이제 노드가 중단되었을 때 과정을 아래 그림을 통해 확인해보겠습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}}}]}},"pageContext":{"basePath":"","paginationPath":"","pageNumber":0,"humanPageNumber":1,"skip":0,"limit":7,"numberOfPages":16,"previousPagePath":"","nextPagePath":"/2"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/llm-dataplatform/page-data.json b/page-data/llm-dataplatform/page-data.json new file mode 100644 index 0000000..b2e3f5a --- /dev/null +++ b/page-data/llm-dataplatform/page-data.json @@ -0,0 +1 @@ +{"componentChunkName":"component---src-templates-post-js","path":"/llm-dataplatform/","result":{"data":{"contentfulPost":{"title":"AI를 통해 진화하는 데이터플랫폼 근황","slug":"llm-dataplatform","metaDescription":null,"publishDate":"January 21, 2024","publishDateISO":"2024-01-21","tags":[{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering"}],"heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다.
\n오늘은 데이터플랫폼 영역에서 AI를 통해 어떤 변화가 나타나고 있는지 정리해보려 합니다.

\n
\n

자연어를 SQL로 변환 (Text2SQL, SQL2Text)

\n

지난 수 년간 클라우드 마이그레이션이 늘어남에 따라 Databrics, Snowflake와 같은 Managed DW 서비스도 함께 성장해왔습니다. Managed DW 서비스가 23년 Summit에 내세운 키워드는 생성형 AI 였습니다. 다양한 기능을 공개했지만 핵심은 Text2SQL, SQL2Text 기술이라고 볼 수 있습니다.

\n

Text2SQL이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다. 쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n

두 번째는 SQL 함수나 자연어 SDK를 추가하는 방식입니다.\n이를 통해 사용자는 개발 과정에도 자연어를 활용할 수 있습니다.\n검색 UI와 달리 사용자의 검증을 거치지 않고 사용할 수 있지만, 일관된 답변을 보장 할 수 없는 관계로 운영 시스템에 직접 연동은 아직 어려울 것 같습니다.

\n

이처럼 다양한 방식을 지원함으로써 사용자는 AI에 쉽게 접근하고 일관된 개발 경험을 가질 수 있습니다.

\n



\n

기술 문서 검색

\n

개발자는 개발 과정에서 문서 검색에 많은 시간을 할애합니다.\nstackoverflow를 통해 검색하는 경우, 내가 사용하고 있는 프레임워크와 버전에 정확히 일치하는 문서를 찾지 못하는 경우도 많았습니다. 이제 데이터플랫폼 내에서 기술 문서와 코드를 기반으로 AI에게 질의할 수 있게 되었습니다.

\n
\n

AWS Amazon Q Assistant

\n

\n \n \n \n

\n

Amazon Q는 AWS에서 출시한 생성형 AI 어시스턴트입니다.\nAWS 콘솔 우측에 추가되어 AWS 클라우드와 관련된 다양한 질의를 수행할 수 있습니다.

\n
\n

GitHub Dosu

\n

\n \n \n \n

\n

오픈소스 영역에서도 생성형 AI를 통해 Issue, Discussion 문의 대응하는 사례가 생기고 있습니다. 위 그림은 LLM 프레임워크인 LangChain에서 사용하는 Dosu 봇 입니다.\n출시 예정인 GitHub Copilot도 이와 유사한 기능을 지원합니다.\n이러한 기능을 통해 사용자는 빠르게 문제를 해결하고 메인테이너는 중요한 의사결정에 집중할 수 있습니다.

\n



\n

데이터 거버넌스 도구

\n

데이터 거버넌스는 정책을 만드는 일보다 운영하는데 더 많은 노력이 들어갑니다.\n거버넌스 내에는 다양한 영역이 있지만 그 중 데이터 디스커버리와 메타데이터 관리에 AI가 활용되고 있습니다.

\n

데이터 디스커버리 영역의 경우, 기존 UI 기반 검색 엔진에 자연어 질의가 추가됩니다.\n이를 통해 앞서 언급한 Text2SQL과 유사한 경험을 제공할 수 있습니다.

\n

다음은 메타데이터 관리 영역입니다. 메타데이터 관리는 데이터 신뢰도를 위해 데이터 생산자와 소비자 모두에게 중요합니다. 하지만 거버넌스 정책이 새로 추가되거나 변경되면 데이터에 대한 오너십을 가지는 도메인 전문가는 이를 항상 인지하기 어렵습니다. 만약 불일치가 발생하면 거버넌스 담당자가 보정하는 작업을 수행하는 경우도 있습니다. 메타데이터 영역의 AI는 거버넌스 정책을 유지하고 메타데이터 입력을 도와주는 역할을 합니다.

\n

\n \n \n \n

\n

Grab의 경우, LLM이 데이터 분류를 위한 태그를 생성하고 거버넌스 담당자가 확인 후 승인하는 프로세스를 개발했습니다. 이를 통해 민감도 분류, 개인정보 컬럼에 PII 태그를 붙이는 등의 거버넌스 정책을 20,000개 이상 데이터에 일관되게 적용할 수 있었습니다.

\n



\n

플랫폼에 AI를 사용하는 이유

\n

\n , 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n , 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":1,"humanPageNumber":2,"skip":6,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering","nextPagePath":"/tag/dataengineering/3"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering/2","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3bc2c838-2281-5852-899f-ba16e366f41b","title":"AI를 통해 진화하는 데이터플랫폼 근황","slug":"llm-dataplatform","publishDate":"January 21, 2024","publishDateISO":"2024-01-21","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다.
\n오늘은 데이터플랫폼 영역에서 AI를 통해 어떤 변화가 나타나고 있는지 정리해보려 합니다.

\n
\n

자연어를 SQL로 변환 (Text2SQL, SQL2Text)

\n

지난 수 년간 클라우드 마이그레이션이 늘어남에 따라 Databrics, Snowflake와 같은 Managed DW 서비스도 함께 성장해왔습니다. Managed DW 서비스가 23년 Summit에 내세운 키워드는 생성형 AI 였습니다. 다양한 기능을 공개했지만 핵심은 Text2SQL, SQL2Text 기술이라고 볼 수 있습니다.

\n

Text2SQL이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다. 쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n

두 번째는 SQL 함수나 자연어 SDK를 추가하는 방식입니다.\n이를 통해 사용자는 개발 과정에도 자연어를 활용할 수 있습니다.\n검색 UI와 달리 사용자의 검증을 거치지 않고 사용할 수 있지만, 일관된 답변을 보장 할 수 없는 관계로 운영 시스템에 직접 연동은 아직 어려울 것 같습니다.

\n

이처럼 다양한 방식을 지원함으로써 사용자는 AI에 쉽게 접근하고 일관된 개발 경험을 가질 수 있습니다.

\n



\n

기술 문서 검색

\n

개발자는 개발 과정에서 문서 검색에 많은 시간을 할애합니다.\nstackoverflow를 통해 검색하는 경우, 내가 사용하고 있는 프레임워크와 버전에 정확히 일치하는 문서를 찾지 못하는 경우도 많았습니다. 이제 데이터플랫폼 내에서 기술 문서와 코드를 기반으로 AI에게 질의할 수 있게 되었습니다.

\n
\n

AWS Amazon Q Assistant

\n

\n \n \n \n

\n

Amazon Q는 AWS에서 출시한 생성형 AI 어시스턴트입니다.\nAWS 콘솔 우측에 추가되어 AWS 클라우드와 관련된 다양한 질의를 수행할 수 있습니다.

\n
\n

GitHub Dosu

\n

\n \n \n \n

\n

오픈소스 영역에서도 생성형 AI를 통해 Issue, Discussion 문의 대응하는 사례가 생기고 있습니다. 위 그림은 LLM 프레임워크인 LangChain에서 사용하는 Dosu 봇 입니다.\n출시 예정인 GitHub Copilot도 이와 유사한 기능을 지원합니다.\n이러한 기능을 통해 사용자는 빠르게 문제를 해결하고 메인테이너는 중요한 의사결정에 집중할 수 있습니다.

\n



\n

데이터 거버넌스 도구

\n

데이터 거버넌스는 정책을 만드는 일보다 운영하는데 더 많은 노력이 들어갑니다.\n거버넌스 내에는 다양한 영역이 있지만 그 중 데이터 디스커버리와 메타데이터 관리에 AI가 활용되고 있습니다.

\n

데이터 디스커버리 영역의 경우, 기존 UI 기반 검색 엔진에 자연어 질의가 추가됩니다.\n이를 통해 앞서 언급한 Text2SQL과 유사한 경험을 제공할 수 있습니다.

\n

다음은 메타데이터 관리 영역입니다. 메타데이터 관리는 데이터 신뢰도를 위해 데이터 생산자와 소비자 모두에게 중요합니다. 하지만 거버넌스 정책이 새로 추가되거나 변경되면 데이터에 대한 오너십을 가지는 도메인 전문가는 이를 항상 인지하기 어렵습니다. 만약 불일치가 발생하면 거버넌스 담당자가 보정하는 작업을 수행하는 경우도 있습니다. 메타데이터 영역의 AI는 거버넌스 정책을 유지하고 메타데이터 입력을 도와주는 역할을 합니다.

\n

\n \n \n \n

\n

Grab의 경우, LLM이 데이터 분류를 위한 태그를 생성하고 거버넌스 담당자가 확인 후 승인하는 프로세스를 개발했습니다. 이를 통해 민감도 분류, 개인정보 컬럼에 PII 태그를 붙이는 등의 거버넌스 정책을 20,000개 이상 데이터에 일관되게 적용할 수 있었습니다.

\n



\n

플랫폼에 AI를 사용하는 이유

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":1,"humanPageNumber":2,"skip":6,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering","nextPagePath":"/tag/dataengineering/3"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/tag/dataengineering/3/page-data.json b/page-data/tag/dataengineering/3/page-data.json index a642980..6479c51 100644 --- a/page-data/tag/dataengineering/3/page-data.json +++ b/page-data/tag/dataengineering/3/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering/3","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3d5aacf4-f336-5c17-a880-4efb995c9b99","title":"AWS에 Hadoop MR 어플리케이션 환경 구축하기","slug":"aws-hadoop","publishDate":"June 13, 2018","publishDateISO":"2018-06-13","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

이번 학기에 하둡 프로그래밍 강의를 들으면서 정말 실습 환경의 개선이 필요하다는 생각이 들었습니다...\n나약한 실습 환경속에서 과제와 기말 프로젝트를 제출해야하는 후배들을 위해 AWS를 추천합니다!

\n
\n

EC2 Amazon Linux2에 기본 환경 구축

\n

AWS에는 EMR이라는 클러스터 서비스가 있지만, 스터디 목적이라면 비용을 생각해서 사용하지 않겠습니다.\nAmazon Linux AMI는 EC2에서 편하게 사용할 수 있도록 지원하고 관리하는 리눅스 이미지입니다.\n만일 학생용 크레딧이 있다면 t2.medium 인스턴스를 추천합니다.

\n

먼저, JAVA JDK와 Hadoop 파일을 받겠습니다. 실습 환경은 자바 7, 하둡 1.2 버전입니다.

\n
$ sudo yum update -y\n$ sudo yum install -y java-1.7.0-openjdk-devel\n$ wget https://archive.apache.org/dist/hadoop/core/hadoop-1.2.1/hadoop-1.2.1.tar.gz\n$ tar xvfz hadoop-1.2.1
\n

그리고 자바 프로젝트를 위해 Maven도 설치해줍니다.

\n
$ wget http://mirror.navercorp.com/apache/maven/maven-3/3.5.3/binaries/apache-maven-3.5.3-bin.tar.gz\n$ tar xvfs apache-maven-3.5.3-bin.tar.gz\n$ mv apache-maven-3.5.3/ apache-maven\n$ sudo vi /etc/profile.d/maven.sh\n\n# Apache Maven Environment Variables\n# MAVEN_HOME for Maven 1 - M2_HOME for Maven 2\n$ export M2_HOME=/home/ec2-user/apache-maven\n$ export PATH=${M2_HOME}/bin:${PATH}\n\n$ chmod +x maven.sh\n$ source /etc/profile.d/maven.sh
\n

정상적으로 설치가 되었다면 아래의 명령어에 대한 결과가 나옵니다.

\n
$ java --version\n$ mvn --version
\n
\n

Hadoop 환경 구축

\n

실습환경은 Pseudo-Distibuted 모드로 진행합니다.\n먼저 Password less SSH Login을 설정해주어야 합니다.\n그리고 편의를 위해 hadoop-1.2.1 폴더에 Symbolic link를 생성하겠습니다.

\n
# ssh login setting\n$ ssh-keygen -t rsa -P \"\"\n$ cat /home/ec2-user/.ssh/id_rsa.pub >> /home/ec2-user/.ssh/authorized_keys\n\n# symbolic link\n$ ln -s hadoop-1.2.1 hadoop
\n

이제 HDFS와 MR 실행을 위해 설정파일을 수정해줍니다.\n먼저 hadoop-env.sh을 열어 JAVA_HOME 환경변수를 지정해줍니다.\n가상분산모드에서는 masters, slaves 파일을 수정할 필요가 없습니다.

\n
$ cd hadoop\n$ vi conf/hadoop-env.sh\n\n# set JAVA_HOME in this file, so that it is correctly defined on\n# remote nodes.\n\n# The java implementation to use. Required.\nexport JAVA_HOME=/usr/lib/jvm/java-1.7.0\n\n# Extra Java CLASSPATH elements.  Optional.\n# export HADOOP_CLASSPATH=
\n

이제 core-site.xml 파일을 아래와 같이 수정해줍니다.\nHDFS 데이터 파일들은 홈 디렉토리의 hadoop-data 폴더에 저장하겠습니다.

\n
$ vi conf/core-site.xml\n\n<configuration>\n    <property>\n        <name>fs.default.name</name>\n        <value>hdfs://localhost:9000</value>\n    </property>\n    <property>\n        <name>hadoop.tmp.dir</name>\n        <value>/home/ec2-user/hadoop-data/</value>\n    </property>\n</configuration>
\n

hdfs-site.xml 파일도 수정해줍니다.\ndfs.replication 프로퍼티는 복제 개수를 의미합니다.\n일반적으로 복제 개수를 3으로 두는 것을 권장하지만,\n실습에서는 Fully-Distributed 모드가 아니기 때문에 1로 설정하겠습니다.

\n
$ vi conf/hdfs-site.xml\n\n<configuration>\n    <property>\n        <name>dfs.replication</name>\n        <value>1</value>\n    </property>\n</configuration>
\n

mapred-site.xml 파일도 수정해줍니다.\nmapred.job.tracker 프로퍼티는 job tracker가 동작하는 서버를 말합니다.

\n
$ vi conf/mapred-site.xml\n\n<configuration>\n    <property>\n        <name>mapred.job.tracker</name>\n        <value>localhost:9001</value>\n    </property>\n</configuration>
\n
\n

Hadoop MR

\n

이제 NameNode를 초기화하고 하둡과 관련된 모든 데몬을 실행합니다.

\n
./bin/hadoop namenode-format\n./bin/start-all.sh
\n

jps를 통해 자바 프로세스가 제대로 실행되었는지 확인할 수 있습니다.

\n
$ jps\n3368 TaskTracker\n2991 DataNode\n3241 JobTracker\n3480 Jps\n2872 NameNode\n3139 SecondaryNameNode
\n

HDFS 웹 인터페이스 주소는 http://localhost:50070 이며,\nMapReduce 웹 인터페이스 주소는 http://localhost:50030 입니다.\n들어가시면 아래와 같은 화면이 나타납니다.

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":2,"humanPageNumber":3,"skip":12,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering/2","nextPagePath":"/tag/dataengineering/4"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering/3","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3bc2c838-2281-5852-899f-ba16e366f41b","title":"AI를 통해 진화하는 데이터플랫폼 근황","slug":"llm-dataplatform","publishDate":"January 21, 2024","publishDateISO":"2024-01-21","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다.
\n오늘은 데이터플랫폼 영역에서 AI를 통해 어떤 변화가 나타나고 있는지 정리해보려 합니다.

\n
\n

자연어를 SQL로 변환 (Text2SQL, SQL2Text)

\n

지난 수 년간 클라우드 마이그레이션이 늘어남에 따라 Databrics, Snowflake와 같은 Managed DW 서비스도 함께 성장해왔습니다. Managed DW 서비스가 23년 Summit에 내세운 키워드는 생성형 AI 였습니다. 다양한 기능을 공개했지만 핵심은 Text2SQL, SQL2Text 기술이라고 볼 수 있습니다.

\n

Text2SQL이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다. 쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n

두 번째는 SQL 함수나 자연어 SDK를 추가하는 방식입니다.\n이를 통해 사용자는 개발 과정에도 자연어를 활용할 수 있습니다.\n검색 UI와 달리 사용자의 검증을 거치지 않고 사용할 수 있지만, 일관된 답변을 보장 할 수 없는 관계로 운영 시스템에 직접 연동은 아직 어려울 것 같습니다.

\n

이처럼 다양한 방식을 지원함으로써 사용자는 AI에 쉽게 접근하고 일관된 개발 경험을 가질 수 있습니다.

\n



\n

기술 문서 검색

\n

개발자는 개발 과정에서 문서 검색에 많은 시간을 할애합니다.\nstackoverflow를 통해 검색하는 경우, 내가 사용하고 있는 프레임워크와 버전에 정확히 일치하는 문서를 찾지 못하는 경우도 많았습니다. 이제 데이터플랫폼 내에서 기술 문서와 코드를 기반으로 AI에게 질의할 수 있게 되었습니다.

\n
\n

AWS Amazon Q Assistant

\n

\n \n \n \n

\n

Amazon Q는 AWS에서 출시한 생성형 AI 어시스턴트입니다.\nAWS 콘솔 우측에 추가되어 AWS 클라우드와 관련된 다양한 질의를 수행할 수 있습니다.

\n
\n

GitHub Dosu

\n

\n \n \n \n

\n

오픈소스 영역에서도 생성형 AI를 통해 Issue, Discussion 문의 대응하는 사례가 생기고 있습니다. 위 그림은 LLM 프레임워크인 LangChain에서 사용하는 Dosu 봇 입니다.\n출시 예정인 GitHub Copilot도 이와 유사한 기능을 지원합니다.\n이러한 기능을 통해 사용자는 빠르게 문제를 해결하고 메인테이너는 중요한 의사결정에 집중할 수 있습니다.

\n



\n

데이터 거버넌스 도구

\n

데이터 거버넌스는 정책을 만드는 일보다 운영하는데 더 많은 노력이 들어갑니다.\n거버넌스 내에는 다양한 영역이 있지만 그 중 데이터 디스커버리와 메타데이터 관리에 AI가 활용되고 있습니다.

\n

데이터 디스커버리 영역의 경우, 기존 UI 기반 검색 엔진에 자연어 질의가 추가됩니다.\n이를 통해 앞서 언급한 Text2SQL과 유사한 경험을 제공할 수 있습니다.

\n

다음은 메타데이터 관리 영역입니다. 메타데이터 관리는 데이터 신뢰도를 위해 데이터 생산자와 소비자 모두에게 중요합니다. 하지만 거버넌스 정책이 새로 추가되거나 변경되면 데이터에 대한 오너십을 가지는 도메인 전문가는 이를 항상 인지하기 어렵습니다. 만약 불일치가 발생하면 거버넌스 담당자가 보정하는 작업을 수행하는 경우도 있습니다. 메타데이터 영역의 AI는 거버넌스 정책을 유지하고 메타데이터 입력을 도와주는 역할을 합니다.

\n

\n \n \n \n

\n

Grab의 경우, LLM이 데이터 분류를 위한 태그를 생성하고 거버넌스 담당자가 확인 후 승인하는 프로세스를 개발했습니다. 이를 통해 민감도 분류, 개인정보 컬럼에 PII 태그를 붙이는 등의 거버넌스 정책을 20,000개 이상 데이터에 일관되게 적용할 수 있었습니다.

\n



\n

플랫폼에 AI를 사용하는 이유

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":2,"humanPageNumber":3,"skip":12,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering/2","nextPagePath":"/tag/dataengineering/4"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/tag/dataengineering/4/page-data.json b/page-data/tag/dataengineering/4/page-data.json index cd35508..f909810 100644 --- a/page-data/tag/dataengineering/4/page-data.json +++ b/page-data/tag/dataengineering/4/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering/4","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3d5aacf4-f336-5c17-a880-4efb995c9b99","title":"AWS에 Hadoop MR 어플리케이션 환경 구축하기","slug":"aws-hadoop","publishDate":"June 13, 2018","publishDateISO":"2018-06-13","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

이번 학기에 하둡 프로그래밍 강의를 들으면서 정말 실습 환경의 개선이 필요하다는 생각이 들었습니다...\n나약한 실습 환경속에서 과제와 기말 프로젝트를 제출해야하는 후배들을 위해 AWS를 추천합니다!

\n
\n

EC2 Amazon Linux2에 기본 환경 구축

\n

AWS에는 EMR이라는 클러스터 서비스가 있지만, 스터디 목적이라면 비용을 생각해서 사용하지 않겠습니다.\nAmazon Linux AMI는 EC2에서 편하게 사용할 수 있도록 지원하고 관리하는 리눅스 이미지입니다.\n만일 학생용 크레딧이 있다면 t2.medium 인스턴스를 추천합니다.

\n

먼저, JAVA JDK와 Hadoop 파일을 받겠습니다. 실습 환경은 자바 7, 하둡 1.2 버전입니다.

\n
$ sudo yum update -y\n$ sudo yum install -y java-1.7.0-openjdk-devel\n$ wget https://archive.apache.org/dist/hadoop/core/hadoop-1.2.1/hadoop-1.2.1.tar.gz\n$ tar xvfz hadoop-1.2.1
\n

그리고 자바 프로젝트를 위해 Maven도 설치해줍니다.

\n
$ wget http://mirror.navercorp.com/apache/maven/maven-3/3.5.3/binaries/apache-maven-3.5.3-bin.tar.gz\n$ tar xvfs apache-maven-3.5.3-bin.tar.gz\n$ mv apache-maven-3.5.3/ apache-maven\n$ sudo vi /etc/profile.d/maven.sh\n\n# Apache Maven Environment Variables\n# MAVEN_HOME for Maven 1 - M2_HOME for Maven 2\n$ export M2_HOME=/home/ec2-user/apache-maven\n$ export PATH=${M2_HOME}/bin:${PATH}\n\n$ chmod +x maven.sh\n$ source /etc/profile.d/maven.sh
\n

정상적으로 설치가 되었다면 아래의 명령어에 대한 결과가 나옵니다.

\n
$ java --version\n$ mvn --version
\n
\n

Hadoop 환경 구축

\n

실습환경은 Pseudo-Distibuted 모드로 진행합니다.\n먼저 Password less SSH Login을 설정해주어야 합니다.\n그리고 편의를 위해 hadoop-1.2.1 폴더에 Symbolic link를 생성하겠습니다.

\n
# ssh login setting\n$ ssh-keygen -t rsa -P \"\"\n$ cat /home/ec2-user/.ssh/id_rsa.pub >> /home/ec2-user/.ssh/authorized_keys\n\n# symbolic link\n$ ln -s hadoop-1.2.1 hadoop
\n

이제 HDFS와 MR 실행을 위해 설정파일을 수정해줍니다.\n먼저 hadoop-env.sh을 열어 JAVA_HOME 환경변수를 지정해줍니다.\n가상분산모드에서는 masters, slaves 파일을 수정할 필요가 없습니다.

\n
$ cd hadoop\n$ vi conf/hadoop-env.sh\n\n# set JAVA_HOME in this file, so that it is correctly defined on\n# remote nodes.\n\n# The java implementation to use. Required.\nexport JAVA_HOME=/usr/lib/jvm/java-1.7.0\n\n# Extra Java CLASSPATH elements.  Optional.\n# export HADOOP_CLASSPATH=
\n

이제 core-site.xml 파일을 아래와 같이 수정해줍니다.\nHDFS 데이터 파일들은 홈 디렉토리의 hadoop-data 폴더에 저장하겠습니다.

\n
$ vi conf/core-site.xml\n\n<configuration>\n    <property>\n        <name>fs.default.name</name>\n        <value>hdfs://localhost:9000</value>\n    </property>\n    <property>\n        <name>hadoop.tmp.dir</name>\n        <value>/home/ec2-user/hadoop-data/</value>\n    </property>\n</configuration>
\n

hdfs-site.xml 파일도 수정해줍니다.\ndfs.replication 프로퍼티는 복제 개수를 의미합니다.\n일반적으로 복제 개수를 3으로 두는 것을 권장하지만,\n실습에서는 Fully-Distributed 모드가 아니기 때문에 1로 설정하겠습니다.

\n
$ vi conf/hdfs-site.xml\n\n<configuration>\n    <property>\n        <name>dfs.replication</name>\n        <value>1</value>\n    </property>\n</configuration>
\n

mapred-site.xml 파일도 수정해줍니다.\nmapred.job.tracker 프로퍼티는 job tracker가 동작하는 서버를 말합니다.

\n
$ vi conf/mapred-site.xml\n\n<configuration>\n    <property>\n        <name>mapred.job.tracker</name>\n        <value>localhost:9001</value>\n    </property>\n</configuration>
\n
\n

Hadoop MR

\n

이제 NameNode를 초기화하고 하둡과 관련된 모든 데몬을 실행합니다.

\n
./bin/hadoop namenode-format\n./bin/start-all.sh
\n

jps를 통해 자바 프로세스가 제대로 실행되었는지 확인할 수 있습니다.

\n
$ jps\n3368 TaskTracker\n2991 DataNode\n3241 JobTracker\n3480 Jps\n2872 NameNode\n3139 SecondaryNameNode
\n

HDFS 웹 인터페이스 주소는 http://localhost:50070 이며,\nMapReduce 웹 인터페이스 주소는 http://localhost:50030 입니다.\n들어가시면 아래와 같은 화면이 나타납니다.

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":3,"humanPageNumber":4,"skip":18,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering/3","nextPagePath":"/tag/dataengineering/5"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering/4","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3bc2c838-2281-5852-899f-ba16e366f41b","title":"AI를 통해 진화하는 데이터플랫폼 근황","slug":"llm-dataplatform","publishDate":"January 21, 2024","publishDateISO":"2024-01-21","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다.
\n오늘은 데이터플랫폼 영역에서 AI를 통해 어떤 변화가 나타나고 있는지 정리해보려 합니다.

\n
\n

자연어를 SQL로 변환 (Text2SQL, SQL2Text)

\n

지난 수 년간 클라우드 마이그레이션이 늘어남에 따라 Databrics, Snowflake와 같은 Managed DW 서비스도 함께 성장해왔습니다. Managed DW 서비스가 23년 Summit에 내세운 키워드는 생성형 AI 였습니다. 다양한 기능을 공개했지만 핵심은 Text2SQL, SQL2Text 기술이라고 볼 수 있습니다.

\n

Text2SQL이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다. 쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n

두 번째는 SQL 함수나 자연어 SDK를 추가하는 방식입니다.\n이를 통해 사용자는 개발 과정에도 자연어를 활용할 수 있습니다.\n검색 UI와 달리 사용자의 검증을 거치지 않고 사용할 수 있지만, 일관된 답변을 보장 할 수 없는 관계로 운영 시스템에 직접 연동은 아직 어려울 것 같습니다.

\n

이처럼 다양한 방식을 지원함으로써 사용자는 AI에 쉽게 접근하고 일관된 개발 경험을 가질 수 있습니다.

\n



\n

기술 문서 검색

\n

개발자는 개발 과정에서 문서 검색에 많은 시간을 할애합니다.\nstackoverflow를 통해 검색하는 경우, 내가 사용하고 있는 프레임워크와 버전에 정확히 일치하는 문서를 찾지 못하는 경우도 많았습니다. 이제 데이터플랫폼 내에서 기술 문서와 코드를 기반으로 AI에게 질의할 수 있게 되었습니다.

\n
\n

AWS Amazon Q Assistant

\n

\n \n \n \n

\n

Amazon Q는 AWS에서 출시한 생성형 AI 어시스턴트입니다.\nAWS 콘솔 우측에 추가되어 AWS 클라우드와 관련된 다양한 질의를 수행할 수 있습니다.

\n
\n

GitHub Dosu

\n

\n \n \n \n

\n

오픈소스 영역에서도 생성형 AI를 통해 Issue, Discussion 문의 대응하는 사례가 생기고 있습니다. 위 그림은 LLM 프레임워크인 LangChain에서 사용하는 Dosu 봇 입니다.\n출시 예정인 GitHub Copilot도 이와 유사한 기능을 지원합니다.\n이러한 기능을 통해 사용자는 빠르게 문제를 해결하고 메인테이너는 중요한 의사결정에 집중할 수 있습니다.

\n



\n

데이터 거버넌스 도구

\n

데이터 거버넌스는 정책을 만드는 일보다 운영하는데 더 많은 노력이 들어갑니다.\n거버넌스 내에는 다양한 영역이 있지만 그 중 데이터 디스커버리와 메타데이터 관리에 AI가 활용되고 있습니다.

\n

데이터 디스커버리 영역의 경우, 기존 UI 기반 검색 엔진에 자연어 질의가 추가됩니다.\n이를 통해 앞서 언급한 Text2SQL과 유사한 경험을 제공할 수 있습니다.

\n

다음은 메타데이터 관리 영역입니다. 메타데이터 관리는 데이터 신뢰도를 위해 데이터 생산자와 소비자 모두에게 중요합니다. 하지만 거버넌스 정책이 새로 추가되거나 변경되면 데이터에 대한 오너십을 가지는 도메인 전문가는 이를 항상 인지하기 어렵습니다. 만약 불일치가 발생하면 거버넌스 담당자가 보정하는 작업을 수행하는 경우도 있습니다. 메타데이터 영역의 AI는 거버넌스 정책을 유지하고 메타데이터 입력을 도와주는 역할을 합니다.

\n

\n \n \n \n

\n

Grab의 경우, LLM이 데이터 분류를 위한 태그를 생성하고 거버넌스 담당자가 확인 후 승인하는 프로세스를 개발했습니다. 이를 통해 민감도 분류, 개인정보 컬럼에 PII 태그를 붙이는 등의 거버넌스 정책을 20,000개 이상 데이터에 일관되게 적용할 수 있었습니다.

\n



\n

플랫폼에 AI를 사용하는 이유

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":3,"humanPageNumber":4,"skip":18,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering/3","nextPagePath":"/tag/dataengineering/5"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/tag/dataengineering/5/page-data.json b/page-data/tag/dataengineering/5/page-data.json index 06a3205..fc92b4d 100644 --- a/page-data/tag/dataengineering/5/page-data.json +++ b/page-data/tag/dataengineering/5/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering/5","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3d5aacf4-f336-5c17-a880-4efb995c9b99","title":"AWS에 Hadoop MR 어플리케이션 환경 구축하기","slug":"aws-hadoop","publishDate":"June 13, 2018","publishDateISO":"2018-06-13","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

이번 학기에 하둡 프로그래밍 강의를 들으면서 정말 실습 환경의 개선이 필요하다는 생각이 들었습니다...\n나약한 실습 환경속에서 과제와 기말 프로젝트를 제출해야하는 후배들을 위해 AWS를 추천합니다!

\n
\n

EC2 Amazon Linux2에 기본 환경 구축

\n

AWS에는 EMR이라는 클러스터 서비스가 있지만, 스터디 목적이라면 비용을 생각해서 사용하지 않겠습니다.\nAmazon Linux AMI는 EC2에서 편하게 사용할 수 있도록 지원하고 관리하는 리눅스 이미지입니다.\n만일 학생용 크레딧이 있다면 t2.medium 인스턴스를 추천합니다.

\n

먼저, JAVA JDK와 Hadoop 파일을 받겠습니다. 실습 환경은 자바 7, 하둡 1.2 버전입니다.

\n
$ sudo yum update -y\n$ sudo yum install -y java-1.7.0-openjdk-devel\n$ wget https://archive.apache.org/dist/hadoop/core/hadoop-1.2.1/hadoop-1.2.1.tar.gz\n$ tar xvfz hadoop-1.2.1
\n

그리고 자바 프로젝트를 위해 Maven도 설치해줍니다.

\n
$ wget http://mirror.navercorp.com/apache/maven/maven-3/3.5.3/binaries/apache-maven-3.5.3-bin.tar.gz\n$ tar xvfs apache-maven-3.5.3-bin.tar.gz\n$ mv apache-maven-3.5.3/ apache-maven\n$ sudo vi /etc/profile.d/maven.sh\n\n# Apache Maven Environment Variables\n# MAVEN_HOME for Maven 1 - M2_HOME for Maven 2\n$ export M2_HOME=/home/ec2-user/apache-maven\n$ export PATH=${M2_HOME}/bin:${PATH}\n\n$ chmod +x maven.sh\n$ source /etc/profile.d/maven.sh
\n

정상적으로 설치가 되었다면 아래의 명령어에 대한 결과가 나옵니다.

\n
$ java --version\n$ mvn --version
\n
\n

Hadoop 환경 구축

\n

실습환경은 Pseudo-Distibuted 모드로 진행합니다.\n먼저 Password less SSH Login을 설정해주어야 합니다.\n그리고 편의를 위해 hadoop-1.2.1 폴더에 Symbolic link를 생성하겠습니다.

\n
# ssh login setting\n$ ssh-keygen -t rsa -P \"\"\n$ cat /home/ec2-user/.ssh/id_rsa.pub >> /home/ec2-user/.ssh/authorized_keys\n\n# symbolic link\n$ ln -s hadoop-1.2.1 hadoop
\n

이제 HDFS와 MR 실행을 위해 설정파일을 수정해줍니다.\n먼저 hadoop-env.sh을 열어 JAVA_HOME 환경변수를 지정해줍니다.\n가상분산모드에서는 masters, slaves 파일을 수정할 필요가 없습니다.

\n
$ cd hadoop\n$ vi conf/hadoop-env.sh\n\n# set JAVA_HOME in this file, so that it is correctly defined on\n# remote nodes.\n\n# The java implementation to use. Required.\nexport JAVA_HOME=/usr/lib/jvm/java-1.7.0\n\n# Extra Java CLASSPATH elements.  Optional.\n# export HADOOP_CLASSPATH=
\n

이제 core-site.xml 파일을 아래와 같이 수정해줍니다.\nHDFS 데이터 파일들은 홈 디렉토리의 hadoop-data 폴더에 저장하겠습니다.

\n
$ vi conf/core-site.xml\n\n<configuration>\n    <property>\n        <name>fs.default.name</name>\n        <value>hdfs://localhost:9000</value>\n    </property>\n    <property>\n        <name>hadoop.tmp.dir</name>\n        <value>/home/ec2-user/hadoop-data/</value>\n    </property>\n</configuration>
\n

hdfs-site.xml 파일도 수정해줍니다.\ndfs.replication 프로퍼티는 복제 개수를 의미합니다.\n일반적으로 복제 개수를 3으로 두는 것을 권장하지만,\n실습에서는 Fully-Distributed 모드가 아니기 때문에 1로 설정하겠습니다.

\n
$ vi conf/hdfs-site.xml\n\n<configuration>\n    <property>\n        <name>dfs.replication</name>\n        <value>1</value>\n    </property>\n</configuration>
\n

mapred-site.xml 파일도 수정해줍니다.\nmapred.job.tracker 프로퍼티는 job tracker가 동작하는 서버를 말합니다.

\n
$ vi conf/mapred-site.xml\n\n<configuration>\n    <property>\n        <name>mapred.job.tracker</name>\n        <value>localhost:9001</value>\n    </property>\n</configuration>
\n
\n

Hadoop MR

\n

이제 NameNode를 초기화하고 하둡과 관련된 모든 데몬을 실행합니다.

\n
./bin/hadoop namenode-format\n./bin/start-all.sh
\n

jps를 통해 자바 프로세스가 제대로 실행되었는지 확인할 수 있습니다.

\n
$ jps\n3368 TaskTracker\n2991 DataNode\n3241 JobTracker\n3480 Jps\n2872 NameNode\n3139 SecondaryNameNode
\n

HDFS 웹 인터페이스 주소는 http://localhost:50070 이며,\nMapReduce 웹 인터페이스 주소는 http://localhost:50030 입니다.\n들어가시면 아래와 같은 화면이 나타납니다.

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":4,"humanPageNumber":5,"skip":24,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering/4","nextPagePath":"/tag/dataengineering/6"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering/5","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3bc2c838-2281-5852-899f-ba16e366f41b","title":"AI를 통해 진화하는 데이터플랫폼 근황","slug":"llm-dataplatform","publishDate":"January 21, 2024","publishDateISO":"2024-01-21","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다.
\n오늘은 데이터플랫폼 영역에서 AI를 통해 어떤 변화가 나타나고 있는지 정리해보려 합니다.

\n
\n

자연어를 SQL로 변환 (Text2SQL, SQL2Text)

\n

지난 수 년간 클라우드 마이그레이션이 늘어남에 따라 Databrics, Snowflake와 같은 Managed DW 서비스도 함께 성장해왔습니다. Managed DW 서비스가 23년 Summit에 내세운 키워드는 생성형 AI 였습니다. 다양한 기능을 공개했지만 핵심은 Text2SQL, SQL2Text 기술이라고 볼 수 있습니다.

\n

Text2SQL이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다. 쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n

두 번째는 SQL 함수나 자연어 SDK를 추가하는 방식입니다.\n이를 통해 사용자는 개발 과정에도 자연어를 활용할 수 있습니다.\n검색 UI와 달리 사용자의 검증을 거치지 않고 사용할 수 있지만, 일관된 답변을 보장 할 수 없는 관계로 운영 시스템에 직접 연동은 아직 어려울 것 같습니다.

\n

이처럼 다양한 방식을 지원함으로써 사용자는 AI에 쉽게 접근하고 일관된 개발 경험을 가질 수 있습니다.

\n



\n

기술 문서 검색

\n

개발자는 개발 과정에서 문서 검색에 많은 시간을 할애합니다.\nstackoverflow를 통해 검색하는 경우, 내가 사용하고 있는 프레임워크와 버전에 정확히 일치하는 문서를 찾지 못하는 경우도 많았습니다. 이제 데이터플랫폼 내에서 기술 문서와 코드를 기반으로 AI에게 질의할 수 있게 되었습니다.

\n
\n

AWS Amazon Q Assistant

\n

\n \n \n \n

\n

Amazon Q는 AWS에서 출시한 생성형 AI 어시스턴트입니다.\nAWS 콘솔 우측에 추가되어 AWS 클라우드와 관련된 다양한 질의를 수행할 수 있습니다.

\n
\n

GitHub Dosu

\n

\n \n \n \n

\n

오픈소스 영역에서도 생성형 AI를 통해 Issue, Discussion 문의 대응하는 사례가 생기고 있습니다. 위 그림은 LLM 프레임워크인 LangChain에서 사용하는 Dosu 봇 입니다.\n출시 예정인 GitHub Copilot도 이와 유사한 기능을 지원합니다.\n이러한 기능을 통해 사용자는 빠르게 문제를 해결하고 메인테이너는 중요한 의사결정에 집중할 수 있습니다.

\n



\n

데이터 거버넌스 도구

\n

데이터 거버넌스는 정책을 만드는 일보다 운영하는데 더 많은 노력이 들어갑니다.\n거버넌스 내에는 다양한 영역이 있지만 그 중 데이터 디스커버리와 메타데이터 관리에 AI가 활용되고 있습니다.

\n

데이터 디스커버리 영역의 경우, 기존 UI 기반 검색 엔진에 자연어 질의가 추가됩니다.\n이를 통해 앞서 언급한 Text2SQL과 유사한 경험을 제공할 수 있습니다.

\n

다음은 메타데이터 관리 영역입니다. 메타데이터 관리는 데이터 신뢰도를 위해 데이터 생산자와 소비자 모두에게 중요합니다. 하지만 거버넌스 정책이 새로 추가되거나 변경되면 데이터에 대한 오너십을 가지는 도메인 전문가는 이를 항상 인지하기 어렵습니다. 만약 불일치가 발생하면 거버넌스 담당자가 보정하는 작업을 수행하는 경우도 있습니다. 메타데이터 영역의 AI는 거버넌스 정책을 유지하고 메타데이터 입력을 도와주는 역할을 합니다.

\n

\n \n \n \n

\n

Grab의 경우, LLM이 데이터 분류를 위한 태그를 생성하고 거버넌스 담당자가 확인 후 승인하는 프로세스를 개발했습니다. 이를 통해 민감도 분류, 개인정보 컬럼에 PII 태그를 붙이는 등의 거버넌스 정책을 20,000개 이상 데이터에 일관되게 적용할 수 있었습니다.

\n



\n

플랫폼에 AI를 사용하는 이유

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":4,"humanPageNumber":5,"skip":24,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering/4","nextPagePath":"/tag/dataengineering/6"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/tag/dataengineering/6/page-data.json b/page-data/tag/dataengineering/6/page-data.json index 26188e6..74fbf3c 100644 --- a/page-data/tag/dataengineering/6/page-data.json +++ b/page-data/tag/dataengineering/6/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering/6","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3d5aacf4-f336-5c17-a880-4efb995c9b99","title":"AWS에 Hadoop MR 어플리케이션 환경 구축하기","slug":"aws-hadoop","publishDate":"June 13, 2018","publishDateISO":"2018-06-13","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

이번 학기에 하둡 프로그래밍 강의를 들으면서 정말 실습 환경의 개선이 필요하다는 생각이 들었습니다...\n나약한 실습 환경속에서 과제와 기말 프로젝트를 제출해야하는 후배들을 위해 AWS를 추천합니다!

\n
\n

EC2 Amazon Linux2에 기본 환경 구축

\n

AWS에는 EMR이라는 클러스터 서비스가 있지만, 스터디 목적이라면 비용을 생각해서 사용하지 않겠습니다.\nAmazon Linux AMI는 EC2에서 편하게 사용할 수 있도록 지원하고 관리하는 리눅스 이미지입니다.\n만일 학생용 크레딧이 있다면 t2.medium 인스턴스를 추천합니다.

\n

먼저, JAVA JDK와 Hadoop 파일을 받겠습니다. 실습 환경은 자바 7, 하둡 1.2 버전입니다.

\n
$ sudo yum update -y\n$ sudo yum install -y java-1.7.0-openjdk-devel\n$ wget https://archive.apache.org/dist/hadoop/core/hadoop-1.2.1/hadoop-1.2.1.tar.gz\n$ tar xvfz hadoop-1.2.1
\n

그리고 자바 프로젝트를 위해 Maven도 설치해줍니다.

\n
$ wget http://mirror.navercorp.com/apache/maven/maven-3/3.5.3/binaries/apache-maven-3.5.3-bin.tar.gz\n$ tar xvfs apache-maven-3.5.3-bin.tar.gz\n$ mv apache-maven-3.5.3/ apache-maven\n$ sudo vi /etc/profile.d/maven.sh\n\n# Apache Maven Environment Variables\n# MAVEN_HOME for Maven 1 - M2_HOME for Maven 2\n$ export M2_HOME=/home/ec2-user/apache-maven\n$ export PATH=${M2_HOME}/bin:${PATH}\n\n$ chmod +x maven.sh\n$ source /etc/profile.d/maven.sh
\n

정상적으로 설치가 되었다면 아래의 명령어에 대한 결과가 나옵니다.

\n
$ java --version\n$ mvn --version
\n
\n

Hadoop 환경 구축

\n

실습환경은 Pseudo-Distibuted 모드로 진행합니다.\n먼저 Password less SSH Login을 설정해주어야 합니다.\n그리고 편의를 위해 hadoop-1.2.1 폴더에 Symbolic link를 생성하겠습니다.

\n
# ssh login setting\n$ ssh-keygen -t rsa -P \"\"\n$ cat /home/ec2-user/.ssh/id_rsa.pub >> /home/ec2-user/.ssh/authorized_keys\n\n# symbolic link\n$ ln -s hadoop-1.2.1 hadoop
\n

이제 HDFS와 MR 실행을 위해 설정파일을 수정해줍니다.\n먼저 hadoop-env.sh을 열어 JAVA_HOME 환경변수를 지정해줍니다.\n가상분산모드에서는 masters, slaves 파일을 수정할 필요가 없습니다.

\n
$ cd hadoop\n$ vi conf/hadoop-env.sh\n\n# set JAVA_HOME in this file, so that it is correctly defined on\n# remote nodes.\n\n# The java implementation to use. Required.\nexport JAVA_HOME=/usr/lib/jvm/java-1.7.0\n\n# Extra Java CLASSPATH elements.  Optional.\n# export HADOOP_CLASSPATH=
\n

이제 core-site.xml 파일을 아래와 같이 수정해줍니다.\nHDFS 데이터 파일들은 홈 디렉토리의 hadoop-data 폴더에 저장하겠습니다.

\n
$ vi conf/core-site.xml\n\n<configuration>\n    <property>\n        <name>fs.default.name</name>\n        <value>hdfs://localhost:9000</value>\n    </property>\n    <property>\n        <name>hadoop.tmp.dir</name>\n        <value>/home/ec2-user/hadoop-data/</value>\n    </property>\n</configuration>
\n

hdfs-site.xml 파일도 수정해줍니다.\ndfs.replication 프로퍼티는 복제 개수를 의미합니다.\n일반적으로 복제 개수를 3으로 두는 것을 권장하지만,\n실습에서는 Fully-Distributed 모드가 아니기 때문에 1로 설정하겠습니다.

\n
$ vi conf/hdfs-site.xml\n\n<configuration>\n    <property>\n        <name>dfs.replication</name>\n        <value>1</value>\n    </property>\n</configuration>
\n

mapred-site.xml 파일도 수정해줍니다.\nmapred.job.tracker 프로퍼티는 job tracker가 동작하는 서버를 말합니다.

\n
$ vi conf/mapred-site.xml\n\n<configuration>\n    <property>\n        <name>mapred.job.tracker</name>\n        <value>localhost:9001</value>\n    </property>\n</configuration>
\n
\n

Hadoop MR

\n

이제 NameNode를 초기화하고 하둡과 관련된 모든 데몬을 실행합니다.

\n
./bin/hadoop namenode-format\n./bin/start-all.sh
\n

jps를 통해 자바 프로세스가 제대로 실행되었는지 확인할 수 있습니다.

\n
$ jps\n3368 TaskTracker\n2991 DataNode\n3241 JobTracker\n3480 Jps\n2872 NameNode\n3139 SecondaryNameNode
\n

HDFS 웹 인터페이스 주소는 http://localhost:50070 이며,\nMapReduce 웹 인터페이스 주소는 http://localhost:50030 입니다.\n들어가시면 아래와 같은 화면이 나타납니다.

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":5,"humanPageNumber":6,"skip":30,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering/5","nextPagePath":"/tag/dataengineering/7"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering/6","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3bc2c838-2281-5852-899f-ba16e366f41b","title":"AI를 통해 진화하는 데이터플랫폼 근황","slug":"llm-dataplatform","publishDate":"January 21, 2024","publishDateISO":"2024-01-21","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다.
\n오늘은 데이터플랫폼 영역에서 AI를 통해 어떤 변화가 나타나고 있는지 정리해보려 합니다.

\n
\n

자연어를 SQL로 변환 (Text2SQL, SQL2Text)

\n

지난 수 년간 클라우드 마이그레이션이 늘어남에 따라 Databrics, Snowflake와 같은 Managed DW 서비스도 함께 성장해왔습니다. Managed DW 서비스가 23년 Summit에 내세운 키워드는 생성형 AI 였습니다. 다양한 기능을 공개했지만 핵심은 Text2SQL, SQL2Text 기술이라고 볼 수 있습니다.

\n

Text2SQL이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다. 쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n

두 번째는 SQL 함수나 자연어 SDK를 추가하는 방식입니다.\n이를 통해 사용자는 개발 과정에도 자연어를 활용할 수 있습니다.\n검색 UI와 달리 사용자의 검증을 거치지 않고 사용할 수 있지만, 일관된 답변을 보장 할 수 없는 관계로 운영 시스템에 직접 연동은 아직 어려울 것 같습니다.

\n

이처럼 다양한 방식을 지원함으로써 사용자는 AI에 쉽게 접근하고 일관된 개발 경험을 가질 수 있습니다.

\n



\n

기술 문서 검색

\n

개발자는 개발 과정에서 문서 검색에 많은 시간을 할애합니다.\nstackoverflow를 통해 검색하는 경우, 내가 사용하고 있는 프레임워크와 버전에 정확히 일치하는 문서를 찾지 못하는 경우도 많았습니다. 이제 데이터플랫폼 내에서 기술 문서와 코드를 기반으로 AI에게 질의할 수 있게 되었습니다.

\n
\n

AWS Amazon Q Assistant

\n

\n \n \n \n

\n

Amazon Q는 AWS에서 출시한 생성형 AI 어시스턴트입니다.\nAWS 콘솔 우측에 추가되어 AWS 클라우드와 관련된 다양한 질의를 수행할 수 있습니다.

\n
\n

GitHub Dosu

\n

\n \n \n \n

\n

오픈소스 영역에서도 생성형 AI를 통해 Issue, Discussion 문의 대응하는 사례가 생기고 있습니다. 위 그림은 LLM 프레임워크인 LangChain에서 사용하는 Dosu 봇 입니다.\n출시 예정인 GitHub Copilot도 이와 유사한 기능을 지원합니다.\n이러한 기능을 통해 사용자는 빠르게 문제를 해결하고 메인테이너는 중요한 의사결정에 집중할 수 있습니다.

\n



\n

데이터 거버넌스 도구

\n

데이터 거버넌스는 정책을 만드는 일보다 운영하는데 더 많은 노력이 들어갑니다.\n거버넌스 내에는 다양한 영역이 있지만 그 중 데이터 디스커버리와 메타데이터 관리에 AI가 활용되고 있습니다.

\n

데이터 디스커버리 영역의 경우, 기존 UI 기반 검색 엔진에 자연어 질의가 추가됩니다.\n이를 통해 앞서 언급한 Text2SQL과 유사한 경험을 제공할 수 있습니다.

\n

다음은 메타데이터 관리 영역입니다. 메타데이터 관리는 데이터 신뢰도를 위해 데이터 생산자와 소비자 모두에게 중요합니다. 하지만 거버넌스 정책이 새로 추가되거나 변경되면 데이터에 대한 오너십을 가지는 도메인 전문가는 이를 항상 인지하기 어렵습니다. 만약 불일치가 발생하면 거버넌스 담당자가 보정하는 작업을 수행하는 경우도 있습니다. 메타데이터 영역의 AI는 거버넌스 정책을 유지하고 메타데이터 입력을 도와주는 역할을 합니다.

\n

\n \n \n \n

\n

Grab의 경우, LLM이 데이터 분류를 위한 태그를 생성하고 거버넌스 담당자가 확인 후 승인하는 프로세스를 개발했습니다. 이를 통해 민감도 분류, 개인정보 컬럼에 PII 태그를 붙이는 등의 거버넌스 정책을 20,000개 이상 데이터에 일관되게 적용할 수 있었습니다.

\n



\n

플랫폼에 AI를 사용하는 이유

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":5,"humanPageNumber":6,"skip":30,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering/5","nextPagePath":"/tag/dataengineering/7"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/tag/dataengineering/7/page-data.json b/page-data/tag/dataengineering/7/page-data.json index db51270..16e0dac 100644 --- a/page-data/tag/dataengineering/7/page-data.json +++ b/page-data/tag/dataengineering/7/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering/7","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3d5aacf4-f336-5c17-a880-4efb995c9b99","title":"AWS에 Hadoop MR 어플리케이션 환경 구축하기","slug":"aws-hadoop","publishDate":"June 13, 2018","publishDateISO":"2018-06-13","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

이번 학기에 하둡 프로그래밍 강의를 들으면서 정말 실습 환경의 개선이 필요하다는 생각이 들었습니다...\n나약한 실습 환경속에서 과제와 기말 프로젝트를 제출해야하는 후배들을 위해 AWS를 추천합니다!

\n
\n

EC2 Amazon Linux2에 기본 환경 구축

\n

AWS에는 EMR이라는 클러스터 서비스가 있지만, 스터디 목적이라면 비용을 생각해서 사용하지 않겠습니다.\nAmazon Linux AMI는 EC2에서 편하게 사용할 수 있도록 지원하고 관리하는 리눅스 이미지입니다.\n만일 학생용 크레딧이 있다면 t2.medium 인스턴스를 추천합니다.

\n

먼저, JAVA JDK와 Hadoop 파일을 받겠습니다. 실습 환경은 자바 7, 하둡 1.2 버전입니다.

\n
$ sudo yum update -y\n$ sudo yum install -y java-1.7.0-openjdk-devel\n$ wget https://archive.apache.org/dist/hadoop/core/hadoop-1.2.1/hadoop-1.2.1.tar.gz\n$ tar xvfz hadoop-1.2.1
\n

그리고 자바 프로젝트를 위해 Maven도 설치해줍니다.

\n
$ wget http://mirror.navercorp.com/apache/maven/maven-3/3.5.3/binaries/apache-maven-3.5.3-bin.tar.gz\n$ tar xvfs apache-maven-3.5.3-bin.tar.gz\n$ mv apache-maven-3.5.3/ apache-maven\n$ sudo vi /etc/profile.d/maven.sh\n\n# Apache Maven Environment Variables\n# MAVEN_HOME for Maven 1 - M2_HOME for Maven 2\n$ export M2_HOME=/home/ec2-user/apache-maven\n$ export PATH=${M2_HOME}/bin:${PATH}\n\n$ chmod +x maven.sh\n$ source /etc/profile.d/maven.sh
\n

정상적으로 설치가 되었다면 아래의 명령어에 대한 결과가 나옵니다.

\n
$ java --version\n$ mvn --version
\n
\n

Hadoop 환경 구축

\n

실습환경은 Pseudo-Distibuted 모드로 진행합니다.\n먼저 Password less SSH Login을 설정해주어야 합니다.\n그리고 편의를 위해 hadoop-1.2.1 폴더에 Symbolic link를 생성하겠습니다.

\n
# ssh login setting\n$ ssh-keygen -t rsa -P \"\"\n$ cat /home/ec2-user/.ssh/id_rsa.pub >> /home/ec2-user/.ssh/authorized_keys\n\n# symbolic link\n$ ln -s hadoop-1.2.1 hadoop
\n

이제 HDFS와 MR 실행을 위해 설정파일을 수정해줍니다.\n먼저 hadoop-env.sh을 열어 JAVA_HOME 환경변수를 지정해줍니다.\n가상분산모드에서는 masters, slaves 파일을 수정할 필요가 없습니다.

\n
$ cd hadoop\n$ vi conf/hadoop-env.sh\n\n# set JAVA_HOME in this file, so that it is correctly defined on\n# remote nodes.\n\n# The java implementation to use. Required.\nexport JAVA_HOME=/usr/lib/jvm/java-1.7.0\n\n# Extra Java CLASSPATH elements.  Optional.\n# export HADOOP_CLASSPATH=
\n

이제 core-site.xml 파일을 아래와 같이 수정해줍니다.\nHDFS 데이터 파일들은 홈 디렉토리의 hadoop-data 폴더에 저장하겠습니다.

\n
$ vi conf/core-site.xml\n\n<configuration>\n    <property>\n        <name>fs.default.name</name>\n        <value>hdfs://localhost:9000</value>\n    </property>\n    <property>\n        <name>hadoop.tmp.dir</name>\n        <value>/home/ec2-user/hadoop-data/</value>\n    </property>\n</configuration>
\n

hdfs-site.xml 파일도 수정해줍니다.\ndfs.replication 프로퍼티는 복제 개수를 의미합니다.\n일반적으로 복제 개수를 3으로 두는 것을 권장하지만,\n실습에서는 Fully-Distributed 모드가 아니기 때문에 1로 설정하겠습니다.

\n
$ vi conf/hdfs-site.xml\n\n<configuration>\n    <property>\n        <name>dfs.replication</name>\n        <value>1</value>\n    </property>\n</configuration>
\n

mapred-site.xml 파일도 수정해줍니다.\nmapred.job.tracker 프로퍼티는 job tracker가 동작하는 서버를 말합니다.

\n
$ vi conf/mapred-site.xml\n\n<configuration>\n    <property>\n        <name>mapred.job.tracker</name>\n        <value>localhost:9001</value>\n    </property>\n</configuration>
\n
\n

Hadoop MR

\n

이제 NameNode를 초기화하고 하둡과 관련된 모든 데몬을 실행합니다.

\n
./bin/hadoop namenode-format\n./bin/start-all.sh
\n

jps를 통해 자바 프로세스가 제대로 실행되었는지 확인할 수 있습니다.

\n
$ jps\n3368 TaskTracker\n2991 DataNode\n3241 JobTracker\n3480 Jps\n2872 NameNode\n3139 SecondaryNameNode
\n

HDFS 웹 인터페이스 주소는 http://localhost:50070 이며,\nMapReduce 웹 인터페이스 주소는 http://localhost:50030 입니다.\n들어가시면 아래와 같은 화면이 나타납니다.

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":6,"humanPageNumber":7,"skip":36,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering/6","nextPagePath":""}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering/7","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3bc2c838-2281-5852-899f-ba16e366f41b","title":"AI를 통해 진화하는 데이터플랫폼 근황","slug":"llm-dataplatform","publishDate":"January 21, 2024","publishDateISO":"2024-01-21","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다.
\n오늘은 데이터플랫폼 영역에서 AI를 통해 어떤 변화가 나타나고 있는지 정리해보려 합니다.

\n
\n

자연어를 SQL로 변환 (Text2SQL, SQL2Text)

\n

지난 수 년간 클라우드 마이그레이션이 늘어남에 따라 Databrics, Snowflake와 같은 Managed DW 서비스도 함께 성장해왔습니다. Managed DW 서비스가 23년 Summit에 내세운 키워드는 생성형 AI 였습니다. 다양한 기능을 공개했지만 핵심은 Text2SQL, SQL2Text 기술이라고 볼 수 있습니다.

\n

Text2SQL이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다. 쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n

두 번째는 SQL 함수나 자연어 SDK를 추가하는 방식입니다.\n이를 통해 사용자는 개발 과정에도 자연어를 활용할 수 있습니다.\n검색 UI와 달리 사용자의 검증을 거치지 않고 사용할 수 있지만, 일관된 답변을 보장 할 수 없는 관계로 운영 시스템에 직접 연동은 아직 어려울 것 같습니다.

\n

이처럼 다양한 방식을 지원함으로써 사용자는 AI에 쉽게 접근하고 일관된 개발 경험을 가질 수 있습니다.

\n



\n

기술 문서 검색

\n

개발자는 개발 과정에서 문서 검색에 많은 시간을 할애합니다.\nstackoverflow를 통해 검색하는 경우, 내가 사용하고 있는 프레임워크와 버전에 정확히 일치하는 문서를 찾지 못하는 경우도 많았습니다. 이제 데이터플랫폼 내에서 기술 문서와 코드를 기반으로 AI에게 질의할 수 있게 되었습니다.

\n
\n

AWS Amazon Q Assistant

\n

\n \n \n \n

\n

Amazon Q는 AWS에서 출시한 생성형 AI 어시스턴트입니다.\nAWS 콘솔 우측에 추가되어 AWS 클라우드와 관련된 다양한 질의를 수행할 수 있습니다.

\n
\n

GitHub Dosu

\n

\n \n \n \n

\n

오픈소스 영역에서도 생성형 AI를 통해 Issue, Discussion 문의 대응하는 사례가 생기고 있습니다. 위 그림은 LLM 프레임워크인 LangChain에서 사용하는 Dosu 봇 입니다.\n출시 예정인 GitHub Copilot도 이와 유사한 기능을 지원합니다.\n이러한 기능을 통해 사용자는 빠르게 문제를 해결하고 메인테이너는 중요한 의사결정에 집중할 수 있습니다.

\n



\n

데이터 거버넌스 도구

\n

데이터 거버넌스는 정책을 만드는 일보다 운영하는데 더 많은 노력이 들어갑니다.\n거버넌스 내에는 다양한 영역이 있지만 그 중 데이터 디스커버리와 메타데이터 관리에 AI가 활용되고 있습니다.

\n

데이터 디스커버리 영역의 경우, 기존 UI 기반 검색 엔진에 자연어 질의가 추가됩니다.\n이를 통해 앞서 언급한 Text2SQL과 유사한 경험을 제공할 수 있습니다.

\n

다음은 메타데이터 관리 영역입니다. 메타데이터 관리는 데이터 신뢰도를 위해 데이터 생산자와 소비자 모두에게 중요합니다. 하지만 거버넌스 정책이 새로 추가되거나 변경되면 데이터에 대한 오너십을 가지는 도메인 전문가는 이를 항상 인지하기 어렵습니다. 만약 불일치가 발생하면 거버넌스 담당자가 보정하는 작업을 수행하는 경우도 있습니다. 메타데이터 영역의 AI는 거버넌스 정책을 유지하고 메타데이터 입력을 도와주는 역할을 합니다.

\n

\n \n \n \n

\n

Grab의 경우, LLM이 데이터 분류를 위한 태그를 생성하고 거버넌스 담당자가 확인 후 승인하는 프로세스를 개발했습니다. 이를 통해 민감도 분류, 개인정보 컬럼에 PII 태그를 붙이는 등의 거버넌스 정책을 20,000개 이상 데이터에 일관되게 적용할 수 있었습니다.

\n



\n

플랫폼에 AI를 사용하는 이유

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":6,"humanPageNumber":7,"skip":36,"limit":6,"numberOfPages":7,"previousPagePath":"/tag/dataengineering/6","nextPagePath":""}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/page-data/tag/dataengineering/page-data.json b/page-data/tag/dataengineering/page-data.json index a9fe02d..0378951 100644 --- a/page-data/tag/dataengineering/page-data.json +++ b/page-data/tag/dataengineering/page-data.json @@ -1 +1 @@ -{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3d5aacf4-f336-5c17-a880-4efb995c9b99","title":"AWS에 Hadoop MR 어플리케이션 환경 구축하기","slug":"aws-hadoop","publishDate":"June 13, 2018","publishDateISO":"2018-06-13","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

이번 학기에 하둡 프로그래밍 강의를 들으면서 정말 실습 환경의 개선이 필요하다는 생각이 들었습니다...\n나약한 실습 환경속에서 과제와 기말 프로젝트를 제출해야하는 후배들을 위해 AWS를 추천합니다!

\n
\n

EC2 Amazon Linux2에 기본 환경 구축

\n

AWS에는 EMR이라는 클러스터 서비스가 있지만, 스터디 목적이라면 비용을 생각해서 사용하지 않겠습니다.\nAmazon Linux AMI는 EC2에서 편하게 사용할 수 있도록 지원하고 관리하는 리눅스 이미지입니다.\n만일 학생용 크레딧이 있다면 t2.medium 인스턴스를 추천합니다.

\n

먼저, JAVA JDK와 Hadoop 파일을 받겠습니다. 실습 환경은 자바 7, 하둡 1.2 버전입니다.

\n
$ sudo yum update -y\n$ sudo yum install -y java-1.7.0-openjdk-devel\n$ wget https://archive.apache.org/dist/hadoop/core/hadoop-1.2.1/hadoop-1.2.1.tar.gz\n$ tar xvfz hadoop-1.2.1
\n

그리고 자바 프로젝트를 위해 Maven도 설치해줍니다.

\n
$ wget http://mirror.navercorp.com/apache/maven/maven-3/3.5.3/binaries/apache-maven-3.5.3-bin.tar.gz\n$ tar xvfs apache-maven-3.5.3-bin.tar.gz\n$ mv apache-maven-3.5.3/ apache-maven\n$ sudo vi /etc/profile.d/maven.sh\n\n# Apache Maven Environment Variables\n# MAVEN_HOME for Maven 1 - M2_HOME for Maven 2\n$ export M2_HOME=/home/ec2-user/apache-maven\n$ export PATH=${M2_HOME}/bin:${PATH}\n\n$ chmod +x maven.sh\n$ source /etc/profile.d/maven.sh
\n

정상적으로 설치가 되었다면 아래의 명령어에 대한 결과가 나옵니다.

\n
$ java --version\n$ mvn --version
\n
\n

Hadoop 환경 구축

\n

실습환경은 Pseudo-Distibuted 모드로 진행합니다.\n먼저 Password less SSH Login을 설정해주어야 합니다.\n그리고 편의를 위해 hadoop-1.2.1 폴더에 Symbolic link를 생성하겠습니다.

\n
# ssh login setting\n$ ssh-keygen -t rsa -P \"\"\n$ cat /home/ec2-user/.ssh/id_rsa.pub >> /home/ec2-user/.ssh/authorized_keys\n\n# symbolic link\n$ ln -s hadoop-1.2.1 hadoop
\n

이제 HDFS와 MR 실행을 위해 설정파일을 수정해줍니다.\n먼저 hadoop-env.sh을 열어 JAVA_HOME 환경변수를 지정해줍니다.\n가상분산모드에서는 masters, slaves 파일을 수정할 필요가 없습니다.

\n
$ cd hadoop\n$ vi conf/hadoop-env.sh\n\n# set JAVA_HOME in this file, so that it is correctly defined on\n# remote nodes.\n\n# The java implementation to use. Required.\nexport JAVA_HOME=/usr/lib/jvm/java-1.7.0\n\n# Extra Java CLASSPATH elements.  Optional.\n# export HADOOP_CLASSPATH=
\n

이제 core-site.xml 파일을 아래와 같이 수정해줍니다.\nHDFS 데이터 파일들은 홈 디렉토리의 hadoop-data 폴더에 저장하겠습니다.

\n
$ vi conf/core-site.xml\n\n<configuration>\n    <property>\n        <name>fs.default.name</name>\n        <value>hdfs://localhost:9000</value>\n    </property>\n    <property>\n        <name>hadoop.tmp.dir</name>\n        <value>/home/ec2-user/hadoop-data/</value>\n    </property>\n</configuration>
\n

hdfs-site.xml 파일도 수정해줍니다.\ndfs.replication 프로퍼티는 복제 개수를 의미합니다.\n일반적으로 복제 개수를 3으로 두는 것을 권장하지만,\n실습에서는 Fully-Distributed 모드가 아니기 때문에 1로 설정하겠습니다.

\n
$ vi conf/hdfs-site.xml\n\n<configuration>\n    <property>\n        <name>dfs.replication</name>\n        <value>1</value>\n    </property>\n</configuration>
\n

mapred-site.xml 파일도 수정해줍니다.\nmapred.job.tracker 프로퍼티는 job tracker가 동작하는 서버를 말합니다.

\n
$ vi conf/mapred-site.xml\n\n<configuration>\n    <property>\n        <name>mapred.job.tracker</name>\n        <value>localhost:9001</value>\n    </property>\n</configuration>
\n
\n

Hadoop MR

\n

이제 NameNode를 초기화하고 하둡과 관련된 모든 데몬을 실행합니다.

\n
./bin/hadoop namenode-format\n./bin/start-all.sh
\n

jps를 통해 자바 프로세스가 제대로 실행되었는지 확인할 수 있습니다.

\n
$ jps\n3368 TaskTracker\n2991 DataNode\n3241 JobTracker\n3480 Jps\n2872 NameNode\n3139 SecondaryNameNode
\n

HDFS 웹 인터페이스 주소는 http://localhost:50070 이며,\nMapReduce 웹 인터페이스 주소는 http://localhost:50030 입니다.\n들어가시면 아래와 같은 화면이 나타납니다.

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":0,"humanPageNumber":1,"skip":0,"limit":6,"numberOfPages":7,"previousPagePath":"","nextPagePath":"/tag/dataengineering/2"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file +{"componentChunkName":"component---src-templates-tag-js","path":"/tag/dataengineering","result":{"data":{"contentfulTag":{"title":"DataEngineering","id":"25d7d0d6-3cf7-5e19-a5cb-9c3fa926046f","slug":"dataengineering","post":[{"id":"3bc2c838-2281-5852-899f-ba16e366f41b","title":"AI를 통해 진화하는 데이터플랫폼 근황","slug":"llm-dataplatform","publishDate":"January 21, 2024","publishDateISO":"2024-01-21","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다.
\n오늘은 데이터플랫폼 영역에서 AI를 통해 어떤 변화가 나타나고 있는지 정리해보려 합니다.

\n
\n

자연어를 SQL로 변환 (Text2SQL, SQL2Text)

\n

지난 수 년간 클라우드 마이그레이션이 늘어남에 따라 Databrics, Snowflake와 같은 Managed DW 서비스도 함께 성장해왔습니다. Managed DW 서비스가 23년 Summit에 내세운 키워드는 생성형 AI 였습니다. 다양한 기능을 공개했지만 핵심은 Text2SQL, SQL2Text 기술이라고 볼 수 있습니다.

\n

Text2SQL이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다. 쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n

두 번째는 SQL 함수나 자연어 SDK를 추가하는 방식입니다.\n이를 통해 사용자는 개발 과정에도 자연어를 활용할 수 있습니다.\n검색 UI와 달리 사용자의 검증을 거치지 않고 사용할 수 있지만, 일관된 답변을 보장 할 수 없는 관계로 운영 시스템에 직접 연동은 아직 어려울 것 같습니다.

\n

이처럼 다양한 방식을 지원함으로써 사용자는 AI에 쉽게 접근하고 일관된 개발 경험을 가질 수 있습니다.

\n



\n

기술 문서 검색

\n

개발자는 개발 과정에서 문서 검색에 많은 시간을 할애합니다.\nstackoverflow를 통해 검색하는 경우, 내가 사용하고 있는 프레임워크와 버전에 정확히 일치하는 문서를 찾지 못하는 경우도 많았습니다. 이제 데이터플랫폼 내에서 기술 문서와 코드를 기반으로 AI에게 질의할 수 있게 되었습니다.

\n
\n

AWS Amazon Q Assistant

\n

\n \n \n \n

\n

Amazon Q는 AWS에서 출시한 생성형 AI 어시스턴트입니다.\nAWS 콘솔 우측에 추가되어 AWS 클라우드와 관련된 다양한 질의를 수행할 수 있습니다.

\n
\n

GitHub Dosu

\n

\n \n \n \n

\n

오픈소스 영역에서도 생성형 AI를 통해 Issue, Discussion 문의 대응하는 사례가 생기고 있습니다. 위 그림은 LLM 프레임워크인 LangChain에서 사용하는 Dosu 봇 입니다.\n출시 예정인 GitHub Copilot도 이와 유사한 기능을 지원합니다.\n이러한 기능을 통해 사용자는 빠르게 문제를 해결하고 메인테이너는 중요한 의사결정에 집중할 수 있습니다.

\n



\n

데이터 거버넌스 도구

\n

데이터 거버넌스는 정책을 만드는 일보다 운영하는데 더 많은 노력이 들어갑니다.\n거버넌스 내에는 다양한 영역이 있지만 그 중 데이터 디스커버리와 메타데이터 관리에 AI가 활용되고 있습니다.

\n

데이터 디스커버리 영역의 경우, 기존 UI 기반 검색 엔진에 자연어 질의가 추가됩니다.\n이를 통해 앞서 언급한 Text2SQL과 유사한 경험을 제공할 수 있습니다.

\n

다음은 메타데이터 관리 영역입니다. 메타데이터 관리는 데이터 신뢰도를 위해 데이터 생산자와 소비자 모두에게 중요합니다. 하지만 거버넌스 정책이 새로 추가되거나 변경되면 데이터에 대한 오너십을 가지는 도메인 전문가는 이를 항상 인지하기 어렵습니다. 만약 불일치가 발생하면 거버넌스 담당자가 보정하는 작업을 수행하는 경우도 있습니다. 메타데이터 영역의 AI는 거버넌스 정책을 유지하고 메타데이터 입력을 도와주는 역할을 합니다.

\n

\n \n \n \n

\n

Grab의 경우, LLM이 데이터 분류를 위한 태그를 생성하고 거버넌스 담당자가 확인 후 승인하는 프로세스를 개발했습니다. 이를 통해 민감도 분류, 개인정보 컬럼에 PII 태그를 붙이는 등의 거버넌스 정책을 20,000개 이상 데이터에 일관되게 적용할 수 있었습니다.

\n



\n

플랫폼에 AI를 사용하는 이유

\n

\n \n \n \n

\n

실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML 모델링 보다 이외의 작업들이 많이 필요합니다. 특히 서비스의 여러 기능에 ML을 적용시키려 하는 경우, 이러한 파이프라인이 복잡해지고 유지보수가 힘든 방향으로 가는 경우가 많습니다. 이러한 이유로 규모있는 IT 서비스 회사들은 공통의 ML 플랫폼을 구축하곤 합니다.

\n

앞으로 소개하려는 Kubeflow는 Kubernetes를 기반으로 하는 오픈소스 ML Toolkit 입니다. 아직 버전이 낮아 production 환경에서 사용하는 곳이 많지 않지만 미리 알아두면 좋을 것 같아 컴포넌트들을 하나씩 분석해보려 합니다.

\n\n
\n

Why Kubeflow?

\n

이미 기존의 인프라를 기반으로 자동화된 ML Workflow가 구축되어 있다면, 굳이 Kubeflow로 옮길 필요는 없습니다. 하지만 아래와 같은 상황을 가진 팀이라면 Kubeflow는 좋은 선택지가 될 수 있습니다.

\n\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n\n
\n

Resource utilization by the Training / Serving modules

\n

테스트 환경을 쉽게 구축할 수 있으며, 클라우드 비용을 최적화시킬 수 있습니다. K8S 클러스터는 동일한 인스턴스에 여러 Pod을 실행시킬 수 있습니다. 따라서, 사용하는 리소스를 팀 또는 프로젝트 단위로 namespace를 분리시켜 리소스 사용량을 모니터링 할 수 있습니다.

\n

일반적인 클라우드 인프라 환경을 서비스 라이프사이클과 연계되어 있지 않기 때문에 training job이 끝난 이후에도 인스턴스가 켜져 있기 때문에 그에 대한 비용을 지불해야 합니다. 하지만 Kubeflow를 사용하는 경우, 사용량에 따라 클러스터를 auto scaling 한다거나 spot instance로 training job을 실행시킬 수 있습니다.

\n
\n

Reference

\n","excerpt":"실제 ML을 서비스에 적용시키는 일은 위 그림에 나타난 바와 같이 ML…"}}},{"id":"a77d5de0-57d3-56d5-bedc-d02ee85072f7","title":"Amazon EKS에 Kubeflow 구축하기","slug":"eks-kubeflow","publishDate":"March 10, 2019","publishDateISO":"2019-03-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EKS는 Fully managed K8S 서비스 입니다. 이번 글에서는 EKS 환경에 Kubeflow를 구축하는 방법에 대해 정리해보겠습니다.

\n\n
\n

기본 환경 설치

\n

Kubeflow를 설치하기 이전에 AWS CLI, Docker가 설치되어 있어야 합니다.\nEKS에서는 최근에 GPU 인스턴스인 P2, P3에 대한 지원을 제공하고 있습니다.\n이를 사용하기 위해 AWS Marketplace에서 EKS-optimized AMI with GPU Support를 구독해주어야 합니다.

\n

EKS는 Web UI 또는 eksctl이라는 cli 도구를 사용해서 클러스터를 구성할 수 있습니다.\neksctl은 kubectl이나 kops와 유사한 명령어를 제공합니다.\n자세한 내용은 https://aws.amazon.com/ko/blogs/opensource/eksctl-eks-cluster-one-command/ 에서 참고하시면 됩니다.

\n
\n

EKS 클러스터 생성

\n
# install eksctl\n$ brew tap weaveworks/tap\n$ brew install weaveworks/tap/eksctl\n\n# create cluster\n$ eksctl create cluster eks-cpu \\\n--node-type=c4.xlarge \\\n--timeout=40m \\\n--nodes=2 \\\n--region=ap-northeast-2\n\n# NVIDIA driver plugin\nkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v1.11/nvidia-device-plugin.yml\nkubectl get nodes \"-o=custom-columns=NAME:.metadata.name,MEMORY:.status.allocatable.memory,CPU:.status.allocatable.cpu,GPU:.status.allocatable.nvidia\\.com/gpu\"
\n\n

\n \n \n \n \n\n \n \n \n

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n\n
\n

LocalExecutor

\n

\n \n \n \n

\n

LocalExecutor는 Scheduler에서 각 task가 subprocess 형태로 돌아가는 구조입니다. Scale-Out이 어렵기 때문에 간단한 테스트 용도로 사용하는 경우가 많습니다.

\n
\n

CeleryExecutor + DAG PV

\n

\n \n \n \n

\n

CeleryExecutor는 Scheduler가 task queue에 작업을 전달하고 worker에서 작업이 수행되는 구조입니다. 지난 번 글에서 언급했듯이 여러 노드에 걸쳐 있는 DAG 파일을 동기화하기 위해 PV, git-sync 2가지 옵션을 지원합니다. 이 옵션은 KubernetesExecutor에서도 지원합니다.

\n
\n

위의 그림에서는 AWS EFS를 기준으로 표현했지만 다른 스토리지에서도 활용 가능합니다. 이 방식은 스토리지를 별도로 두기 때문에 git과 다르게 배포 주기를 가져갈 수 있습니다.\n그리고 worker pod이 statefulset 형태로 변경되었습니다. 이를 통해 각 worker에 PV를 연결하고 airflow UI에서 각 task의 로그를 볼 수 있습니다.

\n
\n

CeleryExecutor + DAG git-sync

\n

\n \n \n \n

\n
\n

KEDA AutoScaler는 공식 차트에만 추가된 옵션입니다.\n기존의 Horizontal Pod Autoscaler는 리소스(CPU, Memory) 메트릭을 기반으로 스케일 여부를 결정하게 됩니다. 반면에 KEDA는 특정 이벤트를 기반으로 스케일 여부를 결정할 수 있습니다. 예를 들어 airflow는 metadb를 통해 현재 실행 중이거나 대기 중인 task가 얼마나 존재하는지 알 수 있습니다. 이러한 이벤트를 활용하여 worker의 scale을 결정한다면 queue에 task가 많이 추가되는 시점에 더 빠르게 확장할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / 16)\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

이를 위해 airflow에서는 KEDA의 PostgreSQL trigger를 활용하였고 실제 위와 같은 쿼리가 등록되어 있습니다. KEDA는 CRD와 custom controller로 구성되어 있기 때문에 기존 HPA와 함께 사용 가능하며 모든 K8S 클러스터에 추가할 수 있습니다.

\n
\n

CeleryExecutor vs KubernetesExecutor

\n

여기까지 CeleryExecutor에 대해 알아보았습니다. CeleryExecutor 또한 Kubernetes 위에 배포하면 Helm 차트를 통한 선언형 리소스 관리, 쉬운 버전 업데이트, DAG 배포 자동화, 쉬운 리소스 확장 등의 장점을 가질 수 있습니다. 하지만 Celery에 대한 의존성이 남아있기 때문에 Redis, Celery Worker에 대한 리소스를 계속 점유하고 있어야 합니다. 다시 말해서, Scale to Zero가 어렵다는 단점이 있습니다. KubernetesExecutor는 task가 존재할때만 pod이 생성되고 task가 완료되면 종료되기 때문에 더 리소스를 효율적으로 사용한다고 볼 수 있습니다.

\n
\n

KubernetesExecutor, KubernetesPodOperator

\n

\n \n \n \n

\n
\n

위의 그림처럼 KubernetesExecutor는 Broker와 같은 리소스를 점유하고 있을 필요가 없습니다. 리소스를 할당하고 스케줄링 하는 역할은 Kubernetes Scheduler가 수행하게 됩니다. Airflow Scheduler는 API Server에게 task 수행을 위한 Pod 생성을 요청합니다. worker는 images.airflow에 설정한 이미지로 Pod이 생성되기 때문에 추가로 필요한 파이썬 패키지가 존재한다면 별도의 이미지를 만들어주어야 합니다. 만일 task pod 마다 다른 이미지와 리소스 설정을 가지도록 하고 싶다면 KubernetesPodOperator를 사용하시면 됩니다. KubernetesPodOperator는 worker를 통해 pod이 생성되는 구조이므로 파라메터를 통해 사용자가 원하는 설정으로 변경할 수 있습니다.

\n
\n

KubernetesExecutor Process

\n

\n \n \n \n

\n
\n

task가 완료되기 전에 Airflow DB 상태 업데이트 단계에서 OOM 등의 이유로 Pod Crash가 언제나 발생할 수 있기 때문에 이에 대한 장애 시나리오도 준비되어 있습니다. DB 업데이트에 실패하더라도 airflow scheduler는 Kubernetes Watch API를 통해 pod의 상태를 전달받아 다시 DB 상태를 업데이트 할 수 있습니다. CeleryExecutor의 경우, task 상태에 대한 처리를 celery에 주기적으로 확인하는 방식이라면 KubernetesExecutor는 이벤트 스트림으로 전달받기 때문에 스케줄러에 대한 부하가 더 낮다고 볼 수 있습니다.

\n
\n

KubernetesExecutor Batch, CronJob

\n

공식 차트에서는 사용자의 편의를 위해 RBAC 초기 사용자를 생성해주는 create-user BatchJob이 추가되었습니다. Helm Hooks (post-install) 를 통해 차트 리소스가 모두 생성된 이후에 수행됩니다. 더 이상 exec 명령어로 bash에 들어가 create-user 명령어를 수행할 필요가 없습니다!

\n

추가로 cleanup CronJob이 있습니다. AIRFLOW__KUBERNETES__DELETE_WORKER_PODS 옵션을 통해 task가 끝나더라도 pod이 종료되지 않도록 설정할 수 있는데 이때 내가 원하는 주기마다 오래된 pod을 삭제할 수 있는 CronJob 입니다.

\n
\n

Official Helm Chart Issue

\n

공식 버전 차트는 아래와 같은 이슈가 남아있지만 2.0 정식 버전 출시와 함께 해결될 예정입니다.\n글을 작성하는 과정에서 DAG 동기화 관련 버그를 발견하였지만 리뷰를 통해 곧바로 수정되었습니다. (PR-9371). stable/airflow 차트와 비교했을때 아쉬운 점은 아래와 같습니다.

\n\n
\n

Deploy

\n

사실 배포와 옵션에 대한 내용은 지난 글에서 말한 내용과 크게 다름이 없습니다. 아직 정식 릴리즈까지 변경될 여지가 많다보니 아래 공식 문서 따라하시는 방법을 추천드립니다 (apache/airflow/chart). 다음 글에서는 KubernetesExecutor의 로깅과 모니터링에 대해 다루어보겠습니다!

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"6458380e-9bc8-5184-a818-51a7dd2dbaa6","title":"Airflow on Kubernetes (1)","slug":"airflow-on-kubernetes-1","publishDate":"June 05, 2020","publishDateISO":"2020-06-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow on Kubernetes

\n

Airflow를 Kubernetes 인프라 위에서 운영하는 방법은 크게 두 가지로 나눌 수 있습니다.\n이 글에서 소개할 방법은 CeleryExecutor의 각 모듈을 Kubernetes 위에 올리는 방식입니다. 기존에 운영하던 형태와 유사하기 때문에 쉽게 적용할 수 있으나 Celery에 대한 의존성이 강하다보니 완전히 Cloud Native한 형태는 아닙니다. 아키텍쳐는 가장 많이 사용하는 stable/airflow Helm Chart를 참고하였습니다. 이제 몇 가지 컴포넌트 설정과 함께 자세히 알아보겠습니다.

\n
\n

Config

\n

Airflow는 airflow.cfg 파일 또는 AIRFLOW__[SECTOR]__[VARIABLES] 환경 변수를 통해 각 컴포넌트의 설정을 관리할 수 있었습니다. Helm Chart에서는 values.yaml의 config 필드를 통해 설정을 관리할 수 있습니다.

\n
config:\n  # CORE\n  AIRFLOW__CORE__DEFAULT_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__CORE__PARALLELISM: \"32\"\n  AIRFLOW__CORE__DAG_CONCURRENCY: \"16\"\n  AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG: \"16\"\n\n  # WEBSERVER\n  AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE: \"Asia/Seoul\"\n  AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL: \"60\"\n\n  # CELERY\n  AIRFLOW__CELERY__WORKER_CONCURRENCY: \"16\"\n\n  # SCHEDULER\n  AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC: \"30\"\n  AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD: \"120\"\n  AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL: \"30\"\n  AIRFLOW__SCHEDULER__RUN_DURATION: \"10800\"\n  AIRFLOW__SCHEDULER__MAX_THREADS: \"2\"
\n
\n

위에 정의한 설정 변수들은 Airflow의 성능과 관련되어 있기 때문에 각자 할당된 리소스에 맞게 설정해주셔야 합니다. 자세한 내용은 공식문서 링크를 참고하시기 바랍니다. 위와 같은 방식으로 DAG에서 활용하는 connection, variables도 정의할 수 있습니다.

\n
\n
# config.yaml\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: airflow-webserver-config\n  namespace: airflow\ndata:\n  webserver_config.py: |\n    APP_THEME = \"flatly.css\"\n\n---\n# values.yaml\nextraConfigmapMounts:\n  - name: airflow-webserver-config\n    mountPath: /opt/airflow/webserver_config.py\n    configMap: airflow-webserver-config\n    readOnly: true\n    subPath: webserver_config.py
\n
\n

위와 같이 ConfigMap이나 Secret을 따로 만들고 참조하도록 연결하는 방식도 가능합니다. 특히 Airflow 1.10의 RBAC을 사용한다면 webserver_config.py를 통해 APP_THEME를 변경해줄 수 있는데 이런 경우에 extraConfigmap을 통해 적용할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

제가 주로 사용하는 테마는 flatly.cssNAVBAR #18bc9c 컬러 조합입니다. 적용된 화면은 위와 같습니다. (+ 태그 기능도 1.10.10 버전에 추가되었습니다)

\n
\n

Celery Worker

\n

\n \n \n \n

\n
\n

CeleryExecutor에서 worker는 실제 task를 수행을 담당하는 컴포넌트입니다. K8S에서는 celery worker가 StatefulSet으로 배포됩니다. 기존에는 worker가 AutoScalingGroup 등을 통해 인스턴스가 자동 확장되도록 구성했다면, K8S에서는 HorizontalPodAutoscaler를 통해 Pod 단위로 확장 가능하도록 구성할 수 있습니다.

\n
\n
workers:\n  replicas: 1\n\n  resources:\n    requests:\n      memory: \"2Gi\"\n\n  autoscaling:\n    enabled: true\n    maxReplicas: 16\n    metrics:\n    - type: Resource\n      resource:\n        name: memory\n        target:\n          type: Utilization\n          averageUtilization: 80
\n
\n

Airflow Ingress

\n

보통 K8S 클러스터에 Ingress Controller를 설정하고 path를 통해 여러 서비스에 접속하는 경우가 많습니다. Airflow Chart 역시 Webserver와 Flower UI에 대한 ingress를 지원합니다. 저는 nginx-ingress controller를 사용해서 진행해보겠습니다. 아래 예시는 각자의 ingress-controller 설정에 맞게 바꾸시면 됩니다.

\n
\n
web:\n  service:\n    annotations: {}\n    type: ClusterIP\n    externalPort: 8080\n    loadBalancerIP: \"\"\n    loadBalancerSourceRanges: []\n\n...\n\ningress:\n  enabled: true\n  web:\n    annotations:\n      kubernetes.io/ingress.class: nginx\n      ingress.kubernetes.io/rewrite-target: /\n      nginx.ingress.kubernetes.io/ssl-redirect: \"false\"\n\n    path: \"/airflow\"\n    host: \"myloadbalancer-domain.com\"
\n

예를 들어 web path에 /airflow 라고 설정하셨다면, UI 접속 주소는 myloadbalancer-domain.com/airflow가 됩니다. flower도 위와 동일한 방식으로 설정하시면 됩니다.

\n
\n

Airflow Auth

\n

Airflow 에서는 다양한 인증 방식을 지원하지만 여기에서는 가장 기본이 되는 Password Auth 방식으로 배포하겠습니다. 새로 추가된 RBAC 설정도 함께 추가해보겠습니다. 먼저 extraPipPackages 설정을 통해 의존성 패키지를 설치해주고 상단에 환경 변수도 추가해줍니다.

\n
\n
config:\n  AIRFLOW__WEBSERVER__RBAC: \"True\"\n  AIRFLOW__WEBSERVER__AUTHENTICATE: \"True\"\n  AIRFLOW__WEBSERVER__AUTH_BACKEND: \"airflow.contrib.auth.backends.password_auth\"\n\n...\n\nweb:\n  extraPipPackages:\n    - \"flask-bcrypt\"\n    - \"flask-oauthlib>=0.9\"
\n
\n

이제 로그인할 사용자를 추가해주어야 합니다. Scheduler Pod의 Bash에서 create_user 명령어를 통해 생성해주시면 됩니다.

\n
\n
$ kubectl exec \\\n  -it \\\n  --namespace airflow \\\n  --container airflow-scheduler \\\n  Deployment/airflow-scheduler \\\n  /bin/bash\n\n$ airflow create_user \\\n--username=admin \\\n--email=test@example.com \\\n--password=mypassword \\\n--role=Admin \\\n--firstname=test \\\n--lastname=park
\n
\n

Airflow IAM Role

\n

AWS EKS와 같은 클라우드 서비스 위에 배포한다면 각 컴포넌트의 세부 권한을 지정해주어야 합니다. 만일 Pod에 IAM Role을 할당하지 않는다면 Airflow는 클러스터의 기본 IAM Role인 EKS worker 설정을 따르게 됩니다. 따라서 보안을 신경쓰셔야 한다면 설정하는 것이 바람직합니다. 특히 Airflow에서 다른 AWS Managed Service(EMR, Athena, Lambda)와 연계하는 DAG이 존재하신다면 필수적입니다.

\n
\n
serviceAccount:\n  create: true\n  name: \"airflow\"\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789999:role/airflow\n\n...\n\nsecurityContext:\n  fsGroup: 1000
\n
\n

values.yaml에는 포함되어 있지 않지만 각 컴포넌트마다 securityContext를 지정해주셔야 IAM Role을 매핑할 수 있습니다. IAM Role for Service Account가 내부적으로 K8S TokenProjection을 사용하기 때문에 설정을 안하면 토큰을 읽을 수 없다는 오류가 발생합니다. IAM Role 설정에 대한 자세한 내용은 EKS 공식 문서를 참고하시기 바랍니다.

\n
\n

DAGs

\n

Airflow는 Scheduler가 DAG 파일을 주기적으로 동기화하며 문법적 오류가 없는지 체크하는 역할을 수행합니다. 단일 노드에서는 로컬에 있는 DAG 파일을 읽으면 되지만 K8S에서는 worker pod가 여러 노드에 걸쳐있기 때문에 모두 같은 DAG 파일을 바라보도록 하는 동기화 설정이 필요합니다. Helm Chart에서는 이를 지원하기 위해 두 가지 옵션을 제공합니다.

\n
\n

1. Git-Sync Sidecar

\n
# git-sync sidecar\ndags:\n  git:\n    url: ssh://git@repo.example.com/example.git\n    repoHost: repo.example.com\n    secret: airflow-git-keys\n    privateKeyName: id_rsa\n\n    gitSync:\n      enabled: true\n      refreshTime: 60
\n
\n

첫 번째 방식은 git-sync 사이드카 컨테이너를 활용하는 방법입니다. 간단히 말하자면 주기적으로 외부 저장소를 당겨오는 방식으로 git 인증이 필요합니다. 사이드카 패턴이 생소하시다면 이전에 작성한 분산 컨테이너에서의 디자인 패턴 글을 참고하시기 바랍니다.

\n
\n

2. Shared Persistent Volume

\n
# EFS PV, PVC\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    name: airflow-dags\n    storage: airflow\nspec:\n  capacity:\n    storage: 20Gi\n  accessModes:\n    - ReadWriteMany\n  nfs:\n    server: 0.0.0.0 <- EFS endpoint\n    path: \"/airflow\"\n\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: airflow-dags\n  namespace: airflow\n  labels:\n    storage: airflow\nspec:\n  storageClassName: \"\"\n  accessModes:\n    - ReadWriteMany\n  resources:\n    requests:\n      storage: 10Gi\n  selector:\n    matchLabels:\n      name: airflow-dags\n\n---\n# shared persistent volume\ndags:\n  persistence:\n    enabled: true\n    existingClaim: \"airflow-dags\"\n    accessMode: ReadWriteMany\n    size: 1Gi
\n
\n

두 번째 방식은 EFS와 같은 공유 파일시스템을 활용한 방법입니다. EFS의 특정 경로에 DAG 파일을 저장하고 마운트를 통해 모든 Pod이 같은 경로를 바라보도록 설정하는 방식입니다. 저는 EFS PV와 PVC를 먼저 추가한다음 existingClaim을 통해 참조하도록 설정해주었습니다.

\n
\n

Deploy

\n

필요한 설정을 완료했다면 배포는 아래 Helm 명령어를 통해 할 수 있습니다. 가능하다면 데이터베이스는 external로 사용하는 방법을 추천드립니다. DB 암호는 secret을 통해 생성하고 참조하도록 설정해주시면 됩니다.

\n
\n
helm install stable/airflow \\\n--version 7.1.1 \\\n--namespace airflow \\\n--name airflow \\\n-f ./values.yaml
\n
\n

배포 이후에 namespace를 보면 아래와 같은 Pod이 존재하는걸 확인할 수 있습니다.

\n
\n

\n \n \n \n

\n
\n

이 글에서 언급한 설정은 FIXME 주석을 해두었으니 궁금하신분들은 https://github.com/Swalloow/airflow-helm 저장소를 확인하시기 바랍니다.

","excerpt":"최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow…"}}},{"id":"0d51ef05-306f-56ae-b726-ab2712215dec","title":"여러 조직이 함께 사용하는 Airflow 만들기","slug":"airflow-multi-tenent-1","publishDate":"August 15, 2021","publishDateISO":"2021-08-15","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":6,"html":"

사내 데이터가 다양해지고 사용자가 많아지면 접근 제어와 권한 등 다양한 고민이 생기게 됩니다.\n이 글에서는 여러 조직이 함께 사용하는 Airflow를 만들 때 알아두면 좋은 내용들에 대해 정리해보려고 합니다.

\n\n
\n

접근 제어가 필요한 경우

\n

먼저 접근 제어는 모든 조직에 필요한 내용은 아닙니다. 다만 아래와 같은 경우에는 필요할 수 있습니다.

\n\n

특히 Airflow Connections, Variable에는 DB 또는 클러스터 접속 정보, API키 등 민감한 정보가 많이 저장됩니다. 물론 마스킹 기능을 통해 UI에서 볼 수 없게 만들 수 있지만 id는 볼 수 있기 때문에 쉽게 값을 가져올 수 있습니다.

\n
from airflow.models import Variable\nfrom airflow.hooks.base_hook import BaseHook\n\nvariable = Variable.get(\"myvar\")\nconnection = BaseHook.get_connection(\"myconn\")
\n
\n

이 문제를 해결하기 위한 방법으로 조직마다 Airflow 환경을 분리하는 방법이 있습니다.\n하지만 이 방법은 운영과 모니터링이 힘들 수 있어 프라이빗 클라우드를 운영해야하는 상황이 아니라면 추천하지 않습니다. 두 번째 방법은 Airflow의 RBAC 기능을 활용하는 방법 입니다.

\n
\n

Airflow RBAC

\n

\n \n \n \n

\n

Connections ViewMenu 와 can_edit Permission 을 조합하면 can edit on Connections라는 PermissionView 가 생성됩니다. 이 권한을 가진 사용자만 Connections UI에서 편집을 할 수 있습니다. 이러한 방식을 Airflow에서는 Resource-Based permissions라고 정의하고 있습니다.

\n

Airflow에는 다양한 리소스에 대해 권한이 이미 정의되어 있고, 기본적으로 Admin을 포함한 5개의 Role을 제공합니다. 조직마다 다른 Role을 가지고 싶은 경우, BaseRole을 정의하고 Copy Role을 통해 새로 만들면 편하게 운영할 수 있습니다.

\n

리소스 기반의 권한 제어도 필요하지만 이 기능에서는 DAGs 라는 단일 리소스로 보고 있기 때문에 DAG 단위로 접근 제어를 할 수 없습니다. 이를 지원하기 위해 2.0+ 버전부터 DAG-level Permission이 추가되었습니다.

\n
\n

DAG-level Permissions

\n

DAG-level Permission을 사용하면 다음과 같은 접근 제어를 할 수 있습니다.

\n\n

DAG-level Permission은 앞서 얘기했던 리소스 기반 접근 제어에 DAG:dag_id라는 리소스를 추가하는 방식으로 구현되었습니다. 예를 들어 A 사용자와 B 사용자에게 example DAG에 대한 읽기 권한을 부여하고 싶은 경우, DAG:example.can_read라는 권한을 추가해주어야 합니다.

\n
with DAG(\n    \"example_dag\",\n    default_args=default_args,\n    description=\"example dags\",\n    schedule_interval=\"@once\",\n    access_control={\"myrole\": {\"can_dag_read\"}},\n    start_date=days_ago(2),\n) as dag:
\n

위와 같이 DAG을 정의하는 단계에서도 access_control 파라메터를 통해 DAG의 접근 권한을 정의해주어야 합니다. 이후 BaseRole에 DAGs 리소스 접근 권한을 제거하면 사용자는 오직 허용된 DAG에 대해서만 접근할 수 있게 됩니다.

\n

DAG access_control이 변경될 때마다 Role에 권한을 추가하는 일은 보통 번거로운 일이 아닙니다. 이를 위해 Airflow에서는 airflow sync-perm 이라는 명령어를 제공합니다. 해당 명령어를 실행하면 모든 DAG에 정의된 권한이 연관된 Role에 반영됩니다. Permission Sync 사이드카 컨테이너를 webserver에 배포하면 이 과정을 자동화할 수 있습니다. 관련 내용은 사이드카 컨테이너로 Airflow 기능 확장하기 글을 참고해주시면 됩니다.

\n
\n

Connection, Variable Access Control

\n

앞서 DAG-level Permission을 보셨다면 느끼셨겠지만 Connection, Variable 또한 각 변수에 대해 접근 제어를 할 수 없고 관련 기능도 없습니다. 하지만 Alternative Secrets Backend 라는 기능을 통해 Custom Backend 클래스를 만들면 접근 제어를 구현할 수 있습니다.

\n
\n

Alternative Secrets Backend

\n

원래 Connection, Variable은 Meta DB에 저장됩니다. 하지만 이 기능을 사용하면 AWS Parameter Store, Vault 등 외부 자원을 저장소로 사용할 수 있습니다. airflow에 구현된 코드는 아래와 같습니다.

\n
@classmethod\ndef get_connection_from_secrets(cls, conn_id: str) -> 'Connection':\n    \"\"\"\n    Get connection by conn_id.\n    :param conn_id: connection id\n    :return: connection\n    \"\"\"\n    for secrets_backend in ensure_secrets_loaded():\n        conn = secrets_backend.get_connection(conn_id=conn_id)\n        if conn:\n            return conn\n    raise AirflowNotFoundException(f\"The conn_id `{conn_id}` not defined\")
\n
\n

BaseHook에서 호출하는 get_connection_from_secrets 메서드는 여러 backend로부터 conn_id에 대한 값을 받아오고 리턴합니다. 즉 기존 Meta DB를 사용하고 있더라도 유지하면서 새로운 backend와 호환 가능합니다.

\n

AWS Parameter Store는 Path 단위로 키를 다르게 값을 저장할 수 있습니다.\n이 점을 활용해서 id 상위 경로로 role을 지정한다면 role 단위로 접근 제어가 가능해집니다.\n접근 제어를 위한 AWS Parameter Store에 저장되는 규칙은 아래와 같습니다.\nAirflow 환경, 역할 별로 구분해서 저장합니다.

\n
secrets:\n    backend: \"airflow...SystemsManagerParameterStoreBackend\"\n    backend_kwargs: {\n        \"connections_prefix\": \"/airflow/prod/connections\",\n        \"variables_prefix\": \"/airflow/prod/variables\",\n        \"profile_name\": null\n    }
\n\n

기본으로 제공하는 Connections, Variables UI는 세부 경로로 값을 가져오는게 아니기 때문에 secrets backend 설정과 함께 Custom UI Plugin이 필요합니다.

\n
\n

Access Control UI Plugin

\n

\n \n \n \n

\n

플러그인의 역할은 다음과 같습니다. myrole이라는 Airflow Role을 가진 사용자가 Connections UI 페이지에 접근하면 Custom Backend를 통해 Paramter Store의 /airflow/prod/connections/myrole 경로 하위의 값들을 받아오도록 요청해야 합니다. list 뿐만 아니라 create, edit, delete에 대한 기능도 추가해주어야 합니다.

\n

이를 위해 UI 플러그인에서 현재 접속한 사용자의 Role 이름을 받아올 수 있어야 합니다. 이 때 flask의 global session을 활용하면 쉽게 받아올 수 있습니다.

\n
from flask import g\n\nrole_name = g.user.roles[0].name
\n

이제 UI에서 추가, 편집, 삭제 시 Secrets Backend를 통해 AWS Parameter Store에 반영됩니다. 오직 권한을 가진 사용자만이 DAG, Connection, Variable에 접근할 수 있습니다.

\n
\n

Cluster Policy

\n

DAG 작성에 대한 가이드가 있더라도 모두 만족하는지 체크하는건 상당히 번거로운 일 입니다.\nAirflow 2.0+에서는 Cluster Policy를 통해 클러스터 전체에서 DAG 또는 task에 대한 정책을 정의하고 강제하도록 설정할 수 있습니다. 예를 들면 다음과 같은 정책을 정의할 수 있습니다.

\n\n

airflow_local_settings.py 파일을 만들고 정의하면 적용할 수 있습니다.\n태그를 강제하는 정책 예시는 아래와 같습니다.

\n
def dag_policy(dag: DAG):\n    \"\"\"Ensure that DAG has at least one tag\"\"\"\n    if not dag.tags:\n        raise AirflowClusterPolicyViolation(\n            f\"DAG {dag.dag_id} has no tags. At least one tag required. File path: {dag.filepath}\"\n        )
\n

위 정책이 적용된 클러스터에 태그가 없는 DAG을 배포하는 경우, AirflowClusterPolicyViolation 오류가 발생하기 때문에 DAG을 등록할 수 없습니다.\n자세한 내용은 공식문서를 참고하시면 됩니다.

\n
\n

정리

\n

최근 Airflow Summit에서 Multi-Tenent와 관련된 영상들이 많이 올라와서 함께 참고하면 도움이 될 것 같습니다.

\n","excerpt":"…"}}},{"id":"685d6694-ca41-5c2f-89a2-86556223c62c","title":"Spark 2.2.0 릴리즈 업데이트 정리","slug":"spark22","publishDate":"July 14, 2017","publishDateISO":"2017-07-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

7월 11일 약 2개월 만에 Spark 2.2.0이 릴리즈 되었습니다.\n어떤 변경 사항들이 있었는지 릴리즈 노트를 통해 간략하게 정리해보았습니다.

\n
\n

pypi 를 통한 PySpark 설치

\n
pip install pyspark
\n

드디어 PySpark이 pip을 지원하게 되었습니다.\npip install pyspark 명령어를 통해 쉽게 설치 가능합니다.\n설치된 버전은 Spark 2.2.0 버전 입니다.

\n

numpy, pandas 파이썬 패키지에 dependency가 있으며,\n자세한 사항은 pypi 패키지 링크를 통해 확인하실 수 있습니다.\n이번 업데이트를 통해 standalone cluster에서 누구나 쉽게 사용해 볼 수 있을 듯 합니다.

\n
\n

Structured Streaming

\n

이번 버전부터 Structured Streaming이 새로 추가 되었습니다.\nStructured Streaming은 스트리밍 어플리케이션을 더 빠르고 쉽게 개발하기 위해 만들어진 패키지입니다.

\n

Spark Streaming이 내부적으로 RDD API를 지원하는 반면, Structured Streaming은 DataFrame, Dataset API를 지원합니다.\n언어는 Scala, Java, Python 모두 지원하며, readStream 이라는 메서드를 통해 다양한 저장소로부터 데이터를 읽을 수 있습니다.\n특히 이번 업데이트를 통해 Apache Kafka 스트리밍 지원이 추가되었습니다.

\n
# Subscribe to 1 topic\ndf = spark \\\n  .readStream \\\n  .format(\"kafka\") \\\n  .option(\"kafka.bootstrap.servers\", \"host1:port1,host2:port2\") \\\n  .option(\"subscribe\", \"topic1\") \\\n  .load()\ndf.selectExpr(\"CAST(key AS STRING)\", \"CAST(value AS STRING)\")
\n

Structured Streaming에 대한 자세한 내용은 http://spark.apache.org/docs/2.2.0/structured-streaming-programming-guide.html 에서 확인하실 수 있습니다.

\n
\n

MLlib

\n

예상했던 대로 MLlib에도 많은 변화가 생겼습니다.\nRDD-based MLlib이 아니라 DataFrame-based MLlib을 확인하시면 됩니다.

\n\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n\n
\n

Reference

\n\n
","excerpt":"7월 11일 약 2개월 만에 Spark 2.2.…"}}},{"id":"dac34ae1-16f1-5a77-9cc9-62b364430ad7","title":"빅데이터 처리에 Scala가 필요한 이유","slug":"scala-for-bigdata","publishDate":"March 17, 2017","publishDateISO":"2017-03-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

StackOverFlow나 Quora를 보면 Scala has taken over the Big Data world. 라는 글을 많이 볼 수 있습니다.\n게다가 Spark의 엔진은 Scala로 구현되어 있습니다. 이 포스팅에서는 데이터를 다루는데에 스칼라가 가지는 강점이 무엇인지 알아보고자 합니다.

\n
\n

Scala가 가지는 강점

\n

Static Typing, Type Inference

\n

스칼라의 val 변수는 한번 지정된 값을 바꾸지 않습니다.\n이러한 변수를 Immutable variable 이라고 부릅니다. 예를 들면 아래와 같습니다.

\n
val msg = \"Hello Scala\"\nString = Hello Scala\n\nval msg = \"Reassign to val\"\nerror: reassignment to val
\n

위의 예제를 보면, msg 변수에 문자열을 할당했지만 어디에도 String 이라는 단어는 없습니다.\n스칼라는 알아서 타입을 추론하여 지정해주기 때문입니다.\n따라서, val 변수에 재할당을 시도하면 reassignment to val 이라는 오류가 발생하게 됩니다.

\n

이처럼 스칼라는 input 타입을 보고 함수나 출력 값의 타입을 추론해주며 이를 통해 코드를 깔끔하게 유지할 수 있습니다. 또한, 다양하고 많은 데이터가 사용되는 경우 정적변수가 문제를 단순화 해주는 효과가 있습니다.

\n
\n

Scalable Language

\n

기존의 Hadoop 기반의 데이터 인프라는 자바 언어를 통해 MapReduce 연산 그리고 알고리즘을 구현해야했습니다.\n하지만 자바는 코드가 너무 길어 생산성 그리고 가독성이 매우 떨어집니다.

\n

스칼라는 모든 것들이 일관성있게 그리고 간결하게 구현되도록 설계되었습니다.\n이를 통해 얻을 수 있는 장점은 \"적은 양의 코드로 방대한 규모의 시스템을 작성할 수 있다\" 는 것입니다.

\n

연산자를 예로 들어보겠습니다.\n자바에서는 '==' 와 같은 비교연산자를 제공합니다.\n하지만 비교연산자는 주소값을 비교하기 때문에\nString과 같은 객체를 비교할 때는 equal() 메서드를 사용해서 비교해야 했습니다.\n이 또한 스칼라의 Scalable과 거리가 멉니다.\n스칼라에서는 모든 것이 Object이기 때문에 == 로 모든 비교가 가능합니다.

\n
\n

Object Oriented, Functional Language

\n
y1 = 2x + 5\ny2 = 4(y1) = 4(2x + 5)
\n

함수형 언어를 이해하기 전에 어렸을 때 배웠던 함수식을 떠올려보겠습니다.\n위의 식에서 x는 input, y는 output이 됩니다.\n우리는 어떤 함수에 input을 넣으면 output이 나온다고 이해하고 있습니다.\n그리고 아래의 식처럼 함수를 인자로 넣을 수도 있습니다 (합성함수).\n함수형 언어도 이와 비슷합니다.

\n

스칼라는 객체지향 프로그래밍과 함수형 프로그래밍을 모두 완벽하게 지원하는 언어입니다.\n스칼라에서는 모든 것이 객체이며 함수가 first object 입니다.\n함수를 마치 하나의 값으로 취급하며 이를 변수 또는 파라미터로 넘길 수 있습니다.

\n

모든 것을 함수로 해결하면 의도하지 않은 동작(Side Effect)이 발생할 일이 없고,\n한번 검증된 함수는 신뢰할 수 있기 때문에 버그가 줄어드는 효과가 있습니다.\n또한, Immutable 변수는 문제를 단순화시켜주기 때문에 데이터 공유, 병렬처리에 강합니다.

\n
\n

Java와 Scala를 비교해보자

\n

Scala는 Interactive한 Shell을 제공합니다.\n이렇게 바로 확인할 수 있는 Shell을 통해 데이터의 탐색적 분석이 가능합니다.\nIntelliJ IDEA에서도 Worksheet이라는 기능을 통해 사용할 수 있습니다.\n스칼라 개발환경은 Scala 2.12.1 이며, IDE는 IntelliJ IDEA 를 사용하였습니다.

\n

\n \n \n \n

\n

GFS는 크게 하나의 master node와 여러 개의 slave node로 구성되어 있습니다.\n기능으로 보면 Master, Chunk Server, Client로 이루어져 있습니다.

\n\n

수행과정은 다음과 같습니다.\n먼저 Client가 Master에게 파일의 읽기, 쓰기를 요청하게 되면,\nMaster는 Client와 가까운 Chunk Server의 정보를 Client에게 전달합니다.\nClient는 전달받은 Chunk Server와 직접 통신하며 IO 작업을 수행하게 됩니다.

\n

GFS의 엄청난 강점은 Failuer Tolerance 입니다.\n다시 말해서, 물리적으로 서버 중 하나가 고장이 나도 정지하지 않고 잘 돌아가도록 설계되었습니다.\n예를 들어, Chunk Server 중 하나가 고장이 나면 Master는 고장나지 않은 Chunk Server의 정보를 전달하고\nMaster Server가 고장이 나면 다른 서버가 Master를 대체하게 됩니다.\n이러한 이유로 Chunk Server는 가격이 저렴한 범용 컴퓨터들로 구성할 수 있게 되었고, 클러스터 환경에서 잘 동작할 수 있게 되었습니다.

\n
\n

MapReduce

\n

Map Reduce는 마찬가지로 2004년 구글의 논문(저자: 구글의 전설 제프 딘)을 통해 소개되었습니다.\n논문의 제목은 MapReduce: Simplified Data Processing on Large Clusters 입니다.\n즉, MapReduce는 말 그대로 대용량 분산 클러스터에서 데이터를 간단히 처리하는 방법입니다.

\n

그는 논문을 통해 2가지 Function을 제시하는데 바로 Map과 Reduce 입니다.\n논문에서 제시한 MapReduce의 예시 수도코드는 다음과 같습니다.

\n
map(String key, String value):\n    // key: document name\n    // value: document contents\n    for each word w in value:\n        EmitIntermediate(w, \"1\")\n\nreduce(String key, Iterator values):\n    // key: a word\n    // values: a list of counts\n    int result = 0;\n    for each v in values:\n        result += ParseInt(v)\n    Emit(AsString(result))
\n

먼저 Map 함수는 어떤 key-value를 input으로 받아서 각 단어와 관련 발생 횟수를 출력합니다.\n그리고 Reduce 함수는 특정 단어에 대해 생성된 모든 카운트를 합산합니다.

\n
map(k1, v1) -> list(k2, v2)\nreduce(k2, list(v2)) -> list(v2)
\n

Map 함수는 key-vale를 읽어서 필터링하거나 다른 값으로 변환시켜주며,\nReduce 함수는 Map을 통해 출력된 리스트에\n새로운 key를 기준으로 Groupping하고 이를 Aggregation한 결과를 출력합니다.

\n

\n \n \n \n

\n

MapReduce는 여러 대의 컴퓨터에서 데이터를 처리하는 경우, 병렬처리를 하기 때문에 확장이 쉽습니다.\n스케줄러가 데이터를 분산 배치하면 worker에서 작업을 수행하고 각 중간 결과는 로컬 디스크에 저장되며,\n나중에 Reduce 연산을 할당받으면 중간 결과를 읽어와서 작업을 수행하고 마찬가지로 파일 시스템에 저장합니다.\n위의 그림과 같이 Master 노드에 모든 데이터를 받아서 처리하던 옛날 방식과 통신 처리면에서 확실히 줄어든 것을 알 수 있습니다.

\n

구글은 MapReduce를 URL 접근빈도, Web-Link Graph를 계산하는데 사용하였고,\n이를 통해 인덱싱, 정렬 등에서 엄청난 성능향상을 보여주었습니다.

\n
\n

HDFS (Hadoop Distributed File System)

\n

Hadoop은 2006년 Doug Cutting과 Mike Cafarella가 개발한 분산처리 프레임워크입니다.\n이들은 구글의 GFS를 대체하기 위해 HDFSMapReduce 를 구현하였습니다.

\n

GFS가 C++로 구현되었다면, Hadoop은 자바로 개발된 데다가 아파치 재단의 오픈소스로 넘어가면서 인기가 많아졌습니다.\nGFS를 구현한 결과물이기 때문에 크게 달라진 것은 없으나\nYARN, Hadoop Ecosystem 등 다른 장점으로 인해 많이 사용됩니다.

\n
\n

Reference

\n\n
","excerpt":"…"}}},{"id":"93e10410-a3f1-5148-a79f-32f260c5b90d","title":"Spark의 Shuffling 이해하기","slug":"spark-shuffling","publishDate":"August 25, 2017","publishDateISO":"2017-08-25","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

효율적인 Spark Application을 개발하기 위해 Shuffling 은 상당히 중요한 개념입니다.\n이에 대해 간단히 정리해보았습니다.

\n
\n

Spark Architecture: Shuffle

\n

\n \n \n \n

\n

몇 가지 사례를 통해 더 자세히 알아보겠습니다.\n만일 데이터가 이미 키 값으로 파티셔닝 되어 있고 키 값에 대해 변화를 주고 싶다면, 좌측의 그림처럼 수행하게 됩니다.\nfilter(), sample(), map(), flatMap() 등의 transformation이 이에 해당하며, 이 경우 Shuffle이 필요 없습니다.\n이를 Narrow Transformation 이라고 합니다.

\n

반면, 서로 다른 파티션으로부터 특정한 값을 기준으로 추출하고 싶은 경우, 그 값을 기준으로 Shuffle이 발생하게 됩니다.\ngroupByKey(), reduceByKey() 등이 이에 해당하며, 이를 Wide Transformation 이라고 합니다.

\n
\n

Shuffled HashJoin

\n

\n \n \n \n

\n

두 개의 테이블을 Join 할 때에도 Shuffle 이 발생할 수 있습니다.\n위의 예시 처럼 두 테이블에서 키 값을 기준으로 Join 하게 되면, 동일한 키를 가진 데이터가 동일한 파티션으로 이동합니다.

\n

하지만 이 때, 셔플 되는 데이터의 양이 성능에 영향을 미칠 수 있습니다.\n만일 C의 데이터의 크기가 A보다 훨씬 크다면, C에 대한 작업으로 인해 전체의 수행시간이 오래 걸리게 될 것 입니다.

\n
\n

Broadcast HashJoin

\n

\n \n \n \n

\n

하지만 Glue ETL와 S3 Batch 서비스는 요금에 비해 활용도가 낮다고 생각한다.\n먼저 Glue ETL은 위 그림과 같이 input과 output을 정의하고 그 사이에 transform 작업을 정의할 수 있다.\nSpark의 DataFrame을 기반으로 하며 DynamicFrame, Built-In Transform 등을 사용하여 스크립트를 작성한다.\n서비스 중간에 추가되는 간단한 ETL Batch에 사용하기는 무난해보이지만 그게 아니라면 아래와 같은 사항들을 고려해야 한다.

\n
\n

Glue ETL은 DPU를 기준으로 요금이 계산된다

\n

Glue ETL의 요금은 DPU라는 하나의 처리 단위를 기준으로 산정되는데 1 DPU는 4CPU와 16GB의 메모리를 가진다.\nDPU 시간당 0.44 USD, 초 단위로 청구되며 Apache Spark 유형 ETL 작업당 최소 시간은 10분이다.\nSpark 기반의 ETL에서는 Executor에 대한 설정이 중요하다.\n작업에 따라 CPU가 많이 필요할 수도 있고 메모리가 많이 필요할 수도 있다.\n하지만 Glue는 DPU라는 단위로 고정되어 있다보니 비용 효율적으로 사용하기 어려웠다.\n만일 자체 클러스터를 사용하고 전체 파이프라인 내에서 리소스를 효율적으로 사용할 수 있다면\nGlueContext가 뜨는 시간까지 고려했을때 정말 저렴한 서비스인지 잘 모르겠다.

\n
\n

Glue ETL은 디버깅, 모니터링 기능이 아직 부족하다

\n

Spark에는 Spark UI 라는 휼륭한 모니터링 대시보드가 존재하지만 Glue에서는 아직 이를 지원하지 않는다.\n대신 자체적으로 CloudWatch를 통해 메모리, 로그를 제공하는데 아직 지표가 많이 부족해보였다.\nDAG가 어떻게 구성되는지와 Shuffle 관련 지표도 볼 수가 없어 무거운 작업이라면 많은 노력이 필요하다. 아직 오픈한지 얼마 지나지 않은 서비스라 이 부분은 앞으로 많이 개선될거라 생각한다.

\n
\n

Step Function을 사용한 ETL Workflow 관리

\n

Step Function은 Serverless 기반의 Workflow 서비스다.\n여기에서는 가장 많이 사용하는 Airflow와 비교해가며 Serverless ETL이 가지는 특징을 설명해보려 한다.

\n
\n

Step Function은 ASL이라는 언어로 정의된다

\n

Step Function에 들어가는 각 단계에는 Lambda, Fargate 등의 서버리스 서비스가 들어갈 수 있다.\n그리고 각 단계는 Amazon States Language 라는 json 기반의 구조화된 언어로 정의된다.\nAirflow가 많이 사용되는 이유 중에 하나가 파이썬으로 DAG를 구성할 수 있다는 점인데\n이에 비해 json 기반의 Step Function은 너무 복잡하게 느껴졌다.

\n
\n

Step Function에는 Operator, Sensor가 없다

\n

\n \n \n \n

\n

반면, groupByKey는 각 노드에 있는 데이터에 대해 바로 Shuffle 과정을 거치게 되고 결과를 내보냅니다.\n따라서 groupByKey는 네트워크를 통해 전송되는 데이터의 양이 많아질 뿐만 아니라, Out of disk 문제가 발생할 수도 있습니다.

\n

Shuffle은 기본적으로 비용이 큰 연산입니다.\ngroupByKey는 reduceByKey로 대체될 수 있기 때문에 많은 문서에서 이를 권장하고 있습니다.

\n
\n

Reference

\n\n
","excerpt":"Spark Application 성능 개선을 위한 에 대해 알아보겠습니다. groupByKey vs reduceBykey…"}}},{"id":"990a6e60-c773-50b0-a6c0-a9c79431c620","title":"AWS EMR에서 S3 사용 시 주의사항","slug":"aws-emr-s3-spark","publishDate":"September 09, 2017","publishDateISO":"2017-09-09","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

AWS EMR에서 Spark을 사용하는 경우, S3를 저장소로 사용하는 경우가 많습니다.\n이때 주의해야 할 사항들을 정리해보았습니다.

\n\n
\n

AWS EMR, Spark 그리고 S3

\n

\n \n \n \n

\n
\n

Daily로 돌려야 하는 ETL 작업의 경우 위와 같이 간단한 아키텍쳐로 구성하는 경우가 많습니다.\n대부분의 경우 저장소로 S3를 적극 활용하게 됩니다.\n최초 입수되는 로그를 저장하기도 하고, Transformation 작업 이후 중간 또는 최종 데이터로 저장하기도 합니다.

\n
\n

문제 상황

\n
java.io.IOException: Connection reset by peer\nERROR ContextCleaner: Error cleaning broadcast 5
\n

최근 Spark RDD 코드를 DataFrame으로 리팩토링 하던 중에 위와 같은 오류를 겪었습니다.\n일별 로그를 불러와서 전처리하고 다시 저장하는데 s3 write 부분에서 갑자기 Executor의 Connection이 끊기는 문제였습니다.

\n

\n \n \n \n

\n
\n

Ganglia 모니터링 결과를 보면 중간에 약 15분의 공백이 있는데,\n이 부분이 Connection이 중간에 끊기고 다시 뜰 때까지 걸리는 시간입니다.

\n
\n

S3N, S3A, S3

\n

먼저 S3는 File System이 아닌 Object Storage 라는 점을 알고 계셔야 합니다.\n따라서, S3에 분산저장하는 경우, 우리는 Hadoop 클라이언트를 거쳐 저장하게 됩니다.\nHadoop은 S3N, S3A, S3 이렇게 세 가지 시스템 클라이언트를 제공합니다. 각 클라이언트는 URI 스키마를 통해 접근할 수 있습니다.

\n\n

EMR은 EMRFS 라는 파일 시스템이 별도로 존재합니다.\nEMR의 S3 파일 시스템과 Hadoop에서의 S3 파일 시스템은 서로 다르기 때문에 항상 주의하셔야 합니다.\nEMR의 경우 s3 로 사용하는 것을 권장하고 있습니다. 반면에 s3a의 경우 EMRFS와 호환되지 않는다고 합니다.\n물론 실행 될 때도 있지만 위와 같은 오류가 발생할 수도 있습니다.

\n
\n

Parquet 저장 성능 개선하기

\n

위의 오류는 URI를 s3로 수정해서 해결할 수 있었습니다.\n하지만 S3에 parquet로 저장하는 속도가 너무 느려 이 부분을 개선해보기로 했습니다.

\n

먼저 Spark에는 Parquet 빌드 속도를 개선하기 위해 DirectParquetOutputCommitter라는 기능이 있었습니다.\n하지만, S3에 저장할 때 이 기능을 사용하는 경우 데이터 유실이 발생할 수 있었습니다.\nSPARK-10063 JIRA 티켓 참고

\n

이러한 이유로 Spark 2.0 버전부터 이 옵션은 사라졌습니다. 그러나, 성능 개선이 필요했기 때문에 Spark 사용자들은 대안을 요구했습니다.\n본래의 FileCommiter가 느린 이유는 rename 연산 때문이었습니다.\n실제 파일 시스템(HDFS)에서 rename 연산은 대상 파일 시스템의 임시 디렉토리로 출력 한 다음, 디렉토리의 이름을 커밋하는 방식으로 O(1)이 소요됩니다.\n하지만 Object Storage에 저장하는 경우, 데이터 사이즈만큼 O(N)이 소요됩니다.

\n

이 문제는 s3guard와 s3a의 도움으로 해결되었습니다.\ngetFileStatus()에서의 S3 HTTP 콜을 생략하고 dynamo metadata 저장 등을 통해 해결했다는데 자세한 내용은 MAPREDUCE-4815 JIRA 티켓을 보시는게 나을 듯 합니다.

\n
spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version 2\nspark.speculation False
\n

적용하는 방법은 위의 Spark property 옵션을 추가해주시면 됩니다. Spark 2.1, Hadoop 2.7.2 버전 이상부터 사용가능 합니다.\n하지만 Spark 문서에도 나와있듯이 아직 failure에 대한 보장이 떨어집니다.\n따라서 먼저 로컬 HDFS에 임시저장 후 distcp 명령어를 사용하여 S3로 저장해주시면 됩니다.\nHadoop 2.8 버전부터는 s3guard가 기본으로 들어가기 때문에 안정화 될 것 이라고 합니다.

\n

결과는 로그 1억 건 기준 약 10배 의 성능 개선을 확인할 수 있었습니다.\n두서없이 정리하다보니 좀 글이 복잡해졌네요. 결론은 '옵션을 추가하자' 입니다.

\n
\n

Reference

\n\n
","excerpt":"AWS EMR에서 Spark을 사용하는 경우, S…"}}},{"id":"376bb950-886b-5e07-b4c5-4a8ab940dfb2","title":"Hive Metastore 구축 관련 문제와 해결과정","slug":"hive-metastore-issue","publishDate":"August 11, 2017","publishDateISO":"2017-08-11","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.1 입니다.

\n
\n

Hive Partition

\n
CREATE EXTERNAL TABLE table_name (\ncol1 STRING,\ncol2 STRING\n)\nPARTITIONED BY (key STRING)\nSTORED AS PARQUET\nLOCATION 'location';
\n

Hive에서 보통 위와 같은 쿼리로 테이블을 생성합니다.\nMetastore는 말 그대로 외부에 있는 테이블의 정보(스키마, 파티션 등)를 저장하는 개념입니다.\n따라서 EXTERNAL TABLE 로 생성하지 않은 상태에서 테이블을 DROP 시키면 다 날아가게 됩니다.

\n
ALTER TABLE table_name\nADD PARTITION (key='2017-08-11');
\n

도중에 Partition key를 추가하고 싶을 때는 위와 같은 쿼리를 통해 추가할 수 있습니다.\n그러나, 추가한 정보가 바로 반영이 안될 때가 있습니다.

\n

이 경우에는 MSCK REPAIR TABLE table_name; 쿼리로 해결할 수 있습니다.\nMSCK는 Metastore Check의 약자라고 합니다.

\n
\n

Hive Metastore, Parquet

\n

먼저 겪었던 문제에 대해 설명드리자면 Hive Metastore에 분명히 테이블이 들어가있고,\nHue에서는 잘 보이는데 Zeppelin에서는 모든 데이터에 null 값이 찍혀있었습니다.

\n

우선 Spark으로 Hive를 사용하는 방식이 2.0 버전 이후 부터 조금 변경되었습니다.\n이전에는 HiveContext를 사용했다면, 이제 SparkSession에서 .enableHiveSupport() 추가만 하면 됩니다.\n제플린에서는 SparkSession이 spark이라는 변수로 제공되는데,\n이 경우 interpreter에 zeppelin.spark.useHiveContext=true를 추가해서 사용할 수 있습니다.

\n

다시 문제로 돌아와서 좀 더 확인해보니 컬럼명에 대문자가 들어가면 모든 값이 null로 출력되고 있었습니다.\nSpark 공식문서에 이와 관련된 내용이 잘 나와있습니다.

\n

Spark SQL에서 Hive metastore로 데이터를 불러오는 경우, 성능 상의 이슈로 SerDe 대신 Spark SQL의 MetastoreParquet 를 사용합니다.\n이때 주의사항으로 Hive는 대소문자를 구분하지 않지만, Parquet는 구분합니다. (Hive is case insensitive, while Parquet is not)

\n

이를 위해 Spark 2.1.1 버전부터 새로운 Spark Properties가 추가되었습니다.

\n

따라서, Zeppelin interpreter에 아래의 설정 값을 추가해주시면 해결됩니다.\nspark.sql.hive.caseSensitiveInferenceMode = INFER_AND_SAVE

\n
\n

Hive TBLPROPERTIES

\n

위에서 말한대로 Spark Properties를 추가하면,\nHive metastore의 parameter에 spark.sql.sources.schema.part가 생기게 됩니다.

\n

여기에서 \"field: name\"에 대소문자가 잘 구분되는 경우, 문제가 없지만 간혹 소문자로 들어오는 경우가 있습니다.\n이 경우에는 아래의 쿼리를 통해 Hive parameter를 수정해주시면 됩니다.

\n
ALTER TABLE table_name SET TBLPROPERTIES (\"spark.sql.sources.schema.part.0\" = \"fix this line\");
\n
\n

Reference

\n\n
","excerpt":"Hive Metastore를 구축하면서 겪은 이슈와 해결과정을 기록해두려고 합니다.\n사용 환경은 Spark 2.1.1, Hive 2.1.…"}}},{"id":"23b4638b-e66d-5c9f-8991-cf5a0965756b","title":"Kafka Connect로 S3에 데이터를 저장해보자","slug":"kafka-connect","publishDate":"November 16, 2018","publishDateISO":"2018-11-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent에서 제공하는 Kafka-Connect-S3를 활용하여\nS3로 데이터를 저장하는 방법에 대해 정리해보려고 합니다.

\n
\n

Kafka Connect

\n

\n \n \n \n

\n

우리는 서버로부터 생성되는 데이터를 실시간으로 Kafka에 보내기도 하고,\nKafka Topic에 쌓여있는 데이터를 실시간으로 RDBMS, Object Storage와 같은 시스템에 보내기도 합니다.\nKafka Connect는 위의 그림과 같이 다양한 시스템과 Kafka 사이의 연결을 도와주는 역할을 하는 컴포넌트입니다.\nSource System에서 Kafka로 들어가는 Connector를 Source Connect라 부르고,\nKafka에서 Target System으로 보내는 Connector를 Sink Connect라 부릅니다.

\n

Kafka Connect는 JSON, Avro, Protobuf 등의 다양한 직렬화 포멧을 지원하며\nKafka Schema Registry와 연동시켜 공통된 스키마 지정을 할 수도 있습니다.

\n

사실 Fluentd와 ELK Stack에서 사용하는 Logstash 등 서로 다른 시스템 간의 브릿지 역할을 하는 프레임워크들은 다양하게 존재합니다.\n하지만 Kafka Connect가 갖는 강점은 Kafka와 긴밀히 연동되어 있다는 점 입니다.

\n

Kafka Connect를 사용하지 않고 데이터를 실시간으로 전달하기 위해서는 Producer, Consumer API를 사용해야 합니다.\n이 과정에서 이미 처리되거나 실패한 데이터를 추적한다거나, 데이터 분산처리, 작업을 배포하는 등의 작업을 수행해야만 합니다.

\n

Kafka Connect는 앞의 모든 작업을 수행할 뿐만 아니라 connector task를 클러스터 전체에 자동으로 배포합니다.\n또한, Connect Worker 중에 하나가 실패하거나 Network partition이 발생하더라도 실행하던 작업을 나머지 Worker들에게 자동으로 재조정합니다.\nOffset을 자동으로 관리, 유지하기 때문에 재시작하더라도 중단 시점부터 다시 시작할 수 있고 (Exactly Once Delivery),\nHigh performance Kafka library로 작성되어 빠르며 불필요한 polling 작업을 수행하지 않습니다.\n무엇보다 코드 한 줄 없이 사용하기 편하다는 것도 큰 강점입니다.\n혹시 Kafka를 이미 중앙 집중형 로그 저장소로 사용하고 있다면 Kafka Connect를 고려해볼만 하다고 생각합니다.

\n
\n

Kafka-Connect-S3

\n

이 글에서는 Confluent로 Kafka를 설치하지 않은 경우를 예시로 들겠습니다.\n이미 confluent-hub를 설치하셨거나 Confluent로 Kafka를 설치하셨다면 공식문서를 따라가시면 됩니다.

\n

\n \n \n \n

\n

데이터 인프라가 AWS 환경에 구축되어 있다면 S3를 Cold Storage로 많이 사용하게 됩니다.\n최대한 단순하게 그림을 그려보면 위의 그림과 같은 아키텍쳐가 나오게 됩니다.\n여기에서는 Kafka에서 S3로 실시간 데이터를 저장하기 위해 Kafka-Connect-S3를 사용하게 됩니다.

\n

먼저 confluent에서 kafka-connect-s3를 다운받아 plugins 경로에 추가합니다.

\n
$ wget https://api.hub.confluent.io/api/plugins/confluentinc/kafka-connect-s3/versions/4.1.1/archive\n$ unzip archive\n$ mkdir -p plugins/kafka-connect-s3\n$ cp confluentinc-kafka-connect-s3-4.1.1/lib/* plugins/kafka-connect-s3/
\n

이제 kafka config 경로에 connect.properties라는 이름으로 설정 파일을 추가합니다.\nbootstrap.serversplugin.path 경로는 상황에 맞게 수정하시면 됩니다.\n추가로 kafka 클러스터를 private network로 연결하고 싶다면 9093 포트를 사용해주시면 됩니다.

\n
# Kafka broker IP addresses to connect to\nbootstrap.servers=localhost:9092\n\n# Path to directory containing the connector jar and dependencies\nplugin.path=/home/ec2-user/kafka/plugins\n\n# Converters to use to convert keys and values\nkey.converter=org.apache.kafka.connect.storage.StringConverter\nvalue.converter=org.apache.kafka.connect.storage.StringConverter\n\n# The internal converters Kafka Connect uses for storing offset and configuration data\ninternal.key.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.value.converter=org.apache.kafka.connect.json.JsonConverter\ninternal.key.converter.schemas.enable=false\ninternal.value.converter.schemas.enable=false\noffset.storage.file.filename=/tmp/connect.offsets
\n
\n

기존 클러스터에 Authentication credentials, encryption이 설정되어 있다면,\nconnect.properties에 관련 설정을 추가해주셔야 합니다.

\n

다음 S3에 데이터가 저장될 Bucket을 생성하고, AWS Credentials를 설정합니다.

\n
$ pip install awscli\n$ aws configure
\n

sink connector 관련 설정 파일을 s3-sink.properties라는 이름으로 config 경로에 추가합니다.\ntopics와 s3.bucket.name의 이름은 맞게 수정해주셔야 합니다.

\n
name=s3-sink\nconnector.class=io.confluent.connect.s3.S3SinkConnector\ntasks.max=1\ntopics=my-topic-name\ns3.region=ap-northeast-2\ns3.bucket.name=my-bucket-name\ns3.compression.type=gzip\ns3.part.size=5242880\nflush.size=3\nstorage.class=io.confluent.connect.s3.storage.S3Storage\nformat.class=io.confluent.connect.s3.format.json.JsonFormat\nschema.generator.class=io.confluent.connect.storage.hive.schema.DefaultSchemaGenerator\npartitioner.class=io.confluent.connect.storage.partitioner.TimeBasedPartitioner\npartition.duration.ms=3600000\npath.format=YYYY-MM-dd\nlocale=KR\ntimezone=UTC\nschema.compatibility=NONE
\n
\n

이제 Kafka 설치 경로로 이동하고 Kafka-Connect를 실행시킵니다.\n여기에서는 standalone mode로 실행시켰지만, 경우에 따라 cluster mode로 실행하거나\ndocker container로 실행시켜도 됩니다.

\n
./bin/connect-standalone.sh connect.properties s3-sink.properties
\n

이제 지정한 S3 Bucket의 topic/my-topic-name/2018-11-16 경로에 가시면\n지정한 설정 값에 따라 파일이 저장되는 것을 확인하실 수 있습니다.

\n

\n \n \n \n

\n

이미 Yahoo의 kafka-manager를 사용하고 계신 분들은 consumers 메뉴로 가시면\ntopic 마다 lag도 모니터링할 수 있습니다.

\n
\n

Kafka-Connect-S3 Configuration

\n

데이터 인프라에 맞게 수정해야할 옵션은 아래와 같습니다.

\n\n

이외에도 Avro Format과 Schema Registry를 사용하신다면 format.class, schema.generator.class를 수정해야 합니다.\n더 자세한 내용은 공식문서에서 확인하시면 됩니다.

\n
\n

Reference

\n

사실 Kafka는 이미 대부분의 데이터 파이프라인에서 활용하고 있다는 것이 강점이라고 생각합니다.\nETL 과정이 다양하고 복잡할 수록 새로운 프레임워크가 추가되고 아키텍쳐가 복잡해지기 마련인데,\nKafka의 다양한 컴포넌트들을 잘 활용하면 아키텍쳐를 단순화시킬 수도 있습니다.

\n","excerpt":"Kafka에는 정말 유용한 컴포넌트들이 존재합니다.\n오늘은 그 중 하나인 Kafka-Connect에 대해 알아보고,\nConfluent…"}}},{"id":"b68b3f15-e560-5485-9b60-204947689edd","title":"Jupyter에서 Scala로 Spark 사용하는 방법","slug":"jupyter-spark","publishDate":"March 22, 2017","publishDateISO":"2017-03-22","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

이 글은 평소에 Jupyter Notebook 에 익숙해져있는 분들께 유용할 듯 합니다.\nZeppelin Notebook을 설정하는 방법은 이전 포스팅을 참고하시면 됩니다.

\n
\n

Apache Toree

\n

\n \n \n \n

\n

Apache Toree 는 Jupyter 커널을 통해 Spark에 접속하도록 해주는 아파치 오픈소스 프로젝트입니다.\n기존의 IPython Notebook은 파이썬에 제한되어 있었지만\nJupyter Kernel을 통해 다른 언어까지 확장 가능하도록 바뀌었습니다 (왼쪽 그림 참조).

\n

여기에서 더 나아가 Apache Toree는 Toree Kernel 을 통해 바로 Spark Driver에 연결함으로써,\nJupyter에서 Scala 언어로 Spark Driver/Context를 사용할 수 있게 만들었습니다.

\n

Toree가 Zeppelin과 다른 점은 Jupyter protocol 을 사용할 수 있다는 점 입니다.\n이미 수많은 생태계가 구축되어 있는 Jupyter에서 Spark가 잘 돌아간다면 굳이 Zeppelin을 쓸 필요가 있을까요 (시각화가 어마어마한 강점이긴 합니다).

\n

GitHub: https://github.com/apache/incubator-toree

\n
\n

Jupyter Notebook에 Toree 설치하기

\n

Jupyter 노트북 커널 설정하는 방법은 Jupyter Notebook 다중커널 설정하기를,\nScala와 Spark을 설치하는 방법은 OS X에서 Homebrew로 Spark, Zeppelin 설치하기를 참고하시기 바랍니다.

\n

Toree는 아직 pre 버전만 존재하기 때문에 --pre 옵션을 붙여주시거나 파이썬 패키지를 통해 설치해주시면 됩니다.\n설치가 완료되면 jupyter kernel에 toree kernel을 설치해주는 과정이 필요한데 명령어를 통해 이 과정을 자동으로 진행합니다.

\n
$ pip install https://dist.apache.org/repos/dist/dev/incubator/toree/0.2.0/snapshots/dev1/toree-pip/toree-0.2.0.dev1.tar.gz\n$ jupyter toree install
\n

혹시 FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/spark/python/lib'\n이런 오류가 난다면, Spark 경로 환경변수를 읽지 못하는 문제입니다. Homebrew 를 통해 설치하셨다면 다음과 같이 환경변수를 등록해주시면 됩니다.

\n
$ export SPARK_HOME=/usr/local/Cellar/apache-spark/2.1.0/libexec
\n
\n

잘 동작하는지 테스트를 해보자

\n

\n \n \n \n

\n

잘 설치되었다면 new 했을 때 Apache-Toree Scala가 보이실 겁니다.\n잘 동작하는지 간단한 WordCounter 예제를 실행시켜 보시면 잘 동작하는 것을 확인할 수 있습니다.

\n

\n \n \n \n

\n

만일 위 그림처럼 여러 노드로 이루어진 분산 서버에서 합의를 이루어내야한다면 어떻게 해야할까요?\n이러한 문제를 distributed consensus problem 이라고 합니다.

\n
\n

Raft Algorithm

\n

Raft의 node는 Follower, Candidate, Leader라는 3가지 state를 가집니다.\n모든 노드는 처음에 Follower state를 가지고 시작합니다.\n만일 Follower가 Leader의 응답을 받지 못하면 Candidate 상태로 전환될 수 있습니다.

\n

\n \n \n \n

\n

Candidate는 다른 노드들에게 투표를 요청하고 노드들은 투표 결과를 응답으로 전달합니다.\n노드 중 가장 많은 표를 얻은 노드는 Leader가 될 수 있습니다.\n이러한 프로세스를 Leader Election 이라고 부릅니다.

\n
\n

Leader Election

\n

Raft는 투표를 관리하기 위해 두 가지 timeout 설정을 가지고 있습니다.\n첫 번째는 Election timeout 입니다.\nElection timeout 이란, Follower에서 Candidate로 전환되기 위해 기다리는 시간을 의미합니다.\n일반적으로 Election timeout은 150ms에서 300ms 사이의 값으로 랜덤하게 설정됩니다.

\n

\n \n \n \n

\n
\n

Log Replication

\n

\n \n \n \n

\n

Leader가 선정되고 난 이후, 시스템의 모든 변화는 Leader를 통해 이루어집니다.\n클라이언트는 Leader에게 데이터를 전달하고, Leader는 데이터의 복제하여 Follower에게 전달합니다.\n이 과정은 앞서 언급했던 Append Entries 메세지를 통해 이루어집니다.

\n

\n \n \n \n

\n

Follower는 받은 데이터를 commit 하고 결과를 Leader에게 전달합니다.\nLeader는 Follow로부터 받은 결과를 Client에게 전달합니다.

\n
\n

Reference

\n

정리하자면 분산 시스템은 fault-tolerence를 보장하기 위해 consensus algorithm을 사용하고 있고,\n분산 시스템을 다루는 프레임워크마다 Consensus 구현이 조금씩 다를 수 있습니다.\n그리고 원활한 Leader Election을 위해 클러스터 노드의 개수는 홀수로 구성하는 것이 좋습니다.

\n

Raft의 경우 Redis cluster에서 응용하여 사용하고 있고,\nElasticsearch cluster 또한 quorum-based consensus algorithm을 사용하고 있습니다.\n아래의 Raft 논문과 시각화 자료 링크를 보시면 더 쉽게 이해할 수 있습니다.

\n\n
","excerpt":"Consensus란 분산 시스템에서 노드 간의 상태를 공유하는 알고리즘을 말합니다.\n가장 유명한 알고리즘으로 Paxos…"}}},{"id":"a393498e-de9e-5231-bc9f-fd1df0495f45","title":"Apache Airflow에 기여하면서 배운 점들","slug":"airflow-contrib","publishDate":"December 08, 2018","publishDateISO":"2018-12-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow 프로젝트에 대한 설명은 다른 글에서도 많이 다루기 때문에 생략하고\n이 글에서는 처음으로 아파치 프로젝트에 기여해본 경험을 정리해보려 한다.

\n
\n

기여하게 된 배경

\n

당시에 관리하던 데이터 인프라에는 의존성이 얽혀있는 배치 작업이 상당히 많았다.\n여기에서 의존성이 얽혀있다는 말은 A 작업과 B 작업이 성공적으로 끝나고 난 뒤 C 작업을 해야하는 경우를 말한다.\n또한 각 작업들은 서로 다른 시간에 스케줄링 되어야 했고, 작업이 실패하는 경우 재시도 또는 특정 로직을 실행시킬 수 있어야 했다.

\n

처음에는 단순한 구조이다 보니 스크립트로 관리했지만 점차 늘어나는 운영 이슈에 대응하기 위해 Airflow를 활용하기로 결정했다.\n하지만 운영하다 보니 AWS 관련 컴포넌트들의 여러 버그를 발견하게 되었고 이를 수정하기 위해 PR을 추가했었다.

\n
\n

아파치 프로젝트 PR 프로세스

\n

아파치 프로젝트는 이슈 관리 도구로 JIRA를 사용한다. CI 도구는 프로젝트마다 다른 편인데 Airflow의 경우 TravisCI를 사용한다.\n모든 프로젝트에는 처음 프로젝트에 기여하려는 개발자를 위해 CONTRIBUTING.md 라는 문서를 제공한다.\n문서에는 개발 및 테스트 환경을 어떻게 구축해야하는지, 지켜야할 규칙, PR 가이드라인 등에 대해 설명되어 있다.\n그리고 PR template를 준수해야 하는데 잘 모르겠다면, 이전 PR들을 확인하고 비슷한 양식으로 작성하면 된다.

\n

내가 처음 접했던 Airflow 문서에는 AWS 관련 Hook, Operator도 반영되어 있지 않았다.\n그래서 첫 PR로 AWS, GCP 관련 컴포넌트를 업데이트하는 문서 기여를 하게 되었다.\n문서 관리에는 readthedocs를 사용하고 있었고 Sphinx 빌드를 통해 문서를 확인할 수 있었다.

\n

사용하다보니 특히 EMR 관련 Hook과 Operator에 버그가 많았다.\n만일 JIRA에 이미 등록되어 있는 이슈가 아니라면 이슈를 새로 생성한 다음 PR을 추가해주어야 한다.

\n

\n \n \n \n

\n

비슷한 이슈를 겪고 있는 사람들이 있어서 좀 신기했다.\n그리고 아주 작은 수정이라도 테스트 케이스를 추가해야 한다는 사실을 알게 되었다.

\n

\n \n \n \n

\n

양식만 잘 지키면 커미터들은 정말 친절하다. 내가 파악하지 못한 부분까지 알려주고, 코드 리뷰도 받을 수 있다.\n다른 PR을 참고하면서 많이 배울 수 있었다.

\n
\n

클라우드 인프라 테스트 방법

\n

AWS는 기본적으로 클라우드 환경이다.\n따라서 과금문제로 인해 실제로 추가, 변경한 오퍼레이터가 잘 동작하는지 매번 확인해보기가 힘들다.\nAirflow에서는 AWS 서비스를 Mocking 하기 위해 moto 라는 라이브러를 활용해서 테스트를 작성한다.

\n
@mock_s3\ndef test_my_model_save():\n    # Create Bucket so that test can run\n    conn = boto3.resource('s3', region_name='us-east-1')\n    conn.create_bucket(Bucket='mybucket')\n    model_instance = MyModel('steve', 'is awesome')\n    model_instance.save()\n    body = conn.Object('mybucket', 'steve').get()['Body'].read().decode()\n\n    assert body == 'is awesome'
\n

위와 같이 moto에서 미리 정의한 mock object를 decorator를 사용하여 쉽게 활용할 수 있다.\n하지만 AWS에서 공식으로 지원하는 라이브러리가 아니다보니 업데이트가 늦어지기도 한다.\n이런 이유로 인해 unittest의 mock으로 작성된 테스트 코드도 많이 있다.

\n
class TestEmrAddStepsOperator(unittest.TestCase):\n    # When\n    _config = [{\n        'Name': 'test_step',\n        'ActionOnFailure': 'CONTINUE',\n        'HadoopJarStep': {\n            'Jar': 'command-runner.jar',\n            'Args': [\n                '/usr/lib/spark/bin/run-example'\n            ]\n        }\n    }]\n\n    def setUp(self):\n        configuration.load_test_config()\n\n        # Mock out the emr_client (moto has incorrect response)\n        self.emr_client_mock = MagicMock()\n        self.operator = EmrAddStepsOperator(\n            task_id='test_task',\n            job_flow_id='j-8989898989',\n            aws_conn_id='aws_default',\n            steps=self._config\n        )\n\n    def test_init(self):\n        self.assertEqual(self.operator.aws_conn_id, 'aws_default')\n        self.assertEqual(self.operator.emr_conn_id, 'emr_default')\n\n    def test_render_template(self):\n        ti = TaskInstance(self.operator, DEFAULT_DATE)\n        ti.render_templates()\n\n        expected_args = [{\n            'Name': 'test_step',\n            'ActionOnFailure': 'CONTINUE',\n            'HadoopJarStep': {\n                'Jar': 'command-runner.jar',\n                'Args': [\n                    '/usr/lib/spark/bin/run-example'\n                ]\n            }\n        }]\n\n        self.assertListEqual(self.operator.steps, expected_args)\n\nif __name__ == '__main__':\n    unittest.main()
\n

unittest로 작성된 테스트 케이스는 API로 주고 받는 json을 직접 정의해줘야 하는 번거로움이 있다.\n테스트 케이스를 작성하고 난 다음 바로 PR을 추가하는 것보다 로컬 CI를 미리 돌려보는게 좋다.

\n

\"\"

\n

TravisCI는 오픈소스인 경우 무료로 사용할 수 있으며, yml 파일에 미리 정의되어 있으니 참고하면 된다. 로컬에서 CI가 통과되고 나면 PR을 추가해도 좋다.\n작업이 길어지면서 커밋이 여러 개로 늘어나는 경우, commit을 squash 해주는 것이 좋다.\n(나중에 문제가 생겼을 때 쉽게 rebase 하기 위함)

\n
\n

잡다한 정리

\n\n

그 동안 5개 정도의 버그를 해결했고 수정했던 AWS EMR 관련 버그들은 1.9 - 10 버전에 모두 반영 되었다.\n이외에도 Airflow에는 여전히 자잘한 버그가 많이 남아있다.\n(Docker로 운영했을 때 로그가 이상하게 나타난다거나, SubDag Deadlock 문제 등)\n당시에 블로그를 열심히 했다면 운영 관련해서 글을 남겼을텐데 하는 아쉬움이 남아있다.

\n

어쨋든 Airflow를 적용하고 난 뒤, 편히 새벽에 잠들 수 있게 되었다.\n지금은 머신러닝 파이프라인 관련 도구가 많이 나왔지만, Airflow도 충분히 해당 영역을 커버할 수 있다.

\n

그리고 오픈소스에 대해 다시 한번 생각해보게 되었다.\n많은 사람들이 참여하는 오픈소스이다 보니 당연히 버그나 이슈가 생길 수 있고,\n문제가 생겼을 때 고쳐달라고 강요하거나 기다리는 것보다 스스로 수정해서 기여하는 것이 올바른 태도가 아닌가 싶다.

","excerpt":"Apache Airflow는 코드를 통해 워크플로우를 관리하고 모니터링 할 수 있도록 도와주는 플랫폼이다.\nAirflow…"}}},{"id":"e7b082d0-f9d8-5371-aeac-66452691f800","title":"Airflow on Kubernetes (3)","slug":"airflow-on-kubernetes-3","publishDate":"February 05, 2021","publishDateISO":"2021-02-05","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

최근 Airflow에는 Kubernetes 지원을 위해 다양한 컴포넌트들이 추가되고 있습니다. 이러한 변화의 흐름에 따라 Airflow를 Kubernetes 위에 배포하고 운영하는 방법에 대해 글을 작성해보고자 합니다. 이 글은 시리즈로 연재됩니다.

\n\n
\n

Airflow Logging

\n

\n \n \n \n

\n

AWS MWAA 처럼 S3를 DAG 저장소로 활용하고 싶은 경우에 S3 Sync 사이드카 컨테이너를 통해 구현할 수 있습니다. S3 Sync 사이드카 컨테이너는 S3 버킷에 올라간 파일을 DAG 경로에 주기적으로 동기화하는 컨테이너입니다. 만약 DAG Serialiaztion 옵션이 활성화되어 있다면 scheduler에만 정의하면 됩니다.

\n

예시는 아래와 같습니다.

\n
scheduler:\n  extraContainers:\n    - name: s3-sync\n      image: myrepository/s3-sync:latest\n      imagePullPolicy: Always\n      volumeMounts:\n        - name: dags\n          mountPath: /opt/airflow/dags\n      env:\n        - name: AWS_BUCKET\n          value: airflow-src\n        - name: KEY_PATH\n          value: dags\n        - name: DEST_PATH\n          value: /opt/airflow/dags\n        - name: INTERVAL\n          value: \"10\"
\n
\n

위와 같이 인스턴스마다 서로 다른 설정이 필요한 값들은 환경변수로 구성할 수 있도록 이미지를 정의합니다. S3 접근 권한은 직접 credential을 사용하는 것보다 EKS의 IRSA를 활용해서 Role 기반으로 제어하는 편이 좋습니다. Dockerfile은 s3sync 저장소를 참고하시면 됩니다.

\n
\n

2. Permission Sync Container

\n

2.0 부터 추가된 DAG level Permission을 사용하는 경우, airflow sync-perm 명령어를 통해 DAG 권한을 갱신해주어야 Role에 권한제어가 정상적으로 반영됩니다. Permission Sync 컨테이너는 webserver에서 주기적으로 sync-perm 명령어를 수행하는 역할을 합니다.

\n

예시는 아래와 같습니다.

\n
webserver:\n  extraContainers:\n    - name: sync-perm\n      image: apache/airflow:2.1.2-python3.7\n      imagePullPolicy: Always\n      command: [\"/bin/sh\"]\n      args: [\"-c\", \"while true; do airflow sync-perm; sleep 60; done\"]\n      volumeMounts:\n        - name: dags\n          mountPath: \"/opt/airflow/dags\"\n      env:\n        - name: AIRFLOW__CORE__SQL_ALCHEMY_CONN\n          valueFrom:\n            secretKeyRef:\n              key: connection\n              name: airflow-dev-airflow-metadata
\n
\n

보시면 Airflow 이미지와 정의된 connection을 재활용 합니다. 컴포넌트 컨테이너와 분리되어 있으니 사이드카에서 발생하는 로그만 따로 확인할 수도 있습니다.

\n
\n

3. Kerberos Container

\n

클러스터에 접근하기 위해 Kerberos 인증이 필요한 경우, Kerberos 컨테이너를 활용하면 인증 토큰 갱신을 자동화할 수 있습니다. Airflow 공식 문서의 production-deployment 부분을 보면 아래와 같은 내용이 있습니다.

\n
\n

In the Kubernetes environment, this can be realized by the\nconcept of side‐car, where both Kerberos token refresher and\nworker are part of the same Pod. Only the Kerberos side‐car has\naccess to Keytab secret and both containers in the same Pod\nshare the volume, where temporary token is written by the side‐\ncare container and read by the worker container.

\n
\n

대략 K8S 환경에서 사이드카 형태로 구성하는 방법에 대한 내용입니다.\n이를 그림으로 그려보면 아래와 같습니다.

\n

\n \n \n \n

\n
    \n
  1. 스팟 인스턴스가 중단되기 약 120초 전에 Termination Handler의 notice 발생
  2. \n
  3. driver가 해당 executor를 blacklist에 추가하고 신규 task의 스케줄링을 차단
  4. \n
  5. 중단되는 노드에 있던 캐시된 데이터, 셔플 파일을 다른 노드로 복제
  6. \n
  7. 실패 처리된 task를 이어서 수행 (복제한 파일을 그대로 활용)
  8. \n
\n
\n

위의 과정을 통해 노드가 중단되었을 때 재계산을 최소화 할 수 있습니다.
\n이 기능에는 다음과 같이 일부 제한 사항도 존재합니다.

\n

120초의 시간 제한이 있기 때문에 옮겨야할 파일이 아주 큰 경우, 일부 파일 손실이 발생할 수 있습니다. 일반적으로 non-SSD 볼륨은 분당 최대 15GB, SSD 볼륨은 35~40GB 까지 가능합니다. 동시에 많은 executor가 spot kill 당하는 경우, 동일한 이유로 파일 손실이 발생할 수 있습니다.

\n
spark.decommission.enabled\nspark.storage.decommission.enabled\nspark.storage.decommission.rddBlocks.enabled\nspark.storage.decommission.shuffleBlocks.enabled
\n

Graceful Executor Decommissioning은 위의 설정을 통해 활성화 할 수 있습니다.

\n



\n

Spark 3.2: Executor PVC Reuse

\n

\n \n \n \n

\n

Executor PVC Reuse는 Spark 3.2 버전에 추가된 기능입니다.\n이 기능을 통해 spot kill 이후에도 동일한 PVC 연결을 통해 셔플 파일을 재사용할 수 있습니다. 이를 사용하려면 먼저 클러스터에 Dynamic PVC에 대한 설정이 필요합니다.

\n

현재는 NVMe 기반의 SSD에서 사용이 어렵다는 제한 사항이 있습니다.
\n또한 PVC가 즉시 재사용 불가능한 상황이라면 race condition이 발생할 수도 있습니다.

\n
spark.kubernetes.driver.reusePersistentVolumeClaim\nspark.kubernetes.driver.ownPersistentVolumeClaim\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.options.*\nspark.kubernetes.executor.volumes.persistentVolumeClaim.data.mount.*
\n

Executor PVC Reuse는 위의 설정을 통해 활성화 할 수 있습니다.

\n
\n

Reference

\n","excerpt":"스팟 인스턴스 유형을 사용하면 온디맨드에 비해 70~9…"}}},{"id":"c4c76da9-9abb-5367-906a-faa948a032fa","title":"컨테이너 환경을 위한 초기화 시스템 (tini, dumb-init)","slug":"container-tini-dumb-init","publishDate":"May 27, 2022","publishDateISO":"2022-05-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 ENTRYPOINTtini, dumb-init과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb-init을, SparkOperator에서는 tini를 사용하고 있습니다. 이 글에서는 컨테이너 환경에서 왜 이러한 초기화 시스템이 필요한지 알아보려 합니다.

\n



\n

PID 1의 역할

\n

\n \n \n \n

\n

백그라운드에서 실행되는 nginx 프로세스를 예시로 들어보겠습니다. 먼저 nginx는 자식 프로세스를 만듭니다. 그리고 nginx 프로세스가 종료됩니다. 고아가 된 nginx 자식 프로세스는 init 프로세스가 거두어들입니다.

\n

이러한 init 프로세스의 역할 덕분에 우리는 어플리케이션을 개발할 때 크게 신경쓰지 않게 되었습니다. 하지만 쿠버네티스를 포함한 컨테이너 환경의 경우, 조금 다릅니다.

\n
\n

컨테이너 내부에서의 프로세스 동작

\n

도커는 컨테이너 ENTRYPOINT(CMD)로 명시된 프로세스를 PID 1로써 새로운 PID 네임스페이스에 정의합니다. 그리고 컨테이너 내부에 있는 PID 1 프로세스에만 신호를 보내 종료할 수 있습니다. 이러한 이유로 컨테이너는 경량화 이미지를 기반으로 단일 프로세스만 실행하는 경우가 많습니다. 두 가지 예시를 살펴보겠습니다.

\n

1. sh 프로세스가 PID 1인 경우
\nDockerfile을 통해 다음과 같은 컨테이너 명령을 지정하면 실행을 위해 쉘에 전달됩니다. 그 결과 아래와 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - /bin/sh (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

쉘을 PID 1로 사용하면 실제로 2번 프로세스에 signal를 보내는 것이 거의 불가능합니다. 쉘로 보낸 신호는 하위 프로세스로 전달되지 않으며 프로세스가 완료될 때까지 셸이 종료되지 않습니다. 이 경우 컨테이너를 종료하기 위해 SIGKILL을 보내야 합니다.

\n

2. 내 프로세스가 PID 1인 경우
\nDockerfile에서 다음과 같이 정의하면 프로세스가 즉시 시작되고 컨테이너의 초기화 시스템으로써 작동하여 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - python my_server.py (PID 1, inside container)
\n

이러한 구조가 1번 예시보다 나은 방법입니다. 프로세스는 이제 실제로 보내는 신호를 수신합니다. 그러나 PID 1이므로 예상대로 응답하지 않을 수 있습니다.

\n
\n

PID 1의 Signal Propagation 문제

\n

컨테이너 환경도 마찬가지로 PID 1은 초기화 시스템의 책임이 있습니다.\n일반적인 프로세스는 TERM에 대한 자체 handler를 등록하여 종료하기 전 cleanup을 수행할 수 있습니다. 프로세스가 signal handler를 등록하지 않은 경우, 커널은 일반적으로 TERM 신호에 대한 기본 동작인 프로세스 종료를 수행합니다.

\n

반면 PID 1은 TERM 신호에 대해 기본 동작으로 실행되지 않습니다. 따라서 signal handler를 등록하지 않은 경우, TERM은 프로세스에 아무런 영향도 미치지 못합니다.\n만약 자식 프로세스가 하위 프로세스를 생성하고 먼저 죽었다면, 컨테이너 상에 좀비 프로세스가 계속 쌓일 수 있습니다.

\n

docker run이 SIGTERM을 수신하면 컨테이너 자체가 죽지 않더라도 신호를 컨테이너로 전달한 다음 종료됩니다. docker stop 명령을 사용해도 마찬가지입니다. TERM signal을 보내고 10초 동안 기다린 다음 프로세스가 여전히 중지되지 않으면 KILL이 전송되어 정리할 기회 없이 즉시 중지됩니다.

\n
\n

dumb-init

\n

dumb-init은 이러한 문제를 해결하고 컨테이너를 일반 프로세스와 같은 형태로 사용할 수 있도록 지원하기 위해 만들어졌습니다. systemd과 달리 컨테이너에서 사용하기 위해 경량화된 형태로 개발된 초기화 시스템입니다. dumb-init을 사용하면 다음과 같은 프로세스 트리가 생성됩니다.

\n
- docker run (on the host machine)\n  - dumb-init (PID 1, inside container)\n    - python my_server.py (PID 2, inside container)
\n

dumb-init은 모든 signal에 대해 signal handler를 등록하고 해당 signal을 프로세스 세션으로 전달합니다. 파이썬 프로세스는 더 이상 PID 1로 실행되지 않기 때문에 dumb-init이 TERM과 같은 신호를 전달할 때 handler를 등록하지 않아도 프로세스 종료가 가능합니다. dumb-init은 signal propagation 뿐만 아니라 고아 상태가 된 자식 프로세스를 거두는 역할(adopt)도 수행합니다.

\n
RUN apt install dumb-init\nENTRYPOINT [\"/usr/bin/dumb-init\", \"--\", \"/my/script\"]
\n

사용 방법은 정말 간단합니다. 이미지에 바이너리를 설치하고 명령어 실행할 때 추가하면 됩니다.

\n
\n

Airflow 이미지에서 dumb-init 사용

\n

Airflow도 dumb-init를 ENTRYPOINT에서 사용하고 있습니다. webserver, worker, scheduler pod에서 bash -c ENTRYPOINT를 사용하는데 bash는 자식에게 signal을 전달 안하기 때문에 dumb-init 사용이 필요합니다. 컨테이너 내에서는 환경변수를 통해 다르게 설정할 수 있도록 지원하고 있습니다. 설정 값의 차이는 아래와 같습니다.

\n\n

공식 차트에서 worker pod은 0으로 나머지는 1로 설정되어 있습니다.
\n이유는 Celery Worker의 warm shutdown을 지원하기 위해서 입니다. 특히 Airflow on Kubernetes 구성에서 CeleryExecutor를 사용하는 경우, task의 정상적인 종료를 위해 필요합니다. 이 부분은 다음 포스트에 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"쿠버네티스 기반의 데이터플랫폼을 운영하다보면 이미지의 에 , 과 같은 명령어를 사용하는 경우가 많습니다. 예를 들어 Airflow에서는 dumb…"}}},{"id":"641c0253-f45e-5b70-90a2-43300aece54b","title":"Airflow worker에 KEDA AutoScaler 적용한 후기","slug":"airflow-worker-keda-autoscaler","publishDate":"June 24, 2022","publishDateISO":"2022-06-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

Airflow에서 실행되는 배치 작업들은 특정 시간 또는 야간에 많이 수행되고 이외의 시간은 상대적으로 여유로운 경우가 많습니다. 이러한 상황에서 오토스케일링을 적용한다면 효율적으로 리소스를 최적화하여 사용할 수 있습니다.

\n

만약 쿠버네티스 위에서 Celery Executor를 사용한다면 worker의 오토스케일링을 위해 KEDA를 고려해볼 수 있습니다. 이 글에서는 Airflow worker에 KEDA AutoScaler를 적용하면서 겪었던 여러 문제들과 해결 과정에 대해 정리해보려 합니다.

\n



\n

KEDA AutoScaler

\n

KEDA는 쿠버네티스에서 이벤트 기반 오토스케일링을 쉽게 구현할 수 있도록 지원하는 컴포넌트입니다. 쿠버네티스의 HPA와 함께 동작하며 다양한 built-in scaler를 통해 유연하게 오토스케일링 조건을 설정할 수 있습니다.

\n

\n \n \n \n

\n

만약 Airflow에 적용한다면 위의 그림과 같은 형태로 구성됩니다.\n사용자는 KEDA의 ScaledObject CRD를 생성하여 클러스터에 배포합니다.\nKEDA는 쿠버네티스의 API Server와 통신하며 Operator와 같은 형태로써 컨트롤 루프에 따라 동작합니다.

\n
apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: airflow-worker\nspec:\n  scaleTargetRef:\n    name: airflow-worker\n  pollingInterval: 10\n  cooldownPeriod: 30\n  minReplicaCount: 3\n  maxReplicaCount: 10\n  triggers:\n    - type: postgresql\n      metadata:\n        connectionFromEnv: AIRFLOW_CONN_AIRFLOW_DB\n        query: \"\"
\n

ScaledObject는 위와 같이 무엇을 기준으로 트리거할지, 스케일링 정책 등을 정의할 수 있습니다. KEDA는 minReplicaCount에 따라 다르게 동작하는데 minReplicaCount가 0인 경우, KEDA가 trigger 지표를 통해 직접 처리하지만 1 이상인 경우에는 KEDA가 Metrics Server에 전달만하고 HPA를 통해 처리됩니다. 각 옵션에 대한 자세한 설명은 공식 문서에서 확인할 수 있습니다.

\n
SELECT ceil(COUNT(*)::decimal / {{ celery.worker_concurrency }})\nFROM task_instance\nWHERE state='running' OR state='queued'
\n

Airflow에서 사용하는 ScaledObject의 트리거 쿼리는 위와 같이celery.worker_concurrency 설정을 기준으로 하고 있습니다. 예를 들어 concurrency 설정이 12이며 running 또는 queued 상태의 task instance가 10에서 23으로 증가한 상황이라고 가정해보겠습니다. desired state가 1에서 2로 변경되었기 때문에 deployment의 replica 수는 2로 확장 됩니다. 스케줄이 모두 종료된 이후 다시 task instance가 10으로 줄어들면 replica 수는 1로 축소 됩니다.

\n

Airflow 공식 차트에서는 KEDA 관련 옵션을 지원하고 있기 때문에 공식 문서를 통해 쉽게 적용할 수 있습니다.
\n하지만 문제는 적용한 이후에 발생했습니다.

\n
\n

적용 후에 발생한 문제

\n

적용 후에 실행 중인 task의 로그가 갑자기 끊기면서 강제로 실패 처리되는 문제가 있었습니다.
\n시간을 보니 worker가 Scale-In 되는 시점에 발생했고 크게 두 가지 문제를 확인할 수 있었습니다.

\n
\n

1. HPA의 replica flapping 문제

\n

먼저 의도한 것보다 Scale-In/Out이 너무 빈번하게 발생했습니다.\n새로 노드가 뜨는데 시간이 소요되므로 배치가 많은 시간 대에도 잦은 스케일 조정이 발생하는 것은 비효율적입니다. 이러한 문제를 HPA에서는 replica flapping 이라고 말합니다.\nHPA는 이를 제어하기 위해 안정화 윈도우와 스케일링 정책을 지원하고 있습니다.

\n
behavior:\n  scaleDown:\n    stabilizationWindowSeconds: 600
\n

위와 같이 stabilizationWindowSeconds 설정을 600으로 설정하면 이전 10분 동안의 모든 목표 상태를 고려해서 가장 높은 값으로 설정합니다. 현재 시점에 scaleDown 조건을 만족하더라도 즉시 수행되는게 아니라 10분이 지난 시점에 scaleDown이 수행됩니다. 이를 통해 잦은 스케일 조정을 제한할 수 있습니다.

\n
behavior:\n  scaleDown:\n    policies:\n    - type: Pods\n      value: 1\n      periodSeconds: 300
\n

scaleDown.polices를 통해 Scale-In 발생 시 replica 변경 허용에 대한 정책을 지정할 수 있습니다. 위의 예시는 5분 내에 최대 1개의 replica를 scaleDown 하도록 허용하는 정책입니다. 이를 통해 계단식으로 천천히 pod를 축소할 수 있습니다.

\n

현재 Airflow 공식 차트에서는 KEDA의 advanced 옵션을 지원하지 않아 PR을 추가했습니다.
\n차트 1.7 버전부터 사용하실 수 있습니다.

\n
\n

2. Worker Warm Shutdown 문제

\n

\n \n \n \n

\n

celery worker의 warm shutdown이 제대로 이루어지지 않았기 때문에 task의 로그가 갑자기 끊기면서 강제로 실패 했습니다. Airflow의 CeleryExecutor는 위와 같이 여러 프로세스를 통해 수행됩니다. 이 때 실제로 task를 실행하는 프로세스는 main 프로세스가 아니라 subprocess 입니다. celery에서는 실행 중인 task가 처리된 이후에 종료할 수 있도록 warm shutdown을 지원하고 있습니다. worker의 main process가 SIGTERM을 받으면 task가 종료될때까지 기다리게 됩니다.

\n
# warm shutdown log\nworker: Warm shutdown (MainProcess)\n\n -------------- celery@fcd56490a11f v4.4.7 (cliffs)\n--- ***** -----\n-- ******* ---- Linux-5.4.0-1045-aws-x86_64-with-debian-10.8\n- *** --- * ---\n- ** ---------- [config]\n- ** ---------- .> app:         airflow.executors.celery_executor:0x7f95\n- ** ---------- .> transport:   redis://redis:6379/0\n- ** ---------- .> results:     postgresql://airflow:**@postgres/airflow\n- *** --- * --- .> concurrency: 16 (prefork)\n-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)\n--- ***** -----\n -------------- [queues]\n                .> default          exchange=default(direct) key=default\n\n[tasks]\n  . airflow.executors.celery_executor.execute_command
\n

이전 글에서 설명한 것처럼 Airflow 공식 차트에서 worker pod은 DUMB_INIT_SETSID=0으로 이미 설정되어 있기 때문에 메인 프로세스에만 SIGNAL이 전파되고 task process는 계속 실행됩니다. 하지만\nscaleDown이 발생한다면, 실행 중이던 worker pod이 종료되기 때문에 pod 내에 있던 task process도 함께 강제 종료되면서 task가 실패하게 됩니다. 장시간 수행되는 task 일수록 이러한 문제를 마주칠 가능성이 높습니다.

\n

\n \n \n \n

\n

이를 해결하기 위해 task의 execution_timeout 시간까지 pod가 종료되지 않도록 terminationGracePeriodSeconds를 지정해주었습니다. 이제 각 컨테이너 내부의 프로세스 1에 SIGTERM이 전달되더라도 pod의 graceful shutdown 시간 동안 대기하므로 task process는 계속 실행됩니다. 시간이 모두 지나면 SIGKILL을 통해 모든 프로세스가 종료되고 pod도 삭제됩니다.

\n
\n

적용 후기

\n

\n '\n

Spark on Kubernetes에서는 Pod Template 또는 node selector 설정을 통해 단일 AZ 노드 그룹에서 실행되도록 설정할 수 있습니다.

\n
\n

클러스터 노드 가용성 계산하기

\n

\n \n \n \n

\n

노드 전체의 리소스를 최대로 사용하기 위해 어느 정도의 리소스를 할당할 수 있는지 계산할 수 있어야 합니다. 모든 Kubernetes 노드는 클러스터 운영을 위해 OS 시스템과 Kubelet에서 일정량의 리소스를 점유하고 있습니다. 따라서 Pod에 할당 가능한 리소스를 계산할 때 이 부분은 제외하고 계산해야 합니다. 만약 노드마다 뜨는 daemonset이나 agent와 같은 어플리케이션을 띄웠다면 해당 리소스도 제외되어야 합니다.

\n

클라우드 인스턴스 유형에 따라 빠르게 보고 싶을 때 Kubernetes Instance Calculator를 사용하면 쉽게 계산할 수 있습니다.

\n
\n

셔플 단계에서의 scratch space 개선

\n

Spark Shuffle 발생 시 중간 파일들이 생기게 되는데, 보통 driver나 executor의 로컬 디렉토리를 사용합니다. 하지만 Kubernetes의 경우, 기본 값으로 Pod 내부의 볼륨(emptyDir)을 사용하고 있습니다.

\n

emptyDir 유형의 볼륨은 Docker Storage Driver의 CoW(Copy-On-Write) 오버헤드로 인해 작은 파일 쓰기를 반복하는 경우 속도가 느려질 수 있습니다. 이를 개선하기 위해 Spark on Kubernetes GA 버전에서는 2가지의 설정이 추가되었습니다.

\n
\n

1. [SPARK-25262] Support tmpfs for local dirs in k8s

\n

먼저 tmpfs를 local dir로 활용하는 방법입니다.\ntmpfs는 RAM 기반 파일 시스템으로 노드 재부팅 시 지워지고, 파일이 컨테이너 메모리 제한에 포함됩니다. 설정 방법은 아래와 같이 간단하지만 tmpfs 사이즈가 커질 수록 Pod OOM이 발생할 가능성이 크다보니 운영할 때는 번거로울 수 있습니다.

\n
\"spark.kubernetes.local.dirs.tmpfs\": \"true\"
\n
\n

2. [SPARK-27499] Support mapping spark.local.dir to hostPath volume

\n

다음은 host에 마운트된 볼륨을 직접 사용하는 방법입니다. hostPath 볼륨을 spark.local.dir에 할당해서 셔플 과정에서의 디스크 성능을 향상시킬 수 있습니다. 다만 인스턴스에 SSD 또는 NVMe와 같은 볼륨을 추가로 마운트하는 경우에 더 좋은 효과를 볼 수 있습니다.

\n
spec:\n  ...\n  volumes:\n    - name: \"spark-local-dir-1\"\n      hostPath:\n        path: \"/tmp/spark-local-dir\"\n  executor:\n    instances: 10\n    cores: 2\n    ....\n    volumeMounts:\n      - name: \"spark-local-dir-1\"
\n
\n

Executor Pod Batch 관련 설정

\n

보통 무거운 작업은 executor 여러 개가 떠서 처리하는 경우가 많습니다.\nSpark on Kubernetes에는 executor pod을 생성할 때 batch size와 delay가 존재합니다.

\n

예를 들어 executor 10개를 띄울 때 기본 설정 값이 batch size = 5, delay = 1로 되어 있다면, executor pod 5개가 동시에 뜨고 1초 지연 이후에 5개가 추가로 생성됩니다.\n이 설정 값은 Kubernetes Scheduler와 driver pod의 부하를 고려해서 설정해주어야 합니다.

\n
\"spark.kubernetes.allocation.batch.size\": \"5\"\n\"spark.kubernetes.allocation.batch.delay\": \"1s\"
\n
\n

반면 아직 3.1 버전 기준으로 지원하지 않는 설정들은 아래와 같습니다.

\n\n
\n

Reference

\n","excerpt":"Spark 3.1 버전부터 Spark on Kubernetes가 GA로 변경되었습니다.\n이 글에서는 Spark on YARN…"}}},{"id":"87397863-28d6-5e79-898e-aeccb9f21920","title":"JupyterHub on Kubernetes","slug":"jupyterhub-on-kubernetes","publishDate":"October 23, 2021","publishDateISO":"2021-10-23","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":4,"html":"

일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.
\n이 글에서는 zero-to-jupyterhub-k8s Helm Chart에 포함된 다양한 기능들에 대해 소개해보려 합니다.

\n

목차

\n\n



\n

KubeSpawner

\n

\n \n \n \n

\n

zero-to-jupyterhub-k8s Helm Chart 의 아키텍쳐는 위의 그림과 같습니다. 기존 JupyterHub와 달리 hook-image-awaiter, jupyterhub-idle-culler 등의 컴포넌트가 추가된 모습을 확인하실 수 있습니다. 이제 대략적으로 어떤 기능을 제공하는지 알아보겠습니다.

\n
\n

Proxy

\n
proxy:\n  service:\n    type: ClusterIP\n  chp:\n    networkPolicy:\n      enabled: false
\n

먼저 CHP(configurable-http-proxy) 설정 부분입니다. JupyterHub에서 Proxy는 인증, 사용자 노트북 라우팅, 헬스 체크 등 다양한 역할을 수행합니다. 차트에서는 유연한 Proxy 설정을 위해 CHP, Traefik 등 다양한 옵션을 지원합니다. 아키텍쳐는 aws-load-balancer-controller를 사용한다는 가정하에 구성한 예시입니다. 위 그림과 같이 사용자는 중간의 Proxy 컴포넌트를 거쳐 JupyterHub에 접속하게 됩니다.

\n
\n

SingleUser, Profile

\n

\n \n \n \n

\n

singleUser는 사용자의 노트북 환경을 의미하며 사용자는 미리 정의된 프로필(이미지)을 선택하여 원하는 노트북 환경을 생성할 수 있습니다. 위 아키텍쳐에서는 PV, PVC를 통해 사용자에게 개인, 공용 볼륨을 할당해주었습니다.

\n
profileList:\n  - display_name: \"Python Notebook\"\n    description: \"Spec: CPU 2, Memory 4G / Spark 3.1\"\n    kubespawner_override:\n      image: jupyter/python-notebook:hub-1.4.2\n      cpu_limit: 2\n      mem_limit: \"4G\"\n      cpu_guarantee: 1\n      mem_guarantee: \"2G\"\n      environment:\n        TZ: Asia/Seoul\n      lifecycle_hooks:\n        postStart:\n          exec:\n            command:
\n

프로필에는 리소스 뿐만 아니라 lifecycle_hook, environment 등 K8S의 다양한 리소스를 함께 정의하여 유연하게 구성할 수 있습니다. 노트북 기본 이미지는 jupyter/docker-stacks 저장소로부터 생성한다면 편하게 패키지 의존성을 관리할 수 있습니다.

\n

resource guarantee
\nresource guarantee는 모든 사용자가 최소한 _guarantee 만큼의 리소스를 사용할 수 있으며 최대 _limit 만큼의 리소스를 제공받을 수 있음을 의미합니다. 예를 들어 사용자에게 2G의 RAM이 보장되는 경우, 사용자는 2G 이상의 RAM을 사용할 수 있습니다. 문서에서는 guarantee 값을 limit의 반으로 설정하는 것을 권장하고 있습니다.

\n
\n

Idle Culler

\n
cull:\n  enabled: true\n  timeout: 86400\n  every: 600\n  concurrency: 10
\n

idle-culler는 일정 주기 동안 미사용된 노트북 리소스를 정리합니다.\n이를 통해 노트북 리소스를 최적화하여 운영할 수 있습니다.\nidle-culler를 활성화하면 JupyterHub Service에 등록되며 이후 JupyterHub API를 통해 사용자 활동을 주기적으로 확인합니다.

\n
\n

User Scheduler

\n

user scheduler는 노트북 리소스를 적절한 노드에 할당하기 위해 추가되었습니다.\n기본 K8S 스케줄러는 여러 노드에 분산하여 리소스를 할당하지만, user scheduler는 가장 리소스를 많이 점유하고 있는 노드에 리소스를 할당합니다. 이를 통해 Cluster AutoScaler, idle-culler와 연계하여 노트북 리소스를 최적화하여 운영할 수 있습니다.

\n

\n \n \n \n

\n

예를 들어 일반적인 설정이라면, pod가 다양한 노드에 분산되어 클러스터 scale-in 조건까지 도달하기가 어렵습니다. 하지만 user-scheduler를 사용한다면, 위 그림과 같이 노드에 할당된 pod의 수가 점진적으로 줄어들게 됩니다.

\n
\n

Image Pre Puller

\n
prePuller:\n  resources:\n    requests:\n      cpu: 10m\n      memory: 8Mi\n  hook:\n    enabled: true\n    pullOnlyOnChanges: true
\n

Image prePuller는 사용자가 노트북을 실행하기 전에 노드에 미리 이미지를 준비하여 노트북 환경 생성 시간을 단축시켜 줍니다. 예를 들어 CA에 의해 노드가 새로 추가된다거나 새로운 이미지가 프로필에 등록된 경우, 미리 노드에 프로필 이미지를 pull 하게 됩니다.

\n
\n

Monitoring

\n

JupyterHub는 /metrics endpoint를 통해 prometheus 메트릭을 지원합니다. 주요 지표로는 활성 사용자 수, 노트북 서버 생성까지 소요되는 시간 등이 있습니다. 사용 가능한 전체 메트릭은 JupyterHub 문서에서 확인하실 수 있습니다.\n또한 jupyterhub/grafana-dashboards 저장소를 통해 미리 정의된 운영 대시보드를 제공합니다. 이를 통해 쉽게 모니터링을 구성할 수 있습니다.

\n
\n

Reference

\n","excerpt":"일반적으로 JupyterHub를 Kubernetes 환경에 배포할 때 Helm Chart를 많이 사용합니다.\n이 글에서는 zero-to…"}}},{"id":"daa589cd-f055-5aef-94ee-0b0b8d1505a0","title":"Spark on Kubernetes: 커스텀 스케줄러 (1)","slug":"spark-on-kubernetes-scheduler","publishDate":"June 08, 2023","publishDateISO":"2023-06-08","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":3,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n그래서 오늘은 커스텀 스케줄러가 왜 필요하고 어떻게 적용할 수 있는지 정리해보려고 합니다.

\n



\n

Spark Kubernetes Scheduling

\n

\n \n \n \n

\n

쿠버네티스 환경에서 spark-submit을 실행하면 pod가 실행되는 순서는 다음과 같습니다.

\n\n

위와 같이 driver가 executor를 관리함에 따라 동적으로 리소스를 확장할 수 있지만\ndriver가 생성되기 전까지 전체 executor에 필요한 리소스를 알 수 없다는 단점이 있습니다.\n이러한 이유로 클러스터 내에 리소스가 고갈된 상황에서 성능 문제가 발생할 수 있습니다.

\n
\n

클러스터 내에 리소스가 고갈된 경우
\n\n \n \n \n

\n

클러스터의 리소스 풀이 요청 받은 리소스보다 부족한 상황이라고 가정해보겠습니다.\n위 그림에서 녹색은 실제로 노드에 할당되어 running 중인 pod, 빨간색은 리소스가 부족으로 인해 pending 상태의 pod 입니다.

\n

각 앱은 리소스 경쟁에 의해 driver와 executor 1개씩 정상적으로 생성되어 3개의 앱이 실행 중인 상태입니다. 하지만 3개의 앱은 executor 리소스를 확보하지 못했기 때문에 작업을 완료할 수 없습니다. EKS 환경이라면 노드 리소스를 확보하더라도 VPC IP 고갈 문제로 인해 이러한 상황을 충분히 마주칠 수 있습니다.

\n
\n

\n \n \n \n

\n

위의 그림은 기본 스케줄러를 적용했을 때 모습입니다.
\n필요한 최소 리소스가 미리 정해져있으나 노드 생성까지 대기 시간이 발생합니다.

\n\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n\n

여기에서 placeholder pod은 아무 동작도 안하지만 미리 리소스를 확보하기 위해 존재하는 dummy pod 입니다. 만약 리소스를 확보하지 못하는 상황이라면 앱은 대기합니다.\nGang Scheduling은 FIFO 큐와 함께 실행하여 리소스 경쟁으로 인한 교착상태에 빠지지 않도록 할 수 있습니다.

\n

\n \n \n \n

\n

또한 동시 실행 Pod가 많을 수록 스케줄링 성능 향상을 기대할 수 있습니다. 위 그림은 Yunikorn에서 kubemark를 통해 벤치마크한 결과입니다. 회사 환경에서 spark 작업 시간을 기준으로 테스트했을 때도 성능 향상을 확인할 수 있었습니다.

\n

다음 글에서는 Spark 3.4 버전에서 공식적으로 지원하는 Volcano, Yunikorn에 대해 이어서 정리해보겠습니다.

\n
\n

Reference

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"8d6b9e00-f4f6-5624-b75c-fabb15be093f","title":"Spark on Kubernetes: 커스텀 스케줄러 (2)","slug":"spark-on-kubernetes-scheduler-2","publishDate":"December 10, 2023","publishDateISO":"2023-12-10","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA 되었습니다 👏🏻
\n오늘은 지난 글에 이어 가장 많이 사용하는 Volcano, Yunikorn 스케줄러에 대해 알아보겠습니다.

\n
\n

3.4 버전 기준으로 Spark에서는 Volcano, Yunikorn 두 가지 커스텀 스케줄러를 공식적으로 지원합니다. 두 가지 오픈소스 모두 네이티브 환경에서 배치 처리를 지원하기 위한 프로젝트이며 최신 버전 기준으로 모두 유사한 기능을 지원하고 있습니다. 먼저 Volcano 부터 살펴보겠습니다.

\n
\n

Volcano

\n

초기의 Volcano는 kube-batch 프로젝트 기반으로 구성되었으나 1.8 버전부터 쿠버네티스 스케줄러 플러그인 방식을 지원하게 되었습니다. 스케줄러 플러그인 기반으로 구성한 커스텀 스케줄러는 기본 스케줄러와 호환 가능하며 버전 업데이트 영향도 적게 받는 장점이 있습니다.

\n

\n \n \n \n

\n

Volcano의 주요 컴포넌트는 다음과 같습니다.

\n\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Volcano 적용 과정
\nVolcano 적용을 위해 필요한 단계는 다음과 같습니다.

\n
    \n
  1. Volcano 환경 및 리소스 배포
  2. \n
  3. Spark Volcano 이미지 빌드 및 배포
  4. \n
  5. Spark configuration 전달
  6. \n
\n
# Specify volcano scheduler and PodGroup template\n--conf spark.kubernetes.scheduler.name=volcano\n--conf spark.kubernetes.scheduler.volcano.podGroupTemplateFile=/path/to/podgroup-template.yaml\n# Specify driver/executor VolcanoFeatureStep\n--conf spark.kubernetes.driver.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep\n--conf spark.kubernetes.executor.pod.featureSteps=org.apache.spark.deploy.k8s.features.VolcanoFeatureStep
\n



\n

Apache Yunikorn

\n

Yunikorn은 Volcano보다 뒤늦게 시작된 Apache 프로젝트입니다.\n컨테이너 오케스트레이션을 위한 경량의 범용 스케줄러를 지향하고 있으며 대부분의 스케줄링 알고리즘도 지원하고 있습니다.\n또한 Volcano와 동일하게 스케줄러 플러그인 방식을 지원합니다. 추가로 Yunikorn은 조직 단위로 리소스 관리가 가능하도록 계층 구조의 큐를 지원합니다.

\n

\n \n properties:\n application.sort.policy: fifo\n application.sort.priority: disabled\n queues:\n - name: prod\n resources:\n guaranteed:\n memory: 300G\n vcore: 30\n max:\n memory: 600G\n vcore: 60\n - name: stage\n resources:\n guaranteed:\n memory: 100G\n vcore: 10\n max:\n memory: 200G\n vcore: 200\n

위와 같이 prod, stage 등 여러 개의 큐를 계층형으로 생성할 수 있습니다.
\n스케줄링 정책은 크게 node sorting 단계와 application sorting 단계로 나누어집니다.
\ngang scheduling을 사용하는 경우, application sorting은 항상 fifo를 사용해야 합니다.

\n

Yunikorn에서 Gang Scheduling이 실행되는 단계는 다음과 같습니다.

\n

\n \n \n \n

\n\n
\n

Yunikorn 적용 과정
\nYunikorn 적용을 위해 필요한 단계는 다음과 같습니다.
\nYunikorn의 경우 annotation 설정을 사용합니다.

\n
    \n
  1. Yunikorn 환경 및 설정 배포
  2. \n
  3. Spark configuration 전달
  4. \n
\n
--conf spark.kubernetes.scheduler.name=yunikorn\n--conf spark.kubernetes.driver.label.queue=root.default\n--conf spark.kubernetes.executor.label.queue=root.default\n--conf spark.kubernetes.driver.annotation.yunikorn.apache.org/app-id={{APP_ID}}\n--conf spark.kubernetes.executor.annotation.yunikorn.apache.org/app-id={{APP_ID}}
\n



\n

Volcano vs Apache Yunikorn

\n

앞서 살펴 본 내용을 통해 각 스케줄러의 장단점을 정리해보면 다음과 같습니다.
\n모두 Helm 차트를 지원하므로 쉽게 구성할 수 있습니다.

\n

Volcano
\n장점: Kubeflow에 대한 지원
\n단점: spark 이미지 빌드, CRD 단위로 관리가 필요

\n
\n

Yunikorn
\n장점: 작업 상태를 확인할 수 있는 Web UI 지원
\n장점: 경량화되어 있으며 계층 구조의 큐를 지원
\n장점: 추가로 필요한 부분이 적어 운영이 편리
\n단점: 주요 설정은 모두 있으나 Volcano 대비 적은 옵션 지원

\n



\n

운영을 하면서 마주칠 수 있는 부분들

\n

다음은 적용한 이후에 운영을 하다보면 마주칠 수 있는 이슈 또는 고민을 정리해보았습니다.

\n

placeholder 리소스 설정
\napplication submit 시 placeholder에 할당할 리소스 사이즈 결정이 필요합니다.\nplaceholder를 작게 설정하면 리소스 확보가 안되어 스케줄링에 영향이 있을 수 있고 지나치게 크게 설정하면 실제로 여유가 있음에도 리소스 부족 현상 발생할 수 있습니다. spark-on-k8s-operator를 사용한다면 스케줄러에 따라 placeholder 사이즈를 결정하는 로직이 포함되어 있으니 편하게 적용이 가능합니다.

\n

큐 사이즈 조정
\n만약 큐의 리소스 제한보다 요청한 리소스가 크다면 application reject이 발생하여 실행이 불가능합니다. 또한 큐의 크기가 전체적으로 작은 경우, 신규 요청한 어플리케이션이 빈번하게 대기하는 상황도 발생할 수 있습니다. 스케줄러에서 Prometheus 메트릭을 제공하니 Grafana를 통해 모니터링 후 적절한 큐 사이즈로 설정하는 과정이 필요합니다.

\n

Spark Dynamic Resource Allocation을 사용하는 경우
\n큐에서 이미 실행 중인 application은 리소스 확장도 가능합니다.\n따라서 Spark의 Dynamic Resource Allocation을 많이 사용한다면 미리 설정해둔 제한을 크게 넘어갈 수도 있습니다. 이러한 경우, 큐를 사용하는 의미가 사라지게 됩니다.

\n

Application Cleanup 관련
\n상황에 따라 application이 accepted 또는 waiting 상태에서 계속 머무르는 이슈가 발생할 수 있습니다. 이처럼 placeholder가 할당되지 못하는 경우, 스케줄러에서 timeout 설정을 통해 실패 처리되어야 다음 작업이 원활하게 진행될 수 있습니다. 만약 좀비 상태로 placeholder가 남는다면 core에서 확인 후 GC를 통해 정리됩니다.

\n



\n

Reference

\n

두 가지 스케줄러 모두 범용적으로 많이 사용되고 있어 운영 중인 환경에 따라 선택하시면 좋을 것 같습니다.
각 스케줄러에 대한 자세한 내용은 아래의 공식문서에서 찾아보실 수 있습니다!

\n","excerpt":"Spark 3.4 버전부터 Customized K8S Scheduler 기능이 GA…"}}},{"id":"ef1e9cc8-27ee-57ae-acf6-96d41704b9a0","title":"Pandas 2.0의 Copy-on-Write에 대하여","slug":"pandas-2-0-copy-on-write","publishDate":"December 24, 2023","publishDateISO":"2023-12-24","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":7,"html":"

Pandas 2.0 버전부터 Copy-on-Write (CoW)가 추가되었으며 3.0 버전부터 기본 값이 활성화로 변경됩니다. 이번 글에서는 Pandas Copy-on-Write가 Pandas가 가진 문제를 어떻게 해결하는지에 대해 알아보겠습니다.

\n\n
\n

Pandas DataFrame

\n

Pandas CoW에 대해 알아보기 이전에 먼저 DataFrame의 내부 구조에 대한 이해가 필요합니다.
DataFrame은 Pandas의 행, 열 기반 2차원 데이터 구조입니다.
\n초기에 Pandas는 아주 느린 컬럼 기반 연산을 빠르게 처리하기 위해 BlockManager를 추가했습니다.

\n

BlockManager
\nBlockManager는 numpy array로 저장된 데이터를 참조하는 블록을 관리하는 역할을 합니다.
\n아래 코드를 통해 자세히 알아보겠습니다.

\n
df = pd.DataFrame(data)\nprint(df)\n\n   c1 c2  c3\n0   1  a  10\n1   2  b  20\n2   3  c  30\n\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame을 생성하고 internal API를 통해 BlockManager 구조에 접근할 수 있습니다.
\n위 예시에서는 2개의 블록이 존재하며 그 중 int 타입을 가지는 c1, c3는 하나의 블록으로 통합되어 있습니다. 이처럼 BlockManager는 메모리 최적화와 효율적인 데이터 접근을 위해 동일한 타입을 하나의 블록으로 통합하여 관리합니다. 이번에는 동일한 타입을 가지는 c4 컬럼을 추가하고 다시 확인해보겠습니다.

\n
df['c4'] = [100,200,300]\nprint(df._data)\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: slice(0, 4, 2), 2 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object\nNumpyBlock: slice(3, 4, 1), 1 x 3, dtype: int64
\n

이번에는 새로운 블록이 추가된 것을 확인할 수 있습니다.
\nBlockManager는 새로운 블록이 추가될때마다 동일한 타입의 블록을 통합하지 않습니다.

\n
df._data.consolidate()\n\nBlockManager\nItems: Index(['c1', 'c2', 'c3', 'c4'], dtype='object')\nAxis 1: RangeIndex(start=0, stop=3, step=1)\nNumpyBlock: [0 2 3], 3 x 3, dtype: int64\nNumpyBlock: slice(1, 2, 1), 1 x 3, dtype: object
\n

DataFrame 연산이 실행되기 직전에 consolidate() 메서드를 통해 자동으로 통합합니다.
\n구체적으로는 블록 통합이 연산에 유리한 경우에만 블록 통합이 이루어집니다.

\n



\n

Pandas SettingWithCopyWarning

\n

앞서 Pandas가 BlockManager를 통해 어떻게 블록을 관리하는지 알아보았습니다.
\n이번에는 CoW에서 해결하고자 하는 SettingWithCopyWarning 문제에 대해 알아보겠습니다.

\n
import pandas as pd\n\ndf = pd.DataFrame(data)\nprint(df)\n\n   student_id grade\n0           1     A\n1           2     C\n2           3     D
\n

위와 같은 DataFrame에서 첫 번째 행의 grade 값을 E로 변경해보겠습니다.

\n
grades = df[\"grade\"]\ngrades.iloc[0] = \"E\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D\n\nSettingWithCopyWarning: \nA value is trying to be set on a copy of a slice from a DataFrame
\n

코드만 보면 grade 변수에만 변경내용이 적용된 것처럼 보입니다.
\n하지만 실제로는 df 내용도 변경되어 있으며 SettingWithCopyWarning 경고 문구가 나타납니다.\nChainedIndexing을 사용한 다른 예시도 확인해보겠습니다.

\n
df[df[\"student_id\"] > 2][\"grades\"] = \"F\"\nprint(df)\n\n   student_id grade\n0           1     E\n1           2     C\n2           3     D
\n

이번에도 SettingWithCopyWarning 경고 문구가 나타나며 df에는 어떠한 변화도 없는 것을 확인할 수 있습니다.\n이러한 문제가 발생하는 원인은 Pandas, Numpy가 내부적으로 view 또는 copy를 반환하는 방식에서 찾아볼 수 있습니다.

\n

Views and Copies

\n
import numpy as np\n\norigin = np.array([1, 6, 4, 8, 9, 2])\nview = origin.view()\ncopy = origin.copy()\n\narr[1] = 3\nprint(origin)\narray([1, 3, 4, 8, 9, 2])\n\nprint(view)\narray([1, 3, 4, 8, 9, 2])\n\nprint(copy)\narray([1, 6, 4, 8, 9, 2])
\n

위 코드 결과를 보면 origin, view는 변경된 값으로 반영되어 있지만 copy는 반영안되어 있는 것을 확인할 수 있습니다. view는 자체적으로 데이터가 없는 numpy 배열 입니다. 반면에 copy는 원본 배열의 요소를 새 배열에 복사하여 전체 복사본의 데이터를 가지고 있습니다.

\n

\n \n \n \n

\n

이처럼 view, copy에 따라 원본 객체인지 아닌지 달라지며 이는 일관된 동작을 보장하지 못하게 됩니다.
\n결국 SettingWithCopyWarning은 코드에서 사용자가 의도하지 않은 동작이 발생할 가능성이 있음을 경고하는 warning 입니다. 이 문제를 해결하기 위해 Pandas 2.0에 Copy-on-Write가 추가되었습니다.

\n



\n

Pandas Copy-on-Write

\n

Pandas Copy-on-Write는 다른 DataFrame으로부터 생성된 모든 DataFrame이 항상 복사본으로 동작하도록 보장합니다. 다시 말해, 더 이상 단일 연산으로 두 가지 이상의 객체가 수정될 수 없습니다. (ex. 처음 예시에서 grade만 변경되고 df는 변경되지 않음)

\n

이를 구현하기 위한 가장 쉬운 방법은 항상 데이터를 복사하는 방법입니다.
\n하지만 적용 시 성능이 크게 떨어지기 때문에 다른 방식을 적용해야 했습니다.

\n

BlockValuesRefs
\n불필요한 복사를 방지하려면 복사를 트리거할 시기를 정확히 알아야 합니다.
\n결국 DataFrame 데이터가 다른 DataFrame과 공유되는 경우에만 복사를 트리거해야 합니다.

\n
df = pd.DataFrame(data)\ndf2 = df[:]
\n

위 코드에서는 df와 df의 view 객체인 df2를 생성합니다.
\n현재 dfdf2는 동일한 numpy 배열을 참조하고 있습니다.

\n
df.iloc[0, 0] = 100
\n

코드를 통해 둘 중 하나가 수정되는 경우, 복사가 트리거됩니다.
\n이 때 다른 Pandas 객체가 참조하고 있는지를 추적해야 합니다.
\n이를 위해 BlockValuesRefs가 추가되었습니다.

\n

\n \n \n \n

\n

BlockValuesRefs는 numpy 배열을 감싸고 이 참조를 내부적으로 저장하는 블록을 가리키는 weakref를 생성합니다.\n위의 예시와 같이 동일한 타입의 a, b 컬럼은 BlockManager를 통해 하나의 블록에 존재합니다.\n그리고 블록에 대해 weakref를 가지는 Block Reference Tracker가 추가됩니다.
\n이제 다음 예시에서 새로운 블록을 추가해보겠습니다.

\n
df2 = df.reset_index(drop=True)
\n

\n \n \n \n

\n

BlockValuesRefs는 이제 df를 위한 블록과 df2를 위해 새로 생성된 블록을 가리킵니다.\n이를 통해 동일한 메모리를 가리키는 모든 DataFrame을 항상 인식할 수 있습니다.\n동일한 numpy 배열을 가리키는 블록이 몇 개 남아 있는지 참조 추적 객체를 통해 알아낼 수 있습니다.\n이러한 과정을 통해 둘 중 하나가 내부에서 수정되면 내부적으로 복사본을 트리거할 수 있습니다.

\n
df2.iloc[0, 0] = 100
\n

\n \n \n \n

\n

copy를 실행하는 경우는 간단합니다. DataFrame df2에 대한 새로운 BlockValuesRefs가 즉시 생성되며 데이터를 공유하지 않습니다.

\n
\n

Optimizing inplace copies
\n앞서 복사를 트리거하는 시점에 대해 알아보았습니다.
\n이번에는 복사본을 최대한 효율적으로 생성하는 방법에 대해 알아보겠습니다.

\n
df.iloc[0, 0] = 100
\n

\n \n

Notebook API를 활용하면 노트북 실행 뿐만 아니라, Cron이나 노트북 권한 설정도 자동화할 수 있습니다.\n자세한 내용은 아래의 공식문서에서 확인하실 수 있습니다.

\n
\n

Reference

\n\n
","excerpt":"제플린 노트북을 사용하다보면 가끔 제플린 어플리케이션을 재시작해야 하는 경우가 있습니다.\n이 때, view 또는 udf…"}}},{"id":"0bf44cfd-a95d-5c55-a158-812503a3e3f3","title":"Spark DataFrame을 MySQL에 저장하는 방법","slug":"spark-df-mysql","publishDate":"July 17, 2017","publishDateISO":"2017-07-17","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.1.0 버전을 사용 중 입니다.

\n
\n

MySQL JDBC Driver

\n

JDBC를 통해 접근하기 때문에 드라이버가 필요합니다.\n만일 SBT를 사용하신다면, build.sbt에 maven의 mysql-connector-java 를 추가하시면 됩니다.

\n

직접 jar 파일을 사용해야하는 상황이라면, 다음 링크를 통해 다운받으시면 됩니다.\nhttps://dev.mysql.com/downloads/connector/j/

\n

그리고 받으신 jar 파일을 -jars 옵션으로 추가해주셔야 합니다.

\n

–jars /home/example/jars/mysql-connector-java-5.1.26.jar

\n

마지막으로 spark-submit 을 사용하신다면, --packages 옵션을 추가해주시면 됩니다.

\n

--packages mysql:mysql-connector-java:5.1.39

\n
\n

Spark DataFrame MySQL

\n

Spark의 DataFrame은 read, write 함수를 통해 쉽게 데이터를 가져오거나 저장할 수 있습니다.\n아래 예시는 Scala 언어로 작성했습니다.

\n
import org.apache.spark.sql.SaveMode\nimport java.util.Properties\n\nval tempDF = List((\"1\", \"2017-06-01\", \"2017-06-03\")).toDF(\"id\", \"start\", \"end\")\nval properties = new Properties()\nproperties.put(\"user\", \"userId\")\nproperties.put(\"password\", \"password\")\ntempDF.write.mode(SaveMode.Append).jdbc(\"jdbc:mysql://url/database\", \"table\", properties)
\n

위 예제에서는 Properties를 통해 설정값을 넣어주었습니다.\n유저 정보나 주소는 맞게 변경해주시면 됩니다.

\n

mode 라는 것이 있는데 SaveMode.Append는 기존의 테이블에 추가하는 방식이고\nSaveMode.Overwrite의 경우 기존의 테이블을 새로운 데이터로 대체하는 방식입니다.

\n
","excerpt":"Spark에서 MySQL에 접근하고 DataFrame을 read, write 하는 방법에 대해 정리해보았습니다.\n참고로 저는 Spark 2.…"}}},{"id":"c78e09d9-7707-54ec-863b-69e21551e3b0","title":"AWS EMR step을 이용한 Spark Batch 작업","slug":"emr-step","publishDate":"July 02, 2017","publishDateISO":"2017-07-02","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch 작업이 있다면 step과 스케줄러를 통해 쉽게 해결할 수 있습니다.

\n
\n

EMR Step

\n

Step은 AWS console 내에서 추가해도 되지만, AWS-Cli를 이용해서 등록해보도록 하겠습니다.\nAWS-Cli로 등록하면 이후에 스크립트로 활용할 수도 있다는 편리함이 있습니다.

\n

AWS EMR step을 등록하는 방법은 아래와 같습니다.\n가독성을 위해 줄바꿈, 띄어쓰기를 했지만 실제로 등록할 때는 전부 붙이셔야 합니다.

\n
$ aws emr add-steps\n    --cluster-id $CLUSTERID,\n    --steps Name=$JOBNAME,\n    Jar=$JARFILE,\n    Args=[\n        /usr/lib/spark/bin/spark-submit,\n        --deploy-mode,client,\n        --properties-file,/etc/spark/conf/spark-defaults.conf,\n        --conf,spark.yarn.executor.memoryOverhead=2048,\n        --conf,spark.executor.memory=4g,\n        --packages,$SPARK_PACKAGES\n    ],\n    ActionOnFailure=${ACTION_ON_FAIL}'
\n

Spark 작업 실행은 Spark-submit을 이용하여 클라이언트에 배포하는 형식입니다.\n이를 위해 jar 파일이 클라이언트의 로컬 경로에 포함되어 있어야 합니다.\nActionOnFailure를 통해 실패 시 Terminate, Stop 등의 옵션을 지정할 수 있습니다.

\n

만약 등록한 작업을 취소하고 싶다면, cancel-steps를 이용하시면 됩니다.

\n
$ aws emr cancel-steps ...
\n

Spark 작업이 주기적으로 실행되어야 한다면,\n가장 간단한 방법은 위의 EMR step 등록 스크립트를 crontab으로 등록하는 것 입니다.\n만약 작업이 다양하고 복잡하다면, AWS Data Pipeline 이라는 제품을 고려해보는 것도 방법입니다.\nhttps://aws.amazon.com/ko/datapipeline/details/

\n
\n

Reference

\n\n
","excerpt":"AWS EMR은 특정 작업을 등록할 수 있는 step 이라는 기능을 제공합니다.\n예를 들어 매일 새벽에 클러스터에서 돌려야하는 Batch…"}}},{"id":"b5ed5b3e-6945-502c-ab66-74d5ac1c4eba","title":"Spark의 Random Sampling에 대하여","slug":"spark-sampling","publishDate":"June 20, 2017","publishDateISO":"2017-06-20","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark에서 랜덤 샘플링을 하는 방법에 대해 정리해보았습니다.

\n
\n

Sample()

\n

Spark RDD API 에는 다양한 sampling 메서드가 존재합니다.\n그 중에서 가장 기본이 되는 sample()에 대해 먼저 알아보겠습니다.

\n
# sample(boolean withReplacement, double fraction, long seed)\nval rdd = sc.parallelize(1 to 10000, 3)\nrdd.sample(false, 0.1, 0).count
\n

첫 번째 인자는 추출 방식을 결정합니다. True면 복원추출, False면 비복원추출 을 실행합니다.\n여기에서 말하는 복원추출이란, 한 번 뽑은 것을 다시 뽑을 수 있게 하는 방법을 말합니다.\n세 번째 인자로 시드 변수를 지정할 수 있습니다.\n시드란, 컴퓨터가 난수를 일정하게 생성하지 않도록 변화를 주는 값을 말합니다.

\n
\n

takeSample()

\n

takeSample()도 랜덤 샘플링을 지원하는 메서드지만, 위와 조금 다른 점이 있습니다.

\n
# takeSample(boolean withReplacement, int num, long seed)\nval rdd = sc.parallelize(1 to 1000, 3)\nrdd.takeSample(false, 100, 1)
\n

takeSample()은 두 번째 인자를 지정하여 몇 개를 추출할 것인지 정할 수 있습니다.\n하지만, 결과 값이 RDD가 아닌 리스트나 배열이기 때문에 메모리에 주의 해야 합니다.\n정리하자면, 크기를 정해놓고 샘플을 추출하고자 한다면 takeSample() 메서드가 적합하고\n메모리를 생각해서 작은 값을 추출할 때 사용하는 것이 좋습니다.

\n

이외에도 sampleByKey, sampleByKeyExtract 메서드가 존재합니다.

\n
\n

Reference

\n\n
","excerpt":"데이터를 분석하다보면 임의의 샘플을 추출해야 하는 상황이 생깁니다.\n그래서 이번에는 Spark…"}}},{"id":"a6401b01-05d6-5899-8c9b-4984720e0f66","title":"Spark의 Temporary View에 대하여","slug":"spark-temp-view","publishDate":"June 16, 2017","publishDateISO":"2017-06-16","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":2,"html":"

SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.0 부터 생긴 Spark Global Temporary View와\n기존의 TempView가 어떤 차이가 있는지 그리고 어떻게 사용해야하는지 알아보곘습니다.

\n
\n

Spark Temporary View

\n

공식문서를 보면 Spark의 Temporary View는 Session-Scope 입니다.\n무슨 말이냐 하면, View의 생명주기가 세션에 달려있다는 뜻 입니다.\n(여기에서 말하는 세션은 SparkSession 입니다)\n그리고, 세션이 종료되면 자동으로 View 테이블이 Drop 됩니다.

\n
\n

CreateOrReplaceTempView

\n
df = spark.sql(query).cache()\nprint df.count()\ndf.CreateOrReplaceTempView(\"TempView\")\ndf.dropTempView(\"TempView\")\ndf.unpersist()
\n

먼저 기존에 사용하던 TempView를 보겠습니다.\n위의 예시는 PySpark 코드입니다.\n세 번째 줄의 createOrReplaceTempView가 View를 생성하는 함수인데,\nSpark은 Lazy evaluation이기 때문에 아직 실행 되기 이전 입니다.\n이후 두 번째 줄에서 count() 함수를 실행하면 생성되며,\nTempView라는 이름으로 메모리에 두고 사용할 수 있게 됩니다.\n다 사용한 다음에는 꼭 unpersist 함수로 할당된 메모리를 해제시켜줘야 합니다.

\n

위와 다르게 Temp View에 대한 명령만 내리고 마지막에 한번에 처리해도 되지만,\n여러 개로 쪼개서 명령을 내리는 것이 상대적으로 빠르다고 합니다.

\n
\n

Global Temporary View

\n
CREATE GLOBAL TEMPORARY VIEW temp_view AS SELECT a, b FROM tbl\nSELECT * FROM global_temp.temp_view\nDROP VIEW global_temp.temp_view
\n

위의 예시는 Spark SQL 코드입니다.\nGlobal Temporary View는 Spark 2.1.0에서 처음 소개되었으며, GLOBAL TEMPORARY VIEW 라는 키워드로 생성합니다.\n그렇게 선언하고 나면 일종의 임시 테이블로 접근할 수 있습니다.\n삭제할 때는 DROP VIEW 라는 키워드로 삭제합니다.

\n

하지만 Global Temporary View는 조금 위험합니다.\n이 View는 말 그대로 전역적인 상태로 남기 위해 시스템의 임시 데이터베이스로 연결됩니다.\n그래서 접근할 때, global_temp로 접근하게 됩니다.

\n

결론부터 말하자면 Global Temporary View는 모든 세션에서 공유 가능하며,\nSpark 어플리케이션이 종료되기 전까지 살아있게 됩니다.\n제 경우 Master 노드의 하드디스크에 저장되어 있었습니다.\n이렇게 되면 일단 IO로 인해 로딩속도가 상당히 느려지고,\n만일 View의 크기가 메모리 용량을 넘어갔더라면 Master가 내려갈 수도 있는 상황입니다.\n이와 같은 이유로 Global Temporary View는 신중히 사용하는 것이 좋습니다.

\n
\n

Reference

\n\n
","excerpt":"SQL의 View 처럼 Spark에서도 View를 지원합니다.\n이 포스팅에서는 Spark 2.1.…"}}},{"id":"99925524-39d0-5943-982f-79148d6dbe29","title":"Pandas DataFrame을 병렬처리 하는 방법","slug":"pandas-parallel","publishDate":"February 27, 2017","publishDateISO":"2017-02-27","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas는 여전히 내부적으로 병렬처리 기능을 지원하지 않습니다.

\n

하지만, 큰 규모의 DataFrame을 돌리다보면 전처리에도 시간이 많이 걸리게 됩니다.\n그런 경우에 병렬처리를 통해 속도를 개선할 수 있습니다.

\n

이 포스팅에서는 가장 간단한 CPU 프로세스 병렬처리를 다루도록 하겠습니다. 방법은 간단합니다.\n거대한 DataFrame을 CPU 코어 수 만큼 분할하고, 전처리 기능을 수행한 다음 다시 합치면 됩니다.

\n
import pandas as pd\nimport numpy as np\nimport seaborn as sns\nfrom multiprocessing import Pool\n\nnum_cores = 4\niris = pd.DataFrame(sns.load_dataset('iris'))
\n

예시로 iris 데이터를 사용하겠습니다.\ncpu 코어의 수는 multiprocessing.cpu_count() 함수를 통해서 얻으실 수 있습니다.

\n
def parallelize_dataframe(df, func):\n    df_split = np.array_split(df, num_cores)\n    pool = Pool(num_cores)\n    df = pd.concat(pool.map(func, df_split))\n    pool.close()\n    pool.join()\n    return df
\n

parallelize_dataframe은 어떤 전처리 함수가 들어왔을 때 CPU 병렬처리를 도와주는 함수입니다.\nmultiprocessing.Pool을 이용하여 분할된 DataFrame에 함수를 적용시키고,\npd.concat()으로 다시 합치는 과정입니다.

\n
def multiply_columns(data):\n    data['length_of_word'] = data['species'].apply(lambda x: len(x))\n    return data
\n

각 종 이름의 글자 수를 세는 전처리 함수를 예로 들어 속도차이를 확인해보겠습니다.\n결과는 아래와 같습니다.

\n
\n

\"pandas-parrallel\"

\n

다른 방법으로 Pandas의 engine에 Dask를 사용하는 방법도 있습니다.\nhttp://dask.readthedocs.io/en/latest/

","excerpt":"Scikit-learn의 모델들은 cython과 joblib으로 최적화 및 자동 병렬처리 되도록 설계되어 있지만,\nPandas…"}}},{"id":"7b5cb907-431b-543e-8953-1ad33cf6b88e","title":"Pandas DataFrame을 MySQL에 저장하는 방법","slug":"dataframe-to-mysql","publishDate":"February 26, 2017","publishDateISO":"2017-02-26","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬3에서는 MySQLdb를 지원하지 않기 때문에, pymysql로 불러와야 합니다.\n꼭 pymysql이 아니어도 상관없지만, 사용해보면 mysql-connector 보다 빠르다는걸 체감할 수 있습니다. 먼저, 필요한 패키지를 설치해줍니다.

\n
# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

SQLAlchemy, pymysql, MySQLdb

\n

install_as_MySQLdb() 함수를 통해 MySQLdb와 호환 가능합니다.\n이제 sqlalchemy를 통해 DB에 연결할 수 있습니다.\n주소에서 root, password는 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\n# MySQL Connector using pymysql\npymysql.install_as_MySQLdb()\nimport MySQLdb\n\nengine = create_engine(\"mysql+mysqldb://root:\"+\"password\"+\"@localhost/db_name\", encoding='utf-8')\nconn = engine.connect()
\n
\n

MySQL에 저장하기

\n

이제 DataFrame을 MySQL에 테이블 형태로 저장할 차례입니다.\n아래와 같이 pandas의 to_sql() 함수를 사용하여 저장하면 됩니다.

\n
df.to_sql(name=table, con=engine, if_exists='append')\npython\n\n자주 사용할 수 있으니 함수로 따로 설정해주면 편합니다.
","excerpt":"Pandas DataFrame을 MySQL에 저장하기 위해 먼저 커넥터가 필요합니다.\n파이썬…"}}},{"id":"79c1215f-bb79-5e21-b334-04fb090a7956","title":"Jupyter Notebook 외부접속 설정하기","slug":"jupyter-config","publishDate":"February 12, 2017","publishDateISO":"2017-02-12","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

이번 포스팅에서는 Jupyter Notebook을 환경구축하고 난 이후에 외부접속을 설정하는 과정에 대해 알아보겠습니다. 환경구축하는 방법에 대해서는 이전의 포스팅 https://swalloow.github.io/jupyter-notebook-kernel 을 참고해주시기 바랍니다.

\n
\n

외부접속 허용하기

\n

우선 ~/.jupyter/jupyter_notebook_config.py 에 있는 Jupyter Notebook의 설정파일을 열어줍니다. 아마 모두 주석이 걸려있을텐데 필요한 부분만 수정해주시면 됩니다.

\n\n
\n

비밀번호 설정하기

\n

비밀번호를 설정하면 url에 접속했을 때, 암호를 입력하는 화면이 나타나게 됩니다. Jupyter Notebook에서는 HASH 값을 통해 암호화된 비밀번호를 적용할 수 있습니다.

\n

먼저, 새로운 노트를 생성하고 다음의 스크립트를 작성합니다. 암호를 설정하는 칸이 나오고 결과 값이 주어지면 그대로 복사해서 c.NotebookApp.password = u'' 여기에 붙여넣기 하시면 됩니다.

\n
from notebook.auth import passwd;\npasswd()
","excerpt":"이번 포스팅에서는 Jupyter Notebook…"}}},{"id":"ea6cffe1-0590-587f-975e-f196ce841ed7","title":"DB 테이블을 DataFrame으로 읽어오는 방법","slug":"db-to-dataframe","publishDate":"January 14, 2017","publishDateISO":"2017-01-14","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":1,"html":"

본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL 뿐만 아니라 모든 데이터베이스에 적용가능합니다.

\n

먼저 sqlalchemy가 설치되어 있지 않다면 설치해줍니다.\nsqlalchemy와 mysql을 연결하는 패키지가 필요합니다.

\n

파이썬2를 사용한다면 mysql-python, 3을 사용한다면 pymysql을 설치해주면 됩니다.

\n
# python2\n$ pip install mysql-python\n$ pip install sqlalchemy\n\n# python3\n$ pip install pymysql\n$ pip install sqlalchemy
\n
\n

이제 sqlalchemy를 통해 DB에 연결해보겠습니다.\n주소에서 root, password, table은 DB에 맞게 변경해야 합니다.

\n
import pandas as pd\nfrom sqlalchemy import create_engine\n\nengine = create_engine('mysql://root:password@localhost/table', convert_unicode=True)\nconn = engine.connect()
\n
\n

마지막으로 pandas를 통해 table을 읽어들일 차례입니다.\npandas의 read_sql() 은 0.19 버전부터 생겨났으며, sqlalchemy를 필수로 사용하도록 되어 있습니다.

\n
data = pd.read_sql_table('table_name', conn)\ndata.head()
\n
\n

MySQL dump 파일을 읽어오는 방법

\n

추가로 외부로부터 데이터를 넘겨받을 때 DB dump 파일 (.sql) 을 넘겨받는 경우가 있습니다.\n데이터베이스 전체를 받은 dump 파일이라면, 커멘드에 다음과 같이 입력합니다.

\n
# root, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database > data.sql
\n
\n

특정 테이블만 받고 싶다면, 커멘드에 다음과 같이 입력합니다.

\n
# root, table, database, data.sql은 알아서 수정\n$ mysqldump -u root -p database table > data.sql
\n
\n

위와 같은 과정이 끝나면, 나의 MySQL 계정에 데이터가 저장된 것을 확인할 수 있습니다.\n이후에는 앞에서 설명한대로 pandas를 통해 DataFrame으로 변환하면 됩니다.

","excerpt":"본 포스팅에서는 예시를 MySQL로 들지만 sqlalchemy의 커넥터만 변경해주면,\nMySQL…"}}},{"id":"5b58d9b9-e77f-55a7-99ee-76786a0036f7","title":"Jupyter Notebook 다중커널 설정하기","slug":"jupyter-notebook-kernel","publishDate":"January 28, 2017","publishDateISO":"2017-01-28","heroImage":{"title":"cover-dataengineering","gatsbyImageData":{"images":{"sources":[{"srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&q=50&fm=webp 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&q=50&fm=webp 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&q=50&fm=webp 1600w","sizes":"(min-width: 1600px) 1600px, 100vw","type":"image/webp"}],"fallback":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg","srcSet":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=400&h=267&fl=progressive&q=50&fm=jpg 400w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=800&h=533&fl=progressive&q=50&fm=jpg 800w,\nhttps://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1600&h=1067&fl=progressive&q=50&fm=jpg 1600w","sizes":"(min-width: 1600px) 1600px, 100vw"}},"layout":"constrained","width":1800,"height":1200,"placeholder":{"fallback":"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAlgCWAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAANABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgb/xAAcEAACAgMBAQAAAAAAAAAAAAAAAQIREiExYeH/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDK1DF0vgtxW9EylQu8nTotmo+gHfAEP//Z"}},"ogimg":{"src":"https://images.ctfassets.net/tushy4jlcik7/7uo9TsqFN9EBsDBqDJ5vXl/4c58a9f94babb15d8fd996c247737656/cover_dataengineering.jpg?w=1800&q=50"}},"body":{"childMarkdownRemark":{"timeToRead":5,"html":"

Jupyer Notebook은 웹 기반의 대화형 노트북 지원으로 수식, 표, 그림 등을 표현하기 쉬운 개발 환경입니다.\n코딩과 문서화(Markdown)까지 한 화면에서 가능하며 커널 확장을 통해 다양한 파이썬 버전 뿐만 아니라 여러 언어를 지원합니다.

\n

이제 파이썬을 처음 설치한다고 가정하고 맥 OS에서 간단하게 jupyter 환경설정하는 방법을 소개해드리고자 합니다.

\n
\n

pyenv 설치하기

\n

1. Homebrew를 통해 pyenv를 설치

\n
$ brew install pyenv
\n
\n

2. pyenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv init -)\"' >> ~/.bashrc
\n
\n

3. pyenv 사용해보기

\n
$ pyenv versions\nsystem (set by /Users/USERNAME/.pyenv/version)
\n
\n

4. pyenv 명령어 정리

\n
$ pyenv install <version>\n$ pyenv uninstall <version>\n$ pyenv install -list\n$ pyenv shell <version>\n$ pyenv activate <environment>\n$ pyenv deactivate <environment>
\n
\n

pyenv-virtualenv 설치하기

\n

1. Homebrew를 통해 pyenv-virtualenv를 설치

\n
$ brew install pyenv-virtualenv
\n
\n

2. virtualenv init을 ~/.bashrc에 추가 (zsh를 사용하는 경우 ~/.zshrc)

\n
$ echo 'eval \"$(pyenv virtualenv-init -)\"' >> ~/.bashrc
\n
\n

2. pyenv-virtualenv 사용해보기

\n
# pyenv virtualenv [python version] [myname]\n$ pyenv virtualenv 2.7.11 python2\n$ pyenv virtualenv 3.5.1 python3
\n
\n

2. virtualenv 명령어 정리

\n
$ pyenv virtualenv versions\n$ pyenv virtualenv [python version] [myname]\n$ pyenv shell [myname]
\n
\n

Jupyter Notebook 설치

\n

이제 방금 설치했던 파이썬 2와 3 버전의 환경에 python, notebook, jupyter를 설치할 차례입니다.\n따라서 방금 설치한 환경을 각각 activate한 다음에 아래와 같은 명령어를 실행시켜야 합니다.

\n
\n

1. pip install (python2, python3 각각 실행)

\n
$ pip install ipython\n$ pip install notebook\n$ pip install jupyter
\n
\n

2. 초기 Jupyter configuration 파일 생성 (마찬가지로 각각 실행)

\n
$ jupyter notebook --generate-config\nInstalled kernelspec python3 in /Users/username/Library/Jupyter/kernels/python3
\n
\n

3. 생성된 jupyter_notebook_config.py 설정 (원하는 경우에만 커스텀 설정)

\n
$ vi /Users/username/Library/Jupyter/kernels/python3/jupyter_notebook_config.py\n\n$ c.NotebookApp.ip = '127.0.0.1'\n$ c.NotebookApp.open_browser = False\n$ c.NotebookApp.port = 8888\n$ c.NotebookApp.password = [SHA password]
\n
\n

4. ipykernel 설정 (마찬가지로 각각 실행)

\n
$ pyenv shell python2\n$ python -m ipykernel install --user\nInstalled kernelspec python2 in /home/seen/.local/share/jupyter/kernels/python2
\n
\n

5. kernel.json 확인 (원하는 경우에만 커스텀 설정)

\n
$ vi /home/seen/.local/share/jupyter/kernels/python2/kernel.json\n{\n  \"display_name\": \"Python 2\",\n  \"language\": \"python\",\n  \"argv\": [\n    \"/home/seen/.pyenv/versions/py27/bin/python\",\n    \"-m\",\n    \"ipykernel\",\n    \"-f\",\n    \"{connection_file}\"\n    ]\n  }\n}
\n
\n

6. jupyter notebook을 실행

\n
$ jupyter notebook\n\n# background running\n$ nohup jupyter notebook &\n\n# kill process\n$ ps -a\n37788 ttys000 0:00:00 ...python (노트북을 실행한 프로세스)\n$ kill 37788
\n
\n

정리

\n

윈도우10 에서 아주 고생했던 환경설정이 맥 OS에서는 아주 간편하게 됩니다…\n잘 안되거나 오류가 생기시면 댓글로 알려주시면 감사하겠습니다!

\n
\n

참고링크

\n","excerpt":"Jupyer Notebook…"}}}]}},"pageContext":{"slug":"dataengineering","basePath":"","paginationPath":"/tag/dataengineering","pageNumber":0,"humanPageNumber":1,"skip":0,"limit":6,"numberOfPages":7,"previousPagePath":"","nextPagePath":"/tag/dataengineering/2"}},"staticQueryHashes":["1946181227","2744905544","3732430097"]} \ No newline at end of file diff --git a/pandas-2-0-copy-on-write/index.html b/pandas-2-0-copy-on-write/index.html index 22a2f40..f1c229c 100644 --- a/pandas-2-0-copy-on-write/index.html +++ b/pandas-2-0-copy-on-write/index.html @@ -430,4 +430,4 @@

https://pandas.pydata.org/docs/user_guide/copy_on_write.html
  • https://pandas.pydata.org/pdeps/0007-copy-on-write.html
  • https://phofl.github.io/cow-deep-dive.html
  • -
    Next →
    \ No newline at end of file +
    ← PrevNext →
    \ No newline at end of file diff --git a/sitemap/sitemap-0.xml b/sitemap/sitemap-0.xml index 23505b6..b52eb98 100644 --- a/sitemap/sitemap-0.xml +++ b/sitemap/sitemap-0.xml @@ -1 +1 @@ -https://swalloow.github.io/pandas-2-0-copy-on-write/daily0.7https://swalloow.github.io/spark-on-kubernetes-scheduler-2/daily0.7https://swalloow.github.io/spark-on-kubernetes-scheduler/daily0.7https://swalloow.github.io/berlin/daily0.7https://swalloow.github.io/mlops-dmls-fsdl/daily0.7https://swalloow.github.io/spark-on-kubernetes-spot-instance/daily0.7https://swalloow.github.io/gpu-utilization/daily0.7https://swalloow.github.io/airflow-worker-keda-autoscaler/daily0.7https://swalloow.github.io/container-tini-dumb-init/daily0.7https://swalloow.github.io/eks-karpenter-groupless-autoscaling/daily0.7https://swalloow.github.io/feat-adr/daily0.7https://swalloow.github.io/jupyterhub-tensorboard/daily0.7https://swalloow.github.io/jupyterhub-on-kubernetes/daily0.7https://swalloow.github.io/data-mesh-principle/daily0.7https://swalloow.github.io/spark-on-kubernetes-perf/daily0.7https://swalloow.github.io/airflow-multi-tenent-1/daily0.7https://swalloow.github.io/airflow-sidecar/daily0.7https://swalloow.github.io/airflow-on-kubernetes-3/daily0.7https://swalloow.github.io/airflow-on-kubernetes-2/daily0.7https://swalloow.github.io/umbrella-helm-chart/daily0.7https://swalloow.github.io/airflow-on-kubernetes-1/daily0.7https://swalloow.github.io/gatsby-contentful/daily0.7https://swalloow.github.io/eks-cidr/daily0.7https://swalloow.github.io/aws-cert/daily0.7https://swalloow.github.io/eks-autoscale/daily0.7https://swalloow.github.io/eks-vpc-cni/daily0.7https://swalloow.github.io/aws-cli-mfa/daily0.7https://swalloow.github.io/tf-tips/daily0.7https://swalloow.github.io/serverless-etl/daily0.7https://swalloow.github.io/openinfra/daily0.7https://swalloow.github.io/container-patterns3/daily0.7https://swalloow.github.io/container-patterns2/daily0.7https://swalloow.github.io/eks-kubeflow/daily0.7https://swalloow.github.io/why-kubeflow/daily0.7https://swalloow.github.io/aws-kops/daily0.7https://swalloow.github.io/container-patterns/daily0.7https://swalloow.github.io/airflow-contrib/daily0.7https://swalloow.github.io/kafka-connect/daily0.7https://swalloow.github.io/start/daily0.7https://swalloow.github.io/raft-consensus/daily0.7https://swalloow.github.io/aws-hadoop/daily0.7https://swalloow.github.io/portfolio-basic/daily0.7https://swalloow.github.io/structuring-tf/daily0.7https://swalloow.github.io/data-science-inconvenient-truth/daily0.7https://swalloow.github.io/deep-learning-style/daily0.7https://swalloow.github.io/zeppelin-bootstrap/daily0.7https://swalloow.github.io/aws-emr-s3-spark/daily0.7https://swalloow.github.io/spark-shuffling/daily0.7https://swalloow.github.io/spark-reduceByKey-groupByKey/daily0.7https://swalloow.github.io/hive-metastore-issue/daily0.7https://swalloow.github.io/bagging-boosting/daily0.7https://swalloow.github.io/spark-df-mysql/daily0.7https://swalloow.github.io/spark22/daily0.7https://swalloow.github.io/scala-sbt/daily0.7https://swalloow.github.io/emr-step/daily0.7https://swalloow.github.io/spark-sampling/daily0.7https://swalloow.github.io/spark-temp-view/daily0.7https://swalloow.github.io/netflix/daily0.7https://swalloow.github.io/sanfran-travel/daily0.7https://swalloow.github.io/influx-grafana1/daily0.7https://swalloow.github.io/influx-grafana2/daily0.7https://swalloow.github.io/spring-boot-jpa/daily0.7https://swalloow.github.io/gitlabci-docker/daily0.7https://swalloow.github.io/dockerfile-ignore/daily0.7https://swalloow.github.io/dockerfile/daily0.7https://swalloow.github.io/polyglot-programming/daily0.7https://swalloow.github.io/system-monitoring/daily0.7https://swalloow.github.io/jupyter-spark/daily0.7https://swalloow.github.io/ssh-tunneling/daily0.7https://swalloow.github.io/scala-for-bigdata/daily0.7https://swalloow.github.io/map-reduce/daily0.7https://swalloow.github.io/aws-ec2/daily0.7https://swalloow.github.io/flask-security/daily0.7https://swalloow.github.io/implement-jwt/daily0.7https://swalloow.github.io/https-ssl/daily0.7https://swalloow.github.io/pandas-parallel/daily0.7https://swalloow.github.io/dataframe-to-mysql/daily0.7https://swalloow.github.io/swagger-api-doc/daily0.7https://swalloow.github.io/git-stash/daily0.7https://swalloow.github.io/docker-command/daily0.7https://swalloow.github.io/docker-install/daily0.7https://swalloow.github.io/linux3/daily0.7https://swalloow.github.io/linux2/daily0.7https://swalloow.github.io/linux1/daily0.7https://swalloow.github.io/jupyter-config/daily0.7https://swalloow.github.io/decision-randomforest/daily0.7https://swalloow.github.io/pyml-intro2/daily0.7https://swalloow.github.io/pyml-intro1/daily0.7https://swalloow.github.io/social-api-cognito/daily0.7https://swalloow.github.io/jupyter-notebook-kernel/daily0.7https://swalloow.github.io/open-api-guide/daily0.7https://swalloow.github.io/db-to-dataframe/daily0.7https://swalloow.github.io/macbook-setting/daily0.7https://swalloow.github.io/about-oauth2/daily0.7https://swalloow.github.io/daily0.7https://swalloow.github.io/2daily0.7https://swalloow.github.io/3daily0.7https://swalloow.github.io/4daily0.7https://swalloow.github.io/5daily0.7https://swalloow.github.io/6daily0.7https://swalloow.github.io/7daily0.7https://swalloow.github.io/8daily0.7https://swalloow.github.io/9daily0.7https://swalloow.github.io/10daily0.7https://swalloow.github.io/11daily0.7https://swalloow.github.io/12daily0.7https://swalloow.github.io/13daily0.7https://swalloow.github.io/14daily0.7https://swalloow.github.io/15daily0.7https://swalloow.github.io/16daily0.7https://swalloow.github.io/tag/financialdaily0.7https://swalloow.github.io/tag/dataengineeringdaily0.7https://swalloow.github.io/tag/dataengineering/2daily0.7https://swalloow.github.io/tag/dataengineering/3daily0.7https://swalloow.github.io/tag/dataengineering/4daily0.7https://swalloow.github.io/tag/dataengineering/5daily0.7https://swalloow.github.io/tag/dataengineering/6daily0.7https://swalloow.github.io/tag/dataengineering/7daily0.7https://swalloow.github.io/tag/datasciencedaily0.7https://swalloow.github.io/tag/devopsdaily0.7https://swalloow.github.io/tag/devops/2daily0.7https://swalloow.github.io/tag/devops/3daily0.7https://swalloow.github.io/tag/devops/4daily0.7https://swalloow.github.io/tag/developdaily0.7https://swalloow.github.io/tag/develop/2daily0.7https://swalloow.github.io/tag/develop/3daily0.7https://swalloow.github.io/tag/personaldaily0.7https://swalloow.github.io/tag/personal/2daily0.7https://swalloow.github.io/about/daily0.7https://swalloow.github.io/contact/daily0.7 \ No newline at end of file +https://swalloow.github.io/llm-dataplatform/daily0.7https://swalloow.github.io/pandas-2-0-copy-on-write/daily0.7https://swalloow.github.io/spark-on-kubernetes-scheduler-2/daily0.7https://swalloow.github.io/spark-on-kubernetes-scheduler/daily0.7https://swalloow.github.io/berlin/daily0.7https://swalloow.github.io/mlops-dmls-fsdl/daily0.7https://swalloow.github.io/spark-on-kubernetes-spot-instance/daily0.7https://swalloow.github.io/gpu-utilization/daily0.7https://swalloow.github.io/airflow-worker-keda-autoscaler/daily0.7https://swalloow.github.io/container-tini-dumb-init/daily0.7https://swalloow.github.io/eks-karpenter-groupless-autoscaling/daily0.7https://swalloow.github.io/feat-adr/daily0.7https://swalloow.github.io/jupyterhub-tensorboard/daily0.7https://swalloow.github.io/jupyterhub-on-kubernetes/daily0.7https://swalloow.github.io/data-mesh-principle/daily0.7https://swalloow.github.io/spark-on-kubernetes-perf/daily0.7https://swalloow.github.io/airflow-multi-tenent-1/daily0.7https://swalloow.github.io/airflow-sidecar/daily0.7https://swalloow.github.io/airflow-on-kubernetes-3/daily0.7https://swalloow.github.io/airflow-on-kubernetes-2/daily0.7https://swalloow.github.io/umbrella-helm-chart/daily0.7https://swalloow.github.io/airflow-on-kubernetes-1/daily0.7https://swalloow.github.io/gatsby-contentful/daily0.7https://swalloow.github.io/eks-cidr/daily0.7https://swalloow.github.io/aws-cert/daily0.7https://swalloow.github.io/eks-autoscale/daily0.7https://swalloow.github.io/eks-vpc-cni/daily0.7https://swalloow.github.io/aws-cli-mfa/daily0.7https://swalloow.github.io/tf-tips/daily0.7https://swalloow.github.io/serverless-etl/daily0.7https://swalloow.github.io/openinfra/daily0.7https://swalloow.github.io/container-patterns3/daily0.7https://swalloow.github.io/container-patterns2/daily0.7https://swalloow.github.io/eks-kubeflow/daily0.7https://swalloow.github.io/why-kubeflow/daily0.7https://swalloow.github.io/aws-kops/daily0.7https://swalloow.github.io/container-patterns/daily0.7https://swalloow.github.io/airflow-contrib/daily0.7https://swalloow.github.io/kafka-connect/daily0.7https://swalloow.github.io/start/daily0.7https://swalloow.github.io/raft-consensus/daily0.7https://swalloow.github.io/aws-hadoop/daily0.7https://swalloow.github.io/portfolio-basic/daily0.7https://swalloow.github.io/structuring-tf/daily0.7https://swalloow.github.io/data-science-inconvenient-truth/daily0.7https://swalloow.github.io/deep-learning-style/daily0.7https://swalloow.github.io/zeppelin-bootstrap/daily0.7https://swalloow.github.io/aws-emr-s3-spark/daily0.7https://swalloow.github.io/spark-shuffling/daily0.7https://swalloow.github.io/spark-reduceByKey-groupByKey/daily0.7https://swalloow.github.io/hive-metastore-issue/daily0.7https://swalloow.github.io/bagging-boosting/daily0.7https://swalloow.github.io/spark-df-mysql/daily0.7https://swalloow.github.io/spark22/daily0.7https://swalloow.github.io/scala-sbt/daily0.7https://swalloow.github.io/emr-step/daily0.7https://swalloow.github.io/spark-sampling/daily0.7https://swalloow.github.io/spark-temp-view/daily0.7https://swalloow.github.io/netflix/daily0.7https://swalloow.github.io/sanfran-travel/daily0.7https://swalloow.github.io/influx-grafana1/daily0.7https://swalloow.github.io/influx-grafana2/daily0.7https://swalloow.github.io/spring-boot-jpa/daily0.7https://swalloow.github.io/gitlabci-docker/daily0.7https://swalloow.github.io/dockerfile-ignore/daily0.7https://swalloow.github.io/dockerfile/daily0.7https://swalloow.github.io/polyglot-programming/daily0.7https://swalloow.github.io/system-monitoring/daily0.7https://swalloow.github.io/jupyter-spark/daily0.7https://swalloow.github.io/ssh-tunneling/daily0.7https://swalloow.github.io/scala-for-bigdata/daily0.7https://swalloow.github.io/map-reduce/daily0.7https://swalloow.github.io/aws-ec2/daily0.7https://swalloow.github.io/flask-security/daily0.7https://swalloow.github.io/implement-jwt/daily0.7https://swalloow.github.io/https-ssl/daily0.7https://swalloow.github.io/pandas-parallel/daily0.7https://swalloow.github.io/dataframe-to-mysql/daily0.7https://swalloow.github.io/swagger-api-doc/daily0.7https://swalloow.github.io/git-stash/daily0.7https://swalloow.github.io/docker-command/daily0.7https://swalloow.github.io/docker-install/daily0.7https://swalloow.github.io/linux3/daily0.7https://swalloow.github.io/linux2/daily0.7https://swalloow.github.io/linux1/daily0.7https://swalloow.github.io/jupyter-config/daily0.7https://swalloow.github.io/decision-randomforest/daily0.7https://swalloow.github.io/pyml-intro2/daily0.7https://swalloow.github.io/pyml-intro1/daily0.7https://swalloow.github.io/social-api-cognito/daily0.7https://swalloow.github.io/jupyter-notebook-kernel/daily0.7https://swalloow.github.io/open-api-guide/daily0.7https://swalloow.github.io/db-to-dataframe/daily0.7https://swalloow.github.io/macbook-setting/daily0.7https://swalloow.github.io/about-oauth2/daily0.7https://swalloow.github.io/daily0.7https://swalloow.github.io/2daily0.7https://swalloow.github.io/3daily0.7https://swalloow.github.io/4daily0.7https://swalloow.github.io/5daily0.7https://swalloow.github.io/6daily0.7https://swalloow.github.io/7daily0.7https://swalloow.github.io/8daily0.7https://swalloow.github.io/9daily0.7https://swalloow.github.io/10daily0.7https://swalloow.github.io/11daily0.7https://swalloow.github.io/12daily0.7https://swalloow.github.io/13daily0.7https://swalloow.github.io/14daily0.7https://swalloow.github.io/15daily0.7https://swalloow.github.io/16daily0.7https://swalloow.github.io/tag/financialdaily0.7https://swalloow.github.io/tag/dataengineeringdaily0.7https://swalloow.github.io/tag/dataengineering/2daily0.7https://swalloow.github.io/tag/dataengineering/3daily0.7https://swalloow.github.io/tag/dataengineering/4daily0.7https://swalloow.github.io/tag/dataengineering/5daily0.7https://swalloow.github.io/tag/dataengineering/6daily0.7https://swalloow.github.io/tag/dataengineering/7daily0.7https://swalloow.github.io/tag/datasciencedaily0.7https://swalloow.github.io/tag/devopsdaily0.7https://swalloow.github.io/tag/devops/2daily0.7https://swalloow.github.io/tag/devops/3daily0.7https://swalloow.github.io/tag/devops/4daily0.7https://swalloow.github.io/tag/developdaily0.7https://swalloow.github.io/tag/develop/2daily0.7https://swalloow.github.io/tag/develop/3daily0.7https://swalloow.github.io/tag/personaldaily0.7https://swalloow.github.io/tag/personal/2daily0.7https://swalloow.github.io/about/daily0.7https://swalloow.github.io/contact/daily0.7 \ No newline at end of file diff --git a/tag/dataengineering/2/index.html b/tag/dataengineering/2/index.html index a864ab9..090a549 100644 --- a/tag/dataengineering/2/index.html +++ b/tag/dataengineering/2/index.html @@ -68,6 +68,6 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
    Skip to content

    41 Posts Tagged: “DataEngineering

    2 / 7
    PrevNext
    • Powered by Contentful
    • COPYRIGHT © 2020 by @swalloow
    \ No newline at end of file diff --git a/tag/dataengineering/3/index.html b/tag/dataengineering/3/index.html index a1b1299..0c2f4ac 100644 --- a/tag/dataengineering/3/index.html +++ b/tag/dataengineering/3/index.html @@ -68,5 +68,5 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
    Skip to content

    41 Posts Tagged: “DataEngineering

    3 / 7
    PrevNext
    • Powered by Contentful
    • COPYRIGHT © 2020 by @swalloow
    \ No newline at end of file +} catch (e) {} })();
    Skip to content

    42 Posts Tagged: “DataEngineering

    3 / 7
    PrevNext
    • Powered by Contentful
    • COPYRIGHT © 2020 by @swalloow
    \ No newline at end of file diff --git a/tag/dataengineering/4/index.html b/tag/dataengineering/4/index.html index 10fd96f..9509b8c 100644 --- a/tag/dataengineering/4/index.html +++ b/tag/dataengineering/4/index.html @@ -68,9 +68,8 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
    Skip to content

    41 Posts Tagged: “DataEngineering

    4 / 7
    PrevNext
    • Powered by Contentful
    • COPYRIGHT © 2020 by @swalloow
    \ No newline at end of file diff --git a/tag/dataengineering/5/index.html b/tag/dataengineering/5/index.html index 5527153..9183b80 100644 --- a/tag/dataengineering/5/index.html +++ b/tag/dataengineering/5/index.html @@ -68,6 +68,7 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
    Skip to content

    41 Posts Tagged: “DataEngineering

    5 / 7
    PrevNext
    • Powered by Contentful
    • COPYRIGHT © 2020 by @swalloow
    \ No newline at end of file diff --git a/tag/dataengineering/6/index.html b/tag/dataengineering/6/index.html index 8e83428..e6897ca 100644 --- a/tag/dataengineering/6/index.html +++ b/tag/dataengineering/6/index.html @@ -68,8 +68,8 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
    Skip to content

    41 Posts Tagged: “DataEngineering

    6 / 7
    PrevNext
    • Powered by Contentful
    • COPYRIGHT © 2020 by @swalloow
    \ No newline at end of file diff --git a/tag/dataengineering/7/index.html b/tag/dataengineering/7/index.html index c3a65a4..2a52413 100644 --- a/tag/dataengineering/7/index.html +++ b/tag/dataengineering/7/index.html @@ -68,7 +68,7 @@ var mode = localStorage.getItem('theme-ui-color-mode'); if (!mode) return document.documentElement.classList.add('theme-ui-' + mode); -} catch (e) {} })();
    Skip to content

    41 Posts Tagged: “DataEngineering