diff --git a/feed.xml b/feed.xml index 962f0f7..9fa22cd 100644 --- a/feed.xml +++ b/feed.xml @@ -1,4 +1,4 @@ -<![CDATA[Swalloow Blog]]>https://swalloow.github.ioGatsbyJSSun, 21 Jan 2024 08:22:35 GMT<![CDATA[AI를 통해 진화하는 데이터플랫폼 근황]]><![CDATA[Swalloow Blog]]>https://swalloow.github.ioGatsbyJSSun, 21 Jan 2024 08:51:51 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 취득 후기]]>
Skip to content
  • cover-dataengineering

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

    January 21, 2024

    4 min read

    생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다. +} 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 index f5ccd50..82a0ca4 100644 --- a/llm-dataplatform/index.html +++ b/llm-dataplatform/index.html @@ -1,7 +1,7 @@ AI를 통해 진화하는 데이터플랫폼 근황 | Swalloow BlogAI를 통해 변화하는 데이터플랫폼 근황 | Swalloow Blog
    Skip to content
    cover-dataengineering

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

    • DataEngineering

    📅 January 21, 2024

    ⏱️4 min read

    생성형 AI의 공개 이후 다양한 영역에서 활용하는 사례가 늘어나고 있습니다.
    +} catch (e) {} })();

    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에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다. +

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

    +

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


    검색 UI 연동

    @@ -102,7 +103,7 @@

    -

    그 중 첫 번째는 검색 UI를 연동하는 방식입니다. +

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


    @@ -111,14 +112,16 @@

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

    -

    두 번째는 SQL 함수나 자연어 SDK를 추가하는 방식입니다. + +# english sdk +new_df = df.ai.transform('get 4 week moving average sales by dept')

    +

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

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



    기술 문서 검색

    -

    개발자는 개발 과정에서 문서 검색에 많은 시간을 할애합니다. +

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


    AWS Amazon Q Assistant

    @@ -144,7 +147,7 @@

    데이터 거버넌스 도구

    -

    데이터 거버넌스는 정책을 만드는 일보다 운영하는데 더 많은 노력이 들어갑니다. +

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

    -

    데이터 디스커버리 영역의 경우, 기존 UI 기반 검색 엔진에 자연어 질의가 추가됩니다. +

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

    -

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

    +

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

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

    -

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

    +

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



    Reference

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

    diff --git a/page-data/index/page-data.json b/page-data/index/page-data.json index ecb3c0f..39ff1f7 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":"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
    • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
    • \n
    • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
    • \n
    • Admission: CRD API에 대한 유효성 검사를 담당합니다.
    • \n
    \n

    PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

    \n
    \n

    스케줄링이 실행되는 워크플로우는 다음과 같습니다.

    \n

    \n \n \n \n

    \n
      \n
    • client가 제출한 작업을 watch하고 캐싱합니다.
    • \n
    • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
    • \n
    • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
    • \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
    • TaskGroup이 정의된 application을 submit 합니다.
    • \n
    • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
    • \n
    • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
    • \n
    • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
    • \n
    • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
    • \n
    • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
    • \n
    • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
    • \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
    • spark-submit 명령어 실행
    • \n
    • Kube API를 통해 driver pod 생성
    • \n
    • driver pod → API Server에 executor 생성 요청
    • \n
    • Kube API를 통해 executor 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
    • driver 리소스 요청 → 1대 생성
    • \n
    • executor 리소스 요청 → 2대 생성
    • \n
    \n

    \n \n \n \n

    \n

    위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
    \n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

    \n
      \n
    • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
    • \n
    • driver, executor pod 즉시 할당
    • \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 +{"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이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다.
    \n쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

    \n
    \n

    검색 UI 연동

    \n

    \n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n# english sdk\nnew_df = df.ai.transform('get 4 week moving average sales by dept')

    \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
    • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
    • \n
    • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
    • \n
    • Admission: CRD API에 대한 유효성 검사를 담당합니다.
    • \n
    \n

    PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

    \n
    \n

    스케줄링이 실행되는 워크플로우는 다음과 같습니다.

    \n

    \n \n \n \n

    \n
      \n
    • client가 제출한 작업을 watch하고 캐싱합니다.
    • \n
    • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
    • \n
    • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
    • \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
    • TaskGroup이 정의된 application을 submit 합니다.
    • \n
    • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
    • \n
    • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
    • \n
    • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
    • \n
    • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
    • \n
    • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
    • \n
    • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
    • \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
    • spark-submit 명령어 실행
    • \n
    • Kube API를 통해 driver pod 생성
    • \n
    • driver pod → API Server에 executor 생성 요청
    • \n
    • Kube API를 통해 executor 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
    • driver 리소스 요청 → 1대 생성
    • \n
    • executor 리소스 요청 → 2대 생성
    • \n
    \n

    \n \n \n \n

    \n

    위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
    \n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

    \n
      \n
    • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
    • \n
    • driver, executor pod 즉시 할당
    • \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 index b2e3f5a..b8583fa 100644 --- a/page-data/llm-dataplatform/page-data.json +++ b/page-data/llm-dataplatform/page-data.json @@ -1 +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 ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n# english sdk\nnew_df = df.ai.transform('get 4 week moving average sales by dept')

    \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 '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
    • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
    • \n
    • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
    • \n
    • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
    • \n
    • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
    • \n
    \n
    \n

    Consistency in Infrastructure

    \n

    Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

    \n
      \n
    • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
    • \n
    • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
    • \n
    • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
    • \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
    • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
    • \n
    • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
    • \n
    • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
    • \n
    • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
    • \n
    \n

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

    \n

    먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

    \n
      \n
    • webserver: Airflow UI, RBAC, DAG monitoring
    • \n
    • scheduler: task monitoring, trigger, DAG sync, DAG processing
    • \n
    • executor: how task instance running (pluggable)
    • \n
    • worker: task instance processing
    • \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
    • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
    • \n
    • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
    • \n
    • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
    • \n
    • 차트에 Ingress 설정에 대한 옵션이 부족
    • \n
    • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
    • \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
    • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
    • \n
    • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
    • \n
    • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
    • \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
    • A 사용자는 A 사용자의 DAG만 볼 수 있음
    • \n
    • A 사용자는 B 사용자의 DAG을 볼 수 없음
    • \n
    • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
    • \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
    • /airflow/prod/connections/myrole/connection_id
    • \n
    • /airflow/prod/variables/myrole/variable_id
    • \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
    • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
    • \n
    • 특정 task의 timeout은 48시간을 넘을 수 없다
    • \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
    • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
    • \n
    • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
    • \n
    • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
    • \n
    • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
    • \n
    \n

    Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

    \n
    \n

    SparkR

    \n

    이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

    \n
      \n
    • R API에 Structured Streaming, Catalog가 추가되었습니다.
    • \n
    • to_json, from_json 메서드가 추가되었습니다.
    • \n
    • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
    • \n
    \n
    \n

    GraphX

    \n

    GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

    \n
      \n
    • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
    • \n
    • PageRank, Pregel API가 개선되었습니다.
    • \n
    \n
    \n

    Core and SparkSQL, Deprecations

    \n

    마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

    \n
      \n
    • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
    • \n
    • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
    • \n
    • Cost-Based Optimizer 성능이 개선되었습니다.
    • \n
    • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
    • \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
    • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
    • \n
    • Chunk Server: 물리적인 서버, 실제 입출력을 처리
    • \n
    • 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
    • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
    • \n
    • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
    • \n
    • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
    • \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
    • s3.part.size: S3의 multi part upload 사이즈를 지정
    • \n
    • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
    • \n
    • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
    • \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
    • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
    • \n
    • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
    • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다.
\n쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n# english sdk\nnew_df = df.ai.transform('get 4 week moving average sales by dept')

\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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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 6479c51..99a10d2 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":"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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다.
\n쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n# english sdk\nnew_df = df.ai.transform('get 4 week moving average sales by dept')\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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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 f909810..9a45380 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":"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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다.
\n쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n# english sdk\nnew_df = df.ai.transform('get 4 week moving average sales by dept')\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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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 fc92b4d..bd58158 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":"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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다.
\n쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n# english sdk\nnew_df = df.ai.transform('get 4 week moving average sales by dept')\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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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 74fbf3c..1718a86 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":"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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다.
\n쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n# english sdk\nnew_df = df.ai.transform('get 4 week moving average sales by dept')\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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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 16e0dac..5f791dd 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":"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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다.
\n쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n# english sdk\nnew_df = df.ai.transform('get 4 week moving average sales by dept')\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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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 0378951..f64330f 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":"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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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이란 주어진 자연어로부터 쿼리문을 생성하는 것을 말합니다.
\n쉽게 말해 사용자가 AI에게 한글로 질문하면 필요한 쿼리를 만들어주는 기능입니다.\n데이터플랫폼에서는 그 동안 쿼리 사용에 어려움을 겪는 비개발자도 쉽게 사용할 수 있도록 다양한 데이터 분석 도구들을 만들어왔습니다. 하지만 이제 UI가 아닌 \"자연어\" 라는 인터페이스를 통해 쉽게 탐색할 수 있게 되었습니다. Text2SQL 기술을 플랫폼에 적용하는 방식은 크게 두 가지로 볼 수 있습니다.

\n
\n

검색 UI 연동

\n

\n ,\n 'Describe Databricks SQL in 30 words.'\n ) AS summary\n\n# english sdk\nnew_df = df.ai.transform('get 4 week moving average sales by dept')\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
  • 이미 Kubernetes 기반의 인프라를 사용하고 있으며, ML 인프라를 구축하려는 경우
  • \n
  • 서비스를 On-premise, Multi-cloud 환경에 배포해야 하는 경우
  • \n
  • Scalable ML이 필수적이며, 기존의 여러 ML 서비스를 쉽게 배포하고 리소스 관리 비용을 줄이려는 경우
  • \n
  • Research Engineer, Data Scientist 를 위한 인프라 관리의 복잡성을 최소화하고 일관된 인터페이스를 제공하여 몇 번의 클릭만으로 설정을 쉽게 하고 싶은 경우
  • \n
\n
\n

Consistency in Infrastructure

\n

Kubeflow는 Kubernetes 기반의 인프라가 가지는 장점을 그대로 가지고 있습니다. 각 서비스에 대한 Monitoring, Health Check, Replication 등의 기본 요구사항을 갖추고 있으며 쉬운 배포 환경을 제공합니다. 이외에도 아래와 같은 usecase에서 활용될 수 있습니다.

\n
    \n
  • Research Engineer들이 인프라가 아닌 모델링에만 집중할 수 있는 환경을 제공할 수 있습니다. 모두가 Docker 기반의 추상화된 환경에서 연구를 할 수 있으며, 동일한 데이터, 연구 결과를 공유할 수 있습니다. 가상화된 GPU 환경에서 모델을 분산 학습시킬 수 있으며, TensorFlow, PyTorch, MXNet 등 다양한 프레임워크 환경을 지원할 수 있습니다.
  • \n
  • Kubeflow는 end-to-end를 제공하기 때문에 ML 프로젝트를 production에 반영하는 과정이 단순해집니다. 지속적인 데이터 파이프라인을 구축하여 argo를 통해 모델을 업데이트 하고, seldon을 통해 production 환경을 테스트해 볼 수 있습니다.
  • \n
  • Katib을 통해 Hyper parameter tuning 과정을 쉽게 자동화 할 수 있습니다. Katib에서 제공하는 인터페이스를 통해 여러 어플리케이션으로 확장시킬 수 있으며, 튜닝 결과를 지속적으로 기록하고 공유할 수 있습니다.
  • \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
  • 먼저 Homebrew로 eksctl을 설치합니다. 이후 아래의 명령어를 통해 c4 인스턴스 기반의 EKS 클러스터를 생성하고 Memory, CPU, GPU 정보를 확인해줍니다.
  • \n
  • GPU 인스턴스로 클러스터를 생성하고 싶다면 생성하기 이전에 EC2 Limit 페이지에서 p2 또는 p3 인스턴스의 limit을 확인해야 합니다. 0으로 되어있다면 Request limit Increase가 필요합니다.
  • \n
  • GPU-enabled worker를 가지는 EKS 클러스터를 생성한다면 NVIDIA driver plugin을 활성화시키는 과정이 필요합니다.
  • \n
  • Create cluster에서 AccessDenied 오류가 발생하는 경우, 사용할 IAM 유저를 생성하고 EKS 관련 permission과 AWSCloudFormationReadOnlyAccess를 추가해주어야 합니다. EKS는 현재 기준 1.11 버전을 default로 사용하고 있습니다.
  • \n
\n

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

\n

먼저 공식 차트 기준으로 executor마다 컴포넌트가 어떤 형태로 올라가는지 알아보겠습니다.\n컴포넌트는 크게 아래와 같이 구분하고 있으며 위의 그림과 같은 라이프사이클에 따라 동작합니다.

\n
    \n
  • webserver: Airflow UI, RBAC, DAG monitoring
  • \n
  • scheduler: task monitoring, trigger, DAG sync, DAG processing
  • \n
  • executor: how task instance running (pluggable)
  • \n
  • worker: task instance processing
  • \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
  • 현재 버전에서는 backend로 postgresql만 지원 (ISSUE-9627)
  • \n
  • pip 등 작업 실행에 필요한 패키지 설치하는 옵션이 없음
  • \n
  • initContainer를 수정해서 설치하거나 이미지 별도로 생성해야함
  • \n
  • 차트에 Ingress 설정에 대한 옵션이 부족
  • \n
  • KubernetesExecutor의 경우 remote logging 설정을 해야 UI에서 로그 확인 가능
  • \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
  • 다른 사람이 실행, 중지 권한을 가져서는 안될 만큼 중요한 DAG이 존재하는 경우
  • \n
  • 민감한 데이터를 다루는 DAG이 존재하는 경우 (HR, 매출 데이터 등)
  • \n
  • 팀에서 운영하는 DAG, Connection, Variable을 우리 팀만 보고 싶은 경우
  • \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
  • A 사용자는 A 사용자의 DAG만 볼 수 있음
  • \n
  • A 사용자는 B 사용자의 DAG을 볼 수 없음
  • \n
  • B 사용자가 A 사용자에게 권한을 부여하면 볼 수 있음
  • \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
  • /airflow/prod/connections/myrole/connection_id
  • \n
  • /airflow/prod/variables/myrole/variable_id
  • \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
  • 모든 DAG에는 적어도 하나의 태그를 달아야 한다
  • \n
  • 특정 task의 timeout은 48시간을 넘을 수 없다
  • \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
  • 기존에 scala API만 지원하던 모델들에 python, R API가 추가되었습니다.
  • \n
  • 지원이 추가된 모델은 Gradient Boosted Trees, Bisecting K-Means, LSH, Distributed PCA, SVD 입니다.
  • \n
  • DataFreame-based MLlib에 새로운 모델이 추가되었습니다.
  • \n
  • 추가된 모델은 **LinearSVC (Linear SVM Classifier), ChiSquare test, Correlation,
  • \n
\n

Imputer feature transformer, Tweedie distribution, FPGrowth frequent pattern mining, AssociationRules** 입니다.

\n
\n

SparkR

\n

이번 업데이트를 통해 SparkR에서 Spark SQL API가 확대되었습니다.

\n
    \n
  • R API에 Structured Streaming, Catalog가 추가되었습니다.
  • \n
  • to_json, from_json 메서드가 추가되었습니다.
  • \n
  • Coalesce, DataFrame checkpointing, Multi-column approxQuantile 기능이 추가되었습니다.
  • \n
\n
\n

GraphX

\n

GraphX는 버그 수정, 최적화 업데이트가 추가되었습니다.\n이번 Structured Steaming이 메인에 추가된 것으로 보아,\n추후에 DataFrame, DataSet API 기반의 GraphFrame이 추가될 수도 있다고 예상합니다.

\n
    \n
  • PageRank, vertexRDD/EdgeRDD checkpoint 버그를 수정했습니다.
  • \n
  • PageRank, Pregel API가 개선되었습니다.
  • \n
\n
\n

Core and SparkSQL, Deprecations

\n

마지막으로 Core, SparkSQL 그리고 Deprecation 업데이트 입니다.\n전체 업데이트 및 기타 자세한 내용은 맨 아래의 링크를 참고하시면 됩니다.

\n
    \n
  • Python 2.6, Java 7, Hadoop 2.5 지원이 종료되었습니다.
  • \n
  • ALTER TABLE table_name ADD COLUMNS 구문이 추가되었습니다.
  • \n
  • Cost-Based Optimizer 성능이 개선되었습니다.
  • \n
  • CSV, JSON 포멧의 File listing/IO 성능이 개선되었습니다.
  • \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
  • Master: GFS 전체를 관리하고 통제하는 중앙 서버의 역할
  • \n
  • Chunk Server: 물리적인 서버, 실제 입출력을 처리
  • \n
  • 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
  • S3N (s3n://) : S3N은 S3에 일반 파일을 읽고 쓰는 기본 파일 시스템입니다. S3N은 안정적이며 널리 사용되고 있지만 현재는 업데이트가 중단되었습니다. S3N의 단점은 파일 엑세스가 한번에 5GB로 제한되어 있다는 점입니다.
  • \n
  • S3A (s3a://) : S3A는 S3N을 개선한 다음 버전의 파일 시스템입니다. S3A는 Amazon의 라이브러리를 사용하여 S3와 상호 작용합니다. S3A는 5GB 이상의 파일 액세스를 지원하며 성능이 많이 향상되었습니다.
  • \n
  • S3 (s3://) : S3는 Hadoop 0.10 버전부터 나온 블록 기반의 S3 파일 시스템 입니다. 따라서 파일이 HDFS에 있는 것과 같이 블록으로 저장됩니다.
  • \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
  • s3.part.size: S3의 multi part upload 사이즈를 지정
  • \n
  • flush.size: file commit 시 저장할 record의 수 (파일 사이즈와 연관)
  • \n
  • partitioner.class: partition 기준을 지정 (TimeBasedPartitioner는 시간을 기준으로 파티셔닝)
  • \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
  • DUMB_INIT_SETSID=1 : 메인 프로세스 그룹의 모든 프로세스에 SIGNAL 전파
  • \n
  • DUMB_INIT_SETSID=0 : 메인 프로세스에만 SIGNAL 전파
  • \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
  • External Shuffle Service는 지원하지 않음
  • \n
  • Job Queue 없음 (Future Work)
  • \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
  • spark-submit 명령어 실행
  • \n
  • Kube API를 통해 driver pod 생성
  • \n
  • driver pod → API Server에 executor 생성 요청
  • \n
  • Kube API를 통해 executor 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
  • driver 리소스 요청 → 1대 생성
  • \n
  • executor 리소스 요청 → 2대 생성
  • \n
\n

\n \n \n \n

\n

위의 그림은 gang 스케줄링을 적용했을 때 모습입니다.
\n한번에 필요한 리소스를 확보하여 대기 시간을 최소화합니다.

\n
    \n
  • driver 리소스 요청 → placeholder 리소스 요청 → 노드 3대 생성
  • \n
  • driver, executor pod 즉시 할당
  • \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
  • Scheduler: 여러 스케줄링 알고리즘을 거쳐 가장 적합한 노드에 작업을 할당합니다.
  • \n
  • ControllerManager: CRD (Queue, PodGroup, VCJob)의 lifecycle을 관리합니다.
  • \n
  • Admission: CRD API에 대한 유효성 검사를 담당합니다.
  • \n
\n

PodGroup을 통해 그룹 단위의 스케줄링이 가능하며, 하나의 Queue에는 여러 개의 PodGroup이 할당될 수 있습니다. 각 PodGroup은 status를 가지고 있어 Pending, Running 등의 상태가 관리됩니다.

\n
\n

스케줄링이 실행되는 워크플로우는 다음과 같습니다.

\n

\n \n \n \n

\n
    \n
  • client가 제출한 작업을 watch하고 캐싱합니다.
  • \n
  • session을 새로 생성하고 스케줄링 사이클을 시작합니다.
  • \n
  • 캐시에 예약되지 않은 작업은 session의 대기열로 보냅니다.
  • \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
  • TaskGroup이 정의된 application을 submit 합니다.
  • \n
  • Shim이 application을 생성하고 이를 Core(Kube scheduler)에 전달합니다.
  • \n
  • Shim은 TaskGroup의 각 member에 대한 placeholder pod를 생성합니다. spark의 경우, member는 driver, executor가 될 수 있습니다.
  • \n
  • pod가 정상적으로 생성되고 나면 AllocationAsks로 처리되어 Core에 전달됩니다.
  • \n
  • placeholder는 Core를 통해 적절한 노드에 바인딩됩니다.
  • \n
  • 이제 실제 pod가 AllocationAsk로 Core에 전달됩니다.
  • \n
  • 실제 pod와 모든 placeholder pod가 스케줄링 완료된 이후 Shim은 실제 pod를 바인딩합니다.
  • \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
  • 실행경로 변경 : c.NotebookApp.default_url = '/tree'
  • \n
  • 외부접속 허용 : c.NotebookApp.ip = '0.0.0.0'
  • \n
  • 포트변경: c.NotebookApp.port = 8888
  • \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/tag/dataengineering/index.html b/tag/dataengineering/index.html index 476e67d..aac61d7 100644 --- a/tag/dataengineering/index.html +++ b/tag/dataengineering/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

42 Posts Tagged: “DataEngineering